STM32 入门 —— 寄存器与 GPIO
STM32 入门 —— 寄存器与 GPIO
STM32 总线构图:
寄存器
什么是寄存器
根据百度百科介绍,寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。简单来说,寄存器就是存放东西的东西,存放的东西是指令、数据或地址
存放数据的寄存器最容易理解,不同的数据存在不同的寄存器下,不同的寄存器有不同的地址,要想获得数据,我们直接访问寄存器,就可以直接获得数据
指令、地址寄存器与数据寄存器相似,存放的都是 0/1 编码,由于单片机只认识机器码,机器码都是 0/1 ,只是在特别的规定下,数据寄存器中的 0/1 编码表示数据,而指令寄存器李存放的表示指令
如何找到寄存器地址
查找《STM32中文参考手册_V10》,在手册的第 28 页给出了不同寄存器的地址范围,但是手册并没有直接给出,需要稍加计算,可以得出寄存器地址
下面以读取 PB3 引脚电平为例,介绍查找步骤
首先,找到 GPIOB 的基地址:
PB3 引脚相关信息可以查看《STM32F103x8, STM32F103xB数据手册》:
可知 PB3 引脚既可以输入也可以输出
然后,需要找到输入端口寄存器的地址偏移:
由手册可知,地址偏移为 0x08 ,最终可知地址为:0x40010c00 + 0x08 = 0x40010c08
最后找到对应的位置:
这个寄存器的位数是 32 位,虽然高 16 位没有用到,每个寄存器都占据 4 个字节,32位,而 CPU 的总线一次可以操作 32 位
最后得出:PB3 的输入数据位于 0x4001 0C08 这个地址上,这个地址上存放数据的右起第 4 个位就是 PB3 引脚对应的高低电平
访问地址代码如下:
unsigned int *pGPIOB_IDR = (unsigned int *)0x40010C08;
unsigned char PB3 = *pGPIOB_IDR & 0x8;//取出从右往左数的第4位
直接访问的操作并不好用,每操作一个寄存器就必须去查看数据手册,然后找找这个寄存器的地址
意法半导体公司为了方便大家使用,就把这些寄存器都起了一目了然的名字,把寄存器与地址映射关系放在他们提供的头文件里。这个文件就是 stm32f10x.h
寄存器映射原理
寄存器映射是在存储器的基础上进行的,所以在了解寄存器映射原理之前,需要了解存储器映射原理
存储器映射
存储器在产家制作完成后是一片没有任何信息的物理存储器,而 CPU 要进行访存就涉及到内存地址的概念,因此存储器映射就是为物理内存按一定编码规则分配地址的行为。值得注意,存储器映射一般是由产家规定,用户不能随意更改
注意:STM32 中,I-Code Bus与D-Code Bus 默认映射到 0x00000000 ~ 0x1FFFFFFF 内存地址段;AHB 系统总线默认映射到0x20000000 ~ 0xDFFFFFFF 和 0xE0100000 ~ 0xFFFFFFFF 两个内存地址段;APB 外设总线默认映射到 0xE0040000 ~ 0xE00FFFFF 内存地址段,但由于 TPIU、ETM 以及 ROM 表占用部分空间,实际可用地址区间为 0xE0042000~0xE00FF000
寄存器
寄存器映射是在存储器映射基础上进行的
STM32 中,对硬件操作,本质上就是对寄存器操作。在存储器片上外设区域,四字节为一个单元,每个单元对应不同的功能。
当我们控制这些单元时就可以驱动外设工作,我们可以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元。
但若每次都是通过这种方式访问地址,不好记忆且易出错。这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名实质上就是寄存器名字。给已分配好地址(通过存储器映射实现)的有特定功能的内存单元取别名的过程就叫寄存器映射。
以 GPIO 寄存器 CRL 为例,给出 CRL 定义:
typedef struct
{
__IO unit32_t CRL;
__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;
在实际使用时,会有 GPIOA->CRL=0x0000 0000 这种写法,表示将 16 进制数 0 赋值给 GPIOA 的 CRL 寄存器所在的存储单元。而 GPIOA->CRL 就构造了一个寄存器映射。具体过程如下:
#define PERIPH_BASE ((unit32_t)0x40000000)
这里属于存储器级别的映射,将外设基地址映射到 0x40000000 ,可以对应下图:
#define APB2PERIPH_BASE (PERIPH_BASE + 0X10000)
这里对外设基地址进行偏移量为 0x10000 的地址偏移,偏移到 APB2 总线对应外设区:
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
这里对 APB2 外设基地址进行偏移量为 0x0800 的地址偏移,偏移到 GPIOA 对应区域:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
这里将 GPIOA 宏定义为 GPIOA 基地址经过强制类型转换为 GPIO_TypeDef 的指针,这样的作用是使 GPIOA 结构体内对应的成员按顺序填充内存区域,如图3所示。因此 GPIOA 的 CRL 寄存器就是作为 GPIOA 基地址后的第一个内存块,GPIOA->CRL 的本质就是这个内存块的地址,或者说是用 GPIOA->CRL 给这个地址取了个形象的别名,即寄存器映射。
地址映射原理
在 STM32 开发中,我们通常使用库进行开发。说白了,32 开发是从底层一层一层封装上去的。到我们开发者这里,就是使用最上层的接口进行开发。但是一层一层看下去,还是对寄存器的控制,要控制寄存器,就需要操作寄存器地址。
stm32 地址映射如下:
在倒数第三紫色区域是片上外设的地址区域,这里反映了片上外设的地址,我们通过操作这些地址,便能操作这些外设寄存器。
在 stm32 中,有三大总线,AHB 总线,APB1 总线以及 APB2 总线。不同的外设挂载在不同的总线上。比如 GPIO,串口 1 ,ADC 以及部分定时器挂载在 APB2 总线上。
打开 stm32f10x.h 这个文件,这个文件主要包含 stm32 中寄存器地址和结构体类型定义,在使用到固件库的地方都要包含该头文件。这里通过一些宏定义代码来说明一下地址映射与挂载总线的关系。
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define PERIPH_BASE ((unit32_t)0x40000000)
从代码可以看到 APB2 之类的字眼,这不是总线么?注意,有一个 PERIPH_BASE 的地址为 0x40000000 ,这不是片上外设的首地址么。这里,这个地址称作外设基地址。同样,APB2PERIPH_BASE 称作 APB2 总线外设基地址,毕竟都有 base。
总线的基地址:
也就是说,该总线上所挂载的模块都在这个地址区间内。下面的图是挂载在总线上面各寄存器以及寄存器组的地址:
GPIO
对于一个单片机,对基本的代码操作就是点亮 LED 灯,也就是说要对 I/O 口进行操作,STM32 中的 I/O 端口就是 GPIO ,软件可以控制的通用输入输出端口,STM32 通过 GPIO 引脚链接外部设备,达到与外部通信,控制以及采集数据的功能
引脚分类与说明如下:
基础知识
STM32F103C8 的开发板里总共有 5 组 I/O 口、 37 个IO口,分别是 GPIOA~GPIOG 。每个 I/O 端口位可以自由编程,可以由软件分别配置成多种模式,但 I/O 端口寄存器必须按 32 位字节访问,不允许半字或单字节访问。
GPIOx_BSRR 和 GPIOx_BRR 寄存器允许对任何 GPIO 寄存器的读/更改的独立访问;这样,在读和更改访问之间产生 IRQ 时不会发生危险。端口位配置 CNFx[1:0]=xxb,MODEx[1:0]=xxb
GPIO_InitTypeDef 是 GPIO 口的一个定义结构体,包含一个 16 位的变量 GPIO_Pin ;一个 GPIOSpeed_TypeDef 枚举结构体GPIO_Speed;一个 GPIOMode_TypeDef 枚举结构体 GPIO_Mode ;这 3 个变量可以在外部被定义,用于初始化或者改变某些 GPIO 的速度跟类型。
STM32 每个 GPI/O 端口有两个 32 位配置寄存器(GPIOx_CRL,GPIOx_CRH),两个32位数据寄存器(GPIOx_IDR,GPIOx_ODR),一个 32 位置位/复位寄存器(GPIOx_BSRR),一个 16 位复位寄存器(GPIOx_BRR)和一个 32 位锁定寄存器(GPIOx_LCKR)。
GPIO 的工作模式主要有八种:4 种输入方式,4 种输出方式,分别为输入浮空,输入上拉,输入下拉,模拟输入;输出方式为开漏输出,开漏复用输出,推挽输出,推挽复用输出。同时,GPIO还支持三种最大翻转速度(2MHz、10MHz、50MHz)
I/O 端口位的基本结构:
5v 兼容 I/O 端口位的基本结构:
GPIO 电平标准
在 STM32 的数据手册中我们可以查到,STM32 单片机的 I/O 口电平兼容 CMOS 电平和 TTL 电平,逻辑电平 0 所代表得电压范围在 0.8v 以下,大于 2v 的话就代表逻辑 1 。
所有 I/O 端口都是 CMOS 和 TTL 兼容(不需软件配置),它们的特性考虑了多数严格的 CMOS 工艺或 TTL 参数:
● 对于 VIH:
− 如果 VDD 是介于 [2.00V~3.08V] ;使用CMOS特性但包含 TTL。
− 如果 VDD 是介于 [3.08V~3.60V] ;使用TTL特性但包含 CMOS。
● 对于 VIL:
− 如果 VDD 是介于 [2.00V~2.28V] ;使用TTL特性但包含 CMOS。
− 如果 VDD 是介于 [2.28V~3.60V] ;使用CMOS特性但包含 TTL。
由于本人不是专业学习电路的,所以这里仅作上述简单介绍
GPIO 端口初始化
GPIO 端口初始化大致可以分为三个步骤:
1.时钟配置
2.输入输出模式设置
3.最大速率设置
时钟配置
对于 STM32 有 5 个时钟源,如下:
简称 | 时钟源名称 | 频率范围 |
---|---|---|
HSI | 高速内部时钟 | 8MHZ |
HSE | 高速外部时钟 | 4MHZ~16MHZ |
LSI | 低速内部时钟 | 40KHZ |
LSE | 低速外部时钟 | 32.768KHZ |
PLL | 锁相环倍频输出 | 输出频率最大不得超过 72MHz |
时钟树如下图:
程序刚启动的时候,stm32采用的为内部高速时钟。如果需要采用外部时钟,需要按照如下的方式配置:
-
时钟初始化,即将时钟的寄存器采用默认值。
-
开始外部时钟且外部时钟起震准备就绪。
-
设置 PLLXTPRE(只能在关闭PLL时才能写入此位),可选择分频不分频。
-
设置进入 PLL 的源时钟(只能在关闭 PLL 时才能写入此位)。因为采用外部时钟所以只有一种设置。
-
设置 PLL 倍频系数 PLLMUL(只有在 PLL 关闭的情况下才可被写入)。
-
开启 PLL ,且准备就绪。
-
设置 SW ,选择时钟源为系统时钟。
-
判断是否是预选的时钟为系统时钟。
输入输出模式设置
-
GPIO_Mode_AIN 模拟输入 (应用ADC模拟输入,或者低功耗下省电)
-
GPIO_Mode_IN_FLOATING 浮空输入 (浮空就是浮在半空,可以被其他物体拉上或者拉下,可以用于按键输入)
-
GPIO_Mode_IPD 下拉输入 (IO内部下拉电阻输入)
-
GPIO_Mode_IPU 上拉输入 (IO内部上拉电阻输入)
-
GPIO_Mode_Out_OD 开漏输出(开漏输出:输出端相当于三极管的集电极. 要得到高电平状态需要上拉电阻才行)
-
GPIO_Mode_Out_PP 推挽输出 (推挽就是有推有拉电平都是确定的,不需要上拉和下拉,IO输出0-接GND, IO输出1 -接VCC,读输入值是未知的 )
-
GPIO_Mode_AF_OD 复用开漏输出(片内外设功能(I2C的SCL,SDA))
-
_Mode_AF_PP 复用推挽输出 (片内外设功能(TX1,MOSI,MISO.SCK.SS))
代码 | 模式 |
---|---|
GPIO_Mode_IPU | 上拉输入 |
GPIO_Mode_AIN | 模拟输入 |
GPIO_Mode_IN_FLOATING | 浮空输入 |
GPIO_Mode_IPD | 下拉输入 |
GPIO_Mode_Out_OD | 开漏输出 |
GPIO_Mode_Out_PP | 推挽输出 |
GPIO_Mode_AF_OD | 复用开漏输出 |
GPIO_Mode_AF_PP | 复用推挽输出 |
输入模式名称 | 作用 | 原理 |
---|---|---|
上拉输入模式 | 在默认状态下(GPIO引脚无输入),读取得的 GPIO 引脚数据为 1,高电平 | 与 VDD 相连的为上拉电阻。再连接到施密特触发器就把电压信号转化为1的数字信号存储在输入数据寄存器( IDR ) |
下拉输入模式 | 在默认状态下( GPIO 引脚无输入),读取得的 GPIO 引脚数据为 0,低电平 | 与 VSS 相连的为下拉电阻。再连接到施密特触发器就把电压信号转化为 0 的数字信号存储在输入数据寄存器( IDR ) |
浮空输入模式 | 配置成这个模式直接用电压表测量其引脚电压为1点几伏,这是个不确定值 | 没有接上拉,也没有接下拉电阻,经由触发器输入 |
模拟输入模式 | 把电压信号传送到片上外设模块,如传送至给 ADC 模块,由 ADC 采集电压信号 | 关闭了施密特触发器,不接上、下拉电阻 |
输出模式名称 | 作用 | 原理 |
---|---|---|
普通推挽输出 | 推挽输出的供电平为 0 伏,高电平为 3.3 pp*伏 | 在输出高电平时,P-MOS 导通,低电平时,N-MOS 管导通。两个管子轮流导通,一个负责灌电流,一个负责拉电流,使其负载能力和开关速度都比普通的方式有很大的提高 |
普通开漏输出 | 控制输出为 0,低电平;若控制输出为 1,为高阻态 | 如果我们控制输出为 0,低电平,则使 N-MOS 管导通,使输出接地,若控制输出为1 (无法直接输出高电平),则既不输出高电平,也不输出低电平,为高阻态 |
复用推挽输出 | GPIO 的引脚用作串口的输出 | |
复用开漏输出 | 用在 IC、SMBUS 这些需要线与功能的复用场合 |
注意:在使用任何一种开漏模式,都需要接上拉电阻
最大速率设置
GPIO 的输出速率:GPIO 电平每秒切换的最大次数
这个输出速率主要体现 I/O 驱动电路的输出反应能力,通过选择不同的输出驱动速率,实现最佳的噪声与和功耗控制。不难理解,选择输出驱动速率越高,噪声也越大,相应的芯片功耗也会越大
当 STM32 的 GPIO 端口设置为输出模式时,有三种速度可以选择:2MHz、10MHz 和 50MHz,这个速度是指 I/O 口驱动电路的速度,是用来选择不同的输出驱动模块,达到最佳的噪声控制和降低功耗的目的。
对于 STM32 GPIO 输出速率的选择问题,我们在开发应用中应多加注意。如果因为这个输出速率选择导致麻烦,原因往往比较隐晦,很难直接从代码语句或程序逻辑上找到突破。
高频的驱动电路,噪声也高,当你不需要高的输出频率时,请选用低频驱动电路,这样非常有利于提高系统的EMI性能。
当然如果你要输出较高频率的信号,但却选用了较低频率的驱动模块,你很可能会得到失真的输出信号。实际上芯片内部在I/O口的输出部分安排了多个响应速度不同的输出驱动电路,用户可以根据自己的需要选择合适的驱动电路。在满足实际应用需求的前提下,速率就低不就高,这对降低功耗、减少噪声、改善 EMI 都有好处
gpio 不同速率设置对实际开发的影响:
1、LED 闪烁快慢不一致
2、Audio 噪声
其中有 I2S 的音频播放功能。在调试时用到 Printf 串口打印,发现使用 printf 输出时会出现噪音,如果关闭 printf 则正常。直到将 UART 的 TX 输出端口的管脚输出速率由 very high 改为 Low 后噪声消失。
3、SPI 通信异常
用到 SPI 通信。STM32 做主,其它外围器件做从,有时发现 SPI 读取数据总是出错。对于这里的通信出错,如果 SPI 通信端口脚的输出速率选择跟实际通信速率不合适的话也会出现。相比实际速率需求,过高或过低了都会导致通信出错。
注意:GPIO 的引脚速度是指 I/O 口驱动电路的响应速度而不是输出信号的速度,输出信号的速度与你的程序有关
代码示例
/* GPIO_InitTypeDef结构体 */
typedef enum
{
GPIO_Speed_10MHz = 1, //枚举常量,值为 1,代表输出速率最高为 10MHz
GPIO_Speed_2MHz, //对不赋值的枚举变量,自动加 1,此常量值为 2
GPIO_Speed_50MHz //常量值为 3
} GPIOSpeed_TypeDef;
typedef enum
{
GPIO_Mode_AIN = 0x0, //模拟输入模式
GPIO_Mode_IN_FLOATING = 0x04, //浮空输入模式
GPIO_Mode_IPD = 0x28, //下拉输入模式
GPIO_Mode_IPU = 0x48, //上拉输入模式
GPIO_Mode_Out_OD = 0x14, //开漏输出模式
GPIO_Mode_Out_PP = 0x10, //通用推挽输出模式
GPIO_Mode_AF_OD = 0x1C, //复用功能开漏输出
GPIO_Mode_AF_PP = 0x18 //复用功能推挽输出
} GPIOMode_TypeDef;
typedef struct
{
uint16_t GPIO_Pin; /* 指定要配置的引脚 */
GPIOSpeed_TypeDef GPIO_Speed; /* 指定GPIO引脚输出的最高频率 */
GPIOMode_TypeDef GPIO_Mode; /* 指定GPIO引脚工作状态 */
} GPIO_InitTypeDef;
/* 初始化GPIO -- GPIO_Init() */
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;
uint32_t tmpreg = 0x00, pinmask = 0x00;
/* 断言,用于检查输入的参数是否正确 */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));
assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));
/*---------------------------- GPIO 的模式配置 -----------------------*/
/*把输入参数 GPIO_Mode 的低四位暂存在 currentmode*/
currentmode = ((uint32_t)GPIO_InitStruct -
> GPIO_Mode) & ((uint32_t)0x0F);
/*判断是否为输出模式,输出模式,可输入参数中输出模式的 bit4 位都是 1*/
if ((((uint32_t)GPIO_InitStruct -
> GPIO_Mode) & ((uint32_t)0x10)) != 0x00)
{
/* 检查输入参数 */
assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));
/* 输出模式,所以要配置 GPIO 的速率:00(输入模式) 01(10MHz) 10(2MHz) 11 */
currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
}
/*----------------------------配置 GPIO 的 CRL 寄存器 -----------------------
-*/
/* 判断要配置的是否为 pin0 ~~ pin7 */
if (((uint32_t)GPIO_InitStruct -
> GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)
{
/*备份原 CRL 寄存器的值*/
tmpreg = GPIOx->CRL;
/*循环,一个循环设置一个寄存器位*/
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
/*pos 的值为 1 左移 pinpos 位*/
pos = ((uint32_t)0x01) << pinpos;
/* 令 pos 与输入参数 GPIO_PIN 作位与运算,为下面的判断作准备 */
currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
/*判断,若 currentpin=pos,说明 GPIO_PIN 参数中含的第 pos 个引脚需要配置*/
if (currentpin == pos)
{
/*pos 的值左移两位(乘以 4),因为寄存器中 4 个寄存器位配置一个引脚*/
pos = pinpos << 2;
/*以下两个句子,把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变*/
pinmask = ((uint32_t)0x0F) << pos;
tmpreg &= ~pinmask;
/* 向寄存器写入将要配置的引脚的模式 */
tmpreg |= (currentmode << pos);
/* 复位 GPIO 引脚的输入输出默认值*/
/*判断是否为下拉输入模式*/
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
/*下拉输入模式,引脚默认置 0,对 BRR 寄存器写 1 可对引脚置 0*/
GPIOx->BRR = (((uint32_t)0x01) << pinpos);
}
else
{
/*判断是否为上拉输入模式*/
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
/*上拉输入模式,引脚默认值为 1,对 BSRR 寄存器写 1 可对引脚置 1*/
GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
}
}
}
}
/*把前面处理后的暂存值写入到 CRL 寄存器之中*/
GPIOx->CRL = tmpreg;
}
/*---------------------------- 以下部分是对 CRH 寄存器配置的 -----------------
--------当要配置的引脚为 pin8 ~~ pin15 的时候,配置 CRH 寄存器, -----
------------- -----这过程和配置 CRL 寄存器类似------------------------------
------
-------读者可自行分析,看看自己是否了解了上述过程--^_^-----------*/
/* Configure the eight high port pins */
if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
{
tmpreg = GPIOx->CRH;
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
pos = (((uint32_t)0x01) << (pinpos + 0x08));
/* Get the port pins position */
currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
if (currentpin == pos)
{
pos = pinpos << 2;
/* Clear the corresponding high control register bits */
pinmask = ((uint32_t)0x0F) << pos;
tmpreg &= ~pinmask;
/* Write the mode configuration in the corresponding bits */
tmpreg |= (currentmode << pos);
/* Reset the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
/* Set the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
}
}
GPIOx->CRH = tmpreg;
}
}
参考资料:
6.GPIO代码详解