STM32+TOF400激光测距与OLED显示及蓝牙回传实战

发布时间:2026/10/13 17:50:36
STM32+TOF400激光测距与OLED显示及蓝牙回传实战 简介这份资料面向嵌入式开发初学者与物联网方向学习者围绕STM32单片机搭建一套TOF400激光测距系统解决距离采集、屏幕显示与无线传输的完整链路问题。项目以STM32为核心通过I2C或SPI读取TOF400飞行时间测距数据驱动OLED1286屏实时显示测量结果并借助蓝牙模块经UART实现与手机等设备的远程通信覆盖传感器应用、显示驱动与低功耗蓝牙等知识点。资源包共172个文件以38个h头文件、35个c源文件为主配合工程配置、编译中间文件与hex固件等压缩包约3.8MB结构完整可直接用于Keil工程复现。目前已有1174人学习下载适合希望打通硬件接口控制、传感器数据采集与无线通信的开发者参考实践。1. 从一颗TOF400到一块OLED这套STM32测距方案到底在解决什么问题如果你正在做嵌入式测距项目大概率绕不开一个组合STM32单片机 TOF400激光测距模块 OLED1286显示 蓝牙回传。这套方案的核心价值在于它把「非接触式高精度测距」这件事从实验室仪器变成了可以嵌入到任意设备里的小型化模块。TOF400基于飞行时间原理通过发射激光并测量反射回来的时间差来计算距离典型量程在0.3米到4米之间精度可以做到厘米级甚至更好。相比超声波方案它响应更快、波束角更小、抗环境干扰能力更强相比红外三角测距它在远距离上的线性度明显更优。这套系统适合谁做智能小车避障的、做工业液位检测的、做仓储物料定位的、做机器人防撞的甚至做互动装置的距离感应触发的都能直接复用。OLED1286负责本地实时显示蓝牙负责把数据推到手机或上位机STM32负责调度整个链路。整条链路不复杂但每个环节都有容易翻车的地方下面按实际落地顺序拆开讲。2. TOF400与STM32的硬件对接从供电到I2C上拉的六个关键点2.1 为什么TOF400的供电不能直接抄3.3VTOF400模块内部集成了激光发射管、接收阵列和一颗专用的测距处理芯片。很多开发者拿到模块后第一反应是直接接STM32的3.3V引脚结果发现数据跳动严重甚至根本不出数。原因在于激光发射瞬间的峰值电流可能超过100mA而STM32板载LDO的3.3V输出通常只预留了几十毫安的余量给外设。正确做法是给TOF400单独走一路低压差线性稳压器或者从系统5V经一颗低压差线性稳压器降到3.3V再供给模块。同时在模块的电源引脚旁边并一颗100μF的电解电容和一颗0.1μF的陶瓷电容前者吸收低频波动后者滤高频噪声。这个细节在数据手册里往往只提一句但实际调试时供电不稳是导致测距值随机跳变的第一大原因。2.2 I2C总线的上拉电阻到底该放多大TOF400通常支持I2C接口STM32作为主机。I2C的SDA和SCL线必须接上拉电阻常见取值是4.7kΩ。但这里有个容易被忽略的点上拉电阻的阻值取决于总线电容和通信速率。如果你把TOF400和OLED1286挂在同一条I2C总线上总线电容会增大4.7kΩ可能导致上升沿变缓高速模式下通信失败。我的经验是先按4.7kΩ焊用示波器看SCL上升时间如果超过1μs就换成2.2kΩ。另外上拉电阻只在一处接即可不要STM32端和模块端各接一组并联后阻值减半反而增加功耗。2.3 最小系统接线表与引脚分配下面这张表是我在多个项目中反复验证过的接线方案以STM32F103C8T6为例功能STM32引脚TOF400引脚OLED1286引脚备注电源3.3V3.3V输出VCCVCCTOF400建议独立LDO地GNDGNDGND单点接地I2C时钟PB6SCLSCL4.7kΩ上拉I2C数据PB7SDASDA4.7kΩ上拉蓝牙串口TXPA9——接蓝牙模块RX蓝牙串口RXPA10——接蓝牙模块TX接线时注意TOF400的激光窗口不要被遮挡OLED1286的I2C地址和TOF400不能冲突。常见TOF400的I2C地址是0x52OLED1286一般是0x3C或0x3D不冲突可以直接挂同一条总线。2.4 初始化代码I2C外设与TOF400寄存器配置#include stm32f10x.h #include i2c.h #include tof400.h #include oled1286.h #define TOF400_ADDR 0x52 #define TOF400_REG_RANGE 0x00 #define TOF400_REG_WHOAMI 0x01 void TOF400_Init(void) { I2C_Init(); // 初始化I2C时钟400kHz7位地址模式 uint8_t id 0; I2C_ReadBytes(TOF400_ADDR, TOF400_REG_WHOAMI, id, 1); if (id ! 0x40) { // 读到的ID不对检查供电和上拉 while(1); } // 配置测距模式连续测量量程4米 uint8_t mode 0x02; I2C_WriteBytes(TOF400_ADDR, TOF400_REG_RANGE, mode, 1); } uint16_t TOF400_ReadDistance(void) { uint8_t buf[2] {0}; I2C_ReadBytes(TOF400_ADDR, 0x1E, buf, 2); // 距离结果寄存器 return (uint16_t)((buf[0] 8) | buf[1]); }这段代码的逻辑很直接先初始化I2C外设然后读TOF400的WHOAMI寄存器确认通信正常再写入测距模式配置。读距离时从结果寄存器取两个字节拼成16位值。参数说明I2C时钟设为400kHz是TOF400的推荐上限再高可能丢包WHOAMI的期望值0x40是常见TOF400兼容芯片的标识如果你用的模块ID不同以实际手册为准。注意每次读距离前不需要重新触发连续测量模式下模块会自动更新寄存器。3. OLED1286显示驱动从字库裁剪到动态刷新不闪烁3.1 OLED1286的显存结构决定了刷新策略OLED1286是一块128x64像素的单色OLED屏内部显存按页组织每页8行共8页每页128字节。很多人第一次写驱动时习惯每次更新都全屏重写结果屏幕闪烁明显。正确的做法是在单片机内存里开辟一块1024字节的显存缓冲区所有绘制操作先改缓冲区再一次性把整块缓冲区通过I2C推给屏幕。这样屏幕上的内容始终是完整的不会出现半帧旧半帧新的情况。缓冲区大小正好是128x64/81024字节STM32F103C8T6的20KB RAM完全放得下。3.2 取模与字库只存用得到的字符如果你只需要显示数字和几个单位字母不要挂完整ASCII字库。完整ASCII字库大约几百字节到1KB但如果你用中文字库会急剧膨胀。我的做法是用取模软件只生成0-9、小数点、m、c、以及几个提示词的字模每个字符按8x16或6x8点阵存成常量数组。下面是一个数字字模的示例const uint8_t Font8x16_0[] { 0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00, 0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00 }; // 每个数字16字节按列优先或行优先取决于取模设置逻辑说明每个8x16字符占16字节前8字节是上半页后8字节是下半页。绘制时根据坐标算出在缓冲区中的页和列偏移逐字节写入。参数说明取模方式必须和驱动中的绘制函数一致常见坑是取模选了「列行式」但驱动按「行列式」解析显示出来是乱码。3.3 动态刷新定时器驱动与局部更新void OLED_Refresh(void) { for (uint8_t page 0; page 8; page) { I2C_WriteBytes(OLED_ADDR, 0x00, 0x10, 0x40 page); I2C_WriteBytes(OLED_ADDR, 0x40, gram[page * 128], 128); } } void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); static uint8_t tick 0; if (tick 10) { // 每100ms刷新一次 tick 0; OLED_Refresh(); } } }这段代码把刷新放在定时器中断里每100ms推一次显存。100ms对应10Hz刷新率对于数字显示足够而且不会占用主循环时间。参数说明TIM2的定时周期设为10ms累计10次触发一次刷新。如果你需要更快的刷新比如显示动态波形可以降到50ms但要注意I2C的带宽——128x64全屏是1024字节400kHz I2C下传输大约需要25ms刷新太快会阻塞其他任务。4. 蓝牙数据回传串口协议设计与手机端接收验证4.1 为什么自定义帧头比直接发字符串可靠很多教程直接让STM32通过串口发「Distance: 123 cm\r\n」这样的字符串手机端用文本模式接收。这在演示时没问题但实际项目中一旦蓝牙信号受干扰丢了一个字节整条字符串就错位了。更稳妥的做法是定义二进制帧帧头0xAA 0x55然后跟距离高字节、距离低字节、校验和。手机端按帧解析丢包也能快速重新同步。下面是我常用的帧格式字节序号内容说明00xAA帧头110x55帧头22距离高8位单位mm3距离低8位单位mm4校验和前4字节异或4.2 STM32端串口发送代码void Bluetooth_SendDistance(uint16_t dist_mm) { uint8_t frame[5]; frame[0] 0xAA; frame[1] 0x55; frame[2] (dist_mm 8) 0xFF; frame[3] dist_mm 0xFF; frame[4] frame[0] ^ frame[1] ^ frame[2] ^ frame[3]; for (uint8_t i 0; i 5; i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, frame[i]); } }逻辑说明把距离值拆成高低字节加上帧头和校验和逐字节发送。发送前检查TXE标志确保上一个字节已经移出。参数说明距离单位用毫米避免浮点运算STM32端和手机端约定好即可。校验和用异或计算量小检错能力对短帧足够。4.3 手机端验证用通用串口助手做快速回环在写手机App之前先用蓝牙串口助手类工具验证数据。把蓝牙模块配对后打开串口助手设置波特率9600或115200与STM32端一致接收模式选HEX。你应该能看到AA 55开头的固定帧。如果收到的数据里AA 55频繁出现但后续字节对不上检查波特率是否偏差过大——蓝牙模块的晶振精度不如有线串口9600比115200更稳。如果完全没数据先确认蓝牙模块的TX/RX是否和STM32的PA10/PA9交叉连接。5. 避坑与排查TOF400测距跳变、OLED花屏、蓝牙断连的五个血泪经验5.1 测距值在特定距离上突然跳变几十厘米现象在0.5米以内测得很准到1.5米左右突然跳到2米多再远又正常。原因TOF400在近距离时接收到的反射光过强导致接收阵列饱和处理芯片误判。解决在TOF400的激光出口前贴一层半透明胶带或者选用带遮光罩的模块版本。如果软件可调降低发射功率或缩短积分时间。这个坑在白色墙面和黑色桌面上表现完全不同白色反射强更容易饱和。5.2 OLED1286显示数字残影现象距离从123变成456时原来的「1」还隐约可见。原因绘制新数字前没有清除该区域的旧内容。解决每次更新数字前先用背景色把该数字的矩形区域填满再绘制新数字。如果用了显存缓冲区直接往缓冲区写0即可。注意不要全屏清否则闪烁。5.3 蓝牙连接后数据时断时续现象手机能连上蓝牙但数据几秒才更新一次甚至丢包。原因STM32端发送频率太高蓝牙模块的串口缓冲区溢出。解决把发送间隔从10ms改成50ms或100ms或者在发送前判断蓝牙模块的忙状态引脚。另外蓝牙模块和TOF400共用3.3V电源时激光发射的电流冲击可能导致蓝牙模块复位给蓝牙模块单独加一颗100μF电容。5.4 I2C总线挂死OLED和TOF400同时无响应现象运行一段时间后OLED不刷新TOF400也读不到数据复位后恢复。原因I2C从设备在某个时刻拉低了SDA不放主机无法产生停止条件。解决在I2C初始化时增加总线恢复函数——把SCL配置为推挽输出手动发送9个时钟脉冲然后发停止条件。另外降低I2C速率到100kHz可以显著减少挂死概率。5.5 上电后TOF400的WHOAMI读出来是0xFF现象初始化时读WHOAMI始终是0xFF但接线检查无误。原因TOF400的上电时序要求VCC稳定后至少等待10ms才能通信而STM32的初始化太快。解决在TOF400_Init开头加10ms延时或者在I2C初始化后先延时再读。这个坑在冷启动时必现热复位时反而正常容易误判为硬件问题。6. 进阶技巧用滑动滤波和自适应阈值把测距精度再提一档如果你已经跑通了基础版本接下来最值得投入的方向是数据滤波。TOF400原始输出的距离值在静态目标上也会有±1到±3厘米的波动直接显示会让人感觉「跳」。我一般用滑动平均加中值滤波的组合开一个长度为5的环形缓冲区每次新数据进来后先取中值再对中值序列做滑动平均。这样既能抑制脉冲噪声又能平滑随机波动。代码实现上环形缓冲区用数组加索引取模即可不需要动态内存。#define FILTER_LEN 5 static uint16_t buf[FILTER_LEN] {0}; static uint8_t idx 0; uint16_t Filter_Update(uint16_t new_val) { buf[idx] new_val; idx (idx 1) % FILTER_LEN; // 复制一份排序取中值 uint16_t tmp[FILTER_LEN]; for (uint8_t i 0; i FILTER_LEN; i) tmp[i] buf[i]; for (uint8_t i 0; i FILTER_LEN - 1; i) { for (uint8_t j 0; j FILTER_LEN - 1 - i; j) { if (tmp[j] tmp[j1]) { uint16_t t tmp[j]; tmp[j] tmp[j1]; tmp[j1] t; } } } uint16_t median tmp[FILTER_LEN / 2]; // 对中值再做一次简单平均 static uint32_t sum 0; static uint8_t cnt 0; sum median; cnt; if (cnt FILTER_LEN) { uint16_t avg sum / cnt; sum 0; cnt 0; return avg; } return median; }这段代码的逻辑是先把新值存入环形缓冲区然后复制出来做冒泡排序取中值最后对中值做累计平均。参数说明FILTER_LEN取5是响应速度和滤波效果的折中取3响应快但滤波弱取7更平滑但延迟明显。如果你的应用需要跟踪快速移动的目标建议降到3并且把中值滤波换成限幅滤波——即新值与上次有效值偏差超过阈值就丢弃。另一个进阶点是自适应阈值报警。比如做防撞时固定阈值在近距离和远距离下的可靠性不同。我的做法是根据当前距离动态调整报警阈值距离越近阈值越小。例如在0.5米内超过2厘米的变化就触发在2米外超过10厘米才触发。这样既避免了远距离的误报又保证了近距离的灵敏度。最后说一个我踩过的坑滤波后的数据不要直接用于OLED显示和蓝牙发送最好保留原始值用于调试。我习惯在OLED上同时显示原始值和滤波值用两个不同的位置这样一旦滤波参数不合适一眼就能看出来。这个习惯帮我省了很多来回改代码的时间。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询