mirrorok 发表于 2006-7-24 14:15:04

基于双缓冲队列的串口通信模块的研究与实现

基于双缓冲队列的串口通信模块的研究与实现

串口通信模块是单片机系统或者目前的嵌入式系统中常见的组成部分,被广泛的用于系统的调试和与外界的通信。一般的MCU都内置了串口的硬件控制模块,用户只需要编写比较简单的控制程序就可以使用串口通信。尽管如此,在实际使用中,考虑到效率和使用的方便性,有必要对串口通信软件进行仔细的设计,使得通过串口通信的其它软件模块能够比较简单和方便的使用,并且尽量不因为串口通信影响整个系统的性能。下面我们通过讨论常见的串口软件使用的方法,并提出基于一种称之为双缓冲队列的串口缓冲区管理策略的串口软件模块的实现方法和其中要注意的问题。



1常用的串口处理方法

      串口部分的底层软件可以认为是串口的驱动程序,对上层软件而言,它应该提供一种比较自然而简洁的使用方式。以串口的发送为例,使用者可以直接调用一个函数输出一个字符串或者就像在通用平台上使用标准C中的printf函数一样。

对上的接口已经确定了,下面我们讨论实现的问题。串口驱动的实现方法通常有两种:

1.基于查询的办法。发送过程中不断检测串口硬件的发送缓冲区是不是为空,如果是,发送一个字节。如果还有数据没有发完,继续上述过程。

下面以三星的S3C44B0X MCU为例,给出了基于查询方法的串口发送的示意代码。

void Uart_SendStr (char *pt)

{

char *p;

p = pt;

while(*p != '')

{

while (!(rUTRSTAT0 & 0x2)); //等待,直到串口缓冲区为空

WrUTXH0(*p++);//发送一个字节

}

}

2.基于中断的方法。在上面的基于查询的方法中,有一个很明显的弊病,那         就是在发送一个字符串的过程中,CPU不能去做其它的事情,必须等待全部字符发送完成后返回。以上述MCU为例,其最高主频为66MHz,由于采用的是ARMv4体系结构,可以达到0.9指令每周期,而其串口最高波特率为115200 bps,这样就有大量的指令周期被浪费,而且在发送较长的字符串时会严重影响系统的实时性。所以在实际的系统中一般更多的采用中断的方法,发送一个字节之后,转去做其它的处理,发送完后自动进入发送中断,再发送下一个字节。这种方式比查询法提高了CPU的利用率,在串口部分进行发送和移位等操作时,CPU不用干预,但是同时也使串口的软件部分变得比较复杂,需要增加相应的中断服务程序(ISR)以及相关的软件缓冲区的管理。由于中断是由硬件触发的,为了使中断进入后能找到要发送的数据,最直接的办法就是设置一个全局的数组和一个指向待发送数据的指针,这样每次中断进入后就发送指针指向的字节,直到发完。



2基于双缓冲队列的方法

      在采用上述的中断方式之后,进一步考虑整个的处理流程,以及中断服务程序和上层程序交互的方便性,对缓冲区进行了仔细设计。

由于串口发送和接收的数据是相对独立的,故将其分开,设置两个缓冲区,一个是发送缓冲区TxBuf,一个是接收缓冲区RxBuf,并为每个缓冲区分别设置两个指针,一个记录中断服务程序将处理的字节,另一个记录使用串口服务的上层程序将处理的字节。以串口发送为例,两个指针分别为inTxBuf, outTxBuf。outTxBuf指向发送中断将要发送的数据,inTxBuf指向上层程序将数据放入缓冲区的起始位置。这种方式我们称之为采用双缓冲队列的方法。

这种方法,保证了数据的顺序。在缓冲区够大的情况下,上层程序可以一次将要发送的数据全部放入发送缓冲区中,而不是一次发送一个字节,而且如果多个上层程序调用发送函数也不会造成混乱,因为每次调用时放入了要发送的全部数据。其原理类似于打印机对打印任务队列的管理,多个用户共享一个打印机,并发出各自的打印任务,但是不会出现不同任务的输出交叉的情况。串口的发送和接收这时成为公共的后台任务,只要发送缓冲区中有待发送的数据,就采用中断间歇性的进行发送,产生接收中断时也类似的进行接收,并通知上层程序。

下面开始分析具体的实现。以下是关于缓冲区和相关指针的声明:

#define TxBufLen1000

#define RxBufLen200

char TxBuf, RxBuf;

char *inTxBuf, *outTxBuf, *inRxBuf, *outRxBuf;

intUartTxCount, UartRxCount;

      在进行串口的硬件初始化之后,进行缓冲区的初始化,初始化后的缓冲区及其指针如图1所示。



         

图1初始化之后缓冲区和相应指针的示意图



      以下是串口发送的相关示例代码:

//发送中断的ISR采用汇编实现,进行现场保护之后调用UartTx进行后续的处理,

//Uart返回后再恢复现场。

void UartTx(void)

{

if (outTxBuf == inTxBuf)//TxBuf空

return;



WrUartBUF(*outTxBuf);//将待发送数据写入串口的寄存器

outTxBuf++;//指向下一个要发送的数据

if (outTxBuf == (TxBuf+TxBufLen))//缓冲区回绕

outTxBuf = TxBuf;



UartTxCount++;//发送计数

}



//上层程序调用串口发送数据的接口,一次发送一个’’结尾的字符串。

void Uart_PrintStr(char *pt)

{

char *t, *p;



t = inTxBuf;

p = pt;



while (*p != '')//逐个放入缓冲区

{

t++;

if (t == (TxBuf+TxBufLen))//回绕

t = TxBuf;

if (t == outTxBuf)//TxBuf满

return;

*inTxBuf = *p++;//放入一个数据

inTxBuf = t;//inTxBuf后移

}



Uart_TxStart();//启动发送,后面有讨论

}



      为便于对缓冲区管理的理解,下面分别给出了TxBuf有待发送数据以及满时的示意图。有阴影的格子表示含有有效数据。



   

图2   TxBuf有待发数据时的示意图





图3   TxBuf满时的示意图



3第一次进入串口发送中断的方法

      上面具体分析了基于双缓冲的方法,但是由于串口发送中断是在串口的硬件缓冲区变为空时触发的,那么自然就存在一个问题,如何产生第一次的发送中断呢?解决这个问题要结合具体的CPU的特点,大致有两种方法。

      对于可以用软件产生硬件中断的CPU,例如Intel 196系列,可以通过直接写INT PENDING寄存器的方法,在发送第一个字节之前手工触发一次中断,其产生的效果和真实的硬件中断一样。以196KC为例,具体方法是设置一个软件flag,初始值为1,表示需要手工触发。先将数据放入发送缓冲区,在发送第一个字节前,将flag置为0,表示不再需要手工触发,然后用下面的代码触发发送中断,

__INT_PEND1 |= 0x01;

      在中断发送的过程中,如果发现TxBuf为空(见上面的示意代码UartTx),在return之前,将flag重置为1,表示本次发送结束,下次发送时还需要手工触发。通过这种采用硬件特性和软件控制的方法,就可以很好的实现数据发送的流程。

      对于无法软件模拟硬件中断的CPU,如S3C44B0X等,可以通过先引导第一个字节发送的方法来实现,进而也可以使整个发送缓冲区发送完。对于先发送的一个字节,需要仔细的考虑,首先,发送的必须是一个有效的字节,否则会引起接收方的混乱。另外,如果直接写一个字节到串口的硬件缓冲区中,可能会引起发送的数据交叉,因为其它发送任务可能还在进行中。解决这种情况有两种办法:

      一是在Uart_PrintStr中检测发送缓冲区是否为空,如果不为空,表明发送已经启动,正在发送之中,不必手工触发中断,可以直接将全部待发送数据放入缓冲区,等待中断来发送即可。如果是,可以把第一个字节之外的数据顺序放入缓冲区,然后把第一个字节直接写入硬件的缓冲区,进而引发中断。

      二是将整个待发送的数据放入缓冲区,然后手工发送由outTxBuf指向的字节,发送中如果发现串口的缓冲不为空,表明正在有数据发送,这时放弃,返回即可。示例代码如下:

void Uart_TxStart(void)

{

if (outTxBuf == inTxBuf)//TxBuf空

return;



if (rUTRSTAT0 & 0x2)//如果硬件缓冲为空,发送一个字节

WrUTXH0(*outTxBuf);

else

retrun;



outTxBuf++;

if (outTxBuf == (TxBuf+TxBufLen))

outTxBuf = TxBuf;



UartTxCount++;

}

两种方法都是可行的,由于第二种方法不用去判断缓冲区是否为空,实现上比较简单,故在具体代码中采用了第二种方法。



4结语

      关于缓冲的大小,上面给出的是一个参考值,可以结合具体的硬件,例如RAM的大小和需要发送的数据的大小与频率来考虑。设置之前也可以进行简单的估算,以波特率为115200为例,每秒最多发送14400字节的数据。然后估计整个系统每秒钟可能产生的最大的输出数据量是多少,首先不能超出串口的能力,然后根据这个值可以进行缓冲区大小的设置。以上方法在笔者的一个基于嵌入式OS的系统中获得了很好的效果,在每个任务中进行串口输出,长期运行也没有出现数据错位和缓冲区溢出的情况,而且给其它模块的编程和调试带来了很大的方便。本文讨论的基于双缓冲队列的方法其实也适用于串口之外的其它外设,因为它关注的更多的是基于中断的缓冲区管理策略,而不是具体硬件的操作。另外附带了采用上述方法的基于Intel 80196KC和Samsung S3C44B0X的串口软件模块的代码。希望这里给出的方法和相关的代码能给其它开发人员提供一些参考或启示。

redriver 发表于 2006-7-26 20:54:32

串口收发数据我一般采用双串口方式,但是在ram较少的场合就不大适合了。比如51系列不扩外部ram情形下。

duqinglin 发表于 2008-7-18 09:11:27

楼主 你的那些图呢

duqinglin 发表于 2008-7-18 09:12:40

帖子当中的图一个都看不到啊 !!!

tclandmei 发表于 2008-7-18 10:16:26

2005年的《单片机与嵌入式系统应用》杂志上看过这篇文章!

tclandmei 发表于 2008-7-18 10:18:37

好像是这篇,我回去再看看确认一下!学习一下!谢谢!

zc3909 发表于 2008-7-18 10:58:08

串口接收为了不丢数据最多做过3个缓冲区

flyingcys 发表于 2010-6-5 15:24:02

本人刚好在学习这个,帖里面有人说没图,把文件直接发上来了
基于双缓冲队列的串口通信模块的研究与实现ourdev_559735.pdf(文件大小:40K) (原文件名:基于双缓冲队列的串口通信模块的研究与实现.pdf)

heyuncun 发表于 2010-6-5 15:54:25

mark

sunmy 发表于 2010-7-10 11:11:35

如何实现 Mega8 CTC模式下输出10Hz- 20K Hz的脉冲时不会受到 串口通信 影响?或者不影响通信?

wxx116zh 发表于 2010-7-10 11:19:37

mark

8s209 发表于 2010-7-25 20:08:19

mark

fanwt 发表于 2010-7-26 00:05:50

mark~

twd3621576 发表于 2010-8-15 13:40:21

mark~

joni 发表于 2010-8-15 17:58:33

mark

yanbin211 发表于 2013-3-29 17:31:57

redriver 发表于 2006-7-26 20:54 static/image/common/back.gif
串口收发数据我一般采用双串口方式,但是在ram较少的场合就不大适合了。比如51系列不扩外部ram情形下。 ...

我用51就做过 没事的 缓冲期别开的太大
页: [1]
查看完整版本: 基于双缓冲队列的串口通信模块的研究与实现