实验概述
实验目的: 掌握 ModBus 通信协议的基本概念、调试方法以及串口转网口模块(NT1-B)的原理和配置步骤。
一句话概括: 用 STM32 通过串口连接 NT1-B 网口模块,模拟一台水分仪(Modbus 从机),然后在电脑上用 MThings 软件模拟上位机(Modbus 主机),通过网络对这台”假水分仪”进行读写操作。
GitHub 仓库: stm32-moisture-meter-modbus
系统架构
整个系统构建了一条完整的工业数据采集链路:
MThings(电脑,Modbus主机) ↓ TCP/IP(以太网)路由器 ↓ 以太网NT1-B 模块(串口转网口,透明转发) ↓ UART 串口(9600bps)STM32F103RC(模拟水分仪,Modbus从机)核心理解: NT1-B 就是一根”无形的串口线”。它把网络数据原样转发给 STM32,把 STM32 串口数据原样通过网络发回。STM32 完全不需要处理 TCP/IP 协议栈,只处理串口上的 Modbus RTU 帧。
硬件准备与接线
所需硬件
| 设备 | 说明 |
|---|---|
| STM32F103RC 开发板 | 主控芯片,运行 Modbus 从机程序 |
| NT1-B 串口转网口模块 | 将串口数据透明转发到网络 |
| 路由器 | 连接 NT1-B 和电脑 |
| 网线 ×2 | NT1-B 接路由器,电脑接路由器 |
| TTL 转 USB 模块 | 连接 USART1 调试口,查看 printf 输出 |
| 杜邦线若干 | 连接 STM32 与 NT1-B、TTL 模块 |
接线方式
STM32 ↔ NT1-B(Modbus 通信,USART2,9600bps):
STM32 NT1-B(串口侧)PA2 (TX) ──→ RXDPA3 (RX) ←── TXDGND ─── GND3.3V ─── VCC(看NT1-B规格,可能需要5V)注意:TX 接 RX,RX 接 TX,交叉连接! TX 接 TX 是最常见的接线错误,会导致完全没有数据传输。
STM32 ↔ TTL-USB 模块(调试输出,USART1,115200bps):
STM32 TTL-USB 模块PA9 (TX) ──→ RXDPA10 (RX) ←── TXDGND ─── GNDNT1-B ↔ 路由器 ↔ 电脑:
NT1-B 网口 ──网线──→ 路由器 LAN 口电脑网口 ──网线──→ 路由器 LAN 口NT1-B 模块配置
NT1-B 通过 EBYTE 网络配置工具(V5.1)进行配置:
| 参数 | 设置值 | 说明 |
|---|---|---|
| 工作模式 | TCP Server | 水分仪协议要求从机作为 TCP 服务器 |
| 本地 IP | 192.168.0.10 | 协议手册规定 |
| 本地端口 | 502 | 协议手册规定(Modbus 标准端口) |
| 串口波特率 | 9600 | 必须与 STM32 代码中 UART_BAUD 一致 |
| 数据位 | 8 | Modbus RTU 标准 |
| 停止位 | 1 | Modbus RTU 标准 |
| 校验位 | 无 | Modbus RTU 标准 |
NT1-B 的本质: 它是一个透明转发器——网口收到 TCP 数据 → 原样从串口发出;串口收到数据 → 原样从网口通过 TCP 发回。它不做任何协议解析,对 STM32 来说完全感知不到网络的存在。
STM32 程序设计
寄存器表
程序用一个静态数组 g_regs[0x20] 模拟 32 个 16 位寄存器:
| 地址 | 内容 | 类型 | 说明 |
|---|---|---|---|
| 0x0000 | 水分仪地址设置 | 读写 | 默认值 1 |
| 0x0001 | 水分参数设置 | 读写 | — |
| 0x0010 | 砂石种类 | 读写 | 1=粗砂,2=细砂… |
| 0x0011 | 实时水分值 | 只读 | 实际值×10,如 128=12.8% |
| 0x0012 | 温度值 | 只读 | 实际值×10,如 253=25.3℃ |
寄存器值采用实际值×10 的整数存储方式,是工业仪表的常规做法:避免浮点数运算,Modbus 协议寄存器为整数类型,MThings 通过配置系数 0.1 自动还原真实值。
支持的功能码
| 功能码 | 名称 | 说明 |
|---|---|---|
| 0x03 | 读寄存器 | 主机请求读取一个或多个寄存器,从机返回值 |
| 0x06 | 写单个寄存器 | 主机写入一个寄存器,从机回显确认 |
Modbus RTU 帧格式
[从机地址][功能码][数据...][CRC_L][CRC_H]例:[01][03][00][11][00][01][C5][CE] → 读寄存器0x0011,数量1CRC16 校验算法
CRC16-Modbus 是 Modbus RTU 协议的校验机制,用于检测数据传输错误:
- 初始化 CRC 寄存器为 0xFFFF
- 逐字节处理帧数据(不含 CRC 本身)
- 每个字节:先与 CRC 低字节异或,然后逐位处理 8 次
- 每位处理:最低位为 1 则右移后异或多项式 0xA001,否则只右移
- 最终得到 16 位 CRC 值,低字节先发、高字节后发
uint16_t CRC16_Modbus(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; else crc >>= 1; } } return crc;}帧处理流程
USART2 中断接收字节 → 存入 g_rx_buf ↓主循环每 10ms 检查一次 ↓g_rx_cnt > 0 ? → 再等 20ms 确保整帧收完 ↓Modbus_Process(): 1. 检查帧长度 >= 8 字节 2. 检查第1字节 == 从机地址 0x01 3. 验证最后2字节 CRC16 4. 根据功能码分发: - 0x03 → Handle_FC03() 读寄存器 - 0x06 → Handle_FC06() 写寄存器 5. 清空接收缓冲,准备下一帧传感器模拟
Sensor_Simulate() 函数每 1 秒更新一次水分和温度值,模拟真实传感器的读数变化:
| 参数 | 寄存器值范围 | 实际含义 | 每次变化幅度 |
|---|---|---|---|
| 水分值 | 50~200 | 5.0%~20.0% | ±1~±5(即 ±0.1%~0.5%) |
| 温度值 | 150~350 | 15.0℃~35.0℃ | ±1~±3(即 ±0.1℃~0.3℃) |
变化方式是带边界的随机游走,到达上下限时被限制住,不会超出合理区间。
MThings 上位机配置
协议选择(关键!)
新建连接时必须选择 “Modbus RTU over TCP”,不能选 “Modbus TCP”。
| 参数 | 设置值 |
|---|---|
| 协议类型 | Modbus RTU over TCP(关键!) |
| IP 地址 | 192.168.0.10(NT1-B 的 IP) |
| 端口 | 502 |
| 从机地址 | 1 |
添加读取点位
| 点位名称 | 寄存器地址 | 功能码 | 数据类型 | 倍率 | 说明 |
|---|---|---|---|---|---|
| 砂石种类 | 0x0010 | 03 | INT16 | 1 | 直接显示 |
| 实时水分 | 0x0011 | 03 | INT16 | 0.1 | 除以10显示(如128→12.8%) |
| 温度值 | 0x0012 | 03 | INT16 | 0.1 | 除以10显示(如253→25.3℃) |
Modbus RTU vs Modbus TCP
这是本实验中遇到并解决的关键问题:
| 特性 | Modbus RTU | Modbus TCP |
|---|---|---|
| 传输介质 | 串口(RS232/RS485) | 以太网(TCP/IP) |
| 帧头 | 从机地址(1字节) | MBAP 头(7字节) |
| 校验 | CRC16(2字节) | 无(TCP 自带校验) |
| 标准端口 | 无 | 502 |
NT1-B 透传场景下的区别:
- 用 Modbus TCP:STM32 收到的第一字节是
0x00(事务 ID 高字节),不是0x01(从机地址),地址校验直接失败,帧被丢弃 - 用 Modbus RTU over TCP:MThings 发送的就是标准 RTU 帧直接塞进 TCP payload,NT1-B 转发后 STM32 收到的和串口发的一模一样
遇到的问题与解决方案
问题1:MThings 显示”写错误”
现象: 用串口助手手动发送 Modbus 帧,STM32 能正常应答。但用 MThings 通过网络发送就失败。
原因: MThings 的协议选错了,选了 “Modbus TCP” 而不是 “Modbus RTU over TCP”。
解决: MThings 连接设置中,协议类型改为 “Modbus RTU over TCP”。
问题2:Keil 编译时 printf 浮点数不显示
现象: printf("Moisture=%.1f%%", val / 10.0) 输出为空或乱码。
原因: Keil ARMCC 编译器默认不链接浮点 printf 支持库。
解决: 改用整数运算代替浮点打印:
printf("Moisture=%d.%d%%", val / 10, val % 10);问题3:程序卡死在 delay_ms 循环
现象: Keil 调试时断点命中在 delay.c 的 SysTick 等待循环。
原因: main.c 中自定义了一个 Delay_ms()(裸循环实现),与正点原子 delay.c 冲突,且 delay_init() 未被调用。
解决: 删除自定义 Delay_ms(),在 main() 开头调用 delay_init(),所有延时改为框架函数 delay_ms()。
调试顺序建议
-
先只连 STM32 和电脑串口(不接网络)
- 烧录程序后,用串口助手连接 USART1(115200bps)查看调试输出
- 确认程序正常启动,能看到启动信息
- 确认传感器数据每秒更新一次
-
用串口助手手动测试 Modbus 帧
- 连接 USART2(9600bps),手动发送:
01 03 00 11 00 01 C5 CE - 应该收到应答帧,包含水分值
- 连接 USART2(9600bps),手动发送:
-
接入 NT1-B 模块
- 按接线图连接 STM32 USART2 ↔ NT1-B
- 浏览器访问 NT1-B 配置页,确认参数正确
ping 192.168.0.10确认网络连通
-
打开 MThings 进行测试
- 新建连接:Modbus RTU over TCP,192.168.0.10:502,从机地址 1
- 读寄存器 0x0011(水分值),观察是否返回数据
- 写寄存器 0x0010(砂石种类),写入后读回确认
实验结果
读操作验证
| 点位名称 | 寄存器原始值 | MThings 显示值 | 说明 |
|---|---|---|---|
| 砂石种类 | 1 | 1 | 直接显示 |
| 实时水分值 | 128(初始) | 12.8%(动态变化) | 每秒随机游走 |
| 温度值 | 253(初始) | 25.3℃(动态变化) | 每秒随机游走 |
写操作验证
| 操作 | MThings 发送帧 | STM32 应答帧 | 结果 |
|---|---|---|---|
| 写入值3 | 01 06 00 10 00 03 C8 0E | 01 06 00 10 00 03 C8 0E | 写入成功,回显相同 |
| 再次读取 | 01 03 00 10 00 01 84 0A | 01 03 02 00 03 F8 45 | 读回值=3,写入有效 |
调试口输出样例
=== Moisture Meter Modbus Slave ===Slave Addr: 0x01USART2: 9600 bps (Modbus)USART1: 115200 bps (Debug)Waiting for Modbus requests...[SENSOR] Moisture=12.8% Temp=25.3C[RX] 8 bytes: 01 03 00 11 00 01 D5 CAFC03: read reg 0x0011, count 1[TX] 7 bytes: 01 03 02 00 80 B9 FC知识点总结
整个实验的本质很简单,记住这三句话:
-
NT1-B 就是一根”无形的串口线”——它把网络数据透明转发给 STM32,STM32 完全不需要处理 TCP,只需要处理串口上的 Modbus RTU 帧。
-
STM32 模拟的是一张”寄存器表”——主机读就返回表里的值,主机写就更新表里的值,其他什么都不做。
-
MThings 是调试工具——它替你自动构造 Modbus 帧、计算 CRC、发送请求、解析应答,你只需要填寄存器地址和数值。
完整代码和工程文件请访问 GitHub 仓库:stm32-moisture-meter-modbus
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时










