单片机基础知识(协议篇)--Modbus RTU

📅 2026/8/5 7:44:04
单片机基础知识(协议篇)--Modbus RTU
Modbus RTU 功能码完整详解 报文实例Modbus RTU 采用二进制串行协议主从问答机制所有报文无起始/结束标记依靠3.5字符静默间隔区分帧。通用帧基础结构所有功能码共用头部从站地址(1B) 功能码(1B) 数据域 CRC校验(2B低字节在前)前置概念区分重中之重避免混淆线圈 Coil1位0/1读写俗称开关量、DO/DI离散输入 Discrete Input1位只读DI保持寄存器 Holding Register16位读写最常用模拟量输出、参数输入寄存器 Input Register16位只读AI采集值地址惯例行业通用00001~09999线圈读写10001~19999离散输入只读30001~39999输入寄存器只读40001~49999保持寄存器读写协议报文内偏移地址从0开始40001寄存器 报文地址0000标准常用功能码列表01H 读线圈 Read Coils作用读取多个线圈1bit0/1读写型开关量请求报文格式从站功能码起始地址(2B)读取数量(2B)CRC1B01AddrHi AddrLoNumHi NumLoCRC Lo,CRC Hi响应报文格式从站功能码字节长度NN字节线圈数据CRC线圈按bit打包低位对应低地址线圈不足8位高位补0实例需求1号从站读取线圈0~6共7个线圈请求01 01 00 00 00 07 7C 0801从站地址101功能码0100 00起始地址0线圈0000100 07读取7个线圈7C 08CRC假设返回线圈状态线圈01线圈10线圈21线圈31线圈40线圈50线圈61bit排布bit0(线圈0) ~ bit6(线圈6) →0101101→ 0x2D响应报文01 01 01 2D 54 0F01从站01功能码01后续数据长度1字节2D线圈bit数据54 0FCRC02H 读离散输入 Read Discrete Inputs作用读取只读开关量DI1bit不可写报文结构与01H完全一致仅功能码不同。实例1号从站读取离散输入地址0~34点DI请求01 02 00 00 00 04 79 F903H 读保持寄存器 Read Holding Registers ⭐最常用作用读取保持寄存器16位读写参数、模拟输出请求格式|从站|03|起始地址(2B)|寄存器数量(2B)|CRC|响应格式|从站|03|字节长度|寄存器数据2字节/个|CRC|实例需求1号从站读取40001、40002寄存器偏移地址0数量2请求报文01 03 00 00 00 02 94 0B01 从站103 读保持寄存器00 00 起始偏移04000100 02 读取2个寄存器响应寄存器00x0A01寄存器10x00C8响应报文01 03 04 0A 01 00 C8 FB 54044字节数据2个寄存器 ×20A 01寄存器000 C8寄存器104H 读输入寄存器 Read Input Registers作用读取只读16位输入寄存器AI模拟采集报文结构与03H完全相同仅功能码04。实例1号从站读取30001偏移01路AI请求01 04 00 00 00 01 31 8405H 写单个线圈 Write Single Coil作用单点开关输出置线圈ON/OFF请求|从站|05|线圈地址(2B)|强制值(2B)|CRC|ONFF 00OFF00 00其他值非法实例1号从站线圈000001置ON请求01 05 00 00 FF 00 8C 3A正常情况下从站原样回复这条报文作为应答。线圈0置OFF01 05 00 00 00 00 CD CA06H 写单个保持寄存器 Write Single Register ⭐高频使用作用写入1个16位保持寄存器请求格式|从站|06|寄存器地址(2B)|写入数值(2B)|CRC|从站正常应答原样返回请求帧实例1号从站40001偏移0写入数值0x1234请求报文01 06 00 00 12 34 A3 91从站成功则直接返回01 06 00 00 12 34 A3 910FH (15) 写多个线圈 Write Multiple Coils作用批量写多个开关量线圈请求|从站|0F|起始地址(2B)|线圈数量(2B)|数据字节长度N|N字节bit数据|CRC|实例1号从站线圈0~2共3个线圈线圈01线圈10线圈21请求01 0F 00 00 00 03 01 05 26 9400033个线圈01数据长度1字节05 → bit01 bit10 bit21成功应答只返回01 0F 00 00 00 03 54 0510H (16) 写多个保持寄存器 Write Multiple Registers ⭐高频作用批量写入一组保持寄存器请求格式|从站|10|起始地址(2B)|寄存器数量(2B)|数据字节长度|寄存器数据|CRC|实例1号从站偏移地址0、1两个寄存器分别写入0x0001、0x0002请求01 10 00 00 00 02 04 00 01 00 02 74 17解析00022个寄存器044字节数据00 01 / 00 02 两个寄存器值成功应答01 10 00 00 00 02 10 08扩展功能码工业设备偶尔用到07H 读异常状态极少用08H 诊断功能码线路测试子功能00返回查询报文用于通讯链路测试示例01 08 00 00 00 00 C0 7B11H (17) 报告从站ID读取从站设备型号、版本信息很多国产设备不支持。17H (23) 读写多个寄存器组合功能合并指令先读一批寄存器同时写入另一批寄存器一次问答完成读写减少总线轮询次数。异常响应格式所有功能码通用通讯出错、地址越界、非法功能码时从站返回异常帧从站地址 | 功能码0x80 | 异常码 | CRC常见异常码01非法功能码02非法数据地址寄存器/线圈不存在03非法数据值04从站设备故障06从站忙请重试示例访问不存在寄存器03功能码异常响应01 83 02 C0 F101从站83030x8002非法地址快速选型建议开发直接参考单点模拟量写入 →06H多点寄存器批量写入 →10H读取参数寄存器 →03HAI采集只读输入寄存器 →04H单个继电器输出 →05H多路继电器批量控制 →0FHDI只读开关采集 →02H可读写开关采集 →01H二、完整报文一份可直接复制到串口调试工具的全部功能码测试报文清单Modbus RTU CRC16简易计算代码C语言区分「寄存器地址偏移」和「40001标准地址」的常见踩坑汇总Modbus RTU 配套资料合集一、全套可直接复制串口调试工具测试报文RTU带CRC使用说明串口工具选择HEX发送间隔 ≥3.5字符9600波特约4ms从站地址统一使用011号从站1. 01H 读线圈 Read Coils需求读取线圈067个线圈0000100007请求01 01 00 00 00 07 7C 082. 02H 读离散输入 Read Discrete Inputs需求读取DI地址034点1000110004请求01 02 00 00 00 04 79 F93. 03H 读保持寄存器 Read Holding Registers需求读取40001、40002偏移02个寄存器请求01 03 00 00 00 02 94 0B4. 04H 读输入寄存器 Read Input Registers需求读取30001偏移01路AI请求01 04 00 00 00 01 31 845. 05H 写单个线圈 Write Single Coil线圈000001置ON01 05 00 00 FF 00 8C 3A线圈000001置OFF01 05 00 00 00 00 CD CA6. 06H 写单个保持寄存器 Write Single Register40001写入 0x123401 06 00 00 12 34 A3 917. 0FH (15) 写多个线圈 Write Multiple Coils线圈0/1/21、0、1请求01 0F 00 00 00 03 01 05 26 948. 10H (16) 写多个保持寄存器 Write Multiple Registers偏移0写入0x0001偏移1写入0x0002请求01 10 00 00 00 02 04 00 01 00 02 74 179. 08H 诊断测试回显测试链路测试从站原样返回数据请求01 08 00 00 00 00 C0 7B10. 异常报文参考访问不存在寄存器主站发送01 03 01 00 00 01 84 0A预期从站异常应答01 83 02 C0 F1二、Modbus CRC16 标准C语言代码嵌入式直接使用多项式0xA001输出CRC低字节在前RTU标准#includestdint.h// Modbus CRC16 计算uint16_tModbus_CRC16(uint8_t*data,uint16_tlen){uint16_tcrc0xFFFF;uint16_ti,j;for(i0;ilen;i){crc^data[i];for(j0;j8;j){if(crc0x0001){crc1;crc^0xA001;}else{crc1;}}}returncrc;}/* 使用示例 uint8_t buf[] {0x01,0x03,0x00,0x00,0x00,0x02}; uint16_t crc Modbus_CRC16(buf, sizeof(buf)); // RTU帧拼接原数组 crc低字节 crc高字节 */三、高频踩坑汇总寄存器地址最大误区1. 地址两套体系区分【重中之重】1PLC/组态软件显示地址用户地址00001 线圈10001 离散输入30001 输入寄存器40001 保持寄存器2Modbus报文内偏移地址协议原始地址从0开始40001 → 报文地址 0x000040002 → 报文地址 0x0001❌ 新手最常错直接把40001填入报文导致访问40002寄存器2. 字节序问题Modbus寄存器内部高字节在前寄存器0x1234 在报文中12 34注意浮点数32bit需要额外区分AB/CD、BA/DC等大小端协议本身不规定。3. CRC顺序RTU CRC低字节先发高字节后发例如CRC0x940B → 报文追加0B 944. 线圈bit排布规则01H/0FH多个线圈字节bit0 起始地址线圈示例1字节数据0x05→ bit01bit10bit215. 功能码异常响应规则正常响应 原功能码出错响应 原功能码 | 0x80最高位置1异常码说明异常码含义01非法功能码02非法数据地址寄存器不存在03非法数据值04从站设备故障06从站忙稍后重试6. 读写数量上限01H/02H最多2008个点位03H/04H最多125个寄存器0FH最多2008线圈10H最多123寄存器超出上限直接返回异常码02四、补充实用小工具建议串口调试推荐Modbus Poll主站仿真、Modbus Slave从站仿真直接粘贴上述报文测试在线CRC计算器搜索Modbus CRC16 A001快速校验报文。三、STM32裸机Modbus RTU从站极简框架代码补充交付STM32 Modbus RTU 从站极简裸机框架 Excel速查表文本版一、Excel可直接粘贴的Modbus RTU功能码速查表复制下面整块内容粘贴进ExcelA1单元格自动分列制表符分隔功能码 名称 英文名称 访问对象 读写属性 单次最大数量 典型用途 01H 读线圈 Read Coils 线圈(0xxxx) 读 2008点 多路开关输出状态读取 02H 读离散输入 Read Discrete Inputs 离散输入(1xxxx) 只读 2008点 DI无源开关采集 03H 读保持寄存器 Read Holding Registers 保持寄存器(4xxxx) 读 125个 读取参数、设定值 04H 读输入寄存器 Read Input Registers 输入寄存器(3xxxx) 只读 125个 AI模拟量采集 05H 写单个线圈 Write Single Coil 线圈(0xxxx) 写 1点 继电器单点开关 06H 写单个保持寄存器 Write Single Register 保持寄存器(4xxxx) 写 1个 单参数下发 08H 诊断 Diagnostics 链路诊断 - - 通讯测试、故障排查 0FH(15) 写多个线圈 Write Multiple Coils 线圈(0xxxx) 写 2008点 批量继电器控制 10H(16) 写多个保持寄存器 Write Multiple Registers 保持寄存器(4xxxx) 写 123个 批量参数下发 11H(17) 报告从站ID Report Slave ID 设备信息 只读 - 读取设备型号固件 17H(23) 读写寄存器 Read/Write Multiple Registers 保持寄存器(4xxxx) 读写 读125/写121 一问一答同时读写二、STM32裸机 Modbus-RTU 从站极简框架标准库可直接移植适配USART19600 8N1依靠串口空闲中断识别RTU帧仅实现最常用03/06/10功能码可自行扩展01/02/05/0F硬件RS485 DE/RE引脚 PA0高电平发送低电平接收1. 头文件 modbus_rtu.h#ifndef__MODBUS_RTU_H#define__MODBUS_RTU_H#includestm32f10x.h#includestdint.h#defineMODBUS_SLAVE_ADDR0x01//从站地址#defineMODBUS_REG_NUM100//保持寄存器总数externuint16_tHoldingReg[MODBUS_REG_NUM];//保持寄存器数组voidModbus_USART_Init(uint32_tbaud);voidModbus_ProcessFrame(uint8_t*buf,uint16_tlen);uint16_tModbus_CRC16(uint8_t*data,uint16_tlen);#endif2. 源文件 modbus_rtu.c#includemodbus_rtu.huint8_ttx_buf[256];uint16_tHoldingReg[MODBUS_REG_NUM]{0};//CRC16 标准函数uint16_tModbus_CRC16(uint8_t*data,uint16_tlen){uint16_tcrc0xFFFF;uint16_ti,j;for(i0;ilen;i){crc^data[i];for(j0;j8;j){if(crc0x0001){crc1;crc^0xA001;}elsecrc1;}}returncrc;}//485发送使能staticvoidRS485_TX_EN(uint8_ten){if(en)GPIO_SetBits(GPIOA,GPIO_Pin_0);elseGPIO_ResetBits(GPIOA,GPIO_Pin_0);}//串口发送数据staticvoidUSART_SendDataBuf(uint8_t*buf,uint16_tlen){RS485_TX_EN(1);uint16_ti;for(i0;ilen;i){USART_SendData(USART1,buf[i]);while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)RESET);}while(USART_GetFlagStatus(USART1,USART_FLAG_TC)RESET);RS485_TX_EN(0);}//发送异常响应staticvoidSendErrResp(uint8_tslave,uint8_tfunc,uint8_terr_code){uint8_tframe[5];uint16_tcrc;frame[0]slave;frame[1]func|0x80;frame[2]err_code;crcModbus_CRC16(frame,3);frame[3]crc0xFF;frame[4](crc8)0xFF;USART_SendDataBuf(frame,5);}//帧解析处理核心voidModbus_ProcessFrame(uint8_t*buf,uint16_tlen){uint8_tslave_addrbuf[0];uint8_tfunc_codebuf[1];uint16_tstart_addr,reg_num,crc_rx,crc_calc;uint16_ti,tx_len0;//校验CRCcrc_rx(buf[len-1]8)|buf[len-2];crc_calcModbus_CRC16(buf,len-2);if(crc_rx!crc_calc)return;//非本机从站地址丢弃if(slave_addr!MODBUS_SLAVE_ADDR)return;start_addr(buf[2]8)|buf[3];switch(func_code){//03 读保持寄存器case0x03:reg_num(buf[4]8)|buf[5];if((start_addrreg_num)MODBUS_REG_NUM||reg_num0||reg_num125){SendErrResp(slave_addr,func_code,0x02);return;}tx_buf[0]slave_addr;tx_buf[1]0x03;tx_buf[2]reg_num*2;tx_len3;for(i0;ireg_num;i){tx_buf[tx_len](HoldingReg[start_addri]8)0xFF;tx_buf[tx_len]HoldingReg[start_addri]0xFF;}break;//06 写单个寄存器case0x06:if(start_addrMODBUS_REG_NUM){SendErrResp(slave_addr,func_code,0x02);return;}HoldingReg[start_addr](buf[4]8)|buf[5];//应答直接返回原请求(去除接收CRC)for(i0;i6;i)tx_buf[i]buf[i];tx_len6;break;//10 写多个寄存器case0x10:reg_num(buf[4]8)|buf[5];if((start_addrreg_num)MODBUS_REG_NUM||reg_num0||reg_num123){SendErrResp(slave_addr,func_code,0x02);return;}uint8_tdata_lenbuf[6];for(i0;ireg_num;i){HoldingReg[start_addri](buf[7i*2]8)|buf[8i*2];}tx_buf[0]slave_addr;tx_buf[1]0x10;tx_buf[2]buf[2];tx_buf[3]buf[3];tx_buf[4]buf[4];tx_buf[5]buf[5];tx_len6;break;default:SendErrResp(slave_addr,func_code,0x01);return;}//追加CRC校验uint16_tcrcModbus_CRC16(tx_buf,tx_len);tx_buf[tx_len]crc0xFF;tx_buf[tx_len](crc8)0xFF;USART_SendDataBuf(tx_buf,tx_len);}//串口初始化 空闲中断voidModbus_USART_Init(uint32_tbaud){GPIO_InitTypeDef gpio_cfg;USART_InitTypeDef usart_cfg;NVIC_InitTypeDef nvic_cfg;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_USART1,ENABLE);//PA0 485收发控制gpio_cfg.GPIO_PinGPIO_Pin_0;gpio_cfg.GPIO_ModeGPIO_Mode_Out_PP;gpio_cfg.GPIO_SpeedGPIO_Speed_50MHz;GPIO_Init(GPIOA,gpio_cfg);RS485_TX_EN(0);//PA9 TX PA10 RXgpio_cfg.GPIO_PinGPIO_Pin_9;gpio_cfg.GPIO_ModeGPIO_Mode_AF_PP;GPIO_Init(GPIOA,gpio_cfg);gpio_cfg.GPIO_PinGPIO_Pin_10;gpio_cfg.GPIO_ModeGPIO_Mode_IN_FLOATING;GPIO_Init(GPIOA,gpio_cfg);usart_cfg.USART_BaudRatebaud;usart_cfg.USART_WordLengthUSART_WordLength_8b;usart_cfg.USART_StopBitsUSART_StopBits_1;usart_cfg.USART_ParityUSART_Parity_No;usart_cfg.USART_HardwareFlowControlUSART_HardwareFlowControl_None;usart_cfg.USART_ModeUSART_Mode_Rx|USART_Mode_Tx;USART_Init(USART1,usart_cfg);//使能空闲中断、接收中断USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);USART_Cmd(USART1,ENABLE);nvic_cfg.NVIC_IRQChannelUSART1_IRQn;nvic_cfg.NVIC_IRQChannelPreemptionPriority1;nvic_cfg.NVIC_IRQChannelSubPriority1;nvic_cfg.NVIC_IRQChannelCmdENABLE;NVIC_Init(nvic_cfg);}3. 中断服务函数 usart1.c#includemodbus_rtu.h#defineREC_BUF_MAX128uint8_trec_buf[REC_BUF_MAX];uint16_trec_cnt0;voidUSART1_IRQHandler(void){uint8_tch;if(USART_GetITStatus(USART1,USART_IT_RXNE)!RESET){chUSART_ReceiveData(USART1);if(rec_cntREC_BUF_MAX)rec_buf[rec_cnt]ch;}//空闲中断一帧接收完成调用解析if(USART_GetITStatus(USART1,USART_IT_IDLE)!RESET){USART_ReceiveData(USART1);//清除IDLE标志if(rec_cnt4){Modbus_ProcessFrame(rec_buf,rec_cnt);}rec_cnt0;}}4. main.c调用示例externuint16_tHoldingReg[];intmain(void){SystemInit();Modbus_USART_Init(9600);HoldingReg[0]0x0001;//40001初始值HoldingReg[1]0x0002;//40002初始值while(1){//后台任务}}三、扩展说明 移植指引如何增加01/02/05/0F功能码在Modbus_ProcessFrame的switch分支追加case参照寄存器处理逻辑增加线圈数组uint8_t CoilBuf[]RTU 3.5字符间隔说明本框架使用串口空闲中断自动实现帧分隔不需要定时器判断静默间隔是嵌入式最常用方案常见移植修改点修改MODBUS_SLAVE_ADDR修改从站地址修改PA0为你硬件对应的485 DE/RE引脚更换USART只需修改串口寄存器与中断名限制最大接收长度REC_BUF_MAX128足够绝大多数工业场景