第一次在TC377上调FOC电流环的时候,我遇到的第一个诡异问题不是PID参数,而是ADC采样值抖得离谱。电机一转起来,用DA输出观察iq电流波形,毛刺密密麻麻,频谱上一堆开关频率边带。后来用示波器同时抓PWM驱动波形和ADC采样时刻,才确认问题根本不是算法,而是采样点正好砸在了MOS管开关沿的振铃上。
把ADC触发时刻挪到PWM周期中点之后,波形瞬间干净了一个数量级。而这套“PWM中点触发ADC + DMA自动搬运”的方案,正是英飞凌TC3xx系列做电机控制、数字电源时最常用的硬件采样链路。这篇文章基于ILLD库,把整条链路的配置流程、原理解析和调试经验完整梳理一遍,适合刚上手AURIX TC3xx的工程师快速落地,也适合想把采样确定性做好的老手对照查漏。
1. 中点触发的意义:为什么ADC必须避开开关沿
1.1 采样点落在开关沿附近会发生什么
PWM驱动半桥或H桥时,开关管在导通和关断瞬间,母线电压和相电流会出现高频振铃。振铃的频率取决于回路寄生电感、电容和器件寄生参数,典型在几十MHz到几百MHz,幅度可能远大于正常信号。
如果ADC采样窗口刚好覆盖了这个振铃区间,采到的就不再是真实的相电流或电压,而是叠加了振铃分量的“瞬时噪声值”。这种噪声不是白噪声,它和PWM开关事件严格同步,所以在频谱上表现为开关频率及其谐波附近的边带,控制器看到的电流信号会出现周期性扰动。更麻烦的是,这种扰动无法靠加大滤波电容完全消除,因为它发生在采样层面,后端滤波只会进一步恶化相位裕度。
所以电力电子控制里有一个基本共识:采样时刻永远不要靠近开关动作时刻。PWM周期中点往往是距离两个开关沿最远的位置,天然成为最佳采样窗口。
1.2 周期中点为什么对应“平均电流”
对于电感电流纹波,在连续导通模式(CCM)下,电流波形近似为三角波。三角波的平均值正好位于三角形的中线,也就是一个PWM周期的时间中点。因此,在中点时刻采样得到的就是这个开关周期的平均电流。
这一点对电流环设计非常关键。电流环的PI控制器实际希望控制的是“平均电流”而不是某一点的瞬时电流。如果采样点偏前或偏后,采到的瞬时值和平均值存在系统性偏差,尤其在占空比大范围变化时,这种偏差会随工作点漂移,导致控制增益不一致。放在中点采样,本质上解决了“采样值等于控制量”的一致性要求。
需要注意的是,这个结论在电感电流连续时成立。如果工作在DCM(断续导通模式),电流纹波不是完整三角波,中点时刻的采样值和平均值会有偏差,需要根据工况评估是否仍采用中点触发。
1.3 为什么不能靠CPU定时器做触发
新手最容易想到的办法是:PWM中断里启动ADC,或者在定时器中断里软件触发一次转换。这在低速控制场景下能用,但有两个致命问题。
一是中断响应延迟的不确定性。中断请求发出到CPU真正执行ADC启动指令,中间有现场保护、其他中断仲裁、流水线冲刷等开销,这个延迟在不同工况下会抖动,抖动幅度可能达到几微秒。对10kHz的PWM周期来说,几微秒已经占了周期的几个百分点,采样点在周期上的位置根本固定不住。
二是CPU中断会打断控制环的实时性。如果PWM频率到20kHz以上,每个周期都触发中断,CPU大部分时间都花在中断进出上,留给核心控制算法的时间被严重压缩。
硬件触发链路则完全不同。GTM的比较事件到EVADC触发输入,是纯硬件信号路由,延迟固定且极短,抖动在纳秒级。采样点可以精确锁死在PWM周期的任一个相位,不受CPU负载影响。这正是“硬件采样”的核心价值。
2. 整条采样链路的硬件路径:GTM -> EVADC -> DMA
2.1 TC3xx三个外设各自扮演什么角色
TC3xx里实现这条链路涉及三个外设模块:
- GTM(Generic Timer Module):负责产生PWM波形,同时产生一个与PWM周期严格同步的触发脉冲。它是整个时间基准的源头。
- EVADC:增强型逐次逼近ADC。接收GTM送来的触发信号,在指定时刻启动转换,转换完成后结果写入结果寄存器,同时发出DMA请求。
- DMA:在EVADC结果寄存器更新后,自动把结果数据搬到内存数组。整个过程不需要CPU参与,CPU只在初始化时配置好一切,之后可以专心跑控制算法。
三个模块的分工非常清晰:GTM管“何时”,EVADC管“采什么”,DMA管“往哪搬”。
2.2 触发信号如何在芯片内部传输
很多初次接触TC3xx的人会误以为,GTM触发ADC需要把GTM的输出引脚和EVADC的触发引脚在板子上短接。实际上完全不需要,这两个模块在芯片内部已经布好了信号路由。
TC3xx的EVADC每个组都有多个触发源可选,其中就包含来自GTM各子模块的输出信号。例如可以选择某个ATOM通道的输出信号作为触发源,也可以选择TOM或其他定时器结构的信号。通过配置EVADC组的触发源选择寄存器,直接把GTM内部信号接进来,外部引脚零开销。
这意味着触发路径是:GTM ATOM通道的比较匹配事件 -> 内部触发信号 -> EVADC组触发输入 -> 启动转换 -> 转换完成 -> 结果寄存器更新 -> 硬件DMA请求 -> DMA执行搬移。整条路径没有任何软件参与,全部由硬件自动完成。
2.3 为什么需要单独一个ATOM通道做触发脉冲
这里有一个关键设计决策。很多人会想:PWM本身由ATOM通道产生,能不能直接在主PWM通道上配置一个“周期中点事件”触发ADC?
从硬件能力上说,ATOM通道的主要比较值有两个:CM0用于定义周期,CM1用于定义占空比翻转点。如果占空比是50%,CM1恰好在周期中点;但如果占空比不是50%,CM1就不在中点。占空比是我们实时调节的对象,不可能为了采样点把它固定到50%。
所以正确的做法是占用两个ATOM通道:通道0产生正常的PWM,输出到引脚;通道1产生一个周期相同、占空比固定为50%的脉冲,作为触发信号。由于占空比固定50%,它的翻转沿始终落在周期中点,这个沿就是精确的采样触发时刻。
两个通道使用同一个时钟基准和相同的周期值,天然保持同步。这样无论占空比怎么变,触发点都稳定在PWM周期的正中央。
2.4 全程不经过CPU到底值多少钱
这套方案最直接的收益是CPU开销几乎为零。PWM频率10kHz时,每100微秒触发一次ADC,DMA自动搬走结果,整个过程不产生任何中断。只有当你需要应用层感知“数据更新了”时,才选择开一个DMA完成中断。
对比一下传统方案:PWM中断 -> 读取占空比/保护逻辑 -> 启动ADC -> ADC中断 -> 搬数据 -> 执行控制环。两路中断、几十微秒的CPU时间,还伴随着不确定的延迟。硬件链路方案把这些全部省掉,CPU可以集中精力跑矢量变换、PID、观测器等核心算法。
在高PWM频率应用里,这个优势更是决定性的。PWM频率上到50kHz甚至100kHz时,每周期留给CPU的时间只有10到20微秒,软件中断采样方案基本跑不动,硬件采样链路成了唯一可行路线。
3. PWM与触发脉冲的生成:两个ATOM通道搭出相位精确的触发信号
3.1 先用ILLD把GTM时钟使能
所有GTM操作的第一步是打开GTM模块时钟和CMU时钟树。ILLD里这两步固定代码如下:
#include "IfxGtm.h" #include "IfxGtm_Atom_Pwm.h" #include "IfxGtm_Cmu.h" /* 使能GTM模块 */ IfxGtm_enable(&MODULE_GTM); /* 使能CMU CLK0,作为ATOM通道的计数时钟 */ IfxGtm_Cmu_enableClocks(&MODULE_GTM, IFXGTM_CMU_CLKEN_CLK0);这里解释一下为什么需要单独使能CMU时钟。GTM内部不是一个单纯定时器,它有一个时钟管理单元(CMU),为各子模块提供多个不同频率的时钟源。如果不使能对应时钟,即使GTM模块本身被打开,ATOM通道也拿不到有效时钟信号,PWM根本不会输出。
CMU CLK0的默认频率一般就是GTM模块频率,也就是SPB总线频率(TC37x上通常为100MHz)。这个频率就是我们后面计算PWM周期值的基础。
3.2 主PWM通道的初始化
PWM通道使用ILLD的IfxGtm_Atom_Pwm驱动。示例配置如下:
#define PWM_FREQ_HZ 10000u /* PWM频率 10kHz */ #define DEADTIME_NS 500u /* 死区时间,仅在互补输出时需要 */ IfxGtm_Atom_Pwm g_pwmDriver; IfxGtm_Atom_Pwm_Config pwmConfig; uint32 gtmFreq = IfxGtm_Cmu_getModuleFrequency(&MODULE_GTM); uint32 period = gtmFreq / PWM_FREQ_HZ; /* 计算周期计数值 */ IfxGtm_Atom_Pwm_initConfig(&pwmConfig, &MODULE_GTM); pwmConfig.atom = &MODULE_GTM.ATOM0; pwmConfig.channel = IfxGtm_Atom_Ch_0; /* ATOM0_CH0 */ pwmConfig.period = period; pwmConfig.dutyCycle = 0.5f; /* 初始占空比50% */ pwmConfig.synchronousUpdateEnabled = TRUE; /* 同步更新使能 */ pwmConfig.pin.outputPin = &IfxGtm_ATOM0_P00_0_OUT; /* 输出到引脚 */ IfxGtm_Atom_Pwm_init(&g_pwmDriver, &pwmConfig); /* 设置初始占空比 */ IfxGtm_Atom_Pwm_setDutyCycle(&g_pwmDriver, 0.5f);几个参数需要重点说:
period的计算:period = gtmFreq / PWM_FREQ_HZ。当GTM频率为100MHz、PWM频率为10kHz时,period = 10000。ATOM通道计数从0递增到period,然后归零重启,形成一个周期。这个值将直接被触发通道复用,保证两个通道周期绝对一致。
synchronousUpdateEnabled:这个字段非常关键。使能后,占空比更新不会立即生效,而是等到当前PWM周期结束才加载新值,防止输出波形出现半个窄脉冲。对数字电源和电机控制来说,这个功能必须打开,否则动态调占空比时可能产生畸变波形。
引脚映射的写法跟具体型号和板卡有关,不同开发板上PWM引脚可能不同。实际使用时需要查阅对应型号的引脚映射表,ILLD头文件里会有对应的IfxGtm_ATOMx_XX_POUT定义。
3.3 中点触发通道的配置
触发通道的核心思路:周期值与主PWM一致,占空比固定50%。这样它的翻转沿恰好出现在周期中点,即CN0计数到period/2的时刻。
IfxGtm_Atom_Pwm g_triggerDriver; IfxGtm_Atom_Pwm_Config trigConfig; IfxGtm_Atom_Pwm_initConfig(&trigConfig, &MODULE_GTM); trigConfig.atom = &MODULE_GTM.ATOM0; trigConfig.channel = IfxGtm_Atom_Ch_1; /* ATOM0_CH1 */ trigConfig.period = period; /* 与主PWM相同 */ trigConfig.dutyCycle = 0.5f; /* 固定50%,翻转沿在中点 */ trigConfig.synchronousUpdateEnabled = TRUE; trigConfig.pin.outputPin = NULL_PTR; /* 内部触发,无需引脚 */ IfxGtm_Atom_Pwm_init(&g_triggerDriver, &trigConfig);这里pin.outputPin设为NULL_PTR,表示不需要把触发信号引到物理引脚。EVADC可以通过内部信号路由直接收到这个通道的输出。
需要提醒的一点:触发通道的“翻转沿”究竟是上升沿还是下降沿,取决于ATOM输出信号的初始极性和计数方向。在这个配置下,通道输出在CN0到达period/2时翻转一次,到达period时再翻转一次。也就是说一个周期内有两个边沿:中点处的翻转和周期结束处的翻转。EVADC触发时只采样其中一个边沿,我习惯用中点处的上升沿作为触发源。具体是哪个边沿,可以在调试阶段通过引脚输出确认,后面验证章节会详细讲。
3.4 触发通道和主PWM如何保证同步
两个通道都在同一个ATOM0模块下,共用同一份CMU时钟和TBU时间基准。初始化完成后,它们的计数器从同一时刻开始递增,周期值又完全一致,所以翻转沿的相对位置天然固定。
但有一个陷阱:如果你在两个独立时刻分别初始化两个PWM通道,由于初始化代码执行需要时间,第二个通道的计数器可能已经比第一个通道晚走了几个tick。虽然周期一致,相位却可能差出几个时钟周期。对10kHz PWM来说,几个tick(10ns级别)的相位误差不至于致命,但如果你追求极致精度,可以在初始化后主动校准相位。
一个简单做法是:先初始化触发通道,再初始化主PWM通道,让主PWM的起始点严格对齐触发通道的当前计数状态。或者更彻底一点,利用ATOM的同步更新机制,两个通道在同一个TBU槽位上启动。ILLD的IfxGtm_Atom_Pwm初始化流程本身已经做了基本的同步处理,对大多数应用已经足够。如果你需要绝对同步,建议查阅你所用芯片参考手册中ATOM的TGC同步章节,配置两个通道的启动事件为同一时刻。
调试阶段建议把触发信号临时引到一个空闲引脚,用示波器同时看PWM和触发信号的相位关系。确认无误后再去掉引脚输出,改回内部触发。
4. EVADC组配置:让ADC在指定时刻精确启动转换
4.1 模块初始化和组初始化
EVADC在TC3xx里也是模块化结构:一个EVADC模块下分多个组(Group),每个组有独立的仲裁器、通道和结果寄存器。初始化分两步:先初始化模块,再初始化组。
#include "IfxEvadc.h" IfxEvadc_Adc g_adc; IfxEvadc_Adc_Group g_adcGroup; /* 模块初始化 */ IfxEvadc_Adc_Config adcConfig; IfxEvadc_Adc_initModuleConfig(&adcConfig, &MODULE_EVADC); IfxEvadc_Adc_initModule(&g_adc, &adcConfig); /* 组初始化 */ IfxEvadc_Adc_GroupConfig groupConfig; IfxEvadc_Adc_initGroupConfig(&groupConfig, &g_adc); groupConfig.groupId = IfxEvadc_GroupId_0; /* 使用Group 0 */ groupConfig.master = TRUE; groupConfig.arbiter.arbiterRounds = IfxEvadc_ArbiterRounds_1; groupConfig.arbiter.priority[IfxEvadc_RequestSource_queue0] = IfxEvadc_ArbiterPriority_0; groupConfig.queueRequest.enabled = TRUE; IfxEvadc_Adc_initGroup(&g_adcGroup, &groupConfig);master = TRUE表示这个组作为ADC主组,产生参考时钟。如果同一个EVADC模块下多个组共用转换时钟,只需要一个组配置为master。
队列请求使能后,EVADC会通过组内的队列调度器处理转换请求。队列模式和通道直连模式的区别在于:队列可以把多个通道按顺序排成一条链,一次触发后依次转换;直连模式每个通道独立触发,需要给每个通道配单独的触发源和优先级。
4.2 触发源选择:把GTM信号接入EVADC
这里是我在单路采样时最常踩坑的地方,几乎每次换芯片型号都要重新核对一遍。
groupConfig.queueRequest.triggerConfig.source = IfxEvadc_TriggerSource_gtmAtom0_1; /* ATOM0_CH1,具体以头文件枚举为准 */ groupConfig.queueRequest.triggerConfig.triggerChannel = IfxEvadc_TriggerChannel_none; groupConfig.queueRequest.triggerConfig.gatingMode = IfxEvadc_GatingMode_always;source就是这个组的触发源。我们在GTM配置里用ATOM0_CH1作为触发通道,这里对应的触发源就是ATOM0_CH1的输出信号。ILLD头文件里的枚举名通常长这样:IfxEvadc_TriggerSource_gtmAtom0_1。但不同型号的头文件可能略有差异,必须打开工程里的IfxEvadc_reg.h或者参考手册的触发源选择表,确认你要用的GTM通道对应的触发源枚举名。
gatingMode设置为always,表示触发信号到来时直接接受,不需要额外门控条件。如果多个PWM相位需要分时采样,可以在gatingMode里配置门控条件,这一步先不展开。
triggerChannel一般用于多通道同步门控,单触发源时保持none即可。
4.3 通道配置与结果寄存器绑定
触发源配置好之后,还需要把具体的ADC通道绑定到结果寄存器,并使能结果寄存器的DMA请求。
IfxEvadc_Adc_Channel g_adcChannel; IfxEvadc_Adc_ChannelConfig chanConfig; IfxEvadc_Adc_initChannelConfig(&chanConfig, &g_adcGroup); chanConfig.channel = IfxEvadc_ChannelId_0; /* 采样通道CH0 */ chanConfig.resultRegister = IfxEvadc_ChannelResult_0; /* 结果写入RES0 */ chanConfig.resultConfig.dmaRequestEnable = TRUE; /* 结果更新后触发DMA */ IfxEvadc_Adc_initChannel(&g_adcChannel, &chanConfig);dmaRequestEnable是整条链路里容易被忽略的一环。很多人的DMA配置看起来完全正确,但数据就是不动,最后发现是EVADC结果寄存器根本没发出DMA请求。这个字段使能后,每次该通道转换完成、结果寄存器更新时,硬件都会自动产生一次DMA请求,不需要软件干预。
如果你一次需要采多路信号(比如相电流Ia/Ib和母线电压Vdc),就按同样方式配置多个通道和结果寄存器,然后在应用层按数组下标区分数据。单触发的队列模式下,触发信号到来后会自动把队列里所有通道依次转换完,非常方便。
4.4 多通道队列的优先级与顺序
如果队列里挂了多个通道,转换顺序取决于各通道在队列初始化时的先后顺序。ILLD的queue初始化接口会按通道配置顺序生成队列:
IfxEvadc_Adc_QueueConfig queueConfig; IfxEvadc_Adc_initQueueConfig(&queueConfig, &g_adcGroup); queueConfig.requestSlotPrio = IfxEvadc_RequestSlotPriority_0; queueConfig.channelConfig[0].channel = IfxEvadc_ChannelId_0; queueConfig.channelConfig[0].resultRegister = IfxEvadc_ChannelResult_0; queueConfig.channelConfig[1].channel = IfxEvadc_ChannelId_1; queueConfig.channelConfig[1].resultRegister = IfxEvadc_ChannelResult_1; IfxEvadc_Adc_initQueue(&g_adcQueue, &queueConfig);一个触发脉冲到来后,队列会依次转换CH0、CH1,结果分别进入RES0、RES1。每个结果寄存器更新时都会产生各自的DMA请求,这个特性在配置DMA时非常重要,下一章会详细分析。
这里特别提醒:队列的转换需要时间。12位精度的ADC转换大约需要几百纳秒到一微秒,取决于ADC时钟配置。假设队列里挂了3个通道,一次触发后,最后一个通道的结果会比第一个通道晚约2微秒。对于10kHz PWM来说,2微秒只占周期的2%,通常可以接受;如果PWM频率更高,就需要评估这个时间差对控制环的影响。
4.5 触发后到结果可用的完整时序
完整时序链条是这样的:
- GTM ATOM0_CH1的CN0到达period/2,输出翻转沿产生。
- 内部触发信号到达EVADC Group 0的触发输入。
- EVADC仲裁器接受请求,启动队列转换。
- 队列中第一个通道开始采样,经历采样保持和逐次逼近转换。
- 转换完成,结果写入RES0寄存器,同时产生DMA请求。
- DMA收到请求,把RES0的值搬到内存。
- 队列移到下一个通道,重复步骤4到6,直到所有队列通道转换完毕。
整个链路从触发沿到第一个结果落地的时间通常在1微秒以内。如果应用对“结果相对于PWM中点”的延迟敏感,可以在算法里做固定延迟补偿,因为这个延迟是确定性的,补偿一个常数即可。
5. DMA自动搬运:零中断开销把结果搬进内存数组
5.1 DMA模块初始化
TC3xx的DMA模块初始化比GTM和EVADC都简单,基本是标准套路:
#include "IfxDma.h" IfxDma_Dma g_dma; IfxDma_Dma_Channel g_dmaChannel; IfxDma_Dma_Config dmaConfig; IfxDma_Dma_initConfig(&dmaConfig, &MODULE_DMA); IfxDma_Dma_init(&g_dma, &dmaConfig);到这里DMA模块已经就绪,剩下的关键是通道配置。DMA通道的参数决定了搬移从哪里来、到哪里去、搬多宽、何时搬。
5.2 DMA通道配置:源地址、目的地址与传输宽度
#define ADC_SAMPLE_COUNT 1u /* 单路采样 */ /* 结果缓冲区 */ uint16 g_adcResult[ADC_SAMPLE_COUNT]; IfxDma_Dma_ChannelConfig dmaChanCfg; IfxDma_Dma_initChannelConfig(&dmaChanCfg, &g_dma); dmaChanCfg.sourceAddress = (uint32)&g_adcGroup.kernel->RES[0].R; /* EVADC RES0寄存器地址 */ dmaChanCfg.destinationAddress = (uint32)&g_adcResult[0]; /* 内存数组首地址 */ dmaChanCfg.transferCount = ADC_SAMPLE_COUNT; /* 搬运次数 */ dmaChanCfg.moveSize = IfxDma_MoveSize_16Bit; /* 每次搬16位 */ dmaChanCfg.requestMode = IfxDma_RequestMode_oneTransfer; /* 一次请求搬完transferCount */ dmaChanCfg.hardwareRequestEnabled = TRUE; /* 使能硬件请求触发 */ dmaChanCfg.channelInterruptEnabled = TRUE; /* 使能DMA通道中断 */ dmaChanCfg.interruptTypeOfEvent = IfxDma_ChannelInterruptType_transferComplete; IfxDma_Dma_createChannel(&g_dmaChannel, &dmaChanCfg); IfxDma_Dma_startChannel(&g_dmaChannel);源地址这里有一个容易写错的地方。g_adcGroup.kernel是EVADC组的寄存器基地址,RES[0].R才是结果寄存器的32位寄存器。如果直接用&g_adcGroup或者&g_adcGroup.kernel,取到的地址很可能指向组配置寄存器而不是结果寄存器。
moveSize选择16Bit,因为EVADC结果是12位,右对齐存入寄存器的低16位。如果选择32Bit搬运,一次可以把整个32位结果寄存器搬过来,但数组类型需要改成uint32_t,读取时再做掩码。两种方式都能用,我习惯用16Bit,内存占用小,读取也直白。
requestMode为oneTransfer表示收到一次硬件请求后执行一次完整的transaction。对于单通道单次采样,transferCount=1即可,每次EVADC结果更新都会触发一次搬移。
5.3 请求源映射:EVADC的DMA请求线怎么接到DMA通道
TC3xx的DMA硬件请求不是所有外设自动连到任意DMA通道的。每个DMA通道能从哪些外设接收请求,由芯片内部的请求多路选择器决定,具体映射关系在参考手册的DMA请求分配表里。
在实际工程中,比如EVADC Group0的某个结果寄存器产生的DMA请求,通常会连接到DMA模块的某个固定请求线号。你需要在手册里找到“EVADC Group0 Result Register Request”对应的请求线,然后把你使用的DMA通道配置成监听这条请求线。ILLD里这个配置字段在不同版本中可能叫requestSource或requestLine,以实际头文件为准。
这里有一个工程上的建议:定义一个专门的宏来管理请求源映射,方便换芯片型号时快速修改。
#define EVADC0_RES0_DMA_REQUEST_LINE 0u /* 根据数据手册填写实际值 */如果手册显示EVADC Group0 RES0的请求线是DMA通道0,那就用DMA通道0;如果用的是其他通道,检查该通道是否支持这条请求线,不支持就换通道。这是整篇文章里最“硬件绑定”的一步,没有捷径,只能查手册。
5.4 多路采样时的DMA组织方式
前面提到,如果队列里有多个通道,每个结果寄存器都会独立产生DMA请求。那么DMA怎么处理多路结果?
方式A:每个结果寄存器一个DMA通道。比如队列里挂了CH0和CH1,那就建两个DMA通道,一个监听RES0的请求线,一个监听RES1的请求线,分别搬到不同的内存变量。这种方式最直观,可靠性最高,缺点是占用DMA通道资源。TC3xx的DMA通道数量很充足,普通控制应用这么用完全没问题。
方式B:单DMA通道 + 地址偏移。利用DMA的地址自动递增功能,第一个结果寄存器更新时启动一次transaction,连续搬运多个寄存器值。这种方式必须保证DMA搬移速度追不上ADC转换速度,否则会读到还没更新的旧值。实际工程里我很少这么用,因为省掉的DMA通道资源远不如数据正确性重要。
这里我强烈推荐方式A,尤其是控制环的关键采样信号,宁可多占一个DMA通道,也别让数据可靠性打折扣。
5.5 DMA完成中断还是轮询
数据搬进内存后,怎么知道“这一轮数据是新数据”?
如果PWM频率不高,或者控制环本身需要中断驱动,可以在DMA通道上使能transferComplete中断。在回调里置一个标志位,主循环或控制任务检测到标志后读取g_adcResult数组。
/* DMA中断服务函数 */ void dmaCompleteIsr(void) { g_adcDataReady = TRUE; }如果PWM频率很高、控制环对确定性要求极严,建议用轮询方式检查DMA的channel busy标志或传输计数寄存器。轮询没有中断现场保护开销,时序抖动更小,代价是CPU需要主动去查状态位。我个人的习惯是:PWM频率低于20kHz时用中断,高于20kHz时用轮询加硬件标志。
6. 初始化顺序、验证方法与实测波形
6.1 正确的初始化顺序
链路上有三个模块,初始化顺序虽然不至于错一个就完全跑不起来,但正确的顺序可以避免很多隐蔽问题。
我的推荐顺序:
1. 使能系统时钟、配置IO复用 2. 初始化GTM(使能模块和CMU时钟) 3. 初始化主PWM通道和触发脉冲通道 4. 初始化DMA模块和DMA通道 5. 初始化EVADC模块和组 6. 启动ADC队列转换 7. 启动DMA通道(如果还没启动)GTM最先初始化,是为了让时间基准先稳定下来;DMA在EVADC之前初始化,是为了保证EVADC结果产生DMA请求时,DMA通道已经处于监听状态。如果顺序反过来,EVADC先跑起来但DMA还没就绪,第一次转换结果可能丢失,容易让人误以为配置有问题。
6.2 用直流电压验证数据链路
整个链路配好后,不要直接上电机或功率级,先用一个已知直流电压验证。
把ADC输入引脚接到一个电位器或稳压源上,输出1.5V。假设ADC参考电压3.3V、12位精度,期望结果大约是1.5 / 3.3 * 4095 ≈ 1861。给板上电,观察g_adcResult的值是否在1860附近波动。如果完全为0,参考第7章的排查步骤;如果数值稳定但大小差很多,检查参考电压配置和输入分压电阻。
这个简单的实验能最快区分“链路没通”和“数值不准”两类问题。
6.3 示波器看触发边沿与PWM的相对位置
验证“中点触发”是否真的是中点,最直接的办法是把触发信号临时引出到引脚。
在GTM触发通道初始化时,给trigConfig.pin.outputPin指定一个空闲引脚。用示波器两个通道同时抓PWM主输出和触发信号,正常应该看到:触发信号翻转沿恰好落在PWM周期的一半位置。如果触发沿明显偏移,优先检查触发通道的period是否和主PWM一致,再检查两个通道是否在同一时刻启动。
确认无误后,把触发通道的引脚配置改回NULL_PTR,重新编译烧录,进入正式工作状态。
6.4 动态调占空比验证中点稳定性
最后一项验证:让占空比从10%缓慢变化到90%,同时观察触发信号的位置是否始终保持在PWM周期的中点。
由于我们用了独立触发通道且占空比固定50%,理论上无论主PWM占空比怎么变化,触发点都纹丝不动。如果发现触发位置随占空比漂移,说明两个通道的周期同步出了问题,检查是否使用了同一个period变量,以及两个通道的时钟分频是否一致。
这一步通过了,就可以放心地把这套采样链路投入到正式控制环里。
7. 踩坑记录:触发丢失、全零结果与中点偏移
7.1 队列一直不触发,PWM却正常输出
症状:PWM波形正常,但内存数组始终不更新,或者在调试器里看到EVADC的忙标志一直没被置位。
排查链路:
- 确认EVADC组的触发源枚举是否选对。最常见的错误是配置时想当然用了
IfxEvadc_TriggerSource_gtmAtom0_0,但触发通道实际建在ATOM0_CH1上。GTM通道编号和EVADC触发源枚举必须一一对应,错一个数字链路就完全不通。 - 确认触发通道是否真的在输出信号。有些情况下触发通道因为占空比、相位配置问题,输出被钳在高或低电平,没有翻转沿,自然触发不了ADC。把触发信号临时引到引脚实测是最快的确认方法。
- 确认组配置里
queueRequest.enabled = TRUE。如果只配了groupConfig而忘了使能队列请求,整个队列调度器不工作,触发信号来了也不会接受请求。
7.2 DMA搬回来的数据全是0
症状:PWM和ADC都正常工作(EVADC寄存器能看到转换结果),但内存数组全为0。
这种问题基本集中在DMA配置上:
- EVADC结果寄存器没开DMA请求。
resultConfig.dmaRequestEnable = TRUE这一行漏掉的话,EVADC永远不会发DMA请求。这是最高频的原因。 - DMA源地址取错。检查
sourceAddress是否指向了结果寄存器本身。调试时可以直接在调试器里查看该地址的值,如果它等于当前转换结果,说明源地址正确。 - DMA请求线映射不对。如果DMA通道接收的请求信号来自别的外设,它永远不会被触发。回到手册查请求分配表。
7.3 数据有时是新值有时是旧值
症状:数组里的值大部分时间正确,但偶尔出现上一次周期残留的旧数据。
这个现象通常有两种原因。一是DMA搬移的结果寄存器在ADC还在更新时被读走,也就是时序竞争。二是多通道队列方案里,DMA按固定偏移连搬多个寄存器,某个寄存器还没完成转换就被读走了。
解决办法:
- 检查
moveSize和transferCount是否匹配。单通道单次转换用16Bit、transferCount=1最安全。 - 如果用了多路队列,改用“每通道独立DMA通道”的方式,避免跨寄存器连搬。
- 在应用层读取结果时,可以比较连续两次读取是否一致,不一致则丢弃本轮数据。这只是兜底方案,正确配置后数据应该是稳定可用的。
7.4 中点位置看起来不对
症状:触发边沿确实存在,但位置不在PWM周期的正中央,总是偏前或偏后几十个计数tick。
常见原因是触发通道和主PWM通道的计数起点不同步。由于两个通道使用相同的period,位置差异通常只有几个tick,但如果你在初始化时先配置了主PWM,跑了一小段时间再配置触发通道,两者的初始相位就会错开。解决办法是把两个通道的初始化放在相邻位置,保证它们的计数器从同一基准开始递增。如果还需要更高精度,用ATOM的同步启动机制,把两个通道的启动事件绑定到同一个时间点。
另一个容易被忽略的原因是输出极性。ATOM通道的输出初始电平在京高/低电平配置不同时,翻转沿的位置可能相差半个周期。示波器实测确认一下,必要时调整通道的输出极性设置。
7.5 高PWM频率下结果波动明显
当PWM频率超过50kHz后,一个周期的时长只有20微秒,EVADC转换时间(假设1微秒)已经占了周期的5%,再算上队列里多通道的排队时间,采样点相对中点的偏差会明显变大。
如果遇到这个问题,优先压缩转换链路时间:
- 将ADC分辨率从12位降到10位,转换时间会缩短约20%。
- 缩短队列长度,把不需要每周期采的通道移出队列,用慢速周期补齐。
- 提高EVADC转换时钟,但要注意不得超过数据手册规定的最大ADC时钟频率。
这些优化如果还不够,就需要考虑双PWM周期采一次、奇偶周期分时采样等策略,但那就属于控制算法层面的权衡了,这里不展开。
最后分享一点我的实际体会
这套硬件采样链路在TC3xx上跑熟以后,最大的感受是“采样这件事终于可以不用操心了”。电机转起来、电源跑起来,CPU里不再有ADC中断的干扰,控制环的时间预算变得充裕,调试PID参数时也少了很多噪声因素。后期我把这个方案从TC377移植到TC364,只改了引脚映射和少量触发源枚举,整体链路几乎没有动过。
如果你正在做TC3xx的电机控制或数字电源项目,我建议直接按这条路径搭:GTM双通道产生PWM和中点触发,EVADC用队列接收触发,DMA独立通道搬运结果。一开始多花半天时间把链路调通、验证中点位置,后面整个项目的调试都会顺很多。