一、前情

电子信息专业,有嵌入式开发项目的经验,但不算完全了解。上课没认真听,项目全靠吃老本,现在为了毕业设计和工作打算认真从头学习嵌入式开发。

二、目前情况

看了一个星期正点原子关于STM32F429库函数开发的书,主要看的部分是ucos、fatfs和音乐播放器这几个部分,看的稀里糊涂的,应该是因为基础太差。从头开始看之后,明白了很多,但还有很多稀里糊涂。就去看了野火的资料,野火写的很好,看完清晰里很多,包括寄存器概念,手册的应用这种基础知识都写得很清晰明了,推荐大家看野火的教材。我目前的打算是看野火的教材,用正点原子的例程和开发板。正点原子的各种项目做的确实很好很有参考性。

三、笔记

(一)STM32芯片

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_引脚

图STM32芯片架构简图

M4内核是ARM公司做的,芯片集成设计由ST设计的。除内核以外的部分,是片上外设,内核与总线之间通过各种总线连接,其中其中主控总线有8条,被控总线有7条,具体见图 STM32F429xx器件的总线接口

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_引脚_02

其中圆圈的部分表示可以通信,黄色部分是主控总线,粉色部分是被控总线。

(二)存储器映射

存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射, 具体见图 STM32F429存储器映射 。如果给存储器再分配一个地址就叫存储器重映射。

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_二进制数_03

 

 这4GB的地址空间中,ARM已经粗线条的平均分成了8个块,每块512MB,每个块也都规定了用途,具体分类见表格 存储器功能分类 。

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_寄存器_04

 

Boock0用来设计成内部FLASH,Block1用来设计成内部RAM,Block2用来设计成片上的外设。这是比较重要的三块。

 

(三)寄存器映射

Block2片上外设,四个字节为一单元,共32bit,每一单元对应不同的功能,控制这些单元时就可以控制不同外设。除了找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元外,还们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。

 比如,我们找到GPIOH端口的输出数据寄存器ODR的地址是0x40021C14(至于这个地址如何找到可以先跳过,后面我们会有详细的讲解), ODR寄存器是32bit,低16bit有效,对应着16个外部IO,写0/1对应的的IO则输出低/高电平。现在我们通过C语言指针的操作方式, 让GPIOH的16个IO都输出高电平,具体见 代码清单:寄存器-1 。

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_寄存器_05

 

 0x4002 1C14在我们看来是GPIOH端口ODR的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数, 要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即(unsigned int *)0x4002 1C14,然后再对这个指针进行 * 操作。

解释:#define GPIOH_ODR   *(unsigned int*)(0x4002 1C14)  表示地址为(0x4002 1C14)的存储单元,其中 (unsigned int*)(0x4002 1C14) 表示0x4002 1C14是一个32位的地址指针,指针指向0x4002 1C14地址,*(unsigned int*)(0x4002 1C14)表示(0x4002 1C14)地址内存单元的内容。这种方式在单片机等底层文件普遍采用

用寄存器的方式来操作,具体见 代码清单:寄存器-2 。

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_引脚_06

 

1111 1111(这里有疑问,怎么变成8个1,不应该是16个1吗)

(四)外设寄存器

以“GPIO端口置位/复位寄存器”为例,教大家如何理解寄存器的说明, 具体见图 GPIO端口置位_复位寄存器说明 。

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_寄存器_07

 

 

  • ①名称寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…I)”这段的意思是该寄存器名为“GPIOx_BSRR”其中的“x”可以为A-I, 也就是说这个寄存器说明适用于GPIOA、GPIOB至GPIOI,这些GPIO端口都有这样的一个寄存器。
  • ②偏移地址偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是0x18,从参考手册中我们可以查到GPIOA外设的基地址为0x4002 0000 , 我们就可以算出GPIOA的这个GPIOA_BSRR寄存器的地址为:0x4002 0000+0x18;同理,由于GPIOB的外设基地址为0x4002 0400, 可算出GPIOB_BSRR寄存器的地址为:0x4002 0400+0x18 。其他GPIO端口以此类推即可。
  • ③寄存器位表紧接着的是本寄存器的位表,表中列出它的0-31位的名称及权限。表上方的数字为位编号,中间为位名称,最下方为读写权限,其中w表示只写,r表示只读, rw表示可读写。本寄存器中的位权限都是w,所以只能写,如果读本寄存器,是无法保证读取到它真正内容的。而有的寄存器位只读, 一般是用于表示STM32外设的某种工作状态的,由STM32硬件自动更改,程序通过读取那些寄存器位来判断外设的工作状态。
  • ④位功能说明位功能是寄存器说明中最重要的部分,它详细介绍了寄存器每一个位的功能。例如本寄存器中有两种寄存器位,分别为BRy及BSy,其中的y数值可以是0-15, 这里的0-15表示端口的引脚号,如BR0、BS0用于控制GPIOx的第0个引脚,若x表示GPIOA,那就是控制GPIOA的第0引脚,而BR1、BS1就是控制GPIOA第1个引脚。

 

其中BRy引脚的说明是“0:不会对相应的ODRx位执行任何操作;1:对相应ODRx位进行复位”。这里的“复位”是将该位设置为0的意思, 而“置位”表示将该位设置为1;说明中的ODRx是另一个寄存器的寄存器位,我们只需要知道ODRx位为1的时候,对应的引脚x输出高电平, 为0的时候对应的引脚输出低电平即可。所以,如果对BR0写入“1”的话, 那么GPIOx的第0个引脚就会输出“低电平”,但是对BR0写入“0”的话,却不会影响ODR0位,所以引脚电平不会改变。要想该引脚输出“高电平”, 就需要对“BS0”位写入“1”,寄存器位BSy与BRy是相反的操作。

(五)C语言对寄存器的封装

为了更方便地访问寄存器,我们引入C语言中的结构体语法对寄存器进行封装, 具体见 代码清单:寄存器-6 。

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_二进制数_08

这段代码用typedef 关键字声明了名为GPIO_TypeDef的结构体类型,结构体内有8个 成员变量,变量名正好对应寄存器的名字。 C语言的语法规定,结构体内变量的存储空间是连续的,其中32位的变量占用4个字节,16位的变量占用2个字节, 具体见图 GPIO_TypeDef结构体成员的地址偏移 。

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_二进制数_09

 

 也就是说,我们定义的这个GPIO_TypeDef ,假如这个结构体的首地址为0x4002 1C00(这也是第一个成员变量MODER的地址), 那么结构体中第二个成员变量OTYPER的地址即为0x4002 1C00 +0x04 ,正是代表MODER所占用的4个字节地址的偏移量, 其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给出,其中的BSRR寄存器分成了低16位BSRRL和高16位BSRRH,BSRRL置1引脚输出高电平, BSRRH置1引脚输出低电平,这里分开只是为了方便操作。

这样的地址偏移与STM32 GPIO外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来, 然后就能以结构体的形式访问寄存器了,具体见 代码清单:寄存器-7。

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_二进制数_10

这段代码先用GPIO_TypeDef类型定义一个结构体指针GPIOx,并让指针指向地址GPIOH_BASE(0x4002 1C00),使用地址确定下来,然后根据C语言访问结构体的语法,用GPIOx->BSRRL、GPIOx->MODER及GPIOx->IDR等方式读写寄存器。

最后,我们更进一步,直接使用宏定义好GPIO_TypeDef类型的指针,而且指针指向各个GPIO端口的首地址,使用时我们直接用该宏访问寄存器即可, 具体 代码清单:寄存器-8 。

stm32mp157正点原子用的是什么镜像 正点原子stm32f429_二进制数_11

(六)修改寄存器的位操作方法

使用C语言对寄存器赋值时,我们常常要求只修改该寄存器的某几位的值,且其它的寄存器位不变,这个时候我们就需要用到C语言的位操作方法了。

5.5.3.1. 把变量的某位清零

此处我们以变量a代表寄存器,并假设寄存器中本来已有数值,此时我们需要把变量a的某一位清零, 且其它位不变,方法见 代码清单:寄存器-9 。

代码清单:寄存器-9 对某位清零


//定义一个变量a = 1001 1111 b (二进制数)
unsigned char a = 0x9f;

//对bit2 清零

a &= ~(1<<2);

//括号中的1左移两位,(1<<2)得二进制数:0000 0100 b
//按位取反,~(1<<2)得1111 1011 b
//假如a中原来的值为二进制数: a = 1001 1111 b
//所得的数与a作”位与&”运算,a = (1001 1111 b)&(1111 1011 b),
//经过运算后,a的值 a=1001 1011 b
// a的bit2 位被被零,而其它位不变。


5.5.3.2. 把变量的某几个连续位清零

由于寄存器中有时会有连续几个寄存器位用于控制某个功能,现假设我们需要把寄存器的某几个连续位清零, 且其它位不变,方法见 代码清单:寄存器-10 。

代码清单:寄存器-10 对某几个连续位清零


//若把a中的二进制位分成2个一组
//即bit0、bit1为第0组,bit2、bit3为第1组,
//  bit4、bit5为第2组,bit6、bit7为第3组
//要对第1组的bit2、bit3清零

a &= ~(3<<2*1);

//括号中的3左移两位,(3<<2*1)得二进制数:0000 1100 b
//按位取反,~(3<<2*1)得1111 0011 b
//假如a中原来的值为二进制数: a = 1001 1111 b
//所得的数与a作”位与&”运算,a = (1001 1111 b)&(1111 0011 b),
//经过运算后,a的值 a=1001 0011 b
// a的第1组的bit2、bit3被清零,而其它位不变。

//上述(~(3<<2*1))中的(1)即为组编号;如清零第3组bit6、bit7此处应为3
//括号中的(2)为每组的位数,每组有2个二进制位;若分成4个一组,此处即为4
//括号中的(3)是组内所有位都为1时的值;若分成4个一组,此处即为二进制数“1111 b”

//例如对第2组bit4、bit5清零
a &= ~(3<<2*2);


5.5.3.3. 对变量的某几位进行赋值。

寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,且其它位不变, 方法见 代码清单:寄存器-11 ,这时候写入的数值一般就是需要设置寄存器的位参数。

代码清单:寄存器-11 对某几位进行赋值


//a = 1000 0011 b
//此时对清零后的第2组bit4、bit5设置成二进制数“01 b ”

a |= (1<<2*2);
//a = 1001 0011 b,成功设置了第2组的值,其它组不变


5.5.3.4. 对变量的某位取反

某些情况下,我们需要对寄存器的某个位进行取反操作,即 1变0 ,0变1,这可以直接用如下操作, 其它位不变,见 代码清单:寄存器-12 。

代码清单:寄存器-12 对某位进行取反操作


//a = 1001 0011 b
//把bit6取反,其它位不变

a ^=(1<<6);
//a = 1101 0011 b


四、总结

火哥真是写的非常清楚明了,对于嵌入式零基础或者接近零基础而言都非常友好,看完总算大彻大悟,以上部分来自F429挑战者的第五章,建议看完这一章再去看火哥的一天stm32入门的PDF,对日后的理解有很大的帮助。(我的笔记比较潦草,还有大段摘抄,主要是针对我自己不太会的地方,勿喷)