资讯动态

STM32F103 PWM入门实战:基于SPL的轻量级TIM1驱动包解析

发布时间:2026/10/26 20:06:14 来源:尧图企业网站定制
简介本资源是一个基于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 / (711) 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% 占空比ARR999 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功能默认状态CH1PA8主输出高有效CH2PA9互补输出低有效带死区CH3PA10主输出高有效CH4PA11互补输出低有效带死区若你的开发板 LED 接在 PB0而非 PA8请勿强行修改代码中的GPIO_Pin_8—— 正确做法是查阅板载原理图确认 PB0 是否属于 TIM3_CH3常见于 F103C8T6复制PWM_Init()函数新建PWM_Init_TIM3_CH3()将所有TIM1替换为TIM3GPIOA替换为GPIOBGPIO_Pin_8替换为GPIO_Pin_0修改RCC_APB2PeriphClockCmd()为RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_GPIOB | RCC_APB1PERIPH_TIM3, ENABLE);在main()中调用新函数并注释掉原PWM_Init()。这种“复制-修改-验证”的方式比全局搜索替换更安全避免误改其他通道逻辑。3.3 占空比动态调节通过串口指令实时控制 CCR 值包内main.c已预留串口接收框架基于 USART1PA9/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在ARR999时等效于1000即 100% 占空比。3.4 编译与烧录验证输出波形的关键观察点编译成功后使用 ST-Link V2 烧录。上电后用示波器探头接触 PA8应看到稳定方波。此时重点观察三项频率测量周期 T计算f 1/T确认是否为 1kHzT ≈ 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 现象启用互补通道CH1CH2后两路波形完全相同无相位差与死区原因未调用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 天然支持此结构CH1PA8与 CH2PA9为一对互补CH3PA10与 CH4PA11为另一对。关键在于死区时间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 输出 PWMCH2/CH3 恒低反转CH2/CH3 输出 PWMCH1/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下 CCR0 对应 0% 占空比即恒低。5.3 验证闭环稳定性用 ADC 采样电流做简单限流保护电机堵转时电流激增需实时监测。sjx_PWM.zip 未集成 ADC但可快速扩展利用 PA0 作为 ADC1_IN0 通道采样串联在电机回路中的 0.1Ω 采样电阻电压。uint16_t adc_value; uint16_t current_limit 2000; // 对应 2A0.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 调节、故障保护——它像一把瑞士军刀不炫技但每一步都踩在确定性的基石上。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑