用 AS32X601 做一块关节电机驱动板:三相桥、电流采样、封波保护、FOC 代码一次讲透

机器人关节这个题目,我前后啃了挺久。第一次真正把它想明白,是看完一台人形关节的内部结构之后:那里头除了电机本体、减速器、磁编码器,还塞着一块巴掌大的驱动板,板子上有主控、有六颗 MOS、有采样电阻、有 CAN 收发器。当时我脑子里冒出的第一个问题不是"这板子怎么画的",而是------为什么每个关节都要配一块带 FOC 的单片机?

答案很朴素:FOC 吃算力。让一颗 MCU 同时跑七八路 FOC,电流环根本来不及算,最后每一路都是"凑合能转"。所以行业的做法是把控制权下放:每个关节里一颗主控,各自管自己的电机,外面只走两根差分线告诉它"转到哪、多快、用多大力"。

那为什么外面这两根线一定是 CAN,而不是 SPI、I2C、UART?三个理由,做过的人都认:

  1. 线少。一条总线两根线,还能挂一串关节;SPI/I2C 每个从机都要单独走线,十几自由度的人形机器人接线会失控。
  2. 关节不是纯从机。它要不停回传位置、速度、力矩、温度、故障码,这是"多主"模型,一主多从的 SPI/I2C 天然别扭。
  3. 抗扰和距离。差分传输 + 差分接收,和电机相线一起走线也扛得住;CAN FD 的数据段还能再提速。

结论就是:关节内部是"MCU + 三相桥 + 编码器"的一体化板卡,关节外部是 CAN/CANFD 总线。我这篇要写的就是前半截------怎么用 AS32X601(RISC-V,LQFP144,180MHz,带 FPU,4 路 CAN FD)把这块板子从原理图到代码做出来。


一、关节驱动板的信号流,先画一张大图

先把整块板子的信息流捋直,后面每个章节都是往这张图里填细节:

复制代码
        母线 24V / 48V
             │
    ┌────────┴────────┐
    │ 输入保护+电源树 │  TVS / 防反接 / 共模扼流
    └────────┬────────┘
             │
  ┌──────────┴─────────────────────────────────────────┐
  │  AS32X601  LQFP144 @180MHz  E7 RISC-V + FPU        │
  │                                                    │
  │  HTIM0 ─6×PWM 互补+死区─► 预驱 ─► 三相桥 ─► 电机    │
  │                                                    │
  │  ADC 注入组 ◄─差分放大◄─ 1mΩ 下桥采样电阻 ×3         │
  │  ADC 规则组 + DMA ◄─分压◄─ 母线电压                  │
  │  DAC + ACMP ─► CMPO ─► 预驱 EN/nBRAKE(硬件封波)    │
  │  ADC 模拟监控器中断 ─► ISR 写 MOE=0(软件封波)       │
  │  TIM4 编码器模式3 ◄─ABZ─ 电机侧编码器                │
  │  SPI4/5 ◄─SSI/BiSS-C─ 输出侧绝对值编码器             │
  │  CANFD ◄──► 关节总线 / 上位机 / 固件升级             │
  └────────────────────────────────────────────────────┘

信号流理清之后,方案里 90% 的引脚冲突和口径问题都能提前暴露。下面直接给结论表。


二、方案速览:先把结论甩出来

模块 设计落地 关键约束与依据
三相逆变桥 外置预驱(DRV8305 / DRV8323 / FD6288 类)+ 6×N-MOS + 自举;PWM 由 HTIM0 CH0~CH2 + CH0N~CH2N 六路互补输出 AS32X601 不集成预驱 ;互补输出与死区只有高级定时器支持
死区 HTIM0 内置 10bit DTG 死区发生器,按预驱/MOS 开关时间配 500ns~1µs MCU 与预驱双重死区取较大者,防止上下桥直通
相电流采样 1mΩ/2W 下桥采样电阻 + 差分放大 50 倍 + 1.65V 跟随器偏置;**注入组 ADC(ISQ0~ISQ3,最多 4 通道)**同步采 3 相电流 + 母线电流;力控档改外接 16bit SPI ADC 片内 ADC ENOB 8.6bit,分辨率是主要瓶颈
母线电压检测 100k/4.7k(0.1%)分压 + RC 滤波 + BAT54S 钳位 → ADC0 规则组 + DMA 单次模式 勘误:DMA 循环模式会功能异常
母线过流保护 三级:① 预驱自身 OCP/desat(ns~µs)② 片内 ACMP(DAC 8bit 阈值 + 迟滞)③ ADC 软件限幅 + HTIM MOE=0 封波 片内无硬件 Break,阈值型硬件兜底必须做在预驱侧
编码器接口 TIM4 硬件编码器模式 3(SMS=0011,四倍频) + Z 索引走 EXTI;5V 编码器必须电平转换;绝对值编码器走 SPI(SSI/BiSS-C) 电平转换与协议时序是常见返工点
实时性 10kHz 电流环(100µs),1kHz 位置环;电流环内完成采样→Clarke/Park→双 PI→SVPWM 控制带宽与 PWM 频率、ADC 节拍强相关
功能安全 可选 AS32A601(车规 AEC-Q100 Grade1 / ISO 26262 ASIL-B 体系) 与 AS32X601 软硬件高度兼容

2.1 三条前置约束,不先定下来原理图一定返工

① 片内 ADC 实测 ENOB 只有 8.6bit (2.5V 参考、1Msps,均值可再提升约 1bit),折算到 1mΩ + 50 倍增益的采样链路约 0.13~0.17A/LSB 。做位置/速度型关节够用,做力控/碰撞检测型关节必须外接 16bit SPI ADC。

② AS32X601 没有硬件 Break 。引脚复用表里没有 BKIN/BRK 外部输入引脚,Break 只能软件写寄存器触发(TIMx_EGR.BG,或 SMU_OTHCFG.timerBreak)。所谓"外部比较器直接拉 BKIN,硬件立刻封波"在 MCU 侧不成立 ,硬件封波点必须做在预驱自身。

③ ADC 基准口径要以实测为准 。设计手册写有"内置 2.5V/2.0V/1.5V",官方 FAQ 又写"没有内部参考,外部参考 2.5~3.3V",两边冲突。统一按外部 3.3V 基准(REF3033 类)+ 1.65V 偏置设计,bring-up 阶段实测确认。


三、硬件五块电路:可以照着画

3.1 三相逆变桥驱动电路

  • 拓扑采用"栅极驱动芯片 + MOS":一颗三相半桥驱动芯片 + 6 个 N 沟道 MOS 构成三相桥,器件可选 JSM6288T / FD6288 / DRV8323 类。
  • 自举电路由自举二极管 + 自举电容组成,作用是抬高上桥臂栅极驱动电压,构成上桥驱动的浮动电源。
  • 栅极驱动电阻做限流、阻尼与开关速度控制,是 MOS 安全开关的关键参数。
  • 上桥/下桥栅源之间各并一颗 10kΩ 下拉,防止预驱失电时栅极浮空导致直通。
  • 加速关断二极管(肖特基)用于加快 MOS 关断、减小开关损耗。
  • 每个半桥下臂串一颗采样功率电阻,大负载工况换更大功率规格。

设计要点 :自举电容本质是浮动电源 ------下桥导通时给 C_boot 充电,上桥导通时 C_boot 充当栅极驱动的浮动电源。选型式为 C_boot ≥ (2×Q_g + I_qbs×T_on + Q_ls) / ΔV_bs,其中 Q_g 是 MOS 栅电荷、ΔV_bs 是允许的驱动电压跌落(一般取 0.1~0.2V)。取值过小 → 驱动电压跌落导致上桥半导通发热;取值过大 → 自举充电时间常数变大,低占空比/低频时充不满。完整取值与计算见 §5.1.2。

3.2 电流采样电路

  • 每个半桥下臂 串一颗 1mΩ / 2W 采样电阻(2512 封装,Kelvin 四端子连接),采集相电流。
  • 采样电阻两端电压经差分放大电路放大 50 倍后送 MCU。
  • 用运放搭跟随器 产生 1.65V 参考电压(电阻分压 + 电压跟随),作为采样电路的偏置。
  • 采样输出到 ADC 引脚之间串 RC(1kΩ + 1nF 量级)做抗混叠,并在 ADC 脚并 BAT54S 钳位。

设计要点 :1.65V = 3.3V/2,双向电流以电源中点为零点。这一步对 AS32X601 尤其关键------片内 ADC 在低输入电压区存在非线性,偏置点选错会直接吃掉小电流区精度(详见 §5.2)。

3.3 直流母线电压检测电路

  • 把高压直流母线电压转换到 MCU 可采的 0~3.3V 模拟信号。
  • 用途:过压保护 + 电压闭环控制 + 制动能量回灌监测。

3.4 直流母线电流过流检测、保护电路

  • 最快的一层用"采样电阻 + 三极管"组合,监测 48V 母线电流。
  • 电流超阈值时 "OC" 信号被拉低,MCU 检测到 OC 低电平后执行保护动作,电机停转。
  • 更靠前的一层必须由预驱自身的 OCP / desat 承担,这一层不经过 CPU。

设计要点 :三极管阈值电路是一个低成本、低精度 的方案。优点是响应快(三极管导通是 ns 级)、成本几乎为零;缺点是阈值由 V_be 决定(约 0.6~0.7V),而 V_be 有约 −2mV/℃ 的温漂 ,在 −40~125℃ 范围内会造成约 330mV(约 55%)的阈值漂移 。折算到 50mV/A 的链路,标称 12A 的阈值实际会在 8~14.6A 之间变化。所以它只能作为最后一道兜底,不能作为唯一的过流保护。

3.5 编码器接口

  • 功能是采集电机旋转角度与转速,用于闭环控制。
  • 增量式编码器走 ABZ 正交解码(TIM4 硬件四倍频),绝对值编码器走 SPI(SSI / BiSS-C)。
  • 若要估计减速器背隙,需要电机侧 + 输出侧双编码器,两侧差值可在线估计背隙与弹性变形。
  • 电平转换、布线与抗干扰要求见 §5.5。

四、系统架构与资源分配

4.1 方案框图(文字版)

复制代码
                    24V / 48V 母线
                         │
              ┌──────────┴──────────┐
              │  输入保护 + 电源树   │  TVS / 反接 / 共模扼流
              │  12V(预驱) 3.3V(MCU) │  3.3V_A(模拟基准) 5V(编码器)
              └──────────┬──────────┘
                         │
  ┌──────────────────────┴──────────────────────────────────────┐
  │  AS32X601(LQFP144)@180MHz  E7 RISC-V + FPU                │
  │                                                             │
  │  HTIM0 ── 6×PWM(互补+DTG死区) ──► 预驱 ──► 三相桥 ──► M     │
  │                                                             │
  │  ADC0 注入组(≤4ch) ◄─ 差分放大 ◄─ 1mΩ 下桥采样电阻 ×3        │
  │      └ 同步触发源:HTIM0(零矢量窗口内采样)                  │
  │  ADC0 规则组 + DMA(单次) ◄─ 分压 ◄─ 母线电压                 │
  │                                                             │
  │  ACMP0(DAC0 8bit 阈值 + 迟滞) ◄─ 采样信号  ──► CMPO 输出      │
  │      └─► CMPO(PF11/PF12) ──► 预驱 EN/nBRAKE(级联封波)      │
  │  ADC 模拟监控器(模拟看门狗)──► 中断 ──► ISR 写 MOE=0       │
  │                                                             │
  │  TIM4 编码器模式3(4 倍频) ◄─ ABZ ──► 电机侧 20000cpr         │
  │  EXTI ◄─ Z 索引(计数复位)                                  │
  │  SPI4/5 ◄─ SSI/BiSS-C 绝对值编码器(输出侧,双编码器架构)    │
  │                                                             │
  │  CANFD ◄─► 关节总线(1kHz 位置环 + 固件升级)                 │
  │  SPI3 从机 ◄─► 可选的运动学协处理器帧格式                     │
  └─────────────────────────────────────────────────────────────┘

对应图形版见同目录 AS32X601_关节电机驱动板电路框图.svg / .png。

4.2 外设资源分配表

功能 外设 / 通道 引脚(已做冲突检查) 关键约束
三相 PWM HTIM0 CH0~CH2 + CH0N~CH2N PB11/PD9、PB12/PD10、PB13/PD11 只有 HTIM0/1/2/5 有互补输出与死区;用了 HTIM0 就不能用 SPI0(引脚完全复用)
相电流采样 ADC0 注入组 ISQ0~ISQ3 PF2 / PF3 / PF4(+ PF5 母线电流) 注入组最多 4 通道,正好 3 相 + 母线;DMA 不支持注入组
母线电压 / 温度 ADC0/1 规则组 + DMA PB0 / PB1 / PB2 DMA 必须单次模式(循环模式有 errata)
过流比较 ACMP0(DAC0 阈值)+ 迟滞 CMP0_O = PF11、CMP1_O = PF12 DAC 仅 8bit;线性区 200mV~AVD−200mV;CMPO 无中断使能位
编码器 TIM4 编码器模式 3 PC14 / PC15 SMS=0011 四倍频;通用定时器无互补/死区但有编码器模式
Z 索引 EXTI 空闲 GPIO 中断内不要做耗时运算
绝对值编码器 SPI4 / SPI5(从机或主机) 视编码器类型 从机模式 SCK ≤ PCLK/8
关节总线 CANFD PD13 / PD14(CANFD1) 4 路 CAN 全支持 CAN FD
调试 仅 JTAG PB7/PB9/PB8/PB6/PB5 无 SWD,必须预留 5 线排针

4.3 三条引脚冲突提醒(画图前先看)

  1. HTIM0 与 SPI0 完全复用 :HTIM0_CH1/CH2/CH3/CH4 = PB12/PB13/PB14/PB15,和 SPI0_CS0/SCK/MOSI/MISO 撞在一起。只要用 HTIM0 出三相 PWM,SPI0 就不可用,需要 SPI 从机接口时必须改到 SPI3。
  2. PG11 与 M2 下桥复用 :CMP0_O/CMP1_O 在 PF11/PF12(功能 1)和 PG10/PG11(功能 3)上都存在,但 PG11 同时是 HTIM1_CH0N。双三相方案里只能用 PF11/PF12,别贪那两个备用脚。
  3. 别动调试脚:仅 JTAG,没有 SWD。PB5/PB6/PB7/PB8/PB9 一旦被占,板子就变砖,只能靠 BOOT 跳线救。

五、关键电路设计:把账算清楚

5.1 三相逆变桥与栅极驱动

5.1.1 拓扑
复制代码
        VBUS
          │
   ┌──────┴──────┐
   │  0.1µF+470µF │  母线去耦(电解 + 陶瓷并联,紧贴桥臂)
   └──────┬──────┘
          │
        ┌─┴─┐  ┌─┴─┐  ┌─┴─┐
        │ Q1│  │ Q3│  │ Q5│   上桥(N-MOS)
        └─┬─┘  └─┬─┘  └─┬─┘
   PWM_U  │      │      │     ← 预驱 HO_U/HO_V/HO_W
          ├──────┼──────┤  ──► 电机 U/V/W
   PWM_UL │      │      │     ← 预驱 LO_U/LO_V/LO_W
        ┌─┴─┐  ┌─┴─┐  ┌─┴─┐
        │ Q2│  │ Q4│  │ Q6│   下桥(N-MOS)
        └─┬─┘  └─┬─┘  └─┬─┘
          │      │      │
        1mΩ    1mΩ    1mΩ     ← 下桥采样电阻(Kelvin 连接)
          │      │      │
         GND    GND    GND
5.1.2 关键参数计算

(1)自举电容

复制代码
C_boot ≥ (2 × Q_g + I_qbs × T_on_max + Q_ls) / ΔV_bs
符号 含义 典型取值
Q_g MOS 栅极总电荷 按选定 MOS 手册,如 30~60nC
I_qbs 预驱上桥驱动静态电流 预驱手册,如 100µA
T_on_max 上桥最长连续导通时间 由最大占空比与 PWM 周期决定
ΔV_bs 允许的自举电压跌落 0.1~0.2V

工程取法:算完再乘以 2~3 倍余量 ,选用 100nF~1µF 的 X7R 陶瓷(靠近预驱 VB/VS 引脚);自举二极管选快恢复或肖特基(反向恢复电荷小,否则高频下自举电压会被抽掉)。

(2)栅极驱动电阻

复制代码
R_g,on  ≈ (V_drive − V_plateau) / I_peak_desired      (开通,控制 dv/dt 与 EMI)
R_g,off ≈ R_g,on / 2 ~ R_g,on / 5                     (关断,加速、抑制直通)

配合加速关断二极管:开通走 R_g,关断走 D + 小电阻,可以让开通慢(降 EMI)、关断快(降开关损耗与直通风险)。MOS 栅极还要并一颗 10kΩ 下拉到源极,防止预驱失电时栅极浮空导致直通。

(3)死区时间

AS32X601 高级定时器内置 10bit DTG 死区发生器,DT 与 DTG 的关系(以设计手册 TIMx_BDTR 与 CKD 字段为准):

复制代码
DTG[7:5] = 0xx  →  DT = DTG[7:0] × t_DTS
t_DTS = t_CK_INT × 2^(CKD)

以 CK_INT = 45MHz(t_DTS ≈ 22.2ns)为例:DTG = 0x17(23)→ DT ≈ 511ns。

取值原则 :MCU 死区 = 预驱死区与 MOS 开关时间之和再留 1.5~2 倍余量。MCU 与预驱的死区是"取大者"关系,两边都设更安全,但会损失低压调制区的有效电压(占空比丢失),在低电压母线(如 24V 系统)尤其要核算。

5.1.3 这一块的三个坑
  1. 只用通用定时器做三相 PWM :TIM3/4/6/7 不支持互补输出与死区插入,必须用 HTIM0/1/2/5。一块板子最多 4 路三相(受高级定时器数量限制)。
  2. 母线去耦电容离桥臂太远:换流回路面积直接决定电压尖峰与 EMI,陶瓷电容必须紧贴桥臂与地平面。
  3. 未加母线泄放与制动路径:电机快速减速时能量回灌,母线电压会抬升,需评估制动斩波或电容耐压余量(≥1.5× 母线额定)。

5.2 相电流采样电路(本方案的重点)

5.2.1 采样链路
复制代码
下桥 MOS ──► 1mΩ(1W/2W, Kelvin 四端子) ──► 差分放大(50V/V) ──► RC 抗混叠 ──► ADC 注入组
                     │                              ▲
                     └── 共模 = 地电位附近            │
                                             VREF/2 偏置(跟随器)
5.2.2 分辨率账(决定方案档位)

额定 12A、峰值 20A、采样电阻 1mΩ、增益 50V/V 时:

量 计算 结果
采样链路增益 1mΩ × 50 50mV/A
额定电流输出 12A × 50mV/A ±0.6V
峰值电流输出 20A × 50mV/A ±1.0V
采样电阻功耗(额定) 12² × 1mΩ 144mW
采样电阻功耗(峰值 20A) 20² × 1mΩ 400mW
采样电阻功耗(过流 30A/10ms) 30² × 1mΩ 900mW(短时可承受)

结论 :1mΩ/1W 在 12A 额定下功耗 144mW,本身很安全;但 30A 过流时瞬时 900mW 已接近额定,规格应升到 2W(2512 封装),或改用 0.5mΩ + 100 倍增益(噪声会被同步放大,不推荐)。

ADC 侧的分辨率:

条件 满量程码数 折算 LSB 电压 折算电流分辨率
理想 12bit @3.3V 参考 4096 0.806mV 16mA
实测 ENOB 8.6bit @2.5V 参考 ≈388 6.4mV ≈130mA
实测 ENOB + 均值算法(+1bit) ≈776 3.2mV ≈65mA
外接 16bit ADC(如 ADS8354/ADS9224) 65536 0.05mV(3.3V 参考) <2mA

这是整块板子最需要提前说清楚的一条 :片内 ADC 在 1mΩ + 50 倍链路下,实际电流分辨率是百毫安级 ,约 12A 满量程的 1% FS。做位置环/速度环足够;做力控、碰撞检测、力矩标定时,100mA 的量化台阶会直接变成力矩纹波,两条路选一条:

  • 方案 A(低成本档):片内 ADC + 多点标定 + 均值(带宽牺牲到 ~1kHz)
  • 方案 B(力控档):外接 16bit SPI 同步采样 ADC(推荐)
5.2.3 偏置点与芯片非线性区的配合

偏置点取 VREF 3.3V 的中点 1.65V,但 AS32X601 必须多做一步核算:

参考电压 偏置点 ±12A 范围 ±20A 范围 评价
3.3V 1.65V 1.05~2.25V 0.65~2.65V 推荐:远离低电压非线性区,且留足过流量程
2.5V 1.25V 0.65~1.85V 0.25~2.25V 不推荐:±20A 时下端 0.25V 落入低电压非线性区

依据 :台架实测口径为"ADC 在 0.1~0.4V 输入区间偏差 10%90%,0.51.8V 偏差 <5%,1.9~2.4V 约 5% "。因此偏置点要选在 1.2~1.65V、且双向摆幅不要进入 0.1~0.4V。

注意 :基准口径本身存在冲突(见 §2.1 ③)。按外部 3.3V 基准 + 1.65V 偏置设计,并在 bring-up 阶段用精密源扫描 0~3.3V 实测确认非线性区(列为待确认项,见 §11)。

5.2.4 偏置电路的三点补充
  1. 偏置源阻抗必须低 :差分放大器的 REF 引脚若直接接分压电阻(如 10k/10k),会因 REF 输入电流与电阻失配引入增益/失调误差,必须用跟随器隔离。
  2. 偏置与 VREF 同源 :偏置分压与 ADC 基准取同一路 3.3V_A,这样基准漂移是共模的(比值式测量),否则温漂会直接变成零点漂移。
  3. 偏置电容:跟随器输出加 100nF + 1µF,降低开关噪声耦合。
5.2.5 采样时刻与窗口(低边采样的硬约束)

低边三电阻采样只能在下桥全部导通的零矢量窗口内采样。 工程上必须做两件事:

  1. 确认触发相位:中心对齐 PWM 一个周期内有两次零矢量,具体落点由计数方向、输出极性、重复计数器共同决定。不要照抄别人的注释,用示波器实测:固定 50% 占空比,用 GPIO 在 ADC 触发时刻翻转,观察该跳变是否落在下桥 V_DS 的低电平平台中部。
  2. 核算最小导通窗口 :高调制比时下桥导通时间被压缩,若小于"ADC 采样保持时间 + 死区",就会采到畸变值。对策:
    • 设置最小脉宽限制(限制最大调制比)
    • 或改用"采两相电流 + 第三相由 KCL 重构",并动态选择可采的两相

文档级提醒:AS32X601 注入组 最多 4 通道 (ISQ0~ISQ3),刚好容纳"3 相电流 + 1 路母线电流",这是这颗芯片在关节电机场景下比较讨巧的资源匹配点;但注入组不支持 DMA,必须由 ADC 中断读取(4 个结果寄存器),中断服务要尽量短。

5.3 直流母线电压检测

5.3.1 分压设计

以 48V 母线、量程留到 72V 为例:

复制代码
R_top = 100kΩ (0.1%)   R_bot = 4.7kΩ (0.1%)
比例 k = 4.7 / (100 + 4.7) = 0.04489
母线电压 ADC 输入 说明
24V 1.08V 24V 系统工作点
48V 2.16V 额定工作点
60V 2.69V 过压告警阈值
72V 3.23V 满量程附近(保护上限)

分压支路电流 ≈ 48V / 104.7kΩ ≈ 0.46mA,功耗 ≈ 22mW,可接受。

5.3.2 滤波与保护
  • RC 滤波:串 1kΩ + 并 100nF → 截止频率 ≈ 1.6kHz,兼顾响应速度与开关噪声抑制。
  • 钳位:输入端并 BAT54S(双肖特基,分别钳到 3.3V 与 GND),或串 10kΩ + 3.3V 稳压管,防止母线浪涌通过分压电容耦合进 ADC 引脚。
  • 源阻抗 :分压等效输出阻抗 = 100k∥4.7k ≈ 4.5kΩ,属于偏高。必须延长 ADC 采样保持时间,或在前端加一级跟随器;否则采样电容充不满会引入与电流采样同步的系统误差(采样时间按手册模型核算,列为待确认项)。
  • ADC 配置 :规则组 + DMA 单次模式(勘误明确 DMA 循环模式会导致功能异常,包括 ADC)。

5.4 母线过流检测与保护(安全等级最高的一块)

5.4.1 低成本三极管阈值电路的适用边界

电路形态:采样电阻 + 三极管,母线电流超过阈值时 OC 被拉低,MCU 检测到低电平后执行停机。

维度 评价
成本 极低(一颗三极管 + 两个电阻)
响应 快(三极管导通为 ns 级)
阈值精度 差:阈值 = V_be ≈ 0.6~0.7V,温漂 −2mV/℃
全温区漂移 −40~125℃ 范围内约 330mV,折算到 50mV/A 链路约 ±3.3A
适用定位 兜底,不能作为唯一保护

一句话表述:"这颗三极管不是精度方案,它是最后一道保险丝式的兜底,别指望它精确在 12A 动作。"

5.4.2 三级保护链
层级 实现 响应时间 作用
L1 硬件级 预驱自身 OCP / desat / 短路保护(DRV8305、DRV8323 类) ns~µs 真正的硬件兜底,独立于 MCU 状态机
L2 硬件+外设级 片内 ACMP (DAC 8bit 阈值 + 迟滞)比较采样信号,CMPO 输出直连预驱 EN / nBRAKE µs 级,不经 CPU 阈值可软件设定、带迟滞,抗噪声优于三极管
L3 软件级 ADC 模拟监控器(模拟看门狗)中断 或 ADC 中断内软件判据 → ISR 写 MOE=0 封波 数 µs(含中断延迟) 可做有效值、积分、温升等复杂判据
5.4.3 片内 ACMP 的具体用法与边界

依据设计手册 DAC/CMP 章节(DAC0 基地址 0x4100_3000,DAC1 基地址 0x4200_2000):

特性 手册口径 对方案的影响
数量 2 个模拟比较器(ACMP) 可一路做过流、一路做母线过压/欠压
正输入选择 CMP_MD:0 = DACO(片内 DAC 阈值),1 = CMP_P(外部引脚) 可用 DAC 阈值 + 外部采样信号做阈值比较
迟滞 CMP_HYS:0 / 3.3 / 16.5 / 33 / 66 / 165 / 264 / 330mV 建议取 33~66mV,抑制开关噪声误触发
结果读取 CMPO(只读) 该寄存器没有中断使能位,比较结果需要软件轮询,或经 CMPO 引脚接 GPIO/EXTI
DAC 分辨率 8bit,Vout = VREFP_I / 255 × DOUT 阈值步进:2.5V 时 ≈9.8mV(折算电流 ≈0.2A @50mV/A)
DAC 线性区 200mV ~ AVD−200mV 阈值不要设在 0.2V 以下
DAC 时钟 工作时钟需低于 5MHz;输出更新间隔 ≥400ns(5MHz 时) 阈值不需要高速更新,够用

口径说明(重要) :"ACMP 中断 → ISR 写 MOE=0 "这个说法在手册上不成立 ------DAC/CMP 章节只提供 CMPO 只读结果位与迟滞档位,没有比较器中断使能位。可用的两条路径是:

  1. CMPO 引脚输出 → 预驱 EN / nBRAKE (硬件级联,推荐)。复用表中已有专用输出:CMP0_O=PF11(104)/功能1、CMP1_O=PF12(102)/功能1;PG10(功能3)/PG11(功能3) 上也有同一对输出,但 PG11 与本方案 M2 下桥 HTIM1_CH0N 复用 ,故只用 PF11/PF12。待确认的是直驱预驱 EN 的电平/极性/上电时序与失效模式,而不是引脚是否存在;
  2. ADC 模拟监控器(模拟看门狗)中断 → ISR 写 MOE=0(软件路径,依赖 ADC 转换节拍)。
5.4.4 MCU 侧封波的硬约束
  • AS32X601 没有硬件 Break 输入 :SMU_OTHCFG.timerBreak"完全由软件控制",引脚复用表中没有 BKIN/BRK 外部输入引脚 。定时器内部有 Break/断路逻辑框架(TIMx_BDTR.BKE、Break 时 MOE 异步清零、TIMx_EGR.BG 软件触发),但没有外部硬件触发源。
  • 因此:不要在对外材料里承诺"外部比较器直接硬件封波" 。硬件封波点必须在预驱内部;MCU 侧的封波时间 = 中断延迟 + 寄存器写入,实际值必须实测(建议用 GPIO 翻转 + 示波器测 ISR 入口到 PWM 输出关断的时间)。
  • 封波后必须锁存故障状态并要求显式复位,禁止自动重试开通(堵转/短路场景下自动重试会炸管)。

5.5 编码器接口

5.5.1 增量式 ABZ
项目 方案
正交解码 TIM4 编码器模式 3 (SMS=0011),在 TI1FP1 与 TI2FP2 双边沿计数 → 四倍频
分辨率 5000 PPR × 4 = 20000 counts/rev
Z 索引 独立 GPIO + EXTI 中断,读取计数值并复位(或记录零位偏移)
电平 5V 编码器输出必须转换到 3.3V (74LVC8T245 或 1k/2k 分压 + 施密特整形);AS32X601 为 3.3V 域,不要按 5V 容忍设计
差分输出编码器 加 RS-422 接收器(AM26LV32 / DS26LV32 类),不要单端直连
5.5.2 输出侧绝对值编码器

若关节使用摆线/行星减速(背隙与传动误差无法忽略),电机侧单编码器不够,需要输出侧绝对值编码器:

  • 接口形态:SSI / BiSS-C 走 SPI,或使用专用转换芯片;
  • SPI 从机模式:SCK ≤ PCLK/8(核对口径为 ≤5.625MHz,工程上取 4MHz 或更低);
  • 分工:电流环用电角度取电机侧 ABZ;位置/速度环用输出侧绝对值(真实关节角);两侧差值可用于在线估计背隙与弹性变形。
5.5.3 布线与抗干扰
  1. AB 相走双绞 + 屏蔽 ,屏蔽层单点接地(编码器侧或板侧,不要两端接)。
  2. 与电机相线分开走线,禁止并行长距离;交叉时垂直交叉。
  3. 编码器引脚串 100Ω + 并 100pF(转折频率 ~16MHz),滤掉开关噪声但保留计数边沿。
  4. 编码器供电独立 LDO + π 滤波,避免与 MOS 开关环路共地噪声。
  5. 上电初始化流程要明确:编码器方向标定 → 电角度对齐(ALIGN,强制 d 轴电流)→ 零位写入 D-Flash。

六、代码实操:从时钟一路写到 FOC 中断

这一节的每一行都拿 SDK 源码比对过:函数名、枚举名、结构体字段名都是现成的,可以直接往工程里搬。只有两处 SDK 自带例程本身有瑕疵,我在正文里点名。

6.1 系统时钟与总线时钟:先开总线,再配 PLL

c 复制代码
void Systemclock_Init(void)
{
    /* ① 先开总线级时钟:
     *    用定时器  → APB0(AXI4TOAPB0 + APBBUS0)
     *    用中断    → PLIC
     *    用延时    → CLINT
     *    用 eflash → AXILITEBUS2
     */
    AXIBUS3_CLK_ENABLE();
    AXI4TOAPB0_CLK_ENABLE();
    APBBUS0_CLK_ENABLE();
    AXI4TOAPB1_CLK_ENABLE();
    APBBUS1_CLK_ENABLE();
    AXILITEBUS1_CLK_ENABLE();
    AXILITEBUS2_CLK_ENABLE();
    EFLASH_CLK_ENABLE();
    PLIC_CLK_ENABLE();
    CLINT_CLK_ENABLE();

    SMU_PLLInitTypeDef   SMU_PLLInitStruct;
    SMU_ClockInitTypeDef SMU_ClockInitStruct;

    /* ② PLL 参数:DivN=0x14(20)、DivF=0xA0(160)、DivR=DivQ=0x01
     *    中间频率 = 20MHz / 20 = 1MHz(要求 0.95~2.1MHz,正好落在区间内)
     *    VCO      = 1MHz × 160 = 160MHz
     *    core     = VCO / Q = 160MHz
     *    想要 180MHz,需要把 DivF 提到 180 并同步改 FLASH_SetCLKFreq 的实参,
     *    上限以芯片手册的频率章节为准(待实测确认)
     */
    SMU_PLLInitStruct.OscillatorType       = SMU_OSCILLATORTYPE_OSC;
    SMU_PLLInitStruct.FIRCOscState         = DISABLE;   /* FIRC 未校准,不做系统时钟 */
    SMU_PLLInitStruct.FIRCCalibrationValue = 0x00;
    SMU_PLLInitStruct.PLLConfig.PLLState   = ENABLE;
    SMU_PLLInitStruct.PLLConfig.PLLSource  = SMU_PLLCLK_OSC;
    SMU_PLLInitStruct.PLLConfig.PLLDivR    = 0x01;
    SMU_PLLInitStruct.PLLConfig.PLLDivQ    = 0x01;
    SMU_PLLInitStruct.PLLConfig.PLLDivN    = 0x14;
    SMU_PLLInitStruct.PLLConfig.PLLDivF    = 0xA0;
    SMU_PLLInit(&SMU_PLLInitStruct);

    /* ③ Flash 时序必须跟着主频一起改,漏掉这一步高频取指会出错 */
    FLASH_UnlockCtrl();
    FLASH_SetCLKFreq(0xA0);

    /* ④ 分频:APB0 与 AXI4Bus3 同频(定时器计数时钟);APB1 走 1/8
     *    APB0 允许 4~45MHz,所以 AXI4Bus3 不能直接给 160MHz
     */
    SMU_ClockInitStruct.SYSCLKSelect    = SMU_SYSCLK_PLL;
    SMU_ClockInitStruct.AXI4Bus3CLKDiv  = AXI4Bus3CLKDiv1;
    SMU_ClockInitStruct.APBBus0CLKDiv   = APBBus0CLKDiv1;
    SMU_ClockInitStruct.APBBus1CLKDiv   = APBBus1CLKDiv8;
    SMU_ClockInitStruct.CANX2CLKDiv     = CANX2CLKDiv1;
    SMU_ClockInit(&SMU_ClockInitStruct);

    EFLASH_CLK_UPDATE_ENABLE();
    EFLASH_CLK_UPDATE_DISABLE();
    FLASH_LockCtrl();

    /* ⑤ 把实际频率读回来,后面算 PWM 周期、波特率都用它,不要手写常数 */
    SMU_GetClocksFreq(&SMU_ClocksStruct);
}

调用顺序上,Systemclock_Init() 之后要立刻 delay_init(SMU_ClocksStruct.AXIBus0_Frequency / 1000000),让软件延时跟实际主频对齐,否则 delay_ms 会整体偏快或偏慢。

6.2 三相 PWM:TIM_BDTRConfig 和 TIM_CtrlPWMOutputs 必须自己补

SDK 的 10_tim_PWM 例程只配了时基和输出比较,没配 BDTR,也没打开主输出。照抄的结果是波形出不来、或者死区为 0。要出三相互补带死区的 PWM,至少得有下面这一整套:

c 复制代码
/* arr=200、psc=0 → PWM 频率 = f_TIM0 / (200+1),按 45MHz 的 APB0 时钟算约 224kHz
 * 关节电机实际取 20kHz:按 45MHz 算,arr ≈ 45M/20k - 1 = 2249
 */
#define PWM_ARR       2249U
#define PWM_DEADTIME  0x17U    /* 见下文死区换算 */

void User_TIM0_Config(uint32_t arr, uint32_t psc, uint32_t rcr)
{
    HTIM0_CLK_ENABLE();

    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_OCInitTypeDef       TIM_OC_InitStructure;
    TIM_BDTRInitTypeDef     TIM_BDTRInitStructure;

    TIM_TimeBaseStructInit(&TIM_TimeBaseInitStructure);
    TIM_TimeBaseInitStructure.TIM_CounterMode       = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_ClockDivision     = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_Period            = arr;
    TIM_TimeBaseInitStructure.TIM_Prescaler         = psc;
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = rcr;
    TIM_TimeBaseInit(TIM0, &TIM_TimeBaseInitStructure);

    /* 六路输出:CH0~CH2 与 CH0N~CH2N,正好驱动三相桥的上下桥 */
    TIM_OCStructInit(&TIM_OC_InitStructure);
    TIM_OC_InitStructure.TIM_OCMode         = TIM_OCMode_PWM1;
    TIM_OC_InitStructure.TIM_OutputState    = TIM_OutputState_Enable;
    TIM_OC_InitStructure.TIM_OutputNState   = TIM_OutputNState_Enable;   /* 互补通道 */
    TIM_OC_InitStructure.TIM_Pulse          = arr / 2;                   /* 上电先给 50% */
    TIM_OC_InitStructure.TIM_OCPolarity     = TIM_OCPolarity_High;
    TIM_OC_InitStructure.TIM_OCNPolarity    = TIM_OCNPolarity_High;
    TIM_OC_InitStructure.TIM_OCIdleState    = TIM_OCIdleState_Set;       /* 空闲时上桥关 */
    TIM_OC_InitStructure.TIM_OCNIdleState   = TIM_OCNIdleState_Reset;    /* 空闲时下桥关 */
    TIM_OC1Init(TIM0, &TIM_OC_InitStructure);
    TIM_OC2Init(TIM0, &TIM_OC_InitStructure);
    TIM_OC3Init(TIM0, &TIM_OC_InitStructure);

    /* ★ 关键:BDTR 决定死区、空闲电平和 Break 行为,SDK 例程里缺的就是这一段 */
    TIM_BDTRStructInit(&TIM_BDTRInitStructure);
    TIM_BDTRInitStructure.TIM_OSSRState       = TIM_OSSRState_Enable;   /* 运行态关断时输出空闲电平 */
    TIM_BDTRInitStructure.TIM_OSSIState       = TIM_OSSIState_Enable;   /* 空闲态也保持该电平 */
    TIM_BDTRInitStructure.TIM_LOCKLevel       = TIM_LOCKLevel_OFF;      /* 调试阶段别锁,量产可上 LOCKLevel_1 */
    TIM_BDTRInitStructure.TIM_DeadTime        = PWM_DEADTIME;
    TIM_BDTRInitStructure.TIM_Break           = TIM_Break_Disable;      /* 没有 BKIN 引脚,这里只能关 */
    TIM_BDTRInitStructure.TIM_BreakPolarity   = TIM_BreakPolarity_Low;
    TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
    TIM_BDTRConfig(TIM0, &TIM_BDTRInitStructure);

    TIM_Cmd(TIM0, ENABLE);

    /* ★ 关键:MOE(主输出使能)不开,CHxN 永远不动 */
    TIM_CtrlPWMOutputs(TIM0, ENABLE);
}

死区时间的换算关系(CK_INT 为定时器计数时钟,CKD 为 TIM_CKD_DIV1 即不分频):

c 复制代码
DTG[7:5] == 0xx  →  DT = DTG[7:0] × t_DTS
t_DTS = t_CK_INT × 2^CKD

按 CK_INT = 45MHz、t_DTS ≈ 22.2ns 算,TIM_DeadTime = 0x17(23)→ DT ≈ 511ns ,正好覆盖大多数 40V/60V 级 MOS 加预驱的死区需求。这个值必须和预驱自身死区一起看:两边叠加会吃掉低压调制区的有效占空比,24V 母线系统尤其明显。

6.3 相电流:注入组一次抓三路

注入组最多 4 个通道(ADC_ISEQ_0 ~ ADC_ISEQ_3),正好放三相电流 + 母线电流。注意注入组不支持 DMA,必须用结果寄存器直接把值读走。

c 复制代码
void User_ADC0_Inject_Init(void)
{
    ADC_InitTypeDef      ADC_InitStructure;
    ADC_ChannelTypeDef   ADC_ChannelStructure;
    GPIO_InitTypeDef     GPIO_InitStructure;

    ADC0_CLK_ENABLE();
    GPIOF_CLK_ENABLE();

    /* 模拟输入脚:不要开上下拉,也不要挂数字输出 */
    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
    GPIO_Init(GPIOF, &GPIO_InitStructure);

    ADC_StructInit(&ADC_InitStructure);
    ADC_InitStructure.ADC_ClockDivide        = ADC_CLK_DIVIDE_4;
    ADC_InitStructure.ADC_Resolution         = ADC_RESOLUTION_12BIT;
    ADC_InitStructure.ADC_Alignment          = ADC_DATA_ALIGN_RIGHT;
    ADC_InitStructure.ADC_Symbol             = ADC_DATA_SELDO_UNSIGNED;

    /* 注入组触发源:软件触发先用 INTERNAL 打通链路,
     * 要交给 HTIM0 定时触发,需要按手册 TRIGSRC 字段选具体定时器源(SDK 枚举只暴露了 EXTERNAL_0/1/2,属待确认项)
     */
    ADC_InitStructure.ADC_RegularTrigger     = ADC_TRIGGER_INTERNAL;
    ADC_InitStructure.ADC_InjectTrigger      = ADC_TRIGGER_INTERNAL;

    ADC_InitStructure.ADC_RegularEnable      = DISABLE;   /* 规则组留给母线电压,这里先关 */
    ADC_InitStructure.ADC_InjectEnable       = ENABLE;
    ADC_InitStructure.ADC_DmaEnable          = DISABLE;   /* 注入组本来就不走 DMA */
    ADC_InitStructure.ADC_ScanModeEn         = ENABLE;
    ADC_InitStructure.ADC_ContinuousModeEn   = DISABLE;   /* 电流环要跟随 PWM 节拍,不能自由跑 */
    ADC_InitStructure.ADC_RegularDiscontinuousModeEn = DISABLE;
    ADC_InitStructure.ADC_InjectDiscontinuousModeEn  = DISABLE;
    ADC_InitStructure.ADC_InjectAutoModeEn   = ENABLE;
    ADC_InitStructure.ADC_IntervalModeEn     = DISABLE;
    ADC_InitStructure.ADC_RegularSequenceLength = 0;
    ADC_InitStructure.ADC_InjectSequenceLength  = 3;      /* 三相电流;含母线电流则写 4 */
    ADC_InitStructure.ADC_PowerEn            = ENABLE;
    ADC_InitStructure.ADC_PowerRegain        = ENABLE;
    ADC_Init(ADC0, &ADC_InitStructure);

    /* 采样时间先给足:分压源阻抗高、运放带容性负载都需要更长采样窗口 */
    ADC_ChannelStructure.ADC_SampleTime   = ADC_SPT_CLK_30;
    ADC_ChannelStructure.ADC_InterruptEn  = DISABLE;

    ADC_ChannelStructure.ADC_Channel = ADC_CH_2;
    ADC_ConfigChannel(ADC0, ADC_ISEQ_0, &ADC_ChannelStructure);
    ADC_ChannelStructure.ADC_Channel = ADC_CH_3;
    ADC_ConfigChannel(ADC0, ADC_ISEQ_1, &ADC_ChannelStructure);
    ADC_ChannelStructure.ADC_Channel = ADC_CH_4;
    ADC_ConfigChannel(ADC0, ADC_ISEQ_2, &ADC_ChannelStructure);
}

/* 电流环里读三路结果:先软触发,再逐个查 EOC */
static inline void ADC_Read_PhaseCurrent(uint16_t *iu, uint16_t *iv, uint16_t *iw)
{
    ADC_SoftInjectConvert(ADC0);
    while (ADC_GetFlagStatus(ADC0, ADC_FLAG_EOC, ADC_ISEQ_0) == RESET) { }
    ADC_GetSeqResult(ADC0, ADC_ISEQ_0, iu);
    while (ADC_GetFlagStatus(ADC0, ADC_FLAG_EOC, ADC_ISEQ_1) == RESET) { }
    ADC_GetSeqResult(ADC0, ADC_ISEQ_1, iv);
    while (ADC_GetFlagStatus(ADC0, ADC_FLAG_EOC, ADC_ISEQ_2) == RESET) { }
    ADC_GetSeqResult(ADC0, ADC_ISEQ_2, iw);
}

采样时刻的硬约束在 §5.2.5 讲过了:低边三电阻只能在下桥导通时采 。软触发这一步在真实工程里必须换成定时器触发、并把触发点放在零矢量窗口内,while 轮询也要换成中断或状态机,不能占着电流环的时间片。

6.4 过流封波:ADC 模拟监控器中断里关 MOE

没有 BKIN 引脚,MCU 侧唯一能自动响应的路径就是 ADC 模拟监控器(AMO,俗称模拟看门狗)中断:超阈值立刻进 ISR,ISR 里把 MOE 拉掉。

c 复制代码
void User_ADC0_AMO_Init(void)
{
    ADC_AmoTypeDef ADC_AmoStructure;

    /* ADC 的 AMO 结构体没有 StructInit 版本,字段要一个个赋 */
    ADC_AmoStructure.ADC_AmoInterruptEn    = ENABLE;
    ADC_AmoStructure.ADC_AmoRegularEn      = DISABLE;
    ADC_AmoStructure.ADC_AmoInjectEn       = ENABLE;     /* 只盯注入组(相电流) */
    ADC_AmoStructure.ADC_AmoSingleModeEn   = ENABLE;     /* 单通道模式,只看指定那一路 */
    ADC_AmoStructure.ADC_AmoSingleChannel  = ADC_CH_2;
    ADC_AmoStructure.ADC_AmoUpThreshold    = 3300;       /* 12bit 满量程附近,按 1.65V±Δ 折算 */
    ADC_AmoStructure.ADC_AmoLowThreshold   = 200;
    ADC_ConfigAMO(ADC0, &ADC_AmoStructure);

    PLIC_InitTypeDef PLIC_InitStructure;
    PLIC_StructInit(&PLIC_InitStructure);
    PLIC_InitStructure.PLIC_IRQChannel    = ADC0_IRQn;
    PLIC_InitStructure.PLIC_IRQPriority   = 1;           /* 比 CANFD 高,故障优先 */
    PLIC_InitStructure.PLIC_IRQChannelCmd = ENABLE;
    PLIC_Init(&PLIC_InitStructure);
}

volatile uint8_t g_fault_latch = 0;

void ADC0_IRQ_Handler(void)
{
    if (ADC_GetFlagStatus(ADC0, ADC_FLAG_AMO, ADC_ISEQ_0) != RESET)
    {
        /* ★ 一行封波:MOE=0,六路输出同时进空闲电平 */
        TIM_CtrlPWMOutputs(TIM0, DISABLE);

        /* 锁存故障,禁止自动恢复;必须由上层显式复位才允许重新使能 */
        g_fault_latch = 1;

        ADC_ClearFlag(ADC0, ADC_FLAG_AMO);
    }
}

这里有两个必须记住的点:

  1. TIM_CtrlPWMOutputs(TIM0, DISABLE) 清的是 MOE 位 ,效果是六路输出立刻回到 TIM_OCIdleState / TIM_OCNIdleState 设定的空闲电平。前面把上桥空闲电平设成 Set、下桥设成 Reset,此刻六颗 MOS 全部关断,这是安全态。
  2. 封波之后不要自动重试。ISR 里只做关断和置标志,恢复必须由上层显式判断(且要判次数、判间隔)。堵转和短路场景下自动重开会炸管。

6.5 阈值比较:DAC 出阈值,比较器出结果

硬件兜底那一层的阈值由片内 DAC 给(8bit),比较器结果通过 CMPO 引脚直连预驱的 EN/nBRAKE。

c 复制代码
void User_DAC0_CMP_Init(void)
{
    DAC_InitTypeDef  DAC_InitStructure;
    DAC_CMPTypedef   DAC_CMPStructure;

    DAC0_CLK_ENABLE();

    /* 初始化结构体一律先 StructInit,再逐字段覆盖 */
    DAC_StructInit(&DAC_InitStructure);
    DAC_InitStructure.DAC_Enable       = ENABLE;
    DAC_InitStructure.DAC_Mode         = DAC_MD_BUFFER_OUT;    /* 带输出缓冲 */
    DAC_InitStructure.DAC_VrefMode     = DAC_VREF_MD_VREFP;    /* 与 ADC 同源,保证阈值可折算 */
    DAC_InitStructure.DAC_ClockDivider = 0x1E;                 /* 工作时钟须低于 5MHz */
    DAC_Init(DAC0, &DAC_InitStructure);
    DAC_Cmd(DAC0, ENABLE);

    DAC_CMP_StructInit(&DAC_CMPStructure);
    DAC_CMPStructure.CMP_Enable     = DAC_CMP_ENABLE;
    DAC_CMPStructure.CMP_Mode       = DAC_CMP_MD_DAC0;         /* 内部 DAC 当阈值源 */
    DAC_CMPStructure.CMP_Hysteresis = DAC_CMP_HYS_66;          /* 66mV 迟滞,抗开关噪声误触发 */
    DAC_CMP_Init(DAC0, &DAC_CMPStructure);
    DAC_CMP_Cmd(DAC0, ENABLE);

    /* 设定过流阈值:DOUT = Vout × 255 / VREFP。按 50mV/A 折算,
     * Vout = 1.65V + I × 0.05V/A 的反向分支需要软件算好 DOUT
     */
    DAC_SetValue(DAC0, 200);
}

几个容易踩的地方:

  • DAC 线性区是 200mV ~ AVD−200mV,阈值不要设到 0.2V 以下,超出这个区间输出不线性,阈值就没有意义。
  • 迟滞一共 7 档 :0 / 3.3 / 16.5 / 33 / 66 / 165 / 330mV。关节里开关噪声大,建议从 33~66mV 起步。
  • CMPO 是只读结果位,没有中断使能位,别指望它给 MCU 发中断。它的正确用法是引脚输出,走硬件。
  • SDK 版本差异 :14_DAC 例程里写的是 DAC_ResetEN,而头文件定义的结构体字段是 DAC_Enable。迁移代码时以手头 SDK 的头文件为准,不然编译不过。

6.6 编码器:TIM4 硬件四倍频

通用定时器没有互补输出,但编码器模式是有的,而且硬件四倍频,不占 CPU。

c 复制代码
void User_TIM4_Encoder_Init(void)
{
    GPIOC_CLK_ENABLE();
    TIM4_CLK_ENABLE();

    GPIO_InitTypeDef  GPIO_InitStructure;
    TIM_ICInitTypeDef TIM_ICInitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;

    /* PC14 / PC15 作 TI1 / TI2 输入 */
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource14, GPIO_AF_TIM4);
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource15, GPIO_AF_TIM4);
    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin     = GPIO_Pin_14 | GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Mode    = GPIO_Mode_IN;
    GPIO_InitStructure.GPIO_IType   = GPIO_IPU;        /* 编码器多为集电极开路输出,需要上拉 */
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    /* 时基按 16bit 自由计数即可,编码器模式会自己按方向加减 */
    TIM_TimeBaseStructInit(&TIM_TimeBaseInitStructure);
    TIM_TimeBaseInitStructure.TIM_CounterMode   = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period        = 0xFFFF;
    TIM_TimeBaseInitStructure.TIM_Prescaler     = 0;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseInitStructure);

    /* 输入滤波:0x0F 是最强档,关节内噪声大时值得开 */
    TIM_ICStructInit(&TIM_ICInitStructure);
    TIM_ICInitStructure.TIM_Channel     = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICPolarity  = TIM_ICPolarity_Rising;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICFilter    = 0x0F;
    TIM_ICInit(TIM4, &TIM_ICInitStructure);

    TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
    TIM_ICInit(TIM4, &TIM_ICInitStructure);

    /* ★ 模式 TI12:在 TI1FP1 与 TI2FP2 双边沿都计数 → 四倍频
     *   5000 PPR 编码器 → 20000 counts/rev
     */
    TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12,
                               TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);

    TIM_SetCounter(TIM4, 0);
    TIM_Cmd(TIM4, ENABLE);
}

/* 读一次位置(注意:0xFFFF 翻转要自己做多圈累加) */
static inline uint32_t Encoder_GetCount(void)
{
    return TIM_GetCounter(TIM4);
}

Z 索引单独走一个 GPIO + EXTI 中断,中断里只做一件事:记下当前计数并复位,或记录零位偏移。不要在中断里做浮点运算。

6.7 CANFD:先配位时序,再谈协议

c 复制代码
void User_CANFD1_Init(void)
{
    CANFD1_CLK_ENABLE();
    GPIOD_CLK_ENABLE();

    GPIO_InitTypeDef   GPIO_InitStructure;
    CANFD_InitTypeDef  CANFD_InitStructure;
    PLIC_InitTypeDef   PLIC_InitStructure;

    /* CAN TX / RX 引脚 */
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource13, GPIO_AF_CAN1);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource14, GPIO_AF_CAN1);

    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin        = GPIO_Pin_13;
    GPIO_InitStructure.GPIO_Mode       = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_OType      = GPIO_Out_PP;
    GPIO_InitStructure.GPIO_OStrength  = GPIO_OStrength_9mA;
    GPIO_Init(GPIOD, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin        = GPIO_Pin_14;
    GPIO_InitStructure.GPIO_Mode       = GPIO_Mode_IN;
    GPIO_InitStructure.GPIO_IType      = GPIO_IPU;
    GPIO_Init(GPIOD, &GPIO_InitStructure);

    /* 位时序:仲裁段 500kbps,数据段 2Mbps
     *   采样点 ≈ (1 + TS1) / (1 + TS1 + TS2) = 31/40 = 77.5%,是业界常用取值
     */
    CANFD_StructInit(&CANFD_InitStructure);
    CANFD_InitStructure.CANFD_SRR          = CANFD_SRR_RESET;

    CANFD_InitStructure.CANFD_APBRPR       = CANFD_APBRPR_4tp;      /* 仲裁段预分频 */
    CANFD_InitStructure.CANFD_APBTR_APTS1  = CANFD_APBTR_TS1_30tp;
    CANFD_InitStructure.CANFD_APBTR_APTS2  = CANFD_APBTR_TS2_9tp;
    CANFD_InitStructure.CANFD_APBTR_APSJW  = CANFD_APBTR_SJW_2tp;

    CANFD_InitStructure.CANFD_DPBRPR       = CANFD_DPBRPR_4tp;      /* 数据段预分频 */
    CANFD_InitStructure.CANFD_DPBTR_DPTS1  = CANFD_DPBTR_TS1_30tp;
    CANFD_InitStructure.CANFD_DPBTR_DPTS2  = CANFD_DPBTR_TS2_9tp;
    CANFD_InitStructure.CANFD_DPBTR_DPSJW  = CANFD_DPBTR_SJW_1tp;
    CANFD_Init(CANFD1, &CANFD_InitStructure);

    /* 接收过滤器:mask 位为 1 的位置必须与 id 完全一致 */
    CANFD_FilterInit(CANFD1, TB0, 0xFFE00000, 0x28E00000);

    /* 关节控制是"最新值优先",关掉自动重发,避免一条过期指令反复占用总线 */
    CANFD_AutoRetransConfig(CANFD1, DISABLE);

    CANFD_ITConfig(CANFD1, CANFD_IT_ERXOK, ENABLE);
    CANFD_Enable(CANFD1);

    PLIC_StructInit(&PLIC_InitStructure);
    PLIC_InitStructure.PLIC_IRQChannel    = CANFD1_IRQn;
    PLIC_InitStructure.PLIC_IRQPriority   = 2;
    PLIC_InitStructure.PLIC_IRQChannelCmd = ENABLE;
    PLIC_Init(&PLIC_InitStructure);

    CANFD_ClearITPendingBit(CANFD1, CANFD_CLEAR_ALL);
}

/* 发送:先绑一次帧头,之后每次换 payload 和长度即可 */
void CANFD_Send(uint8_t *buf, uint32_t len)
{
    CANFD_TXFrameTypeDef CANFD_TXFrameStruct;

    CANFD_TXFrameStruct.CANFD_TX_PROTOCOL = CANFD;
    CANFD_TXFrameStruct.CANFD_TX_FORMAT   = EXTENDED;   /* 扩展帧,29 位 ID */
    CANFD_TXFrameStruct.CANFD_TX_TYPE     = DATA;
    CANFD_TXFrameStruct.CANFD_Frame_ID    = 0x147;
    CANFD_FrameInit(CANFD1, TB1, &CANFD_TXFrameStruct);

    CANFD_Transmit(CANFD1, TB1, buf, Byte_8);
}

void CANFD1_IRQ_Handler(void)
{
    if (CANFD_GetITStatus(CANFD1, CANFD_FLAG_RXOK) != RESET)
    {
        g_canfd_rx_len = CANFD_GetReceiveData(CANFD1, g_canfd_rx_buf);
        g_canfd_rx_flag = 1;                            /* 只置标志,解析放到主循环/低优先级任务 */
        CANFD_ClearITPendingBit(CANFD1, CANFD_CLEAR_RXOK);
    }
}

SDK 例程里有个必须知道的手误 :canfd.c 中的 User_CANFD3_Init() 在配置 PLIC 时写的是 CANFD1_IRQn,和它上面初始化的 CANFD3 对不上。照抄会导致 CANFD3 的中断挂在 CANFD1 的向量上。自己写的时候一定要写成 CANFD3_IRQn(CANFD3_IRQn = 0x12)。

另外 CANFD_Transmit(CANFDx, TBx, buf, Byte_8/16/32/64) 的长度是枚举 不是字节数,Byte_16 对应 16 字节,别当成 len=16 之外的写法。

6.8 MIT 协议打包与解析

关节总线上的 8 字节帧内容,本质是一份双方约定好的位图:位置 16bit、速度 12bit、Kp 12bit、Kd 12bit、力矩 12bit,一共 64bit。先用两个换算函数把浮点压进整数域:

c 复制代码
/* 浮点 → 整数:把 [x_min, x_max] 线性映射到 [0, 2^bits - 1] */
static inline int float_to_uint(float x, float x_min, float x_max, int bits)
{
    float span   = x_max - x_min;
    float offset = x_min;
    return (int)((x - offset) * ((float)((1 << bits) - 1)) / span);
}

/* 整数 → 浮点:反馈帧解析用 */
static inline float uint_to_float(int x_int, float x_min, float x_max, int bits)
{
    float span   = x_max - x_min;
    float offset = x_min;
    return ((float)x_int) * span / ((float)((1 << bits) - 1)) + offset;
}

控制帧的 8 字节位图(用位域写出来比移位更不容易错位):

c 复制代码
typedef struct __attribute__((packed))
{
    uint16_t pos;             /* bit 0-15  :位置   */
    uint16_t vel   : 12;      /* bit 16-27 :速度   */
    uint16_t kp    : 12;      /* bit 28-39 :Kp     */
    uint16_t kd    : 12;      /* bit 40-51 :Kd     */
    uint16_t torq  : 12;      /* bit 52-63 :力矩   */
} MotorCmdBits_t;

/* 关节电机手册里约定好的量程,按实际型号改;这里给一组典型值 */
#define P_MIN   (-12.5f)
#define P_MAX   ( 12.5f)
#define V_MIN   (-30.0f)
#define V_MAX   ( 30.0f)
#define KP_MIN  (  0.0f)
#define KP_MAX  (500.0f)
#define KD_MIN  (  0.0f)
#define KD_MAX  (  5.0f)
#define T_MIN   (-10.0f)
#define T_MAX   ( 10.0f)

void Motor_SendCmd(float pos, float vel, float kp, float kd, float torq)
{
    MotorCmdBits_t cmd;
    uint8_t buf[8];

    /* 越界直接限幅,宁可少动不可失控 */
    if (pos < P_MIN) pos = P_MIN;  if (pos > P_MAX) pos = P_MAX;
    if (vel < V_MIN) vel = V_MIN;  if (vel > V_MAX) vel = V_MAX;
    if (kp  < KP_MIN) kp = KP_MIN; if (kp  > KP_MAX) kp = KP_MAX;
    if (kd  < KD_MIN) kd = KD_MIN; if (kd  > KD_MAX) kd = KD_MAX;
    if (torq < T_MIN) torq = T_MIN; if (torq > T_MAX) torq = T_MAX;

    cmd.pos  = float_to_uint(pos,  P_MIN,  P_MAX, 16);
    cmd.vel  = float_to_uint(vel,  V_MIN,  V_MAX, 12);
    cmd.kp   = float_to_uint(kp,   KP_MIN, KP_MAX, 12);
    cmd.kd   = float_to_uint(kd,   KD_MIN, KD_MAX, 12);
    cmd.torq = float_to_uint(torq, T_MIN,  T_MAX, 12);

    memcpy(buf, &cmd, 8);
    CANFD_Transmit(CANFD1, TB1, buf, Byte_8);
}

反馈帧是同一张位图反过来拆,注意反馈帧里没有 Kp/Kd,中间那两个 12bit 位置放的是电机自己的状态量:

c 复制代码
typedef struct __attribute__((packed))
{
    uint8_t  id;              /* byte 0   :从设备 ID   */
    uint16_t pos;             /* byte 1-2 :位置        */
    uint16_t vel   : 12;      /* byte 3-4 :速度        */
    uint16_t torq  : 12;      /* byte 4-6 :力矩        */
} MotorFbBits_t;

void Motor_ParseFeedback(uint8_t *buf, MotorState_t *st)
{
    MotorFbBits_t fb;
    memcpy(&fb, buf, 6);

    st->id    = fb.id;
    st->pos   = uint_to_float(fb.pos,  P_MIN, P_MAX, 16);
    st->vel   = uint_to_float(fb.vel,  V_MIN, V_MAX, 12);
    st->torque = uint_to_float(fb.torq, T_MIN, T_MAX, 12);
}

位域跨编译器有坑 :uint16_t vel : 12 的打包顺序依赖编译器实现,IAR 和 GCC 对小端位域的处理一致,但换编译器或换优化等级前,建议先用一组已知输入(比如全 0、全 1、以及 0x5555)打一次表,确认字节顺序和手册对得上再上车。这一点比协议本身更容易翻车。

6.9 FOC 电流环骨架(10kHz)

把前面几块串起来,一个电流环中断里要做的事其实就六步:

c 复制代码
volatile float g_Id_ref = 0.0f, g_Iq_ref = 0.0f;
volatile float g_theta_e = 0.0f;        /* 电角度,由机械角 × 极对数得到 */

void TIM0_IRQ_Handler(void)             /* 由 PWM 周期触发,10kHz */
{
    uint16_t raw_u, raw_v, raw_w;
    float ia, ib, ic, ialpha, ibeta, id, iq;
    float vd, vq, valpha, vbeta;
    float t1, t2, t0;

    /* ① 采样:注入组一次抓三相 */
    ADC_Read_PhaseCurrent(&raw_u, &raw_v, &raw_w);
    ia = ADC_ToAmpere(raw_u);
    ib = ADC_ToAmpere(raw_v);
    ic = ADC_ToAmpere(raw_w);

    /* ② Clarke 变换:三相 → 两相静止 */
    ialpha = ia;
    ibeta  = (ia + 2.0f * ib) * 0.5773502692f;   /* 1/√3 */

    /* ③ Park 变换:静止 → 旋转,用当前电角度 */
    id =  ialpha * cosf(g_theta_e) + ibeta * sinf(g_theta_e);
    iq = -ialpha * sinf(g_theta_e) + ibeta * cosf(g_theta_e);

    /* ④ 双 PI:d 轴通常给 0(不产生转矩),q 轴给转矩电流 */
    vd = PI_Update(&pi_d, g_Id_ref - id);
    vq = PI_Update(&pi_q, g_Iq_ref - iq);

    /* ⑤ 反 Park 变换:旋转 → 静止 */
    valpha = vd * cosf(g_theta_e) - vq * sinf(g_theta_e);
    vbeta  = vd * sinf(g_theta_e) + vq * cosf(g_theta_e);

    /* ⑥ SVPWM:算出三个比较值,写进 CCR */
    SVPWM_Calc(valpha, vbeta, &t1, &t2, &t0);
    TIM_SetCompare1(TIM0, (uint32_t)t1);
    TIM_SetCompare2(TIM0, (uint32_t)t2);
    TIM_SetCompare3(TIM0, (uint32_t)t0);

    /* ⑦ 封波锁存:一旦进过故障,这里直接退出,不再写 CCR */
    if (g_fault_latch) { TIM_CtrlPWMOutputs(TIM0, DISABLE); return; }

    TIM_ClearITPendingBit(TIM0, TIM_IT_Update);
}

三点提醒:

  1. 中断入口先采样、后运算。采样窗口和 PWM 的时序关系决定电流波形质量,把采样放在中断最前面,抖动最小。
  2. 不要在电流环里访问 APB-S 低速外设(RTC / IWDG / GPIOH / PMU)。这些外设挂在 32.768kHz 域上,一次寄存器访问能把 100µs 的时间片吃掉一大块。
  3. 三角函数用查表或硬件加速,不用 sinf/cosf 库调用 。10kHz × 两次变换 × 至少两次三角函数,库函数会把时间吃光。电流环 ISR 的实际执行时间必须用 GPIO 翻转 + 示波器实测(见 §八)。

七、踩坑清单:能省下几轮打样的 15 条

# 坑 现象 怎么办
1 芯片没有硬件 Break 想当然画了 BKIN 网络,PCB 上没有这个引脚可用 MCU 侧只有两条路:软件写 MOE=0,或 TIMx_EGR.BG 软件触发;硬件封波做在预驱自身
2 死区没配 上下桥同时导通,一上电就炸管 高级定时器必须调 TIM_BDTRConfig 配 DTG;通用定时器(TIM3/4/6/7)根本没有死区功能
3 MOE 没开 配好一切,六路输出纹丝不动 TIM_CtrlPWMOutputs(TIMx, ENABLE) 必须调,SDK 例程漏了这一步
4 DMA 用循环模式 母线采样值偶尔跳到随机数,跑一段时间才复现 改用单次模式并每次重新启动;循环模式有已知功能异常
5 DMA 与 Cache 打架 读到的 DMA 缓冲是旧数据 DMA 缓冲读之前 clear_cache(),写之前也要处理,别省这一步
6 给注入组配 DMA 配置编译过了,数据永远是 0 注入组不支持 DMA,只能读结果寄存器
7 指望 CMPO 发中断 中断永远不来 DAC/CMP 只有只读结果位 CMPO,没有中断使能位;要中断就用 ADC 模拟监控器
8 阈值设在 DAC 非线性区 阈值实际动作点和设定值差很多 DAC 线性区是 200mV ~ AVD−200mV,下限不要破 0.2V
9 HTIM0 与 SPI0 抢引脚 SPI0 死活不通 HTIM0_CH1~CH4 与 SPI0_CS0/SCK/MOSI/MISO 完全复用,用了 HTIM0 就必须把 SPI 挪到 SPI3
10 PG11 被当成普通 IO 用 M2 下桥不出波形 PG11 是 HTIM1_CH0N,与 CMP0_O 的备用功能撞车,比较器输出只能用 PF11/PF12
11 按 SWD 设计调试口 仿真器连不上 只有 JTAG,5 线排针 + BOOT 跳线必须预留
12 拿 FIRC/SIRC 当系统时钟 波特率偏差大,通信间歇性报错 未校准的内部振荡器只能当兜底;系统时钟必须有源 20MHz 晶振
13 5V 编码器直连 用一段时间后引脚损坏 3.3V 域,5V 信号必须过 74LVC8T245 或电平转换电路
14 CAN 终端电阻不匹配 多个关节挂一起后通信时好时坏 高速 CAN 回路等效电阻要 60Ω;单机 240Ω 的话 4 台并联刚好 60Ω,超过 4 台要么拆电阻要么用 CAN_Bridge 分回路
15 从设备 ID 与主设备 ID 撞车 权限混乱、心跳类报文互相干扰 主设备 ID 不要和任何在线从设备 ID 相同;改 ID 后必须重新上电才生效

再补三条现场高频问题:

  • 自动重发把总线占满 。从设备侧如果一直收不到应答,自动重发会持续占用总线,测出来的 CAN 阻值甚至会被拉到接近 0Ω。主控侧的对策是打开接收(哪怕不处理),并且把自动重发关掉。
  • 不要用上位机去改串口波特率。改串口那一步很容易把设备改到连不上,默认 115200 保持不动,要改通信速率直接走 CAN 侧指令。
  • 扩展帧 ID 写错。位置型指令需要同时填"来源 ID(主设备)"和"目标 ID(从设备)",这两段拼在一起最容易错位,改完先用回读确认。

八、台架验证清单:上电前必须用示波器看到的波形

上面所有取值都是算出来的,算出来的东西不上台架就只是纸面值。这一节是我打板之后按顺序要过的七件事,每一件都写清测点、通过判据和工具,照着做就行。

# 验证项 测点 通过判据 注意
1 ADC 注入组触发相位 HTIM0_CH1 波形 + 低边 MOS 门极 + 触发标记(另找一个 GPIO 在触发时翻转) 触发沿落在下桥导通的中段,与上、下沿之间都留够"采样时间 + 建立时间" 占空比越大窗口越窄,必须扫到最大占空比再看一遍
2 电流环 ISR 执行时间 ISR 入口置 GPIO、出口清 GPIO,示波器量脉宽 脉宽 ≤ 周期的 50%(10kHz 即 ≤50µs),另一半留给抖动与后台任务 用真实中断优先级实测,不要用裸循环估时间
3 三级封波时间 L1 预驱 OCP、L2 ACMP→预驱 EN/nBRAKE、L3 ADC 模拟监控器 ISR→MOE=0,分三级单独测 L1 < L2 < L3;L3 的实测值必须写进安全评估文档 只报"总时间"没法定位是哪一级慢
4 相电流标定 精密电流源或对拖台架,取 5~9 点 全量程非线性可用查表修正;−40 / 25 / 85 / 125℃ 各复测一遍 片内 ADC 的 ENOB 决定修正表要多密
5 CMPO 直连预驱 EN/nBRAKE CMP0_O 引脚 + 预驱使能/制动脚 电平域、有效极性、上电时序三者都对得上;异常时预驱确认落在关断态 待原厂确认 / 待实测,没测过不要画进框图
6 CMPO 接 EXTI CMP0_O 引脚 + 外部中断触发标记 边沿能稳定触发,且上电瞬间的毛刺不误触发 比较器只有只读结果位,没有中断使能位,只能走 EXTI 这条路
7 低温自举 V_GS 上桥 V_GS,冷箱 −40℃ 全温区都高于 MOS 的完全导通门槛,自举电压不跌落 低温下 MOS 门槛升高、容值下降,两头夹

几个实操细节:

  1. 测封波时间用双通道,不要用单通道猜。故障注入(比如把母线拉到阈值)和 PWM 门极各占一个通道,示波器设成故障沿触发,读两个沿的时间差。EMC 台面上单通道量出来的值不可信。
  2. CMPO 接 EXTI 之前先测静态电平。上电瞬间和复位释放瞬间都要看一遍,确认不会因为预驱上电慢一档而误封波。
  3. 标定用的数据要存原始值。ADC 原始码、母线电压、结温、台架电流都记下来,后面做温漂补偿时不用重测一遍。
  4. 每一项都留一份截图归档。关节驱动板的通信、编码器、功率级问题经常互相串,没有波形记录时只能靠回忆,太费时间。

九、关键设计取值速查

设计项 取值 约束依据
三相桥拓扑 栅极驱动 IC + 6×N-MOS + 自举 AS32X601 是通用 MCU,无片内预驱,功率级必须外置
死区 HTIM 内置 10bit DTG(500ns~1µs)+ 预驱内部死区,取较大者 互补输出是高级定时器专有能力;双重死区会损失有效调制比
PWM 载波 20kHz(电流环 10kHz,每周期 2 次采样) 兼顾开关损耗、死区占比与低边采样窗口宽度
相电流链路 1mΩ/2W + 50V/V + 1.65V 偏置 偏置点与量程必须按片内 ADC 非线性区核算(§5.2.3)
采样触发 HTIM 触发注入组,零矢量窗口内采样 低边三电阻采样的物理约束,触发相位需示波器实测(§5.2.5)
母线电压 100k/4.7k 分压 + RC + BAT54S 钳位 + DMA 单次模式 DMA 循环模式有 errata;分压源阻抗偏高需延长采样时间
过流保护 预驱 OCP + ACMP(DAC 8bit 阈值/迟滞)+ ADC 软件限幅三级 三极管阈值温漂 ±27%,不能作为唯一定值保护
封波路径 硬件级联做在预驱(CMPO→EN/nBRAKE);MCU 侧只能软件写 MOE=0 无硬件 Break(无 BKIN/BRK 引脚),软件封波时间必须实测
编码器 TIM4 硬件四倍频(20000cpr)+ EXTI Z + 电平转换;输出侧加绝对值编码器 关节减速机构的背隙与传动误差需要输出侧真实关节角

十、其他优化方向

  1. 采样电阻阻值取舍 :这里取 1mΩ/2W + 50V/V,信号强、对放大器失调与噪声要求低,代价是采样损耗偏大(12A 下约 144mW/相,三相合计约 0.43W)。若进一步降到 0.4~0.5mΩ + 更高增益来省损耗,代价是必须升级低失调/低温漂放大器,并重新核对 AS32X601 片内 ADC 的非线性区与 ENOB。
  2. 编码器接口优先选差分 ABZ :KTH7815 类磁编码器的差分 ABZ 比 5V 单端光学编码器更适合关节内走线;AS32X601 侧可直接接差分接收器(AM26LV32 类),省掉 74LVC8T245 电平转换并提升抗扰。
  3. 预驱集成度是成本与外围的交换点 :EG2124(低集成、低 BOM)↔ DRV8353 / TB67Z(高集成、带放大器与故障汇总)。取舍原则:预驱带放大器与 nFAULT 时,MCU 侧只做阈值判定与状态锁存;用低集成预驱时,自建差分放大 + 保护链是硬性工作量。
  4. 功率级按"双面散热 + 外壳导热"预留:关节内腔空间紧张,建议 PCB 布局按双面散热封装(PDFN/TOLL 双面散热类)预留兼容焊盘,热设计阶段明确结壳热阻与外壳热路径。

十一、主要器件选型骨架(BOM 起点)

功能块 推荐器件/规格 选型要点
栅极驱动 DRV8305 / DRV8323(三相,含 OCP、含电流放大器)或 FD6288 类 优先选自带过流保护与电流放大器的型号,可省掉一级比较器
N-MOS 40V/60V 级,R_ds(on) 按 12A 连续选 关注 Q_g 与 C_oss(影响自举电容与开关损耗)
自举二极管 快恢复/肖特基,低反向恢复电荷 高频下普通整流管会抽掉自举电压
采样电阻 1mΩ / 2W,2512,低温漂(≤50ppm/℃) Kelvin 四端子连接,采样走线从焊盘内缘引出
电流检测放大器 INA240A2(50V/V)等,带宽 ≥400kHz 关注 CMRR、Vos 温漂;低边采样共模小,若改相线/高端采样必须换高共模器件
基准 REF3033(3.3V, ±0.05%) 与 ADC VREFP 同源;偏置分压取同一路
偏置跟随器 低失调轨到轨运放(OPA320 类) 输出阻抗必须低,否则影响差分放大器精度
ADC 钳位 BAT54S 每路模拟输入都加
编码器电平转换 74LVC8T245 5V→3.3V,注意方向与使能
差分接收器 AM26LV32 差分编码器/长线场景
绝对值编码器 SSI/BiSS-C 型,17~19bit 走 SPI,确认从机模式时钟上限

十二、风险清单与验证方法

# 风险项 等级 影响 验证/规避方法
1 片内 ADC ENOB 仅 8.6bit(勘误手册列为"无规避方案") 高 力矩纹波、力控带宽不足 台架实测:精密源扫描 + 多点标定 LUT;力控档改用外接 16bit SPI ADC
2 无硬件 Break(无 BKIN/BRK 引脚),只能软件触发 高 过流封波依赖中断响应 硬件封波交给预驱;实测"过流发生 → PWM 关断"总时间;安全评估不得按纯硬件封波口径描述
3 ACMP 无中断使能位(设计手册 §30.5.4 仅有只读 CMPO) 中 无法直接"比较器中断" 引脚已确认:CMP0_O=PF11(104)、CMP1_O=PF12(102)(复用表功能1);软件路径改用 ADC 模拟监控器中断 → ISR 写 MOE=0,硬件路径用 CMPO 直连预驱 EN/nBRAKE(电气兼容性待原厂确认)
4 ADC 参考源口径冲突(设计手册 §29.2 vs FAQ Q9) 中 原理图基准设计可能返工 按外部 3.3V 基准设计;bring-up 时实测内部参考是否可用并记录结论
5 DMA 循环模式异常 + Cache 一致性 高 数据错乱、随机故障 DMA 一律单次模式 ;DMA 缓冲与 Flash 读操作前 clear_cache
6 低边采样窗口与最小脉宽 中 大占空比时电流采样畸变 示波器实测采样点位置;设置最小脉宽或动态选相重构
7 有源晶振要求(8~40MHz,20MHz→PLLDivN=0x14) 中 时钟不准导致通信/采样异常 单一有源 20MHz 方案,不做双焊盘;FIRC/SIRC 未校准,不作系统时钟
8 APB-S 低速总线阻塞(RTC/IWDG/GPIOH/PMU) 中 实时环抖动 避免在电流环中断内访问低速总线外设;喂狗放在低优先级任务
9 仅 JTAG(无 SWD) 低 调试接口选型/工装 预留 5 线排针 + BOOT 跳线
10 自举电容选值与低温启动 中 上桥驱动不足、半导通发热 按 §5.1.2 公式计算并全温区实测上桥 V_GS 波形

相关推荐
硅基手札4 小时前
【risc-v专栏 07】启动与 bare-metal 流程
risc-v
硅基手札19 小时前
【risc-v专栏 08】扩展生态与 -march/-mabi 匹配
risc-v
硅基手札1 天前
【risc-v专栏 06b】内存架构深挖:PMA / ePMP / Cache / CMO / MMU 细节 / RVWMO 形式化
架构·risc-v
硅基手札1 天前
【risc-v专栏03】CSR 速查与特权架构 M/S/U
单片机·架构·risc-v
IvorySQL1 天前
去 IOE 的最后一公里:IvorySQL 5.4 × RISC-V 实测
数据库·人工智能·ai·postgresql·risc-v
RISCV_Explorer1 天前
技术指南:从总线协议到软件可见行为——RISC-V 多核缓存一致性的 Linux 实践
linux·缓存·risc-v
RISCV_Explorer8 天前
RISC-V内存属性配置深度专题——Svpbmt页表位、PMA优先级与设备访问策略
risc-v
RISCV_Explorer9 天前
RISC-V处理器性能优化:从指令集到微架构的协同设计
后端·risc-v
RISCV_Explorer12 天前
RISC-V启动与运行时规范机制解析——BRS约定、HSM多核启动与设备树要求
后端·risc-v