资讯动态

智慧大棚实战:STM32F103与ESP32-S3双芯片物联网监控系统

发布时间:2026/10/19 4:39:40 来源:尧图企业网站定制
做嵌入式开发这些年我越来越觉得“智慧大棚”这类项目是个特别适合认真练手的方向。它不大不小刚好把传感器采集、执行器控制、图形界面、无线通信和云平台交互这几条主流技能线全部串起来。今天要拆解的这个项目用STM32F103和ESP32-S3搭出一套能真正运行的大棚监控系统界面用LVGL绘制数据和命令在双芯片之间来回流动整套逻辑既不复杂到劝退也不简单到只是点个LED灯。你要是刚啃完单片机基础正琢磨怎么从裸机程序往物联网方向过渡这套项目的源码和调试思路值得完整过一遍。先说清楚这个系统解决的实际问题。大棚种植最怕的就是环境变化没被及时发现温度高没通风、土壤干了没浇水、光照不足没补光等肉眼看出来往往已经晚了。智慧大棚本质上就是把“人的感官和手动操作”变成“传感器的数据和机器的自动动作”整个过程分四步采集、决策、执行、反馈。STM32F103负责采集温湿度、土壤湿度、光照强度并根据设定规则去控制水泵、风机和补光灯ESP32-S3负责跑LVGL图形界面同时把数据通过WiFi传到云端接收远程指令再转给STM32。这样一个人坐在屋里甚至出差在外都能看到大棚实时状态并远程开设备。1. 项目整体设计与技术选型1.1 核心需求本地闭环优先远程监控为辅很多新手做物联网项目容易犯一个错误就是把所有逻辑都放在云端设备本地只做一个“遥控开关”。一旦断网整个系统瘫痪大棚这种场景还必须本地能独立运行。所以这个项目的第一个核心设计原则是本地自动控制闭环优先云平台只是观察和辅助下发的通道。大棚内的环境变化是连续的比如温度从25℃慢慢爬到35℃土壤湿度从80%慢慢掉到30%。如果完全依赖人来盯着不现实如果完全依赖云端判断网络抖动一次就可能导致水泵误动或风机不转。因此STM32F103必须自己完成数据采集、阈值判断、继电器控制这一套闭环。ESP32-S3在网络断开时照样通过LVGL显示数据、手动开关设备。网络恢复后自动重连不影响已经存在的本地控制逻辑。1.2 为什么是STM32F103加ESP32-S3而不是单芯片方案市面上有不少人用ESP32单独做智能家居采集传感器数据、输出PWM、接彩色屏幕看起来也能跑。但实际做农业项目时双芯片方案有明显优势。第一STM32F103在工业控制领域的稳定性经过了大量验证。它的ADC采样精度、定时器资源、GPIO数量、中断响应能力都很成熟用来做模拟量采集和继电器输出非常合适。ESP32虽然号称支持ADC但实际在多路模拟采集时精度和线性度不如STM32尤其要读取土壤湿度这种变化缓慢但干扰较大的模拟信号时STM32外设的稳定性优势就体现出来了。第二ESP32-S3本身是以连接和交互见长的芯片双核240MHz带WiFi和蓝牙又支持大容量PSRAM跑LVGL图形界面很合适。它的GPIO虽然也够用但如果你既要在上面接三个传感器又要接TFT屏还要做网络协议栈很容易出现引脚冲突或任务调度混乱。把感知和控制交给STM32把界面和网络交给ESP32-S3各自做自己擅长的事代码也不容易变成一坨乱麻。第三从项目学习的角度讲双芯片通信本身就是物联网系统里一个必须掌握的技能。无论是工业设备之间的MODBUS采集还是车联网里节点间数据交互底层都逃不开单片机之间通过串口、SPI或CAN交换数据。这个项目用UART把STM32和ESP32-S3连起来定义一套轻量数据帧协议这个过程能让你搞清楚“接口对接”是怎么回事远比单个芯片上写完所有代码有价值。1.3 LVGL在项目里的定位LVGL全称LittlevGL现在也叫LVGL是一个开源的嵌入式图形库专门为资源有限的MCU设计。这个项目里LVGL跑在ESP32-S3上负责渲染整个UI界面。LVGL的作用不只是“画个好看的表盘”它通过对象树、事件回调、样式系统和动画机制让嵌入式界面开发从传统“画点连线”变成了类似桌面端“搭控件”的体验。在项目主界面上时间、温度、湿度、土壤湿度、光照强度都是独立控件各自有自己的事件和刷新逻辑。后面我会详细讲怎么组织这些控件以及怎么避免频繁刷新导致卡顿。2. 硬件选型与系统架构2.1 系统整体架构整个系统分成数据采集层、控制层、网关显示层和云平台层。数据流方向是单向向上的传感器数据由STM32F103采集经解析处理后把环境数据和控制状态打包成协议帧通过串口发送给ESP32-S3。ESP32-S3接收后一方面更新LVGL界面控件另一方面通过MQTT协议发布到云平台。下行指令则反过来用户在LVGL触摸屏上点击或者通过手机/电脑云端操作命令发送到ESP32-S3后组装成控制帧再通过串口下发给STM32执行。这种架构的好处是数据流向非常清晰每一层只依赖下一层提供的接口出现问题可以逐层排查。你可以在STM32串口上直接观察数据帧在ESP32的日志里看解析结果在云平台后台看最终上报值任何一个环节出错都能定位到具体模块。2.2 传感器与执行器清单设备型号/类型接口用途供电温湿度传感器DHT22单总线采集空气温湿度3.3V土壤湿度传感器电容式土壤湿度模块ADC模拟量检测土壤水分含量3.3V/5V光照传感器BH1750I2C测量光照强度lx3.3V继电器模块5V/12V低电平触发GPIO控制水泵/风机/补光灯5V/12V显示屏幕2.4寸TFT SPI接口SPI运行LVGL界面3.3V通信模块ESP32-S3开发板UART/SPI/WiFi网关与图形界面3.3V/5V实际项目中DHT22虽然用起来简单但单总线协议对时序要求比较严格在有较长线路连接时容易受干扰所以一般把它尽量靠近STM32放置导线长度控制在20cm以内。土壤湿度传感器尽量选电容式的不要选裸露铜板的电阻式因为它长期埋在土里电解腐蚀很快读数漂移也严重。光照传感器用它来模拟大棚光照强度从而决策是否打开补光灯。2.3 关键接线与电源设计STM32F103C8T6开发板的资源分配是我实际调试过程中最终定下来的方案PA0土壤湿度传感器ADC输入PA1光敏电阻/光照传感器辅助ADC输入备用PB5DHT22数据引脚PB6/PB7I2C1接BH1750PA2/PA3UART2接ESP32-S3串口PA5/PA6/PA7继电器控制引脚分别控制水泵、风机、补光灯ESP32-S3这边的资源分配则是GPIO 10/11/12/13SPI接TFT液晶屏GPIO 14屏幕背光PWMGPIO 4/5触摸屏I2C或触摸引脚如果屏幕支持GPIO 15UART接收/发送接STM32F103GPIO 48板载LED状态指示电源方面建议采用外部12V适配器供电经过降压模块输出5V给STM32开发板和继电器模块再通过AMS1117-3.3输出3.3V给ESP32-S3、屏幕传感器。继电器是感性负载开关瞬间会产生比较大的电流冲击所以继电器模块的功率地与MCU控制信号之间要避免大环路干扰。我在实际项目里把继电器的地和传感器的地隔开中间用光耦隔离或者至少保证继电器模块与大功率设备之间有单独回地路径否则会经常出现系统随机重启。3. 软件实现STM32侧的采集与控制3.1 程序框架裸机状态机为主STM32F103这个项目我用的不是全套RTOS而是裸机加定时器轮询。原因很简单逻辑分支不复杂用RTOS反而增加排查难度。整体的主循环就是一个随时钟节拍运行的状态机每1秒执行一次传感器采集、控制判断、协议打包和发送。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_UART2_Init(); MX_I2C1_Init(); MX_ADC1_Init(); DHT22_Init(); Relay_Init(); while (1) { time_tick ; if (time_tick % 1000 0) // 每秒钟执行一次 { Read_Sensor_Data(); Run_Auto_Control(); Send_Data_To_ESP32(); } HAL_Delay(1); } }这种方式虽然朴素但排查起来非常直接。我可以随时在发送函数里断点看数据帧是否生成正确也可以在控制函数里加计数变量观察某一条控制路径是否被触发过。3.2 传感器数据采集与滤波传感器采集是整个系统最基础也最容易出错的部分。我实际遇到过的问题是DHT22偶尔返回一个温度50多度、湿度99%的异常值原因可能是采样间隔太短或者线路毛刺干扰。针对这个问题我做了三件事第一两次DHT22读取之间至少间隔2000ms避免传感器内部未准备好第二对DHT22读取结果做连续三次校验任何一次校验失败就丢弃本周期数据保留上次有效值第三对ADC采集的土壤湿度值做滑动窗口滤波取最近5次采样去掉最大值和最小值后的平均值。uint16_t Soil_Read_Filtered(void) { uint16_t buf[5]; for (int i 0; i 5; i) { buf[i] Read_Soil_ADC(); HAL_Delay(10); } // 求去掉最大最小的平均 int maxIdx 0, minIdx 0; for (int i 1; i 5; i) { if (buf[i] buf[maxIdx]) maxIdx i; if (buf[i] buf[minIdx]) minIdx i; } uint32_t sum 0; int cnt 0; for (int i 0; i 5; i) { if (i ! maxIdx i ! minIdx) { sum buf[i]; cnt; } } return (cnt 0) ? 0 : (uint16_t)(sum / cnt); }3.3 控制策略滞回控制防止继电器频繁抖动大棚控制如果只靠一个简单阈值比如土壤湿度低于40%就开泵高于40%就关泵继电器一定会频繁抖动因为土壤湿度值本来就在目标值附近波动任何一次滤波后的微小跳动都可能触发开关动作。频繁开关水泵和风机不仅费电继电器触点寿命也会快速衰减。我采用的方案是滞回控制。以土壤湿度为例设置一个目标下限30%和目标上限60%。低于30%启动水泵等土壤湿度上升到60%才停止。中间那一段区域属于“保持上次状态”的不动作区这样水泵一旦启动就会持续工作一段时间直到明显改善土壤状态才停止避免了在临界点反复跳闸。温度控制类似超过35℃启动风机低于32℃关闭风机。补光灯则是在光照低于2000lx且时间为白天时段才开启既避免晚上无人时补光浪费电也防止传感器偶发瞬时低光照导致误开。4. 软件实现ESP32-S3与LVGL界面4.1 LVGL移植与底层适配LVGL在ESP32-S3上的移植主要有两个关键点驱动接口和内存配置。屏幕驱动这里选的是SPI接口TFT屏我使用自己写的底层驱动的逻辑直接适配ILI9341芯片的SPI时序。初始化时需要配置SPI主机时钟速率、数据模式、屏幕宽高和偏移量。LVGL需要的底层接口其实就是两句将区域内的像素数据flush到屏幕以及读取触摸坐标如果有触摸屏。在ESP32-S3的IDF环境里我单独创建了一个lvgl_task任务主循环里调用lv_timer_handler()处理LVGL的各种事件和动画。LVGL自带软件定时器机制所有控件的刷新、动画、点击响应都靠这个函数周期性驱动。我把它放在一个独立任务里每5ms调用一次CPU占用率实测在10%左右不会影响WiFi和MQTT任务。4.2 LVGL内存配置LVGL在ESP32-S3上跑通常需要搭配PSRAM因为LVGL的控件、样式对象、以及绘图缓冲区都比较吃内存。我的屏幕分辨率是240x320色彩格式使用RGB565每个像素2字节整屏缓冲区需要240x320x2约153KB。我创建的LVGL显示缓冲区分成两块每块120x320的一半屏幕大小这样LVGL可以一边刷新一边从另一个缓冲区准备数据有效减少撕裂感。内存分配上LVGL的所有对象默认通过lv_mem_alloc获取如果不用PSRAM会很容易耗尽。我在lv_conf.h里把LV_MEM_SIZE设为64KB同时把LVGL对象池的分配函数重定向到PSRAM这样复杂页面和图表就不会因为内存不足而闪退。4.3 界面设计与数据刷新界面我设计了三个页面。主页面显示大棚关键参数温度、湿度、土壤湿度、光照强度每个参数用卡片形式展示卡片上有数值、单位和一个小图标。第二页是设备控制页上面有三个开关按钮分别控制水泵、风机和补光灯支持切换手动/自动模式。第三页是历史趋势页用LVGL的Chart控件动态绘制最近一小时温湿度变化曲线。页面的形态是项目验收时最容易出效果的部分但也是最容易做烂的部分。我刚开始把所有控件都堆在一个页面上刷新所有标签屏幕有明显的卡顿感和闪烁。后来按LVGL的刷新原理做优化每个参数单独维护一个标签指针只有数值变化超过阈值时才调用lv_label_set_text()偶数值不变时完全不刷新图表的曲线数据用环形缓冲循环写入每次只更新新数据点。static void lv_update_env_screen(float temp, float humi, uint16_t soil, uint16_t lux) { static float last_temp 999, last_humi 999; static uint16_t last_soil 0, last_lux 0; if (temp ! last_temp) { lv_label_set_text_fmt(label_temp, %.1f°C, temp); last_temp temp; } if (humi ! last_humi) { lv_label_set_text_fmt(label_humi, %.1f%%, humi); last_humi humi; } if (soil ! last_soil) { lv_label_set_text_fmt(label_soil, %d%%, soil); last_soil soil; } if (lux ! last_lux) { lv_label_set_text_fmt(label_lux, %dlx, lux); last_lux lux; } }这样做的效果立竿见影UI交互变得非常流畅因为LVGL本质上只需要关心数值变化了的控件而不是每帧重建整个界面。5. 双芯片通信与物联网数据链路5.1 STM32与ESP32-S3的串口通信协议双芯片之间的通信是整个项目的数据生命线协议设计好了后面所有问题都好排查。我用的协议是自定义的轻量二进制帧格式结构如下帧头0xAA 0x55用来同步识别一帧数据开始数据类型1字节标识当前帧是环境数据还是控制指令数据长度1字节标识有效载荷长度有效载荷数据内容具体格式由类型决定CRC校验1字节对前面内容做异或校验举个例子STM32向ESP32发送环境数据帧有效载荷顺序固定为温度整数、温度小数、湿度整数、湿度小数、土壤湿度、光照强度高字节、光照强度低字节、继电器状态、标志位。这样处理起来非常容易ESP32端拿到数据帧后不需要做复杂的字符串解析直接按字节位置取数就行。void Send_Env_Frame(uint8_t data_type, uint8_t *payload, uint8_t len) { uint8_t txbuf[20]; txbuf[0] 0xAA; txbuf[1] 0x55; txbuf[2] data_type; txbuf[3] len; for (int i 0; i len; i) txbuf[4 i] payload[i]; uint8_t crc 0; for (int i 2; i 4 len; i) crc ^ txbuf[i]; txbuf[4 len] crc; HAL_UART_Transmit(huart2, txbuf, 5 len, 100); }在ESP32端收到数据后先把UART字节流送入一个环形缓冲区然后通过状态机解析只有找到帧头和CRC校验通过的数据帧才认为是有效帧。这一套协议极大减少了串口丢包和错位造成的脏数据。5.2 MQTT上云与远程控制ESP32-S3的WiFi功能用来连接家里或者现场的无线路由器网络层配置好之后通过MQTT协议与云平台通信。这里选择MQTT是因为它轻量、成熟并且几乎所有物联网云平台都支持。我启动一个独立的mqtt_task负责建立连接、维持心跳、发布数据和接收订阅消息。发布主题我定义为/greenhouse/dev001/env负载使用JSON格式内容大致如下{ device_id: dev001, t: 26.5, h: 58.2, soil: 45, lux: 3200, fan: 1, pump: 0, light: 0, mode: 1, ts: 1712345600 }远程控制则通过订阅主题/greenhouse/dev001/cmd实现。云平台下发控制指令后ESP32在MQTT回调函数中解析JSON提取开关状态和设备编号组装成控制帧后通过UART发给STM32执行。整个过程要做好命令去重因为MQTT QoS1可能重复投递消息如果水泵命令被打到两次虽然状态还是开启但日志里会出现重复操作容易误判。5.3 本地控制与远程控制的优先级问题远程控制虽然方便但不能覆盖本地自动控制的优先级。我的设计是设备上有一个物理拨码开关或屏幕上的模式按钮切换“自动”和“手动”。处于自动模式时STM32完全根据传感器数据和控制策略执行继电器操作上位机和云端指令被忽略处于手动模式时才接受来自LVGL屏幕或云端的按钮指令。这个设计避免了一个常见坑你在手机上一次误触发了水泵恰好此时本地又要因为土壤干燥而关闭水泵两个指令冲突可能让水泵状态彻底混乱。本地闭环优先是最稳妥的策略。6. 排障实录我踩过的几个坑6.1 串口乱码和数据包丢失第一次把两个开发板用杜邦线连接打开串口调试助手看到的输出简直就是一团糟温度数据时而正常时而出乱码。排查下来发现原因有三个两块板子的供电地没有连在一起导致双方参考电压不一致波特率不匹配以及杜邦线太长发干扰。解决办法是先把两边的GND用一根粗导线直接连接然后统一把通信波特率设为115200再缩短杜邦线到10cm以内。协议里的CRC校验就是这时候加上去的作为一种兜底手段哪怕偶尔丢一帧下一帧也能自动恢复。6.2 LVGL动画卡顿与碎片化闪烁LVGL的动画默认是平滑过渡的但如果频繁切换页面或者大量更新曲线帧率会明显下降。后来我调整了LVGL的刷新机制把LV_DISP_DEF_REFR_PERIOD缩短同时让lv_timer_handler()每隔5ms运行一次。还有一个细节是使用双缓冲把绘制缓冲区和屏幕显示缓冲区错开肉眼可见的闪烁基本消失。6.3 土壤传感器读数漂移电容式土壤传感器虽然在长期工作寿命上比电阻式好但仍然会受土壤紧实度、温度和水质的影响。同一块地在刚浇完水时测得读数可能是85%第二天没浇水再测可能就变成55%。必须针对你实际使用的土壤类型做标定把传感器完全插入干燥土壤测一个值再完全泡在水里测一个值两者之间做线性映射转换成实际湿度百分比。如果不做这一步直接用ADC原始值去判断阈值控制精度会非常差。6.4 系统随机重启项目测试后期我遇到一个最头疼的毛病运行一两个小时后屏幕突然闪一下系统就自动重启了。起初怀疑是代码问题后来用示波器抓供电轨发现是继电器启动瞬间引起的电源电压跌落。水泵和风机启动电流很大当它们同时运行且电容量不足时系统电压跌到MCU的复位阈值以下于是重启。解决办法是给12V输入端并联一个1000uF大电容并在每个继电器电源引脚附近加一个100nF高频去耦电容同时把继电器负载的启动时间错开软件上延时1秒逐个启动避免电流尖峰叠加。7. 经验复盘与扩展方向7.1 这个项目里我最想强调的三点第一不要一开始就把系统分成“能跑”和“很稳”两个标准。很多Demo连一个传感器数据都读不准就急着搞界面后面调试成本只会越来越大。先把传感器读数校准、控制逻辑验证好再做UI和联网。第二协议和数据格式要提前定下来。哪怕第一版是简化的二进制协议也一定比临时写一个JSON串口协议好处理。因为只要通信双方对字节位置足够清楚所有调试工具都能帮你快速定位问题。第三好的系统架构一定是容易排查故障的。为这个项目加日志是我做过最划算的投资。在设备状态切换、串口帧收发、MQTT消息发布的关键节点都加串口日志任何异常发生后都能从日志时间线推断出问题点而不是凭感觉改代码。关于后续扩展这个项目的底子已经足够打好了。我可以把STM32端的传感器从两三个扩展到多路阵列比如增加土壤EC值、二氧化碳浓度监测ESP32端也可以加一个本地Web服务用浏览器直接打开设备IP看到同样的LVGL数据页面云平台端则可以做一个简单的大棚告警规则当温度超过设定值时推送通知到手机。整个项目就像一个中型农业物联网系统的微缩版框架不变扩展点非常清晰。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑