QEMU CAN总线
在我的博客《qemu常用参数选项说明》中我介绍的一些常用的qemu参数配置,而对于嵌入式开发往往还会涉及到更多形形色色的系统总线和硬件,本文来讲述下使用qemu can总线的用法。
qemu参数配置
qemu支持can模拟的硬件不多,如果是官网下载的qemu我这里推荐使用xlnx zynqmp这个board,但是本文这里直接选择我的系列博客《基于qemu-riscv从0开始构建嵌入式linux系统》中自制的quard-star板,目前开源在github和gitee上的代码均已支持CAN模拟。
qemu启动参数如下:
-M quard-star,canbus=canbus0
-object can-bus,id=canbus0
-object can-host-socketcan,id=socketcan0,if=vcan0,canbus=canbus0
这里参数设置是将host主机上的can设备与qemu模拟的can设备相连接,如果我们host上没有can设备,则需要创建虚拟的vcan,此处vcan0即为我们需要创建的vcan。
vcan0创建
创建config_vcan.sh脚本,内容如下:
#!/bin/bash
set -e
MODE=\
"config_vcan | \
release_vcan"
USER_NAME=$(whoami)
USAGE="usage $0 [$MODE] [<CAN_NAME>] "
if [ $# == 2 ] ; then
CAN_NAME=$1
else
CAN_NAME=vcan0
fi
config_vcan()
{
modprobe vcan
ip link add dev $CAN_NAME type vcan
ip link set up $CAN_NAME
}
release_vcan()
{
ip link set down $CAN_NAME
ip link delete dev $CAN_NAME
}
case "$1" in
config_vcan)
config_vcan
;;
release_vcan)
release_vcan
;;
--help)
echo $USAGE
exit 0
;;
*)
echo $USAGE
exit 1
;;
esac
执行sudo ./config_vcan.sh config_vcan就可以成功创建vcan0,如果你想删除这个vcan则只需sudo ./config_vcan.sh release_vcan。
这里稍微解释下,vcan是linux kernel内的一个driver,提供vcan的设备,因此只需要加载该驱动,就可以使用ip link相关命令来配置vcan设备,如同真实物理can设备一样。
完成vcan配置后,输入ifconfig,一切正常就会显示存在以下设备,此时我们就可以启动qemu了。
vcan0: flags=193<UP,RUNNING,NOARP> mtu 72
unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 1000 (未指定)
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
qemu xlnx-can ip fix
原本此时我们配置好驱动就可以在qemu内的linux系统上使用can总线了,当然你可以裸机自己手写can驱动进行开发,但是我这里实测时发现直接使用linux kerenl的xlnx-can驱动,设备枚举一切正常,但数据会产生错误,经过debug分析最终确认我使用的qemu 7.0.0(截止发文前目前qemu上游mainline依然存在该问题)版本的xlnx-can ip模拟代码可能存在问题,因此这里需要手动修复(如果你使用quard-star则不需要,因为我已经修复它了)。这里描述问题如下:
在测试host上使用vcan与guest上的can通信,结果发现数据包字节序似乎不正确,因此对比kernel内的驱动代码以及qemu的代码发现xlnx的can格式和socket can格式是不匹配的,需要转换,这里阅读《ug1085-zynq-ultrascale-trm.pdf》的Table 20‐3: CAN Message Format就能发现这个问题,奇怪的是qemu-7.0.0/hw/net/can/xlnx-zynqmp-can.c内实现的似乎没有转换xlnx CAN Message Format而是把Socket CAN Format直接转发了,因此导致了错误。我这里添加或修改以下函数,问题得到解决(这里我没有向qemu上游发送patch,因为我不确定是否真实的zynq硬件情况)。
#define XCAN_IDR_IDE_MASK 0x00080000U
#define XCAN_IDR_ID1_MASK 0xFFE00000U
#define XCAN_IDR_ID2_MASK 0x0007FFFEU
#define XCAN_IDR_RTR_MASK 0x00000001U
#define XCAN_IDR_SRR_MASK 0x00100000U
#define XCAN_IDR_ID1_SHIFT 21
#define XCAN_IDR_ID2_SHIFT 1
#define CAN_SFF_ID_BITS 11
#define CAN_EFF_ID_BITS 29
static uint32_t id_xcan2can(uint32_t id)
{
uint32_t ret_id = 0;
/* Change Xilinx CAN ID format to socketCAN ID format */
if (id & XCAN_IDR_IDE_MASK) {
/* The received frame is an Extended format frame */
ret_id = (id & XCAN_IDR_ID1_MASK) >> 3;
ret_id |= (id & XCAN_IDR_ID2_MASK) >>
XCAN_IDR_ID2_SHIFT;
ret_id |= QEMU_CAN_EFF_FLAG;
if (id & XCAN_IDR_RTR_MASK)
ret_id |= QEMU_CAN_RTR_FLAG;
} else {
/* The received frame is a standard format frame */
ret_id = (id & XCAN_IDR_ID1_MASK) >>
XCAN_IDR_ID1_SHIFT;
if (id & XCAN_IDR_SRR_MASK)
ret_id |= QEMU_CAN_RTR_FLAG;
}
return ret_id;
}
static uint32_t id_can2xcan(uint32_t id)
{
uint32_t ret_id = 0;
if (id & QEMU_CAN_EFF_FLAG) {
/* Extended CAN ID format */
ret_id = ((id & QEMU_CAN_EFF_MASK) << XCAN_IDR_ID2_SHIFT) &
XCAN_IDR_ID2_MASK;
ret_id |= (((id & QEMU_CAN_EFF_MASK) >>
(CAN_EFF_ID_BITS - CAN_SFF_ID_BITS)) <<
XCAN_IDR_ID1_SHIFT) & XCAN_IDR_ID1_MASK;
ret_id |= XCAN_IDR_IDE_MASK | XCAN_IDR_SRR_MASK;
if (id & QEMU_CAN_RTR_FLAG)
ret_id |= XCAN_IDR_RTR_MASK;
} else {
/* Standard CAN ID format */
ret_id = ((id & QEMU_CAN_SFF_MASK) << XCAN_IDR_ID1_SHIFT) &
XCAN_IDR_ID1_MASK;
if (id & QEMU_CAN_RTR_FLAG)
ret_id |= XCAN_IDR_SRR_MASK;
}
return ret_id;
}
static void generate_frame(qemu_can_frame *frame, uint32_t *data)
{
frame->can_id = id_xcan2can(data[0]);
frame->can_dlc = FIELD_EX32(data[1], TXFIFO_DLC, DLC);
frame->data[0] = FIELD_EX32(data[2], TXFIFO_DATA1, DB0);
frame->data[1] = FIELD_EX32(data[2], TXFIFO_DATA1, DB1);
frame->data[2] = FIELD_EX32(data[2], TXFIFO_DATA1, DB2);
frame->data[3] = FIELD_EX32(data[2], TXFIFO_DATA1, DB3);
frame->data[4] = FIELD_EX32(data[3], TXFIFO_DATA2, DB4);
frame->data[5] = FIELD_EX32(data[3], TXFIFO_DATA2, DB5);
frame->data[6] = FIELD_EX32(data[3], TXFIFO_DATA2, DB6);
frame->data[7] = FIELD_EX32(data[3], TXFIFO_DATA2, DB7);
}
static void update_rx_fifo(XlnxZynqMPCANState *s, const qemu_can_frame *frame)
{
bool filter_pass = false;
uint16_t timestamp = 0;
/* If no filter is enabled. Message will be stored in FIFO. */
if (!((ARRAY_FIELD_EX32(s->regs, AFR, UAF1)) |
(ARRAY_FIELD_EX32(s->regs, AFR, UAF2)) |
(ARRAY_FIELD_EX32(s->regs, AFR, UAF3)) |
(ARRAY_FIELD_EX32(s->regs, AFR, UAF4)))) {
filter_pass = true;
}
/*
* Messages that pass any of the acceptance filters will be stored in
* the RX FIFO.
*/
if (ARRAY_FIELD_EX32(s->regs, AFR, UAF1)) {
uint32_t id_masked = s->regs[R_AFMR1] & frame->can_id;
uint32_t filter_id_masked = s->regs[R_AFMR1] & s->regs[R_AFIR1];
if (filter_id_masked == id_masked) {
filter_pass = true;
}
}
if (ARRAY_FIELD_EX32(s->regs, AFR, UAF2)) {
uint32_t id_masked = s->regs[R_AFMR2] & frame->can_id;
uint32_t filter_id_masked = s->regs[R_AFMR2] & s->regs[R_AFIR2];
if (filter_id_masked == id_masked) {
filter_pass = true;
}
}
if (ARRAY_FIELD_EX32(s->regs, AFR, UAF3)) {
uint32_t id_masked = s->regs[R_AFMR3] & frame->can_id;
uint32_t filter_id_masked = s->regs[R_AFMR3] & s->regs[R_AFIR3];
if (filter_id_masked == id_masked) {
filter_pass = true;
}
}
if (ARRAY_FIELD_EX32(s->regs, AFR, UAF4)) {
uint32_t id_masked = s->regs[R_AFMR4] & frame->can_id;
uint32_t filter_id_masked = s->regs[R_AFMR4] & s->regs[R_AFIR4];
if (filter_id_masked == id_masked) {
filter_pass = true;
}
}
if (!filter_pass) {
trace_xlnx_can_rx_fifo_filter_reject(frame->can_id, frame->can_dlc);
return;
}
/* Store the message in fifo if it passed through any of the filters. */
if (filter_pass && frame->can_dlc <= MAX_DLC) {
if (fifo32_is_full(&s->rx_fifo)) {
ARRAY_FIELD_DP32(s->regs, INTERRUPT_STATUS_REGISTER, RXOFLW, 1);
} else {
timestamp = CAN_TIMER_MAX - ptimer_get_count(s->can_timer);
fifo32_push(&s->rx_fifo, id_can2xcan(frame->can_id));
fifo32_push(&s->rx_fifo, deposit32(0, R_RXFIFO_DLC_DLC_SHIFT,
R_RXFIFO_DLC_DLC_LENGTH,
frame->can_dlc) |
deposit32(0, R_RXFIFO_DLC_RXT_SHIFT,
R_RXFIFO_DLC_RXT_LENGTH,
timestamp));
/* First 32 bit of the data. */
fifo32_push(&s->rx_fifo, deposit32(0, R_TXFIFO_DATA1_DB0_SHIFT,
R_TXFIFO_DATA1_DB0_LENGTH,
frame->data[0]) |
deposit32(0, R_TXFIFO_DATA1_DB1_SHIFT,
R_TXFIFO_DATA1_DB1_LENGTH,
frame->data[1]) |
deposit32(0, R_TXFIFO_DATA1_DB2_SHIFT,
R_TXFIFO_DATA1_DB2_LENGTH,
frame->data[2]) |
deposit32(0, R_TXFIFO_DATA1_DB3_SHIFT,
R_TXFIFO_DATA1_DB3_LENGTH,
frame->data[3]));
/* Last 32 bit of the data. */
fifo32_push(&s->rx_fifo, deposit32(0, R_TXFIFO_DATA2_DB4_SHIFT,
R_TXFIFO_DATA2_DB4_LENGTH,
frame->data[4]) |
deposit32(0, R_TXFIFO_DATA2_DB5_SHIFT,
R_TXFIFO_DATA2_DB5_LENGTH,
frame->data[5]) |
deposit32(0, R_TXFIFO_DATA2_DB6_SHIFT,
R_TXFIFO_DATA2_DB6_LENGTH,
frame->data[6]) |
deposit32(0, R_TXFIFO_DATA2_DB7_SHIFT,
R_TXFIFO_DATA2_DB7_LENGTH,
frame->data[7]));
ARRAY_FIELD_DP32(s->regs, INTERRUPT_STATUS_REGISTER, RXOK, 1);
trace_xlnx_can_rx_data(frame->can_id, frame->can_dlc,
frame->data[0], frame->data[1],
frame->data[2], frame->data[3],
frame->data[4], frame->data[5],
frame->data[6], frame->data[7]);
}
can_update_irq(s);
}
}
运行qemu
配置即将在qemu内运行的系统设备树
can: can@1000c000 {
compatible = "xlnx,zynq-can-1.0";
status = "disabled";
clock-names = "can_clk", "pclk";
clocks = <&can_clk>, <&pclk>;
reg = <0x0 0x1000c000 0x0 0x1000>;
interrupt-parent = <&plic>;
interrupts = <23>;
tx-fifo-depth = <0x40>;
rx-fifo-depth = <0x40>;
};
进入系统can初始化成功,然后我们通过终端配置can设备(busybox的ip命令不支持can网络相关设置,需要使用iproute2来交叉编译完整版的ip,在我的项目的target_root_app/build.sh里交叉编译该软件包的脚本,target_root_app/iproute2-5.9.0.tar.gz也在项目内有提供)。
/sbin/ip link set can0 type can bitrate 1000000 sample-point 0.750
/sbin/ifconfig can0 up
在qemu内系统输入ifconfig,一切正常则会显示
can0 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
UP RUNNING NOARP MTU:16 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:10
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
Interrupt:41
现在让我们使用can-utils工具包进行调试,我们在host和guest上均安装该工具,guest上自己需要交叉编译,host可以用apt安装。
此时先在host执行
candump vcan0
然后在guest执行
cansend can0 1807EC0B#1122334455667788
cansend can0 5A1#11.22.33.44.55.66.77.88
查看host上输出
vcan0 1807EC0B [08] 11 22 33 44 55 66 77 88
vcan0 5A1 [08] 11 22 33 44 55 66 77 88
交换host和guest上的命令,可以得到同样的结果,到这里can设备在qemu上的模拟就完成了,此时你可以使用标准的linux网络框架通过can总线在host和guest上交换数据了。当然嵌入式工程师也可以不使用linux kernel的驱动,通过裸机开发驱动发送数据到host上。