目录
前言
一、本章重点
二、操作步骤
1.实验方案
2.映射方法
3.代码移植
4.思路讲解(可跳过)
5.完整代码
main.c
Key.c
Key.h
LED.c / LED.h
OLED.c / OLED.h / OLED_Font.h
三、实验现象
下期预告
前言
本文讲述EXTI外部中断。因为实验发现外部中断的代码有点麻烦,所以把它单拎出来讲。从这一章开始我们进入快节奏,一些重复操作就不赘述了。
一、本章重点
CH32与STM32的EXTI外部中断唯一差异就在中断函数上,CH32需要在中断函数之前加上一个特殊的声明,否则在进入一次中断后就无法再进入了。
void EXTI2_IRQHandler(void) __attribute__((interrupt()));
void EXTI2_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line2) == SET)//判断是否是外部中断2号线触发的中断
{
//这里写中断执行内容
EXTI_ClearITPendingBit(EXTI_Line2);//清除外部中断14号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
" __attribute__((interrupt()))" 是GCC 编译器的函数属性扩展语法,这也算是GCC的特色,我们后续编写中断函数的时候都在前面用这个语句声明一下就好。不要把它放在.h文件里,之间与中断函数放一起就好,它也不需要在.h文件里进行声明。
其余代码之间仿照STM32的即可。
二、操作步骤
1.实验方案
我们本章节还是用按键控制LED的翻转,但是我们不手动检测按键是否按下,而是通过EXTI外部中断来操作LED的翻转。
我们将外部中断的通道映射到按键的IO口上,这样按键按下时就会触发中断,我们在中断函数中执行翻转LED的操作即可。
CH32上我们用杜邦线连接左侧对应的引脚
| LED1 | PA0 |
| KEY | PA2 |
CH32与OLED屏幕也用杜邦线连接,OLED主要用于观测运行状态,可接可不接。
| OLED | CH32 |
| GND | GND |
| VCC | 3V3 |
| SCL | PC4 |
| SDA | PC5 |
2.映射方法
我们在开始之前首先要了解一下如何映射外部中断的通道,不要求理解底层原理,但起码要知道大概规则我们才能移植好代码。
CH32与STM32一样内部集成了一组外部中断通道Line0~Line20,而中断通道要有对应的中断控制器IRQn。中断通道可以映射到对应数字的IO口上,例如Line0可以映射到PA0或PB0或PC0等,Line3可以映射到PA3或PB3或PC3等IO口上,只要数字一致即可,但一个通道只能映射一个IO口,是一一对应的关系。
而中断控制器IRQn也与中断通道有对应关系,EXTI0_IRQn~EXTI4_IRQn对应Line0~Line4,后面的中断通道由EXTI9_5_IRQn和EXTI15_10_IRQn来管理。
| PA0 | Line0 | EXTI0_IRQn |
| …… | …… | …… |
| PB4 | Line4 | EXTI4_IRQn |
| PC6 | Line6 | EXTI9_5_IRQn |
| PD13 | Line13 | EXTI15_10_IRQn |
三者之间要一一对应,在移植和编写代码的时候要仔细看好是否三者是对应好的。
3.代码移植
这一节我们新建工程"3-1_EXTI-Key-Led"。然后导入之前使用的OLED模块、Key模块、Led模块,记得在MRS中添加Hardware文件夹路径。然后我们找到江协科技的外部中断示例代码,我这里选用的是"对射式红外传感器计次"工程。

这里外部中断的初始化我们要映射到KEY按键的接口PA2,所以我们要将图中这些项对应修改
| GPIOA | GPIO_Pin_2 | EXTI_Line2 |
EXTI2_IRQn |
修改好的初始化代码如下
void Key_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);//开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//将PA2引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource2);//将外部中断的14号线映射到GPIOB,即选择PA2为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure;//定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line2;//选择配置外部中断的14号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE;//指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure);//将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure;//定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;//选择配置NVIC的EXTI0线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure);//将结构体变量交给NVIC_Init,配置NVIC外设
}
接着就是中断函数,这里先直接给出函数名,后面教大家怎么找。

这里标出的部分要修改,也是一一对应修改,中间部分是我们中断函数的执行内容,需要我们自己写。我这里直接给出修改好的。
void EXTI2_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line2) == SET)//判断是否是外部中断2号线触发的中断
{
LED1_Turn();
EXTI_ClearITPendingBit(EXTI_Line2); //清除外部中断2号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
EXTI_ClearITPendingBit(EXTI_Line2);
}
但是由于CH32与STM32的编译器有所不同,在CH32使用的GCC 编译器中,我们需要在中断函数之前添加下面这样一句声明。这是CH32与STM32的中断之间不太一样的地方。如果不加这一句的话,中断函数只能进入一次,后续就无法进入了,很难排查出问题所在。
void EXTI2_IRQHandler(void) __attribute__((interrupt()));
//或者使用沁恒RISCV的快速中断功能
//void EXTI2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
我们实际编写中直接记住中断函数之前加一句这个就好,把他们两个当作一个整体,这样就不容易漏掉了。

4.思路讲解(可跳过)
在实际场景中我们可能会使用一些其他资料稀缺的国产芯片,这时候我们该如何正确的移植代码呢?那么我们就要学会将理论与实际结合,在代码中寻找答案。
对于CH32这款国产芯片,我们已经知道它的基础库函数基本是跟STM32一样的,那么我们就要用对STM32的理解去验证CH32的代码。
我们首先看芯片介绍可以知道CH32是具备外部中断EXTI功能的,根据前期的验证我们也知道CH32开启资源的流程也跟STM32差不多,但我们还是先观察一手官方例程,毕竟外部中断的配置流程还是比较复杂的。

我们可以看到基本流程是一致的,GPIO – EXTI – NVIC,其次也是先开启AFIO时钟让中断通道映射到GPIO,所以下方的 GPIO_EXTILineConfig 函数名与参数也是与STM32一致的,再往下看EXTI通道和IRQn的宏定义命名方式也是一致的。那么初始化这部分我们就可以直接一比一照搬了。但我们还是选中 EXTI_Line0 右键跳转到定义看一下。

可以看到的确也跟STM32一样,Line0~Line15是通用的,后面是特殊的通道。那么我们再跳转到 EXTI0_IRQn 的定义看一下。

没毛病,这些也是跟STM32一样的,都有都有。
但官方例程中没有写中断函数,我们都知道中断函数必须按照系统命名来写,不可以随便命名,那么我们上哪找呢?打开左侧 "Startup" 文件夹,这两个文件中打开任意一个。

打开文件后稍微下拉,就可以看到EXTI相关的中断函数命名了,熟悉的配方,熟悉的味道。

那么就剩检查中断标志位和清除中断标志位的函数了,我们仍然在系统文件中去找。找到 ch32v30x_exti.h 可以查看相关函数的声明,如果想看函数的具体功能说明,就去找到系统文件ch32v30x_exti.c 。这里我们可以看到也是与STM32一样的库函数。

那么我们现在就可以开始信心满满地把STM32程序移植过来了,然后你就发现怎么程序烧到板子上就是不好使,怎么查都查不出问题,问AI也排查不出问题所在。就是因为你没有在中断函数之前加上这么一句代码。
void EXTI2_IRQHandler(void) __attribute__((interrupt()));
这个问题没有办法避免,不知道就是不知道,例程里没写,数据手册没写,AI也不会告诉你是因为少了这样一句代码。这时候我们只能上网去找别人别的这款芯片的外部中断的代码来分析原因,就好比本篇文章,如果这也找不到就只能找到厂商寻求技术支持了。
但我们有过这一次的经验之后起码下次遇到问题能大概猜到问题的方向,不至于盲目排查,你掌握的芯片使用方法越多,视野也就越广,处理问题的能力也就越强。
5.完整代码
main.c
#include "debug.h"
#include "OLED.h"
#include "Key.h"
#include "LED.h"
uint8_t a = 0;
int main(void)
{
/*模块初始化*/
Delay_Init();
OLED_Init();//OLED初始化
LED_Init();//LED初始化
Key_Init();//按键初始化
while(1)
{
a++;
OLED_ShowNum(2,6,a,3);
Delay_Ms(50);
}
}
Key.c
#include "ch32v30x.h"
#include "debug.h"
#include "LED.h"
/**
* 函 数:按键初始化
* 参 数:无
* 返 回 值:无
*/
void Key_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);//开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//将PA2引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource2);//将外部中断的14号线映射到GPIOB,即选择PA2为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure;//定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line2;//选择配置外部中断的14号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE;//指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure);//将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure;//定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;//选择配置NVIC的EXTI0线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure);//将结构体变量交给NVIC_Init,配置NVIC外设
}
/**
* 函 数:EXTI2外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI2_IRQHandler(void) __attribute__((interrupt()));
void EXTI2_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line2) == SET)//判断是否是外部中断2号线触发的中断
{
LED1_Turn();
EXTI_ClearITPendingBit(EXTI_Line2);//清除外部中断2号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
Key.h
#ifndef __KEY_H
#define __KEY_H
void Key_Init(void);
#endif
LED.c / LED.h
LED模块代码与 第三章 "CH32C307-通用模块" 的模块代码一致。
OLED.c / OLED.h / OLED_Font.h
OLED模块代码与 第二章 "CH32V307-OLED驱动" 的模块代码一致。
三、实验现象
依旧老样子 先编译-后连线-再上电-才烧录 的顺序来操作,要把板子当宝贝。
上电后OLED屏幕中间出现数字一直递增变化,这表明程序在一直跑,没有被阻塞,此时按下按键,LED灯(蓝灯)变亮,几乎观察不到屏幕数字有停顿。再次按下LED灯(蓝灯)熄灭,按键每次按下LED的状态就翻转一次。
下期预告
下一期我们将讲解如何使用CH32V307的定时器功能,同时会给出我整理好的引脚定义图,这一章内容比较多,要好好消化一下,后面我们的节奏会越来越快,请继续加油!




