拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32F103C8T6实现Modbus RTU温湿度从站

简介本资源是一套基于STM32F103C8T6开发板实现Modbus RTU协议通信并读取温湿度传感器数据的完整嵌入式项目工程面向嵌入式初学者与工业通信入门开发者解决环境监测场景中MCU与标准Modbus从设备如SHT/TH系列传感器可靠串口交互的核心问题。压缩包含155个文件涵盖48个.h头文件定义外设驱动与Modbus帧结构、21个.c源文件含HAL库UART初始化、CRC校验、Modbus请求构建与响应解析等关键逻辑、22个.o编译中间文件及工程配置文件.ioc、.uvprojx等整体大小为6.43MB。已有725人学习下载。资源提供可直接编译运行的Keil MDK工程包含完整的STM32 HAL库驱动、Modbus RTU帧封装与解析模块、定时采集逻辑及错误重试机制代码结构清晰、注释详实便于理解协议栈分层设计与实际调试排错过程。1. 项目概述为什么用STM32F103C8T6跑Modbus读温湿度不是“炫技”而是工程刚需我第一次在产线调试这个方案时客户现场的PLC工程师盯着我的最小系统板看了三分钟最后问了一句“这板子真能扛住车间电磁干扰Modbus地址写对了没温湿度数据别飘。”——这句话点破了所有新手容易忽略的本质STM32F103C8T6做Modbus温湿度节点从来不是为了“点亮LED”或“跑个Hello World”而是要嵌入真实工业场景成为PLC、DCS或上位机系统里一个可寻址、可校验、可长期稳定运行的从站设备。它不追求高性能计算但必须做到三点通信零丢帧、数据格式零歧义、掉电重启后地址与功能码自动复位。你搜到的“stm32f103c8t6最小系统板”“ch340串口驱动”“modbus poll密钥”这些热词背后全是实操中踩过的坑比如CH340驱动装错版本导致波特率偏差2%Modbus Poll发03H读寄存器时误设为ASCII模式而收不到响应或者用HAL库默认配置却没关掉UART空闲中断导致DMA接收错位。温湿度传感器常见DHT22、SHT30、AM2301本身输出的是原始数字量但Modbus协议要求你把它们映射成标准的保持寄存器4x区比如40001对应温度整数部分40002对应小数部分40003对应湿度整数部分——这个映射不是随便定的它直接决定上位机能否正确解析。我见过太多项目因为寄存器地址定义混乱导致LabVIEW读出来温度是-273℃湿度是300%最后发现是高低字节顺序搞反了。所以这篇内容不讲抽象协议栈只讲你焊好板子、烧录完程序、接上线缆后第一帧Modbus RTU请求发出去如何确保对方能解出正确的温湿度值。适合刚焊完最小系统板、手头有DHT22模块、正对着串口调试助手发懵的新手也适合需要快速交付工业节点、不想被Modbus校验码算错耽误工期的工程师。核心关键词就四个STM32F103C8T6、Modbus RTU、串口UART、温湿度传感器其他所有热词——无论是“modbus slave密钥”还是“stm32 hal库串口空闲中断”——都是围绕这四点衍生出的具体问题。2. 整体架构设计为什么放弃FreeRTOS、不用TCP死磕RTUUART裸机2.1 协议选型RTU不是妥协而是工业现场的物理法则很多人看到标题里写“Modbus”第一反应是“是不是得用Modbus TCP毕竟现在都上以太网了”。但当你真正把STM32F103C8T6焊在金属机柜里旁边是变频器、接触器、大功率电机——你就明白为什么必须选RTU。Modbus TCP依赖TCP/IP协议栈意味着你要移植LwIP占用至少15KB Flash和8KB RAM而F103C8T6只有64KB Flash、20KB RAM还要留给温湿度采集、CRC校验、串口缓冲区。更致命的是TCP的三次握手、重传机制在强干扰环境下会频繁失败一次网络抖动就可能让整个从站失联。而RTU是二进制编码通过字符间最大3.5个字符时间间隔来判断帧边界物理层直接跑在RS-485总线上抗共模干扰能力比网线强一个数量级。我实测过同一块板子在变频器启动瞬间TCP连接断开3次RTU通信纹丝不动。所以架构第一步就锁定STM32F103C8T6 UART1PA9/PA10 MAX485芯片SN65HVD72或SP3485 RS-485双绞线。注意这里没提USB转串口——因为工业现场根本不用USBCH340或FTDI只是你开发调试用的桥梁最终部署必须走485。2.2 软件框架裸机循环足够FreeRTOS反而增加不确定性网上大量教程鼓吹“freertos移植到stm32f103c8t6”但在这个项目里它是个陷阱。FreeRTOS需要配置SysTick、管理任务堆栈、处理中断优先级而Modbus RTU对时序极其敏感一帧完整数据地址功能码数据CRC必须在3.5个字符时间内完成接收否则主站判定超时。裸机循环里你用状态机控制UART接收每个字节进来立刻存入缓冲区收到最后一个字节后立即校验CRC整个过程耗时10μs。而FreeRTOS的任务切换开销在1~3μs看似不多但一旦任务被抢占哪怕延迟1个SysTick周期通常1ms主站就认为从站无响应。我试过在FreeRTOS下跑Modbus用逻辑分析仪抓波形发现UART中断服务函数ISR执行完后任务切换导致CRC校验延迟了1.2ms结果Modbus Poll显示“Timeout”。所以最终方案是纯裸机主循环只做三件事——采样温湿度、解析Modbus请求、构建响应帧。没有任务调度没有消息队列所有代码都在RAM里跑启动即工作。HAL库用但只用HAL_UART_Receive_IT和HAL_UART_Transmit禁用所有中间件如HAL_UARTEx_Receive_DMA因为DMA传输完成中断可能被其他外设抢占。2.3 硬件链路最小系统板的致命短板与补救你买的“stm32f103c8t6最小系统板”90%没集成MAX485只留了UART引脚PA9/PA10。这意味着你必须自己加一级485转换电路。常见错误是直接用TTL电平接RS-485总线——后果是通信距离超2米就丢帧。正确做法是PA9接MAX485的RO接收输出PA10接DI发送输入再额外用一个GPIO比如PB1控制DE/RE引脚。DE高电平允许发送RE低电平允许接收。关键细节DE/RE不能常高或常低必须严格同步于UART发送过程。我见过太多人把DE一直拉高结果从站疯狂发响应主站收不到请求——因为从站一直在“说话”没留出时间听主站。所以软件里必须实现发送前拉高DE发送完成后延时1ms再拉低DE这个延时是为了等最后一比特发出并稳定在总线上。另外RS-485总线两端必须加120Ω终端电阻否则长距离反射信号会导致CRC校验失败。这不是理论是我用示波器在100米屏蔽双绞线上实测不加电阻时波形过冲严重接收端误判起始位加了之后眼图干净利落。3. 核心细节解析从传感器读取到Modbus寄存器映射的全链路拆解3.1 温湿度传感器选型与底层驱动DHT22的时序陷阱与SHT30的I2C避坑温湿度传感器不是插上就能读不同型号底层协议天差地别。DHT22用单总线协议靠精确延时控制电平翻转而STM32F103C8T6的SysTick精度只有1μs但DHT22要求80μs低电平80μs高电平作为起始信号误差超过±5μs就会失败。我最初用HAL_Delay卡死在初始化阶段后来改用GPIO直接置位NOP指令精准延时才搞定。代码片段如下// DHT22初始化时序伪代码 GPIO_ResetBits(GPIOA, GPIO_Pin_0); // PA0拉低 for(uint8_t i0; i20; i) __NOP(); // 精确80μs GPIO_SetBits(GPIOA, GPIO_Pin_0); for(uint8_t i0; i40; i) __NOP(); // 精确160μs // 然后等待DHT22响应...而SHT30用I2C看似简单但F103C8T6的I2C1时钟频率必须严格设为100kHz标准模式如果设成400kHz快速模式SHT30会返回0xFF。更隐蔽的坑是SHT30的地址是0x44或0x45取决于ADDR引脚接地或接VCC但很多国产模块把ADDR焊死在GND你查手册以为地址是0x44实际是0x45结果I2C扫描永远找不到设备。解决方法用逻辑分析仪抓I2C波形看主站发的地址字节到底是0x880x44左移1位还是0x8A0x45左移1位。3.2 Modbus寄存器映射为什么温度存40001湿度存40003而不是40001和40002Modbus保持寄存器4x区是16位宽而温湿度数据通常需要小数点后一位如25.6℃、65.3%RH。如果直接把25.6存进40001寄存器只能存整数25小数丢失。正确做法是放大10倍存整数25.6℃ → 25665.3%RH → 653。这样40001存温度整数值25640002存湿度整数值653。但问题来了Modbus协议规定读多个寄存器时数据按**高位在前Big Endian**排列。假设温度256的十六进制是0x0100那么40001寄存器里存的就是0x0100而不是0x0001。我第一次调试时LabVIEW读出来温度是25600℃就是因为没注意字节序把0x0100当成了0x0001256→65536。所以映射规则必须明确40001十进制地址1温度整数×1016位无符号整数40002十进制地址2湿度整数×1016位无符号整数40003十进制地址3保留可存传感器状态0正常1超限这样上位机读40001得到256除以10得25.6℃读40002得到653除以10得65.3%RH。所有计算都在从站完成主站只做简单除法避免浮点运算。3.3 CRC16校验码在线计算器背后的数学原理与手算验证“modbus校验码在线计算”是新手最依赖的工具但你必须懂它怎么算否则调试时一头雾水。Modbus RTU的CRC16算法是多项式0x8005初始值0xFFFF最低位先传LSB First。这不是随便定的0x8005是CRC-16-IBM标准LSB First是因为UART传输时低位先发。手算步骤初始化CRC0xFFFF对每个字节从地址开始到最后一个数据字节将字节与CRC低8位异或结果存入tempCRC右移8位temp左移8位与CRC异或temp循环右移8次每次右移前与0x0001相与若为1则CRC与0xA001异或这是0x8005的反码因LSB First需反转多项式最终CRC即为校验码低字节在前高字节在后我写过一个Excel表格把每一步手动算出来发现网上某些“在线计算器”结果不对——它们用了MSB First模式。所以最可靠的方法是用STM32代码生成校验码再用串口调试助手发帧看Modbus Poll是否报“CRC Error”。只要你的代码算出的校验码能让Poll识别就是对的不必纠结理论值。4. 实操过程从Keil工程创建到Modbus Poll成功读取的逐帧解析4.1 Keil工程搭建HAL库配置的关键三步与内存优化新建工程时很多人卡在“HAL_UART_Receive_IT接收不到数据”。根本原因是HAL库默认开启UART空闲中断IDLE Interrupt而Modbus RTU帧结束靠的是3.5字符时间不是空闲中断。F103C8T6的UART空闲中断触发条件是“线路上连续1个字符时间无电平变化”但Modbus RTU帧间间隔是3.5字符时间空闲中断会提前触发导致数据截断。解决方法在MX_USART1_UART_Init()函数里注释掉__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);这一行并手动在主循环里用HAL_UART_Receive_IT(huart1, rx_byte, 1)逐字节接收。第二步关闭HAL库的DMA功能。DMA接收需要配置缓冲区长度但Modbus帧长不固定地址1字节功能码1字节数据N字节CRC2字节DMA无法预知长度容易溢出。第三步优化RAM使用。F103C8T6只有20KB RAMHAL库默认为每个UART分配256字节接收缓冲区两个UART就占512字节。我们只用UART1把huart1.RxXferSize设为1huart1.pRxBuffPtr指向一个单字节变量彻底省掉缓冲区。这样RAM节省下来可以多开几个全局变量存温湿度值。4.2 Modbus请求帧解析以03H功能码为例的逐字节拆解假设Modbus Poll发来请求01 03 00 00 00 02 C4 0B01从站地址你的STM32地址设为103功能码读保持寄存器00 00起始地址40001 → 十进制0 → 0x000000 02读取数量2个寄存器40001和40002C4 0BCRC校验码用你代码算的必须匹配你的程序收到这6个字节后先校验CRC用前6字节01 03 00 00 00 02计算CRC结果必须等于C4 0B。校验通过再解析地址匹配是01本机地址功能码支持是只实现03H不支持16H写寄存器地址范围合法40001~40002在允许范围内假设只开放前10个寄存器数量≤125是2≤125全部通过才进入响应构建。任何一步失败都要发异常响应帧01 83 02地址功能码|0x80异常码02非法数据地址。4.3 响应帧构建如何把25.6℃和65.3%RH打包成标准Modbus格式响应帧结构从站地址 功能码 字节数 数据 CRC从站地址01功能码03与请求一致字节数读2个寄存器每个16位共4字节 →04数据25.6℃ → 256 → 0x010065.3%RH → 653 → 0x028D按Big Endian0x0100拆成01 000x028D拆成02 8D所以数据段为01 00 02 8DCRC对01 03 04 01 00 02 8D计算CRC16假设结果为E3 A5最终响应帧01 03 04 01 00 02 8D E3 A5注意CRC低字节在前E3高字节在后A5这是Modbus RTU硬性规定。我第一次发帧时把CRC写成A5 E3Poll显示“Invalid CRC”折腾半小时才发现字节序反了。4.4 调试实战用串口调试助手和Modbus Poll的双验证法仅靠串口调试助手如XCOM、SSCOM能看到原始十六进制数据但无法验证Modbus协议合规性。必须用Modbus Pollv7.5.0免费版无需密钥。设置步骤Connection → Read/Write Definition → 设置从站地址为1功能码03起始地址0对应40001数量2Serial Port → 波特率9600数据位8停止位1校验NoneRTU模式不校验点击ReadPoll会发01 03 00 00 00 02 C4 0B你的板子回01 03 04 01 00 02 8D E3 A5Poll界面会自动显示Register 1 256Register 2 653换算后即25.6℃、65.3%RH如果Poll报错先看Status栏“Illegal Data Address” → 请求地址超出范围检查40001是否映射到有效变量“CRC Error” → 校验码算错或字节序反了“Timeout” → 从站没响应检查DE/RE控制、UART发送是否卡住我习惯同时打开串口调试助手把Poll的请求帧和你的响应帧都抓下来用十六进制对比一眼看出差异。5. 常见问题与排查技巧实录那些让工程师熬夜的隐藏Bug5.1 串口通信“丢数据”真相不是波特率错是电平不匹配搜索热词里有“linux从串口接收数据丢失”这问题在STM32上同样存在。现象Modbus Poll偶尔读到错误数据比如温度突然跳到65535。用逻辑分析仪抓UART波形发现RX线上有毛刺。根源是CH340 USB转TTL模块输出电平是3.3V而F103C8T6的UART输入耐压是5V但3.3V电平在长导线上传输衰减后可能低于F103的输入阈值约1.8V。解决方案开发时用CH340但务必在CH340的TX接STM32 RX线上串一个1kΩ上拉电阻到3.3V部署时换SP3232ERS-232电平转换芯片它输出±12V抗干扰强绝对不要用PL2303其驱动不稳定Win10下常识别失败提示用万用表测CH340 TX脚对地电压正常应为3.3V如果只有2.5V说明驱动能力不足必须加拉电阻。5.2 Modbus Poll“Port 1 not available”Windows驱动冲突的终极解法热词里高频出现“modbus poll port 1 not available”这不是Poll软件问题而是Windows把COM端口占用了。常见原因Arduino IDE开着占用了COM3蓝牙串口服务在后台运行杀毒软件拦截串口访问排查步骤设备管理器 → 端口COM和LPT→ 查看COM端口号如COM5打开任务管理器 → 详细信息 → 找到所有javaw.exeArduino、chrome.exe某些网页串口工具、avp.exe卡巴斯基进程全部结束在Poll里Connection → Setup → 把Port设为COM5Baud Rate设为9600如果仍报错右键COM5 → 属性 → 高级 → 把“IRQ”改成一个空闲值如7避免硬件中断冲突我遇到过最诡异的一次公司电脑装了某国产工业软件它后台静默占用COM1但设备管理器不显示。最后用mode COM1命令行强制释放才解决。5.3 温湿度数据“飘”传感器供电与PCB布局的物理层教训DHT22数据飘90%是电源问题。F103C8T6的3.3V由AMS1117稳压但DHT22峰值电流达20mAAMS1117压差大、发热高导致3.3V跌落到3.0VDHT22时序紊乱。解决方案DHT22单独用一个LDO如XC6206P332MR供电输入接5V输出3.3V在DHT22电源引脚就近放10μF钽电容0.1μF陶瓷电容PCB布线时DHT22的DATA线远离晶振、SWD接口走线长度10cmSHT30更敏感I2C的SDA/SCL线上必须各串一个2.2kΩ上拉电阻到3.3V且电阻要靠近SHT30芯片否则上升沿缓慢F103C8T6的I2C硬件模块会误判ACK。我曾因上拉电阻放在MCU端导致SHT30每10次读取失败3次换了位置后100%成功。5.4 固件升级后Modbus失效Flash擦写破坏了EEPROM模拟区热词里有“stm32f103c8t6加密”但更多人遇到的是“stm32f103c8t6项目密码锁”相关问题——其实是指用Flash模拟EEPROM存Modbus从站地址。F103C8T6没有独立EEPROM常用最后1页Flash0x0800F800~0x0800FFFF存参数。但Keil默认编译时会把整个Flash擦除包括这页。结果新固件烧录后从站地址变回默认1但用户之前设为5导致Poll连不上。解决方法在Option Bytes里勾选“Read out Protection”为Level 1防止Flash被读出但不要勾选“User Option Bytes”里的nWRP写保护否则无法存地址写地址时先读原页擦除整页再把新地址写入最后校验或者更简单把从站地址固化在代码里通过跳线帽选择PA0接地地址1悬空地址5避免Flash操作注意F103C8T6的Flash页大小是1KB擦除一页会清空1024字节所以存地址时要预留足够空间别和其他参数挤在一起。6. 进阶扩展从单节点到多节点总线的工程化落地6.1 多从站总线设计地址冲突与轮询时序的硬约束一个RS-485总线上挂10个STM32温湿度节点每个地址设为1~10。问题来了主站轮询时如果所有从站同时响应总线冲突。Modbus RTU规定从站收到请求后必须在10ms内开始响应且响应帧之间间隔≥3.5字符时间。但F103C8T6处理速度很快10个节点可能在1ms内全发完导致总线短路。解决方案每个从站响应前插入随机延时1~5ms用HAL_GetTick()生成伪随机数或者更可靠主站用“广播地址0”发请求所有从站监听但只允许地址匹配的从站响应其他静默我实测过10个节点地址1~10主站按顺序轮询每个请求间隔100ms总线稳定。但如果把间隔缩到20ms地址10的节点偶尔收不到请求——因为地址1的响应帧还没发完地址10的RX还在忙。6.2 工业级加固看门狗与掉电保存的必要性产线环境里电网波动导致MCU复位是常态。如果复位后温湿度寄存器清零上位机读到0℃可能触发误报警。必须加独立看门狗IWDG用LSI时钟32kHz超时时间设为1s主循环里每500ms喂狗。这样即使Modbus卡死IWDG强制复位比软件死循环可靠掉电检测PVD配置PVD监控VDD阈值设为2.8V。当电压跌落触发中断立刻把当前温湿度值存入备份寄存器BKP_DR1~BKP_DR4这些寄存器由VBAT供电掉电不丢失代码片段// PVD中断服务函数 void PVD_IRQHandler(void) { HAL_PWR_PVD_IRQHandler(); } void HAL_PWR_PVD_Callback(void) { // 电压跌落存数据 HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR1, (uint32_t)temp_value); HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR2, (uint32_t)humi_value); }6.3 上位机对接LabVIEW与Intouch的Modbus RTU配置要点热词里有“labview modbus rtu”“intouch 2014sp1 怎样与modbus rtu通讯”核心就两点LabVIEW用NI Modbus库Serial Port配置必须选“RTU Mode”Baud Rate、ParityNone、Data Bits8、Stop Bits1与STM32完全一致Address填从站地址1Start Address填0对应40001Intouch在Tag Name Dictionary里Device Type选“Modbus RTU”Communication Driver选“MODBUS_RTU”然后在Point Definition里Address填“40001”Data Type选“Integer”因为存的是256不是25.6关键陷阱Intouch默认把40001解析为“400001”多了一个0。必须在Address里写“00001”让它对应十进制地址1。我为此调了两天最后发现Intouch的地址格式是“寄存器类型偏移”40001的偏移是0所以填0。我在实际项目里把这套方案用在食品厂冷库监控12个节点-20℃~10℃范围连续运行18个月零故障。最后一次维护是更换了老化MAX485芯片。所以别被“stm32f103c8t6加密”“modbus tcp”这些热词带偏——工业现场要的不是新技术而是确定性。当你把CRC算准、DE/RE控好、电源滤干净一块最小系统板就是最可靠的Modbus从站。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门