4. 电流采样与ADC:相电流采样电阻布局、CMS32 ADC多通道同步采样、DMA传输

电流采样是电机控制的「眼睛」。你想想看,没有准确的电流反馈,FOC 算得再漂亮也是白搭。我见过不少工程师,花大价钱买了高性能 MCU,结果在采样电路上省了几毛钱,最后整个系统抖得像筛子。

这一章,咱们就聊聊 CMS32 的电流采样怎么做。从硬件布局到 ADC 配置,再到 DMA 搬运数据,一条龙讲清楚。

4.1 相电流采样电阻布局

采样电阻放哪儿?这是个好问题。常见的方案有两种:低端采样和高端采样。电机控制里,我们几乎只用低端采样——也就是把采样电阻放在 MOSFET 的源极和 GND 之间。

为什么选低端?

  • 共模电压低,运放好选,成本也低
  • 可以直接用单电源运放,不用搞负压
  • 安全!高压侧炸了不会直接窜到 ADC 引脚

具体到三相逆变器,我习惯在每一相的下桥臂 MOSFET 源极串一个采样电阻。这样三个电阻分别对应 U、V、W 三相的电流。注意,采样电阻的阻值不能太大——大了发热严重,压降也大,会影响电机效率。

参数 推荐值 说明
采样电阻阻值 5 ~ 20 mΩ 功率 1W 以上,温漂 50ppm 以内
运放增益 10 ~ 50 倍 根据 ADC 参考电压调整
RC 滤波截止频率 1 ~ 5 MHz 滤除 PWM 开关噪声

布局避坑:采样电阻的走线要用开尔文接法。也就是从电阻两端单独拉两条细线到运放输入端,不要跟大电流路径共用铜皮。我曾经在一个项目里偷懒没这么做,结果采样值飘了 20%,查了三天才找到原因——PCB 铜箔上的压降把采样结果吃了。

4.2 CMS32 ADC 多通道同步采样

CMS32 的 ADC 有个很实用的功能:同步采样。说白了,就是多个 ADC 通道可以在同一个触发信号下同时开始转换。这对电机控制太重要了——三相电流必须采同一个时刻的值,否则算出来的电流矢量就是错的。

我一般用 ADC 的注入组(Injected Group)来做同步采样。配置三个通道,分别对应 U、V、W 三相的电流信号。触发源选定时器 1 的更新事件,这样每个 PWM 周期开始的时候,ADC 自动启动转换。

// CMS32 ADC 同步采样配置示例
void ADC_SyncSample_Init(void)
{
    // 使能 ADC 时钟
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    
    // 配置 ADC 为独立模式
    ADC1->CR1 = 0;
    
    // 设置采样时间:三个通道都用 28.5 个周期
    ADC1->SMPR1 = (7 << 0) | (7 << 3) | (7 << 6);  // CH0, CH1, CH2
    
    // 配置注入组:三个通道,顺序转换
    ADC1->JSQR = (2 << 20) | (0 << 15) | (1 << 10) | (2 << 5);
    // 2 表示 3 个转换(0,1,2 对应三个通道)
    
    // 触发源选择:定时器 1 更新事件
    ADC1->CR2 |= ADC_CR2_JEXTSEL_0 | ADC_CR2_JEXTSEL_1;  // TIM1_CC4 触发
    ADC1->CR2 |= ADC_CR2_JEXTTRIG;  // 使能外部触发
    
    // 使能注入组
    ADC1->CR1 |= ADC_CR1_JAUTO;
    
    // 校准
    ADC1->CR2 |= ADC_CR2_CAL;
    while(ADC1->CR2 & ADC_CR2_CAL);
    
    // 使能 ADC
    ADC1->CR2 |= ADC_CR2_ADON;
}

这里有个细节:注入组的优先级高于规则组。如果规则组正在转换,注入组触发会打断它。电机控制里,我们通常只用注入组,规则组留给其他慢速通道(比如温度、电压检测)。

4.3 DMA 传输:让数据自己跑

ADC 转换完了,数据得搬到内存里。如果每次都用 CPU 去读,那 CPU 啥也别干了,光伺候 ADC 就够呛。这时候 DMA 就派上用场了。

CMS32 的 DMA 支持循环模式和双缓冲模式。我推荐用双缓冲——两个缓冲区轮流接收数据,CPU 处理一个,DMA 往另一个写,互不干扰。

// DMA 双缓冲配置
#define ADC_BUF_SIZE  3  // 三相电流

uint16_t adc_buf1[ADC_BUF_SIZE];
uint16_t adc_buf2[ADC_BUF_SIZE];
volatile uint8_t buf_ready = 0;

void DMA_ADC_Init(void)
{
    // 使能 DMA 时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    
    // 配置 DMA 通道 1(对应 ADC1)
    DMA1_Channel1->CPAR = (uint32_t)&(ADC1->JDR1);  // 外设地址:注入数据寄存器
    DMA1_Channel1->CMAR = (uint32_t)adc_buf1;       // 内存地址:缓冲区1
    DMA1_Channel1->CNDTR = ADC_BUF_SIZE;            // 传输数量:3个数据
    
    // 配置传输方向:外设到内存
    DMA1_Channel1->CCR = DMA_CCR_MINC |             // 内存地址递增
                         DMA_CCR_PSIZE_0 |           // 外设数据宽度 16 位
                         DMA_CCR_MSIZE_0 |           // 内存数据宽度 16 位
                         DMA_CCR_CIRC;               // 循环模式
    
    // 使能传输完成中断
    DMA1_Channel1->CCR |= DMA_CCR_TCIE;
    
    // 使能 DMA 通道
    DMA1_Channel1->CCR |= DMA_CCR_EN;
    
    // 配置 NVIC
    NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}

// DMA 传输完成中断
void DMA1_Channel1_IRQHandler(void)
{
    if(DMA1->ISR & DMA_ISR_TCIF1)
    {
        DMA1->IFCR |= DMA_IFCR_CTCIF1;
        
        // 切换缓冲区
        if(DMA1_Channel1->CMAR == (uint32_t)adc_buf1)
        {
            DMA1_Channel1->CMAR = (uint32_t)adc_buf2;
        }
        else
        {
            DMA1_Channel1->CMAR = (uint32_t)adc_buf1;
        }
        
        buf_ready = 1;  // 通知主循环处理数据
    }
}

我的经验:DMA 中断里尽量少做事。我一般只设一个标志位,真正的电流计算放在主循环或者定时器中断里做。中断里搞太多事情,容易把其他实时任务堵死。

4.4 整体数据流:从采样到 FOC

把上面几块拼起来,整个数据流是这样的:

电流采样数据流 采样电阻 U/V/W 三相 差分运放 增益 10~50 倍 ADC 注入组 同步采样 DMA 双缓冲 内存缓冲区 adc_buf1/2 FOC 计算 Clark/Park 变换 TIM1 触发 关键时序: PWM 中心对齐 → TIM1 更新事件 → ADC 注入组同步采样 → DMA 自动搬运 → 主循环读取缓冲区 整个过程无需 CPU 干预,采样周期 = PWM 周期(典型值 50~100 μs)

这个流程跑通之后,你会发现 CPU 的负载降了一大截。我做过对比:用 DMA 之前,CPU 有 30% 的时间在伺候 ADC;用了 DMA 之后,这个数字降到了 2% 以下。省出来的算力,正好用来跑更复杂的控制算法。

4.5 采样值校准

硬件总有误差。采样电阻的精度、运放的偏置、ADC 的偏移,这些都会让采样值偏离真实值。所以,校准是必须的。

我一般做两步校准:

  1. 零偏校准:电机静止时,理论上电流为 0。这时候采到的值就是偏置。记录下来,后续每个采样值都减去这个偏置。
  2. 增益校准:给电机通一个已知的直流电流,用万用表测实际值,跟 ADC 读数对比,算出增益系数。
// 电流校准函数
typedef struct {
    int16_t offset;     // 零偏值
    float gain;         // 增益系数
} CalibParam;

CalibParam calib[3];   // 三相分别校准

void Current_Calibrate(void)
{
    uint16_t raw[3];
    uint32_t sum[3] = {0, 0, 0};
    
    // 电机静止,采集 1000 次求平均
    for(int i = 0; i < 1000; i++)
    {
        // 触发一次 ADC 注入组转换
        ADC1->CR2 |= ADC_CR2_JSWSTART;
        while(!(ADC1->SR & ADC_SR_JEOC));
        
        raw[0] = ADC1->JDR1;
        raw[1] = ADC1->JDR2;
        raw[2] = ADC1->JDR3;
        
        sum[0] += raw[0];
        sum[1] += raw[1];
        sum[2] += raw[2];
    }
    
    // 计算零偏
    for(int ch = 0; ch < 3; ch++)
    {
        calib[ch].offset = sum[ch] / 1000;
        calib[ch].gain = 1.0f;  // 增益校准需要外部电流源
    }
}

// 获取校准后的电流值(单位:mA)
float Get_Current(int phase, uint16_t raw_value)
{
    int16_t corrected = (int16_t)raw_value - calib[phase].offset;
    return (float)corrected * calib[phase].gain;
}

注意:零偏校准要在电机上电但未转动时做。如果电机已经在转,反电动势会干扰采样结果,校准就不准了。我习惯在系统初始化阶段,PWM 输出使能之前,先跑一遍校准流程。