网站关键词怎么添加,茂名市网站建设,dede title 我的网站,开发一个app需要什么流程1、嵌入式软件与设计模式 思从深而行从简 软件开发#xff0c;难的不是编写软件#xff0c;而是编写功能正常的软件。软件工程化才能保证软件质量和项目进度#xff0c;而设计模式使代码开发真正工程化#xff0c;设计模式是软件工程的基石。
所谓设计模式就是对常见问题的…1、嵌入式软件与设计模式 思从深而行从简 软件开发难的不是编写软件而是编写功能正常的软件。软件工程化才能保证软件质量和项目进度而设计模式使代码开发真正工程化设计模式是软件工程的基石。
所谓设计模式就是对常见问题的通解合理地运用设计模式可以很好地解决很多问题每种模式针对一个通用问题以及该问题的核心解决方案这也是设计模式能被广泛应用的原因。真正的高手能云淡风轻地用最简单的方法解决最复杂的问题这也是高级程序员与新手的本质区别之一。
一般常见的是四人帮模式即GOF的23种设计模式是偏向于可复用的面向对象的软件并不能很完美的契合嵌入式软件因为嵌入式C语言是结构化的语言与硬件关联。虽然也可强制封装结构体实现类似效果复杂的嵌入式应用软件也可使用但对于通用PC的高级语言存在差距。
基于嵌入式系统的工作流选择合适的设计模式或代码框架将复杂软件解耦或者分层提高代码复用度和可扩展性具有一定意义当然代价是对资源和实时性的损耗。
针对嵌入式系统软件提供四大类设计模式。 嵌入式系统软件设计模式 1、硬件访问类 2、并发同步类 3、状态与工作流类 4、安全性与可靠性类 设计模式是在已具备一定开发基础的前提下对软件架构的优化因此部分章节需要熟悉数字电路、RTOS实时操作系统等才能更好的理解。更多理论信息可关注微信公众号【嵌入式系统】的其他文章。
2、硬件访问类设计模式
2.1 硬件访问的概念
嵌入式系统最明显的特性是可直接访问硬件硬件操作通常会包括初始化、配置、控制等步骤嵌入式软件管理硬件给硬件提供命令或数据或者从其获取信息这块即是常说的硬件驱动代码。这类软件设计主要是考虑硬件器件的更换与兼容对业务层的封装和隔离。
2.2 硬件代理模式
硬件代理模式Hardware Proxy Pattem使用结构体封装所有硬件设备访问无论其硬件接口是怎样的代理为客户提供与硬件形态无关的接口。
如果应用层直接访问硬件设备硬件的变化所导致的问题会加剧一个细节的改变应用层均需要重新调整通过提供介于应用层和硬件之间的代理就极大地限制了硬件改变的影响从而减少这样的修改。类似常说的代理律师就是即使不懂法律但可以请律师由代理律师去活动。
硬件代理接口可以形如 init、open、close、read、write、control其内部实现无需开放。函数名称或者结构体内函数指针名只是参考也可以自定义诸如config等这里只是说明对不同的硬件设备统一访问接口封闭细节。
但是也有一定缺点硬件细节已经在硬件代理内部完全封装对运行实时性有不良的影响。其实所有的设计模式都是利于软件维护而不利于实时性。代理模式只是简单封装接口不能实现线程安全除非在封装时额外增加加临界区或者队列保护。
例如开启加速度传感器最初需求只有一颗如BMA425其开启接口为 gsensor_bma425_open但后期硬件替换为SCA720如果是直接操作则需重写驱动将原开启接口全部替换为gsensor_sc7a20_open。这种方式固然效率高但也导致代码维护困难从驱动层到业务层都需要替换接口。而且不能实现软件自动识别其型号自动匹配驱动对项目维护也是较大工作量。
如果改为硬件代理则可以先封装函数指针结构体伪代码形如
typedef struct
{*init;*open;*close;
}gsensor_ops_t;gsensor_ops_t bma425;
gsensor_ops_t sc7a20;
gsensor_ops_t* p_gsensor_ops;所有业务层访问p_gsensor_ops至于其究竟是哪一颗传感器则无需关注扩展或者更换硬件无需修改业务层。最佳的方案是驱动层可以根据硬件固有差异如芯片ID或者I2C从机地址识别出具体硬件型号自动将p_gsensor_ops指向具体的型号的驱动接口对软件版本和生产维护更加友好不管硬件如何变化软件一版即可。
2.3 硬件适配器模式
硬件适配器模式 Hardware Adapter Patterm) 在两个接口之间进行转换使已经存在硬件接口能适应新的期望。
适配器模式的最直观应用是手机充电器市电220V的交流电但手机只支持5V的直流电要确保手机正常充电就是在充电链路中增加适配器将220v的交流转换为5v的直流。
一般来说具有同一个功能的硬件器件其接口往往相似比如前面提到的加速度传感器不管是哪个厂商都是提供I2C或者SPI接口。硬件适配器模式则是在业务层和硬件层之间加入相互转换创建适配器提供客户期望的接口而不是重写硬件设备的接口最少化返工代码。
在软件开发中经常有同个物理量不同硬件的表示数值不同前面提到的加速度传感器因为量程和精度的差异加速度大小1g的表示值不同的传感器xyz三轴数值不同有的是512表示1g有的是256表示1g这样对业务层的逻辑算法就难以统一标准。所以可以在业务算法和硬件驱动之间增加适配转换统一1g的表示值这样才能保证硬件的变化不影响算法。
再比如简单的例子将第三方库移植到不同的平台因为参数等信息不完全相同原来是传入1-100表示百分比但新接口是以0.01~1.00表示则需要在调用接口前转换两者关系。所以硬件适配器模式一般用在硬件器件更换或者软件跨平台移植它并没具体的接口套路是因地制宜按接口形式转换。
2.4 中介者模式
中介者模式Mediator Pattern是用来降低多个元素之间的通信复杂性提供一个中介类协调处理不同类之间的通信各子类之间不直接通信松耦合使代码易于维护。
比如而二手房交易有10个买家与10个卖家如果都直接去沟通对接每人需要和10人对接而且信息没经过过滤沟通效率低如果有个中介所有人都只与中介对接中介再按买家和卖家意愿转达有效意见买家与卖家无需直接交流就能促成满意的交易。每添加新元素就需更新中介者最终可能导致中介者越发难以维护如果中介者出现问题则中介功能包括相关元素程序崩溃这和房产交易中介卷钱跑路一样。 软件中定义两种角色分别为合作者和中介者合作者 (Collaborator) 指所有可能被中介者调用的具体对象中介者 (Mediaior) 协调多个具体合作者。中介者需要明确每个合作者交互的消息从哪来到哪去。当感兴趣的事件发生时合作者可以给中介者发送消息中介者提供协调逻辑或者与消息关联的合作者通信。
中介者与每个具体合作者一般通过多个指针连接如果具体的合作者的接口一致指针数组是最好的。两种角色的结构体定义伪代码形如下针对的场景是根据汽车引擎点火acc状态加速度传感器gsensor监测的震动信息GPS卫星定位获取运行速度三组数据组合判断当前车辆是处于什么状态。
//中介者管理3个关联合作者
typedef struct mediator_t
{colleague_t *acc;colleague_t *gsensor;colleague_t *gps;mediator_relay relay;
}mediator_t;//每个合作者提供2个接口向中介者发消息和接收处理中介者的消息
typedef struct colleague_t
{mediator_t *m_mediator;colleague_send send;colleague_receive receive;
}colleague_t;合作者发送消息时不明确是谁执行只管发送中介者需要识别类型按既定规则转发给对应的合作者执行。这也是中介者模式的缺点中介者代码庞大随着合作者数量的增加会变得复杂难以维护。完整的演示代码如下。
//微信公众号嵌入式系统
#include stdio.htypedef enum
{EVENT_1,EVENT_2,EVENT_3,EVENT_MAX
}event_t;//模拟测试消息struct mediator_t;
typedef int (*mediator_relay)(event_t id,void *data,int len);struct colleague_t;
typedef int (*colleague_send)(event_t id,void *data,int len);
typedef int (*colleague_receive)(event_t id,void *data,int len);typedef struct mediator_t
{colleague_t *acc;colleague_t *gsensor;colleague_t *gps;mediator_relay relay;
}mediator_t;typedef struct colleague_t
{mediator_t *m_mediator;colleague_send send;colleague_receive receive;
}colleague_t;/*******************************************************/
//合作者接口
static colleague_t colleague_acc{0};
static colleague_t colleague_gsensor{0};
static colleague_t colleague_gps{0};
static int colleague_acc_send(event_t id,void *data,int len)
{colleague_t *handlecolleague_acc;handle-m_mediator-relay(id,data,len);
}static int colleague_acc_receive(event_t id,void *data,int len)
{printf(ACC recv id%d,%s\r\n,id,data);
}static int colleague_gsensor_send(event_t id,void *data,int len)
{colleague_t *handlecolleague_gsensor;handle-m_mediator-relay(id,data,len);
}static int colleague_gsensor_receive(event_t id,void *data,int len)
{printf(gSensor recv id%d,%s\r\n,id,data);
}static int colleague_gps_send(event_t id,void *data,int len)
{colleague_t *handlecolleague_gps;handle-m_mediator-relay(id,data,len);
}static int colleague_gps_receive(event_t id,void *data,int len)
{printf(GPS recv id%d,%s\r\n,id,data);
}/*******************************************************/
//中介者接口
static mediator_t mediator_manager{0};//中介者协调全局将对应的事件转发给有需要的合作者范例只是说明用法随意定义的关系
//这个函数中介者模式维护的重点也是它的缺点
static int mediator_msg_relay(event_t id,void *data,int len)
{mediator_t *handlemediator_manager;switch(id){case EVENT_1:handle-gsensor-receive(id,data,len);break;case EVENT_2:handle-gps-receive(id,data,len);break;case EVENT_3:handle-acc-receive(id,data,len);break;default:break;}
}/*******************************************************/
//测试接口
//如果觉得这样有一定耦合度可以由中介者提供注册API给合作者调用传入自身地址给中介者
static void init_member(void)
{colleague_acc.m_mediatormediator_manager;colleague_acc.sendcolleague_acc_send;colleague_acc.receivecolleague_acc_receive;colleague_gsensor.m_mediatormediator_manager;colleague_gsensor.sendcolleague_gsensor_send;colleague_gsensor.receivecolleague_gsensor_receive;colleague_gps.m_mediatormediator_manager;colleague_gps.sendcolleague_gps_send;colleague_gps.receivecolleague_gps_receive;mediator_manager.acccolleague_acc;mediator_manager.gsensorcolleague_gsensor;mediator_manager.gpscolleague_gps;mediator_manager.relaymediator_msg_relay;
}//微信公众号嵌入式系统
int main(void)
{printf(embedded-system\r\n);init_member();colleague_acc.send(EVENT_1,(void*)from acc,0);colleague_gsensor.send(EVENT_2,(void*)from gsensor,0);colleague_gps.send(EVENT_3,(void*)from gps,0);return 0;
}
看懂范例才能更好的理解中介者模式的价值。
//微信公众号嵌入式系统
//运行结果
embedded-system
gSensor recv id0,from acc
GPS recv id1,from gsensor
ACC recv id2,from gps三个关联合作者互相交互只处理与自己关联的事件对外或者app有问题就找中介不与具体的合作者通信。
2.5 观察者模式
观察者模式提供一种方法来使对象“监听”其他对象而不需要修改任何数据服务器。在嵌入式领域适合传感器采样或者某些周期更新的数据转发给关注它的元素比较类似发布--订阅模式。
好比在红绿灯路口交通信号灯由绿变红整条车道的车都会收到该信息并进行制动处理。信号灯本身不关注外界只是按自己的节奏控制灯的变化而众多车主观察到红灯都进行停车动作。
一个目标对象的状态发生改变所有的依赖对象观察者对象都将得到通知。在嵌入式软件的实现上观察者模式通过在数据生产服务器添加订阅和取消订阅服务器端不需要任何客户的先验信息。数据服务器按一定的更新策略通知对其感兴趣的客户。
软件上通知列表最简单的方式是定义一个足够大的数组来包含所有潜在的客户在有很多客户的高度动态的系统中这会浪费内存另一种方案是用链表构建一个系统。数据产生服务提供众多函数指针有需求的客户提供自身句柄一般是回调函数指针当响应数据变化按既定策略执行回调实现数据源的变化信息广播到所有观察者的效果。
例如设备支持GPS卫星定位在驱动上报GPS信息的接口底层提供一个函数指针数组上层用自身的回调函数填充数组。底层获取到GPS信息后查询数组若回调函数非空则执行。每个回调函数由各模块分别实现代码整洁耦合性低。
//微信公众号嵌入式系统
#include string.h
#include stdio.h#define PAL_GNSS_SUBSCRIPTIONS_MAX 5typedef unsigned char uint8_t;
typedef void (*gnss_info_callback)(void* data);typedef struct
{gnss_info_callback m_cb;
} pal_gnss_subscription_info;//订阅池
static pal_gnss_subscription_info g_gnss_subscription_pool[PAL_GNSS_SUBSCRIPTIONS_MAX] {0};//订阅
uint8_t pal_gnss_subscribe(gnss_info_callback callback)
{uint8_t i;uint8_t ret0;if(callback ! NULL){for(i 0; i PAL_GNSS_SUBSCRIPTIONS_MAX; i){if(g_gnss_subscription_pool[i].m_cb NULL){//RTOS注意竞争g_gnss_subscription_pool[i].m_cb callback;ret 1;break;}}}else{ret 0;}return ret;
}//取消订阅
void pal_gnss_unsubscribe(gnss_info_callback callback)
{uint8_t i;for(i 0; i PAL_GNSS_SUBSCRIPTIONS_MAX; i){if(g_gnss_subscription_pool[i].m_cb callback){//RTOS注意竞争g_gnss_subscription_pool[i].m_cb NULL;break;}}
}//广播给观察者执行回调
void pal_gnss_info_update(void)
{uint8_t i;uint8_t data1;//testfor(i 0; i PAL_GNSS_SUBSCRIPTIONS_MAX; i){if(g_gnss_subscription_pool[i].m_cb ! NULL){//RTOS中使用消息队列更好这里只是演示效果g_gnss_subscription_pool[i].m_cb((void*)data);}}
}int main(void)
{printf(embedded-system\r\n);return 0;
}观察者模式订阅-发布机制尤其传感器采集数据分发给不同模块各模块收到广播后按自身需求处理数据的场景。
2.6 消抖过滤模式
这个简单的模式用于消除来自于金属表面间歇性连接引起的多个假事件。
按钮、拨动开关和机电式继电器等机械式输入设备它们都有一个共同的问题即接触金属产生连接金属变形或“弹性”在开关转换时产生间歇连接。从而导致控制系统中有多个电子信号。消抖过滤模式通过在首次检测到异常信号后等待一段时间将多个信号减少到一个信号。简单且常见的场景就是按键消抖这也是嵌入式入门的基础。
嵌入式系统软件检测到首次跳变事件设置延迟定时器 〈如果需要关闭设备中断) 随后检查设备状态。一定时间后去抖动时间如果状态不同则事件一定是真实的则发送相应的信息给应用层。因为按键防抖属于嵌入式软件入门基础这里不做详细描述重点关注定时器可以采用CPU延时等待也可以采用硬件定时实现后者更合理。
关于按键检测底层区分按键码和按键事件类型短按、长按、连续按可以参考 《按键检测》结合观察者模式使用二维数据管理按键回调函数可以实现按键检测驱动与业务的隔离。
2.7 中断模式
物理世界从根本上来说是并发与异步的事情该发生时它就会发生如果嵌入式系统不加以注意这些事件可能丢失。为了及时监测感兴趣的事件硬件中断模式即中断中断服务程序是最有效的方法。
中断模式是一种构造系统的方式用于对传入事件作出适当的反应。在嵌入式系统中事件分为不同等级的紧急度即使在系统非常繁忙地处理其它事件时也必须处理。在本章其他内容中讨论的轮询模式中在系统方便的时候查询感兴趣的事件虽然这是有好处的不中断当前正在执行的过程但它的缺点是高紧急度和高频率的事件可能得不到及时处理。中断模式通过立即停止当前的过程处理传入事件来解决这个问题并且随后返回原来的流程。
通常情况下当中断服务程序ISR执行时关闭中断这意味着中断服务程序必须快速执行以确保不会丢失其他中断。因为中断服务程序必须很短当它们调用其他的系统服务时必须非常小心。如果ISR 处理占用太长时间在共享资源上出现竞争条件或发生死锁问题很难跟踪。
中断模式是嵌入式软件特有的硬件中断的特点是响应及时处理要简短尤其是在RTOS中。
2.8 轮询模式
另一种从硬件获取传感器数据或信号的常用模式是定期检查称为轮询过程。当数据或信号不是非常紧急到不能等待到下一个轮询时段来收取或当数据或信号可用时硬件没有能力生成中断或缺乏中断检测口这时轮询非常有用。
轮询分定期或者不定期进行定期轮询使用定时器按固定间隔查询不定期即机会轮询是当系统方便的时候才轮询没有固定间隔。
定期轮询主要用于周期性的变化或者变化很缓慢的状态按合适的间隔定时查询设备状态如果数据或信号轮询时间加上反应处理时间比数据更新间隔长那就必须引起注意否则数据将会丢失。因为定期轮流其本身检查绑定一个定时器中断因此定期轮询模式其实是中断模式的特殊情况。
不定期轮询是当系统方便的时候才轮询如在主系统功能或在重复执行的周期点之间在低端单片机上比较常见对其他系统从事的活动的及时性影响也小。非定期的模式如果时间非常短也可以嵌套在其它驱动中诸如死循环循环等待某个状态比如查询UART发送完成IIC的ACK响应但是这种循环体一定要注意必须留有一定会退出循环的条件。
2.9 小结
硬件代理模式关注指定硬件细节的封装解决硬件元件更新迭代和多个同类器件的兼容。
而硬件适配器模式为适应不同但是相似的硬件也解决跨平台移植。
硬件器件组合工作或者软件需求的复杂交互则适合中介者模式。
观察者模式为硬件数据支持动态添加和删除客户适合多个模块共享传感器数据的场景。
消抖过滤模式、中断模式、轮询模式用于解决与硬件交互的低层次问题。
软件模式的选择与硬件框架、资源和软件需求、应用场景相关合适的才是最好的。
3、并发同步类设计模式
3.1 并发和RTOS概念
基于RTOS的软件宏观上多个任务并行实际是多任务的分时调度对应着硬件资源可能就是前任务还未完成访问后任务要抢占使用这切换过程中就存在竞争。若没有RTOS相关基础可以先参考基于《RTOS的软件开发理论》 和 《FreeRTOS及其应用万字长文基础入门》 否则本章信息可能无法理解。当任务调度启动后所有的任务独立运行如何设计避免一个资源被多个任务抢占使用按串行访问共享资源
3.2 临界区模式
临界区模式是与任务协调相关的最简单粗暴的模式。禁止任务在区域内转换通过禁用任务转换甚至禁止中断来处理竞争关系保证当前任务不间断的执行直到完成相关操作退出临界区。
临界区模式结构简单受保护的元素是资源而不是任务在临界区开始之前禁止任务切换并在服务结束后重新可用。RTOS提供临界区进出时的任务切换使能处理或者直接在硬件级别配置中断等方式开关中断处理。
临界区模式关闭任务调度或者中断响应实际会影响其他任务的时序。因此要求临界区持续时间很短一般是在同一个任务内使用互斥锁或者临界区接口快速完成相应操作否则可能导致系统异常。例如有2个任务模块共享读写某一块内存数据或者操作某个寄存器就比较适合临界区。
3.3 守卫调用模式
守卫调用模式通过提供的锁定机制串行访问以阻止锁定后其他线程的调用服务简单描述就是A任务占用某个资源后将其锁定优先级高于A的B任务想要使用它得先咨询能否使用如果资源处于锁定状态则延时等待相应的任务阻塞即使优先级更高等到前一个任务使用结束解锁释放资源B任务才能执行。
这里面存在一定问题如果还有任务C其优先级介于A和B之间表面上C会先执行导致优先级低的C竟然比任务B先执行即优先级反转实际不会这样。
一般RTOS信号量支持优先级继承即任务B使用某个信号量等待A任务时临时会将A任务的优先级提高和B相等实际执行顺序是B-A-C。
通过信号量的获取和释放来独占的访问某个硬件或者软件资源其对时间没有太严格要求这种在业务层开发更常见。一般在两个任务或者任务与中断间进行锁定确保共享资源按顺序使用也可用于同步交互。
两个任务同步处理的场景AT指令的发送任务和接收解析任务适合信号量发送任务必须等前一AT回复发出AT后阻塞等待信号量接收解析任务确认接收完成释放信号量这时发送任务才能退出阻塞发送下一个AT。
3.3 队列模式
队列模式是任务间异步通信最常见的实现在非耦合的任务间及时通信通过队列先进先出的数据结构发送者将消息存入队列接收者从队列中取出消息。它也提供一种简单方法串行访问共享资源将访问消息排队并且在稍后的时间中处理这就避免了共享资源常见的相互排斥的问题。
消息队列使用异步通信并且不会遇到互斥问题这是因为没有引用共享资源。消息队列以单一的形式避免并发系统中通过传递引用共享信息产生的资源损坏的问题。在传值共享中制作一个信息的副本并且发送给接收线程进行处理。接收线程完全拥有收到的数据并且因此能够自由修改而不需要考虑由于多个写者或者在一个写者和多个读者中共享它们造成信息损坏。其缺点之一是发送者传递消息后不能立即处理需要进程等待直到接收者任务运行并且能够处理正等待的消息。
队列模式通过对数据和命令排队串行访问数据允许接收者每次处理一个。由于它是异步完成所以消息发送和处理之间的时间是非耦合的。这可能不满足系统的性能需求。守卫调用模式也串行访问但是同步执行以便数据和命令传输发生在时间上更加接近。但关于信号量的释放使用出现问题会导致较严重的错误。
队列的最简单的实现策略是消息元素数组。这具有简单性的优点但是缺乏链表的灵活性队列是很容易实现的但是有很多可能的变体。有时一些消息比其他的更加紧急或者重要并且应该在等待的低优先级消息之前处理。扩展添加多个缓冲区 (每个优先级一个) 实现优先级修改或者基于消息优先级 但是这样会很麻烦。
一般还是固定长度的数组队列但是可以向队列头或者尾插入新数据这种满足绝大部分需求这种情况下需要注意的是接收处理要及时避免队列溢出。
例如只有1个UART通过开关切换分时复用接2个外设就比较适合队列模式读写请求缓存到队列按序取出执行避免出现收发数据不完整的问题。
3.4 汇合模式
任务同步发生在简单的函数调用、共享单一资源或者传递数据可用队列模式或守卫调用模式但如果同步需要的条件更加复杂涉及多个任务间的同步汇合模式更适合解决任务间复杂形式同步的问题。
在这个模式中两个或更多的任务通过操作类似全局变量的位按变量相应位的状态执行不同动作。在RTOS内核中如freeRTOS这个全局变量叫事件组其读写操作也有相应的API。
例如3个子任务各自监测不同外设主任务收到3个子任务反馈的外设连接正常的事件主任务才在UI界面提示所有外设连接正常。
3.5 小结
并发才是嵌入式软件开发的常态事情并行发生必须预防竞争与冲突这也是实时操作系统 (RTOS) 的基本要求。
临界区模式、守卫调用模式和队列模式解决在多任务环境下串行访问资源的问题。临界区模式在资源访问期间关闭任务转换因此防止可能的资源数据损坏但是阻塞了更高优先级任务使它们永远不能访问资源。
守卫调用模式通过信号量完成相同的资源保护目标该模式可能能够导致优先级倒置所以该模式需要使用支持优先级继承的方式。
队列模式串行访问资源通过将请求放在队列中并且按照先进先出 (FIFO) 的顺序从队列取出队列模式概念上非常简单但是导致资源的请求响应延迟影响时效性。
看起来这些模式很高级其实主流的实时操作系统其内核都支持这些模式相关的接口。内核开发的大佬们早就洞悉了并发的风险与模式使用内核的互斥锁、信号量、队列和事件组可以很容易的实现这些模式。
如果基于裸机开发主程序和中断程序也近似存在共享冲突可以自定义全局变量、循环数组实现守卫调用模式或队列模式其他模式裸机基本上也用不上。
PS
全文四类设计模式篇幅过长拆分发布其他模式稍后。文中提到的RTOS开发相关可阅读其它文章并发同步类相关的理论比较重要。