核心目标:掌握工业领域最常用的 Modbus 通信协议,实现 STM32 作为 Modbus 从机与工业主机(如 PLC、组态软件)通信,完成 “温湿度数据上传(寄存器读取)+ 远程控制 LED(线圈操作)” 的工业级交互 —— 这是从 “消费级物联网” 迈向 “工业控制” 的关键技能,广泛应用于工厂自动化、智能仪表等场景。
一、Modbus 协议:工业设备的 “通用语言”(30 分钟)
1. 为什么工业设备偏爱 Modbus?
- 开源免费:无专利限制,任何厂商都可实现;
- 简单可靠:基于串口(RTU 模式)或以太网(TCP 模式),协议帧结构简单,抗干扰能力强;
- 主从架构:1 台主机(如 PLC)可控制多台从机(如传感器、执行器),适合工业多设备组网。
2. Modbus 核心概念(以 RTU 模式为例)
- 通信物理层:基于串口(RS485 差分信号,传输距离远,抗干扰),波特率常用 9600bps,数据格式:8 位数据位 + 1 位停止位 + 无校验(或偶校验)。
- 主从结构:
- 主机:主动发送指令(如 “读取从机 1 的温度寄存器”);
- 从机:被动响应(收到指令后执行操作并返回结果),每个从机有唯一地址(1-247)。
- 数据类型:
- 线圈(Coil):1 位,用于表示开关状态(如 LED 亮 / 灭),可读可写;
- 离散输入(Discrete Input):1 位,用于表示传感器输入(如按键),只读;
- 保持寄存器(Holding Register):16 位,用于存储数值(如温度、湿度),可读可写;
- 输入寄存器(Input Register):16 位,用于存储传感器采集的原始数据,只读。
- 功能码:指令的 “操作类型”,常用功能码:
0x01:读线圈状态(读 1 位开关量);0x05:写单个线圈(控制 1 位开关量,如 LED);0x03:读保持寄存器(读 16 位数值,如温湿度);0x06:写单个保持寄存器(修改数值,如阈值)。
二、硬件准备与接线(15 分钟)
1. 硬件清单
- STM32F103C8T6 开发板、AHT10 温湿度传感器(I2C);
- RS485 模块(如 MAX485,实现串口信号与 RS485 差分信号转换);
- 工业主机模拟器:电脑(安装 Modbus Poll 调试软件,模拟 PLC 作为主机);
- 杜邦线、面包板、3.3V 电源。
2. 接线方式(Modbus RTU 从机)
| 设备 | 引脚连接(STM32 ↔ RS485 ↔ 电脑) | 说明 |
|---|---|---|
| STM32 USART1_TX(PA9) | → RS485 DI(数据输入) | STM32 发送数据到 RS485 模块 |
| STM32 USART1_RX(PA10) | → RS485 RO(数据输出) | STM32 接收 RS485 模块数据 |
| STM32 PA8 | → RS485 DE/RE(方向控制) | 控制 RS485 收发方向(1 = 发送,0 = 接收) |
| RS485 A/B | → 电脑 USB-RS485 模块 A/B | 差分信号传输(A 接 A,B 接 B) |
| AHT10 | 同第八天(SCL→PB6,SDA→PB7) | 提供温湿度数据(存入保持寄存器) |
| LED(PC13) | 作为线圈(0x01 功能码控制) | 演示开关量控制 |
三、Modbus RTU 从机协议实现(60 分钟,核心代码)
目标:STM32 作为 Modbus 从机(地址 0x01),实现:
- 保持寄存器:地址 0x0000 存储温度(放大 10 倍,如 25.3℃→253),0x0001 存储湿度(放大 10 倍);
- 线圈:地址 0x0000 控制 LED(1 = 亮,0 = 灭);
- 支持功能码:0x01(读线圈)、0x05(写线圈)、0x03(读保持寄存器)。
1. 协议帧结构与解析逻辑
Modbus RTU 帧由 “地址码 + 功能码 + 数据 + CRC 校验” 组成,例如:
- 主机读保持寄存器指令(读从机 0x01 的 0x0000-0x0001 共 2 个寄存器):
01 03 00 00 00 02 C4 0B01:从机地址;03:功能码;00 00:起始寄存器地址;00 02:寄存器数量;C4 0B:CRC 校验。
- 从机响应帧:
01 03 04 00 FA 01 C8 75 3B01 03:地址和功能码;04:数据长度(4 字节);00 FA:温度 250(25.0℃);01 C8:湿度 456(45.6% RH);75 3B:CRC 校验。
2. 核心代码实现(STM32 作为从机)
c
/* USER CODE BEGIN 0 */
#include <string.h>
#include <stdio.h>
// Modbus从机配置
#define SLAVE_ADDR 0x01 // 从机地址
#define BAUDRATE 9600 // 波特率
#define COIL_COUNT 1 // 线圈数量(控制LED)
#define HOLD_REG_COUNT 2 // 保持寄存器数量(温度、湿度)
// 数据存储区
uint8_t coil[COIL_COUNT] = {0}; // 线圈(0x0000:LED状态)
uint16_t hold_reg[HOLD_REG_COUNT] = {0}; // 保持寄存器(0x0000:温度,0x0001:湿度)
// AHT10温湿度采集(复用第八天代码)
float temp = 0.0, humi = 0.0;
uint8_t AHT10_ReadData(I2C_HandleTypeDef *hi2c) { ... } // 更新temp和humi
// RS485方向控制(1=发送,0=接收)
#define RS485_TX_EN() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET)
#define RS485_RX_EN() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET)
// 串口接收缓冲区(存储主机指令)
uint8_t modbus_rx_buf[256] = {0};
uint16_t modbus_rx_len = 0;
// CRC16校验计算(Modbus RTU必用)
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 >>= 1;
crc ^= 0xA001; // 多项式
} else {
crc >>= 1;
}
}
}
return crc; // 高低位已交换,直接使用
}
// 解析主机指令并生成响应
void Modbus_ProcessFrame() {
uint8_t tx_buf[256] = {0};
uint16_t tx_len = 0;
uint16_t crc;
// 1. 校验从机地址(只处理目标地址为本机的指令)
if (modbus_rx_buf[0] != SLAVE_ADDR) {
modbus_rx_len = 0;
return;
}
// 2. 校验CRC(指令最后2字节是CRC)
crc = Modbus_CRC16(modbus_rx_buf, modbus_rx_len - 2);
if (*(uint16_t*)&modbus_rx_buf[modbus_rx_len - 2] != crc) {
modbus_rx_len = 0;
return; // CRC错误,不响应
}
// 3. 解析功能码并处理
switch (modbus_rx_buf[1]) {
case 0x01: // 读线圈状态(主机读取LED状态)
// 指令格式:[地址][0x01][起始线圈高8位][起始线圈低8位][数量高8位][数量低8位][CRC]
uint16_t start_coil = (modbus_rx_buf[2] << 8) | modbus_rx_buf[3];
uint16_t coil_num = (modbus_rx_buf[4] << 8) | modbus_rx_buf[5];
// 检查线圈地址是否越界
if (start_coil + coil_num > COIL_COUNT) {
// 异常响应:功能码最高位置1(0x81),异常码0x02(地址越界)
tx_buf[0] = SLAVE_ADDR;
tx_buf[1] = 0x81;
tx_buf[2] = 0x02;
tx_len = 3;
} else {
// 正常响应:[地址][0x01][数据长度][线圈状态字节][CRC]
tx_buf[0] = SLAVE_ADDR;
tx_buf[1] = 0x01;
tx_buf[2] = (coil_num + 7) / 8; // 计算字节数(1字节=8线圈)
// 填充线圈状态(此处只处理1个线圈,直接存coil[0])
tx_buf[3] = coil[0];
tx_len = 4;
}
break;
case 0x05: // 写单个线圈(主机控制LED)
// 指令格式:[地址][0x05][线圈地址高8位][线圈地址低8位][状态高8位][状态低8位][CRC]
uint16_t coil_addr = (modbus_rx_buf[2] << 8) | modbus_rx_buf[3];
uint16_t coil_state = (modbus_rx_buf[4] << 8) | modbus_rx_buf[5];
if (coil_addr >= COIL_COUNT) {
// 异常响应:功能码0x85,异常码0x02
tx_buf[0] = SLAVE_ADDR;
tx_buf[1] = 0x85;
tx_buf[2] = 0x02;
tx_len = 3;
} else {
// 正常响应:返回原指令(确认写入)
memcpy(tx_buf, modbus_rx_buf, 6); // 前6字节是地址+功能码+地址+状态
tx_len = 6;
// 控制LED(coil_state=0xFF00表示1,0x0000表示0)
coil[0] = (coil_state == 0xFF00) ? 1 : 0;
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, coil[0] ? GPIO_PIN_RESET : GPIO_PIN_SET);
}
break;
case 0x03: // 读保持寄存器(主机读取温湿度)
// 指令格式:[地址][0x03][起始寄存器高8位][起始寄存器低8位][数量高8位][数量低8位][CRC]
uint16_t start_reg = (modbus_rx_buf[2] << 8) | modbus_rx_buf[3];
uint16_t reg_num = (modbus_rx_buf[4] << 8) | modbus_rx_buf[5];
if (start_reg + reg_num > HOLD_REG_COUNT) {
// 异常响应:功能码0x83,异常码0x02
tx_buf[0] = SLAVE_ADDR;
tx_buf[1] = 0x83;
tx_buf[2] = 0x02;
tx_len = 3;
} else {
// 正常响应:[地址][0x03][数据长度][寄存器数据(高字节在前)][CRC]
tx_buf[0] = SLAVE_ADDR;
tx_buf[1] = 0x03;
tx_buf[2] = reg_num * 2; // 每个寄存器2字节
// 填充寄存器数据(温度和湿度放大10倍,转为整数)
for (uint16_t i = 0; i < reg_num; i++) {
tx_buf[3 + i*2] = (hold_reg[start_reg + i] >> 8) & 0xFF; // 高8位
tx_buf[4 + i*2] = hold_reg[start_reg + i] & 0xFF; // 低8位
}
tx_len = 3 + reg_num * 2;
}
break;
default: // 不支持的功能码
tx_buf[0] = SLAVE_ADDR;
tx_buf[1] = modbus_rx_buf[1] | 0x80; // 功能码最高位置1
tx_buf[2] = 0x01; // 异常码:不支持的功能码
tx_len = 3;
break;
}
// 4. 计算响应帧CRC并添加到末尾
crc = Modbus_CRC16(tx_buf, tx_len);
tx_buf[tx_len++] = crc & 0xFF; // CRC低8位
tx_buf[tx_len++] = (crc >> 8) & 0xFF; // CRC高8位
// 5. 发送响应(切换RS485为发送模式)
RS485_TX_EN();
HAL_UART_Transmit(&huart1, tx_buf, tx_len, 100);
HAL_Delay(1); // 等待发送完成
RS485_RX_EN(); // 切换回接收模式
// 清空接收缓冲区,准备下一次接收
modbus_rx_len = 0;
memset(modbus_rx_buf, 0, sizeof(modbus_rx_buf));
}
// 串口接收中断(接收主机指令)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
modbus_rx_len++;
// 超过缓冲区大小或检测到帧间隔(超过3.5个字符时间,9600bps时约3.5ms)
// 此处简化处理:假设主机指令最长20字节,满20字节或超时触发解析
if (modbus_rx_len >= 20 || HAL_UART_Receive_IT(&huart1, &modbus_rx_buf[modbus_rx_len], 1) != HAL_OK) {
Modbus_ProcessFrame(); // 解析指令
}
}
}
/* USER CODE END 0 */
3. 主循环与任务调度
c
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init(); // 初始化PA8(RS485方向)、PC13(LED)
MX_USART1_UART_Init(); // 初始化USART1(9600bps,8N1)
MX_I2C1_Init(); // 初始化I2C1(AHT10)
// 初始化RS485为接收模式
RS485_RX_EN();
// 开启串口中断接收(每次接收1字节)
HAL_UART_Receive_IT(&huart1, modbus_rx_buf, 1);
while (1) {
// 1. 定时采集温湿度,更新保持寄存器(每1秒)
static uint32_t last_update = 0;
if (HAL_GetTick() - last_update > 1000) {
AHT10_ReadData(&hi2c1); // 读取temp和humi
hold_reg[0] = (uint16_t)(temp * 10); // 温度放大10倍(25.3℃→253)
hold_reg[1] = (uint16_t)(humi * 10); // 湿度放大10倍
last_update = HAL_GetTick();
}
}
}
四、调试与验证(30 分钟)
1. 主机端配置(Modbus Poll 软件)
-
打开 Modbus Poll,点击 “Connection→Connect”:
- Connection Type:
RTU; - Port:选择电脑连接 RS485 模块的 COM 口;
- Baudrate:
9600; - Parity:
None; - Slave ID:
1(从机地址)。
- Connection Type:
-
测试功能码 0x03(读保持寄存器):
- 点击 “Setup→Read/Write Definition”:
- Function:
03 Read Holding Registers; - Start Address:
0; - Quantity:
2;
- Function:
- 点击 “OK”,软件将周期性发送读取指令,收到温湿度数据(如 253→25.3℃,456→45.6% RH)。
- 点击 “Setup→Read/Write Definition”:
-
测试功能码 0x05(写线圈):
- 点击 “Setup→Read/Write Definition”:
- Function:
05 Write Single Coil; - Start Address:
0; - Value:
1(或0);
- Function:
- 点击 “Send”,LED 将随指令亮灭。
- 点击 “Setup→Read/Write Definition”:
五、第十三天必掌握的 3 个核心点
- Modbus RTU 帧结构:理解 “地址 + 功能码 + 数据 + CRC” 的组成,能手动解析简单指令;
- 从机工作流程:接收指令→校验地址和 CRC→解析功能码→执行操作→返回响应;
- 工业通信特点:RS485 差分传输的抗干扰优势,主从架构在多设备组网中的应用。
总结
Modbus 是工业嵌入式开发的 “必备协议”,今天实现的从机功能可直接用于工厂中的传感器节点(如温湿度采集器)。进阶方向包括:
- 实现 Modbus TCP(基于以太网,适合远距离通信);
- 支持更多功能码(如 0x10 写多个寄存器);
- 多从机组网(通过地址区分不同设备)。

1409

被折叠的 条评论
为什么被折叠?



