在进行底层开发时,尤其是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*)0x40020014
GPIOA_ODR = 0xffff; // 使用暂存器映射
其中使用(unsigned int*)
强制转型的作用是为了让编译器知道它是一个地址类型常数。通常我们为暂存器命名会考虑到它的具体意义,比如GPIOA_ODR代表该GPIO A引脚的通用输出暂存器(Output Data Register),命名尽量便於理解为主
假设我们想实现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_t ODR;
uint32_t BSRR;
uint32_t LCKR;
uint32_t AFRL;
uint32_t AFGH;
}GPIO_TYPEDEF
typedef 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引脚重要的功能部分之一,它决定引脚後续的工作性质、输出速度以及工作状态
进入模式配置类型暂存器介绍之前,我们先用一个盖览图来抓住模式配置类型暂存器的大框架: 依照不同模式去配置不同的引脚特性
配置GPIO引脚的工作模式,包含输入、输出、复用功能开启以及类比功能
GPIO端口首地址偏移量: 0x00
每个引脚由两个位元进行控制,分别有4种不同的模式:
当GPIO选择为输出模式,就需要选择输出模式,主要有推挽模式与开漏模式两种
设置GPIO引脚的输出速度
为了避免引脚在没有任何输入或输出(看引脚是配置成甚麽模式)下产生浮动,也就是说引脚的值是不确定的,需要依照MCU的特性去配置预设状态的电位,PUPDR就是在处理这个问题。例如将输入模式切换成输出模式之间的空档有可能会出现浮动,这时就需要配置一个确定的值
在输出模式下使用上拉模式时,会因为ODR暂存器的预设输出为0而影响,这时候上拉只能小幅度提升电位,输出依然为低电位
输出控制暂存器,当设值成0时输出低电位;设值成1时输出高电位。主要由比特位0~15控制16个引脚,16~31为保留位
使用ODR作为输出控制时,其反应速度会被中断等事件影响,造成时延。另外ODR暂存器是可读可写的暂存器,使用程序控制时要先对其进行读操作,然後再进行写操作
uint32_t tmp;
tmp = GPIO_X->ODR;
tmp = tmp | 0x0001;
GPIO_X->ODR = tmp;
置位复位暂存器,可分为高16位和低16位。低16位(0~15)控制置位操作,也就是输出高电位;高16位(16~31)控制复位操作,也就是输出低电位。其控制规则如下:
低16位
高16位
若动应的置位操作与复位操作同时设成1,则会以置位操作为优先。例如对引脚3的置位与复位同时赋值为1(比特位3和19),则输出高电位
BSRR为只写暂存器,在使用上相较於ODR,不需要读取暂存器内容再写入,可以直接对目标引脚进行操作,例如刚刚的引脚3例子
GPIO_X->ODR |= (1<<3); // 低16位,置位操作
GPIO_X->ODR |= (1<<16<<3); // 高16位,复位操作
输入数据暂存器,其功能是读取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_t ODR;
uint16_t BSRRL;
uint16_t BSRRH;
uint32_t LCKR;
uint32_t AFRL;
uint32_t AFGH;
}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);
else
GPIO_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_register
RCC_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);
#endif
while(1);
}
// void SystemInit(void)
// {
// }
<<: 为了转生而点技能-JavaScript,day14(this下篇: Call、apply呼叫及bind
预计三十天内学会制作一个2D游戏,如果还有时间就继续练习一个3D游戏。 开始先做一个2D的卷轴射击游...
op.28 属於你的避风港 无论你身在哪个时空之中,我一直是你的避风港 昨天我们完成了 NodeM...
21 - Draper 上篇 Design Pattern(1) - Decorator 简单的介绍...
今年是第二次参加,这次一样努力以不断赛为目标 不知不觉默默已经工作好一阵子了,从一开始觉得演算法好像...
Array阵列 简单来说 阵列就是一个有序的序列而且里面可以储存不定数量的任何值 我是把它想像成一个...