架构图
前言
在进行底层开发时,尤其是C语言,我们时常与暂存器打交道,不过到底暂存器的确切定义是甚么?有时很难确切定义
有些书将暂存器想像成一排书柜中的特定一格,对这些特殊抽屉,可以将抽屉打开拿取里面的纸条,也可以把新的纸条放进去。我个人蛮喜欢这个比喻法,但也让我思考,到底能不能用更精準的方式去定义暂存器呢
思考重点
暂存器与记忆体映射之间的关联暂存器存在的意义如何查找数据手册编写一个点灯案例暂存器概念
为了釐清暂存器的概念,我特地找了一块32bits的STM32F4型开发版,核心使用STM429IGT6,其实我们编写程式就是在控制这颗CPU的众多引脚来达到特定需求,例如传感器的输入经过运算后,经由GPIO引脚输出控制
我们可以藉由控制引脚的输出以及输入来达到特定目,。而开发版上的引脚都被分配了一组独一无二的地址位置,透过更改这些地址储存的数值,就可以有效的控制引脚要怎么输出、如何输出。因此我们可以把引脚当作控制的最基本单位,而暂存器就是引脚背后的控制原理
下图是这次使用的STM32开发版引脚图,它拥有176个引脚
记忆体映射
其实记忆体本身是不具有地址概念的,所谓的地址是由芯片厂商或用户自行规划出来的,也就是说地址的概念其实是我们抽象出来的
那重点来了,要如何知道虚拟地址的範围是多大?
为了方便理解,我们从STM官方网站下载相应的data sheet(我的开发版使用STMF429),从下图可以看出记忆体的映射图範围为0x0000 0000
~0xFFFF FFFF
,总共有4294967296个,也就是4G大小的空间。请注意4G大小并不代表核心版的真实储存大小,而是核心版有能力表示这么大的空间,这两者是有差别的
4G = 4294967296 = 2^32,简单来说就是处理器的位元数的次方数。STM32F429这个开发版的核心处理器为32位元,处理器里有很多很多负责存储数据的暂存器,而这些暂存器的长度範围恰好是32bits
我们假设一个长度为32bits的暂存器,它储存了一个整数型态的数据,数值为4,将4转换成二进位制等于0000 0000 0000 0000 0000 0000 0000 0100
,我们分别把这一连串二进位数值存放到0x0000 0000
~0x0000 001F
的地址空间中,这一块32bits长度的连续空间就称为暂存器
由此可知每一个暂存器的起始地址之间存在32bits(4bytes)的差距,起始位置是0x0000 0000
,最大值是0xFFFF FFFF
,这个範围建构了4G大小的寻址空间,官方网站的Memory Mapping就是这么计算出来的。所以我们把这个位记忆体分配空间的行为称为记忆体映射
暂存器映射
为已经记忆体映射完的记忆体地址命名的过程就称为暂存器映射
暂存器映射的目的在于编写程式时可以用定义好的暂存器名进行操作,而不用每次都调用难懂的16进制,例如下面的这段程式
/* GPIOA 16个引脚都输出高电位 */*(unsigned int*)(0x40020014) = 0xffff; // 单存操作暂存器地址#define GPIOA_ODR *(unsigned*)0x40020014GPIOA_ODR = 0xffff; // 使用暂存器映射
其中使用(unsigned int*)
强制转型的作用是为了让编译器知道它是一个地址类型常数。通常我们为暂存器命名会考虑到它的具体意义,比如GPIOA_ODR代表该GPIO A引脚的通用输出暂存器(Output Data Register),命名尽量便于理解为主
C语言结构封装
假设我们想实现GPIOA的暂存器控制,首先必须知道GPIOA的起始地址,于是参考STM官方网站的reference manual手册中的记忆体映射表可以找到GPIOA的地址範围,下图蓝色方框显示GPIOA的起始地址为0X4002 0000
右侧显示GPIOA位于AHB1高速总线区块上(系统的GPIO引脚都位于此处),透过data sheet的查找发现AHB1被分配到名为Block2的分区内(很明显地片上外设都位于Block2),因此我们可以轻易地将GPIOA身处的地址标示出来,这么做的目的是为了编写程式时可以进行3个层次的地址偏移
这三种偏移分别是:
透过查找Memory Table表可以查到外设起始地址、GPIO起始地址以及GPIOA的地址,我们使用嵌套的方式映射这些暂存器地址
/*外设基地址*/#define PERIPH_BASE ((unsigned int)0x40000000) // 外设起始地址 /*总线基地址*/#define AHB1_PERIPH_BASE (PERIPH_BASE + 0x00020000) // GPIO的起始地址 /*GPIO基地址*/#define GPIO_A_BASE (AHB1_PERIPH_BASE + 0x0000) // GPIOA起始地址
定义偏移地址的好处就是更好的扩充性,比如我今天想要define一个GPIOI地址,只需要将AHB1_PERIPH_BASE+0X2000
就好了,不需要从基地址的暂存器地址开始计算偏移
这三种层次由大到小,使用者只须要依照想要使用的引脚範围进行定义,一旦基地址定义完成,开发者只需要选择距离目标引脚地址最小的偏移量基地址开始定义即可,另一方面这种方式也利于开发者阅读
GPIO端口设有10个暂存器,而且连续储存于GPIOA的连续记忆体空间中。因此我们可以透过自定义一个结构体数据类型来模拟内存空间中的暂存器
typedef unsigned int uint32_t;typedef struct{uint32_t MODER;uint32_t OTYPER;uint32_t OSPEEDER;uint32_t PUPDR;uint32_t IDR;uint32_tODR;uint32_tBSRR;uint32_tLCKR;uint32_tAFRL;uint32_tAFGH;}GPIO_TYPEDEFtypedef GPIO_TYPEDEF* GPIO_Typedef; // 指向GPIO结构体的指标
我们透过将指标指向GPIO_X
的基地址,使结构体内的成员的地址刚好与各个暂存器对应上,所以当我们对结构体成员操作时,事实上是在操作GPIO对应的暂存器:
GPIO_Typedef GPIO_X;GPIO_X = GPIOX_BASE;GPIO_X->MODER = 0X0003;GPIO_X->OTYPER = 0X0001;GPIO_X->OSPEEDER = 0X0003;uint32_t tmp;tmp = GPIO_X->IDR; // 读取暂存器
查找手册
查找完手册上对应的外设地址,然后利用程式编写暂存器映射的阵列指标后,我们必须再次查看手册,釐清引脚背后每个暂存器的控制意义,GPIO暂存器的介绍在data sheet的General-purpose I/Os(GPIO)/GPIO registers下可以找到:
STM32F429的每一个GPIO端口均配置10个长度为32bits的暂存器,依种类不同大致可以分成以下5大类:
模式配置类型
模式配置类型是GPIO引脚重要的功能部分之一,它决定引脚后续的工作性质、输出速度以及工作状态
进入模式配置类型暂存器介绍之前,我们先用一个盖览图来抓住模式配置类型暂存器的大框架: 依照不同模式去配置不同的引脚特性
MODER
配置GPIO引脚的工作模式,包含输入、输出、複用功能开启以及类比功能
GPIO端口首地址偏移量: 0x00
每个引脚由两个位元进行控制,分别有4种不同的模式:
OTYPER
当GPIO选择为输出模式,就需要选择输出模式,主要有推挽模式与开漏模式两种
OSPEEDER
设置GPIO引脚的输出速度
PUPDR
为了避免引脚在没有任何输入或输出(看引脚是配置成甚么模式)下产生浮动,也就是说引脚的值是不确定的,需要依照MCU的特性去配置预设状态的电位,PUPDR就是在处理这个问题。例如将输入模式切换成输出模式之间的空档有可能会出现浮动,这时就需要配置一个确定的值
在输出模式下使用上拉模式时,会因为ODR暂存器的预设输出为0而影响,这时候上拉只能小幅度提升电位,输出依然为低电位
数据输出类型
ODR
输出控制暂存器,当设值成0时输出低电位;设值成1时输出高电位。主要由比特位0~15控制16个引脚,16~31为保留位
使用ODR作为输出控制时,其反应速度会被中断等事件影响,造成时延。另外ODR暂存器是可读可写的暂存器,使用程式控制时要先对其进行读操作,然后再进行写操作
uint32_t tmp;tmp = GPIO_X->ODR;tmp = tmp | 0x0001;GPIO_X->ODR = tmp;
BSRR
置位复位暂存器,可分为高16位和低16位。低16位(0~15)控制置位操作,也就是输出高电位;高16位(16~31)控制复位操作,也就是输出低电位。其控制规则如下:
低16位
0: 不进行任何动作1: 输出高电位高16位
0: 不进行任何动作1: 输出低电位若动应的置位操作与复位操作同时设成1,则会以置位操作为优先。例如对引脚3的置位与复位同时赋值为1(比特位3和19),则输出高电位
BSRR为只写暂存器,在使用上相较于ODR,不需要读取暂存器内容再写入,可以直接对目标引脚进行操作,例如刚刚的引脚3例子
GPIO_X->ODR |= (1<<3); // 低16位,置位操作GPIO_X->ODR |= (1<<16<<3); // 高16位,复位操作
数据输入类型
IDR
输入数据暂存器,其功能是读取GPIO端口的所有引脚输入状态。低16位是只读功能的比特位,高16位保留。
想读取特定引脚输入数值只要将IDR的读值进行位元运算即可
複用功能类型
赋用功能暂存器,可以将GPIO引脚转为其他通讯接口功能,例如UART、SPI、I2C等等
由两个暂存器负责处理複用功能操作,AFRL负责引脚0~7,AFRH负责8~15,每个引脚皆有16种可能,由4个比特位控制。预设状态为AF0,并且每个引脚同时只能存在一个複用功能
其配置如下图所示:
点灯
最后我们试着使用一个点灯程式来整合学到的暂存器观念。引脚输出方面,我使用GPIO A的引脚4、5、6作为红、绿、蓝LED输出脚位
首先若要启用GPIO,一定要先对其外设时钟控制暂存器RCC进行置位,我们根据data sheet对Memory Map的描述找到GPIO对应的RCC地址
紧接着同样在header文件中建立暂存器基地址映射、RCC地址映射以及GPIO暂存器结构体
#ifndef __STM32F4XX_H#define __STM32F4XX_H#include <stdio.h>#include <stdint.h>//#define GPIO_register 1/*Memory mapping*/#define PERIPH_BASE ((unsigned int)0x40000000) #define AHB1_PERIPH_BASE (PERIPH_BASE + 0x00020000)#define GPIO_H_BASE (AHB1_PERIPH_BASE + 0x1C00)/*RCC*/#define RCC_BASE (AHB1_PERIPH_BASE + 0x3800)#define RCC_AHB1_ENR*(unsigned int*)(RCC_BASE+0x30)/*GPIO*/typedef struct{uint32_t MODER;uint32_t OTYPER;uint32_t OSPEEDER;uint32_t PUPDR;uint32_t IDR;uint32_tODR;uint16_tBSRRL;uint16_t BSRRH;uint32_tLCKR;uint32_tAFRL;uint32_tAFGH;}GPIO_TYPEDEF;typedef GPIO_TYPEDEF* GPIO_Typedef; //GPIO pointer#endif
我使用两种方式点灯,第一种是纯粹的暂存器控制。第二种是封装成类库函式形式。主要使用GPIO_register
来切换(看mian的条件编译),所以也附上封装的header与source files
#ifndef __STM_GPIO__H#define __STM_GPIO__H#include "stm32f4xx.h"#include <stdbool.h>typedef enum{port_A=0,port_B,port_C,port_D,port_E,port_F,port_G,port_H}port;extern void GPIO_Init(GPIO_Typedef, uint8_t);extern void GPIO_LED_Control(GPIO_Typedef, uint8_t, bool);#endif
#include "stm_gpio.h"void GPIO_Init(GPIO_Typedef gpio, uint8_t port){RCC_AHB1_ENR |= (1<<port);gpio->MODER = 0x00;gpio->OTYPER = 0x00;gpio->OSPEEDER = 0x00;gpio->PUPDR = 0x00;}void GPIO_Config(GPIO_Typedef gpio, uint8_t pin){gpio->MODER |= (1<<2*pin);gpio->OTYPER |= (0<<pin);gpio->OSPEEDER |= (2<<2*pin);gpio->PUPDR |= (1<<2*pin);}void GPIO_SET(GPIO_Typedef gpio, uint8_t pin){gpio->BSRRL &= ~(1<<pin);gpio->BSRRL |= (1<<pin);}void GPIO_RESET(GPIO_Typedef gpio, uint8_t pin){gpio->BSRRH &= ~(1<<pin);gpio->BSRRH |= (1<<pin);}void GPIO_LED_Control(GPIO_Typedef gpio, uint8_t pin, bool output){GPIO_Config(gpio, pin);if(output)GPIO_SET(gpio, pin);elseGPIO_RESET(gpio, pin);}
最后我们编写主函式main,LED依照需求亮灭。编译成功可以将code烧进板子检查看看是否点灯成功
#include "stm32f4xx.h"#include "stm_gpio.h"/** * main */int main(void){GPIO_Typedef GPIO = (GPIO_Typedef)GPIO_A_BASE;#ifdef GPIO_registerRCC_AHB1_ENR |= (1<<0);/*MODER*/GPIO->MODER &= ~(3<<2*4);GPIO->MODER &= ~(3<<2*5); GPIO->MODER &= ~(3<<2*6);GPIO->MODER |= (1<<2*4);GPIO->MODER |= (1<<2*5); GPIO->MODER |= (1<<2*6); /*OTYPER*/GPIO->OTYPER &= ~(1<<4);GPIO->OTYPER &= ~(1<<5);GPIO->OTYPER &= ~(1<<6);GPIO->OTYPER |= (0<<4);GPIO->OTYPER |= (0<<5);GPIO->OTYPER |= (0<<6);/*OSPEEDER*/GPIO->OSPEEDER &= ~(3<<2*4);GPIO->OSPEEDER &= ~(3<<2*5);GPIO->OSPEEDER &= ~(3<<2*6);GPIO->OSPEEDER |= (2<<2*4);GPIO->OSPEEDER |= (2<<2*5);GPIO->OSPEEDER |= (2<<2*6);/*PUPDR*/GPIO->PUPDR &= ~(3<<2*4);GPIO->PUPDR &= ~(3<<2*5);GPIO->PUPDR &= ~(3<<2*6);GPIO->PUPDR |= (1<<2*4);GPIO->PUPDR |= (1<<2*5);GPIO->PUPDR |= (1<<2*6);/*BSRRL*/GPIO->BSRRL &= ~(1<<4);GPIO->BSRRL &= ~(1<<5);GPIO->BSRRL &= ~(1<<6); //GPIO_H->BSRRL |= (1<<4);GPIO->BSRRL |= (1<<5);GPIO->BSRRL |= (1<<6);/*BSRRH*/GPIO->BSRRH &= ~(1<<4);GPIO->BSRRH &= ~(1<<5);GPIO->BSRRH &= ~(1<<6);GPIO->BSRRH |= (1<<4); //GPIO->BSRRH |= (1<<5); //GPIO->BSRRH |= (1<<6);#else/* GPIO A Initial */GPIO_Init(GPIO, port_A);/* pin 4 config*/GPIO_LED_Control(GPIO, 4, 0);/* pin 5 config*/GPIO_LED_Control(GPIO, 5, 1);/* pin 6 config*/GPIO_LED_Control(GPIO, 6, 0);#endifwhile(1);}// void SystemInit(void)// {// }