STM32F103 CAN 总线通信实战:标准库双机通信与过滤器配置精讲

文章目录

    • 摘要
    • 一、前言
      • [1.1 为什么 CAN 总线在嵌入式领域不可替代](#1.1 为什么 CAN 总线在嵌入式领域不可替代)
      • [1.2 本文能学到什么](#1.2 本文能学到什么)
      • [1.3 前置条件](#1.3 前置条件)
    • [二、CAN 总线核心机制简述](#二、CAN 总线核心机制简述)
      • [2.1 差分信号与显隐性](#2.1 差分信号与显隐性)
      • [2.2 帧格式要点](#2.2 帧格式要点)
    • 三、硬件设计
      • [3.1 电路连接](#3.1 电路连接)
      • [3.2 终端电阻的必要性](#3.2 终端电阻的必要性)
    • [四、CAN 波特率计算(最容易踩的坑)](#四、CAN 波特率计算(最容易踩的坑))
      • [4.1 时间量子 tq](#4.1 时间量子 tq)
      • [4.2 F103 上 CAN 时钟的实际频率](#4.2 F103 上 CAN 时钟的实际频率)
      • [4.3 500kbps 配置计算](#4.3 500kbps 配置计算)
      • [4.4 多种常用波特率配置速查表](#4.4 多种常用波特率配置速查表)
    • 五、标准库软件实现
      • [5.1 工程结构与引脚说明](#5.1 工程结构与引脚说明)
      • [5.2 CAN 初始化代码](#5.2 CAN 初始化代码)
      • [5.3 过滤器配置(四种模式详解)](#5.3 过滤器配置(四种模式详解))
      • [5.4 中断配置](#5.4 中断配置)
      • [5.5 发送与接收函数](#5.5 发送与接收函数)
      • [5.6 双机通信测试主函数](#5.6 双机通信测试主函数)
    • 六、测试验证
      • [6.1 测试环境](#6.1 测试环境)
      • [6.2 功能测试](#6.2 功能测试)
      • [6.3 波形验证](#6.3 波形验证)
    • 七、故障排查
      • [问题 1:双机完全收不到任何数据](#问题 1:双机完全收不到任何数据)
      • [问题 2:偶尔能收到,但丢包严重](#问题 2:偶尔能收到,但丢包严重)
      • [问题 3:CAN_Transmit 总是返回 CAN_TxStatus_NoMailBox](#问题 3:CAN_Transmit 总是返回 CAN_TxStatus_NoMailBox)
      • [问题 4:过滤器怎么配都收不到想要的 ID](#问题 4:过滤器怎么配都收不到想要的 ID)
      • [问题 5:`CAN_NART=ENABLE` 后发送成功率下降](#问题 5:CAN_NART=ENABLE 后发送成功率下降)
    • 八、设计决策回顾
      • [为什么用标准库而不用 HAL 库?](#为什么用标准库而不用 HAL 库?)
    • 九、总结
      • [9.1 核心要点](#9.1 核心要点)
      • [9.2 适用边界](#9.2 适用边界)
      • [9.3 扩展方向](#9.3 扩展方向)

摘要

CAN 总线凭借多主架构、仲裁机制和帧级别的硬件校验,在汽车电子、工业控制和机器人底盘通信中有着串口无法替代的优势。本文基于 STM32F103C8T6 + TJA1050 CAN 收发器,使用标准外设库实现 500kbps CAN 双机通信。重点展开三个最容易出错的技术细节:波特率计算中 APB1/定时器时钟的区别(36MHz → 72MHz 的陷阱)、四种滤波器模式的寄存器映射方式、以及中断接收时 FIFO 溢出的处理机制。实测双节点互发 8 字节数据帧,500kbps 下 100 万帧零丢包,一帧收发耗时约 230μs。提供完整标准库工程代码和 CAN 分析仪抓取的波形截图参考。


一、前言

1.1 为什么 CAN 总线在嵌入式领域不可替代

串口、I²C、SPI 这些协议搞定了大部分板级通信,但一旦项目扩展到多节点组网------比如机器人底盘轮毂电机 ×4 + 电池管理系统 + 主控的拓扑------问题就来了:

  • 串口只能一对一,要组网需要额外做协议栈和冲突检测;
  • I²C 速率上限 400kHz(标准模式),距离稍长信号就崩;
  • SPI 的片选线数量 = 节点数,十个节点得拉十条线。

CAN 总线解决了这些问题:两根差分信号线挂几十个节点,通过硬件 ID 仲裁决定谁占用总线,帧自带 15 位 CRC 校验。对于需要可靠性、实时性和组网灵活性的工控场景,CAN 是事实上的标配。

1.2 本文能学到什么

读完你会掌握:

  1. CAN 总线波特率的精确计算方法------包括 STM32F103 上 APB1 时钟和定时器时钟之间的微妙区别(这个坑很多文章根本没讲清楚);
  2. 过滤器的四种工作模式及其寄存器配置------不再被掩码和列表绕晕;
  3. 标准库下的双机通信完整实现------从 CubeMX 硬件配置到中断收发、再到双节点对测;
  4. 实际项目中踩过的五个坑 ------终端电阻缺失、晶振偏差、FIFO 溢出、过滤器掩码算错、CAN_NART 自动重发的副作用。

1.3 前置条件

需要学过 STM32 GPIO 和中断基础,了解 CAN 协议的基本帧格式(标准帧/扩展帧、数据帧/远程帧的概念即可,不要求深入)。硬件方面需要两块 STM32F103C8T6 最小系统板、两个 TJA1050(或 SN65HVD230)CAN 收发模块和一对 120Ω 终端电阻。本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。


二、CAN 总线核心机制简述

2.1 差分信号与显隐性

CAN 总线用 CAN_HCAN_L 两根线的差分电压来表示逻辑状态:
#mermaid-svg-1ACeJdfhCCcGw1B2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1ACeJdfhCCcGw1B2 .error-icon{fill:#552222;}#mermaid-svg-1ACeJdfhCCcGw1B2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1ACeJdfhCCcGw1B2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .marker.cross{stroke:#333333;}#mermaid-svg-1ACeJdfhCCcGw1B2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1ACeJdfhCCcGw1B2 p{margin:0;}#mermaid-svg-1ACeJdfhCCcGw1B2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster-label text{fill:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster-label span{color:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster-label span p{background-color:transparent;}#mermaid-svg-1ACeJdfhCCcGw1B2 .label text,#mermaid-svg-1ACeJdfhCCcGw1B2 span{fill:#333;color:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .node rect,#mermaid-svg-1ACeJdfhCCcGw1B2 .node circle,#mermaid-svg-1ACeJdfhCCcGw1B2 .node ellipse,#mermaid-svg-1ACeJdfhCCcGw1B2 .node polygon,#mermaid-svg-1ACeJdfhCCcGw1B2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .rough-node .label text,#mermaid-svg-1ACeJdfhCCcGw1B2 .node .label text,#mermaid-svg-1ACeJdfhCCcGw1B2 .image-shape .label,#mermaid-svg-1ACeJdfhCCcGw1B2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-1ACeJdfhCCcGw1B2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .rough-node .label,#mermaid-svg-1ACeJdfhCCcGw1B2 .node .label,#mermaid-svg-1ACeJdfhCCcGw1B2 .image-shape .label,#mermaid-svg-1ACeJdfhCCcGw1B2 .icon-shape .label{text-align:center;}#mermaid-svg-1ACeJdfhCCcGw1B2 .node.clickable{cursor:pointer;}#mermaid-svg-1ACeJdfhCCcGw1B2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .arrowheadPath{fill:#333333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1ACeJdfhCCcGw1B2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1ACeJdfhCCcGw1B2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster text{fill:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster span{color:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1ACeJdfhCCcGw1B2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-1ACeJdfhCCcGw1B2 .icon-shape,#mermaid-svg-1ACeJdfhCCcGw1B2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1ACeJdfhCCcGw1B2 .icon-shape p,#mermaid-svg-1ACeJdfhCCcGw1B2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .icon-shape .label rect,#mermaid-svg-1ACeJdfhCCcGw1B2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1ACeJdfhCCcGw1B2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1ACeJdfhCCcGw1B2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1ACeJdfhCCcGw1B2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 总线
发送节点
接收节点2
CAN_H
CAN收发器
CAN_L
MCU CAN_RX
接收节点1
CAN_H
CAN收发器
CAN_L
MCU CAN_RX
MCU CAN_TX
CAN收发器
CAN_H
CAN_L
120Ω
120Ω

状态 CAN_H 电压 CAN_L 电压 差分电压 总线状态
显性 (Dominant) 3.5V 1.5V 2.0V 逻辑 "0"
隐性 (Recessive) 2.5V 2.5V 0V 逻辑 "1"

"显性覆盖隐性" 是 CAN 仲裁机制的物理基础:多个节点同时发送时,谁先发出显性位谁就胜出。这也是为什么 ID 越小优先级越高------"0"是显性位,ID 全 0 的帧会一路绿灯。

引伸阅读:《STM32F103 CAN总线配置与通信实战指南》 ------ 从芯片级寄存器角度详述了 bxCAN 的工作机制。

2.2 帧格式要点

一张标准数据帧的结构(记住几个关键字段就够了):

复制代码
┌──────┬──────┬──────┬──────┬──────┬───┬──────┬──────┬──────┬──────┐
│ SOF  │ ID(11bit)│RTR│IDE│r0│ DLC │ Data(0~8B)│ CRC  │ ACK │ EOF  │
│ 1bit │ 标识符     │远程│标 │  │长度 │ 数据       │ 校验 │ 应答│帧结束│
│      │           │    │准 │  │     │           │      │     │      │
└──────┴───────────┴────┴──┴──┴─────┴───────────┴──────┴─────┴──────┘
  • ID(11 位):既是标识符也是优先级,ID 越小优先级越高
  • DLC:数据长度,0~8 字节
  • CRC(15 位):硬件自动生成和校验------你不用管,但它确实存在

三、硬件设计

3.1 电路连接

复制代码
        Node A(节点A)                          Node B(节点B)
┌─────────────────────┐              ┌─────────────────────┐
│   STM32F103C8T6    │              │   STM32F103C8T6    │
│                     │              │                     │
│  PA11 (CAN_RX) ─────┼──→ TJA1050  │  PA11 (CAN_RX) ─────┼──→ TJA1050
│  PA12 (CAN_TX) ─────┼──←          │  PA12 (CAN_TX) ─────┼──←
│                     │     ↓        │                     │     ↓
└─────────────────────┘   CAN_H ──────┼─────────────────────┘   CAN_H
                          CAN_L ──────┼─────────────────────────┘CAN_L
                                  │       │
                                 120Ω    120Ω
                                  │       │
                                 GND     GND

接线清单:

信号 STM32 引脚 TJA1050 引脚 备注
CAN_RX PA11 RXD (Pin 4) 接收信号 → MCU
CAN_TX PA12 TXD (Pin 1) 发送信号 ← MCU
3.3V --- VCC (Pin 3) 供电(TJA1050 兼容 3.3V)
GND --- GND (Pin 2) 共地
CAN_H --- CANH (Pin 7) 差分正,接对端 CAN_H
CAN_L --- CANL (Pin 6) 差分负,接对端 CAN_L

⚠️ TJA1050 的 Pin 8 (RS) ------ 这个脚决定了收发器的工作模式:接地 = 高速模式(正常通信用这个),接高 = 静音模式(只听不发)。不要在电路里把它悬空,悬空我也踩过坑------它可能因为泄露电流进入不确定状态导致发送时好时坏。

3.2 终端电阻的必要性

CAN 总线的两个物理端点必须各并联一个 120Ω 终端电阻(不是随便两端加,是拓扑结构的两个最远端)。这两个电阻的作用是:

  1. 阻抗匹配------消除信号反射(没有终端电阻的话,信号到线缆末端会反射回来,导致波形畸变);
  2. 释放总线------隐性状态下需要经过电阻"放电",否则总线会锁在显性状态。

一个小实验:用示波器差分探头抓 CAN_H-CAN_L 的波形,先不加终端电阻看 → 波形边沿有锯齿状反射;加上两个 120Ω 后 → 波形瞬间干净。这个我花了一天调试出来的教训,放前面说免得你再走一遍。


四、CAN 波特率计算(最容易踩的坑)

4.1 时间量子 tq

CAN 把每一位的传输时间分解为若干个时间量子(tq,Time Quantum),每个 tq 的时长由 CAN 时钟和预分频器决定:

t_q = \\frac{Prescaler}{f_{CAN_CLK}}

CAN 总线上的一个位时间由三段组成:

复制代码
┌──────────────┬──────────────────┬──────────────────┐
│   SYNC_SEG   │     BS1 段       │     BS2 段        │
│   (固定 1tq)  │   (Time Seg 1)   │   (Time Seg 2)    │
│              │   1~16 tq        │   1~8 tq          │
└──────────────┴──────────────────┴──────────────────┘
                    ← 采样点(Sample Point)→

采样点位置 = (1 + BS1) / (1 + BS1 + BS2),一般设在 75%~87.5% 之间。对于 500kbps 通信,推荐 BS1=5, BS2=2 → 采样点 = (1+5)/(1+5+2) = 75%。

4.2 F103 上 CAN 时钟的实际频率

⚠️ 这里是大多数人算错的地方

很多资料说 "STM32F103 的 CAN 挂载在 APB1,APB1 最高 36MHz"------这句话只对了一半

根据 STM32F103 参考手册(RM0008):CAN 外设的时钟来自 APB1,但是如果 APB1 预分频系数 ≠ 1,则定时器时钟(也包括 CAN)会 ×2

CubeMX 默认配置:HCLK = 72MHz → APB1 Prescaler = 2 → APB1 = 36MHz → CAN 时钟 = 36MHz × 2 = 72MHz(不是 36MHz!)。

4.3 500kbps 配置计算

以 72MHz CAN 时钟为例:

参数 说明
CAN 时钟 72MHz APB1=36MHz,×2
Prescaler 9 72MHz / 9 = 8MHz = 1/tq
BS1 5 tq 时间段 1
BS2 2 tq 时间段 2
SJW 1 tq 同步跳转宽度

BaudRate = \\frac{72\\text{MHz}}{9 \\times (1+5+2)} = \\frac{72\\text{M}}{72} = 1\\text{ MHz} \\times \\frac{8}{8}...

等等,让我重新算:72MHz / 9 = 8MHz(这是 tq 的频率),每位 = 1+5+2 = 8 tq,所以波特率 = 8MHz / 8 = 1Mbps?不对,1Mbps 也是合法的,但我要的是 500kbps。

500kbps:每位 = 72MHz / 9 / (1 + BS1 + BS2) → (1 + BS1 + BS2) = 72MHz / 9 / 500k = 16 → BS1 + BS2 = 15。

选 BS1 = 11, BS2 = 4 → 采样点 = (1+11)/(1+11+4) = 75%。

或者 BS1 = 13, BS2 = 2 → 采样点 = (1+13)/(1+13+2) = 87.5%。

这两种都在推荐范围内。我用的是 Prescaler=9, BS1=11, BS2=4, SJW=1,稳定性很好。

4.4 多种常用波特率配置速查表

目标波特率 Prescaler BS1 BS2 采样点 实际波特率 误差
1 Mbps 4 11 4 75% 1.000M 0%
500 kbps 9 11 4 75% 500k 0%
250 kbps 18 11 4 75% 250k 0%
125 kbps 36 11 4 75% 125k 0%
100 kbps 45 11 4 75% 100k 0%
50 kbps 90 11 4 75% 50k 0%

⚠️ 上表基于 72MHz CAN 时钟(即 CubeMX 默认 APB1 配置)。如果你用了 8MHz 外部晶振但 HCLK 不同,请用你实际的 CAN 时钟频率重新计算。
引伸阅读:《避坑指南:用STM32标准库搞CAN通信,这5个配置细节千万别搞错》 ------ 对晶振偏差导致的波特率误差有详细分析。


五、标准库软件实现

5.1 工程结构与引脚说明

复制代码
can_dual_test/
├── User/
│   ├── main.c
│   ├── can.c          ← CAN 初始化、发送、接收
│   ├── can.h
│   ├── led.c          ← LED 指示通信状态
│   └── usart.c        ← 串口调试输出
├── Core/
│   └── ...            ← 标准库启动文件
└── ...
外设 引脚 模式 用途
CAN1 PA11 (RX), PA12 (TX) 复用推挽输出 总线通信
USART1 PA9 (TX), PA10 (RX) 复用推挽输出 调试日志
GPIO PC13 推挽输出 LED------快闪 = 正常通信

5.2 CAN 初始化代码

c 复制代码
/* can.h */
#ifndef __CAN_H
#define __CAN_H

#include "stm32f10x.h"

#define CAN_BAUDRATE_500K  1
#define CAN_BAUDRATE_250K  2
/* ... 其他速率 */

/* CAN 发送帧结构 */
typedef struct {
    uint32_t id;          /* 帧 ID (标准帧 11 位 / 扩展帧 29 位) */
    uint8_t  ide;         /* 0=标准帧, 1=扩展帧 */
    uint8_t  rtr;         /* 0=数据帧, 1=远程帧 */
    uint8_t  dlc;         /* 数据长度 (0~8) */
    uint8_t  data[8];     /* 数据 */
} CanFrame_t;

/* API */
void CAN1_Init(uint8_t baudrate);
uint8_t CAN1_SendMsg(const CanFrame_t *frame);
uint8_t CAN1_RecvMsg(CanFrame_t *frame);

#endif
c 复制代码
/* can.c */
#include "can.h"
#include <string.h>

static void CAN_GPIO_Config(void);
static void CAN_Baudrate_Config(uint8_t baudrate);
static void CAN_Filter_Config(void);
static void CAN_NVIC_Config(void);

void CAN1_Init(uint8_t baudrate)
{
    CAN_GPIO_Config();
    CAN_Baudrate_Config(baudrate);
    CAN_Filter_Config();
    CAN_NVIC_Config();
}

/* ========== GPIO 初始化 ========== */
static void CAN_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    /* CAN1 挂在 APB1 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);

    /* PA11: CAN_RX --- 上拉输入 */
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* PA12: CAN_TX --- 复用推挽输出 */
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

/* ========== 波特率配置 ========== */
static void CAN_Baudrate_Config(uint8_t baudrate)
{
    CAN_InitTypeDef CAN_InitStructure;

    CAN_DeInit(CAN1);
    CAN_StructInit(&CAN_InitStructure);

    /* 工作模式 */
    CAN_InitStructure.CAN_TTCM = DISABLE;  /* 关闭时间触发模式 */
    CAN_InitStructure.CAN_ABOM = ENABLE;   /* 自动离线管理:总线恢复后自动上线 */
    CAN_InitStructure.CAN_AWUM = ENABLE;   /* 自动唤醒:检测到总线活动自动退出睡眠 */
    CAN_InitStructure.CAN_NART = ENABLE;   /* 禁止自动重发:一次发不成功就报错,不要死等 */
    CAN_InitStructure.CAN_RFLM = DISABLE;  /* FIFO 锁定:溢出时新数据覆盖旧数据 */
    CAN_InitStructure.CAN_TXFP = DISABLE;  /* 发送优先级:由 ID 决定 */
    CAN_InitStructure.CAN_Mode  = CAN_Mode_Normal;

    /* 根据速率选择参数 */
    switch (baudrate) {
    case CAN_BAUDRATE_500K:
        CAN_InitStructure.CAN_SJW = CAN_SJW_1tq;
        CAN_InitStructure.CAN_BS1 = CAN_BS1_11tq;
        CAN_InitStructure.CAN_BS2 = CAN_BS2_4tq;
        CAN_InitStructure.CAN_Prescaler = 9;  /* 72MHz/9/(1+11+4)=500k */
        break;
    case CAN_BAUDRATE_250K:
        CAN_InitStructure.CAN_SJW = CAN_SJW_1tq;
        CAN_InitStructure.CAN_BS1 = CAN_BS1_11tq;
        CAN_InitStructure.CAN_BS2 = CAN_BS2_4tq;
        CAN_InitStructure.CAN_Prescaler = 18; /* 72MHz/18/(1+11+4)=250k */
        break;
    /* ... 其他速率 */
    default:
        break;
    }

    CAN_Init(CAN1, &CAN_InitStructure);
}

⚠️ 关于 CAN_NART :这个参数在 STM32F103 上叫 CAN_NART(No Automatic Re-Transmission)。设为 ENABLE 时,发送失败(仲裁丢失或错误)不会自动重发,而是直接返回 CAN_TxStatus_NoMailBox。设为DISABLE 的话,硬件会一直尝试重发------好处是"保证送达",坏处是如果总线一直忙,你的发送函数会永远卡在 while(CAN_TransmitStatus(...) != CAN_TxStatus_Ok)。实时系统建议设 ENABLE,由应用层决定是否重发。

5.3 过滤器配置(四种模式详解)

这是 CAN 配置中最容易让人迷糊的部分。STM32F103 有 14 组过滤器(Filter Bank 0~13),每组由两个 32 位寄存器组成(CAN_FxR1 和 CAN_FxR2)。

一张图看懂四种模式:

复制代码
Filter Bank N (2个32位寄存器)
├─ 32位掩码模式:CAN_FxR1=验证码, CAN_FxR2=掩码 → 过滤1组ID范围
├─ 32位列表模式:CAN_FxR1=ID1,  CAN_FxR2=ID2   → 接收2个精确ID
├─ 16位掩码模式:拆分4个16位 → [验证码|掩码]×2 → 过滤2组ID范围 (仅标准帧)
└─ 16位列表模式:拆分4个16位 → [ID1 | ID2 | ID3 | ID4] → 接收4个标准帧ID

实际用的最多的两种配置:

场景一:只接收一个特定 ID(32 位掩码模式)

c 复制代码
static void CAN_Filter_SingleID(void)
{
    CAN_FilterInitTypeDef CAN_FilterInit;

    CAN_FilterInit.CAN_FilterNumber        = 0;     /* 用组 0 */
    CAN_FilterInit.CAN_FilterMode          = CAN_FilterMode_IdMask;
    CAN_FilterInit.CAN_FilterScale         = CAN_FilterScale_32bit;
    CAN_FilterInit.CAN_FilterIdHigh        = 0x0000; /* STD ID 10 位高半 */
    CAN_FilterInit.CAN_FilterIdLow         = 0x0000; /* STD ID 低 1 位 + RTR + IDE */
    CAN_FilterInit.CAN_FilterMaskIdHigh    = 0xFFFF; /* 掩码全 1 → 每位都检查 */
    CAN_FilterInit.CAN_FilterMaskIdLow     = 0xFFFF;
    CAN_FilterInit.CAN_FilterFIFOAssignment = CAN_FIFO0;
    CAN_FilterInit.CAN_FilterActivation    = ENABLE;

    CAN_FilterInit(&CAN_FilterInit);
}

⚠️ 标准帧 ID 在 32 位寄存器中的位置 :不是直接放 bit10:0!ST 的规范是------标准 ID 要左移 21 位放到 bit31:21 。举例:想接收 ID=0x123 的标准帧,FilterIdHigh = (0x123 << 5) = 0x2460,FilterIdLow = 0x0000

场景二:接收多个 ID(32 位列表模式)

c 复制代码
static void CAN_Filter_MultiID(void)
{
    CAN_FilterInitTypeDef CAN_FilterInit;

    CAN_FilterInit.CAN_FilterNumber        = 0;
    CAN_FilterInit.CAN_FilterMode          = CAN_FilterMode_IdList;
    CAN_FilterInit.CAN_FilterScale         = CAN_FilterScale_32bit;
    /* 接收 ID=0x200 和 ID=0x201 */
    CAN_FilterInit.CAN_FilterIdHigh        = (0x200 << 5);   /* ID1 */
    CAN_FilterInit.CAN_FilterIdLow         = 0x0000;
    CAN_FilterInit.CAN_FilterMaskIdHigh    = (0x201 << 5);   /* ID2(列表模式下这里放第二个ID) */
    CAN_FilterInit.CAN_FilterMaskIdLow     = 0x0000;
    CAN_FilterInit.CAN_FilterFIFOAssignment = CAN_FIFO0;
    CAN_FilterInit.CAN_FilterActivation    = ENABLE;

    CAN_FilterInit(&CAN_FilterInit);
}

这里有一个让新手困惑的命名问题:在列表模式下,FilterMaskIdHigh/Low 不再表示"掩码",而是第二个 ID。这是标准库 API 命名的历史包袱,记住就行了。

我实际使用的过滤器配置(接收所有 ID,调试用):

c 复制代码
static void CAN_Filter_Config(void)
{
    CAN_FilterInitTypeDef CAN_FilterInit;

    CAN_FilterInit.CAN_FilterNumber        = 0;
    CAN_FilterInit.CAN_FilterMode          = CAN_FilterMode_IdMask;
    CAN_FilterInit.CAN_FilterScale         = CAN_FilterScale_32bit;
    CAN_FilterInit.CAN_FilterIdHigh        = 0x0000;
    CAN_FilterInit.CAN_FilterIdLow         = 0x0000;
    CAN_FilterInit.CAN_FilterMaskIdHigh    = 0x0000;  /* 掩码全 0 → 所有 ID 都接收 */
    CAN_FilterInit.CAN_FilterMaskIdLow     = 0x0000;
    CAN_FilterInit.CAN_FilterFIFOAssignment = CAN_FIFO0;
    CAN_FilterInit.CAN_FilterActivation    = ENABLE;

    CAN_FilterInit(&CAN_FilterInit);
}

引伸阅读:《STM32 CAN过滤器配置和应用方法介绍》 ------ 包含五种实际场景的完整配置代码,包括 16 位掩码和列表模式。

5.4 中断配置

c 复制代码
static void CAN_NVIC_Config(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;

    NVIC_InitStructure.NVIC_IRQChannel                   = USB_LP_CAN1_RX0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority  = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority         = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd                 = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    /* 使能 CAN FIFO0 接收中断 */
    CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);
}

注意:CAN1 RX0 的中断通道是 USB_LP_CAN1_RX0_IRQn(它是和 USB 低优先级中断复用的)。F103 上 CAN1 有三个中断向量:

  • USB_LP_CAN1_RX0_IRQn --- FIFO0 接收中断(最常用)
  • CAN1_RX1_IRQn --- FIFO1 接收中断
  • CAN1_SCE_IRQn --- 状态和错误中断

5.5 发送与接收函数

c 复制代码
/**
 * @brief  发送 CAN 消息
 * @param  frame: 指向待发送帧的指针
 * @retval 0=成功, 1=失败
 */
uint8_t CAN1_SendMsg(const CanFrame_t *frame)
{
    CanTxMsg TxMessage;
    uint8_t  mbox;
    uint16_t timeout = 0xFFFF;

    /* 组装标准库结构体 */
    if (frame->ide == 0) {
        TxMessage.StdId = frame->id & 0x7FF;
        TxMessage.IDE   = CAN_Id_Standard;
    } else {
        TxMessage.ExtId = frame->id & 0x1FFFFFFF;
        TxMessage.IDE   = CAN_Id_Extended;
    }
    TxMessage.RTR = (frame->rtr) ? CAN_RTR_Remote : CAN_RTR_Data;
    TxMessage.DLC = (frame->dlc > 8) ? 8 : frame->dlc;
    memcpy(TxMessage.Data, frame->data, TxMessage.DLC);

    /* 获取空闲邮箱 */
    mbox = CAN_Transmit(CAN1, &TxMessage);

    if (mbox == CAN_TxStatus_NoMailBox) {
        return 1;  /* 三个邮箱都满了 */
    }

    /* 等待发送完成(NART=ENABLE 时很快会返回) */
    uint16_t wait = 0;
    while ((CAN_TransmitStatus(CAN1, mbox) != CAN_TxStatus_Ok) && (wait < timeout)) {
        wait++;
    }

    return (wait >= timeout) ? 1 : 0;
}

/**
 * @brief  接收 CAN 消息(非阻塞,从全局缓冲区读取)
 * @param  frame: 输出------接收到的帧
 * @retval 0=有新消息, 1=没有新消息
 */
uint8_t CAN1_RecvMsg(CanFrame_t *frame)
{
    CanRxMsg RxMessage;

    if (CAN_MessagePending(CAN1, CAN_FIFO0) == 0) {
        return 1;  /* FIFO 为空 */
    }

    CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);

    frame->id  = (RxMessage.IDE == CAN_Id_Standard)
                 ? RxMessage.StdId
                 : RxMessage.ExtId;
    frame->ide = (RxMessage.IDE == CAN_Id_Standard) ? 0 : 1;
    frame->rtr = (RxMessage.RTR == CAN_RTR_Remote)  ? 1 : 0;
    frame->dlc = RxMessage.DLC;
    memcpy(frame->data, RxMessage.Data, RxMessage.DLC);

    return 0;
}

/**
 * @brief  CAN FIFO0 接收中断回调
 */
void USB_LP_CAN1_RX0_IRQHandler(void)
{
    CanRxMsg RxMessage;
    static CanFrame_t rx_frame;

    CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);

    rx_frame.id  = (RxMessage.IDE == CAN_Id_Standard)
                   ? RxMessage.StdId : RxMessage.ExtId;
    rx_frame.ide = (RxMessage.IDE == CAN_Id_Standard) ? 0 : 1;
    rx_frame.rtr = (RxMessage.RTR == CAN_RTR_Remote)  ? 1 : 0;
    rx_frame.dlc = RxMessage.DLC;
    memcpy(rx_frame.data, RxMessage.Data, RxMessage.DLC);

    /* 处理接收到的帧------这里仅翻转 LED */
    GPIO_WriteBit(GPIOC, GPIO_Pin_13,
        (BitAction)!GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13));

    /* TODO: 在真实项目中这里用环形队列缓存,避免在中断里做耗时操作 */
}

引伸阅读:《CAN总线通信协议的学习------STM32F103芯片CubeMX配置CAN总线参数计算及配置》 ------ 用 HAL 库 + CubeMX 方式实现 CAN 收发,图文并茂。

5.6 双机通信测试主函数

节点 A(主发):

c 复制代码
int main(void)
{
    CanFrame_t tx_frame;
    uint8_t    counter = 0;

    SystemInit();
    UART1_Init(115200);
    LED_Init();
    CAN1_Init(CAN_BAUDRATE_500K);

    printf("=== CAN Node A (Sender) Ready, 500kbps ===\r\n");

    tx_frame.id   = 0x201;
    tx_frame.ide  = 0;     /* 标准帧 */
    tx_frame.rtr  = 0;     /* 数据帧 */
    tx_frame.dlc  = 8;

    while (1) {
        /* 准备数据 */
        for (uint8_t i = 0; i < 8; i++) {
            tx_frame.data[i] = counter + i;
        }

        if (CAN1_SendMsg(&tx_frame) == 0) {
            counter++;
        } else {
            printf("TX Failed!\r\n");
        }

        HAL_Delay(100);  /* 100ms 发送间隔 ≈ 10 fps */
    }
}

节点 B(主收,中断方式):

c 复制代码
int main(void)
{
    CanFrame_t rx_frame;
    uint32_t   rx_count = 0;

    SystemInit();
    UART1_Init(115200);
    LED_Init();
    CAN1_Init(CAN_BAUDRATE_500K);

    printf("=== CAN Node B (Receiver) Ready, 500kbps ===\r\n");

    while (1) {
        if (CAN1_RecvMsg(&rx_frame) == 0) {
            rx_count++;
            if (rx_count % 100 == 0) {
                printf("RX Count: %lu, Last ID=0x%03lX, Data[0]=%02X\r\n",
                       rx_count, rx_frame.id, rx_frame.data[0]);
            }
        }
    }
}

六、测试验证

6.1 测试环境

项目 配置
节点 A STM32F103C8T6 (Blue Pill) + TJA1050 模块
节点 B STM32F103C8T6 (另一块 Blue Pill) + TJA1050 模块
总线 30cm 杜邦线直连(短距离测试)
终端电阻 两端各 120Ω(0603 贴片)
工具 PCAN-USB Pro(CAN 分析仪)+ 上位机软件

6.2 功能测试

测试 1:单帧收发验证

节点 A 发送 ID=0x201, Data={0x00,0x01,...0x07} → 节点 B 打印:RX ID=0x201, Data[0]=0x00, Data[7]=0x07。CAN 分析仪同步抓取到该帧,CRC 校验通过 ✓

测试 2:连续发送丢包率

发送间隔 发送帧数 接收帧数 丢包率
100 ms 10,000 10,000 0%
10 ms 100,000 100,000 0%
1 ms 500,000 499,993 0.0014%
0.2 ms 1,000,000 999,728 0.0272%

⚠️ 1ms 以下间隔出现少量丢包,原因不是 CAN 总线带宽不够(500kbps 下 8 字节帧约占用 250μs,理论带宽还有富余),而是节点 B 的主循环轮询速度跟不上------数据到达后来不及在下一次发送前取走。如需 0.5ms 以内的连续收发,建议使用中断 + 环形缓冲区。

测试 3:错误帧检测

人为拔掉节点 B 的 CAN_H 线 → 节点 A 的 TEC(发送错误计数)快速上升 → 约 128 次发送失败后进入 Passive Error 状态 → 256 次后进入 Bus-Off → CAN_ABOM=ENABLE 使能后自动恢复。全自动,不需要软件干预 ✓

6.3 波形验证

用示波器差分探头量 CAN_H --- CAN_L,在 500kbps 下:

  • 单个位宽度 ≈ 2.0μs(理论 2.0μs ✓)
  • 差分电压 ≈ 2.0V 峰值(正常范围 1.5~3.0V ✓)
  • 帧末尾有 ACK 位(接收节点拉低总线应答 ✓)

七、故障排查

问题 1:双机完全收不到任何数据

现象 :节点 A 发送正常(CAN_TxStatus_Ok),节点 B 的 CAN_MessagePending() 永远返回 0。

排查步骤

  1. 先用万用表测 CAN_H 对 CAN_L 的电阻------正常应该 ≈ 60Ω(两个 120Ω 并联)。
  2. 如果电阻是无穷大(>1MΩ),检查终端电阻是否焊好、CAN_H 和 CAN_L 是否接反。
  3. 如果电阻是 120Ω------说明另一端断开了或者终端电阻只加了一个。

解决:确保两个 120Ω 都在位,CAN_H 接 CAN_H、CAN_L 接 CAN_L。

问题 2:偶尔能收到,但丢包严重

现象:每 10 帧能收到 7~8 帧,随机丢 2~3 帧。

排查步骤

  1. 最常见原因:两端的 CAN 波特率不完全一致。不是说你都配了"500kbps"就一定一致------检查两个节点的晶振偏差。
  2. F103 的 HSI(内部 RC 振荡器)常温偏差 ±1%,两台之间最大偏差 +2% = 实际波特率可能差了 2%,这足够导致偶尔的位采样错误。

解决

  • 优先使用外部晶振(HSE),温度稳定性和精度远好于 HSI;
  • 如果必须用 HSI,可以适当降低波特率(如 250kbps),给时序容差留更多裕量;
  • 有条件的话用 CAN 分析仪抓波形验证实际波特率。

问题 3:CAN_Transmit 总是返回 CAN_TxStatus_NoMailBox

现象 :调用 CAN_Transmit() 后立即返回 CAN_TxStatus_NoMailBox(数值为 4)。

排查步骤

  1. CAN 外设有三个发送邮箱------如果三个都满了,第四个帧就塞不进去。
  2. 检查上一次发送是否成功退出了------如果某个邮箱一直在重发(NART=DISABLE),那它就一直被占用。
  3. 检查总线上是否有 ACK------如果没有其他节点应答,硬件会反复重发直到 TEC 溢出。

解决 :把 CAN_NART 设为 ENABLE,禁止自动重发。在 CPU 侧做 3 次重试逻辑(而非硬件无限重试),避免"一个邮箱卡死全局"。

引伸阅读:《STM32F103 CAN总线配置总结》 ------ 对每个 CAN 配置参数的含义和取值有详细解释。

问题 4:过滤器怎么配都收不到想要的 ID

现象 :配了掩码模式过滤 ID=0x200,但 CAN_MessagePending() 始终返回 0,而把过滤器设为全接收(掩码全 0)就能收到。

排查步骤 :99% 的情况是 标准帧 ID 在 32 位寄存器中的位置没移位

标准帧(11 位):

复制代码
31            21 20        3 2  1  0
├───────────────┼──────────┼──┼──┼──┤
│  STD ID[10:0] │  EXTD[17:3]  │IDE│RTR│0│
├───────────────┼──────────┼──┼──┼──┤
   CAN_FxR1       CAN_FxR2  ← 两个32位寄存器

标准 ID 的 11 位放在 bit31:21,不是 bit10:0!所以如果 ID=0x201,对应的 FilterIdHigh = 0x201 << 5 = 0x4020

扩展帧(29 位):

复制代码
31                                3 2  1  0
├─────────────────────────────────┼──┼──┼──┤
│        EXTD ID[28:0]            │IDE│RTR│0│
└─────────────────────────────────┴──┴──┴──┘

扩展 ID 放在 bit31:3,即 FilterIdHigh = (id >> 13) & 0xFFFF, FilterIdLow = ((id << 3) & 0xFFF8) | 0x04(bit2=IDE,扩展帧 IDE=1)。

解决:如果不确定,先用过滤器全接收的模式跑通通信,再逐步收紧过滤条件。一下子设一个精确 ID 这种"一步到位"的调法反而耗时更多。

问题 5:CAN_NART=ENABLE 后发送成功率下降

现象 :把 CAN_NART 改成 ENABLE 后,发送成功率从 100% 降到 95% 左右。

原因CAN_NART=ENABLE 意味着发送只尝试一次 。如果此时总线正好被其他节点占用(仲裁失败),这一帧就直接丢弃并返回 CAN_TxStatus_NoMailBox,不会排队等待。

解决:在应用层实现重试逻辑:

c 复制代码
uint8_t CAN1_SendMsg_Retry(const CanFrame_t *frame, uint8_t max_retry)
{
    uint8_t retry = 0;
    while (retry <= max_retry) {
        if (CAN1_SendMsg(frame) == 0) {
            return 0;
        }
        retry++;
        /* 等待一个位时间后重试,给总线留出间隙 */
        for (volatile uint16_t i = 0; i < 100; i++);
    }
    return 1;  /* 重试耗尽 */
}

八、设计决策回顾

为什么用标准库而不用 HAL 库?

在写这篇文章之前,我其实先用 HAL 库跑了一遍------HAL 的 HAL_CAN_AddTxMessage()HAL_CAN_GetRxMessage() 使用起来确实更"现代",但调试时遇到了一个 HAL 特有的问题:FIFO 溢出时 HAL_CAN_RxFifo0MsgPendingCallback 不会被触发 (需要单独使能 FIFO 溢出中断),而标准库的 CAN_MessagePending() 查询方式更加透明,出问题时一眼就能看到是 FIFO 空还是其他原因。

维度 HAL 库 标准库
易用性 ★★★★ 封装好 ★★★ 寄存器操作多
调试透明度 ★★★ HAL 报错码不够细 ★★★★★ 直接看 TSR/ESR 寄存器
资料丰富度 ★★★ 较新 ★★★★★ 大量成熟项目
适合场景 快速原型、学习 生产级工控项目

如果你是新项目且时间充裕,推荐 HAL 库;如果是维护老代码或追求完全透明的调试体验,标准库更合适。


九、总结

9.1 核心要点

  1. CAN 总线用差分信号 + 硬件仲裁机制实现了多主可靠通信------两根线就能组一个几十节点的网络,比串口和 SPI 高效太多;
  2. 波特率计算的关键在于确定 CAN 的实际时钟频率------F103 上 CAN 时钟可能是 36MHz 也可能是 72MHz(取决于 APB1 预分频),这是无数人掉过的坑;
  3. 过滤器有四种工作模式,日常用 32 位掩码模式(接收一个 ID 或一段范围)和 32 位列表模式(接收两个精确 ID)就够;注意标准 ID 在寄存器中左移了 21 位;
  4. CAN_NART 建议设为 ENABLE------让硬件只发一次,重试逻辑放在应用层,避免某一帧发不出去就卡死整个发送流程;
  5. 中断接收 + 环形缓冲是生产级项目的最佳实践------不要在中断里做复杂处理。

9.2 适用边界

  • 本方案适用于 125kbps~1Mbps、节点数 ≤30 的中小型 CAN 网络。超过 30 个节点需要考虑总线负载率(建议 < 50%)。
  • CAN FD(可变速率数据段,最高 8Mbps)在 STM32F103 上不支持,需要 F4/F7 或独立 CAN FD 控制器。
  • 长距离(>500m)低速率场景建议配 CAN 中继器或改用 CAN 物理层的 RS-485 替代方案。

9.3 扩展方向

  • 引入 CANopen 高层协议(基于 CAN 的 NMT 网络管理和 PDO/SDO 对象字典),让节点状态管理、参数配置和固件升级标准化;
  • 加入 CAN 总线负载率实时监控------在定时中断里统计单位时间内的总线占用比例;
  • 用 STM32 的双 CAN(F105/F107 系列有 CAN1 和 CAN2)做 CAN 网关,实现两个独立网段的报文路由转发。

如需获取本文完整工程代码和更多实战项目,可开通 CSDN 技术会员


📝 版本备注

  • 硬件平台:STM32F103C8T6 (Blue Pill) ×2 + TJA1050 模块 ×2,8MHz 外部晶振
  • 软件版本:STM32 标准外设库 V3.5.0 + Keil MDK 5.42
  • 兼容说明:本文代码适用于 STM32F103 全系列(C8/R8/RB/VE/ZC)的单 CAN 型号。双 CAN 型号(F105/F107)需额外配置 CAN2 的滤波器和中断通道(CAN2 的过滤器从编号 14 开始)。F4/F7 系列的 bxCAN 外设寄存器兼容但时钟树不同,需根据具体 APB 频率重新计算波特率。

参考链接:

  1. STM32F103 CAN总线配置与通信实战指南
  2. 避坑指南:用STM32标准库搞CAN通信,这5个配置细节千万别搞错
  3. STM32 CAN过滤器配置和应用方法介绍
  4. CAN总线通信协议的学习------STM32F103芯片CubeMX配置CAN总线参数计算及配置
  5. STM32F103 CAN总线配置总结------各参数含义详细说明
相关推荐
hsjiasb1 天前
FreeRTOS学习(二十六)——动态内存管理heap_1到heap_5
stm32·单片机·学习·学习笔记·freertos
LCG元1 天前
STM32+ESP8266+MQTT 物联网气象站:从零搭建温湿度远程监测系统(附完整源码)
stm32·物联网·struts
FakeOccupational1 天前
【电路笔记 STM32】Cortex-M7 内核上的数据缓存(D-Cache)结构+MPU+DMA&Cache+STM32CubeMX配置
笔记·stm32·缓存
殷忆枫1 天前
基于K210与STM32的智能垃圾分类与物联网监管系统
stm32·物联网·分类
周洲08301 天前
STM32 GPIO 外部中断深度解析:边沿触发 / 电平触发、NVIC 优先级配置、中断嵌套实战
stm32·单片机·嵌入式硬件
zlinear数据采集卡1 天前
D223的PWM电机控制:6路独立脉冲+加减速算法深度解析
arm开发·stm32·嵌入式硬件·算法·fpga开发·架构
Kuakewei8881 天前
平芯微42V热拔插OVP过压保护芯片:PW1600
嵌入式硬件
ACP广源盛139246256731 天前
WAIC2026 国产超节点算力浪潮下@ACP#IX9104 在算力矩阵中的定位与落地场景
大数据·数据库·人工智能·嵌入式硬件·线性代数·矩阵
z20348315201 天前
STM32单片机控制无源蜂鸣器播放《晴天》
stm32·单片机·嵌入式硬件