引子
上面的点灯例子中,如果想要实现如下功能,使用状态机可以把代码写的简洁通透。
- 按一下全亮
- 再按一下亮度降低50%
- 再按一下跑马灯
- 长按3秒熄灭
状态机设计
我们把上面的功能在分解下,如下:
-
按键检测,如图
- 按下,低电平
- 弹起,高电平
- 按下时间长,可用作区分短按还是长按
- 触发方式,高电平触发,低电平触发,下降沿触发,上升沿触发,根据经验,牵扯到时长判断,触发方式最好选择上升沿触发
-
灯状态
- 全亮
- 50%亮度
- 跑马灯状态
- 全灭
总结,可以设计如下图的两个小状态机相互切换。
功能实现
按键检测
状态迁移如上图,代码如下,功能与上图描述一一对应。
#define APP_KEY_JITTERTIME 50 /* 50ms */
#define APP_KEY_LONGPRESSTIME 3000 /* 3s */
typedef enum
{
KEY_SMSTATUS_UP = 0,
KEY_SMSTATUS_UPING,
KEY_SMSTATUS_DOWN,
KEY_SMSTATUS_DOWNING,
KEY_SMSTATUS_BUTT
}KeySmStatus_e;
typedef struct
{
KeySmStatus_e smStatus; /* up-->downing-->down-->uping-->up */
U64 downingMoment;
U64 jitterTimeBegin;
}KeySm_t;
static KeySm_t gKeySm;
static VOID KEY_SmStatusUp(VOID)
{
U8 keyStatus = 0;
if (KEY_SMSTATUS_UP != gKeySm.smStatus)
{
return;
}
keyStatus = DRV_KEY_GetStatus(DRV_KEY3);
if (DRV_KEY_DOWN == keyStatus)
{
gKeySm.smStatus = KEY_SMSTATUS_DOWNING;
gKeySm.jitterTimeBegin = APP_TimeMs();
APP_TRACE("up --> downing");
}
}
static VOID KEY_SmStatusDowning(VOID)
{
U64 currentTime = 0;
U8 keyStatus = 0;
if (KEY_SMSTATUS_DOWNING != gKeySm.smStatus)
{
return;
}
currentTime = APP_TimeMs();
if (currentTime < (gKeySm.jitterTimeBegin + APP_KEY_JITTERTIME))
{
return;
}
keyStatus = DRV_KEY_GetStatus(DRV_KEY3);
if (DRV_KEY_DOWN == keyStatus)
{
gKeySm.smStatus = KEY_SMSTATUS_DOWN;
gKeySm.downingMoment = APP_TimeMs();
APP_TRACE("downing --> down");
}
else if (DRV_KEY_UP == keyStatus)
{
gKeySm.smStatus = KEY_SMSTATUS_UP;
APP_TRACE("downing --> up");
}
else
{
APP_ERROR("");
}
}
static VOID KEY_SmStatusDown(VOID)
{
U8 keyStatus = 0;
if (KEY_SMSTATUS_DOWN != gKeySm.smStatus)
{
return;
}
keyStatus = DRV_KEY_GetStatus(DRV_KEY3);
if (DRV_KEY_UP == keyStatus)
{
gKeySm.smStatus = KEY_SMSTATUS_UPING;
gKeySm.jitterTimeBegin = APP_TimeMs();
APP_TRACE("down --> uping");
}
}
static VOID KEY_SmStatusUping(VOID)
{
U64 currentTime = 0;
U8 keyStatus = 0;
if (KEY_SMSTATUS_UPING != gKeySm.smStatus)
{
return;
}
currentTime = APP_TimeMs();
if (currentTime < (gKeySm.jitterTimeBegin + APP_KEY_JITTERTIME))
{
return;
}
keyStatus = DRV_KEY_GetStatus(DRV_KEY3);
if (DRV_KEY_DOWN == keyStatus)
{
gKeySm.smStatus = KEY_SMSTATUS_DOWN;
APP_TRACE("uping --> down");
}
else if (DRV_KEY_UP == keyStatus)
{
gKeySm.smStatus = KEY_SMSTATUS_UP;
currentTime = APP_TimeMs();
if (currentTime >= (gKeySm.downingMoment + APP_KEY_LONGPRESSTIME))
{
APP_DEBUG("long press.");
APP_LED_SmSwitch4LongPress();
}
else
{
APP_DEBUG("short press.");
APP_LED_SmSwitch4ShortPress();
}
APP_TRACE("uping --> up");
}
else
{
APP_ERROR("");
}
}
VOID APP_KEY_Loop(VOID)
{
KEY_SmStatusUp();
KEY_SmStatusDowning();
KEY_SmStatusDown();
KEY_SmStatusUping();
}
按键通知LED接口
VOID APP_LED_SmSwitch4LongPress(VOID)
{
if (LED_SMSTATUS_OFF != gLedSm.smStatus)
{
gLedSm.smStatus = LED_SMSTATUS_OFF;
}
}
VOID APP_LED_SmSwitch4ShortPress(VOID)
{
switch (gLedSm.smStatus)
{
case LED_SMSTATUS_OFF:
gLedSm.smStatus = LED_SMSTATUS_ON;
break;
case LED_SMSTATUS_ON:
LED_DoHalfBrightInit();
gLedSm.smStatus = LED_SMSTATUS_HALFBRIGHT;
break;
case LED_SMSTATUS_HALFBRIGHT:
LED_DoWaterfallBrightInit();
gLedSm.smStatus = LED_SMSTATUS_WATERFALL;
break;
case LED_SMSTATUS_WATERFALL:
gLedSm.smStatus = LED_SMSTATUS_ON;
break;
default:
APP_ERROR("error sm status.");
break;
}
}
LED灯状态迁移
与上图中描述完全一致。
typedef enum
{
LED_SMSTATUS_OFF = 0,
LED_SMSTATUS_ON,
LED_SMSTATUS_HALFBRIGHT,
LED_SMSTATUS_WATERFALL,
LED_SMSTATUS_BUTT
}LedSmStatus_e;
typedef struct
{
LedSmStatus_e smStatus;
LedSmStatus_e currentStatus;
}LedSm_t;
static LedSm_t gLedSm;
static VOID LED_LightOn(VOID)
{
if ((LED_SMSTATUS_ON != gLedSm.smStatus) ||
(LED_SMSTATUS_ON == gLedSm.currentStatus))
{
return;
}
APP_TRACE("light on.");
DRV_LED_On(DRV_LED1);
DRV_LED_On(DRV_LED2);
DRV_LED_On(DRV_LED3);
DRV_LED_On(DRV_LED4);
gLedSm.currentStatus = LED_SMSTATUS_ON;
}
static VOID LED_HalfBright(VOID)
{
if (LED_SMSTATUS_HALFBRIGHT != gLedSm.smStatus)
{
return;
}
APP_TRACE("light half.");
LED_DoHalfBright();
gLedSm.currentStatus = LED_SMSTATUS_HALFBRIGHT;
}
static VOID LED_WaterfallBright(VOID)
{
if (LED_SMSTATUS_WATERFALL != gLedSm.smStatus)
{
return;
}
APP_TRACE("light waterfall.");
LED_DoWaterfallBright();
gLedSm.currentStatus = LED_SMSTATUS_WATERFALL;
}
static VOID LED_LightOff(VOID)
{
if ((LED_SMSTATUS_OFF != gLedSm.smStatus) ||
(LED_SMSTATUS_OFF == gLedSm.currentStatus))
{
return;
}
APP_TRACE("light off.");
DRV_LED_Off(DRV_LED1);
DRV_LED_Off(DRV_LED2);
DRV_LED_Off(DRV_LED3);
DRV_LED_Off(DRV_LED4);
gLedSm.currentStatus = LED_SMSTATUS_OFF;
}
VOID APP_LED_Loop(VOID)
{
LED_LightOn();
LED_HalfBright();
LED_WaterfallBright();
LED_LightOff();
}
LED灯亮度控制
通过控制LED快速闪烁,调节亮灭的时间占空比实现的,如下
static VOID LED_DoHalfBright(VOID)
{
U64 time = 0;
time = APP_TimeMs();
if (time > gLightOnMoment + APP_LED_HALFLIGHT_TIME)
{
gLightOnMoment = APP_TimeMs();
gLightCount++;
}
else
{
return;
}
if (1 == gLightCount)
{
DRV_LED_On(DRV_LED1);
DRV_LED_On(DRV_LED2);
DRV_LED_On(DRV_LED3);
DRV_LED_On(DRV_LED4);
}
else
{
DRV_LED_Off(DRV_LED1);
DRV_LED_Off(DRV_LED2);
DRV_LED_Off(DRV_LED3);
DRV_LED_Off(DRV_LED4);
}
/* 调节亮度 */
if (3 == gLightCount)
{
gLightCount = 0;
}
}
跑马灯
即每个灯一次亮灭,如下
static VOID LED_DoWaterfallBright(VOID)
{
U64 time = 0;
time = APP_TimeMs();
if (time > gLightOnMoment + APP_LED_WATERFALL_TIME)
{
gLightOnMoment = APP_TimeMs();
gLightCount++;
}
else
{
return;
}
if (1 == gLightCount)
{
DRV_LED_On(DRV_LED1);
DRV_LED_Off(DRV_LED2);
DRV_LED_Off(DRV_LED3);
DRV_LED_Off(DRV_LED4);
}
if (2 == gLightCount)
{
DRV_LED_Off(DRV_LED1);
DRV_LED_On(DRV_LED2);
DRV_LED_Off(DRV_LED3);
DRV_LED_Off(DRV_LED4);
}
if (3 == gLightCount)
{
DRV_LED_Off(DRV_LED1);
DRV_LED_Off(DRV_LED2);
DRV_LED_On(DRV_LED3);
DRV_LED_Off(DRV_LED4);
}
if (4 == gLightCount)
{
DRV_LED_Off(DRV_LED1);
DRV_LED_Off(DRV_LED2);
DRV_LED_Off(DRV_LED3);
DRV_LED_On(DRV_LED4);
gLightCount = 0;
}
}
总结
- 在“点亮LED灯”例子中,按键的去抖动是要完全占用CPU,阻塞其它功能运行的,用状态机可以实现非阻塞去抖动,且代码量结构清晰代码量极少。
- 代码中的trace、debug、error信息是在编码的时候就考虑到该如何跟踪调试该代码,实际调试中效果非常明显。
- 由于MCU资源的限制,一般不会增加操作系统,并行处理,同步处理,异步处理,非阻塞实现,都要靠状态机实现,实现一个优美简洁的状态机,是功能可靠稳定的前提。
- 在前面的例子时,就把驱动(driver)和应用(app)模块化,在本例中受益很大,如果细心比较本例代码和其它例子代码,可以发现除了应用(app)部分代码被重写之外,其它代码都没做任何改动,继承性非常好,省去了驱动代码的编码调试工作。
代码路径
https://github.com/YaFood/GD32F103/tree/master/TestSM