3. GPIO应用实战:按键输入检测(消抖处理)、蜂鸣器驱动、继电器控制、GPIO位带操作技巧
各位同学,欢迎来到第三章。前两章我们把CMS32的开发环境搭好了,也点亮了LED。这一章咱们来点真正实用的——按键、蜂鸣器、继电器,还有位带操作这个“黑科技”。
说实话,按键消抖这个问题,我当年刚入行时吃过不少亏。有一次做一款工业控制面板,按键没做消抖,结果按一次触发三四次,客户直接退货。从那以后,我对按键检测就特别上心。
- 按键输入检测与硬件/软件消抖
- 有源蜂鸣器驱动(高低电平控制)
- 继电器控制与隔离设计
- GPIO位带操作——加速你的GPIO访问
3.1 按键输入检测——别让抖动坑了你
按键,看起来最简单的外设。但就是这个小东西,让无数新手翻车。为什么?因为机械按键按下和释放时,触点会弹跳,产生一连串的脉冲信号。如果不处理,单片机可能认为你按了十几次。
3.1.1 硬件消抖
硬件消抖,说白了就是用RC低通滤波器把抖动滤掉。我习惯在按键两端并联一个0.1μF电容,再串联一个10kΩ电阻。这样抖动信号就被电容吸收了。
硬件消抖的优点是CPU零开销,缺点是多两个元件。对于量产产品,成本敏感时就要权衡了。
3.1.2 软件消抖——我最常用的方式
软件消抖更灵活,不增加BOM成本。核心思路就一句话:检测到电平变化后,延时10~20ms再读一次。
// CMS32 按键消抖示例(PA0接按键,按下为低电平)
#define KEY_PIN P0
#define KEY_BIT (1 << 0)
uint8_t Key_Scan(void)
{
if((KEY_PIN & KEY_BIT) == 0) // 第一次检测到按下
{
Delay_ms(15); // 延时15ms避开抖动
if((KEY_PIN & KEY_BIT) == 0) // 再次确认
{
// 等待按键释放(可选)
while((KEY_PIN & KEY_BIT) == 0);
return 1; // 有效按键
}
}
return 0;
}
3.2 蜂鸣器驱动——让设备会“说话”
蜂鸣器分两种:有源和无源。名字容易误导人——有源蜂鸣器内部自带振荡电路,通电就响;无源的需要外部提供PWM信号才能发声。
| 类型 | 驱动方式 | 频率控制 | 典型应用 |
|---|---|---|---|
| 有源蜂鸣器 | 高低电平直接驱动 | 固定频率(不可调) | 报警、提示音 |
| 无源蜂鸣器 | PWM驱动 | 可调(改变PWM频率) | 音乐、变调提示 |
我建议初学者先用有源蜂鸣器,简单粗暴。驱动电路注意一点:CMS32的GPIO驱动能力有限,不能直接驱动蜂鸣器。需要加一个NPN三极管(如S8050)做开关。
// 有源蜂鸣器驱动(PB1控制,高电平响)
#define BEEP_PIN P1
#define BEEP_BIT (1 << 1)
void Beep_On(void)
{
BEEP_PIN |= BEEP_BIT; // 拉高,蜂鸣器响
}
void Beep_Off(void)
{
BEEP_PIN &= ~BEEP_BIT; // 拉低,蜂鸣器停
}
void Beep_Twice(void)
{
Beep_On();
Delay_ms(100);
Beep_Off();
Delay_ms(50);
Beep_On();
Delay_ms(100);
Beep_Off();
}
3.3 继电器控制——小电流控大电流
继电器是嵌入式系统控制强电的常用器件。CMS32的GPIO输出3.3V,而继电器线圈通常需要5V或12V。所以需要驱动电路。
我常用的方案是:GPIO → 三极管 → 继电器。三极管基极串一个1kΩ限流电阻,继电器线圈并联续流二极管。
// 继电器控制(PC2控制,高电平吸合)
#define RELAY_PIN P2
#define RELAY_BIT (1 << 2)
void Relay_On(void)
{
RELAY_PIN |= RELAY_BIT;
}
void Relay_Off(void)
{
RELAY_PIN &= ~RELAY_BIT;
}
// 按键控制继电器(按下吸合,再按释放)
void Key_Control_Relay(void)
{
static uint8_t relay_state = 0;
if(Key_Scan()) // 检测到有效按键
{
relay_state = !relay_state;
if(relay_state)
Relay_On();
else
Relay_Off();
}
}
3.4 GPIO位带操作——让GPIO飞起来
位带操作是Cortex-M0内核的一个特色功能。说白了,就是把一个32位的寄存器中的某一位,映射到一个独立的地址上。你直接读写这个地址,就相当于操作那一位。
为什么要用位带?因为传统的读-改-写操作不是原子的。如果中断在中间插一脚,可能出问题。位带操作是原子操作,一条指令搞定。
3.4.1 位带公式
CMS32的GPIO寄存器位于外设位带区。位带映射公式如下:
// 位带地址计算公式
#define BITBAND_PERIPH(reg, bit) (0x40000000 + ((uint32_t)&(reg) - 0x40000000) * 32 + (bit) * 4)
// 举例:操作P0寄存器的第0位
#define P0_BIT0 (*((volatile uint32_t *)BITBAND_PERIPH(P0, 0)))
这个公式看着复杂,其实原理很简单:每个bit被扩展成32位的地址空间。你往这个地址写1,对应的bit就置1;写0就清0。
3.4.2 位带操作实战
// 定义位带宏
#define BITBAND(reg, bit) (*((volatile uint32_t *)(0x40000000 + ((uint32_t)&(reg) - 0x40000000) * 32 + (bit) * 4)))
// 定义常用GPIO位
#define LED_PIN BITBAND(P0, 1) // P0.1 控制LED
#define KEY_PIN BITBAND(P0, 0) // P0.0 读取按键
#define BEEP_PIN BITBAND(P1, 1) // P1.1 控制蜂鸣器
#define RELAY_PIN BITBAND(P2, 2) // P2.2 控制继电器
// 使用位带操作
void main(void)
{
// 初始化GPIO(略)
while(1)
{
if(KEY_PIN == 0) // 读取按键,位带方式
{
Delay_ms(15);
if(KEY_PIN == 0)
{
LED_PIN = !LED_PIN; // 位带方式翻转LED
BEEP_PIN = 1; // 蜂鸣器响
Delay_ms(100);
BEEP_PIN = 0;
RELAY_PIN = !RELAY_PIN; // 继电器切换
}
}
}
}
- 原子操作:不会被中断打断,适合多任务环境
- 代码简洁:一行代码完成读/写/翻转
- 速度快:比读-改-写少2~3条指令
- 可读性强:一眼看出操作哪个引脚
3.5 综合实战:按键控制蜂鸣器和继电器
最后,我们把今天学的知识串起来。做一个完整的示例:按键1控制蜂鸣器响一声,按键2控制继电器吸合/释放,同时LED指示状态。
// 综合示例:按键控制蜂鸣器+继电器+LED
#include "CMS32.h"
// 位带定义
#define KEY1_PIN BITBAND(P0, 0) // 按键1
#define KEY2_PIN BITBAND(P0, 1) // 按键2
#define LED_PIN BITBAND(P1, 0) // 指示灯
#define BEEP_PIN BITBAND(P1, 1) // 蜂鸣器
#define RELAY_PIN BITBAND(P2, 0) // 继电器
void GPIO_Init(void)
{
// P0.0 P0.1 输入模式(按键)
P0CR &= ~(0x03 << 0);
// P1.0 P1.1 输出模式(LED、蜂鸣器)
P1CR |= (0x03 << 0);
// P2.0 输出模式(继电器)
P2CR |= (0x01 << 0);
// 初始状态:LED灭,蜂鸣器停,继电器释放
LED_PIN = 0;
BEEP_PIN = 0;
RELAY_PIN = 0;
}
uint8_t Key1_Scan(void)
{
if(KEY1_PIN == 0)
{
Delay_ms(15);
if(KEY1_PIN == 0)
{
while(KEY1_PIN == 0); // 等待释放
return 1;
}
}
return 0;
}
uint8_t Key2_Scan(void)
{
if(KEY2_PIN == 0)
{
Delay_ms(15);
if(KEY2_PIN == 0)
{
while(KEY2_PIN == 0);
return 1;
}
}
return 0;
}
void main(void)
{
GPIO_Init();
while(1)
{
if(Key1_Scan())
{
// 按键1:蜂鸣器响一声,LED闪烁
BEEP_PIN = 1;
LED_PIN = 1;
Delay_ms(200);
BEEP_PIN = 0;
LED_PIN = 0;
}
if(Key2_Scan())
{
// 按键2:继电器切换,LED指示
RELAY_PIN = !RELAY_PIN;
LED_PIN = RELAY_PIN; // LED跟随继电器状态
}
}
}