嵌入式学习:共用体和结构体

结构体

结构体内存对齐问题

先看一个结构体:

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 包含 41位 的字段。现在,可以通过普通的结构成员运算符 "." 单独给这些字段赋值:

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 的作用下面会介绍

下面是对应的寄存器:

在这里插入图片描述

在这里插入图片描述

我们来看第一个寄存器 SNFRNFCS 占 [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)

这里主要来看 第三个共用体: SCRSCR_SMCISCR_bSCR_SMCI_b ,这两个一个字节大小的数据 和 这两个结构体 都是共用一个内存的。

注意:

  • 上面的代码,虽然定义了 SCR_bSCR_SMCI_b ,这两个共用体又是在同一个大的共用体里面的,所以 SCR_bSCR_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 而且这个结构体的命名也是和寄存器的命名一样的。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nepqiu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值