【嵌入式开发学习】第13天:Modbus 协议与工业设备通信(工业级嵌入式入门)

核心目标:掌握工业领域最常用的 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 0B
    • 01:从机地址;03:功能码;00 00:起始寄存器地址;00 02:寄存器数量;C4 0B:CRC 校验。
  • 从机响应帧:01 03 04 00 FA 01 C8 75 3B
    • 01 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 软件)
  1. 打开 Modbus Poll,点击 “Connection→Connect”:

    • Connection Type:RTU
    • Port:选择电脑连接 RS485 模块的 COM 口;
    • Baudrate:9600
    • Parity:None
    • Slave ID:1(从机地址)。
  2. 测试功能码 0x03(读保持寄存器):

    • 点击 “Setup→Read/Write Definition”:
      • Function:03 Read Holding Registers
      • Start Address:0
      • Quantity:2
    • 点击 “OK”,软件将周期性发送读取指令,收到温湿度数据(如 253→25.3℃,456→45.6% RH)。
  3. 测试功能码 0x05(写线圈):

    • 点击 “Setup→Read/Write Definition”:
      • Function:05 Write Single Coil
      • Start Address:0
      • Value:1(或0);
    • 点击 “Send”,LED 将随指令亮灭。

五、第十三天必掌握的 3 个核心点

  1. Modbus RTU 帧结构:理解 “地址 + 功能码 + 数据 + CRC” 的组成,能手动解析简单指令;
  2. 从机工作流程:接收指令→校验地址和 CRC→解析功能码→执行操作→返回响应;
  3. 工业通信特点:RS485 差分传输的抗干扰优势,主从架构在多设备组网中的应用。

总结

Modbus 是工业嵌入式开发的 “必备协议”,今天实现的从机功能可直接用于工厂中的传感器节点(如温湿度采集器)。进阶方向包括:

  • 实现 Modbus TCP(基于以太网,适合远距离通信);
  • 支持更多功能码(如 0x10 写多个寄存器);
  • 多从机组网(通过地址区分不同设备)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值