从零吃透 Modbus 通信|第 6 章:线圈功能码 05/0F 实现 & Modbus‑TCP 基础入门

博客定位:扩展 Modbus 离散 IO 功能,完成 RTU 全部常用功能码;对比 RTU 与 TCP 差异,建立以太网 Modbus 认知,可直接发布 CSDN。

前言

前面章节我们完成保持寄存器(03/06/16)主机与从机、异常码、故障排查。在工业现场除了寄存器存储数值,还有大量开关量:继电器输出、光耦输入、阀门开关,Modbus 使用线圈 (Coil) 与**离散输入 (Discrete Input)**处理开关信号。

很多教程只讲保持寄存器,跳过线圈功能码,实际 IO 板、PLC、继电器模块大量使用 05、0F 功能码。本章手写实现05 强制单线圈、0F 强制多线圈;同时简单介绍 Modbus‑TCP 协议,对比 RTU 与 TCP 的异同。

读完本章你将掌握:

  1. 线圈、离散输入、保持寄存器、输入寄存器四类对象区别
  2. 05 功能码写单个线圈原理 + STM32 裸机代码
  3. 0F 功能码批量写多个线圈原理 + STM32 裸机代码
  4. 线圈位操作、字节打包、掩码处理工程实现
  5. Modbus‑TCP 报文结构、和 RTU 的核心差异

<hr>

1 Modbus 四类存储对象完整区分

Modbus 协议定义 4 种独立地址空间,地址互相独立,不能混用,很多调试故障来源于混淆四类空间。

对象 功能码 读写属性 用途 数据宽度
线圈 Coils 01 读线圈 / 05 写单线圈 / 0F 写多线圈 可读可写 继电器输出、阀门、开关输出 1bit(开关 0/1)
离散输入 Discrete Inputs 02 读离散输入 只读 外部光耦输入、按键状态、限位信号 1bit(开关 0/1)
保持寄存器 Holding Registers 03 读 /06 写单 /10 写多 可读可写 参数、转速、温度、电压数值 16bit
输入寄存器 Input Registers 04 读输入寄存器 只读 传感器实时采集值 16bit

重点:线圈是按 bit 操作。8 个线圈打包成 1 个字节。低位代表低地址线圈。 例如 1 个字节:bit0 → 线圈地址 0;bit1 →线圈地址 1 ... bit7 →线圈地址 7。
同样注意手册大多1 起始 ,代码数组下标使用0 起始,地址需要减 1。

<hr>

2 05 功能码:强制单个线圈(写单路开关输出)

2.1 协议帧格式

主机请求帧:

表格

从机地址 功能码 0x05 线圈地址高 线圈地址低 状态高字节 状态低字节 CRC 低 CRC 高

线圈状态约定:

  • 置 ON:0xFF 0x00
  • 置 OFF:0x00 0x00

注意:不是写入 1 和 0!协议规定固定使用0xFF00代表打开,0x0000代表关闭。

从机正常应答:原样返回主机请求帧;参数非法返回异常帧。

2.2 从机线圈数据结构定义

复制代码
#define COIL_MAX_NUM    16      //最大线圈数量,16路开关输出
uint8_t coil_buf[(COIL_MAX_NUM +7)/8];  //位存储,每字节存8路线圈

// 设置指定线圈
void Modbus_Coil_Set(uint16_t coil_addr, uint8_t val)
{
    uint16_t byte_idx = coil_addr / 8;
    uint8_t bit_idx = coil_addr % 8;
    if(coil_addr >= COIL_MAX_NUM) return;

    if(val != 0)
    {
        coil_buf[byte_idx] |= (1<<bit_idx);
    }
    else
    {
        coil_buf[byte_idx] &= ~(1<<bit_idx);
    }
}

// 读取指定线圈状态
uint8_t Modbus_Coil_Get(uint16_t coil_addr)
{
    uint16_t byte_idx = coil_addr /8;
    uint8_t bit_idx = coil_addr %8;
    if(coil_addr >= COIL_MAX_NUM) return 0;
    return (coil_buf[byte_idx] >> bit_idx) & 0x01;
}

2.3 从机 05 功能码处理函数

复制代码
uint8_t Slave_Write_Single_Coil(void)
{
    uint16_t coil_addr;
    uint16_t coil_cmd;
    uint8_t tx_len=0;

    coil_addr = (rx_buf[2] <<8) | rx_buf[3];
    coil_cmd  = (rx_buf[4] <<8) | rx_buf[5];

    //地址越界判断
    if(coil_addr >= COIL_MAX_NUM)
    {
        Slave_Return_Exception(0x02);
        return 5;
    }

    //校验合法命令,只允许0xFF00或者0x0000
    if(coil_cmd != 0xFF00 && coil_cmd !=0x0000)
    {
        Slave_Return_Exception(0x03);
        return 5;
    }

    //设置线圈状态
    if(coil_cmd == 0xFF00)
    {
        Modbus_Coil_Set(coil_addr,1);
        //业务:可以在这里控制IO输出继电器
        //Relay_ON(coil_addr);
    }
    else
    {
        Modbus_Coil_Set(coil_addr,0);
        //Relay_OFF(coil_addr);
    }

    //正常应答,原样返回请求帧
    tx_buf[tx_len++] = SLAVE_ADDR;
    tx_buf[tx_len++] = 0x05;
    tx_buf[tx_len++] = rx_buf[2];
    tx_buf[tx_len++] = rx_buf[3];
    tx_buf[tx_len++] = rx_buf[4];
    tx_buf[tx_len++] = rx_buf[5];
    Modbus_Append_CRC(tx_buf,&tx_len);
    return tx_len;
}

2.4 主机 05 发送函数

复制代码
//写单个线圈:slave_addr从机地址,coil_addr线圈地址,val 1打开 0关闭
void Modbus_Master_Write_Single_Coil(uint8_t slave_addr,uint16_t coil_addr,uint8_t val)
{
    tx_len =0;
    modbus_tx_buf[tx_len++] = slave_addr;
    modbus_tx_buf[tx_len++] =0x05;
    modbus_tx_buf[tx_len++] = (coil_addr>>8)&0xFF;
    modbus_tx_buf[tx_len++] = coil_addr &0xFF;

    if(val)
    {
        modbus_tx_buf[tx_len++] =0xFF;
        modbus_tx_buf[tx_len++] =0x00;
    }
    else
    {
        modbus_tx_buf[tx_len++] =0x00;
        modbus_tx_buf[tx_len++] =0x00;
    }

    Modbus_Append_CRC(modbus_tx_buf,&tx_len);
    rx_len =0;
    modbus_tick = HAL_GetTick();
    USART_Send_Data(modbus_tx_buf,tx_len);
}

3 0F 功能码:强制多线圈(批量写多路开关)

3.1 协议说明 0F 实现一次性批量设置多个线圈状态。

主机请求帧: 从机地址 0x0F 起始地址高 起始地址低 线圈数量高 线圈数量低 字节数 N个字节数据 CRC低 CRC高

每一个 bit 代表一路线圈,字节最低 bit 对应线圈起始地址

从机应答:返回:从机地址 +0x0F + 起始地址 2 字节 + 线圈数量 2 字节 +CRC。不返回线圈数据。

3.2 从机 0F 批量写线圈实现

复制代码
uint8_t Slave_Write_Multi_Coil(void)
{
    uint16_t start_addr = (rx_buf[2]<<8)|rx_buf[3];
    uint16_t coil_num   = (rx_buf[4]<<8)|rx_buf[5];
    uint8_t byte_cnt    = rx_buf[6];
    uint16_t i;
    uint8_t bit_pos;
    uint8_t tx_len=0;

    //地址越界检查
    if(start_addr + coil_num > COIL_MAX_NUM)
    {
        Slave_Return_Exception(0x02);
        return 5;
    }

    //字节数校验
    uint8_t expect_byte = (coil_num +7)/8;
    if(byte_cnt != expect_byte)
    {
        Slave_Return_Exception(0x03);
        return 5;
    }

    //循环批量写入线圈
    for(i=0; i< coil_num; i++)
    {
        uint8_t data_byte = rx_buf[7 + i/8];
        bit_pos = i%8;
        uint8_t bit_val = (data_byte >> bit_pos) & 0x01;
        Modbus_Coil_Set(start_addr + i, bit_val);
        //业务层同步控制IO
    }

    //组装应答帧
    tx_buf[tx_len++] = SLAVE_ADDR;
    tx_buf[tx_len++] = 0x0F;
    tx_buf[tx_len++] = rx_buf[2];
    tx_buf[tx_len++] = rx_buf[3];
    tx_buf[tx_len++] = rx_buf[4];
    tx_buf[tx_len++] = rx_buf[5];
    Modbus_Append_CRC(tx_buf,&tx_len);
    return tx_len;
}

常见踩坑:线圈数量不是 8 整数倍时,最后字节高位的无效 bit,设备需要忽略,不处理。

4 Modbus‑TCP 基础入门

4.1 RTU 与 TCP 对比

Modbus‑RTU:串口 485 物理层,帧尾携带 CRC 校验;一主多从;波特率控制通信速度。 Modbus‑TCP:以太网物理层,基于 TCP 协议,默认端口 502没有 CRC 校验,可靠性依靠 TCP。

Modbus‑TCP 报文头部增加 MBAP 头(7 字节) MBAP 头结构:

事务标识 2B 协议标识 2B 长度 2B 单元标识 1B

后面紧跟 PDU(和 RTU 除去 CRC 的 PDU 完全一样)。

  • 事务标识:主机自增序号,匹配请求应答;
  • 协议标识:固定 0;
  • 长度:后面字节总数量;
  • 单元标识:相当于 RTU 的从机地址。

核心:Modbus‑TCP 的 PDU 部分,和 RTU 去掉 CRC 后的 PDU 一模一样,业务逻辑代码可以大量复用,只需要处理 MBAP 包头。

4.2 RTU 与 TCP 开发的区别

  1. RTU 依靠 3.5 字符空闲判断帧结束;TCP 是流式字节流,依靠 MBAP 头长度字段解析完整帧。
  2. RTU 需要 CRC 校验;TCP 不需要 CRC。
  3. RTU 总线一问一答;TCP 可以建立长连接,支持多请求。

工程建议:已经写好的 RTU 功能码处理函数,移植 Modbus‑TCP 时,只需要剥离 CRC,增加 MBAP 包头解析,业务逻辑几乎不用改动。

5 线圈开发工程避坑总结

  1. 05 功能码不是写入 1 和 0,必须使用 0xFF00/0x0000,很多新手这里出错。
  2. 线圈是 bit 存储,注意字节内 bit 顺序,低位对应低线圈地址
  3. 批量写线圈 0F,线圈总数不是 8 倍数时,最后字节高位无效 bit 直接丢弃,不要写入。
  4. 线圈地址同样区分手册 1 起始,代码 0 起始。 5.Modbus‑TCP 无 CRC,不要把 RTU 的 CRC 带到 TCP 报文中。

6 本章总结

  1. 完整区分 Modbus 四类地址空间:线圈、离散输入、保持寄存器、输入寄存器。

  2. 实现 05 写单线圈、0F 批量写多线圈主机与从机代码,可以直接控制继电器开关量 IO。

  3. 理解 bit 打包字节的处理逻辑,工业 IO 板开发必备。

  4. 了解 Modbus‑TCP 报文 MBAP 头部,明白 RTU 与 TCP 的复用关系。

相关推荐
NJCloud44 分钟前
Docker 私有仓库部署:Registry、加密传输与认证鉴权
运维·网络·docker·云原生·容器
CC城子1 小时前
Bootloader + OTA -Ymodem 协议
网络·ymoden
又见情义1 小时前
RK3568 + RTL8211F 网络唤醒(WOL)功能适配全记录
android·网络·驱动开发
zbyyd1 小时前
Linux 进程间通信(IPC):信号、消息队列、共享内存与信号量详解
linux·运维·c语言
Titan20241 小时前
HTTPS基础知识梳理
服务器·网络·c++·网络协议·学习·http·https
自然石人1 小时前
石都随笔:深耕不张扬,平凡自有千钧力
网络·经验分享·百度·传媒·新浪微博
新时代牛马2 小时前
epoll 源码路径:从epoll_ctl 到ep_poll 的就绪唤醒
网络·数据库·网络协议
简单Janeee2 小时前
[Vue 3 从零到上线]-第九篇:更美更强——引入 UI 库与网络请求 (Axios)
网络·vue.js·ui
学习星球2 小时前
空天地一体化网络(NTN)深度解析:从Starlink D2C到3GPP NTN,卫星直连手机是如何实现的?
网络·人工智能·算法·智能手机·php