8 位内核的51 类MCU 的资源往往是最大几K-100K 的flash。 100-几K 字节的RAM, IO, 串口,定时器,8 位数据总线, AD 等简单的资源。 目标确定,单一。结构简单,指令简单。 易于理解和操作,这些特点也是51 能深入人心的因素。 目前依然是高校的主导实验平台。 也是很多企业的应用平台。
随着coretex-m3 内核的STM32 在中国的兴起,引起了广大51 使用者的注意,对于我当初进入时的认识,我觉得STM32 速度非常快, flash,ram 好大。 能操作SD 卡,这简直相当于微控制器的硬盘了。Usb功能这一个51 以前从来没有的东西,终于可以和计算机不需要串口就可以实现通信了。定时器那么多路,可以使我做多少的PWM 控制啊。16 位的FSMC 总线,实现了高分辨率的LCD 也一样可以高速控制了, 再不是51 那个仅仅能使用一些低分辨率且昂贵的LCM 比如12864 这些行将没落的东东。 以前在51 想都不要想的ucos ucgui 都可以STM32 上尽情发挥了。还有好多好的功能,can 控制器轻易实现以前要组合电路才能实现的can 通信以及以太网的应用等等。 这是真正意义的微控领域的SOC 芯片。
初入STM32,可能我们最亲切的就是在51 使用过的keil , 在51 它叫keil c51, 在arm 它叫RealviewMKD-ARM,简称它MDK, 现在版本是MDK4.22, 操作方法基本类似于keil。 我们常用的功能除了编辑工程,编译代码,还会用到下载,调试。 我们在51 时,可能会很少有人用仿真功能,因为51 足够的简单,脑子想的往往就是你所看到的。 直接下载到目标板对你来说更快捷。 所以在51 最常见的是下载器。 但在arm阶段,资源繁杂,寄存器复杂。变量众多,没有一个仿真器,会感到那么的无助。因此 coretex-m3 的使用者基本都会拥有仿真器,一般分为ST-LINK ULINK 及JLINK,尤以JLINK 在中国的应用最为普及。 我们都懂的原因,JLINK V8 的性价比是这几个最好的。 所以,你需要在获得了MDK 后,再拥有一个JLINK。 它不仅仅只支持STM32,它支持绝大多数的ARM 芯片。
51 使用者初入STM32,都会存在一个平台转换带来迷惘的一个短暂过程,这是器件类型变化较大造成的认知差异。
但调整一下,这个不适会很快过去的。
▶▶1 先看看 51 和STM32 具有的相同类型资源是哪些。根据你对51 的熟悉程度, 你会从STM32 的手册上看到。 这些往往是较简单的,也是最容易理解的。 比如IO 口线控制,等等。
▶▶2 STM32 高级一些的资源,往往也是需要较多精力去理解的。这可以在入门后再行学习,比如USB,SDIO等。
▶▶4 编程方式的不同, 比如在 51,用置位或者复位指令就可以很方便的控制IO,而在STM32,由于所有资源的功能都和该资源对应的32 位寄存器组的操作有关系。 因此对于资源的设置和操作都可能需要操作一个或者多个寄存器, 如果用多条指令来控制的话,会引起阅读的障碍,以及日后代码维护复杂,因此ST 公司引入了库函数的概念。用执行库函数的方式解决复杂的资源操作的问题。
▶▶4 STM32 例程的MDK 工程都有相似的程序结构,结合手册多看例程,会使你快速的形成对STM32 例程模板的认识,这个认识一旦形成,剩下的代码细节就好比是你预测到的填空题目。
当你做好了想学习新平台的准备,那就义无反顾的投入CORETEX-M3 的怀抱吧。它会使你进步到一个新的境界。 带给你愉悦的技术享受。
如何迅速入门STM32单片机?
网上有大神说如果会51单片机和C语言一天可入门STM32,仅一天的时间,是否有真的这么快。这个要看自己给自己定的入门的标准了。
我眼中的入门:(前提是你学过51 单片机和C 语言)
▶▶1 知道参考官方的什么资料来学习,而不是陷入一大堆资料中无从下手。
▶▶2 知道如何参考官方的手册和官方的代码来独立写自己的程序,而不是一味的看到人家写的代码就觉得人家很牛逼。
▶▶3 消除对STM32 的恐惧,消除对库开发的恐惧,学习是一个快乐而富有成就感的过程。
学习本文时,配合《STM32 中文参考手册》GPIO 章节一起阅读,效果会更佳,特别是涉及到寄存器说明的部分。
1、51 与STM32 简介51 是嵌入式学习中一款入门级的精典MCU,因其结构简单,易于教学,且可以通过串口编程而不需要额外的仿真器,所以在教学时被大量采用,至今很多大学在嵌入式教学中用的还是51。51 诞生于70 年代,属于传统的8 位单片机,如今,久经岁月的洗礼,既有其辉煌又有其不足。现在的市场产品竞争激烈,对成本极其敏感,相应地对MCU 的要求也更苛刻:功能更多,功耗更低,易用界面和多任务。面对这些要求,51 现有的资源就显得得抓襟见肘了。所以无论是高校教学还是市场需求,都急需一款新的MCU 来为这个领域注入新的活力。
基于这市场的需求, ARM 公司推出了其全新的基于ARMv7 架构的32 位Cortex-M3微控制器内核。紧随其后,ST(意法半导体)公司就推出了基于Cortex-M3 内核的MCU—STM32。STM32 凭借其产品线的多样化、极高的性价比、简单易用的库开发方式,迅速在众多Cortex-M3 MCU 中脱颖而出,成为最闪亮的一颗新星。STM32 一上市就迅速占领了中低端MCU 市场,受到了市场和工程师的无比青睐,颇有星火燎原之势。
作为一名合格的嵌入式工程师,面对新出现的技术,我们不是充耳不闻,而是要尽快吻合市场的需要,跟上技术的潮流。如今STM32 的出现就是一种趋势,一种潮流,我们要做的就是搭上这趟快车,让自己的技术更有竞争力。
51 与STM32 架构的区别
我们先普及一个概念,单片机(即MCU)里面有什么。一个人最重要的是大脑,身体的各个部分都在大脑的指挥下工作。MCU 跟人体很像,简单来说是由一个最重要的内核加其他外设组成,内核就相当于人的大脑,外设就如人体的各个功能器官。
下面我们来简单介绍下51 和STM32 的结构。
151 系统结构
51 系统结构框图
图1 51 系统结构框图
我们说的51 一般是指51 系列的单片机,型号有很多,常见的有STC89C51、AT89S51,其中国内用的最多的是STC89C51/2,下面我们就以STC89C51 来讲解,并以51 简称。
内核
51 由一个IP 核和片上外设组成,IP 核就是上图中的CPU,片上外设就是上图中的:时钟电路、SFR 和RAM、ROM、定时/计数器、并行I/O 口、串行I/O 口、中断系统。IP核跟外设之间由系统总线连接,且是8bit 的,速度有限。
51 内核是上个世纪70 年代intel 公司设计的,速度只有12M,外设是IC 厂商(STC)在内核的基础上添加的,不同的IC 厂商会在内核上添加不同的外设,从而设计出各具特色的单片机。这里intel 属于IP 核厂商,STC 属于IC 厂商。我们后面要讲的STM32 也一样,ARM 属于IP 核厂商,ARM 给ST 授权,ST 公司在Cortex-M3 内核的基础上设计出STM32 单片机。
外设
我们在学习51 的时候,关于内核部分接触的比较少,使用的最多的是片上外设,我们在编程的时候操作的也就是这些外设。
编程的时候操作的寄存器位于SFR 和RAM 这个部分,其中SFR(特殊功能寄存器)占有128 字节(实际上只用了26 个字节,只有26 个寄存器,其他都属于保留区),RAM占有128 字节,我们在程序中定义的变量就是放在RAM 中。其中SFR 和RAM 在地址上是重合的,都是在80~FF 这个地址区间,但在物理区间上是分开的,所以51 的RAM 是有256 个字节。
编写好的程序是烧写到ROM 区。剩下的外设都是我们非常熟悉的IO 口,串口、定时器、中断这几个外设。
2STM32 系统结构
STM32 系统结构框图
图2 STM32 系统结构框图
内核
在系统结构上,STM32 和51 都属于单片机,都是由内核和片上外设组成。只是STM32 使用的Cortex-M3 内核比51 复杂得多,优秀得多,支持的外设也比51 多得多,同时总线宽度也上升到32bit,无论速度、功耗、外设都强与51。
从结构框图上看,对比51 内核只有一种总线,取指和取数共用。Cortex-M3 内部有若干个总线接口,以使CM3 能同时取址和访内(访问内存),它们是:
指令存储区总线(两条)、系统总线、私有外设总线。有两条代码存储区总线负责对代码存储区(即FLASH 外设)的访问,分别是I-Code 总线和D-Code 总线。
I-Code 用于取指,D-Code 用于查表等操作,它们按最佳执行速度进行优化。
系统总线(System)用于访问内存和外设,覆盖的区域包括SRAM,片上外设,片外RAM,片外扩展设备,以及系统级存储区的部分空间。
私有外设总线负责一部分私有外设的访问,主要就是访问调试组件。它们也在系统级存储区。
还有一个MDA 总线,从字面上看,DMA 是data memory access 的意思,是一种连接内核和外设的桥梁,它可以访问外设、内存,传输不受CPU 的控制,并且是双向通信。简而言之,这个家伙就是一个速度很快的且不受老大控制的数据搬运工,这个在51 里面是没有的。
外设
从结构框图上看,STM32 比51 的外设多得多,51 有的串口、定时器、IO 口等外设STM32 都有。STM32 还多了很多特色外设:如FSMC、SDIO、SPI、I2C 等,这些外设按照速度的不同,分别挂载到AHB、APB2、APB1 这三条总线上
3、小结
从内核和外设这两大方面来比较,STM32 之于51 就是一个升级版的单片机。它适应市场,引流潮流,在中低端的微控制器中流光溢彩。
2、学习方法的区别学习51 用寄存器,学习STM32 用库。
以前我们在学习51 的时候,用的是寄存器编程的方法,想要实现什么效果,直接往寄存器里面赋值,优点是直观,简单粗暴,知道自己具体干了啥,心里踏实。
直接操作寄存器之所以在51 上可行,究其原因,我想有两点:
▶▶1 51 主频不高,资源有限,必须注重程序执行的效率,只能直接操作寄存器。关键的地方还得用汇编,不适合用固件库。
要知道当初我们学习51 单片机的时候用的还是汇编,连现在的C 编程都不是,就更别说什么库函数编程。
▶▶2 51 功能简单,寄存器不多。以国内普及最广的STC89C52 为例,寄存器全部加起来不到30 个。按照功能区分来记的话,可以把每个寄存器背的滚瓜烂熟,并且寄存器每一位的功能都可以记得住,在编程的时候做到了然于胸。
现在从51 过度到STM32 的学习,很多人还是喜欢沿用51 的学习方法。接受不了库,在学习库的时候陷入迷糊之中,来回几个月下来,都不知道到底有没学会STM32,因为在这一路的学习中都是在调用库函数,压根就没有操作过寄存器,心里面很不踏实。其实大家在调用库函数的时候心中难道就没有疑问,库的底层是怎么实现的?难道就没有勇气对库的底层一探究竟。可最后当我们开始跟踪库函数底层的时候,看到一堆的宏定义、结构体、指针、各种的文件包含,而且注释全部都是英文的,是不是又心生忌惮。
鉴于此,我想用两个原因来总结下很多初学者畏惧库不愿意用库的原因。
▶▶1 C 语言知识点的欠缺
库在实现寄存器映像时使用的宏定义,强制类型转换,在定义寄存器时使用的结构体,在外设初始化函数时使用的指针,在组织头文件时使用的条件编译等C 语言知识,在大学课程中很少涉及,大多数老师也基本是不讲。在一些简单的51 单片机编程中又很少会用到这些知识。学单片机,做嵌入式开发其实80%的工作都跟C 语言编程相关,剩下的20%的工作就是阅读各种数据手册,熟悉各种硬件外设。所以掌握这些基本的C 语言知识,是嵌入式学习中一道迈不过去的坎,STM32 的库则给了我们一次提升C 的机会。凡是可以从书本中找到的,相信我们基本都可以学会,很多初学者并不是不够聪明或者勤奋,只是缺少方向性的指导罢了。对于这欠缺的知识点我们稍微花点时间就可以掌握,剩下的就是不断地实践调试。这里我为大家推荐一本C 语言的书籍《C 和指针》。
▶▶2 程序架构设计思想的欠缺
这个比较难搞,很多C 语言学习得挺好好的人,也比较难掌握。还好我们遇到了STM32 的库,这给了我们一个学习和提升C 语言绝佳的机会。库的整个架构是如何搭建起来的,代码上是如何如何一步一步写出来的:从寄存器映像开始,到寄存器的封装,然后到函数的编写,到每个外设函数对应的驱动文件,这里面涉及到了大量的条件编译,文件包含的思想,对应刚写过几行51 单片机的初学者来说简直就是噩梦。但是,如果你把这一系列的关系弄明白了,那么对库的整个架构也了解的差不多了,以后你就不用嚷嚷着说要操作寄存器了。
如果你一开始不喜欢用库,对库开发很忌惮,那么请自问:是不是我的C 语学得不够好。库是一种全新的学习方法,是一种潮流,我更把它看做是与C 语言的又一次历练和提升。是否用库,只差你一个闪亮的回眸。
3、用寄存器点亮LED为了顺利过渡到库开发,在STM32 编程的开始,我们对照51 点亮一个LED 的方法,给大家演示一下STM32 如何用操作寄存器的方法点亮一个LED,然后再慢慢讲解到底什么是库,让大家知道库跟寄存器的关系。
1用51 点亮一个LED
在用STM32 点亮一个LED 之前,我们先来复习下用51 如何点亮一个LED。
硬件上我们假设51 单片机的P0 口的第0 位接了一个LED,负逻辑亮。如果我们要点亮这个LED,代码上我们会这么写:
这里面我们用的是总线操作的方法,即是对P0 口的8 个IO 同时操作,但起作用的只是P0^0。
除了这种总线操作的方法,我们还学习过位操作,利用51 编译器的关键字sbit,我们可以定义一个位变量:
那么LED = 0;就点亮了LED,LED = 1;就关闭了LED。为了让程序看起来见名知义,我们定义两个宏:
点亮和关闭LED 的代码就变成了:
上面总线和位操作的的方法,学过51 的朋友是非常熟悉的,也很容易理解。
那么我们再说一下大家容易忽略的几个知识点。
▶▶1 什么是寄存器
在点亮LED 的时候,我们都是用操作寄存器的方法来实现的,那大家是否想过,这个寄存器到底是什么?为什么我们可以直接操作P0 口?
解答上面的问题之前,我们先简单介绍下51 单片机的主要组成部分,这对我们学习其他单片机也有好处。
我们以国内的STC89C51 为例,该单片机主要由51 内核、外设IP、和总线这三大部分组成。内核是由Intel 公司生产的,外设IP 就是STC 公司在内核的基础上添加的诸如定时器、串口、IO 口等这些东西,总线就是用来连接内核和外设的接口单元。Intel 在这里属于IP 核设计公司,STC 属于IC 设计公司。世界上能设计IP 核的公司屈指可数。我们非常熟悉的ARM 公司就属于IP 核设计公司,ARM 给其他公司授权,其他IC 公司就在ARM 内核上设计出各具特色的MCU,我们后面要学习的STM32 就是属于一中基于ARM 内核的MCU。
寄存器则是内置于各个IP 外设中,是一种用于配置外设功能的存储器,就是一种内存,并且有想对应的地址。学过C 语言我们就知道,要操作这些内存就可以使用C 语言中的指针,通过寻址的方式来操作这些具有特殊功能的内存—寄存器。比如P0 口对应的地址是0X80,那么我们要修改0X80 这个地址对应的内存的内容的话,按照常理可以这样操作:
可当我们编译的时候,编译器会报错,在51 里面只能通过SFR 和SBIT 这两个关键字来实现寄存器映像,不能直接操作寄存器对应的地址,这是51 相较于STM32 不同的地方。
51 单片机的这些寄存器位于地址80H~FFH 中,对应着128 个地址,但不是每个地址都是有效的,51 系列的单片机有21 个,52 系列的则有26 个,其他的都是保留区。
图3 51 寄存器映射
▶▶2 寄存器映射
实际上我们在编程的时候并不是通过指针来操作寄存器的,而是直接给P0、P1 这些端口寄存器赋值。那么这些外设资源是如何与地址建立一一对应的关系(寄存器映射定义),这得益与51 特有的两个关键字:SFR 和sbit,其他单片机没有,只能用其他的方式来实现寄存器映射。这两个关键字帮我们实现了所有寄存器的定义,所以我们才可以像操作普通变量一个来操作寄存器。其实我们一开始提到的点亮LED 的代码,全貌应该是这样的:
为了方便起见,我们可以把寄存器映射全部写好封装在一个头文件里面,不用每用一个寄存器就定义一次。其实这方面的工作不用我们做,我们在编程的时候都会在开始的地方添加一个头文件:
这个头文件已经实现了全部寄存器的定义,该文件是keil 自带,在安装目录:KeilC51INC 下可以找到。这个文件实现了字节寄存器和位寄存器的定义。
▶▶3 启动文件—STARTUP.A51
还有一个就是启动代码,这个也是很多初学者容易忽略的地方,对于这部分我们主要总结下它的功能,不详解讲解里面的代码。
单片机在上电复位后,首先执行的是启动文件—STARTUP.A51,而不是我们通常看到的main 函数。我们新建51 工程的时候会有一个提示:是否拷贝启动代码到当前的工程,我们一般选择是。
图4 是否添加启动代码
启动代码用汇编语言编写,主要实现了以下功能:清除内部数据存储器、清除外部数据存储器、清除外部页储存器、初始化small 模式下的可重入栈和指针、初始化large 模式下可重入栈和指针、初始化compact 模式下的可重入栈和指针、初始化8051 硬件栈指针、传递初始化全局变量的控制命令或者在没有初始化全局变量时给main 函数传递命令。然后程序就跳转到main 函数,来到我们熟知的C 世界。
▶▶4 总结
在讲解用51 点亮LED 的时候,我们补充了什么是寄存器、寄存器映射、启动代码这三部分的内容,这三部分内容本来是放到STM32 里面讲解的,但考虑到大家已经有51 的基础,并且对51 比较熟悉,那我再添加点内容,大家自然没有那么抗拒,并且可以根据上面讲的内容亲自实践,学习得也会更深入。那当我再在STM32 讲解这几个内容的时候,大家就会对比着学习,对STM32 也就没有那么忌惮。
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 的方法,希望可以起到抛砖引玉的作用。
4、再接再厉—构建库的雏形学习STM32 存在着一个用寄存器好还是用库好的争议点,就好比编程是用汇编好还是用C 好一样。其实孰优孰劣,市场自有定论,用户群说明一切。
虽然我们上面用寄存器点亮了LED,乍看一下好像代码也很简单,但是我们别侥幸以后就可以一直用寄存器开发。在用寄存器点亮LED 的时候,我们是否发现STM32 的寄存器都是32 位的,在配置的时候非常容易出错,而且代码还很不好理解。所以学习TM32 最好的方法是用库,然后在库的基础上了解底层,看遍所有寄存器。
但是很多人对库还是很忌惮,因为一开始用库的时候有很多代码,很多文件,不知道如何入手。不知道你是否认同这么一句话:一切的恐惧都来源于认知的空缺。我们对库忌惮那是因为我们不知道什么是库,不知道库是怎么实现的。
接下来,我们在寄存器点亮LED 的代码上继续完善,把代码一层层封装,实现库的最初的雏形,相信经过这一步的学习后,你会对库的运用做到游刃有余。这里我们只讲关于GPIO 库,其他外设的我们直接参考库学习即可,不必自己写。
1定义外设寄存器结构体
上面我们在操作寄存器的时候,操作的是寄存器的绝对地址,如果每个寄存器都这样操作,那将非常麻烦。
我们考虑到外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占32 个或者16 个字节,这种方式跟结构体里面的成员类似。所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。
下面我们先定义一个GPIO 寄存器结构体,结构体里面的成员是GPIO 的寄存器,成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。
在《STM32 中文参考手册》8.2 寄存器描述章节,我们可以找到结构体里面的7 个寄存器描述。在点亮LED 的时候我们只用了CRL 和ODR 这两个寄存器,至于其他寄存器的功能大家可以自行看手册了解。
在GPIO 结构体里面我们用了两个数据类型,一个是uint32_t,表示无符号的32 位整型,因为GPIO 的寄存器都是32 位的。这个类型声明在标准头文件stdint.h 里面,我们在程序上只要包含这个头文件即可。
另外一个是__IO,这个是我们自己定义的,原型是volatile,作用就是告诉编译器不要因优化而省略此指令,必须每次都直接读写其值,这样就能确保每次读或者写寄存器都真正执行到位。
关于这两个数据类型,我们添加如下代码:
2外设声明
现在GPIO 寄存器结构体已经定义好了,STM32F1 系列的GPIO 端口分A~G,即GPIOA、GPIOB。。。。。。GPIOG。每个端口都含有GPIO_TypeDef 结构体里面的寄存器,我们可以根据各个端口的基地址把GPIO 的各个端口定义成一个GPIO_TypeDef 类型的指针,然后我们就可以根据端口名(实际上现在是结构体指针了)来操作各个端口的寄存器,码实现如下:
对于其他外设我们也可以这样把外设的名字定义成一个外设寄存器结构体类型的指针,这里我们只讲GPIO。
对于每个GPIO 的基地址我们可以从《STM32 中文参考手册》2.3 小节:存储器映像中找到,如下所示:
图6 APB2 总线外设寄存器起始地址
3外设内存映射
讲到基地址的时候我们再引人一个知识点:Cortex-M3 存储器系统,这个知识点在《Cortex-M3 权威指南》第5 章里面讲到。CM3 的地址空间是4GB,如下图所示:
图7 CM3 内存映射
我们这里要讲的是片上外设,就是我们所说的寄存器的根据地,其大小总共有512MB,512MB 是其极限空间,并不是每个单片机都用得完,实际上各个MCU 厂商都只是用了一部分而已。STM32F1 系列用到了:0x4000 0000 ~0x5003 FFFF。
▶▶1 APB1、APB2、AHB 总线基地址
现在我们说的STM32 的寄存器就是位于这个区域,这里面ST 设计了三条总线:AHB、APB2 和APB1,其中AHB 和APB2 是高速总线,APB1 是低速总线。不同的外设根据速度不同分别挂载到这三条总线上。从下往上依次是:APB1、APB2、AHB,每个总线对应的地址分别是:APB1:0x40000000,APB2:0x4001 0000,AHB:0x4001 8000。
这三条总线的基地址我们是从《STM32 中文参考手册》2.3 小节—存储器映像得到的:APB1 的基地址是TIM2 定时器的起始地址,APB2 的基地址是AFIO 的起始地址,AHB 的基地址是SDIO 的起始地址。
其中APB1 地址又叫做外设基地址,是所有外设的基地址,叫做PERIPH_BASE。
现在我们把这三条总线地址用宏定义出来,以后我们在定义其他外设基地址的时候,只需要在这三条总线的基址上加上偏移地址即可,代码如下:
▶▶2 GPIO 端口基地址
因为GPIO 挂载到APB2 总线上,那么现在我们就可以根据APB2 的基址算出各个GPIO 端口的基地址,用宏定义实现代码如下:
现在我们把上面的代码稍微整理下,如下:
在点亮LED 的时候,我们还开了GPIO 的时钟,用到了RCC 这个外设,现在我们也定义一个RCC 寄存器结构体,加上那些地址定义,总体代码如下:
跟GPIO 不同的是,RCC 这个外设是挂载到AHB 总线上。
现在我们点亮LED 的函数就变成了
对比之前的代码
一个用的是结构体,一个用的是宏,仅仅从这三行代码看不出有啥区别,但是如果要操作其他寄存器的时候,用结构体就可以直接操作,用宏就还要一个个找到寄存器的绝对地址重新定义。
比如我们要操作GPIOB 的BSRR(bit reset register)的时候,用结构体时我们就可以这样操作:
这时候PB0 就输出低电平,LED 被点亮。注意:BRR 低16 位有效,只能以字的形式操作,功能是复位相应的IO 口,写1 清0,写0 没有影响。
图8 GPIO 端口位清除寄存器
现在我们再整理下代码,如下所示:
4小结流程
现在我们来总结下上面代码实现的过程,这个过程也是我们从零开始点亮LED 的过程,代码全部由我们自己编写(除了启动代码),每一行都有根有据,都可以从《STM32中文参考手册》查到。
①、定义一个外设(GPIO)寄存器结构体,结构体的成员包含该外设的所有寄存器,成员的排列顺序跟寄存器偏移地址一样,成员的数据类型跟寄存器的一样。
②外设内存映射,即把地址跟外设建立起一一对应的关系。51 单片机中用SFR 实现,STM32 中用宏定义实现。
③外设声明,即把外设的名字定义成一个外设寄存器结构体类型的指针。
④操作寄存器,实现点亮LED。
5新建头文件stm32f10x.h
为了使代码看起来不那么臃肿,我们这里引入文件的概念,让不同功能的代码放在不同的文件里面。在main.c 里面我们只保留main 函数和一些头文件,把其他的宏定义放到一个单独的文件。
新建一个stm32f10x.h,跟寄存器相关的代码都放在这里,主要是寄存器映像,跟51单片机里面的reg51.h 这个头文件差不多。然后我们在main.c 里面包含这个头文件即可,现在我们的主函数就变成这样:
6新建tm32f10x_gpio.h
上面我们在控制GPIO 输出内容的时候控制的是ODR(Output data register)寄存器,ODR 是一个16 位的寄存器,必须以字的形式控制,相当于51 里面的总线操作。
其实我们还可以控制BSRR 和BRR 这两个寄存器来控制IO 的电平,下面我们简单介绍下BRR 寄存器的功能,BSRR 自行看手册研究。
BRR:bit reset register
图9 GPIO 端口位清除寄存器
位清除寄存器BRR 只能实现位清0 操作,是一个32 位寄存器,低16 位有效,写0 没影响,写1 清0。
现在我们要使PB0 输出低电平,点亮LED,则只要往BRR 的BR0 位写1 即可,其他位为0,代码如下:
这时PB0 就输出了低电平,LED 就被点亮了。
如果要PB2 输出低电平,则是:
如果要PB3/4/5/6。。。。。。这些IO 输出低电平呢?道理是一样的,只要往BRR 的相应位置赋不同的值即可。因为BRR 是一个16 位的寄存器,位数比较多,赋值的时候容易出错,而且从赋值的16 进制数字我们很难清楚的知道控制的是哪个IO。这时,我们是否可以把BRR 的每个位置1 都用宏定义来实现,如GPIO_Pin_0 就表示0X0001,GPIO_Pin_2 就表示0X0004。只要我们定义一次,以后都可以使用,而且还见名知意。
GPIO_pins_define 代码如下:
这时PB0 就输出了低电平的代码就变成了:
为了不使main 函数看起来冗余,GPIO_pins_define 的代码不应该放在main 里面,因为其是跟GPIO 相关的,我们可以把这些宏放在一个单独的头文件里面。
在工程目录下新建stm32f10x_gpio.h,把GPIO_pins_define 代码放里面,然后把这个文件添加到工程里面。这时我们只需要在main.c 里面包含这个头文件即可。
7新建stm32f10x_gpio.c
我们点亮LED 的时候,控制的是PB0 这个IO,如果LED 接到的是其他IO,我们就需要把GPIOB 修改成其他的端口,其实这样修改起来也很快很方便。但是为了提高程序的可读性和可移植性,我们是否可以编写一个专门的函数用来复位GPIO 的某个位,这个函数有两个形参,一个是GPIOX(X=A...G),另外一个是GPIO_Pin(0...15),函数的主体则是根据形参GPIOX 和GPIO_Pin 来控制BRR 寄存器,代码如下:
这时,PB0 输出低电平,点亮LED 的代码就变成了:
同样,因为这个函数是控制GPIO 的函数,我们可以新建一个专门的文件来放跟gpio有关的函数。
在工程目录下新建stm32f10x_gpio.c,把GPIO 相关的函数放里面。
这时我们是否发现刚刚新建了一个头文件stm32f10x_gpio.h,这两个文件存放的都是跟外设GPIO 相关的。C 文件里面的函数会用到h 头文件里面的定义,这两个文件是相辅相成的,故我们在stm32f10x_gpio.c 文件中也包含stm32f10x_gpio.h 这个头文件。别忘了把stm32f10x.h 这个头文件也包含进去,因为有关寄存器的所有定义都在这个头文件里面。
如果我们写其他外设的函数,我们也应该跟GPIO 一样,新建两个文件专门来存函数,比如RCC 这个外设我们可以新建stm32f10x_rcc.c 和stm32f10x_rcc.h。其他外依葫芦画瓢即可。
stm32f10x_gpio.c 文件代码如下:
我们还要记得把void GPIO_ResetBits()在stm32f10x_gpio.h 里面声明下,这样其他文件只要包含stm32f10x_gpio.h 这个头文件就可以使用GPIO_ResetBits()这个函数了。以后不论新增加了什么函数都应该在自己的头文件下声明,这是个C 语言的常识问题。
点亮LED 会了,那关闭LED 怎么办,我们可以控制BSRR 这个寄存器来实现,这里我就直接写代码了:
先写一个GPIO 端口置位函数,放到stm32f10x_gpio.c 文件中,同样在stm32f10x_gpio.h 头文件声明。
PB0 输出高电平,关闭LED,代码如下:
现在我们再来看看main 函数,看看点亮LED 的代码是如何一步一步进化的:
8小结
我们从寄存器映像开始,把内存跟寄存器建立起一一对应的关系,然后操作寄存器点亮LED,再到把寄存器操作封装成一个个函数。为了把不同外设的函数归类,我们引入了相应的文件来放这些函数,这一步一步走来,我们实现了库最简单的雏形,知道库是怎么来的。后面的工作就是不断的增加操作外设的函数,并且把所有的外设都写完,这样一个完整的库就实现了。
什么是库,这就是库。
下面我们用一张图来描述下我们刚刚的代码,让大家有一个整体的把握。
5、新的尝试—用库函数点亮LED
1新建工程
▶▶1 新建本地工程文件夹
为了工程目录更加清晰,我们在本地电脑上新建6 个文件夹,具体如下:
表格1 工程目录文件夹清单
图10 工程文件夹目录
在本地新建好文件夹后,把准备好的库文件添加到相应的文件夹下:
表格2 工程目录文件夹内容清单
▶▶2 新建工程
打开KEIL5,新建一个工程,工程名根据喜好命名,我这里取LED-LIB,保存在ProjectRVMDK(uv4)文件夹下。
选择CPU 型号
这个根据你开发板使用的CPU 具体的型号来选择,比如MINI 选STM32F103VE,ISO 选STM32F103ZE。
图11 选择具体的CPU 型号
在线添加库文件
等下我们手动添加库文件,这里我们点击关掉。
图12 库文件管理
添加组文件夹
在新建的工程中添加5 个组文件夹,用来存放各种不同的文件,文件从本地建好的工程文件夹下获取:
表格3 工程内组文件夹内容清掉
图13 如何在工程中添加文件夹
配置魔术棒选项卡
这一步的配置工作很重要,很多人串口用不了printf 函数,编译有问题,下载有问题,都是这个步骤的配置出了错。
①在Target 中选中微库,为的是在日后编写串口驱动的时候可以使用printf 函数
图14 添加微库
②在Output 选项卡中把输出文件夹定位到我们工程目录下的output 文件夹,如果想在编译的过程中生成hex 文件,那么那Create HEX File 选项勾上。
图15 配置Output 选项卡
③在Listing 选项卡中把输出文件夹定位到我们工程目录下的Listing 文件夹。
图16 配置Listing 选项卡
④在C/C++选项卡中添加处理宏,和编译器编译的时候查找的头文件路径。
STM32F10X_HD:这个宏是为了区分使用STM32F103 系列中不同容量型号的单片机库。我们用的单片机的FLASH 的容量都是512K,属于大容量
STM32F10X_HD:FLASH 大小在256K~512K 之间的STM32F101xx 和STM32F103xx控制器。STM32F10X_MD:FLASH 大小在64K~128K 之间的STM32F101xx 和STM32F103xx 控制器。STM32F10X_LD:FLASH 大小在16K~32K 之间的STM32F101xx和STM32F103xx 控制器。
USE_STDPERIPH_DRIVER:为了包含stm32f10x_conf.h 这个头文件。
在编译器中添加宏的好处就是,只要用了这个模版,就不用源文件中修改代码或者添加头文件。
图17 配置C/C++ 选项卡
Include Paths 这里添加的是头文件的路径,如果编译的时候提示说找不到头文件,一般就是这里配置出了问题。你把头文件放到了哪个文件夹,就把该文件夹添加到这里即可。
下载器配置
这部分的配置最好是在安装好下载器驱动,下载器连接了电脑和开发板,且开发板上电后来配置。
这里面需要根据你使用了什么仿真器来配置,常用的有三种仿真器:JLINK/ARMOB,ST-LINK,ULINK2,而且这个配置不是配置完一次之后以后就不会改变,当你换了芯片型号,或者其他操作(具体原因不明)都会改变下载器的配置。
①JLINK/ARM-OB 配置
要先安装了JLINK 驱动之后,该配置才能下载,两者缺一不可。
图18 JLINK/ARM-OB 下载配置
②ST-LINK 配置
要先安装了ST-LINK 驱动之后,该配置才能下载,两者缺一不可。
图19 ST-LINK 下载配置
③ULINK2 配置
要先安装了ULINK2 驱动之后,该配置才能下载,两者缺一不可。要注意的是设置成ULINK2,而不是ULINK。
图20 ULINK2 下载配置
选择CPU 型号
这一步的配置也不是配置一次之后完事,常常会因为各种原因需要重新选择,当你下载的时候,提示说找不到Device 的时候,请确保该配置是否正确。有时候下载程序之后,不会自动运行,要手动复位的时候,也回来看看这里的Reset and Run 配置是否失效。MINI 和ISO 用的STM32 的FLASH 都是512K,所以选择512K 大容量,如果使用的是其他型号的,要根据实际情况选择。
2固件库分析
在写代码之前,我们先来分析下固件库,看看每个文件的作用是什么,这对我们能否清晰的调用库函数编程非常重要。
STM32 由Cortex-M3 内核和内核之外的各种外设组成,库在编写的时候也遵循这中组成结构,把代码分成两大部分,一种是操作内核外设的,另外一种是内核之外的外设,为了听起来不那么绕,下面我们把内核之外的外设用处理器外设来代替。
下面我们大概分析下每个文件的作用。
▶▶1 处理器相关
startup_stm32f10x_hd.s
这个是由汇编编写的启动文件,是STM32 上电启动的第一个程序,启动文件主要实现了:1、初始化堆栈指针SP;2、设置PC 指针=Reset_Handler ;3、设置向量表的地址,并初始化向量表,向量表里面放的是STM32 所有中断函数的入口地址4、调用库函数SystemInit,把系统时钟配置成72M,SystemInit 在库文件stytem_stm32f10x.c 中定义;5、跳转到标号_main,最终去到C 的世界。
system_stm32f10x.c
这个文件的作用是里面实现了各种常用的系统时钟设置函数,有72M,56M,48,36,24,8M,我们使用的是是把系统时钟设置成72M。
Stm32f10x.h
这个头文件非常重要,可以说是上帝之手。这个头文件实现了:1、处理器外设寄存器的结构体定义2、处理器外设的内存映射3、处理器外设寄存器的位定义。
关于1 和2 我们在用寄存器点亮LED 的时候有讲解。其中3:处理器外设寄存器的位定义,这个非常重要,具体是什么意思?我们知道一个寄存器有很多个位,每个位写1 或者写0 的功能都是不一样的,处理器外设寄存器的位定义就是把外设的每个寄存器的每一个位写1 的16 进制数定义成一个宏,宏名即用该位的名称表示,如果我们操作寄存器要开启某一个功能的话,就不用自己亲自去算这个值是多少,可以直接到这个头文件里面找。
我们以片上外设ADC 为例,假设我们要启动ADC 开始转换,根据手册我们知道是要控制ADC_CR2 寄存器的位0:ADON,即往位0 写1,即:ADC->CR2=0x00000001;这是一般的操作方法。现在这个头文件里面有关于ADON 位的位定义:
#define ADC_CR2_ADON ((uint32_t)0x00000001),有了这个位定义,我们刚刚的代码就变成了:ADC->CR2=ADC_CR2_ADON。这对于我们编程是何其方便,简直就是天降救星,感激之情无以言表。
无论是寄存器编程还是固件库编程,都必须包含这个头文件,有关外设寄存器的说明都在这里面。
stm32f10x_xxx.h
stm32f10x_xxx.h:外设xxx 应用函数库头文件,这里面主要定义了实现外设某一功能的结构体,比如通用定时器有很多功能,有定时功能,有输出比较功能,有输入捕捉功能,而通用定时器有非常多的寄存器要实现某一个功能,比如定时功能,我们根本不知道具体要操作哪些寄存器,这个头文件就为我们打包好了要实现某一个功能的寄存器,是以机构体的形式定义的,比如通用定时器要实现一个定时的功能,我们只需要初始化TIM_TimeBaseInitTypeDef 这个结构体里面的成员即可,里面的成员就是定时所需要操作的寄存器。有了这个头文件,我们就知道要实现某个功能需要操作哪些寄存器,然后再回手册中精度这些寄存器的说明即可。
stm32f10x_xxx.c
stm32f10x_xxx.c:外设xxx 应用函数库,这里面写好了操作xxx 外设的所有常用的函数,我们使用库编程的时候,使用的最多的就是这里的函数。
▶▶2 内核相关
cor_cm3.h
这个头文件实现了:1、内核结构体寄存器定义2、内核寄存器内存映射3、内存寄存器位定义。跟处理器相关的头文件stm32f10x.h 实现的功能一样,一个是针对内核的寄存器,一个是针对内核之外,即处理器的寄存器。
misc.h
内核应用函数库头文件,对应stm32f10x_xxx.h。
misc.c
内核应用函数库文件,对应stm32f10x_xxx.c。在CM3 这个内核里面还有一些功能组件,如NVIC、SCB、ITM、MPU、CoreDebug,CM3 带有非常丰富的功能组件,但是芯片厂商在设计MCU 的时候有一些并不是非要不可的,是可裁剪的,比如MPU、ITM 等在STM32 里面就没有。其中NVIC 在每一个CM3 内核的单片机中都会有,但都会被裁剪,只能是CM3 NVIC 的一个子集。在NVIC 里面还有一个SysTick,是一个系统定时器,可以提供时基,一般为操作系统定时器所用。
misc.h 和mics.c 这两个文件提供了操作这些组件的函数,并可以在CM3 内核单片机直接移植。
3开始写代码
▶▶1 如何管理库的头文件
这么多的库文件,如何调用,如何管理?当我们开始调用库函数写代码的时候,有些库我们不需要,在编译的时候可以不编译,可以通过一个总的头文件stm32f10x_conf.h 来控制,该头文件主要代码如下:
代码1 stm32f10x_conf.h 头文件代码
这里面包含了全部外设的头文件,点亮一个LED 我们只需要RCC 和GPIO 这两个外设的库函数即可,其中RCC 控制的是时钟,GPIO 控制的具体的IO 口。所以其他外设库函数的头文件我们注释掉,当我们需要的时候就把相应头文件的注释去掉即可。
stm32f10x_conf.h 这个头文件在stm32f10x.h 这个头文件的最后面被包含,在第8296行:
代码的意思是,如果定义了USE_STDPERIPH_DRIVER 这个宏的话,就包含stm32f10x_conf.h 这个头文件。我们在新建工程的时候,在魔术棒选项卡C/C++中,我们定义了USE_STDPERIPH_DRIVER 这个宏,所以stm32f10x_conf.h 这个头文件就被stm32f10x.h 包含了,我们在写程序的时候只需要调用一个头文件:stm32f10x.h 即可。
▶▶2 编写LED 初始化函数
经过寄存器点亮LED 的操作,我们知道操作一个GPIO 输出的编程要点大概如下:
1、开启GPIO 的端口时钟
2、选择要具体控制的IO 口,即pin
3、选择IO 口输出的速率,即speed
4、选择IO 口输出的模式,即mode
5、输出高/低电平
STM32 的时钟功能非常丰富,配置灵活,为了降低功耗,每个外设的时钟都可以独自的关闭和开启。STM32 中跟时钟有关的功能都由RCC 这个外设控制,RCC 中有三个寄存器控制着所以外设时钟的开启和关闭:RCC_APHENR、RCC_APB2ENR 和RCC_APB1ENR,AHB、APB2 和APB1 代表着三条总线,所有的外设都是挂载到这三条总线上,GPIO 属于高速的外设,挂载到APB2 总线上,所以其时钟有RCC_APB2ENR 控制。
GPIO 时钟控制
固件库函数:RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE)函数的原型为:
当程序编译一次之后,把光标定位到函数/变量/宏定义处,按键盘的F12 或鼠标右键的Go to definition of,就可以找到原型。固件库的底层操作的就是RCC 外设的APB2ENR这个寄存器,宏RCC_APB2Periph_GPIOB 的原型是:0x00000008,即(1<<3),还原成寄存器操作就是:RCC->APB2ENR |= 1<<<3。相比固件库操作,寄存器操作的代码可读性就很差,只有才查阅寄存器配置才知道具体代码的功能,而固件库操作恰好相反,见名知意。
GPIO 端口配置
GPIO 的pin,速度,模式,都由GPIO 的端口配置寄存器来控制,其中IO0~IO7 由端口配置低寄存器CRL 控制,IO8~IO15 由端口配置高寄存器CRH 配置。
寄存器方式
相比寄存器一句话的代码,固件库的操作就显得有些复杂,但换来的是简单明了。固件库把端口配置的pin,速度和模式封装成一个结构体:
pin 可以是GPIO_Pin_0~GPIO_Pin_15 或者是GPIO_Pin_All,这些都是库预先定义好的宏。
speed 也被封装成一个结构体:
速度可以是10M,2M 或者50M,这个由端口配置寄存器的MODE 位控制,速度是针对IO 口输出的时候而言,在输入的时候可以不用设置。
mode 也被封装成一个结构体:
IO 口的模式有8 种,输入输出各4 种,由端口配置寄存器的CNF 配置。平时用的最多的就是通用推挽输出,可以输出高低电平,驱动能力大,一般用于接数字器件。至于剩下的七种模式的用法和电路原理,我们在后面的GPIO 章节再详细讲解。
所以GPIO 端口的配置,最终用固件库实现就变成这样:
配置好pin,speed,mode 之后,我们最后调用库函数GPIO_Init()把刚刚的参数写到CRL 或者CRH 这两个寄存器中。
GPIO 输出控制
GPIO 输出控制,可以通过端口数据输出寄存器ODR、端口位设置/清除寄存器BSRR和端口位清除寄存器BRR 这三个来控制。
端口输出寄存器ODR 是一个32 位的寄存器,低16 位有效,对应着IO0~IO15,只能以字的形式操作,不能单独对某一个位置位/清除。
代码2 寄存器操作ODR
图21 ODR 寄存器
端口位清除寄存器BRR 是一个32 位的寄存器,低十六位有效,对应着IO0~IO15,只能以字的形式操作,可以单独对某一个位操作,写1 清0。
代码3 寄存器操作BRR
代码4 固件库操作BRR
图22 BRR 寄存器
BSRR 是一个32 位的寄存器,低16 位用于置位,写1 有效,高16 位用于复位,写1有效,相当于BRR 寄存器。高16 位我们一般不用,而是操作BRR 这个寄存器,所以BSRR 这个寄存器一般用来置位操作。
代码5 固件库操作BSRR
图23 BSRR 寄存器
LED GPIO 初始化函数
代码6 寄存器LED GPIO 初始化函数
代码7 固件库LED GPIO 初始化函数
软件延时
简单的通过软件来延时,具体时间不确定,并不能像51 那么通过计算每条指令执行的时间来确切的计算延时时间。要想精确延时,必须通过定时器实现。
主函数
初始化LED 用到的GPIO,在while 死循环中让LED 闪烁。在程序来到main 函数前,系统时钟已经初始化成了72M,有关时钟部分我们在RCC 这个章节中会详细讲解,这里不是重点。
GPIO 其他库函数
有关GPIO 的其他库函数,我们可以在stm32f10x_gpio.h 中找到声明,然后在stm32f10x_gpio.c 中找到函数的原型,根据函数的注释,可以知道每个函数的作用。阅读这些库函数的时候,最好配合《STM32 中文参考手册》寄存器描述部分一起看,这样学习的效果会非常好。
全部0条评论
快来发表一下你的评论吧 !