基于STM8S003F3和SHT30的MODBUS RTU温湿度节点实现
2026/9/12 19:46:53 网站建设 项目流程

简介:一套基于STM8S单片机与SHT30温湿度传感器、采用MODBUS RTU协议与外部设备通信的完整IAR工程源码,适用于工业环境监测、物联网终端及嵌入式温湿度采集场景。资源共82个文件,以33个.h头文件和32个.c源文件为主体,涵盖I²C驱动、SHT30读取、UART1通信、MODBUS寄存器映射及CRC校验等模块;另含IAR工程配置(.ewp/.ewd/.eww)、编译烧录脚本(.bat)、调试日志(.log)和STM8S标准外设库手册(.chm),整体3.81MB,目录结构清晰,从底层驱动到协议应答均有对应文件可查。目前已有407人学习。包内不仅提供可编译的STM8S固件工程,还体现了V2.01版本对通信效率和稳定性的优化思路,开发者可直接导入IAR Embedded Workbench for STM8进行二次开发,或参照源码理解SHT30时序控制、MODBUS从站实现与异常处理,适合作为学习8位MCU与工业总线通信的参考范例。

1. 从“能读温湿度”到“能进工业总线”——这个基础工程踩过的坎

一个环境监测节点要接入现有SCADA或PLC系统,最省事的做法是给上位机暴露MODBUS RTU寄存器,而不是让用户去解析自定义帧。这个WSD_04_V2工程正是这么做的:STM8S003F3做主控,SHT30通过I2C采集温湿度,UART1接RS485收发器,对外走MODBUS协议。很多人以为8位机上跑MODBUS就是调个串口库的事,实际动手才会碰到几个典型问题:SHT30的CRC校验位处理不当会读回跳变的湿度值,MODBUS的寄存器高低字节顺序和上位机预期不一致会导致数据差一大截,I2C时钟在STM8S上用硬件外设还容易受中断干扰。这篇直接把这个工程从启动文件到协议栈拆开讲,适合正在做传感器节点接入总线、或者想把现有裸机程序加上标准工业协议的开发者参考。

2. 工程骨架拆解:IAR目录结构、启动流程与软I2C引脚分配

2.1 拿到源码先看什么

压缩包解压后,第一眼是ReCloseProject.ewpReCloseProject.ewd,这是IAR for STM8的工程文件,分别对应编译配置和调试配置。ReCloseProject.cspy.bat是命令行调试脚本,如果IAR版本对不上打不开GUI,可以直接用这个批处理启动cspy。真正的业务代码集中在Main.cDefine.cbsp_sht3x.cbsp_i2c_gpio.cUART1.cstm8s_it.c,其余是ST官方标准外设库和芯片头文件。

这个工程最有参考价值的不是功能多复杂,而是它的外设分工非常干净。bsp_i2c_gpio.c负责用GPIO模拟I2C时序,bsp_sht3x.c负责SHT30的命令交互,UART1.c负责MODBUS帧的物理收发,Main.c只做调度。实际项目里如果要把整个协议栈换成自定义格式,只需要改UART1.c的解析部分,传感器驱动可以原封不动。

2.1.1 文件职责速查
文件职责依赖
Main.c主循环调度、全局变量声明Define.h
bsp_i2c_gpio.cGPIO模拟I2C起始/停止/读写时序stm8s_conf.h
bsp_sht3x.cSHT30命令封装、温湿度数据解析bsp_i2c_gpio.h
UART1.c串口收发、MODBUS帧组装与解析stm8s_it.c
stm8s_it.cUART1接收中断入口、定时器中断stm8s_it.h
Define.c寄存器地址定义、系统参数初始化stm8s003f3.h

2.2 上电初始化顺序

stm8s_it.c里的main入口做完时钟配置后,进入外设初始化。STM8S003F3默认时钟源是HSI(高速内部RC),频率16MHz,工程里一般直接拿这个做系统时钟。注意这只芯片没有外部晶振引脚,别按STM32的习惯去找HSE相关代码。

以下是一般工程实际使用的初始化序列,各外设之间有关联顺序:

void System_Init(void) { CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1); // 16MHz GPIO_ConfigPorts(); // 配置所有GPIO方向 I2C_SoftInit(); // 拉高SDA/SCL UART1_Init(9600, UART1_WORDLENGTH_08D, UART1_STOPBITS_1); EnableInterrupts(); // 开全局中断 SHT30_Init(); // 发送软复位命令0x30 0xA2 }

这里UART1_Init的波特率是9600,MODBUS RTU默认常用的是9600和19200。如果现场总线速率要求更高,改这一处参数就行,但要注意后续CRC的超时时间窗也要跟着波特率缩放。SHT30_Init里发软复位命令是为了清掉传感器上电后的不确定状态,这一步不做,第一次读取可能拿到全0xFF。

2.2.1 软I2C引脚分配(以普通GPIO为例)

常见做法是把PB4和PB5分别映射为SDA和SCL,这两个引脚在STM8S003F3上支持开漏输出,配合外接4.7kΩ上拉电阻到3.3V就能稳定跑100kHz标准I2C时序。工程里bsp_i2c_gpio.h的宏定义允许直接改引脚:

#define I2C_SDA_PORT GPIOB #define I2C_SDA_PIN GPIO_PIN_4 #define I2C_SCL_PORT GPIOB #define I2C_SCL_PIN GPIO_PIN_5

用GPIO模拟I2C而不是用片上I2C外设,原因很简单:STM8S003F3的I2C外设在从机模式下的时序要求苛刻,而且在主频不高时容易和中断产生竞争。软I2C把时序控制权完全握在手里,虽然多占CPU,但在这个低采样率的温湿度场景里完全够用。引脚还可以随意改到任意GPIO,布线方便很多。如果后续要接多个I2C设备,或者采样频率要求超过10Hz,再考虑切回硬件I2C也不迟。

3. SHT30采集链路:I2C时序、CRC校验与温湿度换算

3.1 SHT30的I2C协议细节

SHT30从机地址是0x44(ADDR引脚接低电平)或0x45(接高电平)。工程里如果SHT30_ADDR宏定义的是0x44,说明硬件上将ADDR直接接地。测量命令用得最多的是0x2C 0x06,对应高重复性、时钟拉伸使能,每秒最多可以触发10次测量,完全满足环境监测的刷新需求。

有个关键点:SHT30返回的数据是6字节,格式是温度MSB、温度LSB、温度CRC、湿度MSB、湿度LSB、湿度CRC。CRC多项式是x^8 + x^5 + x^4 + 1,初始值为0xFF。不校验CRC直接算温湿度,大概率会在湿度读数上看到偶发跳变——别省这一步。

3.2 测量命令发送与数据读取

bsp_sht3x.c中,测量过程分两次I2C操作:先发命令,然后以重复起始条件读取数据。部分工程师习惯发送命令后切换为读模式,这在实际工程里容易因为Tsetup时间不够而丢第一字节数据。稳妥的流程是:

uint8_t SHT30_ReadTempHum(int16_t *temp, uint16_t *humi) { uint8_t buf[6]; uint8_t cmd[2] = {0x2C, 0x06}; I2C_Start(); I2C_WriteByte((SHT30_ADDR << 1) | 0); // 写方向 if (I2C_CheckACK() != 0) { I2C_Stop(); return 1; } I2C_WriteByte(cmd[0]); I2C_WriteByte(cmd[1]); I2C_Stop(); // 等待测量完成,高重复性典型耗时4.5ms DelayMs(10); I2C_Start(); I2C_WriteByte((SHT30_ADDR << 1) | 1); // 读方向 for (int i = 0; i < 6; i++) { buf[i] = I2C_ReadByte(i == 5); // 最后一个字节发NACK } I2C_Stop(); if (CRC8(buf, 2) != buf[2] || CRC8(&buf[3], 2) != buf[5]) { return 2; // CRC错误 } *temp = (int16_t)(((buf[0] << 8) | buf[1]) & 0xFFFC); *humi = (uint16_t)(((buf[3] << 8) | buf[4]) & 0xFFFC); return 0; }

这段代码里I2C_ReadByte(1)表示最后一字节要回复非应答,I2C协议规定读方在最后一个字节必须发NACK,否则传感器会认为主机还要继续读而拉低SDA,导致总线释放不了。另外& 0xFFFC的作用是屏蔽最低2位,SHT30数据手册明确说明这2位恒为0,保留它们会引入噪声。

CRC8的查表实现不多展开了,网上标准表直接抄就行。注意CRC8(buf, 2)只对前两字节做校验,参数不能写6,否则会把湿度数据也卷进来,校验必挂。

3.3 温湿度换算公式

拿到原始16位数据后,要转换成物理量。SHT30的线性公式是:

温度 = -45 + 175 * (原始值 / 65535) 湿度 = 100 * (原始值 / 65535)

浮点运算在STM8S上比较贵,提升运行效能的常见做法是先把原始值除以10,再转成定点数放到MODBUS寄存器里。例如,温度原始值raw_t为0x6A8A(即27306),算出实际温度27.06℃,工程里存储的寄存器值是2706,上位机拿到后除以100就是最终温度。这样整型运算快,而且避免在8位机上频繁触发软件浮点库。

4. MODBUS RTU从站:寄存器映射、CRC16与串口状态机

4.1 寄存器地址规划

MODBUS保持寄存器按惯例从40001开始编号,但在协议帧里地址是从0x0000开始的。工程里Define.c给出的映射关系决定了上位机看到的数据布局,一般会这样分配:

寄存器地址(协议帧)对应MODBUS地址内容数据类型
0x000040001温度值(放大100倍)uint16
0x000140002湿度值(放大100倍)uint16
0x000240003设备状态(0正常,1为CRC错误)uint16
0x000340004软件版本号(0x0201表示V2.01)uint16
4.1.1 数据结构体设计
typedef struct { uint16_t temperature; // 单位0.01℃ uint16_t humidity; // 单位0.01% uint16_t status; uint16_t version; } ModbusRegs_t;

需要特别提醒的是:MODBUS协议规定多字节数据大端在前(高字节先发)。温度值2706在帧里要发0x0A 0x92,而不是0x92 0x0A。如果总是出现上位机读到的数据乘256再加上另一个字节才对得上,就是字节序反了。

4.2 CRC16-MODBUS计算

MODBUS RTU的CRC是多项式0xA001的查表实现。收到帧后对从地址开始的整帧做CRC计算,算出的16位CRC与帧尾两字节比对。帧尾是低字节在前,这是新人最容易搞反的地方。下面这个函数是完整验证帧的核心:

uint16_t MODBUS_CRC16(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; while (len--) { crc ^= *data++; for (int i = 0; i < 8; i++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } return crc; }

整个计算过程里最重要的参数是多项式0xA001,这是MODBUS专用反向多项式,其他协议如CRC16-CCITT用的多项式不一样,不能通用。调用时传入的len必须是从站地址到数据末尾的完整长度,不包括CRC本身等两个字节。

4.3 主轮询状态机

协议栈核心在UART1.c,主循环不断检查串口有没有收到完整帧。MODBUS RTU用帧间隔来定界,标准是3.5个字符时间,9600波特率下大约是4ms。工程里用定时器中断累加判断超时,每收到一个字节就清零定时器,超过4ms没新字节就当作帧结束。

MODBUS_State_t MODBUS_Poll(void) { if (frameComplete == 0) return MODBUS_IDLE; // 校验地址是否匹配 if (rxBuffer[0] != SLAVE_ADDR) { frameComplete = 0; return MODBUS_ADDR_ERR; } // 校验CRC uint16_t crcCalc = MODBUS_CRC16(rxBuffer, rxLen - 2); uint16_t crcRecv = (rxBuffer[rxLen - 1] << 8) | rxBuffer[rxLen - 2]; if (crcCalc != crcRecv) { frameComplete = 0; return MODBUS_CRC_ERR; } switch (rxBuffer[1]) { case 0x03: // 读保持寄存器 MODBUS_HandleReadRegisters(); break; case 0x06: // 写单寄存器 MODBUS_HandleWriteSingleRegister(); break; default: MODBUS_SendException(0x01); // 非法功能码 } frameComplete = 0; return MODBUS_OK; }

注意crcRecv的拼接顺序是rxBuffer[rxLen-1] << 8 | rxBuffer[rxLen-2],因为帧尾CRC低字节在前。如果写反了,所有帧都会报CRC错。这个状态机只用了一个全局frameComplete标志,中断只负责填缓冲区,主循环做解析,结构简单,在裸机上不容易死锁。

4.3.1 读寄存器响应帧构造

MODBUS_HandleReadRegisters里先判断寄存器数量和起始地址合法性,响应帧格式是地址、功能码、字节数、数据、CRC。比如上位机读地址0x0000往后的2个寄存器,响应帧就是:

01 03 04 0A 92 07 6E 34 D6

拆开看:01从站地址,03功能码,04表示后面有4字节数据,0A 92是温度2706,07 6E是湿度1902(19.02%),最后34 D6是CRC。如果看到上位机能请求但一直报超时,检查响应帧字节数是不是比实际少,MODBUS调试工具对长度非常敏感。

5. 下载烧录常见错误与MODBUS Poll实测验证

5.1 IAR编译与烧录配置

用IAR for STM8打开ReCloseProject.ewp后,首先要确认工程选项里芯片型号是STM8S003F3。如果打开工程直接提示fatal error[lms001]: license check failed,这是许可证未激活或版本不匹配,需要打开IAR License Manager重新配置许可证,确认Host ID和许可证绑定的机器一致。

烧录时在Project > Options > Debugger里选择ST-LINK,Download标签页勾选Use flash loader。STM8S003F3的Flash是8KB,这个工程编译出来大约4KB,空间充裕。下载前先执行一次Project > Clean,有时候烧录后程序不跑是因为旧的调试信息和新代码错位。

5.2 用MODBUS Poll端到端验证

MODBUS Poll是调试从站最顺手的工具。工程下载后,USB转485接上STM8S板的A/B线,打开MODBUS Poll做如下设置:

Setup > Connection Serial Port: COM3(实际串口号) Baud Rate: 9600 Data Bits: 8 Parity: None Stop Bits: 1 Mode: RTU

连接后Setup > Read/Write Definition里设置从站地址为1,功能码选03,起始地址填0,读取数量填4。轮询周期设1000ms,对应每秒读一次温湿度。如果读到额定值,用手握住传感器,温度寄存器数值应该缓慢上升;对着传感器哈气,湿度寄存器的变化会非常灵敏。

5.3 常见异常定位

现象可能原因检查方向
能请求,响应超时串口参数不一致(波特率/校验位)核对8N1配置
读回的数值都是0SHT30 I2C通信失败CRC8校验是否通过
读回的数值翻倍字节序错位交换寄存器高/低字节
偶发一个寄存器跳变SHT30读取时机与测量完成时间重合延时至10ms以上
接上485后程序复位隔离电源不足增加TVS管和共地处理

5.4 顺手提高一下通信健壮性

如果在实测中发现偶发CRC错误,可以在stm8s_it.c接收中断里加一个简单的过滤条件:只把地址字段等于本机地址的帧放进缓冲区,其他直接丢弃。不推荐在裸机上上完整状态机队列,8位机SRAM太小,效果有限。另外,如果MODBUS Poll评估版到期不能保存配置,可以改用命令行方式启动,modpoll.exe -m rtu -a 1 -r 0 -c 4 -t 4:hex COM3 9600一句就能发起同样的读取,自动化测试脚本里更好用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询