从51到ARM裸机开发实验(008)STM32F401VE 中断实验

一、场景设计

        中断的概念在上一篇《从51到ARM裸机开发实验(007) AT89C51 中断实验》中已经介绍过,本次使用STM32F401VE来实现一个这样的场景:四个LED依次亮灭,时间间隔最小0.5秒,最大5秒,要求精确延时。使用两个按键分别控制间隔时间的增减,每按一次增或减0.5秒精确延时用定时器中断实现,按键响应使用外部中断实现。(Protues仿真的时间和现实时间差距较大,可以注意Protues左下方的仿真时间或者接示波器观察曲线变化时间间隔,在下面仿真电路一节中会说明)

二、STM32时钟系统

        对于STM32这种高级货,在进行中断系统编程之前必须全面了解下它的时钟系统。时钟是由振荡电路产生的具有周期性的脉冲信号,相当于单片机的心脏,要想使用单片机的外设必须开启相应的时钟。驱动外设的本质是操作寄存器(如串口控制器等),而寄存器是由D触发器构成,而触发器需要时钟才能改写值,所以要想操作寄存器就必须开启对应外设的时钟(如串口控制器的时钟)。对CPU来说,假设CPU在一个时钟周期内执行一条指令(二进制代码),时钟周期等于1/F,为频率的倒数,时钟频率越高则时钟周期越短,就说明相同的时间内CPU能够执行更多的指令,即CPU的运行速度更快。
        STM32时钟系统主要的目的就是给相对独立的外设模块提供时钟,也是为了降低整个芯片的功耗,所有外设时钟默认都是关闭状态,当我们使用某个外设就要开启这个外设的时钟,不同外设需要的时钟频率不同,没必要所有外设都用高速时钟,会造成浪费,而且有些外设也接受不了这么高的频率。这也是为什么STM32有四个时钟源的原因,就是兼容不同速度的外设,STM32的四个时钟源分别为:HSE(高速外部时钟)、 LSE(低速外部时钟)、HSI(高速内部时钟)、LSI(低速内部时钟)。下面对这些时钟源逐个作出解释。

1、系统时钟的来源

        通常比较成熟的开发板在对STM32芯片进行原理图设计时,会使用两个晶振,如下图所示:

在上图中低速晶振通道采用32.768KHz的晶振,接入PC14-OSC32_IN、PC15-OSC32_OUT引脚,高速晶振通道采用8MHz的晶振,接入PH0-OSC_IN、PH1-OSC_OUT引脚。晶振接入芯片后,经过若干次分频或倍频,分配给不同的外设使用,如下图所示。

        从图中可以看出,低速晶振32.768kHz连接LSE,最终到达RTC(Real-time clock)。RTC 是一个独立的定时器。从 Real-time clock可知,RTC可以为系统实时记录当前系统时间和日期,不管芯片有没有掉电。如果想要使用RTC实时记录系统时间,芯片需要接入额外备用电源,通常为纽扣电池。这样以来,RTC在芯片掉电后,可以由电池供电继续运行。对于掉电后不需要记录系统时间的电路板,可以将低速晶振32.768kHz 舍去,精简电路设计。

        高速晶振 8MHz 连接HSE(High-speed external clock signal),最终到达SYSCLK(system clock)。SYSCLK 是系统时钟,为芯片内部各大模块的运转提供动力,不可缺少。高速晶振8MHz作为系统时钟的来源,可以由芯片内部的HSI RC时钟源或芯片外部的 独立时钟源提供。使用芯片内部的HSI RC时钟源做为系统时钟时,时钟常有偏差,可能导致某些外设无法使用。由于芯片内部的 HSI RC 时钟源不够精准,通常采用外部独立时钟源(即外接晶振)来为芯片提供系统时钟。

        HSI(High-Speed Internal Clock signal),高速内部时钟。位于芯片内部,上图中芯片的HSI频率为16M,当HSE故障时,系统时钟 会自动切换到HSI,直到HSE启动成功。可直接作为系统时钟或在2分频后作为PLL输入。HSI RC振荡器能够在不需要任何外部器件的条件下提供系统时钟。它的启动时间比HSE晶体振荡器短。然而,即使在校准之后它的时钟频率精度仍较差。

         LSI(Low-Speed Internal Clock signal),低速内部时钟。LSI RC位于芯片内部,担当一个低功耗时钟源的角色,它可以在停机和待机模式下保持运行,LSI时钟频率大约40kHz(在30kHz和60kHz之间),上图中芯片的LSI为32Hz。其作用是为独立看门狗和自动唤醒单元(RTC)提供时钟。

        从上面可以看出,我们本次实验需要重点关注HSE就可以了,HSE只是初始输入,从上图可以看出,时钟最终会汇聚计算出SystemClock,继而分频出APBx,提供给定时器使用。至于时钟如何配置、如何分频、如何倍频等,可以查看数据手册中时钟相关的章节,配置时钟相关的寄存器进行实现,不在本文的讨论范围。对于STM32中,官方提供的启动代码已完成了时钟相关配置。

2、配置实验时钟频率

        对于STM32F401VE来说,官方提供的启动代码中已经包含了时钟相关的配置,如果我们不做任何改动直接使用,则时钟频率为16MHz,原理如下图所示。

        这是一个STM32时钟配置自动化工具的界面,下载地址为:stsw_stm32091_STM32F40x/41x时钟配置工具 | STMCU中文官网

从图中可以看出,如果直接使用官方的启动代码,不做任何修改的情况下,官方代码默认使用的是HSI时钟,即芯片内部高速时钟,HCLK配置为16MHz,我们本次实验中的TIM2采用的时钟也是16MHz。现在在配置工具上进行一些修改,让本次实验使用HSE,即外部高速时钟,并且把HCLK配置为32MHz。

按照上图所示,分别进行1、2、3步配置,会发现TIM2时钟这是也变成了32MHz。第1步即配置使用HSE时钟;第2步将HSE真实的时钟频率填写上去,本次实验中外接的高速晶振为8MHz;第3步将HCLK修改为32MHz。

接下来点击"Run"按钮,会弹出一个选择框,选择HSE选项,点击OK。接下来点击"Generate"按钮,就会在时钟配置工具所在的目录下生成一个"system_stm32f4xx.c"文件,将这个文件替换掉Keil开发环境中原来的那个system_stm32f4xx.c文件即可。

        替换掉之后可能会报一些错误,对照着改掉即可。比如本次实验中有三个值找不到,一个类型宏定义找不到。

将三个宏定义加上,__I uint8_t 修改为 const uint8_t即可。三个宏定义分别为HSE时钟频率(要和外接的高速晶振值保持一致)、HSI时钟频率(本次实验中没用到)、HSE启动超时时间(参考官方设置,设置为0x0500即可)。

三、STM32中断系统

1、定时器中断

查阅芯片手册可知以下定时器中断相关信息,本实验中采用TIM2定时器:

1、在递增计数模式下,计数器从 0 计数到自动重载值(TIMx_ARR 寄存器的内容),然后重新从 0 开始计数并生成计数器上溢事件。每次发生计数器上溢时会生成更新事件,或将 TIMx_EGR 寄存器中的 UG 位置 1(通过软件或使用从模式控制器)也可以生成更新事件。

2、TIM2时钟使能。这里我们通过 APB1ENR 的第1位来设置TIM2的时钟,因为 Stm32_Clock_Init 函数里面把APB1的分频设置为4了,所以我们的TIM2时钟就是APB1时钟的2倍,等于系统时钟(84M)。

3、设置 TIM2_ARR 和 和 TIM2_PSC  的值。通过这两个寄存器,我们来设置自动重装的值,以及分频系数。这两个参数加上时钟频率就决定了定时器的溢出时间。

4、设置TIM2_DIER允许更新中断。因为我们要使用TIM2的更新中断,所以设置DIER的UIE位为 1,使能更新中断。

5、允许TIM2工作。光配置好定时器还不行,没有开启定时器,照样不能用。我们在配置完后要开启定时器,通过TIM2_CR1的 CEN 位来设置。

6、TIM2中断分组设置。在定时器配置完了之后,因为要产生中断,必不可少的要设置NVIC相关寄存器,以使能TIM2中断。

7、编写中断服务函数。在最后,还是要编写定时器中断服务函数,通过该函数来处理定时器产生的相关中断。在中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作,我们这里使用的是更新(溢出)中断,所以在状态寄存器SR的最低位。在处理完中断之后应该向TIM2_SR的最低位写0,来清除该中断标志。

8、TIM2 寄存器基地址 0x40000000

9、TIM2控制寄存器1(TIM2_CR1)
位0CEN:计数器使能 (Counter enable)-- 0:禁止计数器、1:使能计数器
注意:只有事先通过软件将CEN位置1,才可以使用外部时钟、门控模式和编码器模式。而触发模式可通过硬件自动将CEN位置1。在单脉冲模式下,当发生更新事件时会自动将 CEN 位清零。

10、TIM2预分频器 (TIM2_PSC)位 15:0 PSC[15:0]:
预分频器值 (Prescaler value)
计数器时钟频率CK_CNT等于Fck_psc/(PSC[15:0]+1)。
PSC 包含在每次发生更新事件时要装载到实际预分频器寄存器的值

11、TIM2自动重载寄存器 (TIM2_ARR) CK_CNT = Fck_psc/(PSC[15:0]+1)

12、NVIC_IPRx (x = 0 ~ 59)字节可访问寄存器提供8位优先级字段IP[N](N = 0到239),用于240个中断中的每个中断。每个寄存器保存4个IP[N]字段CMSIS中断优先级数组 NVIC_IPR0 对应 IP[0]~IP[3] 即中断0~3,NVIC_IPR1 对应 IP[4]~IP[7] 即中断4~7。查阅中断向量表得知 TIM2对应的中断号为28 NVIC_IPR7 对应 IP[28]~IP[31],即NVIC_IPR7的低4位对应TIM2的优先级。NVIC_IPR7对应偏移地址:0x41c,为0xE000E41C

13、NVIC_ISER0位0 ~ 31分别表示中断0 ~ 31
NVIC_ISER1位0 ~ 31分别表示中断32 ~ 63
......以此类推
NVIC_ISER0的第28位(从0位算起)表示TIM2的中断使能
中断设置使能寄存器x (NVIC ISERx)
Bits 31:0 SETENA:中断设置使能位。
写:0:没有效果、1:启用中断
读:0:禁用中断、1:使能中断

14、如果一个挂起的中断被启用,NVIC会根据它的优先级激活这个中断。如果一个中断未启用,断言其中断信号将中断状态更改为挂起,但是NVIC永远不会激活中断,不管它的优先级如何。NVIC_ISER7寄存器的16 ~ 31位保留。

下面的程序设计代码中对每一步都做了详细的注释。

2、外部中断

本实验中采用的STM32外部中断具有如下特性:

1、所有端口都具有外部中断功能。要使用外部中断线,必须将端口配置为输入模式。
PA0、PA1配置为输入模式。

2、外部中断/事件控制器包含多达 23 个用于产生事件/中断请求的边沿检测器。每根输入线都可
单独进行配置,以选择类型(中断或事件)和相应的触发事件(上升沿触发、下降沿触发或
边沿触发)。每根输入线还可单独屏蔽。挂起寄存器用于保持中断请求的状态线。

3、要产生中断,必须先配置好并使能中断线。根据需要的边沿检测设置 2 个触发寄存器,同时在
中断屏蔽寄存器的相应位写“1”使能中断请求。当外部中断线上出现选定信号沿时,便会产
生中断请求,对应的挂起位也会置 1。在挂起寄存器的对应位写“1”,将清除该中断请求。
要产生事件,必须先配置好并使能事件线。根据需要的边沿检测设置 2 个触发寄存器,同时
在事件屏蔽寄存器的相应位写“1”允许事件请求。当事件线上出现选定信号沿时,便会产
生事件脉冲,对应的挂起位不会置 1。
通过在软件中对软件中断/事件寄存器写“1”,也可以产生中断/事件请求。

4、要配置 23 根线作为中断源,请执行以下步骤:
● 配置 23 根中断线的屏蔽位 (EXTI_IMR)
● 配置中断线的触发选择位(EXTI_RTSR 和 EXTI_FTSR)
● 配置对应到外部中断控制器 (EXTI) 的 NVIC 中断通道的使能和屏蔽位,使得 23 个中断
线中的请求可以被正确地响应。

5、查阅芯片手册可知实验电路图中连接的PA0和PA1对应EXTI0、EXTI1 

PA0 ~ PI0:EXTI0      SYSCFG_EXTICR1[3:0]  EXTI0
PA1 ~ PI1:EXTI1      SYSCFG_EXTICR1[3:0]  EXTI1

6、EXTI基地址范围 0x4001 3C00 - 0x4001 3FFF

7、中断服务函数名称可以在startup_stm32f401xe.s文件中找到
外部中断服务函数
EXTI0_IRQHandler                                        
EXTI1_IRQHandler

所以外部中断编程步骤如下:

1、所有端口都具有外部中断功能。要使用外部中断线,必须将端口配置为输入模式。
2、开启IO口复用时钟,设置IO口与中断线的映射关系。
3、开启与该IO口相对的线上中断/事件,设置触发条件。

四、仿真电路

这里有一点需要注意下,仿真时间线和现实时间并不一致,比如现实中可能过去三五秒了,仿真时间才过去1秒。仿真时间线注意仿真软件左下角即可。比如本实验中LED 0.5秒切换一次,是指Protues的仿真时间线过去0.5秒才会切换,与现实时间无关。

五、程序设计

1、驱动程序

interrupt.h

#ifndef _INTERRUPT_H_
#define _INTERRUPT_H_

//应用程序中断和复位控制寄存器
#define AIRCR (*(volatile unsigned long *)0xE000ED0C)
//中断优先级数组
#define NVIC_IPR1	(*(volatile unsigned long *)0xE000E404)
#define NVIC_IPR7	(*(volatile unsigned long *)0xE000E41C)
//中断设置使能寄存器
#define NVIC_ISER0 (*(volatile unsigned long *)0xE000E100)
	
//RCCAPB1外设时钟使能寄存器(RCC_APB1ENR)
#define RCC_APB1ENR (*(volatile unsigned long *)0x40023840)
//TIM2控制寄存器1
#define TIM2_CR1 (*(volatile unsigned long *)0x40000000)
//TIM2预分频器寄存器
#define TIM2_PSC (*(volatile unsigned long *)0x40000028)
//TIM2自动重载寄存器
#define TIM2_ARR (*(volatile unsigned long *)0x4000002c)
//TIM2中断使能寄存器
#define TIM2_DIER (*(volatile unsigned long *)0x4000000c)
//TIM2状态寄存器
#define TIM2_SR (*(volatile unsigned long *)0x40000010)

#define TIM2_EGR (*(volatile unsigned long *)0x40000014)

//外部中断配置寄存器1
#define SYSCFG_EXTICR1 (*(volatile unsigned long *)0x40013808)
//中断屏蔽寄存器
#define EXTI_IMR (*(volatile unsigned long *)0x40013C00)
//上升沿触发选择寄存器
#define EXTI_RTSR (*(volatile unsigned long *)0x40013C08)
//下降沿触发选择寄存器
#define EXTI_FTSR (*(volatile unsigned long *)0x40013C0C)
//中断挂起寄存器
#define EXTI_PR (*(volatile unsigned long *)0x40013C14)
	
//端口模式寄存器
#define GPIOA_MODER (*(volatile unsigned long *)0x40020000)
//GPIOA端口上拉/下拉寄存器
#define GPIOA_PUPDR (*(volatile unsigned long *)0x4002000C)
//外设时钟使能寄存器
#define RCC_AHB1ENR (*(volatile unsigned long *)0x40023830)

void set_interrupt_callback(int interrupt_num, void* isr_callback);
void init_timer0_isr();
void init_external_isr();
#endif

interrupt.c

#include "interrupt.h"
#include "delay.h"

void (*callback0)(); // 声明一个指向同样参数、返回值的函数指针类型
void (*callback1)(); // 声明一个指向同样参数、返回值的函数指针类型
void (*callback2)(); // 声明一个指向同样参数、返回值的函数指针类型

//设置中断回调函数
void set_interrupt_callback(int interrupt_num, void* isr_callback){
	if(interrupt_num == 0){
		callback0 = isr_callback;
	}else if(interrupt_num == 1){
		callback1 = isr_callback;
	}else if(interrupt_num == 2){
		callback2 = isr_callback;
	}
}

//定时器中断初始化
void init_timer0_isr(){
	//1、使能TIM2时钟使能
	RCC_APB1ENR |= (0x0001);
	TIM2_EGR &= (~(1<<0));
	//2、CEN置1 开启定时器 ARPE置1 寄存器进行缓冲
	TIM2_CR1 |= (0x0001);
	TIM2_CR1 &= (~(0x0001<<7));
	//3、CK_CNT = Fck_psc/(PSC[15:0]+1)  Fck_psc=32MHz PSC[15:0]设成3199 则CK_CNT = 10KHz 即计数1000耗时0.1秒
	TIM2_PSC = 3199;
	//4、TIM2_ARR装入1000
	TIM2_ARR = 1000;
	//5、清中断
	TIM2_SR &= (~(1<<0));

	//6、设置DIER的UIE位为1 使能更新中断
	TIM2_DIER |= (0x0001);
	//7、按照datasheet要求前16位写入0x05FA
	AIRCR &= 0x0000FFFF;
	AIRCR |= 0x05FA0000;
	//8、对应位写入101 表示高两位是抢占优先级、低两位是响应优先级
	AIRCR |= 0X05<<8;
	//9、设置TIM2抢占优先级是2 响应优先级是 0
	NVIC_IPR7 |= (0x80);
	//10、使能TIM2中断
	NVIC_ISER0 |= 0X01<<28;
}

//按键中断初始化
void init_external_isr(){
	//1、使能GPIOA时钟
	RCC_AHB1ENR |= 0x01;
	//2、设置PA0、PA1为上拉(未按下按键时高电平) 后4位置为0101
	GPIOA_PUPDR &= (~0x0f);
	GPIOA_PUPDR |= 0x05;
	//3、后4位置为0000 PA0、PA1输入模式
	GPIOA_MODER &= (~0x0f);
	//4、后两位配置为11 表示EXTI0和EXTI1下降沿触发
	EXTI_FTSR |= 0x03;
	//5、使能EXTI0、EXTI1中断,中断号6、7(从0开始算)
	NVIC_ISER0 |= 0X03<<6;
	//6、开放来自EXTI0、EXTI1的中断
	EXTI_IMR |= 0x03;
}

//外部中断0事件处理
void EXTI0_IRQHandler()
{
	EXTI_IMR &= (~0x01);
	delayms(10);
	if(EXTI_PR & 0x01){
		EXTI_PR |= 0x01;
		callback0();
	}
	EXTI_IMR |= 0x01;
}

//外部中断1事件处理
void EXTI1_IRQHandler()
{
	EXTI_IMR &= (~0x02);
	delayms(10);
	if(EXTI_PR & 0x02){
		EXTI_PR |= 0x02;
		callback1();
	}
	EXTI_IMR |= 0x02;
}

//定时器0中断事件处理
void TIM2_IRQHandler()
{
	if(TIM2_SR & 0X01){  	   //说明引起了更新中断
		 TIM2_SR &= (~(1<<0)); //中断清零
			callback2();
	}
}

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);
	}
}

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--);
	}
}

2、应用程序

application.h

#ifndef _APPLICATION_H_
#define _APPLICATION_H_
	void timer0_callback();
	void external0_callback();
	void external1_callback();
#endif

application.c

#include "application.h"
#include "interrupt.h"

#define uchar unsigned char
#define uint unsigned int
#define MAX_CYCLE 50
#define MIN_CYCLE 5

//定时器启动后会立即触发一次中断,已做清中断仍会触发。目前原因未知,可能和仿真有关,所以此处计数从-1开始。
uchar num = 0;
uchar time_cycle = 5;

int main(void){
	led_operate(0,0);
	led_operate(1,1);
	led_operate(2,1);
	led_operate(3,1);
	init_timer0_isr();
	init_external_isr();
	set_interrupt_callback(0,external0_callback);
	set_interrupt_callback(1,external1_callback);
	set_interrupt_callback(2,timer0_callback);
	while(1);
}

//外部中断0回调函数
void external0_callback(){
	if(time_cycle < MAX_CYCLE){
		time_cycle = time_cycle + 5;
	}
}

//外部中断1回调函数
void external1_callback(){
	if(time_cycle > MIN_CYCLE){
		time_cycle = time_cycle - 5;
	}
}

//定时器0中断回调函数
void timer0_callback(){
		num++;
		if(num>=time_cycle){
			num=0;
			if(get_led_status(0) == 0){
				led_operate(0,1);
				led_operate(1,0);
			}else if(get_led_status(1) == 0){
				led_operate(1,1);
				led_operate(2,0);
			}else if(get_led_status(2) == 0){
				led_operate(2,1);
				led_operate(3,0);
			}else if(get_led_status(3) == 0){
				led_operate(3,1);
				led_operate(0,0);
			}
		}
}

六、资料下载

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

STM32 Cortex-M4 MCUs and MPUs programming manual:https://download.csdn.net/download/qq_54140018/88715786

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值