尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32红外避障模块与OLED显示实战:接线、滤波与调试全攻略

STM32红外避障模块与OLED显示实战:接线、滤波与调试全攻略 很多刚开始玩STM32F103C8T6的朋友第一次把红外避障模块和OLED屏幕接到一起时十有八九会翻车。不是OLED点不亮就是红外模块乱触发又或者明明是同一个程序换了一块屏就是死活不出字。这些坑我基本都踩过一遍。这篇教程就把硬件接线、底层原理、软件滤波和现象调试一次性讲清楚给准备做避障小车或者环境检测项目的同学一个可以直接抄作业的完整方案。我用的主控是STM32F103C8T6最小系统板也就是大家常说的Blue Pill再加上一个常见的TCRT5000红外反射式避障模块和一块0.96寸I2C接口OLED。三个东西单独拿出来都不复杂但组合在一起细节就多了。接下来我会按实际调试的顺序来写从硬件到代码再到滤波和排错尽量做到你照着操作就能跑通。1. 硬件接线从最小系统板到传感器和屏幕的实际连接1.1 元器件清单和选购避坑先列一下我用的东西方便你对号入座STM32F103C8T6最小系统板蓝色Pill板型板上自带8MHz晶振和AMS1117稳压芯片USB口直连电脑供电下载程序TCRT5000红外避障模块板上带有红外发射管、光电接收管和一个LM393电压比较器输出口有模拟量AO和数字量DO两种0.96寸OLED屏幕SSD1306驱动芯片I2C接口4个引脚VCC、GND、SCL、SDA面包板、杜邦线若干最好多备几条母对母的这里有一个很关键的选购建议红外避障模块尽量买带LM393比较器、有电位器可调阈值的那种不要买那种只有一个红外对管和电阻的裸板子。带比较器的模块可以直接输出稳定的高低电平不需要你再用ADC采样去做阈值判断省掉很多麻烦。OLED部分同样要买4针I2C版本的不要买7针SPI版本。4针I2C只占两个IO口对于F103C8T6这种引脚不算富裕的芯片来说能省则省。而且I2C的OLED驱动代码网上非常多资料也更集中。如果你手里只有SPI版本的屏后面接线和程序都要改但没有必要直接换一块更省心。1.2 引脚分配表和接线实操我最终确定的引脚分配是这样模块引脚STM32引脚说明红外避障模块VCC3.3V或5V供电红外避障模块GNDGND共地红外避障模块DOPA0数字输出接入读取障碍信号红外避障模块AO不接本次不用模拟输出OLEDVCC3.3V必须3.3V严禁接5VOLEDGNDGND共地OLEDSCLPB8软件I2C时钟线OLEDSDAPB9软件I2C数据线这样一个IR传感器占用PA0OLED占用PB8和PB9其余引脚全部空出来后续你如果要加电机驱动、舵机或者蓝牙模块还有充足空间。接线的时候有两点要注意。第一所有模块的地线必须和STM32共地否则信号电平没有参考点读到的数据完全不可靠。第二OLED的VCC不要接到5V上虽然不少模块板载了稳压电路但为了保险起见直接用3.3V供电就好。F103C8T6的IO口是3.3V电平逻辑OLED的I2C接口也是3.3V逻辑两边匹配不需要电平转换。红外模块的供电我建议接5V。TCRT5000内部的红外发射管串联了一个限流电阻5V供电时发射管工作电流更大红外光更强检测距离会更远一些。DO输出的是TTL电平3.3V的单片机读取高电平信号完全没问题。当然你如果不想折腾直接全接3.3V也能正常工作只是有效检测距离会缩短一些。1.3 上拉电阻的问题为什么你的I2C有时灵有时不灵接下来说一个特别容易忽略的坑I2C总线的上拉电阻。I2C协议本身是开漏结构的SCL和SDA线必须靠外部上拉电阻拉高到高电平。很多OLED模块背面已经焊好了4.7kΩ或10kΩ的上拉电阻买回来直接用就行。但也有一些便宜的模块板上没有焊上拉电阻这时候你直接用STM32的GPIO开漏模式去驱动I2C就会出现一个诡异的现场屏幕有时候能亮有时候通电后怎么都不出字用手摸一下杜邦线又有反应把线重新插一遍又好了。这是因为线的分布电容和寄生电容影响了I2C信号的上升沿。开漏输出本身没有主动拉高的能力全靠上拉电阻给电容充电阻值太大、线太长、接触不良上升沿就慢数据就容易出错。如果你遇到这种情况自己动手在SCL和SDA上各接一个4.7kΩ电阻到3.3V就行。如果没有4.7kΩ用2.2kΩ到10kΩ之间都可以阻值越小信号边沿越陡但功耗也稍大一点对OLED这种低速设备来说差别不大。我在调试过程中还发现杜邦线的质量也很关键。好一点的杜邦线镀层均匀、插针紧密坏一点的松垮垮稍微动一下面包板就接触不良。OLED点不亮的时候不要上来就怀疑代码先用手按住所有接头试试很多时候问题就出在物理连接上。2. 红外避障模块与OLED背后的工作原理2.1 红外避障模块到底是怎么判断“有障碍物”的TCRT5000红外避障模块的核心部件其实只有两个一个红外发射管和一个光电二极管接收管。发射管持续向外发射波长为950nm左右的红外光红外光遇到障碍物后发生漫反射部分反射光被旁边的光电二极管接收接收管的内阻随之发生变化从而产生微弱的电流信号。但这个电流信号非常微弱直接接单片机完全没法用。所以模块上设计了LM393比较器电路把光电二极管输出的电压信号和电位器设定的阈值电压做比较。当反射光强大于阈值时比较器输出翻转DO引脚输出低电平反射光弱时输出高电平。这里有个容易误解的地方不同厂家的模块逻辑可能相反。我用的这块是障碍物靠近时DO输出低电平障碍物远离时输出高电平。但有的模块设计是反过来的障碍物靠近时输出高电平。所以拿到模块第一件事不是写代码而是上电后用手在传感器前面晃一晃观察模块上的指示灯和DO引脚电平变化搞清楚你的模块到底是哪种逻辑再去写代码。模块上的电位器旋钮控制的是比较器参考电压也就是灵敏度。顺时针旋转检测阈值变高需要更强的反射光才能触发有效距离变短逆时针旋转阈值变低稍微有点反射光就可能触发检测距离变远但也更容易受到环境光干扰。实际调试时我一般先把电位器旋到中间位置再根据具体场景微调。另外一个非常实用的经验TCRT5000的有效检测距离大约是1cm到8cm超过这个范围就很不稳定了。做避障小车的时候传感器安装高度最好控制在2cm到4cm之间这样既有足够的反应时间又不会误判地面上的深色花纹。2.2 SSD1306驱动OLED的I2C通信机制0.96寸OLED屏幕分辨率为128x64像素内部集成了SSD1306驱动芯片显存一共是128x64bit也就是1024字节。芯片内部把整个屏幕分成8页Page0到Page7每页是128列x8行像素刚好对应一个字节的8个bit位。I2C通信的流程是这样的主机先发送起始信号然后发送从机地址加写位从机应答后接着发送控制字节。控制字节为0x00时表示后面跟的是命令为0x40时表示后面跟的是显存数据。这个地址这里多提一句SSD1306的I2C地址取决于模块上地址引脚的电平绝大多数模块固定为0x3C但也有一些是0x3D。如果初始化半天屏幕没反应把地址改成0x3D试一下往往问题就解决了。写命令的典型序列是这样void OLED_WriteCmd(uint8_t cmd) { OLED_Start(); OLED_WriteByte(OLED_ADDR 1); // 从机地址 写位 OLED_WriteByte(0x00); // 控制字节表示命令 OLED_WriteByte(cmd); // 命令本身 OLED_Stop(); }写显存数据只需要把控制字节换成0x40然后连续写入数据即可。注意SSD1306写入数据是按列地址自动递增的写入8个字节就填满了一个页的一列总共需要写1024个字节才能刷新一整屏。这就解释了为什么刷新一屏的耗时和I2C时钟频率直接相关。2.3 为什么我推荐用软件模拟I2C而不是HAL库硬件I2C用F103C8T6驱动OLED业界有个心照不宣的经验能用软件模拟I2C尽量别用HAL库的硬件I2C。这么说不是HAL库不能用而是HAL库的I2C在STM32F1上有一个历史遗留的坑具体表现就是调用HAL_I2C_Mem_Write的时候偶尔会卡死在忙等判断里面需要超时处理或者重启才能恢复。虽然官方提供了补丁和解决方法但对于初学者来说排查这个问题的成本实在太高了。软件模拟I2C则完全可控。用两个GPIO口开漏模式手动拉高拉低时序完全由自己掌握出了bug也好排查。代码逻辑也不复杂核心就是起始、停止、发字节、收应答这四个操作。void OLED_Start(void) { OLED_SDA_H(); OLED_SCL_H(); OLED_SDA_L(); OLED_SCL_L(); } void OLED_Stop(void) { OLED_SCL_L(); OLED_SDA_L(); OLED_SCL_H(); OLED_SDA_H(); } void OLED_WriteByte(uint8_t data) { for (uint8_t i 0; i 8; i) { if (data 0x80) OLED_SDA_H(); else OLED_SDA_L(); OLED_SCL_H(); OLED_SCL_L(); data 1; } }软件模拟I2C的速率不用刻意去算一个字节8个bit每次翻转之间插入几条空指令或者直接不延时实测跑下来刷新一屏大概十几毫秒显示动态画面完全够用。3. 工程搭建与代码实现从CubeMX到实际显示3.1 CubeMX工程配置要点工欲善其事必先利其器。我这里用的是STM32CubeIDE开发环境配合STM32CubeMX做图形化初始化配置。建工程的时候需要注意几个地方时钟配置选择HSE外部晶振主频拉到72MHz。F103C8T6最高只能跑到72MHz这个没得商量。系统时钟分配好后调试方式选择Serial Wire不然有的板子下载一次程序之后第二次就无法连接了。GPIO配置方面PA0设置为输入模式看你的模块逻辑决定是否需要启动内部上拉或下拉。如果模块平时输出高电平、有障碍物时输出低电平那外部已经有明确的电平驱动内部上下拉都无所谓直接配置成普通输入即可。PB8和PB9设置为开漏输出模式开漏模式是I2C的标准配置。初始化完GPIO后再写几个宏定义来控制引脚拉高拉低。串口建议也顺手配置一个比如USART1的TX引脚PA9和RX引脚PA10波特率115200。后面调试滤波效果的时候把原始数据和滤波后的数据打出来看波形你就知道串口有多重要了。3.2 OLED驱动移植与显示流程OLED的驱动代码网络上流传的版本很多不同作者写的功能也略有差异。我的建议是核心文件自己维护一份包含初始化、清屏、显示单个字符、显示字符串、显示整数和浮点数这几个基础功能就够用了。SSD1306的初始化需要发送一串配置命令这些命令的顺序和值基本都是固定的不用去逐条死记硬背但核心的几个命令最好理解一下。0xAE关闭显示和0xAF开启显示是成对出现的0x8D配合0x14开启内部电荷泵。这里要注意的是必须开启电荷泵否则OLED屏幕即使I2C通信正常也不会发光这也是很多人初始化代码写完屏幕依然全黑的原因。清屏函数实现起来很简单遍历所有页和所有列全部写0x00void OLED_Clear(void) { for (uint8_t page 0; page 8; page) { OLED_SetCursor(page, 0); for (uint8_t col 0; col 128; col) OLED_WriteData(0x00); } }显示字符部分的原理是查表。取模软件生成的字模是一个8字节数组每个字节对应一列8个像素。显示的时候把某个字符的字模数据逐个写入对应位置即可。我自己的经验是显示字符串和整数的时候先准备好一个足够大的缓冲区用sprintf格式化好再一次性调用显示字符串函数。这样做代码简洁而且方便拼接调试信息比如同时显示“Distance:”和具体的传感器状态。3.3 红外采集与主循环逻辑红外模块的读取在代码上非常简单就是读一个GPIO的电平。但放在完整的项目里要思考的问题就多了。我的主循环逻辑大致是这样的先初始化OLED和串口清屏显示启动信息。然后进入死循环每10ms读取一次红外模块状态做去抖滤波处理接着更新屏幕显示通过串口输出调试信息。while (1) { uint8_t ir_raw HAL_GPIO_ReadPin(IR_PORT, IR_PIN); uint8_t ir_filtered IR_Filter(ir_raw); OLED_ShowString(0, 0, IR Status:); if (ir_filtered 0) OLED_ShowString(0, 2, OBSTACLE!); else OLED_ShowString(0, 2, CLEAR ); printf(IR raw%d filtered%d\r\n, ir_raw, ir_filtered); HAL_Delay(10); }这个500ms的显示刷新频率需要和10ms的数据采集频率分开考虑。IR滤波函数内部维护自己的采样节奏而屏幕显示不需要刷那么频繁否则OLED会一直处于忙碌状态而且人眼也看不出那么快的变化。4. 软件滤波如何让传感器数据稳定可靠4.1 红外模块噪声来源和抖动现象红外避障模块的信号真的不是稳定的高低电平至少在真实场景中不是。我遇到过的情况有这么几种障碍物刚好位于检测距离边缘时DO输出会在高低电平之间来回跳动频率还很快目测大概几十赫兹。模块正对阳光或者靠近大功率白炽灯时环境光里的红外成分会干扰接收管导致输出电平异常。还有一个很经典的情况障碍物表面是黑色或者镜面时红外光吸收或定向反射掉传感器明明离得很近却判定为无障碍。这种问题软件滤波救不了需要靠传感器安装位置和角度来弥补。不做任何处理直接读取GPIO的话避障小车会表现为蛇形走位明明前方没有物体轮子却疯狂转向。所以软件滤波不能只做一次要考虑多个维度。4.2 中值滤波加去抖动的双层方案我最后采用的方案是两层滤波每层解决一个不同的问题。第一层是中值滤波解决的是单次采样电平偶然漂移的问题。取连续5次采样值排序后取中间那个数。5次里就算有1次采到了错误电平也不会影响最终结果。采样间隔建议1ms左右这样5次采样一共耗时5ms对实时性影响不大。uint8_t RAW_Buffer[5] {1, 1, 1, 1, 1}; uint8_t IR_MedianFilter(void) { uint8_t temp[5]; uint8_t i, j; for (i 0; i 5; i) temp[i] RAW_Buffer[i]; for (i 0; i 4; i) { for (j i 1; j 5; j) { if (temp[j] temp[i]) { uint8_t t temp[i]; temp[i] temp[j]; temp[j] t; } } } return temp[2]; }第二层是去抖滤波解决的是状态切换时的抖动问题。思路很简单只有连续若干次采样的滤波结果都是同一个值才认为状态发生了真正改变否则维持上一个状态不变。这个去抖的思路非常类似轻触按键消除机械抖动。可以把状态变化想象成一个懒人领导连续喊他三遍同一个指令他才动一下喊一遍他不理你。4.3 滤波参数怎么调滤波参数不是随便拍的需要结合实际的检测距离和运动速度来定。中值滤波的窗口大小我建议3到7之间。窗口越大滤除突刺的能力越强但采样耗时越长反应越迟钝。对TCRT5000这种本身信号就还算干净的传感器来说5足够。去抖滤波的关键参数是连续确认次数。我做避障小车的时候用的连续次数是10次。结合前面采样间隔1ms来算一个状态要稳定10ms才会被确认这个延迟对于低速小车来说完全无感。如果你做的是高速运动的机器人可能需要把这个次数降到3或者4否则遇到突发障碍物会反应不过来。调试参数的时候别瞎调把串口打开。串口每隔10ms输出一次原始状态和滤波后状态观察两种情况下的数据差异。如果原始状态偶尔出现0和1交替但滤波后状态保持稳定说明滤波参数是合适的。如果滤波后状态还在抖动说明连续次数不够继续加大。5. 现象调试排查问题的完整思路5.1 OLED点不亮的排查链路OLED屏幕不亮这个问题大概能占初学者问题的六成。我总结了一套排查链路按顺序走一遍基本都能定位。第一步查供电。万用表测OLED的VCC和GND之间电压应该是稳定的3.3V。如果没有电压顺着线查到面包板可能杜邦线插错了孔位或者面包板内部断路。这一步就排除掉一大半的问题。第二步查I2C地址。写一个简单的扫描函数给每个可能的I2C地址发送起始信号看是否有从设备应答。0x3C和0x3D都试一下。这个函数不用写的很复杂就是一个循环地址加应答判断。第三步查上拉电阻。前面说过没有上拉电阻的模块会表现为时好时坏用手触摸引脚反而恢复正常。如果你用的是那种特别便宜的模块直接补焊两个4.7kΩ电阻上去。第四步查初始化命令。尤其是电荷泵是否开启。可以用逻辑分析仪抓一下I2C波形看初始化命令是否完整发出。没有逻辑分析仪的话也可以试着在初始化之后立刻清屏点亮一个点看屏幕有没有微弱的反应。第五步终极测试换一块OLED屏。有些模块出厂就是坏的别在这种模块上浪费太多时间换一块屏立刻见分晓。我调试的时候踩过最蠢的一个坑OLED模块正常代码正常接线也正常但屏幕就是全黑。最后发现是初始化代码里连续发送命令的时候两条命令之间漏掉了延时导致部分命令没有被执行。后来在OLED_WriteCmd函数的Stop之后加了个微秒级延时问题就消失了。5.2 避障模块不触发或者常触发的检查方法红外避障模块的常见问题有两个方向该触发时不触发和不该触发时乱触发。该触发时不触发先检查模块上的电源指示灯是否亮起。亮起说明供电正常再检查输出指示灯。用手遮挡传感器正面如果输出指示灯不变化说明传感器本身可能有问题或者接线错了。用手遮挡时输出指示灯有变化但主控读不到信号变化检查DO引脚是否接到了正确的IO口程序里读取的引脚号是否和接线一致。这里分享一个技巧可以在主循环里只写一个最简单的GPIO读取程序把原始电平状态用串口循环打印出来。串口数据是整个调试过程的照妖镜能让你快速区分问题是出在硬件层、软件层还是滤波层。该触发时不触发的另一个常见原因是电位器阈值调节不当。传感器距离障碍物10cm电位器阈值调得太高反射光根本达不到触发条件。逆时针旋转电位器降低参考电压直到障碍物在目标距离出现时输出电平能够稳定翻转。乱触发的问题通常是环境光干扰或者安装角度不对。TCRT5000虽然有滤光片但强阳光下还是会误触。把传感器尽量安装在车体下方、背光位置不要正对太阳。另外两个红外传感器同时工作时要注意它们之间的红外光束不能互相干扰否则会频繁误触发。安装间距拉大或者发射管角度略向外偏。5.3 滤波前后数据对比与效果验证滤波到底有没有效果不能靠肉眼感觉要有数据支撑。这是我串口打印设计成同时输出原始值和滤波值的初衷。场景原始信号滤波后信号无障碍物传感器稳定持续高电平持续高电平障碍物稳定靠近低电平偶发毛刺稳定低电平障碍物在边缘区域高低快速抖动稳定的原状态强光照射有时出现误低电平保持原状态从中可以看出滤波主要解决的是毛刺和边缘抖动问题。但看到这个对比有个认知要建立起来滤波不是万能的它只是延迟了判断没有改变根本物理现象。如果一个障碍物在边缘区域来回晃动滤波后的值最终还是会跟着切换只是切换的次数大大减少了。这对电机驱动的避障逻辑来说已经足够友好不会因为单次误判就改变运动方向。5.4 最终效果演示与扩展方向全部调试完成之后OLED屏幕上会显示两行内容第一行是IR Status第二行是CLEAR或者OBSTACLE。用手在传感器前慢慢移动屏幕状态会跟着切换但不会出现快速闪烁。串口会同步输出原始值和滤波值整体响应速度大概在30ms左右体感上就是手一伸过去屏幕立刻变化没有粘滞感。这个项目跑通之后继续扩展的方向很多。最直接的是加两个电机和L298N驱动模块把IR状态转换为避障行为逻辑小车的巡线模块也可以照搬这套滤波方案。OLED上还可以扩展显示更多信息比如障碍物的左右方向、行驶速度、超声波测距值等。F103C8T6的引脚资源和性能足以支撑这些扩展。如果后续想用硬件I2C节省CPU时间也不是不行但需要额外花时间处理HAL库I2C的卡死问题。一个折中方案是IO口配置为开漏复用功能配合定时器做超时重置不过这些属于进阶话题了。现阶段先把软件I2C跑稳把滤波逻辑吃透比什么都强。
返回列表