GD32实战5__状态机

引子

​ 上面的点灯例子中,如果想要实现如下功能,使用状态机可以把代码写的简洁通透。

  1. 按一下全亮
  2. 再按一下亮度降低50%
  3. 再按一下跑马灯
  4. 长按3秒熄灭

状态机设计

​ 我们把上面的功能在分解下,如下:

  1. 按键检测,如图

    1. 按下,低电平
    2. 弹起,高电平
    3. 按下时间长,可用作区分短按还是长按
    4. 触发方式,高电平触发,低电平触发,下降沿触发,上升沿触发,根据经验,牵扯到时长判断,触发方式最好选择上升沿触发

    GD32实战5__状态机

  2. 灯状态

    1. 全亮
    2. 50%亮度
    3. 跑马灯状态
    4. 全灭

    总结,可以设计如下图的两个小状态机相互切换。

    GD32实战5__状态机

功能实现

按键检测

​ 状态迁移如上图,代码如下,功能与上图描述一一对应。

#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;
    }
}

总结

  1. 在“点亮LED灯”例子中,按键的去抖动是要完全占用CPU,阻塞其它功能运行的,用状态机可以实现非阻塞去抖动,且代码量结构清晰代码量极少。
  2. 代码中的trace、debug、error信息是在编码的时候就考虑到该如何跟踪调试该代码,实际调试中效果非常明显。
  3. 由于MCU资源的限制,一般不会增加操作系统,并行处理,同步处理,异步处理,非阻塞实现,都要靠状态机实现,实现一个优美简洁的状态机,是功能可靠稳定的前提。
  4. 在前面的例子时,就把驱动(driver)和应用(app)模块化,在本例中受益很大,如果细心比较本例代码和其它例子代码,可以发现除了应用(app)部分代码被重写之外,其它代码都没做任何改动,继承性非常好,省去了驱动代码的编码调试工作。

代码路径

https://github.com/YaFood/GD32F103/tree/master/TestSM

上一篇:LeetCode 1127.User Purchase Platform


下一篇:jquery 点击浏览器返回上一页按钮并能直接刷新