STM32标准库编程与51单片机直接写寄存器的区别和联系

简介:
        在学完51单片机之后,我们去学习32的时候,会发现编程的方法有很大的区别,让人非常的不适应,但是通过不断的调用相应外设的库函数之后,你也可以去编程STM32,来实现功能,但是你真的了解标准库吗?不少人只会局限在调用标准库,不论你是学完标准库还是初学标准库,都很有必要了解以下标准库的原理。

 寄存器:
我们首先要明确我们编程到底在编些什么?

单片机寄存器在单片机编程中起着至关重要的作用,它们是与单片机硬件紧密关联的特殊存储单元,用于控制各种硬件功能和状态。单片机寄存器与编程之间的联系主要体现在以下几个方面:

1. 硬件控制:单片机寄存器直接控制着单片机的各种硬件功能,如输入/输出端口、定时器、串口通信等。通过编程操作这些寄存器,可以实现对硬件的控制和配置。

2. 状态监测:单片机寄存器中存储着各种硬件的状态信息,如中断标志、定时器计数值、输入端口状态等。通过编程读取这些寄存器的值,可以实时监测硬件的状态,并根据需要进行相应的处理。

3. 中断处理:单片机中断是一种重要的事件处理机制,通过编程配置中断寄存器,可以实现对各种中断事件的响应和处理。例如,可以通过设置中断使能位和中断优先级来控制中断的触发和处理顺序。

4. 外设通信:单片机通常需要与外围设备进行通信,如传感器、执行器、显示器等。编程时需要操作相应的寄存器来配置通信接口和协议,以实现与外设的数据交换和控制。

5. 优化性能:直接操作寄存器可以提高程序的执行效率和性能,因为与使用高级函数库相比,直接操作寄存器可以减少额外的开销和延迟,从而更好地满足实时性要求。

因此,单片机编程中的许多操作都是通过操作寄存器来实现的,程序员需要深入了解单片机寄存器的功能和用法,以充分利用单片机的硬件资源,并编写出高效可靠的程序。

标准库的本质:
        别看标准库调用一大堆函数,其实你不断的通过拆开函数的一层层嵌套,最后还是发现,他还是通过写单片机的寄存器,来实现功能。

寄存器映射:
        既然写寄存器,那就要知道寄存器的地址,这就是寄存器映射。存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射,具体见图存储器映射。如果给存储器再分配一个地址就叫存储器重映射。

我们在编程的时候,可以通过他们的地址找到他们,然后来操作他们(通过 C 语言对它们进行数据的读和写)。

   储存器区域功能划分:

        在这 4GB 的地址空间中, ARM 已经粗线条的平均分成了 8 个块每块 512MB ,每个块也都规定了用途,具体分类见表格存储器功能分类 。每个块的大小都有 512MB ,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而 已。

        在这 8 个 Block 里面,有 3 个块非常重要,也是我们最关心的三个块。 Block0用来设计成内部 FLASH,Block1 用来设计成内部 RAM,Block2 用来设计成片上的外设, 下面我们简单的介绍下这三个 Block 里面的具体区域的功能划分。 其中Block2我们需要特别的去留意
        Block2 用于设计片内的外设根据外设的总线速度不同, Block 被分成了 APB 和 AHB 两部分, 其中 APB 又被分为 APB1 和 APB2, 所以为什么我们写调用库的时候,会有APB1 和APB2以及AHB,因为他们外设所在的地方不同。

        在存储器 Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共 32bit ,每一个 单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作我们可以找到每个单元的起 始地址,然后通过 C 语言指针的操作方式来访问这些单元.如果每次都是通过这种地址的方式 来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
        比如,我们找到 GPIOB 端口的输出数据寄存器 ODR 的地址是 0x40010C0C (至于这个地址如何 找到可以先跳过,后面我们会有详细的讲解),ODR 寄存器是 32bit ,低 16bit 有效,对应着 16 个 外部 IO ,写 0/1 对应的的 IO 则输出低 / 高电平。现在我们通过 C 语言指针的操作方式,让 GPIOB的16 个 IO 都输出高电平。
        
 *(unsigned int*)(0x4001 0C0C) = 0xFFFF;   //c语言代码
        0x4001 0C0C 在我们看来是 GPIOB 端口 ODR 的地址,但是在编译器看来,这只是一个普通的变 量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针, 即 (unsigned int *)0x4001 0C0C ,然后再对这个指针进行 * 操作。
        刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器的方式来操作。
// GPIOB 端口全部输出 高电平
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR = 0xFF;

        记住绝对地址或许有点难,但是记住个寄存器的名字, 似乎简单不少,我们这里直接通过宏定义,实现直接往寄存器赋值。本质还是通过寄存器的地址,通过指针来对该寄存器进行读写。

         stm32外设地址映射:

        片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB1 挂载低速外设,APB2 和 AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地
址也是挂载在该总线上的首个外设的地址。 其中 APB1 总线的地址最低,片上外设从这里开始, 也叫外设基地址。

        总线基地址  :

表格 总线基地址 的“相对外设基地址偏移”即该总线地址与“片上外设”基地址 0x4000 0000 的差值。
       外设基地址:

这里面我们以 GPIO 这个外设来讲解外设的基地址,GPIO 属于高速的外设,挂载到APB2总线 上,具体见表格外设 GPIO 基地址。

         外设寄存器:

        在 XX 外设的地址范围内,分布着的就是该外设的寄存器。以 GPIO 外设为例, GPIO 是通用输入输出端口的简称,简单来说就是 STM32 可控制的引脚,基本功能是控制引脚输出高电平或者低电平。


        GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit ,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以GPIOB 端口为例,来说明 GPIO 都有哪些寄存器,具体见表格 GPIOB 端口的寄存器地址列表 。


c语言对寄存器的封装:

        可以说,只要你看懂这个,那么你几乎就能知道标准库大体是怎么写出来的。

封装总线和外设基地址:

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

        首先定义了“片上外设”基地址 PERIPH_BASE ,接着在 PERIPH_BASE 上加入各个总线的地址偏移,得到 APB1、APB2 总线的地址 APB1PERIPH_BASE、APB2PERIPH_BASE, 在其之上加入外设地址的偏移,得到 GPIOA-G 的外设地址,最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可以用指针读写
 

封装寄存器列表:

        用上面的方法去定义地址,还是稍显繁琐,例如 GPIOA-GPIOE 都各有一组功能相同的寄存器, 如 GPIOA_ODR/GPIOB_ODR/GPIOC_ODR 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入 C 语言中的 结构体 语法对寄存器进行封装。这就是为什么,标准库要定义这么多结构体。
 /* GPIO 寄存器列表 */
 typedef struct 
{
     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; uint32_t CRL; /*GPIO 端口配置低寄存器 地址偏移: 0x00 */
这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 7 个成员变量, 变量名正好对应寄存器的名字。C 语言的语法规定,结构体内变量的存储空间是连续的,其中 32位的变量占用 4 个字节, 16 位的变量占用 2 个字节。

        也就是说,我们定义的这个GPIO_TypeDef ,假如这个结构体的首地址为 0x4001 0C00(这也是第一个成员变量 CRL 的地址),那么结构体中第二个成员变量 CRH 的地址即为 0x4001 0C00 +0x04,加上的这个 0x04,正是代表 CRL 所占用的 4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给。这样的地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器,


 

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

 

最后,我们更进一步,直接使用宏定义好 GPIO_TypeDef 类型的指针,而且指针指向各个 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 中

总结:
因为32的寄存器太多也太复杂,如果说像51单片机来直接对寄存器编程,会非常的麻烦,需要不断的去查询地址,和各个寄存器和各个位的功能,虽然说,他为我们提供了便利,但是,真正的学会这个单片机,还是需要去查阅芯片手册,看看各个寄存器的功能以及单片机整体的架构。

这篇只是大概告诉读者标准库是如何封装寄存器来对stm32中的寄存器进行读写,真正的去看懂标准库和标准库的函数,还是需要读者仔细的去研究标准库的每行代码。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值