STM32F103ZET6总线型温室大棚监控系统设计与实现
简介采用STM32F103ZET6微控制器的总线型温室大棚监控系统完整项目面向嵌入式开发学习者和农业物联网项目开发者帮助掌握多传感器数据采集、执行机构控制与CAN总线组网方法。系统集成一氧化碳、空气/土壤温湿度、光照检测及照明、灌溉、通风模块通过视频显示与监控实现环境可视化工作流程覆盖数据采集、判断决策与设备控制光照不足自动补光、湿度偏低启动灌溉、温度异常联动风扇微控制器根据传感器反馈自动驱动电机、风扇和LED装置软件逻辑清晰。压缩包共608个文件包含两百余个C语言源文件和头文件及工程配置、编译产物等以源码、STM32标准外设库文件、Keil工程文件为主配套PDF文档说明整体25.68MB便于直接参考或二次开发。已有134人学习下载适合电子竞赛、课程设计或毕业设计参考从底层驱动到应用层控制均可借鉴整体结构清晰可快速迁移至类似农业物联网场景。1. 总线型温室大棚监控系统为什么核心选STM32F103ZET6一个八十米长的大棚温度、湿度、光照、CO₂探头加上风机水泵接触器如果按点对点方式接线光布管和拉线就够线工忙两天。总线型方案把分散的采集节点和执行节点挂到同一条通信线上采集端到控制端只走一两对双绞线扩展节点时不用重新拉长线这就是“总线型温室大棚监控系统”这个标题要解决的核心问题用尽可能少的线缆把大棚里分布式的传感器数据汇拢上来再把控制指令分发下去。STM32F103ZET6在这一类方案里几乎是教科书级的选型144脚封装、5路USART、1路CAN、3个ADC和FSMC外部总线扩展接口既能应对多点传感器接入也能作为总线从站或主站承担协议处理。它能做到“一个芯片管一个节点”也能“一个芯片管整棚”两种角色都能落地。下面按“数据采集—总线通信—执行控制—现场联调”这条线把整套思路讲清楚适合做设施农业项目的嵌入式工程师也适合正在选型的学生团队参考。2. 总线型温室大棚监控系统的架构选型RS-485还是CAN总线2.1 为什么“总线型”才是大棚监控的合理解传统大棚监控最普遍的做法是每个传感器一路模拟量或开关量直接拉到控制柜模拟量用4-20mA还能传几百米但线缆成本和敷设工时跟着节点数线性上涨。传感器十来个时还能忍到三五十个节点接线端子排能占掉半个柜子查线更痛苦。总线型架构把现场设备按地址挂在一条总线上主站轮询或事件触发上报线缆固定成一根两芯屏蔽线节点增减只影响软件地址表不影响物理布线。大棚环境湿度大、电机启停频繁总线方案还能用隔离、终端电阻和差分传输抵抗共模干扰这是点对点TTL电平方案做不到的。常见做法是RS-485跑Modbus RTU也有用CAN总线跑自定义协议的。RS-485成本低、工具链成熟、串口资源多Modbus RTU报文结构容易调试CAN总线是差分双线、报文带CRC和仲裁机制抗干扰更强但需要CAN收发器和CANopen或自定义应用层。大棚里最干扰的是风机、卷帘电机启停瞬间的浪涌和接触器火花RS-485加TVS管和磁耦隔离在大多数场合够用如果现场有变频器且布线距离超过500米CAN总线的鲁棒性更值得付出那点成本。2.2 STM32F103ZET6的外设资源和“节点角色”的匹配STM32F103ZET6是STM32F103系列里的高引脚密度型号144脚LQFP封装工作频率72MHzFlash 512KBRAM 64KB。它内部有三个12位ADC共21个输入通道2个DAC5个USART/UART2个I2C3个SPI1个CAN 2.0B还有FSMC外部总线接口。这个外设组合对大棚监控来说几乎不用外扩芯片一片ZET6做中央控制器时FSMC可以挂一块TFT屏或者扩展RAM做采集从站时5路串口可以同时带多路UART传感器3个ADC覆盖模拟量探头。芯片内部的总线结构对排障是有实际影响的。AHB总线把Flash、SRAM、DMA和FSMC连在一起APB2上挂住ADC、USART1和高级定时器APB1上挂住USART2-5、CAN和I2C。USART1时钟是72MHzUSART2-5时钟是36MHz如果某路串口波特率不对先查时钟树多半是把APB1外设直接按72MHz算了。对总线型系统每路串口最好都配对应的DMA通道否则主站轮询时CPU被中断打满采集时间戳和协议超时互相干扰。关于角色分配我一般这样切角色选型任务中央控制节点STM32F103ZET6 RS-485主站轮询从站、数据融合、执行控制、本地显示采集从站STM32F103C8T6 或同系列接传感器收到读指令后上传寄存器执行节点STM32F103ZET6 继电器板接收开关/设定值反馈状态ZET6做中央节点最大的好处是余量大采集点位从32路扩展到128路只是扩大寄存器表和轮询表芯片资源不用动。如果做从站最小系统的C8T6就够成本能压到十元左右但标题既然指定ZET6本文按“中央节点用ZET6、从站根据点位规模选同系列”来展开。2.3 总线协议Modbus RTU还是CAN自定义协议选择决定了源码里最核心的报文处理部分。工业大棚项目里Modbus RTU是默认选项报文格式固定主站一帧是8位地址加8位功能码加N字节数据加16位CRC现场用USB转485工具就能抓流不必上总线分析仪。CAN总线则要处理11位标识符、DLC、数据段和CRC报文本身有硬件错误检测但应用层还要自己定义对象字典开发和排障门槛高不少。我的建议是单棚、从站少于32个、现场无强变频干扰用RS-485加Modbus RTU连栋大棚、跨栋距离长、有大功率变频设备的用CAN总线并在每个节点加DC-DC隔离。后面章节的采集和控制逻辑不依赖具体总线通信层换成CAN时只改驱动。这也是总线型监控系统里常见的“通信层可替换”设计我一般会把RS-485收发切换函数、CRC计算和帧解析独立成模块就是为替换留的接口。3. STM32F103ZET6的数据采集实现从传感器到总线寄存器3.1 大棚传感器接入方式与信号调理大棚监控最常见的数据采集对象是空气温湿度、土壤湿度、光照强度、CO₂浓度和风速。这些传感器输出接口各不一样接入STM32F103ZET6时按三类处理数字单总线DHT22、I2C数字输出SHT30、BH1750、模拟4-20mA或0-10V工业级温湿度变送器、CO₂变送器。数字传感器直接对IO或I2C外设模拟传感器要经过信号调理进ADC引脚。4-20mA变送器是总线型大棚项目里最该优先选的传感器类型电流环抗衰减、抗干扰强两线制接线也简单。ZET6的ADC只能采电压需要把4-20mA转成1-5V或0-3.3V常见做法是用精密电阻取样再用运放跟随和钳位二极管保护ADC引脚。注意ZET6的ADC输入范围是0到VREFVREF默认接到VDDA即3.3V如果直接用250Ω电阻取样20mA时压降5V会超量程所以要改成150Ω电阻20mA时压降3V或者前级先分压再进ADC。3.2 用DMA循环模式采集多路ADCZET6的ADC1支持扫描模式搭配DMA循环传输一次配置就能持续把多路通道的转换结果搬进内存数组CPU不用进中断数据采集的实时性和确定性都好很多。配置代码大致如下#define ADC_CH_NUM 8 uint16_t adc_buf[ADC_CH_NUM]; void ADC1_Init_DMA(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA0-PA7 作为8路模拟输入 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_Init(GPIOA, GPIO_InitStructure); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize ADC_CH_NUM; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; // 扫描模式 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel ADC_CH_NUM; ADC_Init(ADC1, ADC_InitStructure); // 通道0-7采样时间统一设为55.5周期降低信号源阻抗影响 for (uint8_t ch 0; ch ADC_CH_NUM; ch) { ADC_RegularChannelConfig(ADC1, ch, ch 1, ADC_SampleTime_55Cycles5); } ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发连续转换 DMA_Cmd(DMA1_Channel1, ENABLE); }这段代码做了四件事把PA0到PA7配置成模拟输入ADC1配置成8通道扫描连续转换DMA循环把ADC数据寄存器搬到adc_buf数组最后做ADC校准并启动。DMA_Mode_Circular是这里的重点它让DMA在搬运完8个半字后自动回到起始地址继续搬adc_buf里永远是最近一轮的采样值。采样时间选55.5周期是为了适配大棚变送器输出信号内阻较高的情况内阻大而采样时间太短会采出衰减值。读取时直接用adc_buf就行但要注意转换值和工程量之间的换算。12位ADC满量程对应3.3V假设4-20mA经150Ω取样电阻变成0.6到3.0V对应的ADC采样值范围大约是744到3723采集代码里可以直接做线性映射再转成物理量。大棚数据采集不建议在这个环节做复杂滤波因为总线主站侧还要做数据融合从站保留原始转换值反而更容易定位温漂问题。提示ADC输入引脚在不使用时不要悬空建议通过10kΩ电阻接地或接VREF否则浮空脚上的耦合噪声会体现在相邻通道的采样值里。3.3 串口DMA加空闲中断实现Modbus RTU从站帧接收总线型系统中STM32F103ZET6若作为从站串口要同时兼顾“收完整帧”和“不阻塞主循环”。最稳的做法是UART空闲中断加DMA接收DMA把收到的字节源源不断搬进缓冲硬件空闲中断在总线静默时触发软件再判断帧间隔和长度。Modbus RTU规定帧间隔至少3.5个字符时间所以收到完整帧后必须做CRC校验和地址匹配。#define RX_BUF_SIZE 128 uint8_t modbus_rx[RX_BUF_SIZE]; void USART1_DMA_RX_Init(uint32_t baud) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; DMA_InitTypeDef DMA_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; // TX GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; // RX GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate baud; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, USART_InitStructure); DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)modbus_rx; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize RX_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel5, DMA_InitStructure); DMA_Cmd(DMA1_Channel5, ENABLE); USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); USART_Cmd(USART1, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }这段初始化的关键参数是波特率和DMA缓冲区大小。波特率决定3.5字符时间间隔9600波特率下1字符约1.04ms3.5字符约3.65ms115200下只剩约0.3ms这是部分现场用高波特率导致Modbus帧被拆包的原因。大棚总线型监控建议默认9600或19200既满足数据量要求又对线缆质量宽容。DMA_BufferSize设128字节Modbus RTU标准支持到256字节但大棚寄存器数量有限128字节足够容纳一帧完整请求。DMA模式用Normal而不是Circular因为每次收到完整一帧后要重新加载缓冲区才能处理下一帧。空闲中断服务函数里做三件事关DMA、用RX_BUF_SIZE减DMA_GetCurrDataCounter(DMA1_Channel5)算出实际字节数、重新设置DMA_BufferSize并启动再置一个帧完成标志让主循环去解析。注意收到错误帧时不能只清标志还要清串口的ORE错误标志否则串口过载错误会卡住后续所有接收。4. 执行器控制与总线的调度策略从继电器到轮询4.1 大棚执行器和驱动电路设计大棚控制对象主要就是风机、水泵、卷帘电机、侧窗电机、补光灯和电磁阀。ZET6的GPIO输出能力只有约20mA不能直接驱动任何执行器常见做法是GPIO经过光耦隔离后驱动继电器或接触器小功率用继电器模块大功率用接触器。驱动电路里光耦前级串限流电阻光耦输出侧接NPN三极管或达林顿管驱动继电器线圈继电器线圈两端必须反向并联续流二极管否则关断瞬间的感性反电动势会打穿三极管。执行节点和被控设备之间的隔离要和总线通信隔离统一考虑。总线侧用隔离RS-485收发器把通信地和电源地隔开执行侧用光耦再隔一次这样电机侧的地线浪涌不会窜进STM32F103ZET6的GND。如果不做隔离雷击和接触器火花轻则复位重则烧芯片这是大棚项目里返工率最高的问题之一。4.2 阈值控制和增量式PID控温的最低实现大棚控制策略通常分两层简单阈值和PID。风机水泵用阈值就够了温度高于上限开风机低于下限关土壤湿度低于下限开滴灌。阈值控制的死区必须加否则传感器在临界值附近抖动会让继电器频繁启停两三天就能磨坏触点。比如控制上限28度下限设26度中间2度死区代码里的比较逻辑就是温度大于28度开风机小于26度关风机26到28度之间保持现状。卷帘或风机调速需要PID时增量式PID更适合单片机因为输出是增量不会出现积分饱和导致的大超调。典型实现在定时任务里执行typedef struct { float kp, ki, kd; float err_last, err_prev; float out; } PID_Inc; float PID_Inc_Calc(PID_Inc *pid, float setpoint, float measure) { float err setpoint - measure; float delta pid-kp * (err - pid-err_last) pid-ki * err pid-kd * (err - 2.0f * pid-err_last pid-err_prev); pid-err_prev pid-err_last; pid-err_last err; pid-out delta; if (pid-out 100.0f) pid-out 100.0f; // 输出限幅 if (pid-out 0.0f) pid-out 0.0f; return pid-out; }三个参数的作用kp放大当前偏差偏差越大输出增量越大ki消除稳态误差但也最容易引起振荡大棚温度对象惯性大ki要给得很小kd抑制偏差变化趋势对滞后对象有预测作用但传感器噪声大时必须先滤波再进PID。大棚温度控制建议从kp10、ki0.05、kd2这样的初值开始调按“先kp到临界振荡再减半加ki最后加kd”的流程整定不要直接抄网上温控器的参数对象不一样照样振荡。4.3 主站轮询调度与总线冲突处理总线型监控系统的主从式调度是最简单的可靠方案主站周期轮询各从站地址从站只在被点名时回复。轮询表用结构体数组管理一个节点包含地址、超时计数、在线状态和功能码typedef struct { uint8_t addr; uint8_t func; uint8_t online; uint16_t timeout_cnt; } PollNode; PollNode poll_table[8] { {0x01, 0x03, 0, 0}, {0x02, 0x03, 0, 0}, {0x03, 0x03, 0, 0}, {0x04, 0x03, 0, 0}, {0x05, 0x04, 0, 0}, {0x06, 0x04, 0, 0}, {0x07, 0x04, 0, 0}, {0x08, 0x04, 0, 0} };主流程在一个10ms定时中断里维护时间片主循环里的状态机按时间片发送查询帧。发送一帧后进入等待应答状态50ms内没收到就timeout_cnt加1连续3次超时把online置0并在报警寄存器里置位。注意轮询周期必须大于“发送时间加最大响应时间加保护间隔”比如8个节点、每节点50ms预算一轮就是400ms这对大棚数据来说完全够用。CAN总线方案里需要算负载率公式是每秒实际发送位数除以总线波特率再乘100%。一样的节点规模8个从站每200ms上报一次、每帧约110位负载率很低留出的余量可以给事件上报和紧急报文。RS-485侧不用算负载率但要控制广播帧数量广播帧一多多个从站同时回应的冲突就会打乱总线。执行器控制指令建议用功能码0x05写单个线圈或0x10写多个寄存器不要在采集轮询帧里顺便写否则控制指令和采集帧互相放大延迟。急停按钮这类安全逻辑直接进ZET6的外部中断不走总线这是总线型大棚监控系统接线时最容易被忽略的安全底线。5. 现场联调技巧从总线波形到上位机卡顿5.1 先用“USB转485工具加ModbusPoll”验证链路再上STM32F103ZET6联调顺序应是从简到繁。在STM32F103ZET6还没烧程序之前用USB转485工具直接连总线PC上跑ModbusPoll以主站身份去轮询一个已知从站设备先确认物理链路和终端电阻没问题。如果从站是ZET6先在串口调试工具里手动发一帧“01 03 00 00 00 02 C4 0B”看它回不回数据。判断CRC对不对最简单的方法是看返回帧能不能被ModbusPoll解析解析不出来先对照CRC16-Modbus算法很多“总线不通”其实是CRC算错。5.2 C#上位机的数据采集和UI刷新解耦上位机如果用C#写最常见的坑是循环数据采集和UI刷新卡顿。串口DataReceived事件里直接改界面控件波特率一高或总线节点一多界面就卡死。正确做法是把总线轮询放后台线程收到的数据推到并发队列UI线程用定时器每200ms拉一次批量刷新。这类监控系统的采集和展示本来就该解耦历史数据落库界面只读最近快照大棚监控屏不需要毫秒级刷新200ms到500ms的刷新周期足够还能避免界面闪烁。5.3 现场总线排障三板斧总线型监控现场不出数据时按三步排查第一步拿万用表量AB两线之间的静态电压RS-485空闲时应在1.5V到5V之间接近0V说明总线被拉死或收发器损坏第二步用示波器看A、B对地波形正常发送时应看到差分电压翻转只有单端有波形说明有一根线断了第三步量终端电阻总线两端各并联120Ω断电后在任一端量到约60Ω说明匹配正确超过100Ω就检查接线端子。最后留一个细节RS-485的A/B线序在不同厂家设备上定义并不统一接线时先查手册确认A是反相端还是同相端接反的表现是通信偶发成功、长时间不稳定这种故障最耗时间。把A/B标签贴到每个总线端子旁边能省掉后面所有维护的人查图纸的功夫。本文还有配套的精品资源点击获取