全志T113-S3 Linux系统CAN总线驱动配置与应用编程实战
1. 项目概述当国产SoC遇上工业级CAN总线最近在折腾一块全志T113-S3的开发板这板子性价比挺高双核Cortex-A7加一个HiFi4 DSP接口也丰富很适合做一些边缘计算或者轻量级的工控网关。板子引出了CAN接口这让我来了兴趣。CAN-BUS在汽车电子、工业自动化领域几乎是标配但很多玩Linux开发板的朋友可能更熟悉UART、I2C、SPI这些对CAN总线的实战接触不多。这次我就打算基于T113-S3把Linux下的CAN通信从驱动配置、应用层编程到实际调试整个流程跑通并记录下其中的关键点和踩过的坑。简单来说这个项目就是在全志T113-S3的Linux系统上实现标准的CAN总线通信功能。它能让你的开发板变身成为一个CAN网络节点可以跟汽车ECU、工业PLC、电机驱动器等设备“对话”。无论是想做个车载数据记录仪、工业协议转换网关还是学习汽车电子底层通信这都是一个很扎实的起点。你需要对Linux系统有基本操作能力了解一些嵌入式开发的概念但不需要是CAN协议专家因为我会把原理和操作都拆开讲清楚。2. 核心硬件与驱动环境解析2.1 全志T113-S3的CAN控制器外设全志T113-S3芯片内部集成了CAN控制器具体来说是符合CAN 2.0A/B标准的模块。这对我们开发者来说是个大利好意味着我们不需要外挂单独的CAN控制器芯片比如常见的MCP2515直接通过芯片引脚就能接入CAN网络不仅节省成本还能获得更好的性能和更低的延迟。芯片的CAN控制器通常通过一个叫做“CAN PHY”的收发器芯片与物理总线连接。PHY芯片负责将控制器出来的数字信号转换成差分信号CAN_H和CAN_L打到总线上也把总线上的差分信号转换回数字信号给控制器。T113-S3开发板上这个PHY芯片一般是SN65HVD230或其兼容型号。这里有个关键点控制器和PHY之间是TTL电平的而PHY到总线是差分电平。所以如果你测量引脚直接测控制器和PHY之间的TX、RX是测不到经典CAN波形。在Linux内核中这个CAN控制器被实现为一个网络设备。没错CAN在Linux里是一种特殊的网络协议体现在“SocketCAN”这一套实现中。这意味着你可以像操作TCP/UDP套接字一样用socket()系统调用来操作CAN总线非常方便。驱动层内核中会负责将芯片寄存器的操作、中断处理等底层细节封装好向上提供标准的网络设备接口。2.2 Linux内核驱动配置与编译要让系统识别并使用CAN首先得确保内核配置正确。全志官方提供的SDK比如Tina Linux其内核配置通常已经包含了CAN驱动支持但我们需要确认一下有时也需要根据自己需求调整。操作的核心是进入内核配置菜单。在你SDK的kernel目录下执行make menuconfig。接下来需要找到几个关键配置项CAN总线子系统支持Networking support-CAN bus subsystem support-CAN Device Drivers。这里一定要把CAN bus subsystem support编译进内核*而不是模块M因为设备启动早期可能就需要CAN。SocketCAN核心在CAN bus subsystem support下确保Socket CAN被启用。这是应用层编程的基础。全志CAN控制器驱动继续在CAN Device Drivers里寻找一般名为Allwinner Sunxi CAN support或Allwinner D1 CAN supportT113与D1的CAN模块类似。将其编译进内核。CAN收发器驱动可选但推荐有些PHY芯片如TJA1050需要特定的收发器驱动来管理待机模式等。对于常见的SN65HVD230通常不需要特殊驱动但可以搜索CAN transceiver看看是否有你的PHY型号支持。配置完成后保存退出并重新编译内核。将生成的新内核镜像烧录到开发板。启动后通过dmesg | grep can命令查看内核日志如果看到类似“sunxi_can: probe success”或“CAN device driver interface”的信息就说明驱动加载成功了。注意不同版本的内核或SDK配置路径和名称可能有细微差异。如果找不到可以尝试在内核源码目录下使用grep -r “CONFIG_CAN_SUNXI” .config来搜索相关的配置宏。2.3 设备树Device Tree节点配置现代Linux内核通过设备树DTS来描述硬件。CAN控制器的使能、时钟、引脚复用、中断等都在设备树里定义。对于T113-S3我们需要检查或修改设备树源文件.dts或.dtsi。关键是要找到CAN控制器节点。它通常长这样can: can0x02500000 { compatible “allwinner,sunxi-can”; reg 0x0 0x02500000 0x0 0x400; interrupts GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH; clocks ccu CLK_BUS_CAN, ccu CLK_CAN; clock-names “bus”, “can”; resets ccu RST_BUS_CAN; pinctrl-names “default”; pinctrl-0 can_pins_a; status “okay”; };你需要关注几个地方reg控制器内存映射地址一般不用改。interrupts中断号由芯片手册定义通常也已配好。clocks时钟源驱动需要它来工作。pinctrl-0这是最容易出问题的地方。它引用了引脚控制器的一个状态can_pins_a。你必须在pinctrl节点部分找到或添加这个can_pins_a的定义它指定了CAN_TX和CAN_RX具体复用到了哪个芯片引脚上。例如pio { can_pins_a: can-pins0 { pins “PE12”, “PE13”; // 假设CAN TX使用PE12 RX使用PE13 function “can”; // 复用功能为CAN drive-strength 10; // 驱动强度可调整 }; };务必根据你的开发板原理图确认CAN_TX和CAN_RX连接到了哪个GPIO上并在此处正确设置。如果引脚配错CAN设备将无法正常收发数据。修改好设备树源文件后需要重新编译设备树二进制文件.dtb并替换掉开发板上的文件。重启后使用ifconfig -a命令你应该能看到一个名为can0或类似的网络接口这表明CAN设备已被系统成功识别为一个网络设备。3. CAN接口配置与基础工具使用3.1 使用iproute2工具配置CAN网络接口驱动加载成功后can0接口默认是DOWN关闭状态。我们需要用Linux网络配置工具iproute2其命令就是ip来对它进行配置和启动。这是与配置以太网eth0非常相似的过程。首先设置CAN总线的比特率波特率。CAN的比特率计算涉及几个参数时钟频率、预分频器Prescaler、时间段1Tseg1、时间段2Tseg2和采样点Sample Point。对于新手我们可以使用一些标准速率并通过ip命令的bitrate参数简化设置。内核驱动会自动计算合适的时序参数。启动并配置CAN0接口为500kbps比特率# 设置比特率并启动接口 sudo ip link set can0 type can bitrate 500000 sudo ip link set can0 up这两条命令是关键。第一条指定接口类型为can并设置比特率为500kbit/s。第二条将接口启动up。检查接口状态ip -details link show can0这个命令会输出详细信息包括接口状态UP/DOWN、比特率、错误计数器等。看到state UP就说明接口已经就绪。其他常用配置命令设置监听模式不主动发送ACK用于监听总线流量sudo ip link set can0 type can bitrate 500000 listen-only on sudo ip link set can0 up设置环回模式用于自发自收测试sudo ip link set can0 type can bitrate 500000 loopback on sudo ip link set can0 up关闭接口sudo ip link set can0 down实操心得在实验室环境如果总线上只有你一个节点启动接口后可能会看到ERROR-PASSIVE甚至BUS-OFF状态这是因为CAN协议要求总线两端有终端电阻通常120Ω来匹配阻抗防止信号反射。单个节点不加电阻就启动容易因ACK缺失导致错误计数器累积。一个快速的测试方法是同时开启环回模式这样即使物理上未连接控制器内部也能完成收发自检。3.2 必备的CAN调试与监控工具在真正写代码之前有一些命令行工具极其有用它们属于can-utils软件包。1. 安装can-utils通常在构建系统时可以在SDK的包管理配置里添加can-utils。如果开发板系统已经联网也可以直接用apt或opkg安装sudo apt update sudo apt install can-utils2. 核心工具介绍candump- 总线监听器这是最常用的工具像tcpdump之于网络。它打印出总线上所有的CAN帧。candump can0它会持续输出每行显示一个时间戳、接口名、CAN帧ID十六进制、数据长度DLC和具体数据十六进制。这是分析总线活动、调试通信问题的第一选择。cansend- 单帧发送器用于向总线发送一帧特定的CAN数据。cansend can0 123#667788这条命令向can0发送一帧标准帧ID是0x123数据是三个字节0x66 0x77 0x88。canplayer- 日志回放器可以将candump记录的日志文件特定格式重新播放到总线上用于重现场景或测试。canplayer -I recorded_log.log can0cangen- 帧生成器自动生成并发送随机的或规律的CAN帧用于压力测试或填充总线。cangen can0 -g 10 -I 100 -D i这条命令每10毫秒-g 10发送一帧ID从100开始递增-I 100数据为随机递增-D i。3. 基础调试流程在一个终端启动candump can0监控总线。在另一个终端用cansend发送一帧数据。在第一个终端你应该能看到自己发出的帧。如果开启了环回模式即使不接外部设备也能看到。如果能收到自己发出的帧说明驱动、接口配置、基本收发路径是通的。接下来就可以连接真实的CAN设备如USB-CAN适配器、另一个CAN节点进行双机测试了。4. SocketCAN应用层编程实战4.1 SocketCAN编程模型与核心数据结构SocketCAN将CAN设备抽象为网络套接字因此编程模型和TCP/UDP Socket编程非常相似。主要步骤是创建套接字 - 绑定到特定CAN接口 - 发送/接收数据。首先创建套接字时使用的协议族是PF_CAN套接字类型是SOCK_RAW原始套接字可以处理所有CAN帧协议指定为CAN_RAW。int sockfd socket(PF_CAN, SOCK_RAW, CAN_RAW); if (sockfd 0) { perror(“Socket creation failed”); return -1; }接下来需要绑定套接字到一个具体的CAN接口如can0。这通过一个struct sockaddr_can地址结构体来实现。struct sockaddr_can addr; struct ifreq ifr; strcpy(ifr.ifr_name, “can0”); ioctl(sockfd, SIOCGIFINDEX, ifr); // 获取接口索引 addr.can_family AF_CAN; addr.can_ifindex ifr.ifr_ifindex; if (bind(sockfd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(“Bind failed”); close(sockfd); return -1; }这里的关键是ioctl调用它通过接口名can0获取其系统内部的索引号ifr_ifindex绑定需要这个索引。CAN帧的数据结构由struct can_frame定义位于linux/can.hstruct can_frame { canid_t can_id; // 32位的CAN ID 标志位标准帧/扩展帧远程帧等 __u8 can_dlc; // 数据长度码 0-8 __u8 __pad; // 填充字节 __u8 __res0; // 保留字节 __u8 __res1; // 保留字节 __u8 data[8] __attribute__((aligned(8))); // 数据域最多8字节 };can_id不仅包含ID其比特位还定义了帧类型。例如can_id CAN_EFF_FLAG为真表示扩展帧29位IDcan_id CAN_RTR_FLAG为真表示远程帧。can_dlc数据长度0到8。data实际数据负载。4.2 完整的CAN数据收发示例程序下面是一个简单的、完整的示例程序它打开can0接口发送一帧数据然后进入循环接收并打印接收到的所有CAN帧。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include net/if.h #include sys/ioctl.h #include sys/socket.h #include linux/can.h #include linux/can/raw.h int main() { int sockfd; struct sockaddr_can addr; struct ifreq ifr; struct can_frame frame; int nbytes; // 1. 创建SocketCAN原始套接字 sockfd socket(PF_CAN, SOCK_RAW, CAN_RAW); if (sockfd 0) { perror(“Error while opening socket”); return -1; } // 2. 指定CAN接口并获取其索引 strcpy(ifr.ifr_name, “can0”); if (ioctl(sockfd, SIOCGIFINDEX, ifr) 0) { perror(“Error getting interface index”); close(sockfd); return -1; } // 3. 绑定套接字到该CAN接口 addr.can_family AF_CAN; addr.can_ifindex ifr.ifr_ifindex; if (bind(sockfd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(“Error in socket bind”); close(sockfd); return -1; } // 4. 准备并发送一帧CAN数据 frame.can_id 0x123; // 标准帧ID frame.can_dlc 3; // 数据长度3字节 frame.data[0] 0x11; frame.data[1] 0x22; frame.data[2] 0x33; nbytes write(sockfd, frame, sizeof(struct can_frame)); if (nbytes ! sizeof(struct can_frame)) { perror(“Write failed”); } else { printf(“Message sent: ID0x%X, DLC%d, Data%02X %02X %02X\n”, frame.can_id, frame.can_dlc, frame.data[0], frame.data[1], frame.data[2]); } // 5. 循环接收CAN帧 printf(“Start receiving CAN frames…\n”); while (1) { nbytes read(sockfd, frame, sizeof(struct can_frame)); if (nbytes 0) { perror(“Read failed”); break; } if (nbytes sizeof(struct can_frame)) { fprintf(stderr, “Incomplete CAN frame\n”); continue; } // 解析并打印接收到的帧 printf(“Received: ID0x%X, DLC%d, Data:”, frame.can_id, frame.can_dlc); for (int i 0; i frame.can_dlc; i) { printf(“ %02X”, frame.data[i]); } printf(“\n”); } // 6. 关闭套接字通常不会执行到这里 close(sockfd); return 0; }编译与运行将此代码保存为can_test.c在开发板上使用交叉编译工具链或本地gcc编译gcc -o can_test can_test.c运行前请确保can0接口已经用ip link set can0 up启动。sudo ./can_test4.3 高级话题过滤、错误帧与非阻塞操作在实际项目中你可能需要更精细的控制。1. 设置接收过滤器总线上可能有很多ID的帧你的应用可能只关心其中一部分。SocketCAN允许设置过滤规则内核会帮你过滤掉不想要的帧减少用户空间的开销。struct can_filter rfilter[1]; // 只接收ID为0x100到0x1FF的标准帧 rfilter[0].can_id 0x100; rfilter[0].can_mask 0x7F0; // 掩码低4位忽略高7位必须匹配0x100 setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, rfilter, sizeof(rfilter));can_mask为1的位表示必须匹配can_id中对应的位为0的位表示不关心。上述规则匹配所有ID在0x100到0x10F之间的帧因为低4位不关心。如果想接收所有帧可以传递一个空过滤器或设置CAN_RAW_FILTER选项为NULL。2. 接收错误帧默认情况下原始套接字不接收错误帧。如果你想监控总线错误这对于诊断至关重要需要启用一个选项。int enable 1; setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_ERR_FILTER, enable, sizeof(enable));启用后read()到的帧中can_id会包含CAN_ERR_FLAG标志数据区则包含具体的错误码用于分析总线错误、控制器状态等。3. 非阻塞模式与多路复用对于需要同时处理多个I/O如CAN、串口、网络的应用可以使用非阻塞套接字配合select()或poll()。// 设置非阻塞 int flags fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); // 使用select等待数据 fd_set readfds; struct timeval timeout; FD_ZERO(readfds); FD_SET(sockfd, readfds); timeout.tv_sec 5; timeout.tv_usec 0; int ret select(sockfd 1, readfds, NULL, NULL, timeout); if (ret 0 FD_ISSET(sockfd, readfds)) { // 有数据可读 nbytes read(sockfd, frame, sizeof(frame)); // … 处理帧 } else if (ret 0) { printf(“Timeout, no CAN data.\n”); } else { perror(“Select error”); }5. 系统集成与实战调试经验5.1 开机自启动CAN服务配置在嵌入式产品中CAN功能通常需要随系统启动自动配置。有几种常见方法1. Systemd服务推荐适用于使用systemd的现代Linux发行版创建一个服务文件例如/etc/systemd/system/can-setup.service[Unit] DescriptionConfigure CAN interface Afternetwork.target Beforeyour_application.service [Service] Typeoneshot RemainAfterExityes ExecStart/usr/local/bin/setup_can.sh ExecStop/sbin/ip link set can0 down [Install] WantedBymulti-user.target然后创建对应的脚本/usr/local/bin/setup_can.sh并赋予执行权限#!/bin/bash /sbin/ip link set can0 type can bitrate 500000 /sbin/ip link set can0 up最后启用服务sudo systemctl enable can-setup.service sudo systemctl start can-setup.service2. 初始化脚本如/etc/rc.local在/etc/rc.local文件需要系统支持的exit 0之前添加配置命令/sbin/ip link set can0 type can bitrate 500000 /sbin/ip link set can0 up3. 网络配置文件如/etc/network/interfaces 部分系统支持对于Debian系可以在网络配置中定义CAN接口auto can0 iface can0 inet manual pre-up /sbin/ip link set can0 type can bitrate 500000 up /sbin/ip link set can0 up down /sbin/ip link set can0 down实操心得我更喜欢使用systemd服务的方式因为它管理起来更规范启动、停止、状态查看、日志并且可以定义依赖关系比如确保CAN在某个应用启动前就绪。在脚本里除了设置比特率还可以加入一些健壮性检查比如先检查can0设备是否存在或者根据硬件拨码开关动态决定是否启用CAN。5.2 物理层连接与信号质量保障软件调通了硬件连接同样关键。CAN总线是差分信号对物理层的要求比UART要高。终端电阻这是最容易被忽略也最重要的一点。高速CAN总线比特率高于100kbps必须在总线的两个最远端各接一个120Ω的电阻以消除信号反射。如果你的开发板是总线上唯一的节点或者处于总线末端务必在CAN_H和CAN_L之间焊接一个120Ω的电阻。很多开发板会预留这个电阻的位置通过跳线帽选择是否接入。没有终端电阻通信可能极不稳定甚至完全无法进行。线缆选择尽量使用双绞线CAN_H和CAN_L相互绞合这能有效抑制共模干扰。线径不用很粗但屏蔽层如果使用屏蔽双绞线应单点接地。连接器汽车上常用DB9或端子台工业上可能用M12或凤凰端子。确保连接牢固引脚对应正确CAN_H CAN_L GND。GND线一定要接它为收发器提供共地参考。电源与隔离在工业强干扰环境考虑使用带隔离的CAN收发器模块或隔离电源以保护主控芯片免受地电位差或浪涌的损害。5.3 典型问题排查与性能调优即使按照步骤操作也可能会遇到问题。下面是一个排查清单问题ip link set can0 up失败提示“No such device”排查ifconfig -a或ip link里根本没有can0。可能原因内核驱动未加载。检查dmesg | grep can确认驱动probe成功。设备树配置错误特别是引脚复用pinctrl。检查设备树中CAN节点的status是否为“okay”pinctrl引用的引脚组是否正确。硬件上CAN控制器供电或时钟未开启较少见需查芯片手册电源/时钟树。问题接口能UP但candump收不到任何数据cansend后错误计数器猛增排查ip -d link show can0查看状态和错误计数器RX/TX errors。可能原因终端电阻缺失这是最常见原因。确保总线两端有120Ω电阻。比特率不匹配发送和接收节点的比特率必须严格一致。检查所有节点的配置。物理连接问题线断了、接反了CAN_H和CAN_L互换、接触不良。用万用表测量总线电阻应在60Ω左右两个120Ω并联测量CAN_H和CAN_L之间的差分电压静止时约2.5V显性位时差值变化。总线上存在持续的错误帧干扰导致控制器进入BUS-OFF状态。重启接口ip link set can0 down; ip link set can0 up可复位。问题能收到数据但应用层程序收不到排查先用candump确认物理层有数据。再用cat /proc/net/can/stats查看内核CAN统计信息。可能原因应用层套接字绑定bind的接口索引错误或绑定了错误的接口名。应用层设置了接收过滤器Filter过滤掉了你想收的ID。应用层接收缓冲区太小或读操作有误。确保read使用的缓冲区大小是sizeof(struct can_frame)。性能调优提高吞吐量CAN帧本身负载最多8字节协议开销固定。提高有效数据率的方法是优化应用层协议比如将多个短数据打包进一帧或使用CAN FD如果控制器支持T113-S3的CAN控制器是经典CAN不支持FD。降低CPU占用对于高负载总线应用层频繁调用read可能占CPU。可以使用recv带MSG_DONTWAIT标志进行非阻塞读取结合select/poll。适当增大套接字接收缓冲区setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, bufsize, sizeof(bufsize));。考虑使用内核的CAN广播管理器CAN_BCM套接字来处理周期性的发送和接收减少用户空间-内核空间的切换。一个实用的调试流程确认物理层测终端电阻60Ω、测差分电压。确认驱动层dmesg | grep can看驱动ip -d link show can0看接口状态和错误计数。确认链路层用candump看原始帧。先自发自收环回模式确保软件栈基础正常。确认应用层用can-utils工具发送看对方是否能收到反之亦然。最后再用自己的应用程序替换工具进行测试。在全志T113-S3上玩转CAN-BUS从内核到应用走通全链路最关键的是理解“CAN即网络设备”这个Linux设计哲学。驱动和设备树是地基ip和can-utils是趁手的工具而SocketCAN编程则提供了强大且灵活的上层建筑。遇到问题按照物理层-驱动层-链路层-应用层的顺序逐级排查大部分难题都能迎刃而解。这个项目打通后你的开发板就真正具备了与广阔工业、汽车世界对话的能力无论是做数据采集、设备控制还是协议网关都有了坚实的底层通信基础。