ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32智能投影仪外挂控制板:红外、状态机与安全时序设计

STM32智能投影仪外挂控制板:红外、状态机与安全时序设计

1. 先把“智能投影仪控制”这件事拆开看

1.1 控制板到底要控什么,边界在哪里

很多人一说“用STM32控制投影仪”,脑子里想的是一块板子从零驱动DLP光机、点亮LED光源、跑梯形校正算法。这条路走不通,也不该走。投影仪整机内部的光机、驱动板、色轮时序、DMD微镜刷新,都是整机厂做了十几年调校的东西,外人拿一块MCU根本接不进去,就算接进去也没有光学参数可以配合。真正可落地、而且市场上大量产品在用的做法是:STM32做一个外挂的智能控制板,站在投影仪的外面,用投影仪原本就对外开放的接口去“替人按键”。这些接口无非三类——红外遥控接收窗口、串口控制口(工程机、商用机常见)、以及电平触发口(很多机型背部的12V Trigger)。

想清楚这个边界以后,项目范围就非常清晰了:STM32要干的事是“感知环境 + 下发指令 + 守住安全”,而不是“驱动光机”。感知环境靠温湿度、光照、人体存在、电流检测;下发指令靠红外发射和串口协议解析;守住安全靠一套围绕“灯泡/LED光源有余热”的状态机。这三件事拼起来,才是一个完整的、能真正放在会议室或者家庭吊装场景里跑起来的东西。

我见过太多初学者一上手就去啃光机驱动时序,啃了两周发现连个能亮的东西都没有,最后放弃。反过来,先做外挂控制板,一周之内就能看到“板子上电、投影仪自己开机、风扇延时关闭”的完整闭环,成就感完全不一样,而且这套东西是真的能装到实际项目里的。

1.2 为什么是STM32,而不是现成的智能模块

选型的第一个问题永远是“要不要用MCU”。市面上有一堆Wi-Fi模组、智能插座方案、红外宝塔,直接买来配个APP就能用,为什么还要自己写固件?答案在确定性和安全时序上。投影仪这类设备有一个很讨厌的特性:光源关闭后,散热风扇必须继续转足够长的时间把余热排掉,否则光机寿命直线下降,严重的会烤坏透镜镀膜。智能插座断电是“啪”一下全断,风扇跟着停,这是最典型的毁机操作。任何云平台掉线、路由器重启、APP卡死,都不应该影响这件事。本地MCU跑裸机状态机,掉网了照样能守住延时关机,这才叫可靠。

第二个理由是接口丰富度。STM32F103这一档的芯片,两路UART、两路I2C、两路SPI、多个定时器PWM、12位ADC,做红外发射(需要精确的38kHz载波和微秒级时序)、串口协议、NTC采样、风扇调速,一颗芯片全包,BOM成本压得很低。换成纯Wi-Fi模组,PWM精度和定时器数量往往不够,做红外载波抖动大,遥控距离直接腰斩。

第三个理由是生态。HAL库虽然有争议,但对于这种外设多、逻辑不复杂的控制类项目,它把GPIO、定时器、UART、ADC的初始化全部模板化,你只需要关心业务逻辑。我个人的习惯是:初始化全部用CubeMX生成,业务代码手写,绝不依赖库里的延时和阻塞函数。

注意:选型时不要盲目追高性能。这个项目里CPU负载很低,主频跑到72MHz都算浪费,重点是定时器资源和中断响应确定性,不是算力。选一颗定时器够用、供货稳定的型号,比选一颗跑得快的更重要。

2. 硬件设计:那些看起来不起眼但要命的细节

2.1 供电域划分与上电时序

整个控制板最容易被忽略、又最容易烧东西的地方就是供电。投影仪吊装在天花板上,控制板通常从投影仪自身的待机电源取电,或者单独引一路12V。这里必须分清三个域:12V原始输入域、5V中间域、3.3V逻辑域。12V先过一级 buck 降到5V,再由LDO降到3.3V给MCU。为什么中间要加一级5V?因为红外发射管的脉冲电流能到150~300mA,这个瞬间电流如果直接从3.3V LDO抽,会把MCU的电源拉塌,表现为“一发射红外,MCU就复位”。这是我踩过的第一个坑,后来在5V和3.3V之间加了π型滤波(10μF + 磁珠 + 0.1μF)才彻底解决。

上电时序也要想清楚。MCU上电复位需要时间,而继电器或者MOS管控制的对外输出口在上电瞬间是未知态,如果这个口直接接到投影仪的触发输入,可能导致投影仪误开机。稳妥做法是在输出口加下拉电阻,硬件保证默认关闭,软件初始化完成后再主动拉高。另外,如果控制板和外接设备共用一路电源,要考虑谁先上电——MCU先起来、外设后上电是最安全的顺序,反过来容易出现IO口倒灌,长期跑会腐蚀IO。

电源部分的经验值:buck 输出加一颗 470μF 电解应付脉冲负载,LDO 输入端加 TVS 管防浪涌,所有对外接口(串口、触发口)一律串 100Ω 电阻并加 TVS,天花板上的设备最怕的就是雷雨天的感应电压。

2.2 红外发射电路:38kHz载波与驱动能力

红外是消费级投影仪唯一通用的控制入口,做好这一块,项目就成了一半。硬件上分两部分:载波调制和功率驱动。载波由定时器PWM产生,38kHz是绝大多数接收头的中心频率。以72MHz主频计算,分频系数取0、自动重装值取1894,实际频率是 72,000,000 / 1895 ≈ 37994Hz,偏离中心频率约0.016%,接收头的带通滤波完全能接受,遥控距离基本无损。占空比我一般取1/3,因为红外LED允许的脉冲峰值电流远大于平均电流,占空比低可以把峰值电流做大,同时不让管子发热。

功率驱动这一级,千万别直接用IO口推LED,IO的灌/拉电流只有20mA级别,做不了远距离。标准做法是NPN三极管或小MOS做开关,红外LED串限流电阻。算一下:5V供电,红外LED正向压降约1.2V,想要150mA峰值,电阻就是 (5 - 1.2) / 0.15 ≈ 25Ω。实际取22Ω或27Ω都可以,我偏22Ω,留一点余量。要注意的是这个150mA是脉冲电流,不是持续电流,因为载波占空比1/3、编码里还有大半时间是空闲,平均电流不到10mA,一颗普通SOT-23三极管完全扛得住。

提示:红外LED尽量不要和MCU放在同一块小板上朝同一个方向,接收头对发射管的直射光非常敏感,容易自己干扰自己。物理上错开90度,或者在发射管周围加一圈黑色热缩管做遮光。

2.3 串口控制口:电平匹配与隔离

工程投影仪背后那个9针口是宝藏,通过它可以直接下发开关机、切换信号源、查询状态的指令,比红外可靠得多——红外是单向的,发出去不知道有没有收到,串口是双向的,能查询、能确认。但直接拿MCU的TTL电平接上去是错的,必须加电平转换。常见做法是用专用收发芯片,把3.3V的TTL转成±电平,同时注意方向控制。如果板子和投影仪不是同一个电源系统,还要考虑地环路,我一般会加光耦隔离,虽然多花几毛钱,但能避免“插上串口线就死机”这种玄学问题。

协议层面,这类控制口发的都是短帧,比如“开机”“关机”“查询电源状态”,帧格式通常是固定的几个字节加结束符,没有复杂的握手指令。但也有部分机型用的是网络控制协议,那套东西基于TCP,帧结构相对规整,做起来要解析长度字段和校验。我的建议是:先抓包。拿一台样机,用串口助手或者网络调试工具,把原厂控制软件下发的每一帧原样记录下来,做成一张指令表,比翻任何手册都准。

2.4 温度采样:NTC分压的计算与精度核算

测温是这个项目里唯一不能省的模拟量。投影仪出风口温度是最直接的“它是不是热过头了”的指标。我用的是10K NTC(B值3950),上拉10K到3.3V,中间点接ADC。这个分压网络有几个数值得算明白。

25℃时NTC阻值就是10K,分压后电压是 3.3 × 10K/(10K+10K) = 1.65V。12位ADC在3.3V参考下,一个LSB约0.806mV,对应读数2048左右。再看灵敏度:NTC的阻值随温度变化率 dR/dT = -B·R/T²,代入B=3950、R=10000、T=298K,得到约 -445Ω/℃。分压网络对阻值的灵敏度 dV/dR = 3.3 × R_pullup / (R+R_pullup)²,在25℃时约 8.25×10⁻⁵ V/Ω。两者相乘,得到约-36.7mV/℃。也就是说每升高1℃,ADC读数变化约45个LSB——这个分辨率对散热控制来说绰绰有余,不需要外部运放放大。

真正麻烦的是非线性。NTC的阻值-温度曲线是指数型的,直接线性换算在高温段误差会到好几度。工程上的处理方式是查表加分段线性插值:在5℃、15℃、25℃、35℃……每10℃打一个点,用二分查找定位区间,区间内做线性插值。这样整段误差能控制在0.3℃以内,代码不到五十行,比现场跑浮点指数运算省事得多,也更适合没有FPU的芯片。

注意:NTC要紧贴散热鳍片或者出风口,别悬空在机箱里,否则测的是机箱空气温度,反应慢半拍,等它报警的时候光机可能已经超温了。固定方式用导热硅胶或者压片,不要用热熔胶,那个东西七十度就软了。

2.5 风扇调速:PWM频率与转速闭环

散热风扇基本都是四线制:电源、地、PWM输入、转速反馈。PWM频率有讲究,规范要求是25kHz,这个频率高于人耳可闻范围,不会产生啸叫。我见过有人用几百赫兹去驱动,结果整机嗡嗡响,用户投诉噪音大——这不是风扇坏了,是驱动频率选错了。定时器配置上,72MHz下取预分频72、重装值40,得到 72MHz/(72×40) = 25kHz,占空比分辨率2.5%,够用。

转速反馈是一路开漏输出的方波,风扇转一圈给两个脉冲。测速用输入捕获或者定时器计数都行。为什么要闭环?因为风扇老化以后转速会掉,同样的PWM占空比吹不出同样的风量,光靠开环控制,机器用一年就散热不良了。闭环的思路很简单:设定目标转速,测实际转速,误差超过阈值就调整占空比。哪怕是简单的比例控制,也比纯开环强得多。我一般设三个档位:静音档40%占空比、标准档65%、强冷档100%,只在温度超过阈值时切到强冷。

2.6 电流检测:判断“它到底开没开机”

这是一个非常实用但很少有人做的设计。红外发出去了,投影仪到底开了没有?串口查询也可能因为协议实现差异返回假状态。最可靠的判据是电流。投影仪待机功耗只有零点几瓦,开机后光源点亮,整机功耗会跳到几十瓦甚至上百瓦,电流变化极其明显。检测方式有两种:串在主供电回路里的分流电阻(需要隔离运放,成本高),或者非接触式的霍尔电流传感器(串在火线上,输出模拟电压,直接接ADC)。后者更安全,不破坏原有布线,精度对判断“开/关”这种二值状态完全够用。

有了这个信号,整套逻辑就闭环了:发开机指令后,轮询电流值,3秒内电流跃升超过阈值,判定开机成功;超过20秒还没动静,判定失败,重发一次;两次失败就上报异常。这比“发完就当它开了”可靠一百倍,也是整个项目里最像“工业级”的一个设计点。

3. 软件架构:状态机是这套东西的骨架

3.1 分层方式与任务划分

裸机还是RTOS?这个项目我强烈建议裸机。理由很实在:任务数量少,实时性要求集中在红外发射的微秒级时序上,RTOS反而会引入调度抖动。裸机加一个主循环 + 中断 + 软件定时器,逻辑清晰、调试方便、没有栈溢出的风险。

分层上我习惯分三层。驱动层:GPIO、定时器、UART、ADC的初始化与读写,全部由CubeMX生成,自己只在上面包一层“发一个红外码”“读一次温度”这样的接口。逻辑层:状态机、指令解析、安全保护。应用层:业务动作序列,比如“开机”这个动作实际上是一串有序操作——先开风扇、再发红外、再等电流反馈。这样分的好处是,逻辑层完全不知道自己控制的是红外还是串口,将来换机型只改应用层。

主循环的节奏感很重要。我一般用一个1ms的系统滴答做时基,主循环里跑一个10ms的任务调度表:每10ms采样一次温度、每50ms更新一次风扇、每100ms轮询一次通信、每500ms刷新一次状态机。红外发射放在中断里,因为它需要微秒级精度,不能受主循环影响。

3.2 状态机设计:从待机到冷却的六个态

这是整个项目最核心的部分,也是最容易写错的地方。我把状态机分成:待机(IDLE)、自检(SELFTEST)、预热(WARMUP)、运行(RUN)、冷却(COOLING)、故障(FAULT)。

待机态只做两件事:等指令、维持风扇最低转速(如果机型需要)。收到开机指令后进自检:读一次温度,如果超温直接拒绝开机,避免在异常状态下强行点亮光源;检查风扇是否转起来(读转速反馈),转不起来也不许开机。自检通过进预热:发开机红外,然后进入一个持续几十秒的等待期——这个等待期不是随便定的,光源从点亮到稳定输出需要时间,这段时间内不能接受“关机”以外的指令,否则容易出现光源还没起辉就被断电,对灯芯损伤很大。

运行态是主战场:持续监控温度、电流、通信连接。温度超过第一阈值(比如75℃)切强冷风扇;超过第二阈值(比如85℃)主动下调亮度或者发节能指令;超过第三阈值(95℃)直接进故障态强制关机。故障态不是终点,它还会继续维持风扇全速运转,等温度降下来再回待机。

冷却态是被最多人写错的地方。收到关机指令后,先把光源关掉,然后风扇必须继续转120秒以上。这个时间不能拍脑袋,要看机型:小功率LED光源60秒够,大功率灯泡机要180秒甚至更久。我的做法是把延时做成可配置的常量,部署时按机型调整。冷却期间如果收到开机指令,要拒绝或者先等冷却完成,绝对不能直接重启——冷机重启会导致光源热冲击。

提示:状态机里的每一次迁移都应该有日志输出(哪怕只是一串串口打印),否则现场出问题的时候你根本不知道它卡在哪一态。我现在的习惯是每个状态占一个字节的状态码,随时可以通过串口查询,排查效率提升非常明显。

3.3 通信帧格式:为什么要加校验

上位机(手机APP、中控面板、上位网关)和下位机之间的通信,最容易出问题的不是协议本身,而是“不知道自己收到了什么”。我做自定义帧的时候固定这么几段:帧头两个字节(故意选一个不会在数据里出现的组合)、长度字段、命令字、负载、校验字节。校验用CRC8,多项式选常见的那个,代码十几行,查表法更快。

为什么一定要校验?因为现场环境里串口线可能几米长,走的是弱电桥架,旁边就是强电线,干扰不是理论问题。没有校验,偶发的位翻转会导致命令错位,本来是“查询状态”,被解析成“关机”,用户在演示途中投影仪突然黑屏,这种事出一次就够丢脸的。加了CRC以后,误帧直接丢弃,等下一个帧头重新同步,健壮性完全不是一个级别。

接收用中断加环形缓冲区,不要在中断里解析。中断只负责把字节塞进缓冲区,主循环里再去取、找帧头、验长度、算校验、执行命令。这样即使解析逻辑写得再重,也不会阻塞中断,不会丢数据。缓冲区大小我一般给256字节,普通控制指令几十字节,绰绰有余。

3.4 几个必须算准的外设参数

UART波特率误差:115200波特率,72MHz时钟,BRR值为 72,000,000/(16×115200) = 39.06,取整39,实际波特率是 72,000,000/(16×39) = 115384,误差0.16%,远小于3%的容忍上限,通信稳定。但如果主频选了奇怪的晶振,比如用内部RC振荡器,那误差可能到2%以上,长帧就会开始丢字节。所以这类项目必须用外部晶振,别省那两毛钱。

ADC采样时间:NTC分压源阻抗是10K并联,等效约5K,ADC采样保持电容需要足够的充电时间。采样周期设到239.5个周期,转换时间约 239.5+12.5 = 252 个ADC时钟,在12MHz ADC时钟下约21μs,非常宽裕。别为了省时间设最小的1.5周期,那样读数会明显偏低。

红外载波占空比:PWM的比较值取重装值的三分之一,也就是631左右(1894/3)。这个数值决定了红外LED的峰值电流和平均功耗的平衡点,取二分之一也能工作,但管子发热会多一点。

4. 从零到跑通:实操过程记录

4.1 第一步:让红外“发得出去”,先自检再联调

新手最常见的误区是拿真机当调试对象:改一次代码,对着投影仪按一次,看它开不开机。这种方式效率极低,因为变量太多——可能是码不对,可能是载波不对,可能是距离不够,可能是发射管接反了。

正确的顺序是先闭环自检。做法是:在板子上同时装一个红外发射管和一个红外接收头,接收头输出的就是解调后的基带信号,用MCU自己的输入捕获去读它。发一个码,读回来,比对时序。如果能原样读回,说明载波频率、占空比、脉宽都对;读不回来或者读回来是乱的,问题一定出在发射侧,跟投影仪无关。这一步能让调试效率提升好几倍,我第一次做的时候用示波器探头点接收头输出,看着那一串9ms低电平、4.5ms高电平的引导码,心里瞬间就有底了。

验证通过以后,再用“学习模式”把原装遥控器的码抓下来。学习模式下,接收头捕获到的每个脉宽都记进数组,原样存进Flash。这样不管什么机型,学习一次就能用,通用性直接拉满。存储时用两个字节表示一个脉宽(单位是微秒除以10,最大可表示655ms),一个码30多个脉宽,占几十字节,留一页Flash专门存这些码,掉电不丢。

4.2 第二步:把状态机跑起来,用串口打印观察

硬件通路打通后,先别急着接投影仪,把状态机单独跑起来,用串口把每一次状态迁移打印出来。我给自己定了一个规矩:任何一个新写的状态机,必须能在不接任何外设的情况下,靠人为注入事件走完全部状态。开机事件、超温事件、关机事件、故障事件,全部可以从串口模拟注入。这样相当于给状态机写了一套“单元测试”,等真机接上去以后,剩下的就只是硬件时序问题了。

这一步还能顺手验证一个很关键的东西:冷却延时到底有没有被正确执行。很多人的代码在收到关机命令后直接进待机,风扇跟着停,测试的时候不接投影仪看不出来,接上真机跑几次,光源就开始出问题。把延时做成状态机里的独立一态,用软件定时器计时,中途不允许被其他事件打断,这是唯一靠谱的写法。

4.3 第三步:联调现场,把电流反馈接进来

真机联调的时候,最有价值的信息来自电流。我第一次联调时遇到一个诡异现象:红外码完全正确(用接收头验证过),但投影仪有时开有时不开。后来接上电流传感器才发现,问题出在指令发送的时机——投影仪刚上电时,红外接收电路还在初始化,这段时间发的码会被吞掉。解决办法很简单:上电后先延时两秒,再发开机码;如果两秒后电流没起来,隔一秒重发,最多三次。加了重试以后,成功率从七成提升到接近百分之百。

这个经验说明一件事:单向通信必须配合状态确认。发出去不等于送达,只有通过电流、温度这些物理量确认对方真的响应了,这套系统才算闭环。这也是我后来在所有类似项目里都坚持加一个“执行结果确认”环节的原因。

4.4 关键代码:红外发送与温度换算

红外发送的核心是把一串脉宽数组变成载波的通断。用一个定时器做载波输出,另一个定时器做脉宽计时,在计时中断里切换载波输出的使能:

/* 红外发送:按脉宽数组依次切换载波输出使能 */ static void ir_send_frame(const uint16_t *pulses, uint8_t len) { ir_pulses = pulses; ir_len = len; ir_idx = 0; ir_level = 1; /* 第一个脉宽为载波输出 */ IR_CARRIER_ON(); htim_pulse.Init.Period = pulses[0]; /* 单位10us,1us计数 */ HAL_TIM_Base_Start_IT(&htim_pulse); } /* 脉宽计时中断:翻转载波输出,加载下一个脉宽 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim != &htim_pulse) return; ir_idx++; if (ir_idx >= ir_len) { IR_CARRIER_OFF(); HAL_TIM_Base_Stop_IT(&htim_pulse); ir_busy = 0; return; } ir_level = !ir_level; if (ir_level) IR_CARRIER_ON(); else IR_CARRIER_OFF(); __HAL_TIM_SET_AUTORELOAD(&htim_pulse, ir_pulses[ir_idx]); __HAL_TIM_SET_COUNTER(&htim_pulse, 0); }

温度换算用查表加插值,代码里只处理整数,避免浮点开销:

/* NTC 10K B3950 查表,表项为 {ADC读数, 温度×10},按ADC升序排列 */ typedef struct { uint16_t adc; int16_t temp_x10; } ntc_point_t; static const ntc_point_t ntc_tab[] = { { 3800, -100 }, { 3600, 0 }, { 3300, 100 }, { 2900, 200 }, { 2048, 250 }, { 1400, 300 }, { 900, 400 }, { 560, 500 }, { 330, 600 }, { 200, 700 }, { 120, 800 }, { 70, 900 }, }; int16_t ntc_read_temp_x10(uint16_t adc) { uint8_t n = sizeof(ntc_tab) / sizeof(ntc_tab[0]); if (adc >= ntc_tab[0].adc) return ntc_tab[0].temp_x10; if (adc <= ntc_tab[n-1].adc) return ntc_tab[n-1].temp_x10; for (uint8_t i = 0; i < n - 1; i++) { if (adc <= ntc_tab[i].adc && adc >= ntc_tab[i+1].adc) { int32_t d_adc = ntc_tab[i].adc - ntc_tab[i+1].adc; int32_t d_temp = ntc_tab[i].temp_x10 - ntc_tab[i+1].temp_x10; int32_t off = adc - ntc_tab[i+1].adc; return ntc_tab[i+1].temp_x10 + (off * d_temp) / d_adc; } } return 250; }

这两段代码都不是什么高深东西,但胜在结构清楚、没有动态内存、没有浮点、中断里只做最轻的活儿。这类项目里,我宁可代码看起来“土”一点,也不愿意为了优雅引入一堆库和抽象层——现场调试的时候,你不会有心情去翻三层封装去找一个空指针。

4.5 上电时序与安全保护的落地写法

安全保护这块我总结成三条硬规则,写死在代码里,不给任何上层指令覆盖的机会:第一,温度超过硬阈值时,任何开机指令无效;第二,光源关闭后风扇强制运转不少于设定时长,计时器不受外部事件打断;第三,通信丢失超过设定时间(我一般设30秒),自动进入安全关机流程。

第三条尤其重要。中控面板死机、网线被拔、上位机崩溃,都是现实中会发生的事。如果投影仪一直保持开着,会议室没人管,散热、能耗、寿命都是问题。加一个心跳超时保护,成本为零,收益巨大。实现上就是在主循环里记录最后一次收到有效帧的时间戳,超时就触发热关机流程,并且打印一条明确日志说明是超时导致的。

5. 常见问题与排查实录

5.1 红外“有时候灵有时候不灵”

这是出现频率最高的问题,原因通常分四类。载波频率偏了:拿示波器或者逻辑分析仪(带频率计功能)点一下发射管驱动脚,确认是38kHz附近,偏差超过2kHz就开始影响距离。驱动电流不足:表现为近距离能控、三米外失灵,用万用表测脉冲电流不方便,改测平均电流,如果是预期的一半以下,说明三极管没饱和或者限流电阻取大了。接收窗口被遮挡或者角度太偏:投影仪的红外接收窗一般在前脸,吊装后朝下,控制板如果贴在机身侧面,发射角度可能超过30度。环境光干扰:阳光直射、某些LED灯具的调制光会淹没红外信号,这个只能靠提高发射功率或者改用串口来控制。

排查顺序建议从易到难:先换角度,再看波形,最后改电路。别一上来就怀疑代码,红外发射的代码往往是最不容易出错的。

5.2 风扇啸叫、转速不稳、报错停转

风扇问题的排查表我整理过一份,实际用起来挺顺手:

现象可能原因排查手段处理方式
明显啸叫PWM频率落在可闻段示波器测PWM脚频率改为25kHz,避开20kHz以下
转速忽高忽低闭环比例系数过大或采样周期太短打印转速与占空比曲线降低比例系数,转速采样周期放到1秒
低速档停转启动占空比低于风扇启动阈值从0缓慢增加占空比观察低速档不低于35%,或先给100%启动再降速
转速反馈读数为0反馈脚无上拉或开漏未配置万用表测反馈脚电平加10K上拉,输入模式配成上拉输入
高温下风扇异响轴承润滑失效或积灰断电手动拨动扇叶更换风扇,同时检查进风口滤网

提示:风扇的低速启动阈值一定要实测。同一个型号不同批次,启动占空比可能差5%到10%。如果你的静音档设置在临界点附近,冬天能转、夏天就转不动,这种问题最难受。

5.3 继电器或电源开关抖动导致误开机

用继电器控制投影仪电源的那种方案,最容易出现的问题是上电瞬间的抖动被投影仪误判为按键。原因是继电器触点吸合有弹跳,加上线缆电感造成的电压振荡。解决办法有三个层次:硬件上在触点两端并RC吸收(100Ω串0.1μF),软件上吸合后延时50ms再确认状态,系统上干脆不用继电器控电源、改用红外控制。我现在基本不推荐继电器方案,一来有安全隐患,二来投影仪自己就能待机,没必要外部断它的电。

如果是用MOS管做电平触发,抖动的表现是触发输入被拉低又拉高,设备反复进出触发态。检查一下MOS的栅极有没有下拉电阻、驱动信号有没有上拉,通常加一个100K下拉就能解决。

5.4 串口通信偶发丢包、收到乱码

排查这类问题的顺序基本固定。先确认电平方向,TTL直连RS232电平是绝对不行的,会把接收端打坏。再确认波特率误差,尤其是双方都用内部RC振荡器的时候,误差叠加起来超过4%就开始出问题。然后看地线,长距离通信如果只接了TX/RX没接好地,共模干扰会把信号淹掉。

还有一个隐蔽的坑:某些机型的串口是半双工的,或者需要RTS/DTR做方向控制,你按全双工去发,对方收到的就是残缺帧。这种情况必须看时序,用逻辑分析仪抓双向信号,一看就明白。

最后是缓冲区溢出。如果主循环里有阻塞操作(比如某个命令里调了个长延时),接收中断一直在填缓冲区,主循环没空取,满了就丢。解决办法是把所有阻塞延时都改成状态机里的非阻塞等待,这是裸机编程里最基本的纪律。

6. 几个不那么显而易见的经验和后续玩法

有一个坑我印象特别深:冷却延时不能只靠软件定时器。有一次现场反馈,说设备偶尔在关机后风扇立刻停转。查了很久,最后发现是看门狗复位——程序跑飞了,看门狗把它拉回来,重启后状态机从待机开始,风扇自然是停的。后来的改法是:把“冷却中”这个状态写进备份寄存器,重启后读出来,如果还处在冷却期,就继续跑完剩余时间再停风扇。多花二十行代码,解决了一类很难复现的偶发问题。

另一个经验是关于调试接口的保留。这类板子装到天花板上以后,你再想插调试器基本不可能,所以一定要留一路串口出来,做成可以查询状态、注入事件、读取日志的“运维口”。我现在的固件里固定有这么几条命令:查询当前状态、查询温度与风扇转速、查询最近十次错误码、强制进入某个状态。就靠这几条,很多现场问题不用拆机就能定位。

后续扩展的话,我比较看好的方向是把红外学习和云端指令结合起来:板子本地存十几组常用机型的红外码,云端下发一个机型编号,板子自己去选对应码组,这样一套固件能适配一批设备,部署成本压得很低。再进一步是做场景联动——投影仪开机时自动把灯光调暗、把窗帘关上、把空调切到静音模式,这些都可以挂在同一个状态机的事件上,无非是多接几路输出。真正难的部分永远是那套安全时序和状态确认,把它做扎实了,往上面加什么功能都只是顺手的事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进