基于STM32的植物生长箱环境控制系统设计与实现

发布时间:2026/9/1 19:37:48
基于STM32的植物生长箱环境控制系统设计与实现 简介本资源是一套基于STM32F103系列单片机开发的植物生长箱环境控制系统完整工程面向嵌入式初学者、农业物联网爱好者及高校课程设计/毕业设计学生解决植物培育过程中温湿度、光照、土壤墒情与水位等多参数协同监测与智能调控难题。压缩包共265个文件含49个C源文件如stm32f10x_adc.c、gizwits_protocol.c、47个头文件h、47个编译中间文件o及46个依赖描述crf涵盖底层驱动、传感器采集、WIFI通信GizWits云平台对接、OLED显示、蜂鸣报警与双模式控制逻辑另含Keil工程文件uvprojx、hex固件、PCB/SCH原理图及3个实操演示mp4视频整体大小129.17MB。已有88人学习下载提供从硬件连接、传感器标定、阈值逻辑配置到手机APP远程切换手动/自动模式的全链路实现配套OLED实时界面与多级报警机制可直接编译烧录运行是嵌入式软硬协同开发的典型实践案例。 去年帮朋友做了一台植物生长箱的控制器前后折腾了三个版本才稳定下来。最初我也有个挺天真的想法既然只是“测温度、湿度、开加热、开加湿”拿STM32接几个传感器、几个继电器就完事了。实际做下来发现完全不是这么回事——补光灯一开箱内温度马上往上蹿雾化加湿一启动湿度探头的数据延时严重继电器频繁跳闸不到一周触点就发黑。环境控制系统本质是一个强耦合、大惯性的闭环系统不是简单的开关组合。这篇博客我打算从架构设计、传感器采集、执行器驱动、控制策略、人机交互到实测排错完整复盘这个“基于STM32的植物生长箱环境控制系统”项目。主控用的是STM32F103C8T6配合SHT30温湿度、BH1750光照、MQ135等传感器实现温度、湿度、光照、CO2的自动调节。如果你正准备做课程设计、想给阳台花房做一套自动化设备或者在做小型环境监控产品这篇内容可以当成一份可直接参考的实现方案。1. 整体架构先想清楚“测什么、控什么、怎么控”1.1 控制对象拆解别一上来就画电路植物生长箱的控制对象比想象中多。温度、湿度、光照、CO2浓度、土壤湿度这五个变量都不是独立的例如补光会产生大量热量风扇换气会带走湿度和温度雾化加湿又会轻微降温。如果不先把这些耦合关系理清直接画电路、写代码后面调起来会非常痛苦。我先把控制对象整理成一张表后面所有设计都以这张表为基准被控量传感器选型执行器时间常数主要耦合影响温度SHT30 DS18B20PTC加热器、排风扇约5~15分钟补光灯发热显著升温湿度SHT30超声波雾化器 风机约2~5分钟加湿会轻微降温光照BH1750全光谱LED灯带毫秒级LED发热影响温度CO2MQ135排气扇、新风阀数分钟到数十分钟换气同时改变温湿度土壤湿度电容式土壤探头微型水泵、滴灌阀数小时浇水提高空气湿度从这张表能得出几个关键结论温度是整个系统的核心因为它受补光灯、风扇、雾化器三重干扰。光照响应最快适合做定时和PWM渐变控制。土壤湿度时间常数最长用定时浇水策略即可不需要快速闭环。架构设计上我采用了以STM32F103C8T6为主控、传感器采集层和执行器驱动层分离的方案。采集层通过I2C、单总线、ADC把环境参数汇集到主控执行器层用光耦把MCU和强电/大电流设备隔离控制策略在MCU内部以1秒为周期运行。这个分层后面会让调试方便很多某个环节出问题时不用整板排查。1.2 主控选型为什么F103C8T6够用很多新手做项目会陷入一个误区芯片越强越好。实际上植物生长箱控制系统的计算压力很小STM32F103C8T6的资源完全够用。C8T6的核心配置72MHz Cortex-M3、64KB Flash、20KB RAM、3个ADC、2个I2C、3个USART、1个SPI。这些资源在项目中是这样分配的3路ADC一路给MQ135 CO2传感器一路给土壤湿度探头一路留作箱内壁温度冷端补偿或用不上的备用。I2C1接SHT30温湿度和BH1750光照共用一条总线。USART1接ESP8266或上位机调试。USART2预留485接口以后扩展更多执行器。定时器TIM1输出三路PWM一路给补光灯调光一路给风扇调速一路给PTC加热器调功。有人会问F407不更好吗F407的RAM更大、主频更高但在这个场景下用不上。C8T6的劣势主要是Flash小如果我要上LVGL或FreeRTOS大屏显示64KB确实紧张那就得换F407VET6或F103RCT6。如果只是做传感器采集、PID策略、OLED显示、按键交互C8T6能稳稳跑起来而且最小系统板便宜焊坏了也不心疼。1.3 软件架构裸机时间片还是FreeRTOS这是软件设计里必须想清楚的问题。我第一版贪图省事直接在while(1)里顺序执行读传感器、算PID、刷新显示、查按键。结果发现一个严重问题——读取SHT30时I2C通信需要阻塞等待这段时间按键扫描和显示刷新全部卡住PWM输出也会出现微小的抖动。后来我把软件改成“时间片轮询状态机”架构用SysTick产生1ms时基把任务按优先级分到不同的时间间隔里执行。传感器读取1秒一次PID计算1秒一次显示刷新200毫秒一次按键扫描10毫秒一次。这样做的效果立竿见影整个系统响应变得很稳定。那什么时候上FreeRTOS如果你要接入ESP8266跑TCP/MQTT协议栈、同时处理多路串口数据、还需要管理多个异步任务裸机时间片会很累。FreeRTOS的好处是把“读传感器”“控执行器”“跑协议栈”做成独立任务用队列和信号量通信。它的缺点是任务切换会增加调试复杂度内存也得多留一点。按这个项目的体量我的建议是单箱体控制用时间片就够了多箱体联网再上RTOS。时间片轮询的任务划分参考任务周期优先级说明按键扫描10ms高带消抖检测短按/长按OLED刷新200ms中只刷新变化区域避免整屏重绘传感器采集1s中I2C读取SHT30/BH1750、ADC采样控制策略1s高PID计算、滞回判断、仲裁输出串口解析事件触发中串口空闲中断接收解析参数看门狗喂狗500ms高IWDG复位防止程序跑飞2. 传感采集层环境参数测量的关键细节2.1 温湿度采集别用DHT11换SHT30体验完全不同我知道很多人的第一反应是DHT11便宜、教程多。但在这个项目里DHT11的误差实在让人难受温度±2℃、湿度±5%RH而且单总线协议时序非常敏感一旦被中断打断读数就会卡住或者返回错误。植物生长箱里温度变化本来就慢传感器误差太大PID根本调不准。我最后用了SHT30SHT30走I2C接口精度高典型±0.3℃、±2%RH带CRC校验可以挂在I2C总线上和光照传感器共用。它的I2C地址是0x44ADDR引脚接低。测量流程是发送测量命令0x2C 0x06等待15ms读取6字节数据前两字节是温度、后两字节是湿度、中间有CRC。温度转换公式uint16_t temp_raw (data[0] 8) | data[1]; float temperature -45.0f 175.0f * temp_raw / 65535.0f;另一个实用技巧是DS18B20多点测温。SHT30只能测箱内空气温度但植物叶片附近和根部附近的温度可能存在几度差异。如果你想让控制系统更精细可以加几颗DS18B20挂在单总线上分别测箱顶、箱底、根区温度。DS18B20的时序要求比DHT11宽松仍建议在读取时关中断或用定时器辅助时序。2.2 光照与CO2的测量数字传感器和模拟传感器要区别对待光照我用的是BH1750I2C接口直接读出来就是勒克斯值。它的测量范围是0~65535 lx对植物补光场景完全够用。注意BH1750上电后需要先发送断电命令再发送连续高分辨率测量命令然后等120ms左右再去读结果。实测中光照传感器要放在植物冠层高度不能贴着补光灯管否则读出来的是直射光强不是植物接收到的有效光照。CO2浓度是很多人忽略的参数。箱内植物在光合作用旺盛时会消耗CO2如果箱子是密闭的CO2浓度下降会导致光合速率骤降。我用了MQ135它本质上是一个热丝式气敏电阻对CO2、酒精、烟雾都有响应。MQ135有两大问题第一是预热时间长上电后需要至少24小时才能稳定第二是绝对精度差只能做趋势判断。MQ135的用法是把模拟输出接到STM32的ADC引脚通过采样电压反推气体浓度。关键在标定在洁净空气中测出一个基准电压工作时用当前电压与基准电压的比值Rs/Ro估算浓度变化。我的做法是只做两级判断CO2浓度超过设定阈值就打开排气扇换气低于阈值就关闭不做精确的连续调节。2.3 ADC多通道扫描DMA最容易踩坑的信号链路“stm32 adc多通道扫描循环采样dma”是搜索量很大的关键词说明这个坑踩的人很多。我在这个项目里用ADC1采集MQ135和土壤湿度两个通道配置成扫描模式、连续转换模式、DMA循环传输。重点说坑扫描模式下ADC会按Rank顺序依次转换各个通道但DMA缓冲区里的数据顺序是严格按照ADC转换完成顺序写入的不是按你中断里读的顺序。如果你配置时把通道1设为Rank1、通道2设为Rank2那DMA缓冲区的第0个元素就是通道1的数据第1个元素是通道2的数据。看似简单但很多人栽在CubeMX配置里通道顺序和实际接线不一致结果土壤湿度的数据跑到CO2通道上去了。核心配置代码片段// ADC1 初始化, 两个通道 sConfig.Channel ADC_CHANNEL_0; // PA0 - MQ135 sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_1; // PA1 - 土壤湿度 sConfig.Rank ADC_REGULAR_RANK_2; HAL_ADC_ConfigChannel(hadc1, sConfig); // DMA缓冲区, 数量必须等于通道数 uint16_t adc_buf[2]; HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 2);采样时间建议设长一点239.5周期比较稳。实测中如果使用ADCDMA循环模式adc_buf里的数据一直会被自动刷新你只需要在需要时拷贝一份出来避免在中断里处理时数据被更新成一半。另外所有ADC采集的引脚电压都必须是0~3.3VMQ135模块如果输出的是5V电平中间需要用分压电阻或运放调理。2.4 传感器布局与线缆抗干扰这块是在实际运行后才发现的问题。第一版我把SHT30贴在箱体侧壁和加热器只有20厘米距离。结果PTC一工作湿度读数先剧烈下跌再缓慢恢复——其实是热量驱动了传感器周围空气形成局部对流读数根本不能代表箱内平均环境。正确的做法是SHT30放在箱内中间高度用有孔塑料壳罩住避免气流直接吹到探头。光照传感器水平放在植物冠层位置保证补光灯照度和自然光都能覆盖。传感器线缆和继电器线缆分开走线尤其不能和220V交流线绑在一起。给I2C信号线SCL、SDA加4.7kΩ上拉电阻到3.3V线长超过50cm时考虑加TVS管。3. 执行器驱动把控制量变成真实环境变化3.1 隔离与驱动电路怎么设计执行器层面有一个原则强电和弱电必须隔离。MCU是3.3V逻辑继电器、水泵、雾化器是220V或12V/24V系统如果共用电源继电器吸合瞬间的电流冲击很容易把MCU复位。我用的方案是光耦隔离 达林顿管/三极管驱动继电器。光耦选了PC817输入侧串联1kΩ限流电阻电流控制在5~10mA输出侧接继电器线圈。继电器线圈两端必须反向并联一个1N4007续流二极管否则关断瞬间的反向电动势会击穿三极管。如果驱动12V LED灯带或风扇这类直流设备更推荐用低边N-MOS管比如AO3400导通电阻极低发热小。栅极要加10kΩ下拉电阻防止MCU上电瞬间GPIO悬空导致灯带闪烁。如果是PTC加热器这类纯阻性交流负载还有第三种方案用MOC3021光耦可控硅驱动BT136双向可控硅实现过零调功。这种方案比继电器更适合频繁调节因为继电器触点频繁通断寿命会迅速下降。不过可控硅调功需要过零检测电路电路复杂度高一些如果加热器功率不大、控制周期较长用继电器也能接受。3.2 补光灯PWM调光与调温解耦补光灯用PWM调光非常合适。我用的全光谱LED灯带是12V供电总功率约20W用AO3400做低边开关PWM频率选500Hz。之所以不选更高频率是因为500Hz足以避免人眼明显闪烁MOS管开关损耗也小LED驱动器本身能承受。补光灯最大的问题是它一工作就会发热对温度控制产生明显的正向扰动。第一版实测20W LED在密闭箱体里能让温度在半小时内上升3~5℃。PID控制器会先抑制这个扰动但响应有滞后温度出现明显的过冲。解决思路是在策略层加前馈补偿而不是只靠PID反馈。简单来说当补光灯从0%调到100%时估算约增加多少热功率然后在PID输出的基础上直接减去一个偏置量。这个偏置量可以通过实验得出例如50%光照对应约1.5℃升温就先补偿1.5℃的加热输出。这样PID只需要处理净偏差温度稳定性大幅提升。3.3 雾化加湿和水泵的驱动注意事项超声波雾化器是典型的“电力凶猛”设备。我用的一款12V雾化模块标称工作电流3A启动瞬间电流能冲到5A以上。如果你把雾化器和STM32共用同一个12V电源雾化器一起动电压瞬降MCU直接就复位了。这个坑我踩得很实在后面单列一节讲排查过程。驱动方式上雾化器用继电器或大电流MOS开关都可以但必须给它独立的电源供电建议用单独一路12V/5A以上的适配器和主控电源分别供电只在单点共地。另外雾化器工作的环境湿度大电路板要做好三防漆或装在密闭盒子中否则用不了多久继电器引脚和焊盘就会氧化发黑。4. 控制策略温度PID、湿度滞回、光照定时曲线的融合4.1 温度控制PID不能直接套公式温度控制是这个项目里最费心思的部分。网上的PID教程很多但直接套用教程代码大概率会出现两个问题积分饱和导致过冲微分项放大传感器噪声导致输出抖动。我的做法是位置式PID采样周期固定为1秒输出限幅到0~100%然后用时间比例方式控制PTC加热器比如输出30%就是在一个5秒的控制周期里加热器开1.5秒、关3.5秒。这样继电器或可控硅不会频繁通断加热也均匀。PID参数的经验起点参数值调整思路Kp150先只加P直到系统出现小幅振荡再减半Ki1.5慢慢加消除稳态偏差加太快会过冲Kd0温度传感器噪声大微分项弊大于利调参我强烈建议用串口把实时温度、目标温度和PID输出打出来做成曲线。没有曲线光看数字很难判断积分是不是过头了。后面我专门讲了怎么用串口加Python脚本画曲线这台植物生长箱的最终温控精度是±0.8℃。关于抗积分饱和我的实现很简单当PID输出已经达到上限100%时如果误差方向仍然要求继续增加输出就冻结积分项。这样当实际温度慢慢追上目标值时不会因为积分已经积累了一大截而导致过冲。4.2 湿度控制滞回控制比PID更实用湿度控制我开始也试了PID后来放弃了因为湿度系统有两个特点一是加湿器产生的水雾扩散需要时间传感器读数变化严重滞后二是湿度下降慢但加湿过冲很快PID很容易把加湿器开开关关。最终方案是滞回控制设定目标湿度60%RH带回差±5%RH。当湿度低于55%RH时启动加湿升至65%RH时关闭。滞回带避免继电器和雾化器频繁启停雾化器的工作寿命和用电安全都更有保障。加湿还有一个前馈技巧雾化器关闭后箱内雾气还会继续扩散湿度读数通常会继续上升1~2%RH。所以我把关闭阈值设为目标值5%RH而不是目标值实测中湿度基本稳定在目标值±4%RH内。4.3 光照模拟日出日落曲线比直接开关更聪明植物生长需要光照周期但突然开灯、突然关灯对植物来说是一种胁迫。我用STM32的TIM1 PWM输出控制LED亮度配合一个简单的定时器任务生成日出日落渐变曲线。实现逻辑设定每天6:00日出、18:00日落从6:00开始每隔5分钟亮度增加一个步长约1小时后达到目标亮度18:00后开始逐步降低亮度持续1小时后关闭。这样总共12小时光周期其中10小时是稳定光照。光照强度本身也可以做成一个闭环如果白天自然光透过窗户进入箱体BH1750读到光照已经足够就自动降低LED的PWM占空比节省电能。不过这个闭环要加一个限制LED亮度下限不低于20%避免植物光周期紊乱。4.4 多执行器优先级仲裁当控制目标冲突时必须有一个仲裁机制。我遇到过的最典型场景是补光灯需要全开但箱内温度已经接近上限。这时如果继续全开补光温度肯定超限如果直接关灯植物光照又不足。仲裁规则是温度安全 湿度下限 光照需求 CO2换气。具体实现中当温度超过上限时不直接切断补光灯而是把补光亮度降到最低档同时开启排风扇。如果温度继续上升再逐步降低补光亮度。这样系统不会剧烈震荡也不会让植物一下子处于全暗环境。实际运行一个月下来这套仲裁策略很稳定没有出现过温度越限导致设备保护性关停的情况。5. 人机交互与数据管理控制系统需要“看得见”5.1 OLED显示与菜单结构显示部分我用了最常见的0.96寸OLEDSSD1306驱动I2C接口。OLED本身刷新慢不适合频繁整屏刷新所以我把界面分成三个固定页面只在切换页面或数据变化超过阈值时才更新局部区域。三个页面是这样设计的状态页实时显示温度、湿度、光照强度、CO2等级、设备状态图标。参数页显示并修改温度目标值、湿度目标值、光照时段、PID参数。进入参数页后短按按键切换参数项长按进入编辑旋转编码器或按键调整数值。手动页强制开启/关闭加热器、加湿器、补光灯方便现场测试。如果你觉得OLED单色屏不够直观项目想走高级路线可以换带触摸的SPI彩屏这时候C8T6的Flash就不够了需要换大容量芯片并移植LVGL。LVGL在这个体量的项目里能做出一屏多窗、曲线显示、触摸菜单体验会好很多但开发量会翻倍。5.2 参数掉电保存用内部Flash模拟EEPROMPID参数、目标温湿度这些数据如果每次断电都要重新设置用户会崩溃。STM32F103没有内置EEPROM但有Flash可以在启动时从Flash读取参数修改时写入Flash。写Flash有两个必须注意的坑Flash写入前必须先擦除整个扇区F103是每页1KB擦除会使该页数据全部变0xFF。Flash擦写寿命在10万次以下频繁写入会损耗寿命所以不能每次修改参数都立刻写Flash应该先缓存等所有参数修改完后由用户按“保存”键统一写入。写入函数的核心流程解锁Flash、擦除目标页、按地址写入数据、重新上锁。我在Flash末尾专门划分了一页1KB用来存储参数结构体参数不多时足够用。启动时读出来的参数做合法性校验如果读出来全是0xFF或校验失败就用默认参数防止初始化跑飞。5.3 串口调试协议与上位机辅助调参调PID没有实时曲线几乎是盲调。我的项目里用USART1连ESP8266做远程监控同时用USART1的分时复用或者单独用USART2接USB转TTL和PC通信。串口接收不定长数据用到了“HAL库串口空闲中断DMA”的组合。传统按字节中断接收很简单但解析不定长协议时CPU占用高且容易漏数据。HAL库高版本提供了HAL_UARTEx_ReceiveToIdle_DMA()接口开启DMA接收空闲中断表示一帧数据结束这时把DMA缓冲区里的数据提取出来解析效率高很多。我自定义了一个非常简单的帧格式帧头(0xAA 0x55) 命令字(1字节) 数据长度(1字节) 数据 校验和(1字节)上位机我用Python写了不到100行的脚本读取串口传来的数据用matplotlib实时画温度/PID输出曲线。调PID时改一次参数保存到Flash观察曲线和稳态误差再微调。这个流程比看串口打印数字高效得多。6. 实测调试验证从PID震荡到稳定运行6.1 排查一ADC多通道采样数据错位现象OLED上显示的CO2浓度和土壤湿度值互相串了而且数值跳变严重。一开始我怀疑是传感器坏了但单独采集每个通道时读数又正常。排查过程先用万用表量MQ135模块输出端电压稳定在1.2V左右说明传感器本身没问题。检查CubeMX里ADC Channel配置确认Rank1是通道0、Rank2是通道1。在HAL_ADC_ConvCpltCallback里打印adc_buf[0]和adc_buf[1]发现adc_buf[0]对应的竟然是PA1引脚的电压。根因CubeMX生成代码时Rank顺序和DMA缓冲区顺序本身是关联的但我在手动初始化时中断了DMA请求重新配置后通道顺序没同步导致缓冲区数据顺序和物理通道错位。解决重新配置后用固定电压源分别接到PA0和PA1验证DMA缓冲区第0和第1个元素确认对应关系后再接传感器。建议每个项目都做一次通道标定不要想当然。6.2 排查二雾化器一启动MCU就重启现象手动开启加湿器OLED黑屏STM32自动复位。排查过程断开雾化器MCU运行正常加湿器一接上就复位怀疑是电源问题。用万用表测12V电源输出雾化器启动瞬间电压掉到8V以下3.3V稳压输出也被拉垮。单独给雾化器供电MCU不再复位。根因雾化器启动电流太大5A级别和MCU共用电源导致电压瞬降。这是典型的电源设计问题不是固件问题。解决雾化器独立12V/5A电源MCU系统3.3V由单独的低压差稳压器供给两个电源只在主地单点连接。控制信号通过PC817光耦隔离雾化器给主控的干扰被彻底切断。改造后测试连续启动雾化器几百次MCU再没复位过。6.3 排查三PID输出造成继电器频繁吸合现象温度稳定后用万用表监听继电器发现它每隔几秒就“咔哒”一次明显是在频繁切换。排查过程打印PID输出值发现输出在0%和3%之间反复跳动稳态偏差在0.1℃以内但输出不断反复。分析代码位置式PID的输出在误差接近零时积分项已经稳定但比例项会随着0.1℃的微小误差波动导致输出起伏。确认是输出死区缺失和微分噪声放大的问题。解决给控制输出加了2%的死区即PID输出小于2%时按0处理同时把微分项完全禁用。改进后温度维持在目标值附近继电器动作频率大幅降低基本稳定在几分钟才动作一次。6.4 24小时连续运行验证整机调试完成后我做了一次24小时连续运行测试记录若干关键时间点的数据时间温度(℃)湿度(%RH)光照(lx)备注08:0025.161.20升温段09:0026.360.512500日出完成12:0025.862.113200稳定段15:0026.259.812800稳定段18:3025.058.36200日落渐变21:0024.857.10夜间模式温度最大波动约±0.8℃湿度在目标值±4%RH范围内光照曲线平滑。整体功耗在补光全开时约45W夜间约5W对家用植物生长箱来说完全可以接受。最后的经验和扩展想法项目做到这个程度控制系统本身已经能稳定工作了。如果还想继续扩展我建议从三个方向入手一是接入ESP8266通过WiFi上报数据手机端就能看到箱内实时状态和曲线不用每次跑到设备旁边看OLED二是用K210这类带摄像头模组的芯片做植物叶片状态识别通过图像判断植物是否缺光、缺水或生病三是通过485总线扩展更多执行器比如自动施肥泵、多路补光灯和多个箱体的集中管理。整套系统做下来我最大的体会是“环境控制不等于PID万能”。PID解决的是单变量闭环问题但植物生长箱里的温度、湿度、光照、CO2彼此耦合真正让系统稳定的是架构层面的解耦设计和仲裁策略。传感器采集、执行器驱动、控制策略这三层的边界一旦划清楚后面的调试会轻松很多。最后再分享一个小技巧项目调试时养成保存参数版本的习惯每改一次PID参数就在Flash里存一个备份出问题能快速回退别问我怎么知道的。本文还有配套的精品资源点击获取