拓冰建站拓冰建站
首页 / 资讯中心 / 正文

指针表盘与LCD同时不显示的嵌入式排查手册:从ADC采样到显示链路

前几天帮朋友调一个带指针表盘和 LCD 数显的小仪表现象非常刺激仿真软件里表盘的指针一动不动接上实物 LCD 之后屏幕也是一片空白。两个看起来完全独立的显示链路同时罢工这在调试中算比较少见的但恰恰因为少见排查起来反而有套路可循。先说结论在这个项目里指针不显示和 LCD 不显示根因是同一个——ADC 采样这条数据供应链在最前面就断了。文章会从系统架构开始把这套“指针 LCD 双显示”方案的链路拆开把每个可能断掉的位置都走一遍然后给出完整的定位方法和修复代码适合正在被类似问题折磨的嵌入式开发同学。1. 项目背景与显示链路拆解1.1 这套系统到底是怎么工作的我用的主控是 STM32F103C8T6外围有两块显示一块 LCD1602 字符液晶用来显示电压数值一块机械指针表盘由 28BYJ-48 步进电机驱动指针角度代表当前电压。这样的双显示方案在很多仪表类产品里都能看到机械表盘直观反映变化趋势数字屏负责精确读数属于典型的“模拟 数字”混合显示。数据流是这样的电压采样电阻分压 - ADC 转换 - 滤波、标度变换 - 得到当前电压值然后分两个分支走。分支一把电压值映射成指针角度驱动步进电机把指针转到对应刻度分支二把电压值格式化成字符串通过 LCD 显示出来。这段链路在别人看来可能很复杂但在我眼里其实是一条单向管道。指针和 LCD 虽然一个是电机、一个是屏幕但它们吃的都是同一个“电压值”所以当两者都失效时第一个怀疑对象不应该是电机也不是 LCD而是数据源头。这也是这个项目排查过程中最重要的一条判断依据。1.2 指针和 LCD 看似独立实际共用哪些资源指针电机和 LCD 在硬件上毫无交集一个接在 GPIO 上一个走的是 I2C 或并口但代码里它们捆绑得比想象中紧。ADC 转换结果放到全局变量 adc_value 里然后被两个函数读取needle_update() 负责把它转成角度lcd_update() 负责把它格式化成字符串。这两个函数都在主循环里被调用调用前提是 adc_flag 被置 1。如果 adc_flag 永远不置 1那么两个函数就都不会执行。这是最典型的一种“双显示全灭”场景。我常给同事打这个比方一个人用同一张嘴同时指挥两个喇叭嘴没张开两个喇叭当然都不响。指针和 LCD 就是这两个喇叭采样和数据处理就是那张嘴。排查顺序一定要先从嘴开始再去看喇叭否则很容易在电机驱动和液晶初始化里绕圈子。2. 问题现象与初步定位先别急着换硬件2.1 把现象拆成两个独立的层次遇到这种问题最忌讳一上来就怀疑硬件。我在现场做的第一步是把现象拆开。第一层显示链路本身是否可用。我在代码里强制让 LCD 显示一个固定字符串“TEST”如果还是不显示问题在 LCD 驱动再让步进电机强制转 200 步如果指针不动问题在电机驱动或驱动电路。如果两者都能动作说明显示链路本身是好的。第二层数据链路是否在工作。用串口把 adc_value 实时发出来用调试器看 adc_flag 是否置位。实测结果是强制 LCD 显示“TEST”完全正常电机转 200 步也正常但一旦恢复主循环两个显示就都不更新。这就把问题范围圈到了“数据源 主循环调度”上而不是电机或屏幕本身。这个拆分看起来简单但很有效。它帮我少拆了至少两个设备。很多时候你以为是对象坏了其实发送方根本没把数据送到。2.2 用串口打印作为第三方裁判嵌入式调试里我很少在没有观测手段的情况下猜问题。这个项目在 STM32 上引出一路串口波特率 115200在主循环里通过串口把 adc_value 和计算出来的电压值实时发出去。串口的表现提供了决定性线索数据几乎都是 0而且 adc_flag 从置 1 之后再也没有变化。基本可以确定问题出在 ADC 采样环节或者是负责启动 ADC 转换的那段代码根本没跑起来。如果没有串口也有替代方案在关键位置翻转 LED。我发现很多初学者不太爱用 LED 调试实际上一个 LED 接在某个 GPIO 上进入函数设置为低、退出设置为高用逻辑分析仪一看就知道代码有没有执行到那一步非常管用。串口和 LED 这两种手段本质上都是在程序运行过程中插入“检测点”让看不见的程序运行状态变成看得见的信号。3. 根因分析为什么指针和 LCD 会同时不显示3.1 初始化顺序与卡死陷阱这个项目的代码是从一个旧例程改出来的坑就埋在初始化里。原始代码里 LCD_Init() 内部有多个 delay_ms()而 delay_ms() 是基于 SysTick 实现的。后来我把 ADC 配置改成了“定时器触发 中断”的方式在配置过程中不小心动了 SysTick 相关寄存器导致 delay_ms() 进入了一个死循环。也就是说主函数跑到 LCD_Init() 时已经卡死后面的 ADC 配置、主循环都永远执行不到。这种情况下指针和 LCD 当然都不会有任何反应因为整个程序都停摆了。这类问题特别具有迷惑性编译没报错下载也成功看起来程序在跑实际上早就死在某个函数里。发现这种问题的关键就是看主循环里的心跳灯有没有闪。如果心跳灯都不闪第一反应就应该是程序卡死而不是显示驱动的问题。我见过很多人遇到“显示不出来”就不停换 LCD、换电机、换单片机最后才发现是程序在初始化阶段就挂了。3.2 全局变量与数据更新流程被破坏另一个高频原因是全局变量被意外覆盖。LCD 显示函数里如果定义了一个局部数组比如 char buf[8]然后 sprintf 往里面写“%f”格式的数据很容易溢出因为电压值的小数表示加上符号和单位可能超过 8 字节。数组越界后恰好覆盖到 adc_value 或者 adc_flag 变量程序表面上还在跑但数据已经被搞乱了。这类 bug 有个很恶心的特点在仿真里可能表现不出来因为不同编译器和链接器的内存布局不同到了实物上又因为变量排布不同炸的位置也不一样。我在这个项目里遇到的状况是指针的角度值被一个越界写入的字符串破坏导致电机每次收到的角度都是乱码于是指针要么不动要么乱甩而 LCD 那边因为数据还没被污染反而能显示。这里必须重点提醒显示缓冲区必须给足空间并且优先使用 snprintf 而不是 sprintf。宁可多留几个字节也不要冒险越界。越界写入这种问题一旦触发可能把整个数据区都打乱导致各种匪夷所思的现象排查成本极高。3.3 总线冲突导致两边同时失效如果 LCD 和电机驱动共用同一条 I2C 总线比如 LCD 是 PCF8574 转接的 I2C 版本步进电机又挂在一颗 I/O 扩展芯片上那还得考虑总线被某个设备拉死的情况。有一个很隐蔽的现象当程序尝试给不存在的设备地址写数据时I2C 主机会一直等待 ACK如果 SCL 或 SDA 被外部异常拉低整个总线就挂在等 ACK 的状态。此时不管是要访问 LCD 还是要操作电机都无响应双显示自然全灭。排查方法是用逻辑分析仪抓 I2C 总线看看有没有正常的 START、地址、ACK、STOP 时序。如果没有完整的波形就先检查设备地址是否冲突、上拉电阻是否偏大、总线上是否有设备损坏导致 SDA 一直被拉低。这个坑在实物调试中非常常见仿真里反而不容易遇到因为仿真模型默认总线是理想的。4. 三个真实修复案例从代码层面解决“显示全灭”4.1 案例一LCD 初始化函数卡死先看原始代码的致命伤void lcd_init(void) { lcd_write_cmd(0x33); delay_ms(5); lcd_write_cmd(0x32); delay_ms(1); // ... } int main(void) { adc_init(); lcd_init(); /* 如果这里卡住后面的所有逻辑都不执行 */ while (1) { if (adc_flag) { adc_flag 0; value get_voltage(); needle_move(value); lcd_show_value(value); } } }我在 lcd_init() 前后各放了一次 LED 翻转结果 LED 在调用 lcd_init() 之后再也没有翻转说明程序就死在这里。进一步单步发现delay_ms(5) 依赖的 SysTick 被 ADC 配置里的相关代码影响了在某些仿真平台和 MCU 型号上这个问题确实会出现。修复办法是重新梳理初始化顺序先处理时钟和 SysTick再初始化外设确保 delay_ms() 的底层依赖在最开始就绪。我最终把延时函数改成了基于 DWT 的实现不依赖 SysTick代码稳定后没有再出现卡死。这个案例给了我很深的一个教训初始化顺序不是“写完就能跑”的特别是 SysTick、NVIC、DMA 这些全局性资源如果被两个外设同时使用一定要查清楚。你以为自己在初始化 LCD实际上可能在初始化整个系统的公共底座的路上就把路挖断了。4.2 案例二ADC 标志位始终为 0另一个项目里问题更隐蔽。主循环长这样while (1) { if (adc_complete) { adc_complete 0; value adc_value * 3.3 / 4095.0; needle_move(value); lcd_show_value(value); } }串口打印显示主循环确实在转但 adc_complete 永远为 0。原因是 ADC 配置了 DMA 模式和连续转换但 DMA 的通道映射在目标 MCU 型号上配置错了转换完成中断根本没触发所以 adc_complete 永远不被置 1。这种情况在仿真里尤其容易踩坑Proteus 的 MCU 模型对 DMA 的支持并不像真实芯片那么完整容易表现为“程序看起来正常但某个外设功能就是不生效”。我遇到过好几回 Proteus 里 ADC DMA 失效的情况换成查询方式或者中断方式反而正常。修复时我改成了定时器触发 ADC在 ADC 中断里读取结果并置 adc_complete 标志位。改了之后两个显示都恢复正常。这里特别想提醒仿真平台只是参考它不支持的边界你要心里有数。如果程序在仿真里“死活不正常”先考虑是不是仿真模型对某个外设功能支持不全再考虑自己代码的问题。4.3 案例三仿真和实物的“不显示”根本不是同一个原因第三个案例比较有代表性。朋友那边的情况是仿真里指针不动、LCD 也不显示实物上 LCD 也不显示但是 LCD 有背光屏幕看起来微微发白就是没字。分开查之后实际是两个问题叠加。仿真里指针不动是因为 Proteus 里选的表头模型是“电压驱动”类型需要给一个模拟电压信号来移动指针而他写的固件输出的是步进脉冲模型根本不能识别脉冲所以指针一动不动。这不算真正的代码 bug而是仿真对象选错了。实物 LCD 有背光没字根本原因是 LCD1602 的 VO 对比度引脚悬空偏置电压不对导致字符显示不出来。这个问题说明一个现象“仿真和实物都不显示”这句话并不能推导出“同一个根因”。看起来一样实际可能一个在模型匹配层一个在硬件配置层。所以排查时一定要把“仿真不显示”和“实物不显示”当成两个独立问题分别处理最后再汇总结论。5. 显示驱动里的几个隐藏坑5.1 LCD 有背光没字和完全黑屏是两回事LCD1602 的 VO 脚是液晶偏置电压调节端通常要接一个 10k 电位器中点电压调到 1V 左右字符才清晰。很多板子直接把 VO 接 GND看起来对比度最大反而容易“全黑”或者“全黑块”。我的实际经验是完全黑屏先检查使能端、RS、RW 有没有接对初始化时序有没有跑完有背光但没字优先怀疑对比度把电位器从最大往最小慢慢旋总有一个位置字是清晰的显示全是方块大概率是初始化时序没按数据手册来尤其上电后等待时间不足。注意一点在 Proteus 里仿真 LCD1602 时VO 脚怎么接都不影响显示因为仿真模型不考虑对比度。这就是为什么仿真里好好的实物却什么都看不见。所以实物调试时千万不要拿着仿真结果直接对照排错仿真和实物的显示链路差异性处理不好会浪费大半天时间。5.2 指针电机的驱动频率和模型匹配28BYJ-48 这种四相五线步进电机我一般用 1ms 左右的半步时序在启动和停止时加一个缓启动序列防止丢步。如果你用 GPIO 直接翻转驱动时序驱动频率太高会造成力矩下降指针反而会抖着不动。仿真方面更要注意Proteus 的电机模型和真实电机差距很大。想仿真指针表盘很多做法是用一个带有 ANGLE 引脚的仪表元件给它一个模拟电压让指针指示到对应角度而不是驱动真实的步进电机模型。如果固件里写的是电机步进逻辑放到这类模型上自然“不显示”。这不是代码 bug是仿真对象选错了。所以我的建议是仿真阶段先验证角度映射逻辑用一个“电压 - 角度”的函数把数值转换成指针角度然后输出到 ANGLE 引脚实物阶段再用真正的步进电机驱动。两边各测各的避免把仿真模型的局限和真实代码问题混在一起。5.3 显示刷新率与“数值不变”的错觉有时候指针和 LCD 其实一直在刷新但因为数据源本身一直是 0所以看上去像“不显示”。我见过有人把电压值算出来固定是 0.00LCD 显示“0.00”他以为是 LCD 没工作。这种情况的排查方式很简单把采样值强制改成一个常量比如 2048再看指针有没有从零位挪走、LCD 有没有显示一个非零数值。如果强制值时显示正常说明显示逻辑没问题数据采样链路才是重点排查对象。这也是最有效的分而治之方法比盯着代码反复看高效得多。6. 排查清单与工程习惯6.1 问题速查表把这次调试验证过的方法整理成一张表遇到类似问题时可以直接对照现象优先排查常用手段仿真中指针不动Proteus 模型是否支持该类驱动信号查模型文档改用 ANGLE 或电压驱动实物指针不动电机驱动时序、供电电流逻辑分析仪看脉冲万用表测驱动电压LCD 完全黑屏初始化时序、背光、供电单步调试、LED 指示、示波器看数据线LCD 有背光没字VO 对比度、初始化命令调电位器核对时序指针和 LCD 都不动主循环是否运行、公共数据源串口打印、LED 心跳串口有数据但显示不动显示刷新逻辑、变量是否被覆盖强制常量测试debugger watch这张表我贴在工位上很久了遇到显示问题直接对照基本能少走一半弯路。6.2 值得养成的四个工程习惯第一每个显示模块都要有一个自检测试函数。上电后先跑一遍自检强制写入固定内容或转动固定角度自检没通过时通过 LED 或蜂鸣器提示而不是让程序带病运行。第二主循环里必须有一个“心跳”。不管是用定时器翻转 LED还是每 100ms 发一个串口包目的都是让你任何时候都能回答一个问题程序到底活没活着。这个信息在排查双显示全灭问题时是决定性的没有它你只能猜。第三仿真和实物交叉验证但不要互相替代。仿真通过只能说明逻辑层面成立模型不覆盖的硬件特性比如 LCD 对比度、电机驱动电流、I2C 上拉电阻仍然需要在实物上单独验证。第四显示缓冲区一律用安全函数。能写 snprintf 就不要写 sprintf能定义 16 字节就不要只定义 8 字节。数组越界问题在仿真里可能潜伏很久到实物才爆发而到实物阶段排查成本已经高了很多。最后说点个人感受。调试这类“指针和 LCD 都不显示”的问题我最大的教训就是不要被两个显示同时失效的现象吓住也不要把它当成两个问题去排查。先看数据源活着没有再看主循环跑起来没有最后才追显示驱动和硬件。只要这条思路不乱多奇怪的现象都能拆干净。如果你也正在被类似问题卡住建议先把代码里“公共数据”相关的环节翻出来给每个关键函数前后都加上调试用的 LED 翻转或串口输出。有时候一个不起眼的心跳灯能帮你省下好几个小时的瞎猜时间。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门