简介:一份基于MODBUS协议的DSP通讯程序设计文档,面向工业自动化、嵌入式系统及数字信号处理方向的工程师与学习者,系统讲解如何在DSP平台上实现MODBUS通讯。内容从MODBUS协议的消息结构、设备寻址与错误检测机制入手,结合DSP芯片的发展历程、典型应用领域及技术趋势,并深入介绍Motorola DSP56807系列的内核设计,包括算术逻辑单元ALU、地址产生单元AGU等底层模块,兼顾协议理解与硬件编程基础。资源为1个doc格式文档,压缩包大小1.8MB,文档结构完整,配有章节划分与图表说明,便于按需查阅。目前已有523人学习下载,适合作为通讯程序设计与DSP应用开发的参考资料,也可用于课程设计或项目预研时的技术铺垫。
1. 把MODBUS从站塞进DSP:这个任务到底在解决什么问题
产线上的一台变频器要接入上位机系统,组态软件只认MODBUS RTU协议,而你的控制板里是一颗专门跑电机算法的DSP。控制环路早就调通了,剩下的大活儿是:在DSP上写一套MODBUS从站通讯程序,让上位机能读状态、写参数、发控制字。这个任务看着不难——MODBUS是工业现场最老的协议之一,帧格式简单到一张表就能讲完。但只要真在DSP上做过一轮的人都知道,通讯协议本身只占三分之一的工作量,剩下三分之二全耗在“帧边界怎么定”“和主控制循环怎么共存”“现场联调时怎么排查乱帧”上。这篇文章把我自己做这类通讯程序的经验完整拆开:从协议裁剪、寄存器规划,到串口收发、CRC校验、状态机编写,再到联调现场最容易翻车的五个坑,最后给几个能让通讯程序更抗造的进阶技巧。适合正在做电机控制、数字电源、工业设备通讯的嵌入式工程师,也适合拿这类题目做毕业设计、需要把方案落到代码里的同学。
2. 协议裁剪与寄存器规划:MODBUS RTU在DSP上的落地设计
MODBUS协议本身没有规定“哪些寄存器对应哪个物理量”,这一层完全由设备厂商自己定义。所以一份能落地的DSP通讯程序,第一步不是写代码,而是做一张通讯数据映射表。映射表就是DSP主程序和上位机之间的“合同”,这张表定了,后面所有收发解析都是围绕它展开的。
2.1 RTU帧结构与3.5字符间隔
MODBUS RTU帧格式固定,每个字段的字节序和长度都有硬性规定。以从站视角看,一帧完整的请求长这样:
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1字节 | 0x01~0xF7有效,0x00为广播地址 |
| 功能码 | 1字节 | 标识本次操作类型,如03/06/16 |
| 数据区 | N字节 | 寄存器地址、数量、写入值等 |
| CRC16校验 | 2字节 | 低字节在前,高字节在后发送 |
RTU模式和ASCII模式最大的区别在于帧定界:ASCII用固定的起始/结束字符,RTU没有帧长度字段,只能靠“两个字节之间的时间间隔”来判断一帧是否结束。MODBUS规范规定,帧内相邻字节间隔不能超过1.5个字符时间,帧与帧之间必须大于3.5个字符时间。在DSP上,这个3.5字符时间就是判断“收完一帧了,可以开始处理”的唯一依据。很多第一次写通讯程序的人在这里翻车——收完最后一个字节立刻解析,结果数据还没收全,把半帧当完整帧处理。
2.2 功能码选择:够用就好,别贪多
MODBUS定义了一堆功能码,但实际设备上用到的就那么几个。做DSP从站,我一般只实现下面这几个:
| 功能码 | 含义 | 方向 | 典型用途 |
|---|---|---|---|
| 0x03 | 读保持寄存器 | 从站→主站 | 上传运行状态、测量值 |
| 0x06 | 写单个寄存器 | 主站→从站 | 修改控制字、单点参数 |
| 0x10 | 写多个寄存器 | 主站→从站 | 批量下传整定参数 |
| 0x01 / 0x02 | 读线圈/读离散输入 | 从站→主站 | 读DI/DO点位 |
| 0x05 | 写单个线圈 | 主站→从站 | 控制DO输出 |
90%的变频器、伺服、电源类设备,只做03、06、16三个功能码就够用了。线圈类操作在DSP侧如果需要,再加01/02/05。功能码不是越多越好,每多一个,异常分支和边界检查就多一层,通讯程序在实时中断环境里出问题的概率也随之增加。我习惯的做法是:版本一先实现03/06/16,跑通联调后再按需扩展。
2.3 寄存器区规划:把DSP内存映射成MODBUS地址空间
寄存器区规划是整个通讯设计的核心。MODBUS保持寄存器是16位宽度,而DSP(尤其是电机控制常用的C2000系列)的int恰好也是16位,这层对应关系非常自然。我的习惯是把设备数据按功能分成几个连续区段:
| 寄存器地址 | 读写属性 | 内容 |
|---|---|---|
| 40001~40010 | 只读 | 运行状态:母线电压、输出电流、转速、温度 |
| 40011~40020 | 可写 | 控制命令:启停、目标转速、转矩限幅 |
| 40021~40040 | 可写 | 整定参数:PID系数、加减速时间、保护阈值 |
| 40041~40050 | 只读 | 故障记录:最近10条故障码和时间戳 |
在程序里用一个映射表来管理这些对应关系,每一项描述一个寄存器区段。结构体大致长这样:
typedef struct { uint16_t reg_offset; /* MODBUS地址偏移,0对应40001 */ uint16_t reg_count; /* 本段寄存器个数 */ uint16_t access_mask; /* 读写权限:0x01只读,0x02可写 */ void *data_ptr; /* 指向DSP侧真实变量 */ uint16_t data_type; /* 0=int16, 1=uint16, 2=float, 3=bool打包 */ } ModbusRegMap_t;映射表的每一行,就是把一个MODBUS地址区间“绑定”到DSP内存里的具体变量。访问权限用掩码控制,只读区段收到写请求时直接回异常码。数据类型单独标记,因为float跨越两个寄存器,处理逻辑和int16完全不一样。这张表本身就是设计文档,现场调试时所有地址疑问都靠它仲裁。
2.4 数据模型与字节序:DSP的int是16位,别拿PC习惯想
DSP上写MODBUS最容易忽略的是数据宽度差异。PC上int是32位,DSP上int是16位,一个MODBUS寄存器正好对应一个DSP int,不用拆。但float就麻烦了,一个32位浮点数必须拆成两个保持寄存器。MODBUS协议本身没有规定float的字节序,这是现场联调时最大的“玄学”来源——同一包数据,有的PLC按高字在前解析,有的按低字在前解析。
我统一采用“大端序传输”策略:帧内先发高16位,再发低16位,每个16位内部也是先高字节后低字节。这个策略和绝大多数主流PLC默认设置一致。bool点位则按位打包,16个开关量共用一个寄存器。映射表的data_type字段会告诉我这个区段用哪种方式解析,读写代码里根据类型分别处理,不会混。
寄存器映射表做出来之后,我一般会打印一份贴在工位旁边。写协议处理代码时反复对照,联调时上位机同事问“这个地址能不能写”,低头看一眼表就能答。通讯程序本身不难写,难的是让所有参与方对同一张表达成一致。
3. 串口收发与帧定界:SCI中断里的3.5字符间隔
协议规划做完,下一步是把字节可靠地收进来、发出去。DSP上做MODBUS,最核心的底层工作就是串口收发管理。困扰大多数人的问题不是串口怎么初始化,而是“怎么知道一帧收完了”以及“收帧过程会不会被控制算法打断”。
3.1 SCI串口初始化:波特率、帧格式、FIFO触发级
DSP的串口外设通常叫SCI,初始化的核心参数是波特率、数据位、校验位和FIFO配置。以常见的C2000系列为例,一段典型的MODBUS RTU初始化代码长这样:
void Modbus_UART_Init(uint32_t baud, uint16_t cpu_mhz) { /* 1. 配置GPIO复用为SCI功能 */ GpioCtrlRegs.GPAMUX1.bit.GPIO28 = 1; /* SCIRXDA */ GpioCtrlRegs.GPAMUX1.bit.GPIO29 = 1; /* SCITXDA */ /* 2. 复位SCI,配置帧格式:8数据位+无校验+1停止位 */ SciRegs.SCICCR.all = 0x0007; /* 8N1 */ SciRegs.SCICTL1.all = 0x0003; /* 使能TX/RX */ SciRegs.SCICTL2.all = 0x0003; /* 3. 波特率设置:BRR = (CPU频率 / 波特率) / 8 - 1 */ SciRegs.SCIHBAUD.all = (cpu_mhz * 1000000 / baud / 8 - 1) >> 8; SciRegs.SCILBAUD.all = (cpu_mhz * 1000000 / baud / 8 - 1) & 0xFF; /* 4. FIFO配置:接收FIFO触发级设为1,逐字节触发中断 */ SciRegs.SCIFFTX.all = 0xE040; /* 使能FIFO和中断 */ SciRegs.SCIFFRX.all = 0x2021; /* RXFIFO复位,触发级=1 */ SciRegs.SCICTL1.all = 0x0023; /* 退出复位,使能SCI */ }帧格式和波特率要根据上位机组态实际配置来定。工业现场现在常用8N1(8数据位、无校验、1停止位),但也有人用8E1(偶校验),这取决于PLC侧的设置,DSP这边必须和PLC一致。波特率寄存器是16位的,计算BRR时有±2%的误差容限,超出这个范围会在高波特率下出现偶发乱码。
3.2 三种收帧策略:逐字节中断、查询、DMA怎么选
SCI的接收FIFO深度有限,而MODBUS最大帧长256字节,所以“等FIFO满了再处理”这条路走不通。常见的收帧策略有三种:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 逐字节中断 | FIFO触发级设为1,每个字节进FIFO立刻产生中断 | 帧长不定、需要实时判帧边界的场景 |
| 周期查询 | 主循环或定时器里轮询接收状态寄存器 | 对实时性要求不高的简单设备 |
| DMA搬运 | 配置DMA把FIFO数据批量搬到内存 | 帧长固定、数据吞吐量大的场景 |
我一般选逐字节中断。MODBUS帧长从5字节到256字节不等,逐字节中断能在每个字节到达的瞬间重置3.5字符定时器,判帧最及时。DMA批量搬运在帧长固定的场景下效率高,但MODBUS帧长不定,DMA算法写起来复杂,省下的CPU时间也有限,性价比不高。
3.3 3.5字符间隔的工程实现:软定时器喂狗法
帧定界的标准做法是“每收到一个字节就重置3.5字符定时器,定时器超时说明帧已经收完”。我的实现是在接收中断里“喂”一个递减计数器,由一个周期时基中断负责递减。这样即使主循环被控制算法占住,帧结束标志也能准时置位。
#define RX_GAP_TIMEOUT 37 /* 9600bps,100us时基下3.5字符间隔 */ /* SCI接收中断:逐字节存入缓冲区,重置帧间隔计数器 */ interrupt void Modbus_SCI_RX_ISR(void) { while (SciRegs.SCIFFRX.bit.RXFFST > 0) { g_rx_buf[g_rx_len++] = SciRegs.SCIRXBUF.all & 0xFF; if (g_rx_len >= MODBUS_RX_BUF_MAX) { g_rx_len = 0; /* 缓冲溢出,直接丢弃 */ } g_rx_gap_cnt = RX_GAP_TIMEOUT; /* 喂狗:重置3.5字符窗口 */ } } /* 时基中断:100us周期,递减帧间隔计数器 */ interrupt void Modbus_Timebase_ISR(void) { if (g_rx_gap_cnt > 0) { g_rx_gap_cnt--; if (g_rx_gap_cnt == 0) { g_modbus_frame_ready = 1; /* 超过3.5字符间隔,帧结束 */ } } }RX_GAP_TIMEOUT的值取决于波特率和时基周期,公式是:3.5字符时间 ÷ 时基周期,向上取整。3.5字符时间 = 3.5 × 10bit ÷ 波特率。不同波特率下的取值参考:
| 波特率 | 3.5字符时间(ms) | 100us时基下的计数值 |
|---|---|---|
| 9600 | 3.646 | 37 |
| 19200 | 1.823 | 19 |
| 38400 | 0.911 | 10 |
| 115200 | 0.304 | 4 |
注意115200下计数值只有4,如果时基中断本身有抖动,或者中间临时关了总中断,很容易误判帧结束。我实际做的时候会在计算值基础上加1到2个节拍,宁可略微拖延帧结束判定,也不能把一帧拆成两半。
3.4 发送响应与RS485方向切换的时序
DSP侧通常通过RS485收发器挂在总线上,半双工模式下发送前要拉高方向控制引脚DE,发送结束后拉低。这个控制时序看着简单,但方向切换的时机非常关键:DE拉得太早,最后一个字节发送不完整;拉得太晚,从站占用总线时间过长,影响下一条请求。
void Modbus_UART_SendFrame(uint16_t len) { GpioDataRegs.GPASET.bit.GPIO30 = 1; /* DE拉高,进入发送模式 */ for (uint16_t i = 0; i < len; i++) { while (SciRegs.SCICTL2.bit.TXRDY == 0) {} /* 等待发送寄存器空 */ SciRegs.SCITXBUF.all = g_tx_buf[i]; } /* 关键延迟:等最后一个字节真正移出移位寄存器 */ DELAY_US(2 * 10 * 1000000 / MODBUS_BAUD); /* 2个字符时间 */ GpioDataRegs.GPACLEAR.bit.GPIO30 = 1; /* DE拉低,切回接收 */ }发送完最后一个字节后延时2个字符时间再拉低DE,这是我常用的做法,能确保移位寄存器里的数据完整发出去。延时时间算错了也不会出大问题——最多是响应慢几个毫秒,不会烧设备。但DE拉早了一微秒,整帧数据在总线上就是残缺的,上位机收到就是CRC错误,这种问题查起来非常费劲。
4. 状态机、CRC与功能码分派:通讯程序的主循环骨架
底层收发机制搞定后,剩下的就是协议处理逻辑。这一层我习惯用一个简单状态机加一个解析函数完成。状态机管“什么时候可以处理帧”,解析函数管“这一帧到底要干什么”。两个东西分开写,代码结构清晰,也方便后面加功能。
4.1 CRC16查表法:从0xFFFF开始,低字节在前
MODBUS RTU的CRC16是基于多项式0x8005的反射实现,算法固定:初始值为0xFFFF,逐字节异或、移位、再异或多项式,最后低字节先发送。工程上普遍用查表法,256字节的查找表把每个字节的计算从8次循环变成3次异或,在中断环境里开销小得多。
/* CRC16查表实现:入参加待校验数据和长度,返回16位CRC */ uint16_t Modbus_CRC16(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; /* 反射多项式 */ } else { crc >>= 1; } } } return crc; }这是按位计算版本,逻辑直白,适合查问题;实际量产代码我换成查表版本。注意CRC的计算范围是从站地址、功能码到数据区所有字节,不包括CRC自身。校验时先把收到的CRC字段和计算值比对,一致才处理,不一致直接丢弃。很多人第一次写MODBUS都栽在顺序上:算出来的CRC,发的时候先发了高字节,结果上位机永远报校验错误。发送顺序一定是低字节在前。
4.2 主循环处理流程:帧完整 → 校验 → 解析 → 回包
主循环里的处理逻辑用标志位驱动。帧结束标志置位后,主循环取走数据,做完整校验和分派:
void Modbus_Task(void) { if (g_modbus_frame_ready) { g_modbus_frame_ready = 0; /* 1. 校验CRC */ uint16_t crc_calc = Modbus_CRC16(g_rx_buf, g_rx_len - 2); uint16_t crc_recv = g_rx_buf[g_rx_len - 2] | (g_rx_buf[g_rx_len - 1] << 8); if (crc_calc != crc_recv) { g_rx_len = 0; /* CRC错误,丢弃整帧 */ return; } /* 2. 检查地址是否匹配 */ uint8_t slave_addr = g_rx_buf[0]; if (slave_addr != g_slave_addr && slave_addr != 0x00) { g_rx_len = 0; /* 不是发给我,也不广播 */ return; } /* 3. 功能码分派 */ Modbus_Dispatch(g_rx_buf, g_rx_len); g_rx_len = 0; } }帧处理完成记得把接收长度清零,否则下一帧数据会把缓冲区堆满。地址0x00是广播地址,广播帧只处理不回包,这个逻辑在第4步的Dispatch里处理。整体流程就是:校验不过就丢,地址不对就丢,功能码不认识就回异常,数据越界就回异常码。通讯程序不怕功能少,就怕行为不可预期。
4.3 功能码分派:03读、06写单、16写多的实现骨架
功能码分派用switch语句,每个分支做三件事:检查地址和数量是否越界、执行读写、构造响应帧。以03读保持寄存器为例:
static void Modbus_Dispatch(uint8_t *req, uint16_t len) { uint8_t func = req[1]; switch (func) { case 0x03: /* 读保持寄存器 */ { uint16_t start = (req[2] << 8) | req[3]; /* 起始地址 */ uint16_t count = (req[4] << 8) | req[5]; /* 寄存器数量 */ if (count > 0x7D || start + count > REG_TABLE_SIZE) { Modbus_SendException(func, 0x02); /* 非法数据地址 */ break; } g_tx_buf[0] = g_slave_addr; g_tx_buf[1] = 0x03; g_tx_buf[2] = count * 2; /* 后续字节数 */ for (uint16_t i = 0; i < count; i++) { uint16_t val = Modbus_ReadReg(start + i); g_tx_buf[3 + i * 2] = val >> 8; /* 高字节在前 */ g_tx_buf[4 + i * 2] = val & 0xFF; } Modbus_UART_SendFrame(3 + count * 2); break; } case 0x06: /* 写单个寄存器 */ { uint16_t addr = (req[2] << 8) | req[3]; uint16_t value = (req[4] << 8) | req[5]; if (Modbus_WriteReg(addr, value) == false) { Modbus_SendException(func, 0x02); break; } /* 06的响应帧就是请求帧原样返回 */ Modbus_UART_SendFrame(8); break; } case 0x10: /* 写多个寄存器 */ { uint16_t start = (req[2] << 8) | req[3]; uint16_t count = (req[4] << 8) | req[5]; uint8_t byte_cnt = req[6]; if (byte_cnt != count * 2 || start + count > REG_TABLE_SIZE) { Modbus_SendException(func, 0x02); break; } for (uint16_t i = 0; i < count; i++) { uint16_t val = (req[7 + i * 2] << 8) | req[8 + i * 2]; Modbus_WriteReg(start + i, val); } /* 16的响应帧:从站地址+功能码+起始地址+数量 */ g_tx_buf[0] = g_slave_addr; g_tx_buf[1] = 0x10; g_tx_buf[2] = req[2]; g_tx_buf[3] = req[3]; g_tx_buf[4] = req[4]; g_tx_buf[5] = req[5]; Modbus_UART_SendFrame(6); break; } default: Modbus_SendException(func, 0x01); /* 非法功能码 */ break; } }03读操作有个容易漏的细节:一次最多读125个寄存器(0x7D),这是MODBUS规范的限制,因为响应帧长度不能超过255字节。写操作前必须先通过映射表的access_mask确认该地址可写,只读区段被写入时要回异常码02而不是默默忽略。06的响应和请求一模一样,这个行为是协议标准规定的,上位机靠这个确认写入成功。
4.4 异常码设计:让上位机知道错在哪
从站发现请求有问题时,不是不响应,而是回一个异常帧。异常响应帧的格式为:从站地址 + (功能码 | 0x80) + 异常码 + CRC。功能码最高位置1是MODBUS规定的异常标志,上位机看到最高位为1就知道这是异常反馈而不是正常响应。
| 异常码 | 含义 | 触发条件 |
|---|---|---|
| 0x01 | 非法功能码 | 收到不支持的请求,如未知功能码 |
| 0x02 | 非法数据地址 | 寄存器地址越界,或越界访问 |
| 0x03 | 非法数据值 | 数量为0、字节数不匹配、写入值超范围 |
| 0x06 | 从站设备忙 | 正在处理上一帧,暂时无法响应 |
异常码是排查问题的指示灯。现场联调时如果上位机一直报02,先查映射表的reg_count有没有写对;报03,检查写入值的范围限制;报01,看看是不是功能码压根没实现。相比不响应,异常码能让上位机侧的程序员少烧很多脑细胞——最怕的就是那种“发了请求石沉大海”的从站,谁知道是总线断了还是从站死机了。
5. 常见问题排查:从串口助手到现场总线联调的五个坑
通讯程序写完了,真正折磨人的是联调阶段。这一章把我踩过的坑按“现象→原因→解决”整理出来,每一条都在真实项目中碰到过,照着排查能省下大量用串口助手瞎试的时间。
5.1 串口助手看到的全是乱码和方框
现象:用PC串口助手直接看DSP发出的响应,数据全是乱码,尤其看不到0x00字节,全变成方框或者被截断。 原因:串口助手的“字符串显示模式”把二进制数据按ASCII字符解释了。0x00在ASCII里是空字符,很多串口助手的文本框直接把它丢弃,导致帧长度对不上。 解决:串口助手必须切换到十六进制显示模式。调试MODBUS时我不用通用串口助手,直接用专门的MODBUS调试工具,或者自己写个简单的十六进制收发小工具,能按MODBUS帧格式解析最好。
5.2 CRC算出来就是不对,反复对照算法也没问题
现象:代码里的CRC实现和标准算法逐行对比过,但上位机一直报CRC错误。用调试工具看,DSP发出的帧末尾两个字节顺序不对。 原因:MODBUS规定CRC低字节在前、高字节在后。也就是算出来0x840A,发送顺序是0x0A、0x84。很多人按习惯先发高字节,整帧校验直接挂掉。 解决:发帧时先发送crc & 0xFF,再发crc >> 8。这个坑我见过三次,每次都是从头排查才发现是字节序。另外,CRC计算初值必须是0xFFFF,不是0x0000,这两个点一起检查,能排除九成CRC问题。
5.3 一帧数据偶发被拆成两半,时好时坏
现象:上位机发请求,DSP有时候能正常响应,有时候收到的是半帧。用示波器抓总线,发现DSP的响应帧在中间断了一下,过了几十毫秒又继续发后半段。 原因:3.5字符间隔的计数值设置得太紧,或者时基中断被电机控制的PWM中断长时间抢占,导致帧间隔计数器提前归零,DSP误判“帧已结束”,后半段字节被当成新的一帧。 解决:把RX_GAP_TIMEOUT从3.5字符放宽到4.5到5个字符时间,给时基中断的抖动留余量。如果还不行,检查时基中断里有没有长时间关中断的操作——通讯帧定界最怕的就是中断响应不及时。
5.4 浮点数在PLC上显示成天文数字
现象:DSP上报的温度、转速等浮点参数,在上位机上显示成几千万的乱码值,或者小数位完全错位。 原因:MODBUS协议没有规定float的字节序。DSP是小端序,主流PLC按大端序解析。如果直接按默认字节序收发,float的高低字完全颠倒。 解决:在通讯层统一做字节序转换,帧内按“高16位在前,低16位在后”发送,每个16位内部也按高字节在前。如果不确定上位机具体用哪种格式,就做成可配置的,现场拨码选择AB模式或CD模式,联调时快速试出来。
5.5 高波特率下偶发乱帧,降低波特率就正常
现象:9600波特率下通讯一切正常,改成115200后偶尔出现应答超时或数据错误。用示波器看波形,发现起始位边沿有抖动。 原因:DSP的SCI波特率寄存器是16位的,计算BRR时四舍五入带来的误差在低波特率下可以忽略,在高波特率下可能超过2%,超出UART接收的容忍范围。另外有的DSP板载晶振本身有精度误差,高波特率下被放大。 解决:先用公式算出实际波特率,看误差是否在±1.5%内。如果超了,换更准的晶振,或者用DSP内部的高精度时钟源。现场临时方案是把波特率降回19200或9600,很多老设备的通讯不稳定,最后都是降波特率解决的。
6. 进阶技巧:浮点映射、广播帧与通讯超时保护
基础通讯跑通之后,再往工程化方向走三步:浮点数跨寄存器映射、广播帧处理、通讯超时保护。这三件事是设备从“能通讯”到“能交付现场”的分水岭。
浮点数映射的常见做法是把float拆成两个16位寄存器,在映射表里标记为float类型。用union可以简化这个操作:
typedef union { float f; uint16_t u16[2]; } FloatReg_t; /* 读出时按大端序拼装:先高16位,后低16位 */ FloatReg_t reg; reg.u16[1] = Modbus_ReadReg(addr); /* 高16位 */ reg.u16[0] = Modbus_ReadReg(addr + 1); /* 低16位 */ use_float_value(reg.f);广播帧的地址是0x00,总线上所有从站都要接收并执行,但不回任何响应。DSP侧在地址匹配逻辑里对0x00做单独处理:执行写操作,跳过回包。这个逻辑容易忽略的点是,收到广播帧后不能因为“不需要回包”就把帧丢弃,该做的参数写入仍然要做。现场用广播帧的场景通常是对所有设备下发同一组启停命令或时间校准。
通讯超时保护是现场验收的高频检查项。规定时间内没收到任何有效帧,设备必须进入安全状态。实现方式是在时基中断里递增一个计数器,收到有效帧就清零,主控制逻辑里检查阈值:
/* 时基中断里:每100us递增一次 */ g_comms_timeout_cnt++; /* 收到有效帧后清零 */ g_comms_timeout_cnt = 0; /* 主控制循环:超过500ms没通讯就置故障 */ if (g_comms_timeout_cnt > 5000) { /* 5000 * 100us = 500ms */ g_comms_fault = 1; /* 电机禁止启动或降速运行 */ }500ms的阈值是常见的现场验收值,具体按设备安全规范定。通讯程序和电机控制程序的耦合点就在这里:通讯故障不是只报个警就完事,必须影响控制行为,否则现场出了事故通讯模块是没法免责的。我做这类通讯程序的经验是:先把映射表和CRC验算搞定,再谈联调;通讯程序看着简单,但链路里任何一环——串口线、终端电阻、字节序、超时阈值——都能让现场口碑翻车。先把最基础的三件事做扎实,剩下的都是水到渠成。希望帮到你。
本文还有配套的精品资源,点击获取