ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32智能婴儿床系统毕设:传感器采集、PWM控制与调试避坑

STM32智能婴儿床系统毕设:传感器采集、PWM控制与调试避坑 做单片机毕设的同学里十个有六个最后会落到 STM32 这个平台上而智能婴儿床系统几乎是每年都会被翻出来的经典题目。原因很实际它同时压中了传感器采集 电机执行 人机交互 安全告警四类考点老师一眼就能看出工作量答辩时也有的聊。但真正动手的时候多数人卡在同一个地方——功能列表写得漂亮落到引脚、定时器和电源上就全乱套最后做出来一个能亮灯、能响铃、温度还会乱跳的半成品。这篇就把智能婴儿床系统从选题收敛、硬件选型、引脚分配、固件架构到调试排坑的整条链路讲清楚偏向工程落地适合正在做单片机毕设或者准备自己复刻一套的朋友参考。1. 为什么我建议先砍功能再画原理图1.1 从三个没人愿意做的现实痛点说起先想清楚这个题目到底在解决什么。我在帮人看方案的时候最常听到的表述是监测温湿度、检测哭声、自动摇床、手机远程查看。这话没错但它更像一份产品描述不是一份毕设描述。真正有说服力的切入点是三个具体的、可被观测的痛点。第一个痛点是夜间的信息盲区。婴儿床放在主卧或者次卧父母在另一个房间或者人在旁边但睡着了这时候被褥内温湿度变化、孩子踢被子、翻身频繁这些信息没人知道。传统的做法是父母后半夜起来摸一把孩子的后颈再看一眼被子。这个动作可以被传感器替代而且这件事是能量化的——温度和湿度都是能出数字的。第二个痛点是哭声响应的延迟。从孩子开始哭到有人到床边中间往往隔了几十秒到几分钟。这段时间里如果能有一个短促的、分级的提示比如先在床头闪灯超过阈值再蜂鸣体验上会完全不同。这里就有一个很适合在答辩里讲的点为什么不做一哭就响因为那会成为新的噪声源做分级告警才是合理的工程判断。第三个痛点是安抚动作的机械重复。抱着走、轻轻晃本质上是一个低频、小幅度的往复运动。这件事用电机加一个曲柄或者用舵机做摆臂都能做出来而且正好是 PWM 和定时器的绝佳练手场景。把这三个痛点拆开功能的优先级就清楚了。温湿度监测和哭声检测是雪中送炭摇床是锦上添花但也最有观赏性联网属于毕设加分项。我见过太多人一开始就把联网、APP、云端全排进计划结果硬件还没焊完就到答辩周了。提示功能列表的第一版永远要写三列——必做、可做、想做。必做只有三项其余全部砍到可做和想做里最后按剩余时间按顺序捡回来。1.2 一版能跑通的功能清单收敛过程给一个我自己用过的收敛结果你可以直接对着改。优先级功能模块判定标准实现难度必做床内温湿度采集与显示每 2 秒刷新一次误差可接受低必做哭声/异常声音检测与分级告警三级提示误报率可控中必做OLED 本地显示与四按键设置阈值可在线改并掉电保存中必做摇篮电机 PWM 驱动与定时停机有超时互锁保护中可做尿湿检测电极式或湿度传感器中可做体动/离床检测压力或称重方式中高想做无线数据上报上位机看曲线中高想做语音模块播报增加成本中这张表的关键不在于列了什么而在于每一行的判定标准是可验证的。答辩老师最烦的一句话是实现了温度检测功能他最想听的是采样周期 2 秒滑动平均 8 次阈值迟滞 0.5 摄氏度。我还想强调一件事尿湿检测这一项很多人做得非常敷衍。拿两根铜箔贴布上靠水的导电性检测理论上可行实际上遇到几个问题——布是湿的但没形成桥接、长时间电解导致铜箔腐蚀发绿、不同尿液的导电率差异大。真想做得像样一点用湿敏电阻或者电容式湿度传感器(比如贴着床垫的柔性湿度探头)更靠谱。它的原理是吸水后介质常数变化导致电容变化跟纯导电检测完全不是一个层次。1.3 一份实际能买到的物料清单与成本估算下面这份清单是我按常见散件价格整理的实际下单会有浮动但量级不会差太多。你可以直接拿去填进论文的硬件方案章节。类别型号/规格数量参考单价主控STM32F103C8T6 最小系统板112 元温湿度DHT22 或 SHT30115 / 25 元声音驻极体麦克风 LM393 比较器模块14 元显示0.96 寸 SSD1306 OLEDI2C 版110 元电机驱动TB6612FNG 双路驱动板18 元电机12V 减速直流电机或 6V 减速电机115 元舵机SG90 或 MG996R选配18 / 35 元按键6×6 直插轻触按键40.3 元蜂鸣器无源蜂鸣器 5V11 元灯光WS2812B 灯珠或普通 LED 灯条15 元无线ESP-01S112 元电源12V/2A 适配器 MP1584 降压 AMS11171 套20 元结构亚克力板/木板、曲柄连杆、螺丝若干40 元总计在 150 到 200 元之间。这个预算对毕设来说非常友好也方便在论文里做成本对比表格。有个细节值得注意TB6612 比 L298N 更值得选。L298N 的导通压降能到 1.5V 到 2V 以上6V 电机供下去只剩 4V转速直接掉一截TB6612 的导通电阻小得多压降在 0.5V 以内而且待机电流更低做电池方案时差距很明显。2. 主控和外设选型每一颗芯片背后的取舍2.1 STM32F103C8T6 与 51 单片机之间的真实差距每年答辩都有人问为什么不用 51 单片机这个问题你得准备一个有技术含量的回答而不是因为老师要求。真正的差距体现在三个地方。第一是定时器资源。这个系统里至少要同时存在三路时间行为1ms 的系统节拍、20ms 的按键扫描节拍、50Hz 的舵机 PWM。51 单片机普遍只有两个定时器一旦其中一路被 PWM 占用系统节拍就得靠软件延时凑整个程序的实时性立刻崩掉。STM32F103C8T6 有四个通用定时器加两个高级定时器随便分配。第二是中断优先级管理。51 的中断只有两级优先级串口接收和传感器采集之间很容易互相拖累。STM32 的 NVIC 支持多级抢占和子优先级可以把串口中断放低把系统节拍放高让实时任务永远不被通信任务阻塞。第三是调试能力。STM32 带 SWD 接口配合 ST-Link 可以单步、断点、看变量、看外设寄存器调试效率跟串口打印完全不是一个量级。我在第 5 节会讲的那些坑有一大半是靠 SWD 在线调试才定位下来的。至于 STC 系列或者国产增强型 51确实比经典 51 强引脚和频率也够用但生态上外设库、示例代码、社区资料的密度还是差一截。做毕设时间紧张资料密度几乎等于开发速度。2.2 温湿度、声音、尿湿、体动四类传感器的选型对比这四个传感器我都在实物上踩过直接给结论。温湿度DHT22也叫 AM2302单总线精度 0.5 摄氏度、2% 相对湿度价格便宜缺点是采样周期最快 2 秒而且时序对微秒级延时敏感中断期间容易被干扰。SHT30 走 I2C精度更高采样快但贵 10 块钱左右。如果你要频繁刷新选 SHT30如果只是2秒一次DHT22 完全够。不要用 DHT11湿度精度只有 ±5%量程还到不了高湿区测被褥内湿度会很难看。声音市面上两种常见模块。一种是 LM393 比较器模块输出数字高低电平本质上就是超过某个音量就翻转旋钮调阈值另一种是麦克风加放大输出模拟包络需要你自己做 ADC 采样和算法。前者便宜好用适合做有声音/没声音的粗判断后者灵活但需要写算法。我的建议是先上 LM393 把流程跑通有余力再换成模拟采集做能量加过零率的双条件判断答辩时更容易讲出深度。尿湿前面说过电极式方案问题多。用湿敏电阻模块的话接法简单但要注意它是电阻分压输出得做滤波不然一滴水下去数值剧烈抖动。体动/离床最靠谱的是 HX711 加称重传感器但 HX711 是 24 位 ADC需要模拟时序驱动且读数漂移需要做零点校准和去极值平均工作量中等偏上。想做省事的就用薄膜压力传感器加运放只判断有人/没人。传感器接口优点主要坑点DHT22单总线便宜、精度够用2 秒周期、时序怕中断SHT30I2C快、精度高价格翻倍LM393 声音模块GPIO/模拟便宜、接线简单阈值靠电位器易受环境噪声影响湿敏电阻ADC接线简单数值抖动大需滤波HX711 称重模拟时序精度高、能做体动漂移需校准代码量大2.3 执行机构直流电机、舵机和灯光怎么与婴儿安全妥协这是整个项目里最需要动脑子的一块。电动婴儿床天然带着安全争议绕不开所以你的方案里必须要有明确的安全设计而且要能在答辩的时候讲出来这反而是加分项。第一条原则是运动幅度和速度必须被限制在低频小幅度范围。我实际操作里的做法是把 PWM 占空比上限锁死在 60%运行时间上限设为 10 分钟超时无条件停止。这两条限制写在固件里不通过按键开放给用户属于硬约束。第二条原则是必须有物理限位或者行程检测。曲柄连杆驱动的摆动本身有机械死点只要连杆长度和摆臂角度设计好就不会出现无限旋转。但如果你用的是舵机就一定要注意舵机的角度范围设置别让它顶到结构上死磕时间长了会烧齿轮。第三条原则是电机供电与逻辑供电分离。这一点我在第 5 节会展开讲因为它直接决定你的温度数据会不会乱跳。灯光我倾向于用 WS2812B 或者普通 LED 灯条做氛围提示因为它的功耗低、无机械动作、响应快。三级告警的视觉部分就交给它绿色常亮是正常黄色呼吸是提示红色快闪是紧急。用 PWM 调亮度的时候记得频率别落在人眼可感知的范围内一般 1kHz 以上就看不出来闪了。2.4 OLED、按键、蜂鸣器与无线模块的取舍OLED 选 I2C 版还是 SPI 版取决于你的引脚余量。I2C 只占两根线但刷新率受限全屏刷新在 400kHz 下大概几十毫秒做局部刷新完全够。SPI 版快但要占 5 到 6 根线。我一般选 I2C 版因为省下来的引脚能留给别的外设。按键用四个就够设置、加、减、确认。别小看按键它是人机交互里最容易出 bug 的地方消抖、长按、连按三种行为要分开处理。我的做法是按键扫描统一放在 20ms 的节拍里读到低电平后连续确认 3 次才算按下长按计时到 800ms 触发长按事件。蜂鸣器一定要选无源的。有源蜂鸣器只能发一个固定频率的声音做不出分级告警的音调差异。无源蜂鸣器用定时器输出 PWM改频率就能改音调做三档提示音非常方便而且这几行代码在论文里很好看。无线模块如果时间充裕ESP-01S 是最省事的选择AT 指令发数据上门户工作量可控。不用花精力去折腾以太网方案用不上还多出一堆网络配置和硬件开销。3. 引脚、定时器和 ADC 的分配表3.1 先定分配表再画原理图的一条硬规则我见过太多人先画原理图、先焊板子等到写代码才发现两个外设抢同一个引脚或者 PWM 通道冲突。正确的顺序是先列引脚分配表确认没有冲突和外设复用错误再去画原理图。STM32F103C8T6 的引脚复用是有硬约束的。比如 USART1 固定在 PA9/PA10I2C1 固定在 PB6/PB7 或重映射到 PB8/PB9SPI1 在 PA5/PA6/PA7。如果你随口把 OLED 的 SCL 定在 PA6那就跟 SPI1 的 MISO 撞了虽然不一定报错但后面加外设的时候会很难受。下面是我用的分配表可以直接参考。功能引脚外设备注系统调试PA13/PA14SWD保留不要占用OLED SCL/SDAPB6/PB7I2C1硬件 I2C 或 GPIO 模拟温湿度数据PB12GPIO 单总线配 4.7k 上拉声音数字量PB0GPIO/EXTI也可以接 ADC尿湿模拟量PA0ADC1_IN0分压后输入电机 PWM A/BPA6/PA7TIM3_CH1/CH2双路驱动电机方向PB1/PB2GPIO控制正反转舵机 PWMPB8TIM4_CH350Hz蜂鸣器PB9TIM4_CH4无源按键 K1-K4PB3/PB4/PB5/PB10GPIO需注意 PB3/PB4 是 JTAG 脚无线串口PA2/PA3USART2接 ESP-01S注意PB3、PB4 默认是 JTAG 的 JTDO 和 NJTRST要用作普通 GPIO 必须先关闭 JTAG 只保留 SWD否则这两个键永远读不到。这个坑我在第一版上踩了整整一晚上。3.2 定时器资源分配一个 1ms 节拍加两路 PWM定时器是这个系统的心脏分配思路是一个管时间、一个管 PWM。TIM2 做 1ms 的系统节拍。配置成向上计数预分频 7172MHz/72 1MHz自动重装载值 999这样每 1000 个计数就是 1ms开更新中断。所有需要周期执行的任务都挂在这个节拍上用软件计数器分频出 20ms、100ms、500ms 等不同的时间片。// TIM2 初始化72MHz - 1kHz 计数 - 1ms 中断 void TIM2_Init_1ms(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseInitTypeDef tb; tb.TIM_Prescaler 71; // 72MHz / (711) 1MHz tb.TIM_Period 999; // 1MHz / 1000 1kHz - 1ms tb.TIM_ClockDivision TIM_CKD_DIV1; tb.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, tb); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); }TIM3 出两路 PWM 驱动电机。电机的 PWM 频率不要太高太高了驱动芯片的开关损耗和噪声都会上来我一般取 10kHz 到 20kHz 之间。取 20kHz 的好处是正好在人耳听觉上限之上电机不会有啸叫。计算方式72MHz 分频到 1MHz周期 49 就是 1MHz/50 20kHz。// TIM3 CH1/CH220kHz PWM void TIM3_PWM_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); TIM_TimeBaseInitTypeDef tb; tb.TIM_Prescaler 71; tb.TIM_Period 49; // 1MHz / 50 20kHz tb.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, tb); TIM_OCInitTypeDef oc; oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; // 初始占空比 0安全第一 oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, oc); TIM_OC2Init(TIM3, oc); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }TIM4 出舵机 PWM 和蜂鸣器音调。舵机要 50Hz也就是 20ms 周期脉宽 0.5ms 到 2.5ms 对应 0 到 180 度。同一个定时器做两件事要注意周期不同的问题舵机要 20ms 周期蜂鸣器需要几百 Hz 到几 kHz。这时候要么把蜂鸣器挪到另一个定时器要么蜂鸣器用软件翻转 GPIO——我一般把蜂鸣器放在 TIM4_CH4 上周期设置成 100us10kHz然后靠响的时候改占空比、不响的时候设成 0来控制这样舵机那一路用同一个定时器的另一通道就行不通了因为周期被固定成 100us。所以更干净的做法是舵机单独占用一个定时器蜂鸣器用另一个。F103C8T6 有 TIM1、TIM2、TIM3、TIM4用户任务里 TIM2 做节拍、TIM3 做电机、TIM4 做舵机、蜂鸣器再想办法。我的实际方案是蜂鸣器用 TIM4_CH4 的 PWM但在不需要舵机的时候才启用或者在需要同时工作时把蜂鸣器改成 GPIO 软件翻转用一个 100us 的节拍计数器自己生成方波。这是个很典型的资源冲突写方案时就要想清楚不要等做到一半才发现。3.3 ADC 采样、基准电压与滤波的关系STM32F103 的 ADC 是 12 位参考电压默认就是 VDDA一般就是 3.3V。所以最小分辨力是 3.3V/4096大约 0.8mV。这个精度对尿湿检测够用但前提是你的模拟前端不要太离谱。尿湿检测的传感器本质是个可变电阻接法是跟一个固定电阻分压中间点接 ADC。固定电阻选 10k 比较通用。这里有个容易忽略的细节分压点的阻抗不能太高。STM32 的 ADC 采样保持电路要求源阻抗在几十 k 以内否则采样电容充不满读出来的值会偏低而且不稳定。如果你发现读数总是比万用表量出来的低一截八成就是源阻抗太高加个电压跟随器或者减小分压电阻就行。再一个就是滤波。我一般用中值滤波 一阶低通的组合先取 9 个点排序取中间值去掉脉冲干扰再做低通平滑。这两步的代码不长效果立竿见影。// 中值滤波 一阶低通 #define MED_N 9 static uint16_t med_buf[MED_N]; static uint16_t MedianFilter(uint16_t new_val) { uint16_t tmp[MED_N]; uint8_t i, j; for (i MED_N - 1; i 0; i--) med_buf[i] med_buf[i - 1]; med_buf[0] new_val; for (i 0; i MED_N; i) tmp[i] med_buf[i]; for (i 0; i MED_N - 1; i) { for (j 0; j MED_N - 1 - i; j) { if (tmp[j] tmp[j 1]) { uint16_t t tmp[j]; tmp[j] tmp[j 1]; tmp[j 1] t; } } } return tmp[MED_N / 2]; } static float LowPass(float old_val, float new_val, float alpha) { return old_val alpha * (new_val - old_val); // alpha 取 0.1~0.3 }这里要解释清楚alpha 为什么取 0.1 到 0.3。alpha 越小滤波越平滑但响应越慢alpha 越大响应快但噪声大。温度这种慢变量用 0.1 就行尿湿这种需要一定响应速度的用 0.25 左右。答辩时被问为什么是这个值你可以说这是实测几组后的折中同时把响应时间的测试数据甩出来效果比空谈理论好得多。3.4 电源这块别让电机把逻辑电路一起带走电源方案是新手最容易忽略、老手最重视的一块。整个系统里有两个截然不同的负载一个是逻辑电路3.3V电流一两百毫安非常怕电压波动另一个是电机6V 或者 12V启动瞬间电流能冲到 1A 以上换向时还会产生强烈的反向电动势。如果用一套电源直接供电电机启动的瞬间 3.3V 会被拽下去STM32 可能出现复位ADC 读数会跳严重的时候 OLED 直接白屏。我第一版就是这么干的调了三天才反应过来。正确的做法是四层防护。第一层逻辑和电机各自独立供电或者至少独立稳压用一个单独的降压模块给逻辑电路。第二层电源入口加电感加电容电感和一个电解电容构成 LC 滤波拦住高频纹波。第三层电机两端并一个 100nF 陶瓷电容加一个 1000uF 电解电容吸收换向尖峰。第四层PCB 或者洞洞板上把大电流走线和模拟地分开最后单点汇总到电源地。成本上只多花不到十块钱但稳定性完全不是一个量级。4. 固件骨架裸机也得有时间片思维4.1 为什么要坚决放弃 delay 型写法初学者的程序长得都差不多初始化然后 while(1) 里读温度、延时 500ms、读湿度、延时 500ms、检查声音、延时 500ms。这种写法在功能单一的时候没问题但在这个系统里是灾难。原因是这个系统里有多个必须及时响应的事件。声音检测要快慢了就漏掉了按键要快慢了手感很差OLED 刷新可以慢200ms 一次也无所谓温度采集最快也就 2 秒一次。用统一延时的写法只能满足其中最慢的一个其他全被拖死。解决方式就是 1ms 的定时器中断 时间片轮询。中断里只做一件事把各个软件计数器减一或者自增同时处理最紧急的事情比如喂狗、按键扫描。主循环里根据标志位执行不同的任务。volatile uint16_t tick_20ms 0; volatile uint16_t tick_200ms 0; volatile uint16_t tick_2s 0; volatile uint8_t flag_key_scan 0; volatile uint8_t flag_display 0; volatile uint8_t flag_sensor 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (tick_20ms) tick_20ms--; else { tick_20ms 20; flag_key_scan 1; } if (tick_200ms) tick_200ms--; else { tick_200ms 200; flag_display 1; } if (tick_2s) tick_2s--; else { tick_2s 2000; flag_sensor 1; } Key_Tick(); // 按键状态机推进 Alarm_Tick(); // 报警计时推进 } } int main(void) { SystemInit(); TIM2_Init_1ms(); while (1) { if (flag_key_scan) { flag_key_scan 0; Key_Process(); } if (flag_display) { flag_display 0; OLED_Refresh(); } if (flag_sensor) { flag_sensor 0; Sensor_Read_All(); } Motor_Task(); // 电机状态机内部自己判断时间 } }这套骨架带来的最大好处是每个任务的执行周期是独立可控的改一个任务的频率不影响其他任务。这种结构在论文里可以写成基于时间片轮询的非阻塞调度听起来很专业实际上确实就是这么回事。4.2 DHT22 的微秒级时序与中断冲突怎么处理DHT22 是单总线协议靠高低电平的持续时间传递数据时间单位是微秒。问题在于你的系统每 1ms 会有一次定时器中断如果中断恰好插在 DHT22 读数据的中间那段时序就被拉长了读出来的数据全是 0xFF 或者全 0。解决方案有三个我都试过。第一种是读传感器期间关全局中断读完立刻打开。DHT22 一次读取大约 5ms关 5ms 中断对系统影响不大节拍会轻微漂移可以接受。第二种是读之前先关读再重试失败就重来一次最多三次避免总线真的出问题时卡死。第三种是彻底换 SHT30 走 I2CI2C 有 ACK 机制抗干扰强得多。uint8_t DHT22_Read(float *temp, float *humi) { uint8_t retry 0; while (retry 3) { __disable_irq(); // 关中断保护时序 uint8_t ok DHT22_Read_Raw(temp, humi); __enable_irq(); // 立刻恢复 if (ok *temp -40.0f *temp 80.0f *humi 100.0f) { return 1; } Delay_ms(10); } return 0; // 三次都失败上报错误 }提示读失败不要直接显示乱码一定要有传感器异常这个状态位并在 OLED 上展示出来。答辩时老师很可能拔掉传感器看你系统怎么反应有异常处理逻辑的会明显加分。4.3 状态机写电机控制与三级告警电机控制绝对不能用开电机然后 delay 十分钟的写法必须是一个状态机。状态有空闲、启动加速、稳定运转、减速停止、超时保护。每个状态里根据当前速度和目标速度做微调到达时间上限就无条件进入停止。typedef enum { MOT_IDLE, MOT_ACCEL, MOT_RUN, MOT_DECEL, MOT_LOCK } MotState; static MotState mot_state MOT_IDLE; static uint32_t mot_run_ms 0; void Motor_Task(void) { switch (mot_state) { case MOT_IDLE: Motor_SetDuty(0); break; case MOT_ACCEL: Motor_Ramp_Up(); // 每 50ms 加 5% 占空比 if (Motor_GetDuty() MOT_DUTY_MAX) mot_state MOT_RUN; break; case MOT_RUN: mot_run_ms 20; if (mot_run_ms MOT_RUN_LIMIT_MS) { // 默认 10 分钟 mot_state MOT_DECEL; } break; case MOT_DECEL: Motor_Ramp_Down(); if (Motor_GetDuty() 0) { mot_run_ms 0; mot_state MOT_IDLE; } break; case MOT_LOCK: Motor_SetDuty(0); // 故障锁定需要人工复位 break; } }告警也做成三级状态机。第一级是灯光提示第二级是灯光加短鸣第三级是灯光加连续鸣叫并且停止摇床。这里的判定依据建议用迟滞而不是单阈值比如声音强度超过阈值持续 1 秒进二级持续 5 秒进三级但降到阈值 70% 以下才降级。这样能避免在阈值边缘反复横跳答辩演示的时候会稳定很多。4.4 参数掉电保存与 Flash 读写阈值这些参数必须能掉电保存否则每次上电都要重设体验很差。STM32F103C8T6 内部有 64KB Flash程序占不了那么多可以拿最后一页1KB当参数区。关键点有三个。第一Flash 写之前必须先擦除擦除的最小单位是页不是字节。第二擦写期间不能执行同一块 Flash 里的代码所以要么把执行代码放到其他地方要么在擦写的时候保证不取值。保险的做法是先关中断、把参数复制到 RAM 缓冲区、擦除、写入、再开中断。第三一定要加校验和比如写入 10 个字节的参数再加一个字节的累加和读出来校验不对就用默认值。#define PARAM_ADDR 0x0800FC00UL #define PARAM_MAGIC 0x5A5A typedef struct { uint16_t magic; int16_t temp_hi; // 温度上限单位 0.1 摄氏度 int16_t temp_lo; uint16_t sound_th; // 声音阈值 uint16_t run_limit; // 运行时长上限单位秒 uint8_t checksum; } Param_t; void Param_Save(const Param_t *p) { Param_t tmp *p; tmp.magic PARAM_MAGIC; uint8_t *raw (uint8_t *)tmp; uint8_t sum 0; for (int i 0; i (int)sizeof(Param_t) - 1; i) sum raw[i]; tmp.checksum sum; FLASH_Unlock(); FLASH_ErasePage(PARAM_ADDR); uint16_t *pdata (uint16_t *)tmp; for (int i 0; i (int)(sizeof(Param_t) / 2); i) { FLASH_ProgramHalfWord(PARAM_ADDR i * 2, pdata[i]); } FLASH_Lock(); }这段代码在论文里其实挺有分量因为它体现了参数持久化 完整性校验的工程思维比单纯讲我实现了设置功能要扎实。5. 调试阶段真实踩过的六个坑5.1 电机一转温度就跳变三度这是我遇到过的第一个真正棘手的 bug。现象很奇怪电机不转的时候温度读数稳定在 25.2 摄氏度左右电机一转温度就往上飘最高到 28 度多电机停下又慢慢恢复。排查过程我走了三步。第一步确认传感器本身没问题把 DHT22 拔下来接到另一块开发板上单独测数据稳定排除传感器。第二步看现象跟什么相关发现漂移幅度跟电机占空比正相关占空比越大飘得越多而且跟电机正反转也有关系。这就基本锁定了是电机换向产生的电气干扰。第三步用示波器看电源纹波果然在电机启动瞬间3.3V 上出现了 300mV 左右的尖峰。修复方案做了三件事给电机供电和逻辑供电各用一路稳压电机端子并上 100nF 陶瓷电容和 1000uF 电解电容尽量靠近电机PCB 上把电机的地单独走线最后单点接到主电源地。改完之后温度漂移从 3 度降到 0.2 度以内属于可接受范围。这个坑的价值在于它把电源完整性这个概念从课本变成了你真正见过的东西答辩时讲出来非常有说服力。5.2 I2C 死锁与 OLED 白屏第二个坑是 OLED 偶尔白屏重启就好但过一会儿又白。一开始以为是驱动问题换了好几个库都不行。后来才发现是 I2C 总线死锁。硬件 I2C 在主机复位或者噪声干扰的时候有可能从机把 SDA 一直拉低主机无论怎么发时序都读不到 ACK程序就在等待标志位的地方卡死。因为我的 OLED 刷新放在主循环里卡住之后整个 while(1) 就停了看起来就像死机白屏。修复方式是两条腿走路。一是给 I2C 操作加超时读或者写的时候加一个计数上限超了就强制复位 I2C 外设重新初始化。二是给整机加独立看门狗万一软件真的卡死在某个地方1 秒后自动复位。// I2C 超时保护示例 static uint8_t I2C_WaitEvent(uint32_t event, uint32_t timeout) { while (!I2C_CheckEvent(I2C1, event)) { if (timeout-- 0) { I2C_SoftwareResetCmd(I2C1, ENABLE); I2C_SoftwareResetCmd(I2C1, DISABLE); I2C_Init(I2C1, i2c_cfg); // 重新初始化 return 0; } } return 1; }如果你的项目对实时性要求没那么高另一个更省心的方案是用 GPIO 模拟 I2C。软件模拟的 I2C 每一拍你都控制得住超时逻辑自己写死锁的概率低很多而且引脚随便选不占用硬件 I2C 资源。5.3 打开看门狗之后系统反复复位第三个坑是加看门狗之后系统一直在重启。现象是 LED 大概 1 秒闪一次OLED 永远停留在开机画面。原因在于喂狗的位置放错了。我一开始把喂狗放在 1ms 的定时器中断里想当然觉得这样最稳妥。结果发现系统仍然会复位因为真正卡死的地方在中断之外——比如 DHT22 读取超时、OLED 等 ACK。中断还在正常跑狗一直在喂但主循环早就停了看门狗完全没起到作用。正确做法是看门狗必须在主循环里喂而且要确认主循环跑到了预期的位置。我改成用一个任务完成标志主循环每遍历完一圈把所有任务都执行一遍之后才喂狗这样任何一个任务卡死都会导致超时复位。还有一个参数要算清楚。STM32F103 的独立看门狗用的是内部 LSI 时钟标称 40kHz实际可能在 30kHz 到 60kHz 之间波动。溢出时间 (4 × 2^分频系数 × 重载值) / LSI 频率。取分频 64、重载 625理论上是 40kHz 下 1 秒实际可能在 0.67 秒到 1.33 秒之间。所以喂狗周期设计成 200ms 比较稳妥别卡在临界值上。void IWDG_Init(void) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(IWDG_Prescaler_64); // 40kHz / 64 625Hz IWDG_SetReload(625); // 625Hz / 625 1Hz - 约 1s IWDG_ReloadCounter(); IWDG_Enable(); }5.4 声音模块白天一直报警第四个坑是白天声音模块疯狂触发晚上又正常。这个问题很有意思因为它暴露的是阈值设定的场地依赖。LM393 模块上有个电位器调的是比较阈值。我在安静的实验室调好了搬到宿舍就炸了。原因是环境噪声水平差异巨大——宿舍里有空调、有人说话、有键盘声整体底噪就高。解决方案有两层。软件层面加上持续时间判定不是超过阈值就报警而是连续超过阈值 1 秒才报警这样短促的偶发噪声会被过滤掉。硬件层面加一个自动阈值调整开机或者空闲的时候先采样 5 秒环境噪声取平均值加一个余量作为动态阈值这样换环境也能自适应。答辩演示的时候这条逻辑一定要提前演示一遍——先安静然后用手拍一下床板看它多久触发再拍几下说清持续判定的机制。老师看到你有意处理误报印象分立刻上去。5.5 按键抖动、长按与连按的判断第五个坑是按键。最初写的按键代码是检测到低电平就算按下结果一次按下经常被识别成三四次。加了个 10ms 延时消抖连按又不好用了。最后落地的是一个比较完整的按键状态机核心是三个时间常数消抖确认时间 20ms、长按判定时间 800ms、连按重复间隔 200ms。按键扫描放在 20ms 的时间片里每次记录当前状态跟上一个状态比较稳定 3 次才认。typedef struct { uint8_t stable; // 消抖后的稳定电平 uint8_t last; // 上一次采样 uint8_t cnt; // 连续相同次数 uint16_t hold_ms; // 按住时长 uint8_t long_fired; // 长按已触发标志 } Key_t; void Key_Tick(void) { for (int i 0; i KEY_NUM; i) { uint8_t cur KEY_Read(i); // 0 表示按下 if (cur keys[i].last) { if (keys[i].cnt 5) keys[i].cnt; } else { keys[i].cnt 0; keys[i].last cur; } if (keys[i].cnt 3 keys[i].stable ! cur) { // 20ms * 3 60ms 稳定 keys[i].stable cur; if (cur 0) { // 刚按下 keys[i].hold_ms 0; keys[i].long_fired 0; } else { // 刚抬起 if (!keys[i].long_fired) { Key_OnClick(i); // 短按事件 } } } if (keys[i].stable 0) { // 按住期间 keys[i].hold_ms 20; if (keys[i].hold_ms 800 !keys[i].long_fired) { keys[i].long_fired 1; Key_OnLongPress(i); // 长按事件 } else if (keys[i].hold_ms 800 keys[i].hold_ms % 200 0) { Key_OnRepeat(i); // 连按事件 } } } }注意短按事件要放在抬起的时刻处理而不是按下的时刻。否则长按的时候会先触发一次短按再触发长按用户感受很怪。5.6 串口打印一切正常、脱机跑就出问题第六个坑最隐蔽。现象是插着 USB 转串口调试的时候一切正常一拔掉串口线、脱机运行系统就开始不定期抽风。原因有两个。一是串口打印本身有阻塞性。如果你用阻塞式发送每次打印都要等发送完成这会拖慢主循环而插着串口线的时候接收端一直在读缓冲区不会满所以感觉正常一旦拔掉如果没有正确处理 TXE 标志或者缓冲区溢出就可能卡住。二是开发板的供电方式变化。插着调试器的时候开发板可能由调试器供电纹波很小脱机之后走的是自己的电源模块纹波大得多之前被掩盖的电源问题就暴露了。对应的排查方法很朴素但很有效把所有调试打印改成一个可控的宏开关脱机测试时全部关掉同时用万用表加示波器确认脱机状态下的电源质量。另外输出重定向的printf最好换成自己写的轻量级发送函数避免库函数的额外开销。这个问题给我的教训是在调试器下正常不等于系统正常。每次重要功能做完一定要做一轮完全脱机的运行测试时间不少于连续两小时。6. 无线上报、演示脚本与资料整理6.1 用 ESP-01S 做数据上报的最简协议无线部分如果时间紧张不要碰复杂的协议栈用 ESP-01S 走串口 AT 指令发数据到自己的上位机或者门户就够了。关键是设计一个简单到不会出错的帧格式。我用的是这样的文本帧以$开头以#结尾字段用逗号分隔最后加一个累加校验。$BED,25.3,58,1,0,42#\r\n字段依次是温度、湿度、告警等级、电机状态、声音强度。解析端按逗号切分校验不过就丢掉整帧。这种格式的好处是肉眼可读、调试方便、容错简单。二进制协议看着高级但联调的时候你根本不知道发出来的是什么很容易浪费时间。通信异常一定要有超时重连。ESP-01S 的 AT 指令响应时间是变化的别写死等待要做重试和超时。整个通信流程也最好放到一个独立的低优先级任务里不要阻塞主循环。6.2 答辩八分钟怎么演示才不翻车演示脚本我给一个实际用过的版本八分钟能讲完节奏比较紧凑。前三十秒讲痛点不念 PPT直接说我在实际调研里发现婴儿床最需要解决的是夜间信息盲区和哭声响应延迟这两个问题然后一句话说清你的方案。两分钟讲硬件把板子拿起来指着讲主控、传感器、电机驱动、电源分区的布局重点提电源隔离和看门狗这两处设计。三分钟做功能演示温度用捂热或者吹气让它变化湿度用湿棉签靠近传感器哭声用手机放一段录音看三级告警的递进然后按按键改阈值并断电重启验证保存生效。两分钟讲代码结构展示时间片调度的主循环和状态机。最后一分钟留给提问。演示最容易翻车的三个点一是电机不动提前检查电池电量和驱动接线二是传感器读不到准备一个备用传感器三是无线断连演示前把上位机连好如果断了就临时用屏幕上的数据讲不要慌。6.3 代码工程和论文素材怎么整理最后说一个很实际的事答辩结束后老师可能会翻你的代码一个结构混乱的工程会大幅减分。建议按功能分文件bsp_timer.c、bsp_i2c.c、sensor_dht22.c、app_motor.c、app_alarm.c、param_flash.c每个文件配一个头文件。主函数里除了初始化和主循环调度不要放业务逻辑。论文素材要提前攒。每次调试遇到的问题、测出来的数据、改前后的对比随手记在一个文档里。等到写论文的时候你会发现最难写的实验结果与分析章节素材早就有了。我建议至少准备三张表外设引脚分配表、传感器性能对比表、连续运行测试记录表。有这三张表论文的硬件和测试部分基本就有着落了。还有一个容易被忽略的细节代码里所有魔法数字都要定义成宏。比如MOT_DUTY_MAX、MOT_RUN_LIMIT_MS、TEMP_HI_DEFAULT答辩时老师问你这个占空比上限怎么定的你能直接指着宏说这里定义的 60%是考虑到结构摆幅和安全余量做的折中比现场翻代码找数字要专业得多。我自己做完这套东西之后最大的体会是单片机项目真正难的地方从来不是会不会写某个外设的驱动那些都是查手册能解决的事。难的是多个模块同时工作时的相互干扰——电机拖累温度、看门狗掩盖死锁、串口打印掩盖实时性问题。这些只有在系统真正跑起来、连续运行几个小时之后才会暴露。所以如果你现在还在方案阶段建议早点把硬件搭起来跑通最小功能把剩下的时间全留给调试和稳定性验证这部分投入产出比最高。
返回列表