了解了上述内容之后,对串行接口进行编程就非常简单了,大致可分为下列几个步骤。
在发送或接收数据前,必须对串口进行初始化,主要是初始化串口的发送/接收波特率、中断允许方式、奇偶校验、停止位数量、数据位长度等。
比如,下列代码把串口1的工作模式,设置为缺省的Windows超级终端工作模式(波特率为9600,无校验,停止位1,数据位8,无流控):
2.数据发送
对于个人计算机外部设备的数据发送和接收,一般情况下有两种方式:中断方式和轮询方式(对于一些专用的大型计算机,还可以通过IO通道方式进行数据传输)。
轮询方式是发送或接收程序不停地检查外设的状态,一旦发现外设状态可用,便启动数据发送或接收过程。这样即使在外设不可用的时间内,发送或接收程序也不停地运行,会导致无用的CPU占用,降低系统效率。但轮询方式编程非常简单。
中断方式则会大大提高系统整体效率。发送或接收程序启动一个发送或接收过程,然后进入睡眠状态。在设备完成发送或接收数据后,通过中断的方式通知CPU,然后对应的发送或接收程序会被唤醒,从而继续进行数据传输。这个过程对CPU的利用会大大降低。但中断方式编程相对复杂。
下面介绍这两种方式下的串口数据传输。
(1)基于轮询方式的数据发送
轮询方式的数据发送操作比较简单,主要思路是,在发送数据前,检查串口是否准备好,若准备好,则启动发送,否则继续检测。但需要考虑一点,就是避免陷入死循环。下面是一个简单的发送程序,主要完成一个字节字符的发送:
上面的代码比较简单。首先,定义了两个变量:nCounter1和nCounter2,用于控制循环。然后代码进入内层循环,首先检查发送保持寄存器是否为空(LSR寄存器的第5个bit是否为1)。若为空,则发送对应的字节,然后返回成功结果。否则一直循环。若内层循环超出了预定的循环次数,则进入外层循环。外层循环通过调用__MicroDelay函数,来延迟1ms,然后又可重新进入内层循环。
若外层循环结束(3次),则该函数将不再试图发送,而是以失败返回。
(2)基于中断方式的数据发送
基于中断方式的数据发送稍微有些复杂。主要实现思路是,首先判断发送保持寄存器是否为空。若是,则直接发送。否则,发送线程进入睡眠状态,等待发送寄存器恢复。在发送寄存器可用的时候,串口控制芯片会通过中断通知CPU,从而唤醒发送线程,进而完成数据的发送。这时候,需要考虑两个问题:
1)为了提高效率,不要一检测到发送保持寄存器不可用就睡眠,而是稍微等待一段时间,这样可大大提升整体效率。因为线程睡眠涉及线程上下文的切换,也是十分消耗系统资源的。
2)发送线程在睡眠的时候,防止进入永久睡眠,即假如串口发送寄存器始终不可用,发送线程应该能够在一段时间的睡眠之后被唤醒。
下面是一个实现示例:
g_hEvent是一个全局范围内的事件对象,应该在程序开始的时候,完成创建和初始化工作。上述发送过程十分简单,首先检查一下当前串口芯片的发送保持寄存器是否为空(只有为空的时候才能发送)。若为空,则直接通过写入端口,完成数据发送工作,然后返回。需要注意的是,为了提升效率,在检查串口发送保持寄存器状态的时候,采用的是连续检查的策略。即一旦检测到不可用,则会通过循环进行第二次检查,然后是第三次……完成十六次检查后,若仍然不可用,则读取线程进入睡眠状态。这样可避免一次检查失败,就导致线程进入睡眠。因为睡眠操作是很费时的,而串口保持寄存器的状态,变化十分迅速。若一次检查不成功,后续检查可能会成功。这样就可以大大提升系统效率。
需要注意的是,上述操作是一个关键区段操作,即不允许中断。因为若这个操作过程发生中断,可能会导致线程永久睡眠。设想在上述代码中,黑体部分代码执行前,发生一次发送保持寄存器空的中断。中断处理程序会重置事件对象,但此时线程还未进入睡眠状态。这样中断处理程序结束后,写入线程会继续执行,从而进入睡眠状态。这样就可能永远不会被唤醒了。因此,上述操作必须在一个关键区段内完成。
下面就是具体的中断处理程序:
首先判断发生的中断是否就是由COM接口控制芯片引发的中断(判断中断状态寄存器的第一个比特是否为1)。若是,则进一步判断是什么类型的中断。因为COM接口控制芯片可在多种情况下引发中断。然后根据中断类型,做进一步处理。在这里,需重点关注的中断类型是“发送保持寄存器为空”中断(ISR的第二个比特为1)。在COM接口的发送寄存器为空时,会引发中断。若是该类型的中断,则调用SetEvent函数,恢复g_hEvent的信号状态,这会唤醒所有阻塞在该信号上的核心线程。
从中断返回后,若有核心线程阻塞在g_hEvent时间对象上,则统统会被唤醒。在合适的调度时机,就会被重新调度执行。这样由于串口控制器的发送保持寄存器已经空了,所以就可顺利完成发送任务。
需要注意的是,中断处理函数对ISR的检查是循环的,因为有可能发生中断嵌套的情况,即第一个中断得到处理后,又一个后续的中断立即发生。这样连续的两个中断可在同一个中断处理程序中进行,大大提高系统效率。
3.数据接收
(1)基于轮询方式的数据接收
基于轮询方式的数据接收,与基于轮询方式的数据发送程序类似。下面是一个实现示例:
程序首先判断线路状态寄存器的第一个比特是否为1。若是1,则说明数据寄存器中已有接收到的数据,于是通过__inb函数,把数据读取出来,然后返回。需要注意的是,读取数据寄存器的数据,会导致线路状态寄存器的第一个比特清零。
若数据寄存器中没有数据(线路状态寄存器的第一个比特为0),则会进入首轮循环(1024次)。若首轮循环结束后,仍然没有数据到达,则进入外层循环。外层循环会调用__MicroDelay函数,延迟1ms,然后重新尝试。
如果在外轮循环结束后,仍然没有数据到达,则会返回FALSE。调用者应该根据该函数的返回结果,确定是否有正确的数据被取回。
(2)基于中断方式的数据接收
基于中断方式的数据接收程序,与基于中断方式的数据发送程序类似,也是分两步实现:
1)由应用程序主动调用的数据接收函数。该函数检查串口芯片的数据寄存器是否有数据可用,若有,则直接读取后返回。否则进入等待过程。为了提升效率,可在进入等待过程前,多做几次检查。
2)中断处理程序。在数据到达的时候,串口控制芯片会通过中断的方式通知CPU。这时CPU需要唤醒等待读取数据的核心线程。
下面是数据接收函数的实现示例:
实现的思路与基于中断方式的数据发送程序类似,在此不做赘述。
4.串口交互程序的实现
有了上述知识作为铺垫之后,下面正式介绍Hello China附带的串口通信程序的实现。对于这个串口通信程序,分别采用基于轮询方式的编程方式和基于中断方式的编程方式进行实现,因此存在两个版本。在Hello China启动完成进入字符界面后,输入hypertrm或hyptrm2命令,就可启动串口输入/输出程序。其中hypertrm对应轮询方式的实现,而hyptrm2则是中断方式的实现版本。这两个版本的功能是一致的,但基于中断方式的实现,可大大节约CPU资源,然而其复杂性也大大增加了。
本章对这两种实现进行详细描述。这两个程序的实现很有典型意义,从中不但可以看到Hello China设备管理机制的使用方法以及步骤,而且也可以深入体会中断方式和轮询方式的设备控制方式,对于编写任何操作系统的设备驱动程序,都是十分有参考价值的。
5.串口交互程序的使用
在介绍其实现之前,先简单介绍一下串口交互程序的使用,这样可加深读者印象。可通过两种方法验证串口交互程序:
(1)通过PC的串口,控制特定功能的设备。
比如,大多数的数据通信设备(路由器、以太网交换机等),都是通过串口来完成配置和管理的。这种通信模型,如图10-9所示。
图10-9 通过PC的串口控制网络设备
通过一条特殊的串口线(一头为RJ45接头,连接网络设备。另外一头为DB-9接头,连接计算机的COM接口),连接网络设备的控制端口(一般称为Console接口)和PC的COM接口。设置合适的通信参数(波特率、数据位数等),在PC上启用一个终端模拟软件(比如Windows操作系统自带的超级终端),就可对网络设备进行控制了。
在这种方式下,可采用Hello China启动个人计算机,连接好串口线,然后输入hypertrm或hyptrm2,就可启用串口交互程序。这时候,按下回车键或其他键,就可看到输出了。这种情况下,串口驱动程序实际上是替代了Windows的超级终端程序。
需要注意的是,为了实现上的简便,Hello China实现的串口交互程序,在初始化的时候就设定了串口的波特率和数据位数等参数(波特率9600、数据位8位、无奇偶校验、1位停止位)。幸运的是,大多数网络设备,都是在这种工作模式下工作的。若遇到特殊情况,可通过修改串口交互程序的初始化参数,重新编译来实现。
若要退出hypertrm或hyptrm2,只要输入字母z就可以了,该字符用于指示线程运行结束。这是不符合实际应用需求的,实际当中,用户可输入Ctrl+C组合键,退出一个命令行程序。但为了实现上的简便,暂且以这种方式结束程序的运行。读者可通过简单修改代码,使得这个超级终端模拟程序能够支持CTRL+C等组合键的退出。
(2)通过串口连接两台计算机,实现点对点通信。
在上述应用场景中,需要有一台被控制的设备。但很多情况下,可能没有这样的试验设备供使用。这时候要验证串口交互程序,就可采用两台计算机直连的方式。通过一条直连串口线,连接两台PC的COM1接口,然后在两台PC上分别启用hypertrm或hyptrm2程序,在PC1上输入的数据,就可显示在PC2上,反之亦然。这实际上是一个点对点的通信程序。这种情形如图10-10所示。
图10-10 通过串口实现两台计算机的通信
连接两台PC的串口线,可自行制作,也可到电子市场上购买。若自行制作,可到互联网上搜索串口线的制作指南,在此不作赘述。
6.轮询模式的串口交互程序实现
轮询模式的IO交互程序,由下列几个基本功能模块组成:
(1)初始化功能,完成串口的初始化工作。
(2)数据发送功能。该功能接收用户输入,并采用轮询方式发送到串口。
(3)数据接收功能。该功能模块采用轮询方式,读取到达串口的数据,并打印到屏幕上。
(4)轮询模式的程序入口,这是一个符合Hello China定义的核心线程入口函数,该函数调用串口初始化功能模块,并创建两个核心线程:接收线程和发送线程,然后准备就绪,进入阻塞状态,直到用户退出。
初始化功能模块由InitComPort函数组成,该函数代码如下:
该函数完成串口的初始化工作,其中函数的参数base,指定了要初始化的串口端口地址。该函数首先判断串口地址是不是COM1和COM2的端口地址(分别为0x3F8和0x2F8),若不是,则直接返回。因为通常情况下,PC提供两个串口,每个串口采用固定的端口地址。
完成端口地址的确认后,进入初始化过程。首先设置线路控制寄存器的DLAB比特为1,这样就可写入串口工作的波特率因子。在目前的实现中,波特率硬性设置为9600,这可适应大多数的应用场景。
完成波特率因子的设置后,恢复DLAB比特,并设置数据位为8位,1位停止位,不做任何校验。这都是通过写入线路控制寄存器完成的。完成上述设置后,InitComPort函数禁止了COM接口的所有中断,因为该实现是基于轮询方式的,没有安装对应的中断处理程序。若不禁止COM接口的中断,则可能会引发系统异常。
最后,通过读取数据寄存器,来对数据寄存器进行复位。
下面是数据发送模块的实现。数据发送模块由两个函数组成,一个函数完成实际的数据发送功能,另外一个函数是发送线程的入口函数。下面是数据发送函数ComSendByte,该函数采用轮询的方式,向串口发送一个字节。实现代码如下:
该函数的实现,与基于轮询方式的数据发送类似,采用两轮循环,判断数据发送保持寄存器是否为空(通过判断线路状态寄存器)。若为空,则发送数据并返回。若经过两轮循环的等待后,数据发送保持寄存器仍然不为空,则取消发送,返回FALSE。
组成发送模块的另外一个函数是PollSend,该函数是发送核心线程的入口函数,在该函数中,调用了ComSendByte函数。代码如下:
该函数进入一个无限循环,并调用GetMessage函数,从当前核心线程的消息队列中获取消息(消息由系统输入对象发送到该线程的消息队列)。获取消息后,对消息的类型进行判断,若是按键消息,则会根据用户按下的具体键,做不同的处理:
1)若用户按下的键是QUIT_CHARACTER(定义为'z'),则设置一个事件对象。这个事件对象是另外一个线程—接收线程等待的事件对象。若该事件对象被设置,则等待线程会退出。设置完事件对象后,则返回,意味着发送线程退出运行。
2)若用户按下的键是其他形式的键,则会获取按键的ASCII码,然后通过串口发送出去。在发送的时候,调用了SendComByte函数。在发送的时候,做了MAX_SEND_TIMES次发送尝试。若所有发送尝试都不成功,则打印出“Failed to send out,connection may break.”字符串,放弃此次发送。一般情况下,发送会立即完成,只有在连接中断或串口芯片出现故障的时候,才可能出现发送失败的情形。
__BASE_EVENT是临时定义的一个结构体,用于传递参数。因为按照Hello China目前的定义,一个核心线程入口函数,只能接受一个类型是VOID的指针作为参数,为了传递更多的参数,可通过定义结构体来实现。下面是__BASE_EVENT的定义:(www.xing528.com)
其中,wPortBase是串口的输入/输出端口号地址,lpEvent是一个事件对象指针,用于完成发送核心线程和接收核心线程之间的同步操作。该事件对象由hypertrm的主入口函数创建,并作为参数传递给发送线程和接收线程。在用户输入QUIT_CHARACTER键的时候,发送线程会设置该信号。而接收线程则不断检查该信号的状态,一旦该信号为有信号状态(被设置),则会结束运行。采用__BASE_EVENT结构传递参数,在发送模块中也会用到。
这样发送过程就很清晰了,总结如下:
(1)发送模块由两个函数ComSendByte和PollSend组成。其中ComSendByte完成轮询状态下的串口发送工作,PollSend是发送核心线程的入口函数,该函数调用ComSendByte,向串口发送用户输入的字符。同时,该线程还根据用户输入的字符,来判断是否应该结束运行。
(2)发送核心线程也不是一直在运行的,在没有用户输入的时候,发送核心线程阻塞在GetMessage函数上。只有在有用户输入(GetMessage能够获取消息)的时候,发送核心线程才被唤醒。因此,发送核心线程的效率是可以得到保证的。
下面是接收模块的实现。与发送模块类似,接收模块也是由两个函数组成的,ComRecvByte用于从串口接收一个字节,而PollRecv则是接收核心线程的入口函数,该函数不断调用ComRecvByte,若能够接收到数据,则打印在屏幕上。下面是ComRecvByte函数的实现代码:
该函数也是采用双层循环的方式,不断检查线路寄存器的状态。一旦发现有字符到达,则调用__inb,把字符从串口中读取出来,并返回TRUE。若两层循环后仍然没有取得数据,则放弃接收操作,返回FALSE,指示本次接收失败。
下面是接收核心线程的入口函数:
该函数进入一个无限循环,不断调用ComRecvByte函数,试图从串口接收字符。若调用成功,则打印出此字符。在每个循环结束的时候,调用WaitForThisObjectEx函数,检查事件对象的状态。若事件状态被设置,则函数返回,导致接收线程结束。有两个地方需要解释一下:
(1)WaitForThisObjectEx是一个超时等待函数,第二个参数给出了超时值(以毫秒计)。若在超时前,等待的对象可用(如事件对象被设置),则返回OBJECT_WAIT_RESOURCE,若超过等待事件后对象仍不可用,则返回OBJECT_WAIT_TIMEOUT。若以参数0调用该函数,则不会进入阻塞操作,而只是判断一下等待对象的状态。若状态可用,则直接返回OBJECT_WAIT_RESOURCE,若对象不可用,则直接返回OBJECT_WAIT_TIMEOUT。在该实现中,无需等待事件对象,只需判断一下对象的状态即可。只要事件对象被设置,则意味着用户按下了QUIT_CHARACTER键,接收线程会直接结束运行。
(2)获得串口的字符后,需要进一步判断字符是否为回车或换行符。若是,则调用ChangeLine和GotoHome,换行或回车(回到一行的起始处)。若是其他字符,则直接调用PrintCh打印出来。PrintCh是一个PC屏幕输出函数,接收一个WORD(两字节)类型的参数,其中参数的高字节指明了输出到屏幕上的前景和背景颜色,而低字节则指明了要输出的字符的ASCII码。
下面是hypertrm应用程序的主入口函数,也是主入口线程。函数应该遵循Hello China定义的核心线程入口函数原型(以LPVOID为参数,返回DWORD)。该主入口函数实现了下列功能:
(1)初始化COM接口。在当前的实现中,hypertrm直接操作COM1接口。也可通过传入命令行的方式,根据用户输入选择不同的COM接口,但目前为了实现上的方便,采用固定编码的方式,直接操作COM1接口。这可适应大多数的应用场合。
(2)初始化接收线程和发送线程用到的内核对象,比如事件对象等,然后创建接收核心线程和发送核心线程。
(3)等待两个核心线程运行结束,然后完成清理工作,并返回。
下面是其实现代码:
需要注意的是,在完成发送线程和接收线程的创建之后,调用了SetFocusThread,设置了当前输入焦点线程为发送核心线程。这样的结果是,对用户的任何输入(目前来说,只有键盘输入),操作系统核心都会发送给发送线程。这样发送线程就可通过串口发送出去。若不调用该函数,则当前的输入焦点线程是系统shell线程,用户的键盘输入会被shell线程获得,而不会被COM接口发送线程获得,因此无法实现交互。
上述代码形成了整个hypertrm应用程序。最后,需要在EXTCMD.CPP文件中,增加一行代码,把hypertrm的命令字符串和入口函数添加到外部命令列表。这样一旦用户输入“hypertrm”字符串,shell线程就会查找内部和外部命令列表,最终会匹配到刚刚添加的项,于是shell会以hypertrm为入口函数,创建一个核心线程,这样最终会导致hypertrm应用程序的启动。
下面总结一下hypertrm的启动和运行过程:
(1)用户在命令行界面下,输入hypertrm字符串,然后回车。
(2)shell线程获取输入,以输入的字符串为关键字,搜索内部命令列表和外部命令列表。
(3)在外部命令列表搜索中,命中刚才添加的项。
(4)shell以Hypertrm为入口点,创建一个核心线程,并把当前输入焦点(通过调用SetFocusThread函数)设置为刚刚创建的核心线程。
(5)Hypertrm作为hypertrm应用的主入口函数,初始化COM1接口,并创建发送核心线程和接收核心线程,然后重新设置当前输入焦点为发送核心线程,并等待发送线程和接收线程运行结束(等待的过程中,主线程处于阻塞状态)。
(6)发送核心线程调用GetMessage函数,检查消息队列,把用户通过键盘输入的字符,发送到COM接口。需要注意的是,发送核心线程是阻塞在GetMessage函数上的,只有消息到达的时候,才会被唤醒。
(7)接收线程不断检查COM接口的线路状态寄存器,若发现COM接口有字符到达,则读取并打印到屏幕上。另外在每个检查循环结束的时候,接收线程还检查一个事件对象(该事件对象用于同步发送和接收线程),一旦发现事件对象被设置,则退出运行。
(8)一旦用户输入QUIT_CHARACTER(当前定义为'Z'),发送核心线程将设置事件对象(这会导致接收线程也退出),并退出运行。
(9)当发送核心线程和接收核心线程都退出运行的时候,hypertrm的主线程会恢复运行,这时候,主线程做一些收尾工作,释放相应的资源,并退出运行。
7.中断模式的串口交互程序实现
采用轮询方式的hypertrm程序,其发送线程是输入驱动的,即仅当用户有键盘输入的时候,才会被唤醒,若没有输入,则会阻塞在消息队列上,不会空循环而导致CPU资源浪费。但接收线程却是一直在运行的,即使COM接口没有任何数据到达,接收线程也不会阻塞,而是一直处于检查COM接口的状态之中。显然,接收线程会大大浪费CPU资源。这种情形无法避免,这也是轮询方式驱动程序(或应用程序)的最大缺点。
采用中断方式可解决该问题。对于发送线程,会由键盘输入事件驱动,与轮询方式一致,不会浪费任何CPU资源。但对于接收方式,也会由中断驱动。在COM接口有数据到达的时候,COM接口芯片会引发中断。在中断处理程序中,会唤醒接收线程,这样就可避免轮询方式中CPU资源浪费的现象。
但中断方式的数据接收线程,相对轮询方式,其编程方式也会复杂得多。在下面的部分中,会对中断方式的接收线程实现进行详细描述。
需要注意的是,不论是接收还是发送,都可由中断驱动。对于数据接收,中断驱动是很自然的事情,因为无法预期什么时候会有数据到达。但对于发送,却是可预期的,因为发送是主动的(用户输入的时候会引发发送过程)。但有的情况下,也需要中断来配合。因为在发送的时候,需要COM接口的发送保持寄存器处于空状态。这样若发送频率太高,超出了COM接口的处理能力,不查询COM接口的状态而直接发送,会导致信息丢失。这样就需要中断来配合了,在发送大量数据的时候,应用程序可先启动一个发送操作,然后进入等待状态。在COM接口芯片完成发送后,会通过中断通知发送线程,其发送寄存器为空,这时候发送线程可启动后续字符的发送,一直持续到数据发送完毕,这样就不会导致信息发送丢失了。
但本书实现的hyptrm2程序,却不会出现发送大量数据的情形,每次只会发送一个字符。而且唯一的发送来源就是键盘输入。即使输入再快,也不可能超过COM接口的发送能力和计算机的处理能力。因此对于发送过程,仍然采用与轮询方式一致的处理方式。但对于接收过程,采用中断处理方式。
hyptrm2的代码结构与hypertrm结构类似,不同的是增加了一个中断处理程序,用于处理中断。但一些实现细节和数据结构则有较大不同。hypertrm没有采用任何复杂的数据结构,只是简单地把接收到或待发送的数据存储到一个本地变量中,然后直接处理。但对于中断方式的处理程序,因为涉及中断处理程序和接收线程的数据交互和同步,所以采用了环形缓冲区(ring buffer)对象。
下面是中断方式下串口的初始化函数:
与轮询方式不同的是黑体标出的部分。在黑体标出的第一行代码中,启用了数据可用中断(设置中断允许寄存器的第一个比特),所有其他中断,包括发送保持寄存器空中断、发送状态错误中断等,都是禁止的。这样可简化程序的实现,而且功能也不会受太大影响。若需要补充其他类型的中断,只需要设置相应的比特,并在中断处理程序中添加处理代码即可。
黑体标出的第二行允许COM接口芯片引发中断,同时设置了DTR和RTS标记。黑体代码第一行的作用,仅仅是允许哪些事件可引发中断,若没有第二行黑体代码,则COM接口芯片仍然不会引发CPU中断,即使发生了可引发中断的事件。这是由PC/XT设计结构导致的,感觉有些多余。
设置好上述寄存器之后,若一旦有数据到达串口,串口就会引发CPU中断。
在中断模式下的发送线程hyptrm2的实现中,对于发送过程,只采用了一个函数实现,这个函数也是发送核心线程的入口函数。代码如下:
该函数比较简单,在一个无限循环当中检查消息队列,若有键盘输入消息,则会被唤醒进行处理。若用户按下的键是QUIT_CHARACTER(即'z'字符),则设置事件对象,以指示接收线程退出,然后返回,这样就导致自己结束运行。若接收到的字符是其他字符,则调用__outb函数,输送到串口。这里采用了一种最简单的形式,没有判断COM接口线路寄存器的状态。这实际上有些冒险,因为有可能COM接口尚未准备好发送。但在绝大多数情况下,都是可以正常工作的。对于要求十分苛刻的场合,可增加这部分判断,并增加多次尝试(与轮询方式发送类似)。
需要注意的是,若用户不做任何键盘输入,则该发送线程会处于阻塞状态,不会消耗任何CPU资源。这跟轮询方式下的发送过程类似。
上述代码中__BASE_AND_EVENT2是临时定义的一个数据结构,用于完成线程之间的参数传递,定义如下:
其中,wBasePort是COM接口的端口基地址,hTerminateEvent是一个事件对象,用于同步发送线程和接收线程。该事件对象由发送线程设置,用于通知接收线程结束运行。hSendRb和hRecvRb是两个环形缓冲区(__RING_BUFFER)对象,用于完成中断处理程序和发送/接收核心线程的数据交换和同步。在当前的实现中,数据发送线程没有用到环形缓冲区对象,因为没有采用中断方式。而数据接收核心线程却需要使用环形缓冲区对象缓存数据。
发送模块也只有一个函数,该函数也是发送核心线程的主入口函数。实现代码如下:
该函数进入一个无限循环,调用GetRingBuffElement,试图从环形缓冲区中取得数据。环形缓冲区是由hyptrm2的主入口线程创建的,中断处理程序会向该环形缓冲区中放置数据。若环形缓冲区中有数据,则接收线程会被唤醒,根据获取的数据,做不同的处理:
(1)若接收的字符是一个换行符,则调用ChangeLine函数,移动当前光标到下一行。
(2)若接收到的字符是一个回车符,则调用GotoHome函数,把光标返回到当前行的起始位置。
(3)若是其他字符,则调用PrintCh打印该字符到屏幕上。
完成一轮检查之后,再调用WaitForThisObjectEx函数,检查事件对象是否被设置。若是,则直接返回,从而导致接收线程退出,否则进入下一轮循环。
与轮询方式实现的hypertrm最大的不同,就是在hyptrm2的实现中,增加了一个中断处理程序。该中断处理程序处理COM接口芯片引发的中断。下面是中断处理程序的实现代码:
中断处理程序非常简单,首先读取中断状态寄存器,判断是否有中断发生。若中断状态寄存器的第一个比特为0,则说明有中断发生,若为1,则说明无中断发生,直接返回FALSE。
在有中断待处理的情况下,进一步通过读取线路状态寄存器,判断数据寄存器是否有数据待读取。若是,则调用__inb函数,从COM的数据寄存器中读取一个字节的数据,然后调用AddRingBuffElement,添加到环形队列中。AddRingBuffElement函数会唤醒阻塞在该环形队列上的核心线程。我们知道,接收线程就是通过调用GetRingBuffElement阻塞在环形队列上的,在中断程序中,接收线程会被唤醒。
有两个地方需要做进一步解释:
(1)在没有中断要处理的情况下中断程序的返回值。会直接返回FALSE。这样中断调度程序会认为该中断不是当前中断处理程序对应的设备发出的,于是会继续调用其他的中断处理程序(这些中断处理程序对应的设备,与COM接口共享同一中断输入)。若返回TRUE,则表明当前中断处理程序已成功地处理了中断,于是中断调度程序不会再调用其他设备的中断处理程序了。
(2)之所以会出现中断程序被调用,但中断状态寄存器却表明没有中断发生(最低比特为1)的情况,是因为多种设备可共享同一条中断输入。比如,另外一个计算机外设与COM接口芯片连接到了同一条中断输入引脚上(8259芯片的相同引脚),则另外的设备引发中断的时候,COM接口的中断处理程序也可能被调用。因此,需要进一步判断中断是否由COM接口芯片引发,若是,就做进一步处理并返回TRUE,否则返回FALSE,以便另外设备的中断处理程序会被调用。
下面是hyptrm2应用程序的主入口函数,需要符合核心线程入口函数的原型定义。下面是其实现代码:
该函数比较长,但比较简单,主要由三部分组成:
(1)初始化部分代码。在这部分代码中,创建了用于同步接收核心线程和发送核心线程的事件对象,以及用于中断处理程序和发送/接收线程的环形缓冲区对象。然后调用ConnectInterrupt函数,把COM接口的中断处理程序安装到系统中。完成这些工作后,才调用InitComPort2,初始化串口芯片。需要注意的是,一定要在中断处理程序安装完成之后,才能初始化COM接口。因为在初始化COM接口的时候,其中断是被打开的,这时候若还没有安装中断处理程序,一旦COM接口引发一个中断,就会导致系统打印出系统诊断信息而停机。
(2)核心线程创建代码。创建了接收和发送核心线程,并调用WaitForThisObject,等待两个核心线程运行结束。在接收和发送线程运行过程中,主线程是被阻塞的,不作任何处理。
(3)运行结束后的清理代码。这部分代码释放了创建的事件对象和核心线程对象,调用DisconnectInterrupt函数取消了安装在系统中的中断处理程序,并退出运行。需要注意的是,一定要调用DisconnectInterrupt函数取消中断,否则中断处理程序还可能被调用。这时候由于环形缓冲区对象已被销毁,可能会导致内存紊乱。
8.串行通信编程总结
通过上述描述可知,轮询方式的设备驱动程序,比中断方式的设备驱动程序消耗更多的CPU资源,因为轮询方式的驱动程序,需要CPU不停地去查询设备的状态,并做出适当的处理。而中断方式则不然,设备驱动线程只需被动地等待即可,一旦设备有输入,就会引发中断,在中断处理程序中完成设备IO,并唤醒等待的线程。在普通的操作系统环境中,比如个人计算机的操作系统实现中,选择中断方式的设备驱动程序是合适的,因为这可大大节约系统整体资源。
但在嵌入式操作系统中,选择中断方式的设备驱动实现,可能会存在问题。因为嵌入式系统对系统的响应时间要求十分苛刻。若采用中断方式的驱动程序,则正在处理关键任务的核心线程可能会被设备的中断打断,从而延误关键事件的处理。更糟糕的是,若系统外设硬件故障,导致外设不断引发中断,这样可能会使系统一直忙于处理不重要的外部中断,无法响应其他的系统事件。
轮询方式的设备驱动方式可避免此类问题。这时候,对设备的处理代码,实际上是一个核心线程,适当设置该核心线程的优先级,可使得系统中所有核心线程都处于一种有序的、可预测的调度顺序,这样即使外设不断发生中断,也不会对系统中其他关键的线程造成影响,因为关键的线程会优先得到调度。
因此,在设备驱动程序的实现中,可采用轮询方式加中断方式结合的策略:
(1)对于非常重要的且认为可靠的外部设备,可采用中断方式实现其驱动程序。这样可提高系统的整体性能。
(2)对于非关键的、不可靠的外部设备,可采用轮询方式实现其驱动程序。这样可提高系统的整体鲁棒性。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。