STM32学习笔记(2)-点亮LED(分析GPIO口寄存器)
仅供个人学习,记录学习STM32中的要点,加深对知识的巩固
知识回顾
上一节创建了一个新的工程,那么我们在上一节的基础上通过对寄存器的控制来实现对LED的亮灭。
笔者所使用STM32开发板控制LED灯为高电平点亮,低电平熄灭。原理如下图所示:
芯片上所对应的管脚(PA1-PA4):
话不多说,这里直接上干货:
GPIO口寄存器
GPIO口寄存器是STM32微控制器中与GPIO口相关的寄存器,它们用于控制和配置GPIO口的输入和输出。不同的GPIO口寄存器有不同的功能,以下是一些常用GPIO口寄存器的功能介绍:
-
GPIOx_CRH和GPIOx_CRL寄存器:这两个寄存器用于控制GPIO口的输入/输出模式、输出速率、上拉/下拉电阻和复用功能等。例如,可以使用GPIOx_CRH和GPIOx_CRL寄存器将GPIO口配置为推挽输出、开漏输出、浮空输入或带上拉/下拉电阻的输入。
-
GPIOx_IDR寄存器:这个寄存器用于读取GPIO口的输入值。例如,可以使用GPIOx_IDR寄存器读取GPIO口的输入电平。
-
GPIOx_ODR寄存器:这个寄存器用于设置GPIO口的输出值。例如,可以使用GPIOx_ODR寄存器将GPIO口的输出电平设置为高电平或低电平。
-
GPIOx_BSRR寄存器:这个寄存器用于设置GPIO口的输出值,同时还可以通过它将GPIO口的输出值翻转。例如,可以使用GPIOx_BSRR寄存器将GPIO口的输出电平设置为高电平、低电平或翻转输出电平。
-
GPIOx_BRR寄存器:这个寄存器用于将GPIO口的输出值设置为低电平。例如,可以使用GPIOx_BRR寄存器将GPIO口的输出电平设置为低电平。
-
GPIOx_LCKR寄存器:这个寄存器用于锁定GPIO口的配置,防止意外更改。例如,可以使用GPIOx_LCKR寄存器锁定GPIO口的配置,以防止在运行时意外更改GPIO口的配置。
这里主要用的的是GPIOx_CRH和GPIOx_CRL寄存器。
-
GPIOx_CRH寄存器:用于控制GPIO口的高8位引脚的配置。每个引脚对应四位,共8个引脚,可以控制引脚的输入/输出模式、输出速率、上拉/下拉电阻和复用功能等。(PA8-PA15)
-
GPIOx_CRL寄存器:用于控制GPIO口的低8位引脚的配置。每个引脚对应四位,共8个引脚,可以控制引脚的输入/输出模式、输出速率、上拉/下拉电阻和复用功能等。(PA0-PA7)
GPIOx_CRH和GPIOx_CRL寄存器的各个位的功能如下:
-
输入/输出模式位(MODEy):用于设置GPIO口引脚的输入/输出模式,包括输入模式、输出模式、复用输入模式和复用输出模式。MODEy位共2位,分别控制引脚y和引脚y+1的输入/输出模式。
-
输出速率位(CNFy):用于设置GPIO口引脚的输出速率,包括通用推挽输出、开漏输出、复用推挽输出和复用开漏输出等。CNFy位共2位,分别控制引脚y和引脚y+1的输出速率。
-
上拉/下拉电阻位(CNFy):用于设置GPIO口引脚的上拉/下拉电阻,包括上拉电阻、下拉电阻和无上拉/下拉电阻等。CNFy位共2位,分别控制引脚y和引脚y+1的上拉/下拉电阻。
-
复用功能位(CNFy):用于设置GPIO口引脚的复用功能,例如选择复用为定时器输出或串口通信等。CNFy位共2位,分别控制引脚y和引脚y+1的复用功能。
RCC寄存器
RCC 寄存器是 STM32 微控制器中用于配置和控制系统时钟的寄存器。这些寄存器包括:
-
RCC_CR (RCC Control Register):用于控制内部时钟和外部时钟源的开关,以及设置系统时钟源。
-
RCC_CFGR (RCC Configuration Register):用于配置系统时钟源的分频和倍频因子,以及外部时钟源的类型和分频因子。
-
RCC_CIR (RCC Clock Interrupt Register):用于管理时钟中断,包括内部和外部时钟源的错误检测和中断。
-
RCC_APB2RSTR (APB2 Peripheral Reset Register):用于重置 APB2 总线上的外设。
-
RCC_APB1RSTR (APB1 Peripheral Reset Register):用于重置 APB1 总线上的外设。
-
RCC_AHBENR (AHB Peripheral Clock Enable Register):用于控制 AHB 总线上的外设时钟开关。
-
RCC_APB2ENR (APB2 Peripheral Clock Enable Register):用于控制 APB2 总线上的外设时钟开关。
-
RCC_APB1ENR (APB1 Peripheral Clock Enable Register):用于控制 APB1 总线上的外设时钟开关。
-
RCC_BDCR (Backup Domain Control Register):用于控制备份域的开关和配置。
-
RCC_CSR (Control and Status Register):用于配置系统复位和低功耗模式等。
本小节主要用到的寄存器主要有以下三个:
第一个寄存器-GPIOA_CRL寄存器
由于我们控制的端口为PA1-PA4,所以我们主需要注意端口配置低寄存器,即GPIOA_CRL寄存器。如图中红色数字1-4所示。
本质上就是控制GPIOA_CRL寄存器的第4到第19位。
针对PA1而言,即第四位到第七位,要输出的话,将MODEy[1:0]设置为输出模式。这里我们选择11,最大速度50MHz,在输出模式下,点亮LED可以选择通用推挽输出模式,所以为00。
最后,PA1的设置为0011。PA2-PA4设置同理,也为0011。如下图。
我们的地址由基地址和偏移地址构成。我们从之前的图可以看出,GPIOA_CRL寄存器的偏移地址为0x00,那么他的基地址可以再STM32的芯片手册memory map中去查看。
STM32是32位的处理器,他的空间大小为.即最大值0xFFFF FFFF。 我们可以找到
所以,当前GPIOA_CRL的寄存器地址为
那么我们之后编程就会往这个地址中去写第四到第十九位0011,就找到了LED地址。
第二个寄存器-GPIOA_ODR寄存器-
往这个寄存器的1到4位写1输入高电平,写0输入低电平。
所以,当前GPIOA_ODR的寄存器地址为
第三个寄存器-RCC寄存器
我们需要用到哪个外设,就将哪个外设的时钟打开。
我们通过CPU通过地址去找外设,而地址又挂载在总线上,CPU通过管理总线来管理外设。 在STM32微控制器中,APB1和APB2是两种不同的外设总线,用于连接不同类型的外设。具体来说:
-
APB1 (Advanced Peripheral Bus 1) 是用于连接低速外设的总线,包括I2C、SPI、USART、USB等外设。APB1总线的时钟频率通常为APB2总线时钟频率的一半,最高可达42MHz。
-
APB2 (Advanced Peripheral Bus 2) 是用于连接高速外设的总线,包括ADC、TIM、GPIO、EXTI等外设。APB2总线的时钟频率通常与系统时钟频率相同,最高可达72MHz。
需要注意的是,外设总线的时钟频率是由RCC寄存器中的时钟分频因子配置所决定的。具体地说,RCC_CFGR寄存器中的APB1和APB2分频因子可以分别配置APB1和APB2总线时钟频率的倍频因子,以便满足不同外设的时钟要求。例如,如果将APB1分频因子设置为2,而APB2分频因子设置为1,则APB1总线的时钟频率将为系统时钟频率的1/2,而APB2总线的时钟频率将等于系统时钟频率。
上图可以看出我们的GPIOA挂载在ABP2总线上。
可以看出ABP2默认值位0.所以找到其寄存器的第二位,将其置为一。
基地址
所以它的地址为0x40021000 + 0x18 = 0x40021018,将其第二位置一. 就可以使能该GPIO的时钟。
写代码
先看代码,逐一解释。
代码解释:
*(unsigned long *)0x40021018 |= (1<<2);
解释:这行代码就可以使能该GPIO的时钟,将其第二位置一。
(unsigned long *)0x40010800
解释:地址是没有方向的,所以使用unsigned long或者unsigned int。 0x40010800是GPIOA寄存器的基地址。使用(unsigned long *)0x40010800可以将GPIOA寄存器的基地址转换为一个指针,然后可以使用指针访问GPIOA寄存器的不同位。
*(unsigned long *)0x40010800
括号内的* 是强制类型转换,括号外的 *是获取地址的值。
首先要将值清零。清零就是&操作(与操作).置1位(|)或操作
*(unsigned long *)0x40010800 &= 0xFFF0000F;
解释:具体地说,(unsigned long * )0x40010800 是将GPIOA寄存器的基地址转换为一个指针,* 是用于指针解引用的运算符,使得该表达式访问了GPIOA寄存器。然后,&=是按位与并赋值运算符,将GPIOA寄存器的指定位与给定的掩码0xFFF0000F进行按位与运算,将结果赋值回GPIOA寄存器。通过这个操作,GPIOA寄存器的第4到19位被设置为0,而其他位不变。
*(unsigned long *)0x40010800 &= 0xFFF0000F;//清除4-19位
*(unsigned long *)0x40010800 |= 0x00033330;//将PA1-PA4配置为通用推挽输出,工作频率为50MHz[19:4] = 0000 0000 0000 0011 0011 0011 0011 0000
然后控制LED输出低电平。
//GPIO_ODR = 0x40010800 + 0x0c = 0x4001080C
*(unsigned long *)0x4001080C &= ~((1<<1) | (1<<2) | (1<<3) | (1<<4));
解释:用于设置 GPIOA 端口的输入/输出模式。具体来说,这个表达式的作用是将 GPIOA 端口的 1 到 4 位设置为输出模式,保留其他位的状态。
PA1-PA4循环输出高,低电平:
while(1)
{
//点亮PA1-PA4.输出高电平
*(unsigned long *)0x4001080C |= (1<<1) | (1<<2) | (1<<3) | (1<<4);
Delay(0xffff);
//熄灭PA1-PA4.输出低电平
*(unsigned long *)0x4001080C &= ~((1<<1) | (1<<2) | (1<<3) | (1<<4));
Delay(0xffff);
}
void Delay(unsigned long nCount)
{
while(nCount--)
{
}
}
改进
有时候看见这些数字,可读性较差。所以
需要宏定义
#define RCCAPB2ENR (*(volatile unsigned long *)0x40021018)
解释:具体来说,0x40021018 是 RCCAPB2ENR 寄存器的物理地址,volatile unsigned long * 将该地址强制转换为指向无符号长整型的指针,而 * 运算符用于解引用指针并访问该地址处的数据。因为该宏使用了 volatile 关键字,因此编译器不能将其优化或缓存,以确保每次访问都能够获得最新的寄存器值。
RCCAPB2ENR |= (1<<2);
解释:在实际应用中,可以通过修改 RCCAPB2ENR 寄存器的位来控制特定外设的时钟开关。例如,将 RCCAPB2ENR 寄存器的第 2 位设置为 1,可以启用 GPIOA 端口的时钟。使用以上代码将 RCCAPB2ENR 寄存器的第 2 位设置为 1。
所以同样的,整体代码改进为:
main.c
#define RCCAPB2ENR (*(volatile unsigned long *)0x40021018)
#define GPIOACRL (*(volatile unsigned long *)0x40010800)
#define GPIOAODR (*(volatile unsigned long *)0x4001080C)
void Delay(unsigned long nCount);
int main(void)
{
//RCC_APB2ENR= 0x40021000 + 0x18 = 0x40021018
//RCC_APB2ENR bit[2] = 1 使能GPIOA外设时钟
//*(unsigned long *)0x40021018 |= (1<<2);
RCCAPB2ENR |= (1<<2);
//*(unsigned long *)0x40010800 &= 0xFFF0000F;//GPIOA_CRL = 0x40010800 + 0x00 ,清除[19:4]
GPIOACRL &= 0xFFF0000F;
//*(unsigned long *)0x40010800 |= 0x00033330;//[19:4] = 0011 0011 0011 011 将PA1~PA4配置为通用推完输出,工作频率50MHz
GPIOACRL |= 0x00033330;
//GPIO_ODR = 0x40010800 + 0x0C = 0x4001080C
//*(unsigned long *)0x4001080C &= ~((1<<1)|(1<<2)|(1<<3)|(1<<4)); //PA1~PA4输出低电平,LED全灭
GPIOAODR &= ~((1<<1)|(1<<2)|(1<<3)|(1<<4));
while(1)
{
//点亮LED1~LED4 PA1~PA4输出高电平
//*(unsigned long *)0x4001080C |= (1<<1)|(1<<2)|(1<<3)|(1<<4);
GPIOAODR |= (1<<1)|(1<<2)|(1<<3)|(1<<4);
Delay(0xffff);
//熄灭LED1~LED4 PA1~PA4输出低电平
//*(unsigned long *)0x4001080C &= ~((1<<1)|(1<<2)|(1<<3)|(1<<4)); //PA1~PA4输出低电平,LED全灭
GPIOAODR &= ~((1<<1)|(1<<2)|(1<<3)|(1<<4));
Delay(0xffff);
}
}
void Delay(unsigned long nCount)
{
while(nCount--)
{
}
}
}
}
需要注意的是,在进行寄存器操作时,需要事先了解寄存器的功能和用法,并仔细检查代码逻辑和正确性,以确保操作的正确性和有效性。同时,在使用寄存器进行操作时,推荐使用宏定义或函数封装等方式,以提高代码的可读性和可移植性。