简介:本资源是一套基于STM32平台的智能鱼缸嵌入式系统完整实现,面向计算机、自动化、电子信息、人工智能等专业的在校学生、课程设计实践者及嵌入式初学者,解决水温监测、液位检测、LED照明控制、水泵驱动与OLED人机交互等典型物联网应用场景开发问题。压缩包共1713个文件,含393个C源码(核心逻辑与外设驱动)、310个H头文件(模块接口定义)、215个编译中间文件(.d/.o/.i)及配套Python脚本、RTOS配置、LVGL图形示例代码与Keil工程文件(.uvprojx/.uvoptx),整体大小61.29MB,结构规范,便于理解STM32HAL库开发流程与多任务协同设计。已有462人学习下载,项目经课程答辩验证,平均得分94.5分,附完整README说明、可运行二进制镜像(.bin/.hex)及调试配置文件,支持开箱即用、功能复现与二次扩展,适合作为课设范例、毕设原型或嵌入式进阶学习素材。
1. 项目概述:从零打造一个会思考的鱼缸
几年前,我还在用着一个老旧的机械式鱼缸,每天手动开关灯、凭感觉喂食、时不时还得担心加热棒是不是坏了。直到有一次出差,家里的加热棒温控失灵,一缸养了快两年的鱼全没了,那感觉真是糟透了。自那以后,我就琢磨着,能不能用自己熟悉的STM32单片机,给鱼缸装上一个“大脑”,让它能自己照顾自己。这不只是一个简单的温控或定时项目,而是一个集环境感知、智能决策、远程交互于一体的微型生态系统控制器。
这个“基于STM32的智能鱼缸”项目,核心目标就是解放鱼友的双手,同时为鱼儿提供一个更稳定、更健康的生活环境。它不仅仅是一个技术Demo,更是一个可以直接投入日常使用的完整产品原型。项目涵盖了从硬件选型、电路设计、嵌入式软件编程到简单上位机交互的全流程。对于电子爱好者、物联网初学者或者想深入STM32开发的工程师来说,这是一个绝佳的练手项目,因为它几乎触及了嵌入式开发中所有常见的技术点:GPIO控制、ADC采样、定时器、PWM、串口通信、传感器驱动、状态机逻辑,甚至简单的网络通信(如果扩展的话)。
在接下来的内容里,我会把我从构思到实现过程中所有的细节、踩过的坑、以及最终稳定运行的代码逻辑,毫无保留地分享出来。你会发现,用一片几十块钱的STM32核心板,搭配一些常见的传感器和执行器,就能构建一个相当可靠的智能鱼缸控制系统。我们不仅会实现自动温控、自动灯光、定时喂食这些基础功能,还会探讨如何让系统更“智能”,比如根据环境光自动调节灯光亮度,或者通过手机远程查看状态和手动干预。
2. 核心需求与系统架构设计
2.1 功能需求拆解:鱼缸到底需要什么?
在动手画原理图之前,我们必须先想清楚,一个理想的智能鱼缸应该具备哪些能力。我把它归纳为“监测、控制、交互”三个层面。
监测层面(系统的“感官”):
- 水温监测:这是最重要的参数。热带鱼、冷水鱼、水草对温度的要求各不相同,必须持续、精确地监控。通常需要18-30℃的范围,精度最好能达到±0.5℃。
- 水位监测:防止水泵干烧,以及在水位过低时报警。这对于使用外置过滤桶的鱼缸尤为重要。
- 环境光强度监测:用于实现灯光系统的自动启闭和亮度模拟自然日光变化,而不是简单的定时开关。
- 水质监测(进阶):如PH值、溶解氧(TDS)等。这部分传感器通常较贵且需要校准,在基础版本中我们可以预留接口。
控制层面(系统的“手脚”):
- 加热棒控制:根据水温监测结果,通过继电器控制加热棒的电源,实现恒温。
- 灯光控制:不仅控制开关,最好能通过PWM调节LED灯的亮度,模拟日出日落。
- 喂食器控制:通过一个小型舵机或步进电机,在设定时间自动投放定量的鱼食。
- 水泵/造浪泵控制:可以定时启停造浪泵,模拟水流变化;或者根据水位控制主水泵。
- 换水/补水控制(进阶):通过电磁阀和水泵实现自动换水或补水,这涉及更复杂的管路设计。
交互层面(系统的“面孔”):
- 本地显示与设置:一块OLED或LCD屏幕,用于实时显示所有传感器数据和系统状态,并通过几个按键进行参数设置(如目标温度、喂食时间)。
- 远程监控与告警:通过Wi-Fi或蓝牙模块,将数据发送到手机APP或微信小程序,并能在异常时(如温度超限、水位过低)推送报警信息。
- 数据记录:将关键数据(温度曲线、喂食记录)保存在单片机的Flash或外置SD卡中,便于回顾分析。
基于以上需求,一个基础且实用的版本可以聚焦于:温度监测与控制、灯光自动管理、定时喂食、本地显示这四大核心功能。远程交互可以作为扩展功能,我们先保证核心系统的稳定可靠。
2.2 硬件架构选型:为什么是STM32?
市面上常见的单片机如Arduino、ESP32都能做这个项目。我选择STM32,尤其是STM32F103C8T6(俗称“蓝莓派”或“最小系统板”),主要基于以下几点考量:
- 性能与资源充足:STM32F103拥有72MHz的Cortex-M3内核,远超Arduino Uno的16MHz AVR。这意味着我们可以轻松运行一个实时操作系统(如FreeRTOS)来管理多个并发的任务(温控、灯光PWM、按键扫描、显示刷新),而不会显得吃力。其内置的Flash和RAM也足够存放复杂的控制逻辑和数据缓冲区。
- 丰富的外设接口:我们需要用到多个定时器(用于PWM生成、喂食定时)、多个ADC通道(采集温度、光强等模拟信号)、多个USART(连接屏幕、调试、预留Wi-Fi模块)。STM32F103的外设资源正好匹配这些需求,且配置灵活。
- 开发生态成熟:无论是标准外设库(SPL)还是HAL库,都有完善的文档和丰富的社区资源。调试工具(ST-LINK)便宜且易用。这对于项目开发和后期排查问题至关重要。
- 成本与功耗平衡:STM32F103核心板价格已非常亲民,在提供强大性能的同时,其低功耗模式也能满足鱼缸系统7x24小时连续运行的需求。
核心硬件清单:
- 主控:STM32F103C8T6最小系统板。
- 温度传感器:DS18B20(单总线数字传感器,精度±0.5℃,防水探头可直接放入水中)。不推荐使用NTC热敏电阻+ADC的方案,因为校准麻烦且易受干扰。
- 光敏传感器:通用光敏电阻模块(输出模拟电压)或BH1750数字光照强度传感器(I2C接口,更精确)。
- 显示模块:0.96寸OLED显示屏(I2C接口,功耗低,显示效果好)。
- 执行机构:
- 加热棒控制:5V继电器模块(控制220V加热棒通断,注意强电安全!)。
- 灯光控制:MOS管模块(如IRF520)或专用的LED恒流驱动模块,用于PWM调光。
- 自动喂食器:SG90微型舵机,改造一个现成的喂食器或3D打印一个喂食机构。
- 输入:3-4个轻触按键,用于菜单设置。
- 电源:建议采用12V/2A的直流电源适配器作为系统总电源,通过DC-DC降压模块(如LM2596)为STM32、传感器、舵机等提供5V和3.3V电源。务必确保电源功率充足且稳定,这是系统长期稳定运行的基础。
注意:安全第一!控制220V加热棒的继电器模块,其强电部分必须做好绝缘和隔离,接线端子要拧紧,整个系统应放置在干燥、远离水源的地方。建议为强电部分单独制作一个带有安全盖的接线盒。
2.3 软件架构设计:状态机与任务分解
软件上,我们不采用简单的while(1)超级循环,而是引入基于时间片或FreeRTOS的任务思想,让程序结构更清晰。这里以时间片轮询法为例(易于理解,无需操作系统),其核心是一个精准的1ms系统滴答定时器中断。
主要任务分解:
- 传感器数据采集任务(每500ms执行一次):读取DS18B20水温、光敏电阻值。DS18B20读取耗时较长(~750ms),但它是单总线设备,在读取期间会阻塞CPU,因此需要采用非阻塞的状态机驱动,避免影响其他任务。
- 控制决策任务(每1s执行一次):
- 温控:采用增量式PID算法。比较当前水温与设定目标温度,计算PID输出值。输出值可以映射为加热棒在一个控制周期内的开启时间(即PWM占空比),但更简单可靠的方式是设置一个回差(Hysteresis)。例如,设定温度25℃,回差0.5℃。当温度低于24.5℃时开启加热,高于25.5℃时关闭加热。这比简单的开关控制更平稳,比完整的PID更简单且不易振荡。
- 灯光控制:根据环境光强度和预设时间,计算LED灯的目标亮度PWM值。可以设计一个“日光模拟曲线”,让灯光在早晨逐渐变亮,傍晚逐渐变暗。
- 执行器输出任务(每50ms执行一次):更新继电器、LED PWM、舵机的实际输出状态。这是一个快速任务,确保控制决策能及时生效。
- 人机交互任务(每100ms执行一次):
- 按键扫描与处理:检测按键动作,更新系统设置模式(如进入温度设置、时间设置菜单)。
- 显示刷新:根据当前模式,在OLED上更新显示内容(主界面显示实时数据,设置界面显示菜单项)。
- 定时事件任务(在系统滴答中断中检查):维护一个软件定时器列表,用于处理喂食时间点、灯光模式切换时间点等。当系统时间到达预设值时,触发相应事件(如驱动舵机转动一圈)。
这种架构使得每个功能模块相对独立,便于调试和维护。例如,你可以单独测试温度采集和控制逻辑,而不必关心灯光部分。
3. 硬件电路设计与连接要点
3.1 核心控制电路与电源设计
STM32最小系统板本身包含了复位电路、晶振和基本的滤波电路。我们需要重点关注的是电源分配和IO口规划。
电源树设计:
- 输入:12V/2A DC插座。
- 第一级降压:使用LM2596降压模块,将12V降至稳定的5V。该5V为继电器模块、舵机、OLED模块(部分OLED是5V逻辑电平)供电。注意:舵机在转动瞬间电流可能很大(可达500mA-1A),务必确保5V电源能提供足够电流,最好单独一路LM2596或使用大电流LDO。
- 第二级降压:使用AMS1117-3.3稳压芯片(通常核心板上已集成),从5V降压到3.3V,为STM32单片机、DS18B20、光敏传感器模块(通常输出0-3.3V)供电。
IO口规划表(以STM32F103C8T6为例):
| 外设模块 | 接口类型 | STM32引脚 | 备注 |
|---|---|---|---|
| OLED显示屏 | I2C | PB6(SCL), PB7(SDA) | 需配置上拉电阻,通常模块已集成 |
| DS18B20 | 单总线 (GPIO) | PA0 | 需配置为开漏输出模式,并外接4.7K上拉电阻至3.3V |
| 光敏传感器 | ADC | PA1 | 如果使用模拟输出模块,接至ADC通道 |
| 按键1 (设置) | GPIO输入 | PA8 | 配置为上拉输入,按键另一端接地 |
| 按键2 (加/确认) | GPIO输入 | PA9 | 同上 |
| 按键3 (减/返回) | GPIO输入 | PA10 | 同上 |
| 继电器控制 | GPIO输出 | PB0 | 配置为推挽输出,高电平触发继电器吸合 |
| LED调光 | PWM输出 | PA6 (TIM3_CH1) | 配置为复用推挽输出,生成PWM波 |
| 舵机控制 | PWM输出 | PA7 (TIM3_CH2) | 舵机控制需要50Hz的PWM信号,脉宽0.5ms-2.5ms |
实操心得:引脚分配的艺术。分配引脚时,优先考虑外设的复用功能。例如,将PWM输出分配到带有定时器通道的引脚(如PA6、PA7属于TIM3)。将ADC功能分配到带有ADC通道的引脚(如PA1是ADC1的通道1)。这样能最大化利用芯片资源,减少软件模拟的复杂度。同时,将相关的功能引脚尽量分组(如所有按键放在同一组GPIO口),便于代码编写和维护。
3.2 传感器与执行器接口详解
1. DS18B20温度传感器: 这是一款非常经典的数字温度传感器,其单总线协议节省了IO口,但时序要求严格。接线非常简单:VCC接3.3V,GND接GND,DQ数据线接STM32的PA0,同时在DQ和VCC之间接一个4.7KΩ的上拉电阻。 驱动难点在于时序,必须严格按照手册上的微秒级延时要求。在STM32中,由于系统频率高,直接使用for循环做延时非常不准。推荐两种方法:一是使用SysTick系统滴答定时器提供精准的微秒延时函数;二是利用定时器产生精确的延时。我的代码里采用了第一种方法,并实现了非阻塞的状态机驱动,避免在读取温度时卡住整个系统。
2. 光敏传感器与ADC采样: 我选用的是模拟输出模块,它本质上是一个光敏电阻和固定电阻的分压电路,输出0-3.3V的模拟电压。STM32的ADC分辨率是12位(0-4095),我们将ADC值映射为光照强度百分比或勒克斯值(需要校准)。 ADC采样需要注意滤波。由于光线变化不会非常剧烈,我们可以采用软件滤波。最简单有效的是滑动平均滤波:维护一个固定长度的数组(比如10个元素),每次采样新值放入数组,并剔除最旧的值,然后计算平均值作为最终结果。这能有效消除偶然的干扰毛刺。
// 简化的滑动平均滤波示例 #define FILTER_LEN 10 uint16_t adc_filter_buf[FILTER_LEN] = {0}; uint8_t filter_index = 0; uint16_t ADC_Filter(uint16_t new_value) { adc_filter_buf[filter_index] = new_value; filter_index = (filter_index + 1) % FILTER_LEN; uint32_t sum = 0; for(int i=0; i<FILTER_LEN; i++) { sum += adc_filter_buf[i]; } return (uint16_t)(sum / FILTER_LEN); }3. 继电器与强电控制安全: 继电器模块的控制端是三极管驱动,输入高电平(3.3V/5V)即吸合。STM32的GPIO直接驱动即可。关键在强电侧:
- 使用带螺丝端子的继电器模块,确保220V电源线连接牢固。
- 加热棒的电源线火线(L)切断,接入继电器的常开触点(NO)和公共端(COM)。
- 继电器模块与被控的220V电路之间,在物理上要保持足够距离,或使用绝缘胶带、热缩管进行隔离。
- 可以考虑在继电器线圈两端反向并联一个续流二极管(1N4148),防止断开时产生的感应电动势损坏STM32的IO口。不过大多数成品模块已经集成了这个保护电路。
4. PWM调光与舵机控制: 两者都使用PWM,但参数不同。
- LED调光:频率通常选择几百Hz到几KHz(如1KHz),因为人眼对低频闪烁敏感。占空比从0%(全暗)到100%(全亮)线性调节亮度。使用STM32的定时器输出比较功能可以轻松实现。
- 舵机控制:标准舵机控制信号是周期20ms(50Hz),高电平脉宽在0.5ms到2.5ms之间,对应0度到180度。我们需要生成一个固定50Hz,脉宽可变的PWM信号。同样使用定时器,将周期设置为20ms,通过改变通道的比较寄存器值来改变高电平时间。
// 初始化TIM3的CH1 (PA6)为PWM输出,用于LED调光 TIM_OC_InitTypeDef sConfigOC = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 1000 - 1; // 1MHz / 1000 = 1KHz htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim3); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 500; // 初始占空比50% (500/1000) sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);4. 嵌入式软件实现与核心代码解析
4.1 系统初始化与任务调度框架
程序入口main()函数主要完成三件事:硬件初始化、全局变量初始化、然后进入主循环。
int main(void) { // 1. HAL库初始化、系统时钟配置 HAL_Init(); SystemClock_Config(); // 2. 各硬件模块初始化 MX_GPIO_Init(); MX_ADC1_Init(); MX_TIM3_Init(); // PWM定时器 MX_USART1_UART_Init(); // 调试串口 // ... 其他外设初始化 // 3. 初始化系统滴答定时器,配置为1ms中断 HAL_Systick_Config(SystemCoreClock / 1000); // 4. 初始化全局状态结构体、滤波器缓冲区等 SystemState_Init(); // 5. 主循环 - 时间片轮询调度器 while (1) { // 任务调度标志位由1ms SysTick中断服务函数置位 if (task_1ms_flag) { task_1ms_flag = 0; Task_1ms_Handler(); // 扫描按键(防抖需要毫秒级精度) } if (task_10ms_flag) { task_10ms_flag = 0; Task_10ms_Handler(); // 快速执行器更新 } if (task_100ms_flag) { task_100ms_flag = 0; Task_100ms_Handler(); // 显示刷新、慢速逻辑 } if (task_500ms_flag) { task_500ms_flag = 0; Task_500ms_Handler(); // 传感器数据采集 } if (task_1000ms_flag) { task_1000ms_flag = 0; Task_1000ms_Handler(); // 控制算法决策 } // 非阻塞的DS18B20状态机驱动函数,在任何时间都可以被调用 DS18B20_StateMachine_Handler(&ds18b20); } }在SysTick_Handler()中断服务函数中,我们更新系统时间戳,并设置不同的任务标志位。
void SysTick_Handler(void) { HAL_IncTick(); sys_tick_count++; // 全局系统滴答计数 // 设置任务调度标志 task_1ms_flag = 1; if (sys_tick_count % 10 == 0) task_10ms_flag = 1; if (sys_tick_count % 100 == 0) task_100ms_flag = 1; if (sys_tick_count % 500 == 0) task_500ms_flag = 1; if (sys_tick_count % 1000 == 0) task_1000ms_flag = 1; }这种调度方式虽然不是真正的RTOS,但结构清晰,能保证关键任务的实时性(如按键响应),又不会因为某个耗时任务(如DS18B20读取)阻塞整个系统。
4.2 温度采集与PID控制算法实现
DS18B20非阻塞驱动: DS18B20的操作序列(初始化→发送ROM命令→发送功能命令→读取数据)每一步都需要严格的延时。为了实现非阻塞,我们将其设计为一个状态机。
typedef enum { DS18B20_STATE_IDLE, DS18B20_STATE_RESET, DS18B20_STATE_WAIT_PRESENCE, DS18B20_STATE_WRITE_ROM_CMD, DS18B20_STATE_WRITE_FUNC_CMD, DS18B20_STATE_READ_SCRATCHPAD, DS18B20_STATE_CONVERT_TEMP, DS18B20_STATE_WAIT_CONVERSION } DS18B20_State_t; typedef struct { DS18B20_State_t state; uint32_t delay_start_tick; uint8_t data[9]; float temperature; uint8_t is_data_valid; } DS18B20_HandleTypeDef; void DS18B20_StateMachine_Handler(DS18B20_HandleTypeDef *hds) { uint32_t current_tick = HAL_GetTick(); switch(hds->state) { case DS18B20_STATE_IDLE: // 每隔一段时间启动一次转换 if(current_tick - hds->last_convert_tick > 2000) { // 每2秒读一次 hds->state = DS18B20_STATE_RESET; DS18B20_Reset_Pulse(); // 发出复位脉冲 hds->delay_start_tick = current_tick; } break; case DS18B20_STATE_RESET: if(current_tick - hds->delay_start_tick >= 1) { // 等待至少480us if(DS18B20_Check_Presence()) { hds->state = DS18B20_STATE_WRITE_ROM_CMD; DS18B20_Write_Byte(0xCC); // 跳过ROM命令 } else { hds->state = DS18B20_STATE_IDLE; // 设备未响应,重试 } } break; // ... 其他状态处理:发送转换命令(0x44)、等待转换完成、发送读命令(0xBE)、读取9字节数据等 case DS18B20_STATE_READ_SCRATCHPAD: for(int i=0; i<9; i++) { hds->data[i] = DS18B20_Read_Byte(); } // 校验CRC,并计算温度值 if(DS18B20_Check_CRC8(hds->data, 8) == hds->data[8]) { int16_t raw_temp = (hds->data[1] << 8) | hds->data[0]; hds->temperature = raw_temp / 16.0f; // DS18B20精度为0.0625 hds->is_data_valid = 1; } hds->state = DS18B20_STATE_IDLE; hds->last_convert_tick = current_tick; break; } }在主循环中不断调用DS18B20_StateMachine_Handler,它就会在后台自动完成温度读取,而不会阻塞其他任务。读取到的有效温度值存储在hds->temperature中。
回差式温度控制: PID算法对于鱼缸这种大惯性系统有点“杀鸡用牛刀”,且参数整定麻烦。我采用更简单可靠的回差控制。
// 在每秒执行的任务中调用 void Temperature_Control_Task(void) { if(!ds18b20.is_data_valid) return; // 数据无效则跳过 float current_temp = ds18b20.temperature; float target_temp = system_setting.target_temp; // 用户设定的目标温度,例如25.0 float hysteresis = system_setting.temp_hysteresis; // 回差,例如0.5 static uint8_t heater_state = 0; // 0: off, 1: on if(heater_state == 0) { // 加热器关闭状态:当温度低于(目标温度 - 回差)时开启 if(current_temp < (target_temp - hysteresis)) { heater_state = 1; HAL_GPIO_WritePin(HEATER_GPIO_Port, HEATER_Pin, GPIO_PIN_SET); // 打开继电器 } } else { // 加热器开启状态:当温度高于(目标温度 + 回差)时关闭 if(current_temp > (target_temp + hysteresis)) { heater_state = 0; HAL_GPIO_WritePin(HEATER_GPIO_Port, HEATER_Pin, GPIO_PIN_RESET); // 关闭继电器 } } }这种算法能有效防止继电器在临界点附近频繁通断(“抖振”),延长继电器和加热棒寿命,同时控制精度完全满足鱼缸需求(±0.5℃)。
4.3 人机交互:OLED菜单与按键处理
显示界面采用简单的多级菜单结构。我设计了一个状态机来管理菜单。
typedef enum { MENU_MAIN, MENU_SET_TEMP, MENU_SET_LIGHT_TIME, MENU_SET_FEED_TIME, MENU_INFO } MenuState_t; MenuState_t current_menu = MENU_MAIN; uint8_t menu_cursor = 0; // 当前菜单中的光标位置 // 在100ms任务中调用 void Display_Refresh_Task(void) { OLED_Clear(); switch(current_menu) { case MENU_MAIN: OLED_ShowString(0, 0, "Temp: "); OLED_ShowFloat(40, 0, ds18b20.temperature, 1); // 显示一位小数 OLED_ShowString(80, 0, "C"); OLED_ShowString(0, 2, "Light: "); OLED_ShowPercent(40, 2, current_light_pwm); // 显示灯光亮度百分比 OLED_ShowString(0, 4, ">Set Temp"); OLED_ShowString(0, 6, " Set Time"); // 在光标位置显示箭头 OLED_ShowChar(0, 4 + menu_cursor*2, '>'); break; case MENU_SET_TEMP: OLED_ShowString(0, 0, "Set Target Temp:"); OLED_ShowFloat(0, 2, system_setting.target_temp, 1); OLED_ShowString(40, 2, "C"); OLED_ShowString(0, 6, "[OK] [Cancel]"); break; // ... 其他菜单项 } OLED_Refresh(); }按键处理采用状态机防抖,并区分短按和长按。
typedef struct { GPIO_TypeDef* port; uint16_t pin; uint8_t last_state; uint32_t press_start_tick; uint8_t is_pressed; uint8_t is_long_pressed; } Key_HandleTypeDef; Key_HandleTypeDef key_set, key_up, key_down; void Key_Scan_Task(void) { // 在1ms任务中调用 Key_Process(&key_set); Key_Process(&key_up); Key_Process(&key_down); } void Key_Process(Key_HandleTypeDef *key) { uint8_t current_state = HAL_GPIO_ReadPin(key->port, key->pin); // 按键按下为低电平(假设另一端接地) if(current_state == GPIO_PIN_RESET && key->last_state == GPIO_PIN_SET) { // 检测到下降沿,记录按下时刻 key->press_start_tick = HAL_GetTick(); key->is_pressed = 1; } else if(current_state == GPIO_PIN_SET && key->last_state == GPIO_PIN_RESET) { // 检测到上升沿,释放 if(key->is_pressed && !key->is_long_pressed) { // 短按触发事件 Key_ShortPress_Event(key); } key->is_pressed = 0; key->is_long_pressed = 0; } // 长按判断(例如超过1秒) if(key->is_pressed && !key->is_long_pressed) { if(HAL_GetTick() - key->press_start_tick > 1000) { key->is_long_pressed = 1; Key_LongPress_Event(key); } } key->last_state = current_state; } void Key_ShortPress_Event(Key_HandleTypeDef *key) { if(key == &key_set) { switch(current_menu) { case MENU_MAIN: if(menu_cursor == 0) current_menu = MENU_SET_TEMP; // ... 处理其他菜单项进入 break; case MENU_SET_TEMP: // 确认温度设置,保存到EEPROM或Flash Save_Settings(); current_menu = MENU_MAIN; break; } } else if(key == &key_up) { // 增加数值或上移光标 Adjust_Setting(1); } else if(key == &key_down) { // 减少数值或下移光标 Adjust_Setting(-1); } }通过这样的设计,三个按键就能完成复杂的菜单导航和参数设置。
5. 系统调试、优化与问题排查
5.1 上电调试与功能验证步骤
硬件焊接和软件编译完成后,不要急于将所有模块连接。应遵循“分模块调试,逐步集成”的原则。
第一步:核心板与基础调试。
- 只连接ST-LINK和串口线到电脑。
- 编写一个最简单的程序:让一个LED闪烁,并通过串口打印“Hello World”。
- 下载程序,确认LED能正常闪烁,串口助手能收到数据。这一步验证了最小系统、编程器和开发环境是正常的。
第二步:逐个添加传感器。
- DS18B20:单独连接DS18B20到STM32。编写测试代码,连续读取温度并通过串口打印。用手捏住传感器,观察数值是否上升。常见问题:温度读出来是85℃或-127℃。这通常是时序不准或上拉电阻没接导致的。务必用逻辑分析仪或示波器抓取单总线波形,对照DS18B20时序图检查。
- 光敏电阻:连接ADC,读取原始值,用手电筒照射和遮盖传感器,观察ADC值变化是否平滑。测试滑动平均滤波的效果。
- OLED:运行显示测试程序,确认能正常显示字符和图形。
第三步:测试执行器。
- 继电器:先不接加热棒,用万用表通断档测量继电器输出端。在程序中控制GPIO高低电平,听继电器是否有“咔嗒”声,万用表是否显示通断变化。务必确认继电器动作逻辑正确(高电平吸合还是低电平吸合)。
- PWM调光:连接一个普通LED到PWM输出引脚(串联一个220Ω电阻)。编写程序让PWM占空比从0%渐变到100%,观察LED是否平滑地从暗变亮。
- 舵机:单独给舵机供电(注意电流要足),编写程序输出一个1.5ms脉宽的PWM(中位)。观察舵机是否转动到中间位置。然后尝试0.5ms和2.5ms,看是否转到两个极限位置。
第四步:集成与逻辑测试。 将所有模块连接好,上电。通过串口打印所有传感器数据和系统状态。手动改变环境(如用吹风机轻微加热DS18B20),观察温控逻辑是否正确触发继电器。遮挡光敏电阻,观察灯光PWM是否自动调整。到达设定的喂食时间,观察舵机是否动作。
5.2 常见问题与解决方案速查表
在实际搭建和调试过程中,我遇到了不少典型问题,这里总结一下:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 系统完全不上电,或STM32不工作 | 1. 电源接反或电压不对。 2. 电源模块损坏。 3. 核心板Boot引脚配置错误。 | 1. 用万用表测量各点电压:12V输入、5V输出、3.3V输出是否正常。 2. 检查STM32的VDD和VDDA是否都有3.3V。 3. 确认BOOT0和BOOT1引脚已通过电阻下拉到地(通常核心板已处理好)。 |
| DS18B20读出的温度固定为85℃或-127℃ | 1. 单总线时序不准确。 2. 上拉电阻未接或阻值不对。 3. 传感器损坏。 | 1.强烈建议用逻辑分析仪抓取DQ线波形,对照数据手册检查复位脉冲、读写时序的宽度。 2. 确认DQ和3.3V之间有4.7KΩ上拉电阻。 3. 更换一个DS18B20测试。 |
| 继电器频繁“哒哒”响(抖振) | 温控逻辑是简单的开关控制,没有回差。 | 改用上文介绍的回差控制算法。将回差设置为0.3-0.5℃。 |
| OLED显示乱码或完全不显示 | 1. I2C地址不对。 2. I2C引脚未配置上拉。 3. 初始化序列错误。 | 1. 用I2C扫描程序确认OLED的7位地址(通常是0x78或0x7A)。 2. 确认SCL和SDA线上有上拉电阻(4.7K-10K到3.3V)。 3. 检查OLED初始化代码,确保发送了正确的命令序列(如关闭休眠、设置对比度等)。 |
| PWM控制LED,低亮度时闪烁 | PWM频率太低。人眼对低于100Hz的闪烁敏感。 | 提高定时器产生的PWM频率。将频率设置为200Hz以上(如1KHz),闪烁感会消失。 |
| 舵机不动或抖动 | 1. PWM频率不是50Hz。 2. 脉宽范围不对。 3. 供电不足。 | 1. 用示波器测量PWM信号,确认周期是否为20ms(50Hz)。 2. 确认高电平脉宽能在0.5ms到2.5ms之间变化。 3.舵机必须单独供电!不要从STM32的3.3V或5V引脚取电,要用外接的5V电源,且电流能力足够(单个SG90堵转电流可达700mA)。 |
| 系统运行一段时间后死机或复位 | 1. 电源纹波大或功率不足。 2. 软件看门狗未处理。 3. 堆栈溢出。 | 1. 在STM32的电源引脚附近加104和10uF的退耦电容。检查总电源负载,特别是舵机动作时电压是否被拉低。 2. 如果开启了独立看门狗(IWDG),需定期喂狗。 3. 增大启动文件中的堆栈大小。检查是否有递归函数或大型局部数组。 |
| 按键反应不灵或连击 | 按键消抖处理不好。 | 使用上文提到的状态机消抖算法,并适当调整消抖时间(通常10-20ms)。避免在主循环中使用HAL_Delay进行消抖。 |
5.3 系统优化与扩展思路
当基础功能稳定运行后,可以考虑以下优化和扩展,让鱼缸更“智能”:
- 引入实时时钟(RTC):STM32F103内部有RTC模块,但需要外部32.768KHz晶振和备份电池。有了精确的实时时钟,定时功能(喂食、灯光)就不再依赖于上电时间,断电重启后也能保持。
- 数据存储:将用户设置(目标温度、定时时间)保存到STM32内部的Flash(模拟EEPROM)中,避免每次上电重新设置。可以使用
HAL_FLASH_Program和HAL_FLASH_Read函数,但要注意Flash的擦写寿命(约1万次),不要频繁保存。 - 灯光情景模式:不仅仅是简单的开关。可以编程实现“日出日落”模式:在设定的“日出”时间前30分钟,灯光PWM从0%缓慢线性增加到100%;在“日落”时间,则缓慢变暗。这更符合自然规律,对鱼和草都有益。
- 加入水位传感器:使用简单的浮子开关或更精确的超声波测距模块,监测水位。当水位低于下限时,可以声光报警,甚至联动一个水泵自动补水(需要增加水泵和电磁阀控制)。
- 远程监控与控制(Wi-Fi/蓝牙):
- ESP-01S Wi-Fi模块:通过串口AT指令与STM32通信。STM32将传感器数据打包成JSON格式,通过ESP-01S发送到云平台(如阿里云、OneNET)或自己的服务器。手机APP从云端获取数据并发送控制指令。
- HC-05/06蓝牙模块:成本更低,连接手机更方便。可以开发一个简单的手机APP(用App Inventor或MIT App Inventor这类图形化工具就能实现),通过蓝牙与鱼缸通信,实现短距离内的状态查看和控制。
- 使用FreeRTOS:如果后续功能越来越复杂(比如同时处理Wi-Fi通信、复杂的灯光模式、多个传感器),可以考虑将程序移植到FreeRTOS上。将温控、显示、通信、喂食等任务分别创建为独立的FreeRTOS任务,由内核进行调度,代码结构会更清晰,实时性也更有保障。
这个项目最吸引人的地方在于,它从一个具体的需求出发,串联了嵌入式开发中绝大部分核心技能。当你看到自己亲手制作的系统稳定运行,鱼缸里的生态环境因为你的代码而变得更加可控和美好时,那种成就感是无可替代的。从稳定的电源开始,到可靠的传感器读数,再到鲁棒的控制逻辑,每一步都需要严谨的思考和细致的调试。希望我的这些经验和代码片段,能为你点亮自己那盏“智能”之灯提供一些切实的帮助。
本文还有配套的精品资源,点击获取