ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于STM32与CAN总线的粮仓烘干监测控制系统设计

基于STM32与CAN总线的粮仓烘干监测控制系统设计 简介这套基于STM32的粮食烘干系统源码与说明资料主要面向物联网、嵌入式方向的开发者及农业自动化项目学习者可用于实现粮仓内粮堆多点温度、空气温湿度在线检测以及数据中继传输、排风扇控制、节点故障诊断和超限报警等人机交互功能。压缩包共257个文件容量约12.97MB内容以STM32的C语言源文件43个.c、头文件45个.h及Keil工程配置为主同时包含APK安装包、Android端Java/Gradle工程、图片与说明文档等辅助材料既能支撑单片机端开发也覆盖上位机端参考。已有218人学习/下载比较适合需要完整项目案例进行课设、竞赛或工程仿真的初中级开发者。通过源码可以梳理传感器采集、数据汇聚、节点控制与告警反馈的整体逻辑工程文件和目录结构清楚结合说明资料可快速完成环境搭建、烧录验证与基于自身需求的二次开发。1. 从凭手感到按数据一套STM32粮仓烘干系统的拆解粮仓烘干这个场景其实很考验嵌入式功底。粮堆内部的热点不会均匀出现空气湿度随昼夜波动排风扇启动早了浪费电晚了粮堆内部就结露发霉。网上很多方案只做单点测温或者用模拟电路滞回控制可真正要落地还是得用数字总线把分布在各处的传感器接起来再靠单片机统一调度。这个项目就是基于STM32F10x系列的粮仓烘干监测控制系统它把多点温度、多点空气温湿度、排风扇控制和故障诊断全部集成在一个完整的源码包里适合做毕设、农业物联网改造也是想熟悉标准外设库开发的人一个不错的参考。下面我会按架构、驱动、控制、交互和调试几个层面来讲代码也能直接在上面改。2. 粮仓分布式节点架构与CAN总线协议设计这套系统最值得先看的部分是它的分布式节点划分。粮仓面积动辄几十上百平米如果只用一块板子拉线去量所有传感器布线和信号衰减都是麻烦事。所以项目把节点分成三类采集节点负责读粮堆温度和空气温湿度控制节点管理排风扇启停和转速管理节点汇集数据、做人机交互和上传报警。每个节点都用STM32F103最小系统差别只在外围电路和固件。节点之间用CAN总线连接波特率设在500kbps单段干线几十米没有问题而且CAN差分信号本身抗共模干扰强比RS485在潮湿高温环境里更稳。传感器选型上粮堆内部温度用的是DS18B20因为单总线一根线就能挂多个传感器每个芯片有唯一64位ID挂32个都不需要片选。粮仓空气温湿度用的是SHT20I2C接口湿度精度±3%RH温度精度±0.3℃满足烘干过程监控需求。如果预算紧换DHT11/DHT22也行只是底层时序驱动和精度不同。控制节点则通过继电器驱动接触器去控制排风扇的通断如果风机支持变频器还可以把GPIO换成PWM输出用TIM模块产生0100%占空比这样做缓启动和恒温控制更平滑。数据中继的机制也很明确采集节点每2秒采样一次通过CAN发一帧数据管理节点作为CAN总线上的主监听者收到所有节点数据后做缓存和判断当管理节点需要上一级网络比如服务器时再通过串口转WiFi模块上传。这种设计让边缘节点不需要关心上位机在哪整个数据流是单向采集、双向控制的。CAN报文协议按标准帧2.0A设计ID用来区分报文类型和节点地址。实际项目里我常用的分配表如下CAN ID报文类型数据字段0x101粮堆温度Byte0节点IDByte1-2温度值有符号16位单位0.1℃Byte3传感器状态0x102空气温湿度Byte0节点IDByte1-2湿度值单位0.1%RHByte3-4温度值Byte5传感器状态0x201排风扇控制Byte0目标节点IDByte1开/关Byte2占空比0-1000x301心跳包Byte0节点IDByte1节点类型Byte2工作状态标志CAN接收端解析帧时别直接用if去匹配ID那样报文多了之后维护性很差。我一般用一个结构体数组建立ID到回调函数的映射表代码看起来更干净typedef struct { uint16_t id; void (*handler)(uint8_t *data, uint8_t len); } can_handler_t; static void on_temperature(uint8_t *data, uint8_t len) { int16_t temp (data[1] 8) | data[2]; if (temp 0x1FFF) temp - 0x4000; // 符号扩展 store_temp_to_ring_buf(data[0], temp); } static void on_humidity(uint8_t *data, uint8_t len) { uint16_t hum (data[1] 8) | data[2]; store_hum_to_ring_buf(data[0], hum); } static const can_handler_t can_handlers[] { {0x101, on_temperature}, {0x102, on_humidity}, }; void CAN0_RX0_IRQHandler(void) { CanRxMsg msg; CAN_Receive(CAN0, CAN_FIFO0, msg); for (uint8_t i 0; i sizeof(can_handlers)/sizeof(can_handlers[0]); i) { if (can_handlers[i].id msg.StdId) { can_handlers[i].handler(msg.Data, msg.DLC); break; } } }这里把温度数据的高位按16位有符号处理实际上是先用int16_t接收再做边界判断。因为温度值允许到-20℃普通的uint8_t放不下。映射表的好处是新增报文类型时只需要在数组里加一行函数指针不需要改中断函数本体后续做远程升级或扩展保温控制节点也方便。3. STM32外设驱动ADC采集、I2C温湿度读取与CAN收发这一章是源码包里最硬核的部分。STM32F103的标准外设库把寄存器封装得比较薄适合看明白底层流程。先讲时钟配置项目里用的是8MHz外部晶振通过PLL倍频到72MHz。stm32f10x_rcc.c负责把GPIO、ADC、I2C、CAN外设的时钟打开。注意CAN外设挂载在APB1总线上时钟频率是36MHz所以CAN初始化时要设置CAN_Prescaler来从36MHz得到500kHz的波特率我的参数是Prescaler4BS17BS26具体数值要和总线时钟匹配不能照搬网上72MHz加Prescaler9的那套。ADC部分用于测量热电偶或PT100调理后的模拟电压。以3.3V参考电压12位分辨率下1个LSB对应约0.8mV。如果PT100经过电桥放大100倍0.1℃的温度变化对应约0.4mV的电压变化刚好在分辨率边缘。所以硬件上放大倍数要合适软件层还要做一阶低通滤波。下面是ADC的DMA采集代码void ADC_DMA_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1|RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, gpio); DMA_InitTypeDef dma; dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dma.DMA_MemoryBaseAddr (uint32_t)adc_buf; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize 1; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Disable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; dma.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel1, dma); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_InitTypeDef adc; adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; adc.ADC_ContinuousConvMode ENABLE; adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 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); }需要注意的是ADC校准前要等上次校准状态位清除否则第一次转换结果可能不准确。DMA循环模式会自动搬数据CPU只需要读adc_buf[0]。实际工程里我在while(1)循环中用一个volatile uint16_t变量读取再和上一次值做滑动平均这样可以滤掉工频干扰。I2C部分读SHT20标准外设库的I2C时序坑比较多。SHT20的I2C地址为0x40读温湿度要先写入触发命令0xF3测量温度0xF5测量湿度。如果直接用库函数轮询等待测量完成会造成CPU空转所以我用超时退出的方式uint8_t SHT20_ReadHumidity(uint16_t *humidity) { uint8_t buf[2]; if (I2C_WriteByte(I2C1, 0x401, 0xF5) 0) return 1; delay_ms(50); // 等待内部测量14bit分辨率典型耗时29ms if (I2C_ReadBytes(I2C1, 0x401, buf, 2) 0) return 1; uint16_t raw (buf[0] 8) | buf[1]; // 去除状态位只保留14bit有效数据 raw 0xFFFC; *humidity (uint16_t)((125.0 * raw / 65536.0) * 10); // 单位0.1%RH return 0; }这里做了一步关键处理SHT20返回的原始数据低两位是状态位必须清零后再换算。125.0 * raw / 65536.0得到百分比再乘10是为了在整型变量里保留一位小数。如果你用的是DHT22就不能用I2C库了得改用GPIO模拟时序。很多新手把I2C驱动写成死等ACK一旦传感器没插好就卡死整个系统因此在I2C读写函数里我加了超时计数器超过1000次都收不到应答就直接返回失败这样诊断代码才能正常上报传感器故障。CAN发送相对简单但要注意发送邮箱的等待时间。标准库的CAN_Transmit检查邮箱空位发送完成标志位要轮询。如果总线上只有两个节点且没有接终端电阻会导致应答ACK失败发送函数一直返回pending所以我在发送前检查CAN_TransmitStatus连续三次失败就主动清空请求并置一个错误标志避免阻塞主循环uint8_t CAN_SendFrame(uint16_t id, uint8_t *data, uint8_t len) { CanTxMsg msg; msg.StdId id; msg.IDE CAN_Id_Standard; msg.RTR CAN_RTR_Data; msg.DLC len; for (uint8_t i 0; i len; i) msg.Data[i] data[i]; uint8_t mailbox CAN_Transmit(CAN0, msg); for (uint16_t t 0; t 0xFFF; t) { if (CAN_TransmitStatus(CAN0, mailbox) CAN_TxStatus_Failed) break; if (CAN_TransmitStatus(CAN0, mailbox) CAN_TxStatus_Ok) return 0; } CAN_CancelTransmit(CAN0, mailbox); return 1; }4. 排风扇滞回控制与传感器故障诊断实现烘干过程中温度自动控制并不是简单超过阈值就开风扇。如果只在60℃开、59℃关排风扇会频繁启停接触器寿命很快耗尽。项目里用的是滞回控制设置上限阈值T_high和下限阈值T_low当温度超过T_high时开启排风扇之后要温度低于T_low才关闭这样开关之间留有2~3℃的回差。同理湿度控制比如高于75%RH启动低于70%RH停止。滞回量要写在Flash里方便现场通过人机交互面板修改。控制节点的核心逻辑如下void fan_control_task(void) { uint16_t avg_temp get_avg_temp(); // 取所有粮堆温度的平均值 uint16_t avg_hum get_avg_humidity(); if (fan_state FAN_OFF) { if (avg_temp threshold.fan_temp_high || avg_hum threshold.fan_hum_high) { fan_state FAN_ON; GPIO_SetBits(GPIOB, GPIO_Pin_5); // 开启继电器 set_fan_speed(threshold.fan_duty); // PWM占空比 } } else if (fan_state FAN_ON) { if (avg_temp threshold.fan_temp_low avg_hum threshold.fan_hum_low) { fan_state FAN_OFF; GPIO_ResetBits(GPIOB, GPIO_Pin_5); set_fan_speed(0); } } }注意get_avg_temp()要对所有采集节点的温度做去极值平均。比如有6个粮堆测温点先去掉最大和最小值再对剩下4个求和求平均。这样能避免某个传感器被粮食压到、测出异常高值导致排风扇误动作。故障诊断分为三个层级。第一是传感器诊断DS18B20单总线如果读不到应答或者SHT20的Chip ID读出来不对就标记该通道故障不在平均计算中包含它。第二是节点通信诊断管理节点为每个采集节点维护一个心跳计数器如果2秒一个心跳包连续5个周期没收到就认为该节点离线在人机界面显示节点X通信中断。第三是执行器诊断控制节点驱动排风扇后反馈回路里接一个电流互感器检测电流是否超过设定值如果控制信号为开但电流为0则上报排风扇故障。诊断对象判断条件处理动作恢复方式DS18B20单总线应答超时该通道数据置0xFF告警手动插拔后自检SHT20I2C NACK或CRC错误节点数据保持上一有效值下个周期重试CAN节点5个心跳周期未收到从采样列表移除LCD显示离线节点重新上线排风扇PWM输出但电流互感器无信号记录故障码关闭PWM需人工复位诊断结果要能上报到管理节点所以在心跳包里加了一位工作状态标志。0x301报文的Byte2按位定义bit0表示传感器正常bit1表示I2C总线正常bit2表示Flash读写正常bit3以上保留。管理节点收到心跳包后把状态数据更新到UI界面。实时性方面烘干过程不是快变量所以主循环控制周期设为200ms足够。但看门狗不能依赖主循环喂狗我是在定时器中断里把喂狗变量置1主循环检查到该变量再清除看门狗计数器。这样才能防止程序跑飞时主循环卡死但中断还在执行的假活现象。5. 人机交互界面、按键菜单与报警机制人机交互节点上接了一块2.8寸TFT LCD或0.96寸OLED显示四个区域实时温度曲线、空气温湿度、排风扇状态、当前阈值。由于这个项目源码包里带了一个Grain drying system.apk说明管理节点也可以把数据通过蓝牙模块发给Android手机手机端做更图表化的展示。但无论屏幕还是手机底层交互逻辑都离不开按键设置和报警事件。按键菜单我用了一个简单的状态机三个按键确认、加、减。菜单分三层主界面、设置温度上限、设置温度下限、设置湿度上下限、设置PWM占空比。状态机切换逻辑如下typedef enum { MENU_MAIN, MENU_SET_TEMP_HIGH, MENU_SET_TEMP_LOW, MENU_SET_HUM_HIGH, MENU_SET_HUM_LOW, MENU_SET_DUTY } menu_state_t; void menu_handle(void) { static menu_state_t state MENU_MAIN; uint8_t key key_scan(); switch (state) { case MENU_MAIN: if (key KEY_CONFIRM) state MENU_SET_TEMP_HIGH; break; case MENU_SET_TEMP_HIGH: if (key KEY_PLUS) threshold.fan_temp_high; if (key KEY_MINUS) threshold.fan_temp_high--; if (key KEY_CONFIRM) { state MENU_SET_TEMP_LOW; Flash_WriteThreshold(threshold); } break; // 其他状态省略 } }每次修改阈值后立即写进Flash。我用STM32内部Flash的最后一个扇区来存阈值结构体写入前需要先擦除整个扇区。Flash擦除次数有限制频繁按键写入会磨损所以我在写入前加入了延时和确认长按机制只有长按确认键超过1秒才触发Flash写入普通短按只是修改内存变量。报警机制有两种级别预警和严重报警。当温度接近上限差3℃时LCD上的温度数值变黄蜂鸣器每500ms短响一次当超过上限或任一传感器永久故障时温度数值变红蜂鸣器连续鸣叫同时通过蓝牙把报警帧发给手机AppApp弹窗提醒。报警阈值可跟随菜单设置但预警差值固定在3℃。报警状态必须在故障恢复后手动按确认键才能取消避免半夜报警后操作人员赶到现场时声音已经自动停掉。与Android App的通信协议走的是经典UART透传。蓝牙模块HC-05工作在从机模式波特率115200。STM32管理节点每隔500ms向手机发送一帧带CRC校验的JSON字符串{t1:23.5,t2:24.1,hum:62.8,fan:1,alarm:0}手机端解析JSON后刷新图表。使用JSON的好处是当需要增加二氧化碳传感器或粮位传感器时App端不用改协议解析逻辑加个字段就行。但嵌入式端发送JSON要占用约60字节的RAM缓冲区对于最小系统需要检查RAM是否够用STM32F103C8的20KB SRAM足够支撑如果换成F103T8只有8KB就得改用t23.5h62.8这样的轻量格式。6. I2C上拉、CAN终端电阻与掉电保存调试要点最后说几个实际调试中最容易卡壳的地方。I2C总线必须有上拉电阻STM32内部虽然有弱上拉但驱动力不足以达到100kHz及以上的快速模式。我习惯在SCL和SDA上各接一个4.7kΩ电阻到3.3V。如果传感器和MCU距离超过20cm就把上拉电阻改成2.2kΩ总线上拉电阻越小上升沿越快但也导致功耗增大超过10kΩ时波形会变得圆缓误码率升高。CAN总线两端必须各接一个120Ω终端电阻。很多人调试时一根短线上只有两个节点不接电阻也能收到数据但工业现场长线传输时反射会导致错误帧。正确做法是检查CAN收发器如TJA1050的CANH和CANL之间电阻值断电状态下测出来约60Ω才是正常的两个120Ω并联。如果只有60Ω说明终端电阻已经接好如果测出120Ω说明只接了一端加一个就是了。晶振电容的计算容易被忽略。8MHz晶振的负载电容常见为18pF匹配电容C 2 * Cl - Cstray其中Cstray是PCB寄生电容大概3~5pF。所以推荐值C 2*18 - 4 32pF。如果电容选太大晶振起振时间变长更严重时振荡幅度不足导致CAN波特率漂移。我曾遇到一块板子运行一晚上就死机最后发现是晶振电容用了两个20pF导致时钟频率偏低CAN节点采样点发生变化长时间运行后错误帧累积触发了总线关闭。Flash掉电保存这一块除了阈值我还存了一个掉电前是否正在烘干的恢复标志。代码上电后读取该标志如果为真则恢复上次的PWM占空比和运行模式同时亮屏提示系统已从异常断电恢复。掉电检测用STM32的PVD可编程电压监测器当VDD低于阈值触发中断在中断里向Flash写入紧急保存字段。注意Flash写入时间大概几十毫秒如果掉电太快可能来不及所以得在电源输入并一个大电容维持PVD中断发生后至少100ms的供电。关于bootloader项目后期可以加F103的内置DFU模式把固件烧录做成USB或UART升级这样调试时不用每次开盖插ST-Link。源码包里既然有keilkilll.bat说明开发环境是Keil MDK记得在Options for Target里勾选Use MicroLIB标准外设库加printf重定向时能省不少ROM。APM32或GD32这类国产兼容芯片可以直接沿用大部分代码只要把启动文件和Flash算法换成对应厂商的版本即可这也是很多工业产品降低BOM成本的做法。调试时先用SWD单步走一遍传感器读时序再用逻辑分析仪看CAN和I2C波形基本能把这套系统的稳定性调到位。本文还有配套的精品资源点击获取
返回列表