前言
本文以项目的角度出发实现了一个简单的PWM占空比调整的功能。
目录
[2. 调节规则定义](#2. 调节规则定义)
[3. 边界行为规则](#3. 边界行为规则)
[4. 暂不支持的功能](#4. 暂不支持的功能)
一、需求
通过按键交互实现对 LED(亦可扩展至电机转速、液晶屏背光亮度等负载)的输出功率 / 亮度调节控制。
二、逻辑分析
1.基础调节功能
亮度增加:按下 "亮度 +" 按键时,LED 亮度按固定步长提升;
亮度降低:按下 "亮度 -" 按键时,LED 亮度按固定步长下降。
2. 调节规则定义
调节范围:亮度值量化为 0~100 的整数(对应输出功率 0%~100%),其中:
亮度值 = 0:LED 无输出,完全熄灭;
亮度值 = 100:LED 满功率输出,达到最大亮度。
调节步长:单次按键操作的亮度调节步长为 5,兼顾调节实用性与亮度变化的连续性。
3. 边界行为规则
当亮度值已达到上限(100)时,继续按下 "亮度 +" 按键,亮度保持 100 不变;
当亮度值已达到下限(0)时,继续按下 "亮度 -" 按键,亮度保持 0 不变。
4. 暂不支持的功能
按键长按调节功能(仅支持单次按键触发单次调节)。
三、功能模块与接口汇总
|----|--------|---------------------|-----------------------------------------------------------------------|
| 序号 | 功能模块 | 具体功能 | 函数接口 |
| 1 | 独立按键模块 | 采集按键信号,当按键按下时发出中断信号 | void Key_Init(void); 中断服务函数 |
| 2 | 定时器模块 | 按照输入的占空比数值,产生PWM方波 | void TIM2_Init(void); void TIM2_CH2_Set_DutyCycle(uint8_t dutycycle); |
四、硬件分析
1.LED硬件电路

2.独立按键电路

根据LED硬件电路的分析,要点亮LED需要将GPIO引脚拉低,也就是说PWM占空比为0的时候LED亮度达到峰值。
根据独立按键硬件电路的分析,按键按键按下后GPIO引脚会被接入到3.3V高电平,因此检测按键的引脚需要工作在下拉输入模式。
根据硬件信息机芯片的特性,可以统计出本次使用到的GPIO引脚及其初始的工作状态:
|----|------|---------------------------|--------|
| 序号 | 引脚编号 | 工作模式 | 接入电路 |
| 1 | PA1 | 复用功能推挽输出模式 CNF-10;MODE-11 | LED1_R |
| 2 | PA0 | 下拉输入模式 CNF-10;MODE-00 | SW1 |
| 3 | PC13 | 下拉输入模式 CNF-10;MODE-00 | SW2 |
五、代码实现
1.PWM输出部分
(1)涉及到的寄存器
本方案使用STM32F103C8T6这款非常通用的芯片作为MCU,使用芯片自带的通用定时器中的PWM输出模块。在使用寄存器配置的方式实现输出PWM方波时,需要使用到的寄存器统计如下:
|----|---------------------|------------------------|------|---------------------------------------------------------|
| 序号 | 寄存器 / 控制位名称 | 功能 | 配置值 | 说明 |
| 1 | 预分频器 (PSC) | 对输入的时钟信号进行分频处理 | 7199 | 分频系数 = PSC+1=7200,72MHz 输入时钟分频后得到 10KHz 的计数器计数频率 |
| 2 | 自动重装载寄存器 (ARR) | 与 PSC 配合实现时钟计数的溢出周期 | 99 | 10KHz 频率下计数 100 次(0~99)溢出,PWM 频率 = 10KHz/100=100Hz |
| 3 | CR1->ARPE | 控制 ARR 的预装载功能 | 1 | 使能预装载功能,ARR 修改后需等待更新事件才生效,保证 PWM 周期稳定 |
| 4 | CR1->DIR | 控制寄存器的计数方向 | 0 | 向上计数(DIR=0),计数器从 0 递增到 ARR,匹配 PWM 模式 1 的常规使用逻辑 |
| 5 | CCMR1->CC2S[1:0] | 配置定时器通道的输入 / 输出模式 | 00 | 二进制 00,配置通道 2 为输出模式,用于 PWM 输出 |
| 6 | CCMR1->OC2M[2:0] | 配置通道输出模式下的工作模式 | 110 | 二进制 110,配置为 PWM 模式 1(向上计数时,计数值 < CCR2 输出有效电平,反之无效) |
| 7 | CCMR1->OC2PE | 控制 CCR2 的预装载功能 | 1 | 使能预装载功能,CCR2 修改后需等待更新事件生效,避免占空比中途突变 |
| 8 | CCER->CC2E | 使能对应的通道开始输出 | 1 | 使能通道 2 的 PWM 输出功能 |
| 9 | 捕获 / 比较寄存器 (CCR2) | 比较值,与 ARR 配合计算 PWM 占空比 | 99 | 占空比 = CCR2/ARR×100%=99/99×100%=100%,结合硬件逻辑 LED 亮度最低(熄灭) |
| 10 | EGR->UG | 产生更新事件,同步预装载寄存器到影子寄存器 | 1 | 置 1 触发更新事件(硬件自动清零),将 ARR/CCR2 预装载值刷入影子寄存器 |
| 11 | CR1->CEN | 使能定时器,开始输出 PWM 波形 | 1 | 定时器主使能位,置 1 后计数器开始计数,通道 2 输出 PWM 波形 |
(2)配置代码
文件名:tim2.c
cpp
#include "tim2.h"
// 初始化
void TIM2_Init(void)
{
// 1.开启时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// 2.设置GPIOA1引脚的工作模式,PWM方波输出,引脚需要工作在复用功能推挽输出模式:CNF-10;MODE-11;
GPIOA->CRL &= GPIO_CRL_CNF1_0;
GPIOA->CRL |= GPIO_CRL_CNF1_1;
GPIOA->CRL |= GPIO_CRL_MODE1;
// 3.设置TIM2定时器,为了使得LED刷新频率可以达到合适的效果,需要考虑人眼的余晖效应,这里将频率定为100Hz
// 3.1.设置定时器的输入频率,假设ARR的值为99(100次计数),输出100Hz的频率,在输入的时钟频率为72MHz的前提下,PSC的值的计算过程为:
// 输出频率 = 1 / [(1 / (定时器输入时钟频率 / (PSC+1))) * (ARR + 1)]
// 由频率计算公式推到出周期计算公式:
// 输出时钟周期 = (1 / (定时器输入时钟频率 / (PSC+1))) * (ARR + 1)
// 已知:输如频率 = 72MHz ARR+1 = 100; 计算PSC
// 1/100Hz = (1 / (72MHz / (PSC+1))) * 100
// 72MHz / (PSC+1) = 100Hz * 100
// 72MHz / (100Hz * 100) = (PSC+1)
// PSC+1 = 7200
// PSC = 7199
// 不开启预装载功能(默认)
TIM2->CR1 &= ~TIM_CR1_ARPE;
TIM2->PSC = 7199;
// 3.2.设置预装载寄存器的值,为了方便调整PWM的占空比,这个值设定为99,也就是计数100次后溢出。
TIM2->ARR = 99;
// 3.3.设置定时器的计数方向,0代表向上计数
TIM2->CR1 &= ~TIM_CR1_DIR;
// 3.4.设置定时器的通道方向,输出,CCxS-00
TIM2->CCMR1 &= ~TIM_CCMR1_CC2S;
// 3.5.配置定时2器通道2的输出模式:PWM模式1
TIM2->CCMR1 &= ~TIM_CCMR1_OC2M_0;
TIM2->CCMR1 |= TIM_CCMR1_OC2M_1;
TIM2->CCMR1 |= TIM_CCMR1_OC2M_2;
// 3.6.配置一个默认的占空比:99%,因为LED是共阳,因此占空比高代表LED暗。
TIM2->CCR2 = 99;
// 3.7.开启定时器通道输出使能
TIM2->CCER |= TIM_CCER_CC2E;
}
// 开启定时器,开始输出PWM方波
void TIM2_Start(void)
{
TIM2->CR1 |= TIM_CR1_CEN;
}
// 停止定时器,停止输出PWM方波
void TIM2_Stop(void)
{
TIM2->CR1 &= ~TIM_CR1_CEN;
}
// 设置PWM占空比
void SET_DutyCycle(uint8_t dutycycle)
{
TIM2->CCR2 = dutycycle;
}
头文件:tim2.h
cpp
#ifndef __TIME2_H
#define __TIME2_H
#include "stm32f10x.h"
// 函数声明
// 初始化
void TIM2_Init(void);
// 开启定时器,开始输出PWM方波
void TIM2_Start(void);
// 停止定时器,停止输出PWM方波
void TIM2_Stop(void);
// 设置PWM占空比
void SET_DutyCycle(uint8_t dutycycle);
#endif
2.按键检测部分
文件名:key.c
cpp
#include "key.h"
void Key_Init(void)
{
// 1.打开对应的时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
// 2.配置GPIO引脚模式
// 检测按键,下拉输入模式;CNF-10;MODE-00
GPIOA->CRL &= ~GPIO_CRL_CNF0_0;
GPIOA->CRL |= GPIO_CRL_CNF0_1;
GPIOA->CRL &= ~GPIO_CRL_MODE0;
GPIOC->CRH &= ~GPIO_CRH_CNF13_0;
GPIOC->CRH |= GPIO_CRH_CNF13_1;
GPIOC->CRH &= ~GPIO_CRH_MODE13;
// 初始化GPIO引脚为低电平
GPIOA->ODR &= GPIO_ODR_ODR0;
GPIOC->ODR &= GPIO_ODR_ODR13;
// 3.配置AFIO复用功能选择
AFIO->EXTICR[0] |= AFIO_EXTICR1_EXTI0_PA;
AFIO->EXTICR[3] |= AFIO_EXTICR4_EXTI13_PC;
// 4.配置中断触发条件,上升沿触发
EXTI->RTSR |= EXTI_RTSR_TR0;
EXTI->RTSR |= EXTI_RTSR_TR13;
// 5.关闭对应的中断标志位
EXTI->IMR |= EXTI_IMR_MR0;
EXTI->IMR |= EXTI_IMR_MR13;
// 6.配置NVIC中断分组及优先级并开启中断
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(EXTI0_IRQn, 3);
NVIC_SetPriority(EXTI15_10_IRQn, 3);
NVIC_EnableIRQ(EXTI0_IRQn);
NVIC_EnableIRQ(EXTI15_10_IRQn);
}
// 按键中断服务函数
void EXTI0_IRQHandler(void)
{
// 判断中断源
if((EXTI->PR & EXTI_PR_PR0) != 0)
{
// 清除中断挂起标志(置1清除)
EXTI->PR |= EXTI_PR_PR0;
// 延时消抖
Delay_nms(10);
if(GPIOA->IDR & GPIO_IDR_IDR0)
{
add = 1;
}
}
}
// 按键中断服务函数
void EXTI15_10_IRQHandler(void)
{
// 判断中断源
if((EXTI->PR & EXTI_PR_PR13) != 0)
{
// 清除中断挂起标志(置1清除)
EXTI->PR |= EXTI_PR_PR13;
// 延时消抖
Delay_nms(10);
if(GPIOC->IDR & GPIO_IDR_IDR13)
{
sub = 1;
}
}
}
头文件:key.h
cpp
#ifndef __KEY_H
#define __KEY_H
#include "stm32f10x.h"
#include "delay.h"
// 引入外部变量
extern uint8_t add;
extern uint8_t sub;
// 函数声明
// 初始化
void Key_Init(void);
#endif
3.主函数
文件名:main.c
cpp
#include "tim2.h"
#include "delay.h"
#include "key.h"
// 定义全局变量,用于检测按键的状态
uint8_t add = 0;
uint8_t sub = 0;
int main(void)
{
// 初始化定时器2
TIM2_Init();
// 开启定时器
TIM2_Start();
// 初始化按键
Key_Init();
// 定义占空比的初始值
int8_t dutycycle = 100;
while(1)
{
if(add) // 按下增加按键
{
dutycycle += 5;
// 判断当前的占空比的值是否超过边界,并作调整
if(dutycycle > 100)
{
dutycycle = 100;
}
// 清除标志位
add = 0;
}
if(sub) // 按下增加按键
{
dutycycle -= 5;
// 判断当前的占空比的值是否超过边界,并作调整
if(dutycycle < 0)
{
dutycycle = 0;
}
// 清除标志位
sub = 0;
}
// 占空比调整
SET_DutyCycle(dutycycle);
}
}