一、相关原理
(一)芯片的地址映射和寄存器映射原理
1.什么是寄存器?
- 现代的计算机主要包括三级存储,寄存器、内存储器和外存储器,存储数据的速率也依次递减。
- 我们不妨将寄存器和内存储器都抽象成一个大的数组,其中的每个元素都有一个字节(8位)大小,CPU寻址的时候就是以该元素为最小单位完成的。如前一个元素的地址是0x1FFFFFF0的话,那么下一个元素的地址就是0x1FFFFFF1。我们可以理解为硬件构成上寄存器和内存储器也都是由一个8位大小的元器件线性排列组成的,地址对应着上面讲到的数组中元素的地址。
- 寄存器是 CPU 内部的构造,它主要用于信息的存储。
- 为什么会出现寄存器?
我们知道,程序在内存中装载,由 CPU 来运行,CPU 的主要职责就是用来处理数据。那么这个过程势必涉及到从存储器中读取和写入数据,因为它涉及通过控制总线发送数据请求并进入存储器存储单元,通过同一通道获取数据,这个过程非常的繁琐并且会涉及到大量的内存占用,而且有一些常用的内存页存在,其实是没有必要的,因此出现了寄存器,存储在 CPU 内部。
2.地址映射和寄存器映射原理:
- 我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射,那什么叫寄存器映射?寄存器到底是什么?
- 说到映射大家可能就会想到函数映射,脑海里会有一个画面:左边一个集合中的某个元素“射”出一条带箭头的直线指向右边的集合的某个元素。
- 其实外围设备的内存映射原理是一样的,只不过左边的集体变成了CPU,右边的集合变成了外围设备,那条带箭头的线就是连接CPU和外设地址引脚的地址总线。
- 存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射。
- 要知道,一个开发板的CPU地址引脚并不是所有的都与内存元器件相连的,如果该板上有外设(如一块独立显卡),那么CPU就需要分出一些引脚来与该外设的地址引脚相连,相当于将一部分内存寻址的空间分给了外设,那不相当于CPU分出去地址寻址空间为空?
- 事实并非如此,一般的外设为了加快处理速度都有自己的片内RAM(比如说显存,你也知道显存对显卡性能的重要性),分出去的地址空间也就与片内RAM物理连接起来,这样CPU就能像访问内存一样去访问外设的片内RAM,这也就是所谓的内存映射。
- 在存储器的区域单元中,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
(二)GPIO端口的初始化设置步骤
1、单片机的时钟
- 时钟是什么?
stm32的时钟是由内部或外部振荡器产生的“频率”,而被人们形象的称为“系统时钟”。最大为72MHz换成周期T为:1/72MHz≈13.9ns。 - 为什么要用时钟?
因为耗电量,stm32功能强大,能做很多事,但与之同时带来的消耗也越严重,当stm32不引入时钟的话,就像51一样外设全开,相应耗电就很严重了,所以厂家(st公司)为了解决这个问题,引入了“时钟概念”,即使用哪个外设就给哪个外设时钟(频率),不使用的就关掉(不震荡)。此做法大大降低了功耗,续航持久。 - 在51单片机中一个时钟把所有的都包了,而stm32的时钟是有分工的,并且每类时钟的频率不一样,因为没必要所有的时钟都是最高频率,只要够用就行,好比一个门出来水流大小,如果只要洗脸,但是出来的是和洪水一样涌出来的水,那就没必要了,消耗能源也多,所以不同的时钟也会有频率差别,或者在配置的时候可以配置时钟分频。
2、GPIO简介
- GPIO 是通用输入输出端口的简称,简单来说就是 STM32 可控制的引脚,STM32 芯片的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。STM32 芯片的 GPIO 被分成很多组,每组有 16 个引脚。
- 如型号为 STM32F103VET6 型号的芯片有 GPIOA、GPIOB、GPIOC至 GPIOE共 5组 GPIO,芯片一共 100个引脚,其中 GPIO就占了一大部分,所有的 GPIO 引脚都有基本的输入输出功能。
- 最基本的输出功能是由 STM32 控制引脚输出高、低电平,实现开关控制,如把 GPIO引脚接入到 LED灯,那就可以控制 LED灯的亮灭,引脚接入到继电器或三极管,那就可以通过继电器或三极管控制外部大功率电路的通断。
3、GPIO模式
- GPIO的工作模式主要有八种:4种输入方式,4种输出方式。
分别为输入浮空,输入上拉,输入下拉,模拟输入; 输出方式为开漏输出,开漏复用输出,推挽输出,推挽复用输出。
GPIO 8 种工作模式: (1)GPIO_Mode_AIN 模拟输入 (应用ADC模拟输入,或者低功耗下省电) (2)GPIO_Mode_IN_FLOATING 浮空输入 (浮空就是浮在半空,可以被其他物体拉上或者拉下,可以用于按键输入) (3)GPIO_Mode_IPD 下拉输入 (IO内部下拉电阻输入) (4)GPIO_Mode_IPU 上拉输入 (IO内部上拉电阻输入) (5)GPIO_Mode_Out_OD 开漏输出(开漏输出:输出端相当于三极管的集电极. 要得到高电平状态需要上拉电阻才行) (6)GPIO_Mode_Out_PP 推挽输出 (推挽就是有推有拉电平都是确定的,不需要上拉和下拉,IO输出0-接GND, IO输出1 -接VCC,读输入值是未知的 ) (7)GPIO_Mode_AF_OD 复用开漏输出(片内外设功能(I2C的SCL,SDA)) (8)GPIO_Mode_AF_PP 复用推挽输出 (片内外设功能TX1,MOSI,MISO.SCK.SS)
4、输入和输出
- 简单来说:输出是CPU计算后进行控制,输入是读取后给CPU进行计算。
- 输入 进行数据的采集,外部电路通过IO口输入模拟量,然后通过“TTL肖特基触发器”(肖特基触发器是将相对缓慢变化的模拟信号变成矩形信号,便于后面读取),进入输入数据寄存器,最后就能给CPU读取数据。
- 输出 GPIO的输出与51的 IO口是差不多的概念,都是输出高、低电平来控制外部电路:
- 处理过程:CPU下达输出高或低电平指令,指令配置“位设置/清除寄存器(GPIOx_BSRR)”(设置就是“1”高电平,清除就是“0”低电平),再由位寄存器配置输出数据寄存器(GPIOx_ODR),经过一个选择器(选择是一般输出还是复用功能输出),然后进行输出控制,控制是什么模式:推挽、开漏或者关闭,然后输出高或低电平到IO口。
5、GPIO初始化步骤 第一步:使能GPIOx口的时钟 第二步:指明GPIOx口的哪一位,这一位的速度大小以及模式 第三步:调用GPIOx初始化函数进行初始化 第四步:调用GPIO-SetBits函数,进行相应位的置位
二、连接电路
 
三、点亮LED流水灯
(一)C语言实现
1.新建项目
新建项目,点击Project->New uVision Project。  选择项目路径,文件命名并保存。  选择仿真芯片。  什么都不选,直接OK。  点击Options for Target…,在Output界面下,勾选Create HEX File,才生成hex文件。 
2.添加文件
右击Source Group 1,点击Add New Item to Group‘Source Group 1’···。  选择文件类型,点击C File(.c),输入文件名main,点击Add。  将启动文件复制到项目目录。  复制到此。  右击选择。  添加启动文件。 
3.编写代码
main.c代码:
#define GPIOB_BASE 0x40010C00
#define GPIOC_BASE 0x40011000
#define GPIOA_BASE 0x40010800
#define RCC_APB2ENR (*(unsigned int *)0x40021018)
#define GPIOB_CRL (*(unsigned int *)0x40010C00)
#define GPIOC_CRH (*(unsigned int *)0x40011004)
#define GPIOA_CRL (*(unsigned int *)0x40010800)
#define GPIOB_ODR (*(unsigned int *)0x40010C0C)
#define GPIOC_ODR (*(unsigned int *)0x4001100C)
#define GPIOA_ODR (*(unsigned int *)0x4001080C)
void SystemInit(void);
void Delay_ms(volatile unsigned int);
void Delay_ms( volatile unsigned int t)
{
unsigned int i;
while(t--)
for (i=0;i<800;i++);
}
int main(){
// 开启时钟
RCC_APB2ENR |= (1<<3); // 开启 GPIOB 时钟
RCC_APB2ENR |= (1<<4); // 开启 GPIOC 时钟
RCC_APB2ENR |= (1<<2); // 开启 GPIOA 时钟
// 设置 GPIO 为推挽输出
// 设置 GPIOB 最后四位为 0001 (B0)
GPIOB_CRL |= (1<<0); // 最后一位设置为1
GPIOB_CRL &= ~(0xE); // 倒数二、三、四位设置为0
// 设置 GPIOC 前四位为 0001 (C15)
GPIOC_CRH |= (1<<28); // 第四位设置为1
GPIOC_CRH &= ~(0xE0000000); // 前三位设置为0
// 设置 GPIOA 最后四位为 0001 (A0)
GPIOA_CRL |= (1<<0); // 最后一位设置为1
GPIOA_CRL &= ~(0xE); // 倒数二、三、四位设置为0
// 3个LED初始化为不亮(即高点位)
GPIOB_ODR |= (1<<0); // 最后一位设置为1
GPIOC_ODR |= (1<<15); // 倒数第15位设置为1
GPIOA_ODR |= (1<<0); // 最后一位设置为1
while(1){
GPIOB_ODR &= ~(1<<0); // 点灯1
Delay_ms(1000000);
GPIOB_ODR |= (1<<0); // 灭灯1
Delay_ms(1000000);
GPIOC_ODR &= ~(1<<15); // 点灯2
Delay_ms(1000000);
GPIOC_ODR |= (1<<15); // 灭灯2
Delay_ms(1000000);
GPIOA_ODR &= ~(1<<0); // 点灯3
Delay_ms(1000000);
GPIOA_ODR |= (1<<0); // 灭灯3
Delay_ms(1000000);
}
}
void SystemInit(){
}
编译。 
4.点亮
使用FlyMcu或者Mcuisp都行,如图所示操作。   成功。 点击观看视频效果
(二)、汇编语言实现
步骤和C语言实现大差不差。  添加文件.s文件。 代码:
RCC_APB2ENR EQU 0x40021018;配置RCC寄存器,时钟,0x40021018为时钟地址
GPIOB_BASE EQU 0x40010C00
GPIOC_BASE EQU 0x40011000
GPIOA_BASE EQU 0x40010800
GPIOB_CRL EQU 0x40010C00
GPIOC_CRH EQU 0x40011004
GPIOA_CRL EQU 0x40010800
GPIOB_ODR EQU 0x40010C0C
GPIOC_ODR EQU 0x4001100C
GPIOA_ODR EQU 0x4001080C
Stack_Size EQU 0x00000400;栈的大小
AREA STACK, NOINIT, READWRITE, ALIGN=3 ;NOINIT: = NO Init,不初始化。READWRITE : 可读,可写。ALIGN =3 : 2^3 对齐,即8字节对齐。
Stack_Mem SPACE Stack_Size
__initial_sp
AREA RESET, DATA, READONLY
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
AREA |.text|, CODE, READONLY
THUMB
REQUIRE8
PRESERVE8
ENTRY
Reset_Handler
bl LED_Init;bl:带链接的跳转指令。当使用该指令跳转时,当前地址(PC)会自动送入LR寄存器
MainLoop BL LED_ON_C
BL Delay
BL LED_OFF_C
BL Delay
BL LED_ON_A
BL Delay
BL LED_OFF_A
BL Delay
BL LED_ON_B
BL Delay
BL LED_OFF_B
BL Delay
B MainLoop;B:无条件跳转。
LED_Init;LED初始化
PUSH {R0,R1, LR};R0,R1,LR中的值放入堆栈
;控制时钟
LDR R0,=RCC_APB2ENR;LDR是把地址装载到寄存器中(比如R0)。
ORR R0,R0,#0x1c
LDR R1,=RCC_APB2ENR
STR R0,[R1]
;初始化GPIOA_CRL
LDR R0,=GPIOA_CRL
BIC R0,R0,#0x0fffffff;BIC 先把立即数取反,再按位与
LDR R1,=GPIOA_CRL
STR R0,[R1]
LDR R0,=GPIOA_CRL
ORR R0,#0x00000001
LDR R1,=GPIOA_CRL
STR R0,[R1]
;将PA0置1
MOV R0,#0x01
LDR R1,=GPIOA_ORD
STR R0,[R1]
;初始化GPIOB_CRL
LDR R0,=GPIOB_CRL
BIC R0,R0,#0x0fffffff;BIC 先把立即数取反,再按位与
LDR R1,=GPIOB_CRL
STR R0,[R1]
LDR R0,=GPIOB_CRL
ORR R0,#0x00000001
LDR R1,=GPIOB_CRL
STR R0,[R1]
;将PB0置1
MOV R0,#0x01
LDR R1,=GPIOA_ORD
STR R0,[R1]
;初始化GPIOC
LDR R0,=GPIOC_CRH
BIC R0,R0,#0x0fffffff
LDR R1,=GPIOC_CRH
STR R0,[R1]
LDR R0,=GPIOC_CRH
ORR R0,#0x01000000
LDR R1,=GPIOC_CRH
STR R0,[R1]
;将PC15置1
MOV R0,#0x8000
LDR R1,=GPIOC_ORD
STR R0,[R1]
POP {R0,R1,PC};将栈中之前存的R0,R1,LR的值返还给R0,R1,PC
LED_ON_A
PUSH {R0,R1, LR}
MOV R0,#0x00
LDR R1,=GPIOA_ORD
STR R0,[R1]
POP {R0,R1,PC}
LED_OFF_A
PUSH {R0,R1, LR}
MOV R0,#0x01
LDR R1,=GPIOA_ORD
STR R0,[R1]
POP {R0,R1,PC}
LED_ON_B;亮灯
PUSH {R0,R1, LR}
MOV R0,#0x00
LDR R1,=GPIOB_ORD
STR R0,[R1]
POP {R0,R1,PC}
LED_OFF_B;熄灯
PUSH {R0,R1, LR}
MOV R0,#0x01
LDR R1,=GPIOB_ORD
STR R0,[R1]
POP {R0,R1,PC}
LED_ON_C;亮灯
PUSH {R0,R1, LR}
MOV R0,#0x00
LDR R1,=GPIOC_ORD
STR R0,[R1]
POP {R0,R1,PC}
LED_OFF_C;熄灯
PUSH {R0,R1, LR}
MOV R0,#0x0100
LDR R1,=GPIOC_ORD
STR R0,[R1]
POP {R0,R1,PC}
Delay
PUSH {R0,R1, LR}
MOVS R0,#0
MOVS R1,#0
MOVS R2,#0
DelayLoop0
ADDS R0,R0,#1
CMP R0,#330
BCC DelayLoop0
MOVS R0,#0
ADDS R1,R1,#1
CMP R1,#330
BCC DelayLoop0
MOVS R0,#0
MOVS R1,#0
ADDS R2,R2,#1
CMP R2,#15
BCC DelayLoop0
POP {R0,R1,PC}
NOP
END
四、总结
实验编程部分不难,但是实操非常考验动手能力,遇到太多小问题。线路接错,设置出错等等,总之做这个实验需要耐心和细心。
参考文献
寄存器映射原理详解,GPIO端口的初始化设置步骤 STM32 F103之点亮LED流水灯 (STM32入门学习) STM32寄存器的简介、地址查找,与直接操作寄存器 STM32从地址到寄存器
|