本文为野火学习笔记。
储存器地址
由51引入
实际上,单片机对任何端口的控制都是通过操作内存实现的,说出这句话很抽象,我们可以以从51单片机类比来说明这是什么意思。
如下的电路图:
如果我们想点亮LED_G,在51中我们可能会如此编程
#include <reg51.h>
#define PB = 0xfe
或者
#include <reg51.h>
sbit LED_G = PB^5;
void main()
{
LED_G = 0;
}
其中第一种叫宏定义,第二种叫位定义。那么为什么我们使用这些操作后PB0就输出低电平了呢。我们可以从<reg51.h>这个文件中看出原因。
可以看见,在头文件中对类似P0的IO口进行了宏定义,而定义的就是其对应内存地址,所以说单片机对任何端口的控制都是通过操作内存实现的。
那么在stm32中应该如何操作呢?
STM32的储存器映射
STM32把内存分配出许多块,每块对应不同的功能。这里要点亮LED当然是要使用输入输出IO端口了,在STM32中这些IO叫GPIO。实际和51的P1,P2,P3的IO一样,只是功能更强些。这里以GPIO来介绍。
从STM32的参考手册储存器映像我们可以找的上图,我们可以看见每个GPIO对应的地址区,想要配置这些GPIO让对应的端口输出低电平,我们必须去查看GPIO的寄存器说明,里面写明了这些端口的功能和对应配置方式。下面以CRL为例。
这里我们可以看见,这个寄存器或者说这个地址可以设置GPIO的是输入状态还是输出状态,不同状态的特殊模式和速度等。同理,我们可以发现ODR可以直接设置输出的是1还是0;在每个寄存器下写的寄存器偏移就是这个寄存器相对基地址的偏移量;
如GPIOB_ODR就相对于GPIOB的基地址高0x0c;GPIOB的基地址0x40010c00,GPIOB_ODR寄存器的地址就是0x40010c00+0x0c=0x40010c0c;
所以由此我们可以编出这样直接的操作地址的代码
对寄存器地址的操作
直接的地址操作
int main(void)
{
//打开时钟
*(unsigned int*)0x40021018 |= (1<<3);
//配置IO口为输出,强推挽
*(unsigned int*)0x40010c00 &= ~(0x0f<<(4*0));//清零,先把这四位清零再赋值,可确保其准确
*(unsigned int*)0x40010c00 |= (1<<(4*0));
//控制ODR寄存器输出0
*(unsigned int*)0x40010c0c &=~(1<<0);
}
对设置强推挽做出解释 :其中0x40010c00就是GPIOB_CRL寄存器的地址
,但直接写出这个数会被编译器认为是立即数而非地址,所以要先用 (usigned int*)
转换为指针,再用 (*)
取值,把立即数真正转化为一个地址,最后用(|=)操作赋值,设置最低的8位的最低位为1。
可以从张地址和实际储存位的关系中更好理解上面代码操作的意义。
通过宏定义增强可读性
上面的代码确实可以实现效果,但可读性太差了。我们想到用宏定义来增强可读性;
#define PERI_BASE (0x40000000) //外设的起始地址
#define APB1PERI_BASE PERI_BASE //APB1总线起始地址
#define APB2PERI_BASE (PERI_BASE+0x10000)
#define AHBPERI_BASE (PERI_BASE+0x20000)
#define RCC_BASE (AHBPERI_BASE+0X1000)
#define GPIOB_BASE (APB2PERI_BASE+0X0c00)
//注意强制转换和取值
#define RCC_APB2ENR *(unsigned int*)(RCC_BASE+0x18) //时钟地址
#define GPIOB_CRL *(unsigned int*)(GPIOB_BASE+0X00)
#define GPIOB_CRH *(unsigned int*)(GPIOB_BASE+0X04)
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0X0C)
int main()
{
RCC_APB2ENR |= (1<<3);
//配置CRL为输出,强推挽
GPIOB_CRL &= ~(0x0f<<(4*0));//先清零,保证赋值效果
GPIOB_CRL |= (1<<(4*0));
//控制ODR寄存器输出0
GPIOB_ODR &= ~(1<<0);
}
巧用结构体
上面我们增强了可读性,但要一个一个定义所有的寄存器太麻烦了。有什么好办法呢?我可以发现,这些寄存器的地址是连续的,而结构体中定义的成员变量地址也是连续的,只要把结构体的地址指向对应外设的首地址,变量的地址就自动按照顺序对应排好了,而且有了结构体,定义不同的结构体变量就可以对应不同的首地址,从而实现了简便的目的。
#define PERI_BASE (0x40000000)
#define APB1PERI_BASE PERI_BASE
#define APB2PERI_BASE (PERI_BASE+0x10000)
#define AHBPERI_BASE (PERI_BASE+0x20000)
#define RCC_BASE (AHBPERI_BASE+0X1000)
#define GPIOB_BASE (APB2PERI_BASE+0X0c00)
typedef struct
{
uint32_t CRL;
uint32_t CRH;
uint32_t IDR;
uint32_t ODR;
uint32_t BSRR;
uint32_t BRR;
uint32_t LCKR;
}GPIO_TypeDef;
#define GPIOB ((GPIO_TypeDef*)GPIOB_BASE)//
int main()
{
RCC_APB2ENR |= (1<<3);
//配置CRL为输出,强推挽
GPIOB->CRL &= ~(0x0f<<(4*0));//先清零,保证赋值效果
GPIOB->CRL |= (1<<(4*0));
//控制ODR寄存器输出0
GPIOB->ODR &=~(1<<0);
}
进入固件库编程
用结构体定义后我们仍不满足,因为这样我们仍不知道代码对应的是什么效果。我们想要一些函数。这些函数可以通过传入参数帮我们初始化GPIO,帮我们置位,可读性更强。拿这就进入结构体编程了。
形似 stm32f10x_xx.c 的C文件里写好了对应外设的一些操作函数。而几乎所有的外设都有一个初始化函数,在这个初始化函数里传入对应参数就可以初始化这个端口。
想要了解一个 .c 文件里有那些函数,最好就是去对应 .h 文件的末尾查看。
打开GPIO对应的h文件,翻到最底下,我们可以看见GPIO_Init()的函数。在他的函数名上按F12或右击->go to def……
这里可以看见C文件里的注释已经为我们写好了这个函数的各项要求;
@brief 中写的是这个函数的简介
@param 中告知了这个函数参数的选取,往往里面会列出可用的参数
@retval 中会写明有什么返回值
根据GPIO_InitStruct中的参数初始化GPIO
参数:GPIOx: 其中x可为 (A…G)
参数:GPIO_InitStruct
我们对GPIO_InitStruct这个参数不太了解,继续F12
这里我们可以看见,这是个结构体,里面定义了要使用的端口,速度,模式。再往上翻我们可以看见,官方已经用枚举把这些量的可取值定义好。
可见固件库已经为我们安排好一且,以后每次实现功能只需去手册找到对应的端口和寄存器功能,再去h文件里找到对应的函数实现就好,几乎所有的参数都已经被固件库配置完成。
展示一下固件库编程实现的LED点亮
#define LED_G_GPIO_PIN GPIO_Pin_0
#define LED_G_GPIO_RORT GPIOB
#define LED_G_GPIO_CLK RCC_APB2Periph_GPIOB
//由于涉及到大量硬件,定义宏来增强可移植性
void LED_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
//打开时钟
RCC_APB2PeriphClockCmd(LED_G_GPIO_CLK, ENABLE);
//绿灯配置
GPIO_InitStruct.GPIO_Pin = LED_G_GPIO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
//对LED初始化
GPIO_Init(LED_G_GPIO_RORT,&GPIO_InitStruct);
//输出0
GPIO_ResetBits(LED_G_GPIO_RORT,LED_G_GPIO_PIN);
}
int main(void)
{
LED_GPIO_Config();
}