STM32动态SPWM生成:告别查表法,用定时器+DMA释放Flash空间
在嵌入式系统开发中,资源优化永远是工程师们绕不开的话题。当你面对一颗Flash只有64KB的STM32F030,或是需要同时生成多路高精度SPWM信号时,传统的查表法很快就会让你陷入存储空间不足的困境。每个周期200个采样点的正弦表,对于8路PWM来说就意味着16KB的Flash占用——这还没考虑更高精度或多周期波形存储的需求。
1. 为什么我们需要动态生成SPWM?
传统查表法虽然简单直接,但存在三个致命缺陷:
- 存储空间占用大:高精度波形需要大量采样点,多路输出时存储需求成倍增长
- 灵活性差:波形参数(如频率、幅度)固定,难以动态调整
- 资源浪费:对于对称波形(如正弦波),存储整个周期是冗余的
动态生成技术的核心优势在于:
// 传统查表法 vs 动态生成 #define TABLE_SIZE 200 // 传统方法需要存储200个点 uint16_t sin_table[TABLE_SIZE]; // 动态方法只需存储关键参数 float amplitude = 0.8; // 幅值 float frequency = 50.0; // 频率 float phase = 0.0; // 相位提示:对于Cortex-M4及以上内核的STM32,利用硬件FPU进行实时计算几乎不会增加CPU负担。
2. 硬件架构设计:定时器+DMA的黄金组合
2.1 定时器配置要点
STM32的高级定时器(TIM1/TIM8)是生成SPWM的理想选择,关键配置参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 计数模式 | 中心对齐模式1 | 产生对称PWM,减少谐波失真 |
| 预分频器(PSC) | 0 | 根据时钟频率调整 |
| 自动重载值(ARR) | 根据频率需求设置 | 决定PWM基频 |
| 死区时间 | 50-100ns | 全桥电路必需,防止上下管直通 |
// CubeMX生成的定时器初始化片段 TIM_HandleTypeDef htim1; htim1.Instance = TIM1; htim1.Init.Prescaler = 0; htim1.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED1; htim1.Init.Period = 1599; // 对于100kHz开关频率(80MHz时钟) htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim1);2.2 DMA传输配置技巧
DMA在此方案中扮演关键角色,负责将计算好的占空比数据搬运到定时器的CCR寄存器:
- 内存到外设模式:DMA从内存数组读取数据写入TIMx_CCRx
- 循环模式:实现波形连续输出无需CPU干预
- 数据宽度匹配:确保CCR寄存器与内存数据宽度一致(通常16位)
// DMA配置示例 DMA_HandleTypeDef hdma_tim1_ch1; hdma_tim1_ch1.Instance = DMA1_Channel1; hdma_tim1_ch1.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tim1_ch1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tim1_ch1.Init.MemInc = DMA_MINC_ENABLE; hdma_tim1_ch1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_tim1_ch1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_tim1_ch1.Init.Mode = DMA_CIRCULAR; HAL_DMA_Init(&hdma_tim1_ch1);3. 动态生成算法选型与优化
3.1 CORDIC算法实现
CORDIC(坐标旋转数字计算机)是嵌入式系统中计算三角函数的经典算法,特别适合没有FPU的低端MCU:
优势:
- 仅需移位和加法操作
- 可流水线实现
- 精度可配置
// 简化版CORDIC实现(固定点运算) #define CORDIC_ITERATIONS 10 int16_t cordic_sin(uint16_t angle) { int32_t x = 39797; // 0.607252935 * 2^16 int32_t y = 0; const int32_t angles[] = {11520, 6801, 3593, 1824, 916, 458, 229, 115, 57, 29}; for(int i=0; i<CORDIC_ITERATIONS; i++) { int32_t x_new, y_new; if(angle < 32768) { // 判断角度符号 x_new = x - (y >> i); y_new = y + (x >> i); angle += angles[i]; } else { x_new = x + (y >> i); y_new = y - (x >> i); angle -= angles[i]; } x = x_new; y = y_new; } return (int16_t)(y >> 16); }3.2 增量计算法
对于有FPU的高端STM32(如F4/H7系列),增量计算法更为高效:
// 增量式正弦波生成 typedef struct { float amplitude; float phase; float phase_increment; } SineGenerator; void sine_init(SineGenerator* gen, float freq, float amp, float sample_rate) { gen->amplitude = amp; gen->phase = 0.0f; gen->phase_increment = 2.0f * M_PI * freq / sample_rate; } float sine_next(SineGenerator* gen) { float value = gen->amplitude * sinf(gen->phase); gen->phase += gen->phase_increment; if(gen->phase > 2.0f * M_PI) { gen->phase -= 2.0f * M_PI; } return value; }性能对比:
| 方法 | CPU占用率(F030) | 精度(12bit) | Flash占用 |
|---|---|---|---|
| 查表法(200点) | 1% | ±2LSB | 400字节 |
| CORDIC(10次迭代) | 15% | ±5LSB | 150字节 |
| 增量计算(FPU) | 5% | ±1LSB | 50字节 |
4. 完整实现与调试技巧
4.1 系统初始化流程
时钟配置:
- 确保定时器时钟与DMA时钟使能
- 对于高频率PWM,考虑使用PLL倍频
GPIO配置:
- 复用功能映射到正确的定时器通道
- 输出模式设置为复用推挽
中断配置:
- 使能DMA传输完成中断
- 设置合适的NVIC优先级
// 完整初始化示例 void PWM_Init(void) { // 1. 定时器基础配置 HAL_TIM_PWM_Init(&htim1); // 2. PWM通道配置 TIM_OC_InitTypeDef sConfigOC; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1); // 3. DMA链接 HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1, (uint32_t*)pwm_buffer, BUFFER_SIZE); }4.2 常见问题排查
波形失真可能原因:
- DMA传输速率跟不上PWM更新需求
- 计算算法耗时超过PWM周期
- 内存带宽不足(特别是使用DMA时)
优化技巧:
- 使用双缓冲技术减少波形断续
- 预计算部分波形点,减轻实时计算压力
- 对于多路PWM,考虑TIM主从模式同步
注意:调试时建议先用示波器观察单个PWM通道,确认基础波形正确后再测试互补输出。