第 5 章 - 适用于 Azure RTOS ThreadX 的设备驱动程序

本章介绍适用于 Azure RTOS ThreadX 的设备驱动程序。 本章介绍的信息旨在帮助开发人员编写特定于应用程序的驱动程序。

设备驱动程序简介

与外部环境的通信是大多数嵌入式应用程序的重要组成部分。 此通信通过嵌入式应用程序软件可访问的硬件设备实现。 负责管理此类设备的软件组件通常称为设备驱动程序。

嵌入式实时系统中的设备驱动程序本质上依赖于应用程序。 这是因为以下两个主要原因:目标硬件非常多样化,对各个实时应用程序施加的性能要求同样巨大。 因此,实际上不可能提供一组可满足每个应用程序的要求的通用驱动程序。 由于这些原因,本章中的信息旨在帮助用户自定义现成的 ThreadX 设备驱动程序以及编写自己的特定驱动程序。

驱动程序函数

ThreadX 设备驱动程序由八个基本函数区域组成,如下所示。

  • 驱动程序初始化
  • 驱动程序控制
  • 驱动程序访问
  • 驱动程序输入
  • 驱动程序输出
  • 驱动程序中断
  • 驱动程序状态
  • 驱动程序终止

每个驱动程序函数区域都为可选,但初始化除外。 此外,每个区域中的确切处理特定于设备驱动程序。

驱动程序初始化

此函数区域负责初始化实际的硬件设备和驱动程序的内部数据结构。 在初始化完成之前,不允许调用其他驱动程序服务。

注意

通常可通过 tx_application_define 函数或初始化线程调用驱动程序的初始化函数组件。

驱动程序控制

驱动程序初始化并准备好运行后,此函数区域负责运行时控制。 通常,运行时控制包括对基础硬件设备进行更改。 例如,更改串行设备的波特率或查找磁盘上的新扇区。

驱动程序访问

某些设备驱动程序只能通过单个应用程序线程调用。 在这种情况下,不需要此函数区域。 但是,在多个线程需要同时访问驱动程序的应用程序中,必须通过在设备驱动程序中添加分配/释放功能来控制这些线程的交互。 或者,应用程序也可以使用信号灯来控制驱动程序访问,并避免驱动程序内部出现额外的开销和复杂情况。

驱动程序输入

此函数区域负责所有设备输入。 与驱动程序输入相关的主要问题通常涉及如何缓冲输入以及线程如何等待此类输入。

驱动程序输出

此函数区域负责所有设备输出。 与驱动程序输出相关的主要问题通常涉及如何缓冲输出以及线程如何等待执行输出。

驱动程序中断

大多数实时系统都依赖硬件中断向驱动程序通知设备输入、输出、控制和错误事件。 中断可为此类外部事件提供有保证的响应时间。 驱动程序软件可能会定期检查外部硬件以查找此类事件,而不依赖中断。 这种技术称为轮询。 它的实时性低于中断,但轮询对于一些实时性较低的应用程序可能有意义。

驱动程序状态

此函数区域负责提供与驱动程序操作关联的运行时状态和统计信息。 此函数区域管理的信息通常包括以下内容。

  • 当前设备状态
  • 输入字节数
  • 输出字节数
  • 设备错误计数

驱动程序终止

此函数区域为可选。 仅当驱动程序和/或物理硬件设备需要关闭时,才需要此函数区域。 终止后,在重新初始化之前不能再次调用驱动程序。

简单驱动程序示例

举例是介绍设备驱动程序的最佳方式。 在此示例中,驱动程序假定简单的串行硬件设备具有配置寄存器、输入寄存器和输出寄存器。 此简单驱动程序示例阐释了初始化、输入、输出和中断函数区域。

简单驱动程序初始化

简单驱动程序的 tx_sdriver_initialize 函数可创建两个计数信号灯,用于管理驱动程序的输入和输出操作。 当串行硬件设备收到字符时,输入信号灯由输入 ISR 设置。 因此,所创建的输入信号灯的初始计数为零。

而输出信号灯用于指示串行硬件传输寄存器的可用性。 创建输出信号灯时,将其值设为 1,表示传输寄存器在初始时可用。

初始化函数还负责为输入和输出通知安装低级别中断向量处理程序。 与其他 ThreadX 中断服务例程一样,低级别处理程序在调用简单驱动程序 ISR 之前必须调用 _tx_thread_context_save。 驱动程序 ISR 返回后,低级别处理程序必须调用 _tx_thread_context_restore

重要

*请务必在调用任何其他驱动程序函数之前调用初始化。 通常可通过 tx_application_define 调用驱动程序初始化。

VOID tx_sdriver_initialize(VOID)
{
    /* Initialize the two counting semaphores used to control
    the simple driver I/O. */
    tx_semaphore_create(&tx_sdriver_input_semaphore,
                        "simple driver input semaphore", 0);
    tx_semaphore_create(&tx_sdriver_output_semaphore,
                        "simple driver output semaphore", 1);

    /* Setup interrupt vectors for input and output ISRs.
    The initial vector handling should call the ISRs
    defined in this file. */

    /* Configure serial device hardware for RX/TX interrupt
    generation, baud rate, stop bits, etc. */
}

图 9. 简单驱动程序初始化

简单驱动程序输入

简单驱动程序的输入以输入信号灯为中心。 系统收到串行设备输入中断时,会设置输入信号灯。 如果一个或多个线程正在等待从驱动程序接收字符,则会恢复等待时间最长的线程。 如果没有任何线程正在等待,则信号灯会保持不变,直到线程调用驱动程序输入函数为止。

系统在处理简单驱动程序输入时有几个限制。 其中最重要的一点是,系统可能会删除输入字符。 之所以有这种可能性,是因为系统在处理前一个字符之前无法缓冲到达的输入字符。 可以通过添加输入字符缓冲区来轻松解决此限制。

注意

仅允许线程调用tx_sdriver_input函数。

图 10 显示了与简单驱动程序输入关联的源代码。

UCHAR tx_sdriver_input(VOID)
{
  /* Determine if there is a character waiting. If not,
  suspend. */
  tx_semaphore_get(&tx_sdriver_input_semaphore,
  TX_WAIT_FOREVER;

  /* Return character from serial RX hardware register. */
  return(*serial_hardware_input_ptr);
}
  VOID tx_sdriver_input_ISR(VOID)
{
  /* See if an input character notification is pending. */
  if (!tx_sdriver_input_semaphore.tx_semaphore_count)
  {
    /* If not, notify thread of an input character. */
    tx_semaphore_put(&tx_sdriver_input_semaphore);
  }
}

图 10. 简单驱动程序输入

简单驱动程序输出

当串行设备的传输寄存器空闲时,输出处理将使用输出信号灯发出信号。 在将输出字符实际写入到设备之前,系统会获取输出信号灯的相关信息。 如果该信号灯不可用,则表示之前的传输尚未完成。

输出 ISR 负责处理传输完成中断。 处理输出 ISR 等同于设置输出信号灯,从而允许输出另一个字符。

注意

仅允许线程调用tx_sdriver_output函数。

图 11 显示了与简单驱动程序输出关联的源代码。

VOID tx_sdriver_output(UCHAR alpha)
{
  /* Determine if the hardware is ready to transmit a
  character. If not, suspend until the previous output
  completes. */
  tx_semaphore_get(&tx_sdriver_output_semaphore,
                                          TX_WAIT_FOREVER);

  /* Send the character through the hardware. */
  *serial_hardware_output_ptr = alpha;
}
  
VOID tx_sdriver_output_ISR(VOID)
{
  /* Notify thread last character transmit is
  complete. */
  tx_semaphore_put(&tx_sdriver_output_semaphore);
}

图 11. 简单驱动程序输出

简单驱动程序的缺点

此简单设备驱动程序示例阐释了 ThreadX 设备驱动程序的基本概念。 但是,由于简单设备驱动程序无法解决数据缓冲问题或任何开销问题,因此它不能完全代表实际 ThreadX 驱动程序。 下一节介绍了与设备驱动程序相关的一些更高级的问题。

驱动程序高级问题

如前所述,设备驱动程序具有与应用程序不同的要求。 某些应用程序可能需要缓冲大量数据,而其他应用程序可能由于设备中断频率较高而需要优化的驱动程序 ISR。

I/O 缓冲

实时嵌入式应用程序中的数据缓冲需要进行大量规划。 有些设计由基础硬件设备决定。 如果设备提供基本的字节 I/O,则简单的循环缓冲区可能符合要求。 但是,如果设备提供块 I/O、DMA I/O 或数据包 I/O,则可能需要缓冲区管理方案。

循环字节缓冲区

循环字节缓冲区通常在管理简单串行硬件设备(例如 UART)的驱动程序中使用。 这种情况下最常使用两个循环缓冲区:一个用于输入,另一个用于输出。

每个循环字节缓冲区都包含一个字节内存区域(通常是一个 UCHAR 数组)、一个读指针和一个写指针。 如果读指针和写指针引用缓冲区中的同一个内存位置,则将缓冲区视为空的。 驱动程序初始化会将缓冲区读指针和缓冲区写指针设置为缓冲区的起始地址。

循环缓冲区输入

输入缓冲区用于在应用程序准备就绪之前保存到达的字符。 收到输入字符(通常是在中断服务例程中)时,将从硬件设备检索新字符,并将其放入输入缓冲区中写指针所指向的位置。 然后,写指针将前进到缓冲区中的下一个位置。 如果下一个位置超出了缓冲区的末尾,则写指针将设置为缓冲区的开头。 如果新的写指针与读指针相同,则通过取消写指针前进来解决队列已满状况。

向驱动程序发送的应用程序输入字节请求首先检查输入缓冲区的读指针和写指针。 如果读指针与写指针相同,则缓冲区为空。 否则,如果读指针不相同,则会从输入缓冲区中复制读指针所指向的字节,并且读指针会前进到下一个缓冲区位置。 如果新的读指针超出了缓冲区的末尾,则将其重置为开头。 图 12 显示了循环输入缓冲区的逻辑。

UCHAR   tx_input_buffer[MAX_SIZE];
UCHAR   tx_input_write_ptr;
UCHAR   tx_input_read_ptr;

/* Initialization. */
tx_input_write_ptr =    &tx_input_buffer[0];
tx_input_read_ptr =     &tx_input_buffer[0];

/* Input byte ISR... UCHAR alpha has character from device. */
save_ptr = tx_input_write_ptr;
*tx_input_write_ptr++ = alpha;
if (tx_input_write_ptr > &tx_input_buffer[MAX_SIZE-1])
    tx_input_write_ptr = &tx_input_buffer[0]; /* Wrap */
if (tx_input_write_ptr == tx_input_read_ptr)
    tx_input_write_ptr = save_ptr; /* Buffer full */

/* Retrieve input byte from buffer... */
if (tx_input_read_ptr != tx_input_write_ptr)
{
  alpha = *tx_input_read_ptr++;
  if (tx_input_read_ptr > &tx_input_buffer[MAX_SIZE-1])
      tx_input_read_ptr = &tx_input_buffer[0];
}

图 12. 循环输入缓冲区的逻辑

注意

*为了确保操作的可靠性,在操作循环输入缓冲区以及循环输出缓冲区的读指针和写指针时可能需要锁定中断。 *

循环输出缓冲区

输出缓冲区用于在硬件设备完成发送前一个字节之前保存到达用于输出的字符。 输出缓冲区处理类似于输入缓冲区处理,不同之处在于传输完成中断处理操作输出读指针,而应用程序输出请求使用输出写指针。 在其他方面,输出缓冲区处理与输入缓冲区处理相同。 图 13 显示了循环输出缓冲区的逻辑。

UCHAR   tx_output_buffer[MAX_SIZE];
UCHAR   tx_output_write_ptr;
UCHAR   tx_output_read_ptr;

/* Initialization. */
tx_output_write_ptr = &tx_output_buffer[0];
tx_output_read_ptr = &tx_output_buffer[0];

/* Transmit complete ISR... Device ready to send. */
if (tx_output_read_ptr != tx_output_write_ptr)
{
  *device_reg = *tx_output_read_ptr++;
  if (tx_output_read_reg > &tx_output_buffer[MAX_SIZE-1])
      tx_output_read_ptr = &tx_output_buffer[0];
}

/* Output byte driver service. If device busy, buffer! */
save_ptr = tx_output_write_ptr;
*tx_output_write_ptr++ = alpha;
if (tx_output_write_ptr > &tx_output_buffer[MAX_SIZE-1])
    tx_output_write_ptr = &tx_output_buffer[0]; /* Wrap */
if (tx_output_write_ptr == tx_output_read_ptr)
    tx_output_write_ptr = save_ptr; /* Buffer full! */

图 13. 循环输出缓冲区的逻辑

缓冲区 I/O 管理

为了提高嵌入式微处理器的性能,许多外围设备使用软件提供的缓冲区来传输和接收数据。 在某些实现中,可能会使用多个缓冲区来传输或接收单个数据包。

I/O 缓冲区的大小和位置由应用程序和/或驱动程序软件确定。 通常,缓冲区的大小是固定的,并在 ThreadX 块内存池中进行管理。 图 14 介绍了典型 I/O 缓冲区以及管理缓冲区分配的 ThreadX 块内存池。

typedef struct TX_IO_BUFFER_STRUCT
{
      struct TX_IO_BUFFER_STRUCT *tx_next_packet;
      struct TX_IO_BUFFER_STRUCT *tx_next_buffer;
      UCHAR tx_buffer_area[TX_MAX_BUFFER_SIZE];
} TX_IO_BUFFER;

TX_BLOCK_POOL tx_io_block_pool;

/* Create a pool of I/O buffers. Assume that the pointer
"free_memory_ptr"points to an available memory area that
is 64 KBytes in size. */
tx_block_pool_create(&tx_io_block_pool,
                  "Sample IO Driver Buffer Pool",
                  free_memory_ptr, 0x10000,
                  sizeof(TX_IO_BUFFER));

图 14. I/O 缓冲区

TX_IO_BUFFER

typedef TX_IO_BUFFER 包含两个指针。 tx_next_packet 指针用于在输入列表或输出列表中链接多个数据包。 tx_next_buffer 指针用于将构成来自设备的单个数据包的缓冲区链接到一起。 从池中分配缓冲区时,这两个指针都设置为 NULL。 此外,某些设备可能需要另一个字段来指示实际包含数据的缓冲区大小。

缓冲 I/O 的优点

缓冲区 I/O 方案的优点是什么? 最大的优点是不会在设备寄存器和应用程序的内存之间复制数据。 相反,驱动程序会为设备提供一系列缓冲区指针。 物理设备 I/O 直接利用提供的缓冲区内存。

使用处理器复制信息的输入数据包或输出数据包会产生非常高的开销,在任何高吞吐量 I/O 情况下应避免使用这种方法。

缓冲 I/O 方法的另一个优点是输入列表和输出列表不会出现已满状况。 所有可用的缓冲区在任何时候都可以位于任一列表中。 这与本章前面介绍的简单字节循环缓冲区相反。 每个缓冲区都具有固定大小,该大小在编译时确定。

缓冲驱动程序的职责

缓冲设备驱动程序只负责管理 I/O 缓冲区的链接列表。 对于在应用程序软件准备就绪之前接收的数据包,会维护一个输入缓冲区列表。 相反,对于发送速率比硬件设备的处理速度更快的数据包,会维护一个输出缓冲区列表。 图 15 显示了包含数据包以及构成每个数据包的缓冲区的输入列表和输出列表的简单链接。

输入列表

Input List

输出列表

Output List

图 15. 输入列表-输出列表

应用程序使用相同的 I/O 缓冲区与缓冲驱动程序交互。 传输时,应用程序软件会为驱动程序提供一个或多个缓冲区用于传输数据。 当应用程序软件请求输入时,驱动程序将在 I/O 缓冲区中返回输入数据。

注意

在某些应用程序中,构建要求应用程序为驱动程序的输入缓冲区交换可用缓冲区的驱动程序输入接口可能会很有用。 这可以减少驱动程序内部的一些缓冲区分配处理。

中断管理

在某些应用程序中,设备中断频率太高可能会禁止以 C 源代码格式编写 ISR,或者在每次中断时禁止与 ThreadX 交互。 例如,如果需要 25us 来保存并还原中断的上下文,则当中断频率为 50us 时,建议不要执行完整上下文保存。 在这种情况下,可以使用小型汇编语言 ISR 来处理大多数设备中断。 此低开销 ISR 仅在必要时才与 ThreadX 交互。

在第 3 章末尾的中断管理讨论中可以找到类似的讨论。

线程暂停

在本章前面介绍的简单驱动程序示例中,输入服务的调用方在某个字符不可用时会暂停。 在某些应用程序中,这可能是无法接受的。

例如,如果负责处理驱动程序输入的线程还承担其他职责,则只在驱动程序输入时暂停可能无法起作用。 相反,需要对驱动程序进行自定义,使其以向线程发送其他处理请求的相似方式请求处理。

在大多数情况下,输入缓冲区放置在链接列表中,而输入事件消息会发送到线程的输入队列。