资讯详情

STM32智慧仓库管理系统:从传感器到上位机的完整实现

📅 2026/10/30 21:11:08 | 华诺云谱 👁 阅读
STM32智慧仓库管理系统:从传感器到上位机的完整实现
简介一份基于STM32的智慧仓库管理系统完整毕业设计源码包面向正在准备毕设的计算机相关专业学生及需要项目实战的C语言学习者也可直接用作课程设计或期末大作业。资源共403个文件压缩包约15.47MB核心代码以C语言源码.c/.h、STM32工程配置文件.uvprojx、.axf、.hex为主同时包含Android客户端.java、.jar、数据库脚本及说明文档文件夹区分清晰便于按模块调用。项目经过严格调试下载后无需复杂配置即可运行STM32端涵盖定时器、FLASH、RCC、ADC、I2C等外设驱动配合仓库管理的业务逻辑可帮助理解物联网软硬件联调流程。目前已有4690人学习下载无论是毕业答辩还是项目实训都能提供完整的参考实现。1. 基于stm32的智慧仓库管理系统先想清楚这套系统到底在解决什么问题仓库管理的痛点从来不在于“货多”而在于“你不知道货在哪、还剩多少、环境合不合适”。人工盘点一小时不如传感器三秒钟。基于stm32的智慧仓库管理系统核心做法就是让一块几十块钱的STM32单片机把温湿度、料位、门禁状态这些物理量采集上来再通过串口或无线模块上报给上位机形成一张能看到库存与环境的看板。这套东西既能作为嵌入式毕业设计的完整课题也能真正改造成小规模仓库的实用方案。要动手做的人通常是两类一是做stm32毕业设计、需要源码和完整文档的学生二是想用最低成本把一个小仓库数字化、又不愿意买整套工业WMS的小团队。下面这套方案按“硬件选型→下位机程序→通信协议→上位机展示→调试排错”的顺序讲照着搭就能跑起来。2. 架构与器件选型stm32在仓库管理系统里的角色和常用硬件组合2.1 主控芯片怎么选为什么毕设和小型仓库项目普遍用F103C8T6常见做法是选STM32F103C8T6也就是大家常说的“C8T6蓝板”。这颗芯片在智慧仓库这个场景里够用主频72MHz64KB Flash20KB SRAM片上有3个USART、2个I2C、3个定时器12位ADC。仓库管理系统的外设无非是温湿度传感器、超声波的料位检测、继电器控制、OLED显示这些外设全部用片内外设就能解决不需要外扩芯片。有人问要不要上F407或F103ZET6我的看法是如果你只是课程设计和毕设C8T6足够如果你要接摄像头做视觉盘点那才需要F407甚至H7。C8T6的另一个好处是资料极其丰富标准库和HAL库的例程满天飞调试遇到问题搜一下就有答案这对新手太重要了。供电设计也要提前想。仓库环境一般用12V供电板上需要12V转5V、5V转3.3V两级降压。3.3V的LDO选AMS1117-3.3就行12V转5V可以用MP1584这类DC-DC模块效率高、发热小。我见过有人直接用AMS1117从12V往下硬压结果芯片烫手、板子重启这就是典型的选型翻车。电源的余量也很重要STM32加上ESP8266、传感器和显示屏峰值电流可能到300mA以上DC-DC模块的额定电流选1A以上才稳。2.2 传感器和执行器的选型温湿度、料位检测、门磁与告警温湿度选DHT22而不是DHT11贵几块钱但精度差异明显。DHT22湿度精度±2%RH温度精度±0.5℃完全够仓库环境监测DHT11的±5%RH在夏天高湿环境下读数经常偏离实际做毕设答辩时被老师一问就容易露馅。如果要更高精度可以用SHT30走I2C接口代码更简单但价格是DHT22的三倍左右看预算取舍。料位检测是仓库系统的关键功能。常见做法有两种测距方案用HC-SR04超声波模块贴在货架上方往下测距离变化反映货位是否被占用漫反射光电开关成本低一些但需要货物本身能可靠反射红外光。我建议毕设做超声波方案因为HC-SR04接口简单、学得到定时器输入捕获的知识点而且做演示时用手在模块下方晃一晃就能看到料位变化效果直观。超声波模块的测量范围在2cm到400cm之间仓库货架层高一般不超过2米够用。执行器方面通风排风扇用一个5V继电器模块控制继电器由STM32的GPIO加三极管驱动。蜂鸣器选有源蜂鸣器接GPIO直接拉高就能响用来做温湿度越限告警。显示用0.96寸OLEDSSD1306I2C接口或1.44寸TFTOLED功耗低、显示温湿度和仓位状态足够清晰。通信模块放在下一节单独说因为它的选型决定了整个系统的数据流向。2.3 通信方案怎么定RS485走线、Wi-Fi上报、USB虚拟串口的取舍通信是整个系统中坑最多的部分选型上我把它分成三种适用场景。场景一是仓库里已有布线条件或者设备之间距离在几十米以内用RS485。STM32的USART加一个MAX3485芯片就能转成RS485差分信号抗干扰能力强线可以拉到100米以上。缺点是上位机要接USB转485的适配器而且RS485是半双工代码里要控制收发方向切换。每台从机设置不同的Modbus地址后一台主机可以轮询多块STM32对应多个仓位或者多个仓库。场景二是仓库里不方便布线走Wi-Fi。在STM32上挂ESP8266STM32通过USART发AT指令给ESP8266ESP8266连接无线路由器后将数据以MQTT协议发到服务器或云平台。这个方案最灵活也是当前智慧仓储项目的主流扩展方向。缺点是ESP8266用串口AT指令的方式效率偏低如果你擅长ESP32直接用ESP32做主控也行但本标题锁定了STM32那就用STM32加ESP8266的组合。场景三是纯调试和毕设演示STM32通过USB转串口芯片连PC或者干脆用STM32内部的USB模块做虚拟串口上位机直接读COM口。STM32F103的USB模块可以枚举成CDC类设备这就是很多人搜的“stm32 usb虚拟串口发送数据”方案免驱动在有线调试时很香。毕设做演示时用这个方案最省事不需要额外买Wi-Fi模块电脑上打开串口助手就能看到数据流答辩现场不容易翻车。我一般给的方案是“标配RS485加选配Wi-Fi”下位机先走RS485把数据送到一台边缘网关可以是PC或树莓派网关再视条件往云端转发。这样做的好处是下位机的通信链路是稳定的有线连接调试程序的时候不会因为Wi-Fi丢包而分不清是传感器的问题还是网络的问题。2.4 最小硬件清单和引脚分配参考下面给出一份可直接照着采购和接线的最小清单。器件型号/规格数量用途主控板STM32F103C8T6 最小系统板1数据采集与逻辑控制温湿度传感器DHT22AM23021仓库温湿度采集超声波模块HC-SR042两个货位的料位检测继电器模块5V低电平触发1控制排风扇蜂鸣器5V有源蜂鸣器1超限告警显示屏0.96寸 OLEDI2C1现场状态显示RS485模块MAX3485 TTL转RS4851上位机通信无线模块ESP8266选配1Wi-Fi/云上报电源12V2A适配器MP1584AMS1117各1供电引脚分配的原则是尽量避免把需要复用功能的引脚全挤在一起。超声波模块的Trig用普通GPIO输出、Echo用定时器输入捕获通道DHT22的数据脚也走普通GPIOOLED和AT24C02挂在同一组I2C上但地址不同不会冲突。典型分配如下外设引脚说明DHT22 DATAPA0普通GPIO开漏加4.7k上拉HC-SR04 TrigPA1 / PA2普通GPIO输出HC-SR04 EchoPA6 / PA7TIM3_CH1 / TIM3_CH2 输入捕获OLED SCL / SDAPB6 / PB7I2C1RS485 DE/REPB8方向控制高为发送MAX3485 DI/ROPA9 / PA10USART1 TX/RX继电器 INPB9低电平触发蜂鸣器PB10高电平响舵机仓库门禁演示PB11PWM输出可选这个表可以直接画进你的原理图或毕设文档里。要特别说明的是Echo引脚接定时器输入捕获这样测超声波回波脉宽时用的是硬件计时CPU不会被阻塞后面程序章节会具体讲。3. 下位机主程序从传感器采集到串口上报的完整流程3.1 用定时器输入捕获测超声波回波仓库料位检测的核心代码HC-SR04的工作原理是STM32给Trig引脚一个至少10us的高电平模块内部发射超声波并拉高Echo引脚收到回波后拉低EchoEcho高电平的持续时间就是超声波往返时间。距离 高电平时间 × 声速 / 2。声速在15℃时约340m/s也就是0.034cm/us所以距离约等于脉宽微秒数除以58。用延时加GPIO读电平的方式也能测但CPU会被占死仓库系统里还等着处理串口和传感器所以正确的做法是定时器输入捕获。TIM3的CH1和CH2分别对应PA6和PA7配置成上升沿和下降沿都捕获在捕获中断里记录两次计数器值差值就是脉宽。直接抄代码前先理解一个参数定时器的计数频率决定分辨率。TIM3挂在APB1上频率72MHz预分频设72得到1MHz计数频率每个计数代表1us直接拿计数值就是微秒数省去换算。/* TIM3 输入捕获初始化PA6/PA7 分别接两路 HC-SR04 Echo 信号 */ void TIM3_Cap_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_ICInitTypeDef TIM_ICInitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; // 定时器计数值上限自动重装载 TIM_TimeBaseStructure.TIM_Prescaler psc; // 预分频72-171 时计数频率为 1MHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); /* 两路输入捕获先只捕获上升沿下降沿在中断里切换 */ TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0f; // 输入滤波滤掉毛刺 TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Falling; TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Falling); // ch1 下降沿捕获 TIM_OC2PolarityConfig(TIM3, TIM_ICPolarity_Falling); // ch2 下降沿捕获 TIM_ITConfig(TIM3, TIM_IT_CC1 | TIM_IT_CC2, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); }上面这段配置里TIM_ICFilter设为0x0f非常关键。仓库里电机、继电器动作会产生电磁干扰Echo线上容易混入毛刺导致输入捕获误触发。开启滤波后定时器会连续采样多次确认电平稳定才认定有效边沿能滤掉大部分窄脉冲干扰。如果你的超声波模块在强干扰下读数乱跳先检查这个值而不是换模块。中断处理函数里要区分上升沿和下降沿、区分是哪个通道触发/* 上升沿记录起始计数下降沿计算脉宽并转成距离 */ void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); if (TIM_GetCapture1(TIM3) ! 0 capture1_start 0) { /* 约定第一次进入且没记录起始值视为上升沿 */ } if (echo1_rising) { cap1_start TIM_GetCapture1(TIM3); // 记录上升沿时刻 echo1_rising 0; TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Falling); // 切换为下降沿捕获 } else { cap1_end TIM_GetCapture1(TIM3); echo1_width cap1_end - cap1_start; // 脉宽单位 us echo1_rising 1; TIM_OC1PolarityConfig(TIM3, TIM_ICPolarity_Rising); // 切回上升沿 } } /* TIM_IT_CC2 通道处理逻辑完全相同省略 */ }这段逻辑要注意一个坑定时器计数溢出时capture_end减去capture_start如果出现负值说明回波脉宽超过了定时器重装值。1MHz计数频率下如果定时器重装值是65535最大可测脉宽65535us对应距离约11米超出HC-SR04实际量程所以用默认65535重装值即可不用特殊处理。测量结果算出来后还有一个细节是多次采样取中值而不是平均值。超声波偶尔会收到远处货架的回波平均值会被拉偏中值滤波能扛住这种野值。3.2 DHT22温湿度读取时序、校验和采样间隔的坑DHT22是单总线器件数据脚在空闲时被上拉电阻拉高。读取的时序本质就是主机和传感器之间通过拉低/释放总线来交换信息。一次完整读取流程是主机拉低总线至少18us实际建议1ms以上然后释放传感器应答后连续输出40位数据其中最高16位是湿度、中间16位是温度、最低8位是校验和校验和等于前四字节之和的低8位。每一位数据的0和1是靠高电平持续时间区分的——26us到28us的高电平表示070us左右表示1。/* 从 DHT22 读取 40 位数据成功返回 0校验失败返回 -1 */ uint8_t DHT22_Read_Data(int16_t *temp, uint16_t *humi) { uint8_t data[5] {0}; uint8_t i, j; GPIO_InitTypeDef GPIO_InitStructure; /* 主机起始信号拉低至少 1ms */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_ResetBits(GPIOA, GPIO_Pin_0); delay_ms(2); /* 拉低 2ms确保传感器识别起始信号 */ /* 释放总线切换为输入模式 */ GPIO_SetBits(GPIOA, GPIO_Pin_0); delay_us(30); /* 释放 30us等传感器响应 */ GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); /* 等待传感器把总线拉低作为应答信号 */ while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 1); /* 等待拉低 */ while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 0); /* 等待拉高 */ while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 1); /* 跳过应答高电平 */ /* 逐位读取 40 位数据 */ for (j 0; j 5; j) { for (i 0; i 8; i) { while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 0); /* 等待每一位的低电平结束 */ delay_us(40); if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 1) data[j] | (0x80 i); /* 高电平持续时间超过 40us 则为 1 */ } } /* 校验前四个字节累加和等于第五个字节 */ if ((uint8_t)(data[0] data[1] data[2] data[3]) ! data[4]) return -1; *humi (data[0] 8) | data[1]; *temp ((data[2] 8) | data[3]) 0x7FFF; /* 去掉符号位 */ if (data[2] 0x80) *temp -*temp; /* 温度为负 */ return 0; }这段代码里最容易翻车的是bit读取逻辑。每个bit的前半段是一个低电平脉冲长度从20us到40us不等我用先等低电平结束、再延时40us、再读电平的方式判断。40us是0和1的分界线0的高电平持续约26us延时40us后已经回到低电平读到01的高电平持续约70us延时40us后还是高电平读到1。有些例程是延时30us再读到了高温环境下器件时序略有偏移时会误判我给40us留了余量。至于为什么DHT22的读取间隔不能低于1秒是因为传感器内部测量周期本身要2秒左右连续读取时它会不响应。很多人遇到DHT22读出来偶尔是0或者固定值就是这个原因主循环里加个每秒读一次的标志位就行。再有就是I2C接口的SHT30不需要这么麻烦的时序但从做毕业设计的“技术含量”角度看DHT22的单总线时序反而是加分项因为能讲清楚电平翻转和时序容差。3.3 Modbus RTU上报把数据打包成上位机认识的协议传感器数据采集到了接下来要解决通信格式问题。仓库系统里最常见的做法是Modbus RTU它的帧结构是从机地址1字节、功能码1字节、数据区N字节、CRC校验2字节低字节在前。读寄存器用功能码0x03写单个寄存器用0x06写多个用0x10。上位机作为主机定期轮询各从机从机收到完整帧后返回响应。CRC16-Modbus的计算可以用查表法也可以用位运算法位运算法在STM32上跑9个字节大概几十微秒完全可接受。发送函数里要特别注意RS485的方向控制半双工模式下发送前要把DE/RE引脚拉高发完最后一个字节要等数据完全移位输出后才能拉低。这个“等”的时机很微妙拉早了最后一个字节发不出去拉晚了又会在接收状态读到自己发出的数据。正确做法是发完最后一个字节后加一个字节时间的延时9600波特率下约1ms再用USART的TC标志确认发送完成。/* Modbus RTU CRC16 计算Modbus 多项式 0x8005初始值 0xFFFF */ uint16_t Modbus_CRC16(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; uint16_t i, j; for (i 0; i len; i) { crc ^ buf[i]; for (j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; /* 0xA001 是 0x8005 的反向多项式 */ else crc 1; } } return crc; } /* 发送一个 Modbus 响应帧data 为数据区指针len 为数据长度 */ void Modbus_SendResponse(uint8_t slave_addr, uint8_t func, uint8_t *data, uint8_t len) { uint8_t frame[64]; uint16_t crc, i; uint16_t frame_len 3 len; /* 地址 功能码 数据 CRC2字节 */ frame[0] slave_addr; frame[1] func; for (i 0; i len; i) frame[2 i] data[i]; crc Modbus_CRC16(frame, 3 len - 2 2); /* 注意CRC 只算到数据结束 */ frame[frame_len - 2] crc 0xFF; /* CRC 低字节在前 */ frame[frame_len - 1] crc 8; RS485_DE(1); /* 拉高 DE进入发送模式 */ delay_us(20); /* 方向切换稳定时间 */ for (i 0; i frame_len; i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); /* 发送数据寄存器空 */ USART_SendData(USART1, frame[i]); } while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); /* 等待最后一个字节发出完成 */ RS485_DE(0); /* 拉低 DE切回接收模式 */ }参数上有两个地方值得说明。第一CRC计算的范围是从地址字节开始到数据区结束2个CRC字节本身不参与计算上面代码容易出错的位置就在这里很多新手把CRC也代入计算导致校验永远不过。第二RS485_DE(1)之后的20us延时是给MAX3485的方向切换留的不同芯片切换时间不一样ST3485这类芯片需要纳秒级国产的一些型号需要微秒级宁可多留一点。如果切回接收模式的时机太早总线还在发送余波里上位机可能收到重复帧。接收端用串口中断逐字节收攒够一帧再解析。一个简单可靠的接收状态机是空闲态收到地址字节后进入接收态按帧结构累计字节数够8个字节读寄存器请求帧固定为8字节后做CRC校验通过则执行功能并组织响应。你可以搜到很多Modbus从机协议栈但仓库管理系统的寄存器数量就十来个自己写状态机完全够还能在毕设里讲清楚每一帧的处理过程。3.4 参数存储和告警联动AT24C02保存阈值越限自动排风仓库系统不能只上报数据还要有现场告警能力。温湿度阈值应该存在掉电不丢失的存储器里下次开机直接读出来用而不是每次重新编译写死。AT24C02是2Kbit的I2C EEPROM存20个阈值参数绰绰有余。它的写入是按页的一页8字节单字节写需要等待内部写周期最长5ms。写的时候要判断“写保护”状态不过C8T6最小系统板上集成的AT24C02一般没接WP引脚不用处理。告警联动逻辑建议这样设计主循环里每秒读一次温湿度和阈值比较超过阈值就把继电器引脚拉低低电平触发继电器导通排风扇启动温度回落到阈值以下2℃再关这个“回差”设计是为了防止继电器在阈值边界反复吸合那个咔哒声加上电火花很影响传感器读数稳定性。蜂鸣器在超限时以1Hz频率响提醒现场人员处理。OLED上轮流显示两个仓位的料位状态和当前温湿度用SSD1306的I2C驱动库刷新间隔2秒左右即可刷新太频繁OLED闪烁反而看不清。4. 上位机与数据链路一个不依赖商业WMS的轻量看板方案4.1 通信协议设计帧头、长度、CRC别把调试时间耗在报文解析上如果你不想用标准的Modbus协议或者你的毕设题目里要求“自定义协议”那么自定协议时有些原则必须守住。帧格式建议帧头2字节固定0xAA 0x55、设备地址1字节、功能码1字节、数据长度1字节、数据区N字节、CRC162字节、帧尾1字节固定0x0A。帧头用0xAA55是为了和普通数据区分开来选择这两个值是因为它们的二进制形态10101010 01010101不容易在自然数据流中出现。协议设计里有三个坑我都是踩过之后才长记性的。第一不要用帧头来判断一帧的起始而忽略帧尾只找帧头很容易在数据区里出现假帧头时误判保护帧尾能多一层防线。第二长度字段必须包含在CRC校验范围内否则有人改了长度字段会导致后续所有解析错位而CRC仍然校验通过。第三下发指令和上报数据要分开功能码不要复用比如0x01是上报传感器数据、0x03是修改阈值、0x06是查询历史清晰的功能码让终端调试时不用猜报文含义。如果做成查询式上位机定时发查询帧下位机应答调试时可以先用串口助手人工发查询帧看响应很容易定位问题在下位机还是上位机。如果做成主动上报式下位机每秒主动发一帧上位机被动接收代码简单但调试时无法知道某帧是否丢失。仓库场景数据量不大两种都行但查询式更适合“系统”的气质因为上位机能主动控制下位机执行动作比如打开风扇。4.2 用Python实现仓库看板PySerial读串口SQLite存历史上位机端我习惯用Python快速搭一个看板比起C#的WinFormPython写起来快而且将来想接Web前端也很容易。PySerial负责读串口数据帧解析后写入SQLite数据库用Flask提供一个简单的HTTP接口给前端展示。这套方案跑在一台普通PC上24小时挂机没问题SQLite承受仓库这种每秒一条的数据量绰绰有余。import serial import sqlite3 import struct import time # 串口参数要和下位机一致9600,8,N,1 ser serial.Serial(COM3, 9600, timeout2) conn sqlite3.connect(warehouse.db) conn.execute(CREATE TABLE IF NOT EXISTS sensor_data (id INTEGER PRIMARY KEY AUTOINCREMENT, ts DATETIME DEFAULT CURRENT_TIMESTAMP, temp REAL, humi REAL, slot1 INT, slot2 INT)) def crc16_modbus(data: bytes) - int: crc 0xFFFF for b in data: crc ^ b for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return crc def parse_frame(frame: bytes): # 帧格式AA 55 地址 功能码 长度 数据... CRC低 CRC高 0A if len(frame) 8 or frame[0] ! 0xAA or frame[1] ! 0x55: return None if frame[-1] ! 0x0A: return None # CRC 校验 calc_crc crc16_modbus(frame[2:-3]) recv_crc frame[-3] | (frame[-2] 8) if calc_crc ! recv_crc: return None # 数据区[temp_high, temp_low, humi_high, humi_low, slot1, slot2] data frame[5:-3] temp struct.unpack(h, bytes(data[0:2]))[0] / 10.0 humi struct.unpack(H, bytes(data[2:4]))[0] / 10.0 return temp, humi, data[4], data[5] while True: buf ser.read(14) # 固定帧长 14 字节 if len(buf) 14: result parse_frame(buf) if result: temp, humi, slot1, slot2 result conn.execute( INSERT INTO sensor_data (temp, humi, slot1, slot2) VALUES (?,?,?,?), (temp, humi, slot1, slot2) ) conn.commit() print(f温度:{temp}°C 湿度:{humi}% 仓位1:{slot1} 仓位2:{slot2}) time.sleep(0.5)PySerial的read参数填14是固定帧长的前提。如果你的协议是可变长度就得按长度字段读取read(1)逐字节分析更稳妥。SQLite的写入要放在循环里提交但不要每条都commit批量插入的性能会更好我这里为了演示简单直接commit了。真要连续跑几天建议每10条或每10秒commit一次减少磁盘写放大。上位机显示部分用Flask加一个模板就够了。后端从SQLite里查最新记录渲染成HTML表格温度曲线可以用简单的Chart.js画模板和路由代码量不大但是展示效果很加分答辩时评委能看到历史曲线和实时数据同屏刷新比串口助手的黑底白字专业得多。如果你不想写Web那也可以用PyQt5做一个桌面看板逻辑一样只是UI框架不同。4.3 串口调试阶段怎么定位问题看灯、看设备管理器、看HEX数据上位机联调时有一套固定的排查顺序我每次做都会按这个来。第一步看设备管理器确认USB转串口芯片被识别成了哪个COM口如果是CH340芯片驱动没装好时设备管理器里会出现一个带感叹号的设备这时候下位机怎么发都没用。第二步用串口助手直接收数据别急着上Python先用串口助手看能不能收到原始HEX数据帧头是不是0xAA 0x55CRC对不对这样能快速判断问题在下位机还是在上位机解析代码。第三步才运行Python脚本如果Python收到的数据是乱的先怀疑串口参数不匹配波特率、数据位、停止位任何一个不对都会导致乱码。还有一种情况是串口助手能收Python收不到多数是串口号不对或者串口被占用。Windows下COM口的编号在不同USB口插入时会变写Python脚本时最好通过设备描述自动识别CH340对应的COM号或者直接打印可用串口列表让用户选这样换USB口插错位置也不至于一头雾水。下位机侧串口收发指示灯是个好东西没有硬件指示灯就加个逻辑分析仪抓TX/RX引脚的波形一次抓100帧看是否有毛刺或用错波特率。仓库现场如果有变频器、电机这类干扰源波形上会看到明显的电平抖动这时候优先处理干扰而不是改软件。5. 参数调优与常见问题5条花了一周才搞定的血泪经验5.1 DHT22读数偶尔跳变或固定在最大值采样间隔和供电纹波同时排查现象DHT22湿度读数偶尔从50%跳到99.9%温度偶尔跳到-5℃过一会儿又自己恢复。原因有两个一是DHT22的采样周期本身就要2秒左右你如果500ms读一次传感器还没准备好就应答返回的数据就是上次测量值的误差放大结果二是供电纹波太大会干扰单总线上的电平判断数据脚的上拉电阻如果接到3.3V而纹波大高电平会被误判成低电平。解决主循环里加一个“距离上次读取超过2秒”的条件再读同时给DHT22的VCC和GND之间并一个100uF电解电容加0.1uF瓷片电容数据引脚上串一个100欧电阻做缓冲。如果解决了供电、也加了间隔还是偶尔跳那就在软件里连续读三次取中位数把野值滤掉。5.2 超声波料位检测在金属货架环境中读数乱跳现象HC-SR04装在金属货架上货架没人动时距离值也在正负十几厘米范围内乱跳。原因超声波模块发出的是圆锥形波束在金属表面会产生多次反射如果货架横梁正好在波束路径上回波会叠加出一个假的目标导致脉宽不是真实货位的距离。还有一个容易被忽略的因素是HC-SR04的引脚间距2.54mm模块外壳没有屏蔽电机继电器动作时的电磁干扰会串进Echo信号。解决软件上做三件事——测量范围限制在10到200厘米之间超出范围直接丢弃连续测5次排序取中值对相邻两次读数做最大差值限制超过20厘米就算无效。硬件上把模块用塑料支架固定反面贴一块铜箔胶带接地确实有改善但不必强求软件滤波已经能解决绝大多数情况。5.3 RS485通信时好时坏先查共地和终端电阻再怀疑程序现象ST-Link和串口调试都正常一接RS485就出现上半场正常下半场超时或者距离一远就丢包。原因RS485是差分信号但它依赖收发器之间的地电位作为共模参考。很多人只接A、B两根线不接GND两个设备的开关电源地之间存在电压差共模电压超过收发器允许范围时就解调出错。终端电阻也是重灾区RS485要求在总线两端各接一个120欧电阻长度超过几十米不接电阻会出现反射波形振铃导致数据错乱。解决下位机和USB转485模块之间必须把GND接上在总线的物理两端各跨接120欧电阻波特率不要贪高仓库场景9600完全够距离100米以上时用9600比38400稳定得多。另外检查A/B是否接反接反的表现是“能收到数据但是全是乱码”这种情况在调试助手上看HEX会呈现有规律但完全不对的字节直接对调就能解决。5.4 STM32程序下载失败提示No target connected现象程序写完后接上ST-LinkKeil提示No target connected或者下载到一半提示Cannot access Target但板子LED还在闪说明程序在跑。原因最常见的是程序把SWD引脚占用或者复用了。STM32的SWDIO是PA13、SWCLK是PA14如果你初始化GPIO时把这些脚配成了普通输出或者复用功能ST-Link就断连了。还有一种是GPIO_Config里把SWD引脚配置成模拟输入这种配置会直接关掉SWD功能。解决如果程序还能跑按住复位键再点下载下载开始瞬间松开复位可以抢在程序初始化GPIO之前让ST-Link连上芯片这个方法不行就用stlink utility这个工具选择connect under reset模式很多时候能救回来。这也是很多人搜“stm32禁用jtag”“stm32 st-link utility”的典型场景。如果你用的是标准库新建工程默认GPIO初始化代码里没有禁用JTAG照抄网上工程时容易把SWD配置混进去。5.5 delay函数卡死或程序跑飞现象程序运行几分钟后卡住定位发现在delay_ms里一直循环出不来或者进了一个硬件错误中断HardFault。原因如果你用的是SysTick做的延时而你的代码里同时有中断服务函数调用延时SysTick的中断优先级比外设中断低时外设中断长时间占用CPUSysTick得不到执行延时就会比预期长严重时看起来像卡死。另一个常见原因是某个外设中断触发后没有清除标志位导致反复进中断CPU资源被耗尽。解决中断服务函数里不要调用延时函数用HAL库的HAL_Delay之前确保SysTick中断优先级是最高的检查所有外设中断的响应函数末尾都调用了清除标志位的语句串口的ORE标志、DMA的TC标志都是重灾区。HardFault则多半是数组越界或者指针操作错误用Keil的调试器在HardFault_Handler处打断点查看PC指针和堆栈内容定位到具体函数。6. 验证方法进阶从样机到能交付的检查清单系统能跑起来只是第一步能不能稳定工作七天不重启才是仓库现场的真实标准。我建议做一套验证矩阵覆盖通信误码率、断电恢复、传感器一致性三个维度。误码率测试是把下位机连到PC上跑一个Python脚本统计24小时内收到的帧数、CRC校验失败的帧数这个值应该低于万分之五达不到就回头查终端电阻和波特率。断电恢复测试是让系统运行中直接拔电、再上电确认能自动重新初始化所有外设并能正常连接上位机设计上要注意AT24C02里的阈值参数在上电瞬间不能被误写。传感器一致性测试是把模块放到恒温恒湿环境或者空调房加湿器旁边两小时对比读数漂移DHT22的漂移应该在±1%RH以内。进阶方向有三个按投入产出比排序。第一是给下位机加FreeRTOS把传感器读取、通信上报、显示刷新拆成三个任务用消息队列传递数据这样ADC采样或传感器阻塞时通信任务不受影响这套系统才真正有多任务实时系统的样子。第二是把RS485总线换成LoRa或4G模块仓库面积大、工位分散时布线成本会超过无线模块成本换无线后每个工位一个STM32节点边缘网关统一汇聚。第三是上位机从本地SQLite升级到MySQL加Web端多人同时查看库存和温湿度历史这才是“管理系统”的完整形态。我个人的习惯是做完一个项目后把调参记录和踩坑笔记整理成一份调试日志下次做相似项目直接翻日志省掉大量重复排查时间。这套系统的价值不在代码量而在你把传感器数据变成可用信息的完整链路希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑