控制GPIO的那些寄存器,都在位带区。
根据上一篇讲的原理,要想每次只操作这些寄存的某一个bit而不影响别的bit,可以使用与这些bit相对应的位带别名区。
因此,在使用GPIO的位带操作之前,先要按上篇讲的原理,先在软件上做好位带区的每一个bit位与位带别名区的关连。
下面这些代码就实现这个目的:
一行行来看,首先看:
一行一行来看,先看第一行:
这个是使用宏定义,来定义和传递参数。(可以看到在用宏定义传递形参时,并没有事先另外去声明形能addr和bitnum的数据类型,直接就这样定义并使用了!见识了!)
当编译器看到BITBAND(0x40000,3),它就会把传进来实参,传到后面的公式进行计算,从来得到位带区某一个bit在位带别名区的地址。
读代码时,直接把它看作已计算好的位带别名区的地址就行了。
第二行:
这一行是根据位带区的基地址,取出对应的值。
这个*((volatile unsigned long *)(addr)) 有两层,第一层是:
(volatile unsigned long *)(addr),
这是强制声明一个32位(unsigned long)的数据类型,这个数据类型是一个指针变量,这个变量保存的是一个32位的地址
volatile是一个关键字,其作用是:
volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。
如 果没有 volatile 关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。所以遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
第二是把第一层的内容是括号包起来,在外面又加了一个*,这表示取出这个地址里面的数据。简言之,就是取出那个地址里的byte。读代码时,直接把它看作是位带区对应地址里的值就可以了。
(为方便理解这两层,可以以*((int *) a)这样一个结构作作说明:
int a, 这是声明a是一个整数型变量。
int *a ,这表明a是指针变量,a里存放的是一个地址,而不是数据。这个时侯的a与int a完全不同的变量声明了,因为它是指针变量。
在int *a这里,不要把*作为取值理解,它仅仅是声明a是指针变量作用,不要把它当成运算符里的a
而*((int *) a),就是操作a所向的地址内的值,最外面的这个*,才是运算符,表示取这个指针变量指向地址里的值)
第三行:
这一行的作用,就是操作位带别名区地址里的值。
理解了前面的三行后,后面的就很好理解了!
比如,当我main.c中输入一个语句PAout(1)时,编译器会直接使用
来替代,并且把其中的n置换为1,
把GPIOA_ODR_Addr置换为:
也就是对应端口的ODR寄存器的地址。
所以,当我输入PAout(1) =1时,根据上面所说的第三行的作用,那我通过对位带别名区对应地址的操作,而更改到了ODR寄存器里数据的某一bit位,从而实现了对某一个具体的pin的输入输出的直接操作!
现在,把整个的逻辑再理一遍:
假定我要让GPIO 的A端口的第1个引脚输出一个高电压,那么,我首先要找到GPIO A端口的控制输出的寄存器ODR:
其以看到我只要使ODR0变为高电平,那么GPIO 的A端口的第1个引脚就输出一个高电压。
而GPIOA_ODR在哪里呢?
根据前面的memory mapping也就是地址总图:
我知道A端口的所有寄存器的基地址是0x4001 0800,这就是GPIOA_BASE。
在这个基地址上,偏移0Ch,也就是12个地址,就是GPIOA_ODR的地址。
(其它的端口都是依此类推,都是在各自的基地址上偏移0Ch),
所以从这里我知道了ODR这个寄存器的地址,也由第几个引脚,知道在这个地址数据的第几个bit位。(这个地方与前面的描述有不一致,前面这个bit只有0-7这三8个值,但是这里可以从0-15,但根据公式,只要基地址不变,那么算过来的相应的地址也会是正确的,这个n在公式里,就起到每多一个位,就偏移4个地址的作用。)
再由前面的公式,就算出对应的位带别名区的地址,并对该地址的值进行操作。而对其的操作,就是对应ODR里的某一个bit位的操作(硬件设计决定的)
把这些宏定义加到程序里,用位带操作来控制LED,试验是成功的。