嵌入式软件开发人员需要掌握的一项基本技能是了解如何编写驱动程序。在嵌入式系统中,通常有两种类型的驱动程序:微控制器外围驱动程序和通过 I2C、SPI 或 UART 等接口连接的外部设备驱动程序。在当今的许多情况下,微控制器供应商为其芯片提供示例驱动程序,这些驱动程序可以按原样使用,也可能需要修改以进行生产。外部驱动程序可能包含伪代码,但开发人员几乎总是自己负责编写驱动程序。
重要的是要意识到编写驱动程序的方法不止一种,而且编写驱动程序的方式会极大地影响系统性能、能耗以及我们在开发产品时喜欢跟踪的许多其他因素。在这篇文章中,我们将研究几种常见的驱动程序设计模式以及它们如何影响应用程序代码。我们将从基础开始,向更复杂的模式努力。
技术 #1 – 轮询驱动程序
第一种技术,也是最基本的技术,是开发一个轮询外围设备(或外部设备)的驱动程序,以查看它是否准备好发送或接收信息。轮询驱动程序很容易实现,因为它们通常只是轮询一个标志。例如,模数转换器 (ADC) 驱动程序可能会启动转换序列,然后简单地阻止处理器执行并不断检查 ADC 完成标志。此代码如下所示:
Adc_Start();
而(ADC_COMPLETE_FLAG == FALSE);
AdcResults = Adc_ReadAll();
返回 AdcResults;
如您所见,上面的代码不断地轮询 ADC_COMPLETE_FLAG,大概是映射到一个硬件位,以便查看数据何时可用。虽然像这样测试硬件位被称为轮询,但它会产生一些有用的特性来讨论。
首先,当我们有一个使用轮询的驱动程序时,在大多数实现中,该驱动程序将是一个阻塞驱动程序。这意味着一旦我们调用驱动程序,它不会从驱动程序返回,直到我们得到我们需要的结果。还有其他实现,我们可以让驱动程序检查一次结果然后返回。在这种情况下,应用程序负责轮询驱动程序,我们认为驱动程序是非阻塞的。从设计的角度来看,由开发人员决定轮询应该在哪里进行。在驱动程序中,应用程序不必这样做,但如果应用程序这样做,则可以灵活地执行其他活动并以较低的速率轮询驱动程序。
其次,总的来说,轮询很容易实现。通常,开发人员需要做的就是观察寄存器中的一些位并监视它们以决定何时与设备交互。最后,虽然它很容易实现,但轮询通常被认为是低效的。其他技术(例如使用中断)可以在需要执行某些操作时通知 CPU,这使得轮询效率相当低。我经常将民意调查与长途旅行中的一个孩子联系起来,他不断地问“我们到了吗?”。民意调查不断地问“你准备好了吗?现在怎么样?现在?”。
这给我们带来了一个更高效但稍微复杂一点的驱动程序实现,即使用中断。
技巧#2——中断驱动的驱动程序
在驱动程序中使用中断非常棒,因为它可以显着提高代码执行效率。中断不是不断检查是否该做某事,而是告诉处理器驱动程序现在准备就绪,我们跳转到处理中断。一般来说,我们可以使用两种类型的中断驱动驱动机制:事件驱动和调度。当外围设备发生需要处理的事件时,事件驱动驱动程序将触发中断。例如,我们可能有一个 UART 驱动程序,当缓冲区中接收到一个新字符时,它会触发一个中断。另一方面,我们可能有一个 ADC 驱动程序,它使用计时器来安排访问以开始采样或处理接收到的数据。
使用中断驱动的驱动程序虽然效率更高,但会为设计增加额外的实现复杂性。首先,开发人员需要启用适当的中断以供驱动程序使用,例如接收、发送和缓冲区满。我通常发现由于现代中断控制器的复杂性,开发人员很难让中断工作。它们通常需要在通用寄存器中、外设级别设置中断,有时甚至需要配置优先级和其他设置。几年前,我整理了一份配置中断的分步指南,可在此处下载。
接下来,使用中断可能需要遵循一整套额外的最佳实践。例如,最好的做法是:
- 保持中断简短
- 将共享变量声明为 volatile
- 处理高优先级项目,然后卸载到应用程序进行处理
您不希望在事件发生时执行数千行代码的驱动程序中断。相反,您希望处理关键任务,例如从 UART 缓冲区中取出一个字符并将其放入应用程序的循环缓冲区中。
最后,我们还需要担心中断被禁用、中断时序和运行速率、优先级以及是否有可能错过中断等问题。虽然其中一些项目看起来额外的复杂性可能不值得付出努力,但执行时间的改进可能是巨大的。例如,电池供电的设备可能会进入深度睡眠模式,只有在将字符存储在缓冲区中时才醒来,然后再重新进入睡眠状态。这样做可以节省大量能源。
在某些情况下,在驱动程序中使用中断确实是处理外围事件的最佳方式。例如,您可以编写一个轮询 I2C 驱动程序,但编写一个中断 ack、nack 等传输序列中发生的不同事件的驱动程序会产生更清洁、更小和更高效的驱动程序。
我们将在下一篇文章中查看中断驱动驱动程序的代码。现在,让我们看看我们可以用来编写驱动程序的第三种技术,它是利用直接内存访问 (DMA) 控制器。
技术#3 – DMA 驱动的驱动程序
有一些驱动程序会通过系统移动大量数据,例如 I2S 和 SDIO。管理这些类型接口上的缓冲区可能需要 CPU 不断采取行动。如果 CPU 落后或必须处理另一个系统事件,则数据可能会丢失或延迟,这可能会给用户带来明显的问题,例如音频跳过。关注吞吐量的开发人员可以改为使用 DMA 控制器在微控制器周围为 CPU 移动数据。
这些驱动程序背后的想法是 DMA 控制器可以通过以下方式在微控制器周围移动数据:
- 内存外围
- 记忆到记忆
- 内存到外设
使用 DMA 的优点是 CPU 可以在 DMA 通道为驱动程序移动数据时停止做其他事情,基本上可以同时完成两件事。
虽然非常希望在驱动程序中使用 DMA 控制器来减少 CPU 执行的需要,但大多数微控制器的可用 DMA 通道数量有限。因此,不能编写每个驱动程序来使用 DMA。相反,开发人员需要选择将受到带宽限制且将从 DMA 中受益匪浅的外设,例如用于外部存储器、ADC 和通信通道的接口。
在没有 I2S 或 SDIO 的应用程序中,开发人员可以使用 DMA 将传入的 UART 字符移动到一个循环缓冲区中,一旦设置了某个限制,该缓冲区就会被处理。可以通过轮询应用程序结构或通过 DMA 控制器设置中断来监控此限制。可以想象,DMA 驱动程序是驱动程序最有效的实现,但根据开发人员的技能水平以及他们以前是否使用过 DMA,它们的实现也可能很复杂。这不应该阻止开发人员尝试在其驱动程序中使用 DMA。
结论
在这篇文章中,我们研究了嵌入式开发人员可以用来为其微控制器外设和外部设备编写驱动程序的三种主要技术。为了比较总结这些技术,下面的表 1 显示了我们讨论的每种技术,以及实现的相对复杂性和由此产生的执行效率。
表 1:驱动程序设计技术的相对复杂性和
效率。
技术 | 复杂 | 效率 |
轮询 | 低的 | 低的 |
打断 | 中等的 | 中等的 |
DMA | 中等的 | 高的 |
一般而言,开发人员应该默认使用中断驱动程序实现而不是轮询实现,除非正在使用的外设速度很快,即几个 Mbps。DMA 可用于任何驱动程序,但我通常为需要高吞吐量的接口(例如外部存储器或通信接口)保留 DMA 通道。您选择的选项将高度依赖于最终应用程序。
在下一篇文章中,我们将通过研究如何为模数转换器开发一个简单的驱动程序来探索如何更深入地研究这些概念。
评论
查看更多