RT-Thread论坛
直播中

王桂英

8年用户 1410经验值
私信 关注
[问答]

CAN发送只能使用中断或者DMA,为什么?

今天看了CAN发送的底层配置发现,rtt的CAN设备发送好像只能选择DMA或者中断的方式进行发送,这种设定是为什么呢?

  • rt_device_open(can1,RT_DEVICE_FLAG_INT_RX|RT_DEVICE_FLAG_INT_TX );


我明明可以选择一个定时器,时间到了之后直接手动发送就完成了,配置成中断发送模式涉及线程切换势必要浪费不必要的时间,使用DMA又会白白浪费一个DAM通道,这种设计是处于什么目的呢?
补充:
一下是CAN发送的底层,位置在工程->rt_thread->drivers->can->can.c文件的低460行。
rt_can_write()函数,在这个函数中,它会先进行一个判断:
if ((dev->open_flag & RT_DEVICE_FLAG_INT_TX) && (dev->ref_count > 0))
所以看起来必须配置RT_DEVICE_FLAG_INT_TX能执行一个CAN正常的发送。


回帖(1)

万航渡路

2025-10-11 16:03:23

好的,我们来分析一下为什么在RT-Thread(以及绝大多数类似的嵌入式RTOS/HAL设计中)的CAN设备驱动默认强烈推荐使用中断或DMA方式进行发送,而不是你设想的定时器轮询方式。这背后的原因涉及到实时性、效率、资源利用和硬件特性等多方面。


你的观察(rt_can_write()函数检查发送模式标志)是正确的设计选择,原因如下:




  1. 实时性与响应速度(核心原因)



    • CAN的实时要求:CAN总线广泛应用于汽车、工业控制等强实时性场景。数据帧(尤其是较高优先级ID的帧)需要尽快地被发送出去,以抢占总线、满足响应时间要求。

    • 中断的及时性:当发送邮箱(Tx Mailbox)或发送缓冲区(Tx Buffer)空闲时,CAN硬件控制器会立即产生一个发送完成中断(Tx Complete interrupt)空闲缓冲区中断(Tx Buffer Free interrupt)。这允许驱动立即将队列中的下一个帧(如果有)加载到硬件进行发送。RT_DEVICE_FLAG_INT_TX正是为了利用这个中断。

    • 定时器轮询的延迟:使用定时器意味着你需要在固定的时间间隔去“询问”硬件:“你现在有空闲邮箱给我发下一帧吗?”无论定时器周期设置得多短(比如1ms),在最坏情况下(比如你刚检查完,邮箱恰好空了),你的帧都要等到下一个定时器中断到来时才能被加载发送。这种“固定间隔”的延迟是不可接受的,尤其是在需要低延迟响应或传输批量数据时。中断保证了“事件驱动”(邮箱空了就立刻处理),而定时器轮询是“时间驱动”(到点了才去查看)。两者在实时性上差距巨大。




  2. CPU利用率和功耗



    • 定时器轮询的频繁检查浪费CPU:为了减小第1点提到的延迟,你不得不把定时器周期设置得非常短(比如100us甚至更短)。这会导致:

      • CPU需要非常频繁地进入定时器中断,检查CAN状态(通常是访问寄存器)。即使邮箱大多数时候是满的(不需要操作),这个检查也得做。

      • 这意味着大量无谓的CPU中断上下文切换和状态查询开销,严重占用CPU时间(尤其是当帧发送频率不高时,浪费特别明显)。

      • 在低功耗应用中,频繁唤醒CPU检查会使设备无法进入深睡眠状态。


    • 中断/DMA的效率:使用发送完成中断时,CPU只在真正需要加载下一帧时(邮箱确确实实空了) 才被触发执行一次操作。这是一个“有需要才工作”的模式,大大降低了无谓的开销。使用DMA则更进一步(见第4点)。




  3. 保障发送完成确认



    • CAN发送不仅仅是把数据塞进硬件邮箱就万事大吉了。发送是否成功(成功仲裁出去、被ACK)、是否触发错误、是否因为总线错误需要重发等,都是重要信息。

    • 使用中断时,无论是发送完成中断(成功发送)还是错误中断,都能让驱动及时获知消息发送的最终状态并更新软件状态(如释放缓冲区对象、通知等待的线程等)。

    • 定时器轮询方式虽然也可以在查询邮箱状态时检查发送状态(Tx OK, Tx Error),但:

      • 它的检查是延迟的(不是立刻发生的)。

      • 与发送完成事件本身是异步的,状态解析可能更复杂。





  4. DMA的优势(传输大型信号块时)



    • 你提到浪费DMA通道。这在通道极其紧张的小型MCU上是合理的顾虑。然而,DMA的初衷是解放CPU:

      • CPU释放:在发送多帧消息(如某些诊断协议)或带大量数据的帧(如8字节Data Field)时,特别是配合DMA传输的中间件服务,模拟一个FIFO的行为,CAN传输期间,DMA可以直接把内存中的数据自动搬运到CAN外设的发送数据寄存器(Tx Data Register)。CPU在这个过程中完全被解放出来去做其他任务(除非等CB传输完成中断)。

      • 潜在的性能提升:在密集发送的场景下,DMA避免了CPU重复地用多条指令(LD/STR)搬运每个字节到外设寄存器的开销,吞吐量更高,CPU占用率更低。

      • 通道的付出是值得的:对于高优先级、需要高带宽或批次传输数据的CAN节点,使用一个宝贵的DMA通道换取CPU利用率大幅降低是非常划算的设计选择RT_DEVICE_FLAG_DMA_TX正是用于启用这种高效模式。框架需要为这两种异步方式设计。





  5. 符合硬件设计范例和广泛实践



    • 几乎所有的MCU厂商提供的CAN驱动库(HAL/Low Level Driver)都是围绕发送完成中断(或结合错误中断、接收中断)来构建基础结构(硬件特性决定的默认流程)。在此基础上再可选地加入DMA支持。

    • 所有成熟的嵌入式OS(如FreeRTOS+Drivers, Zephyr RTOS, embOS, μC/OS-III等)和设备驱动模型,对于异步报文传输(串口、SPI、I2C、CAN、USB等)都是默认基于中断回调模式实现的。轮询方式是作为最低配置(初始化测试/故障安全后备),用于极简单、无实时要求的应用。

    • rt_device_open中使用RT_DEVICE_FLAG_INT_RXRT_DEVICE_FLAG_INT_TX是遵循这个标准模式。




针对你的补充点(rt_can_write()函数)的分析:


驱动代码中那个检查逻辑正是这种设计理念的体现:



  1. 缓冲rt_can_write()通常并不“立刻开始发送”,它把要发送的帧对象(struct can_msg)放入一个准备好的软件队列(发送邮箱FIFO)中。

  2. 检查状态:然后它调用_can_xmit(), 这个函数会检查:

    • 当前硬件发送状态state(如RT_CAN_STATE_READY)是否空闲。CAN内部有硬件队列或多个发送邮箱。

    • 发送邮箱是否可用:相当于检查hdr(hardware device register)中有没有被占用的邮箱。


  3. 尝试直接发送(轮询方式):

    • 如果条件允许(硬件有空闲发送邮箱,且驱动可能允许无锁直接发送),就会立即尝试把队列头的帧取出,并通过_transmit()函数(直接写控制器寄存器的方式)发起一次写操作。在低速CAN或者发送间隙较长时,它可能就这样完成了。这可以看作是在发送路径上利用硬件邮箱队列深度进行一次优化的轮询尝试(体现了硬件FIFO设计的价值)。


  4. 触发中断或异步发送机制(设计精髓):如果在第2/3步因为发送邮箱忙而无法立即发送帧,函数直接返回。但关键点在于:驱动已经在构造函数中设置好了CAN发送器的发送完成中断(Tx Complete Interrupt)

    • 硬件发送完成引发中断:当一个CAN帧从发送邮箱成功发送出去后(或发送失败),就会立刻触发中断。

    • 中断服务程序 (ISR) 处理:在这个中断服务程序(rt_hw_can_isr或类似函数) 中:

      • 清理发送完成的标志。

      • 释放该硬件邮箱供后续使用。

      • 最关键:再次调用_can_xmit()或类似函数,检查软件队列是否还有待发送帧。如果有,并且当前硬件正好有邮箱可用,就立即把新的帧加载进去开始发送!


    • 这就是所谓的“利用中断驱动发送链路启动”:中断事件(发送完成)确保了只要有待发数据和空闲邮箱,发送就能近乎立即地继续进行下去。其核心仍然是利用了中断驱动的实时性、按需处理特性。



结论与建议:



  1. RT-Thread(及类似驱动框架)的选择是正确的:优先利用中断或DMA发送是嵌入式实时系统中高效、低延迟处理CAN通信的标准且最优实践。它确保了:

    • 最佳实时性(帧尽快发送)。

    • 最高CPU利用率(只在真正需要时行动)。

    • 最省电(减少不必要的轮询唤醒)。

    • 有效利用硬件特性(响应事件中断,支持DMA卸载)。


  2. 线程切换开销问题:

    • 这个开销是存在的,但:

      • CAN中断服务程序(ISR)非常短小精悍,通常只做标志清除、邮箱加载、任务调度通知等关键操作。

      • 付出的这一点点中断切换和调度开销(通常us级别),带来的实时性提升是巨大的、值得的。相对于定时器轮询引入的周期性大延迟高CPU消耗(如果强行设定高频定时器去降低延迟),这点开销微乎其微且是必要的交换


    • RTT精心设计了驱动流程(如_can_xmit写法),充分利用硬件特性,尽可能让发送同步完成(只要刚好有邮箱空闲),只有需要异步等待时才真正切走运行队列。


  3. DMA浪费问题:

    • 对于高带宽或低CPU裕量场景,使用DMA发送非常值得换取一个通道。它显著提升系统并行能力。对于极低带宽节点,完全可以只选用RT_DEVICE_FLAG_INT_TX而不使用DMA。


  4. 为什么通常不提供纯轮询(如定时器)发送模式:

    • 性能劣势明显:延迟高、CPU消耗大(高频时)、不适合实时应用。

    • 没有必要:硬件本身的设计就是为了搭配中断工作最有效(每个邮箱空就有中断)。软件强行用轮询是逆流而上。

    • 增加了驱动复杂性:需要额外维护一个定时器数据结构,并提供一个相比异步方式劣化太多的API序列。

    • 不符合产品基本能力要求:如果你会因为定时器+查询导致的延迟过大出问题,那说明你的应用场景本质上是需要利用中断驱动或DMA的异步即时交互来满足的,这点开发外设要理解硬件特性,嵌入式的核心是针对硬件设计模型和产品运行场景选择最省资源的调度方案而已。


  5. 如果你坚持要定时器轮询方式(非常不推荐,但技术上可能):

    • 你需要为您的CAN设备单独实现一个带有用户可配置定时器轮询间隔的新版本can_open模式,如RT_DEVICE_FLAG_TRA_POLLING

    • 在驱动中找到处理邮箱状态数据结构的函数(通常是rt_hw_can_isr中被调用的ISR后处理部分),将其包装为一个可以被定时器函数调用的核心轮询函数(_can_poll_tx函数或者定义一个新函数如can_device->ops->poll_tx)。

    • 在驱动的业务逻辑中集成定时器,在定时器中断中调用这个轮询函数然后调用对应设备的poll_tx回调函数。

    • rt_can_write()调用变体版本的_can_xmit()函数需要适应新的轮询模式与避免中断上下文相关问题。

    • 这将打破标准的驱动模型,增加维护成本,并且性能远低于中断模式。仅在测试特定场景或极其特殊且资源受限的情况下才可考虑,而不是作为一种优化的方案。



总而言之,RT-Thread CAN驱动优先使用中断或DMA发送是经过深思熟虑的设计,是为了在实时性、效率和资源利用三者之间取得最佳平衡。你所设想的定时器轮询方式在理论上有实现的可能,但在实践中面对实时系统的要求,其固有的延迟和CPU消耗缺点使它成为不可取甚至不合格的替代方案,尤其是在CAN这种对时效性要求极高的设备上,不建议尝试推翻这套默认的设计。使用中断才是正道、官方最佳实践。

举报

更多回帖

发帖
×
20
完善资料,
赚取积分