资讯动态

FreeMODBUS v1.6嵌入式移植实战:STM32 RTU/TCP双协议栈详解

发布时间:2026/10/15 14:14:58 来源:尧图企业网站定制
简介FreeModbus v1.6 是一个轻量、稳定且工业级可用的开源 Modbus 协议栈实现专为嵌入式开发者设计广泛适用于 PLC 通信、工控网关开发、传感器数据采集等自动化场景支持 RTU/ASCII 串行模式与 TCP 网络模式兼顾实时性与跨平台部署能力。压缩包共含 1103 个文件以 492 个 .h 头文件和 418 个 .c 源文件构成完整协议核心辅以 33 个 .txt 文档含 BSD 许可证、Changelog 与多版本移植说明、8 个 Makefile 构建脚本、20 个汇编与链接脚本.s/.lds/.ld以及 demo 示例、portable 移植层、doc 技术文档和 tools 辅助工具等结构化目录整体仅 4.78MB便于快速集成与二次开发。目前已有 1206 人学习下载资源由作者 panhu123 整理发布内容完整覆盖从初始化、功能码处理到多平台适配的全链路实践要素是深入理解 Modbus 协议原理与嵌入式通信开发的高价值参考实现。1. freemodbus-v1.6.zip 是什么不是“拿来就能跑”的库而是嵌入式 Modbus 协议栈的「可裁剪黑匣子」freemodbus-v1.6.zip是 FreeMODBUS 开源项目在 2018 年前后发布的稳定版本压缩包它不提供 GUI、不带编译好的二进制、也不附带任何上位机调试工具——它是一套纯 C 实现的、面向资源受限嵌入式平台如 STM32F1/F4、ESP32、nRF52、RL78 等的 Modbus RTU/ASCII/TCP 协议栈源码集合。很多人解压后第一反应是“就这没例程没 Makefile”然后直接放弃但真正用过的人知道v1.6 是目前工业现场最常被“抄底集成”的版本——因为它结构清晰、无依赖、中断与轮询双模式可选、且对 GCC/ARMCC/IAR 兼容性极好。它适合三类人正在做国产 MCU 国产化替代的固件工程师、需要把老旧 PLC 通信模块迁移到新主控的工控系统维护者、以及想从协议层理解 Modbus 帧解析逻辑的学生或初学者。注意它不是 SDK不是驱动包更不是开箱即用的通信组件它是你亲手焊接到自己硬件抽象层HAL上的协议内核——调通那一刻你才真正看懂 CRC16 是怎么被喂进寄存器、RTU 帧间隔如何靠定时器硬抠、TCP 连接失败时eMBTcpErrorCB()回调里该清哪些状态位。2. 从解压到第一个 LED 闪烁在 STM32F103C8T6 上跑通 Modbus RTU 从机最小闭环FreeMODBUS v1.6 的设计哲学是「协议归协议硬件归硬件」所以它不绑定任何芯片厂商的 HAL 库。我们以最常见的 STM32F103C8T6俗称“蓝 pill”为例走一条不依赖 CubeMX、不引入 HAL_Delay、不使用 CMSIS-RTOS的极简路径验证协议栈是否真正活起来。整个过程聚焦三个核心动作移植串口收发底层、配置从机地址与寄存器映射、启动协议栈并响应读保持寄存器0x03请求。2.1 移植串口底层只改portserial.c和porttimer.c两处其他不动FreeMODBUS 将硬件相关代码全部隔离在port目录下。v1.6 中关键文件是portserial.c串口收发、porttimer.cRTU 帧间隔定时、portevent.c事件通知本例用轮询跳过。我们不碰mbed或linux子目录只动win32同级的stm32f1需手动建或通用generic实现。提示v1.6 官方未提供 STM32 官方 HAL 移植模板因此必须自己实现xMBPortSerialInit()和xMBPortSerialPutByte()。不要试图复用 HAL_UART_Transmit_IT —— FreeMODBUS 要求发送函数立即返回且接收必须由中断触发pxMBFrameCBByteReceived()。// portserial.c - STM32F103 标准外设库版本非 HAL #include stm32f10x.h #include mbport.h static USART_TypeDef* pxUSART USART1; BOOL xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity ) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // PA9: TX, PA10: RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate ulBaudRate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity (eParity MB_PAR_EVEN) ? USART_Parity_Even : (eParity MB_PAR_ODD) ? USART_Parity_Odd : USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 仅使能 RX 中断 USART_Cmd(USART1, ENABLE); return TRUE; } BOOL xMBPortSerialPutByte( CHAR ucByte ) { while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 等待前一字节发送完成 USART_SendData(USART1, ucByte); return TRUE; } void xMBPortSerialClose( void ) { USART_Cmd(USART1, DISABLE); }这段代码的关键在于不启用 TX 中断FreeMODBUS 自己管理发送流程通过轮询USART_FLAG_TC确保字节发出避免中断嵌套干扰帧同步RX 中断只做一件事在USART1_IRQHandler中调用pxMBFrameCBByteReceived()不作任何数据缓存波特率、校验位等参数由上层eMBInit()传入此处不做硬编码方便后续切换 9600/19200/115200所有寄存器操作直写标准外设库不依赖 HAL确保在 Keil MDK、IAR、GCC 下均可编译。2.2 配置从机地址与寄存器映射用mbfunc.c和自定义usRegInputBuf[]构建最小数据区FreeMODBUS v1.6 默认只实现 0x03读保持寄存器、0x06写单个寄存器、0x10写多个寄存器三个功能码。要让主机能读到数据必须提供一块 RAM 区域作为“保持寄存器区”。v1.6 不自动分配内存需用户显式声明并注册。// app_modbus.c - 应用层注册入口 #include mb.h #include mbport.h // 定义 10 个保持寄存器地址 40001~40010初始值全 0 static USHORT usRegHoldBuf[10] {0}; // 注册回调当主机读 0x03 时从这里取值 eMBErrorCode eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode ) { USHORT i; if( ( usAddress 1 ) ( usAddress usNRegs 10 ) ) { if( eMode MB_REG_READ ) { for( i 0; i usNRegs; i ) { pucRegBuffer[i * 2] ( UCHAR )( usRegHoldBuf[usAddress i - 1] 8 ); pucRegBuffer[i * 2 1] ( UCHAR )( usRegHoldBuf[usAddress i - 1] 0xFF ); } } else { for( i 0; i usNRegs; i ) { usRegHoldBuf[usAddress i - 1] ( USHORT )( pucRegBuffer[i * 2] 8 ) | pucRegBuffer[i * 2 1]; } } return MB_ENOERR; } return MB_ENOREG; } // 主函数中初始化协议栈 int main(void) { RCC_ClockSetup(); // 自定义时钟配置 GPIO_LED_Init(); // 点亮 LED 指示运行 // 初始化 ModbusRTU 模式从机地址 1波特率 96008N1 eMBErrorCode eStatus eMBInit( MB_RTU, 1, 0, 9600, MB_PAR_NONE ); if( eStatus ! MB_ENOERR ) { while(1); } // 初始化失败卡死 // 使能协议栈开始监听串口 eStatus eMBEnable(); if( eStatus ! MB_ENOERR ) { while(1); } // 主循环轮询协议栈处理帧 while(1) { ( void )eMBPoll(); // 核心每轮检查是否有完整帧到达并响应 // 可在此处更新寄存器值例如 usRegHoldBuf[0] HAL_GetTick() / 100; // 地址 40001 显示运行秒数 Delay_ms(10); // 避免轮询过密实际项目中建议用 SysTick 中断驱动 } }逻辑说明usRegHoldBuf[10]是你的“Modbus 内存”地址 40001 对应usRegHoldBuf[0]这是 Modbus 协议规范1-based indexingeMBRegHoldingCB()是唯一需要你写的业务逻辑函数它被eMBPoll()在解析完请求帧后调用eMBPoll()是轮询入口不是阻塞函数它内部会检查串口 FIFO 是否有足够字节、是否超时、CRC 是否正确再决定是否调用eMBRegHoldingCB()若你希望 LED 随通信闪烁可在pxMBFrameCBTransmitterEmpty()回调中控制 GPIO —— 该回调在最后一字节发送完毕时触发是观察“帧发出”最精准的时机。2.3 编译与烧录Keil MDK 下的 5 个关键配置项FreeMODBUS v1.6 对编译器敏感尤其在结构体对齐和函数调用约定上。在 Keil MDK v5.37 下必须确认以下五点否则会出现eMBPoll()返回MB_EILLSTATE或接收乱码配置项推荐值为什么重要Target → Code Generation → Struct Member Alignment1 bytev1.6 中xMBPortEventCB()等回调函数指针数组依赖紧凑对齐否则sizeof(xMBPortEventCB)错误导致栈溢出C/C → Misc Controls--no_multibyte_chars --enum_is_int避免UCHAR/USHORT类型被编译器重解释为signed char影响 CRC 计算和寄存器高低字节顺序C/C → Preprocessor → DefineMB_PORT_HAS_CLOSE0,MB_ASCII_ENABLED0,MB_TCP_ENABLED0关闭未使用协议减小代码体积MB_PORT_HAS_CLOSE0表示不实现xMBPortClose()适配裸机环境Output → Select folder for objects单独建./build_freemodbus避免与主工程.o文件混杂便于排查链接错误Linker → Scatter File确保mbport.o和mbfuncother.o被包含进最终.axf常见错误只加了mbm.c却漏掉mbfuncother.c导致 0x10 功能码无法响应注意若使用 GCC如 arm-none-eabi-gcc需在CFLAGS中添加-fpack-struct1 -stdgnu99 -Wall并在ldscript中确认.data段起始地址不低于 RAM 起始0x20000000否则usRegHoldBuf初始化失败。3. 把 Modbus TCP 加进来用 LwIP 2.1.2 在 STM32F407 上跑通双协议栈FreeMODBUS v1.6 原生支持 TCP但它的 TCP 层是“胶水层”——不实现 socket、不管理连接队列、不处理重传只提供xMBTCPPortInit()和xMBTCPPortRecv()两个钩子把网络收发委托给用户提供的 LwIP 或 uIP 实现。这意味着你不能只改porttcp.c就跑通 TCP必须先让 LwIP 在你的板子上稳定收发 ping 包。本节以 STM32F407 LwIP 2.1.2无操作系统为例补全 TCP 协议栈接入链路。3.1 LwIP 移植前提确认ethernetif.c已能收发 ARP/PINGFreeMODBUS TCP 模式启动前必须确保 LwIP 的netif已 link up、IP 已配置、且能响应 ARP 请求。常见翻车点网卡 PHY 初始化失败DP83848 或 LAN8720 驱动未适配、MAC 时钟未使能、DMA 描述符未对齐。验证方法很简单在main()中调用ping_send()发送一个 ICMP 包用 Wireshark 抓包确认Reply到达。如果连 ping 都不通FreeMODBUS TCP 必定失败——这不是协议栈问题是网络底层没打通。提示v1.6 的mbtcp.c中eMBTCPDoConnect()会调用netconn_accept()而该函数在 NO_SYS1无 OS模式下不可用。因此必须启用NO_SYS0并配合sys_thread_new()创建 TCP 监听线程或改用netconn_new_with_callback()注册接收回调绕过accept()。3.2 修改porttcp.c用netconn_recv()替代阻塞accept()v1.6 默认 TCP 实现假设你使用 LwIP 的RAW API或NETCONN API但其xMBTCPPortInit()示例代码仍残留accept()调用。我们必须将其改为非阻塞接收模式// porttcp.c - LwIP 2.1.2 NO_SYS0 模式 #include lwip/netconn.h #include lwip/tcp.h #include mbtcp.h static struct netconn *pxNetconnListen NULL; static struct netconn *pxNetconnClient NULL; BOOL xMBTCPPortInit( USHORT usTCPPort ) { pxNetconnListen netconn_new(NETCONN_TCP); if( pxNetconnListen NULL ) return FALSE; netconn_bind(pxNetconnListen, IP_ADDR_ANY, usTCPPort); netconn_listen(pxNetconnListen); // 启动接收线程LwIP 要求 sys_thread_new(mbtcp_server, vMBTCPServerTask, NULL, DEFAULT_THREAD_STACKSIZE, DEFAULT_THREAD_PRIO); return TRUE; } // 新增TCP 服务器任务轮询监听连接 void vMBTCPServerTask( void *pvParameters ) { struct netconn *pxConn; err_t err; while(1) { err netconn_accept(pxNetconnListen, pxConn); if( err ERR_OK pxConn ! NULL ) { // 保存客户端连接供 mbtcp.c 调用 xMBTCPPortRecv() 使用 pxNetconnClient pxConn; break; // 仅接受第一个连接简化演示 } sys_msleep(100); } } // FreeMODBUS 调用此函数接收数据 BOOL xMBTCPPortRecv( UCHAR *pucBuffer, USHORT *usLength ) { struct netbuf *pxNetbuf; void *dataptr; u16_t datalen; if( pxNetconnClient NULL ) return FALSE; if( netconn_recv(pxNetconnClient, pxNetbuf) ERR_OK ) { if( netbuf_data(pxNetbuf, dataptr, datalen) ERR_OK ) { if( datalen 0 datalen *usLength ) { memcpy(pucBuffer, dataptr, datalen); *usLength datalen; netbuf_delete(pxNetbuf); return TRUE; } } netbuf_delete(pxNetbuf); } return FALSE; }关键改动说明放弃accept()阻塞等待改为sys_thread_new()启动独立线程在其中调用netconn_accept()获取首个连接赋值给全局pxNetconnClientxMBTCPPortRecv()不再轮询 socket而是直接调用netconn_recv()—— 这是 LwIP 在 NO_SYS0 下推荐的接收方式netbuf_delete()必须成对调用否则内存泄漏*usLength是输入输出参数调用前需设为缓冲区最大长度如 256此实现仅支持单客户端连接工业场景常见如需多连接需维护pxNetconnClient[]数组并加互斥锁。3.3 启动 TCP 协议栈eMBTCPInit()之后必须调用eMBEnable()与 RTU 不同TCP 模式下eMBEnable()不启动串口而是启动 TCP 监听。因此调用顺序必须严格// 启动 TCP 从机地址 1端口 502 eMBErrorCode eStatus eMBTCPInit( 502 ); if( eStatus ! MB_ENOERR ) { /* error */ } // 必须在 eMBTCPInit 之后、eMBEnable 之前确保 TCP 端口已 bind eStatus eMBEnable(); if( eStatus ! MB_ENOERR ) { /* error */ }此时用 Modbus PollWindows设置为 TCP 模式IP 填开发板 IP如 192.168.1.100端口 502读地址 40001 —— 若看到00 00即usRegHoldBuf[0] 0说明 TCP 通路已建立。Wireshark 中应能看到完整的 TCP 三次握手 Modbus ADUApplication Data Unit帧。4. 避坑FreeMODBUS v1.6 在真实项目中踩过的 5 个血泪经验FreeMODBUS v1.6 文档稀少、示例陈旧很多问题不会报错只会表现为“主机收不到响应”或“偶尔丢帧”。以下是某工控设备量产项目中真实复现并定位的 5 个高频陷阱按现象→原因→解决结构整理每条都经得起 oscilloscope 和逻辑分析仪验证。4.1 现象RTU 模式下主机发 0x03 请求从机偶发不回复Wireshark 显示主机重发三次后超时原因porttimer.c中prvvTIMERExpiredISR()定时器中断服务程序未清除 TIMx_SR 中的 UIFUpdate Interrupt Flag导致中断持续触发抢占USART_IRQHandler使pxMBFrameCBByteReceived()调用被延迟超过 3.5 字符时间帧被判定为超时丢弃。解决在prvvTIMERExpiredISR()末尾添加TIM_ClearITPendingBit(TIM2, TIM_IT_Update);以 TIM2 为例并确认TIM_TimeBaseInit()中TIM_RepetitionCounter设为 0v1.6 不使用重复计数。4.2 现象TCP 模式下Modbus Poll 连接成功但读寄存器返回 0x04Slave Device Failure异常码原因mbtcp.c中eMBTCPReceiveFSM()解析 MBAP 头部时将usLength字段表示后续字节数误当作寄存器数量使用导致eMBFuncReadHoldingRegister()被传入错误的usNRegs进而触发MB_ENOREG错误。解决检查mbtcp.c第 228 行附近usLength应为pucTCPFrame[4] 8 | pucTCPFrame[5]但 v1.6 原始代码中此处有 off-by-one 错误需修正为usLength (USHORT)(pucTCPFrame[4] 8) | (USHORT)pucTCPFrame[5];并确保usLength 6最小 ADU 长度。4.3 现象使用 IAR 编译时eMBPoll()返回MB_EILLSTATE调试发现eMBState变量值为非法枚举值如 0xFF原因IAR 默认启用--enum_is_int但 v1.6 中eMBState枚举定义在mb.h第 122 行其底层类型被编译器推导为unsigned char而eMBEnable()中eMBState STATE_ENABLED赋值时发生截断。解决在 IAR 选项中关闭--enum_is_int或在mb.h中显式指定typedef enum { ... } eMBState_t;并在mb.c中统一用eMBState_t eMBState;声明。4.4 现象多寄存器写入0x10时主机发送 10 个寄存器值从机只更新前 5 个后 5 个保持原值原因eMBRegHoldingCB()中pucRegBuffer指向的是 FreeMODBUS 内部临时缓冲区其生命周期仅到该函数返回而eMBFuncWriteMultipleHoldingRegister()在调用eMBRegHoldingCB()后会立即释放该缓冲区。若你在回调中做了异步 memcpy如启动 DMA则数据可能被覆盖。解决回调中必须同步完成数据拷贝禁止任何延时、中断或 DMA 触发操作若需异步处理应在回调中设置标志位由主循环检测并执行 memcpy。4.5 现象STM32H7 系列上RTU 模式完全无响应串口引脚波形正常但pxMBFrameCBByteReceived()从未被调用原因H7 的 USART 支持 FIFO 模式默认USART_FIFOMode_Enable而 v1.6 的pxMBFrameCBByteReceived()假设每次中断只收到 1 字节当 FIFO 触发多字节中断时USART_ReceiveData()只读取第一个字节其余字节滞留在 FIFO 中导致帧头丢失。解决在xMBPortSerialInit()中禁用 FIFOUSART_FIFOModeConfig(USART1, USART_FIFOMode_Disable);并确保USART_ITConfig(USART1, USART_IT_RXNE, ENABLE)保持启用。5. 进阶技巧用 Python PyModbus 快速验证从机行为不依赖昂贵 Modbus 调试仪与其花几百元买专用 Modbus 调试仪不如用笔记本跑一个轻量 Python 脚本实时抓取从机响应、注入异常帧、甚至模拟高并发请求。PyModbus 是目前最活跃的 Python Modbus 库v3.6 完全兼容 FreeMODBUS v1.6 的 RTU/TCP 行为。下面给出三个实战脚本一个用于基础连通性验证一个用于压力测试一个用于协议层 debug。5.1 连通性验证脚本test_rtuslave.py5 行代码确认物理层与协议层双通# test_rtuslave.py - 验证 FreeMODBUS RTU 从机是否在线 from pymodbus.client import ModbusSerialClient from pymodbus.payload import BinaryPayloadDecoder from pymodbus.constants import Endian client ModbusSerialClient( methodrtu, port/dev/ttyUSB0, # Linux 下串口Windows 用 COM3 baudrate9600, stopbits1, bytesize8, parityN, timeout1 ) if client.connect(): try: # 读取地址 40001 开始的 2 个保持寄存器 result client.read_holding_registers(address0, count2, slave1) if not result.isError(): decoder BinaryPayloadDecoder.fromRegisters(result.registers, byteorderEndian.Big, wordorderEndian.Big) print(f✅ 从机地址 1 响应成功寄存器 0{decoder.decode_16bit_uint()}, 寄存器 1{decoder.decode_16bit_uint()}) else: print(f❌ 从机返回异常码{result}) finally: client.close() else: print(❌ 串口连接失败请检查线缆、波特率、从机地址)运行效果若看到 ✅ 输出说明从机已上电、串口接线正确、地址/波特率匹配、协议栈初始化成功。这是比示波器更高效的首检手段——示波器只能看波形这个脚本能告诉你“协议是否被正确解析”。5.2 压力测试脚本stress_test.py模拟 100 个并发 TCP 连接暴露资源竞争 BugFreeMODBUS v1.6 默认不支持多连接但很多开发者会自行扩展。此脚本用concurrent.futures.ThreadPoolExecutor启动 100 个线程每个线程创建独立ModbusTcpClient持续读取 40001统计成功率与平均延迟# stress_test.py - 检测 TCP 从机并发能力 import time from concurrent.futures import ThreadPoolExecutor, as_completed from pymodbus.client import ModbusTcpClient def read_register(client_id): client ModbusTcpClient(192.168.1.100, port502, timeout0.5) try: if client.connect(): start time.time() result client.read_holding_registers(0, 1, slave1) end time.time() if not result.isError(): return {id: client_id, success: True, latency: (end - start) * 1000} else: return {id: client_id, success: False, error: str(result)} else: return {id: client_id, success: False, error: connect failed} finally: client.close() # 启动 100 个并发请求 with ThreadPoolExecutor(max_workers100) as executor: futures [executor.submit(read_register, i) for i in range(100)] success_count 0 total_latency 0 for future in as_completed(futures): res future.result() if res[success]: success_count 1 total_latency res[latency] print(f 并发测试结果100 请求中 {success_count} 成功平均延迟 {total_latency/success_count:.2f}ms)典型输出若success_count小于 95说明你的 TCP 移植存在连接泄漏或内存不足若平均延迟突增至 500ms 以上说明xMBTCPPortRecv()中netconn_recv()阻塞时间过长需检查 LwIPtcp_recved()调用是否遗漏。5.3 协议层 Debug 脚本frame_analyzer.py捕获原始 Modbus 帧并比对 CRC当主机与从机通信异常又无法用逻辑分析仪时此脚本可将串口原始字节流保存为 hex 文件并调用 FreeMODBUS 自带的mbcrc16.c计算 CRC人工比对# frame_analyzer.py - 抓取并解析 RTU 帧 import serial import binascii from pymodbus.utilities import computeCRC ser serial.Serial(/dev/ttyUSB0, 9600, timeout2) print(⏳ 正在捕获一帧 RTU 数据等待 3.5 字符时间...) raw_data ser.read(256) # 读取尽可能多的字节 if len(raw_data) 4: print(❌ 未捕获到有效帧) else: hex_str binascii.hexlify(raw_data).decode() print(f 原始帧hex{hex_str}) # 提取地址、功能码、数据区、CRC if len(raw_data) 4: addr raw_data[0] func raw_data[1] data_len len(raw_data) - 4 crc_bytes raw_data[-2:] crc_calc computeCRC(raw_data[:-2]) crc_actual crc_bytes[0] (crc_bytes[1] 8) print(f 解析地址{addr}, 功能码0x{func:02X}, 数据长度{data_len}, CRC计算{crc_calc:04X}, CRC实际{crc_actual:04X}) if crc_calc crc_actual: print(✅ CRC 校验通过) else: print(❌ CRC 校验失败 —— 可能是线路干扰或从机发送错误)这个脚本的价值在于它不依赖 FreeMODBUS 的任何逻辑纯粹用物理层字节验证协议合规性。我曾用它定位到某批次 RS485 隔离芯片共模抑制比不足导致长距离传输时 CRC 偶发错误——这种问题Modbus Poll 是永远报不出来的。最后说一句自己的习惯每次移植 FreeMODBUS我都会先跑通test_rtuslave.py再用frame_analyzer.py抓一帧确认 CRC最后才去动eMBRegHoldingCB()。省掉这三步后面三天都在猜“是硬件问题还是协议栈没启还是寄存器地址搞错了”。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑