系列文章目录
第一章:GD32学习笔记1(高难度工程,点亮一个LED灯)
第二章:GD32学习笔记2(按键控制LED灯)
第三章:GD32学习笔记3(PWM控制LED,LED:嘿嘿又是我)
文章目录
前言
大家有没有发现单片机在一般情况下输出的电压都是一定的,那么当它连接LED灯后,LED的亮度就是一定的,那如果要控制LED不那么亮,该怎么搞呢?因为一个问题是明确的,管脚要么输出1,要么输出0。但是如果频繁的在0和1之间切换,让LED闪起来,当闪烁的频率超过人眼能观察的极限时,我们看起来就像是灯光变暗了一样,假如在极短的单位时间内,10%的时间输出1,90%的时间输出0,那么灯光就只有直接输出高电平亮度的10%,有了这样一个理论,我们就可以开始编程实践了。
一、PWM?介系个嘛呀?
以下内容来自百度百科:
对于脉冲,我个人的理解,就是单片机首先发出一个低电平,在这个低电平上突然产生一个高电平,在示波器上显示就是一个竖杠,当把这个竖杠放大来看,发现它是一个没有底的矩形,电平由低变高再变低,就会有所谓的上升沿和下降沿,而PWM波就是一个个这样的矩形组成的,且分布有规律,每两个矩形间的距离相等,即第一个上升沿到第二个上升沿的距离是相等的,下降沿也是。
这样的波形刚好能够满足我们在前言里面提到的控制LED灯光信号的要求,它在一个周期(第一个上升沿到第二个上升沿的时间)内有精确的高电平和低电平各自所占的时间,由于是单片机产生,它的工作频率(一秒钟产生了多少个上升沿)可以轻松超过人眼的极限,我们只需要调整上升沿到下降沿间隔的时间,也即是高电平的时间,就可以调整灯光亮度,高电平时间在整个周期内存在的时间用专业的话将叫占空比,自此,PWM的几大参数就讲完了:
名称 | 含义 |
---|---|
频率 | 一秒钟能产生上升沿或下降沿的数量 |
周期 | 第一个高/低电平到第二个高/低电平的时间 |
占空比 | 高电平与周期之比 |
幅度 | 由单片机输出能力决定 |
二、单片机如何输出PWM
1.关键在于时间
在单片机的外设之中有一个十分重要的东西叫定时器,GD32在程序中管它叫TIMER,ST的芯片习惯叫TIM,其实指的是一个东西,我们都知道单片机的工作是有时钟频率的,以GD32F4为例,它的定时器1最大频率是168MHz,在我的板子上外接了一个25MHz的晶振,进行4倍倍频,能够使定时器1达到最大100MHz,也就是说我每秒钟可以让单片机进行最大100000000次计数。
2.是不是所有的管脚都能使用定时器
从数据手册上可以得知各个定时器通道能够映射到哪个管脚,也即是这个管脚能够复用哪个功能,我们接LED灯的管脚是PC6,数据手册显示PC6可以复用TIMER2_CH0,也即是定时器2的0通道。这样理论和硬件准备就都完成了。
三、添加新的源文件gd32f4xx_timer.c
从工程分组管理添加官方库里面的gd32f4xx_timer.c,所有与定时器有关的函数都在这里面,后面对定时器进行初始化设置需要用到的函数,都要从这里面调用。
四、代码实现
代码实现还是那老三步:管脚、外设、主函数
我们在昨天的笔记里面已经写好了一个按键控制LED的工程,今天直接从上面改,要实现的功能就是通过按键改变PWM的占空比,然后将这个PWM从PC6输出,用来控制LED。
1.修改管脚初始化函数
上一章的管脚初始化中,我们已经写好了PA0输入、带上拉,这部分不用改,关键在于控制LED灯的PC6,原本的PC6是输出模式、推挽输出,但是今天要复用定时器功能,就不能直接输出了。
/*!
\brief set GPIO mode
\param[in] gpio_periph: GPIO port
only one parameter can be selected which is shown as below:
\arg GPIOx(x = A,B,C,D,E,F,G,H,I)
\param[in] mode: GPIO pin mode
\arg GPIO_MODE_INPUT: input mode
\arg GPIO_MODE_OUTPUT: output mode
\arg GPIO_MODE_AF: alternate function mode
\arg GPIO_MODE_ANALOG: analog mode
\param[in] pull_up_down: GPIO pin with pull-up or pull-down resistor
\arg GPIO_PUPD_NONE: floating mode, no pull-up and pull-down resistors
\arg GPIO_PUPD_PULLUP: with pull-up resistor
\arg GPIO_PUPD_PULLDOWN:with pull-down resistor
\param[in] pin: GPIO pin
one or more parameters can be selected which are shown as below:
\arg GPIO_PIN_x(x=0..15), GPIO_PIN_ALL
\param[out] none
\retval none
*/
void gpio_mode_set(uint32_t gpio_periph, uint32_t mode, uint32_t pull_up_down, uint32_t pin)
再把gpio_mode_set这个函数拿出来复习一下,此前mode这个参数我们只用了INPUT和OUTPUT,今天要将它设置为AF复用模式,第三个参数依然是无电阻。
当GPIO模式被设置为复用之后,就要使用一个新函数了:GPIO复用功能设置
/*!
\brief set GPIO alternate function
\param[in] gpio_periph: GPIO port
only one parameter can be selected which is shown as below:
\arg GPIOx(x = A,B,C,D,E,F,G,H,I)
\param[in] alt_func_num: GPIO pin af function
\arg GPIO_AF_0: SYSTEM
\arg GPIO_AF_1: TIMER0, TIMER1
\arg GPIO_AF_2: TIMER2, TIMER3, TIMER4
\arg GPIO_AF_3: TIMER7, TIMER8, TIMER9, TIMER10
\arg GPIO_AF_4: I2C0, I2C1, I2C2
\arg GPIO_AF_5: SPI0, SPI1, SPI2, SPI3, SPI4, SPI5
\arg GPIO_AF_6: SPI1, SPI2, SAI0
\arg GPIO_AF_7: USART0, USART1, USART2
\arg GPIO_AF_8: UART3, UART4, USART5, UART6, UART7
\arg GPIO_AF_9: CAN0, CAN1, TLI, TIMER11, TIMER12, TIMER13
\arg GPIO_AF_10: USB_FS, USB_HS
\arg GPIO_AF_11: ENET
\arg GPIO_AF_12: EXMC, SDIO, USB_HS
\arg GPIO_AF_13: DCI
\arg GPIO_AF_14: TLI
\arg GPIO_AF_15: EVENTOUT
\param[in] pin: GPIO pin
one or more parameters can be selected which are shown as below:
\arg GPIO_PIN_x(x=0..15), GPIO_PIN_ALL
\param[out] none
\retval none
*/
void gpio_af_set(uint32_t gpio_periph, uint32_t alt_func_num, uint32_t pin)
在官方的注释中给了很详细的参数功能说明,一共能设置16种复用功能参数,在前面的数据手册里面已经找到了PC6能复用TIMER2,所以我们将alt_func_num这个参数设置为GPIO_AF_2。
改好这一点后,你就获得了如下的管脚初始化函数:
void GPIO_Config(void)
{
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_GPIOC);
/*LED*/
gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_6);
gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6);
gpio_af_set(GPIOC,GPIO_AF_2,GPIO_PIN_6);
/*KEY*/
gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_0);
gpio_output_options_set(GPIOA, GPIO_PUPD_NONE, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
}
2.定时器初始化函数
打开新添加的源文件,好家伙里面的函数辣么多,但其实我们实现今天的功能,用不到几个,为了节省篇幅,我先把定时器初始化函数先粘贴出来,再解释一下为什么用它,参数为什么要这么设置:
void TIMER_Config(void)
{
timer_oc_parameter_struct timer_ocintpara;
timer_parameter_struct timer_initpara;
rcu_periph_clock_enable(RCU_TIMER2);
timer_deinit(TIMER2);
timer_initpara.prescaler = 99;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = 19999;
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(TIMER2,&timer_initpara);
timer_ocintpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
timer_ocintpara.outputstate = TIMER_CCX_ENABLE;
timer_ocintpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
timer_ocintpara.outputnstate = TIMER_CCXN_DISABLE;
timer_ocintpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW;
timer_ocintpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
timer_channel_output_config(TIMER2,TIMER_CH_0,&timer_ocintpara);
timer_channel_output_mode_config(TIMER2,TIMER_CH_0,TIMER_OC_MODE_PWM0);
timer_channel_output_shadow_config(TIMER2,TIMER_CH_0,TIMER_OC_SHADOW_DISABLE);
timer_primary_output_config(TIMER2, ENABLE);
timer_auto_reload_shadow_enable(TIMER2);
timer_enable(TIMER2);
}
C语言语法性的东西我们不讲,这个初始化函数里面最关键的参数是三个:prescaler(预分频),period(周期值),repetitioncounter(重装载值)。
我们前面讲过了现在的PC6有最大100MHz的频率,那要想输出一个方波该怎么搞,我们先不管占空比,先把频率和周期搞定,我们先做一个周期20ms,频率50Hz的pwm。控制LED其实对频率和周期没什么要求,但考虑到以后可能会写跟电机控制有关的内容,这个部分就不挖坑了(我感觉说了好几句废话,频率跟周期其实是一个东西,互为倒数关系而已)。
预分频的作用是把当前工作频率先做个除法,周期值就是把得到的预分频得到频率再做一个除法(那它为什么要叫周期值呢?)就得到了最终PWM波的频率,所以我们先把预分频设置为100(0~99就是100,从0开始计数),把周期值设置为20000,100MHz/100/20000=50Hz。有小伙伴可能要问了,为啥是100和20000呢?10和200000不行吗?或者1000和2000不行吗?这个一会讲占空比再说。
再使用timer_channel_output_mode_config函数将定时器通道配置为PWM输出模式,这个模式是怎么实现的就不讲了,太底层(其实我也不明白)。
3.按键控制PWM占空比函数
根据昨天的介绍,最外面的一层判断是通过输入状态来判断按键是否被按下,再通过一个100ms的延时函数进行消抖,再通过以下这个函数调节PWM占空比:
/*!
\brief configure TIMER channel output pulse value
\param[in] timer_periph: please refer to the following parameters
\param[in] channel:
only one parameter can be selected which is shown as below:
\arg TIMER_CH_0: TIMER channel0(TIMERx(x=0..4,7..13))
\arg TIMER_CH_1: TIMER channel1(TIMERx(x=0..4,7,8,11))
\arg TIMER_CH_2: TIMER channel2(TIMERx(x=0..4,7))
\arg TIMER_CH_3: TIMER channel3(TIMERx(x=0..4,7))
\param[in] pulse: channel output pulse value,0~65535
\param[out] none
\retval none
*/
void timer_channel_output_pulse_value_config(uint32_t timer_periph, uint16_t channel, uint32_t pulse)
它的关键参数就一个pulse(输出通道脉冲值),占空比也就是这个值与前面周期值之比,在一个周期内,占空比决定高电平所占时间。这里把前面的小坑填上,在官方的注释中,输出通道脉冲值可取值的范围是0~65535,如果我们将周期值设置的远远大于65535,那会导致输出通道脉冲值即使取到最大值,占空比也会很小,也就是占空比的可调范围变小了,达不到0% ~100%,如果周期值取得很小,那占空比可调级数就会变小吗,比如周期值为10,调节PWM占空比就要以10%为基数,所以周期值与输出通道脉冲值最好在一个数量级,且不能太大或太小。
这次我希望达到的功能是每按下一次按键,PWM的占空比就增加20%,达到100%时再按下按键,占空比归0,控制函数如下:
void Key_Control(void)
{
uint16_t i = 0;
while(1)
{
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_0,i);
if(RESET == gpio_input_bit_get(GPIOA, GPIO_PIN_0))
{
delay_1ms(100);
if(RESET == gpio_input_bit_get(GPIOA, GPIO_PIN_0))
{
i += 4000;
if(i > 20000)
{
i = 0;
}
}
}
}
}
4.主函数
咱们的主函数还是简洁明了,几个初始化函数之后,直接调用按键控制函数:
int main(void)
{
systick_config();
GPIO_Config();
TIMER_Config();
Key_Control();
}
由于在按键控制函数中有了while(1),主函数里面连while(1)都省了
五、来看看效果
1.灯光效果
PWM-LED效果
2.波形效果
PWM-LED波形
总结
其实今天有两个点有点混乱,第一个是为什么PWM的占空比会等于输出通道脉冲值与前面周期值之比,我理解的输出通道脉冲值应该是指在同一个周期内输出多少个脉冲,这些脉冲的宽度都是1个预分频后的时钟周期,即1us,那么周期值是指将20000个预分频后的时钟周期放在一个周期里,所以波形上就是20ms,数据上是对的上的;第二个点是外部晶振频率是25MHz,为什么倍频系数为4,在代码里面和数据手册里面没有找到解释这个的地方,但找到了以下这段代码,在官方提供的系统函数中,倍频应该是8倍到200MHz,就很奇怪。
/*!
\brief configure the system clock to 200M by PLL which selects HXTAL(25M) as its clock source
\param[in] none
\param[out] none
\retval none
*/
static void system_clock_200m_25m_hxtal(void)
然后就是要注意配置频率和占空比的方法。(我好像又挖了个坑,重装载值是不是没讲?算了以后再说)