Duke's Blog

Learning is a life long journey.

RS485 地址自动分配

记得之前公司的七轴机械臂内部使用 RS485 通讯,每个关节都是一个 RS485 节点,当时有个命令可以用来修改设备地址,同时目标地址为 255 的命令为广播命令,所有节点都会响应, 然后,我不小心发送了一个广播改地址的命令,结果所有关节地址都相同了,也就没办法再次通过命令单独修改各关节的地址,最终要把每个关节拆开重新配置,差点把手臂毁掉,因为关节拆开后,编码器的校準也失效了,所谓牵一发动全身,悔的肠子都青了,那以后,我都会为设备加上自动分配地址的功能。

传统很多设备都用硬件拨码开关来设置地址,然而很多产品譬如机械臂关节空间很小,没有地方放置拨码开关,而且很多产品增加拨码开关后外壳处理会十分麻烦,没有软件配置灵活。


为实现地址自动分配,首先要来选择基本的 RS485 通讯协议,多个节点通讯时,建议每条通讯命令都包含源地址和目的地址,这样做不易出错,也比较简洁和统一。

建议使用 CDBUS 格式,你可能没有听过这个名字,但你可能曾经或正在使用相似的协议,它的组成包含 3 个部分:

  • 3 个字节的头:「源地址,目标地址,用户数据长度」
  • 0~255 字节的用户数据(因为数据长度用 1 个字节表示)
  • 2 个字节的 CRC 校验,涵盖整个数据包,校验算法同 ModBus.

数据包与数据包之间要有一定的空闲时间,来隔开不同的数据包,详细请参见 CDBUS 的协议定义: https://github.com/dukelec/cdbus_ip (CDBUS 最大的好处是支持硬件控制器增强,主动避让冲突,支持并发读写、多主机、对等通讯、数据主动上报等。)

譬如地址 0x00 为主机,0x01 为 1 号从机,那么主机发送两个字节数据 0x10 0x11 给 1 号从机的完整数据为:
[00 01 02 10 11 49 f0]

然后 1 号从机回覆单个 0x10 给主机:
[01 00 01 10 04 b8]

注:CDBUS 中地址 255 为广播地址;总线用户数据长度通常限制在 253 字节,方便用于硬件控制器及串口转发数据;CDBUS 硬件控制器只有在地址不冲突的情况下可以主动避让数据冲突。


然而 CDBUS 只是最底层的协议,接下来我们要定义上述用户数据的格式,最简单常用的方式就是首字节为命令号,然后后面跟可选命令参数; 回覆数据第一个字节通常为状态,然后是返回的数据。

这种方式完善之后也有一个名字,叫 CDNET, 它除了上面说的最基本的形式之外,还支持类似电脑的网络端口形式、支持多网络、确保数据完整性、大数据拆包等功能, 有兴趣可以详见:https://github.com/dukelec/cdnet

而这篇文章只涉及 CDNET 最基本的部分,举个例子,譬如命令 0x01 的定义是查询设备信息:
[00 01 01 01 91 b4]

返回设备信息的字符串:
[01 00 0f 40 4d 3a 20 … 34 crc_l crc_h]

  • 40 表示当前数据包为回覆(区分请求,详见 CDNET 定义);
  • 4d 到 34 对应的字符串为 "M: c1; S: 1234"(M 后跟设备型号,S 后面跟设备序列号,也就是唯一码),用字符串比较方便,可一次返回所有信息,也方便扩充,譬如增加版本号,设备端实现也很简便。


接下来开始进入主题,我们需要定义两个命令,一个用来查询设备信息,一个用来设置设备地址,

查询设备信息的 0x01 命令上面已经说了一半,也就是没有跟参数的情况下,直接返回设备信息, 它还可以跟以下 4 个参数来配合地址自动分配:
[max_time, mac_start, mac_end, "filter string"]

  • max_time 是两个字节,单位毫秒的时间长度,设备生成一个不大于此数的随机数,等待相应随机时间才能回覆;
  • mac_start 和 mac_end 是需要当前设备地址介于这两个数之间时,才能回覆;
  • "filter string" 是需要当前设备信息包含此字串的情况下,才能回覆(方便用于查询特定设备当前的地址)。

跟参数与不跟参数都是返回相同格式的设备信息字符串。

另一个命令是设置地址,命令号为 0x03, 它有 3 个参数,第 3 个为可选:
[0x00, new_mac, "filter string"]

  • 0x00 是代表 mac 地址设置,因为 CDNET 可以跨网,所以还有其它值代表设置网络号等;
  • new_mac 为设置的新地址;
  • "filter string" 同样是需要当前设备信息包含此字串的情况下,才能执行命令,否则忽略且不返回。

返回空数据表示成功(40 表示当前数据包为回覆这个还是要的;返回之后才改变地址)。


具体的地址自动分配流程是这样的:

  1. 先用带参数的 0x01 命令来查询总线上有哪些设备,过滤字串可以为空,地址范围可以默认为 1 ~ 254. 如果设备比较多,需要指定相对较大的等待时间范围,减少冲突的可能性,因为正常通讯时每个节点在发送数据前会先确保总线空闲,所以冲突的概率会比较低(不检测总线空闲也完全可以;另外也可以使用 CDBUS 的硬件控制器 CDCTL 进一步降低空闲检测的死区时间)(如果发送前检测到总线忙,那就等空闲立刻发送,虽然重新生成新的等待时间可以降低冲突概率,但会增加流程复杂度,没有必要)。
  2. 扫描到的设备信息都会包含唯一码,那么把目标设备的唯一码字串当作设置地址的第 3 个过滤参数,那么便可以成功修改目标设备的地址,即使当前地址被多个设备佔用。(主机不是收到回复立马修改地址,而是等最大等待时间到了之后,确保大家都回复完了之后,再慢慢整理信息、分配地址。)
  3. 指定地址范围不仅可以减少冲突,同时也可以用来忽略已经分配好地址的设备,譬如 1~9 的地址已经分配好了,那么接下来就只扫描 10 以后的地址范围。
  4. 最后还要记得改变主机地址到一个非 0x00 的地址,然后反过来测试一下是否有其它设备佔用了 0x00 地址。当然,如果你的代码规定设备地址永远不为 0x00 也就可以直接忽略此步骤。

扫描过程一般要多扫描几次,确保不会因为冲突等因素漏掉一些回覆,通常要连续扫描 3 次得到相同结果才行。


以上命令的具体定义可以参见上面的 CDNET 连接,里面有包含;
而具体的实现代码可以参考 CDBUS Bridge 的固件:https://github.com/dukelec/cdbus_bridge
(相关代码在 fw/usr/common_services.c 路径)


有很多朋友问到唯一码相关的问题,所以再补充一下:

通常唯一码的获取方法是取 CPU ID 信息,每个片子都不同。

如果片子没有 CPU ID 唯一码,也可以取周边设备,譬如蓝牙、WiFi 等 MAC 地址。

如果这些都没有,可以在代码里面放一个特定的数据,烧录工具每次烧录时动态搜寻并替换 BIN 文件中对应的数据,这样程序运行的时候取到的数据就是不同的了。也可以用烧录工具单独写 FLASH、写 EEPROM、写 OTP 区域等等。(通常是烧录工具配合条码扫描枪来录入唯一码。)

还有一个不需要唯一码的做法,就是每次开机随机生成一个序例码,譬如 6 或 8 个字节,那么重复的可能性也是很小的了。就算重复我们还有随机时间回复的机制,所以多扫描几次也能确保检测出来,万一真遇到重复的情况,发一个重启命令让部分或全部设备重新生成序例码即可。

要知道,唯一码不仅仅只是用来做地址分配,同时可以实现产品售后管理等需求,也可以用来做一些防抄保护,也让你的产品看起来更专业。


Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License.

Comments: