ARTICLE DETAIL

资讯详情

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

STM8S+SHT30实现MODBUS RTU温湿度从站节点

STM8S+SHT30实现MODBUS RTU温湿度从站节点 简介这是基于STM8S单片机与SHT30温湿度传感器的MODBUS通信工程适用于环境监测与工业数据采集场景面向需要掌握嵌入式温湿度采集和串口通信的开发者。压缩包共82个文件由33个头文件、32个C源文件以及IAR工程配置、编译脚本、外设库手册组成整体大小约3.81MB结构清晰便于查阅。已有407人学习下载。工程实现了SHT30驱动、I²C引脚初始化、UART1串口上的MODBUS从站通信、寄存器映射、CRC校验与错误处理V2.01版本优化了通信效率和数据稳定性。开发者通过该工程可学习I²C时序、传感器寄存器读写、从站地址与功能码定义、报文组帧和响应解析等细节。代码按功能模块划分同时附带调试记录和工程备份能够帮助读者快速理解代码结构也便于移植到其他STM8S系列产品中。1. 这个STM8SSHT30的MODBUS温湿度节点到底在解决什么问题工业现场要采集温湿度最常见的不是“传感器读不出来”而是“数据怎么稳定地交给上一级系统”。很多工程师用STM32跑MODBUS主机程序但到了低成本、低功耗的从站节点却容易被一颗8位MCU卡住。WSD_04 V2这套程序要解决的核心问题就是让STM8S在IAR开发环境下把SHT30的温湿度数据整理成标准MODBUS RTU帧通过RS485或TTL串口交给PLC、触摸屏、Modbus Poll这类主站工具。它强调的不是“怎么读SHT30”而是“外部系统怎么用统一协议把数据拿走”并且能容忍现场常见的粘包、干扰和非法请求。这个标题里的V2暗示这是迭代后的版本重点已经从“能跑通”变成了“协议稳定、异常可处理、参数可配置”。适合三类人看一是要把传感器挂进已有MODBUS网络的嵌入式工程师二是想用状态机处理UART收帧的单片机开发者三是评估从站代码量和寄存器规划的系统集成人员。下面这套方案就是我按这个标题会落地的东西。2. 接口设计与寄存器规划先定好STM8S的“对外话语体系”2.1 为什么选MODBUS RTU而不是自研协议MODBUS RTU最大的价值不是协议本身有多高级而是生态成熟。你不需要自己写上位机电脑上装个Modbus Poll就能模拟主站现场PLC、组态软件、触摸屏大多也原生支持MODBUS协议。对STM8S这种Flash和RAM都有限的芯片来说MODBUS RTU的实现成本也极低只依赖一个UART和一个1ms定时器帧格式只有地址、功能码、数据、CRC16四段。自研协议的问题在于你每加一个从站就要给上位机做一套解析还要自己实现应答超时和错误码。MODBUS RTU的“请求-响应”模型天然能套进传感器轮询场景。另一个容易忽略的原因是CRC16-MODBUS的成熟度网上有大量测试向量、现成工具和调试手段而自研协议的校验往往到现场才发现约束不够。所以在资源允许的前提下用MODBUS RTU是让STM8S传感器节点“能被别人接走”的最短路径。2.2 SHT30通过I2C接入STM8S的硬件连接要点SHT30是I2C接口默认7位地址是0x44ADDR引脚接高电平后变成0x45。STM8S的硬件I2C在部分库函数版本下容易卡在BUSY状态所以我一般直接用GPIO模拟I2C把PB4和PB5配置成开漏输出带上拉电阻。这样做还有一个好处I2C时序的延时函数可以用delay_us()精确控制不会被IAR的优化选项影响太多。硬件上注意三点SCL和SDA各加一只4.7kΩ上拉电阻Sensor与MCU连线尽量控制在10cm以内SHT30的供电范围是2.4V~5.5V如果STM8S用3.3V可以共用一个LDO但要保证传感器上电后至少等10ms再发第一次I2C命令如果节点走RS485那么STM8S的UART TX/RX要经过485收发器再出去收发器DE/RE引脚通常接在同一个GPIO上用做方向切换这个引脚在初始化时必须先置为接收方向否则一上电就占用总线。2.3 寄存器映射表让外部主站一眼看懂的数据布局写MODBUS从站程序前先定寄存器地址这个顺序不能反。如果先把代码写完再补协议表后面接PLC或者组态软件时会非常痛苦。这里给出一个经过实际项目验证的寄存器映射方案特点是“只读区在前写读区在后”外部主站只需要按这张表就能完成数据采集和参数配置。寄存器地址类型内容单位说明0x0000只读温度值0.01℃有符号25.30℃对应25300x0001只读湿度值0.01%RH无符号60.12%对应60120x0002只读设备状态bitbit0为传感器故障, bit1为通信异常0x0010读写从站地址1-247默认1写入后重启生效0x0011读写采样间隔秒范围1-600表里的温度用有符号整型是因为工业环境里有负温湿度用无符号整型因为相对湿度不可能为负。单位全部取0.01是为了让Modbus Poll和组态软件里能直接显示带两位小数的数值而不需要上位机再除10。功能码分配也按惯例来03读保持寄存器、04读输入寄存器都可以读0x0000和0x000106写单个寄存器用来改0x0010和0x001116写多个寄存器在V2版本里只用于恢复出厂配置。0x0002设备状态寄存器不要存连续变化的错误码只放标志位不然上位机里要做一堆判断。这张映射表同时要写进代码头的注释里每次改协议都先改表再改代码。还有一点0x0010从站地址寄存器一定要支持“写入后保存到EEPROM或Flash”不然设备断电后地址又变回1Modbus Poll里怎么扫都扫不到。3. IAR环境下STM8S的驱动实现从I2C读SHT30到串口帧接收3.1 在IAR EW for STM8里搭建最小工程与时钟配置IAR for STM8这个工具链和MDK、STM32CubeIDE很不一样它没有图形化的初始化代码生成器裸机程序的起点就是中断向量文件和主循环。常见做法是新建空工程后在stm8s.h里把主频定义成内部16MHz RC振荡器因为后面UART波特率、I2C时序全都依赖这个时钟。如果选错型号比如把STM8S105K4配成STM8S103F3链接时会出现外设寄存器地址越界而且不太好排查。编译选项里有个坑IAR的项目 Options C/C Compiler 默认可能是Balanced优化但如果你为了省Flash选High Size编译器可能会把delay_ms里的空循环整个优化掉导致SHT30的时序一塌糊涂。我的做法是把I2C驱动文件和延时函数所在文件单独设为Balanced其余文件用Size优化。另外IAR安装后会提示fatal error [Lms001]: license check failed多数情况下不是License真的过期而是License Manager里的32位组件没装好卸载重装时别跳过IAR IDE自带的驱动签名更新。启动文件方面STM8S的中断向量表是固定的USART接收中断和定时器中断要在项目里以__interrupt关键字声明并放在中断向量文件对应的位置。不要在普通函数上直接加__interrupt然后靠链接器去猜那样会出现重复向量程序一进中断就跑飞。3.2 用命令码方式驱动SHT30单次测量和周期测量的取舍SHT30支持周期测量和单次测量。周期测量能省电但数据是“后台准备好”的从站必须记录数据新鲜度标志Modbus主站来读时可能读到上一周期的旧值。单次测量更贴合MODBUS的请求-响应模型代价是每次查询前要有一次测量等待时间。我的做法是主循环里每250ms触发一次单次测量量完立即存到全局变量Modbus主站来读时直接返回这个变量。这样传感器数据的更新频率远高于上位机常见的1秒轮询周期即使主站请求频率很高每次读到的也是最近一次完整测量结果。#define SHT30_ADDR_W 0x88 // 0x44 1I2C写地址 #define SHT30_CMD_HIGH 0x2C #define SHT30_CMD_LOW 0x06 uint8_t sht30_read_temp_humi(int16_t *temp, uint16_t *humi) { uint8_t buf[6]; // 1. 发送单次测量命令 i2c_start(); i2c_write(SHT30_ADDR_W); i2c_write(SHT30_CMD_HIGH); i2c_write(SHT30_CMD_LOW); i2c_stop(); delay_ms(15); // 高可重复性测量, 典型15ms // 2. 读6字节温度数据和CRC8, 湿度数据和CRC8 i2c_start(); i2c_write(SHT30_ADDR_W | 0x01); for (int i 0; i 6; i) { buf[i] i2c_read(i 5); // 前5字节回ACK, 末字节回NACK } i2c_stop(); // 3. 每3字节做一次SHT30专用CRC8, 多项式0x31 if (sht30_crc8(buf[0], buf[1], buf[2]) ! 0) return 1; if (sht30_crc8(buf[3], buf[4], buf[5]) ! 0) return 1; *temp (int16_t)(((uint16_t)buf[0] 8) | buf[1]); *humi (uint16_t)(((uint16_t)buf[3] 8) | buf[4]); // 4. 换算成0.01单位: 2530表示25.30℃, 6012表示60.12%RH *temp (int16_t)(((int32_t)*temp * 17500L) / 65535L - 4500L); *humi (uint16_t)(((uint32_t)*humi * 10000L) / 65535L); return 0; }这段代码的关键是“必须先发命令再从无寄存器地址的I2C读接口读数据”SHT30不像EEPROM那样带寄存器地址。i2c_read参数里的i 5直接决定ACK和NACK最后一个字节如果不回NACK传感器会认为主站还想继续读总线被SDA拉低。换算公式里的17500对应SHT30量程4500对应-45℃偏移和STM32例程里那套浮点公式完全等价但改成整数运算能省下STM8S做浮点运算的开销。需要特别留意的是(int32_t)*temp这个强转IAR里int默认16位如果你直接写*temp * 17500L编译器先按16位乘再转32位数据一溢出结果就是负数。3.3 UART接收状态机解决MODBUS帧的粘包与超时MODBUS RTU没有帧头帧尾靠的是帧间静默时间做分帧标准要求3.5个字符时间。在19200波特率下一个字符约0.52ms3.5字符约1.8ms。程序里不可能精确卡到1.8ms所以无论如何都要建一个1ms基础定时器用它记录“帧内字节间隔”超过阈值就认为这一帧结束。这个功能不能放在主循环里轮询因为在临界状态下主循环可能正在做SHT30的15ms测量等待会把帧尾漏掉。typedef enum { RX_IDLE, RX_RECEIVING, RX_COMPLETE } rx_state_t; rx_state_t rx_state RX_IDLE; uint8_t rx_buf[64]; uint8_t rx_len 0; volatile uint16_t g_rx_timeout_cnt 0; // USART接收中断 void USART_RX_IRQHandler(void) { uint8_t b UART_ReceiveData8(); if (rx_state RX_IDLE) { rx_state RX_RECEIVING; rx_len 0; } if (rx_len 64) { rx_buf[rx_len] b; } g_rx_timeout_cnt 0; // 收到字节就清零计时 } // 1ms定时器中断 void TIMER_IRQHandler(void) { if (rx_state RX_RECEIVING g_rx_timeout_cnt 3) { rx_state RX_COMPLETE; // 3ms内没有新字节, 认为一帧结束 } }这个状态机处理粘包的思路是如果两帧之间间隔小于超时阈值它们会被合并成一帧最后CRC校验一定不通过直接丢弃如果主站Modbus Poll的轮询周期比较快每一帧在3ms计时阈值内都能收完就不会发生错帧。rx_buf[64]对于温湿度传感器足够用但如果以后要扩展写多个寄存器记得把缓冲区长度提到128同时监控rx_len等于64的情况避免覆盖。主循环里的处理代码不会和中断产生竞争因为只有主循环把所有字节都收完、状态切到RX_COMPLETE后才会去读rx_buf而下一个新帧的起始字节会把状态切回RX_RECEIVING此时主循环必须已经把CRC算完。3.4 CRC16-MODBUS校验的计算细节CRC16不是MODBUS独有但MODBUS的CRC算法和常见的CRC16-CCITT完全不同网上抄代码时最容易抄错。MODBUS CRC的初始化值是0xFFFF多项式是0x8005的反射形式代码里直接写成0xA001按位异或8次。这里给出一个没有查表、适合STM8S这样Flash小的逐位实现uint16_t modbus_crc16(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ data[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }关键在于发送顺序RTU帧里的CRC是“低字节在前高字节在后”。假设算出来是0x1234那么发送序列最后两字节是0x34、0x12。在IAR的Memory窗口或者串口助手里看到数据发送正常但Modbus Poll一直报CRC错误先检查这个字节序。如果想用查表版本提高速度STM8S的Flash也放得下256字节表但要注意表里的每个值必须是对多项式0xA001逐位计算出来的不要用PC端工具自动生成的那种针对0x8005正序的表。调试时可以用Modbus Poll自带的主站功能给从站发一个已知帧01 03 00 00 00 02 C4 0B如果从站能正确回温度、湿度就说明CRC收发都通了。4. MODBUS从站处理把功能码变成寄存器读写动作4.1 功能码03/04/06/16的处理框架主循环发现rx_state为RX_COMPLETE后并不会立即切换RS485方向而是先把rx_buf里的完整帧交给协议处理函数。对RS485来说应答前要把DE引脚拉高到发送方向发送完最后一字节再延时一小段拉低否则485收发器的输出会在总线上产生一个毛刺。uint8_t slave_addr 1; // 默认从站地址 extern int16_t g_temperature; // 0.01℃ extern uint16_t g_humidity; // 0.01%RH void modbus_handle_frame(void) { uint8_t func rx_buf[1]; if (rx_buf[0] ! slave_addr rx_buf[0] ! 0) return; // 先校验CRC再处理功能码 uint16_t crc_received (uint16_t)rx_buf[rx_len - 1] 8 | rx_buf[rx_len - 2]; if (modbus_crc16(rx_buf, rx_len - 2) ! crc_received) return; switch (func) { case 0x03: // 读保持寄存器 case 0x04: // 读输入寄存器 modbus_read_registers(rx_buf[1]); break; case 0x06: // 写单个寄存器 modbus_write_single(); break; case 0x10: // 写多个寄存器 modbus_write_multi(); break; default: send_exception(0x01); // 非法功能码 } }这里把03和04放在同一个处理函数里对传感器从站来说是完全正确的外部可以用03读保持寄存器也可以用04读输入寄存器。modbus_read_registers内部要判断寄存器区域0x0000和0x0001返回温湿度0x0002返回状态标志0x0010、0x0011返回配置参数。响应帧格式为地址1字节功能码1字节字节数1字节数据N字节CRC16两字节。在IAR的调试窗口里可以设一个断点在modbus_handle_frame开头观察rx_buf[0]和rx_len就能确认状态机有没有把帧完整收进来。4.2 异常码与非法地址响应让Modbus Poll测试一次通过用Modbus Poll测试从站时最常见的现象不是不回包而是回了一个错误的异常码。MODBUS明确区分了“非法数据地址”和“非法数据值”很多实现里把这两者混在一起导致组态软件把从站判定成故障。正确处理原则是寄存器地址超出映射表范围回异常码0x02寄存器地址合法但写入值越限回0x03收到不支持的功能码回0x01。异常响应帧必须带CRC并且功能码要置最高位。异常码含义触发条件0x01非法功能码收到03/04/06/16之外的功能码0x02非法数据地址寄存器地址超出映射表范围0x03非法数据值写入值超过1-247或采样间隔超过6000x04从站设备故障SHT30连续10次CRC校验失败void send_exception(uint8_t code) { uint8_t resp[5]; resp[0] rx_buf[0]; // 原地址 resp[1] rx_buf[1] | 0x80; // 功能码最高位置1 resp[2] code; send_rtu_frame(resp, 3); // 内部完成追加CRC }实现时modbus_read_registers里要先判断起始地址和寄存器数量是否同时合法。例如请求01 03 00 00 00 02起始地址0x00数量2此时0x00和0x01都在映射表内才允许构造6字节数据区。如果请求起始地址是0x0012数量2那么0x0012本身越界直接回异常码0x02如果请求0x0011数量20x0011合法但0x0012越界同样回0x02。Modbus Poll收到异常码后会在主站界面显示对应的英文文本只要按上表实现主站就能明确区分故障原因。4.3 从站地址配置与广播帧处理从站地址寄存器0x0010写入后应该保存到STM8S的EEPROM里下次上电直接加载。STM8S的EEPROM是自带字编程接口的但写入时要关闭中断防止写EEPROM过程中UART中断计时被打断。更安全的方式是采用“双缓冲生效”写入0x0010后先存到RAM变量pending_addr收到重启命令或写入0x0011后再把pending_addr真正写入EEPROM。这样做能让Modbus Poll连续下发多个寄存器时不会因为地址切换丢帧。广播帧的地址是0x00按MODBUS协议从站收到广播帧后执行写操作但不回任何响应。在温湿度节点里广播帧最常见的用途是同步所有传感器的采样间隔。所以modbus_write_single里要判断rx_buf[0]为0时设置一个广播标志在send_rtu_frame前拦截if (rx_buf[0] 0) { // 广播帧不响应, 但已经执行写操作 g_is_broadcast 1; }如果广播帧同时携带了写0x0010从站地址的操作应该直接忽略因为所有从站同时改成一个地址会造成总线冲突。这也是为什么我在协议表里把从站地址寄存器设计成“写入后需要恢复出厂或重启才生效”而不是一写就切。5. 用Modbus Poll做回归测试验证温湿度数据和IAR调试技巧5.1 最小验证步骤把从站程序编译下载到STM8S后第一步不要急着挂到真实总线上用USB转RS485模块连接电脑。打开Modbus Poll按Modbus Poll的Setup菜单配置串口参数波特率9600或19200、8位数据、无校验、1位停止位从站地址填1。在“Read/Write Definition”里设置功能码03起始地址0读取长度2轮询间隔1000ms。如果CRC和地址都对温度、湿度两列会按0.01精度刷新。验证写功能码时把功能码改成06往0x0011写入5再读回来确认变成5。这里要注意Modbus Poll的密钥或未注册版本会有间隔弹窗但不影响基本测试。真正需要盯的是“离线”时间如果Modbus Poll里某些请求出现超时错误先在串口助手里看从站有没有发出响应帧。5.2 常见坑SHT30 CRC、485方向切换、IAR编译优化SHT30的数据里自带CRC8很多人会忽略它导致温度偶尔跳变。SHT30的CRC8多项式是0x31初值0xFF和MODBUS的CRC16完全是两套东西别复用函数。以我的经验SHT30单次测量后15ms等待时间不能压缩IAR的High优化会改变delay_ms里的空循环所以延时函数要么写汇编要么把优化级别调到Low。RS485方向切换的坑最隐蔽。发送完最后一个字节后如果立即拉低DE485收发器会把最后一个停止位截掉主站就会收不到响应或CRC错误。标准做法是发送完成中断里再延时半个字节时间再拉低方向引脚。IAR里可以在UART发送完成后软件延时delay_us(50)19200波特率下一个字节约520us延时50us到100us足够安全。最后是IAR调试的细节在IAR的Watch窗口观察rx_state时它可能停在RX_RECEIVING状态这不是Bug而是因为1ms定时器没有触发。排查定时器中断优先级是否正确STM8S的ITC模块里定时器中断优先级应低于UART接收中断这样才能保证Modbus帧在接收过程中不会被定时器打断到丢字节。本文还有配套的精品资源点击获取
返回列表