接上文:系统启动篇(三)[上]
在计算机中,用于数据传输的方式总共分为两种:①串行通信(Serial Communication),传输数据时只用一根线——按位发送和接受字节,速度慢但能够实现远距离通信,使用串行端口的设备有鼠标和USB等。②并行通信(Parallel Communication),使用多条线将每个数据的二进制位同时进行传输,传输速度较快但因为存在干扰而不能实现远距离通信,一般用来连接打印机和扫描仪等,在计算机内部也采用这种方式传输数据。这里我们主要将注意力集中在串行通信上,在这种通信方式中使用的最主要的参数分别为波特率、数据位、停止以及奇偶校验位,这些位的概要解释如下:
- 波特率:表示每秒传送的位的个数,主要用来衡量通信速度。假设波特率被设置为3600,那么对应的时钟周期则为3600Hz,即在数据线上的采样率为3600Hz。波特率通常和距离成反比。
- 数据位:当计算机发送一个信息帧时,实际传输的数据所采用的标准值是5、6、7或8位——这取决于数据位的设置方式。一个信息帧中通常涵盖开始/停止位,数据位和奇偶校验位。
- 停止位:表示一个信息单元的最后一位,可以是1,1.5和2位。停止位的位数越多,数据传输率越慢。
- 奇偶校验位:在通信过程中用于对数据进行检错。
串行端口通信相关设施
在计算机内部实际上存在三块芯片,它们分别是:
- 8250 UART(Universal Asynchronous Receiver/Transmitter) [通用异步发送/接收装置]
- 8259 PIC(Programmable Interrupt Controller) [可编程中断控制器]
- 8086 CPU(Central Processing Unit) [中央处理单元]
以上列出的三者并非指单一型号的芯片,而是涵盖了一类芯片家族,比如8086 CPU实际指的是x86/pentium/core系列等兼容芯片。出于降低设备成本的考虑,它们通常被放置在同一块硅片上。在许多情形下,为早期IBM PC兼容机所写的用于执行串行通信的软件,同样能在安装了最新版本的Windows/Linux系统的现代机器上运行。在这三者中,我们主要关注8250 UART。同8086家族一样,8250 UART也经历了快速的发展,例如至今已发展成为16550 UART系列。使得UART芯片家族快速演化的源动力主要来自于能够跟上CPU越来越快的执行速度的要求。
如前所述,在计算机内部采用并行通信方式,因此在将数据传输至串行外设之前首先需要将其转换为串行数据流,接收数据时也需要做相应的转换,而这项工作正是由UART来完成的。具体的执行方式为:UART将CPU将所要输出的数据封装为一个消息帧,该帧的格式是以一个低位起始位作为开始,后跟5、6、7或8个数据位,一个可用的奇偶位以及一个或多个高位停止位。当消息帧到达目的地后,接收器随即检测起始位,开始接收有效载荷(payload),并尝试与发送装置同步时钟频率,而其后的奇偶位则帮助错误校验。在接收串行外设的输入时,UART除了去掉消息帧中的起始位、停止位并进行相应的校验工作之外,也需要将串行数据流转换为并行数据。控制台的初始化正是根据内核命令行中的选项对数据的传输以及转换进行相应的设置。
即便从理论上来讲可以通过增加串行端口的方式连接更多的外设,但在大多数PC机上仅有两个串行端口——COM1及COM2。其中COM1的物理地址为0x3f8,COM2的物理地址为0x2f8,以这两个I/O端口地址之一起始的连续8个字节的物理内存被映射为UART芯片上12个不同的寄存器,这意味着多个不同的寄存器将共用同一个I/O端口地址,并且当前的I/O端口地址被映射为哪一个寄存器具体取决于当前的上下文环境及UART的配置方式。虽然BIOS为串行通信提供了一些中断例程,但通常都是通过访问寄存器中的位来实施对UART的控制。下表列出了UART中存在的寄存器:
偏移量 | 访问控制 | 概要描述 |
+0 | 只写 | 用于将数据输出外设(Transmitter Data Register) [TXR] |
+0 | 只读 | 从外设接收数据(Receive Data Register) [RXR] |
+0 | 可读/可写 | 波特率除数低位字节(Baud Rate Divisor Latch Low Byte) [DLL] |
+1 | 可读/可写 | 中断允许寄存器(Interrupt Enable Register) [IER) |
+1 | 可读/可写 | 波特率除数高位字节(Baud Rate Divisor Latch High Byte) [DLH) |
+2 | 只读 | 中断识别寄存器(Interrupt Identification Register) [IIR] |
+2 | 只写 | 先进先出控制寄存器(FIFO Control Register) [FCR] |
+3 | 可读/可写 | 行控制寄存器(Line Control Register) [LCR] |
+4 | 可读/可写 | 调制解调控制寄存器(Modem Control Register) [MCR] |
+5 | 只读 | 行状态寄存器(Line Status Register) [LSR] |
+6 | 只读 | 调制解调状态寄存器(Modem Status Register) [MSR] |
+7 | 可读/可写 | 额外备用寄存器(Scratch Register) [SR] |
以下是对各个寄存器的详细描述(除额外备用寄存器Scratch Register之外):
1、传输/接收数据寄存器(Transmitter/Receive Data Register,TXR/RXR)
这两个寄存器就是串行数据通信的“心脏”,数据通过这两个寄存器被传输至串行外设或从外设接收数据,由于其他的计算机对于本机来讲也可看成是一类外设,所以计算机之间的数据传输也是通过这两个寄存器完成的。现在的一类芯片比如16650一次可以发送或接受16个字节的数据,相比早期一次只能处理1个字节的数据,这在多任务处理环境中显得非常有用。这两者共用同一个I/O端口地址——通过写I/O地址0x3f8或0x2f8则自动切换至传输寄存器,读相应的I/O地址切换至数据接收寄存器。为了确定是否可以输出或接收数据,即对应的传输寄存器是否为空或接收寄存器是否已满,可以通过行状态寄存器(Line Status Register,LSR)中的相应位查看。
2、波特率除数占用字(Baud Rate Divisor Latch Bytes)
波特率除数寄存器是一个与传输/接收寄存器以及中断允许寄存器共享I/O地址的16位寄存器,行控制寄存器(Line Control Register,LCR)中的第7位用来选择是否将这两个字节解释为波特率除数寄存器。这个寄存器的作用正如其名字所示,它被用作除数以决定芯片传输数据的速率——更准确的说是波特率,而其值本质上就是由UART所使用的递减计数时钟(count-down clock),在每次传输一个比特时,递减计数寄存器(count-down register )被重新置为该值,当递减至0时传输下一个比特,而这个时钟通常被设置为115.2KHz。波特率的计算公式如下所示:
下表列出了有关波特率及其根据上述公式计算得出的除数字的一些表项:
波特率 | 除数(十进制格式) | 除数高位字节 | 除数低位字节 |
50 | 2304 | 0x09 | 0x00 |
110 | 1047 | 0x04 | 0x17 |
220 | 524 | 0x02 | 0x0c |
300 | 384 | 0x01 | 0x80 |
600 | 192 | 0x00 | 0xc0 |
1200 | 96 | 0x00 | 0x60 |
2400 | 48 | 0x00 | 0x30 |
4800 | 24 | 0x00 | 0x18 |
9600 | 12 | 0x00 | 0x0c |
19200 | 6 | 0x00 | 0x06 |
38400 | 3 | 0x00 | 0x03 |
57600 | 2 | 0x00 | 0x02 |
115200 | 1 | 0x00 | 0x01 |
注意除了上表所列的典型波特率值之外还可以为其他的值,但绝不能将波特率除数占用字设置为0,否则这将损坏UART芯片,因为这种情形下UART传输串行数据的行为是不可预知的。
3、中断允许寄存器(Interrupt Enable Register,IER)
这个寄存器用来控制与串行端口通信相关的硬件中断。8250 UART提供四种类型的中断源:①数据到达中断;②传输装置为空中断;③接收装置行状态中断;④调制解调中断。可以通过对IER中的某个位写1或是0以启用或禁用相应的中断,该寄存器的布局如下所示:
图1——Interrupt Enable Register(IER)
以下是各个位的详细解释:
- 接收数据中断通知所需要的数据已由某个外设送达,相比其他位来说这是最长使用的一个位。
- 传输装置为空中断通知可将数据写入输出缓冲区,这使得数据的传输可以像流水线(Streamline the data transmission),从而减少CPU的运行时间。
- 接收装置行状态中断指示LSR(Line Status Register,行状态寄存器)中某些位发生改变,这通常是一个出错状态。
- 调制解调状态中断通知与计算机相连的外部调制解调装置发生改变,该位同样能够帮助判断外部调制器或数据设备能否持续接收数据。
- 剩余的两个模式是在16750 UART中引入的,主要帮助芯片切换至低功耗状态,这两个位可以在膝上型电脑或嵌入式控制器等能源有限的设备中使用
4、中断识别寄存器(Interrupt Identification Register,IIR)
中断识别寄存器指示中断是否挂起以及五类中断源中的哪些需要被处理,注意多个串行通信设备可以共享这些硬件中断。该寄存器的布局如下所示:
图2——Interrupt Identification Register(IIR)
以下是中断识别寄存器(IIR)其中各个位的详细解释:
- 该寄存器的第0个位指示某个设备引发中断,因此需要检查各个I/O地址空间对应的串行设备。由于在同一时刻可能有多于一个中断被触发,因此明智的做法是检查所有的串行设备。当该位置零,它指示UART触发了一个中断,反之则不存在中断。
- 第1、2和3位指示中断事件的类型,各个中断事件如图2中所示。如果同一时间触发多个中断,那么需要调用多个对应的中断处理例程分别进行处理。上图中所示的重置方法(Reset Method)描述了指定中断的处理方式,在某个中断发生时,通过访问该中断所对应的重置方法中所提及的寄存器,即可清除相应的中断状态。
- 第6、7位指示FIFO缓冲区(由FIFO控制寄存器控制)的当前状态,该缓冲区用于传输和接受数据。
当某个指定中断发生时,IIR的第0位被清零,接下来的三个位则包含该中断源所对应的中断号,因此可以使用该值作为表的索引以指向合适的中断处理例程。此外,由上文所述同一时刻可能会有多个中断被触发,然而中断挂起位只占一位,这也就是说IIR在某个时刻只能报告一个中断,为了能够处理同时发生的多个中断,UART为每个中断事件赋予不同的优先级,其中中断源110为最高优先级,随着中断源号的减小优先级逐渐降低,中断源000为最低优先级。所以当处理完当前中断之后,若中断挂起位仍置零,那么第1~3位将变为次高优先级的中断源号。
5、FIFO控制寄存器(FIFO Control Register,FCR)
这是一个只写寄存器,其主要用于控制FIFO缓冲区并帮助调整该缓冲区在应用程序中的性能。其布局如下所示:
图3——FIFO Control Register(FCR)
以下是其中各个位的详细解释:
- 第0位置零则禁用FIFO缓冲区,并使该寄存器中的其余位变为无效。仅当准备重置串行通信协议并清除应用程序中的工作缓冲区时才应该禁用FIFO缓冲区,并且在将该位置零的同时应该清空FIFO缓冲区。
- 第1、2位被用来清除FIFO缓冲区。一旦将这两个位中的任意一个置1,那么其后该位将自动重置为0,将这两个位置0指示UART不需要重置FIFO缓冲区。
- 第3个位用于选择DMA(Direct Memory Access,直接内存存取)模式,将串行线路上接收到的数据直接存入内存缓冲区中,而不需要再经过CPU。
- 第5个位置1允许16750 UART将FIFO缓冲区从16字节扩展为64字节,此时该位不仅影响缓冲区的大小,同时也将改变触发器的阀值(trigger threshold)。置0则FIFO缓冲区仍为16字节大小。
- 第6、7两个位描述触发器阀值,这两个值指示中断被触发前,FIFO缓冲区将会存放的字节数,其中的中断报告数据将从FIFO缓冲区中被移出。触发器的最大值之所以小于FIFO缓冲区的大小,主要是因为应用程序在访问UART并获取数据时需要花费一定的时间,如果应用程序还未来得及将缓冲区中的所有数据全部接收完毕,串行线路上的数据又被送至该缓冲区则将导致数据丢失,因此一旦到达阀值应确保已经获取数据。
6、行控制寄存器(Line Control Register,LCR)
该寄存器主要有两个用途:①设置接收/传输串行数据过程中的位模式,包括数据的大小,停止位的个数,校验及强行中止;②设置波特率除数占用字(Baud Rate Divisor Latch Bytes)的值。注意,一次串行数据传输由一个起始位、5~8个数据位、奇偶校验位以及1、1.5或2个停止位构成,起始位通知UART或其他设备所需数据在串行线路上等待被接收,而停止位则指示一个消息帧的结束。该寄存器的布局图如下所示:
图4——Line Control Register(LCR)
以下是上述寄存器中各个位的详细解释:
- 起始的两个位(第0、1位)设置串行数据位数,对于大多数现代的串行通信系统来说通常均设置为8位,一般来说,数据位数越少传输速度越快。另外如果连接到一些固定字长的设备上,应设置对应的字长以匹配该设备。
- 第2位设置消息帧中停止位的个数,置零表示选择1个停止位,反之则选择2个停止位,在5个数据位的情形中使用1.5个停止位代替2个停止位——在一次传输过程中使用1个停止位,接下来的一次传输过程使用2个停止位。较多的停止位意味着更长的传输时间,对于现代的大多数设备来说均选择1个停止位。
- 第3、4、5位设置串行数据对应的校验方式,其用来检测传输过程中可能产生的错误。当第3位置零时在消息帧中的数据字后不添加额外的校验位,反之则将增加一个额外的校验位,以下是各种校验方式的概要解释:
①奇校验——将消息帧中的各个位相加,若所得结果为奇数则校验位置1
②偶校验——与奇校验相反,若所得结果为偶数则将校验位置1,反之置零
③标记(Mark Parity)——这种情况中校验位始终置1,此时可以将其当做额外的”停止位“
④留空(Space Parity)——如标记方式一样,在这种情况中校验位始终置0 - 第6位为中止位(break bit),若该位置1则向外设(或远程系统)传输一个中止信号,这将中止在远程系统上的程序的运行并触发一个合适的中断。
- 第7位置1时,I/O端口地址空间中偏移量为0和1处的两个字节被解释为波特率除数占用字,而当该位清零则这两个字节仍对应数据寄存器(Data Register,TXR/RXR)以及中断允许寄存器(Interrupt Enable Register,IER),仅在设置UART的传输速度时才将该位置1。
7、调制解调控制寄存器(Modem Control Register,MCR)
该寄存器能够直接操作UART上四条不同的通信线路,与其对应的在该寄存器中的四个位允许被设置为任意的逻辑状态。需要注意的一点是,在大多数UART上位Auxiliary Output 2被设置为1以允许中断。以下是该寄存器的布局图:
图5——Modem Control Register(MCR)
以下是MCR中各个位的详细解释:
- 第0位控制UART芯片上的DTR通信线,该位置1则表示对应的通信线处于活动状态,若某个终端的该线路不处于活动状态,则其余终端将无法向其传输数据。
- 与DTR位类似,MCR寄存器中的第1位控制芯片上的RTS通信线,置1则表示线路处于活动状态,在此状态下可以发送数据。
- 第3位(即Auxiliary Output 2)被设计为连接到一个外部的门以允许或禁用中断,再次注意该位必须置1以允许中断,除非不使用中断。
- 第4位连接数据传输寄存器(TXR)以及数据接收寄存器(RXR),若该位置1则启用环回模式,此时传输装置发送的所有数据将立刻回到接收寄存器,这一位通常很少使用。
- 第5位在16750型号的UART芯片上存在,该位将根据FIFO缓冲区的当前状态,为数据流直接控制DTS/RTS通信线路的状态。
8、行状态寄存器(Line Status Register,LSR)
行状态寄存器是一个只读寄存器,它将根据所接收的数据提供UART芯片中存在的一些错误状态信息,这将帮助诊断串行数据通信中可能存在的问题,其布局图如下所示:
图6——Line Status Register(LSR)
其中各个位的详细解释如下:
- 第0位置1通知接收数据寄存器(RXR)中的数据可用,若UART芯片带有FIFO缓冲区并处于活动状态,那么该位仍将置1,除非已读取FIFO缓冲区中的所有数据。该位置1时将产生一个中断,通过读取接收到的数据可将该位清零。
- 第1位指示溢出错误,因为RXR寄存器在某一时刻至多只能存放一个字节的数据,若该字节尚未被读取,但第二个字节已经到达并被存入该寄存器,则第一个字节将被清除,此时该位置1以指示产生溢出错误状态。若UART带有FIFO缓冲区,则该缓冲区满时同样将该位置1以指示溢出错误。适当地降低串行传输的波特率是解决该问题的方式之一。
- 第2位指示校验错误,若在接收一个字节时检测到校验错误则将该位置1。只有当在行控制寄存器(LCR)中允许校验操作时校验错误才会发生,并且此时UART芯片将产生一个错误中断,当读该寄存器后这一位将自动重置为零。
- 第3位是成帧出错位,当接收到的消息帧中不含有效的停止位时该位被置1以指示这一错误,更精确地说停止位为0则表示无效,在排查出错原因时应首先确保UART中的所有设置(包括数据位长度、校验及停止位等)的合理性,在读该寄存器后这一位将自动被清零。
- 第4位为打断中断位(break interrupt),当串行数据线在一段时间内没有接收到任何新的位(包括起始位、数据位、校验及停止位)时该位被置1,这意味着传输串行数据的设备由于某些不知名的原因而停止工作了,同样在读该寄存器后这一位将自动清零。
- 在UART芯片内实际上有两个寄存器与接收装置有关,他们分别是:①移出寄存器(shift register)——包含实际被移至串行线路上的数据;②持有寄存器(holding register)——包含准备写入移出寄存器的值,写入操作将在移出寄存器移出当前数据后执行。第5位置1时指明持有寄存器为空,UART芯片可接受下一个字节的数据,可以通过读LSR寄存器或写传输数据寄存器(TXR)以清除该位。而当移出/持有寄存器均为空时,第6位被置1,当这两个寄存器中任意一个包含数据时该位被清零。
- 与以上各个位列出的状态相关的数据可能位于接收数据寄存器(RXR)中或是FIFO缓冲区中,若第7位置1则指示数据源位于FIFO缓冲区中,此时应将该缓冲区清空,若UART芯片不带有FIFO缓冲区则该位被保留。
9、调制解调状态寄存器(Modem Status Register,MSR)
这是一个只读寄存器,它用来通知调制解调装置的当前状态。该寄存器的布局图如下所示:
图7——Modem Status Register(MSR)
以下是各个位的详细解释:
- MSR中从第0位至第3位均为改变位(或称"delta"位),当对应的调制解调状态位改变时这些位被置1,同时也将产生调制解调中断,通过读MSR寄存器可以将这些位清除。注意状态位的改变是指与之相关的位在连续两次访问MSR时处于不同的逻辑状态。
- 第4位是清除发送位——一个握手信号,该位通常和远程设备上的RTS(Request To Send)信号相连,当远程设备上的RTS信号断言通信线处于活动状态则可以开始传输数据。
- 数据装置就绪位(第5位)被置1,若远程设备不处于繁忙状态,该输入通常与远程设备上的DTR(Data Terminal Ready)通信线相连。
- 若调制解调器断言其铃声指示器通信线处于活动状态,则UART芯片将该铃声指示(第6位)位置1,通常很少使用该信号。
- 第7位为数据载波检测位,若调制解调装置在电话线上检测到载波信号则该位置1,反之当处于逻辑状态0时则可假设为连接中断。
控制台初始化[续]
以上是有关串行通信及其硬件设施的简要介绍,接着我们再根据前文的调用关系图从parse_earlyprintk函数入手深入分析控制台的初始化过程。该函数位于arch\x86\boot\Early_serial_console.c文件中:
- static void parse_earlyprintk(void)
- char arg[32];
- /* get the value in accordance with "earlyprintk" */
- if (cmdline_find_option("earlyprintk", arg, sizeof arg) > 0)
static void parse_earlyprintk(void)
char arg[32];
/* get the value in accordance with "earlyprintk" */
if (cmdline_find_option("earlyprintk", arg, sizeof arg) > 0)
首先调用cmdline_find_option函数,该函数的具体剖析过程详见系统启动篇(三)[上],所完成的功能是查找内核命令行中的非布尔类型的选项,若在命令行中重复出现该选项则返回最后一个实例,再次强调选项的格式为"option=argument"。因此if条件判断语句中的表达式首先从命令行中找到与"earlyprintk"对应的选项,并将其结果存入参数arg中,然后返回argument的长度。若返回值大于0,则说明找到相应的选项,因此接着执行如下语句块:
- if (!strncmp(arg, "serial", 6)) {
- port = DEFAULT_SERIAL_PORT; /*指定I/O串行通信端口的基地址*/
- pos += 6; /*指示选项值中的当前偏移量*/
- }
- if (arg[pos] == ',')
- pos++;
- /* #define DEFAULT_SERIAL_PORT 0x3f8 */
if (!strncmp(arg, "serial", 6)) {
port = DEFAULT_SERIAL_PORT; /*指定I/O串行通信端口的基地址*/
pos += 6; /*指示选项值中的当前偏移量*/
}
if (arg[pos] == ',')
pos++;
/* #define DEFAULT_SERIAL_PORT 0x3f8 */
在得到命令行中的选项之后首先调用函数strncmp(arg, "serial", 6)判断所得到的参数是否为serial,若是则将将port赋值为0x3f8,它是串行端口COM1的物理地址。接着将pos加上6,跳过返回的选项值中的前6个字符"serial",其后判断当前所指向的值是否为',',若是则将偏移再自增1,因此若这两个if条件判断语句中的表达式都满足,那么pos当前的值应为7。
- /*
- * make sure we have
- * "serial,0x3f8,115200"
- * "serial,ttyS0,115200"
- * "ttyS0,115200"
- */
- char *e;
- if (pos == 7 && !strncmp(arg + pos, "0x", 2)) { /*strncmp判断arg[7]所在的两个字符是否为"0x"*/
- port = simple_strtoull(arg + pos, &e, 16); /*注意e指向紧随待转换字符之后的第一个字符*/
- if (port == 0 || arg + pos == e) /*若arg[7]处返回的无符号长整型为0或e的值为arg[7]*/
- port = DEFAULT_SERIAL_PORT; /*仍将port赋值为0x3f8*/
- else
- pos = e - arg; /*反之则将偏移量pos改变为e-arg,这使得arg[pos]的值即为e*/
/*
* make sure we have
* "serial,0x3f8,115200"
* "serial,ttyS0,115200"
* "ttyS0,115200"
*/
char *e;
if (pos == 7 && !strncmp(arg + pos, "0x", 2)) { /*strncmp判断arg[7]所在的两个字符是否为"0x"*/
port = simple_strtoull(arg + pos, &e, 16); /*注意e指向紧随待转换字符之后的第一个字符*/
if (port == 0 || arg + pos == e) /*若arg[7]处返回的无符号长整型为0或e的值为arg[7]*/
port = DEFAULT_SERIAL_PORT; /*仍将port赋值为0x3f8*/
else
pos = e - arg; /*反之则将偏移量pos改变为e-arg,这使得arg[pos]的值即为e*/
上述注释说明了以下代码段是为了确保cmdline_find_option函数所得到的返回值为"serial,0x3f8,115200"、"serial,ttyS0,115200"以及"ttyS0,115200"这三者之一。首先判断pos的值是否为7,若是则继续判断返回值中接下来的两个字符是否为"0x",如果这两个表达式都满足,那么执行simple_strtoull函数(剖析过程见系统启动篇(三)[上]),这个函数将arg[7]处所在的字符串按16进制格式转换为无符号长整型并复制给port,表明使用arg[7]处明确指明的值作为I/O串行通信端口的基地址,随后根据port以及pos的值做适当的修正操作。
- /*这里的else if与if (pos == 7 && !strncmp(arg + pos, "0x", 2))相对应*/
- } else if (!strncmp(arg + pos, "ttyS", 4)) { /*判断arg[pos]是否与ttyS相等*/
- static const int bases[] = { 0x3f8, 0x2f8 };
- int idx = 0;
- if (!strncmp(arg + pos, "ttyS", 4)) /*if条件总是满足*/
- pos += 4; /*因此pos的值将自增4*/
- if (arg[pos++] == '1')
- idx = 1;
- port = bases[idx];
- }
/*这里的else if与if (pos == 7 && !strncmp(arg + pos, "0x", 2))相对应*/
} else if (!strncmp(arg + pos, "ttyS", 4)) { /*判断arg[pos]是否与ttyS相等*/
static const int bases[] = { 0x3f8, 0x2f8 };
int idx = 0;
if (!strncmp(arg + pos, "ttyS", 4)) /*if条件总是满足*/
pos += 4; /*因此pos的值将自增4*/
if (arg[pos++] == '1')
idx = 1;
port = bases[idx];
}
反之,若if (pos == 7 && !strncmp(arg + pos, "0x", 2))条件判断语句不成立,则执行else if分支,同样首先调用strncmp判断arg[pos]是否等于ttyS,其后再根据紧随"ttyS"之后的字符是'1'或'0',选择以0x3f8或者0x2f8作为I/O串行端口通信的基地址,再次申明0x3f8对应的COM1的基地址,而0x2f8则对应的是COM2的基地址。
- 什么是ttyS?
tty一词源于teletypewrites,原指电传打字机,不过现在通常被用来指代各种类型的终端设备,例如键盘或显示器,而其后的S则表示Serial,因此ttyS用来指代与串行端口相连的串行终端,其中ttyS0与串行通信端口COM1相对应,ttyS1与COM2相对应。注意这些终端均为字符设备。
- if (arg[pos] == ',') /*跳过字符 ','*/
- pos++;
- baud = simple_strtoull(arg + pos, &e, 0);
- if (baud == 0 || arg + pos == e)
- baud = DEFAULT_BAUD;
- /* #define DEFAULT_BAUD 9600 */
if (arg[pos] == ',') /*跳过字符 ','*/
pos++;
baud = simple_strtoull(arg + pos, &e, 0);
if (baud == 0 || arg + pos == e)
baud = DEFAULT_BAUD;
/* #define DEFAULT_BAUD 9600 */
接着调用simple_strtoull函数将arg[pos]处的字符串转换为无符号长整型,该处的字符串此刻被解释为命令行中设置的波特率,随后根据baud以及pos的值做相应的修正操作,若baud等于0或是arg[pos]等于e,那么就将baud设置为默认的波特率(9600)。而之所以需要进行前后这两次修正操作,是因为选项"earlyprintk"的返回值中用于设置端口port以及波特率baud的值有可能并不符合要求,因此需将其重置为默认值。
- /* 此处已跳出if (cmdline_find_option("earlyprintk", arg, sizeof arg) > 0)条件语句块 */
- if (port)
- early_serial_init(port, baud);
/* 此处已跳出if (cmdline_find_option("earlyprintk", arg, sizeof arg) > 0)条件语句块 */
if (port)
early_serial_init(port, baud);
因为在if (cmdline_find_option("earlyprintk", arg, sizeof arg) > 0)语句块中已合理设置端口port的值,所以执行early_serial_init(port,baud)过程,该过程定义在arch\x86\boot\Early_serial_console.c文件中。
- static void early_serial_init(int port, int baud)
- outb(0x3, port + LCR); /* 8n1 */
- outb(0, port + IER); /* no interrupt */
- outb(0, port + FCR); /* no fifo */
- outb(0x3, port + MCR); /* DTR + RTS */
- /*其中outb函数在arch\x86\boot\Boot.h文件中定义如下*/
- static inline void outb(u8 v, u16 port)
- {
- /* 等价于 outb v, port*/
- asm volatile("outb %0,%1" : : "a" (v), "dN" (port));
- }
static void early_serial_init(int port, int baud)
outb(0x3, port + LCR); /* 8n1 */
outb(0, port + IER); /* no interrupt */
outb(0, port + FCR); /* no fifo */
outb(0x3, port + MCR); /* DTR + RTS */
/*其中outb函数在arch\x86\boot\Boot.h文件中定义如下*/
static inline void outb(u8 v, u16 port)
{
/* 等价于 outb v, port*/
asm volatile("outb %0,%1" : : "a" (v), "dN" (port));
}
第一条指令首先设置LCR寄存器,由图4并且根据立即数0x3=0000 0011可知,该指令将消息帧中的数据位设置为8且后跟1个停止位,但并不在消息帧中使用奇偶校验位。下一条指令设置IER寄存器,如前所述主要用于控制相关硬件中断,由于将该寄存器的所有位均设置为零,因此不使用中断。而outb(0, port+FCR)指令则用于设置FIFO缓冲区,根据图3可知,因为第0位被设置为0,所以不使用缓冲区。接下来的一条指令则设置MCR寄存器,根据图5可知这表示串行数据通信线处于活动状态,因此可以发送和接受数据,接着设置其余寄存器。
- unsigned char c;
- unsigned divisor;
- divisor = 115200 / baud;
- c = inb(port + LCR);
- outb(c | DLAB, port + LCR); /* #define DLAB 0x80 */
- outb(divisor & 0xff, port + DLL); /*设置除数低位字节*/
- outb((divisor >> 8) & 0xff, port + DLH); /*设置除数高位字节*/
- outb(c & ~DLAB, port + LCR);
- early_serial_base = port; /* early_serial_base为全局变量 */
- /*其中inb函数在arch\x86\boot\Boot.h文件中定义如下*/
- static inline u8 inb(u16 port)
- {
- u8 v;
- /* 等价于 inb port, v*/
- asm volatile("inb %1,%0" : "=a" (v) : "dN" (port));
- return v;
- }
unsigned char c;
unsigned divisor;
divisor = 115200 / baud;
c = inb(port + LCR);
outb(c | DLAB, port + LCR); /* #define DLAB 0x80 */
outb(divisor & 0xff, port + DLL); /*设置除数低位字节*/
outb((divisor >> 8) & 0xff, port + DLH); /*设置除数高位字节*/
outb(c & ~DLAB, port + LCR);
early_serial_base = port; /* early_serial_base为全局变量 */
/*其中inb函数在arch\x86\boot\Boot.h文件中定义如下*/
static inline u8 inb(u16 port)
{
u8 v;
/* 等价于 inb port, v*/
asm volatile("inb %1,%0" : "=a" (v) : "dN" (port));
return v;
}
接着执行inb(port+LCR)指令将LCR寄存器中的当前数据存入变量c中,再将该变量与DLAB执行或运算,得到立即数0x83并送入LCR寄存器,这意味着并不改变消息帧的格式,但将偏移量为0和1处的数据寄存器及中断允许寄存器解释为波特率除数占用字。根据前文所述,由公式BaudRate = 115200/(Divisor Latch Value)以及所要设置的BuadRate即可得到Divisor Latch Value,在上述代码中即为divisor变量,后续的两条指令分别将divisor的低位及高位字节送入相应的寄存器从而完成波特率的设置。最后将表示端口的变量port赋值给early_serial_base变量。总结一下上述过程:查找内核命令行中"earlyprintk"对应的选项,根据该选项的返回值设置一系列串行端口通信所需设施。
由于early_serial_base已被正确设置为相应的串行端口,因此其值非0,因此在console_init过程中(见系统启动篇(三)[上])继续调用pase_console_uart8250()子过程,其同样定义于arch\x86\boot\Early_serial_console.c文件中:
- static void parse_console_uart8250(void)
- char optstr[64];
- /*
- * console=uart8250,io,0x3f8,115200n8
- * need to make sure it is last one console !
- */
- if (cmdline_find_option("console", optstr, sizeof optstr) <= 0)
- return;
static void parse_console_uart8250(void)
char optstr[64];
/*
* console=uart8250,io,0x3f8,115200n8
* need to make sure it is last one console !
*/
if (cmdline_find_option("console", optstr, sizeof optstr) <= 0)
return;
同样该过程首先搜索内核命令行中名为"console"的选项,并将对应的返回值存入optstr字符数组中,由注释可知该选项在内核命令行中一般被设置为console=uart8250,io,0x3f8,115200n8。
- char *options;
- options = optstr;
- if (!strncmp(options, "uart8250,io,", 12)) /*比较options所指向的前12字符串是否为"uart8250,io,"*/
- port = simple_strtoull(options + 12, &options, 0);
- else if (!strncmp(options, "uart,io,", 8)) /*同上*/
- port = simple_strtoull(options + 8, &options, 0);
- else
- return;
char *options;
options = optstr;
if (!strncmp(options, "uart8250,io,", 12)) /*比较options所指向的前12字符串是否为"uart8250,io,"*/
port = simple_strtoull(options + 12, &options, 0);
else if (!strncmp(options, "uart,io,", 8)) /*同上*/
port = simple_strtoull(options + 8, &options, 0);
else
return;
接着同样再执行一系列的字符比较过程,并根据所得到的实际选项调用simple_strtoull子过程得到串行端口并将其赋值给变量port。
- if (options && (options[0] == ',')) /*判断首字符是否为','并转换后续的波特率*/
- baud = simple_strtoull(options + 1, &options, 0);
- else
- baud = probe_baud(port); /*反之则调用probe_baud子过程*/
if (options && (options[0] == ',')) /*判断首字符是否为','并转换后续的波特率*/
baud = simple_strtoull(options + 1, &options, 0);
else
baud = probe_baud(port); /*反之则调用probe_baud子过程*/
由上述注释可知,在一般情况下,if条件表达式中的语句都被判定为假,因此将执行probe_baud(port)函数,该函数同样定义于arch\x86\boot\Early_serial_console.c文件中:
- #define BASE_BAUD (1843200/16) /* BASE_BAUD=115200 */
- static unsigned int probe_baud(int port)
- {
- unsigned char lcr, dll, dlh;
- unsigned int quot;
- lcr = inb(port + LCR);
- outb(lcr | DLAB, port + LCR);
- dll = inb(port + DLL); /* 读取除数低位字节 */
- dlh = inb(port + DLH); /* 读取高位字节 */
- outb(lcr, port + LCR); /* 将LCR中的DLAB位清零 */
- quot = (dlh << 8) | dll;
- return BASE_BAUD / quot; /* 等价于 115200/divisor */
- }
#define BASE_BAUD (1843200/16) /* BASE_BAUD=115200 */
static unsigned int probe_baud(int port)
{
unsigned char lcr, dll, dlh;
unsigned int quot;
lcr = inb(port + LCR);
outb(lcr | DLAB, port + LCR);
dll = inb(port + DLL); /* 读取除数低位字节 */
dlh = inb(port + DLH); /* 读取高位字节 */
outb(lcr, port + LCR); /* 将LCR中的DLAB位清零 */
quot = (dlh << 8) | dll;
return BASE_BAUD / quot; /* 等价于 115200/divisor */
}
同样首先执行inb(port+LCR)指令读取LCR寄存器的值,随后将(lcr | DLAB)写入LCR寄存器,在不改变LCR寄存器中原有消息帧的设置的情形下将DLAB位置位,而此时并非设置波特率除数占用字,而是使用inb指令读取其中的值并将高位及低位字节写入dll与dlh变量,最后通过quot=(dlh << 8) | dll;表达式将quot设置为波特率除数占用字,并根据BaudRate = 115200/(Divisor Latch Value)公式返回当前设置的波特率。
- if (port)
- early_serial_init(port, baud);
if (port)
early_serial_init(port, baud);
接着根据所得到的串行通信端口及波特率再次设置相应的串行通信设施,而其中early_serial_init的剖析过程如前所示。
总的来讲,控制台初始化过程就是根据内核命令行中所填写的选项设置相应的串行通信设施,而这部分过程其实无非只做了如下几件事:①简单设置了一下帧格式;②禁用硬件中断并禁用FIFO缓冲区;③设置通信的速度——即波特率。我们在之前的通信设施的介绍中可以看到硬件电路所提供的功能相当复杂,但Linux其实并没有充分使用硬件所提供的功能,至少在控制台初始化过程中没有,至于后续是否将会步步完善,我们拭目以待。