帮一台老投影仪换控制板那天,我才真正明白“智能”这两个字在嵌入式里值多少行代码。那台机器的问题是:遥控器一半按键失灵、开机要手动按三次、风扇永远全速、断电后镜头停在半路。拆开一看,原厂主板已经被修过两次,飞线到处都是。与其继续修,不如自己用STM32做一块控制板,把电源、镜头、风扇、温度、红外这几路子全部接管过来。这篇就聊这件事:用STM32实现简单的智能投影仪控制,从一个真实改造场景出发,把需求拆解、器件选型、电机驱动、状态机时序、红外解码、散热保护、现场踩坑全部走一遍。适合手里有STM32基础、想做一个“带电、带机械、带时序联锁”的完整项目的朋友参考,也适合做智能家居控制板、小型机电一体化产品的同学抄作业。
1. 把一台投影仪拆成可控对象,需求才不会跑偏
很多人一上来就问“用哪个型号的STM32”,这个问题的正确答案其实藏在需求清单里。投影仪看着是个整体,拆开之后能动的、能测的、能控的东西就那么几类,先把它们列清楚,选型是水到渠成的事。我当时的做法是拿一张A4纸,左边写“人要做什么”,右边写“板子要输出什么信号”,写完发现工作量比想象中小一半。
1.1 光机、电源、镜头、散热这四组才是真正要管的
投影仪的控制逻辑本质上是四组对象的协同:电源组负责整机上下电和待机;光机组负责光源开关、亮度档位、信号源切换;镜头组负责对焦、变焦、梯形校正;散热组负责温度采样和风扇调速。这四组之间有严格的先后顺序,不是随便哪个先开都行。
电源组最典型的做法是“双路供电”:一路待机电源(3.3V/5V)常通,给MCU和红外接收头供电;另一路主电源(12V或220V)通过继电器或大功率MOS控制,给光机和风扇供电。这样待机功耗能压到1W以内,遥控器随时能唤醒。我自己实测下来,待机电流控制在8mA左右,对一块小板子来说完全可接受。
光机组要小心。DLP光机的LED驱动电流很大,直接由MCU的IO控制是不现实的,必须经过驱动级或者发指令给光机模组本身。我的方案是只做“开关信号+亮度PWM”两路输出,具体的恒流驱动交给光机自带的驱动板,MCU只负责给使能信号和PWM占空比。这样做的好处是安全边界清晰,坏处是亮度精度受限于对方驱动板,不过对“简单控制”这个目标完全够用。
镜头组是唯一涉及机械运动的部分,也是整个项目里最容易出问题的环节。对焦电机、变焦电机、镜头盖电机都需要精确的步进控制,而且要有限位保护,否则一旦丢步就会顶到机械限位,轻则发出咔咔声,重则损坏齿轮。
散热组看似简单,其实是最需要“提前量”的部分。投影仪的光源一旦点亮,温度上升很快,风扇必须在光源开启之前就进入工作状态;关机时反过来,光源先灭,风扇要继续跑一到两分钟把余热排掉。这个时序如果写错,光源寿命会明显缩短。
1.2 把需求翻译成引脚和定时器资源
需求列完之后,我把它翻译成一张资源占用表。这张表是后面选型和布线的依据,也是排查“为什么这个功能不工作”的第一手资料。
| 功能 | 执行器件 | STM32资源占用 | 关键注意点 |
|---|---|---|---|
| 主电源开关 | 继电器模块 | GPIO推挽输出 | 需要三极管或达林顿驱动 |
| 待机指示 | 双色LED | GPIO×2 | 限流电阻按3.3V算 |
| 对焦电机 | 28BYJ-48或42步进 | TIM PWM×4通道 | 必须能输出可调频率 |
| 变焦电机 | 同上 | 复用另一组通道 | 与对焦共用驱动芯片时需分时 |
| 风扇调速 | 4线风扇PWM | TIM通道×1 | 频率25kHz左右最合适 |
| 温度采样 | NTC 10K | ADC通道×1 | 需要稳定的参考电压 |
| 红外接收 | 一体化接收头 | TIM输入捕获 | 捕获精度决定解码成功率 |
| 按键 | 轻触开关×4 | GPIO外部中断 | 硬件RC+软件消抖 |
| 上位机通信 | UART | USART×1 | 加TVS和串阻 |
| 蜂鸣器 | 有源蜂鸣器 | GPIO或PWM | 有源直接推,无源要PWM |
这张表一列完,资源账就很清楚了:至少需要1个高级定时器(或两个通用定时器)用来输出PWM和做输入捕获,1个ADC通道,1个USART,再加上十几个GPIO。STM32F103C8T6这种48脚、64KB Flash、20KB RAM的芯片刚好卡在够用和富余之间,价格也友好。
1.3 哪些动作必须联锁,哪些可以独立跑
这是很多人写固件时忽略的一层。投影仪里有一批动作是有因果关系的,不能当成独立命令处理。我把它们整理成三条硬规则:
- 风扇未达到设定转速,禁止点亮光源。这条规则防止光源在无风状态下过热。
- 光源未关闭且风扇未延时结束,禁止切断主电源。这条防止余热堆积。
- 任何一路电机运动期间,禁止执行另一路电机的运动命令。这条防止电流叠加导致电源跌落复位。
反过来,红外接收、按键扫描、温度采样、串口通信这几件事是完全独立的,任何时候都可以并行跑,不需要互相等待。把这两类事情区分开,固件架构自然就清晰了:独立的走轮询或中断,联锁的走状态机。
2. 主控选型与驱动电路:钱花在刀刃上的几个判断
选型这件事最怕两种极端:一种是拿最小系统板硬扛,结果引脚不够、定时器不够,天天改板子;另一种是一上来就上高性能型号,成本翻倍但功能没用上。我的判断标准很朴素:先看定时器数量,再看ADC精度,最后看引脚数。投影仪控制这个场景里,定时器比算力重要得多。
2.1 算一遍资源账,别凭感觉选芯片
把常见的几档芯片横向摆一下,差别一目了然。
| 型号 | 主频 | Flash/RAM | 定时器情况 | 适合度 |
|---|---|---|---|---|
| STM32F030F4 | 48MHz | 16KB/4KB | 定时器少,无高级定时器 | 功能砍到底才够用 |
| STM32F103C8T6 | 72MHz | 64KB/20KB | 4个定时器,1个高级 | 本项目的甜点区 |
| STM32F401CCU6 | 84MHz | 256KB/64KB | 定时器丰富,带FPU | 需要复杂算法时选 |
| STM32G071 | 64MHz | 128KB/36KB | 定时器灵活,外设新 | 新设计优先考虑 |
F103C8T6之所以合适,核心原因是它有TIM1这个高级定时器,可以同时输出多路互补PWM还带死区控制,驱动步进电机和风扇都不紧张;TIM2/TIM3/TIM4做输入捕获和普通PWM也够用。20KB RAM对于这个项目来说绰绰有余,状态机、缓冲区、查表数据加起来不到4KB。
如果后面想加梯形校正的实时计算,或者要做简单的图像几何变换,那就该考虑带FPU的F4系列了。浮点运算在F103上跑一次变换要几百微秒,实时性就没了;F401有硬件浮点,同样的运算几十微秒搞定。这个差别在做四角校正的连续调节时能明显感觉出来,旋钮转动手感是否跟手,很大程度上取决于这个。
2.2 继电器、MOS管、光耦各自守哪条边界
驱动这块最容易出事,也最容易被简化处理。我的原则是:交流负载用继电器,直流大电流用MOS,跨电压域用光耦,三者不要混着用。
主电源如果是220V交流进线,只能用继电器。选型时看三个参数:触点容量(建议留两倍余量)、线圈电压(5V或12V,看板上电源)、触点形式(一组常开就够)。继电器线圈两端必须并一个反向续流二极管,比如1N4148或1N4007,否则线圈断电瞬间的反向电动势能轻松击穿三极管,甚至顺着地线干扰到MCU。我第一次做的时候忘了这个二极管,结果每次继电器释放,MCU就随机复位一次,查了整整一个下午。
直流侧比如风扇、LED指示灯、镜头盖电机,用MOS管更合适。低边驱动选AO3400这类逻辑电平MOS,栅极串一个100Ω电阻抑制振铃,栅源之间并一个10kΩ下拉电阻保证默认关断。这里有个细节:逻辑电平MOS的Vgs(th)要低于3.3V,别拿IRF540这种标准MOS直接接3.3V IO,它根本开不透,会发热烧掉。
光耦用在需要隔离的地方,比如外部信号输入、长线通信。PC817是最常见的选择,但它的传输速度慢,只适合开关量;如果要传PWM或者串口数据,得选6N137这类高速光耦。我在串口上加了TVS和22Ω串阻,没有用光耦,因为这块板和上位机共地,隔离意义不大,加了反而增加成本和故障点。
2.3 电源树和地线:省这一步后面全是玄学
电源部分我是这么分的:12V输入经过LM2596降到5V,供继电器、风扇、步进电机驱动;5V再经过AMS1117-3.3降到3.3V,供MCU、红外接收头、ADC基准。两级降压的好处是电机侧的电流波动不会直接冲击MCU的供电。
地线处理上,我在PCB上做了单点接地:功率地(电机、继电器、风扇)和数字地(MCU、晶振、ADC)在电源入口处汇合,中间用一个0Ω电阻或者磁珠连接。这个做法在低速板子上看起来有点讲究过头,但实测下来,电机启动时的ADC采样抖动能从±8个LSB降到±2个LSB,温度读数的稳定性提升非常明显。
ADC的参考电压也要注意。F103的VDDA如果直接接3.3V,而3.3V又被电机干扰,温度采样就会跳。我的做法是在VDDA引脚旁边放一个10μF钽电容加一个100nF陶瓷电容,物理位置尽量靠近引脚。这一颗电容的钱,比后面写滤波算法省事多了。
3. 镜头对焦与梯形校正:步进电机是整个项目的手感来源
投影仪的用户体验有一半压在镜头上。对焦慢一点、抖动一下、到头咔咔响,用户立刻觉得这机器“廉价”。所以这部分我花的时间最多,也是最值得展开讲的地方。
3.1 细分驱动和加减速曲线,决定了镜头是“丝滑”还是“抽搐”
先说电机选型。小功率场合用28BYJ-48这种带减速箱的步进电机最省事,它内部减速比大约是1:64,输出轴转一圈需要2048个八拍步,分辨率足够做精细对焦,而且价格便宜、驱动简单,一个ULN2003就能推。缺点是扭矩小、转速低,镜头负载稍大就会丢步。
如果镜头模组比较重,就得上42步进配DRV8825或A4988这类细分驱动。1/16细分下,200步的电机转一圈需要3200个脉冲,运动平滑度明显更好。细分的好处不只是平滑,还有噪音低——投影仪放在客厅,对焦时“嗡嗡”声太明显会很掉价。
加减速曲线这块,我一开始图省事用的是固定频率启动,结果电机在低速段还行,一加速就直接失步,镜头走一半停住。后来改成梯形加减速:启动阶段频率从400Hz线性升到2kHz,匀速段保持,停止前再降回400Hz。实现方式很简单,用定时器更新中断,每次中断根据当前阶段调整ARR的值。
typedef enum { MOTOR_IDLE = 0, MOTOR_ACCEL, MOTOR_RUN, MOTOR_DECEL } motor_state_t; typedef struct { motor_state_t state; uint32_t steps_total; uint32_t steps_done; uint16_t freq_now; uint16_t freq_min; uint16_t freq_max; } stepper_t; /* 定时器更新中断里调用,每次中断走一步 */ void stepper_tick(stepper_t *m) { if (m->state == MOTOR_IDLE) return; m->steps_done++; if (m->state == MOTOR_ACCEL) { m->freq_now += 40; if (m->freq_now >= m->freq_max) { m->freq_now = m->freq_max; m->state = MOTOR_RUN; } } else if (m->state == MOTOR_DECEL) { if (m->freq_now > 40) m->freq_now -= 40; else m->freq_now = m->freq_min; } /* 剩余步数进入减速区,提前切换状态 */ if (m->state == MOTOR_RUN && (m->steps_total - m->steps_done) <= (m->freq_max / 40)) { m->state = MOTOR_DECEL; } if (m->steps_done >= m->steps_total) { m->state = MOTOR_IDLE; TIM_Cmd(TIM3, DISABLE); return; } /* 重装载值 = 定时器时钟 / (2 * 目标频率) / 预分频 */ uint32_t arr = 72000000UL / (2UL * m->freq_now * 72UL); __HAL_TIM_SET_AUTORELOAD(&htim3, arr); }这段代码里有个容易忽略的点:减速要提前触发,不能等走到最后一步才开始减速,否则最后几步会以高速冲过去,机械冲击很大。我按“当前最高频率除以加速度”估算减速所需步数,简单但够用。
3.2 限位和零位标定,不能只信软件计数
只靠软件计数判断镜头位置,是我踩过最贵的坑。有一次测试时电机因为负载突变丢了几十步,软件以为还在行程中间,实际已经顶到机械限位,电机持续堵转,线圈温度迅速升高,摸上去烫手。虽然没烧,但那次之后我老老实实加了限位开关。
限位开关用光电对射或者微动开关都行,我选的是带挡片的光电开关,无接触、寿命长。电路上做上拉输入,开关动作时拉低,配一个100nF电容做硬件消抖。软件里在EXTI中断里立刻停电机,同时把当前位置计数器清零。
零位标定放在上电初始化阶段:电机先往一个方向慢速走,直到触发限位,然后反向走固定步数回到行程中点附近作为“零位”。整个过程大约2秒,用户感知是“开机时镜头轻微动一下”,完全可以接受。
另外加了一重保险:行程超时检测。每次运动启动时记录时间戳,如果超过预估时间的1.5倍还没结束,直接停机并上报错误。这个机制拦住过一次齿轮卡异物的情况,否则电机可能会一直堵转到过热。
3.3 梯形校正用查表还是实时算,取决于你的芯片有没有FPU
梯形校正在简单方案里其实就是镜头位移加一点垂直偏移,不需要做真正的几何变换。我的做法是把它简化成“垂直位移量”和“水平位移量”两个参数,每个参数对应步进电机的步数,用户按上下左右键,电机按固定步长移动。
真正需要算法的是四角校正。如果只做垂直梯形校正,一张一维查表就够了:把校正角度从-30°到+30°分成61档,每档对应一个Y方向缩放系数和偏移量,预先算好存在Flash里。运行时按当前角度索引,线性插值,整个计算量只有几次乘加,F103跑起来毫无压力。
四角校正就复杂多了,需要做双线性变换,涉及除法和小数运算。在F103上我用定点数实现,把坐标放大256倍用int32运算,精度够用但代码写起来啰嗦。后来换到带FPU的芯片,直接上浮点,代码量少一半,调试也直观。这就是我前面说的“要不要上F4”的实际判断依据——如果你确定要做四角校正并且要求实时响应,别省这点钱。
4. 固件架构:一个非阻塞状态机管住整机时序
固件结构这件事,写着写着就会失控。一开始我用的是最直觉的写法:开机函数里delay 500ms,然后开风扇,再delay 300ms,再开光源。单测没问题,一接上红外接收就完蛋——那500ms里遥控器按什么都没反应,用户会以为死机了。
4.1 为什么delay串行执行在投影仪上必然翻车
投影仪同时要做的事至少有五件:红外解码、按键扫描、温度采样、风扇调速、电机运动控制。这五件事的时间尺度完全不同:红外解码是微秒级,按键扫描是毫秒级,温度采样是百毫秒级,风扇调速是秒级。用delay串行执行,等于把所有事情都拖到最慢的那个节奏上。
更要命的是喂狗。看门狗复位周期设的是2秒,如果某个流程里有三个500ms的delay,再加上电机运动的等待,很容易超时复位。产品上出现“开机偶尔重启”这种问题,十有八九是喂狗没处理好。
正确的做法是把所有“等待”变成“状态+时间戳”的轮询。主循环里跑一个调度器,每个任务有自己的周期计数器,到点执行,不阻塞。
typedef struct { void (*task)(void); uint16_t period_ms; uint16_t counter; } task_t; static task_t tasks[] = { { ir_poll_task, 10, 0 }, { key_scan_task, 10, 0 }, { temp_sample_task, 200, 0 }, { fan_ctrl_task, 500, 0 }, { led_task, 100, 0 }, }; void scheduler_run(void) { for (uint8_t i = 0; i < sizeof(tasks)/sizeof(task_t); i++) { if (tasks[i].counter == 0) { tasks[i].task(); tasks[i].counter = tasks[i].period_ms / 10; } else { tasks[i].counter--; } } } /* 主循环 */ while (1) { HAL_Delay(10); /* 基础节拍10ms */ scheduler_run(); state_machine_run(); HAL_IWDG_Refresh(&hiwdg); }这个调度器很朴素,但胜在好懂好改。所有任务的执行时间必须远小于10ms,否则会拖慢整个节拍。实测下来最重的是红外解码,单次处理大概200微秒,完全没问题。
4.2 状态机的三层结构:整机、子系统、故障
我把状态机分成三层。整机状态只有几个:待机、启动中、运行、关机中、故障。子系统状态是各个模块自己的,比如电机的IDLE/MOVING、风扇的STOPPING/RUNNING、光源的OFF/ON。故障状态单独一层,任何模块检测到异常都可以置位,整机状态机轮询到故障位就进入保护流程。
整机状态机的核心是一张时序表,把每个状态要做的事和停留时间写清楚。
| 状态 | 进入动作 | 停留时间 | 跳转条件 |
|---|---|---|---|
| 待机 | 关闭主电源,红灯常亮 | 无限 | 收到开机命令 |
| 启动中 | 开风扇→等转速→开光源 | 约1.5秒 | 光源确认点亮 |
| 运行 | 维持各子系统 | 无限 | 收到关机命令或故障 |
| 关机中 | 关光源→风扇延时→断主电 | 约90秒 | 延时结束 |
| 故障 | 关光源→风扇全速→报错闪灯 | 无限 | 断电重启 |
这个表看着简单,但每一项都要落实到代码里的超时判断。比如“等转速”不能死等,得设一个2秒超时,超时还没达到就进故障状态。同理“光源确认点亮”也要有超时。所有这些超时机制加起来,才让这块板子在现场跑得住。
4.3 上电、关机、故障三条时序的联锁细节
上电时序我踩过坑。最开始写的是:开风扇 → 等300ms → 开光源。看起来合理,但实际测试发现,风扇从静止到额定转速需要大概800ms,300ms的时候转速还很低,光源点亮瞬间温度局部位上升很快。后来改成检测风扇的FG转速反馈信号,确认转速达到设定值的80%以上再开光源,温升曲线明显平缓。
关机时序更讲究。断电之后风扇立刻停转,光源余热排不出去,长期下来光源亮度衰减会加快。我的做法是收到关机命令后先关光源,然后风扇继续以60%转速跑90秒,最后才断主电源。这个90秒是实测出来的:环境温度30℃时,光源表面温度从80℃降到45℃大概需要75秒,留15秒余量。
故障时序要“安全优先”。一旦检测到温度超限、风扇堵转、电机超时、光源异常中的任何一项,立刻关光源,风扇拉到100%,红灯闪烁报错,同时通过串口把错误码发给上位机。这里有个细节:故障状态下不要自动断电重启,否则会陷入“故障-重启-故障”的循环,用户看到的就是不停地闪。必须等用户手动断电再上电才清除故障标志。
5. 红外、按键、串口:人机交互的三种入口
投影仪的用户入口就三个:遥控器、机身按键、上位机或手机App。这三条路径的可靠性直接决定用户对整机的印象。遥控器按十次有一次没反应,用户就会觉得这机器不行,哪怕其他功能都完美。
5.1 NEC红外解码,为什么建议走定时器输入捕获
红外接收头(比如HS0038这类)输出的是解调后的数字信号,38kHz载波已经被滤掉了,MCU拿到的就是一串高低电平。NEC协议里,引导码是9ms低电平加4.5ms高电平,数据位用560μs低电平加不同的高电平区分:高电平560μs表示0,高电平1.69ms表示1。一个完整的帧是引导码加32位数据加一个停止位。
用普通GPIO轮询解码是可行的,但对时序要求高,主循环稍微卡一下就会丢帧。用定时器输入捕获就稳得多:把接收头输出接到TIM的捕获通道,配置成双边沿捕获,每次边沿触发中断,在中断里读取捕获寄存器的值,两次捕获的时间差就是电平宽度。
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { static uint16_t last_val = 0; static uint8_t bit_cnt = 0; static uint32_t data = 0; uint16_t now = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); uint16_t width = (now >= last_val) ? (now - last_val) : (0xFFFF - last_val + now); last_val = now; if (width > 8000 && width < 10000) { /* 引导码 9ms */ bit_cnt = 0; data = 0; ir_state = IR_WAIT_START; } else if (ir_state == IR_WAIT_START && width > 3500 && width < 5500) { /* 4.5ms 起始位结束 */ ir_state = IR_RECV; } else if (ir_state == IR_RECV) { if (width > 400 && width < 800) { data <<= 1; /* 收到0 */ } else if (width > 1400 && width < 2000) { data = (data << 1) | 1; /* 收到1 */ } else { ir_state = IR_IDLE; return; } if (++bit_cnt >= 32) { ir_frame_ready = 1; ir_frame_data = data; ir_code = data & 0xFF; ir_state = IR_IDLE; } } }这段代码的关键是容差范围要留够。红外信号经过长距离传输和不同品牌遥控器,脉宽会漂移10%到20%,判定窗口太窄就会大量丢帧。我用的窗口是±30%,实测手里五六种遥控器都能正常识别。
还有一个坑是重复码。NEC协议里,按住不放时不会重复发完整帧,而是每108ms发一次引导码加一个特殊的重复标识。如果代码里不做处理,用户按住音量键只会响应一次。处理方式是在捕获到引导码后判断后续是高电平2.25ms且只有一位,就认为是重复码,直接沿用上一次的键值。
5.2 按键消抖别用delay,用状态机更稳
机身按键只有四个:电源、菜单、上、下。数量少,但手感要好。最原始的消抖是“检测到低电平 delay 20ms 再检测”,这在主循环里会阻塞,我已经在前面把它干掉了。改成状态机方式,每10ms采样一次,连续三次采样一致才认为稳定。
typedef enum { KEY_UP = 0, KEY_DEBOUNCE, KEY_DOWN, KEY_LONG } key_state_t; void key_scan_task(void) { uint8_t raw = HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN); switch (key.state) { case KEY_UP: if (raw == 0) { key.state = KEY_DEBOUNCE; key.cnt = 0; } break; case KEY_DEBOUNCE: if (raw == 0) { if (++key.cnt >= 3) { key.state = KEY_DOWN; key.press_time = 0; } } else { key.state = KEY_UP; } break; case KEY_DOWN: if (raw != 0) { key.state = KEY_UP; key_event = KEY_SHORT; } else if (++key.press_time >= 120) { key.state = KEY_LONG; key_event = KEY_LONG_PRESS; } break; case KEY_LONG: if (raw != 0) { key.state = KEY_UP; } break; } }这个结构顺带把“长按”也做掉了:按住超过1.2秒触发长按事件,用来做连续调节或者恢复出厂设置。用状态机的好处是每个按键完全独立,多路按键只需要把结构体数组化,逻辑不用改。
5.3 串口协议的帧格式,别图省事只发裸数据
板上留了一路串口跟上位机通信,用来配置参数、读取状态、升级固件。最早的版本我直接发裸字节,结果现场经常出现数据错位,排查半天发现是上位机重启时缓冲区里有残留字节导致的。后来老老实实加了帧头、长度、命令字、CRC。
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2字节 | 固定0xAA 0x55 |
| 长度 | 1字节 | 数据段长度 |
| 命令 | 1字节 | 功能码 |
| 数据 | N字节 | 参数 |
| CRC16 | 2字节 | 从长度到数据末尾 |
接收侧用一个环形缓冲区,在串口中断里往缓冲区塞字节,主循环里做帧解析。解析状态机逐字节判断:先找帧头,再读长度,再收数据,最后校验CRC。校验失败就丢弃并重新找帧头。这个“重新找帧头”的动作很重要,不能简单丢弃整包就完事,否则遇到连续的噪声会一直卡死。
6. 散热与保护:投影仪最不能省的一层
投影仪是典型的“热量换亮度”设备,散热做不好,光源寿命直接缩短一半。这一块我踩的坑不比电机少,尤其是温度采样的准确性和风扇控制的稳定性。
6.1 NTC采样想准,硬件和软件得一起改
NTC测温看起来简单,一个分压电阻加ADC就完事,实际精度能差到±5℃。原因有几个:分压电阻的精度、NTC本身的B值一致性、ADC参考电压的漂移、还有自热效应。
硬件上我做了三件事:分压电阻用1%精度的金属膜电阻;NTC选B值3950、25℃时10kΩ的常用型号;在NTC两端并一个100nF电容滤高频干扰。软件上用了两步:先做16次过采样求平均,再用查表加线性插值把ADC值转成温度。
/* 简化版:只列关键表格片段,实际表格按整度分点 */ static const int16_t ntc_table[] = { /* ADC值对应的温度×10,从高温到低温排列 */ 2100, 2000, 1890, 1780, 1660, 1540, 1410, 1280, 1150, 1020, 890, 770, 660, 560, 470, 390 }; int16_t ntc_get_temp(uint16_t adc) { uint32_t sum = 0; for (uint8_t i = 0; i < 16; i++) { sum += adc_read_once(); } adc = sum / 16; uint8_t i = 0; while (i < sizeof(ntc_table)/sizeof(ntc_table[0]) - 1 && adc < ntc_table[i]) { i++; } if (i == 0) return 850; if (i >= sizeof(ntc_table)/sizeof(ntc_table[0])) return -100; /* 线性插值 */ int16_t t_high = 800 - (i - 1) * 50; int16_t t_low = 800 - i * 50; int16_t d_adc = ntc_table[i-1] - ntc_table[i]; int16_t d_val = adc - ntc_table[i]; return t_low + (int16_t)((int32_t)d_val * 50 / d_adc); }在实际产品里,我还会在开机后做一次室温校准:如果此时光源没开、机器已经静置超过10分钟,就把当前读数当作环境温度基准,后续的温升判断都相对于这个基准。这样不同季节、不同地区使用都不会误报。
6.2 风扇PWM频率选错,噪音和寿命都会出问题
4线风扇的PWM调速信号有讲究。频率太低(比如100Hz)会有明显的“嗡嗡”声,因为落在了人耳敏感的频段;频率太高(比如100kHz)部分风扇的驱动电路响应不过来。25kHz是行业内比较通用的选择,刚好在人耳听觉上限附近。
占空比和转速不是线性关系,低占空比区间(0到20%)风扇可能根本不转,中间段(30%到80%)变化最明显,高段接近饱和。所以我做了一张占空比到转速的映射表,用分段线性插值来实现平滑调速。
| 目标转速占比 | PWM占空比 | 说明 |
|---|---|---|
| 0% | 0% | 完全停转 |
| 20% | 25% | 起步段,占空比要抬高 |
| 50% | 45% | 中速 |
| 80% | 70% | 高速 |
| 100% | 100% | 全速 |
风扇的FG信号是转速反馈,一般是每转输出两个脉冲。用它做闭环有两个好处:一是能检测堵转,二是能确认风扇真的在转而不是“以为在转”。我设置的堵转判断是:给定PWM后2秒内FG没有脉冲输入,判定为堵转,进入故障状态。
6.3 掉电瞬间那几百毫秒,值得单独设计
投影仪正常关机有90秒的风扇延时,但突然断电是完全不同的事。市电断掉的一瞬间,12V电源的储能电容还能撑几十毫秒到几百毫秒,这段时间足够MCU做点事:保存当前状态、记录错误日志、把镜头退回安全位置。
我的做法是加一路掉电检测:12V经过分压后接到一个比较器或者ADC通道,主循环每10ms检查一次。一旦检测到电压低于阈值,立刻进入掉电中断服务程序,在电容放电的窗口内把关键参数写进Flash的备份区,同时尽量把镜头退回零位。
这里有个坑:Flash写入本身要几毫秒,而且写入期间不能断电,否则可能损坏整个扇区。所以我把参数分开放置,用两个扇区轮流写,写之前先擦除另一个扇区,写完再更新标志位。这样即使写到一半掉电,至少还有一个完整扇区可用。
7. 现场调试踩过的坑,复盘一遍比看十篇文档有用
理论讲完,说说实际调试。这块板子从打样到稳定运行花了差不多两个月,中间遇到的问题有一半在文档里找不到答案,全是靠示波器和万用表一点点摸出来的。挑三个印象最深的分享。
7.1 继电器一吸合单片机就复位,问题不在电源
第一次上电测试,一切正常。但只要按下开机键让继电器吸合,MCU立刻复位,串口打印也跟着乱码。第一反应是电源功率不够,换了个大功率电源,还是复位。第二反应是地线干扰,加了磁珠,改善但没根治。
真正的原因是继电器线圈的反向电动势通过共地路径窜进了复位引脚。示波器抓复位引脚,继电器释放瞬间能看到一个负向尖峰,幅度大概-1.5V,宽度几微秒,足够触发复位。解决办法是三个一起上:线圈并续流二极管、复位引脚加100nF电容到地、复位引脚上拉电阻从10k改成4.7k。三个措施单独任何一个都不够,必须组合。
这件事给我的教训是:涉及感性负载的项目,续流二极管不是可选项,是必选项。而且不只是继电器,任何线圈、电机、电磁阀都要加。
7.2 电机丢步导致限位撞死,软件计数不能全信
前面提过的那次堵转,事后分析原因有三层。表层是电机扭矩不够,负载稍大就丢步;中层是软件只信计数,没有交叉验证;深层是加减速曲线的加速度设得太激进,启动瞬间就要求电机从0跳到2kHz,远超它的能力。
修完之后我把加速度降了一半,同时加了三重保护:限位开关硬件中断、行程超时检测、电机电流采样。电流采样用的是串在驱动芯片电源脚的0.1Ω电阻加运放放大,堵转时电流会明显上升。这个方案比单纯靠时间判断可靠得多,而且成本很低。
7.3 红外在强光下误码,问题出在接收头的安装位置
实验室测试一切正常,搬到有阳光直射的会议室,遥控距离从8米掉到2米。换了几个品牌的接收头都一样,最后发现问题在接收头的安装位置正对着窗户。
红外接收头内部有带通滤波,只对38kHz附近的信号敏感,但强光(尤其是含红外成分的阳光、白炽灯)会让内部放大器饱和,灵敏度大幅下降。解决办法是给接收头加一个遮光罩,只留正面一个小孔,同时把安装位置从机器顶部改到前面板,避开顶部直射光。
这个小改动让遥控距离恢复到7米以上,成本几乎为零。后来我在其他项目里也沿用了这个做法:红外接收头永远不要暴露在直射光下,哪怕加个黑色热缩管套一圈都有效果。
8. 这套方案后续还能往哪扩
这块板子现在跑得挺稳,日常使用没什么问题,但架构上留了扩展空间,后面想做的话有几条路。
一条路是把串口协议扩展成完整的设备管理接口,支持参数批量读写、固件在线升级、运行日志导出。这需要在Flash里划一个专门的参数区,再加一个Bootloader,工作量不大但很实用,尤其是后续改参数不用再拆机。
另一条路是加一路Wi-Fi或蓝牙模组,把遥控功能搬到手机上。通信层已经用串口抽象好了,模组接上去只需要在协议层加几条命令,状态机那边基本不用动。要注意的是无线模组的供电要单独处理,发射瞬间的电流峰值能到300mA,直接挂在3.3V上会把MCU拉复位。
还有一条是梯形校正的算法升级,从一维查表改成四角实时计算。这需要换带FPU的芯片,或者把算法改成定点实现。如果只是家用场景,一维校正其实够用;但如果是商用吊装场景,镜头和幕布有夹角,四角校正是刚需,那就值得投入。
我个人在实际操作中的体会是,这类“软硬结合、带机械运动”的项目,调试时间一定是写代码时间的三倍以上。前期把时序表、联锁规则、故障处理这些想清楚,后面能省掉大量返工。反过来,如果一开始就急着写驱动、调电机,很容易陷入“改好这个坏了那个”的循环。真要动手,建议先把电源和地线这两件事做扎实,再谈别的。