本篇文章中,我们以实例讲解如何通过控制寄存器来点亮LED 灯。
建立一个新工程,见图1,可看到一共有三个文件,分别startup_stm32f10x_hd.s 、stm32f10x.h 以及main.c,下面我们对这三个文件进行讲解。
图 1 工程文件结构
硬件连接
在本教程中STM32 芯片与LED 灯的连接见图2,这是一个RGB 灯,里面由红蓝绿三个小灯构成,使用PWM控制时可以混合成256 不同的颜色。
图2 LED 灯电路连接图
图中从3 个LED 灯的阳极引出连接到3.3V 电源,阴极各经过1 个限流电阻引入至STM32 的3 个GPIO 引脚中,所以我们只要控制这三个引脚输出高低电平,即可控制其所连接LED 灯的亮灭。如果您的实验板STM32 连接到LED 灯的引脚或极性不一样,只需要修改程序到对应的GPIO 引脚即可,工作原理都是一样的。
我们的目标是把GPIO 的引脚设置成推挽输出模式并且默认下拉,输出低电平,这样就能让LED 灯亮起来了。
启动文件
名为“startup_stm32f10x_hd.s”的文件,它里边使用汇编语言写好了基本程序,当STM32 芯片上电启动的时候,首先会执行这里的汇编程序,从而建立起C 语言的运行环境,所以我们把这个文件称为启动文件。
startup_stm32f10x_hd.s 文件由官方提供,一般有需要也是在官方的基础上修改,不会自己完全重写。该文件从 ST 固件库里面找到,找到该文件后把启动文件添加到工程里面即可。不同型号的芯片以及不同编译环境下使用的汇编文件是不一样的,但功能相同。
对于启动文件这部分我们主要总结它的功能,不详解讲解里面的代码,其功能如下:
初始化堆栈指针SP;
初始化程序计数器指针PC;
设置堆、栈的大小;
初始化中断向量表;
配置外部SRAM 作为数据存储器(这个由用户配置,一般的开发板可没有外部SRAM);
调用SystemIni() 函数配置STM32 的系统时;
设置C 库的分支入口“__main”(最终用来调用main 函数);
先去除繁枝细节,挑重点的讲,主要理解最后两点,在启动文件中有一段复位后立即执行的程序,代码见代码清单1。在实际工程中阅读时,可使用编辑器的搜索(Ctrl+F)功能查找这段代码在文件中的位置,搜索Reset_Handler 即可找到。
代码清单1 复位后执行的程序
开头的是程序注释,在汇编里面注释用的是“;”,相当于 C 语言的“//”注释符
第二行是定义了一个子程序:Reset_Handler。PROC 是子程序定义伪指令。这里就相当于C 语言里定义了一个函数,函数名为Reset_Handler。
第三行 EXPORT 表示 Reset_Handler 这个子程序可供其他模块调用。相当于C 语言的函数声明。关键字[WEAK] 表示弱定义,如果编译器发现在别处定义了同名的函数,则在链接时用别处的地址进行链接,如果其它地方没有定义,编译器也不报错,以此处地址进行链接。
第四行和第五行 IMPORT 说明 SystemInit 和__main 这两个标号在其他文件,在链接的时候需要到其他文件去寻找。相当于C 语言中,从其它文件引入函数声明。以便下面对外部函数进行调用。
SystemInit 需要由我们自己实现,即我们要编写一个具有该名称的函数,用来初始化STM32 芯片的时钟,一般包括初始化AHB、APB 等各总线的时钟,需要经过一系列的配置STM32 才能达到稳定运行的状态。其实这个函数在固件库里面有提供,官方已经为我们写好。
__main 其实不是我们定义的(不要与C 语言中的main 函数混淆),这是一个C 库函数,当编译器编译时,只要遇到这个标号就会定义这个函数,该函数的主要功能是:负责初始化栈、堆,配置系统环境,并在函数的最后调用用户编写的 main 函数,从此来到 C 的世界。
第六行把 SystemInit 的地址加载到寄存器 R0。
第七行程序跳转到 R0 中的地址执行程序,即执行SystemInit 函数的内容。
第八行把__main 的地址加载到寄存器 R0。
第九行程序跳转到 R0 中的地址执行程序,即执行__main 函数,执行完毕之后就去到我们熟知的 C 世界,进入main 函数。
第十行表示子程序的结束。
总之,看完这段代码后,了解到如下内容即可:我们需要在外部定义一个SystemInit函数设置STM32 的时钟;STM32 上电后,会执行SystemInit 函数,最后执行我们C 语言中的main 函数。
stm32f10x.h 文件
看完启动文件,那我们立即写SystemInit 和main 函数吧?别着急,定义好了SystemInit 函数和main 我们又能写什么内容?连接LED 灯的GPIO 引脚,是要通过读写寄存器来控制的,就这样空着手,如何控制寄存器。我们知道寄存器就是给一个已经分配好地址的特殊的内存空间取的一个别名,这个特殊的内存空间可以通过指针来操作。在编程之前我们要先实现寄存器映射,有关寄存器映射的代码都统一写在stm32f10x.h 文件中,见代码清单2。
代码清单2 外设地址定义
使用GPIO 外设必须开启它的时钟。
此时直接编译的话,会出现如下错误:
“Error: L6218E: Undefined symbol SystemInit (referred from startup_stm32f10x.o)”
错误提示SystemInit 没有定义。从分析启动文件时我们知道,Reset_Handler 调用了该函数用来初始化SMT32 系统时钟,为了简单起见,我们在 main 文件里面定义一个SystemInit 空函数,什么也不做,为的是骗过编译器,把这个错误去掉。关于配置系统时钟我们在后面再写。当我们不配置系统时钟时,STM32 会把HSI 当作系统时钟,HSI=8M,由芯片内部的振荡器提供。我们在main 中添加如下函数:
这时再编译就没有错了, 完美解决。还有一个方法就是在启动文件中把有关SystemInit 的代码注释掉也可以,见代码清单3。
代码清单3 注释掉启动文件中调用SystemInit的代码
接下来在main 函数中添加代码,实现我们的点灯之旅
GPIO 模式
首先我们把连接到LED 灯的GPIO 引脚PB0 配置成输出模式,即配置GPIO 的端口配置低寄存器CRL,见图 8-9。CRL 中包含0-7 号引脚,每个引脚占用4 个寄存器位。MODE 位用来配置输出的速度,CNF 位用来配置各种输入输出模式。在这里我们把PB0 配置为通用推挽输出,输出的速度为10M,具体见代码清单4。
代码清单4 配置输出模式
在代码中,我们先把控制PB0 的端口位清0,然后再向它赋值“0001 b”,从而使GPIOB0 引脚设置成输出模式,速度为10M。
代码中使用了“&=~”、“|=”这种操作方法是为了避免影响到寄存器中的其它位,因为寄存器不能按位读写,假如我们直接给CRL 寄存器赋值:
1 GPIOB_CRL = 0x0000001;
这时CRL 的的低4 位被设置成“0001”输出模式,但其它GPIO 引脚就有意见了,因为其它引脚的MODER 位都已被设置成输入模式。
控制引脚输出电平
在输出模式时,对端口位设置/清除寄存器BSRR 寄存器、端口位清除寄存器BRR 和ODR 寄存器写入参数即可控制引脚的电平状态,其中操作BSRR 和BRR 最终影响的都是ODR 寄存器,然后再通过ODR 寄存器的输出来控制GPIO。为了一步到位,我们在这里直接操作ODR 寄存器来控制GPIO 的电平。具体见代码清单5。
代码清单5 控制引脚输出电平
图3 GPIO 端口控制低寄存器CRL
图4 GPIO 数据输出寄存器ODR
开启外设时钟
设置完GPIO的引脚,控制电平输出,以为现在总算可以点亮 LED 了吧,其实还差最后一步。由于STM32 的 外设很多,为了降低功耗,每个外设都对应着一个时钟,在芯片刚上电的时候这些时钟都是被关闭的,如果想要外设工作,必须把相应的时钟打开。
STM32 的所有外设的时钟由一个专门的外设来管理,叫 RCC(reset and clockcontrol),
所有的 GPIO都挂载到 APB2 总线上,具体的时钟由APB2 外设时钟使能寄存器(RCC_APB2ENR)来控制,具体见代码清单6。
代码清单6 开启端口时钟
图5 APB2 外设时钟使能寄存器
水到渠成
开启时钟,配置引脚模式,控制电平,经过这三步,我们总算可以控制一个 LED 了。现在我们完整组织下用 STM32 控制一个 LED 的代码,见代码清单7。