外设中的中断模式与轮询模式

当编写一个外设驱动时,其编程模式到底采用中断模式触发还是轮询模式触发往往是驱动开发人员首先要考虑的问题,并且这个问题在实时操作系统与分时操作系统中差异还非常大。因为轮询模式本身采用顺序执行的方式:查询到相应的事件然后进行对应的处理。所以轮询模式从实现上来说,相对简单清晰。例如往串口中写入数据,仅当串口控制器写完一个数据时,程序代码才写入下一个数据(否则这个数据丢弃掉)。相应的代码可以是这样的:

  1. /* 轮询模式向串口写入数据 */
  2. while (size)
  3. {
  4. /* 判断UART外设中数据是否发送完毕 */
  5. while (!(uart->uart_device->SR & USART_FLAG_TXE));
  6. /* 当所有数据发送完毕后,才发送下一个数据 */
  7. uart->uart_device->DR = (*ptr & 0x1FF);
  8.  
  9. ++ptr; --size;
  10. }

但是在实时系统中轮询模式可能会出现非常大问题,因为在实时操作系统中,当一个程序持续地执行时(轮询时),它所在的线程会一直运行,比它优先级低的线程都不会得到运行。而分时系统中,这点恰恰相反,几乎没有优先级之分,可以在一个时间片运行这个程序,然后在另外一段时间片上运行另外一段程序。

所以通常情况下,实时系统中更多采用的是中断模式来驱动外设。当数据达到时,由中断唤醒相关的处理线程,再继续进行后续的动作。例如一些携带FIFO(包含一定数据量的先进先出队列)的串口外设,其写入过程可以是这样的,如下图所示:

线程先向串口的FIFO中写入数据,当FIFO满时,线程主动挂起。串口控制器持续地从FIFO中取出数据并以配置的波特率(例如115200bps)发送出去。当FIFO中所有数据都发送完成时,将向处理器触发一个中断;当中断服务例程得到执行时,可以唤醒这个线程。这里举例的是FIFO类型的设备,在现实中也有DMA类型的设备,原理类似。

对于低速设备这种模式非常好,因为在串口外设把FIFO中的数据发送出去前,处理器可以运行其他的线程,这样就提高了系统的整体运行效率(甚至对于分时系统来说,这样的设计也是非常必要)。但是对于一些高速设备,例如传输速度达到10Mbps的时候,假设一次发送的数据量是32字节,我们可以计算出发送这样一段数据量需要的时间是:

  1. (32*8) * 1/10Mbps = 25us

当数据需要持续传输时,系统将在25us后触发一个中断以唤醒上层线程继续下次传递。假设系统的任务切换时间是8us(通常实时操作系统的任务上下文切换时间只有几个us),那么当整个系统运行时,对于数据带宽利用率将只有25/(25 + 8) = 75.8%。但是采用轮询模式,数据带宽的利用率则可能达到100%。这个也是大家普遍认为实时系统中数据吞吐量不足的缘故,系统开销消耗在了任务切换上(有些实时系统甚至会如本章前面说的,采用底半处理,分级的中断处理方式,相当于再行拉长中断到发送线程的时间开销,效率会更进一步下降)。

通过上述的计算过程,我们可以看出其中的一些关键因素:发送数据量越小,发送速度越快,对于数据吞吐量的影响也将越大。归根结底,系统中产生中断的频度如何。当一个实时系统想要提升数据吞吐量时,可以考虑的几种方式:

  • 增加每次数据量发送的长度,每次尽量让外设尽量多地发送数据;
  • 必要情况下更改中断模式为轮询模式。同时为了解决轮询方式一直抢占处理机,其他低优先级线程得不到运行的情况,可以把轮询线程的优先级适当降低。