笔者的第一篇博客,由于知识水平有限,如有错误,欢迎指正
前言
今天笔者学习完B站野火科技的STM32教程中的系统滴答定时器(SysTick)部分,自己着手写代码时却出现了一些问题。
明明代码,逻辑写得都是一样的,为什么LED就是不闪烁?后来仔细对照了自己的代码和野火科技的例程,才发现我的变量类型少了__IO(volatile)限定符。因为自己写代码时很少关注限定符,所以这才注意到限定符在单片机编程中的重要性。
volatile限定符
在STM32的HAL库中,给出了以下宏定义:
#define __IO volatile /*!< Defines 'read / write' permissions */
其实从中从注释中就可以看出volatile的作用。为了讲解的更清楚,笔者引用权威书籍《C Primer Plus》中关于volatile的说明:
volatile 限定符告知计算机,代理(而不是变量所在的程序)可以改变
该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中
共享数据。例如,一个地址上可能储存着当前的时钟时间,无论程序做什
么,地址上的值都随时间的变化而改变。或者一个地址用于接受另一台计算
机传入的信息。现在,如果声明中没有volatile关键字,编译器会假定变量的值在使用
过程中不变,然后再尝试优化代码。——《C Primer Plus》
简单来说,volatile就是避免了编译器对代码的一种优化方式。假设有以下代码:
XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;
编译器会预先判断到XBYTE[2]的最终值为0x58,那么就会优化代码,只执行最后一条赋值语句,而一旦加上了volatile限定符,那么编译器就会执行每一条赋值语句。
为什么必须要使用volatile?
以野火STM32教程的编程思路为例。我们在主程序中传递需要中断次数,然后在中断初始化函数中传递重装载值,在定时器运转的过程中,每隔(1/频率)秒的时间,重装载值向下递减一次。因此,重装载值/频率*中断次数=延迟的时间。在这个过程中,中断服务函数调用中断次数递减的函数,这就类似于《C Primer Plus》中提到的“代理”。如果没有volatile限定符,那么代理就没法对延迟的最小时间单位的次数进行修改,主进程就会卡在while死循环中,导致LED灯并没有向预期的那样闪烁。
/*在stm32f7xx_it.c中*/
void SysTick_Handler(void)//中断服务函数,每次重装载值为0时触发中断,调用次数-1的函数
{
TimingDelay_Decrement();
}
/*在用户自定义的bsp_SysTick.c中*/
void Delay_us(__IO u32 nTime)//主函数调用的延时函数,传递中断次数
{
TimingDelay = nTime;
while (TimingDelay != 0);
}
void SysTick_Init(void)
{
if (HAL_SYSTICK_Config(SystemCoreClock / 100000)) {//设置重装载值,延时最小单位时间为10us(10^-5s)
/* Capture error */
while (1);
}
}
void TimingDelay_Decrement(void)//每调用一次该函数,中断次数-1
{
if (TimingDelay != 0x00) {
TimingDelay--;
}
}
/*在main.c中*/
int main(void)
{
/* 系统时钟初始化成 72MHz */
SystemClock_Config();
/* LED 端口初始化 */
LED_GPIO_Config();
/* 配置 SysTick 为 10us 中断一次, 时间到后触发定时中断,进入 stm32f7xx_it.c
文件的 SysTick_Handler()句柄函数处理,通过中断次数计时*/
SysTick_Init();
while (1) {
LED_RED;
Delay_us(100000); // 100000 * 10us = 1s
LED_GREEN;
Delay_us(100000); // 100000 * 10us = 1s
LED_BLUE;
Delay_us(100000); // 100000 * 10us = 1s
}
}
拓展
打开一个Keil5工程,搜索__IO,可以发现许多变量都使用__IO的限定符,这也进一步证明了__IO是不可以任意省略的
结语
从这个例子中,我们就能看到限定符对底层的编程时有一定影响的。仔细阅读HAL库中的限定符,而不是随意的删减,或许能有很大的收获。