目录
前言
我们以前一直使用寄存器编程,通过参观官方手册进行寄存器的配置,这些是最底层的东西。但是我们不能一直用寄存器编程,在基本学会寄存器编程后,我们应该接着学习固件库的编程。
自己写库——构建库函数雏形(第一节)
基本宏定义(GPIOB)
备份上一节的代码,打开stm32f10x.h文件,输入以下代码,这些代码通过宏定义#define定义了GPIOB端口的寄存器,通过编写地址再将地址赋给设定好的寄存器名称,即可以从该名称读取地址以及调用寄存器。
//用来存放STM32寄存器映射的代码
//外设 perirhral
#define PERIPH_BASE ((unsigned int)0x40000000)
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0c00)
#define RCC_APB2ENR *(unsigned int*)(RCC_BASE + 0x18)
#define GPIOB_CRL *(unsigned int*)(GPIOB BASE+0x00)
#define GPIOB_CRH *(unsigned int*)(GPIOB BASE+0x04)
#define GPIOB_IDR *(unsigned int*)(GPIOB BASE+0x08)
#define GPIOB_ODR *(unsigned int*)(GPIOB BASE+0x0c)
#define GPIOB_BSRR *(unsigned int*)(GPIOB BASE+0x10)
#define GPIOB_BRR *(unsigned int*)(GPIOB BASE+0x14)
#define GPIOB_LCKR *(unsigned int*)(GPIOB BASE+0x18)
定义结构体(GPIOx)
通过学习官方手册,我们可以发现,在GPIO寄存器地址映像中,GPIOx所含的7个寄存器都是32位,占用了四个字节,而且逐级递增。那么,学习过C语言的同学就能发现,这和结构体类型非常相似,那么我们是否可以尝试定义一个结构体,把我们所需要的GPIOx里的7个寄存器包含进来呢?接下来我们一起来尝试一下。
至于为什么定义结构体,接下来写代码的时候就明白了。接下来我们定义一个类型为GPIO_TypeDef的结构体,把7个寄存器的名称用定义形式通过结构体进行定义。这样我们就定义了一个结构体。
typedef struct
{
uint32_t CRL;
uint32_t CRH;
uint32_t IDR;
uint32_t ODR;
uint32_t BSRR;
uint32_t BRR;
uint32_t LCKR;
}GPIO_TypeDef;
当然,在此之前,我们要用typedef关键字重新定义关于int和short的名称:
typedef unsigned int uint32_t;
typedef unsigned short uint16_t;
那么为什么呢?
GPIO_BASE是32位的,通过指针只能访问4字节的内存,而如果我们想要通过外设的基地址来访问外设所有的寄存器的话,那么我们如果使用结构体类型的指针,那么我们就可以访问一大块空间。那么就比直接访问每个外设的每个寄存器的地址要方便很多。
如何转换基地址为GPIO_TypeDef的指针?
在C语言中,有一种转换叫强制类型转换,如下代码:
#define GPIOB ((GPIO_TypeDef*)GPIOB_BASE)
我们将基地址强制转换成GPIO_TypeDef结构体的指针之后,不妨宏定义该指针为GPIOB,而GPIOB就指向了GPIOB寄存器的内存,在这个内存里有着GPIOB端口的所有寄存器。
接下来我们就可以通过GPIOB调用GPIO_TypeDef这个结构体中的寄存器地址,代码如图所示:
#include "stm32f10x.h"
int main(void)
{
#if 0
//打开 GPIOB 端口的时钟
*(unsigned int*)0x40021018 |= (1<<3);
//配置IO口为输出
*(unsigned int*)0x40010c00 |= ((1)<<(4*5));//*0,*1,*5
//控制 ODR 寄存器
*(unsigned int*)0x40010c0c &= ~(1<<0);
#elif 0
//打开 GPIOB 端口的时钟
RCC_APB2ENR |= (1<<3);
//配置IO口为输出
GPIOB_CRL |= ((1)<<(4*5));//*0,*1,*5
//控制 ODR 寄存器
GPIOB_ODR &= ~(1<<0);
#elif 1
//打开 GPIOB 端口的时钟
RCC_APB2ENR |= (1<<3);
//配置IO口为输出
GPIOB->CRL |= ((1)<<(4*5));//*0,*1,*5
//控制 ODR 寄存器
GPIOB->ODR &= ~(1<<0);
#endif
}
void SystemInit(void)
{
//函数体为空,目的是为了骗过编译器不报错
}
我们可以看到虽然在代码层面没有什么较大的变化,但是从方法论来说已经进步不少了。在创建结构体时,结构体有很多成员,而类型及成员分布的顺序都和寄存器排列的顺序一样。我们找到这个外设的基地址,以GPIOB端口为例,我们只要找到GPIOB_BASE这个基地址,然后把这个地址强制类型转换成结构体类型的指针,就可以操作所有外设的寄存器了。
关于为什么使用int
使用int类型的定义,占四个字节,而正好GPIOx每个寄存器占用四个字节的地址,所以使用int类型。
小结
本节课讲解了自己写库——构建库函数雏形,包括基本的宏定义(直接操作寄存器)以及使用结构体(库函数)。下节课我们讲解如何实现 RCC 这个外设的寄存器结构体声明,把时钟相关的代码改成寄存器结构体操作的方式,以及第二节内容。
点个关注不迷路,三连支持一下吧!