STM32 KEIL C99编程(一)使用指针修改const变量的探索

本文详细解析了const变量在STM32中的行为,展示了如何理解const和volatile的区别,特别关注了const全局变量的读写权限、结构体成员的__I修饰以及静态变量的访问控制。实验表明__I并未阻止Flash中的常量被意外修改,实际权限管理需依赖get/set接口。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、关于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变量被修改。

### LVGL框架移植到STM32硬件平台(无需触摸屏) #### 1. 下载并准备LVGL库 从官方GitHub仓库下载指定版本的LVGL源码[^2]。假设使用的是v7.1.0版本,则需要关注`src/`目录下的核心文件以及配置头文件`lv_conf_template.h`。 将`lv_conf_template.h`复制到项目路径下,并重命名为`lv_conf.h`以便编译器识别。此文件包含了所有关于LVGL的功能开关和参数设置选项,后续可以根据需求修改其中的内容。 #### 2. 修改示例端口代码 进入`lvgl/examples/porting`文件夹,在该位置存在多个`.h`文件用于演示不同环境下的移植方法[^1]。按照说明文档指示,把这些文件顶部原有的条件编译指令由`#if 0`更改为`#if 1`以启用相应部分的实现逻辑。 #### 3. 设置C标准至C99及以上 确保使用的IDE或者工具链支持现代C语言特性。对于大多数基于Keil MDK或者其他主流开发套件而言,默认可能并未开启较新的语法解析模式。因此需手动调整项目的构建属性使其兼容C99规范以上的要求^。 #### 4. 屏幕初始化与刷新函数定义 由于目标系统仅涉及显示而不含触控交互组件,所以重点在于提供给定分辨率下的图形缓冲区管理机制及其实际渲染操作接口: ```c // 声明全局变量保存当前帧缓存指针 static lv_color_t *disp_buf; void my_disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p){ uint16_t x; uint16_t y; for(y = area->y1; y <= area->y2; y++) { for(x = area->x1; x <= area->x2; x++) { /* 将颜色写入物理显示屏 */ LCD_SetPixelColor(x,y,*color_p); color_p++; } } /* Inform the graphics library that flushing is complete */ lv_disp_flush_ready(drv); } ``` 上述片段展示了如何自定义个简单的逐像素绘制过程作为底层驱动层的部分^。当然具体细节取决于所选用的具体型号LCD控制器芯片规格书描述的方法论执行步骤。 #### 5. 构建显示器驱动结构体实例化对象 继续完善之前提到过的回调处理程序之后,还需创建相应的设备句柄并通过注册关联起来完成整个体系搭建工作流程如下所示: ```c /* Allocate memory for a single buffer (assuming monochrome display)*/ uint16_t buf_size = LV_HOR_RES_MAX*LV_VER_RES_MAX; disp_buf = malloc(buf_size*sizeof(lv_color_t)); /* Initialize and register the driver with the library*/ static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); /* Basic initialization*/ disp_drv.flush_cb = my_disp_flush; /* Set your custom flush function here */ disp_drv.buffer = disp_buf; lv_disp_drv_register(&disp_drv); /* Finally add this new instance into system context */ ``` 通过这样的方式就可以成功加载起基础运行时所需的必要组成部分了^。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值