简介:这是一份基于STM32的智能鱼缸设计完整项目资料,面向电子信息、自动化、物联网等专业学生及嵌入式开发入门者,适合毕业设计、课程设计或项目演示。项目包含全部源码与详细文档,涵盖传感器数据采集、水温控制、自动喂食、远程监控等典型智能硬件功能,经过测试运行,可作为从技术方案到代码落地的参照。压缩包共1714个文件,大小约61.29MB,主要由393个C源码、310个H头文件、195个O目标文件构成,并附带39个Python辅助脚本、73张PNG图片及4个GIF演示;MD/RST文档便于阅读,Keil工程文件、bin/hex/axf固件输出和MAP/LST文件可直接查看编译与链接细节。项目说明显示答辩评审分达95分并获导师认可,已有175人学习;目录结构清晰,便于在现有基础上做二次功能扩展,也能为STM32外设驱动、LVGL界面移植等环节提供直接参考,适合答辩前的快速理解与二次开发。
1. 从"能养鱼"到"能自己决定怎么养鱼"
养鱼这件事在工程上其实是个典型的嵌入式闭环系统,简单说就是采集环境数据、按规则判断、驱动执行设备再回去校准结果。STM32 在其中扮演的是决策核心,CPU 以固定节拍读取水温、浑浊度、水位这些传感器信息,再用 PWM 调整加热棒或者水泵的出力,整个过程不需要人为干预。这套智能鱼缸设计资料并不只是给了套源码,而是把从传感器选型、最小系统板引脚分配、定时器与 ADC 外设初始化,到 LCD 界面状态刷新这一整条链路都串了起来。
适合谁去看?如果你是正在做 STM32 相关课设、准备电赛项目,或者刚开始接触嵌入式裸机开发又想看到一个相对完整的外设组合,这套资料里的System.bin固件、Smart_Fish_Tank.bin烧录镜像以及带有详细注释的工程文件,能让你在现有代码上直接改功能,而不是从 LED 闪烁开始重复造轮子。真正要注意的是:项目用的是标准外设库而非 HAL 库,这意味着中断回调、寄存器操作和时钟树配置都走得比较底层,看懂了这套代码,换到任何 Cortex-M 内核平台都能平移思路。
2. 硬件选型与系统架构:先算功耗、再定引脚,最后才谈写代码
STM32 系列在鱼缸这种慢变系统里的优势不是算力强,而是低功耗模式丰富、定时器资源多、ADC 通道够用。比如你希望温度采样误差不超过正负 0.5 摄氏度,就需要选择内置 12 位 ADC 且有足够参考电压精度的型号,同时加热棒这种非线性负载要用定时器的 PWM 输出控制占空比,不能靠 GPIO 高低电平硬切。我在拆这套项目时发现,它把系统分成了四层:感知层(温度、水位、光照、浑浊度)、决策层(STM32 主控内的逻辑判断)、执行层(加热棒、水泵、增氧机、补光灯)、交互层(LCD 显示与按键输入)。
评估一个智能系统好不好用,不能只看它能干什么,还要看它在异常情况下怎么退避。比如传感器断线、水位过低时加热棒继续工作会导致干烧,这是鱼缸场景里最容易出问题的地方。这套项目中给这类异常留了独立检测路径:水位信号通过比较器输出数字电平接入 STM32 的 EXTI 引脚,一旦低于阈值就触发外部中断,在中断服务函数里立即禁用 PWM 输出。下面用一个表格把典型外设与引脚功能顺一下。
| 外设模块 | 接口类型 | STM32 外设资源 | 典型功能 | 备注 |
|---|---|---|---|---|
| DS18B20 水温探头 | 单总线 GPIO | GPIO 普通输入 | 实时水温采集 | 需要 4.7k 上拉电阻 |
| 水位传感器 | 数字电平 | EXTI 外部中断 | 低水位报警 / 干烧保护 | 比较器输出 |
| 浑浊度传感器 | 模拟电压 | ADC 独立通道 | 水质浑浊度判断 | 电压输出范围 0~4.5V |
| 加热棒继电器 | 低电平触发 | 定时器 PWM 或 GPIO | 水温加热控制 | 用光耦隔离 |
| 水泵 / 增氧机 | Motor driver | 定时器 PWM 互补输出 | 水循环与增氧 | 需考虑启动电流 |
从 PCB 布局角度,我一般会建议把模拟电源和数字电源用 0 欧电阻单点连接,避免水泵电机启停时的地弹干扰 ADC 采样。如果是自己打板,注意 STM32 晶振电容计算这块,像项目里用的 8MHz 晶振,负载电容取 18pF 到 22pF 都可以,关键要看晶振本身规格书的 CL 参数,直接用公式 CL = (C1*C2)/(C1+C2) + Cstray 来算,Cstray 通常在 2~5pF 之间。原理图里下载的接线图和 SDK 包里没有包含,这点建议拿到资料后对照参考设计核对一遍。
2.1 最小系统板评估:为什么不用开发板直接怼
很多初学者喜欢直接用正点原子或者野火的开发板来做鱼缸项目,这当然能跑通,但有两个问题:第一,开发板上的外设电路(比如 USB 转串口、LCD 接口、LED)会额外消耗电流和引脚,而且它们占据的引脚位置往往与传感器需要的引脚冲突;第二,开发板的布局并不适合直接放进鱼缸外壳,你需要重新设计转接板。所以这套智能鱼缸设计选择了最小系统板加外围模块的方式,通过排针把 GPIO 引出来。
那么最小系统板需要哪些必要条件?首先是供电,鱼缸环境里 220V 转 5V 的电源模块会给水族设备供电,STM32 需要 3.3V,所以板子上要有 AMS1117 或者 RT9013 这类 LDO;其次是复位电路和启动模式配置,BOOT0 必须接下拉电阻到地,才能让芯片从 Flash 启动;最后是 SWD 调试接口,至少留出 SWDIO、SWCLK、GND 三个引脚,方便用 ST-Link 烧录和调试。
在这套源码里,时钟配置是用标准库函数SystemInit()配合stm32f10x.h头文件完成的。串口调试信息用 UART1,重定向了fputc()到串口,这样直接调用printf()就能在串口助手看到运行日志。如果你拿到代码后在别的板子上跑不通过,先检查晶振频率和启动文件是否匹配,比如库编写时假定 72MHz,而你的板上焊接的是 12MHz 晶振,那SystemCoreClock就会被算成 108MHz,时序全部错位。常见做法是在system_stm32f10x.c中把PLL_M、PLL_N这些参数按实际晶振改过来。
2.2 引脚复用冲突排查:GPIO 不是想怎么配就怎么配
操作 STM32 的 GPIO 看起来简单,实际项目里一半以上的坑来自重映射冲突。比如定时器 2 的 PWM 输出默认引脚是 PA0~PA3,但 PA0 同时是 ADC1 的通道 0,如果你既要用定时器 2 输出 PWM 控制加热棒,又要在同一引脚接温度传感器的模拟输出,那 ADC 转换结果必然抖动。解决方法是把定时器重映射到 PB 或者 PC 口,配置GPIO_PinRemapConfig(GPIO_Remap1_SWJ_JTAGDisable, ENABLE)这类函数,注意 HAL 库和标准库的函数名不同,在这套资料的代码中对应的是GPIO_PinRemapConfig()。
另外一个容易忽略的问题是 5V 容忍引脚。STM32F103 的 PA、PB、PC 大部分是 FT(5V tolerant)引脚,可以直接接 5V 逻辑电平传感器,但 ADC 引脚如 PA0~PA7 是 3.3V 容忍,如果浑浊度传感器输出 4.2V 或者 4.5V 的满量程电压,直连会有烧毁风险。稳妥的办法是加一个电阻分压网络,比如 10k 和 20k 电阻串联接地,把 4.5V 分压到 3V,再进 ADC。因为这套项目中已经带了三路 ADC 初始化代码,把浑浊度传感器接在 ADC 通道 2(PA2 脚)时,需要注意这个引脚同时支持 TIM5_CH3 的输出功能,如果你同时打开了定时器 5 的 PWM,那就会出现电位互相干扰,表现为数值乱跳且刷新率不稳定。
后面涉及按键扫描和继电器控制时,建议把隔离做彻底。水泵电机启动瞬间电流能达到额定电流 5~10 倍,如果控制信号和功率回路只通过一根地线相连,ADC 采样值会被抬高或者出现周期性毛刺。在这类低成本方案中,用 TLP521 或 PC817 光耦把 STM32 的 GPIO 与继电器驱动三极管完全隔离,再接续流二极管,可以避免板子死机或者程序跑飞。
3. 传感器驱动与参数平滑:从原始值到可用数据的过滤链路
传感器部分最怕的不是数值不准,而是数值波动导致系统频繁启停。水温用 DS18B20,它是单总线协议,时序要求严格,而且它本身需要至少 750ms 完成一次 12 位温度转换。直接连续读会导致芯片内部忙,返回 85 度或者 0 度这种错误值。在这套项目里,它用了状态机的方式去调度 DS18B20 的读取流程:先发复位脉冲,等待存在应答,再发 ROM 指令跳过序列号,然后发转换命令,等待转换完成,最后读取暂存器中的两个字节。代码实现比较标准,但是值得改进的是它的主循环没有加时间片保护,如果传感器拔出或者总线被拉低,程序会卡在while等待应答的位置。
uint8_t DS18B20_ReadBit(void) { uint8_t bit_val = 0; DS18B20_DQ_OUT_LOW(); delay_us(2); DS18B20_DQ_OUT_HIGH(); delay_us(15); bit_val = DS18B20_DQ_IN(); delay_us(50); return bit_val; }上面的函数逻辑是主控把总线拉低 2 微秒后释放,再延时 15 微秒采样,因为 DS18B20 的时序要求主机读位时必须在 15 微秒窗口内读取数据有效区域。这里的delay_us()用的是 SysTick 延时,所以精度比较好,不受编译器优化级别影响。如果你在 Keil 工程里做-O2优化编译,发现温度读数一直是 0 或者 85,大概率是delay_us()被优化成了空循环,解决方法是给延时函数里的局部变量加上volatile关键字,或者在函数声明处加__attribute__((optimize("O0")))。
水位检测则不用 ADC,因为水导电后探针间电阻会明显下降,通过比较器设置阈值后直接输出高低电平,从而省去在代码里做复杂判断。比较器本身有回差设计,电压滞回大约 30mV,这样水面轻微波动不会导致 GPIO 频繁跳变。读到 EXTI 下降沿后,程序置位标志位,主循环里处理低水位逻辑,而不是在中断函数里做整个动作,这是嵌入式编程中常见的"中断置标志、主循环做处理"模式。
| 传感器类型 | 输出形式 | 处理方式 | 采样周期 | 缓存策略 |
|---|---|---|---|---|
| 水温 DS18B20 | 12 位温度值 | 读取后做滑动平均 | 5s | 缓存 8 次读数 |
| 浑浊度 | 0~3.3V 模拟电压 | ADC 采集 + 卡尔曼滤波轻量版 | 200ms | 缓存 16 个采样点 |
| 水位 | 数字电平 | 状态沿检测 | 事件触发 | 无 |
| 光照传感器 | 模拟电压或数字 I2C | ADC 采样,阈值比较 | 500ms | 缓存 4 次取最大 |
3.1.1 温度控制中的 PID 参数整定思路
水温控制是鱼缸系统中最能体现嵌入式算法能力的地方,很多人直接用滞回控制,结果加热棒继电器一小时动作几十次,触点寿命短,温度过冲超过 2 度。改用 PID 后,只需要控制 PWM 的占空比,就能让加热棒温度平滑稳定。但是鱼缸水体热容量大,系统滞后严重,所以 PID 参数不能照搬网上电机的调参表格。我的方法是先用 Ziegler-Nichols 的临界比例法,把积分和微分去掉,只保留比例项,把增益调大直到温度出现等幅振荡,记下此时的临界增益 Ku 和振荡周期 Tu,再按公式计算 P、I、D 的参数。
这个项目代码里没直接给出 PID 函数,但给出了温度采集和 PWM 输出的完整接口,所以补一个位置式 PID 是很容易的。需要注意积分饱和问题:加热停止后,如果水温仍高于目标值,积分项会持续累加负误差,导致下次加热时总输出被拉低,出现不加热的现象。解决办法是在代码里对积分限幅,并且采用增量式 PID 来规避。增量式的输出是当前输出在上一次基础上的变化量,不需要清空积分,抗积分饱和能力更强。由于鱼缸控制周期在 1 到 2 秒,对运算速度要求很低,STM32F103 跑 72MHz 完全有余量,关键是把 PID 计算放在定时器中断里,保证固定周期。
float pid_calc(PID_Typedef* pid, float target, float current) { float error = target - current; pid->integral += error; if (pid->integral > pid->out_max) pid->integral = pid->out_max; if (pid->integral < pid->out_min) pid->integral = pid->out_min; float output = pid->kp * error + pid->ki * pid->integral + pid->kd * (error - pid->last_error); pid->last_error = error; if (output > pid->out_max) output = pid->out_max; if (output < pid->out_min) output = pid->out_min; return output; }这段 PID 函数中out_max和out_min需要映射到 PWM 的占空比范围。如果定时器设置为 8 位计数器,也就是 ARR 值为 255,那out_max可以设置为 200,out_min设置为 0,预留一定的余量。ki的取值决定稳态误差消除速度,但过大会造成振荡,我一般先把kp调好,再按ki = kp / 60起步,kd = kp * 4起步,然后在实际水温曲线里微调。注意加热棒第一次上电时水温低、误差大,输出会达到满占空比,这是正常现象,但要用软件限制最长连续加热时间,防止鱼缸在极端低温环境中加热棒表面结垢。
3.1.2 定时器与 ADC 采样同步问题
鱼缸里的传感器采样有一个特殊点:浑浊度传感器容易受气泡干扰。水泵增氧产生的气泡如果经过传感器探头,输出电压会瞬间跳变,造成 ADC 读数异常。这种情况下单靠滑动平均滤波是不够的,因为异常值会污染整个滑动窗口,导致报警延迟或误报。更可靠的做法是把 ADC 采样时机固定在水泵关断后的 50ms,或者每次采样前先延时,让气泡过去再读值。
定时器 3 在这个系统里充当了采样节拍发生器。它的更新中断频率被配置为 10Hz,每次中断触发时依次调用一次温度采集、ADC 采集和状态刷新。用一根时间基准去驱动整个传感器系统,而不是在主循环里用HAL_Delay()穿插,可以避免代码调试时出现的时序问题。STM32 的 ADC 支持注入组和规则组,规则组可以配置多个通道按顺序扫描,在这个系统里由于三路 ADC 是不同类型、不同采样周期的传感器,分开触发比统一扫描更合理,所以我建议规则组只放浑浊度和光照两路,温度单独用单总线,采样周期 5 秒,避免高频读单总线影响主循环响应按键。
4. 交互界面与主控状态机:不止是显示数据,还要能被人理解
智能鱼缸的人机交互,在大多数 DIY 项目中往往被放在最后考虑,但经验是他们恰恰是答辩时评委最关注的部分。这套资料里包含了两个文件:lv_demo_printer_img_scan_example.c和lv_demo_printer_img_btn_bg_1.c,说明显示部分用了 LVGL 图形库的 demo 适配。LVGL 在 STM32F103 上移植时,最麻烦的是内存问题,因为每增加一个控件都要占用 RAM 做对象结构体,而 F103 的 SRAM 通常只有 20KB 到 64KB。如果直接在主循环里动态分配内存,频繁创建和删除控件后堆碎片会让系统崩溃。
实际项目中比较稳的做法是预编译 UI 界面,即在初始化阶段创建所有控件并隐藏不需要的部分,通过修改控件的可见性来做界面切换,而不是销毁重建。按键输入用 GPIO 中断加扫描,响应优先级高,但不要在中断回调里调用 LVGL 的lv_btn_set_state()这种耗时函数,正确姿势是使用lv_timer_handler()在事件循环里统一处理。这个文件名的printer_img_scan看起来是 LVGL 的 demo 例子,你可以在这个基础上把鱼缸的温度曲线重绘成折线图。
void UI_Update(void) { lv_label_set_text_fmt(label_temperature, "Temp: %.1f C", g_current_temp); lv_bar_set_value(bar_water_level, g_water_level_percent, LV_ANIM_ON); if (g_alarm_flag == LOW_WATER_ALARM) { lv_obj_clear_flag(label_alarm, LV_OBJ_FLAG_HIDDEN); lv_label_set_text(label_alarm, "LOW WATER LEVEL!"); } else { lv_obj_add_flag(label_alarm, LV_OBJ_FLAG_HIDDEN); } }这个刷新函数典型地展示了把传感器变量映射到 UI 控件的过程。g_current_temp和g_water_level_percent是全局变量,由传感器采集函数更新,UI 刷新函数只做读操作。LVGL 的刷新频率没有必要太高,20Hz 即每秒刷新 20 次已经足够顺滑,过高反而加重 CPU 负载。在 F103 这种平台上,建议把LV_TICK_CUSTOM配置为 0,使用专门的定时器中断来提供时钟节拍,避免与系统延时冲突。
主控逻辑建议用状态机来实现,而不是简单的 if-else 嵌套。鱼缸系统的工作模式可以分为几个状态:INIT上电初始化、NORMAL正常工作、HEATING加热中、LOW_WATER低水位停机、MANUAL手动模式。每个状态的事件转换条件要清晰定义。比如在LOW_WATER状态下,即便水温低于目标值,加热 PWM 也必须保持在 0 输出,只有水位恢复正常并按键确认后才能退出这个状态。这种设计保证了系统在任何异常情况下都不会对设备产生破坏性操作。
| 状态 | 触发事件 | 动作 | 退出条件 |
|---|---|---|---|
| INIT | 系统上电 | 初始化外设、读取 Flash 配置、LCD 显示 logo | 初始化完成 |
| NORMAL | 无事件 | 采集传感器、刷新屏幕、检查是否需要加热 | 收到按键事件 |
| HEATING | 水温低于目标-0.5°C | 开启 PWM 占空比输出 | 水温达到目标+0.2°C |
| LOW_WATER | EXTI 下降沿 | 关闭加热和水泵、显示告警信息 | 水位正常且按键确认 |
| MANUAL | 按键长按 | 用户手动控制水泵/灯光 | 超时 60s 自动退出 |
在实现状态机的过程中,推荐用函数指针数组或者 switch-case 加状态枚举。前者扩展性好,后者代码直观,项目代码量不大,用 switch-case 就够了。注意所有状态机的跳转都要经过统一的state_change()函数处理,在这个函数里做进入和退出动作,比如离开HEATING时把 PWM 占空比清 0,进入LOW_WATER时置位蜂鸣器,这样就不会遗漏状态边界条件。
4.1 串口日志与调试命令:裸机开发也有 Debug 入口
很多初学者在 STM32 上做调试只靠点灯,但灯只能表明程序跑到哪里,看不出变量值的变化。这套项目把 UART1 作为调试串口,通过重定向printf()到串口助手,可以在电脑上实时观察温度、水位、PWM 占空比这些关键参数。这里有一个使用细节:标准库的printf()默认会检查stdout是否指向串口,需要在初始化里调用setvbuf(stdout, NULL, _IONBF, 0)关闭缓冲,否则输出不全。
除了周期日志,我通常还会加一个简单的命令解析功能,比如输入字符t立即返回当前温度,输入pwm 100手动设置加热占空比。这样在调参的时候直接在串口助手下发命令,不用烧录代码就能验证控制效果。实现方案是开启 UART 接收中断,把收到的字节存入环形缓冲区,主循环里逐字节解析,遇到换行符再执行命令。注意命令解析和传感器采集都在同一个循环里跑,不要让解析逻辑阻塞采集逻辑,环形缓冲区的设计用头尾指针加计数变量即可。
如果是调试 SPI 或者 I2C 外设,使用逻辑分析仪比示波器更方便,可以同时抓多个通道观察时序关系。调试 LVGL 显示时,如果界面出现花屏,先检查 SDRAM 初始化是否完成,再刷一个纯色背景测试,如果纯色正常说明显存地址没问题,问题在图层叠加或颜色格式配置上。
4.2 掉电保存与参数配置:EEPROM 模拟,还是外挂 Flash
鱼缸的控制参数(目标温度、加热回差值、灯光开关时间)需要在掉电后保存,否则每次上电都要重新设置。STM32F103 内部有 Flash,可以用自带的 EEPROM 模拟功能,通过操作 Flash 扇区来保存数据,但擦写次数有限(大约一万次)。如果用外部 I2C EEPROM(比如 AT24C02),则性能和寿命都更好,但增加成本和布线面积。这套项目里没有明确给出参数存储这块,所以你自己扩展时建议设计一个简单的协议,用结构体打包参数,加上帧头帧尾和 CRC8 校验,存放在固定地址。
typedef struct { uint16_t magic; float target_temp; uint8_t hysteresis; uint8_t light_on_hour; uint8_t light_on_min; uint8_t pump_speed_level; uint8_t crc; } SystemConfig; #define CONFIG_FLASH_ADDR 0x0800FC00 SystemConfig g_config; void Config_Save(void) { g_config.magic = 0xA5A5; g_config.crc = Calc_CRC8((uint8_t*)&g_config, sizeof(SystemConfig) - 1); FLASH_Unlock(); FLASH_ErasePage(CONFIG_FLASH_ADDR); uint32_t* p = (uint32_t*)&g_config; for (uint8_t i = 0; i < sizeof(SystemConfig) / 4; i++) { FLASH_ProgramWord(CONFIG_FLASH_ADDR + 4 * i, p[i]); } FLASH_Lock(); }0x0800FC00是 F103 的最后 1KB 地址空间,用于存放参数足够。写 Flash 前必须擦除整页,F103 的页大小是 1KB 或 2KB 取决于型号,如果结构体很小,就直接把整页擦掉重写。这段代码里的Calc_CRC8可以用查表法做,效率高且占用空间小。要注意的是:Flash 编程前必须读出来比较,如果内容相同就不要擦写,否则每次上电都做一次擦写会大幅缩短 Flash 寿命。读取配置时,若magic不等于0xA5A5,说明 Flash 为空或数据损坏,此时填充默认参数。
5. 从裸机到实用:四个改善体验的进阶调试技巧
裸机框架把系统跑通只是第一步,让它长时间稳定运行、功耗更低、操作更顺手,有时比加功能更重要。基于对这套代码的阅读和实际运行的体会,归纳四个可以直接修改的改进点,不需要改变整体架构就能落地。
5.1 用空闲中断完美处理不定长串口指令
标准库的 UART 接收中断是收到一个字节进一次中断,如果要做setpwm 120这样的字符串命令解析,必须凑够固定长度才能判定一帧结束,这导致灵活性差。用 USART 的空闲中断(IDLE)配合 DMA 接收,可以实现不定长数据帧的自动接收。初始化时使能USART_IT_IDLE,当串口线上超过一个字节时间没有新数据时触发中断,此时 DMA 已经把所有数据搬到了缓冲区,只需要在中断里读取 DMA 剩余计数器就能算出本次收到的字节数。这种方式在事件上报、GPS 解析等场景是通用技巧。
void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE)) { USART_ReceiveData(USART1); rx_len = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); rx_complete_flag = 1; DMA_ClearFlag(DMA1_FLAG_GL5); } }代码里先读一次USART_ReceiveData清除 IDLE 标志,再去 DMA 的当前数据计数寄存器反推接收长度。如果 DMA 缓冲区不够大,超过缓冲区长度的数据会被截断,解决办法是把缓冲区开大一点,或者启动循环 DMA 加双缓冲。这套项目里的串口接收是单字节中断模式,改装成上述方案后调试效率会明显提升。
5.2 低功耗模式:把静态电流从 20mA 降到 5mA 以内
鱼缸系统需要 24 小时运行,功耗不是测量精度问题,而是电费和发热问题。STM32 的三种低功耗模式中,Sleep 模式只停 CPU,外设还能干活;Stop 模式停掉所有时钟,可通过 EXTI 唤醒;Standby 模式几乎完全关断。鱼缸里传感器需要周期唤醒,所以更适合 Stop 模式加 RTC 闹钟唤醒,每 5 秒醒来采集一次数据和刷新显示,然后重新进入 Stop。
实现时注意 ADC 和看门狗在 Stop 模式下会停止工作,如果你开了 IWDG(独立看门狗),需要在进入 Stop 模式前刷新喂狗,否则醒来后直接复位。另一个细节是进入 Stop 模式之前关闭所有 LED 指示灯和外设时钟,用__WFI()指令等待事件。按键和外部传感器中断都能唤醒芯片,如果主控板是电池供电,建议把 LCD 背光控制到单独的 GPIO,休眠时拉低背光,省下几十毫安电流。
5.3 异常重试与自动恢复机制
传感器在长期运行中难免接触不良,DS18B20 单总线没有 CRC 校验的话,偶尔读到乱数据会成为控制系统的噪声源。可以在读取温度后追加一个合理性检查,比如当前读数和上次读数差超过 8 度就丢弃本次值,并记录错误计数,连续 5 次异常则把传感器状态置为OFFLINE,报警但不影响其他功能。对于 I2C 外设,如果通信无应答,先软复位外设,再重试两次,最后才报错。这个重试机制可以抽象成一个通用的I2C_ReadWithRetry()封装,避免主逻辑代码重复。
重启策略要谨慎,如果是看门狗复位或硬件错误,复位本身可能导致继电器重复吸合,对加热棒就有风险。所以软件复位重启前必须执行安全动作,比如清掉定时器 PWM 输出,关闭继电器。建议加一个复位原因检测函数,通过读取RCC->CSR寄存器区分是上电复位、外部复位还是看门狗复位。
5.4 自动喂食与定时任务的扩展接口
这套项目源码里的定时器预留已经足够,你可以额外接一个舵机模块,利用定时器的另一个通道输出 50Hz 的 PWM 信号,通过修改比较寄存器控制舵机角度,实现定时开仓喂食。定时任务的调度建议用一个软件定时器数组,每个元素保存触发时间和回调函数指针,SysTick 中断中循环检查是否超时。这种方式比在多个外设中断里各写一套延时判断要清晰得多,扩展时直接往数组里增加配置项即可。
最后一个值得留意的细节是:鱼缸控制板会放在潮湿环境中,PCB 要做三防漆处理,并且所有对外接插件都要选用防水端子。实际运行过程中,水泵和水体之间的漏电流会造成地电位抬升,极端情况下 STM32 复位或程序跑飞,看门狗不是万能的,它只复位 CPU,不恢复外设状态。稳妥的做法是上电初始化时总是把继电器和电机输出置为关闭状态,等待系统完全初始化完成并检查传感器正常后再开启,这样能避免上电瞬间设备的误动作。
本文还有配套的精品资源,点击获取