
前面经常出现这个关键字volatile。最早写空循环延时时它常常长这样volatileuint32_ti;for(i0;i72000U;i){}看起来只是给i多加了一个单词。这个单词是有实际约束的它在告诉编译器这个变量的每一次读写都别省必须真的做出来。如果没有 volatile空循环还会剩下什么先把volatile去掉uint32_ti;for(i0;i72000U;i){}这个循环没有点亮 LED没有改变一个之后会被使用的变量也没有调用函数。它做完以后程序的可见结果和不做这段循环一模一样。编译器在打开优化后会想既然结果没有变化这 72000 次加一又有什么必要它可以把整个循环删掉。原本想等一会儿实际可能一瞬间就过去了。加上volatile后volatileuint32_ti;for(i0;i72000U;i){}i不再只是编译器眼里无关紧要的计算。每一轮都要对i做真实的读取和写入循环不能被直接当成废话扔掉。以前的空循环延时依然不精确主频和优化等级都会影响它但至少“循环可能被优化没了”这一层问题被挡住了。有些值会在代码看不见的地方变化再看一个变量staticuint32_ttick_ms0U;假设定时器每过 1ms 进入一次中断中断函数里把它加一voidTIM1_UP_IRQHandler(void){if(TIM_GetITStatus(TIM1,TIM_IT_Update)!RESET){tick_ms;TIM_ClearITPendingBit(TIM1,TIM_IT_Update);}}主循环另一边等待它变到 1000while(tick_ms1000U){}只看主循环tick_ms在while里面没有被改过。可它确实会改变因为中断函数会在另一条执行路径上给它加一。要把这个事实告诉编译器staticvolatileuint32_ttick_ms0U;这样主循环每次判断tick_ms 1000U时都会重新取一次它当前的值。没有volatile时编译器可能只取到第一次的 0再也不看新的值中断早已把tick_ms加到 1000主循环却还在原地等那个旧的 0。所以volatile常用在两类地方情况为什么要写volatile空循环的计数变量这几次读写本身就是循环存在的意义不能被删掉中断和主循环共同访问的变量值可能在当前代码看不见的地方被中断改变-O0 让问题安静不等于问题不存在写到这里我想说一下我自己的不好的习惯我平时经常随手就把优化等级设成-O0让编译器不要动我的代码少写volatile的代码也能正常运行。但是当同一份代码交给队友、换了优化等级或者将来为了缩小程序体积打开优化后编译器的处理方式可能改变。该标记为volatile的变量还是应该明确标出来这样工程设置变了代码表达的意思不会跟着变。想让两步操作变成不可分割的一步、让多个地方同时修改同一个变量时自动排好顺序、或者解决按键抖动这些都不是volatile能提供的。SysTick-VAL 为什么会自己变化volatile说清楚以后再回来看延时函数里的这一行current_valSysTick-VAL;current_val是普通变量右边的SysTick-VAL却不是。VAL是 SysTick 的当前计数寄存器。SysTick 启动后内核时钟每走一拍硬件就把VAL减一减到 0 后再按重装值重新开始。没有任何一行 C 代码给VAL减一它仍然会不断变化因为改它的是 SysTick 这块硬件。芯片的头文件把 SysTick 的几个寄存器排成了一个结构体typedefstruct{__IOuint32_tCTRL;__IOuint32_tLOAD;__IOuint32_tVAL;__Iuint32_tCALIB;}SysTick_Type;随后又把SysTick定义成指向这组寄存器的指针。因此SysTick-VAL就是“找到 SysTick 这组寄存器再读其中的VAL”。-仍然是 C 语言里的结构体指针成员访问不同的是这一格不在普通内存里而在芯片规定好的硬件地址上。标准外设库的core_cm3.h中__IO被定义成volatile。也就是说头文件已经替VAL标记好了每次读取SysTick-VAL都必须真的去读取硬件当前的计数值。所以延时函数里这两行的含义并不相同start_valSysTick-VAL;current_valSysTick-VAL;右边两次都是去问硬件“你现在是多少”左边的start_val、current_val则是普通变量用来保存两次答案。假如第一次读到 30000过了一会儿读到 28000变化的是 SysTick 寄存器程序再用两个保存下来的值做差便能算出经过了多少计数。不只有 SysTick 才有寄存器寄存器是软件观察和控制硬件的窗口不是 SysTick 独有的东西。之前已经碰到过的定时器和 GPIO也各自有一组寄存器写法谁在改变它读到的是什么SysTick-VALSysTick 计数器当前剩余计数TIM2-CNTTIM2 定时器当前计数值GPIOB-IDR引脚上的实际电平PB 端口各引脚的输入状态按键按下时PB11 的电平从高变低不是程序给GPIOB-IDR赋了值而是按键把引脚接到了 GND。定时器计数时TIM2-CNT一直加一或减一也不是main在一遍遍写它。标准库函数只是把这些寄存器包成了更好记的函数。例如GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)最终读的就是GPIOB-IDR中 PB11 对应的那一位。函数名帮助我们少记一些细节硬件寄存器仍然在原地工作。也有寄存器主要用来写配置。SysTick-LOAD告诉 SysTick 每轮从多少开始倒数定时器的配置寄存器决定它按什么方式计数GPIO 的配置寄存器决定引脚是输入还是输出。读状态、写配置方向不同都是在和同一块硬件打交道。最后橘猫说volatile先解决的是“编译器会不会把必要读写省掉”寄存器再告诉我们“为什么有些值不需要 C 代码赋值也会改变”。SysTick-VAL会倒数TIM2-CNT会计数GPIOB-IDR会跟着按键电平改变。以后看到一个寄存器名先问一句“是谁在改变它”比急着记住每一位的名字更重要。最后蜘蛛说初见端倪……