故事很多,我打算用一辈子来跟你讲,你准备好了吗?(狗头
没必要一次看完,多看几遍消化消化。
所有操作,最终目的都是操作寄存器
stm32编程实质上是修改寄存器的32位的具体的值
单片机
sfr P0=0x80; //关键字sfr 声明地址和名称的映射
P0=0x00; //将0x00赋值给P0口的8位(51单片机一组IO为8位)
STM32
GPIOA->ODR=0x00000000 //为GPIOA的ODR寄存器地址赋值0x00000000
什么是寄存器?
寄存器是中央处理器内的组成部分,它可以从其他寄存器,或者内存中获取指令,地址,数据然后给CPU用,有时候 由于内存太慢,内存和寄存器之间要加一个叫Cache的东西,为了弥补内存的不足,内存-》Cache-》寄存器-》CPU。
指令、地址寄存器与数据寄存器都类似,里边存放的都是0和1,毕竟单片机也只认识机器码,机器码都是0或1,只是特别的规定下,数据寄存器里面存放的0和1表示数据,指令寄存器里存放的表示指令。
既然有不同的寄存器,那CPU怎么去区分这些寄存器?
---通过地址,STM32给不同的寄存器分配了不同的地址。
怎么找到某个寄存器的地址?---查看数据手册
补充:32位cpu代表了什么,stm32也是32位
代表的是数据总线32位,也就是说cpu一次可以处理32位数据,而上面我们说了,cpu要从寄存器取数据,所以它寄存器也是32位的,所以32位代表什么?代表它的寄存器是32位的。
敲黑板:stm32的寄存器是32位的,都是存储8位16进制的数或者32位2进制的01数字。
什么是地址映射?
所谓地址映射,就是将芯片上的寄存器 甚至I/O等资源与地址建立一一对应的关系。
如果某地址对应着某寄存器,我们就可以运用C语言的指针来寻址并修改这个地址上的内容,从而实现修改该寄存器的内容。
所以我们能操作IO资源(如引脚)的根本原因是,这些引脚由于地址映射与地址建立了对应关系,而 部分地址 又和 寄存器建立了对应关系,所以,我们说操作某些资源就是操作某些寄存器应该不难理解了
IO资源---地址---寄存器
正是因为头文件中有了对于各种 寄存器 和 I/O端口 的地址映射,我们才可以在51单片机程序中方便地使用P2^0 =0xFF; TMOD =0xFF等赋值句子对寄存器进行配置,从而控制单片机。
我们可以看出地址才最重要的纽带,那么我们怎么去访问地址,能访问多少?怎么抽象出地址这一概念的?是不是得有一些东西支撑?这个东西就是地址总线。cpu通过地址总线访问地址,我们操作地址当然也是通过地址总线啦,因为我们操作着CPU嘛(滑稽),假如一个芯片有32根地址总线,每根线有两种状态(0/1)所以32根能表示2^32个状态,一个状态代表一个地址,所以这一共有4G的地址能表示(访问)。
2^10 = 1024 = 1Kb
2^30 = 1024x1024x1024= 1024*(1024*1kb) = 1024Mb = 1G
2^32 = 1G x 4= 4G
APB2总线就是指特定的一段地址,如:0x4001 0000—XXXX
然后官方书上有段话,大致意思:你能理解这句话的深刻含义吗?
程序存储器、数据存储器、寄存器和输入输出端口 被 组织在同一个4GB的线性地址空间内。
说到映射大家可能就会想到函数映射,脑海里会有一个画面:左边一个集合中的某个元素“射”出一条带箭头的直线指向右边的集合的某个元素。如果你高数或离散数学再好一点儿的话会想到单射、双射、满射、恒等映射(你要是这个都想到了,那你牛B!)……其实外围设备的内存映射原理是一样的,只不过左边的集体变成了CPU,右边的集合变成了外围设备,那条带箭头的线就是连接CPU和外设地址引脚的地址总线。
学习stm32最困难的地方,就是理解功能对应的寄存器的逻辑关系,甚至一个简单的功能往往是多个寄存器的调用的结果,理解寄存器之间的关系,学会查看寄存器的值。
以GPIOA为例说明STM32寄存器和名称的映射
GPIOA下的某个寄存器,挂载在GPIOA下,地址为GPIOA基地址+偏移量
GPIOA挂载在APB2总线,地址为APB2总线基地址+GPIOA偏移量
ABP2挂载加外设基地址,地址为外设基地址+ABP2偏移量
源码中可以找到:
//外部总线基地址 8位16进行不刚好是32位2进制吗?---地址总线的作用别忘了
#define PERIPH_BASE ((uint32_t)0x40000000)
//APB2基地址=外部总线基地址+偏移量
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
//GPIOA基地址=APB2基地址+偏移量
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
//GPIOA将地址顺序分配给7个32位寄存器(结构体分配)
#define GPIOA ((GPIO_TypeDef*)GPIOA_BASE)
/将寄存器地址映射到7个32位寄存器,分别控制
typedef struct
{
IO unit32_t CRL;
IO unit32_t CRH;
IO unit32_t ODR;
IO unit32_t IDR;
IO unit32_t BSRR;
IO unit32_t BRR;
IO unit32_t LCKR;
}GPIO_TypeDef;
关于寄存器的都说了,怎么查寄存器的地址呢?
上面说了利用参考手册找,参考手册网盘链接在文末
假如你想读取PB3引脚的电平
第一步 找到GPIOB的基地址:(以后找就来这)
第二步找到端口输入寄存器的地址偏移(结论是0x4001 0C00+8 = 0x4001 0C08),地址偏移上面写着 8,PB3的数据位于从右往左数第4个 即IDR3
这张图片包含了很多信息,首先这两行表示的是一个寄存器的内部结构,上面一行告诉我们保留,就是放着不用,始终读为0,而下面就是0~15个引脚位置,最下面的r的意思是只读,不能写
总结:PB3的输入数据位于0x4001 0C08这个地址上,这个地址上所存放数据的右起第4个位就是PB3引脚 对应 的高低电平
我们可以简单粗暴地直接访问这个地址:
unsigned int *pGPIOB_IDR = (unsigned int *)0x40010C08;
unsigned char PB3 = *pGPIOB_IDR & 0x8;//取出从右往左数的第4位
直接访问的操作并不好用,每操作一个寄存器就必须去查看数据手册,然后找找这个寄存器的地址。
意法半导体公司为了方便大家使用,就把这些寄存器都起了一目了然的名字,把寄存器与地址映射关系放在他们提供的头文件里。这个文件就是stm32f10x.h。(我们上面地址映射那段讲到的知识又用到了,现在是读这段话是不是觉得理所当然,是不是知道自己再做什么?而不是无脑比着其他代码写)
直接操作寄存器来点亮LED。
我的板子对应的LED是PB8。首先要配置时钟使能。
为什么配置时钟?为了省电,默认的时钟都是关闭的。配置STM32的任何资源前,都必须首先使能时钟。配置哪个时钟?
时钟的信息在参考手册里边,参考手册十分巨大,不用通读,就像一个字典,需要什么查什么。
时钟控制名字叫做RCC,属于AHB总线。GPIOB属于APB2。
下图系统结构可以看到时钟的从属关系,此图位于手册P25页,十分重要。可以看出AHB总线包含RCC时钟控制,GPIO是属于APB2的。
我们已经知道,GPIO端口B的地址从0x4001 0C00开始。接下来只寻找时钟使能寄存器的地址:
复位和时钟控制RCC的地址从0x4002 1000开始。
可以在6.3.7小节找到APB2外设时钟使能寄存器(RCC_APB2ENR),偏移地址是0x18,所以APB2的地址就是0x4002 1018。
看手册RCC_APB2ENR,位3是IOPBEN---IO端口B时钟使能,就是我们想要的。把RCC_APB2ENR的位3赋值为1,就是开启GPIOB时钟。配置为通用输出
既然叫做IO,那么肯定就是可以输入,可以输出,到底是输入还是输出呢?
控制LED需要输出高电平或是低电平,所以需要配置为输出。
一个STM32的IO需要4个位来配置,所以一个32位的寄存器最大只能配置8个IO。STM32中,用端口配置低寄存器(GPIOx_CRL)来配置引脚Px0-Px7, 用端口配置高寄存器(GPIOx_CRH)来配置引脚Px8-Px15。
配置引脚PB8,使用的寄存器是GPIOB_CRH。下面我们来寻找这个寄存器的地址。
因为是PB8所以要配置对应的:CNF8和MODE8
关于此寄存器的说明位于8.2.2小节。先看标题GPIOx,表示PA,PB,PC直到PE都能用。
偏移地址是0x04,意思是在基地址的基础上再加0x04,所以,对于GPIOB来说就是0x4001 0c04。如果配置PB0-PB7,那么需要的寄存器是低位的寄存器GPIOB_CRL,它的地址是0x4001 0c00。我们需要配置的寄存器是GPIOB_CRH。
找到需要操作的寄存器后,把它配置为通用输出。
复位值是0x4444 4444,并不是0x0000 0000。所谓的复位值,就是指如果没有操作这个寄存器时,寄存器存放的默认值。复位值按位拆分0x4 = 0b0100,0x表示16进制,0b表示二进制,也就是默认CNF 01,MODE 00,是浮空输入。
我们需要的是输出高低电平,所以要设置为输出。输出模式又有好几种输出:
推挽输出:可以输出高,低电平,连接数字器件;推挽结构一般是指两个三极管分别受两互补信号的控制,总是在一个三极管导通的时候另一个截止。
开漏输出:输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行,适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内)。
开漏是需要外接上拉电阻才可以输出高电平的,这里并不适合。所以需要设置为推挽输出。
功能是否是复用呢?复用的意思是有别的功能在这个脚上,比如USB,CAN,串口等,所以这些个脚就可能有多个功能。暂时讲多了反而会迷惑,等用到了这些功能再讲解,我直接告诉大家,PB8没有复用。
所以配置为输出模式,通用推挽输出。速度暂时不关注,随便填写一个50MHz吧,其它速度当然也可以。所以设置GPIOB_CRH的MODE8与CNF8为0b0011,即0x3。此寄存器中其它的位暂时不做修改,使用默认值,也就是GPIOB_CRH设置为:0x4444 4443。点亮LED需要输出低电平
在单片机的编程中,要想做某件事,必须寻找相应的寄存器。
在8.2.4小节,可以找到端口输出数据寄存器(GPIOx_ODR),就是我们需要的。我们需要输出0。但是中文手册有一个小小的BUG,0x0C写成了0Ch,可以参考英文原版。得知地址的偏移是0x0C,所以这个数据寄存器的地址就是0x4001 0C0C,把第8位写为0就行。默认就是0,但是也得学一下怎么写,万一是高电平点亮呢。
使用直接赋值的方式写寄存器的地址
在搞清楚我们要用的几个寄存器的地址,以及寄存器中需要装填的数值以后,现在用一个简单粗暴的方法来操作这些寄存器——直接操作。(注意,这段代码不是实用的代码,只是为了写出一个最简单的LED,有些部分是不可取的。)将main函数修改为:
int main(void)
{
unsigned int *pRCC_APB2ENR = (unsigned int *)0x40021018;
unsigned int *pGPIOB_CRH = (unsigned int *)0x40010c04;
unsigned int *pGPIOB_ODR = (unsigned int *)0x40010c0c;
*pRCC_APB2ENR = 0x00000008;
*pGPIOB_CRH = 0x44444443;
*pGPIOB_ODR = 0x00000000;
return 0;
}
C语言总是从main函数开始执行。
定义几个指针,指向刚刚看到的地址。对于编译器来说,它并不知道0x40021018代表的是数据还是指针,所以用(unsigned int *)作强制的类型转换,告诉编译器0x40021018是个指针。指针可以理解为地址。操作指针,把这些地址存放的值修改。
我们写了一段另类的代码,直接操作寄存器的地址,就是想得到这么一个结论:不论代码怎么写,不论是寄存器,库函数,还是其他的操作系统,要在STM32F103这个单片机点亮LED灯,肯定需要把时钟和GPIO这几个相关的特殊地址,进行赋值或修改数值的操作。有点像打篮球,不论进攻时有怎样花哨的运球与传切配合,最后都要完成把球放入篮筐的动作,才能得分。
参考手册第10版: