电机驱动开发学习21. R3_2 双相扇区电流采样与第三相重构

电机驱动开发学习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 注入配置源码(本课实现))
  • [三、扇区 ↔ 采样相对照表](#三、扇区 ↔ 采样相对照表)
    • [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.cR3_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(注入序列结束)。

含义:

  1. TIM1_CC4 触发后,ADC 按 JSQR 先采 Rank1,再采 Rank2;
  2. 两路都结束后,硬件置位注入序列结束标志(文档/参考手册里的 JEOS);
  3. 若已 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

  1. 时刻要对齐:采样必须落在 PWM 周期固定相位(本课在中点),主循环轮询相位漂移,电流会抖、环会乱。
  2. 延迟要短 :电流环在 ISR 里算,注入结束立刻进 dq_current_loop_hf(),不能等主循环碰巧跑到。
  3. 可与扇区联动:下一拍换扇区时改注入通道,仍由同一 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.cadc1_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 之后立刻更新采样意图):

  1. bsp_pid_app.cdq_apply_svpwm_output():正常双环 Run
  2. bsp_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_sector
  • adc_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.hFOC_ADC_SAMPLE_MODE=0,编译烧录:

复制代码
s 0 → 等校准
r
t 500
w 1

现象应与 lesson20 同级:RPM 爬到目标,θ 连续。可用 adc_dbg_log_offsets() 确认 mode=0pair=0

5.2 扇区采样(MODE=1,默认)

  1. 确认 FOC_ADC_SAMPLE_MODE=1 后编译烧录。
  2. 同上启动;观察扇区变化时 pair 在 0/1/2 间切换,三相电流无异常跳变。
  3. 提高目标转速或加载,对比 MODE=0:Iq 不应周期性塌到接近 0。
  4. 停机:w 0 / s 0

t 400 时曲线图(mode=1):


六、小结与下一章

本章把电流反馈从「能转的固定双相」推进到「按 SVPWM 扇区选窗的 R3_2 风格采样」:

  1. FOC_ADC_SAMPLE_MODE 可在 L20 基线与扇区模式间切换;
  2. adc_curr_set_sectoradc_inj_program_pair 在运行中改注入通道;
  3. JEOS 按 pair 做 Offset−ADC 与第三相重构,再进 dq_current_loop_hf
  4. 本拍/下一拍扇区分离,避免通道与解算错位。

为后面更高调制、更重负载和无感打下采样基础。

实现检查清单:

  • FOC_ADC_SAMPLE_MODE 文档化,默认可切到扇区模式
  • adc_curr_set_sector() 按扇区配置下一拍采样相
  • JEOS 回调按扇区取数 + 第三相重构
  • U/V Offset 独立;W 占位后运行中补齐
  • MODE=0 / MODE=1 实机均可稳定 t 500 / w 1

相关推荐
听雨入夜1 小时前
CMake构建学习笔记-libxml库的构建
笔记·学习
不在逃避q2 小时前
一步一步学习使用LiveBindings()TListView进阶使用(),打造天气预报程序
服务器·数据库·学习
sukalot3 小时前
windows网络适配器驱动开发-双 STA 连接(下)
windows·驱动开发·stm32
知识分享小能手3 小时前
高等数学学习教程,从入门到精通,微分中值定理与导数的应用(5)
学习·机器学习·概率论
520拼好饭被践踏3 小时前
JAVA+Agent学习day25
java·开发语言·学习
魔术师Dix3 小时前
StartGame:Unity TDD 外部工程指南
学习·游戏·unity·c#·测试驱动开发
小弥儿4 小时前
GitHub今日热榜 | 2026-07-31:AI Agent工作流共享赛道升温
人工智能·学习·github
小蒋学算法13 小时前
算法-M个非重叠子数组最大和II-WQS二分学习
数据结构·学习·算法
阿三081215 小时前
AI 重构跨部门协同:如何利用协同平台打通部门信息壁垒
人工智能·重构