首页 > 编程学习 > C语言对寄存器的封装

C语言对寄存器的封装

发布时间:2022/11/9 13:40:39

C语言对寄存器的封装

文章目录

  • C语言对寄存器的封装
    • 1 封装总线和外设基地址
      • (1)封装基地址为宏
      • (2)将基地址宏强转为指针
    • 2 封装寄存器列表
      • (1)封装寄存器为结构体
      • (2)将寄存器地址强转为结构体指针
      • (3)封装寄存器结构体指针为宏
    • 3 总结

以 GPIO 这个外设为例,讲解 C 语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。

这部分工作都由固件库帮我们完成了,这里我们只是分析了下这个封装的过程。

1 封装总线和外设基地址

(1)封装基地址为宏

在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名。

/*总线和外设基址宏定义*/
/* 外设基地址 */
#define PERIPH_BASE ((unsigned int)0x40000000)

/* 总线基地址 */
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000)

/* GPIO 外设基地址 */
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)

/* 寄存器基地址,以 GPIOB 为例 */
#define GPIOB_CRL (GPIOB_BASE + 0x00)
#define GPIOB_CRH (GPIOB_BASE + 0x04)
#define GPIOB_IDR (GPIOB_BASE + 0x08)
#define GPIOB_ODR (GPIOB_BASE + 0x0C)
#define GPIOB_BSRR (GPIOB_BASE + 0x10)
#define GPIOB_BRR (GPIOB_BASE + 0x14)
#define GPIOB_LCKR (GPIOB_BASE + 0x18)

首先定义了“片上外设”基地址 PERIPH_BASE,接着在 PERIPH_BASE 上加入 各个总线的地址偏移,得到 APB1APB2 总线的地址 APB1PERIPH_BASEAPB2PERIPH_BASE, 在其之上加入外设地址的偏移,得到 GPIOA-G 的外设地址,最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。

(2)将基地址宏强转为指针

一旦有了具体地址,就可以用指针读写:

/*使用指针控制 BSRR 寄存器*/
/* 控制 GPIOB 引脚 0 输出低电平 (BSRR 寄存器的 BR0 置 1) */
*(unsigned int*)GPIOB_BSRR = (0x01 << (16 + 0));
/* 控制 GPIOB 引脚 0 输出高电平 (BSRR 寄存器的 BS0 置 1) */
*(unsigned int*)GPIOB_BSRR = 0x01 << 0;
unsigned int temp;
/* 读取 GPIOB 端口所有引脚的电平 (读 IDR 寄存器) */
temp = *(unsigned int*)GPIOB_IDR;

该代码使用 (unsigned int *)GPIOB_BSRR 宏的数值强制转换成了地址,然后再用“*”号做取指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取 STM32 外设的状态。

2 封装寄存器列表

(1)封装寄存器为结构体

用上面的方法去定义地址,还是稍显繁琐,例如 GPIOA-GPIOE 都各有一组功能相同的寄存器, 如 GPIOA_ODR/GPIOB_ODR/GPIOC_ODR 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入 C 语言中的结构体语法对寄存器进行封装。

/*使用结构体对 GPIO 寄存器组的封装*/
typedef unsigned int uint32_t;       /* 无符号 32 位变量 */
typedef unsigned short int uint16_t; /* 无符号 16 位变量 */

/* GPIO 寄存器列表 */
typedef struct {
    uint32_t CRL;  /*GPIO 端口配置低寄存器 地址偏移: 0x00 */
    uint32_t CRH;  /*GPIO 端口配置高寄存器 地址偏移: 0x04 */
    uint32_t IDR;  /*GPIO 数据输入寄存器 地址偏移: 0x08 */
    uint32_t ODR;  /*GPIO 数据输出寄存器 地址偏移: 0x0C */
    uint32_t BSRR; /*GPIO 位设置/清除寄存器 地址偏移: 0x10 */
    uint32_t BRR;  /*GPIO 端口位清除寄存器 地址偏移: 0x14 */
    uint16_t LCKR; /*GPIO 端口配置锁定寄存器 地址偏移: 0x18 */
} GPIO_TypeDef;

这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 7 个成员变量, 变量名正好对应寄存器的名字。

结构体成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。

C 语言的语法规定,结构体内变量的存储空间是连续的,其中 32 位的变量占用 4 个字节,16 位的变量占用 2 个字节,具体见下图 GPIO_TypeDef 结构体成员的地址偏移 。

在这里插入图片描述

假如这个结构体的首地址为 0x4001 0C00(这也是第 一个成员变量 CRL 的地址),那么结构体中第二个成员变量 CRH 的地址即为 0x4001 0C00 +0x04 ,加上的这个 0x04,正是代表 CRL 所占用的 4 个字节地址的偏移量,其他成员以此类推。

这样的地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来。

(2)将寄存器地址强转为结构体指针

然后就能以结构体的形式访问寄存器,如下:

/*通过结构体指针访问寄存器*/
GPIO_TypeDef* GPIOx;  //定义一个 GPIO_TypeDef 型结构体指针 GPIOx
GPIOx = GPIOB_BASE;   //把指针地址设置为宏 GPIOB_BASE 地址
GPIOx->IDR = 0xFFFF;  //访问IDR端口
GPIOx->ODR = 0xFFFF;  //访问ODR端口

uint32_t temp;
temp = GPIOx->IDR;  //读取 GPIOB_IDR 寄存器的值到变量 temp 中

这段代码先用 GPIO_TypeDef 类型定义一个结构体指针 GPIOx,并让指针指向地址 GPIOB_BASE(0x4001 0C00),使用地址确定下来,然后根据 C 语言访问结构体的语法,用 GPIOx->ODRGPIOx->IDR 等方式读写寄存器。

(3)封装寄存器结构体指针为宏

最后,我们更进一步,直接使用宏来定义好 GPIO_TypeDef 类型的指针,而且指针指向各个 GPIO 端口的首地址,使用时我们直接用该宏访问寄存器即可:

/*定义好 GPIO 端口首地址址针*/
/* 使用 GPIO_TypeDef 把地址强制转换成指针 */
#define GPIOA ((GPIO_TypeDef*)GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef*)GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef*)GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef*)GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef*)GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef*)GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef*)GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef*)GPIOH_BASE)

/* 使用定义好的宏直接访问 */
/* 访问 GPIOB 端口的寄存器 */
GPIOB->BSRR = 0xFFFF;  //通过指针访问并修改 GPIOB_BSRR 寄存器
GPIOB->CRL = 0xFFFF;   //修改 GPIOB_CRL 寄存器
GPIOB->ODR = 0xFFFF;   //修改 GPIOB_ODR 寄存器

uint32_t temp;
temp = GPIOB->IDR;  //读取 GPIOB_IDR 寄存器的值到变量 temp 中

/* 访问 GPIOA 端口的寄存器 */
GPIOA->BSRR = 0xFFFF;
GPIOA->CRL = 0xFFFF;
GPIOA->ODR = 0xFFFF;

uint32_t temp;
temp = GPIOA->IDR;  //读取 GPIOA_IDR 寄存器的值到变量 temp 中

3 总结

在这里插入图片描述

定义好外设寄存器结构体,实现完外设存储器映射后,我们再把外设的基址宏强制类型转换成相应的外设寄存器结构体指针,然后再把该指针声明成外设名,这样一来,外设名就跟外设的地址对应起来了,而且该外设名还是一个该外设类型的寄存器结构体指针,通过该指针可以直接操作该外设的全部寄存器。

Copyright © 2010-2022 dgrt.cn 版权所有 |关于我们| 联系方式