基于状态机的单片机编程(以按键状态转移为例)

前言

在设计锂离子电池充电器时,对于以前的根据系统状态进行判断,置标志位的方法,会显得程序臃肿,且架构混乱,变量交错复杂,移植困难。

所以结合实际项目,给出了一种基于状态机的编程方法。

有纰漏请指出,转载请说明。

学习交流请发邮件 1280253714@qq.com


2024.07.31更新,功能裁剪以适应小容量8位单片机

对于功能稍微简单的小容量8位单片机,不需要太复杂的应用,所以这里示范单击/长按/长按抬起的功能。

KEY.H

#ifndef key_H
#define key_H

#include "sys.h"

typedef enum {
	eKeyNum1 = 0,
	eKeyNum2,
    eKeyNum3,
    eKeyNum4,
	eKeySum,
} KeyNum_E;

typedef enum {
	eKeyNoAction = 0,
	eKeyWait1,
    eKeyWait2,
	eKeyShort,
	eKeyLong,
	eKeyLongUp,
} KeySta_E;

typedef struct key_s {
	u8	tmpCnt;
	KeyNum_E num;
	KeySta_E sta;	
	KeySta_E preSta;	
	void (*pfnKeyCallBack)(void);
} KEY_S;

extern KEY_S key1;
extern KEY_S key2;
extern KEY_S key3;
extern KEY_S key4;

#define KEY_LONG_TIME			100

#define		KEY_ON      0	//按键平时为高电平,按下为低电平
#define		KEY_OFF     1


void KeyOsInit(void);
void KeyTask(void);
void keyxScanLoop(KEY_S *keyx);
void KeyTaskProc(void);
void key1TaskProc(void);
void key2TaskProc(void);
void key3TaskProc(void);
void key4TaskProc(void);

#endif

 KEY.C

KEY_S key1;
KEY_S key2;
KEY_S key3;
KEY_S key4;

void KeyOsInit(void)
{	
	key1.num = eKeyNum1;
	key1.sta = eKeyNoAction;
	key1.pfnKeyCallBack = key1TaskProc;
	
	key2.num = eKeyNum2;
	key2.sta = eKeyNoAction;
	key2.pfnKeyCallBack = key2TaskProc;
	
	key3.num = eKeyNum3;
	key3.sta = eKeyNoAction;
	key3.pfnKeyCallBack = key3TaskProc;
	
	key4.num = eKeyNum4;
	key4.sta = eKeyNoAction;
	key4.pfnKeyCallBack = key3TaskProc;
}

static u8 readKeyGpioLevel(u8 keyNum)
{
	u8 keyRes = KEY_OFF;
	switch ( keyNum ){
		case eKeyNum1:
			keyRes = PA1;
			break;
		case eKeyNum2:
			keyRes = PA2;
			break;
		case eKeyNum3:
			keyRes = PA3;
			break;
		case eKeyNum4:
			keyRes = PA4;
			break;
		default:
			break;
	}
	return keyRes;
}

void keyxScanLoop(KEY_S *keyx)
{
	switch (keyx->sta)
	{
		case eKeyNoAction:
			//在按键无任何动作时,疑似按键按下,进入eKeyWait1				-_
			if( KEY_ON == readKeyGpioLevel(keyx->num) ){
				keyx->sta = eKeyWait1;
			}
			break;
		case eKeyWait1:
			if( KEY_ON == readKeyGpioLevel(keyx->num) ){
				//疑似按键按下后,再次确认按键按下,进入eKeyWait2	-__
				keyx->sta = eKeyWait2;
			} else {
				//否则可能是有抖动或者干扰,回归到按键无动作状态			-_-
				keyx->sta = eKeyNoAction;
			}
			break;
		case eKeyWait2:
			if( KEY_OFF == readKeyGpioLevel(keyx->num) ){
				//如果KEY_LONG_TIME时间内按键释放,进入eKeyWait3			-_____-
				keyx->sta = eKeyShort;
			} else {
				keyx->tmpCnt++;
				if (keyx->tmpCnt > KEY_LONG_TIME) {
					//否则确认为长按										-______
					keyx->tmpCnt = 0;
					keyx->sta = eKeyLong;
				}
			}
			break;
        case eKeyLong:
			if( KEY_ON == readKeyGpioLevel(keyx->num) ){
				keyx->sta = eKeyLong;
			} else {
				//长按后按键抬起			-____________-----
				keyx->sta = eKeyLongUp;
			}
			break;
		case eKeyShort:
		case eKeyLongUp:
			keyx->sta = eKeyNoAction;
			break;
	}
	
	if (keyx->sta != keyx->preSta)
	{
		keyx->tmpCnt = 0;
		keyx->preSta = keyx->sta;
        keyx->pfnKeyCallBack();
	}	
}

void KeyTask(void)
{
	keyxScanLoop(&key1);
	keyxScanLoop(&key2);
	keyxScanLoop(&key3);
}

2024.07.17更新,以按键状态转移为例讲解状态机编程

最近在项目中有用到按键,之前的按键逻辑比较复杂,操作也存在局限(按键周期固定,响应时间很局限,没有长按抬起检测,在某些场合不适用),所以最近基于状态机,编写了一套程序,整体效果还不错。

KEY.H

#ifndef __KEY_H
#define __KEY_H	 

#include "includes.h"

typedef enum {
	eKeyNum1 = 0,
	eKeyNum2,
	eKeySum,
} KeyNum_E;

typedef enum {
	eKeyNoAction = 0,
	eKeyWait1,
	eKeyWait2,
	eKeyWait3,
	eKeyWait4,
	eKeyWait5,
	eKeyWait6,
	eKeyWait7,
	eKeyShort,
	eKeyDouble,
	eKeyLong,
	eKeyLongUp,
} KeySta_E;

typedef struct key_s {
	u8	tmpCnt;
	u8	u8Wait2Cnt;
	u8	u8Wait3Cnt;
	u8	u8Wait5Cnt;
	u8	u8LongCnt;
	KeyNum_E num;
	KeySta_E sta;	
	KeySta_E preSta;	
	void (*pfnKeyCallBack)(void);
} KEY_S;

extern KEY_S key1;
extern KEY_S key2;

#define KEY_SHORT_TIME			40
#define KEY_DOUBLE_WAIT_TIME 	60
#define KEY_LONG_TIME			120
#define KEY_LONG_REPET_CHECK_TIME 5

#define		KEY_ON      0	//按键平时为高电平,按下为低电平
#define		KEY_OFF     1

#define KEY1_GPIO_PIN           GPIO_Pin_15
#define KEY1_GPIO_PORT          GPIOC
#define KEY1_GPIO_CLK           RCC_APB2Periph_GPIOC

#define KEY2_GPIO_PIN           GPIO_Pin_14
#define KEY2_GPIO_PORT          GPIOC
#define KEY2_GPIO_CLK           RCC_APB2Periph_GPIOC

void KeyOsInit(void);
void keyxScanLoop(KEY_S *keyx);
void key1TaskProc(void);
void key2TaskProc(void);
void KeyTask(void);

#endif

KEY.C

#include "includes.h"

KEY_S key1;
KEY_S key2;

//按键GPIO初始化
static void Key_GPIO_Config(void)
{
	GPIO_InitTypeDef  GPIO_InitStruct;
	
	RCC_APB2PeriphClockCmd(KEY1_GPIO_CLK, ENABLE);	
	RCC_APB2PeriphClockCmd(KEY2_GPIO_CLK, ENABLE);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;		
	
	GPIO_InitStruct.GPIO_Pin = KEY1_GPIO_PIN;
	GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStruct);	
		
	GPIO_InitStruct.GPIO_Pin = KEY2_GPIO_PIN;
	GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStruct);		
}


void KeyOsInit(void)
{
	Key_GPIO_Config();
	
	key1.num = eKeyNum1;
	key1.sta = eKeyNoAction;
	key1.pfnKeyCallBack = key1TaskProc;
	
	key2.num = eKeyNum2;
	key2.sta = eKeyNoAction;
	key2.pfnKeyCallBack = key2TaskProc;
}


static bool readKeyGpioLevel(u8 keyNum)
{
	bool keyRes = KEY_OFF;
	switch ( keyNum ){
		case eKeyNum1:
			keyRes = GPIO_ReadInputDataBit(KEY1_GPIO_PORT, KEY1_GPIO_PIN);
			break;
		case eKeyNum2:
			keyRes = GPIO_ReadInputDataBit(KEY2_GPIO_PORT, KEY2_GPIO_PIN);
			break;	
		default:
			break;
	}
	return keyRes;
}

//此函数放在主循环中,通过获取定时器时间来进行轮询,10ms一次
void keyxScanLoop(KEY_S *keyx)
{
	switch (keyx->sta)
	{
		case eKeyNoAction:
			//在按键无任何动作时,疑似按键按下,进入eKeyWait1				-_
			if( KEY_ON == readKeyGpioLevel(keyx->num) ){
				keyx->sta = eKeyWait1;
			}
			break;
		case eKeyWait1:
			if( KEY_ON == readKeyGpioLevel(keyx->num) ){
				//疑似按键按下后,再次确认按键按下,进入eKeyWait2	-__
				keyx->sta = eKeyWait2;
			} else {
				//否则可能是有抖动或者干扰,回归到按键无动作状态			-_-
				keyx->sta = eKeyNoAction;
			}
			break;
		case eKeyWait2:
			if( KEY_OFF == readKeyGpioLevel(keyx->num) ){
				//如果KEY_LONG_TIME时间内按键释放,进入eKeyWait3			-_____-
				keyx->sta = eKeyWait3;
			} else {
				keyx->tmpCnt++;
				if (keyx->tmpCnt > KEY_LONG_TIME) {
					//否则确认为长按										-______
					keyx->tmpCnt = 0;
					keyx->sta = eKeyLong;
				}
			}
			break;
		case eKeyWait3:
			if( KEY_ON == readKeyGpioLevel(keyx->num) ){
				//如果之前按下过一次,按键抬起,在KEY_SHORT_TIME内疑似按键按下,进入eKeyWait4
				//														-____-_
				keyx->sta = eKeyWait4;
			} else {
				keyx->tmpCnt++;
				if (keyx->tmpCnt > KEY_SHORT_TIME) {
					//否则确认为短按										-____----------
					keyx->tmpCnt = 0;
					keyx->sta = eKeyShort;
				}
			}
			break;
		case eKeyWait4:
			if( KEY_ON == readKeyGpioLevel(keyx->num) ){
				//疑似按键按下后,再次确认按键按下,进入eKeyWait4			-____-____
				keyx->sta = eKeyWait5;
			} else {
				//否则确认为短按											-____-_----
				keyx->sta = eKeyShort;
			}
			break;
		case eKeyWait5:
			if( KEY_OFF == readKeyGpioLevel(keyx->num) ){
				//如果之前按下过两次,在KEY_LONG_TIME内按键抬起,进入eKeyDouble
				//														-____-____-
				keyx->sta = eKeyDouble;
			} else {
				keyx->tmpCnt++;
				if (keyx->tmpCnt > KEY_LONG_TIME) {
					//否则确认为长按										-____-___________
					keyx->tmpCnt = 0;
					keyx->sta = eKeyLong;			
				}
			}
			break;
		case eKeyLong:
			keyx->tmpCnt++;
			if (keyx->tmpCnt > KEY_LONG_REPET_CHECK_TIME) {
				//每KEY_LONG_REPET_CHECK_TIME时间进行事件保存			-_________|__________|________
				keyx->tmpCnt = 0;
				keyx->sta = eKeyWait6;
			}
			break;
		case eKeyWait6:
			if( KEY_ON == readKeyGpioLevel(keyx->num) ){
				keyx->sta = eKeyLong;
			} else {
				//如果长按时疑似按键抬起,进入eKeyWait7					-____________-
				keyx->sta = eKeyWait7;
			}
			break;
		case eKeyWait7:
			if( KEY_ON == readKeyGpioLevel(keyx->num) ){
				keyx->sta = eKeyLong;
			} else {
				//如果长按后疑似按键抬起,再次确认有抬起					-____________-----
				keyx->sta = eKeyLongUp;
			}
			break;
		case eKeyDouble:
			keyx->tmpCnt++;
			if (keyx->tmpCnt > KEY_DOUBLE_WAIT_TIME) {
				//双击后持续KEY_DOUBLE_WAIT_TIME才进入下一次按键周期,仿止误动作
				//														-____-____----------
				keyx->tmpCnt = 0;
				keyx->sta = eKeyNoAction;
			}
			break;
		case eKeyShort:
		case eKeyLongUp:
			keyx->sta = eKeyNoAction;
			break;
	}
	
	if (keyx->sta != keyx->preSta)
	{
		keyx->tmpCnt = 0;
		keyx->preSta = keyx->sta;
		//此处存放按键回调,或者报错按键信息,这里演示执行按键回调
        keyx->pfnKeyCallBack();
	}	
}

__IO u16 keyStopTick = 0;
__IO u16 keyTimeInterval = 0;
__IO u16 keyStartTick = 0;
void KeyTask(void)
{
    /* 获取时间间隔T */
    keyStopTick = GetT2Cnt();
    if( keyStartTick > keyStopTick ) {
        keyTimeInterval = keyStopTick + (0xFFFF - keyStartTick);
    } else {
        keyTimeInterval = (keyStopTick - keyStartTick);
    }
    if( keyTimeInterval < 10000 ) {
        return;
    }
    keyStartTick = GetT2Cnt();
	
	keyxScanLoop(&key1);
	keyxScanLoop(&key2);}


void key1TaskProc(void)
{
	switch (key1.sta)
    {
	    case eKeyShort:
		    //执行短按功能
		    break;
	    case eKeyDouble:
		    //执行双击功能
		    break;
	    case eKeyLong:
		    //执行长按功能
		    break;
	    case eKeyLongUp:
		    //执行长按抬起功能
		    break;
    }
}

void key2TaskProc(void)
{
	switch (key2.sta)
    {
	    case eKeyShort:
		    //执行短按功能
		    break;
	    case eKeyDouble:
		    //执行双击功能
		    break;
	    case eKeyLong:
		    //执行长按功能
		    break;
	    case eKeyLongUp:
		    //执行长按抬起功能
		    break;
    }
}

方法论

对于电池包,一般会有几个端子与充电器进行通信识别,有BS(Battery Select)、NTC(负温度系数的电阻),正负极、通信口。

有些电池包内置NTC,通过通信将信息发给充电器,有些是直接接NTC给到充电器。

如果按照以往的编程方法,需要经常if else,而且各种标志位flag散乱于程序各处,代码臃肿,调试困难,bug不断,移植困难。

所以我提出了这种基于状态机的编程方法,对各个变量(电压、电流、温度、BS等),持续检测,同时各个状态可以随时转移。

最终任务级函数BatStateCheck判别参数状态,进行参数选择。

同时为了防止系统抖动,需要多次判断才能进行状态转移,demo如下。


变量状态转移流程框图


系统状态转移流程框图


系统状态转移函数示例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值