STM32的芯片架构
以STM32F103ZET6为例
STM32芯片主要由内核和片上外设组成,其中内核是由ARM公司设计的,例如Cortex-M3内核,内核和外设的关系就好比电脑的处理器与显卡,内存,硬盘关系,常用的如GPIO,USART,IIC,ADC,DAC等都属于片上外设,这些片上外设都是由ST公司设计的,ARM公司只负责对芯片技术进行授权。
详细图:
存储器映射与寄存器映射
芯片上各种功能部件被排列在一个4GB的空间中,这个4GB被分成8个区域,也就是被分成8个存储器,每个区域各有512kb的存储空间,这些区域被分成代码区,SRAM区,外设区等等
每一个存储器本身并没有地址,给每个存储器分配地址的过程叫做存储器映射,这是由生产厂商分配的,而给存储器重新分配地址叫做存储器的重映射
每一个存储器里面由各种外设各种寄存器,给寄存器分配地址的过程叫做寄存器映射,重新分配地址叫做寄存器的重映射
简单做了个图帮助理解:
总线基地址
片上外设区有三条总线,根据速度不同分位APB1、APB2、AHB总线,总线的最低地址就是总线的基地址,同时也是挂载在该总线上的首个外设的地址,由于APB1总线的地址最低,各种外设的地址从这里开始,所以APB1的地址也叫外设基地址
总线名称 | 总线基地址 | 相对外设基地址的偏移 |
APB1 | 0x4000 0000 | 0x0 |
APB2 | 0x4001 0000 | 0x0001 0000 |
AHB | 0x4001 8000 | 0x0001 8000 |
外设基地址
总线上有各种外设,每个外设也有自己的地址范围,前面的图也有体现,外设的首个地址也叫外设基地址,这里以GPIO为例
外设名称 | 外设基地址 | 相对 APB2 总线的地址偏移 |
GPIOA | 0x4001 0800 | 0x0000 0800 |
GPIOB | 0x4001 0C00 | 0x0000 0C00 |
GPIOC | 0x4001 1000 | 0x0000 1000 |
GPIOD | 0x4001 1400 | 0x0000 1400 |
GPIOE | 0x4001 1800 | 0x0000 1800 |
GPIOF | 0x4001 1C00 | 0x0000 1C00 |
GPIOG | 0x4001 2000 | 0x0000 2000 |
如何操作单片机上各种各样的寄存器?固件库开发的简介
首先这里总结一下,想要找到一个寄存器的步骤:
举个栗子,我想找到GPIOE上ODR寄存器的位置要怎么办呢?
- 打开STM32参考手册,找到寄存器所在总线
比如这里找到的GPIOE的外设地址是 0x4001 1800 - 接着可以去找手册上对应GPIO外设上ODR寄存器的偏移地址
这里ODR寄存器的偏移地址是0x0C - 操作GPIOE的ODR寄存器的地址是
0x4001 1800+0x0C=0x4001 180C
知道地址后要怎么操作呢?
学过C语言我们知道,指针就是来操作地址的那么是否可以用将得到的数值强制转化成指针再进而操作对应地址的寄存器呢?当然可以
首先要把得到的数值转化成地址,用C语言的强制转化操作
(unsigned int *)0x4001180C
这时候得到的就是才是寄存器的地址,得到地址后还不能操作,因为这只是个地址,还不是一个寄存器本身,要想操作地址对应的寄存器,还要再用一个指针的知识,在前面加个*号
*(unsigned int *)0x4001180C
这样得到的就是一个可以操作的寄存器,由于STM32是32位的,所以每个寄存器都是由32个bit组成,我们现在用这样一段代码给GPIOE ODR寄存器第6个位写入1
*(unsigned int *)0x4001180C |= ( 1 << 5 );
用位操作可以增强程序的可读性
但是每次都这样操作未免太过于麻烦,如果能将这么复杂的一大串数字用一个一段可读性强的字符串表示那不是更好?这时候宏定义的作用就体现出来了
#define GPIOE_ODR *(unsigned int *)0x4001180C
GPIOE_ODR |= ( 1 << 5 );
用宏定义给原本复杂的寄存器地址一个可读的新名字,程序的可读性更强,一看就知道你操作的是什么寄存器,这一步类比51单片机,起到了sbit的作用,只不过这个地址名没有用类似reg51.h的头文件进行封装,虽然这样方便了很多,但是细心去看参考手册就知道,一个外设对应了可不止一个寄存器,例如GPIO这个外设就有ODR、IDR、CRH、CRL、BRR等等寄存器,每一次都这样定义岂不是很麻烦?有没有什么办法解决呢?这时候就要用到C语言中的结构体了,细心一点你就会发现,结构体上每两个变量相邻变量间的地址之差刚好是4,同时外设上每两个相邻的寄存器间的地址之差也是4,那么这时候只要在结构体中按顺序定义外设上各个寄存器,就可以用结构体来储存每个寄存器的配置了
typedef unsigned int uint32_t;
typedef struct //对于32位的单片机,结构体每一个成员的位置偏移量为4
{
uint32_t CRL;
uint32_t CRH;
uint32_t IDR;
uint32_t ODR;
uint32_t BSRR;
uint32_t BRR;
uint32_t LCKR;
}GPIO_TypeDef;
得到这样一个结构体后我们知道当你新建一个结构体变量,其地址是随机分配的,如果要将其指向一个特殊地址,比如指向GPIOE的外设基地址,就得用宏定义进行强制转化,但是在这之前我们可以定义一下外设基地址,总线基地址,以及特定外设的基地址,再通过相加减得到所需要的寄存器的地址,事实上ST官方的固件库就是这样做的
#define PERIPH_BASE ((unsigned int)0x40000000) //外设基地址
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) //APB2总线基地址
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800) //GPIOE基地址
#define GPIOE ((GPIO_TypeDef *)GPIOE_BASE) //把GPIOB_BASE 强制转换成 GPIO指针
这样假如你要操作GPIOE上的CRL寄存器就可以这样操作
GPIOE->CRL = xxxxx;
方便了许多
后面我们还可以再定义一个函数用于操作所需要操作的寄存器,这时候只需要填入参数就可以实现运用函数来操作配置各种寄存器或者各种外设的功能了,这也就是固件库实现的基本思想。
这是固件库操作GPIO外设的一个函数,可以简单参考一下
void GPIO_SetBits (GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) //GPIO_Pin 每一位对应为1 已经在头文件中声明
{
GPIOx->BSRR |= GPIO_Pin; //设置对应的ODRy位为1
}