在线时间58 小时
UID175586
注册时间2010-3-14
NXP金币0
TA的每日心情 | 奋斗 2017-1-17 10:45 |
---|
签到天数: 3 天 连续签到: 1 天 [LV.2]偶尔看看I
高级会员

- 积分
- 780
- 最后登录
- 2023-11-17
|
嵌入式系统软件的典型开发框架
- 基本的超循环结构
- 使用中断的前后台结构
- 完全依赖中断的事件驱动系统
- 状态机驱动系统——玩转按键
- 总结
4. 状态机驱动系统
4.1 程序结构
状态机驱动系统是在前后台系统的基础之上,显式指定了一组专用状态标志描述系统当前所处的状态,从而根据状态进入对应的程序执行流程的系统结构。同样是基于前后台系统,“事件驱动系统”将响应事件与处理事件的过程都放在中断服务程序中执行,而通常情况下,“状态机驱动系统”的中断服务程序只是用来改变状态标志的值,即维护“状态机”的状态,而对事件的处理过程则交由后台运行在超循环中的“调度器”根据“状态机”的状态进行分派。
图4
“事件驱动系统”和“状态机驱动系统”都是对前后台系统的具体应用,在前后台系统的基础之上增加了不同设计应用程序的约束条件。“事件驱动系统”强化了前台程序,侧重于对外部事件的响应。“状态机驱动系统”是在后台程序中的超循环进行了强化,这样就可以由CPU主导,完成更加复杂的处理流程。而相对于基本的前后台系统设计,每个子任务的设计思路变得更加清晰:在前台沟通外部事件同状态机交互,更新状态机的状态;在后台根据状态机的状态执行处理。
由于基于状态机的程序结构对硬件依赖小,同时又将复杂的程序结构划分成了一个个相对独立的任务,很容易地将复杂的功能分解成多个独立的功能,使得整个功能易于实现。因此,有很多协议栈在实现时,都选择使用状态机模型搭建框架结构。最典型的就是RTOS,任务调度器就是一个状态机解析器。
4.2 样例程序
这里通过设计一个机械按键的检测程序来说明状态机设计结构的用法。
在这个案例中,封装了一个按键组件“Key”。在应用按键时,程序使用定时器周期扫描按键引脚的输入电平,形成按键输入逻辑电平的采样流,采样波形中包含“按键抖动”。可以由按键组件实时地处理按键采样的波形,通过延时滤波器过滤掉“按键抖动”,然后由状态机实时地识别按键的按下、短按弹起、长按、连击、长按弹起等事件,最后交由超循环响应按键事件。
在这个案例中,分别在延时滤波器、检测按键事件及在超循环中对按键事件的处理上分别都用到了状态机的设计方法。
【延时滤波器】
使用延时滤波器对按键引脚采样流进行预处理,是为了过滤掉“按键抖动”,从而简化后续按键事件的检测
。
“延时滤波”使用的是比较“保守”的状态判决策略:只有当输入信号稳定在另一个状态足够长时间后,才进行状态跃迁,否则保持原状态。
在“延时滤波器”的状态机中,有三种状态:当前稳定状态、下一个可能的稳定状态和无效状态,它们在获得新的信号输入之后进行切换。通过代码清单可以说明其中的变化过程。
- typedef struct
- {
- /* 用户配置属性 */
- uint32_t FilterCountMax; /* 滤波计数,只有当输出值保持足够长时间之后才发生变化 */
- /* 内部统计信息 */
- uint32_t StableValue; /* 当前输出值 */
- uint32_t NextValue; /* 检测最新的输出值,只有当输出值保持足够长时间后才切换此值 */
- uint32_t AccCounterForNextValue; /* 新值保持的时间 */
- } Filter_Handler_T;
- void Filter_Init(Filter_Handler_T *handler, uint32_t initValue, uint32_t filterCount)
- {
- assert(NULL != handler);
- /* 保存用户属性. */
- handler->FilterCountMax = filterCount-1U;
- /* 初始化统计信息. */
- handler->StableValue = initValue;
- handler->NextValue = initValue;
- handler->AccCounterForNextValue = 1U;
- }
- uint32_t Filter_GetFiltedOutput(Filter_Handler_T *handler, uint32_t curUnfiltedValue)
- {
- if (curUnfiltedValue != handler->StableValue)
- {
- if (curUnfiltedValue != handler->NextValue)
- {
- handler->NextValue = curUnfiltedValue;
- handler->AccCounterForNextValue = 1U;
- }
- else
- {
- if (handler->AccCounterForNextValue < handler->FilterCountMax)
- {
- handler->AccCounterForNextValue++;
- }
- else
- {
- handler->StableValue = handler->NextValue;
- }
- }
- }
- return handler->StableValue;
- }
复制代码
【检测按键事件】
为了满足检测按键事件的需求,需要首先定义出按键的静态状态模式,只有在按键的静态状态之间相互切换时,才会产生事件。具体的,这里首先对按键的事件模式进行描述,如下:
- 按下事件 - 按键从未按下状态到按下状态转换的过程。
- 短按弹起事件 - 按键从按下状态返回到未按下状态,在按下状态保持事件未达到长按事件需要的时间长度。
- 首次长按事件 - 在首次切换到按下状态后(按下事件),连续保持一段时间,达到长按事件需要的时间长度。
- 连击事件 - 在长按事件发生后,继续保持按下状态,继续保持了一段时间。连击事件是长按事件的一个细分的事件。
- 长按弹起事件 - 在长按事件(包括连击事件)发生后,按键弹起。
通过对按键事件的明确定义,可以确定需要检测到按键输入流的几种稳定的状态模式,包括:按下状态、未按下状态、长按状态、连击状态。状态之前切换的过程发生事件。特别地,在连击状态下,为了产生变化以触发事件的发生,将连击状态分为连击A和连击B,这两个连击状态相互切换也可以产生连续的连击事件。状态及状态变化产生的事件如图5所示。
图5
- typedef enum
- {
- eKey_State_UnPressed = 0, /*!< 未按下状态 */
- eKey_State_FirstPressedDown, /*!< 进入长按状态之前的按下状态 */
- eKey_State_InLongPressedDownA, /*!< 长按A状态 */
- eKey_State_InLongPressedDownB, /*!< 长按B状态 */
- } Key_State_T;
- typedef enum
- {
- eKey_Event_Nothing = 0,
- eKey_Event_PressedDownEdge, /*!< 按下事件 */
- eKey_Event_PressedDownLongFirst, /*!< 首次检测到长按事件 */
- eKey_Event_PressedDownLongContinue, /*!< 长按状态下的连击事件 */
- eKey_Event_PressedUpEdgeShort, /*!< 短按弹起事件 */
- eKey_Event_PressedUpEdgeLong, /*!< 长按弹起事件 */
- } Key_Event_T;
- /* Keep all the static variable and information for each key. */
- typedef struct
- {
- /* State. */
- Key_State_T PreState;
- Key_State_T CurState;
- /* Attr. */
- uint32_t PressedDownLongMaxFirst; /* To detect the start of long pressed. */
- uint32_t PressedDownLongMaxContinue; /* To detect the continue long pressed. */
- /* Condition. */
- uint32_t PressedDownCounter; /* Count for the period that keeping the key pressed. */
- uint32_t PressedDownStepCounter;
- } Key_Handler_T;
- Key_State_T Key_GetState(Key_Handler_T *handler, bool isKeyDown)
- {
- /* 保存之前的状态 */
- if (handler->PreState != handler->CurState)
- {
- handler->PreState = handler->CurState;
- }
- /* 查看最新状况,进行状态切换 */
- switch (handler->CurState)
- {
- case eKey_State_UnPressed:
- /*
- 在按键未按下的情况下,可能转换到的状态只有两个:
- - 在检测到按键按下的情况下,转入最初的按下状态;
- - 在未检测到按键按下的情况下,继续保持未按下的状态。
- 进行一次判断后等待下次按键信号再次进行判断。
- */
- if (isKeyDown) /* 在没有按下的情况下检测到按键被按下,转入按下状态。 */
- {
- handler->CurState = eKey_State_FirstPressedDown;
- handler->PressedDownCounter++; /* 启动按下计数器。 */
- }
- break;
- case eKey_State_FirstPressedDown:
- /*
- 在按键最初被按下的状态下,下一个状态可能有三个:
- - 在检测到按键继续按下并且保持一段足够长的时间之后,可以转换到长按状态;
- - 在检测到按键继续按下但是并未达到长按事件需要的时间前,保持现状;
- - 在任何情况下检测到按键弹起,总是还原到未按下状态。
- */
- if (isKeyDown) /* 在最初的按下之后继续坚持按下,有机会发展为长按事件。 */
- {
- handler->PressedDownCounter++;
- if (handler->PressedDownCounter >= handler->PressedDownLongMaxFirst)
- {
- handler->CurState = eKey_State_InLongPressedDownA;
- handler->PressedDownCounter = 0U; /* 此时计数器转为每个连击事件计数。 */
- handler->PressedDownStepCounter++; /* 连击事件计数器开始计数。 */
- }
- /*
- else
- {
- // 在检测到按键继续按下,但是并未达长按事件切换点时,保持现状。
- }
- */
- }
- else /* 在第一次按下之后没持续多久就按键弹起了? */
- {
- handler->CurState = eKey_State_UnPressed; /* 切换为弹起状态。 */
- handler->PressedDownCounter = 0U; /* 清空按键按下计数器。*/
- handler->PressedDownStepCounter = 0U;
- }
- break;
- case eKey_State_InLongPressedDownA:
- /*
- 在进入长按状态后,实际上只能等着还原回未按下状态或者保持现状了,但实际上为了检测长按过程中的连击过程,必须在长按过程中产生状态切换。为此,这里设计了在长按过程中的A状态和B状态,即使同样在长按过程中,仍然可以A状态和B状态之间相互切换,在切换的时刻产生连击事件。此时,在长按A状态
- 下可以转入的下一状态从两个变成了三个:
- - 在检测到按键继续按下并保持一段足够长的时间后,达到连击切换点,可以转换到长按B状态;
- - 在检测到按键继续按下但是并未达到下一个连击切换点时,保持现状;
- - 在任何情况下检测到按键弹起,总是还原到未按下状态。
- */
- if (isKeyDown) /* 在连击状态下的按下之后继续坚持按下,有机会发展为另一个连击事件。 */
- {
- handler->PressedDownCounter++;
- if (handler->PressedDownCounter >= handler->PressedDownLongMaxContinue)
- {
- // 检测到按键继续按下并保持一段足够长的时间后,达到连击切换点,可以转换到长按B状态
- handler->CurState = eKey_State_InLongPressedDownB;
- handler->PressedDownCounter = 0U; /* 此时计数器转为每个连击事件计数。 */
- handler->PressedDownStepCounter++; /* 连击事件计数器开始计数。 */
- }
- /*
- else
- {
- // 在检测到按键继续按下,但是并未达到下一个连击切换点时,保持现状。
- }
- */
- }
- else /* 在任何情况下检测到按键弹起,总是还原到未按下状态。 */
- {
- handler->CurState = eKey_State_UnPressed; /* 切换为弹起状态。 */
- handler->PressedDownCounter = 0U; /* 清空按键按下计数器。*/
- handler->PressedDownStepCounter = 0U;
- }
- break;
- /*
- 长按B状态同长按A状态在实际上是一个状态,只是在达到连击切换点时相互切换,所以长按A状态和长按B
- 状态除了相互切换成为彼此的下一个状态之外,一切的判断过程都是完全相同的。
- */
- case eKey_State_InLongPressedDownB:
- if (isKeyDown) /* 在连击状态下的按下之后继续坚持按下,有机会发展为另一个连击事件。 */
- {
- handler->PressedDownCounter++;
- if (handler->PressedDownCounter >= handler->PressedDownLongMaxContinue)
- {
- handler->CurState = eKey_State_InLongPressedDownA;
- handler->PressedDownCounter = 0U; /* 此时计数器转为每个连击事件计数。 */
- handler->PressedDownStepCounter++; /* 连击事件计数器开始计数。 */
- }
- /*
- else
- {
- // 在检测到按键继续按下,但是并未达到下一个连击切换点时,保持现状。
- }
- */
- }
- else /* 在任何情况下检测到按键弹起,总是还原到未按下状态。 */
- {
- handler->CurState = eKey_State_UnPressed; /* 切换为弹起状态。 */
- handler->PressedDownCounter = 0U; /* 清空按键按下计数器。*/
- handler->PressedDownStepCounter = 0U;
- }
- break;
- default:
- break;
- }
- return handler->CurState;
- }
- /* 在状态的切换过程中提取事件 */
- Key_Event_T Key_GetEvent(Key_Handler_T *handler)
- {
- Key_Event_T retEvent = eKey_Event_Nothing;
- if (handler->PreState == handler->CurState)
- {
- retEvent = eKey_Event_Nothing;
- }
- else if ((eKey_State_UnPressed == handler->PreState)
- && (eKey_State_FirstPressedDown == handler->CurState))
- {
- retEvent = eKey_Event_PressedDownEdge;
- }
- else if ((eKey_State_FirstPressedDown == handler->PreState)
- && (eKey_State_InLongPressedDownA == handler->CurState))
- {
- retEvent = eKey_Event_PressedDownLongFirst;
- }
- else if ( ((eKey_State_InLongPressedDownA == handler->PreState)
- && (eKey_State_InLongPressedDownB == handler->CurState))
- || ((eKey_State_InLongPressedDownB == handler->PreState)
- && (eKey_State_InLongPressedDownA == handler->CurState)) )
- {
- retEvent = eKey_Event_PressedDownLongContinue;
- }
- else if ( ( (eKey_State_InLongPressedDownB == handler->PreState)
- || (eKey_State_InLongPressedDownA == handler->PreState) )
- && (eKey_State_UnPressed == handler->CurState) )
- {
- retEvent = eKey_Event_PressedUpEdgeLong;
- }
- else if ((eKey_State_FirstPressedDown == handler->PreState)
- && (eKey_State_UnPressed == handler->CurState))
- {
- retEvent = eKey_Event_PressedUpEdgeShort;
- }
- return retEvent;
- }
复制代码 【响应按键事件】
按键输入信号使用延时滤波器进行预处理,然后送入按键事件检测状态机中,分析按键事件等,都是实时处理的过程,而对按键事件进行响应属于用户行为,执行时间可能会比较长,执行逻辑也可能比较复杂,所以通常被安排在超循环中。
- while (1)
- {
- /* 获得按键事件并处理. */
- if (!RBUF_IsEmpty(gRingBufHandler))
- {
- switch (RBUF_GetDataOut(gRingBufHandler))
- {
- case eKey_Event_PressedDownEdge:
- printf("1:eKey_Event_PressedDownEdge\r\n");
- break;
- case eKey_Event_PressedDownLongFirst:
- printf("2:eKey_Event_PressedDownLongFirst\r\n");
- break;
- case eKey_Event_PressedDownLongContinue:
- printf("3:eKey_Event_PressedDownLongContinue\r\n");
- break;
- case eKey_Event_PressedUpEdgeShort:
- printf("4:eKey_Event_PressedUpEdgeShort\r\n");
- break;
- case eKey_Event_PressedUpEdgeLong:
- printf("5:eKey_Event_PressedUpEdgeLong\r\n");
- break;
- default:
- break;
- }
- }
- }
复制代码
这里使用了一个循环队列,由扫描按键的定时器中断服务程序将按键事件传递给超循环,是为了避免事件丢失。一般来说,扫描按键的定时器周期是非常短的,若是简单使用通常方法通过全局变量传递数据,一旦按键事件产生并传递给超循环,那么超循环就必须响应并处理完成,若是在接下来连续两次扫描到按键事件之时超循环仍在处理上次按键的事件,表示按键事件的全局变量在保存了第一次事件后,在未得到响应时就会被第二次按键事件冲掉。这样,在超循环处理最早的按键事件时,接下来的第一次按键事件就会被第二次按键事件冲掉,从而产生丢失按键事件的情况。如果超循环中对按键事件的进行处理时间特别长,那么丢失按键的情况也会进一步恶化。因此,在按键的样例程序中使用了循环队列实现了一个按键事件的FIFO,按键事件被缓存在FIFO中,由超循环逐个取出进行处理,确保了不会出现丢失按键事件的情况。
运行使用了延时滤波器和按键状态机的样例程序“Key_StateMachineQueue”,操作渡鸦K64开发板上的KEY1按键,可以通过上位机的串口终端观察到对按键事件的检测情况。
图6
|
|