当我们想要讨论CAN协议底层发送到数据线上的二进制信号的编码方式时,最值得关注的两点是:字节编码(Endianness)和比特顺序(Bit Numbering)。
先一句话揭晓答案:
CAN协议的字节编码是大端编码,比特顺序是逆序。
使用CANOE提供的CAN报文编辑工具,可以直观地看到CAN报文在内存中如下图所示:
一般情况下,对于汽车控制器的开发人员来说,是不需要知道CAN报文的编码方式的,因为通常会购买一整套的CAN协议栈代码包,来完成报文组装的工作。
不过如果开发或维护过AutoSAR的CAN协议栈的话,就会知道,CAN协议栈通过构建若干静态表的方式,将信号在内存中的Buffer、信号的起始和结束比特位、报文在内存中的Buffer等串联在一起。每一个信号都有一对Read和Write(Receive/Send)接口,间接地对信号的Buffer进行操作。所以,应用开发者只通过调用接口,将信号值写入信号的Buffer时,CAN协议栈就会自动将信号值填入报文Buffer的对应位置,并且在报文下一个发送周期将新的报文发送到CAN总线。
什么是字节编码?
不论任何数据,在现代电子和计算机系统里进行存储或传输,最终都是以二进制的形式发生的,这里的讨论不包括早期的三进制计算机和现在最新的量子计算机。受限于存储器单元尺寸或总线宽度,数据会被按8bit、16bit、32bit、64bit等长度分段切割。不同的存储器,存放被分段的二进制数据时,会使用不同的顺序,这就是不同的字节编码。
也就是说,当数据长度超过8bit时,数据的存放顺序才会被字节编码影响。
字节编码有大端编码(Big-Endian)、小端编码(Little-Endian)和混合编码(Bi-Endian or Middle-Endian)三种。
,三种字节编码方式如下所示:
- 小端编码
- 大端编码
- 混合编码,抱歉这不是CAN协议支持的格式,CANOE生成不了图片了,只能以0x0A0B0C0D为例,用表格表示一下吧:
0x0B | 0x0A | 0x0D | 0x0C
什么是比特顺序?
其实比特顺序算是汽车行业的术语,并不是很标准的一个定义,用于表示一个信号在一段消息中占据的Bit位置。通常比特顺序分为正序和反序两种,也就是通常当我们谈到八成计算机、通信协议、存储器等的Byte中的Bit的顺序时,它都是正序的。而只有当我们谈到CAN协议时,它才有可能是反序的。换句话说,CAN信号也有可能使用正序的比特顺序,毕竟它是人为定义的。
以一个16Bit长的信号为例,在大端编码的情况下,如下展示两种比特顺序:
- 正序
如图所示,Sample_Signal_6从第15Bit开始,到第0Bit结束。
- 逆序
也就是说,Sample_Signal_6从第8Bit开始,到第7Bit结束。这就变得在逻辑上有些奇怪了。目前为止,我也没查到CAN协议为什么要选择逆序的比特顺序,如果有知道原因朋友,请不妨向我透露一下。
熟悉CAN协议的同学,可能也听说过Intel格式和Motorola格式。那么,
什么是英特尔格式(Intel Order),什么又是摩托罗拉格式(Motorola Order)?
小端编码格式被称为英特尔格式,而大端编码格式也被称作摩托罗拉格式,这两种说法是等价。不论采用哪一种格式,字节编码在计算机系统中都是处于最底层的存在,要么体现在内存中字节的排列、要么体现在通信线上的码流的字节顺序,在现代计算机系统中,自动就被处理了,所以这一切对绝大多数程序开发人员来说,是透明的。
正因如此,现在已经很少讨论,为什么字节编码格式会和两家科技巨头的名字产生关系了。而且我搜索了许久,发现中文世界主要是CAN总线工程师会用Intel格式和Motorola格式的说法,却没人讨论这种说法的缘由。所以我决定对这个历史问题进行了一番探索。
最早的计算机都是大端编码。1965年前后,IBM在开发IBM System/360时,使用了大端编码格式,而后IBM开发出的一系列元祖级计算机System/370,、ESA/390、z/Architecture、Series/1等等,都是继承了这一格式。所以,大端编码为什么不叫“IBM格式”?
在这一时期,上古计算机大佬DEC(在2002年被HP吞并)开发出的PDP-10系列同样使用大端编码。可是,接下来在生产16位机PDP-11系列时,DEC却剑走偏锋,使用了一种现在被称为“混合编码”的格式。现在我们已经很陌生了,但是PDP-11可以说是那个时代最重要的计算机产品,在生涯服役期间创下了60万台的销量,一举为DEC打下计算机半壁江山。PDP-11的设计激发了Intel X86和Motorola的设计理念,同时其搭载的操作系统部分设计,也对MS-DOS产生了深远的影响。甚至现在大名鼎鼎的UNIX,第一次正式亮相也是搭载在PDP-11系列上,顺便证明了C语言在计算机编程的优越性。
IBM为了与PDP-11竞争,生产出同样是16位机的Series/1,为了提升竞争力,IBM决定把PDP-11的UNIX系统移植到Series/1上,在这次的移植中,人类第一次遭遇了字节编码差异产生的bug,这就是著名的“NUXI问题”。
IBM工程师最早移植的一段Unix函数中,有本应一个打印一串“Unix”字符串的功能,可是在实际运行时,打印出的确实“nUxi”。这是因为在混合编码的PDP-11上,“Unix”字符串在内存中的存储顺序为:
"n" | "U" | "i" | "x"
系统在读取时做了字节顺序调整。可是在大端编码的Series/1上,内存中顺序是本来就是:
"U" | "n" | "i" | "x"
系统用同样的方法调整字节顺序,反而造成了错误。
其实,虽然早期的IBM芯片使用的是大端编码,但是后来在PowerPC时代使用的却主要是混合编码了。
说完了IBM,接下来聊一聊英特尔。
那个计算机芯片军备竞赛如火如荼的时代,英特尔还是场中名不见经传的参赛者。他们接到了一个重要的生意,为CTC(Computer Terminal Corporation,早期PC大佬,现在也只剩名字还流传了)的Datapoint 2200供应芯片。这台Datapoint 2200是小端编码的计算机,因此英特尔也用小端编码设计他们的8008。虽然8008的设计周期并没有赶上Datapoint 2200,以至于2200不得不选用其他的替代方案,但是8008经验却为Intel提供了宝贵的财产。
首先,Intel 8008的指令集,在后继者——“网红爆款&时代宠儿”——Intel 8080上被继承,并且最终演化成为大名鼎鼎的x86指令集。
其次,小端编码在英特尔x86家族上被继承下来,以至于直到现在,最新的Intel x86或AMD64或x86-64家族的芯片上,即使已经能够兼容大端和小端编码了,但是实际上在最低层使用的仍旧是小端编码,只是系统多增加了一步自动转换的操作。
又说道摩托罗拉这边,没什么特别的故事了。他们家的芯片,直到变成Freescale为止都是以大端编码为主。现在Freescale都已经变成NXP了,即使产品已经引入了ARM架构,也和那个Motorola没关系了。
至于ARM架构,我们知道ARM是很“年轻”的芯片公司,再加上他们只做设计不从事生产的经营模式,产品没有那么多历史包袱。在ARM3之前都是小端编码,而之后一直以混合编码为主导。
讲到这里,答案应很清晰了,在计算机历史的长河里,Intel始终是小端编码阵营的开拓者,而Motorola的整个生涯都在大端编码的领土耕耘。所以,现在人们把“Intel 编码”等同于小端编码,而“Motorola 编码”等同于大端编码。
可是AMD也是小端编码阵营的忠实信徒,为什么没有“AMD 格式”的说法呢?
其实,大端和小端,或者说Intel格式或者Motorola格式,像那个时代的很多设计方案一样,并不是某种预知未来般的抉择,仅仅是一种更具随机性的二选一。于是,在机缘巧合之下,一方选择了大端编码,而另一方选择了小端,却变成了一种传承或者执念。