开发环境:
IDE:MounRiver Studio
MCU:CH32V208
1 呼吸灯的工作原理
呼吸灯,就是指灯光设备的亮度随着时间由暗到亮逐渐增强,再由亮到暗逐渐衰减,很有节奏感地一起一伏,就像是在呼吸一样,因而被广泛应用于手机、电脑等电子设备的指示灯中。
要使用数字器件控制灯光的强弱,我们很自然就想到 PWM(脉冲宽度调制)技术。假如以LED 作为灯光设备,且由控制器输出的 PWM 信号可以直接驱动 LED,PWM 信号中的低电平可点亮 LED 灯。当 LED 以较高的频率进行开关(亮灭)切换时,由于视觉暂留效应,人眼是看不到 LED 灯的闪烁现象的,反映到人眼中能感觉到的是亮度的差别。即以一定的时间长度为周期,LED 灯亮的平均时间越长,亮度就越高,反之越暗。因此,我们可以使用高频率的 PWM 信号,通过调制信号的占空比,控制 LED 灯的亮度。
那么具体我们应该控制 LED 灯以怎样的亮度曲线变化能够达到最好的效果呢?亮度随着时间逐渐变强再衰减,可以用两种常见的数学函数表示,分别是半个周期的正弦函数与指数上升曲线及其对称得到的下降曲线。
相对来说,使用下凹函数曲线灯光处于暗的状态更长,所以指数函数的曲线更符合我们呼吸灯的亮度变化要求。
2 呼吸灯实现
2.1 简单方式
笔者先用最简单的方式来实现,也就是定时改变比较寄存器的值。
1.初始化 GPIO
下面分析具体的定时器配置代码。本实验使用 PB0 作为定时器 PWM 输出通道,先对它进行初始化。作 PWM 输出通道的引脚需要被配置为复用推挽输出模式。
static void TIM_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 ;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
2.配置定时器模式
在TIM_Mode_Config()函数中,完成了呼吸灯所需要的定时器 PWM 输出模式配置。
static void TIM_Mode_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_TimeBaseStructure.TIM_Period = 255;
TIM_TimeBaseStructure.TIM_Prescaler =143;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
TIM_OC3Init(TIM3, &TIM_OCInitStructure);
TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_Cmd(TIM3, ENABLE);
}
这个定时器的模式配置主要分为两个部分,分别为时基初始化,输出模式初始化。
代码中前10行是定时器的时基初始化,这部分主要负责配置定时器的定时周期、时钟频率、计数方式等。它使用到库函数 TIM_TimeBaseInit,利用结构体TIM_TimeBaseInitTypeDef 进行配置,该结构体有以下成员:
1) TIM_Period
定时周期,实质是存储到重载寄存器 TIMx_ARR 的数值,脉冲计数器从 0 累加到这个值上溢或从这个值自减至 0 下溢。这个数值加 1 然后乘以时钟源周期就是实际定时周期。
本实验中向该成员赋值为 255,即定时周期为(255+1)* T ,T 为定时器的时钟周期。
2) TIM_Prescaler
对定时器时钟 TIMxCLK 的预分频值,分频后作为脉冲计数器 TIMx_CNT 的驱动时钟,得到脉冲计数器的时钟频率为:fCK_CNT=fTIMx_CLK/(N+1),其中 N 为即为赋给本成员的时钟分频值。
3) TIM_ClockDivision
时钟分频因子。怎么又出现一个配置时钟分频的呢?要注意这个TIM_ClockDivision 和上面的 TIM_Prescaler 是不一样的。TIM_Prescaler 预分频配置是对TIMxCLK进行分频,分频后的时钟被输出到脉冲计数器TIMx_CNT ,而TIM_ClockDivision 虽然也是对 TIMxCLK 进行分频,但它分频后的时钟频率为 fDTS,是被输出到定时器的 ETRP 数字滤波器部分,会影响滤波器的采样频率。TIM_ClockDivision 可以被配置为 1 分频(fDTS=fTIMxCLK)、2 分频及 4 分频。ETRP 数字滤波器的作用是对外部时钟 TIMxETR 进行滤波。
本实验中是使用内部时钟 TIMxCLK 作为定时器时钟源的,没有进行滤波所以配置TIM_ClockDivision 为任何数值都没有影响。
4) TIM_CounterMode
本成员配置的为脉冲计数器 TIMx_CNT 的计数模式,分别为向上计数,向下计数,及中央对齐模式。向上计数即 TIMxCNT 从 0 向上累加到 TIM_Period 中的值,(重载寄存器 TIMx_ATRLR的值),产生上溢事件;向下计数则 TIMxCNT 从 TIM_Period 的值累减至0,产生下溢事件。而中央对齐模式则为向上、向下计数的合体,TIMxCNT 从 0 累加到TIM_Period 的值减 1 时,产生一个上溢事件,然后向下计数到 1 时,产生一个计数器下溢事件,再从 0 开始重新计数。
本实验中 TIM_CounterMode 成员被赋值为 TIM_CounterMode_Up(向上计数模式)。填充完配置参数后,调用库函数 TIM_TimeBaseInit()把这些控制参数写到寄存器中,定时器的时基配置就完成了。
在本函数代码的后面是关于定时器的输出模式配置的。通用定时器的输出模式由 TIM_OCInitTypeDef 类型结构体的以下几个成员来设置:
1) TIM_OCMode
输出模式配置,主要使用的为 PWM1 和 PWM2 模式。PWM1 模式是:在向上计数时,当 TIMx_CNT<TIMx_CHnCVR (比较寄存器,其数值等于 TIM_Pulse 成员的内容) 时,通道 n 输出为有效电平,否则为无效电平;在向下计数时,当 TIMx_CNT>TIMx_CHnCVR时通道 n 为无效电平,否则为有效电平。PWM2 模式跟 PWM1模式相反。
其中的有效电平和无效电平并不是固定地对应高电平和低电平,也是需要配置的,由下面介绍的 TIM_OCPolarity 成员配置。本实验中使用 PWM1 输出模式。
2) TIM_OutputState
配置输出模式的状态使能或关闭输出。
本实验中向该成员赋值为 TIM_OutputState_Enable(使能输出)。
3) TIM_OCPolarity
有效电平的极性,把 PWM 模式中的有效电平设置为高电平或低电平。
本实验中向该成员赋值为 TIM_OCPolarity_Low(有效电平为低电平),因为在上面把输出模式配置为 PWM1 模式,向上计数,所以在 TIMx_CNT<TIMx_CHnCVR时,通道 n 输出为低电平,否则为高电平。
4) TIM_Pulse
直译为跳动,本成员的参数值即为比较寄存器 TIMx_CHnCVR的数值,当脉冲计数器TIMx_CNT 与 TIMx_CHnCVR的比较结果发生变化时,输出脉冲将发生跳变。
本实验中就是通过不断改变比较寄存器 TIMx_CHnCVR的值,赋予它指数曲线数据,达到控制 PWM 信号的占空比呈指数曲线变化的目的。在本函数代码中,我们对该成员赋予初始为 0,而改变比较寄存器 TIMx_CHnCVR值的操作是在中断服务函数中修改的。填充完输出模式初始化结构体后,调用输出模式初始化函数 TIM_OCxInit()对通道进行初始化。
以上是最基本的PWM输出调制实现呼吸灯,由 TIM3_CH2 输出 PWM 来控制LED的亮度。下面我们介绍通过库函数来配置该功能的步骤。
1)开启 TIM3 时钟以及复用功能时钟,配置 PB0为复用输出。
要使用 TIM3,我们必须先开启 TIM3 的时钟,这点相信大家看了这么多代码,应该明白了。这里我们还要配置 PB0为复用输出,此时,PB0属于复用功能输出。在此只列出库函数设置 AFIO 时钟的方法。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
其余的和前面的配置一样,就不再列出了。
2)初始化 TIM3,设置 TIM3 的 ATRLR和 PSC。
3)设置 TIM3_CH3的 PWM 模式,使能 TIM3 的 CH3输出。
在库函数中, PWM 通道设置是通过函数 TIM_OC1Init()~TIM_OC4Init()来设置的, 不同的通道的设置函数不一样,这里我们使用的是通道3,所以使用的函数是 TIM_OC3Init()。
void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
4)使能 TIM3。
在完成以上设置了之后,我们需要使能 TIM3。使能 TIM3 的方法前面已经讲解过:
TIM_Cmd(TIM3, ENABLE);
5)修改 TIM3_CH3CVR来控制占空比。
最后,在经过以上设置之后, PWM 其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改 TIM3_CH3CVR则可以控制 CH3的输出占空比。继而控制LED的亮度。在库函数中,修改 TIM3_CH3CVR占空比的函数是:
void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare2);
当然也可直接操作寄存器TIM3->CCR3。
理所当然,对于其他通道,分别有一个函数名字, 函数格式为 TIM_SetComparex(x=1,2,3,4)。通过以上5个步骤,我们就可以控制 TIM3 的 CH3输出 PWM 波了。
接下来看看主函数的代码:
int main(void)
{
uint16_t i = 0;
FlagStatus breath_flag = SET;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
SysTick_Init();
BSP_Breathing_Init(255, 143);
while( 1 )
{
Delay_ms(5);
if(SET == breath_flag)
{
i++;
}
else
{
i
}
if(255 < i)
{
breath_flag = RESET;
}
if(0 >= i)
{
breath_flag = SET;
}
TIM_SetCompare3(TIM3, i);
//TIM3->CH3CVR = (uint32_t)i;
}
}
代码很简单,就是不断改变CH3CVR的值从而控制 CH3的输出占空比。
2.2 中断方式
1.生成指数曲线 PWM 数据
要实现 LED 亮度随着指数曲线变化,我们需要使用占空比呈指数曲线变化的 PWM 信号,而这样的信号由定时器经过查表产生。这个表的数据存储在程序中的数组 indexWave中。
uint8_t indexWave[] = {1,1,2,2,3,4,6,8,10,14,19,25,33,44,59,80,
107,143,191,255,255,191,143,107,80,59,44,33,25,19,14,10,8,6,4,3,2,2,1,1};
这个表有 40 个数字,从图中可以看到这些数字呈指数上升再衰减,正好是呼吸灯的一个控制周期。数字的大小范围是 0255,即把 LED 的亮度分为了 0255 个等级。
假如我们把定时器的脉冲计数器 TIMx_CNT 上限设置为 255,把这个表的数据一个一个地赋值到定时器的比较寄存器 TIMx_CHnCVR中,那么在每个 PWM 周期中,当 TIMx_CNT的计数值小于比较寄存器 TIMx_CHnCVR的值时, 就会在通道中输出低电平,点亮 LED,而随着 TIMx_CHnCVR的值由 LED 亮度表得来,所以 LED 点亮的时间就会呈图中的曲线变化,实现呼吸灯的功能。
这个表的数据是使用 matlab 软件生成的。该代码运行后会生成一个“index_wave.c”的文件,用户把该文件中的数据复制到工程中的数组中即可。
clear;
x = [0 : 8/19 : 8];
up = 2.^x ;
up = uint8(up);
y = [8: -8/19 :0];
down = 2.^y ;
down = uint8(down);
line = [[0:8/19:8],[8:8/19:16]]
val = [up , down]
dlmwrite('index_wave.c',val);
plot(line,val,'.');
2.初始化 GPIO
前面已经讲过了,这里就不赘述了。
3.配置定时器模式
在 TIM_Mode_Config 函数中,完成了呼吸灯所需要的定时器 PWM 输出模式配置。
static void TIM_Mode_Config(uint16_t Arr,uint16_t Psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_TimeBaseStructure.TIM_Period = Arr;
TIM_TimeBaseStructure.TIM_Prescaler = Psc;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
TIM_OC3Init(TIM3, &TIM_OCInitStructure);
TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_Cmd(TIM3, ENABLE);
TIM_ITConfig(TIM3,TIM_IT_Update, ENABLE);
NVIC_Config_PWM();
}
这个定时器的模式配置主要分为三个部分,分别为时基初始化,输出模式初始化和中断配置。
时基初始化,输出模式初始化和不同方式差不多,就不再赘述了。下面谈谈中断部分。
本函数剩下的代码用 TIM_OCxPreloadConfig() 配置了各通道的比较寄存器 TIM_CHnCVR预装载使能;使用 TIM_ARRPreloadConfig()把重载寄存器 TIMx_ARR 使能,调用了TIM_ITConfig()配置定时器更新中断,每个定时周期结束后触发一次。该中断的优先级由函数 NVIC_Config_PWM()配置。
static void NVIC_Config_PWM(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
配置好中断,下面就要编写中断服务函数。
void TIM3_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void TIM3_IRQHandler(void)
{
static uint8_t pwm_index = 0;
static uint8_t period_cnt = 0;
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
period_cnt++;
if(period_cnt >= 10)
{
TIM3->CH3CVR= indexWave[pwm_index];
pwm_index++;
if( pwm_index >= 40)
{
pwm_index=0;
}
period_cnt=0;
}
TIM_ClearITPendingBit (TIM3, TIM_IT_Update);
}
}
本中断服务函数在每次定时器更新事件发生时执行一次(即 256 个定时器时钟周期)。函数中使用了静态变量 pwm_index 和 period_cnt,它们分别用来查找 PWM 表元素和记录同样占空比的脉冲输出了多少次。
本代码的目的是每 10 次定时器中断更新一次 PWM 表中的数据到比较寄存器TIMx_CHnCVR中,当遍历完 PWM 表的 40 个元素时,再重头开始遍历 PWM 表,周而复始,重复 LED 的呼吸过程。
整个呼吸过程的时间计算方法如下:
因为定时器的 TIM_Prescaler 设置为 1999;
所以定时器的时钟频率:fTIM = 144000000/(TIM_Prescaler+1) = 72000 Hz
即定时器的时钟周期为:tTIM = 1/fTIM = 1/72000 s
因为定时器的 TIM_Period 设置为 255;
所以定时器的中断周期为:tint= tTIM * (TIM_Period+1) =0.00355 s
因为 PWM 表有 pwm_index = 40 个亮度占空比数据,同种占空比信号输出 period_cnt =10 次
所以一个呼吸周期 T = tint *40 *10 = 1.42s
3 呼吸灯的实验现象
将程序编译好下载到板子中,可一看到LED像呼吸一样渐渐变明或者渐渐变暗。