4. GPIO实战:按键输入检测、LED流水灯、蜂鸣器驱动、GPIO中断配置
GPIO是MCU最基础、最常用的外设。说白了,它就是芯片和外界打交道的「手脚」。你想想看,一个单片机系统,无论多复杂,最终都要通过GPIO去读按键、点灯、驱动蜂鸣器、控制继电器。
我个人习惯,拿到一款新MCU,第一件事就是先把GPIO玩透。因为GPIO的初始化流程、寄存器配置、中断响应机制,几乎代表了这颗芯片的设计哲学。CMS32的GPIO模块设计得挺清爽,但有几个坑,我当年第一次用的时候也踩过。
本章核心目标:
- 掌握CMS32 GPIO的输入/输出模式配置
- 实现按键消抖与状态检测
- 完成LED流水灯与蜂鸣器驱动
- 理解并配置GPIO外部中断
4.1 GPIO模式配置:输入与输出
CMS32的GPIO端口,每个引脚都可以独立配置。我习惯把GPIO配置分成三步走:开时钟、设模式、写数据。
| 配置步骤 | 寄存器/操作 | 说明 |
|---|---|---|
| 1. 使能端口时钟 | PMCx / PFCx | 先要让GPIO模块「通电」 |
| 2. 设置方向 | PMx 寄存器 | 0=输出,1=输入 |
| 3. 读/写数据 | Px 寄存器 | 输出写Px,输入读Px |
我的习惯:初始化GPIO时,先把所有用到的引脚配置成输入(PMx=1),再逐个改成输出。这样可以防止上电瞬间引脚电平不确定导致误动作。我在一个车载项目中,就因为没注意这个顺序,上电时蜂鸣器「嘀」了一声,被客户投诉了。
4.2 按键输入检测与消抖
按键检测看起来简单,但实际项目中坑不少。机械按键按下和释放时,触点会抖动,持续时间通常5~20ms。如果不做消抖,一次按键可能被误判成多次。
我常用的消抖方案有两种:
- 硬件消抖:RC低通滤波 + 施密特触发器。适合对成本不敏感的场景。
- 软件消抖:检测到电平变化后,延时10~20ms再读一次。这是最通用的做法。
// 按键消抖示例代码
uint8_t Key_Scan(void)
{
static uint8_t key_last = 1; // 上次状态
uint8_t key_now;
uint8_t key_val = 0;
key_now = (P1 & 0x01); // 读取P1.0引脚
if (key_last != key_now) {
// 检测到变化,启动延时消抖
Delay_ms(10); // 延时10ms
key_now = (P1 & 0x01);
if (key_last == 1 && key_now == 0) {
key_val = 1; // 下降沿有效,按键按下
}
key_last = key_now;
}
return key_val;
}
注意:延时消抖期间,MCU会阻塞。如果系统有实时性要求,建议用定时器+状态机的方式做非阻塞消抖。我曾经在一个电机控制项目中,因为延时消抖导致PWM响应延迟,电机出现了抖动。
4.3 LED流水灯实现
流水灯是GPIO输出的经典练习。CMS32的GPIO驱动能力不错,直接推LED没问题,但记得串限流电阻,一般330Ω~1kΩ。
我个人喜欢用查表法做流水灯,代码简洁,扩展性好:
// 流水灯模式表
const uint8_t led_pattern[] = {
0x01, 0x02, 0x04, 0x08, // 左移
0x08, 0x04, 0x02, 0x01 // 右移
};
void LED_Flow(void)
{
static uint8_t index = 0;
P0 = led_pattern[index]; // 直接赋值到P0口
index++;
if (index >= 8) {
index = 0;
}
Delay_ms(200); // 每200ms切换一次
}
小技巧:如果想让流水灯更平滑,可以用PWM控制每个LED的亮度,实现「呼吸」效果。CMS32的定时器可以输出多路PWM,配合DMA简直绝配。
4.4 蜂鸣器驱动
蜂鸣器分有源和无源两种。有源蜂鸣器给电就响,无源蜂鸣器需要PWM驱动。CMS32的GPIO可以直接驱动有源蜂鸣器,但要注意电流限制。
| 类型 | 驱动方式 | 频率控制 | 适用场景 |
|---|---|---|---|
| 有源蜂鸣器 | GPIO高低电平 | 不可控 | 报警、提示音 |
| 无源蜂鸣器 | PWM输出 | 可控 | 音乐、变调 |
// 有源蜂鸣器驱动
#define BUZZER_PIN P2_0
void Buzzer_On(void)
{
BUZZER_PIN = 1; // 高电平驱动
}
void Buzzer_Off(void)
{
BUZZER_PIN = 0;
}
// 无源蜂鸣器PWM驱动(使用定时器)
void Buzzer_PWM_Init(void)
{
// 配置定时器输出2kHz PWM
// 占空比50%
TMR_Config(TMR0, 2000, 50);
TMR_Start(TMR0);
}
我曾经踩过的坑:有源蜂鸣器内部有振荡电路,驱动电压不能太高。有一次我直接用5V驱动一个3V的蜂鸣器,结果烧了。一定要看数据手册的额定电压。
4.5 GPIO中断配置
GPIO中断是嵌入式系统的「事件驱动」核心。CMS32支持外部中断,每个端口都可以配置为中断源。
中断配置流程:
- 使能GPIO端口时钟
- 配置引脚为输入模式
- 设置中断触发方式(上升沿/下降沿/双边沿)
- 清除中断标志位
- 使能中断(全局中断 + 外设中断)
- 编写中断服务函数
// GPIO中断配置示例
void GPIO_Interrupt_Init(void)
{
// 1. 使能时钟
PMC1 |= 0x01; // P1.0 使能
// 2. 配置为输入
PM1 |= 0x01; // P1.0 输入模式
// 3. 设置下降沿触发
EGP1 |= 0x01; // 下降沿使能
// 或者上升沿:EGP1 &= ~0x01; 然后 PNP1 |= 0x01;
// 4. 清除中断标志
PIF1 &= ~0x01;
// 5. 使能中断
INTEN |= 0x01; // 使能外部中断
__enable_irq(); // 使能全局中断
}
// 中断服务函数
void INT_P1_IRQHandler(void)
{
if (PIF1 & 0x01) {
// 按键中断处理
// 注意:中断里不要做耗时操作
PIF1 &= ~0x01; // 清除中断标志
}
}
中断服务函数注意事项:
- 尽量短小精悍,只做标志位设置
- 必须清除中断标志,否则会一直触发
- 不要在中断里调用延时函数
- 注意中断优先级,避免嵌套过深
4.6 综合实战:按键控制流水灯模式
把前面几个知识点串起来,做一个完整的例子:按键切换流水灯模式,蜂鸣器给出提示音。
// 主循环
void main(void)
{
uint8_t mode = 0;
System_Init(); // 系统时钟初始化
GPIO_Init(); // GPIO初始化
Buzzer_Init(); // 蜂鸣器初始化
GPIO_Interrupt_Init(); // 中断初始化
while(1) {
if (key_flag) { // 中断中置位
key_flag = 0;
mode++;
if (mode > 3) mode = 0;
Buzzer_Beep(100); // 蜂鸣器提示
}
switch(mode) {
case 0: LED_LeftShift(); break;
case 1: LED_RightShift(); break;
case 2: LED_Breath(); break;
case 3: LED_AllBlink(); break;
}
Delay_ms(100);
}
}
我的建议:实际项目中,按键检测和LED控制最好用状态机实现,不要在主循环里用Delay。CMS32的定时器资源丰富,用定时器中断做时间基准,主循环只处理状态切换,这样系统响应更快,也更省电。
嗯,GPIO这部分内容就这些。看似基础,但每个点展开都有不少细节。你把这些代码跑通了,CMS32的GPIO就算入门了。后面章节我们会用到这些基础操作,到时候再深入。