打印
[485通信]

大家有没有人觉得 MODBUS 不好用

[复制链接]
楼主: jing43
手机看帖
扫描二维码
随时随地手机跟帖
21
jing43 发表于 2015-9-4 09:23
是有点烦琐的。比如,0x10 写多个寄存器这个指令,既有寄存器数量,又有字节数这两个参数,感觉重复了; ...

你只需实现必须的功能码,然后在设备手册上交代清楚。

使用特权

评论回复
22
acguy| | 2015-9-4 09:43 | 只看该作者
jing43 发表于 2015-9-4 09:26
是啊,有包头包尾的通信协议里,如果发送数据包中不允许再出现包头包尾,那当要发送的数据正好就是包头或 ...

如果在数据包中也有包头包尾字节,而不进行转义,有可能出现无法同步,或数据张冠李代。

使用特权

评论回复
23
jing43|  楼主 | 2015-9-4 09:49 | 只看该作者
本帖最后由 jing43 于 2015-9-4 09:58 编辑

我想在这里,分享一个我们正在使用的自定义通信协议,大家都可以拿去用,不收版权税:lol
大家如果用了这个协议,如果发现有可以优化的地方,也提出来让用这个协议的人都一起改进。
如果大家有兴趣,我整理出这个通信协议的下位机与上位机的通信源代码。

这个协议的前一部分是另一个 IO 卡的,还没有完整的修改为新的 IO 卡,后面部分是新的 IO 卡的 MODBUS 协议,两个 IO 卡就是点数不同,其它操作是相似的,对于探讨这个协议应该没有影响。

IO板通信协议.pdf

284.3 KB

使用特权

评论回复
24
jing43|  楼主 | 2015-9-4 09:50 | 只看该作者
acguy 发表于 2015-9-4 09:43
如果在数据包中也有包头包尾字节,而不进行转义,有可能出现无法同步,或数据张冠李代。 ...

这个肯定是要避免的,我们的协议就是解决了这个逻辑问题,CPU 的消耗还很小。

使用特权

评论回复
25
acguy| | 2015-9-4 10:21 | 只看该作者
jing43 发表于 2015-9-4 09:50
这个肯定是要避免的,我们的协议就是解决了这个逻辑问题,CPU 的消耗还很小。 ...

没看到如何解决这个问题.
如果INFO中的信息正好构成一个有效的数据包.
而另一端又错过了INFO之前的包头字节.

使用特权

评论回复
26
jing43|  楼主 | 2015-9-4 10:56 | 只看该作者
acguy 发表于 2015-9-4 10:21
没看到如何解决这个问题.
如果INFO中的信息正好构成一个有效的数据包.
而另一端又错过了INFO之前的包头字 ...

不会的,这个协议肯定是测试过,把一个完整的数据包放入 INFO 内容再发送,让下位机完整返回,这是可以成功的。
下面第一次通信发送 INFO 为 FEFE EFEF EEEE FFFF,可以看到成功返回;用虚拟串口,可以得到这条命令的完整数据为 EE 01 04 00 FE FE EF EF EE EE FF FF 47 EF ,我们把这整条命令做为 INFO,再发测试命令,成功的返回了测试 INFO。

QQ截图20150904105356.png (41.04 KB )

QQ截图20150904105356.png

使用特权

评论回复
27
lboy| | 2015-9-4 11:21 | 只看该作者
通用就是一种标准。如果不用于不同厂家互连,最好自定义。

使用特权

评论回复
28
jing43|  楼主 | 2015-9-4 11:25 | 只看该作者
lboy 发表于 2015-9-4 11:21
通用就是一种标准。如果不用于不同厂家互连,最好自定义。

确实。但是,要想一个好一点的自定义协议,也是比较伤脑筋的,我想给大家提供一个,大家拿来就可以用。

使用特权

评论回复
29
yhn1973| | 2015-9-4 11:30 | 只看该作者
其实你认为简单的,别人不一定认为简单,ModBus-RTU通讯机理是大多数人都认为最简单可靠的串口协议,之所以用CRC,完全是为了校验的准确性,比用累加和或异或和要准确的多

使用特权

评论回复
30
yhn1973| | 2015-9-4 11:40 | 只看该作者
ModBus-RTU只要校验正确后提取数据用数据块复制就行了,而ASCII码协议还要进行数制转换,包头包尾加转义字符还要进行判断,极坏情况下还要很复杂的判断,CRC校验虽然多花了一些时间(并且这个时间并不多多少),但得到了校验的可靠行和数据提取的简便性、快速性

使用特权

评论回复
31
acguy| | 2015-9-4 11:53 | 只看该作者
jing43 发表于 2015-9-4 10:56
不会的,这个协议肯定是测试过,把一个完整的数据包放入 INFO 内容再发送,让下位机完整返回,这是可以成 ...

串口通讯要考虑到误码率的.
假设第一个EE被干扰了, 或者从站就是在第一个EE发送后上电的,  后面的INFO会不会当作包来解析?

使用特权

评论回复
32
acguy| | 2015-9-4 12:04 | 只看该作者
本帖最后由 acguy 于 2015-9-4 12:06 编辑
jing43 发表于 2015-9-4 10:56
不会的,这个协议肯定是测试过,把一个完整的数据包放入 INFO 内容再发送,让下位机完整返回,这是可以成 ...

再极端点, 假设主站发送请求, 如果从站没有应答, 一直重复发送.

INFO有4个字节:  EE xx 02 xx,  
整个包是这样的 EE ADDR 4 CMD EE xx 02 xx SUM EF
如果从站没有应答, 主站会重复发这个包.

如果从站是在第一个EE后上电的. 从站会一直略过其它字节, 直到INFO里的EE, 从站认为是包开始, 然后读到02, 认为是INFO的长度, 主站的第一个包发送结束后, 从站认为还少一个SUM与EF, 一直在等.
主站没等到应答, 发第二个包, 从站把第2个包的包头EE当作SUM, 校验错, 重新等包头, 又等到INFO段的EE, 重新开始解析.

如此重复, 从站一直无**解地解析到一个包.

使用特权

评论回复
33
acguy| | 2015-9-4 12:07 | 只看该作者
jing43 发表于 2015-9-4 10:56
不会的,这个协议肯定是测试过,把一个完整的数据包放入 INFO 内容再发送,让下位机完整返回,这是可以成 ...

另外累加和的检错能力也太次了.
CRC16也只是每个字节多用几个时钟周期而已.

使用特权

评论回复
34
jing43|  楼主 | 2015-9-4 13:44 | 只看该作者
acguy 发表于 2015-9-4 12:07
另外累加和的检错能力也太次了.
CRC16也只是每个字节多用几个时钟周期而已. ...

这里有包头、包尾、地址、和校验几个同时约定,我们还有对每一个命令进行数据范围约束,一般是不会出问题的。还有,CRC 可以可以加进去的,是一个可选校验。那个协议里就有命令使用到了 CRC。

使用特权

评论回复
35
jing43|  楼主 | 2015-9-4 13:57 | 只看该作者
acguy 发表于 2015-9-4 12:04
再极端点, 假设主站发送请求, 如果从站没有应答, 一直重复发送.

INFO有4个字节:  EE xx 02 xx,  

不会一直等的,一包数据在发送时,中间不允许停顿大于 10ms,下位在收到了部分的数据包时,如果总线上持续有 15ms 没有收到数据,将重置接收缓冲。如果缓冲区的数据大于等于了最大长度,也会重新找包头。其实,数据包的可靠传送,有几个重要的参数同时约定:包头、包尾、地址、长度、校验和、接收时间,出错的机会比较少。当然,出错的可能是存在的,但绝不会进入死锁。对一些关键数据,是有另加 CRC 校验的。上位机在发送命令后,会在 100ms 内等待下位机响应,如果没有响应就再尝试,最多三次还没有得到正确的响应,将认为当前命令不成功。

使用特权

评论回复
36
jing43|  楼主 | 2015-9-4 14:07 | 只看该作者
下面是下位机的接收代码片断:
// SCI 变量 .h 文件:
#define SciHeader 0xee    // 包头和包尾字节的选取关系到 maxInfoLen 的最大允许值,不可以乱改。
#define SciHeaderH 0xee00  // 把这个值手动修改为上一个值 * 256
#define SciTeil 0xef
#define MaxInfoLen 64   // 单位为:字(word),最大值为 116。
struct sciMessage {
Uint16 addr;     // 接收后变成地址,发送时不用管。
Uint16 cmd;     // 接收后变成命令,发送时不用管。
Uint16 info[MaxInfoLen];  // 发送与接收的信息内容,发送要填充,接收要读取。
Uint16 len;     // 接收后变成上面数组实际接收的长度,发送时不用管。
};
struct sciMessage sciTxMessage, sciRxMessage;
Uint16 baudrate, address;
volatile Uint16 sciTxCounter, sciTxLength, sciRxCounter, sciRxLength, sciCheckSum;
// .c 文件:
sci(){
register Uint16 schedulerTemp;
/*头******* 支持 SCI **************************************************************************************/
if (delayms[0] == 0) {
  sciRxCounter = 0;
}
// send
while (sciTxLength > sciTxCounter && SciaRegs.SCICTL2.bit.TXRDY == 1) {
  if ((sciTxCounter & 1) == 0) // 编译成汇编时可以发现, == 0 的写**比 == 1 的写法速度快。
   SciaRegs.SCITXBUF = *(&sciTxMessage.addr + (sciTxCounter >> 1)) >> 8; // 先发高位,再发低位。
  else
   SciaRegs.SCITXBUF = *(&sciTxMessage.addr + (sciTxCounter >> 1)) & 0xFF;
  sciTxCounter++;
}
// receive
// 00000 Receive FIFO is empty, 00001 Receive FIFO has 1 word,... 00100 Receive FIFO has 4 words
while (SciaRegs.SCIFFRX.bit.RXFFST != 0) {
  // 要读出数据以清空缓存。
  schedulerTemp = SciaRegs.SCIRXBUF.all;
  delayms[0] = 20; //
  if (sciRxCounter == 0) { // 认 SciHeader ,但又不是 ,这时丢弃数据
   if (schedulerTemp != SciHeader)
    continue;
   sciRxLength = 2;
  } else if (sciRxCounter == 1) { // == 1  表示前一字节收到了包头,这一字节接收到了地址。
   if (schedulerTemp == address || schedulerTemp == 0) {
    // 包头 + 地址 已经 OK ,将进入新一轮接收,清除上一次接收成功
    sciCheckSum = 0;
   } else {
    sciRxCounter = 0;
   }
  } else if (sciRxCounter == 2) {
   if (schedulerTemp > MaxInfoLen) {
    sciRxCounter = 0;
   } else
    sciRxLength = (schedulerTemp << 1) + 5; // MaxInfoLen 的限制保证接收数据放入数组时不越界。
  } else if (schedulerTemp == SciTeil && sciRxCounter == sciRxLength) {
   sciRxMessage.len = sciRxMessage.cmd >> 8; // length
   if ((sciCheckSum & 0xff) == 0) { // 校验和  OK
    sciRxMessage.cmd &= 0xff; // cmd
    // force(0);// 接收完,接下来就是分析数据。不能使用这一行,因为那个函数会重开 INT14。
    // 再加入这个线程到执行队列,使它在执行完 currentTcb 指向的线程后立即执行。
    force(0, 1); //
//    tcbs[0].nextTcb = currentTcb->nextTcb;
//    tcbs[0].previousTcb = currentTcb->nextTcb->previousTcb;
//    tcbs[0].nextTcb->previousTcb = &tcbs[0];
//    tcbs[0].previousTcb->nextTcb = &tcbs[0];
      // 关闭接收,只有在 SCI 线程执行完一圈后,才能重开,这样可以保证不出执行队列中不会出现两个 tcbs[0] 线程。
    SciaRegs.SCICTL1.bit.RXENA = 0; // disable receive,保护接收缓冲区。
   } // else 如果校验和不匹配,直接跳过。
     // sciRxMessage.cmd = 0xFF; // cmd == 0xFF --> checkSum error
  }
  if (sciRxCounter < sciRxLength) {
   sciCheckSum += schedulerTemp;
   if (sciRxCounter & 1)
    *(&sciRxMessage.addr + (sciRxCounter >> 1)) += schedulerTemp;
   else
    *(&sciRxMessage.addr + (sciRxCounter >> 1)) = schedulerTemp << 8;
   sciRxCounter++;
  } else
   sciRxCounter = 0;
}
/*尾****** 支持 SCI ***************************************************************************************/
}
// 应用:
void sciTask(void) { // 这个函数不能被调用,当接收到了数据后,自动被唤醒。
Uint16 i; // 注意,死循环内部不允许定义变量。
  // 下面的 死循环由接收到完整的数据包后自动进入。
for (;;) {
  sciRxMessage.addr &= 255;
  switch (sciRxMessage.cmd) {
   case 0x00:
    // echo back
    for (i = 0; i < sciRxMessage.len; i++)
     sciTxMessage.info[i] = sciRxMessage.info[i];
    send(InfoOK, sciRxMessage.len);
   break;
  }
}
// 发送:
/*
* 定义 CRC16。
* SCI 如果有 DE 控制,定义 SciDE
*/
#define CRC16
#define SciDE
#define SciDEOff()        GpioDataRegs.GPASET.bit.GPIO7 = 1   // == 1 receive
#define SciDEOn()         GpioDataRegs.GPACLEAR.bit.GPIO7 = 1 // == 0 send
#define HalfDuplex // 半双工时,接收完成后主动延时 8ms 后再发送。
// 半双工时,主动延时。如果有收发控制,发送之后要等待一定时间才切换为接收。
// 全双工时,直接调用 sciSend(returnNo, length) 函数。
#ifdef HalfDuplex
#pragma CODE_SECTION(send, "ramfuncs"); // 这个将用于 bootloader,一定要放入 ram 区。
void send(Uint16 returnNo, Uint16 length);
void send(Uint16 returnNo, Uint16 length) {
#ifdef SciDE
if(sciRxMessage.addr == 0) // 如果是广播命令,不操作 DE。
return;
SciDEOn();
sleep(8);
sciSend(returnNo, length);
sleep(2);// 这里启动发送
while(SciaRegs.SCICTL2.bit.TXEMPTY == 0)// 这里发送完成。多级缓冲发送,把缓冲区发送至空,只有发完的时候才可能出现。
sleep(1);
SciDEOff();
#else // SciDE
sleep(8);// 半双工时,主动延时。
sciSend(returnNo, length);
sleep(2);// 这里启动发送
while(SciaRegs.SCICTL2.bit.TXEMPTY == 0)// 这里发送完成。多级缓冲发送,把缓冲区发送至空,只有发完的时候才可能出现。
sleep(1);
#endif // SciDE
}
#else // HalfDuplex
#define send(returnNo, length) sciSend(returnNo, length)
#endif // HalfDuplex
void sciSend(Uint16 returnNo, Uint16 length) {
Uint16 i, sum;
if (length > MaxInfoLen || sciRxMessage.addr == 0) // 忽略广播命令
  return;
sciTxMessage.addr = SciHeaderH + address;
sciTxMessage.cmd = (length << 8) + returnNo;
for (i = 1, sum = 0xff00 - address; i < length + 2; i++) {
  sum -= (*(&sciTxMessage.addr + i) & 0xff);
  sum -= (*(&sciTxMessage.addr + i) >> 8);
}
*(&sciTxMessage.addr + i) = (sum << 8) + SciTeil; // 这里一定在 sciTxMessage 结构体内,这是正确的。
sciTxCounter = 0; // 立即发送。
sciTxLength = 6 + length + length;
}

使用特权

评论回复
37
jing43|  楼主 | 2015-9-4 14:18 | 只看该作者
刚才点到了打赏,我还以为是发问题分呢,不好意思,我暂时不想开通 21IC 的钱包,刚才被我打赏的两位,可以回复微信号领取

使用特权

评论回复
38
acguy| | 2015-9-4 14:40 | 只看该作者
jing43 发表于 2015-9-4 13:57
不会一直等的,一包数据在发送时,中间不允许停顿大于 10ms,下位在收到了部分的数据包时,如果总线上持 ...

我上面提的另一个问题, INFO段中有完整的帧, 因为同步出错, 被当作一个包解析了.  这个问题还未解决.

不如用10ms作为帧间隔.

使用特权

评论回复
39
jing43|  楼主 | 2015-9-4 14:57 | 只看该作者
acguy 发表于 2015-9-4 14:40
我上面提的另一个问题, INFO段中有完整的帧, 因为同步出错, 被当作一个包解析了.  这个问题还未解决.

不 ...

INFO 内部有完整的数据帧,这种情况基本上是不存在的,同时又出现同步出错,那就更加难得了,你说是不是?我们这个,正常发送时帧间隔是大于 10ms,下位机有超过 10ms 没有收到数据,将会重新找包头呢,也相当于双重帧间隔判定。

使用特权

评论回复
40
acguy| | 2015-9-4 15:04 | 只看该作者
jing43 发表于 2015-9-4 14:57
INFO 内部有完整的数据帧,这种情况基本上是不存在的,同时又出现同步出错,那就更加难得了,你说是不是 ...

理想的协议, 只要遵守这个协议, 即使某些节点是恶意节点, 也不影响其它设备的运行.

一般情况下INFO内部不可能有完整的数据帧, 但是如果有恶意节点, 那就是有可能充满恶意帧.
其它设备只要一失去同步, 就可能造成重大后果.  而且你无法找这些恶意节点索赔, 因为人家遵守协议.

所在MODBUS这种用于多方设备互联的协议, 必须要严谨.

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则