电机驱动开发学习21. R3_2 双相扇区电流采样与第三相重构
- 一、本章在系列中的位置
-
- [1.1 与前面章节的衔接](#1.1 与前面章节的衔接)
- [1.2 本章目标](#1.2 本章目标)
- [1.3 本章新增知识点](#1.3 本章新增知识点)
- [二、为什么固定 U+V 不够](#二、为什么固定 U+V 不够)
-
- [2.1 三电阻采样的物理约束](#2.1 三电阻采样的物理约束)
- [2.2 空间矢量扇区与最佳采样相的数学关系](#2.2 空间矢量扇区与最佳采样相的数学关系)
- [2.3 本课与官方硬件使用差异](#2.3 本课与官方硬件使用差异)
- [2.4 ADC「注入转换」解释](#2.4 ADC「注入转换」解释)
-
- [2.4.1 和「普通(规则)转换」差在哪](#2.4.1 和「普通(规则)转换」差在哪)
- [2.4.2 本课具体怎么「注入」](#2.4.2 本课具体怎么「注入」)
- [2.4.3 为什么 FOC 必须用注入,而不是主循环里 `HAL_ADC_Start`](#2.4.3 为什么 FOC 必须用注入,而不是主循环里
HAL_ADC_Start) - [2.4.4 「注入连采两相」和官方「双 ADC 同时注入」](#2.4.4 「注入连采两相」和官方「双 ADC 同时注入」)
- [2.4.5 注入配置源码(本课实现)](#2.4.5 注入配置源码(本课实现))
-
- [(1)TIM1_CH4:在 PWM 中点打出触发](#(1)TIM1_CH4:在 PWM 中点打出触发)
- [(2)ADC1 本体初始化 + 默认注入对](#(2)ADC1 本体初始化 + 默认注入对)
- (3)注入通道表:`adc_inj_program_pair()`(核心)
- [三、扇区 ↔ 采样相对照表](#三、扇区 ↔ 采样相对照表)
-
- [3.1 三相电流重构公式矩阵](#3.1 三相电流重构公式矩阵)
- [3.2 极性修正与硬件偏置推导](#3.2 极性修正与硬件偏置推导)
- 四、程序设计(本章代码实现详解)
-
- [4.1 目录与变更焦点](#4.1 目录与变更焦点)
- [4.2 模式宏 `FOC_ADC_SAMPLE_MODE`](#4.2 模式宏
FOC_ADC_SAMPLE_MODE) - [4.3 扇区 → 采样对:`adc_sector_to_pair()`](#4.3 扇区 → 采样对:
adc_sector_to_pair()) - [4.4 运行中改注入通道:`adc_inj_program_pair()`](#4.4 运行中改注入通道:
adc_inj_program_pair()) - [4.5 扇区下发:`adc_curr_set_sector()`](#4.5 扇区下发:
adc_curr_set_sector()) -
- [本拍 / 下一拍为何要分开?](#本拍 / 下一拍为何要分开?)
- [4.6 JEOS 回调:取数 + 重构 + 进电流环](#4.6 JEOS 回调:取数 + 重构 + 进电流环)
- [4.7 三相 Offset 校准](#4.7 三相 Offset 校准)
- [4.8 与电流环、启动的衔接](#4.8 与电流环、启动的衔接)
- [4.9 关键 API 一览](#4.9 关键 API 一览)
- [4.10 FreeMASTER / 串口建议观察量](#4.10 FreeMASTER / 串口建议观察量)
- 五、实验步骤
-
- [5.1 回归(MODE=0)](#5.1 回归(MODE=0))
- [5.2 扇区采样(MODE=1,默认)](#5.2 扇区采样(MODE=1,默认))
- 六、小结与下一章

一、本章在系列中的位置
1.1 与前面章节的衔接
| 章节 | 已掌握 | 本章如何用到 |
|---|---|---|
| 6 三相电流采集 | 分流电阻、运放偏置、过流 | 升级为与 SVPWM 扇区同步 的双相注入采样 |
| 16~17 电流/速度环 | Id/Iq、速度 PI | 反馈电流质量直接决定环路可信度 |
| 18~20 SVPWM + 官方 Hall FOC | 官方 PWMC_SetPhaseVoltage、Charge→Offset→Run、ISR 电流环 |
基座保留 ;把 lesson20「固定采 U+V」升级为官方 R3_2 扇区双相 |
lesson20 证明「约定对齐就能转」;
lesson21 证明「高调制下电流仍然采得对」------对照 ST
r3_2_f4xx_pwm_curr_fdbk。
1.2 本章目标
在 lesson20 闭环工程 上,完成电流采样链路升级:
SVPWM 扇区 → 选择本拍可采的两相 → ADC1 注入采这两相
→ I = Offset − ADC → 第三相 I₃ = −I₁ − I₂ → Clark/Park
| # | 目标 | 工程落点 |
|---|---|---|
| 1 | 扇区感知采样 | adc_curr_set_sector() 按扇区改注入通道 |
| 2 | 第三相基尔霍夫重构 | JEOS 回调里 I₃ = −I₁ − I₂ |
| 3 | 极性 Offset−ADC | adc_raw_to_ma(),与 L20 / 官方一致 |
| 4 | 采样模式可切换 | FOC_ADC_SAMPLE_MODE:0=固定 UV,1=扇区双相 |
| 5 | 高调制仍可读流 | 某相占空比贴边时改采另外两相 |
| 6 | 闭环不回退 | Charge→Offset→Run、官方 SVPWM、霍尔 θ 与 Lesson20 同级 |
排线仍用 无刷电机驱动接口 1 。三相分流在 ADC1:PA3/PA4/PA6;ADC2 轮询母线/温度。
1.3 本章新增知识点
| # | 新知识点 | lesson20 | 本课 |
|---|---|---|---|
| 1 | 为何不能永远采同一对相 | 固定 U+V | 按扇区换 UV / VW / WU |
| 2 | R3_2 GetPhaseCurrents 扇区表 |
仅 W=-U-V |
扇区 1~6 → (采哪两相, 谁重构) |
| 3 | 运行中改注入通道 | 上电配一次 | adc_inj_program_pair() 改 JSQR(注入通道表,见 §2.4) |
| 4 | 本拍 / 下一拍扇区分离 | --- | s_samp_sector / s_prog_sector |
| 5 | 三相 Offset | W 用 UV 均值近似 | U/V 校准;W 占位后运行中精修 |
官方对照:MCLib/F4xx/Src/r3_2_f4xx_pwm_curr_fdbk.c → R3_2_GetPhaseCurrents。
二、为什么固定 U+V 不够
2.1 三电阻采样的物理约束
在三相逆变器中,电流运放的输入端并联在下桥臂 MOS 管的低侧分流电阻 (Shunt Resistor) 两端。只有当下桥臂开关管导通(即上桥臂关闭)时,相电流才会流过分流电阻。即下桥分流电阻上的电流,只有在 该相低边导通足够长 时才可靠。SVPWM 某扇区内总有一相占空比很高(低边时间短)------若仍去采这一相,ADC 采到的接近偏置,电流环会以为 Iq≈0 。
在一个完整的 PWM 周期内,从下桥臂开启到 ADC 能够稳定采样,必须满足以下时间窗口约束:
T l o w ≥ T m i n = T d e a d + T n o i s e + T s e t t l i n g + T s a m p l e T_{\mathrm{low}} \ge T_{\mathrm{min}} = T_{\mathrm{dead}} + T_{\mathrm{noise}} + T_{\mathrm{settling}} + T_{\mathrm{sample}} Tlow≥Tmin=Tdead+Tnoise+Tsettling+Tsample
其中:
- T d e a d T_{\mathrm{dead}} Tdead: 互补 PWM 的死区时间;
- T n o i s e T_{\mathrm{noise}} Tnoise: 功率管开关动作引起的接地弹跳 (Ground Bounce) 与振铃持续时间;
- T s e t t l i n g T_{\mathrm{settling}} Tsettling: 运放响应及输出电压建立时间;
- T s a m p l e T_{\mathrm{sample}} Tsample: ADC 内部采样保持回路所需的最短时间 (Sample Window)。
当 SVPWM 输出高调制比(如电机高速运行或重载需要高输出电压)时,最大占空比相的上桥臂导通时间接近 100%。这意味着其下桥臂导通时间 T l o w < T m i n T_{\mathrm{low}} < T_{\mathrm{min}} Tlow<Tmin。
如果此时强行在 PWM 中点对该相进行 ADC 采样,ADC 实际采到的将是开关噪声发生期的无效电压,导致测得电流大幅偏离真实值(表现为 ADC 读数偏向偏置电压,系统误以为电流为 0)。这会在控制上引发致命的 I q I_q Iq 电流塌陷,导致电机产生剧烈震动甚至触发过流保护。
2.2 空间矢量扇区与最佳采样相的数学关系
SVPWM 将一个电角度周期分为 6 个扇区。在任意一个扇区中,三相上桥臂的比较值( T u , T v , T w T_u, T_v, T_w Tu,Tv,Tw)存在明确的大小顺序。根据矢量合成原理:
- 比较值最大的一相:上桥臂导通时间最长 → \to → 下桥臂导通时间最短(不满足 T m i n T_{\mathrm{min}} Tmin,不可采);
- 比较值较小和最小的两相:下桥臂导通时间最充足 → \to → 可可靠采样。
例如,在 扇区 1 中,基本矢量由 V 1 ( 100 ) \mathbf{V}_1(100) V1(100) 和 V 2 ( 110 ) \mathbf{V}_2(110) V2(110) 合成:
- 占空比顺序为: T u > T v > T w T_u > T_v > T_w Tu>Tv>Tw;
- 下桥臂导通时间顺序为: T l o w , W > T l o w , V > T l o w , U T_{\mathrm{low}, W} > T_{\mathrm{low}, V} > T_{\mathrm{low}, U} Tlow,W>Tlow,V>Tlow,U;
- 因此,在扇区 1 中,V 相和 W 相 的下桥臂导通时间最长,是最佳采样对象;U 相电流则通过基尔霍夫定律 I U = − I V − I W I_U = -I_V - I_W IU=−IV−IW 重构。
2.3 本课与官方硬件使用差异
| 项 | 官方典型 R3_2 | 骄阳接口 1 + 本课 |
|---|---|---|
| 电流 ADC | ADC1 + ADC2 同时各采一相 | 仅 ADC1 注入连采两相(µs 级间隔) |
| ADC2 | 参与电流 | 母线 PB0 + 温度 PB1 软件轮询 |
| 通道切换 | ADCConfig1/2[sector] |
运行中改 ADC1 注入 rank 通道 |
算法与扇区表对齐官方;「双 ADC 真正同时采样」为进阶选做。
2.4 ADC「注入转换」解释
注入 (Injected Conversion),是 STM32 ADC 的一种工作模式,
它是:用定时器硬件触发,在指定时刻插队采集若干通道,专门给 FOC 这种「必须和 PWM 对齐」的场景用。
2.4.1 和「普通(规则)转换」差在哪
| 规则组 Regular | 注入组 Injected | |
|---|---|---|
| 典型用途 | 温度、母线、慢变化量 | 相电流(要跟 PWM) |
| 谁启动 | 软件 Start、DMA 连续扫 |
定时器边沿(本课 TIM1_CH4) |
| 优先级 | 较低 | 更高:可打断正在进行的规则转换 |
| 结果存放 | 普通数据寄存器 / DMA 缓冲 | 注入数据寄存器(本课 Rank1、Rank2 各一路) |
| 本课用法 | ADC2 轮询 Vbus / 温度 | ADC1 采两相电流 |
可以记:规则组 = 后台慢慢采;注入组 = PWM 打个响指,立刻采当前该采的相。
lesson6 早期三相电流多用连续/扫描思路;从 lesson20/21 的 FOC 起,相电流改为 注入 + TIM1_CC4 中心触发,才能保证采样落在下桥臂导通窗口内。
2.4.2 本课具体怎么「注入」
TIM1 中心对齐 PWM
│
│ 计数到中点附近 → CH4 比较匹配(CCR4)
▼
硬件触发 ADC1 注入序列(无需 CPU 去「现在采一下」)
│
├─ Injected Rank1 → 本拍采样对的第一相(如 U 或 V 或 W)
└─ Injected Rank2 → 第二相(µs 级后完成)
│
▼
JEOS(注入序列结束)中断
→ 读两路 raw → Offset−ADC → 重构第三相 → 电流环
对应代码概念(展开见下两小节):
| 名词 | 本课含义 |
|---|---|
| JSQR | 注入序列寄存器:规定 Rank1/Rank2 采哪路通道、采几路 |
adc_inj_program_pair() |
按 UV/VW/WU 改写 JSQR(运行中可改) |
pair=0/1/2 |
当前注入的是哪一对相(JSQR 内容的软件镜像) |
| JEOS | 注入序列全部采完产生的结束标志/中断 |
| JEOS 回调 | HAL_ADCEx_InjectedConvCpltCallback:读数 + 重构 + 电流环入口 |
JSQR:注入序列配置寄存器(采谁、采几路)
JSQR = J njected S equence Register(注入序列寄存器)。
可以把它理解成 ADC 注入组的一份 点菜菜单:
| 菜单项 | 本课配置 | 说明 |
|---|---|---|
| 一顿采几道菜 | InjectedNbrOfConversion = 2 |
Rank1 + Rank2 共两相 |
| 第一道 / 第二道 | Rank1 / Rank2 的 InjectedChannel |
如 UV / VW / WU |
| 谁喊上菜 | ExternalTrigInjecConv = T1_CC4 |
TIM1 中点触发(写在注入配置里,与 JSQR 一起生效) |
HAL 里不直接写 ADC1->JSQR = ...,而是:
c
HAL_ADCEx_InjectedConfigChannel(&ADC_Handle, &adc_inj);
/* 内部把 InjectedChannel / Rank / NbrOfConversion 等写入 JSQR 等寄存器 */
因此文中说「改 JSQR」「运行中改注入通道」,就是指 adc_inj_program_pair() 按扇区换菜单上的两道菜;触发源一般不变,仍是 TIM1_CC4。
和规则组对比:规则组有 SQR (Regular Sequence Register);注入组对应的是 JSQR 。名字差一个 J(Injected)。
JEOS:注入序列结束(两相都采完了)
JEOS = J njected E nd o f Sequence(注入序列结束)。
含义:
- TIM1_CC4 触发后,ADC 按 JSQR 先采 Rank1,再采 Rank2;
- 两路都结束后,硬件置位注入序列结束标志(文档/参考手册里的 JEOS);
- 若已
InjectedStart_IT,则进 ADC 中断;HAL 映射为:
c
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
{
/* 本课:读 Rank1/Rank2 → Offset−ADC → 第三相重构 → dq_current_loop_hf() */
}
| 缩写 | 全称(常见说法) | 一句话 |
|---|---|---|
| JSQR | Injected Sequence Register | 采之前的配置:采谁、采几路 |
| JEOS | Injected End of Sequence | 采之后的事件:序列结束,可以读数了 |
时序关系:
写 JSQR(上电或扇区切换)
→ 等待 TIM1_CC4
→ Rank1 转换 → Rank2 转换
→ JEOS 置位 / 进回调
→ 软件读 JDR1、JDR2(HAL:InjectedGetValue Rank1/2)
注意:还有 JEOC (单次注入通道结束)。本课关心的是 整段序列结束 JEOS(两相都齐了再算电流),不要和「Rank1 刚采完」的中间状态混用。
2.4.3 为什么 FOC 必须用注入,而不是主循环里 HAL_ADC_Start
- 时刻要对齐:采样必须落在 PWM 周期固定相位(本课在中点),主循环轮询相位漂移,电流会抖、环会乱。
- 延迟要短 :电流环在 ISR 里算,注入结束立刻进
dq_current_loop_hf(),不能等主循环碰巧跑到。 - 可与扇区联动:下一拍换扇区时改注入通道,仍由同一 TIM1_CC4 触发,硬件节奏不变。
2.4.4 「注入连采两相」和官方「双 ADC 同时注入」
- 官方 :ADC1、ADC2 各注入一相,同一触发沿几乎同时得到两相。
- 本课 :只有 ADC1,Rank1 → Rank2 串行,两相相差约 1~2 µs(相对 15 kHz 的 PWM 周期可忽略,见 §2.3)。
所以文中写「ADC1 注入 连采两相」= 用注入模式、一次触发采两个 rank;不是「注入电流控制」那种电机术语。
2.4.5 注入配置源码(本课实现)
配置分三块:定时器给出触发沿 → ADC1 打开注入能力 → 写入 Rank1/Rank2 通道(可按扇区改)。
(1)TIM1_CH4:在 PWM 中点打出触发
bsp_motor_tim.c 里 CH1~CH3 出 SVPWM,CH4 专用作 ADC 触发(不驱动功率管):
c
/* CH4:PWM2 模式,Pulse = ARR/2 附近 → 中心对齐计数到中点时产生比较匹配 */
tim_oc_init.OCMode = TIM_OCMODE_PWM2;
tim_oc_init.Pulse = MOTOR_PWM_ADC_TRGO_PULSE; /* ≈ MOTOR_PWM_PERIOD_COUNT/2 - 1 */
HAL_TIM_PWM_ConfigChannel(&motor_htimx_bldcm, &tim_oc_init, TIM_CHANNEL_4);
/* ... */
HAL_TIM_PWM_Start(&motor_htimx_bldcm, TIM_CHANNEL_4); /* 必须启动,ADC 才有触发沿 */
宏定义(bsp_motor_tim.h):
c
#define MOTOR_PWM_ADC_TRGO_PULSE ((MOTOR_PWM_PERIOD_COUNT / 2U) - 1U)
(2)ADC1 本体初始化 + 默认注入对
规则组触发关掉(电流不走软件 Start);扫描使能以便注入多 rank;最后挂上默认 UV 并开 JEOS 中断:
c
/* bsp_adc.c → adc1_inj_init() */
static void adc1_inj_init(void)
{
CURR_ADC_CLK_ENABLE();
ADC_Handle.Instance = CURR_ADC; /* ADC1 */
ADC_Handle.Init.ClockPrescaler = ADC_CLOCKPRESCALER_PCLK_DIV4;
ADC_Handle.Init.Resolution = ADC_RESOLUTION_12B;
ADC_Handle.Init.ScanConvMode = ENABLE; /* 多注入 rank 需要 */
ADC_Handle.Init.ContinuousConvMode = DISABLE; /* 一拍触发采一次,不连扫 */
ADC_Handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
ADC_Handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 规则组不用硬件触发 */
ADC_Handle.Init.NbrOfConversion = 1;
ADC_Handle.Init.DMAContinuousRequests = DISABLE;
HAL_ADC_Init(&ADC_Handle);
adc_inj_channels_config(&ADC_Handle); /* 内部即 adc_inj_program_pair(0) → 默认 UV */
HAL_NVIC_SetPriority(ADC_INJ_IRQ, 1, 0); /* 高优先级,保证电流环节拍 */
HAL_NVIC_EnableIRQ(ADC_INJ_IRQ);
}
/* ADC_Init() 末尾启动注入中断等待 TIM1_CC4 */
HAL_ADCEx_InjectedStart_IT(&ADC_Handle);
要点:
- Continuous = DISABLE :每个 PWM 周期由 TIM1_CC4 触发一次注入序列,不是后台不停转。
- Scan = ENABLE:一次触发按 Rank1→Rank2 顺序采完。
- 规则组 ExternalTrig 关掉:电流路径只认注入触发。
(3)注入通道表:adc_inj_program_pair()(核心)
这里把「注入」落到寄存器:指定 触发源 = TIM1_CC4 上升沿,并配置两路 Rank:
c
/* bsp_adc.c → adc_inj_program_pair() */
static void adc_inj_program_pair(uint8_t pair)
{
ADC_InjectionConfTypeDef adc_inj = {0};
uint32_t ch1, ch2;
if (pair == 1U) { /* VW */
ch1 = CURR_V_ADC_CHANNEL; ch2 = CURR_W_ADC_CHANNEL;
} else if (pair == 2U) { /* WU */
ch1 = CURR_W_ADC_CHANNEL; ch2 = CURR_U_ADC_CHANNEL;
} else { /* UV,默认 */
ch1 = CURR_U_ADC_CHANNEL; ch2 = CURR_V_ADC_CHANNEL;
pair = 0U;
}
/* ------ Rank1:第一相 ------ */
adc_inj.InjectedChannel = ch1;
adc_inj.InjectedRank = ADC_INJECTED_RANK_1;
adc_inj.InjectedNbrOfConversion = 2; /* 一次触发采 2 路 */
adc_inj.InjectedSamplingTime = ADC_SAMPLETIME_15CYCLES;
adc_inj.ExternalTrigInjecConvEdge = ADC_EXTERNALTRIGINJECCONVEDGE_RISING;
adc_inj.ExternalTrigInjecConv = ADC_EXTERNALTRIGINJECCONV_T1_CC4; /* 与 CH4 对齐 */
adc_inj.AutoInjectedConv = DISABLE;
adc_inj.InjectedDiscontinuousConvMode = DISABLE;
adc_inj.InjectedOffset = 0; /* 零偏在软件里减,不用硬件 Offset */
HAL_ADCEx_InjectedConfigChannel(&ADC_Handle, &adc_inj);
/* ------ Rank2:第二相(触发/采样时间等沿用上面配置)------ */
adc_inj.InjectedChannel = ch2;
adc_inj.InjectedRank = ADC_INJECTED_RANK_2;
HAL_ADCEx_InjectedConfigChannel(&ADC_Handle, &adc_inj);
s_inj_pair = pair; /* 供 JEOS 与 FreeMASTER g_fm_sample_pair 使用 */
}
字段对照(读代码时对着看):
| 配置项 | 本课取值 | 作用 |
|---|---|---|
ExternalTrigInjecConv |
T1_CC4 |
谁触发注入 |
ExternalTrigInjecConvEdge |
Rising | 用比较匹配上升沿 |
InjectedNbrOfConversion |
2 |
一拍采两相 |
InjectedRank 1 / 2 |
两相通道 | 结果进两个注入数据寄存器 |
InjectedSamplingTime |
15 cycles | 建立时间,与 ADC 时钟一起决定单次耗时 |
InjectedOffset |
0 |
偏置在软件 Offset−ADC 做 |
扇区变化时只改通道、不改触发源:
c
/* MODE=1:扇区跨采样对才重配,避免每拍狂写 ADC */
pair = adc_sector_to_pair(sector_1_to_6);
if (pair != s_inj_pair)
adc_inj_program_pair(pair);
两路采完后的读数入口(与配置成对出现):
c
/* HAL_ADCEx_InjectedConvCpltCallback */
raw1 = HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_1);
raw2 = HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_2);
/* 再按 s_inj_pair 做 Offset−ADC + 第三相重构 */
完整文件:User/adc/bsp_adc.c(adc1_inj_init / adc_inj_program_pair)、User/tim/bsp_motor_tim.c(TIM1_CH4)。
三、扇区 ↔ 采样相对照表
3.1 三相电流重构公式矩阵
与官方 R3_2_GetPhaseCurrents 一致(扇区号 1~6):
| SVPWM 扇区 | 实测两相 | 注入 pair 编码 | 重构 |
|---|---|---|---|
| 4, 5 | Ia, Ib(U, V) | 0 = UV |
Ic = −Ia − Ib |
| 6, 1 | Ib, Ic(V, W) | 1 = VW |
Ia = −Ib − Ic |
| 2, 3 | Ia, Ic(U, W) | 2 = WU |
Ib = −Ia − Ic |
极性一律:
I p h a s e = O f f s e t p h a s e − A D C r a w \] \[I_{\\mathrm{phase}} = \\mathrm{Offset}_{\\mathrm{phase}} - \\mathrm{ADC}_{\\mathrm{raw}}\] \[Iphase=Offsetphase−ADCraw
3.2 极性修正与硬件偏置推导
由于运放电路采用下桥臂差分/单端偏置放大结构(偏置点通常设为 V r e f / 2 ≈ 1.65 V V_{\mathrm{ref}}/2 \approx 1.65\mathrm{V} Vref/2≈1.65V):
- 当相电流为零时, V a d c = V o f f s e t V_{\mathrm{adc}} = V_{\mathrm{offset}} Vadc=Voffset;
- 当流向电机绕组的相电流为正(上桥导通,低边流出)时,分流电阻上的采样电压降下降,运放输出下降: V a d c < V o f f s e t V_{\mathrm{adc}} < V_{\mathrm{offset}} Vadc<Voffset;
- 当相电流为负时,运放输出上升: V a d c > V o f f s e t V_{\mathrm{adc}} > V_{\mathrm{offset}} Vadc>Voffset。
因此,真实物理相电流计算公式严格定义为: I p h a s e = ( O f f s e t p h a s e − A D C r a w _ p h a s e ) × K a m p I_{\mathrm{phase}} = \left( \mathrm{Offset}{\mathrm{phase}} - \mathrm{ADC}{\mathrm{raw\phase}} \right) \times K{\mathrm{amp}} Iphase=(Offsetphase−ADCraw_phase)×Kamp
四、程序设计(本章代码实现详解)
4.1 目录与变更焦点
User/
├── foc/
│ └── bsp_motor_params.h ★ FOC_ADC_SAMPLE_MODE 开关
├── adc/
│ ├── bsp_adc.h ★ 扇区 API / 调试读数声明
│ └── bsp_adc.c ★ 本章主要位置:扇区表、改 JSQR、JEOS 重构
├── foc/bsp_foc_boot.c 启动 SVPWM 后调用 adc_curr_set_sector
├── pid/bsp_pid_app.c Run 态 dq_apply_svpwm_output 同调扇区
└── foc/bsp_svpwm.c 输出 Sector(1~6),算法与 上一课 相同
控制链路:
#mermaid-svg-dSImywUzmDO6hpNM{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-dSImywUzmDO6hpNM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dSImywUzmDO6hpNM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dSImywUzmDO6hpNM .error-icon{fill:#552222;}#mermaid-svg-dSImywUzmDO6hpNM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dSImywUzmDO6hpNM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dSImywUzmDO6hpNM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dSImywUzmDO6hpNM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dSImywUzmDO6hpNM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dSImywUzmDO6hpNM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dSImywUzmDO6hpNM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dSImywUzmDO6hpNM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dSImywUzmDO6hpNM .marker.cross{stroke:#333333;}#mermaid-svg-dSImywUzmDO6hpNM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dSImywUzmDO6hpNM p{margin:0;}#mermaid-svg-dSImywUzmDO6hpNM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dSImywUzmDO6hpNM .cluster-label text{fill:#333;}#mermaid-svg-dSImywUzmDO6hpNM .cluster-label span{color:#333;}#mermaid-svg-dSImywUzmDO6hpNM .cluster-label span p{background-color:transparent;}#mermaid-svg-dSImywUzmDO6hpNM .label text,#mermaid-svg-dSImywUzmDO6hpNM span{fill:#333;color:#333;}#mermaid-svg-dSImywUzmDO6hpNM .node rect,#mermaid-svg-dSImywUzmDO6hpNM .node circle,#mermaid-svg-dSImywUzmDO6hpNM .node ellipse,#mermaid-svg-dSImywUzmDO6hpNM .node polygon,#mermaid-svg-dSImywUzmDO6hpNM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dSImywUzmDO6hpNM .rough-node .label text,#mermaid-svg-dSImywUzmDO6hpNM .node .label text,#mermaid-svg-dSImywUzmDO6hpNM .image-shape .label,#mermaid-svg-dSImywUzmDO6hpNM .icon-shape .label{text-anchor:middle;}#mermaid-svg-dSImywUzmDO6hpNM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dSImywUzmDO6hpNM .rough-node .label,#mermaid-svg-dSImywUzmDO6hpNM .node .label,#mermaid-svg-dSImywUzmDO6hpNM .image-shape .label,#mermaid-svg-dSImywUzmDO6hpNM .icon-shape .label{text-align:center;}#mermaid-svg-dSImywUzmDO6hpNM .node.clickable{cursor:pointer;}#mermaid-svg-dSImywUzmDO6hpNM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dSImywUzmDO6hpNM .arrowheadPath{fill:#333333;}#mermaid-svg-dSImywUzmDO6hpNM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dSImywUzmDO6hpNM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dSImywUzmDO6hpNM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dSImywUzmDO6hpNM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dSImywUzmDO6hpNM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dSImywUzmDO6hpNM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dSImywUzmDO6hpNM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dSImywUzmDO6hpNM .cluster text{fill:#333;}#mermaid-svg-dSImywUzmDO6hpNM .cluster span{color:#333;}#mermaid-svg-dSImywUzmDO6hpNM 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-dSImywUzmDO6hpNM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dSImywUzmDO6hpNM rect.text{fill:none;stroke-width:0;}#mermaid-svg-dSImywUzmDO6hpNM .icon-shape,#mermaid-svg-dSImywUzmDO6hpNM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dSImywUzmDO6hpNM .icon-shape p,#mermaid-svg-dSImywUzmDO6hpNM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dSImywUzmDO6hpNM .icon-shape .label rect,#mermaid-svg-dSImywUzmDO6hpNM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dSImywUzmDO6hpNM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dSImywUzmDO6hpNM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dSImywUzmDO6hpNM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-dSImywUzmDO6hpNM .stage>*{fill:#f0f4f8!important;stroke:#94a3b8!important;stroke-width:1px!important;color:#1e293b!important;}#mermaid-svg-dSImywUzmDO6hpNM .stage span{fill:#f0f4f8!important;stroke:#94a3b8!important;stroke-width:1px!important;color:#1e293b!important;}#mermaid-svg-dSImywUzmDO6hpNM .stage tspan{fill:#1e293b!important;} ④ 中断处理 和 算法
③ 硬件触发 和 采样
② ADC 通道配置
① SVPWM 扇区计算
svpwm_calc
→ Sector
adc_curr_set_sector
adc_inj_program_pair
改下一拍通道
TIM1_CC4 触发
ADC1 注入 Rank1/2
InjectedConvCpltCallback
I = Offset − ADC
第三相重构
dq_current_loop_hf
4.2 模式宏 FOC_ADC_SAMPLE_MODE
定义于 bsp_motor_params.h,默认 = 1(本章主路径):
c
/* 0 = 固定 U+V,W=-U-V(lesson20 回归)
* 1 = 按扇区切换注入双相 + 第三相重构(本章) */
#ifndef FOC_ADC_SAMPLE_MODE
#define FOC_ADC_SAMPLE_MODE 1
#endif
| 取值 | adc_curr_set_sector |
JEOS 取数 |
|---|---|---|
| 0 | 直接 return,不改通道 | 强制 pair=0(UV),W=-U-V |
| 1 | 扇区→pair,变化时 adc_inj_program_pair |
按 s_inj_pair 分支重构 |
调试建议:先 MODE=0 确认能按 L20 转;再切 MODE=1 对比高调制时 g_fm_iq_actual。
4.3 扇区 → 采样对:adc_sector_to_pair()
c
/* 4/5→UV(0),6/1→VW(1),2/3→WU(2) */
static uint8_t adc_sector_to_pair(uint8_t sector_1_to_6)
{
switch (sector_1_to_6)
{
case 6U:
case 1U: return 1U; /* V+W */
case 2U:
case 3U: return 2U; /* W+U */
case 4U:
case 5U:
default: return 0U; /* U+V */
}
}
与 第三章 表格一一对应。
4.4 运行中改注入通道:adc_inj_program_pair()
运行中改的是 下一拍两个模拟通道,触发仍固定为 TIM1_CC4。
ADC1 配置为 2 路注入 ,触发源 TIM1_CC4 上升沿(PWM 周期中点附近):
| pair | Rank1 通道 | Rank2 通道 | 引脚 |
|---|---|---|---|
| 0 UV | CH3 (U) | CH4 (V) | PA3 / PA4 |
| 1 VW | CH4 (V) | CH6 (W) | PA4 / PA6 |
| 2 WU | CH6 (W) | CH3 (U) | PA6 / PA3 |
核心动作:对注入 rank 调用 HAL_ADCEx_InjectedConfigChannel,更新 s_inj_pair。
上电默认 adc_inj_program_pair(0)(UV),与官方校准常用 SECTOR_4 一致。
4.5 扇区下发:adc_curr_set_sector()
c
void adc_curr_set_sector(uint8_t sector_1_to_6)
{
s_prog_sector = sector_1_to_6; /* 记下「下一拍」扇区 */
#if (FOC_ADC_SAMPLE_MODE == 0)
return; /* L20:永不改通道 */
#else
pair = adc_sector_to_pair(sector_1_to_6);
if (pair != s_inj_pair) /* 仅在采样对变化时改 JSQR,减寄存器抖动 */
adc_inj_program_pair(pair);
#endif
}
调用点(SVPWM 写 CCR 之后立刻更新采样意图):
bsp_pid_app.c→dq_apply_svpwm_output():正常双环 Runbsp_foc_boot.c→ 启动阶段开环 SVPWM
c
g_fm_svpwm_sector = (float)svpwm.Sector;
adc_curr_set_sector(svpwm.Sector);
本拍 / 下一拍为何要分开?
| 变量 | 含义 |
|---|---|
s_prog_sector |
SVPWM 刚算完 的扇区 → 配置 下一拍 注入通道 |
s_samp_sector |
JEOS 时刻锁存:s_samp_sector = s_prog_sector,表示 本拍数据 对应的扇区 |
避免「本拍还在采旧通道,却用新扇区公式解算」的错位。调试可读:
adc_curr_get_sample_sector()→s_samp_sectoradc_curr_get_sample_pair()→0/1/2
4.6 JEOS 回调:取数 + 重构 + 进电流环
HAL_ADCEx_InjectedConvCpltCallback(ADC1)流程:
1. raw1 = Injected Rank1,raw2 = Rank2
2. s_samp_sector = s_prog_sector;pair = s_inj_pair(MODE=0 强制 0)
3. 按 pair 分支:
pair0:Iu,Iv = f(raw);Iw = −Iu−Iv
pair1:Iv,Iw = f(raw);Iu = −Iv−Iw
pair2:Iw,Iu = f(raw);Iv = −Iw−Iu
4. 校准未完成则 adc_cal_push;完成则 I = Offset − ADC
5. 若已校准且电流环使能且非 boot 阶段 → dq_current_loop_hf()
极性换算(与 lesson20 / 官方一致):
c
/* I(mA) = counts_to_ma(Offset − raw) */
return adc_counts_to_ma((int32_t)cal->adc_offset - (int32_t)raw);
对外接口:adc_get_phase_currents_ma() / get_curr_val_u/v/w() 读 last_curr_*_ma。
4.7 三相 Offset 校准
| 阶段 | 行为 |
|---|---|
adc_curr_recalibrate() |
清三相 cal;强制 pair=UV;扇区记为 4 |
| 稳定等待 | ADC_CURR_CAL_SETTLE_MS(800 ms)内不累计 |
| 累计 | 每相至少 ADC_CURR_CAL_SAMPLES(64)次 raw 取均值 → adc_offset |
| W 占位 | 若 U/V 已完成而 W 尚未直接采到:Offset_W = (Offset_U+Offset_V)/2,标记 done;运行中采到 W 再精修 |
adc_curr_is_calibrated():三相 cal_done 皆 1。
调试:adc_dbg_log_offsets() 打印 Offset / raw / mode / pair。
4.8 与电流环、启动的衔接
svpwm_calc dq_current_loop_hf JEOS 回调 ADC1 注入 TIM1 PWM+CC4 svpwm_calc dq_current_loop_hf JEOS 回调 ADC1 注入 TIM1 PWM+CC4 #mermaid-svg-DZBvxkpGl1GNb3oq{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-DZBvxkpGl1GNb3oq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DZBvxkpGl1GNb3oq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DZBvxkpGl1GNb3oq .error-icon{fill:#552222;}#mermaid-svg-DZBvxkpGl1GNb3oq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DZBvxkpGl1GNb3oq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DZBvxkpGl1GNb3oq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DZBvxkpGl1GNb3oq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DZBvxkpGl1GNb3oq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DZBvxkpGl1GNb3oq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DZBvxkpGl1GNb3oq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DZBvxkpGl1GNb3oq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DZBvxkpGl1GNb3oq .marker.cross{stroke:#333333;}#mermaid-svg-DZBvxkpGl1GNb3oq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DZBvxkpGl1GNb3oq p{margin:0;}#mermaid-svg-DZBvxkpGl1GNb3oq .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DZBvxkpGl1GNb3oq text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-DZBvxkpGl1GNb3oq .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DZBvxkpGl1GNb3oq .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-DZBvxkpGl1GNb3oq .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-DZBvxkpGl1GNb3oq .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-DZBvxkpGl1GNb3oq #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-DZBvxkpGl1GNb3oq .sequenceNumber{fill:white;}#mermaid-svg-DZBvxkpGl1GNb3oq #sequencenumber{fill:#333;}#mermaid-svg-DZBvxkpGl1GNb3oq #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-DZBvxkpGl1GNb3oq .messageText{fill:#333;stroke:none;}#mermaid-svg-DZBvxkpGl1GNb3oq .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DZBvxkpGl1GNb3oq .labelText,#mermaid-svg-DZBvxkpGl1GNb3oq .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-DZBvxkpGl1GNb3oq .loopText,#mermaid-svg-DZBvxkpGl1GNb3oq .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-DZBvxkpGl1GNb3oq .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DZBvxkpGl1GNb3oq .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DZBvxkpGl1GNb3oq .noteText,#mermaid-svg-DZBvxkpGl1GNb3oq .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-DZBvxkpGl1GNb3oq .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DZBvxkpGl1GNb3oq .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DZBvxkpGl1GNb3oq .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DZBvxkpGl1GNb3oq .actorPopupMenu{position:absolute;}#mermaid-svg-DZBvxkpGl1GNb3oq .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-DZBvxkpGl1GNb3oq .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DZBvxkpGl1GNb3oq .actor-man circle,#mermaid-svg-DZBvxkpGl1GNb3oq line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-DZBvxkpGl1GNb3oq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 下一拍按新扇区采 CC4 触发注入 Rank1/2 完成 Offset−ADC + 第三相重构 Clark/Park/PI/RevPark Vαβ → Sector + CCR adc_curr_set_sector(Sector)
- Charge / Offset:TIM1 必须继续跑,CC4 才能触发注入(与 L20 相同)。
- boot 阶段 :
foc_boot_is_active()为真时 JEOS 不进dq_current_loop_hf,避免启动矢量被闭环抢。 - Run :校准完成 +
dq_curr_is_enabled()→ 每 PWM 周期一次电流环。
4.9 关键 API 一览
| API | 作用 |
|---|---|
adc_curr_set_sector(1~6) |
按扇区配置下一拍采样对 |
adc_curr_get_sample_sector() |
本拍扇区 |
adc_curr_get_sample_pair() |
本拍 pair:0=UV 1=VW 2=WU |
adc_get_phase_currents_ma() |
读重构后三相 (mA) |
adc_curr_recalibrate() |
重做零偏(停转) |
adc_dbg_log_offsets() |
串口打印 Offset/raw/mode/pair |
adc_dbg_get_last_raw_u/v/w() |
FreeMASTER 可观察 raw |
4.10 FreeMASTER / 串口建议观察量
| 变量 | 看什么 |
|---|---|
g_fm_svpwm_sector |
1~6 是否连续循环 |
g_fm_iu_ma / iv / iw |
重构后三相之和应接近 0 |
g_fm_iq_actual vs g_fm_target_iq |
高调制下是否仍跟随 |
adc_curr_get_sample_pair() |
扇区变化时是否真切到 0/1/2 |
| θ 锯齿、RPM | 与 L20 同级,确认闭环未回退 |
重点:θ 锯齿、RPM 跟速、Iq 跟随;三相电流看「和≈0」、加速时无大面积掉零。

g_fm_sample_pair: 本拍采哪两相(UV / VW / WU),在 0、1、2 之间反复切换,MODE=1 在按扇区换注入通道。
| 扇区 | pair |
|---|---|
| 4、5 | 0(UV) |
| 6、1 | 1(VW) |
| 2、3 | 2(WU) |
五、实验步骤
5.1 回归(MODE=0)
bsp_motor_params.h 设 FOC_ADC_SAMPLE_MODE=0,编译烧录:
s 0 → 等校准
r
t 500
w 1
现象应与 lesson20 同级:RPM 爬到目标,θ 连续。可用 adc_dbg_log_offsets() 确认 mode=0、pair=0。
5.2 扇区采样(MODE=1,默认)
- 确认
FOC_ADC_SAMPLE_MODE=1后编译烧录。 - 同上启动;观察扇区变化时
pair在 0/1/2 间切换,三相电流无异常跳变。 - 提高目标转速或加载,对比 MODE=0:Iq 不应周期性塌到接近 0。
- 停机:
w 0/s 0。
t 400 时曲线图(mode=1):

六、小结与下一章
本章把电流反馈从「能转的固定双相」推进到「按 SVPWM 扇区选窗的 R3_2 风格采样」:
FOC_ADC_SAMPLE_MODE可在 L20 基线与扇区模式间切换;adc_curr_set_sector→adc_inj_program_pair在运行中改注入通道;- JEOS 按 pair 做 Offset−ADC 与第三相重构,再进
dq_current_loop_hf; - 本拍/下一拍扇区分离,避免通道与解算错位。
为后面更高调制、更重负载和无感打下采样基础。
实现检查清单:
FOC_ADC_SAMPLE_MODE文档化,默认可切到扇区模式adc_curr_set_sector()按扇区配置下一拍采样相- JEOS 回调按扇区取数 + 第三相重构
- U/V Offset 独立;W 占位后运行中补齐
- MODE=0 / MODE=1 实机均可稳定
t 500/w 1