6.3 实验:使用寄存器点亮LED
本小节以实例讲解如何控制寄存器来点亮LED。此处侧重于讲解原理,读者可直接用KEIL5软件打开我们提供的实验例程配合阅读,先了解原理,学习完本小节后,再尝试自己建立一个同样的工程。本节配套例程名为“GPIO输出—寄存器点亮LED”,在工程目录下找到后缀为“.uvprojx”的文件,用KEIL5打开即可。
自己尝试新建工程时,请参考第5章。
若没有安装KEIL5软件,请参考第1章。
打开该工程,可看到一共有3个文件,分别是startup_stm32f429_439xx.s、stm32f4xx.h以及main.c,见图6-3。下面对这3个工程进行讲解。
图6-3 工程文件目录
6.3.1 硬件连接
在本实验中,STM32芯片与LED的连接见图6-4。
图6-4 LED电路连接
图6-4中3个LED的阳极连接到3.3V电源,各阴极分别经过1个电阻引至STM32的3个GPIO引脚PH10、PH11、PH12,所以只要控制这3个引脚输出高低电平,即可控制其所连接LED的亮灭。如果你的实验板STM32连接到LED的引脚或极性不一样,只需要修改程序到对应的GPIO引脚即可,工作原理都是一样的。
我们的目标是把GPIO的引脚设置成推挽输出模式,并且默认下拉模式,输出低电平,这样就能让LED亮起来了。
6.3.2 启动文件
名为startup_stm32f429_439xx.s的文件中使用汇编语言写好了基本程序,当STM32芯片上电启动的时候,首先会执行这里的汇编程序,从而建立C语言的运行环境,所以这个文件称为启动文件。该文件使用的汇编指令是Cortex-M4内核支持的指令,可从《Cortex-M4 Technical Reference Manual》中查到,也可参考《Cortex-M3权威指南(中文版)》,M3与M4的大部分汇编指令相同。
startup_stm32f429_439xx.s文件是由官方提供的,若要修改可在官方文件的基础上进行,不用自己重写。该文件可以从KEIL5安装目录中找到,也可以从ST库里找到。找到该文件后,把该启动文件添加到工程里即可。不同型号的芯片以及不同编译环境下使用的汇编文件是不一样的,但功能相同。
对于启动文件这部分内容,我们主要介绍它的功能,不详细讲解里面的代码。其功能如下:
□ 初始化堆栈指针SP。
□ 初始化程序计数器指针PC。
□ 设置堆和栈的大小。
□ 设置中断向量表的入口地址。
□ 配置外部SRAM作为数据存储器(由用户配置,一般的开发板没有外部SRAM)。
□ 调用SystemInit()函数配置STM32的系统时钟。
□ 设置C库的分支入口“__main”(最终用来调用main函数)。
先去除细枝末节,挑重点的讲,主要理解最后两点。在启动文件中有一段复位后立即执行的程序,见代码清单6-1。在阅读工程文件代码时,可使用编辑器的搜索(Ctrl+F)功能迅速查找这段代码在文件中的位置。
代码清单6-1 复位后执行的程序
1 ;Reset handler 2 Reset_Handler PROC 3 EXPORT Reset_Handler [WEAK] 4 IMPORT SystemInit 5 IMPORT __main 6 7 LDR R0, =SystemInit 8 BLX R0 9 LDR R0, =__main 10 BX R0 11 ENDP
第1行是程序注释,在汇编里面注释用的是“;”,相当于C语言的“//”注释符。
第2行定义了一个子程序:Reset_Handler。PROC是子程序定义伪指令。这里相当于C语言里定义了一个函数,函数名为Reset_Handler。
第3行EXPORT表示Reset_Handler这个子程序可供其他模块调用。相当于C语言的函数声明。关键字“[WEAK]”表示弱定义,如果编译器发现在别处定义了同名的函数,则在链接时用别处的地址进行链接,如果其他地方没有定义,编译器也不报错,以此处地址进行链接。
第4行和第5行用IMPORT说明SystemInit和__main这两个标号在其他文件中,在链接时需要到其他文件中寻找。相当于C语言中从其他文件引入函数声明,以便后面程序对外部函数进行调用。
SystemInit需要由我们自己实现,即要编写一个具有该名称的函数,用来初始化STM32芯片的时钟,一般包括初始化AHB、APB等各总线的时钟。STM32需要经过一系列的配置才能达到稳定运行的状态。
__main其实不是我们定义的(不要与C语言中的main函数混淆),当编译器编译时,只要遇到这个标号就会定义这个函数。该函数的主要功能是:初始化栈、堆,配置系统环境,准备好C语言,并在最后跳转到用户自定义的main函数,从此进入C的世界。
第6行把SystemInit的地址加载到寄存器R0。
第7行跳转到R0中的地址里执行程序,即执行SystemInit函数的内容。
第8行把__main的地址加载到寄存器R0。
第9行跳转到R0中的地址里执行程序,即执行__main函数,执行完毕就转到我们熟知的C世界,进入main函数了。
第10行表示子程序的结束。
总之,看完这段代码后,了解到如下内容即可:需要在外部定义一个SystemInit函数设置STM32的时钟;STM32上电后,会执行SystemInit函数,最后执行C语言中的main函数。
6.3.3 stm32f4xx.h文件
看完启动文件,就能立即开始编写SystemInit和main函数吗?定义好了SystemInit函数和main,连接LED的GPIO引脚时,是要通过读写寄存器来控制的,就这样空着手,如何控制寄存器呢?在第5章,我们知道寄存器就是特殊的内存空间,可以通过指针操作访问。所以此处可根据STM32的存储分配先定义好各个寄存器的地址,把这些地址定义都统一写在stm32f4xx.h文件中,见代码清单6-2。
代码清单6-2 外设地址定义
1 /*片上外设基地址*/ 2 #define PERIPH_BASE ((unsigned int)0x40000000) 3 /*总线基地址*/ 4 #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000) 5 /*GPIO外设基地址*/ 6 #define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00) 7 8 /* GPIOH寄存器地址,强制转换成指针 */ 9 #define GPIOH_MODER *(unsigned int*)(GPIOH_BASE+0x00) 10 #define GPIOH_OTYPER *(unsigned int*)(GPIOH_BASE+0x04) 11 #define GPIOH_OSPEEDR *(unsigned int*)(GPIOH_BASE+0x08) 12 #define GPIOH_PUPDR *(unsigned int*)(GPIOH_BASE+0x0C) 13 #define GPIOH_IDR *(unsigned int*)(GPIOH_BASE+0x10) 14 #define GPIOH_ODR *(unsigned int*)(GPIOH_BASE+0x14) 15 #define GPIOH_BSRR *(unsigned int*)(GPIOH_BASE+0x18) 16 #define GPIOH_LCKR *(unsigned int*)(GPIOH_BASE+0x1C) 17 #define GPIOH_AFRL *(unsigned int*)(GPIOH_BASE+0x20) 18 #define GPIOH_AFRH *(unsigned int*)(GPIOH_BASE+0x24) 19 20 /*RCC外设基地址*/ 21 #define RCC_BASE (AHB1PERIPH_BASE + 0x3800) 22 /*RCC的AHB1时钟使能寄存器地址,强制转换成指针*/ 23 #define RCC_AHB1ENR *(unsigned int*)(RCC_BASE+0x30)
GPIO外设的地址与第5章讲解的相同,不过此处把寄存器的地址值都直接强制转换成了指针,方便使用。代码的最后两段是RCC外部寄存器的地址定义,RCC外设是用来设置时钟的,后文会详细分析,在本实验中只要了解使用GPIO外设必须开启它的时钟即可。
6.3.4 main文件
现在就可以开始编写程序了。在main文件中先编写一个main函数,但使其暂时为空。
1 int main (void) 2 { 3 }
此时直接编译的话,会出现如下错误:
“Error: L6218E: Undefined symbol SystemInit(referred from startup_stm32f429_439xx.o)”
错误提示SystemInit没有定义。分析启动文件可知,Reset_Handler调用了该函数用来初始化SMT32系统时钟,为了简单起见,我们在main文件里面定义一个SystemInit空函数,什么也不做,为的是“骗过”编译器,把这个错误去掉。关于系统时钟配置后文有讲解。当不配置系统时钟时,STM32芯片会自动按系统内部的默认时钟运行,程序还是能运行的。我们在main中添加如下函数:
1 //函数为空,目的是“骗过”编译器不报错 2 void SystemInit(void) 3 { 4 }
这时再编译就不会报错了。还有一个方法是在启动文件中把有关SystemInit的代码注释掉,见代码清单6-3。
代码清单6-3 注释掉启动文件中调用SystemInit的代码
1 //Reset handler 2 Reset_Handler PROC 3 EXPORT Reset_Handler [WEAK] 4 ;IMPORT SystemInit 5 IMPORT __main 6 7 ;LDR R0, =SystemInit 8 ;BLX R0 9 LDR R0, =__main 10 BX R0 11 ENDP
接下来在main函数中添加代码,对寄存器进行控制,寄存器的控制参数可参考表6-1或《STM32F4xx参考手册》。
1. GPIO模式
首先我们把连接到LED的PH10引脚配置成输出模式,即配置GPIO的MODER寄存器,见图6-5。MODER中包含0~15号引脚,每个引脚占用两个寄存器位。这两个寄存器位设置成“01”时即为GPIO的输出模式,见代码清单6-4。
图6-5 MODER寄存器说明(摘自《STM32F4xx参考手册》)
代码清单6-4 配置输出模式
1 /*GPIOH MODER10清空*/ 2 GPIOH_MODER &= ~( 0x03<< (2*10)); 3 /*PH10 MODER10 = 01b输出模式*/ 4 GPIOH_MODER |= (1<<2*10);
在代码中,我们先把GPIOH MODER寄存器的MODER10对应位清0,然后再向它赋值“01”,从而使GPIOH10引脚设置成输出模式。
代码中使用了“&=~”“|=”这种复杂的位操作方法,目的是为了避免影响寄存器中的其他位。因为寄存器不能按位读写,假如我们直接给MODER寄存器赋值:
1 GPIOH_MODER = 0x00100000;
这时MODER10的两个位被设置成“01”,即输出模式,但其他GPIO引脚就有问题了,因为其他引脚的MODER位都已被设置成了输入模式。
2.输出类型
GPIO输出有推挽和开漏两种类型,我们知道,开漏类型不能直接输出高电平,要输出高电平还要在芯片外部接上拉电阻,不符合我们的硬件设计,所以直接使用推挽模式。配置OTYPER中的OTYPER10寄存器位,该位设置为0时PH10引脚即为推挽模式,见代码清单6-5。
代码清单6-5 设置为推挽模式
1 /*GPIOH OTYPER10清空*/ 2 GPIOH_OTYPER &= ~(1<<1*10); 3 /*PH10 OTYPER10 = 0b 推挽模式*/ 4 GPIOH_OTYPER |= (0<<1*10);
3. 输出速度
GPIO引脚的输出速度是引脚支持高低电平切换的最高频率,本实验中可以随便设置。此处我们配置OSPEEDR寄存器中的寄存器位OSPEEDR10即可控制PH10的输出速度,见代码清单6-6。
代码清单6-6 设置输出速度为2MHz
1 /*GPIOH OSPEEDR10清空*/ 2 GPIOH_OSPEEDR &= ~(0x03<<2*10); 3 /*PH10 OSPEEDR10 = 0b 速率2MHz*/ 4 GPIOH_OSPEEDR |= (0<<2*10);
4. 上拉/下拉模式
当GPIO引脚用于输入时,引脚的上拉/下拉模式可以控制引脚的默认状态。但现在的GPIO引脚用于输出,引脚受ODR寄存器影响。ODR寄存器对应引脚初始化后默认值为0,引脚输出低电平,所以这时配置上拉/下拉模式不会影响引脚电平状态。但因此处上拉电阻能小幅提高电流输出能力,所以配置它为上拉模式,即将PUPDR寄存器的PUPDR10位设置为二进制值“01”,见代码清单6-7。
代码清单6-7 设置为上拉模式
1 /*GPIOH PUPDR10清空*/ 2 GPIOH_PUPDR &= ~(0x03<<2*10); 3 /*PH10 PUPDR10 = 01b 上拉模式*/ 4 GPIOH_PUPDR |= (1<<2*10);
5. 控制引脚输出电平
在输出模式时,对BSRR寄存器和ODR寄存器写入参数即可控制引脚的电平状态。简单起见,此处使用BSRR寄存器控制,将相应的BR10位设置为1时,PH10为低电平,点亮LED;将它的BS10位设置为1时,PH10为高电平,关闭LED。见代码清单6-8。
代码清单6-8 控制引脚输出电平
1 /*PH10 BSRR寄存器的 BR10置1,使引脚输出低电平*/ 2 GPIOH_BSRR |= (1<<16<<10); 3 4 /*PH10 BSRR寄存器的 BS10置1,使引脚输出高电平*/ 5 GPIOH_BSRR |= (1<<10);
6. 开启外设时钟
设置完GPIO的引脚,以控制电平输出,那么就可以点亮LED了吧?其实还差最后一步。
STM32的外设很多,为了降低功耗,每个外设都对应着一个时钟,在芯片刚上电的时候这些时钟都是关闭的,如果想要外设工作,必须把相应的时钟打开。
STM32所有外设的时钟由一个专门的外设来管理,叫RCC(Reset and ClockControl),其功能见《STM32中文参考手册》第6章。
所有的GPIO都挂载到AHB1总线上,所以它们的时钟由AHB1外设时钟使能寄存器(RCC_AHB1ENR)来控制,其中GPIOH端口的时钟由该寄存器的位7写1使能,开启GPIOH端口时钟。后文会详细解释STM32的时钟系统,此处只要了解在访问GPIO的寄存器之前,要先使能它的时钟即可。使用代码清单6-9中的代码可以开启GPIOH时钟。
代码清单6-9 开启端口时钟
1 /*开启GPIOH时钟,使用外设时都要先开启它的时钟*/ 2 RCC_AHB1ENR |= (1<<7);
7. 水到渠成
开启时钟,配置引脚模式,控制电平,经过这3步,我们总算可以控制一个LED了。现在我们完整地组织一下用STM32控制一个LED的代码,见代码清单6-10。
代码清单6-10 main文件中控制LED的代码
1 2 /* 3 * 使用寄存器的方法点亮LED 4 */ 5 #include "stm32f4xx.h" 6 7 8 /** 9 * 主函数 10 */ 11 int main(void) 12 { 13 /*开启 GPIOH 时钟,使用外设时都要先开启它的时钟*/ 14 RCC_AHB1ENR |= (1<<7); 15 16 /* LED 端口初始化 */ 17 18 /*GPIOH MODER10清空*/ 19 GPIOH_MODER &= ~( 0x03<< (2*10)); 20 /*PH10 MODER10 = 01b 输出模式*/ 21 GPIOH_MODER |= (1<<2*10); 22 23 /*GPIOH OTYPER10清空*/ 24 GPIOH_OTYPER &= ~(1<<1*10); 25 /*PH10 OTYPER10 = 0b 推挽模式*/ 26 GPIOH_OTYPER |= (0<<1*10); 27 28 /*GPIOH OSPEEDR10清空*/ 29 GPIOH_OSPEEDR &= ~(0x03<<2*10); 30 /*PH10 OSPEEDR10 = 0b 速率2MHz*/ 31 GPIOH_OSPEEDR |= (0<<2*10); 32 33 /*GPIOH PUPDR10清空*/ 34 GPIOH_PUPDR &= ~(0x03<<2*10); 35 /*PH10 PUPDR10 = 01b 上拉模式*/ 36 GPIOH_PUPDR |= (1<<2*10); 37 38 /*PH10 BSRR寄存器的 BR10置1,使引脚输出低电平*/ 39 GPIOH_BSRR |= (1<<16<<10); 40 41 /*PH10 BSRR寄存器的 BS10置1,使引脚输出高电平*/ 42 //GPIOH_BSRR |= (1<<10); 43 44 while (1); 45 46 } 47 48 //函数为空,目的是“骗过”编译器不报错 49 void SystemInit(void) 50 { 51 }
在本节中,要求完全理解stm32f4xx.h文件及main文件的内容(RCC相关部分除外)。
6.3.5 下载验证
把编译好的程序下载到开发板并复位,可看到板子上的LED被点亮。