简介:本资源是一个基于STM32F1系列的双极性SPWM波形生成与控制工程代码包,面向电力电子、嵌入式开发及电机驱动方向的初学者与进阶工程师,解决逆变器系统中正弦化输出波形生成、滤波参数可调、硬件PWM精准控制等核心实践问题。压缩包共142个文件,5.84MB,涵盖19个C源文件(含HAL库底层驱动如stm32f1xx_hal_tim.c、gpio.c等)、46个头文件(h)、20个编译中间文件(o/d/crf),以及KEIL工程配置(uvprojx、ioc、sct等)和可执行镜像(axf、hex),完整呈现从算法实现、外设配置到烧录调试的全链路开发结构。已有787人学习下载,读者可直接导入KEIL环境编译运行,快速掌握双极性SPWM载波比较逻辑、调制比调节方法、滤波后频率动态调整技巧,并复用模块化驱动代码适配同类逆变或电机控制项目。
1. sjx_PWM.zip 是什么:一个轻量但边界清晰的 PWM 控制代码包,专为嵌入式初学者和快速验证场景设计
你手头有一块 STM32F103C8T6 最小系统板,想让 LED 呼吸灯动起来,或驱动一个直流电机调速,又或者刚学完定时器通道捕获/比较原理,正卡在“理论懂了,代码写不出来”这道坎上——这时候,sjx_PWM.zip 很可能就是你翻遍 GitHub 和 CSDN 后点开的第一个压缩包。它不是 SDK 全家桶,不带 GUI 配置工具,也不封装成 HAL 库风格的抽象层;它是一份用标准外设库(SPL)写的、仅含 3 个 .c 文件 + 2 个 .h 文件 + 1 份 Keil 工程配置的纯 C 实现,核心逻辑全部落在 TIM1 的 CH1~CH4 上,支持互补输出+死区插入(可选),且所有寄存器配置都做了逐行注释。它解决的不是“工业级高可靠性 PWM”,而是“我能不能在 20 分钟内让 PA8 脚输出占空比可调的方波”。适合刚从 51 过渡到 Cortex-M、对 APB 总线时钟树还半信半疑的开发者,也适合需要快速复现某段底层时序逻辑做对比实验的调试者。如果你正在找的是基于 CMSIS-DSP 的高级 PWM 同步算法,或支持多路独立频率的复杂调度器,那这个包会显得单薄;但如果你要的是“抄过去改两行就能亮”的确定性,它反而比一堆宏定义套娃的 HAL 示例更透明、更可控。
2. 为什么是 sjx_PWM.zip 而不是其他方案:从寄存器直写到 SPL 封装的选型权衡
2.1 它没选 HAL 库:避免抽象层带来的时序黑匣子
HAL 库对 PWM 的封装(如HAL_TIM_PWM_Start())把 ARR、CCR、CCER、BDTR 等寄存器操作全收进函数体内,好处是接口统一,坏处是当你发现输出波形有 1~2 个周期的抖动、或死区时间始终不对时,你得一层层扒 HAL 源码,再对照 Reference Manual 查每个函数里是否执行了TIM_BDTRConfig()、是否在TIM_Cmd(ENABLE)前完成了预装载使能。而 sjx_PWM.zip 直接暴露TIM_TimeBaseInit()和TIM_OCInit()的结构体初始化过程,比如:
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 999; // 自动重装载值,决定频率 TIM_TimeBaseStructure.TIM_Prescaler = 71; // 预分频系数,72MHz / (71+1) = 1MHz 计数基准 TIM_TimeBaseStructure.TIM_ClockDivision = 0; // 不分频 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数 TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);提示:
TIM_Period = 999对应 1kHz 频率(1MHz / 1000),这是绝大多数电机驱动和 LED 调光的起始安全频点。新手常误填TIM_Period = 1000,结果频率变成 999.001Hz,虽不影响功能,但调试示波器时容易因标称值偏差产生困惑。
这段代码的意义远不止“设置定时器”,它强制你确认三件事:主频是否真为 72MHz(查 RCC_CFGR)、APB2 是否被分频(查 RCC_PCLK2Config)、以及TIM_Period是否按“计数值从 0 开始”理解(即 0~999 共 1000 个状态)。这种显式控制,在你后续要接入编码器测速、或做 PWM 输入捕获同步时,会成为可追溯的起点。
2.2 它坚持 SPL 而非寄存器直写:平衡可读性与执行效率
有人会说:“直接操作TIM1->ARR = 999; TIM1->CCR1 = 500;不更干脆?”——确实快,但代价是可维护性归零。SPL 封装的价值在于:
- 结构体字段名(如
.TIM_Pulse)比TIM1->CCR1更具语义; - 初始化函数内部自动处理了寄存器位宽对齐(如 32 位寄存器中只写低 16 位)、预装载使能(
TIM_ARRPreloadConfig())等易漏细节; - 当你需要切换到 TIM2 或 TIM3 时,只需改参数中的
TIMx句柄,无需重算地址偏移。
sjx_PWM.zip 中的PWM_Init()函数正是这一思路的体现:
void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 1. 使能 GPIOA 和 TIM1 时钟 RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA | RCC_APB2PERIPH_TIM1, ENABLE); // 2. 配置 PA8 为复用推挽输出(TIM1_CH1) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 配置 TIM1_CH1 输出模式:PWM1 模式,初始占空比 50% TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 500; // CCR1 初始值,对应 50% 占空比(ARR=999) TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); // 关键!开启预装载,避免运行中突变 // 4. 启动 TIM1 TIM_Cmd(TIM1, ENABLE); }注意第 3 步中TIM_OC1PreloadConfig()的调用——这是很多初学者忽略的“后悔药开关”。若不开启预装载,当你在中断里动态修改TIM1->CCR1时,新值会立即生效,导致某次计数周期内高电平时间突变,波形出现毛刺。而开启后,新值只在下一个更新事件(UEV)时载入影子寄存器,保证每个周期波形严格一致。这个细节在 HAL 库里默认开启,但在 SPL 中必须手动写,sjx_PWM.zip 把它明明白白放在初始化流程里,而不是藏在某个“高级配置”函数中。
2.3 它只聚焦 TIM1:放弃通用性换取确定性
整个包未提供PWM_Init_For_ANY_Timer()这类泛化接口,所有函数名、引脚定义、时钟使能都绑定在 TIM1 + PA8/PA9/PA10/PA11。这不是偷懒,而是工程取舍:
- TIM1 是高级定时器,原生支持互补输出、死区插入、刹车功能,而通用定时器(TIM2~TIM4)无此能力;
- PA8~PA11 是 TIM1_CH1~CH4 的固定重映射引脚,无需查 AFIO 寄存器配置重映射位;
- 所有示例代码(包括呼吸灯、电机启停)都基于同一组硬件资源,避免“这个例程用 PA8,那个用 PB0,结果引脚冲突”的混乱。
这种“不通用”,恰恰是它能在 20 分钟内跑通的核心原因——你不需要做任何“适配决策”,只需要确认你的板子 PA8 确实连着 LED 或 MOSFET 栅极,然后烧录即可。
3. 怎么用:从解压到波形输出的四步落地流程
3.1 解压与工程导入:Keil MDK-ARM v5.27+ 的兼容性确认
下载得到sjx_PWM.zip后,解压到不含中文和空格的路径(如D:\pwm_demo\),进入Project文件夹,双击sjx_PWM.uvprojx。若使用 Keil v5.27 或更高版本,工程可直接打开;若为 v5.14 及更早版本,需手动升级:右键工程 → “Upgrade Project to Latest Version”。升级后检查Options for Target → Device是否仍为STM32F103C8,并确认Use MicroLIB未勾选(SPL 不依赖该库)。
注意:该工程默认使用
Standard Peripheral Library v3.5.0,若你本地安装的是 v3.6.0 或 v3.4.0,需替换Libraries\CMSIS\Device\ST\STM32F1xx\Include\和Libraries\STM32F1xx_StdPeriph_Driver\Inc\下的头文件,并重新添加Src文件夹中对应版本的.c文件(如stm32f10x_tim.c)。不同版本间TIM_BDTRInitTypeDef结构体字段顺序有微小差异,直接混用会导致编译报错unknown type name 'TIM_OSSIState_TypeDef'。
3.2 引脚与外设确认:对照原理图完成物理连接
sjx_PWM.zip 默认输出引脚为:
| 通道 | GPIO | 功能 | 默认状态 |
|---|---|---|---|
| CH1 | PA8 | 主输出 | 高有效 |
| CH2 | PA9 | 互补输出 | 低有效(带死区) |
| CH3 | PA10 | 主输出 | 高有效 |
| CH4 | PA11 | 互补输出 | 低有效(带死区) |
若你的开发板 LED 接在 PB0,而非 PA8,请勿强行修改代码中的GPIO_Pin_8—— 正确做法是:
- 查阅板载原理图,确认 PB0 是否属于 TIM3_CH3(常见于 F103C8T6);
- 复制
PWM_Init()函数,新建PWM_Init_TIM3_CH3(),将所有TIM1替换为TIM3,GPIOA替换为GPIOB,GPIO_Pin_8替换为GPIO_Pin_0; - 修改
RCC_APB2PeriphClockCmd()为RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_GPIOB | RCC_APB1PERIPH_TIM3, ENABLE);; - 在
main()中调用新函数,并注释掉原PWM_Init()。
这种“复制-修改-验证”的方式,比全局搜索替换更安全,避免误改其他通道逻辑。
3.3 占空比动态调节:通过串口指令实时控制 CCR 值
包内main.c已预留串口接收框架(基于 USART1,PA9/RX、PA10/TX),但未实现完整协议。实际使用时,你只需在USART1_IRQHandler()中添加如下逻辑:
extern __IO uint16_t CCR1_Val; // 声明为外部变量,已在 pwm.c 中定义 void USART1_IRQHandler(void) { uint8_t res; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { res = USART_ReceiveData(USART1); if(res >= '0' && res <= '9') // 接收 0~9 字符 { uint8_t num = res - '0'; CCR1_Val = (num * 100); // 0→0%, 1→10%, ..., 9→90% TIM_SetCompare1(TIM1, CCR1_Val); // 立即更新 CCR1 } } }逻辑说明:
TIM_SetCompare1()是 SPL 提供的寄存器直写函数,绕过TIM_OC1Init()的初始化流程,直接修改影子寄存器值。由于已开启预装载(见 2.2 节),该调用不会造成波形毛刺,而是等待下一个更新事件生效。参数CCR1_Val必须在0 ~ TIM_Period范围内,否则超出部分会被硬件截断(如CCR1_Val = 1200在ARR=999时等效于1000,即 100% 占空比)。
3.4 编译与烧录:验证输出波形的关键观察点
编译成功后,使用 ST-Link V2 烧录。上电后,用示波器探头接触 PA8,应看到稳定方波。此时重点观察三项:
- 频率:测量周期 T,计算
f = 1/T,确认是否为 1kHz(T ≈ 1ms); - 占空比:测量高电平时间
t_high,计算Duty = t_high / T,初始应为 50%; - 边沿质量:上升/下降沿是否陡峭(< 100ns),有无过冲或振铃(若有,检查 PCB 走线是否过长、是否缺少 100nF 退耦电容)。
若波形异常,先断开负载(如电机、LED),仅接示波器探头测试——很多“无输出”问题实为驱动能力不足导致的电平塌陷。
4. 避坑:五个真实踩过的坑与血泪排查记录
4.1 现象:PA8 无任何波形输出,示波器显示恒定低电平
原因:GPIO_Init()中GPIO_Mode被误设为GPIO_Mode_Out_PP(通用推挽),而非GPIO_Mode_AF_PP(复用推挽)。通用模式下,GPIO 外设复用功能被禁用,TIM1_CH1 信号无法到达 PA8 引脚。
解决:检查gpio.c中GPIO_InitStructure.GPIO_Mode的赋值,确保为GPIO_Mode_AF_PP;同时确认RCC_APB2PeriphClockCmd()中已使能RCC_APB2PERIPH_GPIOA。
4.2 现象:波形频率正确,但占空比始终为 0% 或 100%,调节TIM_SetCompare1()无效
原因:TIM_OCInitStructure.TIM_OCMode被设为TIM_OCMode_Active或TIM_OCMode_Inactive(电平翻转模式),而非TIM_OCMode_PWM1。这两种模式下,CCR 值仅控制比较匹配时的电平跳变时机,不生成 PWM 波形。
解决:回到pwm.c,定位TIM_OC1Init()调用前的结构体初始化,将.TIM_OCMode明确设为TIM_OCMode_PWM1(高电平有效)或TIM_OCMode_PWM2(低电平有效)。
4.3 现象:启用互补通道(CH1+CH2)后,两路波形完全相同,无相位差与死区
原因:未调用TIM_BDTRConfig()配置死区寄存器,且TIM_OCInitStructure.TIM_OutputNState未设为TIM_OutputNState_Enable。互补输出需同时启用主通道(CH1)和互补通道(CH2),并配置 BDTR 寄存器使能 MOE(主输出使能)位。
解决:在PWM_Init()中TIM_Cmd(TIM1, ENABLE)前添加:
TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime = 100; // 死区时间 100 个计数周期(约 100us) TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_Low; TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Disable; TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 关键!必须开启 MOE4.4 现象:串口调节占空比时,波形出现明显抖动或跳变
原因:TIM_SetCompare1()被在非中断上下文(如main()循环中)频繁调用,且未关闭定时器更新中断。每次TIM_SetCompare1()触发后,若恰逢更新事件(UEV)发生,可能导致 CCR 值在计数中途被载入,破坏当前周期完整性。
解决:将占空比更新逻辑移至TIM1_UP_IRQHandler()中,并添加标志位控制:
volatile uint8_t update_flag = 0; uint16_t new_CCR1 = 500; void TIM1_UP_IRQHandler(void) { if(TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); if(update_flag) { TIM_SetCompare1(TIM1, new_CCR1); update_flag = 0; } } } // 串口接收中改为: if(res >= '0' && res <= '9') { new_CCR1 = (res - '0') * 100; update_flag = 1; // 仅置位,不在中断外修改 CCR }4.5 现象:烧录后程序不运行,MCU 进入 HardFault_Handler
原因:SystemInit()中SetSysClockTo72()函数未正确配置 PLL 倍频,导致系统时钟非 72MHz,进而使TIM_Period和TIM_Prescaler计算失效。常见于使用非官方启动文件(如自定义startup_stm32f10x_md.s)时,SystemInit()被跳过。
解决:在main()开头强制调用SystemInit(),并在main()中添加时钟验证:
RCC_ClocksTypeDef RCC_Clocks; RCC_GetClocksFreq(&RCC_Clocks); printf("SYSCLK: %d Hz\r\n", RCC_Clocks.SYSCLK_Frequency); // 应输出 72000000若输出非 72MHz,则需检查system_stm32f10x.c中SetSysClockTo72()的 PLLSRC、PLLMUL、HPRE 等配置是否与你的晶振频率(通常为 8MHz)匹配。
5. 进阶技巧:用它做电机 H 桥驱动的最小闭环验证
5.1 构建 H 桥控制逻辑:主-互补通道配对与防直通
H 桥驱动需两组互补 PWM 信号(如左桥臂 CH1/CH2,右桥臂 CH3/CH4),且必须确保同一桥臂上下管不同时导通。sjx_PWM.zip 的 TIM1 天然支持此结构:CH1(PA8)与 CH2(PA9)为一对互补,CH3(PA10)与 CH4(PA11)为另一对。关键在于死区时间(Dead Time)的协同配置——若仅给 CH1/CH2 设 100ns 死区,而 CH3/CH4 未设,则右桥臂可能直通。
实际做法是:在PWM_Init()中,为四路通道统一配置 BDTR:
// 同时启用所有互补通道 TIM_OC1Init(TIM1, &TIM_OCInitStructure); // CH1 主 TIM_OC1NInit(TIM1, &TIM_OCInitStructure); // CH2 互补 TIM_OC2Init(TIM1, &TIM_OCInitStructure); // CH3 主 TIM_OC2NInit(TIM1, &TIM_OCInitStructure); // CH4 互补 // 统一死区:所有通道共享同一 BDTR 配置 TIM_BDTRInitStructure.TIM_DeadTime = 200; // 加大至 200,覆盖 MOSFET 开关延迟 TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE);注意:
TIM_OC1NInit()与TIM_OC1Init()必须成对出现,且TIM_OCInitStructure.TIM_OutputNState必须为TIM_OutputNState_Enable,否则互补通道无输出。
5.2 实现方向控制:通过 GPIO 模拟逻辑电平切换
H 桥方向由两组 PWM 的相位关系决定。最简方案是:
- 正转:CH1/CH4 输出 PWM,CH2/CH3 恒低;
- 反转:CH2/CH3 输出 PWM,CH1/CH4 恒低。
这无需修改 TIM1 配置,仅需控制 GPIO 输出电平:
#define H_BRIDGE_LEFT_HIGH() do{ GPIO_ResetBits(GPIOA, GPIO_Pin_9); GPIO_SetBits(GPIOA, GPIO_Pin_8); }while(0) #define H_BRIDGE_LEFT_LOW() do{ GPIO_SetBits(GPIOA, GPIO_Pin_9); GPIO_ResetBits(GPIOA, GPIO_Pin_8); }while(0) #define H_BRIDGE_RIGHT_HIGH() do{ GPIO_ResetBits(GPIOA, GPIO_Pin_11); GPIO_SetBits(GPIOA, GPIO_Pin_10); }while(0) #define H_BRIDGE_RIGHT_LOW() do{ GPIO_SetBits(GPIOA, GPIO_Pin_11); GPIO_ResetBits(GPIOA, GPIO_Pin_10); }while(0) void Motor_Dir_Forward(void) { H_BRIDGE_LEFT_HIGH(); H_BRIDGE_RIGHT_LOW(); TIM_SetCompare1(TIM1, 600); // CH1 占空比 60% TIM_SetCompare2(TIM1, 0); // CH2 关闭(互补通道自动为低) TIM_SetCompare3(TIM1, 0); // CH3 关闭 TIM_SetCompare4(TIM1, 600); // CH4 占空比 60% }此方案利用互补通道的“反相”特性,当 CH1 输出 PWM 时,CH2 自动输出反相波形;若需 CH2 恒低,则直接TIM_SetCompare2(TIM1, 0)即可(因TIM_OCMode_PWM1下 CCR=0 对应 0% 占空比,即恒低)。
5.3 验证闭环稳定性:用 ADC 采样电流做简单限流保护
电机堵转时电流激增,需实时监测。sjx_PWM.zip 未集成 ADC,但可快速扩展:利用 PA0 作为 ADC1_IN0 通道,采样串联在电机回路中的 0.1Ω 采样电阻电压。
uint16_t adc_value; uint16_t current_limit = 2000; // 对应 2A(0.1Ω * 2A = 0.2V → ADC 值 ≈ 2000/4095*3.3V) void ADC_Init(void) { ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA | RCC_APB2PERIPH_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55_5Cycles); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); } // 在 TIM1 更新中断中加入: if(adc_value > current_limit) { TIM_CtrlPWMOutputs(TIM1, DISABLE); // 立即关闭所有 PWM 输出 // 触发故障指示(如点亮红灯) }血泪经验:ADC 采样必须在 PWM 周期的“谷底”进行(即电感电流最稳定时),否则纹波干扰导致误触发。建议在
TIM1_CC_IRQHandler()中,当检测到 CH1 下降沿时启动 ADC 转换,比在TIM1_UP_IRQHandler()中更精准。
从那以后我每次做电机驱动验证,都会先用 sjx_PWM.zip 搭出基础 PWM 波形,再逐步叠加 ADC 采样、PID 调节、故障保护——它像一把瑞士军刀,不炫技,但每一步都踩在确定性的基石上。希望帮到你。
本文还有配套的精品资源,点击获取