本文目录
1.背景
学汽车电子的同学可能都无法逃过CAN总线这道坎,我个人认为入门CAN总线是学习汽车电子的第一课。如何根据信号对CAN信号进行打包和如何根据CAN协议对数据进行解包,是在实践中必须解决的问题,得不到CAN线上的数据,说啥都是白扯。写这篇博客的目的是因为自己在前期对CAN通信矩阵的理解不到位,比如摩托罗拉编码和因特尔编码在CAN矩阵中的形式。这篇博客我也是将最近学到的知识分享出来,在此也特别感谢某主机厂的工程师在这几天在代码生成方面对我的指导,没有他的帮助,我也不会很快将某些概念理解透彻。
2.所需要的软件与硬件
- 电脑一台。
- Matlab/Simulink(Simulink V&V Toolbox,Vehicle Network Toolbox,Embedded Coder)
- Keil5编译器。(用于生成的代码集成)
3.Motorola编码和Intel编码
作为在校学生,学校里面的项目或者外购的一些产品回来后,都无法获得产品的DBC文件(商家总说说我们没有,我TM…),所以几乎都需要自己手动根据商家提供的各种花里胡哨的文档进行CAN协议的处理。
CAN总线数据场有8个字节,扩展帧和标准帧是一样的,只不过ID范围更多,这里只讨论标准帧。因为我目前还不会制作DBC文件,所以就用手绘图的形式表达一下CAN通信矩阵。如下图所示。
对于一个uint8的字节有8个位,数据场有8个字节,所以整个数据场能包含的最大数值是2的64方减去1。只需要确定报文的ID和数据长度,就可以将这8个字节发送出去,其他节点收到后进行解包即可。回到主题,Inter编码和Motorola编码是两种不同的方式,计算机有两种储存数据的方式:大端字节序(Big Endian)和小端字节序(Little Endian)。
大端字节序:高位字节在前,低位字节在后,这是咱们人的写字方式。
小端字节序:低位字节在前,高位字节在后。
如果大家不理解我上一个图给大家看一下。这里我们定义一个车速信号,
这里我假设一个车速信号在CAN矩阵的起始位是bit20,长度是8个bit。那么按照小端字节(Intel)就数据在CAN矩阵中应该如下图所示。
很明显,从低字节到高字节。那么如果按照大端字节序(Motorola)来定义就应该按照下图所示。
从图看很明显,高位字节在前,低位字节在后。不过在非跨字节的信号中,摩托罗拉和因特尔编码没有区别。
在数据场中,信号还会涉及到分辨率和偏移量这两个概念。比如车速信号的分辨率是0.0078125,偏移量是1,那么在解包的时候就需要按照如下公式计算。
非跨字节:解包值 = 字节数据*分辨率+偏移量。跨字节就是需要视情况而定了,毕竟占位不同。
在打包的时候是不是就可以按照相反方向计算了呢?
1.自动打包和解包
第一次接触自动打包和解析还是使用的dSPACE的RTICAN,确实非常方便。最近我发现Simulink工具箱中的Vehicle Network Toolbox也可以做到,但是…,支持的硬件就只有Vector,NI等土豪朋友使用的,淘宝的几百块钱的CAN卡是不支持的,屌丝还是退出直播间吧。不过不影响打包和解包模块的使用。
使用方法很简单,就用上一节的例子。车速信号,Inter编码,起始位值是第3个字节的第5位,长度8位,分辨率0.5,无偏移量。在CAN PACK和CAN UNPACK模块中设置如下。
打包和解包的信号定义必须完全一致,ID必须完全一致,长度也必须完全一致。那么运行仿真看一下,可以发现输入和输出的值是一样的。
2.手动解包
如果需要手动进行解包应该如何处理呢,那就需要用到C的知识了,我相信看我博客的编程能力肯定都比我厉害。我再次看一下信号的定义,属于跨字节定义。计算公式如下:
速度值 = 第三个字节的高四位 +第四个字节的低四位。
我认为CAN矩阵可以给我带来最直观的认识,这也是我为什么在解包和打包的时候会将CAN矩阵画出来再看信号定义。
既然知道怎么做了,那么我们就可以开始手动解包了。
感觉手动解包也是非常简单的,用到了C语言的位运算和移位处理。第3个字节首先与“11110000”进行与运算,取出该字节的高四位,然后进行右移4位的运算计算低4位的实际值;第4个字节首先与“00001111”进行与运算取出车速数据的高四位,再乘2的4次方(高字节占了4个位),最后进行相加就可以得到车速值了。
下面我将会用一个非常实际的例子,给各位演示如何进行CAN手动打包,并且和官方模块对比进行Mil测试,然后生成代码进行Sil测试,最后将生成的代码集成在编译器中。上面只是开胃小菜,下面整点硬的。
4.实例
在非Autosar架构下,CAN报文的解包和打包放在ASW进行解决我认为还是可行的,当然在非Autosar架构下,至少在学校做相关项目来看,ASW和BSW的界限还是不明显的。话又说回来,目前Autosar上实车还是比较少的,不过大势所趋,必须去了解学习!主要我采用基于模型设计是因为手写C能力目前不够,自动生成的代码肯定比我手写C效率高的,主要原因还是我太菜。
下面是FSAC中国大学生方程式赛车关于能量计的CAN协议,将车辆的数据通过CAN总线发送至能量计,供主办方回收车辆数据。下面直接贴图(直接给dbc不香吗,又不说什么编码,差评)。
简化需求:通过动力CAN和车身CAN上的信号数据,从新按照上图所示的CAN通信协议打包,输出可以直接发送至能量计的CAN数据。
由于第一次做,还是将矩阵信息列出来,这样方便我这种菜鸟入门理解。这里有列举0x502的矩阵协议。(居然整个17个bit的,我真是醉了。)
1.打包模型建立
直接上图,手动解包的模型如下。
这里贴上源代码逻辑是很清晰的了,convert作用为了求和的时候将数据类型一致,不然会报错。输出我定义了一个bus信号,生成代码后就是一个结构体变量了,结构体里有CAN的数据,ID和数据长度。如果您对解包逻辑不清晰,可以对照我上节讲的和手绘的CAN矩阵,再结合二进制和位运算的相关概念进行分析。下面我贴出剩下两个ID的报文协议源码。
2.数据对象管理
生成高可读性的代码是需要对模型里的对象进行数据类型的定义,这是非常关键。建完模型,直接Ctrl+B,代码不是不能用,而是用起来很困难,更没法后期标定了。整个模型架构如下。
这里我以Maltab2020a为例,2019b经过更改Simulink界面用起来更方便了,如果您使用低版本也是可以关联数据字典的。
首先,关联数据字典。
其次,新建一个数据字典DD。
然后,建立完后,点击左下角外部数据。
然后,在您的数据字典里面定义所有的信号和参数,并且指定您的信号的类型,维度和存储类型等等!存储类型十分重要,您接触过代码生成的话,相信这个存储类型重要性不言而喻。下面是我对本模型的数据字典内容。您可以参考一下。由于本次生成代码只是演示过程,方便集成的时候变量我已经定义,我将输入输出变量都定义为全局变量,事实上,输入变量为内部输入更好。
其中,我用Simulink AliasType重新定义了uin8,double等名字(在Add处就可以添加),填入你想替换系统自定义的数据信息。因为我认为这样定义更符合我使用的硬件芯片习惯。在DD中进行定义后,您需要在如下图所示的地方进行替换。
还有一点需要注意的是,我在输出信号用BUS信号以便于维护和管理模型,那么在数据字典中也需要定义Bus信号,因为有三个Bus,我这里列举一个Bus对象。具体过程如下图所示。点击Add添加Bus对象。并且注意您data数组的维度!
在模型中您需要进行下列更改。
在最外层的输出模块上,您需要做一下操作。当您完成操作的时候,输出模块会显示内圈,和之前不同。之后还需要进队这条信号线进行数据关联,数据类型选择您定义好的Bus就行了。
最后完成三次,因为有三个不同的Bus对象,最后到Top层进行Signal关联到信号线。完成数据管理工作。
本次实例忽略单元测试覆盖率测试和模型检查,因为还没来得及设计大量的功能测试用例,去验证模型有无死逻辑和覆盖率能否达到要求,但是在后续的工作中这个工作是必须做到的,模型的可靠才是最重要的。
3.对比测试
官方的CANPACK模块肯定是经过严格的测试,可以放心用。那么我将手动解析模块和官方的进行对比,在大量的功能测试用例进行测试后,输出一致就可以表明我的模型是正确的,是可用的。这里为了简化,我就设计一组测试用例示例一下如何进行测试。
首先,右键空白界面,如图创建一个测试用例!我称这块为Mil测试!
如图选择,然后点击Ok
生成后的测试用例如下(输出进行了一些更改,方便对比,只输出data数据)。
我用excel简单写了一个没有任何意义的数值,作为本次的测试用例。并且将Excel表格导入Signal Builder模块,具体导入操作各位可以百度一下。非常简单,这里我就不多演示了。
将输出信号全部Logging,方便我们在SDI中查看两次仿真结果的对比。
然后点击运行。这个时候仿真结果已经出来了,那么我重新使用CANPACK模块打包,模型建立过程就不写了,十分简单,只需要在模块对话框中输入CAN协议的信息即可。就是比较花时间。同样生成一组测试用例,用刚才的测试用例对官方解包程序进行测试。官方PACK模块建模后和生成测试用例分别如下两图所示。
下面直接上结果,通过SDI查看,两者所有数据都是一致的,曲线吻合。
这里我没有Compare的原因是三个data数据的位置不一致,对比出来问题,所有我就一个一个进行对比,看看曲线是否重合。
4.等效性测试
当验证完模型的正确性后,那要开始生成代码了,如果您出于对代码生成工具的不信任,做Sil测试是必须的,当然测试方法也很简单,用之前的开发模型的功能测试用例做等效性测试是没有问题的。之前通过V&V工具箱生成了测试环境,并且模型引用了源模型,只需要右键模型->Block parameters。选择Sil即可,非常方便。
然后您引用的模型,就会出现SiL样式。
然后点击运行,这个时候软件会帮您自动把模型编译成可执行文件,这个时候验证的就是C代码了,而不是模型。运行完可以看一下对比结果。这个Sil测试需要时间长一些,可能我用的7年前的电脑原因吧-_-!。完成后进入SDI->Compare,对比Sil测试结果。误差选择0.000001。
从结果看非常舒爽对吧,说明生成的代码没有任何问题,和模型一致!然后您可以通过Embedded Coder quick start按钮进行代码生成,如何生成代码网上太多教程了,这里就不详细操作了。
5.代码集成
首先,在生成代码后会有一个代码生成报告(需要在设置中打开)。很多同学喜欢直接去看.c文件中的代码,我觉得完全没有必要,因为我可以保证我的代码100%的功能正确,所以我只要关心程序和.h头文件方便集成代码。
先看左上角的代码接口报告(Code Interface Report)。
输出为结构体。那么可以看看结构体里面有哪些东西呢,所以直接去看.h文件。
上图,宏定义非常符合手写代码的习惯。下面看结构体定义。
这下非常清楚了,我们只需要调用.c文件中的如下函数就可以实现模型的功能了。所有模型算法都在这个step函数中。
当然我们看一下自动代码生成的算法,很简洁清晰,比我手写不知道高到哪去了。
我们将一个.c和两个.h文件复制出来,ert.main文件直接放弃。将这几个文件放在您的编译器对应目录下,具体根据您的单片机使用而定,这里我用的是Stm32单片机。
在生成代码的时候是必须使用定步长的求解器,那么所谓的采样时间就是任务调度时间,我现在的板子上面跑的是裸机,没有RTOS,所以我只能根据定时器中断来计数,自己写一个任务调度,假设现在定时器中断时间为1ms,那么我的这个程序需要10ms调度一次。那么程序需要放在定时器中断服务函数中,并且自己写一个简单的任务调度程序,最后我需要通过CAN发送这个数据,库函数发CAN就非常简单了。
在CAN发送报文的函数如下,从函数接口就可以清晰对应生成出来代码的接口了。
如果您使用过STM32单片机可以很容易看懂这个操作,当然你需要在timer.c(定时器C文件)中include自动生成代码的.h文件。当然如果需要的实时性不高,您可以把这个任务放在main函数中的while(1)循环中,并且通过相应的标志位触发,只有对实时性要求很高的算法一般才放在定时器中断服务函数中。目前我在个人学习中还没有接触实时操作系统。最后编译一下代码,看看会不会出错。
代码非常好,一次成功,不过这一次成功也是经过了大量的验证,并不是建模以后就直接用了,这一点在学生时代最容易犯错,不经过大量的测试,就直接用,上车是非常危险的。
6.踩坑
本次实例踩的坑也很多啦。可能您会问,为什么不用已有的模块自动生成代码呢,原因如下:
1.没有DBC。
2.代码效率不够高。
3.锻炼自己一下对CAN报文的理解和简单的C基础。
之所以我说代码效率不高,是因为我已经做过测试了。下面是手动解包的代码行数和总字节数,在报告中都可以查看。
一共216行代码,用了93个字节。下面是自带模块的生成代码报告截图。相同的数据对象管理,生成都是RAM优先。
一共是729行代码和162个字节。所以再三权衡,我考虑手动打包信号。
5.总结
首先感谢某乎用户AutoMan对我在CANPACK模块的代码生成上进行指导,回答的答复比我问的问题字数多出好几倍,非常厉害的工程师。如果您想了解CANPACK模块和UNPACK自带模块如何生成代码的,可以关注他的知乎账号。我直接给传送门。
链接: Simulink CAN Pack/Unpack模块的代码生成.
通过这一周的学习,还是学到了怎么进行一个比较好的开发流程。总得来说Simulink就是一个工具,它可以更高效的帮助算法的开发, 在可视化的界面下进行操作,并且可以直接生成C/C++,对于无人驾驶算法,还可以生成ROS直接可用的C++代码。工具仅仅是工具。
我始终相信一句话,在学校里面就把该犯的错犯了,该闹的笑话闹了,也不至于以后丢饭碗。
如果本次内容您觉得有错的地方可以直接向我指出,欢迎各位私信交流。