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

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

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

<think>好的,用户问的是如何在Keil5中优化STM32的代码。我需要先回忆Keil MDK-ARM的优化选项和相关知识。首先,优化通常分为几个方面:编译器优化设置、代码结构优化、内存管理、使用硬件特性等。 用户可能是个嵌入式开发者,刚接触STM32或者Keil,想提高代码效率,减少资源占用。他们可能遇到了程序运行慢或者Flash/RAM不足的问题。深层需求可能是想让代码更高效,或者项目接近资源极限,需要优化。 首先,编译器优化选项是关键。Keil中的-O0到-O3,还有-Os(优化大小)。需要解释每个选项的作用,比如-O3是最高速度优化,但可能增加代码体积。-Os则是平衡速度和大小。但要注意,高优化级别可能影响调试,因为变量可能被优化掉。 然后是代码结构优化,比如避免使用浮点运算,用整数代替;减少全局变量使用局部变量;用查表代替复杂计算。这些方法能减少CPU负担和内存使用。 内联函数和汇编,这部分可能高级用户会用,内联减少函数调用开销,关键部分用汇编优化。但需要谨慎,因为可能降低可读性。 内存优化,比如使用const将数据放到Flash,节省RAM;合理使用内存池,避免碎片。DMA传输减轻CPU负担,提高效率,特别是大量数据传输时。 启动文件配置,比如关闭未使用的外设时钟,设置正确时钟源和分频,确保系统时钟最优。还有链接脚本调整,确保内存分配合理。 工具使用方面,Keil的map和htm文件分析内存和代码大小,找出占用大的函数。Profiler工具定位性能瓶颈。 需要提醒用户优化可能带来的问题,比如高优化级别导致意外行为,调试困难。建议逐步优化,测试每步的效果和稳定性。 可能用户没提到的点:使用编译器特定指令或属性,比如__attribute__((section("某个段"))),或者#pragma优化特定函数。还有C99特性比如restrict指针帮助编译器优化。 另外,C库的选择,比如使用MicroLib减小代码体积,但可能缺少某些功能。在Target选项里可以选择。 总结回答结构:先讲编译器设置,再代码层面的优化,内存管理,硬件特性利用,工具分析,最后注意事项。确保步骤清晰,用户能逐步实施。</think>在Keil MDK-ARM(Keil5)中优化STM32代码时,可从编译器设置、代码结构和硬件特性三个层面入手,以下是分步优化指南: --- ### 、编译器优化设置 1. **优化等级选择** - 在`Options for Target` → `C/C++`选项卡设置: - `-O0`:关闭优化(调试阶段推荐) - `-O1`:基础优化(平衡代码大小与速度) - `-O2`:中等优化(侧重速度) - `-O3`:激进优化(可能增加代码体积) - `-Os`:优化代码体积(适合Flash空间紧张时) 2. **特定优化选项** - 勾选`One ELF Section per Function`:消除未调用函数 - 启用`Link-Time Optimization`:跨文件优化(需勾选`--lto`) --- ### 二、代码结构优化 1. **减少计算复杂度** - 用位操作替代乘除(如`x << 3`代替`x * 8`) - 查表法替代实时计算(如三角函数) - 避免浮点运算,改用定点数(STM32F1等无硬件FPU时) 2. **函数与变量优化** - 高频调用的小函数声明为`static inline` - 全局变量添加`const`修饰符(存储到Flash) - 使用`register`关键字声明频繁访问的变量 3. **循环与条件优化** - 循环展开(手动或通过`#pragma unroll`) - 减少循环内条件判断 ```c // 优化前 for(int i=0; i<100; i++){ if(flag) sum += array[i]; } // 优化后 if(flag){ for(int i=0; i<100; i++) sum += array[i]; } ``` --- ### 三、内存与硬件优化 1. **内存分配策略** - 使用`__attribute__((section(".ccmram")))`将关键数据放入CCM RAM(高速访问) - 通过分散加载文件(Scatter File)自定义内存布局 2. **硬件加速** - 启用DMA传输(替代CPU搬运数据) - 使用硬件CRC、FPU(若有)等协处理器 3. **低功耗优化** - 空闲时调用`__WFI()`进入休眠模式 - 关闭未使用的外设时钟(`__HAL_RCC_GPIOA_CLK_DISABLE()`) --- ### 四、调试与分析工具 1. **Map文件分析** 查看生成的`.map`文件,定位占用Flash/RAM较大的模块。 2. **反汇编验证** 在Debug模式下查看反汇编窗口,验证关键代码是否被正确优化。 3. **性能剖析** 使用Keil的`Event Recorder`或硬件定时器测量函数执行时间。 --- ### 五、注意事项 1. **调试与优化的平衡** - 高优化等级(如-O3)可能导致变量被优化,调试时建议使用-O0或-O1。 2. **验证稳定性** - 优化后需全面测试功能,激进优化可能引发未定义行为。 3. **利用CMSIS库** - 使用ST官方优化的CMSIS-DSP库进行数学运算。 --- 通过以上方法,可显著提升STM32代码的执行效率和资源利用率。建议优化后对比编译输出信息(如Program Size字段)验证效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值