
做电机控制的朋友应该都对这些场景不陌生PWM中心对齐、定时器触发ADC采样、注入组和规则组穿插使用。前阵子我在STM32G4上做三相PMSM控制器就撞上了一个非常典型的ADC注入不连续模式问题。配置看起来完全没毛病PWM触发、注入组、不连续模式每个寄存器都按参考手册填了结果ADC要么一次触发把四个注入通道全转完要么转完一轮后就罢工愣是查了一整天最后才搞清楚论坛里说的“Hard bit setting required”究竟是什么意思。这篇就把这个问题的完整排查过程、G4注入不连续模式的原理以及最终可复现的配置方案整理出来。想用STM32G4做FOC、数字电源或者多通道注入采样的朋友这篇应该能帮你省下不少调试时间。就算你已经跳过了这个坑也建议看看第3章的排查思路类似的触发类问题以后多半还会遇到。1. 现象与需求从一次FOC采样异常说起1.1 项目背景与硬件配置我这块板子用的是STM32G431CBT6主控做一套低压PMSM的FOC驱动。电流环频率设定在20kHzTIM1工作在中心对齐PWM模式用TIM1的TRGO2作为ADC1的外部触发源这样电流采样点天然就落在PWM中点谐波小、信噪比高。规则组负责同步采集两相电流这部分用DMA循环搬运跑得很稳。问题出在注入组。FOC除了两相电流还需要实时监测母线电压、IGBT温度、定子温度这些辅助信号。我一开始的想法很简单把这些低速信号放到注入组里利用注入组优先级高于规则组的特点让它们定期“插入”到电流采样之间。这样规则组的高速电流环不受影响辅助信号也能得到更新。1.2 配置思路注入组 不连续模式这里有个细节。如果我只把母线电压、温度和备用通道全部塞进注入序列然后让每个PWM周期都启动一次注入转换结果就是这3到4个通道会在同一个PWM周期内连续转换。这会产生两个问题一是多个通道共用同一个采样时刻点相当于母线电压、温度和电流都在同一瞬间被采样对于温度这类慢变量问题不大但对于母线电压这种可能含有高频纹波的信号来说采样点固定在一个相位上反而容易引入偏差二是注入组转换时会打断规则组的电流采样连续转4个通道的时间会占用相当大一段窗口对20kHz的电流环来说不够友好。所以我选择了注入组不连续模式JDISCEN1。配置JECEI为0也就是每个外部触发边沿只启动1个注入通道的转换4个通道分4个PWM周期依次转完。这样每个周期只占用一个很短的注入转换窗口而且4个辅助信号天然被分散到4个不同的相位点采样对后续做信号平均和滤波反而更有利。1.3 问题表现三种诡异的ADC行为代码写完后上电我预期看到的现象是JDR1到JDR4依次更新每个PWM周期只刷新其中一个。但实际观测到的现象却分成了三种而且随配置细节不同而不同。第一种第一次PWM触发到来后注入组一口气把JSQ1到JSQ4全部转换完随后注入组彻底“沉默”之后再来触发也没反应。第二种第一次触发后只转换了JSQ1之后无论来多少个触发边沿JDR1都不再更新也没有JEOC中断。第三种最隐蔽注入组转换完一轮后JEOC确实置位了但如果你按数据手册用软件方式重新触发JSWTRIG发现根本启动不了下一轮。这三种现象表面上看完全不像同一个原因但实际上都指向同一个环节注入组的“武装-触发-转换-重新武装”闭环没有闭合。后面我会详细说这个闭环是什么。2. 原理拆解注入组、不连续模式与触发闭环2.1 注入组为什么会“打断”规则组在开始讲不连续模式之前先把注入组区别于规则组的底层逻辑说清楚。STM32G4的ADC有两个独立的转换序列规则组Regular group和注入组Injected group。规则组就是我们平时最常用的扫描序列适合一轮一轮地转换多个通道。注入组则是一个最多4个通道的高优先级序列它的设计初衷就是“临时插入”一次转换。用生活化的比喻来说规则组就像生产线上的常规质检流程按部就班地循环检查每一个产品。注入组则像是巡检员手里的“红牌”随时可以拦住生产线插队检查一个重点物件检查完生产线继续跑。ADC硬件上也是如此注入组触发一旦到来如果此时规则组正在转换规则组会被中止等注入组转换完成后再从上次中断的位置恢复。这种打断机制在电机控制里的价值不言而喻——你可以让最关键的两个相电流采样规则化进行同时又能在特定时刻插入母线电压或温度的测量。但注意注入组的插入也意味着规则组转换窗口会被“挤占”。如果你的注入组一次触发要连续转换4个通道那规则组被中止的时间就会明显拉长。这也是我一开始坚持要用不连续模式的原因——把一次注入拆成多次触发每次只占用一个转换窗口。2.2 不连续模式的真实行为JDISCEN与JECEI不连续模式Discontinuous mode在很多人的理解里就是“触发一次转一个通道”这个说法对了一半。准确地说它控制的是“一次触发最多转换几个通道”。STM32G4的ADC_CFGR里有JDISCEN位用来使能注入组不连续模式。而JECEI位段则指定每次触发实际转换的通道个数编码方式是“通道数减1”JECEI0表示1个通道JECEI1表示2个通道依此类推。所以当你启用JDISCEN并设置JECEI0时注入组的行为就是第一个上升沿到来序列指针指向JSQ1只转换JSQ1通道第二个上升沿到来指针前进到JSQ2只转换JSQ2通道。这就是我想要的“一个PWM周期只采一个辅助信号”的效果。这里有一个新手很容易犯的认知错误。很多人以为设置了不连续模式之后软件触发JSWTRIG一旦置位就会“自动”按节奏轮流触发序列中的每个通道。实际上不是。不连续模式定义的是“单次触发后的转换个数”它并没有定义“下一个触发从哪里继续”。真正驱动序列指针逐次前进的是连续到来的触发边沿。没有边沿指针就停在原地ADC不会自己去推进。2.3 注入组序列结束后的“冻结”JQM与JQDIS当4个通道依次转换完毕也就是注入序列走到了末尾此时如果没有额外机制注入组会进入一个“队列禁用JQDIS”状态。这是G4系列ADC比较新的设计注入序列转换完成后整个注入组的触发链路会被冻结必须重新“武装”才能开始下一轮。控制这个行为的位是ADC_CFGR里的JQMInjected Queue Mode。JQM0时队列模式关闭序列结束后JQDIS被置1注入组冻结直到你手动清除JQDIS或者重新配置JSQR。JQM1时队列模式开启序列完成一轮后自动回到队首JQDIS不会被置位相当于注入组始终处于“待命”状态。对于FOC这种需要周期性持续注入采样的场景JQM必须设为1否则你每4个PWM周期就要处理一次“冻结”此时要么在中断里写JSQR重新武装要么清除JQDIS麻烦且容易出bug。我在1.3节里看到的第二种现象——转完一轮后彻底罢工就是典型的JQM0导致队列冻结。2.4 为什么G4的注入组比F1/F4更容易踩坑很多从STM32F1或F4转到G4的工程师会在这里栽跟头因为老型号的ADC注入组逻辑要简单得多。F1的注入组转换完成后只要外部触发还能来它就会一直响应不存在“队列冻结”一说。F4则可以通过配置JEXTEN来自动重新武装但它的触发链路相对宽松没那么容易出现“配了但不动”的窘境。G4在ADC这块加入了很多新特性可配置的自动延迟转换、注入队列模式、独立的JECEI位段、更大的触发源选择表等等。这些特性本质上是把ADC的调度逻辑做得更精细、更灵活但也让寄存器的耦合关系变得更强。换句话说G4的ADC更像一个“小状态机”很多控制位之间是有关联的不是孤立地配置某一个位就能生效。第3章里的所谓“Hard bit”正是这种状态机耦合关系下被逼出来的硬性要求。3. 定位过程“Hard bit”到底需要设置什么3.1 第一步确认触发边沿究竟有没有进来排查这类问题我一般不会直接去翻寄存器配置而是先确认物理层面的触发信号有没有送到ADC。因为G4支持很多种触发源JEXTSEL位段又很长选错触发源这种事太常见了。当时我用调试器的逻辑分析仪功能同时观测PWM输出和ADC的触发事件确认TIM1的TRGO2边沿确实在每个PWM周期都会出现频率与设定值吻合。确认完触发源后我再回头查寄存器。这里有个实用的技巧直接在调试器里实时查看ADC1-JSQR的JEXTSEL位段和JEXTEN位段确认它们与目标触发源一致。很多时候问题就出在这里——你以为是TRGO2但代码里JEXTSEL对应的编号却是另一个定时器事件。3.2 第二步观察JQDIS标志判断队列状态接下来我在ADC中断里加了一个断点每次触发都停下来检查两个关键标志ADC_ISR里的JEOC注入转换结束和JQDIS注入队列禁用。这一步能很快把问题分成两类一类是注入组从头到尾就没有正确启动另一类是注入组启动了但转换完一轮后陷入冻结。实测时我看到的标志状态非常有规律第一轮JEOC正常置位JDR寄存器依次更新但第二轮触发时JQDIS已经变成1注入组不再响应。这就直接证实了“队列冻结”的判断——JQM没有正确配置导致4个通道转完后注入组的触发链路被硬件锁住。继续往下查我还发现了一个更隐蔽的细节即使把JQM设为1如果JEXTEN位段为00也就是外部触发被禁用注入组依然存在无法重新武装的情况。3.3 第三步验证“仅软件触发”的陷阱说到这里必须提一个我在实际项目中反复确认过的经验在STM32G4上注入组的不连续模式对触发边沿有很强的依赖如果你完全依赖软件触发JSWTRIG并且把JEXTEN设成00那么即便JQM1也可能出现首次转换正常、后续无法持续触发的情况。这不是凭空猜测。在ST官方社区里关于G4注入不连续模式的帖子不在少数很多人最后查出来的根因都是JEXTEN必须非0。我之前也翻过参考手册JEXTEN的字面描述确实只是“选择外部触发的边沿类型”看不出它和注入组“武装”有什么关系。但实测下来G4的注入组触发状态机似乎依赖JEXTEN非零来维持一种“可触发”状态。所以我们最终在产品代码里做了一个非常保守的选择无论如何都让JEXTEN处于非零状态配合JEXTSEL选一个不会误触发的触发源或者直接用定时器触发。这也是我会把这个问题概括为“Hard bit setting required”的原因。所谓“硬位”指的不是某个神秘的隐藏寄存器而是在实际工程中必须硬性满足的配置组合JDISCEN使能不连续、JECEI指定每次转换数、JQM开启队列自动重装、JEXTEN非零维持触发链路。缺一个注入组的状态机就可能在某个环节卡死。3.4 定位结论必须满足的四个配置条件把所有这些实验和排查收敛一下在STM32G4上使用注入组不连续模式至少要满足下面四个条件配置项推荐值说明JDISCEN1使能注入组不连续模式让序列按触发逐次推进JECEI0每次触发只转换1个注入通道实现“一阵一通道”JQM1注入队列自动重装避免序列完成后队列冻结JEXTEN01或10保持外部触发链路接通维持注入组可“重新武装”状态这四条全部满足后我板子上的注入组终于按照预期工作每个PWM周期只转换一个辅助通道4个通道轮流更新JEOC在每个通道转换完后都会产生一次中断。让我觉得比较踏实的是这个结论不是只针对我的板子。后来我同事在G474上做数字电源用的也是这个配置组合同样一次通过。4. 完整可复现方案配置代码与验证方法4.1 定时器侧配置让TRGO2产生正确边沿先看定时器侧的配置。我使用TIM1的TRGO2它在G4上通过MMS2位段选择。为了让ADC在PWM中心点附近触发注入转换我可以选择“更新事件”或者“OC4REF”等作为TRGO2的来源具体看需要把采样点落在哪个时刻。为了采样点尽量居中且稳定我一般建议把触发边沿与PWM更新事件或比较通道对齐并且留出足够的采样保持时间。下面是使用STM32CubeIDE和HAL库时的TIM1初始化片段。重点是CR2寄存器的MMS2位段别和传统TIM1的MMS位段搞混// TIM1 PWM初始化中心对齐模式 htim1.Init.Period 2400 - 1; // 20kHz主频170MHz中心对齐 htim1.Init.Prescaler 0; htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim1); // 配置TIM1的TRGO2为更新事件用于触发ADC // G4的TIM_CR2_MMS2位段具体值请参考目标芯片头文件 TIM1-CR2 ~TIM_CR2_MMS2; TIM1-CR2 | (0x1U TIM_CR2_MMS2_Pos); // 更新事件作为TRGO2输出注意如果你在项目里同时使用HAL_TIM_PWM_Start和TIM1-CR2务必确认这两者不会冲突。最好的做法是先用CubeMX把TRGO2配好再把PWM启动避免初始化顺序导致的配置覆盖。4.2 ADC1基础配置与注入组寄存器设置ADC侧我采用“HAL库初始化基础时钟、寄存器配置关键细节”的混合方式。因为HAL库对注入组不连续模式的支持很有限强行用HAL的注入API反而很难配出我们需要的组合。下面这段代码是我实际工程里提炼出来的核心部分直接放在ADC使能之后、注入采样启动之前。// 1) 等待ADC就绪后先做校准略然后配置注入组JSQR // 假定使用ADC1的注入组序列长度为4触发源为TIM1_TRGO2上升沿触发 ADC1-JSQR 0; // 序列长度JL3表示4个注入转换JL编码为长度减1 ADC1-JSQR | (ADC_JSQR_JL_1 | ADC_JSQR_JL_0); // 设置触发源编号请参考RM0440中ADC触发映射表按目标触发源填入 ADC1-JSQR | (INJ_TRIG_SEL_TIM1_TRGO2 ADC_JSQR_JEXTSEL_Pos); // JEXTEN 01上升沿触发 ADC1-JSQR | (0x1U ADC_JSQR_JEXTEN_Pos); // 4个注入通道的编号按实际硬件连接填入 // 我这里是JSQ1母线电压(ADC_IN9)JSQ2驱动温度(ADC_IN3)JSQ3绕组温度(ADC_IN4)JSQ4备用(ADC_IN5) ADC1-JSQR | (9U ADC_JSQR_JSQ1_Pos); ADC1-JSQR | (3U ADC_JSQR_JSQ2_Pos); ADC1-JSQR | (4U ADC_JSQR_JSQ3_Pos); ADC1-JSQR | (5U ADC_JSQR_JSQ4_Pos); // 2) 关键配置不连续模式 队列模式 每次触发转换1个通道 ADC1-CFGR | ADC_CFGR_JDISCEN; // 注入组不连续模式 ADC1-CFGR | ADC_CFGR_JQM; // 注入队列自动重新武装 // JECEI默认值为0即每次触发转换1个注入通道正好满足需求 // 如果确实想每次转2个通道再把JECEI设为1 // 3) 使能注入转换结束中断 ADC1-IER | ADC_IER_JEOCIE; // 4) 使能NVIC中断服务函数中处理JEOC HAL_NVIC_SetPriority(ADC1_2_IRQn, 3, 0); HAL_NVIC_EnableIRQ(ADC1_2_IRQn); // 5) 启动ADC HAL_ADC_Start(hadc1);这里要强烈提醒一点INJ_TRIG_SEL_TIM1_TRGO2只是一个语义化宏实际值取决于你用的具体芯片型号和触发映射表。千万不要觉得所有型号都一样G4系列里不同封装、不同ADC序号触发源编号也可能有差异。我在调试时就因为把F4时代的触发源编号直接搬过来导致ADC完全没反应整整排查了一个多小时。最可靠的办法是把芯片头文件里的触发枚举值和参考手册的映射表对照一遍再填进代码。4.3 中断服务函数JEOC处理与序列顺序注入转换结束中断处理其实不难但有个顺序问题值得单独说。G4的JDR1到JDR4并不是固定的“四个通道的数据寄存器”而是“当前注入序列中第一到第四个转换的数据寄存器”。也就是说JDR1对应的是当前这一轮序列中的JSQ1通道JDR2对应JSQ2通道以此类推。如果你在中断里按“JDR1母线电压”这种固定逻辑去读那么在连续多轮转换中数据会一直按照JSQ1到JSQ4的顺序排列同一轮里JDR的编号是稳定的所以这个倒不用太担心。真正需要注意的是清除标志的顺序。我建议先把ISR寄存器整体读到一个局部变量里再判断JEOC最后写入清除。这样做可以避免在读数据和清标志之间又插入新的转换导致标志状态抖动。下面的代码是我在工程里用的精简版void ADC1_2_IRQHandler(void) { uint32_t isr ADC1-ISR; if (isr ADC_ISR_JEOC) { uint16_t jdr[4]; jdr[0] ADC1-JDR1; jdr[1] ADC1-JDR2; jdr[2] ADC1-JDR3; jdr[3] ADC1-JDR4; // 根据序列顺序处理数据 // 例如update_bus_voltage(jdr[0]); update_temp(jdr[1]); ... // 如果JQCEN?不JDR4不可用时需要看JSQR配置 ADC1-ISR ADC_ISR_JEOC; // 清除标志 } // 如果JQM1JQDIS不会正常触发这里不处理 }有朋友可能会问既然不连续模式下每次触发只转换1个通道为什么这里还把JDR1到JDR4都读出来因为硬件上一次注入