找回密码
 注册
搜索
热搜: 超星 读书 找书
查看: 425|回复: 0

[【推荐】] 通讯类单片机设计心得

[复制链接]
发表于 2009-12-8 20:12:31 | 显示全部楼层 |阅读模式
1个前提,2个基本假设,3个关键问题,另外1个推荐的设计模式。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1个前提:

系统用于通讯而不是用于控制,没有实时性保证。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

2个基本假设:

1. 静态假设。所有对象和系统的生命周期都是一样的。这个假设在绝大多数情况下都可以成立,尽可能使用全局变量减少栈的负担,也减少因为使用堆造成的效率问题。除非有特殊算法需要。
2. 尽快的中断处理原则,ASAP,而不是立即处理。可能大家会觉得这个假设是不是有问题,但是对于面向通讯的应用来说,能接受,就像我们用的Windows也不是实时的,用在通讯方面OK的。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


3个关键问题

第一:并发模型带来的问题,共享访问冲突,两种对象和过程的边界。

Concurrency Model: MCU本身就是并发的,这里指的就是Main和所有的ISR,是不同的Thread。ISR Thread可以在任何时刻抢先(Pre-empt)Main Thread,反之则不能。

遇到的问题是共享数据的访问冲突,相信各位前辈都比我更理解这个问题。解决方法也很简单,访问共享数据时禁止中断,Mutex(Mutually Exclusive)。这看起来似乎不太麻烦。但是且慢。

我们会把很多功能分层封装成模块来实现。有些很贴近底层硬件,有些贴近通讯协议,有些则是设备的应用层功能,而最上层是程序的核心逻辑。那么问题是,在静态模型的假设下,所有变量都是全局变量,我们在写一个模块的某个函数式,做什么样的假定?这个函数是否可以被ISR调用?如果假设是的话,那么它访问的所有共享数据都必须使用Mutex方法来处理,这很疯狂,不但影响速度,而且会过多的屏蔽中断,越是上层的功能代码越是如此。

事实上必须在ISR可以调用的Procedure和不可以调用的Procedure之间有一个明确的鸿沟。

允许ISR调用的Procedure(在TinyOS里被称为Async Command)。它可以在Main Thread中运行时,被ISR抢断调用。TinyOS对写Async函数的建议是:Programming Hint 3: Keep code synchronous when you can. Code should be async only if its timing is very important or if it might be used by something whose timing is important. 我实际看了一下TinyOS中Async函数的代码,绝大多数是动作非常快的硬件操作,在过程中全程使用atomic关键字(相当于关闭中断)。通常提供这样的功能的模块会完整封装一些底层硬件或者全局变量(如果用C++的话,那很好,这些变量就成了Local的了,在编译时就能保证隐蔽性),其他模块必须通过这个界面来访问底层硬件或者全局变量,否则就可能出现共享访问冲突。

所以在模块封装上,硬件、或者共享资源、或者一些需要原子性的工具过程(比如在16bit MCU上实现32bit运算的库),应该干脆独立封装,提供的所有方法均为Async方法。这样就很容易避免Shared Data Access问题。

TinyOS使用了特殊的nesC编译器和语法是有原因的。首先的优势就是它允许了Async关键字,并且在编译时即可检查Async函数是否调用普通的Sync函数,这是不允许的,因为根据假定,Sync是不能被ISR调用的,因为Sync函数没处理共享冲突问题,这意味着如果Main里面正在使用这个Sync函数的时候,如果被一个同样调用了该Sync函数的Async函数运行在ISR中抢断,就会发生共享冲突问题。

在其他的C编译器上,没有这个关键字也没有检查机制了,开发者只能通过类或者函数命名上提醒自己一下。IAR提供了一个函数的修饰字,__monitor,是可以保证函数原子性的。这也算个便利吧。把那些封装共享资源的类中的所有函数都标成Monitor即可。

除了Async封装的对象或者函数之外,其他的所有变量和函数都是Sync的了,他们只会在Main Thread里运行,不论你使用什么样的结构和机制,任务调度、消息分发、信号灯轮回检查,Main不会去抢断自己。每个过程都是有原子性的,就不必到处开中断关中断了。事实上TinyOS也建议尽可能的写sync代码,而不是Async。Async越少越好。

第二:异步调用和任务管理器

这两个词儿听起来挺可怕,其实很简单。我有充分的证据证明这不是在做overkill的事儿。

简单的说有三个方法实现主程序框架:

1. 基于信号灯的轮询,每个信号灯访问用Async封装以避免冲突。一个大大的switch或者if/then。
2. 事件分发。事件分发在本质上和信号灯轮询没什么分别,效率可能还会稍微差一点,因为要维护那个消息队列。但是代码结构上远比信号灯更容易理解,系统可以定义统一的Event数据格式和名称,在代码重用方面也好得多。

但是这两个都是动态方法。在异步调用的时候,我们在静态的时候就知道谁该是某个事件/信号的处理者。比如一段程序调用另外一个模块要发送一个数据包。它不必等待发送完成(Blocking),而是直接返回干别的事情。等得到发送结果,成功完成或者失败,再继续处理。这个就是异步调用,或者说就是把调用处理拆开成两截。那么更高效的办法就是干脆把处理结果的程序指针作为回调地址送过去。这样在被调用的模块,得到最后一个字节发送完成的中断之后,就可以立刻调用处理程序,这个效率比发送事件或者修改信号灯然后走一个switch分支要高效,尤其是Switch很大的时候。

TinyOS就是这样实现的,它把回调称为Event(而不是常见的OO系统中统一的Event数据结构)。它使用了一些特殊语法,同样保证在编译时有严格检查。但是Anyway,在普通的C编译器上同样可以实现这个功能。

在并发模型下,处理信号灯或者处理事件,在TinyOS上就成了维护回调函数指针的队列,它把这个称为任务管理器。很简单的一个函数指针地址队列。提供一个POST方法。它的效率大大优于大面积的switch/case语句。因为是指针吗,拿出来就用。

但是,新的问题来了,参数怎么办?TinyOS做了一个非常简化的假设,所有扔到管理器的函数必须没参数没返回值。哦?听起来很成问题。其实不。Callback本身当然要允许有参数,但是在这些参数其实在异步调用的被调模块那里都有副本,所以在这个模块里可以写一个void Callback(void)函数,在这个函数体内真正的调用那个Callback。带上参数,一句话。这里多了一次Call/Return问题也不大,几个时钟而已,仍然优于switch/case,switch/case即使不做运算,访问内存的时钟也不少了。

它还有另外的好处。如果你把这个Callback wrapper扔到了task队列里,它就在main thread里运行了。这意味着(1)在ISR里你也可以这么做(POST动作是ASync的),之后的回调切换了线程,不会导致任何共享冲突;(2)这个扔到task队列里的任务,会在ISR退出后,当前的代码段执行完毕之后才可能被执行,这意味着不会产生嵌套或者Blocking或者因为call -> callback来来回回把栈给毁了,在每次task完成的时候,栈都归零。(3)你很容易实现deferred procedure call。比如你可以把一个很长时间的计算任务,拆成小的,一个一个post到task队列里。这样就不会占用过多的CPU,block其他的task。系统会更加responsive。当然这个机制你也有别的方式实现,在代码里到处分布检查信号灯的调用,或者通过自定义的Event递进式的进行运算,但是都不如这个是用分拆task的办法更简单,不易出错,且容易debug。

但是有一点要说明,就是ASAP原则,对中断的真正响应时间no promise。因为ISR最终是把回调扔到队列里以保证共享原则不被破坏的,所以它必须等待前面的task完成才能被处理。用于通讯这不是问题,电脑上也就这么回事儿。用于控制这有问题。

实际上在系统内部之间的所有调用都可以不用信号灯或者Event来处理,因为在静态的时候你明确知道谁Call了谁。Callback/Scheduler就够了。但是在处理外部和内部的通讯时,情况有不同。

第三:Event Dispatcher

通讯界面上的接受装置什么时候来了中断是不知道的,它也没有内部的Caller,也不可能在静态的时候就知道该Call谁——行动需要取决于接受的数据。这个时候Event Dispatcher还是需要的。TinyOS在系统一级定义了一个很高Level的Event,用于处理各种无线、有线、甚至Flash存储上的数据包格式。协议可扩展。用到很多经常用于协议处理的设计模式,Facade,Decorator等等。不细说了,有兴趣的看文档,这基本上就是协议设计和处理的问题,和是否单片机没关系了。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

最后一个设计模式是在TinyOS里大量使用的。官方叫它Service Instance,我查了一下Wiki,一般可能更倾向于叫做Service Locator之类。它的用途是这样的。比如Timer,硬件可能只有一个TimerA,而系统的三个模块需要使用Timer,那么可以在底层封装一个TimerA Service,同时再定义一层允许多个实例的Timer对象,实际上只是一个Service ID。把所有的功能都映射到底层服务上去,这样使用起来很便利。TinyOS对几乎所有的共享资源,包括有限资源,如时钟,或者总线设备,如SPI/I2C,都进行了这样的封装。它Virtually给使用者提供了一个独占的访问接口。

实际上在进行这样的封装之后,你也会发现访问共享资源被Block的情况其实会很普遍,Timer这种是可以模拟并发的;但是总线就没戏了。这也看出TinyOS提供异步调用机制是多么必要。这将大大减少CPU的无故等待。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

小节:

1. 在main thread中使用的对象与函数,与在ISR中允许使用的,要有明确的界限,解决共享冲突问题。
2. 异步调用和任务管理器提供高效的内部调用机制,不破坏1的模型,同时允许deferred procedure call,更高效的使用MCU资源。
3. Event Dispatcher处理外部消息。
4. Service Instance简化共享资源封装和使用。


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

说一下语言的问题。很多搞单片机的前辈反对C++。我觉得这不必。C++的好处很明显,它提供namespace,变量和函数命名都方便多了,模块封装可以更简单。C++提供继承和多态,而且可以静态实现,不会带来额外开销,实际上我看了一下IAR提供的EC++的手册,动态绑定的Virtual根本也不被支持。C++的最大负担在错误机制上,和很多库会大量使用动态内存上。前者呢,可以不用,IAR的EC++同样不支持这个。后者是个编程者自己的问题。基于我们前面的静态假设,避免使用动态内存就可以了。另外我其实很喜欢C++的模板,在实现通讯上定义和类型无关的界面很方便,可惜EC++也没支持。这个TinyOS上的nesC则是支持的。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

说一下TinyOS,这是UC Berkeley的一个小组开发了近10年的一套系统,面向无线传感网,支持很多的MCU,Sensor,和Wireless Transceiver。我看中它的原因有三点:第一它大规模部署过,是被实际应用检验过的;第二它面向的正是我们的目标应用,传感器,无线通讯,和超低功耗。第三;虽然我不打算使用nesC和TinyOS平台,但是它是开源免费的,大量的传感器驱动,无线组网协议,在设计和代码两方面都可以提供绝佳设计参考。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

老兄初学单片机,从这个意义上讲应该算小弟,看了一点儿TinyOS的设计就来卖弄了,请前辈们不吝斧正。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|小黑屋|网上读书园地

GMT+8, 2024-5-27 18:05 , Processed in 0.277456 second(s), 5 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表