结构体
结构体内存对齐问题
先看一个结构体:
typedef struct {
char a1;
int a2;
char a3;
short a4;
} test_t;
在32位编译系统下这一个结构体的字节数是多少呢?是1+4+1+2=8字节吗?不是的,实际结果为12字节。为什么呢?因为编译器会对不足4字节的变量空间自动补齐为4个字节(这就是内存对齐),以提高CPU的寻址效率(32位CPU以4个字节步长寻址的)。
内存对齐是编译器的“管辖范围”。编译器为程序中的每个”数据单元“安排在适当的位置上,以便于能快速的找到每个“数据单元”。对于32bit的CPU,其寻址的步长为4个字节(即unsigned int 字节长度),这就是常说的“4字节对齐”。同理,对于64bit的CPU,就有“8字节对齐”。
下面代码为4字节对齐:
#include <stdio.h>
typedef struct {
char a1;
int a2;
char a3;
short a4;
} test_t;
int main(void) {
test_t test;
printf("\nsizeof(T) = %d\n", sizeof(test));
printf("a1地址:%d\n", (unsigned int)&test.a1);
printf("a2地址:%d\n", (unsigned int)&test.a2);
printf("a3地址:%d\n", (unsigned int)&test.a3);
printf("a4地址:%d\n", (unsigned int)&test.a4);
return 0;
}
运行结果为:
可见,正好印证了上述的说法,补齐之后结构体成员a1、a2、a3的地址之间正好相差4个字节,a3与a4之间相差两个字节也是因为在其中多留出了1个空白字节。该程序的运行结果可形象地描述为下图:
a1只占用一个字节,为了内存对齐保留了三个空白字节
a3和a4加起来共3字节,为了内存对齐保留了1个空白字节。这就是编译器存储变量时做的见不得人的”手脚“,以方便其雇主——CPU能更快地找到这些变量。
字节对齐
上面讲了字节对齐是因为:
对内存操作时按整字存取才能达到最高效率,相当于是以空间换取时间
但是这样虽然效率上提高了,但是也会带来一些麻烦,我们在处理一些特定数据的时候,如果不进行1字节对齐的话可能会出现意想不到的结果,那么我们怎么才能使结构体1字节对齐呢?
可以使用:
#pragma pack(1) //开始1字节对齐
typedef struct {
char a1;
int a2;
char a3;
short a4;
} test_t;
#pragma pack() //恢复默认的字节对齐
#pragma pack(1)
:使结构体按照1字节对齐#pragma pack()
:取消指定对齐,恢复缺省对齐
现在我们再来看看上面的例子:
现在结构体所占用的字节数就是8字节了。
另外:
- 在网上经常能看到说:用
typedef __packed struct
定义结构体也可以使其一字节对齐,但我尝试了一下没有效果,也不太清楚具体用法,就没写上去了。
位段
有时候我们会看到如下代码:
struct data {
uint8_t a : 2;
uint8_t b : 6;
uint8_t c : 4;
uint8_t d : 4;
uint32_t i;
};
其中冒号表示啥意思?
C语言中,这叫 “位段”,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。
利用位段能够用较少的位数存储数据, :
后面的数字用来限定成员变量占用的位数。
位域通过一个结构声明来建立:该结构声明为每个字段提供标签,并确定该字段的宽度。例如,下面的声明建立了个4个1位的字段:
struct abcd {
unsigned int a : 1; //用来限定成员变量占用的位数
unsigned int b : 1;
unsigned int c : 1;
unsigned int d : 1;
};
根据该声明, abcd
包含 4 个 1位 的字段。现在,可以通过普通的结构成员运算符 "."
单独给这些字段赋值:
abcd.a = 0:
abcd.b = 1;
由于每个字段恰好为 1位 ,所以只能为其赋值 1 或 0 。
位段经常与上面提到的 字节对齐 和 下面将要提到的 共用体 一起使用。
另外
位段也可以直接用于 占位 ,在对寄存器操作的时候特别好用,比如有些寄存器是 保留位 ,这时候就可以直接定义为 uint8_t : 2;
这样就可以:
/* following defines should be used for structure members */
#define __IM volatile const /*! Defines 'read only' structure member permissions */
#define __OM volatile /*! Defines 'write only' structure member permissions */
#define __IOM volatile /*! Defines 'read / write' structure member permissions */
union {
__IOM uint8_t SNFR; /*!< (@ 0x00000008) Noise Filter Setting Register */
struct {
__IOM uint8_t NFCS : 3; /*!< [2..0] Noise Filter Clock Select */
uint8_t : 5;
} SNFR_b;
};
union {
__IOM uint8_t SIMR1; /*!< (@ 0x00000009) I2C Mode Register 1 */
struct {
__IOM uint8_t IICM : 1; /*!< [0..0] Simple I2C Mode Select */
uint8_t : 2;
__IOM uint8_t IICDL : 5; /*!< [7..3] SDA Delay Output SelectCycles below are of the clock
signal from the on-chip baud rate generator. */
} SIMR1_b;
};
union
的作用下面会介绍
下面是对应的寄存器:
我们来看第一个寄存器 SNFR
, NFCS
占 [2:0] 大小为3bit,其余都是保留位占5bit:
__IOM uint8_t NFCS : 3; /*!< [2..0] Noise Filter Clock Select */
uint8_t : 5;
第二个也是同理。
参考文章:
共用体
结构体和共用体在使用上基本一样都是用运算符 "."
去给成员赋值。
结构体和共用体的区别在于:
-
结构体的各个成员会占用不同的内存,互相之间没有影响;
-
而共用体的所有成员 占用同一段内存 ,修改一个成员会影响其余所有成员。
大小端
用于表示数据在存储器中的存放顺序:
-
所谓的大端模式,是指数据的低位保存在内存的 高地址 中,而数据的高位,保存在内存的 低地址 中
-
所谓的小端模式,是指数据的低位保存在内存的 低地址 中,而数据的高位,保存在内存的 高地址 中
验证大小端
#include <stdio.h>
typedef unsigned int uint32_t;
typedef unsigned char uint8_t;
union bit32_data {
uint32_t data;
struct {
uint8_t byte0;
uint8_t byte1;
uint8_t byte2;
uint8_t byte3;
} byte;
};
int main(void) {
union bit32_data num;
num.data = 0x12345678;
if (0x78 == num.byte.byte0) {
printf("Little endian\r\n");
} else if (0x78 == num.byte.byte3) {
printf("Big endian\r\n");
} else {
/* 无效 */
}
return 0;
}
例子
1、操作寄存器
最经典的例子,肯定就是在操作寄存器上面了。
我们看一看TI的寄存器封装是怎么做的:
所有的寄存器被封装成联合体类型的,联合体里边的成员是一个32bit
的整数及一个结构体,该结构体以位域的形式体现。这样就可以达到直接操控寄存器的某些位了。比如,我们要设置PA0
引脚的GPAQSEL1
寄存器的[1:0]
两位都为1,则我们只操控两个bit
就可以很方便的这么设置:
GpioCtrlRegs.GPAQSEL1.bit.GPIO0 = 3
或者直接操控整个寄存器:
GpioCtrlRegs.GPAQSEL1.all |=0x03
如果不是工作于芯片原厂,寄存器的封装应该离我们很远。但我们可以学习使用这种方法,然后用于我们的实际应用开发中。
下面就看一种实际应用:管理一些状态变量
。
示例代码:
union sys_status {
uint32_t all_status;
struct {
bool status1 : 1; // FALSE / TRUE
bool status2 : 1; //
bool status3 : 1; //
bool status4 : 1; //
bool status5 : 1; //
bool status6 : 1; //
bool status7 : 1; //
bool status8 : 1; //
bool status9 : 1; //
bool status10 : 1; //
//...
} bit;
};
2、矩阵
比如说想写一个3 * 3的矩阵,可以这样写:
struct matrix {
union {
float f[3][3];
struct {
float _f11, _f12, _f13, _f21, _f22, _f23, _f31, _f32, _f33;
};
};
};
struct matrix m;
f[3][3]
和 _f11, _f12, _f13, _f21, _f22, _f23, _f31, _f32, _f33
这一串浮点型变量共同使用相同的空间,所以没有空间浪费,在需要整体用矩阵的时候可以用m.f
(比如说传参,或者是整体赋值等);
需要用其中的几个元素的时候可以用m._f11
那样可以避免用m.f[0][0]
(这样不大直观,而且容易出错)。
要记得验证 数据在存储器中的存放顺序 ,也就是验证大小端,上面有讲到。
3、数据传输
传输浮点数据
union f_data {
float f;
struct {
uint8_t byte[4];
};
};
这样在进行数据传输的时候会方便很多,比如串口传输只需要把这个数组 byte[4]
进行传输就可以了。
参考文章:
小结
上面讲了 结构体、共用体、位段 等一些概念,下面来看看一个比较综合的例子。
下面是 瑞萨 的固件库里面,对串口寄存器的部分代码,下面以 SCR 寄存器举例子。
(下面代码来自RA6M5官方固件库,.\ra\fsp\src\bsp\cmsis\Device\RENESAS\Include\R7FA6M5BH.h
)
/* following defines should be used for structure members */
#define __IM volatile const /*! Defines 'read only' structure member permissions */
#define __OM volatile /*! Defines 'write only' structure member permissions */
#define __IOM volatile /*! Defines 'read / write' structure member permissions */
/*!< (@ 0x40118000) R_SCI0 Structure */
typedef struct {
/* 第一个共用体 */
union {
union {
__IOM uint8_t SMR; /*!< (@ 0x00000000) Serial Mode Register (SCMR.SMIF = 0) */
struct {
__IOM uint8_t CKS : 2; /*!< [1..0] Clock Select */
__IOM uint8_t MP : 1; /*!< [2..2] Multi-Processor Mode(Valid only in asynchronous mode) */
__IOM uint8_t STOP : 1; /*!< [3..3] Stop Bit Length(Valid only in asynchronous mode) */
__IOM uint8_t PM : 1; /*!< [4..4] Parity Mode (Valid only when the PE bit is 1) */
__IOM uint8_t PE : 1; /*!< [5..5] Parity Enable(Valid only in asynchronous mode) */
__IOM uint8_t CHR : 1; /*!< [6..6] Character Length(Valid only in asynchronous mode) */
__IOM uint8_t CM : 1; /*!< [7..7] Communication Mode */
} SMR_b;
};
union {
__IOM uint8_t SMR_SMCI; /*!< (@ 0x00000000) Serial mode register (SCMR.SMIF = 1) */
struct {
__IOM uint8_t CKS : 2; /*!< [1..0] Clock Select */
__IOM uint8_t BCP : 2; /*!< [3..2] Base Clock Pulse(Valid only in asynchronous mode) */
__IOM uint8_t PM : 1; /*!< [4..4] Parity Mode (Valid only when the PE bit is 1) */
__IOM uint8_t PE : 1; /*!< [5..5] Parity Enable(Valid only in asynchronous mode) */
__IOM uint8_t BLK : 1; /*!< [6..6] Block Transfer Mode */
__IOM uint8_t GM : 1; /*!< [7..7] GSM Mode */
} SMR_SMCI_b;
};
};
/* 第二个共用体 */
union {
__IOM uint8_t BRR; /*!< (@ 0x00000001) Bit Rate Register */
struct {
__IOM uint8_t BRR : 8; /*!< [7..0] BRR is an 8-bit register that adjusts the bit rate. */
} BRR_b;
};
/* 第三个共用体 */
union {
union {
__IOM uint8_t SCR; /*!< (@ 0x00000002) Serial Control Register (SCMR.SMIF = 0) */
struct {
__IOM uint8_t CKE : 2; /*!< [1..0] Clock Enable */
__IOM uint8_t TEIE : 1; /*!< [2..2] Transmit End Interrupt Enable */
__IOM uint8_t MPIE : 1; /*!< [3..3] Multi-Processor Interrupt Enable(Valid in asynchronous
mode when SMR.MP = 1) */
__IOM uint8_t RE : 1; /*!< [4..4] Receive Enable */
__IOM uint8_t TE : 1; /*!< [5..5] Transmit Enable */
__IOM uint8_t RIE : 1; /*!< [6..6] Receive Interrupt Enable */
__IOM uint8_t TIE : 1; /*!< [7..7] Transmit Interrupt Enable */
} SCR_b;
};
union {
__IOM uint8_t SCR_SMCI; /*!< (@ 0x00000002) Serial Control Register (SCMR.SMIF =1) */
struct {
__IOM uint8_t CKE : 2; /*!< [1..0] Clock Enable */
__IOM uint8_t TEIE : 1; /*!< [2..2] Transmit End Interrupt Enable */
__IOM uint8_t MPIE : 1; /*!< [3..3] Multi-Processor Interrupt Enable */
__IOM uint8_t RE : 1; /*!< [4..4] Receive Enable */
__IOM uint8_t TE : 1; /*!< [5..5] Transmit Enable */
__IOM uint8_t RIE : 1; /*!< [6..6] Receive Interrupt Enable */
__IOM uint8_t TIE : 1; /*!< [7..7] Transmit Interrupt Enable */
} SCR_SMCI_b;
};
};
/* 省略了很多其他部分 */
} R_SCI0_Type; /*!< Size = 52 (0x34) */
/* 宏 */
#define R_SCI0_BASE 0x40118000UL
#define R_SCI0 ((R_SCI0_Type*)R_SCI0_BASE)
这里主要来看 第三个共用体: SCR
、 SCR_SMCI
、SCR_b
和 SCR_SMCI_b
,这两个一个字节大小的数据 和 这两个结构体 都是共用一个内存的。
注意:
- 上面的代码,虽然定义了
SCR_b
和SCR_SMCI_b
,这两个共用体又是在同一个大的共用体里面的,所以SCR_b
和SCR_SMCI_b
这两个共用体也是共用一段内存的,具体这是因为不同情况下,需要操作的寄存器不一样,所以瑞萨官方做了区分;- 在结构体里面定义 共用体 和 结构体 ,可以不用给其命名
union { /* code */ };
,访问的时候也可以直接访问到内部成员,比如R_SCI0->SCR
。
我们通过查看瑞萨官方的手册可以发现:
这里我们看串口0,也就是SCI0的基地址,通过上面给的公式
SCIn = 0x4011_8000 + 0x0100 × n (n = 0, 3 to 9)
SCIm = 0x4011_8000 + 0x0100 × m (m = 1, 2)
算出 SCI0 = 0x4011_8000 + 0x0100 × 0 = 0x40118000
,算出基地址,通过宏 #define R_SCI0_BASE 0x40118000UL
来确定这个结构体所在的位置地址。
那么我们要操作这个寄存器,我们除了要知道基地址,我们还要知道偏移地址,这里偏移地址是 0x02
,这里我们回到上面的代码,我们可以看到,这个串口的总结构体 R_SCI0_Type
里面是有3个大的共用体的(只放出来上面的3个出来分析),第一个共用体数据地址偏移为 0x00000000
因为在结构体中,数据的不共用内存的,存放空间大小为一个字节;由于第一个共用体占用大小为一个字节,所以第二个共用体数据地址偏移为 0x00000001
,并且同样的,存放空间大小为一个字节;第三个,也就是我们要看的 SCR 寄存器的结构体,刚好就是偏移了 0x00000002
,也和手册上面相对应。
上面我们也讲到了 位段 的概念,和 大小端 的概念,所以我们只需对这个结构体里面的成员进行操作就,就相当于对对应的寄存器的位进行操作了:
R_SCI0->SCR_b.TE = 0;
R_SCI0->SCR_b.RE = 0;
便可以对 SCR 寄存器的对应的 TE、RE
位清零了。
平时我们去操作寄存器的时候,我们其实只要找到了 R_SCI4
这个外设的总的结构体,查看结构体定义,看注释和一些命名方式就可以知道,我们想要操作的寄存器,所对应的结构体、共用体是哪个了,比如:
__IOM uint8_t SCR; /*!< (@ 0x00000002) Serial Control Register (SCMR.SMIF = 0) */
注释中也写得很清楚了, (@ 0x00000002)
就是表明偏移地址是 0x02
而且这个结构体的命名也是和寄存器的命名一样的。