4、CanFestival协议栈移植:源码结构分析、Linux平台适配层、定时器与任务调度实现
好,咱们进入正题。CanFestival这个协议栈,说实话我第一次看源码的时候也有点头大——文件不少,命名风格也不太统一。但摸清楚它的骨架之后,你会发现它其实挺清爽的。今天我就带你把它的源码结构拆开看看,再聊聊怎么把它搬到Linux上跑起来。
4.1 源码结构:别被文件夹吓到
CanFestival的源码目录,核心就这几个部分。我习惯先看src/和include/,其他都是外围。
| 目录/文件 | 作用 | 我的备注 |
|---|---|---|
src/ |
核心协议栈源码(对象字典、SDO、PDO、NMT等) | 这部分基本不用改,跨平台通用 |
include/ |
头文件,定义数据结构、宏、API | 移植时重点关注canfestival.h和timerscfg.h |
drivers/ |
各平台适配层(Linux、Windows、STM32等) | 咱们要改的就是这里,尤其是Linux那个文件夹 |
objdictgen/ |
对象字典编辑器(Python写的图形工具) | 开发阶段用,部署时不需要 |
examples/ |
示例工程 | 参考价值高,但别直接复制 |
你想想看,协议栈的核心逻辑——比如SDO的分段传输、PDO的映射处理——这些跟操作系统没关系。真正需要动手的,是驱动层那薄薄的一层适配代码。
4.2 Linux平台适配层:到底要适配什么?
说白了,CanFestival对Linux的依赖就三个东西:Socket CAN、定时器、线程/任务。我当年第一次移植时,以为要改很多,结果发现核心接口就那么几个函数。
适配层核心接口(位于 drivers/canfestival.h 或类似位置)
canOpen()/canClose()—— 打开/关闭CAN设备canSend()—— 发送一帧CAN消息canReceive()—— 接收一帧CAN消息(通常由回调触发)setTimer()/getElapsedTime()—— 定时器管理StartTimerLoop()/StopTimerLoop()—— 定时器任务启停
嗯,这里要注意:CanFestival的驱动层设计是“轮询+回调”混合模式。Linux下我们用Socket CAN,接收可以用select()或epoll来等,不用死等。
我贴一段简化版的canOpen()实现,帮你找找感觉:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/can.h>
#include <linux/can/raw.h>
int canOpen(s_BOARD *board, char *ifname)
{
int s;
struct sockaddr_can addr;
struct ifreq ifr;
// 创建Socket CAN套接字
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (s < 0) {
perror("socket");
return -1;
}
// 指定接口名称,比如"can0"
strcpy(ifr.ifr_name, ifname);
if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) {
perror("ioctl");
close(s);
return -1;
}
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
// 绑定
if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
close(s);
return -1;
}
// 保存套接字描述符到board结构体
board->fd = s;
return 0;
}
这段代码你看着眼熟吧?就是标准的Socket CAN三板斧。CanFestival的board结构体会保存这个fd,后续收发都靠它。
4.3 定时器实现:别小看这玩意儿
CANopen协议栈对时间敏感。比如心跳报文要定时发,PDO的禁止时间要精确控制。CanFestival内部维护了一个定时器链表,每个定时器节点包含超时时间、回调函数、周期标志。
我见过有人直接用sleep()来实现定时器——千万别这么干。那会阻塞整个协议栈。Linux下正确的做法是用timerfd或者POSIX定时器。
我的经验:用timerfd_create() + timerfd_settime()。这样定时器超时事件可以跟CAN接收事件一起用epoll等,统一事件循环,代码结构特别干净。
CanFestival的定时器回调长这样:
// 定时器回调函数原型
typedef uint8_t (*TimerCallback_t)(CO_Data *d, uint32_t id);
// 定时器结构体
typedef struct {
uint32_t id; // 定时器ID
uint32_t timeout; // 超时时间(微秒)
uint32_t elapsed; // 已过去的时间
TimerCallback_t callback; // 回调函数
uint8_t periodic; // 是否周期定时器
} s_timer_entry;
适配层需要实现两个关键函数:
setTimer()—— 往定时器链表里插入一个新定时器getElapsedTime()—— 返回自上次调用以来经过的时间(微秒级)
我习惯用clock_gettime(CLOCK_MONOTONIC, ...)来获取时间戳,精度够用,而且不受系统时间调整影响。
4.4 任务调度:协议栈的“心脏”
CanFestival本身不是多线程的。它的核心是一个状态机,每次被调用时处理一批待办事项。你想想看,如果不用RTOS,怎么让它“自动”运行?
答案是:开一个单独的线程,循环调用canReceive()和timerLoop()。Linux下用pthread就行。
注意:CanFestival的API不是线程安全的。如果你在多个线程里同时调用它的函数,必须加锁。我踩过这个坑——主线程发SDO,定时器线程同时处理心跳,结果对象字典被写乱了。后来加了一把pthread_mutex_t才搞定。
一个典型的主循环框架:
void *canopen_task(void *arg)
{
CO_Data *d = (CO_Data *)arg;
struct timespec ts;
int ret;
while (running) {
// 1. 处理接收到的CAN帧
canReceive(d);
// 2. 处理定时器事件
timerLoop(d);
// 3. 睡眠到下一个最小超时时间
// 这里可以用epoll_wait + timerfd实现更精确的等待
usleep(1000); // 1ms轮询,实际项目别这么糙
}
return NULL;
}
当然,实际产品里我不会用usleep(1000)。我会用epoll同时等CAN套接字和timerfd,这样CPU占用率极低,响应也及时。
4.5 整体架构:一张图说清楚
下面这张图,是我自己画的一个CanFestival移植后的分层结构。你看一眼就明白各层的关系了。
你看,真正需要动手的只有绿色那层。上面红色的协议栈核心,基本不动。下面紫色的系统接口,Linux内核已经帮你做好了。
4.6 移植步骤总结
我习惯按这个顺序来,不容易漏:
- 搭建Socket CAN环境 —— 确保
can0能起来,用candump和cansend先测通物理层。 - 实现
canOpen()/canClose()—— 绑定CAN设备,保存fd。 - 实现
canSend()—— 封装write()系统调用,注意CAN帧结构体的转换。 - 实现
canReceive()—— 用非阻塞或select()方式读取。 - 实现定时器接口 —— 用
timerfd或clock_gettime+ 链表管理。 - 创建协议栈任务线程 —— 循环调用
canReceive()+timerLoop()。 - 测试基本通信 —— 用
candump看NMT启动报文是否发出。
快速验证方法:移植完成后,让从站发一个心跳报文。如果能在candump can0里看到周期性的0x700 + 节点ID的报文,说明移植基本成功了。我曾经有一次折腾了两天,结果发现是定时器回调里忘记调用timerLoop()——心跳死活不出来。