概述
CAN 总线 是汽车电子行业常用的通信协议. Nvidia 推出的边缘 AI 推理设备 JETSON TX2 / AGX Xavier 两款开发板支持 CAN 总线通信. 这里以 AGX Xavier 开发者套件 为例, 介绍如何配置实现基本的 CAN 总线数据收发, 并使用 Node.js 编写了基本的 CAN 通信应用 示例.
在 JETSON 上实现 CAN 通信应用的步骤大致是:
- 安装依赖项
- 配置寄存器
- 设置 CAN 总线参数
- 打开 CAN 总线端口
- 建立 Socket 连接
- 收发数据
其中步骤 1 是先决条件, 步骤 2-4 可以用系统命令脚本实现, 步骤 5-6 可以使用我们喜欢的语言实现.
下面的内容也基本按照这一思路展开介绍.
配置寄存器
由于 JETSON 系列开发板参照了树莓派(RaspberryPi)的 40 针引脚设计, 所以开发板上预留的 CAN0/CAN1 两组引脚默认映射到为通用输入输出端口(GPIO)上. 这就需要我们更新系统寄存器配置, 来将这些引脚重映射到 CAN0/CAN1 上. 一些教程和博客给出了修改 Pinmux
配置文件的实现方法. 这些方法由于需要刷机, 可能需要涉及 Linux 系统底层原理的知识, 也伴随着一些风险. 这里参考https://github.com/hmxf/can_xavier 和 https://blog.csdn.net/weifengdq/article/details/103093111 中给出的方法, 使用 busybox
的 devmem
工具来修改系统配置.
需要将下面默认寄存器配置修改为应用 CAN 总线的寄存器配置.
# 默认寄存器配置
# 寄存器 寄存器装载值
pinmux.0x0c303000 = 0x0000c055; # can1_dout_paa0: rsvd1, pull-down, tristate-enable, input-enable
pinmux.0x0c303008 = 0x0000c055; # can1_din_paa1: rsvd1, pull-down, tristate-enable, input-enable
pinmux.0x0c303010 = 0x0000c059; # can0_dout_paa2: rsvd1, pull-up, tristate-enable, input-enable
pinmux.0x0c303018 = 0x0000c059; # can0_din_paa3: rsvd1, pull-up, tristate-enable, input-enable
# 应用 CAN 总线的寄存器配置
# 寄存器 寄存器装载值
pinmux.0x0c303000 = 0x0000c400; # can1_dout_paa0: rsvd1, pull-down, tristate-enable, input-enable
pinmux.0x0c303008 = 0x0000c458; # can1_din_paa1: rsvd1, pull-down, tristate-enable, input-enable
pinmux.0x0c303010 = 0x0000c400; # can0_dout_paa2: rsvd1, pull-up, tristate-enable, input-enable
pinmux.0x0c303018 = 0x0000c458; # can0_din_paa3: rsvd1, pull-up, tristate-enable, input-enable
在开始修改前, 首先需要安装 busybox
,
sudo apt install busybox
可以先检查一下当前寄存器装在的值.
sudo busybox devmem 0x0c303000 # 0x0000C055
sudo busybox devmem 0x0c303008 # 0x0000C055
sudo busybox devmem 0x0c303010 # 0x0000C059
sudo busybox devmem 0x0c303018 # 0x0000C059
接下来修改寄存器配置.
sudo busybox devmem 0x0c303000 32 0x0000C400
sudo busybox devmem 0x0c303008 32 0x0000C458
sudo busybox devmem 0x0c303010 32 0x0000C400
sudo busybox devmem 0x0c303018 32 0x0000C458
可以再检查一下寄存器的值是否修改正确.
sudo busybox devmem 0x0c303000 # 0x0000C400
sudo busybox devmem 0x0c303008 # 0x0000C458
sudo busybox devmem 0x0c303010 # 0x0000C400
sudo busybox devmem 0x0c303018 # 0x0000C458
如果终端输出与 #
后面的值相同, 那么就证明完成了寄存器的配置.
设置 CAN 总线参数
我们使用 mttcan
管理系统的 CAN 总线, 这样就可以在应用中使用 Socket 打通处理 CAN 总线数据的环节.
首先需要在操作系统上挂载 mttcan
和相关组件.
sudo modprobe can
sudo modprobe can_raw
sudo modprobe can_dev
sudo modprobe mttcan
使用 lsmod
命令检查 mttcan
是否成功挂载.
$ lsmod
Module Size Used by
mttcan 66187 0
can_dev 13306 1 mttcan
can_raw 10388 0
can 46600 1 can_raw
bnep 16562 2
fuse 103841 5
zram 26166 8
overlay 48691 0
nvgpu 1569917 33
bluedroid_pm 13912 0
ip_tables 19441 0
x_tables 28951 1 ip_tables
在设置参数之前, 最好确保 ip link
未打开 can0
/ can1
两个端口, 所以先手动关闭can0
/ can1
.
sudo ip link set down can0
sudo ip link set down can1
接下来配置 CAN 总线的参数. 其中最重要的参数是 bitrate
也就是波特率, 除此以外的配置可以输入 ip link help can
来查看. 这里我们设置 bitrate
为 1Mb/s.
sudo ip link set can0 type can bitrate 1000000
sudo ip link set can1 type can bitrate 1000000
打开 CAN 总线端口
配置好参数后就可以打开 can0
/ can1
了.
sudo ip link set up can0
sudo ip link set up can1
可以使用 ifconfig
来检查输出中是否包含了 can0
/ can1
, 如果有, 那么我们已经成功打开这两个端口.
$ ifconfig
can0: flags=193<UP,RUNNING,NOARP> mtu 16
unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 10 (UNSPEC)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device interrupt 66
can1: flags=193<UP,RUNNING,NOARP> mtu 16
unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 10 (UNSPEC)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device interrupt 67
一段简单的测试
在编写应用实现 CAN 总线数据收发之前, 可以先在终端进行简单测试, 检查软硬件是否正确配置. 测试分为两步: 1) 软件层面是否可以正常收发数据; 2) 两个端口
can0
和can1
之间是否可以通过硬件线路完成数据收发.我们使用
can-utils
进行测试, 首先需要安装这个工具.sudo apt install can-utils
进行第 1) 步测试, 将
can0
端口设置为loopback
模式, 自发自收.sudo ip link set down can0 sudo ip link set can0 type can bitrate 1000000 loopback on sudo ip link set up can0
然后打开两个终端, 用终端 1 监听
can0
端口接收的数据, 用终端 2 发送数据到can0
.# 终端1 candump can0 # 终端2 <can_id>#{data}, 用.隔开,每一位是一个16进制数 cansend can0 123#99.95.42.07.2B.96.66.6E
发送数据后, 终端 1 输出:
can0 123 [8] 99 95 42 07 2B 96 66 6E can0 123 [8] 99 95 42 07 2B 96 66 6E
接下来进行第 2) 步测试, 使用
can0
向can1
发送数据. 首先要连好硬件电路. 然后打开两个终端, 终端 1 监听can1
接收的数据, 终端 2 通过can0
发送数据.# 终端1 candump can1 # 终端2 cansend can0 123#99.95.42.07.2B.96.66.6E
发送数据后, 终端 1 输出:
can1 123 [8] 99 95 42 07 2B 96 66 6E
如此, 则测试通过.
自动化脚本
上述的配置在 JETSON 重启之后会丢失, 为了快速实现上述配置步骤, 可以用 shell 脚本实现.
将下面的命令写入 set_can.sh
文件里.
# 检查寄存器配置
sudo busybox devmem 0x0c303000 && sudo busybox devmem 0x0c303008 && sudo busybox devmem 0x0c303010 && sudo busybox devmem 0x0c303018
# 修改寄存器配置
sudo busybox devmem 0x0c303000 32 0x0000C400 && sudo busybox devmem 0x0c303008 32 0x0000C458 && sudo busybox devmem 0x0c303010 32 0x0000C400 && sudo busybox devmem 0x0c303018 32 0x0000C458
# 检查一下寄存器的值是否修改正确
# 0x0000C400
# 0x0000C458
# 0x0000C400
# 0x0000C458
sudo busybox devmem 0x0c303000
sudo busybox devmem 0x0c303008
sudo busybox devmem 0x0c303010
sudo busybox devmem 0x0c303018
# 在操作系统上挂载 `mttcan` 和相关组件
sudo modprobe can
sudo modprobe can_raw
sudo modprobe can_dev
sudo modprobe mttcan
# 使用 `lsmod` 命令检查 `mttcan` 是否成功挂载
lsmod
# 接下来配置 CAN 总线的参数. 我们设置 `bitrate` 为 1Mb/s.
sudo ip link set down can0
sudo ip link set down can1
sudo ip link set can0 type can bitrate 1000000
sudo ip link set can1 type can bitrate 1000000
sudo ip link set up can0
sudo ip link set up can1
# 可以使用 `ifconfig` 来检查输出中是否包含了 `can0` / `can1`, 如果有, 那么我们已经成功打开这两个端口.
ifconfig can0
ifconfig can1
这样, 在重启机器后可以使用在set_can.sh
文件夹下运行 bash set_can.sh
完成配置.
使用基于 Node.js 封装的 SocketCAN 收发 CAN 总线数据
SocketCAN
是一个 Linux 系统下使用 C 语言 编写的 CAN 协议实现方法, 实现了各种 CAN 总线数据收发功能. rawcan
则提供了基于 Node.js 的封装, 使得我们可以使用 JavaScript 来编写自己的应用. 这个库符合 Node.js 事件驱动的理念, 对于 can 通信的接口也简单直接.
我们使用 can0
向 can1
发送数据. 首先要连好硬件电路. 然后打开两个终端, 终端 1 运行 can1.js
, 监听 can1
接收的数据; 终端 2 运行 can0.js
, 通过 can0
发送数据.
首先应该在 JETSON 上安装 Node.js, 然后新建一个文件夹, 在该文件夹下, 用 npm / cnpm 安装 rawcan
.
npm install --save rawcan
接下来, 写入 can0.js
和 can1.js
两个文件.
/********* can0.js *********/
const can = require('rawcan') // 引用 rawcan
const socket = can.createSocket('can0') // 创建一个新的 Socket, 绑定到 can0 端口
// 设置定时中断
setInterval(() => {
// 中断回调函数
// 定时通过 can0 发送数据, socket.send(id: number, buffer: Buffer | string | number[])
socket.send(can.EFF_FLAG | 0x23c89f, 'hello')
}, 1000) // 定时间隔 1000ms
/********* can1.js *********/
const can = require('rawcan') // 引用 rawcan
const socket = can.createSocket('can1') // 创建一个新的 Socket, 绑定到 can1 端口
// 如果 Socket 提交了 'error' 事件, 则在控制台打印这一错误
socket.on('error', (err) => {
console.log('socket error: ' + err)
})
socket.on('message', (id, buffer) => {
// 如果 Socket 接收到了新的 'message', 则
// 将 message 的 id 和 buffer 转为16进制编码的字符串并打印
console.log(
'received frame [' + id.toString(16) + '] ' + buffer.toString('hex')
)
})
在终端 1 和终端 2 分别依次运行这两个文件.
# 终端1
node can1.js
# 终端2
node can0.js
终端 1 每间隔 1s 输出一帧接收到的消息.
#终端1
received frame [8023c89f] 68656c6c6f
如此, 则可以在此基础上实现自己的应用.