前言
在设计锂离子电池充电器时,对于以前的根据系统状态进行判断,置标志位的方法,会显得程序臃肿,且架构混乱,变量交错复杂,移植困难。
所以结合实际项目,给出了一种基于状态机的编程方法。
有纰漏请指出,转载请说明。
学习交流请发邮件 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如下。