STM32开发笔记01---暂存器简介
时间:2023-10-12 04:07:00
架构图
前言
在底层开发中,特别是C语言,我们经常处理临时存储器,但临时存储器的确切定义是什么?有时很难准确地定义它
有些书把临时存储器想象成一排书柜里的一个特定的网格。对于这些特殊的抽屉,你可以打开抽屉拿里面的纸条,或者把新的纸条放进去。就我个人而言,我喜欢这个隐喻,但它也让我我是否可以以更准确的方式定义临时存储器
思考重点
- 暂存器与记忆体映射的关系
- 存在暂存器的意义
- 如何查找数据手册?
- 编写点灯案例
暂存器概念
为了厘清暂存器的概念,我特地找了一块32bits的STM32F4型开发版,核心使用STM429IGT6.事实上,我们的编控制这个程序CPU许多引脚可以满足特定的需求,例如,传感器的输入是在运行后通过的GPIO引脚输出控制
我们可以通过控制引脚的输出和输入来实现特定的目标,。开发版本中的引脚分配了一组独特的地址位置。通过改变这些地址存储的值,可以有效地控制引脚的输出和输出。因此,我们可以将引脚作为控制的最基本单位,而临时存储器是引脚背后的控制原理
这次使用下图STM32开发版引脚图,它拥有176个引脚
记忆体映射
事实上,记忆本身并没有地址的概念,所谓的地址它是由芯片制造商或用户计划的,也就是说,地址的概念实际上是我们抽象的
关键是如何知道虚拟地址的范围?
为了便于理解,我们从STM相应的官方网站下载data sheet(使用我的开发版STMF429)从下图可以看出,记忆体的映射范围是0x0000 0000
~0xFFFF FFFF
,共有4294967296,即4296G大小空间。请注意4G大小不代表核心版的真实存储大小,而是核心版有能力说明这么大的空间,两者区别。
4G = 4294967296 = 2^简单来说就是处理器位元数的次方数。STM32F429开发版的核心处理器是32位元,有许多临时存储器负责存储数据,而这些临时存储器的长度范围只有32位bits
假设一个长度是32bits存储一个整数型态数据的临时存储器为4,将4转换为二进位系统等于0000 0000 0000 0000 0000 0000 0000 0100
,我们将这一系列二进位数存储在一起0x0000 0000
~0x0000 001F
的地址空间中,这一块32bits长度的连续空间称为暂存器
可以看出,每个临时存储器的起始地址之间有32个bits(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),命名尽可能容易理解
C语言结构包装
假设我们想实现GPIOA首先要知道暂存器的控制GPIOA所以参考起始地址STM官方网站的reference manual可以在手册中找到记忆体映射表GPIOA蓝色方框显示下图的地址范围GPIOA起始地址为0X4002 0000
右侧显示GPIOA位于AHB1高速总线块(系统)GPIO引脚位于这里),通过data sheet的查找发现AHB被分配到名字上Block2的分区内(很明显地片上外设都位于Block2),所以我们可以很容易地做到GPIOA标记地址的目的是在编写程式时偏移三个层次的地址
这三种偏移分别是:
- 外设基地址偏移
- 总线基地址偏移
- GPIO基地址偏移
透过查找Memory Table外设的起始地址可以在表中找到,GPIO起始地址以及GPIOA我们使用嵌套来映射这些临存器地址
/*外设基地址*/ #define PERIPH_BASE ((unsigned int)0x40000000) // 外设起始地址 /*总线基地址*/ #define AHB1_PERIPH_BAE (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引脚重要的功能部分之一,它决定引脚后续的工作性质、输出速度以及工作状态
进入模式配置类型暂存器介绍之前,我们先用一个盖览图来抓住模式配置类型暂存器的大框架: 依照不同模式去配置不同的引脚特性
MODER
配置GPIO引脚的工作模式,包含输入、输出、複用功能开启以及类比功能
GPIO端口首地址偏移量: 0x00
每个引脚由两个位元进行控制,分别有4种不同的模式:
- 00: GPIO输入模式
- 01: GPIO输出模式
- 10: 开启複用功能(UART、SPI、I2C…)
- 11: 类比输入输出(输入输出会由ADC/DAC进行转换)
OTYPER
当GPIO选择为输出模式,就需要选择输出模式,主要有推挽模式与开漏模式两种
- 0: 推挽模式
- 可以输出高低两电位,适合连接数位器件,为一般引脚最常见的配置
- 输出高电位为强高电位,驱动能力较强
- 输出反映快速
- 1: 开漏模式
- 通常运用在I2C、SMBUS等需要wired AND(线与)功能的汇流排(总线)电路上
- 输出低电位以及强低电位,也就是说开漏输出无法真正达到输出高电位
- 控制输出为1时,为高阻态,即强低电位(不高也不低),需要靠外部电路上拉才能实现真正高电位
OSPEEDER
设置GPIO引脚的输出速度
- 00: 低速模式(2MHz)
- 01: 中速模式(25MHz)
- 10: 快速模式(50MHz)
- 11: 高速模式(100MHz)
PUPDR
为了避免引脚在没有任何输入或输出(看引脚是配置成甚麽模式)下产生浮动,也就是说引脚的值是不确定的,需要依照MCU的特性去配置预设状态的电位,PUPDR就是在处理这个问题。例如将输入模式切换成输出模式之间的空档有可能会出现浮动,这时就需要配置一个确定的值
- 00: 没有设定上下拉
- 01: 上拉,预设为高电位
- 10: 下拉,预设为低电位
- 11: 保留,目前尚未开发的功能(尽量避免使用)
在输出模式下使用上拉模式时,会因为ODR暂存器的预设输出为0而影响,这时候上拉只能小幅度提升电位,输出依然为低电位
数据输出类型
ODR
输出控制暂存器,当设值成0时输出低电位;设值成1时输出高电位。主要由比特位0~15控制16个引脚,16~31为保留位
- 0: 输出低电位
- 1: 输出高电位
使用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
#include
//#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
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)
// {
// }