
先说结论省得你继续翻手册在STM32H753上FDCAN1/FDCAN2 的 RX/TX 消息帧本身不支持 DMA 搬运HAL 库里也没有HAL_FDCAN_Receive_DMA或者HAL_FDCAN_Transmit_DMA这样的接口。我最初也不信因为在 H7 上 SPI、UART、ADC 全都可以 DMA为什么唯独 FDCAN 不行这个疑问拖了几天之后我翻了参考手册、翻了 CubeMX 生成的代码、又在逻辑分析仪上做了对比实验最后确认不是配置问题是硬件设计就没给 FDCAN 接 DMA 请求线。这篇文章把来龙去脉、验证过程以及没有 DMA 时的工程方案都写清楚给后来者避坑。1. 这个老问题到底在问什么FDCAN 和 bxCAN 带来的 DMA 错觉先别急着看代码。这个问题我在多个技术社区里见过提问的人通常是从 STM32F1/F4 升级到 H7 的老工程师或者刚用 HAL 库做 CAN 的新手。大家看到 H753 这颗芯片性能那么强UART 能做 DMA 环形缓冲SPI 能 DMA 收发ADC 能 DMA 循环采样心里自然会冒出一个念头FDCAN 这种速度更快的总线不可能没有 DMA 吧但这个直觉反而容易把人带偏。F1/F4 时代用的是 bxCAN它的报文缓冲区是几个固定的邮箱。虽然邮箱也有填好了就让硬件发送的机制但也没有真正的 DMA 概念。到了 H7bxCAN 换成了 Bosch M_CAN 内核也就是 FDCAN。名字变了接收路径上多出了 RxFIFO 和硬件过滤器发送路径上多了 TxFIFO/Tx队列给人的感觉是这个东西很高级应该有 DMA。可高级归高级DMA 不是靠感觉来的得看芯片内部有没有把 DMA 请求信号引到 DMA 控制器上。还有一个容易混淆的点有人以为H7 有 DMAMUX所有外设都能映射到 DMA 通道。H7 的 DMAMUX 确实很灵活能把 UART、SPI、I2C、ADC 这些外设的请求映射到任意 DMA 通道但前提是外设自己必须产生 DMA 请求信号。FDCAN 压根没有把这个信号引出来DMAMUX 也无能为力。这就好比你家门口的路修得很宽但 FDCAN 这辆车根本没接上这条路你再怎么改交通规划也没用。真正想问的问题藏在DMA 支持吗背后我能不能让 FDCAN 收发数据时不占用 CPU如果 CPU 被 CAN 报文频繁打断会不会影响控制周期这个需求是合理的但解决方案不一定只有 DMA。我把硬件机制和工程替代方案都写清楚之后你会发现很多时候 FDCAN 不需要 DMA 也能把 CPU 占用压得很低。2. 硬件层面找证据Message RAM 访问机制与 DMA 请求映射表2.1 FDCAN 的 Message RAM 不是普通内存要理解 FDCAN 为什么没有 DMA最直接的方法是去看 FDCAN 内部的 Message RAM 访问方式。H753 的 FDCAN 内核自带一块 Message RAM这块 RAM 不是被映射到系统地址空间里的你不能像操作普通 SRAM 那样用一个地址指针直接读写它。CPU 访问 Message RAM 的路径是这样的先通过 FDCAN 寄存器发起操作FDCAN 内核再把数据写到 Message RAM或者从 Message RAM 读出来。也就是说FDCAN 的帧数据对 CPU 来说更像是外设寄存器堆不是内存。DMA 控制器本质上是一台 AHB 总线上的搬运引擎它需要一个源地址和一个目的地址。如果源地址或目的地址根本不在系统地址映射表里DMA 就没法发起传输。你可以类比成 NAND Flash 的 Page 读取你不能把 NAND 的存储阵列直接映射到地址空间只能通过控制器寄存器去操作所以 NAND 也没有真正意义上的直接内存映射 DMA。FDCAN 的 Message RAM 在这里扮演的是类似角色。2.2 DMA 请求映射表里根本没有 FDCAN如果不信寄存器层面的话那直接看芯片参考手册里的 DMA 请求映射表。H743/H753 这类芯片的参考手册RM0433里有一个独立的 DMA 请求表列出了所有能触发 DMA 传输的外设请求源。DMA1/DMA2 的请求包括 ADC、TIM、SPI、UART、I2C、SDMMC、HASH、CRYP 这些BDMA 的请求列表里也没有 FDCAN。我翻了几遍FDCAN1 和 FDCAN2 从未出现在任何一张 DMA 请求映射表里。这个证据比任何博客文章都硬。芯片设计阶段就没有把 FDCAN 的事件连接给 DMA 控制器。就算你在 CubeMX 里把工程翻个底朝天也不可能有 FDCAN 的 DMA 选项。所以不要再花时间找隐藏开关了它不存在。2.3 FDCAN 自己有哪些提升效率的硬件机制没有 DMA 不代表 FDCAN 效率低。实际上 FDCAN 在硬件层面已经把很多 CPU 工作接管了典型的几个机制RxFIFO0/RxFIFO1 硬件缓冲FDCAN 可以同时配置多个接收 FIFO报文进来之后先由硬件存到 FIFOCPU 有空了再去读。不像某些低端 CAN 控制器只有一个邮箱来一帧就得立刻处理一帧。硬件过滤器标准帧和扩展帧都有独立滤波器可以在硬件层把不关心的 ID 直接丢弃。这些帧根本不会惊动 CPU。TxFIFO/Tx队列发送侧可以一次把多帧报文提交给硬件硬件排队发送CPU 不用死等总线仲裁结果。Tx Event FIFO硬件可以记录每帧发送完成的时间戳和消息标记不需要 CPU 在中断里自己打时间戳。这些机制组合起来已经覆盖了绝大多数工业应用的需求。我做了实测之后发现在 H753 这种 480MHz 的 M7 上FDCAN 中断收发的 CPU 占用率低到可以忽略。这部分数据放在第四章。3. HAL 库的 FDCAN 收发路径代码里为什么没有 Receive_DMA3.1 先看 API 全貌如果你用 STM32CubeMX 生成过 H753 的 FDCAN 工程就会发现 FDCAN 相关的 HAL 函数列表里没有一个带 DMA 后缀的。UART 有HAL_UART_Receive_DMA、HAL_UART_Transmit_DMASPI 有HAL_SPI_Transmit_DMA、HAL_SPI_Receive_DMA但 FDCAN 只有这些核心 APIHAL_FDCAN_AddMessageToTxFifoQHAL_FDCAN_AddMessageToTxBufferHAL_FDCAN_GetTxEventHAL_FDCAN_GetRxMessageHAL_FDCAN_ActivateNotification以及对应的回调函数这个 API 结构不是 ST 偷懒它反映了硬件能力。HAL 库不会为了看起来完整而去给一个没有硬件 DMA 信号的外设硬凑 DMA 接口。3.2 一帧报文从总线到用户缓冲区的完整路径我们可以把 FDCAN 的收发路径拆开看这样能更清楚地理解数据经过了哪些环节。接收方向总线上的电平变化被 CAN 收发器转成 RX 信号FDCAN 内核完成同步、解码、CRC 校验、过滤然后把帧写入内部 Message RAM 的 RxFIFO。此时 CPU 可以通过两种方式知道有报文轮询标志位或者开中断。CPU 收到通知后调用HAL_FDCAN_GetRxMessageHAL 库内部先从 FDCAN 寄存器读出帧头信息再把数据域从 Message RAM 复制到用户提供的 buffer。整个过程里FDCAN 内核到 Message RAM 的写入是硬件完成的这已经算一种硬件搬运但 Message RAM 到用户内存的这一步必须由 CPU 逐字节复制。发送方向发送时把FDCAN_TxHeaderTypeDef和 data buffer 传给 HAL 库HAL 库内部把帧头和数据写入 FDCAN 寄存器FDCAN 内核再写入 Message RAM然后硬件开始仲裁和发送。CPU 参与到用户内存 - Message RAM这一段的复制。问题就在这里DMA 能加速的恰恰是用户内存 - 外设数据寄存器这种复制。但 FDCAN 的 Message RAM 并不是用户可以直接访问的内存映射区所以 DMA 没法绕过 HAL 库直接去读写它。3.3 用 DMA 搬运“中间缓冲”不是不行但别理解错有读者可能会问我先把要发送的多帧报文用 DMA 从内存 A 搬到内存 B然后在中断里把 B 的内容调用 HAL 发送出去这样算不算 DMA 加速 FDCAN这只能叫DMA 辅助的数据搬移不是 FDCAN 发送 DMA。因为真正进入 FDCAN Message RAM 的那一次复制仍然是由 CPU 在HAL_FDCAN_AddMessageToTxFifoQ内部完成的。用 DMA 做内存到内存的搬运当然可以但在 H753 这种 M7 上内存到内存 DMA 往往比 CPU 直接 memcpy 更慢还牵扯到 D-Cache 一致性维护的问题。除了增加复杂度几乎没有收益。所以我从不建议在这个方向上折腾。4. 实测验证H753 中断收发与 CPU 占用附 120Ω 终端电阻教训4.1 环境与 CubeMX 配置我手头是一块 H753 核心板外接了 CAN FD 收发器和 USB-CAN 工具。时钟方面FDCAN 内核时钟配置为 80MHz。CubeMX 里 FDCAN1 设置为FDCAN_FRAME_FD_BRS仲裁段 1Mbps数据段 5Mbps用中断接收。关键参数如下代码经过精简hfdcan1.Instance FDCAN1; hfdcan1.Init.ClockDivider FDCAN_CLOCK_DIV1; hfdcan1.Init.FrameFormat FDCAN_FRAME_FD_BRS; hfdcan1.Init.Mode FDCAN_MODE_NORMAL; hfdcan1.Init.AutoRetransmission ENABLE; hfdcan1.Init.TransmitPause DISABLE; hfdcan1.Init.ProtocolException DISABLE; /* 80MHz时钟下Nominal段分频5Tq总数16 1Mbps */ hfdcan1.Init.NominalPrescaler 5; hfdcan1.Init.NominalSyncJumpWidth 4; hfdcan1.Init.NominalTimeSeg1 13; hfdcan1.Init.NominalTimeSeg2 2; /* Data段分频1Tq总数16 5Mbps */ hfdcan1.Init.DataPrescaler 1; hfdcan1.Init.DataSyncJumpWidth 4; hfdcan1.Init.DataTimeSeg1 13; hfdcan1.Init.DataTimeSeg2 2; hfdcan1.Init.StdFiltersNbr 1; hfdcan1.Init.ExtFiltersNbr 0; hfdcan1.Init.TxEventsNbr 0; hfdcan1.Init.MsgRamOffset 0; hfdcan1.Init.MaxTxBuffers 0; hfdcan1.Init.MaxRxBuffers 0; hfdcan1.Init.TxBuffersNbr 0; hfdcan1.Init.TxFifoQueueNbr 8; hfdcan1.Init.TxFifoQueueMode FDCAN_TX_FIFO_OPERATION; HAL_FDCAN_Init(hfdcan1);滤波器配成接收所有标准帧 ID进入 RxFIFO0FDCAN_FilterTypeDef filt; filt.IdType FDCAN_STANDARD_ID; filt.FilterIndex 0; filt.FilterType FDCAN_FILTER_MASK; filt.FilterConfig FDCAN_FILTER_TO_RXFIFO0; filt.FilterID1 0x000; filt.FilterID2 0x7FF; /* mask 模式全 ID 通过 */ HAL_FDCAN_ConfigFilter(hfdcan1, filt); HAL_FDCAN_Start(hfdcan1); HAL_FDCAN_ActivateNotification(hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0);接收回调如下void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs) { FDCAN_RxHeaderTypeDef rxHeader; uint8_t rxData[64]; if ((RxFifo0ITs FDCAN_IT_RX_FIFO0_NEW_MESSAGE) ! 0) { if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, rxHeader, rxData) HAL_OK) { /* 在这里做数据搬运或置标志位 */ } } }4.2 用 GPIO 翻转粗略测量中断耗时为了验证中断占据的 CPU 时间我在回调入口把一个 GPIO 拉高回调出口拉低然后用逻辑分析仪看高电平宽度。DLC 为 8 字节时整个HAL_FDCAN_GetRxMessage加字段解析大约在 2~3µs 左右DLC 为 64 字节时大约在 4~6µs。H753