单片机实现寄存器点亮LED实验

风流意气都作罢 提交于 2019-12-26 13:56:39

为了顺利过渡到库开发,在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的世界。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!