单片机实现寄存器点亮LED实验
为了顺利过渡到库开发,在STM32编程的开始,我们对照51点亮一个LED的方法,给大家演示一下STM32如何用操作寄存器的方法点亮一个LED,然后再慢慢讲解到底什么是库,让大家知道库跟寄存器的关系。
1. 用51点亮一个LED
在用STM32点亮一个LED之前,我们先来复习下用51如何点亮一个LED。
硬件上我们假设51单片机的P0口的第0位接了一个LED,负逻辑亮。如果我们要点亮这个LED,代码上我们会这么写:
P0 = 0XFE;//总线操作点亮 LED
这时候我们就把LED点亮了,如果要关掉LED ,则是:
P0 = 0XFF;//总线操作关闭 LED
这里面我们用的是总线操作的方法,即是对P0口的8个IO同时操作,但起作用的只是P0^0。
除了这种总线操作的方法,我们还学习过位操作,利用51编译器的关键字sbit,我们可以定义一个位变量:
sbit LED = P0^0;
那么LED = 0,就点亮了LED;
LED = 1,就关闭了 LED。
为了让程序看起来见名知义,我们定义两个宏:
#define ON 0 #define OFF 1
点亮和关闭LED的代码就变成了:
LED = ON;//位操作点亮LED LED = OFF;//位操作关闭LED
稍微整理一下代码,整体效果就是:
//假设51单片机的PO~0口接LED,负逻辑点亮 #define ON 0 #define OFF 1 sbit LED = P0^0; void main(void) { P0 = 0XFE;//总线操作点亮LED P0 = 0XEF;//总线操作关闭LED LED = ON;//位操作点亮LED LED = OFF;//位操作关闭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 这个地址对应的内存的内容的话,按照常理可以这样操作:
*(*0X80) = 0XFE;//点亮LED
可当我们编译的时候,编译器会报错,在51里面只能通过sfr和sbit这两个关键字来实现寄存器映象,不能直接操作寄存器对应的地址,这是51相较于STM32不同的地方。
51单片机的这些寄存器位于地址80H~FFH中,对应着128个地址,但不是每个地址都是有效的,51系列的单片机有21个,52系列的则有26个,其他的都是保留区。
寄存器映射
2. 寄存器映射
实际上我们在编程的时候并不是通过指针来操作寄存器的,而是直接给P0、P1这些端口寄存器赋值。那么这些外设资源是如何与地址建立一一对应的关系(寄存器映射定义),这得益与51特有的两个关键字:sfr和sbit,其他单片机没有,只能用其他的方式来实现寄存器映射。这两个关键字帮我们实现了所有寄存器的定义,所以我们才可以像操作普通变量一样来操作寄存器。其实我们一开始提到的点亮LED的代码,全貌应该是这样的:
sfr P0 = 0x80;//寄存器定义 P0 = 0XFE;//总线操作点亮LED
为了方便起见,我们可以把寄存器映射全部写好封装在一个头文件里面,不用每用一个寄存器就定义一次。其实这方面的工作不用我们做,我们在编程的时候都会在开始的地方添加一个头文件:
#include <reg51.h>
这个头文件已经实现了全部寄存器的定义,该文件是keil自带,在安装目录:
Keil\C51\INC下可以找到。这个文件实现了字节寄存器和位寄存器的定义。
1 /*----------------------------------------------------------------- 2 3 REG51.H 4 5 Header file for generic 80C51 and 80C31 microcontroller. 6 7 Copyright(c)1988-2002 Keil Elektronik GmbH and Keil Software,Inc. 8 9 All rights reserved. 10 11 -------------------------------------------------------------------*/ 12 13 #ifndef __REG51_H_ 14 15 #define __REG51_H_ 16 17 /*BYTE Register */ 18 19 sfr P0 = 0x80; 20 21 sfr P1 = 0×90; 22 23 sfr P2 = 0xA0; 24 25 sfr P3 = 0xB0; 26 27 sfr PSW = 0xD0; 28 29 sfr ACC = 0xE0; 30 31 sfr B = 0XF0; 32 33 sfr SP= 0×81; 34 35 sfr DPL= 0×82; 36 37 sfr DPH = 0×83; 38 39 sfr PCON = 0×87; 40 41 sfr TCON = 0x88; 42 43 sfr TMOD = 0×89; 44 45 sfr TL0 = 0×8A; 46 47 sfr TL1 = 0×8B; 48 49 sfr TH0 = 0×8C; 50 51 sfr TH1 = 0×8D; 52 53 sfr IE = 0xA8; 54 55 sfr IP = 0×B8; 56 57 sfr SCON = 0x98; 58 59 sfr SBUF = 0x99; 60 61 /*BIT Register */ 62 63 /*PSW*/ 64 65 sbit CY = 0xD7; 66 67 sbit AC = 0xD6; 68 69 sbit F0 =0xD5 70 71 sbit RS1 = 0xD4; 72 73 sbit RS0 = 0xD3; 74 75 sbit OV = 0xD2; 76 77 sbit P = 0xD0; 78 79 80 81 /*TCON*/ 82 83 sbit TF1 = 0x8F; 84 85 sbit TR1 = 0x8E; 86 87 sbit TF0 = 0×8D; 88 89 sbit TR0 = 0×8C; 90 91 sbit IE1 = 0×8B; 92 93 sbit IT1 = 0×8A; 94 95 sbit IE0 = 0×89; 96 97 sbit IT0 = 0×88; 98 99 100 101 /*IE*/ 102 103 sbit EA = 0XAF; 104 105 sbit ES = 0xAC; 106 107 sbit ET1 = 0xAB; 108 109 sbit EX1 = 0xAA; 110 111 sbit ET0 = 0xA9; 112 113 sbit EX0 = 0xA8; 114 115 116 117 /*IP*/ 118 119 sbit PS = 0xBC; 120 121 sbit PT1 =0xBB; 122 123 sbit PX1 = 0xBA; 124 125 sbit PT0 = 0xB9; 126 127 sbit PX0 = 0xB8; 128 129 130 131 /*P3*/ 132 133 sbit RD = 0xB7; 134 135 sbit WR = 0xB6; 136 137 sbit T1 = 0xB5; 138 139 sbit T0 = 0xB4; 140 141 sbit INT1 = 0xB3; 142 143 sbit INT0 = 0xB2; 144 145 sbit TXD = 0xB1; 146 147 sbit RXD = 0xBO; 148 149 150 151 /*SCON*/ 152 153 sbit SM0 = 0x9F; 154 155 sbit SM1 = 0x9E; 156 157 sbit SM2 = 0x9D; 158 159 sbit REN = 0x9C; 160 161 sbit TB8 = 0x9B; 162 163 sbit RB8 = 0×9A; 164 165 sbit TI = 0x99; 166 167 sbit RI = 0x98; 168 169 #endif
3. 启动文件一STARTUP.A51
还有一个就是启动代码,这个也是很多初学者容易忽略的地方,对于这部分我们主要总结下它的功能,不详解讲解里面的代码。
单片机在上电复位后,首先执行的是启动文件一STARTUP.A51,而不是我们通常看到的main函数。我们新建51工程的时候会有一个提示:是否拷贝启动代码到当前的工程,我们一般选择是。
是否添加启动代码
启动代码用汇编语言编写,主要实现了以下功能:
清除内部数据存储器、清除外部数据存储器、清除外部页储存器、初始化small模式下的可重入栈和指针、初始化large模式下可重入栈和指针、初始化compact模式下的可重入栈和指针、初始化8051硬件栈指针、传递初始化全局变量的控制命令或者在没有初始化全局变量时给main函数传递命令。然后程序就跳转到main函数,来到我们熟知的C世界。
2. 用STM32点亮一个LED
1. 新建工程
用KEIL5新建一个工程,把工程放在一个事先建好的文件夹内,工程命名为REG后保存。然后在工程目录下添加启动文件:startup_stm32fl0x_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、跳转到标号_main,最终来到C的世界。
这里我们先去除繁枝细节,挑重点的讲,主要理解第4和第5点,在启动文件的147~155行,是复位处理函数,代码如下:
1 ;Reset handler 2 Reset Handler PROC 3 EXPORT Reset_Handler [WEAK] 4 IMPORT __main 5 IMPORT SystemInit 6 LDR R0, =SystemInit 7 BLX R08LDRRO,=main 8 LDR R0, =__main 9 BX R0 10 ENDP
这里我们简单介绍下这10行代码。
第1行是程序注释,在汇编里面注释用的是“;”,跟C语言不一样。
第2行是定义了一个子程序:Reset_Handler。PROC是子程序定义伪指令。一般用法为:
1 子程序名 PROC NEAR(或FAR) 2 3 ...... 4 5 ret 6 7 子程序名 ENDP
其中NEAR和FAR是属性词。
NEAR属性(段内近调用):调用程序和子程序在同一代码段中,只能被相同代码段的其他程序调用。
FAR属性(段间远调用):调用程序和子程序不在同一代码段中,可以被相同或不同代码段的程序调用。
第3行EXPORT表示Reset_Handler这个子程序可供其他模块调用。关键字[WEAK]表示弱定义,如果编译器发现在别处定义了同名的函数,则在链接时用别处的地址进行链接,如果其它地方没有定义,编译器也不报错,以此处地址进行链接。
第4行和第5行IMPORT说明Systemlnit和__main这两个标号在其他文件,在链接的时候需要到其他文件去寻找。
Systemlnit在库文件system_stm32f10x.c实现,用来初始化STM32的一系列时钟,把系统时钟设置为72MHZ。STM32的时钟比51单片机复杂,需要经过一系列的配置才能达到稳定运行的状态。
__main其实不是我们定义的,当编译器编译时,只要遇到这个标号就会定义这个函数,该函数的主要功能是:负责初始化栈、堆,配置系统环境,并在最后跳转到用户自定义的main函数,从此来到C的世界。
第6行把Systemlnit的地址加载到寄存器R0。
第7行程序跳转到R0中的地址执行程序,之后系统的时钟就被设置成72MHZ。
第8行把__main的地址加载到寄存器R0。
第9行程序跳转到R0中的地址执行程序,执行完毕之后就去到我们熟知的C世界。
第10行表示子程序的结束。
总结下就是,Reset_Handler这个函数执行了两个函数调用,一个是Systemlnit,把系统时钟设置成72M,另一个是__main,初始化好系统环境,最终调用C的main,从此去到C的世界。
__main函数由编译器生成,负责初始化栈、堆等,并在最后跳转到用户自定义的main()函数,来到C的世界。