stm32是如何将配置从库函数调用一步一步到寄存器的?
0. 前言
在平时的学习和工作中,可能很少有人会实际去操作寄存器,但是去了解库函数是如何去操作寄存器是很有必要的。不仅可以加深对stm32的理解还能学习借鉴它库函数的封装架构。
1.综述
stm32是目前市面上最常见的微处理器之一。这得益于它性价比和低门槛。在使用stm32时,往往没去注意它库函数是怎么将底层寄存器操作封装起来的,为什么stm32用库函数开发的驱动具有很好的移植性?这里我们将来谈一谈。
在stm32里面,同类型的外设的寄存器地址映射是规律是相同的。
上图中GPIOx 代表 GPIOA GPIOB等等,从最左边的Offset可以看出,不同端口的GPIO内部寄存器的映射方式是一样的,也就是说在GPIOA中,GPIOA_CRH在0x04这个偏移地址处,那么GPIOB的GPIOB_CRH也在0x04。这就意味着,在写驱动时,我们可以用同一个GPIO驱动去控制GPIOA GPIOB ,只需要把GPIO模块的偏移地址改变就可以了。
我们还要需要了解一下的就是在stm32f10x系列处理器上,GPIOA是挂载在APB2总线上的,因此,访问GPIOA的流程就应该是:先找到APB2的地址,然后在上面找到GPIOA的地址,然后再在GPIOA中找到相应寄存器的地址。知道了这些我们就可以继续说了。
大概是这样一个计算公式:
GPIOA的CRH寄存器地址=APB2地址+GPIOA相对于APB2的偏移地址+CRH相对于GPIOA的偏移地址。
1.1 流程
这里我会以stm32f103c8t6单片机最常见的GPIO控制LED灯为例,由顶向下来展开,也就是从我们调用GPIO_Init
开始,追根溯源,看控制字是怎么一步一步写入到寄存器的。
2.正式开始
2.1 LED库函数初始化
首先,库函数里声明了一个GPIO_InitTypeDef
的结构体,它包含了GPIO配置的成员。
包含了我们要配置哪个引脚,引脚需要多高驱动频率,GPIO的模式是什么。
typedef struct
{
uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */
GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIOSpeed_TypeDef */
GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIOMode_TypeDef */
}GPIO_InitTypeDef;
我们可以看到以上代码的结构体中还包含了一些结构体,其实他们不是结构体,而是一些枚举变量,这样我们在给上面结构体赋值时就可以用预先定义好的那些枚举变量了。
/**
* @brief Output Maximum frequency selection
*/
typedef enum
{
GPIO_Speed_10MHz = 1,
GPIO_Speed_2MHz,
GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;
在下图中我们就用到了GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
来给GPIO_InitStructure.GPIO_Speed
赋值。
然后我们需要使用GPIO来操作LED灯时,就会先定义一个GPIO_InitTypeDef
结构体,然后将对应的结构体成员赋值。最后调用GPIO_Init
来初始化GPIO。
#include "stm32f10x.h" // Device header
#include "LED.h"
void LED_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB, ENABLE);
//PB3 PB4
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//PA15
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
2.2 GPIO_TypeDef
在使用GPIO_Init(GPIOA, &GPIO_InitStructure);
初始化GPIO时, GPIO_InitStructure
是我们自己定义的,那GPIOA是哪里来的呢?
在stm32f10x.h
这个头文件里有这样的一些宏:
这里的GPIOA就是我们在GPIO_Init
时填写的第一个参数。这里宏定义是什么意思呢?
对于GPIOA来说,首先我们将GPIOA_BASE强制转换为一个GPIO_TypeDef *类型的指针,以后我们
访问GPIOA_BASE就可以使用GPIOA啦!
这里的GPIOA_BASE其实就是GPIOA相对于APB2的偏移地址。我们找到GPIOA_BASE的定义:
从这里也验证了我们上面的说法,GPIOA的地址确实是APB2的基地址+GPIOA的偏移地址。
APB2PERIPH_BASE就是APB2相对于外设总线的偏移地址:
PERIPH_BASE就是外设总线基地址了:
好,上三图合起来看也可以算出来,这里简写,
:GPIOA=0x40000000+0x10000+0x0800=0x40010800
恰好是下图从用户手册上查找到的值。
这里的0x0800就是偏移地址。从数据手册查得:
然后我们回到 **访问GPIOA_BASE就可以使用GPIOA啦!**意思就是我们访问GPIOA这个结构体就可以访问到具体的硬件了。现在让我们来看一下这个结构体都包含什么:
/**
* @brief General Purpose I/O
*/
typedef struct
{
__IO uint32_t CRL; //0x00
__IO uint32_t CRH; //0x04
__IO uint32_t IDR; //0x08
__IO uint32_t ODR;//...
__IO uint32_t BSRR;//...
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
上面的代码中我在后面注释了0x00,0x04都是什么意思?没错这些都是对应寄存器相对于GPIOA的偏移地址。
2.3 GPIOA的访问方法
重点: 有木有发现register map 里,寄存器排列是 CHL,CHR,IDR…,而在GPIO_TypeDef结构体里也是CHL,CHR,IDR…。底层没有明确定义各个寄存器的地址,而是通过将寄存器从低地址向高地址排列,定义的类型恰好是uint32_t,所以结构体成员相对于结构体首地址的偏移地址是以4个字节为单位的,恰好单片机也是32位的,每个寄存器是32位的,偏移单位也是4个字节。通过这样子,就将结构体成员和寄存器一一对应上了。而前面讲了,将GPIOA的基地址转换成了GPIO_TypeDef 的指针,所以以后我们通过访问GPIOA指向的结构体成员 就可以准确的访问到对应的寄存器啦 。*
2.4 GPIO_Init函数
通过上面的操作: GPIO_Init(GPIOA, &GPIO_InitStructure); 我们将要在GPIO_Init里,把GPIO_InitStructure结构体里的控制字写入到GPIOA对应的寄存器去了。进入GPIO_Init函数。
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;
uint32_t tmpreg = 0x00, pinmask = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));
assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));
/*---------------------------- GPIO Mode Configuration -----------------------*/
.....................
....................
/*---------------------------- GPIO CRL Configuration ------------------------*/
/* Configure the eight low port pins */
.....................
....................
/*---------------------------- GPIO CRH Configuration ------------------------*/
/* Configure the eight high port pins */
if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
{
tmpreg = GPIOx->CRH;
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
........................
if (currentpin == pos)
{
......................
/* Reset the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
/* Set the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
}
}
GPIOx->CRH = tmpreg;
}
}
限于篇幅,这里省略了很多代码,并且这个函数的实现不是我们讨论的重点,因此省略了大部分。但是我们依然可以看到,传入参数后首先通过断言assert_param
判断参数正确性,方便调试时定位,然后大部分操作都是在 与或非 移位等操作,也就是将GPIO_InitStructure里的控制字分离出来,写到对应的寄存器去。
比如:
GPIOx->CRH = tmpreg;
比如这行代码,就是将分离出来的值写入GPIOx的CRH寄存器。这里的GPIOx也就是GPIO_Init(GPIOA, &GPIO_InitStructure);
传入的GPIOA(结构体指针)。
3.总结
这里是以GPIO的初始化为例,其实其他的外设也大同小异。
stm32大量使用了结构体和枚举来实现标准库,并且将具有相同特点的外设模块分门别类,提高了代码复用,也使编程大大简化。
其实这里面最关键的一点就是:
外设寄存器地址=总线基地址地址+外设偏移地址+寄存器偏移地址。
还请大家不吝指正。编辑不易,给个赞呗!哥们儿!