合泰单片机 | HT66F3195 | 个人库开发过程 | 【8】GPIO - 按键

本文详细介绍了如何在单片机中使用外部中断和GPIO定时扫描进行按键检测,包括配置外部中断引脚、选择触发边沿、消抖处理和规范化操作。作者还分享了实际项目中的代码实现和注意事项,适用于初学者和有经验开发者参考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

按键

目前有2种方式做按键检测:

  1. 外部中断。
  2. GPIO定时扫描。

硬件电路

在这里插入图片描述
一般电路设计都是引脚带上拉电阻,然后接一个按键再接一个小电阻接地。
一开始我不是很明白,如果用外部中断来做的话,就只能捕获下降沿触发了,如果我要上升沿触发呢?
这个单片机是没有内置下拉电阻的,所以只能设计成VDD接小电阻然后接按键,IO外置一个大的下拉电阻,比起上面那种设计多了一个外部电阻不是很合适。

外部中断按键检测

HT66F3195有两个外部中断引脚,如果懒得在引脚图上面一个一个找,直接在数据手册里面的“输入/输出端口”->“引脚共用功能”的最后一块内容里面找。
在这里插入图片描述
这个意思也很简单了,比如INT0可以在PB0上,如果PB0用作其他用途,就换成PA1来使,但是同时只能用一个,PB0和PA1是不能同时都为INT0外部中断功能的。很多有几年开发经验的人,也经常把这个漏掉,如果用封装成库的话,就会大大减少这种低级错误的发生了,因为一个模块的大部分内容直接被我从数据手册里面搬到头文件里封装好了😏

程序配置

INT.h

#ifndef _INT_H_
#define _INT_H_


#define INT_0_Cmd(x)            _int0e = x
#define INT_1_Cmd(x)            _int1e = x


#define INT_0_IntAddress        0x04
#define INT_1_IntAddress        0x24


#define INT_0_Flag              _int0f
#define INT_1_Flag              _int1f

#define INT_0_FlagReset()       _int0f = 0
#define INT_1_FlagReset()       _int1f = 0


#define INT_0_Source(x)         _int0ps = x
typedef enum
{
    INT_0_PB0 = 0,
    INT_0_PA1,
}INT_0_Source_T;


#define INT_1_Source(x)         _int1ps = x
typedef enum
{
    INT_1_PB1 = 0,
    INT_1_PA2,
}INT_1_Source_T;


#define INT_0_Trigger(x)        _integ = (_integ&(~(REG_2_BIT<<0)))|(x<<0)

#define INT_1_Trigger(x)        _integ = (_integ&(~(REG_2_BIT<<2)))|(x<<2)
typedef enum
{
    INT_Disable = 0,
    INT_Edge_Up,
    INT_Edge_Down,
    INT_Edge_Both,
}INT_Trigger_T;


void INT_0_Init();
void INT_1_Init();

#endif

对应的按键任务结构体:

typedef struct
{
    unsigned char status_after;
    unsigned char status_before;
    unsigned char debounce_cnt;
    unsigned char trigger_condition;        /* 0x01:按下,0xFF:松开,0x00:无 */

}Key_Task;

那个trigger_condition目前只是预留两种状态,后续可能会加上一些常见的操作,比如双击,长按这种。

模块初始化

void INT_1_Init()
{
    /* 引脚功能复用 */
    GPIOA_2_Mode(IO_Mode1);
    GPIOA_2_Pullup(Enable);
    GPIOA_2_Control(IO_Input);

    /* 模块属性 */
    INT_1_Source(INT_1_PA2);
    INT_1_Trigger(INT_Edge_Down);
    INT_1_Cmd(Enable);
}

外部中断没有太复杂的配置,没有特定的功能复用,跟普通IO口一样使用,但是要配置成输入状态,至于要不要开上拉电阻要看外部设计,由于这边做的是按键实验,所以要把上拉打开。以下是数据手册的原话。

外部中断引脚和普通 I/O 口共用。
该引脚必须通过设置端口控制寄存器,将该引脚设置为输入口。

还有一点忘记说了,因为外部中断模块跟其他模块不太一样,其他模块配置的流程基本是:模块使能,模块中断使能这两个核心步骤。
外部中断只有中断使能,但是我库里面写的直接Cmd,内容其实就是中断使能,打开这个之后有外部中断触发就能进对应的中断函数了。

中断程序

DEFINE_ISR(external_int1, INT_1_IntAddress)
{
	INT_1_FlagReset();
    GPIOA_6_Control(IO_Output);
    GPIOA_6_Toggle();
}

我这边只是通过按键翻转一个LED。说来也奇怪,跟其他单片机不太一样,我的按键是使用那种665的黑色微动按键那种,用合泰的单片机好像外部中断不用消抖,多次测试也没有出现异常状况,不过还是建议要进行按键消抖操作的。
所以理论上进入中断还是应该是开始给一个变量赋值,注意不是在中断里面死循环延时,在外面的时基里面进行倒计时,如果再判断对应的状态进行对应的操作。

上面的配置其实也能用,但是有风险而且写法也不规范。下面就写一个正常一点的操作。

模块初始化

Key_Task INT_Key;

void INT_1_Init()
{
    /* 引脚功能复用 */
    GPIOA_2_Mode(IO_Mode1);
    GPIOA_2_Pullup(Enable);
    GPIOA_2_Control(IO_Input);

    /* 模块属性 */
    INT_1_Source(INT_1_PA2);
    INT_1_Trigger(INT_Edge_Both);
    INT_1_Cmd(Enable);

	INT_Key.status_after = 0;
    INT_Key.status_before = 0;
    INT_Key.debounce_cnt = 0;
    INT_Key.trigger_condition = 0;
}

这边我把那个触发边沿改成双沿触发,用外部中断的检测来完成按下和松开的动作捕捉。

中断程序

DEFINE_ISR(external_int1, INT_1_IntAddress)
{
	INT_1_FlagReset();
	INT_Key.status_after = !!(GPIOA_Get_Status() & Pin2);
	INT_Key.debounce_cnt = 2;
}

这里只做两件事情:

  1. 记录触发时候的电平。
  2. 给消抖倒计时变量赋值,跳出中断之后开始延时。

按键消抖

if(TB0.FragmentFlag & Fragment_10ms_Mask)
{
    TB0.FragmentFlag ^= Fragment_10ms_Mask;

    if(INT_Key.debounce_cnt != 0)
    {
    	INT_Key.debounce_cnt -= 1;
    	if(INT_Key.debounce_cnt == 0)
    	{
    		INT_Key.status_before = !!(GPIOA_Get_Status() & Pin2);

    		if(INT_Key.status_after == INT_Key.status_before)
    		{
    			if(INT_Key.status_before == 0)
    			{
    				INT_Key.trigger_condition = 0x01;	/* 按键按下触发标志位 */
    			}
    			else
    			{
    				INT_Key.trigger_condition = 0xFF;	/* 按键松开触发标志位 */
    			}
    		}
    	}
    }
}

这里用的是10ms级别的时基碎片,等2个回合也就是20ms。

按键有效触发操作

if(TB0.FragmentFlag & Fragment_1ms_Mask)
{
    TB0.FragmentFlag ^= Fragment_1ms_Mask;
    if(INT_Key.trigger_condition != 0)
    {
    	if(INT_Key.trigger_condition == 0x01)
    	{
    		/* 按键按下操作 */
    		GPIOA_6_Control(IO_Output);
    		GPIOA_6_Toggle();
    	}
    	else if(INT_Key.trigger_condition == 0xFF)
    	{
    		/* 按键松开操作 */
    	}
    	INT_Key.trigger_condition = 0;
    }
}

有效触发检测操作的时间必须要比消抖频率快,所以这里直接放在1ms的时基碎片里面,其实放主函数里面也可以,那样速度会更快。
先判断完是什么状态然后再清掉,如果先清掉的话就判断不了了。
这里同样是把一个IO口翻转,这边接一个LED出来也行,方便观察现象。

GPIO按键检测

程序配置

KEY.h

#ifndef _KEY_H_
#define _KEY_H_

/*-------------------宏定义封装------------------- */
#define ALL_MICRO_KEY_NUM   2
#define KEY_DEBOUNCE_TIME   10

#define KEY_1               0
#define KEY_2               1

/*-------------------函数声明------------------- */
void Key_GPIO_Init();
void Key_Scan();
void Key_Trigger_Func();


/*-------------------变量声明------------------- */
typedef struct
{
    unsigned char status_after;
    unsigned char status_before;
    unsigned char debounce_cnt;
    unsigned char trigger_condition;        /* 0x01:按下,0xFF:松开,0x00:无 */

}Key_Task;

extern Key_Task Micro_Key[];

extern Key_Task INT_Key;

extern const unsigned char Micro_Key_Map[ALL_MICRO_KEY_NUM][3];

#endif

初始化

const unsigned char Micro_Key_Map[][3] = 
{
    {KEY_1,  GPIOB,  Pin4},
    {KEY_2,  GPIOA,  Pin2},
};

Key_Task Micro_Key[ALL_MICRO_KEY_NUM];

void Key_GPIO_Init()
{
    unsigned char i;

    /* PB4 - GPIO_Key */
    GPIOB_4_Control(IO_Input);
    GPIOB_4_Pullup(Enable);
    GPIOB_4_Mode(IO_Mode1);
    
    /* PA2 - GPIO_Key */
    GPIOA_2_Control(IO_Input);
    GPIOA_2_Pullup(Enable);
    GPIOA_2_Mode(IO_Mode1);

    for(i = 0; i < ALL_MICRO_KEY_NUM; i++)
    {
        Micro_Key[i].status_after = 0;
        Micro_Key[i].status_before = 0;
        Micro_Key[i].debounce_cnt = 0;
        Micro_Key[i].trigger_condition = 0;
    }
}

这里暂时只演示一个按键的写法,手头上有虽说有一个按键矩阵模块,但是这个做法并不是针对矩阵做的,原理其实差不多只是逻辑部分要根据硬件电路做一点变动。

按键检测

void Key_Scan()
{
    unsigned char i;
    for(i = 0; i < ALL_MICRO_KEY_NUM; i++)
    {
        Micro_Key[i].status_before = GPIO_Map_Read(Micro_Key_Map[i][GPIO_Map_Group],Micro_Key_Map[i][GPIO_Map_Pin]);

        if(Micro_Key[i].status_before == IO_Error)
            continue ;

        if(Micro_Key[i].status_before != Micro_Key[i].status_after)         /* 如果前后两次状态不一样则开始消抖,另外也要记录当前的电平状态 */
        {
            Micro_Key[i].debounce_cnt = 2;
            Micro_Key[i].status_after = Micro_Key[i].status_before;
        }
        else if(Micro_Key[i].debounce_cnt != 0)                             /* 消抖 */
        {
            Micro_Key[i].debounce_cnt -= 1;
            if(Micro_Key[i].debounce_cnt == 0)                              /* 消抖完成 */
            {
                Micro_Key[i].status_before = GPIO_Map_Read(Micro_Key_Map[i][GPIO_Map_Group],Micro_Key_Map[i][GPIO_Map_Pin]);

                if(Micro_Key[i].status_before == Micro_Key[i].status_after) /* 如果消抖之后电平状态跟消抖之前状态一样,则说明按键有效触发 */
                {
                    if(Micro_Key[i].status_before == 0)
                    {
                        Micro_Key[i].trigger_condition = 0x01;     /* 按键按下 */
                    }
                    else
                    {
                        Micro_Key[i].trigger_condition = 0xFF;     /* 按键松开 */
                    }
                }
            }
        }
    }
}

这个扫描时间要配合里面的消抖时间,这里设定为2,消抖时间20ms的话,这个函数需要放到10ms时基碎片里面。

if(TB0.FragmentFlag & Fragment_10ms_Mask)
{
    TB0.FragmentFlag ^= Fragment_10ms_Mask;
    Key_Scan();
}

按键触发功能

if(TB0.FragmentFlag & Fragment_1ms_Mask)
{
    TB0.FragmentFlag ^= Fragment_1ms_Mask;
    Key_Trigger_Func();
}
void Key_Trigger_Func()
{
    unsigned char i;
    for(i = 0; i < ALL_MICRO_KEY_NUM; i++)
    {
        if(i == KEY_1)
        {
            if(Micro_Key[KEY_1].trigger_condition == 0x01)
            {
                
            }
            else if(Micro_Key[KEY_1].trigger_condition == 0xFF)
            {
				GPIOA_6_Control(IO_Output);
                GPIOA_6_Toggle();
            }
            Micro_Key[KEY_1].trigger_condition = 0;
        }
        else if(i == KEY_2)
        {
            if(Micro_Key[KEY_2].trigger_condition == 0x01)
            {
                GPIOA_6_Control(IO_Output);
                GPIOA_6_Toggle();
            }
            else if(Micro_Key[KEY_2].trigger_condition == 0xFF)
            {
				
            }
            Micro_Key[KEY_2].trigger_condition = 0;
        }
        else
        {
            if(Micro_Key[i].trigger_condition != 0x00)
                Micro_Key[i].trigger_condition = 0;
        }
    }
}

按键对应触发的功能还是需要尽可能快地反应,这个可以放在1ms的时基碎片里面。
需要做出反应的按键,要配置好对应的分支,目前只需要处理按下和松开即可。其余按键如果不需要做出反应也要清除对应的标志位。
这样子写的差不多了,后来想想这样子写法其实也是有一个缺陷,如果其他地方有调用ms级别的延时,刚好在这段延时时间里面触发了按键,可能就没有来得及检测,直接错过了。我也没有办法针对这种情况进行修改,无论是GPIO的按键还是外部中断按键,碰到长延时都会这样,所以就尽可能在开发中不要使用长延时功能😕

<think>嗯,用户想要找合泰单片机HT66F002C的示例程序或者应用案例。首先,我需要回忆一下合泰单片机的相关资料,特别是HT66F002C这个型号。根据之前用户提供的引用内容,好像他们提到过HT66F0185、HT66F2390这些型号的开发板和教程,但HT66F002C可能是一个不同的型号,所以可能需要一些不同的处理。 首先,我应该考虑HT66F002C的数据手册,因为这是最权威的来源。用户之前提到过数据手册中有关于I/O口的寄存器配置,比如PAPUn、PACn这些寄存器,所以可能需要类似的步骤来配置GPIO。不过不同型号的单片机寄存器名称或地址可能不同,所以需要确认正确的寄存器名称。 然后,用户可能需要示例代码,比如点灯程序,这是最基本的例子。根据引用[3],用户之前是通过创建工程、配置I/O口、编写代码、编译和烧录这些步骤来点灯的。但HT66F002C的寄存器可能和2390不同,所以需要参考对应的数据手册。 另外,用户可能遇到的问题包括如何烧录程序,这在之前的引用中也提到过,用户曾遇到烧录信息不足的情况。所以可能需要指导用户使用合泰的烧录工具,比如使用HT-IDE3000或者专门的烧录器。 还需要考虑外部中断的示例,引用[2]提到外部中断的函数格式和中断向量的处理,可能需要类似的代码结构,但需要根据002C的中断向量表来调整。 另外,用户可能需要PWM或定时器的例子,比如呼吸灯,这可能涉及到定时器配置和PWM寄存器的设置。但同样需要查阅002C的数据手册来确定相关寄存器的配置方法。 可能还需要提醒用户注意时钟源的配置,因为不同的应用可能需要不同的时钟设置,比如内部RC振荡器或外部晶振。 最后,建议用户参考合泰的官方资源,比如官网的范例代码、应用笔记,或者联系技术支持获取更直接的帮助。如果官方没有提供足够的示例,可能需要参考其他相似型号的代码,但注意寄存器的差异。 总结下来,回答应该包括基本的点灯示例,步骤包括工程创建、配置I/O、编写代码、编译烧录,并提醒用户查阅数据手册调整寄存器名称。同时提供其他可能的示例方向,如外部中断、PWM,并建议官方资源和社区支持。</think>针对合泰单片机HT66F002C的示例程序和应用案例,结合其硬件特性与开发需求,整理以下技术实现路径: ### 一、基础GPIO控制(LED点灯示例) 1. **硬件配置分析** HT66F002C的I/O口结构与HT66F2390类似,需通过寄存器控制输入/输出模式。关键寄存器包括: - `PAC`(端口A控制寄存器):设置引脚方向(0=输出,1=输入) - `PA`(端口A数据寄存器):写入输出电平 - `PAPU`(端口A上拉使能寄存器)[^1] 2. **代码实现** ```c #include "HT66F002C.h" void main() { _pac0 = 0; // 设置PA0为输出模式 _papu0 = 0; // 关闭PA0上拉电阻 while(1) { _pa0 = 1; // LED亮(假设共阴极接法) _delay_ms(500); _pa0 = 0; // LED灭 _delay_ms(500); } } ``` *注:需根据实际电路调整LED连接方式(共阴/共阳)* ### 二、进阶功能实现 #### 1. 外部中断配置 ```c #pragma vector INT0_ISR @ 0x04 void INT0_Handler() { if(_int0f) { _pa0 = ~_pa0; // 翻转LED状态 _int0f = 0; // 清除中断标志[^2] } } void main() { _pac0 = 0; // PA0输出 _papu1 = 1; // PA1上拉使能 _int0e = 1; // 使能INT0中断 _emi = 1; // 全局中断使能 // 其他初始化... } ``` #### 2. PWM输出(呼吸灯实现) ```c void PWM_Init() { _tm0c0 = 0x20; // Timer0时钟源选择 _tm0c1 = 0xFA; // 预分频设置 _tm0al = 100; // PWM周期 _tm0ah = 0x00; _pdpc0 = 1; // 使能PWM输出引脚 _tm0ae = 1; // 启动Timer0 } void main() { // 初始化代码... while(1) { for(int duty=0; duty<100; duty++){ _tm0rp = duty; // 调节占空比 _delay_ms(10); } } } ``` ### 三、开发注意事项 1. **时钟源配置** 需在`_hirc`寄存器中设置内部高速RC振荡器: ```c _hirc = 0x88; // 启用16MHz内部时钟 ``` 2. **烧录工具选择** 推荐使用HT-ICE3000仿真器配合HT-IDE3000开发环境,烧录步骤如下: 1. 编译生成`.rom`文件 2. 连接开发板与仿真器 3. 在IDE中选择`Programmer->Auto Program` 3. **寄存器差异提醒** 与HT66F2390相比,HT66F002C的: - 中断向量地址不同 - PWM模块寄存器命名有差异 - 可用定时器数量减少[^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值