文章目录
- 目的
- GPIO(通用输入输出接口)
- 基础说明
- 初始化
- 输出
- 输入与电平读取
- 锁定机制
- EXTI(外部中断)
- 基础说明
- 使用演示
- 总结
目的
GPIO是单片机最基础的功能,EXTI最常用的场景就是GPIO用于输入时使用。这篇文章将对CH32V307中相关内容进行说明。
本文使用沁恒官方的开发板 (CH32V307-EVT-R1沁恒RISC-V模块MCU赤兔评估板) 进行演示。
本文演示中需要用到开发板上的KEY和LED,默认只是引入接口到排针,并没有和芯片GPIO口相连,下文使用中需要手动用杜邦线连接。
GPIO(通用输入输出接口)
基础说明
CH32V307的GPIO和大部分单片机一样支持多种工作模式: 浮空输入
上拉输入
下拉输入
模拟输入
开漏输出
推挽输出
复用功能的输入和输出
。
复位后 ,GPIO口运行在初始状态,这时大多数IO口都是运行在浮空输入状态 ,但也有HSE等外设相关的引脚是运行在外设复用的功能上。
沁恒官方提供了库函数用于操作GPIO口,主要是 ch32v30x_gpio.h
和 ch32v30x_gpio.c
两个文件,前者中声明了提供给用户调用的函数以及相关的枚举和宏定义类型等。
下面只介绍些GPIO的基础使用,剩余的功能大多数是结合外设复用或是中断等进行的,会在包含在那些内中中进行介绍。需要详细了解也可以直接查看上面的库函数文件。
初始化
初始化GPIO只要配置需要使用的GPIO口和工作模式及其附加参数等,比如下面方式:
// 下面函数将初始化 PA0 为推挽输出模式
void GPIO_Toggle_INIT(void)
{GPIO_InitTypeDef GPIO_InitStructure = {0}; // GPIO后初始化结构体RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //初始化GPIOA时钟GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 使用 Pin0GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出模式GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 输出切换频率 // 这个通常在满足需求的情况下越低越好,比如使用 GPIO_Speed_2MHz GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIO口
}
上面代码中出现的 GPIOA
GPIO_Pin_0
GPIO_Mode_Out_PP
等枚举和宏定义类型都可以在 ch32v30x_gpio.h
文件中找到,其中同一组的 GPIO_Pin
可以多个一起使用,比如下面这样:
// 同时使用 PIN 0、2、4
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_2 | GPIO_Pin_4;
输出
这里以推挽输出模式进行介绍,这里直接以点亮控制控制开发板上的LED进行测试,用杜邦线将 LED1
和 LED2
分别与 PC0
和 PC1
相连接,然后使用 MounRiverStudio
新建 CH32V307VC
项目,将 main.c
中代码替换成如下即可:
#include "debug.h"int main(void)
{Delay_Init();// 以下初始化 GPIOC 的 PIN0 和 PIN1RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);GPIO_InitTypeDef GPIO_InitStructure = {0};GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;GPIO_Init(GPIOC, &GPIO_InitStructure);while(1){GPIO_SetBits(GPIOC, GPIO_Pin_0 | GPIO_Pin_1); // PC0 和 PC1 输出高电平,根据电路这将会熄灭LEDDelay_Ms(1000);GPIO_ResetBits(GPIOC, GPIO_Pin_0 | GPIO_Pin_1); // PC0 和 PC1 输出低电平,根据电路这将会点亮LEDDelay_Ms(500);}
}
编译程序下载到开发板就可以看到LED闪烁了。
输出相关操作库函数有下面一些:
// 指定IO口输出高电平
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
// 指定IO口输出低电平
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
// 写指定IO口的电平值,BitAction可选Bit_RESET(0)和Bit_SET(1)
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
// 写一组IO口的输出值
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
也可以使用调试器来查看GPIOC的输出寄存器状态了解输出情况:
上面的 OUTDR
就是输出数据寄存器了,该寄存器数据也可以通过下面库函数进行读取:
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
输入与电平读取
这里以下拉输入模式进行介绍,用杜邦线将 KEY
与 PB2
相连接, PB2
设置为浮空输入模式,根据电路连接,当按键松开时读取到的输入电平为高,按键被按下时读取到的输入电平为低。
修改 main.c
代码如下:
#include "debug.h"int main(void)
{Delay_Init();USART_Printf_Init(115200);// 以下初始化 PB2 为浮空输入模式RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitTypeDef GPIO_InitStructure = {0};GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init(GPIOB, &GPIO_InitStructure);while(1){Delay_Ms(2000);// 读取PB2输入电平值printf("PB2 Input Data:%d\r\n", GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2));}
}
输入数据读取相关操作库函数有下面一些:
// 读取指定IO口电平值
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
// 读取一组IO口电平值
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
锁定机制
锁定机制可以锁定IO口的配置,在一些可靠性要求高的场合下比较有用。经过特定的一个写序列后,选定的 IO 引脚配置将被锁定,在下
一个复位前无法更改。使用库函数的话只需要使用下面函数即可:
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
EXTI(外部中断)
基础说明
CH32V307有二十多条外部中断线,大部分都是给GPIO用的,这里也只对这个进行介绍,其它的中断线都是用于特定功能了,会在那些功能下进行介绍。
GPIO使用外部中断通常用于输入模式下,根据中断配置当外部电平上升或下降变化时会触发中断。
GPIO上的外部中断使用时需要处理的内容如下:
- 初始化GPIO;
- 初始化EXTI;
- 中断都受到中断控制器控制(NVIC),所以相应用能也需要初始化;
- 编写外部中断触发时的回调函数;
外部中断的库函数主要位于 ch32v30x_exti.h
和 ch32v30x_exti.c
两个文件,前者中声明了提供给用户调用的函数以及相关的枚举和宏定义类型等。
使用演示
这里使用PB2触发外部中断,电路上保持 KEY
与 PB2
的连接。将 main.c
代码改为如下:
#include "debug.h"int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 将外部中断配置为分组二,这样中断父优先级和子优先级取值都为0~3Delay_Init();USART_Printf_Init(115200);// 以下初始化 PB2 为浮空输入模式RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOB, ENABLE); // 此处需要把复用时钟也打开GPIO_InitTypeDef GPIO_InitStructure = {0};GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init(GPIOB, &GPIO_InitStructure);// EXTI_ClearITPendingBit(EXTI_Line2); // 推荐在使能中断前先清除一次中断,防止意外发生// 以下初始化外部中断EXTI_InitTypeDef EXTI_InitStructure = {0};GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource2); // 配置外部中断源为 PB2EXTI_InitStructure.EXTI_Line = EXTI_Line2; // 外部中断线2EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 配置为外部中断EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; // 配置上升沿下降沿都触发中断EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 使能外部中断EXTI_Init(&EXTI_InitStructure);// 以下初始化中断控制器NVIC_InitTypeDef NVIC_InitStructure = {0};NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; // 配置使用 EXTI2 通道NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 父优先级设置为1,数值越小优先级越高。父优先级高的中断会抢占优先级低的中断NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; // 子优先级设置为2,数值越小优先级越高,子优先级不会引起抢占NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能中断NVIC_Init(&NVIC_InitStructure);while(1){}
}// 下面是中断回调函数的声明,该函数名是在启动文件 startup_ch32v30x_D8C.S 中定义的
// 需要注意的是后面的 __attribute__((interrupt())); 是必须的这是 GCC For RISCV 对于中断的一种处理
// 另外也可以使用__attribute__((interrupt("WCH-Interrupt-fast"))); 这样可以使用沁恒RISCV的快速中断功能
void EXTI2_IRQHandler(void) __attribute__((interrupt()));// 下面是中断回调函数的定义
void EXTI2_IRQHandler(void)
{if(EXTI_GetITStatus(EXTI_Line2)!=RESET) // 检查是否是中断线2的中断{printf("PB2 Input Data:%d\r\n", GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2));EXTI_ClearITPendingBit(EXTI_Line2); // 清除中断标志,这样才能触发下次中断}
}
这里特别需要注意的是这颗RISC-V内核的单片机使用时中断处理函数需要特殊的标识声明后才能正常使用,比如上面代码中使用 ( void EXTI2_IRQHandler(void) __attribute__((interrupt()));
) 方式进行声明,不然进一次中断后程序就跑飞了。
演示中读取到的数据异常是由于按键抖动引起的,实际使用中需要添加软件和硬件上的按键消抖功能。
总结
GPIO与EXTI虽然是使用频率比较高的功能,但总体使用比较简单,没有太多需要详细说明的地方。