最近在看傻孩子的一本书,里面有很多值得学习的东西。在学习的过程中还是要注重做笔记。
阻塞指的就是死等在这里,以 LED 闪烁为例,我们来说明一下阻塞代码和非阻塞代码的区别。
首先,我们以阻塞代式来编写一个延时函数:
//! 阻塞形式的延时代码
void delay( uint32_t wTime )
{
while(wTime--)
{
NOP(); //!<加入 nop 可以有效防止编译器对改循环进行优化
}
}
接下来,我们在超级循环中实现一个简单的 LED 闪烁功能:
extern void led_on(void); //!<硬件无关的 LED 操作函数,点亮 LED
extern void led_off(void); //!<硬件无关的 LED 操作函数,熄灭 LED
void led_task(void)
{
led_on(); //!<点亮 LED
delay( DELAY_500MS ); //!<延时 500 毫秒, DELAY_500MS 是一个宏
led_off(); //!<熄灭 LED
delay( DELAY_500MS ); //!<延时 500 毫秒
}
void main(void)
{
...
while(1)
{
...
led_task(); //!<这是一个阻塞任务,只要它不完成一次灯的闪烁就不会退出
...
}
}
这是一个典型的例子,堪称嵌入式学习的“hello world”。这也是一个阻塞型代码,当系统调用 delay()
函数进行延时的时候, CPU 处于一种“死等”的状态,无论你的内核多么强劲,死等都会把你“一句打
回解放前” ——这就是阻塞代码的威力,一夫当关万夫莫开,“阻塞”的含义体现的淋漓尽致。 在操作
系统环境下,情况得到了改观,即便某个任务以阻塞代码的形式编写,且没有使用 OS 提供的带有任务
休眠功能的延时函数 sleep(),当高优先级任务就绪时,系统也会毫不犹豫的打断当前“阻塞”的任务,
保证了整个系统其它任务的流畅运行。如果我们用状态机的非阻塞形式来实现上述功能,则是另外一番
景象。
首先,我们实现一个非阻塞的延时函数:
typedef enum
{
fsm_rt_on_going = 0, //!<状态量 on going,表示状态机正在执行
fsm_rt_cpl = 1, //!<状态量 complete, 表示状态机执行完毕
} fsm_rt_t;
#define RESET_FSM() do { s_tState = START; } while(0)
//! 状态机编写的非阻塞延时函数
fsm_rt_t delay_fsm( uint32_t wTime )//其实就是这个延时函数需要不停地调用(每调用一次计数值就减1),不是只调用一次就一直在延时。
{
static enum
{
START = 0,
WAIT_FOR_DELAY,
} s_tState = START; //!<定义一个状态变量,用以表示状态机当前的状态
static uint32_t s_wDelay; //!<实际用于延时的静态局部变量
switch (s_tState)
{
/* START 状态机的进入“事件”,在状态机复位后首次运行时,运行且仅运行一次,用于初始化
状态机环境(比如状态机用到的各类变量) */
case START:
s_wDelay = wTime; //!<记录要延时的时间长度
s_tState = WAIT_FOR_DELAY; //!<切换状态
//break; //!<使用 fall-through 的方法,提高效率
/* 实际用于延时的状态 */
case WAIT_FOR_DELAY:
if (0 == s_wDelay)
{
RESET_FSM(); //!<复位状态机
return fsm_rt_cpl; //!<说明状态机执行完成
}
s_wDelay --; //!<更新计数器
break;
}
return fsm_rt_on_going; //!<默认情况下返回状态机正在执行
}
接下来我们在超级循环中实现一个非阻塞的状态机,同样实现 LED 的闪烁功能:
fsm_rt_t led_task_fsm(void)
{
static enum
{
START = 0,
LED_ON,
DELAY_1,
LED_OFF,
DELAY_2
} s_tState = START;
switch (s_tState)
{
case START: //!< START 作为基本格式保留
s_tState = LED_ON; //!<切换状态
//break;
case LED_ON:
led_on(); //!<点亮 LED
s_tState = DELAY_1; //!<切换状态
break;
case DELAY_1:
//! 调用延时子状态机
if (fsm_rt_cpl == delay_fsm( DELAY_500MS )) //太经典了!进入delay_fsm这个函数后立马返回,而不会死等在里面!
{
//! 如果延时完成了
s_tState = LED_OFF; //!<切换状态
}
break;
case LED_OFF:
led_off(); //!<熄灭 LED
s_tState = DELAY_2; //!<切换状态
break;
case DELAY_2:
//! 调用延时子状态机
if (fsm_rt_cpl == delay_fsm( DELAY_500MS ))
{
//! 如果延时完成了
RESET_FSM(); //!<复位状态机
return fsm_rt_cpl; //!<返回状态机完成
}
break;
}
return fsm_rt_on_going; //!<默认返回状态机正在执行
}
void main(void)
{
while(1)
{
//! 这是一个非阻塞的任务,无论是否在延时,状态机都会很快释放 CPU。
led_task_fsm();
}
}
由于状态机具有非阻塞的特性,因而多任务协同时往往表现出较好的实时性,这是缺乏调度算法的协程
所难以实现的。我们可以简单的认为:相比线程,状态机编写更复杂,但掌控性较强,资源的占用较小;
相比协程,状态机的编写更复杂,但在提高任务实时性方面较为容易。
总结:仔细看函数的调用过程,不难发现,非阻塞的延时函数需要每次调用(每次调用后计数值减1),led灯的函数也是状态机的结构循环往复调用延时函数。
阻塞延时:就是调用这个延时函数就会一直被阻塞在计数值递减上,做减法操作。