从零吃透 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 的复用关系。

相关推荐
贞明电子18 小时前
工业三色报警灯怎么接入项目系统?开关量与RS485控制应用指南
运维·网络·微信公众平台·贞明电子
明天也要努力学习18 小时前
HTTP 协议的基本格式和 fiddler 的用法
网络协议·http·fiddler
HideF18 小时前
Linux
linux·服务器·网络
万联WANFLOW18 小时前
海外网络故障怎么排查?Cloudflare 三季度断网报告解读与 mtr 实战
运维·网络·故障分析
听风说ml18 小时前
socket编程 【C语言】
c语言·udp·tcp·socket编程
sbjdhjd18 小时前
网工进阶笔记:H3CSE 园区网全栈技术与协议汇总
网络·笔记·云计算·ssh·网络工程·vlan·aaa
学逆向的20 小时前
C 局部静态变量
c语言·开发语言·网络安全
taktok_ai21 小时前
用 cc-switch 连接 Taktok
网络
小嘉丶学长1 天前
Day14 · 字典与集合
java·开发语言·网络·python·学习·1024程序员节
LCG米1 天前
STM32驱动W5500以太网模块实测:TCP吞吐瓶颈定位、TX指针回绕陷阱与断线重连保活落地
stm32·单片机·嵌入式硬件·tcp/ip