备案网站分布地点,江苏镇江论坛,鲜花网站建设策划书,销售网站怎么做的大多数嵌入式的初学者都是从单片机裸机编程开始的#xff0c;对于初学者来说#xff0c;裸机编程更加直观、简单#xff0c;代码所见及所得#xff0c;调试也非常方便#xff0c;区别于使用操作系统需要先了解大量的操作系统基础知识#xff0c;调度的基本常识#xff0…大多数嵌入式的初学者都是从单片机裸机编程开始的对于初学者来说裸机编程更加直观、简单代码所见及所得调试也非常方便区别于使用操作系统需要先了解大量的操作系统基础知识调度的基本常识还需要注意各种资源的共享与竞争等概念并且调试也没有那么直观等等。裸机编程在一些比较简单的项目上还是具有一定的优势的。
接下来我们来看看裸机编程的常见模式和架构。
1.主循环轮询模式
主循环轮询模式就是在主函数中使用一个永不退出的 while(1) 来承载所有的应用逻辑如下
int main(void) {while(1){do_a();do_b();do_c();}
}
do_a、do_b、do_c 三个函数依次执行全部执行完毕后再次从 do_a 逻辑开始以此不断循环。
这种模式是最简单也是最初级的模式但其也存在很多问题。由于上述三个逻辑会依次执行那么就会相互影响do_b 必须要等 do_a 执行完后再执行do_c 必须要等 do_a 和 do_b 都执行完后才执行一旦前置逻辑中存在大量的延时后续逻辑就无法得到及时的运行。
比如后续逻辑中存在一些交互行为do_b 会判断一个按键的按下状态并做出响应而此时还在 do_a 中执行延时指令那么整体运行就会显得非常卡顿甚至还会因为错过用户按键的时机而导致即使按下了按键也没有执行对应的反馈。 2.中断执行模式
针对于上面的问题很多人就会使用中断来解决。对于一些需要立即响应的操作将其放在中断中从而避免其被主程序中的其他逻辑所影响此时代码可能如下所示
//按键中断
void key_isr(void){do_b(); //按键按下的操作
}int main(void) {while(1){do_a();do_c();}
} 主循环中还是正常执行非交互式的逻辑而对于上例中按键交互的逻辑 do_b则放到对应的按键信号捕获中断中如 GPIO 外部中断。此时即使在执行主循环中的其他逻辑由于中断会打断主循环立即运行所以按键信号会被立刻检测到并响应。
无法及时得到响应的问题解决了对于一些非常简单的逻辑这种模式就足够了但如果主循环中的逻辑有一定的周期性要求如 do_a 需要每隔 100 毫秒执行一次 do_c 需要 50 毫秒执行一次于是 do_a 和 do_c 下就会存在 delay(100) 和 delay(50) 的代码
// 按键中断
void key_isr(void) {do_b(); // 按键按下的操作
}void do_a(void) {delay(100); // 延时100ms// do_a 逻辑
}void do_c(void) {delay(50); // 延时50ms// do_c 逻辑
}int main(void) {while (1) {do_a();do_c();}
}
此时无论 do_a 和 do_c 谁前谁后他们的执行周期都会拉长到至少 150 毫秒因为顺序执行的原因你必须等待上一个逻辑执行完才能执行下一个逻辑。 这种情况下 do_a 和 do_c 任何一个逻辑的周期都无法被满足这种模式的缺陷也就显现出来了。
3.中断定时器主循环的前后台架构
上例的一个最大问题就是主循环的每次执行都要完整地将所有逻辑都执行一遍而每个逻辑中为了控制自身的周期又用了延时。各个延时就不可避免地影响到其他逻辑的执行再由于顺序执行的逻辑其他逻辑的执行又影响到了自身产生恶性循环最终没有一个逻辑是符合其自身的周期的。
既然如此我们可以使用定时器产生一个时间标志这个标志代表了当前系统运行的时间主循环中的逻辑再检测这个时间如果满足自身执行的时间那么就执行自身逻辑如果不满足则直接跳出让其他逻辑执行中断逻辑仍然不变。这种情况下前台就是中断后台就是主循环其代码形式如下
// 按键中断
void key_isr(void) {do_b(); // 按键按下的操作
}// 定时器中断 1ms 进一次
unsigned int tick 0;
void timer_isr(void) {tick;if (tick 10000) tick 0;
}void do_a(void) {if (tick % 100 0) {// do_a 逻辑} else {return;}
}void do_c(void) {if (tick % 50 0) {// do c 逻辑} else {return;}
}int main(void) {while (1) {do_a();do_c();}
} 由上述代码可以看到定时器中断为 1 毫秒每进一次中断 tick 加 1在主循环中的 do_a 和 do_c 会首先判断 tick 的值一旦发现与自己的运行周期相同则执行自身逻辑否则退出。此时理想的运行图如下 由于去掉了每个逻辑中的延时取而代之的是标志位的判断其执行速度是非常快的如上图所示 灰色的块表示在运行判断逻辑并且没有满足运行要求。这种情况下每个逻辑都能在其指定的周期内得到执行。
这种架构在裸机编程中可以算得上一种中高级的架构能够满足大多数不是特别复杂的需求。当然在上图中我们可以看到 do_a 和 do_b 一个为 100 毫秒一个为 50 毫秒存在公倍数情况也就是说在某一时刻如这里的 0 毫秒和 100 毫秒就会出现两个逻辑同时运行的场景。实际在项目中如果要求比较严格会对这个周期进行一个控制和计算尽量减少各逻辑同时执行的概率避免由于同时执行的逻辑过多且过于频繁执行时间的总和仍然会太长从而影响整体运行稳定性的问题。
到这里请思考一下假如 do_a 逻辑本身的执行时间就很长比如进行一个非常复杂的运算或者需要读取一个 G 级别的文件导致单一逻辑的执行时间就超过了最小周期如例子中的 50 毫秒那即使 50 毫秒的周期到了由于 do_a 还没运行完do_c 也无法得到运行这时候时间标志已经形同虚设甚至由于此处是取余判断假如 do_a 运行了 51 毫秒结束do_b 在判断的时候已经是 52 毫秒52%50 不为零do_b 直接无法执行时间标志甚至产生了负面影响
虽说将 “通过取余运算判断是否可以执行的逻辑” 修改为 “设置多个时间标志如 50ms_flag、100ms_flag等在中断中判断满足时间就将这些标志置位主循环中直接对这些标志进行判断的逻辑” 可以避免由于时间后延导致的无法触发逻辑执行问题但仍然无法解决周期被影响的本质。
怎么办
4.前后台 状态机架构
既然上面的问题是由于主循环中单个应用逻辑自身执行时间太长导致那么我们就将其拆分原本一个逻辑只能一次执行完现在就拆分成多个步骤每次执行只运行一个步骤而不是完整的逻辑再用一个变量去记录当前执行到了哪个步骤下次进入就执行下一个步骤。
这就是状态机编程以 do_a 为例其他主循环逻辑同 do_a
void do_a(void) {static unsigned char step 0;if (tick % 100 0) {switch (step) {case 0:// 执行第一步step;break;case 1:// 执行第二步step;break;case 2:// 执行第三步step 0;break;default:// 未知步骤归零重来step 0;break;}} else {return;}
} 可以看到原本 do_a 我们将它看作一个完整不可分割的逻辑执行完整个 do_a 才会退出而现在我们将其拆分成了3个步骤每执行完一个步骤就会退出 do_a 函数直到下一次进入才会执行下一个步骤这样一来就能有效缩短一次 do_a 执行的时间从而大大降低其一次执行时间会超过所有逻辑中最小周期的可能性。主循环中其他应用逻辑也和 do_a 一样利用更加细分的状态机模式来加快主循环的响应效率进一步提高了裸机编程的稳定性和时间可控性。
状态机的加入也使得裸机编程走向了其终极形态使其能够处理更加复杂的逻辑与应用与此同时其代码量和复杂度也极速上升尤其是当你的主循环中有十几个甚至几十个任务逻辑此时你就会面临地狱级的编程难度。
当然即使你能够接受地狱级挑战最终也仍然会遇到一个问题 —— 随着应用逻辑的增多同一时间执行了大量的状态机分支步骤这些步骤仅凭人工已经很难再进行拆分了并且很不幸它们执行时间的总和超过了预定的周期最终导致了各种各样的问题。
此时恭喜你已经走到了裸机编程的巅峰同时也是裸机编程的尽头。是时候迈开脚步走向操作系统编程这条路了