本文假设读者具备一定的 CAN 协议知识;即使不了解也没有关系,CAN 协议本身非常简单。协议文档虽然篇幅很大,但其中很多是硬件特性的介绍。从软件角度看,CAN 通信无外乎两件事:发送报文与接收报文。
从软件定义的角度,AUTOSAR 与 Linux 的 CAN 报文结构分别如下:
// AUTOSAR
typedef struct {
uint8_t length;
Can_IdType id;
uint8_t *sdu;
} Can_PduType;
// Linux
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 can_dlc; /* frame payload length in byte (0 .. 8) */
__u8 __pad; /* padding */
__u8 __res0; /* reserved / padding */
__u8 __res1; /* reserved / padding */
__u8 data[8] __attribute__((aligned(8)));
};
CAN 网络一个突出的特性是广播:同一总线上某个节点发送的报文,可以被其他所有节点同时接收到。基于这一特性,我们可以在 Windows 或 Linux 操作系统上利用 socket 模拟总线行为,实现 CAN 网络通信的仿真。
Linux 内核本身就提供了 virtual CAN(vcan)接口,有兴趣的读者可以深入研究;而 Windows 上目前没有类似的原生机制,这正是本项目自研基于 socket 的 CAN 总线仿真方案的原因。

如上图所示,CAN Sim 是一个 TCP Server,N0、N1、N2 是 TCP Client,各自与 CAN Sim 建立 TCP 连接。当 N0 发送 CAN 报文时,报文先发送给 CAN Sim,再由 CAN Sim 转发给 N1 和 N2,从而实现虚拟广播。这就是 v1 版 TCP 仿真器,其源码为 can_simulator.c,实现相当简单,代码不足 500 行,有兴趣的读者可以阅读理解。
v2 版仿真器是目前推荐使用的版本。它不再需要中心 TCP 服务器,而是直接采用 UDP 组播(组播地址 224.244.224.245,UDP 端口为 8000 + 总线号):
v2 客户端实现位于 simulator_can_v2.cpp,对应的设备名为 simulator_v2。v1 的 TCP 方案则保留兼容,多年来它稳定支撑了 CanTp、Dcm、Com、CanNm、OsekNm 等各通信栈基础模块的开发与验证,也辅助验证了完整的 bootloader 解决方案。
Client 端同样开源,头文件 canlib.h 提供的 API 如下:
/* 扩展帧:将 CAN_ID_EXTENDED 与 ID 按位或 */
#define CAN_ID_EXTENDED 0x80000000U
#define CAN_ID_ANY ((uint32_t)-1) /* read/wait 过滤:任意 ID */
#define CAN_ID_MONITOR ((uint32_t)-2) /* read/wait 收发混合 trace 队列 */
typedef struct {
uint32_t canid; /* 11/29 位 ID,扩展帧需 OR CAN_ID_EXTENDED */
uint8_t dlc; /* 0 .. 64(CAN FD) */
uint8_t data[64];
uint64_t timestamp; /* 微秒 */
} can_frame_t;
int can_open(const char *device_name, uint32_t port, uint32_t baudrate);
bool can_write(int busid, uint32_t canid, uint8_t dlc, const uint8_t *data);
bool can_read(int busid, uint32_t *canid /* InOut */, uint8_t *dlc /* InOut */, uint8_t *data);
bool can_write_v2(int busid, can_frame_t *can_frame); /* 可自带时间戳,支持 CAN FD */
bool can_read_v2(int busid, can_frame_t *can_frame); /* 同时返回时间戳 */
bool can_close(int busid);
bool can_reset(int busid);
bool can_wait(int busid, uint32_t canid, uint32_t timeoutMs);
bool can_wait_v2(int busid, uint32_t canid, uint32_t timeoutMs); /* 低延迟,供 ISO-TP v2 刷写使用 */
目前 canlib 支持的设备如下:
| device_name | 设备 |
|---|---|
simulator |
TCP CAN 总线仿真器(v1,需启动 CanSimulator) |
simulator_v2 |
UDP 组播 CAN 总线仿真器(v2,推荐,无需服务器进程) |
qemu |
QEMU 串口虚拟 CAN |
vxl |
Vector XL Driver Library(如 CANcaseXL) |
peak |
PEAK CAN(经典 CAN) |
peakfd |
PEAK CAN(CAN FD) |
zlg |
周立功 ZLG CAN |
API 数量很少。为了与 AUTOSAR 兼容,项目在 canlib 之上又封装了一层符合 AUTOSAR CAN 驱动规范的接口,代码位于 simulator/Can.cpp,API 如下:
void Can_Init(const Can_ConfigType *Config);
void Can_DeInit(void);
Std_ReturnType Can_Write(Can_HwHandleType Hth, const Can_PduType *PduInfo);
Std_ReturnType Can_SetControllerMode(uint8_t Controller, Can_ControllerStateType Transition);
void CanIf_RxIndication(const Can_HwType *Mailbox, const PduInfoType *PduInfoPtr);
canlib 及其 AUTOSAR CAN 驱动的具体使用方法将在后续文章中介绍。
下面的演示使用 v1 版 TCP 仿真器(需要先运行 CanSimulator)。如果使用推荐的 v2 版仿真器,无需编译和启动 CanSimulator,只要把 AsPy.can() 的设备名从 'simulator' 改为 'simulator_v2' 即可,不需要任何中心进程。
# step 1: 切换到 app 标签页,编译仿真器和 AsPy 库
D:\repository\as>scons --app=CanSimulator
D:\repository\as>scons --lib=AsPy
D:\repository\as>cp build\nt\GCC\AsPy\AsPy.dll AsPy.pyd
# sim 标签页:启动 CAN 总线 0 的仿真器(simulator_v2 无需此步)
D:\repository\as>build\nt\GCC\CanSimulator\CanSimulator.exe 0
# step 2: 切换到 app 标签页,启动 Python 交互环境,模拟 CAN 节点 N0
D:\repository\as>python
>>> import os
>>> os.add_dll_directory('C:/msys64/mingw64/bin')
>>> import AsPy
>>> n0 = AsPy.can('simulator', 0)
# step 3: 切换到 boot 标签页,再启动一个 Python 交互环境,模拟 CAN 节点 N1
D:\repository\as>python
>>> import os
>>> os.add_dll_directory('C:/msys64/mingw64/bin')
>>> import AsPy
>>> n1 = AsPy.can('simulator', 0)
# step 4: 切回 sim 标签页,可以看到 CAN Sim 检测到两个节点上线
can socket 104 on-line!
can socket 108 on-line!
# step 5: 在 app 标签页,N0 发送一条 CAN 报文
>>> n0.write(0x731, bytes(range(8)))
True
# step 6: sim 标签页可以看到 N0 的报文已发送
canid=00000731,dlc=08,data=[00,01,02,03,04,05,06,07,] [........] @ 1011.104675 s rel 625132.69 ms
# step 7: 在 boot 标签页,N1 接收 N0 的报文,再回复一条
>>> n1.read(0x731)
[True, 1841, b'\x00\x01\x02\x03\x04\x05\x06\x07']
# N0 发出的 ID 为 0x731 的报文已被 N1 收到
>>> n1.write(0x732, bytes(range(8)))
True
# step 8: sim 标签页可以看到 N1 的报文
canid=00000732,dlc=08,data=[00,01,02,03,04,05,06,07,] [........] @ 1098.185181 s rel 87080.51 ms
# step 9: 回到 app 标签页,N0 接收 N1 的报文
>>> n0.read(0x732)
[True, 1842, b'\x00\x01\x02\x03\x04\x05\x06\x07']
# N1 发出的 ID 为 0x732 的报文同样被 N0 收到
可以看到,借助 canlib,只用 Python 就能轻松访问 CAN 设备、操纵报文收发,可以快速编写 CAN 相关测试用例和上位机工具。