基于STM32的智能鱼缸毕设:效率提升的软硬件协同优化实践
一、裸机轮询的“三座大山”
做毕设时,我最早用的就是“while(1)大循环+Delay”经典套餐。结果越写到后面越发现:
- 传感器越多,循环越长。DS18B20 一次转换 750 ms,DHT11 一次 2 s,水位开关还要消抖,CPU 70% 时间都在空等。
- 执行器响应迟钝。喂鱼舵机、打氧泵、加热棒全靠
if(flag)触发,flag 被其他外设阻塞,舵机转一半就卡死。 - 功耗居高不下。STM32F103C8T6 跑 72 MHz,整机 45 mA,4 节 18650 只能撑两天,老板还以为我偷换了电池。
这三座大山直接导致“系统迟钝 + 电池短命”,毕设答辩 PPT 再好看也救不回来。
二、RTOS 选型:FreeRTOS 胜出
我把市面上能移植的轻量内核都拉出来跑分,场景固定:8 路传感器、3 路 PWM、2 路继电器、1 路 UART 上报。对比结果如下:
| 指标 | 裸机 | RT-Thread Lite | FreeRTOS |
|---|---|---|---|
| 任务切换延迟 | 无,函数调用 | 8 µs | 6 µs |
| RAM 开销 | 0 B | 4.2 KB | 3.6 KB |
| Flash 开销 | 0 KB | 22 KB | 18 KB |
| 开发资料 | 遍地是 | 中文多但版本分裂 | 英文多但社区活跃 |
| 低功耗 API | 自己写 | 不完整 | 官方 Tickless |
结论:FreeRTOS 在延迟、资料、低功耗支持上最均衡,毕设周期只有 6 周,资料全=节省时间=效率提升。
三、核心实现:中断+队列+Stop Mode
1. 总体架构
+----------------+ +----------------+ | 中断采集 |-----> | 队列(锁-free) |-----> | 用户任务 | | (水位、温度) | | 16 字节单元 | | (调度、OLED) | +----------------+ +----------------+ +----------------+ | | +--------------> RTC 唤醒 --------------------+2. 中断端:水位开关 + 温度 ADC
/* 水位开关 EXTI0 中断,消抖用 8 ms 定时器 */ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { static uint32_t last = 0; if (GPIO_Pin == GPIO_PIN_0) { uint32_t now = HAL_GetTick(); if (now - last > 8) { last = now; xQueueMsg_t msg = { .type = EV_LEVEL, .val = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) }; xQueueSendFromISR(qSensor, &msg, NULL); } } }3. 温度 ADC 用 TIM3 触发+DMA 双缓冲,转换完进中断直接发队列,CPU 不占轮询。
4. 低功耗:Tickless Idle + Stop Mode
/* FreeRTOS 钩子,无任务时进入 Stop Mode,RTC 每 30 s 唤醒一次 */ void vApplicationIdleHook(void) { /* 关中断前确认队列空 */ if (eTaskConfirmSleepModeStatus() == eAbortSleep) return; HAL_SuspendTick(); HAL_PWR_EnterSTOPMode(PWR_REGULATOR_LOWPOWER, PWR_STOPENTRY_WFI); HAL_ResumeTick(); }5. 任务划分优先级
TaskFeed优先级 4(喂鱼,瞬时)TaskHeat优先级 3(加热,PID)TaskUpload优先级 2(UART 上报)TaskDisplay优先级 1(OLED 刷新)
四、完整代码片段(可直接复用)
/* 1. 创建队列与任务 */ QueueHandle_t qSensor; void bsp_queue_init(void) { qSensor = xQueueCreate(16, sizeof(xQueueMsg_t)); configASSERT(qSensor); } /* 2. 传感器任务:阻塞等队列,非阻塞处理 */ void vTaskSensor(void *pv) { xQueueMsg_t msg; for (;;) { if (xQueueReceive(qSensor, &msg, portMAX_DELAY) == pdTRUE) { switch (msg.type) { case EV_LEVEL: if (msg.val == 0) // 缺水 xTaskNotify(xTaskHeatHandle, EV_LOW_LEVEL, eSetBits); break; case EV_TEMP: xTaskNotify(xTaskHeatHandle, msg.val, eSetValueWithOverwrite); break; } } } } /* 3. 低功耗封装,带唤醒后重配时钟 */ void PWR_EnterStop(void) { __HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); /* 唤醒后必须重配 PLL */ SystemClock_Config(); }五、性能对比:数据说话
| 场景 | CPU 占用 | 平均响应 | 待机电流 |
|---|---|---|---|
| 裸机轮询 | 72 % | 210 ms | 42 mA |
| FreeRTOS 无低功耗 | 35 % | 45 ms | 38 mA |
| FreeRTOS + Stop Mode | 8 % | 28 ms | 1.8 mA |
测试条件:8 MHz HSI,3.3 V,Amperemeter 串联 1 Ω 采样
六、生产级避坑指南
ADC 采样抖动
传感器引线 >20 cm 时,采样窗口末尾 10% 数据舍弃,用 DMA 均值滤波,可抑制 ±3 LSB 抖动。继电器 EMC
在线圈并 104 电容+反向二极管,仍出现 MCU 复位;最终把继电器供电与 MCU 分开,LDO 前加 220 µF 钽电容,复位问题解决。看门狗喂狗策略
中断里喂狗=白看门;我在vTaskUpload主循环计数,每 200 ms 喂一次,异常任务阻塞时自动复位,调试阶段省了不少时间。栈溢出
FreeRTOS 自带configCHECK_FOR_STACK_OVERFLOW设为 2,加vApplicationStackOverflowHook空循环,提前发现TaskDisplay开了 128 字节不够,改成 256 字节后稳定运行。
七、还能再省?零硬件成本思路
- 软件温度补偿:加热棒惯性大,用一阶惯性滤波+预测关闭,实测每天少工作 7 分钟,电池延长 5%。
- 动态频率:PID 稳态后把主频降到 24 MHz,Stop 前再升回 72 MHz,唤醒时间不变,平均电流再降 0.4 mA。
- 传感器分组唤醒:把“水质”这类慢变量放到 RTC 唤醒周期 2 min,而“水位”保持实时,只多写两行代码,功耗对半砍。
八、小结与思考
毕设从“大循环”到“中断+FreeRTOS+Stop Mode”,代码量没翻倍,响应却快了 7 倍,电池多撑了整整一周。最重要的是,模块化队列、可裁剪任务让后续加“APP 远程控制”时,直接插任务即可,不再牵一发动全身。
如果你也在资源受限的 MCU 上死磕效率,不妨先问自己:
“下一个功能,能不能在不加硬件的前提下,只靠更聪明的调度策略,再让系统多睡一会儿?”