驱动开发没那么高大上

驱动开发在很多人眼中都是一项极具挑战性的任务,可当你真正去开发一个驱动时,你也许会发现它并没有看上去那样困难,可对大多数人而言驱动开发是八竿子打不着的话题,故而更增强了驱动开发在许多人心中的神秘感。

驱动是操作系统控制硬件的接口,它直接与硬件及操作系统打交道。下面我将从硬件与操作系统两个方面描述驱动开发的大致内容。

1. 驱动与硬件

驱动中对于硬件的控制主要通过配置寄存器来完成。这一过程可能十分复杂却不见得有多大的挑战,更不会有太多的自由发展空间。它无疑是驱动开发中最枯燥无味的环节,然而你一旦忽视,那配置中即便只有一个小小的问题,硬件可能也会罢工,你就得重新梳理,比对寄存器配置,直至找到问题。

配置寄存器以驱动硬件

配置寄存器时的参考资料是芯片手册与官方 demo。如果芯片手册写得比较清楚,那么配置起来便少了许多压力,如果芯片手册写得让人摸不着头脑,那么寄存器配置也会让你头大。或许参考下官方 demo 能够减轻你的压力,只是这种条件并不是任何时候都能够具备。

确认配置生效

当寄存器配置完成后,你需要调试来确认寄存器是否真的按照代码逻辑进行了配置。你常常会先入为主的认为代码中写的逻辑没有太大问题,可实际的执行结果并不一定能够达到你的预期。即便你配置的是正确的也有可能不能正常工作,你还需要考虑编译器优化可能造成的问题。也许你会说一个 volatile 便能够解决大多数的问题,可这样的处理方式并不被建议。那么你需要去研究屏障的使用。你要确认编译屏障、内存屏障该使用在怎样的时机。哎!配个寄存器都会横生枝节,写到这里我也长长的叹了口气!

排除硬件问题

完成了初始化配置后可以写一个简单的中断服务程序进行测试,如果不能正常工作,那么可以进一步查看引脚波形,使用示波器来打一下使用的引脚,从波形可以看出一些端倪,这也是最客观的证据。

其实更客观的步骤应该是先用万用表来确认下引脚是否真的连通,确认没有问题后再使用示波器来进一步分析。只是我们很多时候都不会这样去做。一旦真的遇到这样的问题,那么可能要绕好几圈才能够想起这些问题。

寄存器配置的总结

也许寄存器配置也并非是个难题。只是它是与芯片高度耦合,不同的芯片之间可能会有很大的差异。这也就意味着你在某一个芯片上开发驱动所积累的经验很大一部分不能应用到新的芯片上。其实类似功能硬件的大致工作流程倒也相差不多,只是寄存器配置又得重新学习。

2.驱动与操作系统

驱动与操作系统的之间主要是软件问题。中断服务程序是一个核心。不同的平台中中断处理的过程不尽相同,不同的处理过程所带来的实时性问题也需要考虑。

中断服务程序应该写的尽可能简短。甚至可以将中断服务程序的处理过程进行分离,只在中断服务程序中处理最紧急的操作,其它的操作可以延后处理。linux 中的上半部分与下半部分的处理就是一个很好的实例。

2.1 中间层

中间层是一个抽象的软件层。它向上层提供服务的同时也规定了驱动中的接口。中间层的意义在于更灵活的控制驱动,提供调用驱动的统一接口,同时降低驱动开发的难度。

对于具有中间层的平台,驱动需要适配中间层,需要实现中间层依赖的底层函数,这些函数一般会填充到虚函数表中,通过注册来导入。

中间层可能会特别复杂。驱动开发人员实际开发中并不需要对中间层的逻辑完全了解,只需要专注于实现中间层规定的接口。

2.2 阻塞与非阻塞调用

阻塞与非阻塞的调用一般都由中间层实现。

当用户任务调用 api 接口读取数据时,阻塞方式会在没有数据处理时挂起用户进程,非阻塞方式则会立刻返回。对于阻塞方式,用户进程被挂起意味着内核需要提供某种唤醒机制来在设定的条件达到时唤醒用户进程。常见的方式有信号量、信号、消息队列等。

假设驱动中使用了 linux 内核提供的软中断功能。在中断服务程序中处理最紧急的部分,然后激活软中断,按照设定的优先级调度执行。由于这种下半部分处理机制的优先级仅次于中断处理程序,因此在激活后通常能够很快得到执行。在执行的这段预先注册的内核函数中,判断上层的请求是否完成,完成则唤醒用户进程,这样便完成了一次阻塞的调用过程。如果不使用下半部分的处理机制,这一过程可以在中断服务程序中完成。

2.3 共享变量的保护

在适配中间层时,需要对驱动与中间层共享的变量进行保护,防止数据不一致问题的产生。开发者必须清楚哪些变量不需要保护便能直接访问,哪些变量必须要进行保护。

保护的方法是调用操作系统提供的锁,如自旋锁、互斥锁、读写锁等。尽可能减少共享变量的数量,同时将对共享变量的操作聚合,这样有利于控制锁的范围。至于需要使用什么锁,应该根据实际情况选择。

总结

本文描述了驱动开发中涉及到的硬件与软件部分。限于篇幅与经验,仅仅摘取了主要的部分。麻雀虽小,五脏俱全。希望能让读者对驱动开发中涉及的知识有基本的认识,以拉下驱动开发神秘的面纱。