2用STM32 点亮一个LED
对比着51 点亮LED 的方法,我们先用操作寄存器的方法用STM32 点亮一个LED,然后再一步步完善代码,构建最简单的库函数,让我们知道库是怎么建立起来的。
在写代码之前,我们先建一个工程。大家要注意的是,虽然51 跟STM32 用的都是keil,但是针对的MCU 是不一样,软件在安装的时候要安装在不同的目录且不能安装在英文目录,不然会起冲突。我们这里用的是keil5,MDK5.15 版本。
▶▶1 新建工程
用KEIL5 新建一个工程,把工程放在一个事先建好的文件夹内,工程命名为REG 后保存。然后在工程目录下添加启动文件:startup_stm32f10x_hd.s,该文件可以从KEIL5 安装目录找到,也可以从ST 库里面找到,然后把启动文件添加到工程里面。
▶▶2 启动文件—startup_stm32f10x_hd.s
启动文件由汇编语言编写,具体功能跟51 里面的启动文件:STARTUP.A51 差不多。STM32 的启动文件主要实现了:
1、设置初始SP 。
2、设置初始PC=Reset_Handler
3、设置向量表入口地址,并初始化向量表。
4、调用库函数SystemInit,把系统时钟配置成72M,SystemInit 在库文件system_stm32f10.c 定义。5、跳转到标号_mian,最终来到C 的世界。这里我们先去除繁枝细节,挑重点的讲,主要理解第四和第五点,在启动文件的147~155 行,是复位处理函数,代码如下:
这里我们简单介绍下这10 行代码。
第一行是程序注释,在汇编里面注释用的是“;”,跟C 语言不一样。
第二行是定义了一个子程序:Reset_Handler。PROC 是子程序定义伪指令。一般用法为:
其中NEAR 和FAR 是属性词。NEAR 属性(段内近调用): 调用程序和子程序在同一代码段中,只能被相同代码段的其他程序调用。FAR 属性(段间远调用): 调用程序和子程序不在同一代码段中,可以被相同或不同代码段的程序调用。
第三行EXPORT 表示Reset_Handler 这个子程序可供其他模块调用。
关键字[WEAK] 表示弱定义,如果编译器发现在别处定义了同名的函数,则在链接时用别处的地址进行链接,如果其它地方没有定义,编译器也不报错,以此处地址进行链接。
第四行和第五行IMPORT 说明SystemInit 和__main 这两个标号在其他文件,在链接的时候需要到其他文件去寻找。
SystemInit 在库文件system_stm32f10x.c 实现,用来初始化STM32 的一系列时钟,把系统时钟设置为72MHZ。STM32 的时钟比51 单片机复杂,需要经过一系列的配置才能达到稳定运行的状态。
__main 其实不是我们定义的,当编译器编译时,只要遇到这个标号就会定义这个函数,该函数的主要功能是:负责初始化栈、堆,配置系统环境,并在最后跳转到用户自定义的main 函数,从此来到C 的世界。
第六行把SystemInit 的地址加载到寄存器R0。
第七行程序跳转到R0 中的地址执行程序,之后系统的时钟就被设置成72MHZ。
第八行把_main 的地址加载到寄存器R0。
第九行程序跳转到R0 中的地址执行程序,执行完毕之后就去到我们熟知的C 世界。
第十行表示子程序的结束。
总结下就是,Reset_Handler 这个函数执行了两个函数调用,一个是SystemInit,把系统时钟设置成72M,令一个是__main,初始化好系统环境,最终调用C 的main,从此去到C 的世界。等下我们点亮LED 的时候采用最简单的方法,直接使用内部的LSI 时钟(8MHZ)作为主时钟即可,不使用外部时钟LSE。__main 函数由编译器生成,负责初始化栈、堆等,并在最后跳转到用户自定义的main()函数,来到C 的世界。
▶▶3 新建main.c
用记事本新建一个main.c 文件放到工程目录下,然后把main.c 添加到工程中。现在我们就可以开始编写程序了,我们先编写一个main 函数,里面啥都没有,暂时为空。这时跟编写51 程序时是不是很像。
现在我们可以编译看看,看看有啥现象。这时候出现如下错误:
错误提示说SystemInit 没有定义。从分析启动文件时我们知道,Reset_Handler 调用了该函数用来初始化系统时钟,而该函数是在库文件system_stm32f10x.c 中实现的。我们重新写一个这样的函数也可以,把功能完整实现一遍,但是为了简单起见,我们在main 文件里面定义一个SystemInit 空函数,为的是骗过编译器,把这个错误去掉。关于配置系统时钟我们在后面再写简单的代码。
这时我们再编译就没有错了,完美解决。还有一个方法就是在启动文件中把有关SystemInit 的代码注释掉也可以,代码如下所示:
▶▶4 控制IO 口
下面我们从三个方面来讲解STM32 的IO 在控制LED 时跟51 的区别。有关STM32 的IO 的寄存器介绍,我们可以看《STM32 中文参考手册》的第八章即可,下面涉及到的IO寄存器均来自这一章的第二小节:8.2 GPIO 寄存器描述
电平控制
51 单片机的IO 口如果要输出1 和0,可以直接赋值,不用控制其他寄存器。而STM32 的IO 口比较复杂,如果要输出1 和0,则要通过控制:端口输出数据寄存器ODR 来实现,ODR 是:Output data register 的简写,在STM32 里面,其寄存器的命名名称都是英文的简写,很容易记住。从手册上我们知道ODR 是一个32 位的寄存器,低16位有效,高16 位保留。低16 位对应着IO0~IO16,只要往相应的位置写入0 或者1 就可以输出低或者高电平。
PB0 输出低电平,代码如下:
这时候编译,我们会发现有个错误,说GPIOB_ODR 没有定义,不过我们确实没有定义。在51 单片机中,我们可以直接往P0 口赋值,那是因为在reg51.h 这个头文件中实现了P0 口这个寄存器的映像,用的是51 特有的关键字SFR 来定义的。
STM32 跟51 不一样,没有SFR,只能用其他的方式来实现寄存器映像。因为寄存器实际上就是具有特殊功能的内存,那么我们可以通过宏定义来实现寄存器映像,其实ST的库函数中用的也是这种方法。
从手册中我们看到ODR 寄存器的地址偏移是:0CH,这个偏移地址是基于端口的起始地址而言的。在STM32 中,每个外设都有一个起始地址,叫做外设基地址,外设的寄存器就以这个基地址为标准按照顺序排列,跟结构体里面的成员差不多。在手册中的第二章:存储器和总线构架的2.3:存储器映像小节中可以查看到所有外设的基地址,如下:
图5 STM32 寄存器组起始地址
其中GPIOB 的起始地址是:0X4001 0C00,这样就可以算出GPIOB_ODR 寄存器的地址是:0X4001 0C00 + 0X0C = 0X4001 0C0C。现在我们就可以定义GPIOB_ODR 这个寄存器了,代码如下:
有了这个寄存器定义,我们就可以直接操作GPIOB_ODR 了。
方向控制
虽然配置了ODR 寄存器,但是这个时候还不能点亮LED,因为STM32 的IO 口还要配置方向,这个由端口配置寄存器来控制。端口配置寄存器分为高低两个,每4bit 控制一个IO 口,所以端口配置低寄存器:CRL 控制这IO 口的低8 位,端口配置高寄存器:CRH控制这IO 口的高8bit。在4 位一组的控制位中,CNFy[1:0] 用来控制端口的输入输出,MODEy[1:0]用来控制输出模式的速率,即输出时,IO 电平翻转的速度。输入有三种模式,输出有4 中模式,我们在控制LED 的时候选择通用推挽输出。
输出速率有三种模式:2M、10M、50M,这里我们选择2M。同GPIOB_ODR 一样,我们也可以算出GPIO_CRL 的地址为:0x40010C00。那么设置PB0 为通用推挽输出,输出速率为2M 的代码则如下所示:
时钟控制
当我们设置了IO 口的方向,并在相应的输出寄存器里面输入了值的时候,以为现在总算可以点亮LED 了吧,其实还差最后一步。
STM32 外设很多,为了降低功耗,每个外设都对应着一个时钟,在系统复位的时候这些时钟都是被关闭的,如果想要外设工作,必须把相应的时钟打开。STM32 的所有外设的时钟由一个专门的外设来管理,叫RCC(reset and clockcontrol),RCC 在STM32 中文参考手册的第六章。
STM32 的外设因为速率的不同,分别挂载到三条总系上:AHB、APB2、APB1,APB为高速总线,APB2 次之,APB1 再次之。所以的IO 口都挂载到APB2 总线上,属于高速外设。时钟由APB2 外设时钟使能寄存器(RCC_APB2ENR)来控制,其中PB 端口的时钟由该寄存器的位3 写1 使能。
同ODR 和CRL,我们可以算出RCC_APB2ENR 的地址为:0x40021018。那么使能PB 口的时钟代码则如下所示:
如果你足够细心,你会发现我们虽然开了端口时钟,那这个时钟到底是多大?时钟到底是从哪里来的?
如果我们用的是库,那么有个库函数SystemInit,会帮我们把系统时钟设置成72M。现在我们没有使用库,那现在时钟是多少?答案是8M,当外部HSE 没有开启或者出现故障的时候,系统时钟由内部低速时钟LSI 提供,现在我们是没有开启HSE,所以系统默认的时钟是LSI=8M。至于更深入的细节我们在后面的RCC 时钟树中再详细分析。如果你想自己先尝鲜,那么看RCC 外设中的:时钟控制寄存器(RCC_CR)和时钟配置寄存器(RCC_CFGR)这两个寄存器即可。
水到渠成
控制了电平,配置了方向,开启了时钟,经过这三步,我们总算可以控制一个LED了。比起51 直接输出电平,控制STM32 的IO 多了两步:即配置方向可开启时钟。比起AVR 和PIC 这两种单片机则多了开启时钟这一步。现在我们完整组织下用STM32 控制一个LED 的代码:
很多人说学习STM32 很难,一堆的寄存器,不知道怎么操作,特别是那些刚学习完51 的朋友,不知道怎么过度。这里我们对比了51 的编程方法,写了个简单的用STM32 寄存器点亮LED 的方法,希望可以起到抛砖引玉的作用。
转帖自网络