<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>嵌入式通信 on Tech Snippets - 嵌入式技术笔记</title>
    <link>https://tech-snippets.xyz/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F%E9%80%9A%E4%BF%A1/</link>
    <description>Recent content in 嵌入式通信 on Tech Snippets - 嵌入式技术笔记</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Sat, 23 May 2026 19:00:00 +0800</lastBuildDate>
    <atom:link href="https://tech-snippets.xyz/tags/%E5%B5%8C%E5%85%A5%E5%BC%8F%E9%80%9A%E4%BF%A1/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>STM32 CAN 总线通信深度实战指南：从协议原理到 bxCAN 工程落地</title>
      <link>https://tech-snippets.xyz/posts/stm32-can-bus-communication-guide/</link>
      <pubDate>Sat, 23 May 2026 19:00:00 +0800</pubDate>
      <guid>https://tech-snippets.xyz/posts/stm32-can-bus-communication-guide/</guid>
      <description>前言 如果你做过几年嵌入式开发，迟早会撞到 CAN 总线这堵墙。它在汽车电子里几乎是默认标配，在工业控制、轨道交通、电梯、医疗设备、甚至是无人机机臂内部通信里，到处都有它的身影。我第一次和 CAN 打交道是在一个新能源 BMS 项目上——电池包要把每串单体电压、温度、SOC 上报给整车控制器（VCU），现场没有以太网，没有 RS485，PCB 上焊得整整齐齐就两根线：CAN_H 和 CAN_L。当时我天真地以为，无非就是另一个 UART，能收能发就完事。结果第一周就被仲裁机制、位时序、错误帧、Bus-Off 教育得明明白白。
写这篇文章的初衷，是想把这些年踩过的坑、读过的手册、调过的示波器波形，整理成一份能让后来人少走弯路的实战指南。我不会停留在「CAN 是一种串行通信总线」这种百度百科式的介绍，而是从协议本质讲起，一路下沉到 STM32 的 bxCAN 外设寄存器层，再上升到工程代码里那些「为什么我的滤波器配了却收不到包」的具体问题。
CAN 总线（Controller Area Network）由德国博世公司在 1986 年推出，最初的目标是给汽车上日益增多的电子控制单元（ECU）之间提供一条可靠、低成本的通信骨干。在 CAN 出现之前，每两个 ECU 之间都要拉一根专线，一辆中高端车里光线束就能有上百公斤重。CAN 把所有节点挂在同一条双绞线上，靠协议本身去解决冲突和优先级问题，线束重量瞬间砍掉一大半。这个设计哲学在今天看来依然非常先进——它把网络的复杂性下沉到了协议层，让物理层简单到极致。
放到 2026 年的视角，CAN 当然不算「新技术」。CAN FD（Flexible Data-rate）已经成为汽车主流，传输速率突破 5 Mbps，数据载荷扩展到 64 字节；CAN XL 已经在路上，可以飙到 10 Mbps、2048 字节负载；车载以太网（Automotive Ethernet）也在逐步蚕食骨干网的份额。但即便如此，传统 CAN 2.0B 在嵌入式工程里依然不可替代。原因很简单：它便宜、皮实、抗干扰能力恐怖、协议成熟到几乎不会出 bug，而且几乎每一颗工业级 MCU 都自带 CAN 控制器。
一、为什么是 CAN？四个无法替代的设计优势 要理解 CAN 为什么能屹立不倒近四十年，必须先看清它的核心设计。我把它总结成四点：
第一，多主架构（Multi-master）。 总线上没有「主机」和「从机」的区分，任何节点在总线空闲时都可以主动发起传输。这跟 SPI 那种「主机点名才能说话」的模式完全不同。在汽车里，发动机 ECU 和刹车 ECU 是平等的两个节点，谁需要谁就说。这种架构的好处是没有单点故障——主机挂了整条线就废这种事不会发生。
第二，非破坏性逐位仲裁（Non-destructive bitwise arbitration）。 这是 CAN 协议的精髓所在，也是最容易被新手忽略的点。当多个节点同时抢占总线时，CAN 通过比较报文 ID 的每一位来决定谁优先发送，而且——胜出的那一帧不会被损坏。技术上，CAN 总线物理层定义了「显性位」（Dominant，逻辑 0）和「隐性位」（Recessive，逻辑 1）。在线与（wired-AND）逻辑下，只要有一个节点输出 0，整条总线就是 0。每个节点在发送时会同时监听总线状态，一旦发现自己发送的是 1 但总线上是 0，立刻退出仲裁、转为接收方。ID 数值越小（高位 0 越多）的报文，优先级越高。</description>
      <content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>如果你做过几年嵌入式开发，迟早会撞到 CAN 总线这堵墙。它在汽车电子里几乎是默认标配，在工业控制、轨道交通、电梯、医疗设备、甚至是无人机机臂内部通信里，到处都有它的身影。我第一次和 CAN 打交道是在一个新能源 BMS 项目上——电池包要把每串单体电压、温度、SOC 上报给整车控制器（VCU），现场没有以太网，没有 RS485，PCB 上焊得整整齐齐就两根线：CAN_H 和 CAN_L。当时我天真地以为，无非就是另一个 UART，能收能发就完事。结果第一周就被仲裁机制、位时序、错误帧、Bus-Off 教育得明明白白。</p>
<p>写这篇文章的初衷，是想把这些年踩过的坑、读过的手册、调过的示波器波形，整理成一份能让后来人少走弯路的实战指南。我不会停留在「CAN 是一种串行通信总线」这种百度百科式的介绍，而是从协议本质讲起，一路下沉到 STM32 的 bxCAN 外设寄存器层，再上升到工程代码里那些「为什么我的滤波器配了却收不到包」的具体问题。</p>
<p>CAN 总线（Controller Area Network）由德国博世公司在 1986 年推出，最初的目标是给汽车上日益增多的电子控制单元（ECU）之间提供一条可靠、低成本的通信骨干。在 CAN 出现之前，每两个 ECU 之间都要拉一根专线，一辆中高端车里光线束就能有上百公斤重。CAN 把所有节点挂在同一条双绞线上，靠协议本身去解决冲突和优先级问题，线束重量瞬间砍掉一大半。这个设计哲学在今天看来依然非常先进——它把网络的复杂性下沉到了协议层，让物理层简单到极致。</p>
<p>放到 2026 年的视角，CAN 当然不算「新技术」。CAN FD（Flexible Data-rate）已经成为汽车主流，传输速率突破 5 Mbps，数据载荷扩展到 64 字节；CAN XL 已经在路上，可以飙到 10 Mbps、2048 字节负载；车载以太网（Automotive Ethernet）也在逐步蚕食骨干网的份额。但即便如此，传统 CAN 2.0B 在嵌入式工程里依然不可替代。原因很简单：它便宜、皮实、抗干扰能力恐怖、协议成熟到几乎不会出 bug，而且几乎每一颗工业级 MCU 都自带 CAN 控制器。</p>
<h2 id="一为什么是-can四个无法替代的设计优势">一、为什么是 CAN？四个无法替代的设计优势</h2>
<p>要理解 CAN 为什么能屹立不倒近四十年，必须先看清它的核心设计。我把它总结成四点：</p>
<p><strong>第一，多主架构（Multi-master）。</strong> 总线上没有「主机」和「从机」的区分，任何节点在总线空闲时都可以主动发起传输。这跟 SPI 那种「主机点名才能说话」的模式完全不同。在汽车里，发动机 ECU 和刹车 ECU 是平等的两个节点，谁需要谁就说。这种架构的好处是没有单点故障——主机挂了整条线就废这种事不会发生。</p>
<p><strong>第二，非破坏性逐位仲裁（Non-destructive bitwise arbitration）。</strong> 这是 CAN 协议的精髓所在，也是最容易被新手忽略的点。当多个节点同时抢占总线时，CAN 通过比较报文 ID 的每一位来决定谁优先发送，而且——胜出的那一帧不会被损坏。技术上，CAN 总线物理层定义了「显性位」（Dominant，逻辑 0）和「隐性位」（Recessive，逻辑 1）。在线与（wired-AND）逻辑下，只要有一个节点输出 0，整条总线就是 0。每个节点在发送时会同时监听总线状态，一旦发现自己发送的是 1 但总线上是 0，立刻退出仲裁、转为接收方。ID 数值越小（高位 0 越多）的报文，优先级越高。</p>
<p><strong>第三，差分信号传输。</strong> CAN_H 和 CAN_L 是一对双绞线，传输的是差分电压。隐性状态下两根线电压都是 2.5V，差值为 0；显性状态下 CAN_H 升到 3.5V，CAN_L 降到 1.5V，差值 2V。这种差分方式对共模干扰具有天然的抑制能力，配合双绞线的扭绞，可以在强电磁环境下（比如电机控制柜里）稳定通信。这也是为什么工业现场宁可用 CAN，也不爱用单端的 UART。</p>
<p><strong>第四，强大的错误检测与故障约束机制。</strong> CAN 协议内置了五种错误检测：CRC 校验、ACK 应答、帧格式检查、位监听、位填充。任何一种错误被检测到，整条总线上的节点都会立即丢弃当前帧并进入错误处理。更关键的是 CAN 的「错误计数器」机制——每个节点都有 TEC（发送错误计数）和 REC（接收错误计数）寄存器，超过 127 进入被动错误状态，超过 255 进入 Bus-Off 状态自动脱离总线，避免一个坏节点把整条总线拖死。这个故障约束机制保证了系统级的鲁棒性。</p>
<p><img alt="CAN 总线拓扑结构" loading="lazy" src="/images/posts/2026/05/can-bus-topology.svg"></p>
<h2 id="二can-物理层那些手册上不写的细节">二、CAN 物理层：那些手册上不写的细节</h2>
<p>讲完协议哲学，我们来看物理层。CAN 的物理层标准在 ISO 11898 里定义得很细，但工程实践中有几个点特别容易翻车：</p>
<p><strong>终端电阻必须严格 120Ω，而且只能在总线两端各接一个。</strong> 我见过太多新手要么忘了接，要么在每个节点都接一个。终端电阻的作用是阻抗匹配，吸收信号反射。CAN 是分布参数的传输线，没有终端电阻，信号在线缆两端会发生反射，导致波形畸变、ACK 错误、误码率飙升。如果中间某个节点接了终端电阻，总线阻抗变成 60Ω 甚至 40Ω，差分电压幅度变小、收发器驱动能力不足，同样会出问题。</p>
<p><strong>线缆长度与波特率成反比。</strong> 这是因为 CAN 的仲裁机制要求显性位的电平变化在一个位时间（Bit Time）内传遍整条总线，让所有节点都能采样到一致的电平。光在导线里传播大约 5ns/m，一个 1 Mbps 的位时间是 1μs，留给信号传播的余量大概只够 40 米。所以你会看到典型的对应关系：1 Mbps 对应 40 米、500 kbps 对应 100 米、250 kbps 对应 250 米、125 kbps 对应 500 米。超出这个范围就老老实实降速，别硬怼。</p>
<p><strong>收发器（Transceiver）的选型很重要。</strong> STM32 内部的 CAN 控制器输出的是 TTL 电平的 CAN_TX/CAN_RX 单端信号，必须外接收发器芯片转换成差分总线信号。汽车级常用 TJA1050、TJA1042，工业级用 SN65HVD230、MCP2551。要重点关注几个参数：通信速率（CAN FD 必须用 5Mbps 以上的收发器）、节点数量上限（一般 110 个左右）、待机功耗（电池供电场景）、隔离方式（高压环境用 ADM3053 这种带隔离的型号）。</p>
<h2 id="三can-数据帧协议层的语法书">三、CAN 数据帧：协议层的语法书</h2>
<p>CAN 协议定义了四种帧：数据帧、远程帧、错误帧、过载帧。99% 的应用只用数据帧，剩下三种主要是协议层自动处理。数据帧又分两种格式：CAN 2.0A 的标准帧（11 位 ID）和 CAN 2.0B 的扩展帧（29 位 ID）。我们重点看标准数据帧。</p>
<p><img alt="CAN 2.0A 标准数据帧结构" loading="lazy" src="/images/posts/2026/05/can-frame-structure.svg"></p>
<p>一个完整的标准数据帧从 SOF（帧起始）的一个显性位开始，紧跟着 11 位 ID（仲裁段）、1 位 RTR（远程发送请求，数据帧为 0）、6 位控制段（包含 IDE 标识扩展位、保留位 r0、4 位 DLC 数据长度码）、0~64 位的数据段、15 位 CRC 加 1 位界定符、2 位 ACK 段、7 位 EOF 帧结束，最后是 3 位 IFS 帧间隔。算下来一个空帧 47 位、满载（8 字节）数据帧 111 位（不算位填充）。所以在 1 Mbps 速率下，理论最高每秒约 9000 帧，实际由于位填充和帧间隔会再低一些。</p>
<p>这里面**位填充（Bit Stuffing）**是个容易被忽视但非常重要的机制。CAN 没有独立的时钟线，发送方和接收方靠相邻位的跳变沿来重新同步时钟。如果连续出现 5 个相同的位，发送方会强行插入一个相反的位（接收方再自动去掉），保证总线上始终有足够的跳变。这个机制让 CAN 能在没有时钟线的情况下做到几乎 0 错位采样，但代价是同一份数据在总线上的实际长度是不固定的。在做总线带宽精确计算时要把位填充的开销算进去，一般按帧长的 20% 估算。</p>
<p>（第一部分完，约 3100 字）</p>
<h2 id="四stm32-bxcan-外设硬件结构剖析">四、STM32 bxCAN 外设：硬件结构剖析</h2>
<p>STM32F1/F4/F7/H7 系列的 CAN 控制器叫做 <strong>bxCAN</strong>（Basic Extended CAN），全面支持 CAN 2.0A 和 2.0B 协议，最高速率 1 Mbps。STM32G4/H7 部分型号开始上 <strong>FDCAN</strong>，支持 CAN FD。我们先把 bxCAN 吃透，FDCAN 的差异主要在数据段长度和位时序，理解了 bxCAN 切过去问题不大。</p>
<p>bxCAN 内部最核心的三块东西：<strong>3 个发送邮箱（Tx Mailbox）</strong>、<strong>2 个接收 FIFO（每个 3 级深度）</strong>、<strong>14 个过滤器组</strong>（连接两路 CAN 的型号是 28 个，主从拆分使用）。</p>
<p><img alt="STM32 bxCAN 内部结构" loading="lazy" src="/images/posts/2026/05/stm32-bxcan-architecture.svg"></p>
<p><strong>发送邮箱</strong>是发送方向的核心。当应用层调用 <code>HAL_CAN_AddTxMessage()</code> 时，HAL 库会找一个空闲的 Tx Mailbox 把帧数据填进去，硬件自动负责后续的总线仲裁、CRC 计算、错误重传。三个邮箱之间默认按报文 ID 优先级排队发送（小 ID 先发），也可以通过 <code>CAN_MCR_TXFP</code> 位改成 FIFO 顺序发送。在高优先级周期帧（比如 10ms 的发动机扭矩）和低优先级事件帧（比如故障码上报）混合的场景里，三个邮箱设计得刚刚好——基本不会出现「关键帧排队等待非关键帧」的优先级反转。</p>
<p><strong>接收 FIFO</strong> 是接收方向的核心。bxCAN 有两个独立的 FIFO（FIFO0 和 FIFO1），每个 3 级深度。一帧合法报文进来后，先过过滤器组，根据过滤器配置决定送进 FIFO0 还是 FIFO1。FIFO 满了之后默认会丢弃新到的报文并置 FOVR（FIFO Overrun）标志位，也可以配置成覆盖最旧的报文。在中断模式下，FIFO 非空触发 <code>CAN_RX0/RX1</code> 中断，应用层在中断里调用 <code>HAL_CAN_GetRxMessage()</code> 取出报文。</p>
<p><strong>过滤器组</strong>是最容易让新手翻车的地方。它的作用是硬件层面提前过滤掉不感兴趣的报文，避免大量无关报文塞满 FIFO、把 CPU 拖死在中断里。在汽车 CAN 网络上每秒几千帧是常态，没有硬件过滤靠 CPU 软件判断根本扛不住。</p>
<h2 id="五过滤器组的工作原理一次说清楚">五、过滤器组的工作原理：一次说清楚</h2>
<p>过滤器组的配置有两个维度：<strong>位宽</strong>（16 位或 32 位）和<strong>模式</strong>（掩码模式 Mask Mode 或列表模式 List Mode）。两两组合就是四种工作模式：</p>
<ul>
<li><strong>32 位 / 列表模式</strong>：一组可以精确匹配 2 个完整的 29 位扩展 ID</li>
<li><strong>32 位 / 掩码模式</strong>：一组可以匹配 1 个 ID + 1 个掩码（指定哪些位需要匹配）</li>
<li><strong>16 位 / 列表模式</strong>：一组可以精确匹配 4 个 11 位标准 ID</li>
<li><strong>16 位 / 掩码模式</strong>：一组可以匹配 2 组 ID + 掩码</li>
</ul>
<p><strong>掩码模式</strong>最常用，举个例子：你希望接收所有 ID 在 0x100~0x1FF 范围内的标准帧。配置 ID 为 <code>0x100</code>，掩码为 <code>0x700</code>（二进制 111 0000 0000），意思是「ID 的高 3 位必须等于 100 ，低 8 位可以是任意值」。这样硬件就只让 0x100~0x1FF 的报文进 FIFO。</p>
<p><strong>列表模式</strong>适合接收的报文 ID 数量少且固定的场景，比如只接收 0x123、0x456、0x789 三个 ID，配置进列表里硬件就只放这三个进来。</p>
<p>实际工程里我的经验是：<strong>先用最宽松的掩码（ID=0, Mask=0，即接收所有报文）让通信跑通，再根据节点的报文表逐步收紧过滤器。</strong> 一上来就配精确过滤，调通信问题时根本不知道是没收到包还是被过滤掉了。</p>
<p>这里还有一个<strong>经典坑</strong>：HAL 库里 <code>CAN_FilterTypeDef</code> 结构体的 <code>FilterIdHigh</code> 和 <code>FilterIdLow</code> 字段是<strong>左移 5 位后的值</strong>！标准帧 11 位 ID 写在高 16 位的 [15:5]。比如你要过滤 ID = 0x123 的标准帧，要写 <code>FilterIdHigh = 0x123 &lt;&lt; 5 = 0x2460</code>。这个坑我见过至少 5 个新手踩过，包括我自己第一次用 bxCAN 的时候。STM32CubeMX 也不会替你算，必须自己手动左移。</p>
<h2 id="六can-位时序那个让人头大的-tq">六、CAN 位时序：那个让人头大的 Tq</h2>
<p>CAN 的波特率配置不是简单设个数字就完事，它涉及**位时序（Bit Timing）**的精细调整。一个位时间被划分成几段：</p>
<ul>
<li><strong>SyncSeg（同步段）</strong>：固定 1 个 Tq（时间量子），用于跨节点同步</li>
<li><strong>PropSeg（传播段）</strong>：补偿物理传播延迟，1~8 Tq</li>
<li><strong>PhaseSeg1（相位段 1）</strong>：1~8 Tq</li>
<li><strong>PhaseSeg2（相位段 2）</strong>：1~8 Tq</li>
</ul>
<p>STM32 把 PropSeg 和 PhaseSeg1 合并成一个 <code>BS1</code>（Time Segment 1），PhaseSeg2 叫 <code>BS2</code>。波特率的计算公式：</p>
<pre tabindex="0"><code>波特率 = CAN_Clock / (Prescaler × (1 + BS1 + BS2))
</code></pre><p>举个例子，STM32F4 的 APB1 时钟一般是 42 MHz，给 CAN 用。要配 500 kbps：</p>
<pre tabindex="0"><code>42 MHz / (Prescaler × (1 + BS1 + BS2)) = 500000
=&gt; Prescaler × (1 + BS1 + BS2) = 84
</code></pre><p>一种选法是 <code>Prescaler = 6</code>, <code>BS1 = 11 Tq</code>, <code>BS2 = 2 Tq</code>，刚好等于 84，采样点在 (1+11)/14 ≈ 85.7%，落在推荐的 75%~87.5% 区间。<strong>采样点位置很关键</strong>，太靠前抗干扰差，太靠后留给同步调整的余量不够。汽车行业普遍推荐 75% 或 87.5%（CiA 标准）。</p>
<p>不想手动算？用在线工具 <a href="http://www.bittiming.can-wiki.info/">bittiming.can-wiki.info</a>，输入 MCU 型号、时钟频率、目标波特率，直接出最优配置，比自己拍脑袋强。</p>
<h2 id="七cubemx-初始化配置一步一步来">七、CubeMX 初始化配置：一步一步来</h2>
<p>打开 STM32CubeMX，选好芯片（以 STM32F407 为例）：</p>
<ol>
<li><strong>RCC 配置</strong>：HSE 接外部 8MHz 晶振，PLL 倍频到 168MHz（APB1 = 42MHz）</li>
<li><strong>使能 CAN1</strong>：左侧 Connectivity → CAN1 → Activated 打勾，模式选 Master</li>
<li><strong>引脚分配</strong>：CAN1_TX → PA12（或 PB9），CAN1_RX → PA11（或 PB8）。注意 PA12 和 USB_DP 复用，做 USB 设备时要避开</li>
<li><strong>参数配置</strong>：
<ul>
<li>Prescaler = 6</li>
<li>Time Quanta in Bit Segment 1 = 12 Times</li>
<li>Time Quanta in Bit Segment 2 = 2 Times</li>
<li>ReSynchronization Jump Width = 1 Time</li>
<li>Operating Mode = Normal（调试时可选 Loopback 自环测试）</li>
<li>Time Triggered Communication Mode = Disable</li>
<li>Automatic Bus-Off Management = <strong>Enable</strong>（重要！让 Bus-Off 后能自动恢复）</li>
<li>Automatic Wake-Up Mode = Enable</li>
<li>Automatic Retransmission = Enable</li>
<li>Receive Fifo Locked Mode = Disable</li>
<li>Transmit Fifo Priority = Disable</li>
</ul>
</li>
<li><strong>NVIC 配置</strong>：使能 CAN1 RX0 interrupt（FIFO0 消息挂起中断）</li>
<li>生成代码</li>
</ol>
<p>生成的 <code>MX_CAN1_Init()</code> 函数会包含所有参数。我们要做的事情通常是在 <code>main()</code> 里：</p>
<ul>
<li>配置过滤器组（<code>HAL_CAN_ConfigFilter</code>）</li>
<li>启动 CAN（<code>HAL_CAN_Start</code>）</li>
<li>使能 RX 中断（<code>HAL_CAN_ActivateNotification</code>）</li>
<li>在中断回调里取报文，在主循环里发报文</li>
</ul>
<p>下一部分我们就把这套完整代码撸出来。</p>
<p>（第二部分完，约 2500 字）</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
