找回密码
 立即注册
首页 业界区 安全 嵌入式软件架构漫谈

嵌入式软件架构漫谈

请蒂 昨天 17:50
软件架构的意义在于提高开发效率和代码可维护性、可扩展性。
刚好最近需要用到裸机开发,在此自我总结一下经验和见解。如有错误,欢迎评论区指出。
架构需要做到两个维度的解耦:
纵向的分层;
横向的模块化

分层好理解,可以看一下一个基于RTOS的软件架构:
1.png

其作用在于后期的移植和排查只需要关注某一层级即可,比如更换芯片,那只需要修改驱动层即可;更换RTOS,那只需修改OS抽象层即可;按键任务控制LED任务功能失效,那就按照层级逐层排查(OS层的消息同步是否有问题?LED的亮灭驱动层是否有问题?)即可。
软件上的实现其实就是把函数尽量封装成抽象的接口。以存储功能为例子:

  • 对于上层应用调用者来说,需求很简单,能存能读即可;
  • 不同存储类型有不同的存储步骤,如flash需要按页擦写,EEPROM可以按字节写入还不需要擦操作;
  • 如果存储数据量小、存储频繁,那还要考虑擦写均衡;
  • 不同存储芯片有不同厂商不同型号,那寄存器的定义自然不同;
  • 芯片的通信方式也有不同,SPI、IIC等等;
一个小小的存储需求,在实现细节上却五花八门,我们可以简单分出三个层级:
2.png

这样,后续的项目甚至可以像搭积木一样,开发者专心适配积木接口,可以极大提高开发效率,且由于其复用得到反复验证,稳定性也会不断提高。
难点在于如何做好横向的模块化。因为本身模块之间就需要交互,层级也不是都只在应用层,那天然就会和追求的模块独立性相矛盾。
设计模块化会涉及到三个问题:
1.怎么分模块?

模块的划分可以参考“功能”单一性、通用性和分层来划分。
以AT指令AT+LED控制LED亮灭功能为例,整个程序的实现链路如下:uart串口接收指令->指令解析->控制LED。那就可以分成三个模块:
1)驱动模块:负责接收指令;
2)协议模块:负责解析AT指令;
3)LED模块:负责控制LED灯亮灭;
每个模块只负责单一功能,这样任一模块的变动都不会影响到其它模块。同时可以复用模块,协议模块不仅可以解析串口收到的指令,也能同时解析如usb收到的指令。
2.模块怎么运行?

a.时间片线性轮询

这是裸机最常用的架构,固定时间片对所有程序走一遍。注意这个时间片的长度设计,太长会导致系统响应慢,太短会导致轮询一遍的时间大于时间片。
优点是结构简单明了。
缺点也很明显,实时性会比较查。
  1. int g_10ms_flag = 0;
  2. void systick_irq_handle(void)
  3. {
  4.     g_10ms_flag = 1;
  5. }
  6. int main(void)
  7. {
  8.     sysytick_init(); // 10ms
  9.     while(1) {
  10.         if (g_10ms_flag == 1) {
  11.             g_10ms_flag = 0;
  12.             key_process();
  13.             led_process();
  14.         }
  15.     }
  16. }
复制代码
b.调度表驱动

这种方式是时间片线性轮询的进阶版,上面提到其缺点是实时性较差且时间片不能设置太短。因为所有任务不是都必须在同一周期进行轮询,那我们可以把任务划分,比如10ms轮询、200ms轮询等。而同一周期的还可以进一步作起点偏移,比如A偏移0ms,B偏移3ms,那就会在0ms执行A,3ms执行B,10ms执行A,13ms执行B......如此尽管AB任务周期都是10ms,但避免了同一时刻触发有效分散系统负载。
缺点是所有模块都是在调度表中静态配置,严格周期执行的,灵活性稍有欠缺。
  1. #define TASK_NUM 2
  2. typedef struct {
  3.     int offset;    // 相对于周期起点的偏移
  4.     int period;    // 周期
  5.     void (*task_func)(void);  // 任务函数指针
  6. } schedule_entry_t;
  7. int g_system_tick = 0;
  8. void key_process(void);
  9. int led_process(void);
  10. schedule_entry_t schedule_table[TASK_NUM] = {
  11.     { .offset = 0,   .period = 20, .task_func = key_process },
  12.     { .offset = 3,   .period = 10, .task_func = led_process },
  13. };
  14. void systick_irq_handle(void)
  15. {
  16.     g_system_tick++;
  17. }
  18. // 调度器函数
  19. void scheduler_tick(void) {
  20.     for (int i = 0; i < TASK_NUM; i++) {
  21.         schedule_entry_t *task = &schedule_table[i];
  22.         if (((g_system_tick - task->offset) % task->period) == 0) {
  23.             task->task_func();
  24.         }
  25.     }
  26. }
  27. int main(void)
  28. {
  29.     sysytick_init(); // 1ms
  30.     while (1) {
  31.         scheduler_tick();
  32.     }
  33.     return 0;
  34. }
复制代码
c.事件驱动

有的模块并不需要去周期轮询,而只有当某个事件触发后才去执行。比如,只有按键按下才去亮灯。
好处是响应会更快,不需要等下一个时间片才执行;解耦性也好,每个模块只要定义好什么事件触发,需要执行其它模块执行也只要发出事件即可。
缺点事件如果一多,那管理和逻辑也会变得复杂,而且解耦性越好,那时序的控制力会越弱。
为方便演示,先写一个简单的示例:
[code]#define PIN_KEY_1  P1_0#define EVENT_TICK (1
您需要登录后才可以回帖 登录 | 立即注册