嵌入式软件按照其所使用的操作系统可以分为三类:Rich OS、RTOS、Bare-metal。应用最广泛的应该是Bare-metal了,Bare-metal即裸机,也就是不采用任何操作系统的嵌入式系统,其程序内仅有主循环和中断服务例程,通常也称作前后台系统。如果套用线程的概念,那么前后台系统可以认为是单线程的。在单线程模式下,无外乎轮询驱动、事件驱动以及轮询和事件组合的混合驱动这三种编程模型。这三种编程模型适合前后台系统,也适合有操作系统但希望将部分程序限制在单线程环境内的情况。
mingdu.zheng at gmail dot com
https://blog.csdn.net/zoomdy/article/details/79662512
轮询是上层询问下层有没有完成操作,是上层调用下层(call
),最下层就是硬件了。轮询的优点是简单明了,缺点是需要多次检查下层状态,造成时钟和电量的浪费。
最基本的轮询驱动就是繁忙等待轮询,这种轮询发起操作请求,然后不断检查状态,直到操作完成。繁忙等待轮询是最简单最容易实现的编程模式。
// 发起操作请求
low_level_request();
// 等待操作完成
while(!low_level_is_done()) {
}
休眠轮询是对繁忙等待轮询的改进,在轮询期间做适当的休眠,使用休眠轮询可以保持程序简单性的同时降低功耗。
// 发起操作请求
low_level_request();
// 等待操作完成
while(1) {
// 检查操作是否完成,如果已经完成,那么退出轮询;如果尚未完成,那么休眠片刻
if(low_level_is_done()) {
break;
} else {
// 休眠片刻,最好是利用某种机制,当外设就绪时可以自动唤醒
// 例如利用Cortex-M的SEVONPEND特性
sleep_for_moment();
}
}
并发轮询可以对若干个外设(也可以是其他软件组件提供的功能)同时进行操作,比起前两种轮询方式显得略微复杂,但是提供了更好的并发性能,几个相对独立的任务可以在同一时间段内完成,并发操作可以降低功耗以及缩短处理时间。并发轮询通常需要状态机来保存当前的运行状态,确保下次来轮询时能够按照当前的状态做对应的检查。
int main()
{
// 三个外设同时发起请求
a_low_level_request();
b_low_level_request();
c_low_level_request();
// 主循环内依次轮询三个外设的完成情况
while(1) {
// 如果外设A完成了,那么执行后续操作
if(a_low_level_is_done()) {
a_low_level_next_request();
}
// 轮询设备B
if(b_low_level_is_done()) {
b_low_level_next_request();
}
// 轮询设备C
if(c_low_level_is_done()) {
c_low_level_next_request();
}
}
}
休眠并发轮询是对并发轮询的改进,在适当的时候进行休眠可以进一步降低功耗。
int main()
{
// 三个外设同时发起请求
a_low_level_request();
b_low_level_request();
c_low_level_request();
// 主循环内依次轮询三个外设的完成情况
while(1) {
// 如果外设A完成了,那么执行后续操作
if(a_low_level_is_done()) {
a_low_level_next_request();
}
// 轮询设备B
if(b_low_level_is_done()) {
b_low_level_next_request();
}
// 轮询设备C
if(c_low_level_is_done()) {
c_low_level_next_request();
}
// 三个外设全部轮询一遍之后休眠片刻,
// 最好是利用某种机制,当外设就绪时可以自动唤醒,
// 例如利用Cortex-M的SEVONPEND特性
sleep_for_monent();
}
}
事件驱动是下层向上层汇报操作完成或发生错误,是下层回调上层(callback
)。事件驱动包含两步,第一步发起操作请求并注册回调函数;第二部分当操作完成后执行第一步注册的回调函数。相比轮询驱动,事件驱动是下层在完成操作后调用先前注册的回调函数,没有不断轮询的过程,这节约了时钟和电量。事件驱动是一种异步的编程模型,实现以及理解起来会略微难一些。
引入事件调度器的情况下,回调函数可以通过事件调度器执行。中断(硬件产生的事件)发生后,中断服务例程将回调函数压入事件队列,事件调度器不断地从事件队列取得并执行回调函数。回调函数不仅可以通过中断服务例程压入事件队列,也可以由任何其它代码压入事件队列,通常是下层组件在完成操作后将上层组件注册的回调函数压入事件队列。
// 外设A的中断服务例程,当操作完成后,产生中断并调用该函数
void a_isr()
{
// 中断服务例程将回调函数压入事件队列等待执行
event_queue_push(a_callback);
}
void b_isr()
{
event_queue_push(b_callback);
}
int main()
{
// 发起请求,并注册回调函数
a_request(a_on_response);
b_request(b_on_response);
// 事件调度器
while(1) {
if(event_queue_not_empty) {
// 从事件队列取出回调函数
cb = event_queue_pop();
// 执行回调函数
cb();
}
}
}
在没有事件调度器的情况下,组件在操作完成后可以直接调用回调函数,而不再是将回调函数压入事件队列。lwIP就是只管自己的回调的。这样不用依赖于任何的事件调度器。
int main()
{
// 发起请求并注册回调函数
a_request(&a_on_response);
b_request(&b_on_response);
while(1) {
// 外设A处理请求,处理完毕后直接调用回调函数,而不是压入事件队列
a_process();
b_process();
}
}
系统中的部分组件通过事件调度器来调用回调函数,部分组件直接调用回调函数,这也是可行的。
// 外设A直接调用回调函数
// 外设B通过事件调度器调用回调函数
void b_isr()
{
event_queue_push(b_callback);
}
int main()
{
// 发起请求并注册回调函数
a_request(&a_on_response);
b_request(&b_on_response);
while(1) {
// 外设A处理请求,处理完毕后直接调用回调函数,而不是压入事件队列
a_process();
// 事件调度器
if(event_queue_not_empty) {
// 从事件队列取出回调函数
cb = event_queue_pop();
// 执行回调函数
cb();
}
}
}
很难找到纯粹的轮询驱动的软件,也很难找到纯粹的事件驱动的软件,多数是二者的组合体。轮询驱动和事件驱动有各自的优缺点,通常会组合起来使用以达到最好的效果。不同的组件可以使用不同的模型,同一组件的不同层次也可以采用不同的编程模型。例如lwIP,它最底层接收数据的那一层可以是轮询驱动的,数据包往上层传递的过程是事件驱动的。事件可以是硬件中断触发,也可以是轮询到某种条件后触发的。
// 外设A直接调用回调函数
// 外设B通过事件调度器调用回调函数
// 外设C使用并发轮询驱动
void b_isr()
{
event_queue_push(b_callback);
}
int main()
{
a_request(&a_on_response);
b_request(&b_on_response);
c_request();
while(1) {
// 外设A处理请求,处理完毕后直接调用回调函数,而不是压入事件队列
a_process();
// 事件调度器
if(event_queue_not_empty) {
// 从事件队列取出回调函数
cb = event_queue_pop();
// 执行回调函数
cb();
}
// 外设C使用轮询驱动
if(c_low_level_is_done()) {
c_low_level_next_request();
}
}
}
模型 | 难易程度 | 空闲时休眠 | 并发 | 能效比 | 同步/异步 |
---|---|---|---|---|---|
繁忙等待轮询 | 最简单 | N | N | 最低 | 同步 |
休眠轮询 | 比较简单 | Y | N | 较低 | 同步 |
并发轮询 | 比较复杂 | N | Y | 较低 | 异步 |
休眠并发轮询 | 比较复杂 | Y | Y | 较高 | 异步 |
基于事件调度器的回调 | 最复杂 | Y | Y | 高 | 异步 |
各管各的回调 | 比较复杂 | Y | Y | 高 | 异步 |
轮询驱动
同步轮询
,上文所述繁忙等待轮询和休眠轮询都属于同步轮询异步轮询
,上文所述并发轮询和休眠并发轮询都属于异步轮询事件驱动
,事件驱动必然是异步的共享事件队列的事件驱动
,即上文所述基于事件调度器的回调独立事件队列的事件驱动
,即上文所述各管各的回调同步阻塞
。同步非阻塞
。异步
。异步编程通常伴随着回调、事件等。