嵌入式 - UART使用进阶

UART – Advanced Features

概要 / Overview

最简单直接的使用UART接口的方式,是在轮循操作中来设置和处理UART接口。 轮询式UART的问题是轮询方式本身就是低效率的。 如果我们的UART被配置为115200的波特率和8N1,那么传输一个字符需要多长时间?  每个字节的数据需要10个符号来发送。 这意味着我们每秒钟可以传输11520个字符。 这意味着1个字符大约需要86微秒的时间来发送。

86微秒可能看起来并不长,但如果我们的微处理器以50Mhz的速度运行,这相当于每个字符发送需要4341个时钟周期。 这是一个相当多的时钟周期,为了发送一串字符中的下一个字符而要一直等待。

我们将研究一些硬件和软件技术,以减少我们等待数据传输或接收的时间。

中断 / Interrupts

减少等待UART的时钟周期数的最明显方法之一是让UART产生中断。 UART中断允许主应用程序在UART不活动时执行其他任务。 UART通常产生接收和发送中断。 当有新数据到达时,UART会产生一个接收中断。 对于接收中断,应用程序和ISR之间的交互是非常直接的。  中断服务例程将新数据放入SRAM,提醒主应用程序,并清除中断。 由于中断只在数据被接收时发生,处理器可以自由地进行其他操作。

发送中断有一点不同。  一些微控制器的UART在发送缓冲区为空时产生一个中断。 这个中断是为了指示应用程序可以通过UART发送下一个字节。  我们想要避免的是这样的情况,即应用程序没有传输数据,结果UART不断产生发送数据为空的中断。这种情况会使应用程序处于空转状态,因为UART处理程序会被不断执行。 如果你使用的微控制器在Tx硬件FIFO中没有数据时不断产生Tx空中断,你可以使用以下过程来消除主应用程序的空转:

1,当UART的循环发送缓冲区为空时,UART处理程序会禁用它自己的发送空中断。 这可以防止中断的连续产生。

2,当应用程序希望发送数据时,它将数据添加到UART FIFO中,然后重新启用发送数据为空的中断。

 管道 FIFOs

在某些情况下,仅靠中断是无法减少浪费的时钟周期的。 一个例子是打印出一长串的字符。 我们可以立即发送字符串中的第一个字符到UART暂存寄存器,但我们仍然要依次发送字符串中的一个个字符,发完一个等待发送下一个。 中断可以帮助我们确定何时可以发送下一个字符,但它们并不能消除这样一个事实,即在主程序传送完所有字符之前,应用程序不能继续往前跑。

一个可以减少等待发送(或接收)字符的时间的机制是UART FIFO。 具有硬件FIFO的UART允许应用程序向UART写入一个以上的字符。 UART将写入的字符放入队列,然后一个个发送,直到FIFO为空。 我们不再需要等待单个字符的传输。 我们可以向UART发送一个字符块并立即返回。

FIFO的结果是减少中断。 只有在传输/接收了一个数据块后才会出现传输和接收中断。 每个中断都会导致上下文切换和ISR的执行。 减少中断的数量可以减少对UART中断做出反应所占用的时钟周期。

在使用FIFO时,我们需要考虑的一个特殊情况是当UART产生一个接收中断时。 带有FIFO的UART只有在接收FIFO满(或接近满)时才可能产生中断。 那么,当收到一个单字节的数据,而FIFO被配置为只在FIFO满时产生中断时,会发生什么? 我们不希望发生的是数据被卡在接收FIFO中,因为FIFO没有满。 为了避免这种情况,通常有一个接收超时中断。 当数据在接收FIFO中,但没有足够的数据来触发FIFO满的中断时,这个中断会被定期触发。

Circular Buffers( Ring buffer) 环形缓冲区

硬件FIFO也不能解决我们所有的等待问题。 硬件FIFO的大小是固定的。 如果打印的字符串超过了硬件FIFO的容量,应用程序仍将忙于等待。 由于我们不能动态地增加硬件FIFO的大小,我们将在内存中创建一个软件的数据结构来缓冲运往UART的字符。 最常用的软件结构是一个循环缓冲器。

那么,为什么我们要使用循环缓冲器而不是链表呢? 链表让我们可以自由地缓冲字符,直到SRAM用完。  我们不使用链表的原因是内存利用率。 对于存储在链接列表中的每一个字节,我们还必须分配一个下一个指针,这需要消耗4个字节。  这大概是在一个链接列表中存储字符的80%的内存开销。 由于我们的微处理器(microprocessor)中只有几KB的SRAM,这不是一个可以接受的解决方案。

另一方面,循环缓冲器的开销很小。 循环缓冲区必须存储一些关于在哪里插入下一个字符的信息,但总的来说,循环缓冲区只是一个数组。 循环缓冲区的缺点是,我们不能轻易地动态增加缓冲区的大小。 如果向循环缓冲区添加数据的速度大于从循环缓冲区删除数据的速度,数据就会被覆盖并丢失。

Producer Consumer Model 生产消费者模型

我们的UART实现将包括硬件FIFO、软件循环缓冲器、中断处理程序和主应用程序。 我们将使用生产者-消费者模型来确定何时以及如何在应用程序和中断服务程序之间进行数据通信。 发送和接收操作都需要各自的循环缓冲器,并独立检查。

  • Receive 接收

当接收数据时,UART中断服务例程将作为生产者,将字符插入到接收循环缓冲器中。 当接收或接收超时中断被激活时,ISR将从硬件FIFO中移除数据,直到硬件FIFO为空。 每个数据条目都被放入接收循环缓冲区。 当硬件FIFO为空时,ISR将清除接收中断并返回。

主应用程序充当接收FIFO的消费者。 该应用程序不直接从UART抓取数据。循环缓冲器不为空时,应用程序处理接收循环缓冲器的数据,然后删除里面的数据条目。 如果循环缓冲器中没有数据,应用程序可以选择等待(阻塞)直到数据到达,或继续进行其他操作。

我们必须考虑的一个设计问题是,当接收循环缓冲区满了,发生接收中断的情况。 当接收数据的速度超过主程序的消耗速度时,就会出现这种情况。 这种情况总是会导致数据丢失。 问题是,哪些数据会被丢弃? 最老的数据还是最新的数据? 根据我的经验,最古老的数据会被丢弃,但这个问题没有正确的答案。 在这种情况下,我们唯一真正的解决方案是实施一种流量控制(flow control)。

Application Receive Routine Flow Diagram 应用程序接收数据的流程图

ISR Receive Flow Diagram 接收中断流程图

  • Transmit 发送

当发送数据时,应用程序通过将字符插入发送循环缓冲器来提供数据。 当一个发送数据为空的中断发生时,UART ISR作为消费者,从发送循环缓冲区中取得然后删除数据。

当使用循环缓冲器发送数据时,当循环缓冲器满了会发生什么? 我们可以覆盖最旧的数据,但在大多数情况下,你应该持续等待,直到循环缓冲区不再满。 持续等待并不是最佳选择,但总比丢失数据要好。

我们要解决的另一个情况是,如果发送数据为空的中断被禁用,数据如何被发送。 如上所述,在应用程序没有数据要发送时,发送数据为空的中断被禁用。 这避免了UART ISR的不断调用。 如果我们的应用程序将数据添加到发送循环缓冲区,并且发送数据为空的中断被禁用,那么发送数据为空的中断就不会被触发,循环缓冲区中的数据也不会被UART消耗。

当我们发送数据时,应用程序将检查循环缓冲区。 如果循环缓冲区是空的,而且硬件FIFO没有满,我们将把数据直接放入硬件FIFO,并重新启用发送空中断。 其结果是UART传输硬件FIFO中的字符,当硬件FIFO为空时将产生一个新的传输中断。 然后UART ISR将检查发送循环缓冲器。 如果循环缓冲区内有数据,ISR会消耗字符,直到硬件FIFO满或循环缓冲区空。 UART将继续发送字符并产生中断,直到UART ISR检测到发送循环缓冲区为空,此时UART ISR将禁用发送空中断。

Application Transmit Routine Flow Diagram 应用程序发送数据的流程图

ISR Transmit Routine Flow Diagram 发送数据中断处理程序流程图

Race condition 竞争条件

当应用程序从循环缓冲区移除(或添加)数据时,我们必须确保修改循环缓冲区的操作是原子性的(atomic)。 原子操作的意思是,所有涉及到修改循环缓冲区的操作不能被打断。  如果我们不确保这些操作是原子的,那么就会出现一个race condition,当主程序试图修改循环缓冲区的内容时,发生UART中断,也去修改循环缓冲区内容。

由于一个指令序列只能被更高优先级的中断打断,我们不需要担心UART ISR会被应用程序打断。 另一方面,我们确实需要防止应用程序在向循环缓冲区添加或移除数据时被中断。 我们可以使用某种形式的信号semaphore,但最简单的方法是让主程序在修改循环缓冲区之前暂时禁用中断,然后在修改数据后重新启用中断。

参考:

1,UART使用进阶

UART – Advanced Features – ECE353: Introduction to Microprocessor Systems – UW–Madison

猜你喜欢

转载自blog.csdn.net/guoqx/article/details/131517322