一、关于const变量
const变量常被理解为常变量,一般不可被修改。在CMSIS中,一般常用__I表示:
#define __I volatile const /*!< Defines 'read only' permissions */
实际上在keil默认的C99编译器中const修饰的变量仍然是变量,只是不允许出现在左值中。它作为一个变量,不能用来定义数组的长度,同样,在很多情况下依然是可以被修改的。
在GCC或者VC下,const变量的特点可以参考这一篇文章,个人觉得写的比较全面。
你真的理解了const和volatile关键字么?(我看不一定)
二、使用const修饰的全局变量
代码如下(示例):
const int testfault = 1;
int main(void)
{
int *p = (int*)&testfault;
*p=10;
}
在VC中编译通过,但是运行报错!
那么在KEIL中呢?
首先我们看看map文件,找一下testfault所在的位置
可以看到该变量位于STM32的内部Flash中紧挨着最后一段Thumb Code。那么是不是意味着这个变量没法通过指针修改值,甚至运行时出现异常呢?我们一起来看指令
C代码中52行这一句话对应MOVS和STR这两行指令,截图中当MOVS指令执行完毕以后,R1寄存器被填入了立即数10,我们再下一步执行STR指令。
执行完了,神马也没有发生!我们来检查一下memory里面的值
不仅testfault的值没有变,连*p值也没有变!值没变,程序没报错,指令有在执行说明也没有被编译器优化。
结论:使用const修饰的全局变量位于内部flash/ROM中紧挨着代码段,值不会被修改。和VC不同的是,STM32程序不会报错。
三、用结构体组织的全局变量中的成员使用const修饰
代码如下(示例):
/* pwm_globalvariable.c */
typedef struct
{
__I uint16_t PWMValue;
uint16_t AC_Freq16;
float AC_Freq;
}PWM_VARI;
__IO PWM_VARI pwm_vari;
结构体中PWMValue成员被__IO修饰,即volatile const,我们还是先看看map文件
可以看到这个结构体变量的地址位置和其他的全局变量没什么不同,都是从0x20000010开始的(2000000~20000009存的是RCC的内容)。
这个成员的地址也没有什么特殊的,就是结构体的首地址。我们来试一下用指针修改它的值。
不出所料,成功修改!
四、在三的基础上,再加上static修饰
STM32的中断服务函数只能是 void function(void),所以全局变量往往是不可避免的。为了防止全局变量满天飞,也为了避免修改时数组越界,或者修改一半时触发中断等等导致全局变量被意外“污损”,这里对全局变量进行static修饰,防止其被其他c文件的函数直接修改,并设计get/set接口供其他C文件的函数访问。
pwm_globalvariable.h
/* pwm_globalvariable.h
定义结构体类型*/
typedef struct
{
__I uint16_t PWMValue;
uint16_t AC_Freq16;
float AC_Freq;
}PWM_VARI;
typedef struct
{
uint16_t PWMValue;
uint16_t AC_Freq16;
float AC_Freq;
}USER_PWM_VARI;
/* get/set接口 */
void set_pwm_vari(USER_PWM_VARI *p);
void set_pwm_member_u16( USER_PWM_VARI *base, uint16_t *member );
void set_pwm_member_f( USER_PWM_VARI *base, float *member );
void get_pwm_vari(USER_PWM_VARI *p);
void get_pwm_member_u16( USER_PWM_VARI *base, uint16_t *member );
void get_pwm_member_f( USER_PWM_VARI *base, float *member );
pwm_globalvariable.c
/* pwm_globalvariable.c
定义全局变量,带有static
跨文件访问只能使用接口*/
#include "./vari/PWM_GlobalVariable.h"
__IO static PWM_VARI pwm_vari;
void set_pwm_vari(USER_PWM_VARI *p)
{
pwm_vari.AC_Freq = p->AC_Freq;
pwm_vari.AC_Freq16 = p->AC_Freq16;
}
void set_pwm_member_f( USER_PWM_VARI *base, float *member )
{
float *cl;
cl = (float *) ((uint32_t)&pwm_vari + ((uint32_t)member - (uint32_t)base));
*cl = *member;
}
void set_pwm_member_u16( USER_PWM_VARI *base, uint16_t *member )
{
uint16_t *cl;
cl = (uint16_t *) ((uint32_t)&pwm_vari + ((uint32_t)member - (uint32_t)base));
*cl = *member;
}
void get_pwm_vari(USER_PWM_VARI *p)
{
p->PWMValue = pwm_vari.PWMValue;
p->AC_Freq = pwm_vari.AC_Freq;
p->AC_Freq16 = pwm_vari.AC_Freq16;
}
void get_pwm_member_f( USER_PWM_VARI *base, float *member )
{
float *cl;
cl = (float *) ((uint32_t)&pwm_vari + ((uint32_t)member - (uint32_t)base));
*member = *cl;
}
void get_pwm_member_u16( USER_PWM_VARI *base, uint16_t *member )
{
uint16_t *cl;
cl = (uint16_t *) ((uint32_t)&pwm_vari + ((uint32_t)member - (uint32_t)base));
*member = *cl;
}
h文件中设计了两个成员组织完全一样的结构体,唯一的区别就是用于保存全局变量的PWM_VARI中的第一个成员使用了__I修饰,而用于副本的USER_PWM_VARI则没有。采用__I修饰的成员一般都使用了DMA,因此设计时希望它禁止被软件修改故加上__I。那么,加上__I能够达到目的吗?我们尝试使用set_pwm_member_u16函数来修改pwm_vari.PWMValue(试验时禁用了DMA)。
其实和第三节的情况没什么变化,结构体依然被分配到了相同的地址。不同点在于使用了接口函数,而接口函数内部也是在使用指针操作。__I并没有起到防止变量被修改的效果,真正想要实现全局变量的IO权限,只能自己动手把get/set方法实现好。
五、改进的set方法
这里只是根据本文中用于DMA传输的变量全部布置在结构体头部的特点,编写了宏函数
#define CHECK_POINT_THEN_ASSIGN(checkP, mem),其中&(pwm_vari.AC_Freq16)为第一个非__I变量的地址用于判断修改位置,并不具备一般参考性,未来仍需改进。
#define CHECK_POINT_THEN_ASSIGN(checkP, mem) \
if((uint32_t)(checkP) >= (uint32_t)&(pwm_vari.AC_Freq16)) \
*(checkP) = *(mem); \
else \
return;
void set_pwm_vari(USER_PWM_VARI *p)
{
pwm_vari.AC_Freq = p->AC_Freq;
pwm_vari.AC_Freq16 = p->AC_Freq16;
}
void set_pwm_member_f( USER_PWM_VARI *base, float *member )
{
float *cl;
cl = (float *) ((uint32_t)&pwm_vari + ((uint32_t)member - (uint32_t)base));
CHECK_POINT_THEN_ASSIGN(cl, member)
}
void set_pwm_member_u16( USER_PWM_VARI *base, uint16_t *member )
{
uint16_t *cl;
cl = (uint16_t *) ((uint32_t)&pwm_vari + ((uint32_t)member - (uint32_t)base));
CHECK_POINT_THEN_ASSIGN(cl, member)
}
指令执行到BCC以后跳转,不执行LDRH和STRH。防止了__I变量被修改。