尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32 PWM呼吸灯实战:定时器配置、占空比计算与DMA优化

STM32 PWM呼吸灯实战:定时器配置、占空比计算与DMA优化 简介这份资源围绕STM32F103C8T6在PA1引脚通过TIM1输出PWM实现呼吸灯效果面向嵌入式开发初学者、电子竞赛备赛者及需要完成课程设计的读者。压缩包共180个文件大小约5.44MB除C/H源文件外还包含uvproj/uvopt等Keil工程配置、o/lst/map/axf/hex等编译中间产物及可烧录文件便于直接打开工程、查看编译过程或烧录到开发板验证效果。内容完整介绍TIM1时钟使能、定时器时基初始化、PWM通道配置、占空比预装载以及通过软件定时或更新中断方式动态调节占空比实现呼吸渐变同时涵盖GPIO复用、定时器计数原理和常见配置易错点可帮助理解STM32定时器PWM底层机制。已有908人学习下载适合希望结合完整工程快速掌握PWM呼吸灯实现思路并进一步扩展到电机调速、LED调光等应用的学习者。1. 先对引脚PA1 默认是 TIM2_CH2不是 TIM1STM32F103C8T6 最小系统板上做 PWM 呼吸灯最容易踩的第一个坑不是代码而是引脚和定时器的对应关系。PA1 在默认 AFIO 映射下挂在 TIM2_CH2 上而 TIM1 的四个输出通道固定在 PA8~PA11标题里写的“time1”如果被理解成“定时器 TIM1”和 PA1 实际对不上。很多人照着例程抄灯不亮换成 PA8 又发现板子上飞线对不上。这篇文章把频率选型、占空比计算、标准库初始化和呼吸效果优化一次讲完PA1 怎么接、TIM1/TIM2 怎么选、波形怎么验证都有对应命令和参数。2. 呼吸灯 PWM 的频率、占空比与 ARR/PSC 参数设计2.1 呼吸灯频率500Hz 是人眼底线1kHz 是保守起点LED 的呼吸灯效果本质是快速开关 LED让人眼因为视觉暂留而感知到平均亮度。PWM 频率太低时亮度波动会被人眼捕捉到典型表现是“能看见灯在一跳一跳地闪”。人眼对 50Hz 左右的闪烁最敏感通常 100Hz 以上才比较安全而在实际工程里我一般直接从 1kHz 起步留出余量。对 STM32F103C8T6 来说1kHz 的 PWM 频率在定时器资源上没有任何压力CCR 分辨率也可以做得很细。如果 PWM 后面还要驱动三极管或 MOSFET 放大电路频率也不能盲目拉高。1kHz 对双极型三极管是舒服的工作区间开关损耗可以忽略如果用 MOSFET 驱动大功率 LED频率可以提高到 20kHz 以上但此时要额外关注栅极驱动电阻和振铃。先确定频率再算 ARR 和 PSC这是后面所有参数的地基。2.2 占空比与亮度的关系先算公式再选分辨率PWM 输出频率由定时器时钟、PSC 预分频器和 ARR 自动重装载值共同决定。F103 的定时器时钟是 72MHz标准公式是PWM_Freq 72MHz / ((PSC 1) * (ARR 1))占空比则取决于比较寄存器 CCR 与 ARR 的比值。在向上计数模式下Duty CCR / (ARR 1) * 100%注意公式右侧是 ARR1不是 ARR。很多人初始化时写TIM_Period 999然后在计算占空比时直接除以 999误差在 0.1% 以内可以忽略但当你把 ARR 设成 0 或者 255 这类小数值时误差会被放大。我习惯把ARR1当成整个周期的“总份数”来理解CCR 就是“高电平份数”这样逻辑上不容易乱。分辨率的概念也由这个公式推导出来占空比可调档位数等于ARR1。ARR999 时是 1000 档ARR255 时是 256 档。档位太少呼吸过程会看到明显台阶亮度不是连续爬升而是“一跳一跳”的档位太多则 CCR 的加减步长要相应调整否则一个呼吸周期要跑很久。呼吸灯场景下10bit 即 1024 档是个既平滑又不浪费资源的选择。2.3 C8T6 常用三组参数8bit / 10bit / 12bit给出一组可以直接抄的参数表都是以 APB1 定时器时钟 72MHz 为前提。如果你的板子没有焊接外部 8MHz 晶振、系统时钟不是 72MHz下面所有频率都会按比例偏移这是排查“呼吸灯频率不对”时的第一怀疑对象。方案ARRPSCPWM 频率占空比分辨率适用场景8bit255713906 Hz256 级指示灯、对频率不敏感的场景10bit1023351953 Hz1024 级呼吸灯推荐平滑且频率足够高12bit409535488 Hz4096 级仅测试分辨率时用不适合长期点亮10bit 方案的频率接近 2kHz远超闪烁阈值分辨率又有 1024 档是呼吸灯项目里最稳的平衡点。12bit 方案虽然档位细腻但 488Hz 已经低于 500Hz部分视力敏感的人能感觉到轻微频闪我不建议在成品里用。8bit 方案适合快速验证代码流程引脚上只要看到亮度变化说明 PWM 通路已经打通再切回 10bit 做效果优化。3. 标准库最小实现PA1 输出 PWM 并跑通呼吸循环3.1 GPIO、定时器、OC 三段式初始化PA1 默认复用功能是 TIM2_CH2。标准库初始化分三步开时钟、配 GPIO 复用推挽、配置定时器和输出比较通道。下面是一份可以直接在 Keil 工程里跑的初始化代码void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 1. 开时钟TIM2 在 APB1 上GPIOA 和 AFIO 在 APB2 上 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // 2. PA1 复用推挽输出速度 50MHz 足够 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 定时器时基72MHz / (72 * 1000) 1kHz TIM_TimeBaseStructure.TIM_Period 999; // ARR TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 4. CH2 输出比较PWM1 模式极性高 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; // 初始 CCR 0 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC2Init(TIM2, TIM_OCInitStructure); // 5. 预装载CCR 和 ARR 都在更新事件时生效 TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); // 6. 启动定时器 TIM_Cmd(TIM2, ENABLE); }代码里有三处值得说明。第一GPIO 模式必须用GPIO_Mode_AF_PP复用推挽不能用通用推挽输出否则引脚不受定时器控制。第二PWM1 模式下向上计数时 CNT 小于 CCR 输出有效电平所以 CCR 越大高电平时间越长亮度越高。第三预装载配置让 CCR 的修改不在当下生效而是在下一个更新事件统一生效避免在计数中途修改造成一个异常宽的脉冲。3.2 主循环用三角波更新 CCR延时选多大初始化完成后主循环里只需要做一件事让 CCR 从 0 加到 ARR再从 ARR 减到 0。最简单的方式是线性三角波int main(void) { uint16_t cc 0; int8_t dir 1; Delay_Init(); // 基于 SysTick 的毫秒延时 PWM_Init(); while (1) { TIM_SetCompare2(TIM2, cc); // 更新 CCR cc dir * 2; // 每一步跳 2 档减少循环次数 if (cc 999) { cc 999; dir -1; } if (cc 0) { cc 0; dir 1; } Delay_ms(2); // 每一步停留 2ms } }参数搭配决定了呼吸周期。CCR 从 0 加到 999每步跳 2 需要 500 步每步 2ms上升段是 1 秒一个完整呼吸周期约 2 秒节奏接近自然呼吸。如果觉得太快把Delay_ms改成 4 或 5周期会线性拉长。这里不建议用HAL_Delay或自旋延时以外的方案因为呼吸灯对实时性要求不高阻塞式延时反而是最可控的。3.3 若坚持用 TIM1改通道、开 APB2、加 TIM_CtrlPWMOutputs如果板子上 PA1 位置不方便或者项目要求必须用 TIM1那就得换思路。TIM1 是高级定时器四个通道默认映射在 PA8~PA11除非做了重映射否则不可能从 PA1 输出 TIM1 的 PWM。这是芯片引脚复用决定的硬约束不是软件能绕开的。把 CH2 的代码换成 TIM1_CH1 时改动点有三个时钟从 APB1 换到 APB2引脚换成 PA8通道初始化函数从TIM_OC2Init换成TIM_OC1Init。还有一个新手最容易漏的坑——高级定时器的主输出使能位 MOE 默认是关闭的不加下面这行PA8 永远是低电平TIM_CtrlPWMOutputs(TIM1, ENABLE); // 打开 TIM1 主输出 MOE这行代码必须在TIM_Cmd(TIM1, ENABLE)前后调用我习惯放在使能定时器之前。忘记它的典型现象是寄存器配置全部正确在线调试看到 CCR 在跳但示波器或 LED 上完全没有波形。初级定时器 TIM2~TIM4 没有这个位这也是很多人从 TIM2 移植到 TIM1 后卡住半天的原因。3.4 常见坑复用开漏、板载占用、HSE 未焊接先看引脚占用。C8T6 最小系统板上的 PA1 通常被引出到排针但有些板子会把 PA1 同时接到板载按键或 LED 上直接驱动外部 LED 时可能被板载器件分压亮度上不去。拿到板子先看原理图确认 PA1 这一路是独立引出的。其次是 GPIO 模式有人把GPIO_Mode_AF_PP写成GPIO_Mode_AF_OD即复用开漏此时 PA1 必须外接上拉电阻才能输出高电平LED 亮度会明显偏暗且上升沿变缓。最后是时钟问题若最小系统板没焊 8MHz 晶振代码里又是基于 72MHz 算的 PSC 和 ARR此时 PWM 频率会整体跑偏呼吸灯要么闪得厉害要么亮灭周期异常。排查时先用RCC_GetClocksFreq读出系统时钟再回头对公式。4. 让呼吸更自然正弦表、gamma 校正与 DMA 更新 CCR4.1 生成 100 点正弦表替代线性三角波线性三角波做出来的呼吸灯人眼看起来“有气无力”亮点区域停留时间短暗区变化又太突然。原因是人眼对亮度的感知并不是线性的而呼吸灯的物理过程——吸气、屏息、呼气——本身也更接近正弦曲线的平滑过渡。改用正弦表驱动 CCR 是成本最低的优化。#define TABLE_SIZE 100 uint16_t sine_ccr[TABLE_SIZE]; void SineTable_Generate(void) { for (uint16_t i 0; i TABLE_SIZE; i) { float phase (float)i / TABLE_SIZE * 2.0f * 3.14159265f; float brightness 0.5f 0.5f * sinf(phase); // 0.0 ~ 1.0 sine_ccr[i] (uint16_t)(brightness * 999.0f); // 映射到 CCR } }表长 100、一个周期 2 秒时每 20ms 更新一档呼吸过渡非常顺滑。启动时算一次表运行中只查表不计算避免sinf在循环里反复执行。brightness本身不含负值配合0.5 0.5 * sin的平移保证正弦波中心在 50% 占空比LED 不会出现完全熄灭的停顿感。4.2 人眼对数感知给 CCR 做 gamma 2.2 映射正弦表解决了“亮度随时间的变化节奏”但从 CCR 数值到人眼感知亮度之间还隔着一层非线性。LED 的物理亮度与电流近似线性而人眼感知却是对数的CCR 从 0 加到 100 时亮度变化很明显从 900 加到 999 时几乎看不出区别。输出的 CCR 应该先做 gamma 校正让“数值的线性变化”对应“感知的线性变化”。for (uint16_t i 0; i TABLE_SIZE; i) { float t (float)i / (TABLE_SIZE - 1); // 归一化时间 0~1 float breath 0.5f 0.5f * sinf(2.0f * 3.14159265f * t); float corrected powf(breath, 1.0f / 2.2f); // gamma 校正 pwm_table[i] (uint16_t)(corrected * 999.0f); }powf的指数取1/2.2约等于 0.4545这是对感知曲线的最常见近似。经过映射后低亮度区间会被拉长CCR 在 0~300 之间分配的档位变多暗部过渡更细腻高亮度区间的档位被压缩但人眼本来就不敏感。这张表同时融合了正弦时间曲线和 gamma 校正一个数组就够用。注意powf在 Cortex-M3 上耗时偏高但在启动时只算 100 次对性能没有影响。4.3 用 TIM_UP 事件触发 DMA 自动更新 CCR主循环里反复调TIM_SetCompare2虽然简单但会让 CPU 一直处于“醒来、改寄存器、睡过去”的状态。更工程化的做法是用 DMA 把 CCR 更新自动化查表数据放在内存DMA 定时搬运到TIM2-CCR2CPU 完全不参与刷新过程。这里有个隐藏约束——PWM 输出本身的 update 事件频率如果是 1kHzDMA 也会以 1kHz 的节奏搬数据100 个点的表 100ms 就跑完呼吸太快。所以 DMA 触发源不能直接用 PWM 定时器的 update而是另开一个定时器专门产生“呼吸节奏”。void DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // TIM3 只作为 DMA 触发源不输出 PWM RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // TIM3 定时 20ms72MHz / 7200 / 200 50Hz TIM_TimeBaseStructure.TIM_Prescaler 7199; TIM_TimeBaseStructure.TIM_Period 199; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_DMACmd(TIM3, TIM_DMA_Update, ENABLE); // update 事件触发 DMA 请求 TIM_Cmd(TIM3, ENABLE); // DMA1_Channel2 是 TIM3_UP 的请求通道 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)TIM2-CCR2; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)pwm_table; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize TABLE_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环搬运永不停止 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel2, DMA_InitStructure); DMA_Cmd(DMA1_Channel2, ENABLE); }这段代码的关键认知是DMA 的请求源和传输目标地址是两回事。请求源 TIM3_UP 只负责决定“什么时候搬一次”目标地址指向 TIM2-CCR2 决定“搬到哪”。PWM 输出由 TIM2 维持 1kHz 不变CCR 的值由 DMA 每 20ms 刷新一次100 个点正好 2 秒一个周期。实际调试中把pwm_table换成上一小节的 gamma 校正表即可CPU 空出来后可以把精力放在其他逻辑上。5. 没有示波器怎么验证 PA1 的 PWM 波形与亮度示波器不是人人都有但验证 PWM 是否正常工作有更朴素的等效手段。第一种是万用表直流档测 PA1 对地平均电压。PWM 信号经过万用表内部滤波后读数大约是3.3V * 占空比把 CCR 固定在 499 也就是约 50% 占空比时万用表应该读到 1.6~1.7V。这个现象同时能证明引脚确实在输出 PWM顺便能估算实际占空比是否和配置一致。注意 F103 的 GPIO 高电平接近 3.3V如果你的板子供电是 3.0V读数会相应降低。第二种是固定三档亮度做肉眼判断。把 CCR 依次设为 0、ARR/2、ARR观察 LED 亮度应有“全灭、中等、最亮”三档变化。如果 RCC 和 GPIO 配置有问题三档亮度没有任何区别如果定时器时钟配置错误中间档的亮度会明显不符合 50% 的预期。这个测试比直接跑呼吸循环更容易定位问题因为它把变量拆开了。第三种是用逻辑分析仪抓波形。C8T6 的 PA1 是普通 3.3V 逻辑电平逻辑分析仪采样率设 10MHz 以上就能看到频率 1kHz、脉宽随 CCR 变化的矩形波。观察要点有两个一是高电平幅值是否接近 3.3V若只有 2V 左右多半是引脚被板载负载分压二是上升沿是否干净肉眼可见的缓坡说明 GPIO 输出驱动能力不足或外部电容过大。示波器能看到的振铃和过冲逻辑分析仪上看不到但至少能确认时序逻辑完全正确。最后提醒一个 LED 驱动层面的问题PA1 直接驱动 LED 时GPIO 的灌电流或拉电流能力有限F103 的数据手册给出的绝对最大值是 25mA我一般把电流控制在 10mA 以内。红色 LED 的管压降约 2V3.3V 供电下限流电阻取(3.3 - 2) / 0.01 130Ω实际取 330Ω 也能获得肉眼舒适的呼吸亮度。如果目标 LED 功率较大PA1 只能接三极管或 MOSFET 的栅极/基极此时把 PWM 频率保持在 1kHz三极管的开关损耗可以忽略不计。把 CCR 换成其他通道号后这套验证流程在新板子上可以原样复用。本文还有配套的精品资源点击获取
返回列表