HAL库+CubeMX 点灯(延时函数、定时器、PWM)
硬件:RoBoMaster C板(STM32F407IG)
1、HAL_Delay 闪烁 LED
1.1、内容
1)硬件原理图上的上拉电阻与下拉电阻
2)cubeMX中配置GPIO基本操作
1.2、程序学习
1.2.1、LED原理图
1.2.2、cubeMX中配置GPIO基本操作
- 通过上面的原理图可以看出三个LED灯的引脚为PH10,PH11,PH12
- 在cubeMX中配置GPIO为输出模式,在cubeMX找到对应引脚,配置成GPIO_Output模式。
- 在cubeMX中修改对应引脚的名字。
1)在左侧找到System core->GPIO;
2)找到对应的GPIO,例如PH12;
3)在下方的配置单中user label填写命名,填好后会在芯片缩略图中更新。 - 生成代码
点击GENERATE CODE按键。
1.2.3、函数讲解
1)HAL_GPIO_WritePin函数
HAL库中提供一个操作GPIO电平的函数:HAL_GPIO_WritePin函数
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
2)滴答计时器介绍以及HAL_Init初始化
滴答定时器也称为SysTick,是stm32内置的倒计时定时器,每当计数到0时,触发一次SysTick中断,并重载寄存器值。滴答计时器的初始化在HAL_Init函数中完成,配置成1ms的中断。HAL_Init的内容如下,通过注释了解到:在HAL_Init中,实现了SysTick以及底层硬件的初始化。
HAL_StatusTypeDef HAL_Init(void) {
/* Configure Flash prefetch, Instruction cache, Data cache */
#if (INSTRUCTION_CACHE_ENABLE != 0U)
__HAL_FLASH_INSTRUCTION_CACHE_ENABLE();
#endif /* INSTRUCTION_CACHE_ENABLE */
#if (DATA_CACHE_ENABLE != 0U)
__HAL_FLASH_DATA_CACHE_ENABLE();
#endif /* DATA_CACHE_ENABLE */
#if (PREFETCH_ENABLE != 0U)
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
#endif /* PREFETCH_ENABLE */
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) *
/ HAL_InitTick(TICK_INT_PRIORITY); /* Init the low level hardware */
HAL_MspInit();
/* Return function status */
return HAL_OK;
}
在HAL_Init的实现中,关注滴答定时器初始化函数。
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
HAL_InitTick函数将设定SysTick的定时周期为1ms,即频率为1000Hz,并使SysTick开始工作。
每当滴答计时器递减到0时,会触发中断,使程序进入SysTick中断处理函数
void SysTick_Handler(void) {
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
在其中会调用HAL_IncTick函数,该函数实现如下
__weak void HAL_IncTick(void) {
uwTick += uwTickFreq;
}
在uwTick变量中存储的是从stm32的Systick初始化以来所经过的时间(ms),uwTick的存在相当于给整个程序提供了一个绝对的时间基准,而HAL_Delay函数延时功能便是通过uwTick的值完成的。
获取当前的uwTick值可以使用HAL库提供的函数HAL_GetTick
uint32_t HAL_GetTick(void)
3)HAL_Delay 毫秒级延迟的函数
HAL库提供了(声明于stm32f4xx_hal.h)用于毫秒级延迟的函数(如果需要时间更短的延时函数则必须使用用户编写的延时函数),HAL_Delay函数(使用_weak修饰符说明该函数是可以用户重定义的)。
在HAL_Delay的实现中,调用HAL_GetTick函数获取基准时间uwTick,说明了HAL_Delay函数的实现是基于滴答计时器(Systick)。
__weak void HAL_Delay(uint32_t Delay) {
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay;
/* Add a freq to guarantee minimum wait */
if (wait < HAL_MAX_DELAY) {
wait += (uint32_t)(uwTickFreq);
}
while((HAL_GetTick() - tickstart) < wait) { }
}
4) HAL_GPIO_TogglePin 电平翻转函数
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
1.3 、程序流程图
2、定时器 闪烁 LED
2.1、内容
1)定时器的配置
2)定时器中断以及中断优先级讲解
3)cubeMX中的中断管理
2.2、程序学习
2.2.1、定时器在cubeMX中配置
- 在左侧的标签页中选择Timer,点击标签页下的TIM1。
- 在弹出的TIM1 Mode and Configuration中,在ClockSouce的右侧下拉菜单中选中Internal Clock。
- 此时TIM1得到使能,接下来需要配置TIM1的运转周期。需要打开Clock Configuration标签页。
下图为STM32的时钟树结构,通过配置这个结构中各处的分频/倍频比,可以控制最后输出到各个外设的时钟。
如果HSE处为灰色,则先去Pinout&Configuration标签页下确定是否使能外部晶振。
通过查阅数据手册资料,可以知道TIM1的时钟源来自APB2总线,此外也可以在程序中的_HAL_RCC_TIM1_CLK_ENABLE定义里面找到,可以看到代码中的APB2,说明TIM1挂载在APB2上。
#define __HAL_RCC_TIM1_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_TIM1EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_TIM1EN);\
UNUSED(tmpreg); \
} while(0U)
注意到在时钟树配置页面下的APB2 Timer clocks(MHz)为168MHz,这意味着提供给TIM1预分频寄存器的频率就是168MHz。
下面通过设置分频比和重载值来控制定时器的周期,详细的计算步骤可以查看进阶学习的部分。
如果想要得到周期为500毫秒的定时器,则可以按照进阶学习介绍的公式来对分频值和重载值进行设定。回到Pinout&Configuration标签页下,对应TIMx_PSC寄存器的Prescaler项和对应TIMx_ARR寄存器的Counter Period项。500ms对应的频率为2Hz,为了得到2Hz的频率,可以将分频值设为16799,重载值设为4999,则可以计算出定时器触发频率为
2.2.2、中断优先级讲解
回顾之前所说的中断概念,在STM32专门用于处理中断的控制器叫做NVIC,即嵌套向量中断控制器 (Nested Vectored Interrupt Controller)。
NVIC的功能非常强大,支持中断优先级和中断嵌套的功能,中断优先级即给不同的中断划分不同的响应等级,如果多个中断同时产生,则STM32优先处理高优先级的中断。
中断嵌套即允许在处理中断时,如果有更高优先级的中断产生,则挂起当前中断,先去处理产生的高优先级中断,处理完后再恢复到原来的中断继续处理。
这个过程理解起来就像是在上文的情境中,主人听到水烧开了,正打算去厨房时突然听到门口响起了急促的敲门声,那么主人就会先去执行开门的操作,然后再去厨房处理开水。
为了在有限的寄存器位数中实现更加丰富的中断优先级,NVIC使用了中断分组机制。STM32将先将中断进行分组,然后又将优先级划分为抢占优先级 (Prem priority) 和响应优先级 (Subpriority),抢占优先级和响应优先级的数量均可以通过NVIC中AIRCR寄存器的PRIGROUP[8:10]位进行配置,从而规定了两种优先级对NVIC_IPRx[7:4]的划分,根据划分决定两种优先级的数量。总共可以分成下表中的5种情况
拥有相同抢占优先级的中断处于同一个中断分组下。
当多个中断发生时,先根据抢占优先级判断哪个中断分组能够优先响应,再到这个中断分组中根据各个中断的响应优先级判断哪个中断优先响应。
2.2.3、cubeMX中的中断配置以及中断函数管理
在cubeMX的NVIC标签页下可以看到当前系统中的中断配置
列表中显示了当前系统中所有中断的使能情况与优先级设置。要使能中断则在Enable一栏打勾,这里选中TIM1 update interrupt,打勾,开启该中断。此外还可以在该页面下进行抢占优先级和响应优先级的分配和中断的两种优先级的配置。这里为定时器1的中断保持默认的0,0优先级。
点击Generate code,生成代码。
下面来看一下HAL库是如何对中断进行处理的。
在stm32f4xx_it.c中,找到cubeMX自动生成的中断处理函数
void TIM1_UP_TIM10_IRQHandler(void) {
/* USER CODE BEGIN TIM1_UP_TIM10_IRQn 0 */
/* USER CODE END TIM1_UP_TIM10_IRQn 0 */
HAL_TIM_IRQHandler(&htim1);
/* USER CODE BEGIN TIM1_UP_TIM10_IRQn 1 */
/* USER CODE END TIM1_UP_TIM10_IRQn 1 */
}
该函数调用了HAL库提供的HAL_TIM_IRQHandler这一函数
void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim)
在HAL_TIM_IRQHandler对各个涉及中断的寄存器进行了处理之后,会自动调用中断回调函数HAL_TIM_PeriodElapsedCallback,该函数使用__weak修饰符修饰,即用户可以在别处重新声明该函数,调用时将优先进入用户声明的函数。
__weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
一般我们需要在中断回调函数中判断中断来源并执行相应的用户操作。
2.2.4、HAL_TIM_PeriodElapsedCallback 定时器回调函数介绍
如前文所介绍的,HAL库在完成定时器的中断服务函数后会自动调用定时器回调函数。
通过配置TIM1的分频值和重载值,使得TIM1的中断以500ms的周期被触发。因此中断回调函数也是以500ms为周期被调用。在main.c中重新声明定时器回调函数,并编写内容如下:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim1)
{ //500ms trigger
bsp_led_toggle();
}
}
可以看到首先在回调函数中进行了中断来源的判断,判断其来源是否是定时器1。如果有其他的定时器产生中断,同样会调用该定时器回调函数,因此需要进行来源的判断。
在确认了中断源为定时器1后,调用bsp_led_toggle函数,翻转RGB三色LED引脚的输出电平。
2.2.5、HAL_TIM_Base_Start函数
如果不开启中断,仅让定时器以定时功能工作,为了使定时器开始工作,需要调用HAL库提供的函数。
HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim)
如果需要使用定时中断,则需要调用函数
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim)
以上两个函数如果要使用则都需要在主循环while(1)之前调用。
2.3、 程序流程
3、PWM 实现呼吸灯
3.1、内容
1)aRGB三原色合成灯效讲解
2)cubeMX中配置PWM定时器配置
3.2、aRGB三原色
aRGB为一种色彩模式,aRGB分别代表了alpha(透明度)Red(红色)Green(绿色)和Blue(蓝色)四个要素,一般我们给每个要素设置十进制下0-255的取值范围,通过16进制表示就是0x00-0xFF,因此一个aRGB值可以通过八位十六进制数来描述,从前到后每两位依次对应a,R,G,B。
在aRGB中,alpha值越大色彩越不透明,RGB中哪个值越大,对应的色彩就越强。比如纯红色可以用8位16进制表示为0xFFFF0000,纯绿色可以表示为0xFF00FF00,纯蓝色可以表示为0xFF0000FF,黄色由蓝色和绿色合成,所以可以表示为0xFF00FFFF。
3.3、程序学习
3.3.1、PWM在cubeMX中配置
在cubeMX中设置定时器5的通道1,2,3为PWM输出。可以注意到三个通道对应的引脚正是之前的实验中使用的LED引脚。
定时器5如下配置,设置重载值为65535。
点击Generate Code,生成工程代码。
3.3.2、 PWM配置介绍
在上一节课中介绍了STM32的定时器,并提到PWM输出是STM32的定时器的功能之一,为了实现PWM功能,需要使用定时器中的比较寄存器(TIMx_CCRx)。
当定时器以PWM模式工作时,会自动将TIMx_CCRx的值与TIMx_CNT(计数寄存器)中的值做比较,当TIMx_CNT中的值小于TIMx_CCRx的值时,PWM输出引脚输出高电平,大于时则输出低电平。因此知道了PWM信号的周期和占空比可以通过设置比较寄存器TIMx_CCRx和定时器重载寄存器TIMx_ARR来控制。PWM的占空比可以通过下图公式计算:
以下图为例,该定时器的重载值为8,比较寄存器值为4,输出信号为OCXREF,则其占空比为44.4%。
一个定时器工作在PWM输出模式下时,有4个通道可以进行PWM信号的输出,每一个定时器都有对应标号的比较寄存器,比如5号定时器的1号通道对应的比较寄存器为TIM5_CCR1。
修改比较寄存器TIMx_CCRx的值来控制PWM输出的占空比。在函数aRGB_led_show中,首先通过与运算与移位运算提取出对应的alpha,R,G和B通道值,然后用透明度alpha与R,G,B三者依次相乘,最后将其赋值通过__HAL_TIM_SetCompare函数给对应的比较寄存器TIM5->CCRx。通过控制不同的PWM占空比,控制某个颜色的LED的亮度,以这样的方式就可以通过设置aRGB的值来控制最后输出的LED灯效。
3.3.3、 函数介绍
3.3.3.1、设置PWM比较值函数
#define __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \
(((__CHANNEL__) == TIM_CHANNEL_1) ? ((__HANDLE__)->Instance->CCR1 = (__COMPARE__)) :\
((__CHANNEL__) == TIM_CHANNEL_2) ? ((__HANDLE__)->Instance->CCR2 = (__COMPARE__)) :\
((__CHANNEL__) == TIM_CHANNEL_3) ? ((__HANDLE__)->Instance->CCR3 = (__COMPARE__)) :\
((__HANDLE__)->Instance->CCR4 = (__COMPARE__)))
3.3.3.2 、HAL_TIM_PWM_Start 函数
为了使定时器开始PWM输出,除了要通过HAL_TIM_Base_Start使定时器开始工作,还需要在初始化时调用HAL库提供的PWM初始化函数HAL_TIM_PWM_Start。
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel)
3.4、流程图
3.5、三色呼吸灯主要代码
void aRGB_Led_Breath(void)
{
uint16_t red = 0, green = 0, blue = 0, i;
for(; green < 0xFFFF; green++){
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_1, blue);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_2, green);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_3, red);
for(i=0; i<1000; i++){
}
}
for(; green > 0; green--){
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_1, blue);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_2, green);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_3, red);
for(i=0; i<1000; i++){
}
}
for(; blue < 0xFFFF; blue++){
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_1, blue);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_2, green);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_3, red);
for(i=0; i<1000; i++){
}
}
for(; blue > 0; blue--){
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_1, blue);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_2, green);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_3, red);
for(i=0; i<1000; i++){
}
}
for(; red < 0xFFFF; red++){
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_1, blue);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_2, green);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_3, red);
for(i=0; i<1000; i++){
}
}
for(; red > 0; red--){
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_1, blue);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_2, green);
__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_3, red);
for(i=0; i<1000; i++){
}
}
}