嵌入式Linux下CAN总线驱动开发:实现汽车电子场景的可靠数据交互
在车载诊断(OBD)、电池管理系统(BMS)、车身控制模块(BCM)等汽车电子场景中,CAN总线依然是数据交互的事实标准。Linux从2.6内核开始引入SocketCAN子系统,将CAN总线抽象为网络设备,开发者可以用标准socket API操作CAN报文。本文将演示如何从设备树配置、驱动移植到用户空间通信,实现一个可靠的CAN数据链路。
SocketCAN架构概览
SocketCAN在Linux网络栈中增加了CAN协议族(AF_CAN),提供RAW、BCM(广播管理器)、ISO-TP等多种套接字类型。驱动层只需要实现struct net_device_ops和struct can_priv,并将接收到的CAN帧通过netif_rx()送入协议栈。用户空间则通过socket(PF_CAN, SOCK_RAW, CAN_RAW)创建套接字。
设备树配置CAN控制器
以常见的STM32MP1或i.MX6ULL为例,其内置的FlexCAN或bxCAN控制器需要在设备树中描述:
&can1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_can1>;
status = "okay";
};
&pinctrl {
pinctrl_can1: can1grp {
fsl,pins = <
MX6UL_PAD_UART3_CTS_B__CAN1_TX 0x1b020
MX6UL_PAD_UART3_RTS_B__CAN1_RX 0x1b020
>;
};
};
对于外挂的MCP2515 SPI转CAN模块,则需要添加SPI子节点:
&spi1 {
can0: mcp2515@0 {
compatible = "microchip,mcp2515";
reg = <0>;
spi-max-frequency = <10000000>;
clocks = <&clk16m>;
interrupt-parent = <&gpio4>;
interrupts = <14 IRQ_TYPE_LEVEL_LOW>;
status = "okay";
};
};
驱动核心:发送与接收
以内核自带flexcan驱动为例,发送函数核心逻辑如下:
static netdev_tx_t flexcan_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
struct flexcan_priv *priv = netdev_priv(ndev);
struct can_frame *cf = (struct can_frame *)skb->data;
struct flexcan_regs __iomem *regs = priv->regs;
/* 将CAN帧写入硬件发送邮箱 */
writel(flexcan_mb_code(TXMB, FLEXCAN_MB_CODE_TX_DATA), ®s->mb[TXMB].ctrl);
writel(cf->can_id, ®s->mb[TXMB].id);
writel(*(u32 *)cf->data, ®s->mb[TXMB].data[0]);
writel(*(u32 *)(cf->data + 4), ®s->mb[TXMB].data[1]);
/* 触发发送请求 */
writel(FLEXCAN_MB_CNT_CODE(TXMB, FLEXCAN_MB_CODE_TX_DATA) | FLEXCAN_MB_CNT_TX,
®s->mb[TXMB].ctrl);
return NETDEV_TX_OK;
}
接收则在中断处理函数中完成,通过can_rx_offload机制将帧送入协议栈:
static irqreturn_t flexcan_irq(int irq, void *dev_id)
{
struct flexcan_priv *priv = dev_id;
struct net_device *ndev = priv->dev;
struct sk_buff *skb;
struct can_frame *cf;
skb = alloc_can_skb(ndev, &cf);
if (!skb) return IRQ_HANDLED;
cf->can_id = readl(®s->mb[RXMB].id);
*(u32 *)cf->data = readl(®s->mb[RXMB].data[0]);
*(u32 *)(cf->data + 4) = readl(®s->mb[RXMB].data[1]);
cf->can_dlc = get_len(readl(®s->mb[RXMB].ctrl));
netif_receive_skb(skb);
return IRQ_HANDLED;
}
用户空间通信:can-utils
驱动正常工作后,使用ip命令配置CAN接口:
ip link set can0 type can bitrate 500000
ip link set can0 up
发送和接收报文用cansend和candump:
# 发送标准帧 ID=0x123 数据 0x11 0x22 0x33
cansend can0 123#112233
# 监听所有报文
candump can0
# 过滤特定ID
candump can0,0x123:0x7FF
若需在应用程序中编程,使用SocketCAN API:
int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
struct sockaddr_can addr;
struct ifreq ifr;
strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr *)&addr, sizeof(addr));
struct can_frame frame;
frame.can_id = 0x123;
frame.can_dlc = 3;
frame.data[0] = 0x11;
frame.data[1] = 0x22;
frame.data[2] = 0x33;
write(s, &frame, sizeof(frame)); // 发送
read(s, &frame, sizeof(frame)); // 接收
可靠性增强措施
汽车电子对可靠性要求极高,以下几点值得注意:
错误处理:监听CAN_ERR_FLAG,当总线关闭时自动重启。驱动中可配置can_priv->state状态机。
DLC校验:严格检查数据长度,防止溢出。
电源管理:在休眠/唤醒时调用can_change_state()通知协议栈。
实时性:对于高优先级报文(如刹车指令),可使用SO_PRIORITY设置socket优先级,并配合rtprio调度。
写在最后
从设备树配置到驱动编写,再到用户空间SocketCAN编程,Linux为CAN总线提供了完整的开发栈。对于汽车电子工程师而言,掌握这套体系意味着可以快速适配不同CAN控制器(FlexCAN、MCP2515、SJA1000),并在应用层实现符合ISO 15765、J1939等高层协议的数据交互。CAN虽老,但在确定性实时通信领域,它依然是嵌入式Linux最可靠的选择之一。





