机器人关节这个题目,我前后啃了挺久。第一次真正把它想明白,是看完一台人形关节的内部结构之后:那里头除了电机本体、减速器、磁编码器,还塞着一块巴掌大的驱动板,板子上有主控、有六颗 MOS、有采样电阻、有 CAN 收发器。当时我脑子里冒出的第一个问题不是"这板子怎么画的",而是------为什么每个关节都要配一块带 FOC 的单片机?
答案很朴素:FOC 吃算力。让一颗 MCU 同时跑七八路 FOC,电流环根本来不及算,最后每一路都是"凑合能转"。所以行业的做法是把控制权下放:每个关节里一颗主控,各自管自己的电机,外面只走两根差分线告诉它"转到哪、多快、用多大力"。
那为什么外面这两根线一定是 CAN,而不是 SPI、I2C、UART?三个理由,做过的人都认:
- 线少。一条总线两根线,还能挂一串关节;SPI/I2C 每个从机都要单独走线,十几自由度的人形机器人接线会失控。
- 关节不是纯从机。它要不停回传位置、速度、力矩、温度、故障码,这是"多主"模型,一主多从的 SPI/I2C 天然别扭。
- 抗扰和距离。差分传输 + 差分接收,和电机相线一起走线也扛得住;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 三条引脚冲突提醒(画图前先看)
- HTIM0 与 SPI0 完全复用 :
HTIM0_CH1/CH2/CH3/CH4 = PB12/PB13/PB14/PB15,和SPI0_CS0/SCK/MOSI/MISO撞在一起。只要用 HTIM0 出三相 PWM,SPI0 就不可用,需要 SPI 从机接口时必须改到 SPI3。 - PG11 与 M2 下桥复用 :
CMP0_O/CMP1_O在 PF11/PF12(功能 1)和 PG10/PG11(功能 3)上都存在,但 PG11 同时是 HTIM1_CH0N。双三相方案里只能用 PF11/PF12,别贪那两个备用脚。 - 别动调试脚:仅 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 这一块的三个坑
- 只用通用定时器做三相 PWM :TIM3/4/6/7 不支持互补输出与死区插入,必须用 HTIM0/1/2/5。一块板子最多 4 路三相(受高级定时器数量限制)。
- 母线去耦电容离桥臂太远:换流回路面积直接决定电压尖峰与 EMI,陶瓷电容必须紧贴桥臂与地平面。
- 未加母线泄放与制动路径:电机快速减速时能量回灌,母线电压会抬升,需评估制动斩波或电容耐压余量(≥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 偏置电路的三点补充
- 偏置源阻抗必须低 :差分放大器的 REF 引脚若直接接分压电阻(如 10k/10k),会因 REF 输入电流与电阻失配引入增益/失调误差,必须用跟随器隔离。
- 偏置与 VREF 同源 :偏置分压与 ADC 基准取同一路 3.3V_A,这样基准漂移是共模的(比值式测量),否则温漂会直接变成零点漂移。
- 偏置电容:跟随器输出加 100nF + 1µF,降低开关噪声耦合。
5.2.5 采样时刻与窗口(低边采样的硬约束)
低边三电阻采样只能在下桥全部导通的零矢量窗口内采样。 工程上必须做两件事:
- 确认触发相位:中心对齐 PWM 一个周期内有两次零矢量,具体落点由计数方向、输出极性、重复计数器共同决定。不要照抄别人的注释,用示波器实测:固定 50% 占空比,用 GPIO 在 ADC 触发时刻翻转,观察该跳变是否落在下桥 V_DS 的低电平平台中部。
- 核算最小导通窗口 :高调制比时下桥导通时间被压缩,若小于"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只读结果位与迟滞档位,没有比较器中断使能位。可用的两条路径是:
- CMPO 引脚输出 → 预驱 EN / nBRAKE (硬件级联,推荐)。复用表中已有专用输出:
CMP0_O=PF11(104)/功能1、CMP1_O=PF12(102)/功能1;PG10(功能3)/PG11(功能3)上也有同一对输出,但 PG11 与本方案 M2 下桥HTIM1_CH0N复用 ,故只用 PF11/PF12。待确认的是直驱预驱 EN 的电平/极性/上电时序与失效模式,而不是引脚是否存在;- 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 布线与抗干扰
- AB 相走双绞 + 屏蔽 ,屏蔽层单点接地(编码器侧或板侧,不要两端接)。
- 与电机相线分开走线,禁止并行长距离;交叉时垂直交叉。
- 编码器引脚串 100Ω + 并 100pF(转折频率 ~16MHz),滤掉开关噪声但保留计数边沿。
- 编码器供电独立 LDO + π 滤波,避免与 MOS 开关环路共地噪声。
- 上电初始化流程要明确:编码器方向标定 → 电角度对齐(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);
}
}
这里有两个必须记住的点:
TIM_CtrlPWMOutputs(TIM0, DISABLE)清的是 MOE 位 ,效果是六路输出立刻回到TIM_OCIdleState/TIM_OCNIdleState设定的空闲电平。前面把上桥空闲电平设成 Set、下桥设成 Reset,此刻六颗 MOS 全部关断,这是安全态。- 封波之后不要自动重试。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);
}
三点提醒:
- 中断入口先采样、后运算。采样窗口和 PWM 的时序关系决定电流波形质量,把采样放在中断最前面,抖动最小。
- 不要在电流环里访问 APB-S 低速外设(RTC / IWDG / GPIOH / PMU)。这些外设挂在 32.768kHz 域上,一次寄存器访问能把 100µs 的时间片吃掉一大块。
- 三角函数用查表或硬件加速,不用
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 门槛升高、容值下降,两头夹 |
几个实操细节:
- 测封波时间用双通道,不要用单通道猜。故障注入(比如把母线拉到阈值)和 PWM 门极各占一个通道,示波器设成故障沿触发,读两个沿的时间差。EMC 台面上单通道量出来的值不可信。
CMPO接 EXTI 之前先测静态电平。上电瞬间和复位释放瞬间都要看一遍,确认不会因为预驱上电慢一档而误封波。- 标定用的数据要存原始值。ADC 原始码、母线电压、结温、台架电流都记下来,后面做温漂补偿时不用重测一遍。
- 每一项都留一份截图归档。关节驱动板的通信、编码器、功率级问题经常互相串,没有波形记录时只能靠回忆,太费时间。
九、关键设计取值速查
| 设计项 | 取值 | 约束依据 |
|---|---|---|
| 三相桥拓扑 | 栅极驱动 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 + 电平转换;输出侧加绝对值编码器 | 关节减速机构的背隙与传动误差需要输出侧真实关节角 |
十、其他优化方向
- 采样电阻阻值取舍 :这里取 1mΩ/2W + 50V/V,信号强、对放大器失调与噪声要求低,代价是采样损耗偏大(12A 下约 144mW/相,三相合计约 0.43W)。若进一步降到 0.4~0.5mΩ + 更高增益来省损耗,代价是必须升级低失调/低温漂放大器,并重新核对 AS32X601 片内 ADC 的非线性区与 ENOB。
- 编码器接口优先选差分 ABZ :KTH7815 类磁编码器的差分 ABZ 比 5V 单端光学编码器更适合关节内走线;AS32X601 侧可直接接差分接收器(AM26LV32 类),省掉 74LVC8T245 电平转换并提升抗扰。
- 预驱集成度是成本与外围的交换点 :EG2124(低集成、低 BOM)↔ DRV8353 / TB67Z(高集成、带放大器与故障汇总)。取舍原则:预驱带放大器与 nFAULT 时,MCU 侧只做阈值判定与状态锁存;用低集成预驱时,自建差分放大 + 保护链是硬性工作量。
- 功率级按"双面散热 + 外壳导热"预留:关节内腔空间紧张,建议 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 波形 |