历史上的今天
返回首页

历史上的今天

今天是:2026年03月17日(星期二)

正在发生

2023年03月17日 | 单片机(MCU)如何才能不死机之对齐访问(Aligned Access)

2023-03-17 来源:zhihu

从一个结构体说起。如下,在 STM32F0 的程序中,我们定义了一个结构体My_Struct ,那么这个结构体占用多少内存呢?

struct Struct_Def {
uint8_t Var_B;
uint16_t Var_W0;
uint16_t Var_W1;
uint32_t Var_DW;
};
struct Struct_Def My_Struct;
int main(void)
{
My_Struct.Var_B = 0x01;
My_Struct.Var_W0 = 0x0203;
My_Struct.Var_W1 = 0x0405;
My_Struct.Var_DW = 0x06070809;
while(1);
}

我们粗略一算,1 + 2 + 2 + 4 = 9 Bytes 。

下载到芯片,观察一下变量,似乎没错。

如果有更进一步的好奇心,我们来到内存中实际看一下,可能会有出乎意料的发现:

编译器在 Var_B 之后插入了一个字节,在 Var_W1 之后插入了两个字节。这个结构体在内存中实际占用了 1 + 1 + 2 + 2 + 2 + 4 = 12 Bytes 。

为什么会这样呢?这是 ARM Cortex M0 体系决定的,它只支持对齐访问 ( Aligned Access )。比如我们访问一个 4 字节 (Double Word) 型的变量时,如果这个变量的起始地址是能被 4 整除的话,我们说这种访问是双字节对齐的。如果访问一个 2 字节 ( Word ) 变量,当起始地址能被 2 整除时是对齐的。访问字节 ( Byte ) 型变量,总是对齐的。

那么如果进行了非对齐访问呢?那就会产生一个严重错误 ( HardFault ) !!!

大家看一下例子中的这一个赋值语句:

My_Struct.Var_DW = 0x06070809;

它是一个 4 字节 ( Double Word ) 型的变量赋值。Var_DW 这个成员,如果按照在结构体中的顺序,应该紧随 Var_W1 之后,分配在 0x20000012,但是这个地址是不能被 4 整除的,所以编译器在填充了 2 个字节 0 之后,把 Var_DW 的起始地址分配在了 0x20000014 。

到这里大家肯定会有一个疑问,这样岂不是很浪费 RAM 吗?RAM 又是相对来说价格比较高的。特别是在结构体比较多的情况下,大量的 RAM 白白浪费了!

还好,在这里我们可以用到伪指令 #pragma pack 了。

如下例所示,#pragma pack(1) 将会使结构体中的变量一个字节紧挨着一个字节在内存中分配,而不再考虑是否对齐的问题。可以看到结构体占用从 0x2000000C 到 0x20000014 的9个字节 RAM空间。

#pragma pack(1)
struct Struct_Def {
uint8_t Var_B;
uint16_t Var_W0;
uint16_t Var_W1;
uint32_t Var_DW;
};

struct Struct_Def My_Struct;
#pragma pack()

那么问题来了,当我们读写地址非对齐的变量时,不就会产生 HardFault 吗?

在这里,编译器采取了曲线救国的方针。大家看下面赋值语句对应的汇编部分就会看到,它用 4 个STRB 指令(单字节操作,无论任何地址都是对齐操作), 代替了 1 个 STR 指令 ( 4 字节操作 )。如此,牺牲了一些效率,但是节省了内存空间。

这种用法节省了 RAM,但是带来了一种比较隐蔽的错误。 尤其是当我们用指针方式访问这些变量时,编译器无法发现错误,而且只有当语句实际执行时才会引起问题。所以在使用指针式要特别注意,指针所指向的地址,是否和指针类型所需要的地址对齐方式吻合。

以上面的 RAM 分配方式为例,非对齐访问时会导致 MCU 进入 HardFault 。

volatile uint32_t Test_Var;

Test_Var = *(uint8_t *)(&My_Struct.Var_B); // 这句是可以正确执行的

Test_Var = *(uint16_t *)(&My_Struct.Var_W0); // 非对齐访问,进入 HardFault

Test_Var = *(uint32_t *)(&My_Struct.Var_DW); // 非对齐访问,进入 HardFault

对于变量的定义,我们还可以用下面的伪指令把变量以 n 字节对齐:

__align(n)


推荐阅读

史海拾趣

问答坊 | AI 解惑

关于verilog的一个问题

请帮我分析个程序吧我用40M晶振,那一个时钟周期就是25nsclk为输入,ledout为输出,想每一秒让ledout的电平变化一次程序编译都通过了,但就是下到实验板上后灯不闪请指教 module delay1s(clk,ledout);output ledout;integer count1,count2;//intege ...…

查看全部问答>

一些变频器说明书

大家共享,希望大家有更多的变频器,PLC,伺服驱动等资料与大家共享 [ 本帖最后由 yurongchun 于 2008-5-27 10:38 编辑 ]…

查看全部问答>

05月11日 CPU部分电路图,待商榷版

昨天花了一天时间画的原理图,这是CPU控制部分,与数据采集板的接口部分还没有打网络标号(包括电平转换的芯片),打算先把这一块板给做出來调软件,本来打算用LPC2103作为从控制器,后来画到后面想想还是换成了MSP430F135,因为LPC2103要用双电源供电,如果 ...…

查看全部问答>

Linux下ARM汇编点滴

第一部分 Linux下ARM汇编语法尽管在Linux下使用C或C++编写程序很方便,但汇编源程序用于系统最基本的初始化,如初始化堆栈指针、设置页表、操作 ARM的协处理器等。初始化完成后就可以跳转到C代码执行。需要注意的是,GNU的汇编器遵循AT&T的汇编语法 ...…

查看全部问答>

ST五张图片展现STM8L的低功耗

太阳能供电 一块土豆就可以供电,相比较某些产品用苹果展示,土豆应该是更省电的吧,呵呵 利用RF无线传输能量 利用手摇电筒制作成发电机供电 一杯热水就能供电?杯子底部的热电偶产生电能,一样可以供MCU使用 背景资料: 意法半 ...…

查看全部问答>

自制简单LED频闪灯

在千篇一律的DIY、MOD场合里,许多玩家费尽心机所改造出来的灯光效果都是静止的,如果在Lanparty上千人一面的改造MOD比比皆是,你的改造又怎么能脱颖而出呢?增加一个动感的超炫灯光效果势在必行!想像一下在姹紫嫣红的炫目光彩照耀下,观众早已疲 ...…

查看全部问答>

动了谁的PPP?

把手机作为modem连接PC,用PC的调制解调器拨号时,究竟modem的PPP协议栈是否工作?谁看过相关的源码能确定这个问题。…

查看全部问答>

在获取滚动条的SB_THUMBPOSITION事件位置值,总是不对

当触发滚动条的SB_THUMBPOSITION事件时,我把滚动条的滑块托到最后,得到值不是SCROLLINFO中的nMax值,为什么啊。…

查看全部问答>

猎头职位:Hardware Design Engineer (欧洲一家做控制系统的公司,地点:上海)

Listed in London Stock Exchange, our client is a global group of five businesses that supply control and automation solutions that deliver efficiency, safety and effectiveness in process automation, controls and transportation e ...…

查看全部问答>