一句话结论:Modbus RTU 从站只有三件事——3.5 字符间隔判帧尾、CRC 校验、按功能码搬运寄存器,难点全在边界条件上。
一、完整工程下载
压缩包内含全部源码、platformio.ini、Makefile、README.md,解压即用,不需要额外配置。
下载 modbus-rtu.zip (9.8 KB,共 9 个文件)
.gitignore
Makefile
README.md
include/
modbus_crc.h
modbus_slave.h
platformio.ini
src/
modbus_crc.c
modbus_slave.c
test/
test_modbus.cModbus RTU 报文长什么样
请求(读保持寄存器 03):
01 03 00 00 00 0A C5 CD
| | | | |
| | | | +-- CRC16(低字节在前!)
| | | +-------- 寄存器数量 10
| | +--------------- 起始地址 0
| +------------------- 功能码 03(读保持寄存器)
+----------------------- 从站地址 01响应:
01 03 14 <20 字节数据> CRC
| |
| +-- 字节数 = 寄存器数 × 2
+------ 功能码三个最容易错的地方
- CRC 低字节在前。Modbus 是全协议小端(除了地址和功能码本来就是单字节)。
用 (crc >> 8) | (crc << 8) 交换后再发送是标准做法。
- 异常响应不是返回错误码,而是把功能码的最高位置 1,后面跟一个异常码:
``text 请求: 01 03 00 00 00 0A CRC 异常: 01 83 02 CRC # 0x03|0x80 = 0x83,02 = 非法数据地址 ``
- 帧间隔判帧。RTU 靠「静默 3.5 个字符时间」表示一帧结束。
9600bps 时约 4ms;115200bps 时理论 0.3ms, 实操上直接用定时器超时 + 串口空闲中断,取 1~2ms 就够了。
本工程实现的功能码
| 功能码 | 名称 | 说明 |
|---|---|---|
| 0x03 | 读保持寄存器 | 最常用,读参数/测量值 |
| 0x06 | 写单个寄存器 | 写一个参数 |
| 0x10 (16) | 写多个寄存器 | 批量写参数 |
| 0x08 | 诊断(仅子功能 0x0000) | 回显,用于环回测试 |
工程内容
modbus_slave.c/h:完整从站,持有寄存器表(保持寄存器 + 输入寄存器)modbus_crc.c/h:CRC16/Modbus(多项式 0xA001)与表驱动快速版本
(表由代码在首次调用时生成——手抄 256 项 CRC 表出错率极高, 本工程第一版就是抄错了,靠测试向量 "123456789" -> 0x4B37 抓出来的)
test/test_modbus.c:主机端测试,逐字节比对标准报文:- 03 正常读、03 跨边界读(触发异常)
- 06 正常写、06 写非法地址
- 16 批量写、数量超限
- 从站地址过滤(不是发给我的直接丢弃)
- CRC 错误丢弃
- 功能码不支持时返回异常码 0x01
寄存器表怎么映射到你的硬件
/* 把测量值放进去,上位机读 40001 就能拿到温度 */
modbus_set_input_reg(0, temperature_x10); /* 0.1℃ 分辨率 */
modbus_set_holding_reg(6, pwm_duty); /* 上位机可写 */
/* 上位机写了参数,从站收到回调 */
static void on_holding_write(uint16_t addr, uint16_t value) {
switch (addr) {
case 6: set_pwm(value); break;
case 7: target_rpm = value; break;
default: break;
}
}调试要点
- 先用 PC 上的 Modbus Poll / QModMaster 连,通了再上自己的 PLC。
现场问题一半是 A/B 线反了、一半是终端电阻没加
- RS485 接线三件事:A 接 A、B 接 B(不要凭感觉);总线两端各一个 120Ω 终端电阻;
屏蔽层单端接地
- 地址躲开 0 和 248~255:0 是广播地址,248 以上常被保留
- 一次别读太多寄存器:标准限制单次 03 最多 125 个寄存器,
超了要么分几次读,要么报异常码 03
- 发完别忘了切换 485 收发方向:
RS485_DE拉高发送,发完立刻拉低,
拉低晚了会吃掉对方的应答
进阶方向
- 加 0x01/0x02(读线圈/离散输入),支持开关量
- 加 0x0F(写多个线圈),配合继电器板做 IO 控制
- 同时支持 ASCII 模式 与 TCP 模式(MBAP 头),做成三合一协议栈
- 加掉电保存:把保持寄存器写进 Flash(本站另有 Flash 日志专文)
完整代码
Makefile
CC ?= gcc
CFLAGS ?= -std=c99 -Wall -Wextra -O2 -Iinclude
LDLIBS ?=
SRC = src/modbus_crc.c src/modbus_slave.c
TEST = test/test_modbus.c
ifeq ($(OS),Windows_NT)
EXT = .exe
endif
BIN = build/test$(EXT)
all: run
$(BIN): $(SRC) $(TEST)
@mkdir -p build
$(CC) $(CFLAGS) $(SRC) $(TEST) -o $(BIN) $(LDLIBS)
run: $(BIN)
@$(BIN)
clean:
rm -rf build
.PHONY: all run cleaninclude/modbus_crc.h
/**
* modbus_crc.h - Modbus 专用 CRC16(多项式 0xA001,初值 0xFFFF,低字节先发)
*/
#ifndef MODBUS_CRC_H
#define MODBUS_CRC_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** 逐位计算,代码小,适合放 Flash 紧张的芯片 */
uint16_t modbus_crc16(const uint8_t *data, uint16_t len);
/** 查表法,速度快约 8 倍,需要 512 字节表空间 */
uint16_t modbus_crc16_fast(const uint8_t *data, uint16_t len);
#ifdef __cplusplus
}
#endif
#endif /* MODBUS_CRC_H */include/modbus_slave.h
/**
* modbus_slave.h - Modbus RTU 从站(功能码 03/06/08/16)
*
* 使用流程:
* 1. modbus_slave_init(addr) 设置从站地址
* 2. 串口收到一个字节就调用 modbus_slave_feed()
* 3. 判定帧尾后(3.5 字符静默)调用 modbus_slave_poll(),它会生成响应
* 4. 把响应缓冲区里的字节写进串口(记得控制 RS485 方向脚)
*/
#ifndef MODBUS_SLAVE_H
#define MODBUS_SLAVE_H
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define MODBUS_HOLDING_NUM 64u /* 4xxxx 保持寄存器(可读写) */
#define MODBUS_INPUT_NUM 64u /* 3xxxx 输入寄存器(只读) */
#define MODBUS_COIL_NUM 32u
#define MODBUS_MAX_RESP 256u
/* 异常码 */
#define MODBUS_EX_ILLEGAL_FUNCTION 0x01u
#define MODBUS_EX_ILLEGAL_ADDRESS 0x02u
#define MODBUS_EX_ILLEGAL_VALUE 0x03u
#define MODBUS_EX_SLAVE_FAILURE 0x04u
typedef void (*modbus_write_cb_t)(uint16_t addr, uint16_t value);
typedef struct {
uint8_t addr;
uint16_t holding[MODBUS_HOLDING_NUM];
uint16_t input[MODBUS_INPUT_NUM];
uint8_t coil[MODBUS_COIL_NUM];
modbus_write_cb_t on_holding_write;
/* 接收缓冲与状态 */
uint8_t rx[MODBUS_MAX_RESP];
uint16_t rx_len;
uint8_t resp[MODBUS_MAX_RESP];
uint16_t resp_len;
bool resp_ready;
/* 统计 */
uint32_t cnt_ok;
uint32_t cnt_crc_err;
uint32_t cnt_not_for_me;
uint32_t cnt_exception;
} modbus_slave_t;
void modbus_slave_init(modbus_slave_t *s, uint8_t addr);
/** 串口每收到一个字节就调用一次 */
void modbus_slave_feed(modbus_slave_t *s, uint8_t byte);
/**
* 帧间隔超时后调用:校验 + 执行 + 生成响应
* @return true 表示 resp 中有待发送数据
*/
bool modbus_slave_poll(modbus_slave_t *s);
/** 便捷写寄存器(会自动调用回调) */
void modbus_set_holding(modbus_slave_t *s, uint16_t addr, uint16_t v);
void modbus_set_input(modbus_slave_t *s, uint16_t addr, uint16_t v);
#ifdef __cplusplus
}
#endif
#endif /* MODBUS_SLAVE_H */platformio.ini
[platformio]
default_envs = bluepill
[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
build_flags =
-Wall
-Wextra
-Isrc
lib_ldf_mode = deep+src/modbus_crc.c
#include "modbus_crc.h"
#include <stdbool.h>
uint16_t modbus_crc16(const uint8_t *data, uint16_t len)
{
uint16_t crc = 0xFFFFu;
uint16_t i;
uint8_t b;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (b = 0; b < 8u; b++) {
if (crc & 0x0001u) {
crc = (uint16_t)((crc >> 1) ^ 0xA001u);
} else {
crc = (uint16_t)(crc >> 1);
}
}
}
return crc;
}
/* ------------------------------------------------------------------
* 查表法。表在首次调用时由代码生成。
*
* 为什么不直接抄一张 256 项的常量表?
* 因为手抄/复制粘贴的 CRC 表出错概率极高,而且出错后现象是「偶尔校验失败」,
* 极难排查。让机器从多项式算出来,只可能对。
*
* 注意:首次调用会做 256×8 次循环(几十微秒),之后无额外开销。
* 如果对中断延迟敏感,可以在初始化阶段先调用一次 modbus_crc16_fast(NULL, 0)。
* ------------------------------------------------------------------ */
static uint16_t s_table[256];
static bool s_table_ready = false;
static void build_table(void)
{
uint16_t i;
uint8_t b;
for (i = 0; i < 256u; i++) {
uint16_t crc = i;
for (b = 0; b < 8u; b++) {
if (crc & 0x0001u) {
crc = (uint16_t)((crc >> 1) ^ 0xA001u);
} else {
crc = (uint16_t)(crc >> 1);
}
}
s_table[i] = crc;
}
s_table_ready = true;
}
uint16_t modbus_crc16_fast(const uint8_t *data, uint16_t len)
{
uint16_t crc = 0xFFFFu;
uint16_t i;
if (!s_table_ready) {
build_table();
}
for (i = 0; i < len; i++) {
uint8_t idx = (uint8_t)((crc ^ data[i]) & 0xFFu);
crc = (uint16_t)((crc >> 8) ^ s_table[idx]);
}
return crc;
}src/modbus_slave.c
#include "modbus_slave.h"
#include <string.h>
#include "modbus_crc.h"
void modbus_slave_init(modbus_slave_t *s, uint8_t addr)
{
memset(s, 0, sizeof(*s));
s->addr = addr;
}
void modbus_set_holding(modbus_slave_t *s, uint16_t addr, uint16_t v)
{
if (addr < MODBUS_HOLDING_NUM) {
s->holding[addr] = v;
}
}
void modbus_set_input(modbus_slave_t *s, uint16_t addr, uint16_t v)
{
if (addr < MODBUS_INPUT_NUM) {
s->input[addr] = v;
}
}
void modbus_slave_feed(modbus_slave_t *s, uint8_t byte)
{
if (s->rx_len < MODBUS_MAX_RESP) {
s->rx[s->rx_len++] = byte;
} else {
s->rx_len = 0; /* 溢出保护:丢掉整帧重新收集 */
}
}
static uint16_t put_crc(uint8_t *buf, uint16_t len)
{
uint16_t crc = modbus_crc16(buf, len);
buf[len++] = (uint8_t)(crc & 0xFFu); /* 低字节在前 */
buf[len++] = (uint8_t)((crc >> 8) & 0xFFu);
return len;
}
static uint16_t make_exception(modbus_slave_t *s, uint8_t func, uint8_t code)
{
s->resp[0] = s->addr;
s->resp[1] = (uint8_t)(func | 0x80u);
s->resp[2] = code;
s->cnt_exception++;
return put_crc(s->resp, 3);
}
bool modbus_slave_poll(modbus_slave_t *s)
{
uint16_t len = s->rx_len;
uint8_t func;
uint16_t crc_calc;
uint16_t crc_recv;
s->resp_ready = false;
if (len < 4) { /* 最短合法帧 = 地址+功能码+CRC2 = 4 */
s->rx_len = 0;
return false;
}
crc_calc = modbus_crc16(s->rx, (uint16_t)(len - 2));
crc_recv = (uint16_t)(s->rx[len - 2] | ((uint16_t)s->rx[len - 1] << 8));
if (crc_calc != crc_recv) {
s->cnt_crc_err++;
s->rx_len = 0;
return false;
}
if (s->rx[0] != s->addr) {
s->cnt_not_for_me++;
s->rx_len = 0;
return false;
}
func = s->rx[1];
switch (func) {
case 0x03: { /* 读保持寄存器 */
uint16_t start = (uint16_t)((s->rx[2] << 8) | s->rx[3]);
uint16_t qty = (uint16_t)((s->rx[4] << 8) | s->rx[5]);
uint16_t i;
if (qty == 0 || qty > 125) {
s->resp_len = make_exception(s, func, MODBUS_EX_ILLEGAL_VALUE);
break;
}
if ((uint32_t)start + qty > MODBUS_HOLDING_NUM) {
s->resp_len = make_exception(s, func, MODBUS_EX_ILLEGAL_ADDRESS);
break;
}
s->resp[0] = s->addr;
s->resp[1] = func;
s->resp[2] = (uint8_t)(qty * 2u);
for (i = 0; i < qty; i++) {
uint16_t v = s->holding[start + i];
s->resp[3 + i * 2] = (uint8_t)(v >> 8); /* 寄存器大端 */
s->resp[4 + i * 2] = (uint8_t)(v & 0xFFu);
}
s->resp_len = put_crc(s->resp, (uint16_t)(3u + qty * 2u));
break;
}
case 0x04: { /* 读输入寄存器 */
uint16_t start = (uint16_t)((s->rx[2] << 8) | s->rx[3]);
uint16_t qty = (uint16_t)((s->rx[4] << 8) | s->rx[5]);
uint16_t i;
if (qty == 0 || qty > 125) {
s->resp_len = make_exception(s, func, MODBUS_EX_ILLEGAL_VALUE);
break;
}
if ((uint32_t)start + qty > MODBUS_INPUT_NUM) {
s->resp_len = make_exception(s, func, MODBUS_EX_ILLEGAL_ADDRESS);
break;
}
s->resp[0] = s->addr;
s->resp[1] = func;
s->resp[2] = (uint8_t)(qty * 2u);
for (i = 0; i < qty; i++) {
uint16_t v = s->input[start + i];
s->resp[3 + i * 2] = (uint8_t)(v >> 8);
s->resp[4 + i * 2] = (uint8_t)(v & 0xFFu);
}
s->resp_len = put_crc(s->resp, (uint16_t)(3u + qty * 2u));
break;
}
case 0x06: { /* 写单个保持寄存器 */
uint16_t addr = (uint16_t)((s->rx[2] << 8) | s->rx[3]);
uint16_t v = (uint16_t)((s->rx[4] << 8) | s->rx[5]);
if (addr >= MODBUS_HOLDING_NUM) {
s->resp_len = make_exception(s, func, MODBUS_EX_ILLEGAL_ADDRESS);
break;
}
s->holding[addr] = v;
if (s->on_holding_write != NULL) {
s->on_holding_write(addr, v);
}
/* 正常响应就是原样回显请求的前 6 字节 */
memcpy(s->resp, s->rx, 6);
s->resp_len = put_crc(s->resp, 6);
break;
}
case 0x10: { /* 写多个保持寄存器 */
uint16_t start = (uint16_t)((s->rx[2] << 8) | s->rx[3]);
uint16_t qty = (uint16_t)((s->rx[4] << 8) | s->rx[5]);
uint8_t byte_cnt = s->rx[6];
uint16_t i;
if (qty == 0 || qty > 123 || byte_cnt != (uint8_t)(qty * 2u)) {
s->resp_len = make_exception(s, func, MODBUS_EX_ILLEGAL_VALUE);
break;
}
if ((uint32_t)start + qty > MODBUS_HOLDING_NUM) {
s->resp_len = make_exception(s, func, MODBUS_EX_ILLEGAL_ADDRESS);
break;
}
if (len < (uint16_t)(9u + qty * 2u)) {
s->resp_len = make_exception(s, func, MODBUS_EX_ILLEGAL_VALUE);
break;
}
for (i = 0; i < qty; i++) {
uint16_t v = (uint16_t)((s->rx[7 + i * 2] << 8) | s->rx[8 + i * 2]);
s->holding[start + i] = v;
if (s->on_holding_write != NULL) {
s->on_holding_write((uint16_t)(start + i), v);
}
}
memcpy(s->resp, s->rx, 6);
s->resp_len = put_crc(s->resp, 6);
break;
}
case 0x08: { /* 诊断:子功能 0x0000 回显 */
uint16_t sub = (uint16_t)((s->rx[2] << 8) | s->rx[3]);
if (sub != 0x0000u) {
s->resp_len = make_exception(s, func, MODBUS_EX_ILLEGAL_VALUE);
break;
}
memcpy(s->resp, s->rx, (size_t)((len - 2u) < 6u ? (len - 2u) : 6u));
s->resp_len = put_crc(s->resp, 6);
break;
}
default:
s->resp_len = make_exception(s, func, MODBUS_EX_ILLEGAL_FUNCTION);
break;
}
s->cnt_ok++;
s->resp_ready = true;
s->rx_len = 0;
return true;
}test/test_modbus.c
/**
* 主机端测试:用标准报文逐字节验证从站响应
* gcc -std=c99 -Wall -Wextra -Iinclude src/modbus_crc.c src/modbus_slave.c test/test_modbus.c -o build/test
*/
#include <stdio.h>
#include <string.h>
#include "modbus_crc.h"
#include "modbus_slave.h"
static int g_pass = 0;
static int g_fail = 0;
#define CHECK(cond, msg) \
do { \
if (cond) { g_pass++; } \
else { g_fail++; printf(" [FAIL] %s (line %d)\n", msg, __LINE__); } \
} while (0)
static modbus_slave_t slave;
static uint16_t cb_addr;
static uint16_t cb_value;
static int cb_count;
static void on_write(uint16_t addr, uint16_t v)
{
cb_addr = addr;
cb_value = v;
cb_count++;
}
static void hexdump(const char *tag, const uint8_t *p, uint16_t n)
{
printf(" %-10s", tag);
for (uint16_t i = 0; i < n; i++) {
printf(" %02X", p[i]);
}
printf("\n");
}
/* 把请求喂进去并返回响应 */
static uint16_t do_request(modbus_slave_t *s, const uint8_t *req, uint16_t n)
{
for (uint16_t i = 0; i < n; i++) {
modbus_slave_feed(s, req[i]);
}
if (!modbus_slave_poll(s)) {
return 0;
}
return s->resp_len;
}
int main(void)
{
uint8_t req[32];
uint16_t reqlen;
uint16_t n;
printf("===== Modbus RTU 从站测试 =====\n\n");
printf("[0] CRC16/Modbus 测试向量:\"123456789\" -> 0x4B37\n");
{
uint16_t c1 = modbus_crc16((const uint8_t *)"123456789", 9);
uint16_t c2 = modbus_crc16_fast((const uint8_t *)"123456789", 9);
printf(" 逐位 = 0x%04X,查表 = 0x%04X\n", c1, c2);
CHECK(c1 == 0x4B37, "逐位 CRC 正确");
CHECK(c1 == c2, "两种实现结果一致");
}
modbus_slave_init(&slave, 0x01);
slave.on_holding_write = on_write;
for (int i = 0; i < 10; i++) {
modbus_set_holding(&slave, (uint16_t)i, (uint16_t)(0x1100 + i));
}
printf("\n[1] 03 读保持寄存器:01 03 00 00 00 0A\n");
{
const uint8_t r[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x0A};
memcpy(req, r, sizeof(r));
reqlen = 6;
{
uint16_t crc = modbus_crc16(req, reqlen);
req[reqlen++] = (uint8_t)(crc & 0xFF);
req[reqlen++] = (uint8_t)(crc >> 8);
}
hexdump("请求:", req, reqlen);
n = do_request(&slave, req, reqlen);
hexdump("响应:", slave.resp, n);
CHECK(n == 3 + 20 + 2, "响应长度 = 地址+功能码+字节数+20+CRC");
CHECK(slave.resp[0] == 0x01 && slave.resp[1] == 0x03, "地址与功能码回显");
CHECK(slave.resp[2] == 20, "字节数 = 20");
CHECK(slave.resp[3] == 0x11 && slave.resp[4] == 0x00, "第 0 个寄存器 = 0x1100");
CHECK(slave.resp[21] == 0x11 && slave.resp[22] == 0x09, "第 9 个寄存器 = 0x1109");
}
printf("\n[2] 03 跨边界读:起始 60,数量 10(超出 64 个寄存器)\n");
{
const uint8_t r[] = {0x01, 0x03, 0x00, 0x3C, 0x00, 0x0A};
memcpy(req, r, sizeof(r));
reqlen = 6;
{
uint16_t crc = modbus_crc16(req, reqlen);
req[reqlen++] = (uint8_t)(crc & 0xFF);
req[reqlen++] = (uint8_t)(crc >> 8);
}
n = do_request(&slave, req, reqlen);
hexdump("响应:", slave.resp, n);
CHECK(n == 5, "异常响应 5 字节");
CHECK(slave.resp[1] == 0x83, "功能码最高位置 1");
CHECK(slave.resp[2] == MODBUS_EX_ILLEGAL_ADDRESS, "异常码 02 非法数据地址");
}
printf("\n[3] 06 写单个寄存器:地址 6 = 0x00FF\n");
{
const uint8_t r[] = {0x01, 0x06, 0x00, 0x06, 0x00, 0xFF};
memcpy(req, r, sizeof(r));
reqlen = 6;
{
uint16_t crc = modbus_crc16(req, reqlen);
req[reqlen++] = (uint8_t)(crc & 0xFF);
req[reqlen++] = (uint8_t)(crc >> 8);
}
cb_count = 0;
n = do_request(&slave, req, reqlen);
hexdump("响应:", slave.resp, n);
CHECK(n == 8, "响应 8 字节(回显)");
CHECK(memcmp(slave.resp, req, 6) == 0, "原样回显前 6 字节");
CHECK(slave.holding[6] == 0x00FF, "寄存器已更新");
CHECK(cb_count == 1 && cb_addr == 6 && cb_value == 0x00FF, "写回调被正确触发");
}
printf("\n[4] 16 写多个寄存器:地址 0x10 起写 3 个\n");
{
const uint8_t r[] = {0x01, 0x10, 0x00, 0x10, 0x00, 0x03, 0x06,
0x00, 0x0A, 0x00, 0x14, 0x00, 0x1E};
memcpy(req, r, sizeof(r));
reqlen = 13;
{
uint16_t crc = modbus_crc16(req, reqlen);
req[reqlen++] = (uint8_t)(crc & 0xFF);
req[reqlen++] = (uint8_t)(crc >> 8);
}
cb_count = 0;
n = do_request(&slave, req, reqlen);
hexdump("响应:", slave.resp, n);
CHECK(n == 8, "响应 8 字节");
CHECK(slave.holding[0x10] == 10 && slave.holding[0x11] == 20 &&
slave.holding[0x12] == 30, "三个寄存器都写入成功");
CHECK(cb_count == 3, "回调触发 3 次");
}
printf("\n[5] 从站地址不匹配:01 -> 02,必须静默\n");
{
const uint8_t r[] = {0x02, 0x03, 0x00, 0x00, 0x00, 0x01};
memcpy(req, r, sizeof(r));
reqlen = 6;
{
uint16_t crc = modbus_crc16(req, reqlen);
req[reqlen++] = (uint8_t)(crc & 0xFF);
req[reqlen++] = (uint8_t)(crc >> 8);
}
n = do_request(&slave, req, reqlen);
CHECK(n == 0, "不是发给自己的帧不响应");
CHECK(slave.cnt_not_for_me >= 1, "not_for_me 计数 +1");
}
printf("\n[6] CRC 错误必须丢弃\n");
{
const uint8_t r[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01};
memcpy(req, r, sizeof(r));
reqlen = 6;
{
uint16_t crc = modbus_crc16(req, reqlen);
req[reqlen++] = (uint8_t)(crc & 0xFF);
req[reqlen++] = (uint8_t)(crc >> 8);
}
req[2] ^= 0x55; /* 改数据但不改 CRC */
n = do_request(&slave, req, reqlen);
CHECK(n == 0, "坏 CRC 不响应");
CHECK(slave.cnt_crc_err >= 1, "crc_err 计数 +1");
}
printf("\n[7] 不支持的功能码 0x09 -> 异常码 01\n");
{
const uint8_t r[] = {0x01, 0x09, 0x00, 0x00};
memcpy(req, r, sizeof(r));
reqlen = 4;
{
uint16_t crc = modbus_crc16(req, reqlen);
req[reqlen++] = (uint8_t)(crc & 0xFF);
req[reqlen++] = (uint8_t)(crc >> 8);
}
n = do_request(&slave, req, reqlen);
hexdump("响应:", slave.resp, n);
CHECK(n == 5 && slave.resp[1] == 0x89 &&
slave.resp[2] == MODBUS_EX_ILLEGAL_FUNCTION, "返回异常码 01");
}
printf("\n[8] 04 读输入寄存器(只读区)\n");
{
const uint8_t r[] = {0x01, 0x04, 0x00, 0x00, 0x00, 0x02};
modbus_set_input(&slave, 0, 1234);
modbus_set_input(&slave, 1, 5678);
memcpy(req, r, sizeof(r));
reqlen = 6;
{
uint16_t crc = modbus_crc16(req, reqlen);
req[reqlen++] = (uint8_t)(crc & 0xFF);
req[reqlen++] = (uint8_t)(crc >> 8);
}
n = do_request(&slave, req, reqlen);
hexdump("响应:", slave.resp, n);
CHECK(n == 9, "响应 9 字节");
CHECK(slave.resp[3] == 0x04 && slave.resp[4] == 0xD2, "1234 = 0x04D2");
CHECK(slave.resp[5] == 0x16 && slave.resp[6] == 0x2E, "5678 = 0x162E");
}
printf("\n[9] 08 诊断回显\n");
{
const uint8_t r[] = {0x01, 0x08, 0x00, 0x00, 0x12, 0x34};
memcpy(req, r, sizeof(r));
reqlen = 6;
{
uint16_t crc = modbus_crc16(req, reqlen);
req[reqlen++] = (uint8_t)(crc & 0xFF);
req[reqlen++] = (uint8_t)(crc >> 8);
}
n = do_request(&slave, req, reqlen);
hexdump("响应:", slave.resp, n);
CHECK(n == 8 && memcmp(slave.resp, req, 6) == 0, "数据被原样回显");
}
printf("\n统计: ok=%u crc_err=%u not_for_me=%u exception=%u\n",
slave.cnt_ok, slave.cnt_crc_err, slave.cnt_not_for_me,
slave.cnt_exception);
printf("\n----- 通过 %d 项,失败 %d 项 -----\n", g_pass, g_fail);
printf("===== 注:以上全部由本机 gcc 实编译实运行 =====\n");
return g_fail == 0 ? 0 : 1;
}实测输出
下面这段输出是把上面的核心算法用 本机 gcc 真编译、真运行得到的(不含任何硬件依赖):
===== Modbus RTU 从站测试 =====
[0] CRC16/Modbus 测试向量:"123456789" -> 0x4B37
逐位 = 0x4B37,查表 = 0x4B37
[1] 03 读保持寄存器:01 03 00 00 00 0A
请求: 01 03 00 00 00 0A C5 CD
响应: 01 03 14 11 00 11 01 11 02 11 03 11 04 11 05 11 06 11 07 11 08 11 09 1C E6
[2] 03 跨边界读:起始 60,数量 10(超出 64 个寄存器)
响应: 01 83 02 C0 F1
[3] 06 写单个寄存器:地址 6 = 0x00FF
响应: 01 06 00 06 00 FF 29 8B
[4] 16 写多个寄存器:地址 0x10 起写 3 个
响应: 01 10 00 10 00 03 81 CD
[5] 从站地址不匹配:01 -> 02,必须静默
[6] CRC 错误必须丢弃
[7] 不支持的功能码 0x09 -> 异常码 01
响应: 01 89 01 86 50
[8] 04 读输入寄存器(只读区)
响应: 01 04 04 04 D2 16 2E D4 F1
[9] 08 诊断回显
响应: 01 08 00 00 12 34 ED 7C
统计: ok=7 crc_err=1 not_for_me=1 exception=2
----- 通过 26 项,失败 0 项 -----
===== 注:以上全部由本机 gcc 实编译实运行 =====