仿照“从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