深入理解STM32外设驱动原理

目录

前言

一、处理器控制外设原理

二、如何控制(以GPIO为例)

三、举例——串口(USART1)

完结



前言

        在刚接触接触单片机时,都是不管三七二十一的去调用各种函数去驱动外设(尤其是STM32,其提供了固件库以及HAL库),以此来学习单片机的功能,特别是在第一次时,通过调用GPIO相关函数来点亮LED灯。后来慢慢知道了,是单片机内核(CPU)通过配置各种外设的寄存器来驱动它的。那么,这篇文章就来揭秘这个神秘的过程,到底是如何配置寄存器以达到控制外设的(以STM32为例)。


一、处理器控制外设原理

        CPU本身是不能直接控制硬件的,硬件一般是由其对应的控制器来控制,处理器中将各个硬件控制器的寄存器映射到CPU地址空间中的一段范围,这样CPU就可以通过读写寄存器来间接控制硬件。

地址映射:在一个处理器中,一般会将ROM、RAM、寄存器等存储设备分别映射到寻址空间的不同地址段。地址映射、寻址空间等内容在下面文章中有具体提到:

①51单片机内核及其工作原理-CSDN博客

②STM32内核——Cortex M3-CSDN博客

        简单的说,就是单片机通过地址总线,给出一定的地址,用来分配给各种存储设备,使每个存储设备都有属于自己的地址(只有存储设备有了地址,CPU才能找得到它,并访问它,寄存器也属于是存储设备),这一过程叫地址映射;而这个地址的总量就是寻址空间(由地址总线的宽度决定),它的大小决定了可以连接多少存储设备。比如,STM32地址总线宽度为32位,可以映射2^{23}个地址,即为4G的大小,所以STM32的寻址空间为4G。

        如下图,STM324G的地址空间分配为:

可以看到,单片机上的外设就放在地址空间的0x4000 0000~0x5FFF FFFF中,之后我们通过操作这片空间上各种外设对应的寄存器,便可以实现控制外设的效果。

补充:C语言在ARM中的数据类型有


二、如何控制(以GPIO为例)

        在这里就以STM32的GPIO这个外设为例进行讲解,设定GPIO的端口为B,引脚为5,即PB5。

首先,查询GPIOB的地址。

通过手册可以看到GPIOB的起始地址为0x40010C00,并且在为其分配的地址空间上又连接着GPIO相关的寄存器,如下:

如想控制PB5输出高电平,只需要将GPIOB地址空间里的ODR寄存器的第5位置一便可。算出GPIOB-ODR寄存器的地址,即0x40010C00+0xC=0x4001 0C0C(起始地址加上偏移量)

        知道了寄存器地址了,接下来该如何操作地址呢?我们都知道,0x4001 0C0C只是一个数字,我们需要先将其转变为地址。

( unsigned int *)0x40010C0C;

这样我们便把这个数据强制转化成32位无符号的指针,在C语言中,指针即代表了地址。然后对该指针赋值,使其第五位为1。

*( unsigned int *)0x40010C0C |=1<<5;

在指针前加上*号便可以对指针所指的内容进行操作,即寄存器GPIOB_ODR;然后将其第五位写1,需要注意的是,这里需要先将他们进行按位或操作,否则会改变该寄存器里其他位的值,从而会破坏原来的控制。这样,我们便可以直接操作地址控制PB5引脚输出高电平。如下LED闪烁程序:

为了方便阅读,我们可以将该地址宏定义,如下:

#define ODR   *( unsigned int *)0x40010C0C
ODR |= 1<<5;

但在实际操作中,这样还是不够简便,代码的可移植性很低,并且GPIOB的地址空间中还有其他寄存器。每个端口都有七个寄存器:CRL、CRH、IDR、ODR、BSRR、BRR、LCKR,这些寄存器之间的地址是连续的。那么如何将这些寄存器全部定义在一起呢?

        这时你可能想到了C语言中的数组,因为它的每个单元之间的地址空间就是连续的,但是数组不行,因为数组的每个单元的空间都是相同的,不能改变,而有些外设的各个寄存器之间的空间大小是不一样的。所以这个时候最好的选择就是结构体。结构体相关内容可以查阅下面内容:

C语言——结构体(Struct)详解+运用举例-CSDN博客

结构体内的各个成员之间的空间是连续的,而且可以放不同类型的数据。如下定义:

#define     __IO    volatile  
#define uint32_t    unsigned int

typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;

 上面定义中出现了一个关键字volatile,不清楚的可以查阅C语言——难点关键字(extern、static、struct、enum、union、volatile)-CSDN博客。

通过以上结构体的定义,便把每个端口的各个寄存器全部打包起来,只需访问这个结构体便可以进行端口的控制。但是,还差一步,就是把地址和结构体关联起来:GPIOB的起始地址为0x40010C0C,我们只需要把地址0x40010C0C变成上面结构体类型的地址即可,为了方便阅读,同时将其进行宏定义。如下:

#define GPIOB   ( GPIO_TypeDef *)0x40010C0C

这就是官方库中给出的定义,通过此定义,方便了寄存器的操作。

但在开发中,往往不用寄存器开发,通常用库函数以及HAL库就行,而这些库函数就是通过封装寄存器实现的。如GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal)这个函数:

我们只需要调用函数就行,这样不仅方便阅读,而且不用过多去了解寄存器的地址,减小了开发难度。如下,就是正常操作时的程序:


三、举例——串口(USART1)

  首先,地址映射。

#define PERIPH_BASE         ((uint32_t)0x40000000) //外设基地址    
#define APB2PERIPH_BASE     (PERIPH_BASE + 0x10000)//APB2总线的基地址
#define USART1_BASE         (APB2PERIPH_BASE+0x3800)//串口1的起始地址

然后,定义结构体。

/** 
  * @brief Universal Synchronous Asynchronous Receiver Transmitter
  */
 
typedef struct
{
  __IO uint16_t SR;
  uint16_t  RESERVED0;
  __IO uint16_t DR;
  uint16_t  RESERVED1;
  __IO uint16_t BRR;
  uint16_t  RESERVED2;
  __IO uint16_t CR1;
  uint16_t  RESERVED3;
  __IO uint16_t CR2;
  uint16_t  RESERVED4;
  __IO uint16_t CR3;
  uint16_t  RESERVED5;
  __IO uint16_t GTPR;
  uint16_t  RESERVED6;
} USART_TypeDef;

最后,将地址转换成该结构体指针。

#define USART1               ((USART_TypeDef *)USART1_BASE)//强制转换成串口1结构体类型的地址。

在使用USART1发送一个字节的数据时,可以使用下面的语句:

USART1->DR=mydata;

进一步处理将外设操作封装成函数的形式:

/**
  * @brief  Transmits single data through the USARTx peripheral.
  * @param  USARTx: Select the USART or the UART peripheral. 
  *   This parameter can be one of the following values:
  *   USART1, USART2, USART3, UART4 or UART5.
  * @param  Data: the data to transmit.
  * @retval None
  */
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
{
  /* Check the parameters */
  assert_param(IS_USART_ALL_PERIPH(USARTx));
  assert_param(IS_USART_DATA(Data)); 
    
  /* Transmit Data */
  USARTx->DR = (Data & (uint16_t)0x01FF);
}

        所以,STM32固件库函数就是基于这种思路设计的。应该注意的是,在定义USART_TypeDef结构时,使用了__IO变量类型,该类型实质上volatile的宏定义(该宏定义包含在core_m3.h文件中)。外设访问定义指针时,需要使用volatile关键字。volatile用于防止相关变量被优化。


完结

有误之处望指正

物联沃分享整理
物联沃-IOTWORD物联网 » 深入理解STM32外设驱动原理

发表回复