02 IO口的操作
前言
之前已经介绍了环境的搭建和调试的方法,这一篇文章我们就开始介绍一下如何对外设进行操作,这一节我会结合多种外设的操作来将所有方式的操作介绍给大家,并手把手介绍如何使用一个小显示屏OLED,这对我们后面的操作有很大的帮助。
一、IO的概念
1.IO接口
IO接口是能让CPU和外设能够进行信息交换的逻辑电路,在stm32中,IO接口是已经集成进单片机中了,我们不需要自己搭建一个IO接口电路,可以直接就使用其中的IO接口了。
2.IO端口
IO端口是CPU可以直接访问的一些寄存器,这些寄存器可以控制外设的状态和传输一些信息。
我们对IO设备进行操作时其实就是对IO端口进行一些操作,只不过在stm32中,这个设备更加的高级,这个设备被称为GPIO,接下来我们就是对这些GPIO口进行操作,以达到我们控制外设的目的。
二、CPU和外设进行数据传输的方法
这里总共有4种方法,在这一节中我都会涉及到,只不过中断和DMA我会简单的介绍,先会用,后面还会详细的介绍一下对应的内部结构。
1.程序控制方式
这个是由程序直接进行控制,这里有两种方法,一种是无条件,另一种是查询方式。
1.1 无条件
无条件传输方式是默认外设已经是处于就绪状态,就比如我们对LED灯进行操作,就可以直接使用无条件方式,因为它已经处于就绪状态,不会有忙状态,所以可以直接进行数据的传输。
这种方法的特点就是程序简单,但缺点就是没办法对特别复杂的电路进行操作。
1.2 查询方式
查询方式是在我们要进行数据传输之前,先查看一下外设的状态或者是条件是否满足,如果就绪或者是条件满足则开始进行数据的传输,否则就就绪查询。比如我们通过按键对外设进行控制,或者有些外设在给它传递信号后他会返回一个应答信号或者是其他的信号,当它发送后我们才能继续操作就可以使用这种方法。
这种方法的优点就是可以根据条件来控制外设的状态,缺点就是实时性差。
2.中断方式
如果外设准备好时,会主动发送一个信号过来,我们通过程序将这个信号设定为中断出发,当接受到这个信号后,CPU会停止当前执行的代码,转去执行对应设定好的中断服务程序。
这种方法的优点就是实时性好,速度快,但是代码比较难写。
我们可以使用中断的方法来代替查询方法,这样可以提高CPU的实时性和速度,但是就是代码比较复杂,但速度是最主要的,不要太在意复杂性。
3.DMA方式
这种方法又称为直接数据传送方法,之前外设和内存进行数据交换的时候,需要先经过CPU,然后才可以进行数据交换,但是DMA就是直接跳过这个过程,不经过CPU,直接就让外设和内存进行数据交换,这种方法又一个DMA控制器(DMAC)进行控制。
这种方法的优点是速度快,适合大量数据的交换,缺点就是需要依赖硬件环境。
上面介绍了一下传送数据的方式,接下来我们就把每一个的方法详细介绍一下。
一、方法介绍和代码编写
这里需要先介绍一下stm32的GPIO口和对应的引脚。
1.前置知识
在STM32中将IO端口分为了多个GPIO口,每个GPIO口又有多个引脚,这些我们可以直接通过C语言进行控制。
我们使用的stm32f103c8t6有GPIOA到GPIOC这总共3个GPIO口,而每个GPIO口中又有16个引脚,我们可以对这每个引脚进行操作。
2.程序方式
1.1 无条件方式
无条件方式是在学单片机操作中首先会给大家介绍的一种方法,这种方法很简单,操作起来也很容易。
用无条件方法对GPIO操作我们可以简单分为一下这几步:
1.打开对应的GPIO口
2.初始化对应的GPIO引脚
3.对GPIO引脚进行操作
1.1.1 打开对应的GPIO口
首先在操作之前我们需要开启对应操作的GPIO引脚的时钟,使用的函数是RCC_APB2PeriphClockCmd()
这个函数可以开启你要使用的GPIO口的时钟,当开启时钟后我们可以对其中的引脚进行操作。
函数原型如下:
void RCC_APB2PeriphClockCmd(u32 RCC_APB2Periph, FunctionalState NewState);
第一个成熟是选择对应的APB2,我们可以选择的APB2的值如下:
值 | 描述 |
---|---|
RCC_APB2Periph_AFIO | 功能复用I/O时钟 |
RCC_APB2Periph_GPIOA | GPIOA时钟 |
RCC_APB2Periph_GPIOB | GPIOB时钟 |
RCC_APB2Periph_GPIOC | GPIOC时钟 |
RCC_APB2Periph_GPIOD | GPIOD时钟 |
RCC_APB2Periph_GPIOE | GPIOE时钟 |
RCC_APB2Periph_ADC1 | ADC1的时钟 |
RCC_APB2Periph_ADC2 | ADC2的时钟 |
RCC_APB2Periph_TIM1 | TIM1的时钟 |
RCC_APB2Periph_SPI1 | SPI1的时钟 |
RCC_APB2Periph_USART1 | USART1的时钟 |
RCC_APB2Periph_ALL | 开启全部的APB2的时钟 |
这里先介绍一下APB2的一些内容,后面会用到APB1还会给大家介绍一下。
第二个参数就是使能或者失能,可以填写使能ENABLE
或者失能DISABLE
。
例如我们在GPIOB口中的PB0引脚上接了一个LED灯,我们的初始化就可以这样写:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
这样我们就可以打开对应的GPIO口的时钟了,就可以继续对GPIO口中的引脚进行操作了。
这里需要注意一下,这个打开时钟的操作是必须的,如果你没有对其打开,那会导致引脚没办法使用,时钟这个东西就是CPU的心脏,如果没有心脏,那它就shi了。
1.1.2 初始化对应的GPIO引脚
当打开时钟后我们就可以对其中的引脚进行操作了。引脚是在GPIO口中的,必须得上一步初始化后我们对引脚初始化才有用。
这里对对应的引脚进行初始化需要用到一个结构体,然后将写好的结构体传递到函数中进行初始化,我们使用的结构体类型为:GPIO_InitTypeDef
,我们需要先利用它来创建一个结构体变量,然后对这个变量进行初始化即可。
这个结构体的原型如下:
typedef struct{
u16 GPIO_Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode_TypeDef GPIO_Mode;
}GPIO_InitTypeDef;
GPIO_Pin
是需要填写的对应GPIO引脚
参数 | 描述 |
---|---|
GPIO_Pin_None | 没有引脚被选中 |
GPIO_Pin_x | 选中GPIOx引脚 |
GPIO_Pin_All | 全部选中 |
GPIO_Speed
是设置引脚的速率
参数 | 描述 |
---|---|
GPIO_Speed_2MHz | 最高输出速率2MHz |
GPIO_Speed_10MHz | 最高输出速率10MHz |
GPIO_Speed_50MHz | 最高输出速率10MHz |
GPIO_Mode
是设置引脚的工作模式
参数 | 描述 |
---|---|
GPIO_Mode_Out_PP | 推挽输出 |
GPIO_Mode_Out_OD | 开漏输出 |
GPIO_Mode_AF_PP | 复用开漏输出 |
GPIO_Mode_AF_OD | 复用推挽输出 |
GPIO_Mode_AIN | 模拟输入 |
GPIO_Mode_IN_FLOATING | 浮空输入 |
GPIO_Mode_IPU | 上拉输入 |
GPIO_Mode_IPD | 下拉输入 |
这个还是比较复杂,所以我这给大家一个一个的介绍一下:
1.1.2.1 推挽输出
这个可以理解为输出的就是最大电压和0电压,效率高。高电平就是5V,低电平就是0V。
1.1.2.2 开漏输出
这个高电平不是很高,如果要让其输出高电平则需要外接一个上拉电阻,但它对电流的吸收很强,这种模式一般对那种需要复用的引脚使用,用来接收电平信息。
1.1.2.3 浮空输入
没有上拉和下拉电阻的输入,默认为一种中间态,一点浮动都会被接收到。
1.1.2.4 上拉输入
在输入内部增加了一个上拉电阻,在没有电平来的情况下,这个引脚默认的电平是高电平,一般用这种方式来接受下降沿的电平信息。
1.1.2.5 下拉输入
在输入的内部增加了一个下拉电阻,在没有电平进入的情况下,这个引脚默认的电平是低电平,一般用这种方式来接受上升沿的电平信息。
1.1.2.6 模拟输入
一般用直接引脚来输入模拟信号后进行数模转换,后面在数模转换的时候会使用到这个引脚。
其实这些方式慢慢使用都能记得住的,我们知道了初始化结构体后我们就可以创建一个初始化结构体并为其配置
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
如果要对多个引脚进行初始化的话,我们可以使用逻辑与运算符,将多个引脚连接在一起,比如我要初始化0,1,5,7引脚,那么代码可以这样写:
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
当这样配置好后我们需要使用到GPIO_Init()
函数将GPIO口和GPIO引脚进行初始化,函数原型:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
第一个参数是GPIO口的名称,第二个参数是刚才初始化的GPIO引脚的结构体地址。
我们将刚才设置好的内容拿来初始化吧:
GPIO_Init(GPIOB, &GPIO_InitStruct);
这样就可以完成对GPIO口和对应的引脚进行初始化了。
如果要对多个不同模式的引脚初始化,我们可以在写好一个初始化结构体后调用一下初始化函数,例如PA5是推挽输出,PC3是上拉输入,那我们的初始化代码可以这样写:
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOC, &GPIO_InitStruct);
1.1.3 对其中的GPIO引脚进行操作
这里就可以对其引脚进行操作了,一般操作就是输入或者是输出,这个操作需要看上面初始化的模式是什么,如果是输出,那只能输出,如果是输入,那只能输入。
这里有一个特殊的,就是开漏输出,这个模式也可以使用输入的函数来读取外部的引脚电平。
1.1.3.1 GPIO_SetBits
这个函数可以将对应的引脚置为1。
函数原型如下:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, u16 GPIO_Pin);
第一个参数是对应的GPIO口,第二个是GPIO引脚。例如我要对PB6置1,代码可以这样写:
GPIO_SetBits(GPIOB, GPIO_Pin_6);
这里注明一下,在单片机开发中,有一个类型规范,这个规范是在标准库中是定义好的,我们可以直接拿来使用,比如上面的u16
就是对应着unsigned short
无符号的短整形,其他的可以类推。
1.1.3.2 GPIO_ResetBits
这个函数可以将对应的引脚置为0.
函数原型如下:
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, u16 GPIO_Pin);
1.1.3.3 GPIO_Write
这个引脚是对GPIO口的所有引脚进行操作。
函数原型如下:
void GPIO_Write(GPIO_TypeDef* GPIOx, u16 PortVal);
第一个参数是GPIO口,第二个参数是这个GPIO口的所有引脚的状态,例如我们要让GPIOA的PA7引脚为高电平,其他引脚为低电平,那么代码可以这样写:
GPIO_Write(GPIOA, 0x0080); // 0000 0000 1000 0000
1.1.3.4 GPIO_WriteBit
这个函数是对特定位进行操作。
函数原型如下:
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, u16 GPIO_Pin, BitAction BitVal);
第一个参数是GPIO口,第二个参数是对应的引脚,第三个是设置的电平,这有两个参数可以填写,高电平Bit_SET
和低电平Bit_RESET
,当然,如果你记不住,你也可以直接用0和1来代替。
比如我们要对PB0变成高电平,那么代码可以这样写:
GPIO_WriteBits(GPIOB, GPIO_Pin_0, Bit_SET);
// 或者
GPIO_WriteBits(GPIOB, GPIO_Pin_0, 1);
上面两种都是可以的写法。
上面的都是输出的函数,现在我们利用这个来做一个LED流水灯,首先先用面包版搭建一个电路
流水灯的引脚挨着连着单片机的PA口的0到7引脚,然后对其进行初始化,代码如下:
#include <stm32f10x.h>
// 延时函数,使用的是SysTick定时器
void delay(unsigned int time){
unsigned int temp;
SysTick -> LOAD = 9000 * time;
SysTick -> CTRL = 0x01;
SysTick -> VAL = 0;
do{
temp = SysTick -> CTRL;
}while((temp & 0x01) && (!(temp & (1 << 16))));
SysTick -> CTRL = 0;
SysTick -> VAL = 0;
}
int main(){
GPIO_InitTypeDef GPIO_InitStruct = {0};
unsigned int value = 0x0001;
unsigned char i;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
while(1){
value = 0x0001;
for (i = 0; i < 8; i++){
GPIO_Write(GPIOA, ~value);
value <<= 1;
delay(1000);
}
}
return 0;
}
这里是直接使用GPIO_Write()
函数对GPIOA口进行操作,这种方式很简单,如果直接用GPIO_WriteBit()
,就很麻烦,大家可以试试。
3.查询方式
这种方式需要查询外设的状态后才能开始接下来的工作,使用查询方式的大概步骤如下:
1.设置输入模式
2.判断是否满足条件
3.满足条件传输数据
4.不满足继续查询
3.1 设置输入模式
和之前设置输出一样,使用初始化函数即可初始化,这里可以选择上拉输入、输出模式和浮空输入,当然也可以设置成开漏输出。
这个选择需要根据外设的电路来决定,就拿按键来举例,假如按钮的另一边的接地,当按下按键后,单片机的引脚就会受到地电平,如果设置成下拉输入,那单片机的接口默认是低电平,按下后还是接受到低电平,这样就没有区别,所以就不能设置成下拉输入。
如果设置成浮空输入,那只要有一点变化就会发生电平变化,不稳定也不确定,所以一般在严格规范下最好不要使用,自己玩可以。
如果设置成上拉输入,在默认情况下引脚的电平是高电平,当外设输入低电平时就会发生电平的变化,所以就能选择出选择的模式。
根据外设可以确定好输入的模式后就可以进行初始化。
3.2 判断条件
这里使用到判断和输入函数,所以这里给大家介绍一下全部的输入模式。
3.2.1 GPIO_ReadInputDataBit
这个函数是读取指定引脚的电平。
函数原型如下:
u8 GPIO_ReadInitputDataBit(GPIO_TypeDef* GPIOx, u16 GPIO_Pin);
3.2.2 GPIO_ReadInputData
读取指定GPIO口的电平值。
u16 GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
3.2.3 GPIO_ReadOutputDataBit
读取指定引脚输出的电平值。
u8 GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, u16 GPIO_Pin);
3.2.4 GPIO_ReadOutputData
读取指定GPIO口的输出的电平值。
u16 GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
学习完输入函数后我们可以利用一下输入函数来做一个查询方式的流水灯。当按键按下时,流水灯启动,没按下时就停止。
这里假如只有一个按键接到PB0上,按下按钮后接受到低电平,我们的判断代码就可以这样写:
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0){}
// 或者
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == Bit_RESET){}
3.3 执行内容
我们在写好的判断语句内部写上我们需要执行的代码即可完成查询方式的数据交换。
完整代码如下:
#include "stm32f10x.h"
void delay(unsigned int time){
unsigned int temp;
SysTick -> LOAD = 9000 * time;
SysTick -> CTRL = 0x01;
SysTick -> VAL = 0;
do{
temp = SysTick -> CTRL;
}while((temp & 0x01) && (!(temp & (1 << 16))));
SysTick -> CTRL = 0;
SysTick -> VAL = 0;
}
int main(){
GPIO_InitTypeDef GPIO_InitStruct = {0};
unsigned short value = 0x0001;
u8 i;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &GPIO_InitStruct);
while(1){
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0){
value = 0x0001;
for (i = 0; i < 8; i++){
GPIO_Write(GPIOA, ~value);
value <<= 1;
delay(1000);
}
}
}
return 0;
}
当按键按下后,流水灯会开始启动,执行完一次后就会结束,我们可以感觉这个代码再来改写一下,当按下按钮后流水灯启动直到再次按下按钮后结束流水灯。
改写后的代码如下:
#include "stm32f10x.h"
void delay(unsigned int time){
unsigned int temp;
SysTick -> LOAD = 9000 * time;
SysTick -> CTRL = 0x01;
SysTick -> VAL = 0;
do{
temp = SysTick -> CTRL;
}while((temp & 0x01) && (!(temp & (1 << 16))));
SysTick -> CTRL = 0;
SysTick -> VAL = 0;
}
int main(){
GPIO_InitTypeDef GPIO_InitStruct = {0};
unsigned short value = 0x0001;
u8 i, flag = 0;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &GPIO_InitStruct);
while(1){
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0){
delay(100); // 消抖
flag = 0;
while(1){
value = 0x0001;
for (i = 0; i < 8; i++){
GPIO_Write(GPIOA, ~value);
value <<= 1;
delay(1000);
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0){
flag = 1;
break;
}
}
if (flag){
delay(100);
break;
}
}
}
}
return 0;
}
这样就可以实现改功能了。
在上面的代码中出现了个消抖,因为按键在按下的过程中是会有抖动的,我们需要消除这个抖动让接受的数据唯一,并且要错过按下后电平还是原来的电平时又进入到循环中的问题。
上面的只是用延时函数简单的消除了一下,可以在延时完成后加上一个循环来保证该电平又恢复到原来的状态。
4.中断方式
这种方式的原理不会在这一篇文章中说明,等后面会专门出一篇文章来介绍一下原理和复杂应用后面再说,先简单的介绍一下。
中断设置方式很简单
1.初始化引脚
2.初始化中断EXTI
3.设置NVIC的参数
4.重写中断服务程序
4.1 初始化引脚
这里初始化引脚和前面的一样,也是设置为输入模式,但设置完成后需要将初始化的引脚绑定到EXTI外部中断上,这里使用的函数是GPIO_EXTILineConfig()
。
函数的原型是
void GPIO_EXTILineConfig(u8 GPIO_PortSource, u8 GPIO_PinSource);
将指定引脚映射到EXTI上,然后我们就可以初始化EXTI外部中断了。
第二个参数并不是直接填写引脚号,这个是需要填写通道值,总共有0到15可以选择,这0到15需要感觉选择的引脚号了决定,因为32单片机的中断是将GPIO的引脚号一致的接入到一个通道中,比如通道0接的是PA0到PE0中的所有0号引脚,然后以此类推。
比如我们要将GPIOB口的PB4引脚打开EXTI中断,那么映射函数如下:
GPIO_EXTILineConfig(GPIOB, GPIO_PinSource4);
4.2 初始化外部中断
这里需要使用到EXTI的结构体进行初始化,该结构体类型为:EXTI_InitTypeDef
,原型为:
typedef struct{
u32 EXTI_Line;
EXTIMode_TypeDef EXTI_Mode;
EXTIrigger_TypeDef EXTI_Trigger;
FunctionalState EXTI_LineCmd;
}GPIO_InitTypeDef;
第一个参数EXTI_Line
是选择中断线路,总共有18根外部中断线,这个可以根据自己的安排来安排对应的线路,对应的中断服务入口地址是不同的。
第二个参数EXTI_Mode
是选择上面选择的线路模式。有两个选项可以填写:
EXTI_Mode_Event
:设置EXTI线路为事件请求。
EXTI_Mode_Interrupt
:设置EXTI线路为中断请求。
第三个参数是设置线路的触发边沿,有三个可选的参数:
EXTI_Trigger_Falling
:设置输入线路为下降沿触发。
EXTI_Trigger_Rising
:设置输入线路为上升沿触发。
EXTI_Trigger_Rising_Falling
:设置输入线路为上升沿和下降沿触发。
第四个参数是选中的线路是否开启,有两个参数,一个是使能ENABLE
另一个是失能DISABLE
。
当设置完成后我们可以用EXTI_Init()
函数将刚才初始化的结构体传入进去进行初始化。
4.3 设置NVIC
NVIC是一个中断优先级判断的一个逻辑电路,我们可以为我们前面设置的中断设置一下中断的优先级,优先级后面会详细的说明,这里只简单的介绍一下如何初始化。
设置NVIC也是需要使用到一个结构体来进行初始化,这个结构体类型为:NVIC_InitTypeDef
,结构体原型如下:
typedef struct{
u8 NVIC_IRQChannel;
u8 NVIC_IRQChannelPreemptionPrior;
u8 NviC_IRQChannelSubPriority;
FunctionalState NVIC_IRQChannelCmd;
}NVIC_InitTypeDef;
第一个参数是选择指定的IRQ通道,这里简单介绍一下我们要用的外部中断线的中断通道:
参数 | 描述 |
---|---|
EXTI0_IRQn | 外部中断线0中断 |
EXTI1_IRQn | 外部中断线1中断 |
EXTI2_IRQn | 外部中断线2中断 |
EXTI3_IRQn | 外部中断线3中断 |
EXTI4_IRQn | 外部中断线4中断 |
EXTI9_5_IRQn | 外部中断线5~9中断 |
EXTI15_10_IRQn | 外部中断线10~15中断 |
第二个参数是设置IRQ通道的抢占优先级,抢占优先级就是当在执行这个中断的时候,由来了一个中断,那这个新来的中断是否会干扰当前执行的中断就得看这个抢占优先级,指定的优先级数值越低,优先级越高,这里填写数值就可以了。
第三个参数是优先级,也是填写数值即可,优先级是确定当同一时刻都接受到中断源时,谁先执行就按照这个优先级来确定。
第四个参数是使能或者失能,也就是ENABLE
和DISABLE
。
当填写好这个初始化结构体后我们调用NVIC_Init()
函数将填写好的结构体传入进去即可。
4.4 重写中断服务函数
这个中断服务函数可以在startup_stm32f10x_hd.s
文件中查看,其中这个就是中断服务函数的列表:
比如说我们这设置好外部中断通道0中断,那么我们要重写EXTI0_IRQHandler
函数,重写方法很简单,我们先定义一下这个函数:
void EXTI0_IRQHandler(){}
然后在内部先判断一下是不是该通道的线路触发,因为我们可以让一个通道选择多条线,所以需要有一个判断,当确实是这个线路后我们清除一下该通道的标志位然后就可以开始我们的工作了。
判断和清理的代码如下:
void EXTI0_IRQHandler(){
if (EXTI_GetITStatus(EXTI_Line0) == SET){ // 判断线路0是否触发
EXTI_ClearFlag(EXTI_Line0); // 清理线路0的标志位
}
}
当然,如果只有一个线路,那就可以不用判断,如果你嫌麻烦。
知道了中断的设置后我们可以写出对应的代码了,完整代码如下:
#include "stm32f10x.h"
u8 flag = 0;
void MX_EXTI_Init(){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 开启中断的时钟
// 中断初始化函数
EXTI_InitTypeDef EXTI_InitStruct = {0};
NVIC_InitTypeDef NVIC_InitStruct = {0};
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); // 设置外部线路
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // NVIC优先级分组
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
void delay(unsigned int time){
unsigned int temp;
SysTick -> LOAD = 9000 * time;
SysTick -> CTRL = 0x01;
SysTick -> VAL = 0;
do{
temp = SysTick -> CTRL;
}while((temp & 0x01) && (!(temp & (1 << 16))));
SysTick -> CTRL = 0;
SysTick -> VAL = 0;
}
void EXTI0_IRQHandler(void){
if (EXTI_GetITStatus(EXTI_Line0) == SET){
EXTI_ClearFlag(EXTI_Line0);
flag = 1;
}
}
int main(){
u32 value;
u8 i;
MX_EXTI_Init();
while(1){
if (flag){
flag = 0;
value = 0x0001;
for (i = 0; i < 8; i++){
// GPIO_WriteBit(GPIOA, GPIO_Pin_0, 1);
GPIO_Write(GPIOA, ~value);
value <<= 1;
delay(1000);
}
}
}
return 0;
}
5.DMA方式
DMA方式只适合大量数据的交换,这里不给大家介绍,等后面介绍一下存储芯片会使用到这种方式的。
总结
这一章是学会单片机的起始,后面的内容都会围绕着这些东西来进行操作,像什么点灯、控制OLED屏幕或者大屏幕、操作传感器、控制通讯模块等等等等都是和IO口的操作脱离不了关系的,学完这个后后面会很容易的掌握单片机的很多操作,我们只要勤加练习就可以学会单片机,单片机其实很简单。