2011-2014,构建原子力显微镜的感悟

以下内容是我2011-2014年间搭建原子力显微镜的主要总结。


不知不觉在学校度过了三年光阴,回想这三年,我几乎把所有的精力都放在了原子力显微镜的研发上。无论在安捷伦学技术,还是是学校里创新和复刻,这种能全局把握整个系统搭建的感觉无疑是非常令人兴奋的。三年结束了,我也将投身另一个更加复杂的系统--核磁共振。我也觉得我有必要将我的key-learning总结一下:


光学与机械部分:

---尚未写完---


电子部分:

---尚未写完---


软件部分:

一年前,我一直认为软件是整个仪器中最简单的部分,不需要各种阻抗匹配,不需要算传递函数,不需要时序逻辑仿真,不需要准直光路,不会烧片子,不会受各种伤……总觉得把光学和机械部分搞定,然后把FPGA和MCU固件写好,软件不过就是显示和保存数据而已,没什么大不了的。其实不然,当仪器的数据速率和数据量达到一定的程度之后,软件同样会成为整个系统的瓶颈。试想如果软件来不及记录数据,或是无法连贯地显示数据,那即使硬件再快、再牛X也是白搭。

  在参考了众多设计,走了n多弯路之后,我这里想把感悟写出来和大家分享一下。(以下内容基于.NET 4.5 和 WPF)

当前在WPF中流行的MVVM设计模式指出,程序应当分为Model,View和ViewModel三个层次。不过MVVM是一种普适的设计模式,对于仪器软件而言,由于还涉及到数据采集和控制的方面,所以还应该把Model层分为分成HAL(硬件抽象层),Core和DataStorage三层。

在属于Model的这三个子层中,HAL用于包装驱动,如果驱动是C++或者更底层的,一般就需要和非托管的代码混编来实现对设备和数据的访问。不过一般来说正规的接口芯片(如cypress等)或仪器设备供应商(如oceanoptics等)都会提供.NET包装过的API,HAL部分直接引用驱动中的dll文件即可得到托管语言的接口函数。除了读取和写入数据,HAL往往还承担这数据格式转换,装包/拆包,指令翻译等工作。此外,如果硬件设备不支持数据推送,或者驱动接口中没有数据到达之类的事件,那HAL还有一个任务就是执行轮询,并当查询到新数据时向上层发送数据到达事件。Core的功能是数据融合和指令封装。一台仪器往往会有许多不同的硬件模块组成,也就是说,一个仪器软件中可能需要和多个HAL层的模块打交道。Core的功能就是将来自于多个硬件模块的数据融合到一起,生成用户需要的数据(比如将来自多个心电电极的电压信号统一起来,获得有意义EEG信号),或是将一个用户的指令分派到不同的硬件模块中去(比如用户输入了一个2维平移向量,Core需要将这个指令拆分,并分别发送到X和Y两个步进电机中去)。同时Core还必须控制好各个模块之间的同步关系。DataStorage有两方面的功能,一是存储Core传上来的数据,并将其整理、储存在内存中,而是为界面提供数据。

View和ViewModel在各种技术博客和教科书中铺天盖地,这里就不赘述了。不过在设计ViewModel和View的时候我有几点经验可以和大家分享一下:1.将HAL,Core,DataStorage,ViewModel,View分成五个不同的工程,前四个是Library,View是WPF,这样可以最大程度上督促自己及合作者严格将数据和展现分离。2.理论上,ViewModel可以控制View的一切行为。任何界面逻辑都应该体现在ViewModel层面,而非View层面。换句话说,即使View层所有的类都是Private的,我们依然可以完全控制界面的行为。3.View层不应该出现代码。微软设计的xaml可以充分描述View中的所有元素,如果出现了xaml无法描述,必须使用代码控制的内容,请确认它属于以下几种类型之一:a.构造器及调用InitializeComponents()函数 b.声明及包装依赖属性。c.在依赖属性变化时处理界面显示元素。d.处理鼠标事件 e.定义命令(仅限实现ICommand接口的、单例的命令,CanExecute()和Executed()应该定义在ViewModel中)。如果有代码不能够被分类到这几种类型中,则说明在架构设计上存在缺陷。此外,View中的使用代码定义的函数和变量都应该为protected或private修饰的。因为View相关的操作都应该在ViewModel中对应的操作,所有的界面操作函数应该都出现在ViewModel层里。

这里还有一个小Trick,那就是线程的使用。高速仪器软件使用多线程是不可避免的,当软件复杂度提高时,各种线程干扰问题就会让人很头大。我来谈谈我的解决方法:1.在HAL与Core通信的函数(包括事件处理函数)上使用async和await关键字,尽量避免手动开线程(的确对同步性要求很高的场合除外)。2.以DataStorage为分界,DataStorage以下的层中只使用一个worker线程或async task,DataStorage以上的部分只使用主线程和async task,这样一来,唯一可能出现线程不安全的地方就在DataStorage里,在DataStorage里适当加锁就可以保证整个程序线程安全。3.主线程不要响应来自DataStorage层以下的事件,也不要从DataStorage层以下去获取数据。按照通常思维,Core里收到新的数据,就应该被显示出来,但是高速仪器的数据速度往往会远超人眼的处理速度,所以如果每帧数据都显示,很用可能造成冗余显示——不仅显示闪烁过快,而且拖慢整体性能。一个比较好的办法是在DataStorage里加一个频率合适的DispacherTimer,以一个固定的、合适的速率轮询DataStorage获取用于显示的数据,相当于把DataStorage作为实际数据与显示数据之间的缓存。虽然这样显示的数据只是实际数据的一个片段,但后期需要分析数据时依然可以查询到所有的历史数据。

暂时就想到这么多了,以后有想法了再补充吧~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值