从51到ARM裸机开发实验(004)STM32F401VE GPIO实验

        仿照“从51到ARM裸机开发实验(003) AT89C51 GPIO实验”,同样实现这样一种场景:四个按键作为开关、四个LED作为响应,每个开关控制一盏灯,按一次开灯,再按一次关灯。再接入一个蜂鸣和另外两个按键作为音量加和音量减,音量加每按一次蜂鸣器音量增加5%,音量减每按一次音量减少5%。这次将MCU替换成STM32F401VE芯片,属于ARM-Cortex 系列,为Cortex-M4内核。虽然实现相同的场景,但是开发配置和编码方式却大不一样。从电路图可以看出STM32F401VE有五组IO口,分别为PA~PE。每组有16个引脚,Px0~Px15,但Protues仿真图中没有PB11引脚, STM32F401VE的PB11引脚不能当GPIO使用, 同时必须外接2.2uF电容,在仿真图中略去了。每个GPIO模块内,主要包含了寄存器和驱动器,寄存器是一段特殊的存储器,内核可以通过APB总线对寄存器进行读写;驱动器是用来增强信号的驱动能力。STM32为32位单片机,但每组IO端口却只有16个引脚,是因为只用低16位的端口,高16位没有用到。注意:STM32F401VE必须加载.elf、.hex等可执行文件后才能运行仿真,未加载可执行文件时直接运行仿真会报错

一、仿真电路图设计

Each general-purpose I/O port has four 32-bit configuration registers (GPIOx_MODER,GPIOx_OTYPER, GPIOx_OSPEEDR and GPIOx_PUPDR), two 32-bit data registers(GPIOx_IDR and GPIOx_ODR), a 32-bit set/reset register (GPIOx_BSRR), a 32-bit lockingregister (GPIOx_LCKR) and two 32-bit alternate function selection register (GPIOx_AFRHand GPIOx_AFRL).

         相对于51单片机来说,STM32的功能显然要强大的多,其配置过程也复杂的多,上面是STM32F401xD/E官方文档中关于GPIO配置的介绍。每个通用 I/O 端口包括 4 个 32 位配置寄存器(GPIOx_MODER、GPIOx_OTYPER、GPIOx_OSPEEDR 和 GPIOx_PUPDR)、2 个 32 位数据寄存器(GPIOx_IDR 和GPIOx_ODR)、1 个 32 位置位/复位寄存器 (GPIOx_BSRR)、1 个 32 位锁定寄存器(GPIOx_LCKR) 和 2 个 32 位复用功能选择寄存器(GPIOx_AFRH 和GPIOx_AFRL)。下面列出即将要用到的端口配置,当然这还只是STM32的冰山一角,更多功能请参考官方的芯片手册。

1、端口模式

        根据数据手册中列出的每个 I/O 端口的特性,可通过软件将通用 I/O (GPIO) 端口的各个端口位分别配置为多种模式:
● 输入浮空
● 输入上拉
● 输入下拉
● 模拟功能
● 具有上拉或下拉功能的开漏输出
● 具有上拉或下拉功能的推挽输出
● 具有上拉或下拉功能的复用功能推挽
● 具有上拉或下拉功能的复用功能开漏
每个 I/O 端口位均可自由编程,但 I/O 端口寄存器必须按 32 位字、半字或字节进行访问。
GPIOx_BSRR 寄存器旨在实现对 GPIO ODR 寄存器进行原子读取/修改访问。这样便可确保
在读取和修改访问之间发生中断请求也不会有问题

2、端口配置

2.1、I/O 端口控制寄存器

        每个 GPIO 有 4 个 32 位存储器映射的控制寄存器(GPIOx_MODER、GPIOx_OTYPER、
GPIOx_OSPEEDR、GPIOx_PUPDR),可配置多达 16 个 I/O。GPIOx_MODER 寄存器用于
选择 I/O 方向(输入、输出、AF、模拟)
GPIOx_OTYPER 和 GPIOx_OSPEEDR 寄存器分
别用于选择输出类型(推挽或开漏)和速度 (无论采用哪种 I/O 方向,都会直接将 I/O 速度引
脚连接到相应的 GPIOx_OSPEEDR 寄存器位)。无论采用哪种 I/O 方向,GPIOx_PUPDR 寄
存器都用于选择上拉/下拉

2.2、I/O 端口数据寄存器

每个 GPIO 都具有 2 个 16 位数据寄存器:输入和输出数据寄存器(GPIOx_IDR 和GPIOx_ODR)。GPIOx_ODR 用于存储待输出数据,可对其进行读/写访问。通过 I/O 输入的数据存储到输入数据寄存器 (GPIOx_IDR) 中,它是一个只读寄存器

二、使用Keil5开发STM32

1、地址映射

        STM32为32位单片机,其可访问4G的内存地址,范围为0~(2^32-1),用16进制表示为0x00000000~0xFFFFFFFF。在这4G的内存地址上,有些地址是和各种寄存器对应起来的。比如GPIO控制相关的寄存器就和如下的内存地址相对应。如GPIOA相关的配置寄存器、数据寄存器、速度寄存器等都映射在0x40020000~0x400203FF这段地址空间内,根据芯片手册,对某个地址空间写入数据,就是把数据写入到寄存器中去了。如果写入到配置寄存器,则该端口(引脚)状态按照配置情况生效。如果数据写入到数据寄存器,则根据配置,引脚可输出高低电平或从引脚上读取到0或1。

根据电路图,用到的端口位PA、PB、PD,即GPIOA、GPIOB、GPIOD,其对应相关配置寄存器如下:

1.1、GPIO 端口模式寄存器 (GPIOx_MODER) (x = A..E and H)

复位值(复位的时候此寄存器中的默认值):
● 0xA800 0000(端口 A)
● 0x0000 0280(端口 B)
● 0x0000 0000(其它端口)

 偏移地址:0x00 即
寄存器GPIOA_MODER的地址 = 0x40020000 + 0x00
寄存器GPIOB_MODER的地址 = 0x40020400 + 0x00
寄存器GPIOD_MODER的地址 = 0x40020C00 + 0x00

每个寄存器有32位,每2位控制一个引脚,32位控制16个引脚。2位数据就会有四种状态(模式):

00:输入模式(复位状态)
01:通用输出模式
10:复用功能模式
11:模拟模式

本场景中连接按键的引脚配置为输入模式、其他引脚配置为输出模式。

1.2、GPIO 端口输出类型寄存器 (GPIOx_OTYPER)(x = A..E and H)

复位值(复位的时候此寄存器中的默认值):● 0x0000 0000

 偏移地址:0x04 即
寄存器GPIOA_OTYPER的地址 = 0x40020000 + 0x04
寄存器GPIOB_OTYPER的地址 = 0x40020400 + 0x04
寄存器GPIOD_OTYPER的地址 = 0x40020C00 + 0x04

 此寄存器的高16位为保留,低16位每位控制一个引脚。每位有0和1两种状态:

0:输出推挽(复位状态)(可以输出高、低电平,连接数字器件)
1:输出开漏 (输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行。
适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内))

本场景中采用输出推挽即可。

1.3、GPIO 端口输出速度寄存器  (GPIOx_OSPEEDR)(x = A..E and H)

复位值(复位的时候此寄存器中的默认值):

• 0x0C00 0000 for port A
• 0x0000 00C0 for port B
• 0x0000 0000 for other ports

 偏移地址:0x08 即
寄存器GPIOA_OSPEEDR的地址 = 0x40020000 + 0x08
寄存器GPIOB_OSPEEDR的地址 = 0x40020400 + 0x08
寄存器GPIOD_OSPEEDR的地址 = 0x40020C00 + 0x08

 速度寄存器的配置方式也是每2位控制一个引脚,32位控制16个引脚,2位组合的四种状态如下:

00:2 MHz(低速)
01:25 MHz(中速)
10:50 MHz(快速)
11:30 pF 时为 100 MHz(高速)(15 pF 时为 80 MHz 输出(最大速度))

1.4、GPIO 端口上拉/ 下拉寄存器 (GPIOx_PUPDR)(x = A..E and H)

复位值(复位的时候此寄存器中的默认值):

• 0x6400 0000 for port A
• 0x0000 0100 for port B
• 0x0000 0000 for other ports

 偏移地址:0x0c 即
寄存器GPIOA_PUPDR的地址 = 0x40020000 + 0x0c
寄存器GPIOB_PUPDR的地址 = 0x40020400 + 0x0c
寄存器GPIOD_PUPDR的地址 = 0x40020C00 + 0x0c

同样每2位控制一个引脚,32位控制16个引脚,2位组合的四种状态如下: 

00:无上拉或下拉
01:上拉(上拉后引脚为高电平)
10:下拉(下拉后引脚为低电平)
11:保留

1.5、GPIO 端口输入数据寄存器(GPIOx_IDR) (x = A..E and H)

复位值:0x0000 XXXX(其中 X 表示未定义)

偏移地址:0x10 即

寄存器GPIOA_IDR的地址 = 0x40020000 + 0x10
寄存器GPIOB_IDR的地址 = 0x40020400 + 0x10
寄存器GPIOD_IDR的地址 = 0x40020C00 + 0x10

高16位保留,未使用。低16位分别对应16个引脚, 这些位为只读形式,只能在字模式下访问。它们包含相应 I/O 端口的输入值。

1.6、GPIO 端口输出数据寄存器 (GPIOx_ODR) (x = A..E and H)

复位值:0x0000 0000

偏移地址:0x14

寄存器GPIOA_ODR的地址 = 0x40020000 + 0x14
寄存器GPIOB_ODR的地址 = 0x40020400 + 0x14
寄存器GPIOD_ODR的地址 = 0x40020C00 + 0x14

 高16位保留,低16位对应16个引脚,每位写入1/0即可从引脚输出高低电平。

1.7、RCC AHB1 外设时钟使能寄存器 (RCC_AHB1ENR)

偏移地址:0x30
复位值:0x0010 0000
访问:无等待周期,按字、半字和字节访问。

本场景中使用到了第0、1、3位
位3 GPIODEN :IO 端口 D 时钟使能 (IO port D clock enable)
由软件置 1 和清零。
0:禁止 IO 端口 D 时钟
1:使能 IO 端口 D 时钟
位 2 GPIOCEN :IO 端口 C 时钟使能 (IO port C clock enable)
由软件置 1 和清零。
0:禁止 IO 端口 C 时钟
1:使能 IO 端口 C 时钟
位 1 GPIOBEN :IO 端口 B 时钟使能 (IO port B clock enable)
由软件置 1 和清零。
0:禁止 IO 端口 B 时钟
1:使能 IO 端口 B 时钟
位 0 GPIOAEN :IO 端口 A 时钟使能 (IO port A clock enable)
由软件置 1 和清零。
0:禁止 IO 端口 A 时钟
1:使能 IO 端口 A 时钟

2、创建STM32工程

2.1、工程命名 

 2.2、选择芯片。注意Device一定要选择“Software Packs”,然后搜索STM32F401,选择本次使用的芯片STM32F401VEHx。如果搜索不到则说明此芯片支持包没有安装,需要安装芯片支持包。

 2.3、如果上一步中找不到对应的芯片支持包,则需要安装支持包

 2.4、支持包下载界面如下图所示,左侧搜索到要开发的芯片,右侧是相关支持包。可以直接点击Install。如果下载失败,则下方的信息框中会给出提示和下载地址。可以将下载地址复制出来使用下载工具进行下载(如迅雷等),如果还无法下载,请在可访问Google的环境下进行下载。下载好的支持包可以点击File --> Import进行导入。

 2.5、芯片支持包导入后,Manager Run-Time Environment会出现如下界面,此处CORE和Startup为必选选项。

 2.6、仿照上一篇文章中51单片机GPIO开发那样建立相应分组和文件夹

  2.7、创建完分组和文件夹并完成路径配置后,创建以下文件:

Keil5_STM32F401VE_GPIO_Project\DRIVER\include文件夹下创建:buzzer.h、delay.h、key.h、led.h。
Keil5_STM32F401VE_GPIO_Project\DRIVER\source文件夹下创建:buzzer.c、delay.c、key.c、led.c。并添加到工程的DRIVER分组。
Keil5_STM32F401VE_GPIO_Project\APP文件夹下创建:application.c。并添加到工程的APP分组。

3、GPIO驱动程序

3.1、LED驱动

led.h

#ifndef _LED_H_
#define _LED_H_
	void led_init();
	void led_on(unsigned char site);
	void led_off(unsigned char site);
	char get_led_status(unsigned char site);
	void led_operate(unsigned char site,unsigned char on_off);
#endif

led.c

#include "led.h"

#define GPIOD_MODER (*(volatile unsigned long *)0x40020C00)
#define GPIOD_OTYPER (*(volatile unsigned long *)0x40020C04)
#define GPIOD_PUPDR (*(volatile unsigned long *)0x40020C0C)
#define GPIOD_IDR (*(volatile unsigned long *)0x40020C10)
#define GPIOD_ODR (*(volatile unsigned long *)0x40020C14)
#define RCC_AHB1ENR (*(volatile unsigned long *)0x40023830)

//LED状态输出初始化
void led_set_init(){
	//1、使能GPIOD时钟
	RCC_AHB1ENR |= (0x01<<3);
	//2、后八位置为 01010101 PD0~PD3通用输出
	GPIOD_MODER = (GPIOD_MODER|0x000000ff)&0xffffff55;
	//3、PD0~PD3设为推挽输出
	GPIOD_OTYPER = GPIOD_OTYPER & 0xfffffff0;
}

void led_on(unsigned char site){
	led_set_init();
	switch(site){
			case 0: 
				GPIOD_ODR &= ~(0x01); //PD0置0
				break;
			case 1:
				GPIOD_ODR &= ~(0x01<<1); //PD1置0
				break;
			case 2: 
				GPIOD_ODR &= ~(0x01<<2); //PD2置0
				break;
			case 3: 
				GPIOD_ODR &= ~(0x01<<3); //PD3置0
				break;
			default:
				break;
	}
}

void led_off(unsigned char site){
	led_set_init();
	switch(site){
			case 0: 
				GPIOD_ODR |= (0x01); 	//PD0置1
				break;
			case 1:
				GPIOD_ODR |= (0x01<<1); //PD1置1
				break;
			case 2: 
				GPIOD_ODR |= (0x01<<2); //PD2置1
				break;
			case 3: 
				GPIOD_ODR |= (0x01<<3); //PD3置1
				break;
			default:
				break;
	}
}

char get_led_status(unsigned char site){
		switch(site){
			case 0: 
				return (GPIOD_IDR >> 0) & (0x01);
			case 1:
				return (GPIOD_IDR >> 1) & (0x01);
			case 2: 
				return (GPIOD_IDR >> 2) & (0x01);
			case 3: 
				return (GPIOD_IDR >> 3) & (0x01);
			default:
				return -1;
	}
}
//on_off 0:开灯 1:关灯
void led_operate(unsigned char site,unsigned char on_off){
	if(on_off == 0){
		led_on(site);
	}else if(on_off == 1){
		led_off(site);
	}
}

3.2、Key驱动

key.h

#ifndef _KEY_H_
#define _KEY_H_
	char scan_keyboard();
#endif

key.c

#include "delay.h"
#include "key.h"

#define GPIOA_MODER (*(volatile unsigned long *)0x40020000)
#define GPIOA_OTYPER (*(volatile unsigned long *)0x40020004)
#define GPIOA_PUPDR (*(volatile unsigned long *)0x4002000C)
#define GPIOA_IDR (*(volatile unsigned long *)0x40020010)
#define GPIOA_ODR (*(volatile unsigned long *)0x40020014)
#define RCC_AHB1ENR (*(volatile unsigned long *)0x40023830)

#define GET_GPIOA_IDR(x) ((GPIOA_IDR >> x) & (0x01))
	
//Key状态输入初始化
void key_get_init(){
	//1、使能GPIOA时钟
	RCC_AHB1ENR |= 0x01;
	//2、后12位置为 0000 0000 0000 PA0~PA5输入模式
	GPIOA_MODER = (GPIOA_MODER&0xfffff000);
}

char scan_keyboard(){ //返回当前操作过的按键位置
	key_get_init();
	char site = -1;
	if(GET_GPIOA_IDR(0) == 0){
		delayms(10);
		if(GET_GPIOA_IDR(0) == 0){
			while(GET_GPIOA_IDR(0)==0);
			site = 0;
		}
	}else if(GET_GPIOA_IDR(1) == 0){
		delayms(10);
		if( GET_GPIOA_IDR(1) == 0){
			while(GET_GPIOA_IDR(1) == 0);
			site = 1;
		}
	}else if(GET_GPIOA_IDR(2) == 0){
		delayms(10);
		if( GET_GPIOA_IDR(2) == 0){
			while(GET_GPIOA_IDR(2) == 0);
			site = 2;
		}
	}else if(GET_GPIOA_IDR(3) == 0){
		delayms(10);
		if( GET_GPIOA_IDR(3) == 0){
			while(GET_GPIOA_IDR(3) == 0);
			site = 3;
		}
	}else if(GET_GPIOA_IDR(4) == 0){
		delayms(10);
		if( GET_GPIOA_IDR(4) == 0){
			while(GET_GPIOA_IDR(4) == 0);
			site = 4;
		}
	}else if(GET_GPIOA_IDR(5) == 0){
		delayms(10);
		if( GET_GPIOA_IDR(5) == 0){
			while(GET_GPIOA_IDR(5) == 0);
			site = 5;
		}
	}
	return site;
}

delay.h

#ifndef _DELAY_H_
#define _DELAY_H_
	void delayms(unsigned int xms);
#endif

delay.c

#include "delay.h"

void delayms(unsigned int xms){	//毫秒级延时函数
	unsigned int i,j;
	for(i=xms;i>0;i--){
		for(j=1500;j>0;j--);
	}
}

3.3、蜂鸣器驱动

buzzer.h

#ifndef _BUZZER_H_
#define _BUZZER_H_
	void buzzer_open();
	void buzzer_off();
#endif

buzzer.c

#include "buzzer.h"

#define GPIOB_MODER (*(volatile unsigned long *)0x40020400)
#define GPIOB_OTYPER (*(volatile unsigned long *)0x40020404)
#define GPIOB_PUPDR (*(volatile unsigned long *)0x4002040C)
#define GPIOB_IDR (*(volatile unsigned long *)0x40020410)
#define GPIOB_ODR (*(volatile unsigned long *)0x40020414)
#define RCC_AHB1ENR (*(volatile unsigned long *)0x40023830)

//LED状态输出初始化
void buzzer_set_init(){
	//1、使能GPIOB时钟
	RCC_AHB1ENR |= (0x01<<1);
	//2、后2位置为 01 PB0通用输出
	GPIOB_MODER = (GPIOB_MODER|0x00000003)&0xfffffff1;
	//3、PB0设为推挽输出
	GPIOB_OTYPER = GPIOB_OTYPER | 0xfffffffe;
	//4、后2位置为 01 PB0使用上拉
	GPIOB_PUPDR = (GPIOB_PUPDR|0x00000003)&0xfffffff1;
}

void buzzer_open(){
	buzzer_set_init();
	GPIOB_ODR |= (0x01); 	//PB0置1
}

void buzzer_off(){
	buzzer_set_init();
	GPIOB_ODR &= ~(0x01); //PB0置0
}

4、GPIO应用程序

application.c

#include "led.h"
#include "key.h"
#include "buzzer.h"
#define MAX_VOL 20
#define MIN_VOL 1

int main(void){
	unsigned char volume = 10;
	//周期计数,忽略键盘扫描,用于蜂鸣器控制
	//如volume = 10,即20个周期有10个输出高电平
	unsigned char cycle = 0;
	led_operate(0,1);
	led_operate(1,1);
	led_operate(2,1);
	led_operate(3,1);
	while(1){
		unsigned char key_site = scan_keyboard(); //扫描按键状态
		char led_status = -1;
		//延迟10ms给蜂鸣器一个响应时间,否则在Proteus仿真环境下蜂鸣器可能由于得不到(电脑的)CPU而不响
		delayms(10);	
		cycle++;
		switch(key_site){
				case 0:													//按键一被按了一次
						led_status = get_led_status(0);
						if(led_status == 0){
								led_operate(0,1);						//D1状态改变
						}else if(led_status == 1){
								led_operate(0,0);						//D1状态改变
						}
						break;
				case 1:
						led_status = get_led_status(1);
						if(led_status == 0){
								led_operate(1,1);						//D2状态改变
						}else if(led_status == 1){
								led_operate(1,0);						//D2状态改变
						}
						break;
					
				case 2:
						led_status = get_led_status(2);
						if(led_status == 0){
								led_operate(2,1);						//D3状态改变
						}else if(led_status == 1){
								led_operate(2,0);						//D3状态改变
						}
						break;
					
				case 3:
						led_status = get_led_status(3);
						if(led_status == 0){
								led_operate(3,1);						//D4状态改变
						}else if(led_status == 1){
								led_operate(3,0);						//D4状态改变
						}
						break;
				case 4:													//蜂鸣器音量+
						if(volume < MAX_VOL){
							volume++;
						}
						break;
				
				case 5:													//蜂鸣器音量-
						if(volume > MIN_VOL){
							volume--;
						}
						break;
					default:
						break;
		}
		if(cycle >= 0){
			buzzer_open();
		}if(cycle >= volume){
			buzzer_off();
		}if(cycle >= (MAX_VOL+1)){
			cycle = 0;
		}
	}
	return 0;
}

在Proteus仿真中蜂鸣器的音量变化不太明显,所以在蜂鸣器电路中加了个指示灯。调整音量的时候可以看到指示灯一个周期内亮灭时间变化。音量越大亮的时间越长。

5、编译并仿真

5.1、编译出hex文件。其生成位置在Keil5_STM32F401VE_GPIO_Project\Objects

 5.2、双击STM32F401VE芯片,加载hex文件

此外STM32还有很多开发方式。如基于标准外设库SPL开发、基于HAL开发。还可以使用Eclipse C++进行开发、使用Linux交叉编译进行开发等等。

三、资料下载 

源码与仿真电路下载地址:https://download.csdn.net/download/qq_54140018/87687153

芯片手册与参考资料下载地址:https://download.csdn.net/download/qq_54140018/87687152

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值