嵌入式系统架构浅谈——访问硬件的设计模式

关注、星标公众号,直达精彩内容

来源:网络素材 | ZeroMing222

这系列开始谈软件上面的设计,对设计模式在面向对象里面应该各位都知道,或许你在实际开发当中用到,也或许你见过别人的代码中用到。当你程序的代码足够庞大的时候,你会发现维护寸步难行,牵一发而动全身,这个时候你就能够理解在开发初期对程序架构的搭建重要性。而架构最基本熟知的其中就是设计模式,使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。尝试去研究优秀的开源代码,你会惊叹别人对程序的掌控,这时你会稍稍明白架构的目的所在。

文章基于《C嵌入式编程设计模式》这本书,英文是Design Patterns for Embedded Systems in C。主要是做个笔记,并添加一点个人的理解,分享出来与各位探讨。比较针对嵌入式系统,单片机,程序已C语言为主,尽管是面向过程,但不妨碍我们使用面向对象的思维来开发。


1. 访问硬件的设计模式

嵌入式系统,特别单片机最明显的是对硬件的直接访问。基础硬件不仅有CPU,内存,键盘,传感器,通讯RS232等这样的设备。做单片机的不得不对硬件进行控制,读,写操作,而这篇文章已解决管理和操作这些硬件通常的一个模式。或许对你来说并不陌生,但是是否能够系统的,详细的表达出来这就不仅仅只是了解就能达到的。

下面讨论的设计模式已经在操作硬件上得到证明是可靠有效的。简单总结说,硬件代理模式是以封装详细信息为目的的硬件抽象的一个原型模式,它有可能改变提供给硬件或来自硬件的信息处理方法。硬件适配器模式扩展硬件代理模式,以提供支持不同硬件接口的能力。中介者支持多种硬件设备的协调,实现系统级行为。观察者模式是发布遥感数据到需要的软件元素的方法。去抖动模式和中断模式是硬件设备接口简单重用的方法。定时器模式扩展中断定时器为嵌入式系统提供精确时序。

1.1 硬件代理模式

硬件代理模式概念是对访问硬件接口的封装,限制客户直接访问硬件造成问题。

1.1.1 模式结构

模式结构非常简单,可能客户会有多个,但是每个硬件设备仅有一个硬件代理,客户只能访问代理接口,无法直接访问硬件就是这个模式的目的。

1.1.2 角色

1.1.2.1 硬件设备(HardwareDevice)

硬件设备可以是各种,内存,传感器等,包含了端口地址,内存地址,寄存器地址等等元素。与硬件代理的关联是通过软件寻址方式,对硬件的读写操作。

1.1.2.2 硬件代理(HardwareProxy)

这个是系统中的主功能。给上层应用提供的硬件访问接口,上层应用无须详细关心硬件的具体实现。基本上通常每个代理都有initialize()、configure()和disable()函数。大部分还会有对设备的值读取访问,或者写访问接口。但是一般不能随意读写,会详细到读取到最终的值。

函数包括:

access():从设备返回一个特殊值。大多数情况下,代理会对每个来自设备单独的信息提供单独的函数。例如返回传感器的温度,湿度值。

configure():提供硬件配置的方法。一般会有参数列表,通过传入参数来配置正确的工作状态。

disable()、enable():提供设备的安全禁用或开启的方法。

initialize():用于第一次启动时候的初始化硬件。

mutate():用于向设备写入数据,通常总是有一个或更多的输入参数。

marshal()、unmarshal():这两个为私有函数,用于把客户数据格式转为硬件所需格式,后者相反,把硬件原始数据格式转换为客户格式。常用于加密解密,压缩解压缩等。

deviceAddr:是一个私有变量,提供底层直接访问硬件的地址。必须隐藏在代理中,不能给客户访问的机会,所以特别注意到一些接口,是否会通过了指针把该变量暴露出去。

1.1.2.3 代理客户(ProxyClient)

客户代码调用硬件代理服务来访问硬件设备。

1.1.3 效果

该模式非常普遍并且具有封装硬件接口以及编码系统的所有优点。这为不对客户端进行任何改变而从根本上改变实际硬件接口提供了灵活性。基本上所有的硬件设备都能用此模式搭建,注意的是不能暴露细节,只能返回一个最后的结果,特别在读写操作,否则就不具备有封装性了。

1.1.4 实现

可以有很多不同方法用C语言实现,最常见的是如linux驱动,使用结构体里的函数指针统一硬件的接口。然后在具体的硬件设备上实现。

 

1.2 适配器模式

硬件适配器模式提供一种方法,使已经存在硬件接口能适用应用期望。可以说是在硬件代理模式基础上,为了能够适应底层不同的硬件设备,在中间增加一层适配器。比如在通讯上面在硬件上都存在RS232,RS485,程序需要在不同情况下使用232通讯或485通讯,而适配器可以提供统一的接口给客户层,通过指针指向所需通讯,则可以实现。最大的特点是在运行中选择,相比使用宏定义需要生成不同执行程序,可以在程序中实现自适应的功能。

1.2.1 模式结构

 

1.2.2 角色

1.2.2.1 硬件适配器(HardwareAdapter)

硬件适配器在客户和硬件代理之间执行匹配。客户告知适配器所需的硬件设备,适配器执行客户的请求。

1.2.2.2 客户硬件接口(HardwareInterfaceToClient)

客户的硬件接口表示客户期望硬件代理提供的一组服务和参数列表。仅仅作为接口,并没有实现,是通过适配器提供硬件实现。

1.2.2.3 硬件设备(HardwareDevice)

与硬件代理模式中描述一致。

1.2.2.4 硬件代理(HardwareProxy)

与硬件代理模式中描述一致。

1.2.3 效果

该模式允许使用各种硬件代理,并且在不同的应用中使用与它们相关的硬件设备,同时亦有的应用使用不同的硬件设备时不需要做改变。我个人理解有点类似是面向对象语言中的多态概念。

1.2.4 实现

同样如linux系统驱动,创建一个结构体的接口代理,硬件设备使用这些接口具体实现,然后使用一个指向结构体接口的指针,把需要使用的硬件设备注册到指针上,客户代码只需调用这个指针,即可操作具体的硬件设备,而且可以动态的修改指针的指向,从而实现动态的加载切换。

 

1.3 中介者模式

中介者模式提供的是为一组硬件设备复杂交互协调的一个方法。

1.3.1 模式结构

 中介者模式使用一个中介类来协调各个设备集合的行为,来达到整理的一个效果。这里举一个具体的例子,比如一台车有4个轮子,也就4个电机设备,当向前行驶的时候四个轮子都是向前前进,这时候中介者就承担了控制4个轮子的责任。所以说中介者其实就是一个中央控制。

1.3.2 角色

1.3.2.1 合作者接口(CollaboaratorInterface)

是被中介者调用的接口,对于硬件通常是initaialize(),enable(),reset()等这类函数,但是具体的是在具体合作者实现。

1.3.2.2 中介者(Mediator)

在模式中协调所有的具体合作者。中介者对于每个具体合作者都有一个链接,以便他能给具体合作者发送信息。此外,当有事情发生时,每个具体合作者必须能给中介者发送消息。中介者提供协调的逻辑。

1.3.2.3 具体合作者(SpecificCollaborator)

表示一个硬件设备。可以从中介者获取命令,也可以发送信息给中介者。

1.3.3 效果

该模式创建中介者来协调合作具体硬件,但是对客户来说又不需要直接耦合硬件设备,极大的简化了整理的设计。很多嵌入式系统必须高精度时间相应,动作的延时可能造成不可估计的影响,中介者能够在这些规定时间反应很重要。

1.3.4 实现

中介者的实现可以通过指针数组,链表等,能够连接到每个具体的合作者。另外统一接口能够给中介者代码上带来很多便利。

 

1.4 观察者模式

观察者模式非常的普遍,你可以在任何地方看到它的身影。这模式提供一个方法来“监听”所感兴趣的消息,而不需要修改数据服务器,这意味着传感器数据很容易分享给所需的客户。

1.4.1 模式结构

 观察者模式,另外一个名字是“发布-订阅模式”。首先模式下数据服务器不需要清楚客户,相反是由客户通知数据服务器,也就是订阅。订阅意思是允许数据服务器在通知列表中添加(和删除)自身。最常见的通知策略是当新数据到达服务器时,服务器发送数据。但是客户也能定期更新,向服务器获取数据,以减小服务器的计算负担,确保客户具有实时数据。另外更复杂的模式是在数据服务器和客户中间添加一层中央控制器,用于连接服务器与客户的通讯,这样服务器就完全不需要与客户直接联系。如果有大量使用消息使用观察者模式,添加中央不失为一种好方法。

1.4.2 角色

1.4.2.1 抽象客户接口(AbstratClient)

它包含了accept(Datum)函数,当AbstratClient订阅时或者AbstratSubject认为有适合发送数据去调用它。AbstratClient是抽象的,不提供任何具体实现。

1.4.2.2 抽象发布接口(AbstratSubject)

在模式中AbstratSubject是数据服务器。在提供模式相关的3个函数。subscribe(acceptPtr)服务添加指向接收函数通知列表的指针。unsubscribe(acceptPtr)函数从通知列表中删除接收功能。最后,notify()函数遍历通知列表通知订阅的客户。

1.4.2.3 具体客户(concreteClient)

concreteClient是AbstratClient接口的具体实现。

1.4.2.4 具体发布(concreteSubject)

concreteSubject是AbstratSubject接口的具体实现。不仅提供函数的实现,而且提供获取和管理它发布数据的方法。扮演concreteSubject也可以是硬件设备,传感器等。

1.4.2.5 数据(Datum)

该元素是实际的数据包,可以是int,更多的是复杂的结构体。

1.4.2.6 回调接口(NotificationHandle)

NotificationHandle是调用客户的accept方法的代表。最常见的实现方式是函数指针。

1.4.3 效果

观察者模式是在服务器分配数据的过程,并且在运行时可以动态地管理客户列表。实际一个例子,读取硬件的值,通常我们可能是使用轮询的方式读取,轮询的弊端是响应不及时,读取间隔时间很难去固定和评估。另一种方法是定时中断读取,但是定时读取未必每次都会有数据产生。还有是触发中断的方法,如果在中断读取数据后,需要计算,在中断里进行可能不太好,原则是尽量不要中断占用太多的CPU。这个时候观察者模式的好处体现出来了,首先能够保证响应及时,因为使用的回调方式,第二能一个硬件发布,多个接收客户,一对多的模式,第三能够确保每次执行客户回调都能有数据产生。其实观察者模式随处可见,ROS系统的节点通讯就是基于这个策略。该模式明显的缺点是实现较复杂,而且当然也不是所有情况都适应,希望各位能够详细分析后,选择合适的方法。

1.4.4 实现

该模式复杂的方面在通知句柄的实现,以及通知句柄列表的管理。通知句柄通常是一个回调函数指针。通知列表最简单的方式是定义足够大的数组来包含所有潜在用户,但是实际占用空间大浪费内存,所以并不常用。另一个常见的是使用链表管理,也就是给每个通知句柄添加在链表上,这样只要遍历链表即可通知所有客户,强烈推荐使用链表形式。

 

1.5 去抖动模式

这个模式用于消除来自硬件金属表面间歇性连接引起的多个假时间。

1.5.1 模式结构

 解决的方案是接受第一次发生的事件,等待抖动减弱,然后再对读取它的状态。

1.5.2 角色

1.5.2.1 应用客户(ApplicationClient)

该元素是去抖动最后的接受者。当在抖动消除后,使用deviceEventReceive()接收最后读取到的值。

1.5.2.2 具体硬件(BouncingDevice)

代表了硬件设备。这个设备绝大部分都是全硬件,机械特性的,所以才会引起抖动的现象。sendEvent()用于发送事件,激活中断接收到首次的响应。getState()操作时通过读取内存或IO端口显示,读取具体的硬件值。deviceState通常是二值属性,即ON或OFF。

1.5.2.3 硬件客户(DeviceClient)

是用于处理进入事件的中断,去除抖动,并读取确保代表实际设备状态。它的eventRecevie()函数通过BouncingDevice的sendEvent()函数激活。同时,它需要设置延时定时器,去抖动事件过后,如果状态与第一次读取的一致,证明值是真实的。这样它就发送相应的信息给ApplicationClient。旧状态保存在变量oldState中,每当状态发生改变的时候更新这个变量。

1.5.2.4 定时器(DebouncingTimer)

这个定时器可以通过delay()服务来提供空闲等待。可以使用while()等待,或者硬件定时器实现。

1.5.3 效果

通常去抖动的任务是由软件来承担,这是一个简单的去抖动,应用程序只需要关心硬件状态产生的真实值才接收。

1.5.4 实现

硬件客户通常使用中断来通知应用客户。或者使用观察者模式混合也可以给等个客户提供信号。在RTOS系统去抖动必须注意时间单位的延时时间,比如如果想要45毫秒的延时,那么必须使用大于等于期望时间最接近时间精度。如果在等待去抖动时,你不介意完全占用CPU,那么这就很简单,使用while(loop--)循环就好了。

 

1.6 中断模式

在嵌入式系统,硬件设备很多时候都是自主发生,如果你不加以注意,这些事件就会丢失。当一个你感兴趣的事件发生时,使用中断来通知是非常有效的方法。基本上芯片都支持外部硬件中断的方式。中断能保证响应的及时,但是中断会抢占CPU的控制,所以中断里面不适合处理算法等这种耗时长的任务。这个模式下可以是纯软件的中断模式。

1.6.1 模式结构

 确保中断函数一般是没有入参,和返回值的。

1.6.2 角色

1.6.2.1 中断响应(InterruptHandler)

是中断模式里面唯一有具体行为的元素。它能够安装和卸载中断向量的功能。install()函数运行时,拷贝传入的中断句柄到向量表中,使用合适的中断服务程序地址。deinstall()函数相反,用于卸载回复原本的向量表。

每个handleInterrupt_x()函数处理指定的中断。

1.6.2.2 中断向量表(InterruptVectorTable)

就是中断服务程序的地址数组。它依赖在指定的内存位置上。当中断号x出现时,CPU挂起当前进程,调用这个数组中相应的第x个索引地址。

1.6.2.3 向量指针(VectorPtr)

VectorPtr是数据类型,具体是一个没有参数和返回值的函数指针。

1.6.3 效果

该模式最大的优势是可以高响应处理感兴趣的事件。通常情况下,当中断服务程序执行时,关闭中断,这意味着中断服务程序必须快速执行以确保不会丢失其他中断。

使用中断有点特别注意是资源的保护。当有可能会在中断和普通程序中处理了同一个元素,设想当普通程序读取数据中途发生了中断,而中断会导致普通程序暂停,然后在中断里面修改了数据返回。普通函数将会读取损坏的数据,即部分是新数据,部分是旧数据。解决方法有1.在普通函数读取数据时禁止中断,访问完成后恢复中断。2.使用互斥信号量。

1.6.4 实现

中断函数执行之前必须保存现场,在执行完成需要恢复现场。事实上每个中断服务程序必须:

  1. 保存CPU寄存器,包括CPU指令指针和任何处理器标志,如进位,奇偶校验。

  2. 清除终端位。

  3. 执行适当处理。

  4. 恢复CPU寄存器。

  5. 返回。

 

1.7 轮询模式

另一种从硬件获取数据常用的模式是定期检查,称为轮询过程。当数据或信号不是很紧急,或者当数据可用时,硬件没有能力产生中断,又或者硬件本身能保留数据到下次读取情况,这是轮询就非常好用。

1.7.1 模式结构

 轮询模式是读取硬件上数据最简单的方法。轮询能够定期或不定期进行,可以是定时器读取,也可以当系统需要时读取。

1.7.2 角色

1.7.2.1 应用过程(ApplicationProcessingElement)

这个元素用于循环调用poll()操作。这个也可以是定时器中断里面。

1.7.2.2 硬件(Device)

Device通过可访问的函数提供数据或设备状态信息。这个类上面实例了两个方法,getData()用来获取数据,getState()用来获取数据状态。MAX_POLL_DEVICE连接到所有的硬件设备,以便poll()函数全部能够扫描并通知给客户。

1.7.2.3 轮询者(OpportunisticPoller)

具有poll()函数,用于扫描连接的设备以读取数据和状态,并把数据转发给客户。这个元素可以添加定时器操作,实现定期读取数据的功能。

1.7.2.4 客户(PollDataClient)

该元素来自一个或多个设备数据和状态信息的客户。

1.7.3 效果

轮询比使用中断服务简单的多,能够同时检测多种不同的设备,但是基本上没有中断响应那样及时,所以使用轮询最好确保最长的读取间隔时间,确保在一个时间内至少读取一次数据,否则数据将会丢失,但有时候这不是一个问题,所以具体情况就具体分析。

1.7.4 实现

最简单的显示方法是在系统操作的主进程循环中插入硬件检查,这叫做“对称机会轮询”,因为它总是以同样的方式操作,即使处理循环事件长度可能变化。非对称机会轮询是在整个进程中方便时候读取新数据(对称是固定位置读取数据,非对称是在需要的时候再读取),这种方式具有更好的响应,但是对主流程有更大影响,而且难以维护。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

推荐阅读:

嵌入式编程专辑Linux 学习专辑C/C++编程专辑
Qt进阶学习专辑

 关注公众号『技术让梦想更伟大』,后台回复关键字:『Qt』『C语言基础』『C语言难点』『C++』『Linux』『freertos』『指针』『数据结构与算法』『经验技巧篇』『疑问篇』『基础理论篇』『实战篇』『架构篇』『模块化编程』『状态机』『实用工具』『心声社区』『期刊』『视频』······等,查看更多精选内容。 


关注我的微信公众号,回复“加群”按规则加入技术交流群。


这是我另一个技术号,程序员的编程学习基地,注重编程思想,欢迎关注!


点击“阅读原文”查看更多分享。

  • 1
    点赞
  • 0
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值