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用于防止相关变量被优化。


完结

有误之处望指正

  • 16
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
根据引用\[1\]中的信息,ADS1118模块的驱动是通过代码实现的SPI时序驱动,而不是使用STM32单片机内部集成的SPI外设驱动。因此,使用STM32CubeMX来驱动ADS1118模块时,需要将驱动源码添加到HAL库或标准库工程中,然后可以像使用库函数中的函数一样使用这个源码中的函数。 根据引用\[2\]中的信息,ADS1118模块是一款基于德州仪器(Texas Instruments)ADS1118芯片的高精度、超小型、低功耗模拟/数字转换器模块。该模块具有四个单端或两个差分输入通道,可以通过SPI接口进行配置和控制。 综上所述,使用STM32CubeMX来驱动ADS1118模块时,需要将驱动源码添加到HAL库或标准库工程中,并通过SPI接口进行配置和控制。 #### 引用[.reference_title] - *1* *2* [STM32CubeMX驱动ADS1118模块](https://blog.csdn.net/qq_52158753/article/details/130177131)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [STM32CubeMX学习笔记(48)——USB接口使用(MSC基于外部Flash模拟U盘)](https://blog.csdn.net/qq_36347513/article/details/128001270)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

画凉ZL

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

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

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

打赏作者

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

抵扣说明:

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

余额充值