☰
CAN通信代码升级实战:从能收发到可靠收发的嵌入式设计
2026/10/16 8:16:09 网站建设 项目流程

搞嵌入式或者汽车电子的朋友,对CAN总线应该都不陌生。车载网络里到处是CAN,发动机、车身控制器、电池管理、仪表,全靠一根差分对在传数据。去年我在某个项目上写了一套CAN通信代码,当时主要是验证功能,跑得倒也正常,但一放到真实工程里就暴露出不少问题:配置写死在代码里、接收靠轮询、多帧数据来不及处理会丢、出错之后不会恢复,传输中断只能重启。这次出的升级版,把这些问题逐个收拾掉了,顺手把结构重构成能直接拿到其他项目复用的模块。这篇文章主要讲清楚升级版是怎么设计的、每个模块为什么这么写、移植时又踩了哪些坑,适合正在写或者准备用CAN通信代码的嵌入式工程师,尤其是想从“能收发”往“可靠收发”走一步的朋友。

1. 升级版究竟升级了什么:先给初版“算算账”

1.1 初版代码的几个经典痛点

先说初版代码。当时验证功能,思路很简单:初始化一个CAN控制器,配置好波特率,然后在主循环里轮询接收标志位,有帧就进中断或者标志判断里读出来,打印到串口。代码几十行就能跑通,在桌面上用两个开发板对测,效果挺理想。可事情一旦换到真实项目里,问题就全冒出来了。

第一个痛点是配置硬编码。波特率、滤波ID、工作模式全都写在初始化函数里,换一个MCU型号,整个文件就得重写;甚至同一个芯片换了一个应用场景,也要重新翻代码改参数。这个问题的根源不是配置项本身多,而是配置和驱动逻辑搅在一起,一改配置就容易把驱动逻辑改坏。

第二个痛点是纯轮询收发。主循环在等着一帧数据的时候,如果总线上一段时间没有报文,整个CPU全被空转占掉;中断里有数据要处理的时候,主循环又在忙其他的,两边一撞就丢帧。说白了就是没有缓冲机制,数据来一条收一条,收不过来就放弃。

第三个痛点是错误处理约等于零。初版代码只在出错时点亮一颗LED,具体是什么错误、需不需要恢复、恢复之后怎么重新参与通信,完全没有设计。CAN总线上任何一个节点出错进入bus-off状态,如果不处理,整个节点就永远脱离网络了,这在整车上是很严重的问题。

1.2 升级版定下的四个目标

升级版动手之前,我给自己定了四个目标,后面所有代码都是围绕这四个目标展开的。

一是可配置。所有的波特率、过滤器、工作模式、中断优先级,全部通过配置文件注入,驱动代码里不允许出现魔法数字。这样换个场景只需要改配置头文件,驱动层一行不用动。

二是可移植。把代码分成两层:跟具体芯片寄存器相关的HAL层,和通用逻辑的协议/驱动层。换芯片只改HAL层,上层逻辑原封不动搬走。这一步看似费工,但_project多了以后回报很高。

三是可靠。错误计数器要能读出来,状态迁移要有清晰逻辑,bus-off要能自动恢复,收发要有缓冲区,不能让“来不及处理”成为丢帧的借口。

四是可扩展。数据结构里预留好扩展帧、远程帧、CAN FD帧的位置,后续如果需要诊断协议或升级到CAN FD,不用把整个模块重写。

这四条定下来,整个代码的骨架就比较清楚了。下面直接说架构和实现。

2. 代码怎么分层才经得起折腾:一个清晰的框架比什么都重要

2.1 从“裸机直写”到“分层模块”的思路

初版代码最大的问题就是“一把梭”。所有逻辑都堆在一个文件里,寄存器操作、协议解析、应用处理全部混在一起,看着代码不多,维护起来很痛苦。升级版第一件事就是把职责拆开。

我采用的是四层结构:

  • 应用层:只需要调用发送接口、注册接收回调,完全不关心CAN控制器寄存器长什么样。
  • 协议/驱动层:负责帧封装、解析、校验、过滤规则匹配,这里不出现具体寄存器操作。
  • HAL层:封装所有跟芯片相关的寄存器读写、中断入口、时钟配置,这一层换平台时才动。
  • 配置层:一份配置文件,定义波特率、过滤器、通道数量和回调函数。

打个比方,这就像快递链条。HAL层是各家物流网点,负责把包裹搬上车;协议层是运输路线和分拣规则;应用层是收件人,只关心包裹到没到、内容对不对。各干各的,哪一环坏了只修那一环,不用把整条链拆了。

层与层之间的接口要定义干净。我用一个can_send(ch, msg)和一个接收回调can_on_rx(ch, msg)作为上下层之间的唯一通道。HAL层收到中断,把数据搬进缓冲区;驱动层在缓冲区里取帧;应用层在回调里拿最终结果。这样每一层都能单独测试,也方便将来换成RTOS环境。

2.2 核心数据结构是怎么设计的

升级版里最重要的一个结构体是报文消息,所有上层收发都靠它传递数据。我把它设计成这样:

typedef struct { uint32_t id; /* 标准帧11bit,扩展帧29bit */ uint8_t data[64]; /* 经典CAN最多8字节,CAN FD最多64字节 */ uint8_t dlc; /* 当前帧实际数据长度 */ uint8_t ch; /* 通道号,多路CAN时用 */ uint8_t flags; /* bit0: 扩展帧; bit1: 远程帧; bit2: FD帧 */ uint32_t ts_ms; /* 接收时间戳,单位毫秒 */ } can_msg_t;

这里每个字段都有讲究。id用uint32_t是为了同时容纳标准帧和扩展帧,避免为ID类型单独开两个结构体。data[64]看着有点浪费,但我特意留到64字节,因为后续如果做CAN FD升级,数据结构这块就不用再动了。flags比单纯一个bool好用,它把所有“帧类型”信息合成一个字节,将来加新类型也容易。ts_ms是最容易被忽略的字段,但调试的时候特别有用——能知道某条报文距离上一条过去了多久,定位周期超时问题靠它。

配置文件里的过滤器和运行参数,也用结构体统一管理:

typedef struct { uint32_t id; /* 参考ID */ uint32_t mask; /* 掩码,0表示不关心,1表示必须匹配 */ uint8_t extended; /* 1-扩展帧,0-标准帧 */ } can_filter_t; typedef struct { uint32_t baudrate; /* 期望波特率,如500000 */ uint8_t sample_pos; /* 采样点,百分制,建议75~85 */ uint8_t mode; /* 0-正常,1-回环,2-静默 */ can_filter_t filters[8]; uint8_t filter_num; uint8_t fd_enable; /* 是否启用CAN FD */ } can_config_t;

为什么过滤规则要用“ID + 掩码”而不是给一串ID列表?因为当前市面上的CAN控制器几乎都支持“掩码模式”,内核里是一套位比较逻辑,比逐个ID匹配省资源,而且能通过一个配置覆盖一段地址范围的帧。比如要接收ID 0x100到0x10F的所有帧,列表模式要写16条,掩码模式一条就够:ID设为0x100,掩码设为0x1F0,低4位不关心。

2.3 环形缓冲区:中断和主循环之间的“快递中转站”

这个问题在初版代码里完全没处理,升级版必须补上。真实总线上数据不会等你,主循环可能在刷写Flash或者在处理优先级更高的事,中断里读出来的帧不可能一直保存在寄存器里,必须搬到内存缓冲区。收发两端速度不匹配,就用一个队列来“削峰填谷”。

我用的是一块环形缓冲区,变量就两个:head写指针和tail读指针。

#define RX_FIFO_SIZE 32 typedef struct { can_msg_t buf[RX_FIFO_SIZE]; volatile uint16_t head; volatile uint16_t tail; uint32_t overruns; } can_rx_fifo_t; static void fifo_push(can_rx_fifo_t *f, const can_msg_t *m) { uint16_t next = (uint16_t)((f->head + 1) % RX_FIFO_SIZE); if (next != f->tail) { f->buf[f->head] = *m; f->head = next; } else { f->overruns++; } } static int fifo_pop(can_rx_fifo_t *f, can_msg_t *m) { if (f->head == f->tail) { return 0; } *m = f->buf[f->tail]; f->tail = (uint16_t)((f->tail + 1) % RX_FIFO_SIZE); return 1; }

这套代码看着简单,有几个关键点必须注意。

head和tail加了volatile,因为它们在中断和主循环两侧被访问。单生产者单消费者模式下,中断只写head,主循环只写tail,不需要加锁;但如果你用了我上面这种“先取next再判断满”的写法,要确认编译器的内存序处理。更稳妥的做法是在fifo_push前后关闭CAN接收中断,但那样会增加中断关闭时间,我在实测中采用“只用两个变量+顺序写”的方案,跑了一年多没出问题。如果你的平台有DMA或者双核共享内存,这里需要重新评估。

缓冲区大小RX_FIFO_SIZE选32帧。经典CAN一条报文最多8字节,32帧也就256字节,对RAM压力几乎为零。但如果你跑的是那种一包接一包的刷写场景,建议评估峰值帧率,算出一个保守值再往上加。我后面的调试经历里就因为这个吃了亏。

3. 核心代码实现全过程:该抄作业的环节都在这了

3.1 配置项与波特率计算:用公式替代魔法数字

配置文件的引入,让“换个波特率就要翻代码”这件事彻底成为过去。但配置只是第一步,波特率怎么计算成寄存器值,里面的坑不少。

CAN的位时间由四段组成:同步段、传播段、相位缓冲段1、相位缓冲段2。波特率公式是:

can_bus_clk / (presc * (1 + seg1 + seg2))

其中同步段固定在1个TQ,seg1是传播段加相位缓冲段1,seg2是相位缓冲段2。采样点就是(1 + seg1) / (1 + seg1 + seg2),一般建议75%到85%之间,放在比较靠后的位置,是因为总线存在信号传播延迟和节点时钟偏差,采样点太靠前容易误判显性位的边沿。

我写了一个工具函数,传入外设时钟、目标波特率和采样点百分比,自动算出预分频和段长度:

static int can_brr_calc(uint32_t sys_clk, uint32_t baud, uint8_t sample_point, uint32_t *brr_reg) { uint32_t target = sys_clk / baud; for (uint32_t ps = 1; ps <= 64; ps++) { if (target % ps != 0) { continue; } uint32_t tq = target / ps; if (tq < 8 || tq > 25) { continue; } uint8_t seg1 = (uint8_t)((tq * sample_point / 100) - 1); uint8_t seg2 = (uint8_t)(tq - seg1 - 2); if (seg1 >= 1 && seg2 >= 1 && seg2 <= 8) { *brr_reg = (ps - 1) | ((seg1 - 1) << 8) | ((seg2 - 1) << 12); return 0; } } return -1; }

这里brr_reg的位域定义是我按通用CAN控制器画的示意,具体芯片可能不同,但“预分频、时间段1、时间段2”三段式的思路是通用的。拿到实际数据手册后,把位域的位置对应改一下就能用。

说一个经验:target = sys_clk / baud能整除的情况最理想,如果除不尽,别硬凑。有一条CAN规则是位时间TQ总数差值不能太大,8到25这个范围是我在实际选型时常用的安全区间。波特率偏差大于1.5%左右就很难在一个长报文上保证同步了,所以优先选能整除的预分频值。

采样点这个值也值得认真选。我在一个项目里刚开始用50%的采样点,点对点通信没问题,可一旦总线挂了十几个节点、线缆长度超过3米,错误帧明显变多。改成80%之后,问题消失。原因就是采样点太靠中间,干扰叠加后很容易在采样时刻读到错误电平。建议不要低于70%。

3.2 初始化流程的顺序不能乱

初始化代码很多人随手写:先配置GPIO再开启时钟,或者先配CAN控制器再配过滤器。实际上顺序错了,大概率跑不通。我整理了一个稳定的初始化顺序,写成函数后就不用再动:

  1. 开启CAN外设时钟、GPIO时钟;
  2. 配置CAN_TX、CAN_RX引脚为复用功能;
  3. 先退出初始化模式,再进设置模式,按控制器状态机要求操作;
  4. 把波特率寄存器值写入位时序寄存器;
  5. 设置工作模式(正常/回环/静默);
  6. 配置过滤器组和掩码;
  7. 清空中断标志,使能收发中断;
  8. 请求进入正常模式,确认模式切换成功。

这里第3步和第8步是最容易漏的。很多控制器有“初始化模式”和“正常模式”之间的切换过程,模式切换不是瞬间完成,要读模式状态寄存器确认,如果直接写一个“请求正常模式”的位然后立刻开中断,中断往往没真正触发。

初始化失败的情况真要遇到,我建议把模式状态寄存器、时钟状态寄存器、错误寄存器全部打出来,先确认控制器是不是真的进了想要的状态。别急着改GPIO配置,因为大部分“一上电不能通信”的问题根本不是引脚配错,而是模式没切过去。

3.3 报文发送:阻塞、超时与发送完成标志

发送函数的设计核心是“不能因为发送失败把主循环堵死”。我种做法是:发送函数只负责“把帧写入发送邮箱并请求发送”,发送完成与否交给发送完成中断来置标志。主循环调用发送函数时,等待一个超时时间,超时就返回错误。

int can_send(uint8_t ch, can_msg_t *msg) { can_dev_t *dev = &g_can[ch]; uint32_t tickout = get_ticks() + 10; while (dev->tx_busy) { if (tick_before(get_ticks(), tickout)) { return CAN_ERR_TIMEOUT; } } if (dev->mode == CAN_MODE_LOOPBACK) { /* 回环模式:直接进接收路径,不走总线 */ } fill_tx_mailbox(dev, msg); dev->tx_busy = 1; return CAN_OK; }

这个实现看着简单,背后的思路是“忙标志”管理。tx_busy在发送完成中断里被清零:

void CAN_TX_ISR(can_dev_t *dev) { if (tx_ok_flag(dev)) { dev->tx_busy = 0; clear_tx_ok_flag(dev); } }

为什么要用发送完成中断而不是发送请求后直接说完成?因为硬件写进邮箱只是“排队成功”,真正把报文发送到总线上还需要时间,如果邮箱忙或总线正处于错误恢复阶段,请求发送的动作可能失败。等发送完成中断拉低tx_busy才能准确知道这帧已经送出去了。发送失败时控制器会置一个错误标志,此时及时清标志并返回,应用层才能决定是否重发。

阻塞等待的10ms是拍脑袋定的吗?不是。500kbps下最长的经典CAN报文(扩展帧+8字节数据)总长约145ns到160ns,换算下来1ms能传几千帧,发送一帧从请求到送出的时间在微秒到几百微秒量级。10ms的超时足够宽容,又能瞒住“控制器已经异常”这种严重情况。如果你的波特率特别低,比如125kbps,单帧时间会更长,超时值要相应放大。

3.4 中断接收、过滤器与回调处理

接收这半边是升级版的重头戏。我的做法是:CAN接收中断里用最快的速度把邮箱里的帧搬进环形缓冲区,清标志位,然后把“有数据”这件事用一个全局标志暴露给主循环;主循环或应用层再从缓冲区里取帧处理。

中断服务函数里只有三个动作,任何多余判断都往外拿:

void CAN_RX_ISR(can_dev_t *dev) { while (rx_mailbox_pending(dev)) { can_msg_t m; read_rx_mailbox(dev, &m); fifo_push(&dev->rx_fifo, &m); } clear_rx_flag(dev); }

这里用while循环把邮箱里所有待读帧一次搬完,清一次标志就行。如果你在中断里只读一帧就退出,而硬件FIFO里还有好几帧,下次中断什么时候来就不确定了——有的控制器会立刻重新触发,有的不会,表现就是“丢帧其实是没读干净”。这是我踩过的一个坑。

过滤器这块单独说一下。掩码模式下的“1”到底是“必须检查”还是“忽略”,不同芯片定义不一样,有的甚至反过来。我看到很多初学者在这里栽过,配置完过滤器之后一条帧都收不到,或者什么帧都能收到。所以我的建议是:先回环模式跑一遍,确认过滤逻辑跟预期一致,再接真实总线。

void CAN_FILTER_CFG(can_dev_t *dev, can_filter_t *f, int num) { /* 对每个f,按芯片寄存器要求写入ID和掩码 */ for (int i = 0; i < num; i++) { set_filter_id(dev, i, f[i].id); set_filter_mask(dev, i, f[i].mask); set_filter_ext(dev, i, f[i].extended); } }

应用层收数据的回调我故意做得很薄,回调里只允许做两件事:拷贝消息、置一个“应用层待处理”的信号量或标志。绝对不要在回调里做协议解析、打印或者Flash写入。原因很简单,回调如果执行时间过长,接收缓冲区很快就满了,新帧只能被丢弃。真实项目中我有一次把一条UDS响应解析放在接收回调里,结果刷写速率一下子掉了30%,去掉之后立刻恢复。

3.5 错误状态迁移与自动恢复

错误处理是升级版里最值钱的部分。CAN控制器内部有发送错误计数和接收错误计数两个寄存器,根据计数大小,节点会处于三种状态:

状态错误计数范围行为表现
错误主动0 ~ 127正常参与通信,出错时可以主动发送错误帧
错误被动128 ~ 255只监听总线,不能主动发错误帧,重传前需要等待6个隐藏位
总线关闭256以上完全脱离总线,既不收发也不应答

真实项目里最容易出现的是连上总线后谁都不说话,查了半天发现某个节点的错误计数已经超过256,处于bus-off状态。这种情况下的恢复策略是:

void can_err_state_update(can_dev_t *dev) { uint8_t tec = get_tec(dev); uint8_t rec = get_rec(dev); if (dev->err_state == ERR_BUSOFF) { if (tec == 0) { /* 控制器内部已经恢复,重新参与通信 */ dev->err_state = ERR_ACTIVE; request_normal_mode(dev); } return; } if (tec >= 128 || rec >= 128) { dev->err_state = ERR_PASSIVE; } else { dev->err_state = ERR_ACTIVE; } }

总线关闭的恢复,不同控制器有不同策略。有些硬件会自动重发等待,有些需要软件把控制器复位重来一遍。我的建议是:能软件恢复就不要硬件断电。在整车环境里,一个节点反复重启会拖累整个网络的稳定性,而且重启过程中如果总线上正好在跑多帧传输,也容易干扰其他节点。

错误中断里除了读计数,还要记一条日志。哪怕是往一个全局数组里塞一个错误码,也比“亮个灯然后什么都不知道”强。我调试时最常用的做法是把错误码和时间戳打印出来,看是发送错误、接收错误还是应答错误——总线负载高时,这三种错误的处理思路完全不一样。

4. 移植和调试中遇到的典型问题:这些问题基本人人都遇到过

4.1 波特率差一点点,总线就静默

我遇到过一个很典型的问题:两个板子都配置成500kbps,软件里波特率寄存器看起来也对,但连上总线就是通信失败,用示波器看波形却看不出问题。后来用CAN协议分析仪去读实际波特率,发现一个节点实际是497kbps,另一个是503kbps,差了1.2%。单帧短的时候没事,一碰上连续多帧传输偶尔就撞出错误帧,表现成“时好时坏”。

原因是两个节点用了不同的时钟源:一个外接20MHz晶振,精度高;另一个用的是MCU内部RC振荡器,标称20MHz实际偏差在±1%以上。这种问题在实验室用开发板测试时不容易出现,因为开发板上都有晶振,一放到量产的控制器上就暴露了。排查思路是:先用工具函数把波特率误差算出来,再确认两个节点的时钟源精度;如果对精度没把握,优先选低波特率,比如把500k降到250k,容忍度会高很多。

4.2 过滤器把该收的帧全挡了

过滤器的问题是“配了等于没配”和“不配就收垃圾”两个方向都存在。

有过一次经历,我把过滤器配置成“全部通过”,结果总线上所有节点的报文都进了缓冲区,主循环被塞到死循环,其它任务全卡住。问题根源不是缓冲区太小,而是“所有帧都接收”这个配置让中断频率变得极高,CPU大部分时间花在搬数据上。

反过来,把掩码配成“全部必须精确匹配”之后,一条帧都收不到。排查时看了半天,最后发现是掩码寄存器里面位定义里,1表示忽略而不是必须匹配,我之前理解反了。所以这个坑的教训是:写过滤寄存器前,先把数据手册里“mask”位的含义抄到代码注释里,避免下次再踩。

过滤器还有一个容易出错的地方是扩展帧和标准帧的ID存放位置不一样。29位扩展ID在寄存器里被拆成两段,低16位和高13位,你如果按标准帧的11位格式往寄存器里塞,扩展帧永远收不到。

4.3 字节序、位序、DLC的坑

CAN协议本身并没有规定数据场的字节序,用大端还是小端完全看应用层怎么定义。做整车诊断的时候经常遇到两种格式混用:有的信号按Motorola格式高字节在前,有的按Intel格式低字节在前。初版代码里我直接拿memcpy复制数据,结果解析出来的值完全不对。

正确的做法是写专门的打包/解包函数,所有跨字节的信号都明确指定字节序和位序,不要依赖编译器对齐和平台端序。要警惕DLC。DLC是数据长度代码,在某些变种协议里它代表的是“数据长度相对8的倍数”,而不是真正的字节数。CAN FD出现后,DLC编码方式变了,8字节以下表示通常长度,8以上按离散值对应到12、16、20、24等。所以我在数据结构里专门存真实的dlc,收发转换时在驱动层做一次编码映射,上层永远只跟“真实字节数”打交道。

4.4 高负载下丢帧与接收溢出

在某个刷写场景里,上位机以接近零间隔的方式发UDS写入请求,一包接一包不停地发,升级版代码出现了偶发丢帧。排查时先看接收缓冲区overruns计数器,发现确实有溢出,于是把RX_FIFO_SIZE从16改成32,情况好了很多,但压力再大时还是偶发。

进一步的定位是:主循环里在处理其它任务,处理时间超过了相邻两帧的间隔,缓冲区的读取速度跟不上写入速度。最终方案不是无限加大缓冲区,而是做了三件事:把CAN接收中断的优先级提到最高一级,确保中断响应不会被别的任务阻塞;中断里只拷贝不做处理;主循环里的耗时操作全部改到空闲时间片执行。这样改动之后,即使缓冲区只有16帧也很稳。

这里想强调一点:缓冲区空间是预算,不是容量。如果设计时让缓冲区经常处于接近满的状态,说明系统整体处理能力不足,该优化的是处理路径而不是缓冲区。

4.5 “能发不能收”的排查流程

“能发不能收”是我收到问得最多的问题。这类问题的排查有一个固定套路,按顺序走一遍,大部分都能快速定位。

第一,先确认是不是回环模式没关。如果控制器的回环模式开启,发出的帧不会出现在总线上,自己又能收到,从本机看“收发都正常”,另一台设备当然什么都收不到。第二,检查接收中断是否使能,这个看似简单,但很多芯片的中断使能位是多级开关,有一级漏配就全断。第三,检查过滤器。第四,确认总线PHY芯片的STB、RS引脚电平,有一款常见的收发器芯片,引脚不接电阻默认进入静默模式,只收不发。这类问题最容易蒙住人:因为“能收到”说明总线物理层没断,但“不能发”又指向驱动层,实际上硬件配置就有问题。

排查顺序最好整理成一张自检清单,我在代码里都加了相应的调试宏,每步都往串口打一个状态标记,把定位时间从几小时压缩到几分钟。

5. 性能、可靠性与后续扩展:跑通只是第一步

5.1 CPU占用率怎么量化:别拍脑袋

很多人说“我的CAN代码占CPU很少”,实际上是凭感觉。量化方法很简单:在接收中断入口拉高一个GPIO,出口拉低,用示波器看这个GPIO的占空比,就是接收中断消耗的时间比例。同理发送完成中断、错误中断都可以各分配一个GPIO来测。

我实测过一组数据:500kbps总线下,帧间隔大约几十微秒,一帧接收中断从进入ISR到退出大概是2微秒到4微秒(取决于读取邮箱和拷贝次数),占空比通常不到10%。但如果中断里做了协议解析或者调用慢速外设,这个比例能飙到50%以上。做实时系统时,这个数字必须心中有数。

5.2 重传策略别做成“重传风暴”

发送失败后要不要重传,我的结论是:只重传重要的、应用层需要保证送达的帧,而且重传要有退避机制。

初版代码里我做过一个错误的示范:发送失败后无限重传,总线负载一高,几个节点都在疯狂重发,错误帧越来越多,形成恶性循环。升级版的策略是:发送失败时先进入一个“退避状态”,间隔指数增长重试,最多重试3次;同时把失败次数上报到应用层,由应用层决定是降级处理还是放弃。对于周期性报文,简单跳过一帧往往比重传更合理,因为下一帧很快就来了;对于偶发性事件帧,才需要可靠性保证。

5.3 回环模式让调试效率翻倍

升级版的配置里保留了三种模式:正常、回环、静默。回环模式不经过总线,在内部把发送数据直接送回接收路径,最适合单板自测。写新代码或者改过滤器配置之后,先切到回环模式验证一遍驱动逻辑,再把模式改成正常接总线,能省掉大量双板联调的痛苦。

回环模式还有一个好处:可以提前验证中断优先级和缓冲区是否够用。因为回环模式下收发频率完全由你自己控制,可以压极限速率,把缓冲区溢出问题在接真实总线之前暴露出来。

5.4 给CAN FD留好接口

经典CAN在8字节数据场和1Mbps速率之下已经越来越紧张,CAN FD把数据场扩到最多64字节,还能在数据段用更高的速率传输。升级版在设计时已经把消息结构的data[64]和flags的FD位预留好,底层HAL层只需要增加一组寄存器操作,驱动层的帧封装逻辑要改动的地方不多。

但有一点要特别提醒:CAN FD不是所有控制器都支持,换硬件之前要先确认。而且CAN FD的DLC编码规则和位时序跟前文介绍的不一样,数据段的采样点也要重新算。如果你现在用的还是经典CAN控制器,数据结构先按上面的方式预留,将来迁移时会轻松很多。

最后再分享一点个人体会。CAN通信代码真正做到“可靠”,不是把寄存器的每一个位配对就行,而是在“配置、缓冲、异常处理、分层结构”这些看不见的地方下功夫。我先做回环自测,再用GPIO把中断的CPU占用量出来,最后才敢上真实总线。这套代码现在被我抽成固定模板,换平台只需要改HAL层和配置头文件。如果读者你现在正从零写一套CAN驱动,我建议不要只盯着“能收到数据”这个目标,多花一点时间想想“收到之后怎么办、出错之后怎么办”,这个投入绝对值得。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询