资讯动态

基于STM32F103ZET6的CAN总线温室大棚监控系统设计

发布时间:2026/9/16 7:58:04 来源:尧图企业网站定制
简介一份基于STM32F103ZET6的总线型温室大棚监控系统完整项目包面向嵌入式开发学习者、农业物联网爱好者及课程设计、毕业设计人群。系统以STM32F103ZET6作为数据采集与控制核心集成一氧化碳、空气/土壤温湿度、光照检测模块并配有光电开关实现人员进出判断照明、灌溉、通风模块通过继电器与风扇执行控制CAN总线负责监测节点与控制节点互联同时具备视频显示与监控功能覆盖环境感知到执行控制的完整链路。压缩包共608个文件包含大量C语言源码.c/.h、Keil工程配置.uvprojx/.uvoptx以及编译产物.axf/.hex另有PDF文档与说明文件整体大小约25.68MB目录结构清晰便于直接移植或二次开发。当前已有134人学习下载适合需要参考完整软硬件联调思路的开发者快速上手。1. 为什么总线型温室监控用 STM32F103ZET6 而不是 WiFi 从站在温室大棚里做环境监控最容易翻车的地方是通信链路。WiFi 和 LoRa 在空地上测试很漂亮一旦把传感器节点挂在金属棚架、卷膜电机附近信号衰减和丢包率就会让后台曲线变成断崖。这套总线型温室大棚监控系统给出的答案是用 STM32F103ZET6 做数据采集和控制芯片所有采集节点、控制节点、视频显示模块通过 CAN 总线连接形成一条可实时传输环境参数的现场总线网络。源码包里的工程已经实现了传感器采集、继电器控制、总线收发和文档说明直接改 CAN 报文 ID 就能用到自己的大棚或者农业物联网课题里。这里的关键不是 STM32F103ZET6 比别的芯片强多少而是它有硬件 CAN 控制器和外设中断能在不占用 CPU 连续轮询的情况下完成多帧收发。对于一氧化碳、温湿度、光照这类变化缓慢但偶尔需要急促告警的现场信号CAN 的优先级仲裁比 RS485 的轮询机制更灵活。下面从节点角色、外设初始化开始把整套工程拆开看。2. CAN 总线节点架构与 STM32F103ZET6 外设分配2.1 总线型方案里节点角色划分总线型系统里不要把所有逻辑都塞进主控STM32F103ZET6 更像一个协调者它既采集本地传感器也通过 CAN 总线接收远端节点数据。采集节点负责把一氧化碳传感器、空气/土壤温湿度传感器、光照传感器转为数字量控制节点根据总线上的指令驱动照明模块、灌溉继电器和通风风扇。视频显示模块也挂在同一根 CAN 总线上接收环境数据后在屏幕展示而监控模块的图像不走 CAN而是走独立模拟或数字接口避免大数据量挤占控制总线带宽。节点之间用屏蔽双绞线连接总线两端各放一个 120 欧终端电阻。每个节点上的 STM32 都用同一个波特率工程里默认配置为 250Kbps。这样做的最大好处是新增节点不用重新规划采样周期只要给新节点分配一个唯一的报文 ID主控就能在接收中断里区分它。源码目录里那组CAN_Try、CAN_1文件就是熟悉总线收发时留下的调试版本正式工程通常直接使用CAN相关文件。2.2 STM32F103ZET6 的 CAN 引脚配置与时钟STM32F103ZET6 上有两个 CAN 控制器CAN1 和 CAN2。这套工程使用的是 CAN1默认引脚为 PA11(CAN1_RX) 和 PA12(CAN1_TX)。如果你的板子上 PA11/PA12 被串口或者 USB 占用可以重映射到 PB8/PB9但需要调用GPIO_PinRemapConfig。建议优先使用默认引脚因为硬件资料和文档里的管脚定义都是按 PA11/PA12 来的。外设引脚类型说明CAN1_RXPA11上拉输入CAN 接收必须配置成输入模式CAN1_TXPA12复用推挽输出CAN 发送输出速度建议 50MHzUART1_TXPA9复用推挽输出调试打印UART1_RXPA10浮空输入调试串口ADC 输入PA0-PA3模拟输入一氧化碳、温湿度、光照采样CAN 外设时钟挂在 APB1 总线上默认 36MHz。APB1 的时钟分频如果不匹配波特率会完全错误。常见做法是系统时钟 72MHzAPB1 分频系数为 2所以 APB1 为 36MHz。工程里RCC_APB1PeriphClockCmd就是使能 CAN1 时钟不要漏掉 GPIO 和 AFIO 时钟。2.3 CAN 控制器初始化代码与参数说明拿到工程后先看bsp_can.c的初始化而不是直接改业务代码。下面这段是完整配置逻辑注释里给出了波特率计算方式void CAN1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; 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_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); CAN_InitStructure.CAN_Prescaler 9; CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_8tq; CAN_InitStructure.CAN_BS2 CAN_BS2_7tq; CAN_InitStructure.CAN_TTCM DISABLE; CAN_InitStructure.CAN_ABOM ENABLE; CAN_InitStructure.CAN_AWUM ENABLE; CAN_InitStructure.CAN_NART DISABLE; CAN_InitStructure.CAN_RFLM DISABLE; CAN_InitStructure.CAN_TXFP ENABLE; CAN_Init(CAN1, CAN_InitStructure); CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN1, CAN_FilterInitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel USB_HP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }波特率计算要按公式CAN_CLK / Prescaler / (1 BS1 BS2)。这里 CAN_CLK 是 36MHzPrescaler 为 9BS1 为 8BS2 为 7所以结果是36MHz / 9 / 16 250Kbps。SJW 不影响波特率只影响重同步补偿窗口。CAN_ABOM 置 ENABLE 很重要它能让总线在短路恢复后自动退出离线状态不用人工复位。CAN_TXFP 置 ENABLE 表示按发送请求顺序排队适合这种没有严格实时性要求的监控系统。CAN_FilterMaskIdHigh 0x0000和MaskIdLow 0x0000是接收所有报文。如果只需要接收特定 ID把 Mask 置 0xFFFF、ID 设为目标 ID 即可。工程里为了方便调试先这样配正式项目建议按 4.1 节协议收紧过滤条件。3. 传感器数据采集与执行机构控制代码实战3.1 一氧化碳与温湿度采集流程一氧化碳传感器通常输出 0~3V 模拟电压直接接到 STM32 的 ADC 通道。下面是 ADC 单次采样的通用代码ch对应 PA0-PA3 的通道号uint16_t ADC1_ReadValue(uint8_t ch) { ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_55Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); return ADC_GetConversionValue(ADC1); }ADC_SampleTime_55Cycles5是 55.5 个 ADC 时钟周期对于一氧化碳这类缓慢变化的信号足够。采样值要转成 PPM需要提前标定传感器零点电压和灵敏度这个数据一般写在传感器数据手册里。注意工程里 PA0 接了多个传感器的说法是错误的模拟量采集必须一个传感器一个通道或者用模拟开关切换。如果发现所有通道读数都跟着一路跳八成是 ADC 引脚配置成了复用推挽导致管脚间短路。空气/土壤温湿度传感器在工程里使用的是单总线协议类似 DHT11。读取一帧数据需要主机先拉低总线 18ms再释放然后等待传感器应答。代码可以放在dht11.c中注意时序必须是微秒级建议在关中断状态下读取uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; DHT11_Port_Output(); GPIO_ResetBits(DHT11_GPIO, DHT11_PIN); delay_ms(18); GPIO_SetBits(DHT11_GPIO, DHT11_PIN); delay_us(30); DHT11_Port_Input(); if (DHT11_ReadBit() 0) { // 等待应答结束 while (DHT11_ReadBit() 0); while (DHT11_ReadBit() 1); for (int i 0; i 5; i) { for (int j 0; j 8; j) { while (DHT11_ReadBit() 0); delay_us(40); buf[i] (buf[i] 1) | DHT11_ReadBit(); } } // 校验 if ((buf[0] buf[1] buf[2] buf[3]) buf[4]) { *humi buf[0]; *temp buf[2]; return 1; } } return 0; }3.2 光照报警、光电开关与照明控制逻辑光照传感器同样走 ADC但报警阈值不能写死在程序里设计上应该放在 EEPROM 或协议参数区。很多工程直接用一个#define LIGHT_ALARM_THRESHOLD 500虽然方便但后期需要重新编译。下面的逻辑把阈值放在全局变量里便于 CAN 远程修改uint8_t Light_Check(uint16_t current_light) { if (current_light light_alarm_threshold) { Light_Alarm_On(); LightControl(LIGHT_ON); return 1; } return 0; }光电开关的作用是通过接收反射光线判断是否有人进入大棚。它本质是一个开关量输入STM32 读取 PB5 电平即可。这里要特别注意输入上拉/下拉设置光电开关输出低电平有效时引脚配置为输入上拉输出高电平有效时配置为浮空或下拉。判断到人进入时除了打开照明往往还要延迟关闭避免人还没走出大棚灯就灭了。void Photoelectric_Check(void) { static uint32_t last_enter_time 0; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5) RESET) { last_enter_time GetTick(); Lighting_On(); } if ((GetTick() - last_enter_time) 30000) { // 30秒无触发关灯 Lighting_Off(); } }这个延时判断依赖系统 tick 计数不是delay_ms死等。如果要严格一点再加上光照条件判断白天有人进入也不开灯。工程里的视频显示模块和照明共用这个状态所以照明状态最好用一个全局结构体保存。3.3 灌溉继电器与通风风扇的控制时序灌溉通过继电器控制水泵通风通过风扇继电器控制卷膜或风机。继电器一般由 NPN 三极管或光耦驱动STM32 引脚输出高电平吸合。为了避免上电瞬间误动作初始化时要把继电器引脚先拉低再配置 GPIO 模式void Relay_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin RELAY_IRRIGATE | RELAY_FAN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_2MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_ResetBits(GPIOB, RELAY_IRRIGATE | RELAY_FAN); }灌溉时序不能只看土壤湿度一个点常见做法是加一个状态变量typedef enum { IRRIGATE_IDLE, IRRIGATE_WAIT_CONFIRM, IRRIGATE_RUNNING, IRRIGATE_STOP } IrrigateState; void Irrigate_Task(void) { switch (irrigate_state) { case IRRIGATE_WAIT_CONFIRM: if (Humidity_Soil HUMI_DRY_THRESHOLD) { irr_time_start GetTick(); irrigate_state IRRIGATE_RUNNING; } else { irrigate_state IRRIGATE_IDLE; } break; case IRRIGATE_RUNNING: Relay_Irrigate_On(); if ((GetTick() - irr_time_start) IRRIGATE_DURATION_MS) { Relay_Irrigate_Off(); irrigate_state IRRIGATE_STOP; } break; case IRRIGATE_STOP: // 等待下一次使能 break; } }通风风扇的控制逻辑相对简单温度超过阈值就开启低于阈值且延时一段时间后关闭避免风机频繁启停。控制频率要慢建议状态切换最小周期为 5 秒。如果接到的是变频器控制的卷膜电机就不能用继电器直接通断需要换成接触器或变频器控制端子。3.4 主循环状态机整合把传感器采集、报警、继电器控制放到一个循环里会互相卡延时。工程里采用的是时间片轮询 状态机主循环每 50ms 执行一次任务调度void Main_Task(void) { static uint32_t tick_counter 0; uint32_t now GetTick(); if ((now - tick_counter) 50) { tick_counter now; Sensor_Collect(); // ADC DHT11 Light_Check(GetLightValue()); Photoelectric_Check(); Irrigate_Task(); Fan_Task(); CAN_Task(); // 发送环境数据帧 } }CAN_Task并不是每 50ms 都发送内部根据发送周期和报文 ID 区分。例如环境数据 500ms 发送一次告警数据立即发送。这样总线不会拥塞也没延迟告警。真正需要快的数据只有人进入和烟雾报警其他都可以慢慢传。4. CAN 报文协议设计、丢帧排查与总线调试4.1 报文 ID 与数据字段规划CAN 报文不能像串口那样自定义帧头帧尾所有语义都靠 ID 和数据字节约定。工程里使用标准帧 11 位 ID。这里给出一个推荐分配表可以直接套用报文 ID方向数据长度内容说明0x100采集节点 - 主控8 字节一氧化碳(2)空气温度(2)空气湿度(2)光照(2)0x101主控 - 控制节点3 字节照明状态(1)灌溉继电器(1)风扇状态(1)0x102主控 - 显示节点8 字节温湿度数值和报警状态0x103光电开关节点 - 主控1 字节0x01 有人0x00 无人0x1FF广播告警2 字节类型 严重等级数据字段里如果数值超过 255尽量拆成两个字节低字节在前。例如一氧化碳浓度 1536 PPM就存0x00 0x06。这样上位机解析时不用做字节序转换。4.2 报文发送与接收过滤配置发送函数要检查 CAN 发送邮箱状态避免连续发送时因为邮箱满而丢帧。下面是带超时的发送代码uint8_t CAN1_SendData(uint32_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; TxMessage.StdId id; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC len; for (uint8_t i 0; i len; i) { TxMessage.Data[i] data[i]; } uint8_t mailBox 0; while (CAN_TransmitStatus(CAN1, mailBox) CAN_TxStatus_Failed) { mailBox; if (mailBox 3) { return 0; // 三个邮箱都在等待发送失败 } } CAN_Transmit(CAN1, TxMessage); return 1; }接收端使用 FIFO0 中断报文 ID 不同不需要在中断里做复杂拆包直接解析缓存void USB_HP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; CAN_Receive(CAN1, CAN_FIFO0, RxMessage); switch (RxMessage.StdId) { case 0x103: human_detect_flag RxMessage.Data[0]; break; case 0x1FF: alarm_type RxMessage.Data[0]; alarm_level RxMessage.Data[1]; break; default: break; } }这里要特别提醒CAN1_RX0_IRQHandler里不能做耗时操作。如果需要在收到控制指令后翻转继电器不要在中断里直接调用GPIO_SetBits而是设置一个 volatile 变量让主循环去消费。因为继电器吸合瞬间电压跌落会影响 CAN 收发器供电在中断里操作容易重入。4.3 用示波器和逻辑分析仪排查总线故障如果总线完全不通先查 CAN 收发器 TJA1050 的 RXD 引脚是否有波形。用示波器看 RXD 波形正常情况下应该能看到规则方波。看不到方波时按顺序排查量收发器 VCC 和 GNDTJA1050 的 5V 供电是否稳定。量 CANH 和 CANL 的静态电压默认都是 2.5V 左右。如果 CANH 和 CANL 对地短路波形会拉低。用万用表测总线两端电阻应该是 60 欧姆左右如果接近 120 欧姆说明只有一端接了终端电阻。检查 STM32 的 TX 引脚有没有输出没有输出则是初始化问题可能是时钟没开或者复用配置错误。逻辑分析仪配合 SocketCAN 可以抓完整报文。如果某个节点掉线优先看它的电源是否正常总线型系统 70% 的故障都是节点电源纹波过大导致收发器离线。给每个节点加一个 100uF 电容并且保持共地。5. 视频显示与监控模块接入技巧视频显示模块接收传感器数据并显示。主控把环境数据通过 CAN 发给显示节点显示节点再用串口驱动屏幕。这里有一个值得注意的细节CAN 数据里是二进制显示模块往往需要 ASCII 字符串。常见做法是在显示节点上做格式化转换而不是在主控里做。把数值转换放到显示端有利于主控节省 CPU也让 CAN 报文保持紧凑。显示节点里可以维护一个显示缓冲区用sprintf格式化char display_buf[64]; sprintf(display_buf, T:%d.%dC H:%d.%d%% CO:%dppm, temp_int, temp_dec, humi_int, humi_dec, co_ppm);然后通过串口或内部 SPI 发送给 LCD。注意sprintf在 STM32 上默认使用微小库时浮点格式%f可能不支持所以要先拆成整数和小数两个变量。这也是这个工程里为什么温湿度数据是分两个字节传输的原因。如果你想快速验证 CAN 收发链路不需要先接显示模块直接用 USB-CAN 适配器接在总线上PC 端用调试软件发报文。主控程序里的调试串口会打印接收到的 ID 和数据对比两边就能确认节点是否收到指令。另一个技巧是把CAN1_Init里的CAN_Mode改成CAN_Mode_LoopBack不上总线也能跑通整个逻辑适合在实验室里调试业务代码但实际部署前一定要改回CAN_Mode_Normal。监控模块的图像信号一般走模拟摄像头或者数字摄像头CAN 总线只负责传输控制指令比如云台转动、视频切换。如果摄像头和主控不共地接上后会烧坏 CAN 收发器务必在所有节点之间拉一条公用参考地然后只用屏蔽双绞线同时传输信号和地不要让屏蔽层单端接地。最后建议把源码包里那些CAN_Try、CAN_1调试文件备份后清理掉只保留正式工程和文档。分析代码时从stm32f10x_tim.c里的定时器配置入手可以很快找到传感器采样周期和继电器延时逻辑的实际来源。本文还有配套的精品资源点击获取

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

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

免费获取报价