从printXX看tty设备(5)串口终端

一、引言

在嵌入式系统中,串口几乎是系统操作的唯一途径,所以串口的使用在嵌入式系统中有着重要作用。因为嵌入式是一个爹不亲,娘不爱的苦命娃,它一般成本比较低,当它被制造出来之后,人们就希望它这么安安静静、平平安安、兢兢业业的运行一辈子。也就是这些设备和人(human being)的交互机会比较少,但是作为一个研发或者技术维护人员,如果这个嵌入式设备万一有个三长两短、伤风感冒的话,还是要关注一下。此时串口在嵌入式中就可以作为一个基本的配置,这样当设备真的出现问题的时候,就可以通过串口连接到设备上进行诊断。当然,网口也是可以的,知识网口对硬件和软件的要求都比串口高,而且总不能为了一个调试专门为不需要网络的设备添加一个网口和网络模块吧。

在我们现在的PC系统中,默认是有一个串口的。在windows系统中,我们通过“设备管理器”中可以看到有一项“端口(COM1和LPT)”,其中的COM1就是我们的串口。在windows的命令行中,通过 echo something > COM1是可以看到没有错误,但是执行 echo otherthing>COM2会提示错误

I:\Documents and Settings\tsecer>echo 10 > COM1

I:\Documents and Settings\tsecer>echo 10 > COM2
系统找不到指定的文件。

所以可以认为我的电脑上只有一个串口,也就是COM1代表的设备文件。但是我的Vmware的虚拟机上却有两个串口,也不知道是为啥

[tsecer@Harry ~]$ cat /proc/ioports

……

01f0-01f7 : ata_piix
02f8-02ff : serial
0376-0376 : 0000:00:07.1
  0376-0376 : ata_piix
0378-037a : parport0
03c0-03df : vga+
03f2-03f2 : floppy
03f4-03f5 : floppy
03f6-03f6 : 0000:00:07.1
  03f6-03f6 : ata_piix
03f7-03f7 : floppy
03f8-03ff : serial
二、linux下串口的初始化

在386个人PC及很多的嵌入式设备中,使用的串口都是由linux-2.6.21\drivers\serial\8250.c提供的功能,而这个功能的初始化则是由serial8250_init函数完成,这个函数具有__init属性,也就是do_initcalls函数遍历执行的这些入口函数。从该文件的代码可以看到,这个serial8250也是一个非常常用的控制台,可能PC中是VT的天下,而嵌入式中则是这个8250的地盘吧。因为这个控制台设备的初始化和vt的初始化一样,是放在console_initcall宏中做特殊处理的,也就是会在linux-2.6.21\drivers\char\tty_io.c:console_init中遍历到,为了类比,列出两个文件的位置

static int __init serial8250_console_init(void)
{
 serial8250_isa_init_ports();
 register_console(&serial8250_console);
 return 0;
}
console_initcall(
serial8250_console_init);

console_initcall(con_init);
在serial8250_console_init-->>serial8250_isa_init_ports

for (i = 0, up = serial8250_ports;
      i < ARRAY_SIZE(old_serial_port) && i < nr_uarts;
      i++, up++) {

这里使用的old_serial_port变量就是在该文件中定义的数组变量,

static const struct old_serial_port old_serial_port[] = {
 SERIAL_PORT_DFNS /* defined in asm/serial.h */
};

这个变量中内容的声明位于linux-2.6.21\include\asm-i386\serial.h

#define SERIAL_PORT_DFNS   \
 /* UART CLK   PORT IRQ     FLAGS        */   \
 { 0, BASE_BAUD, 0x3F8, 4, STD_COM_FLAGS }, /* ttyS0 */ \
 { 0, BASE_BAUD, 0x2F8, 3, STD_COM_FLAGS }, /* ttyS1 */ \
 { 0, BASE_BAUD, 0x3E8, 4, STD_COM_FLAGS }, /* ttyS2 */ \
 { 0, BASE_BAUD, 0x2E8, 3, STD_COM4_FLAGS }, /* ttyS3 */

也就是PC中最多可以配置4个串口,由于看起来有些中断是共享的,但是大部分的电脑上只是使用了一个或者两个。进一步说,无论从vmware还是从qemu上看,其中都没有出现过4个串口。在386的默认配置文件linux-2.6.21\arch\i386\defconfig中,其中定义了CONFIG_SERIAL_8250_NR_UARTS=4,也就是默认都是有4个配置的,这也就是说,在内核的启动过程中,串口自动完成了对串口物理存在性德检测工作,只有真正存在的串口设备才会被注册到系统中。这个检测是在哪里完成的呢?

话不多说,同样是上最为直观的调用链

(gdb) bt
#0  autoconfig (up=0xd0, probeflags=3487795700) at drivers/serial/8250.c:900
#1  0xc0406f27 in serial8250_config_port (port=0xc0ac24e0, flags=1)
    at drivers/serial/8250.c:2224
#2  0xc0402bc7 in uart_configure_port (drv=0xc09a1bc0, state=0xcffd94e0, 
    port=0xc0ac24e0) at drivers/serial/serial_core.c:2102
#3  0xc040317a in uart_add_one_port (drv=0xc09a1bc0, port=0xc0ac24e0)
    at drivers/serial/serial_core.c:2294
#4  0xc0a2ea42 in serial8250_register_ports (drv=0xc09a1bc0, dev=0xcf93ca08)
    at drivers/serial/8250.c:2332
#5  0xc0a2ed9d in serial8250_init () at drivers/serial/8250.c:2765
#6  0xc09fa34d in do_initcalls () at init/main.c:672
#7  0xc09fa474 in do_basic_setup () at init/main.c:712
#8  0xc09fa502 in init (unused=0x0) at init/main.c:803
#9  0xc01086d3 in ?? ()
在autoconfig函数中,其中对静态配置的串口进行了简单的检测,从而判断物理串口是否真正存在,判断的代码位于

if (!(up->port.flags & UPF_BUGGY_UART)) {
  /*
   * Do a simple existence test first; if we fail this,
   * there's no point trying anything else.

……

if (scratch2 != 0 || scratch3 != 0x0F) {
   /*
    * We failed; there's nothing here
    */
   DEBUG_AUTOCONF("IER test failed (%02x, %02x) ",
           scratch2, scratch3);
   goto out;
  }

不存在的串口将无法通过这个简单校验,从而在这个位置返回。同时,跳过初始化也就意味着这个up(Uart Port)的up->port.type 成员无法初始化,用助记符来说,它的值就是

#define PORT_UNKNOWN 0
从而它不具备被系统识别的资格,相当于被剥夺了“串口权”。

三、串口中断的由来

为了让qemu使用uart作为控制台,我们在qemu的启动命令的最后添加这样的命令 console=ttyS0,从而执行串口的配置流程

(gdb) bt
#0  request_irq (irq=4, handler=0xc0405317 <serial8250_interrupt>, irqflags=0, 
    devname=0xc0887cce "serial", dev_id=0xc0ac1e00) at kernel/irq/manage.c:516
#1  0xc0405692 in serial_link_irq_chain (up=0xc0ac24e0)
    at drivers/serial/8250.c:1452
#2  0xc0406021 in serial8250_startup (port=0xc0ac24e0)
    at drivers/serial/8250.c:1752
#3  0xc03fe3b4 in uart_startup (state=0xcffd94e0, init_hw=0)
    at drivers/serial/serial_core.c:179
#4  0xc0401c17 in uart_open (tty=0xcf973800, filp=0xcfe296e0)
    at drivers/serial/serial_core.c:1622
#5  0xc03d207e in tty_open (inode=0xc12da2ec, filp=0xcfe296e0)
    at drivers/char/tty_io.c:2577
#6  0xc01c3bce in chrdev_open (inode=0xc12da2ec, filp=0xcfe296e0)
    at fs/char_dev.c:399
#7  0xc01bdc46 in __dentry_open (dentry=0xc12d8d10, mnt=0xc126c7a0, flags=2, 
    f=0xcfe296e0, open=0xc01c3967 <chrdev_open>) at fs/open.c:700
#8  0xc01bdf99 in nameidata_to_filp (nd=0xcfe8ff00, flags=2) at fs/open.c:826
#9  0xc01bde0a in do_filp_open (dfd=-100, filename=0xcfea6000 "/dev/console", 
    flags=2, mode=0) at fs/open.c:761
#10 0xc01be324 in do_sys_open (dfd=-100, filename=0xc084b3b9 "/dev/console", 
    flags=2, mode=0) at fs/open.c:962
#11 0xc01be41f in sys_open (filename=0xc084b3b9 "/dev/console", flags=2, 
    mode=0) at fs/open.c:983
---Type <return> to continue, or q <return> to quit---
#12 0xc010012b in init_post () at init/main.c:744
#13 0xc09fa542 in init (unused=0x0) at init/main.c:823
#14 0xc01086d3 in ?? ()
这里注册了串口的中断处理程序,,现在其实我们关心的是串口的数据来源,因为对于串口数据的发送,我想应该是比较直观的,通过tty_dirver->write接口直接进行写入就可以了。但是对于tty的读出接口,可能不是很直观,因为从tty_read----->>>read_chan中可以看到,其中只是简单的从自己的

c = tty->read_buf[tty->read_tail];

中来取数据,明显的,这个读取时相对于系统用户来说的,也就是用户只是从这个地方来取数据,但是这些数据是由谁放入的呢?此时就要看中断对数据的接受处理了。

四、串口数据的接收

由于我没有从HOST连接虚拟机的串口,所以我无法调试这个中断,那就只有走查一下这个代码的流程了,大致的调用关系为

serial8250_interrupt--->>>>serial8250_handle_port--->>>receive_chars--->>>uart_insert_char--->>>tty_insert_flip_char--->>>tty_insert_flip_string_flags

int tty_insert_flip_string_flags(struct tty_struct *tty,
  const unsigned char *chars, const char *flags, size_t size)
{
 int copied = 0;
 do {
  int space = tty_buffer_request_room(tty, size - copied);
  struct tty_buffer *tb = tty->buf.tail;
  /* If there is no space then tb may be NULL */
  if(unlikely(space == 0))
   break;
  memcpy(tb->char_buf_ptr + tb->used, chars, space);
  memcpy(tb->flag_buf_ptr + tb->used, flags, space);
  tb->used += space;
  copied += space;
  chars += space;
  flags += space;
  /* There is a small chance that we need to split the data over
     several buffers. If this is the case we must loop */
 } while (unlikely(size > copied));
 return copied;
}
receive_chars---->>>tty_flip_buffer_push--->>.schedule_delayed_work(&tty->buf.work, 1);

走入这个流程之后,也就开始了其实和键盘相同的处理流程,也就是执行

 INIT_DELAYED_WORK(&tty->buf.work, flush_to_ldisc);
中设置的flush_to_ldisc函数,其中的流程为

flush_to_ldisc--->>>>disc->receive_buf(tty, char_buf, flag_buf, count)--->>>n_tty_receive_buf--->>>n_tty_receive_char

handle_newline:
   spin_lock_irqsave(&tty->read_lock, flags);
   set_bit(tty->read_head, tty->read_flags);
   put_tty_queue_nolock(c, tty);
   tty->canon_head = tty->read_head;
   tty->canon_data++;
   spin_unlock_irqrestore(&tty->read_lock, flags);
   kill_fasync(&tty->fasync, SIGIO, POLL_IN);
   if (waitqueue_active(&tty->read_wait))
    wake_up_interruptible(&tty->read_wait);

在正则模式下,这里进行一次唤醒。

或者非正则模式下,则根据设置唤醒

n_tty_receive_buf

 if (!tty->icanon && (tty->read_cnt >= tty->minimum_to_wake)) {
  kill_fasync(&tty->fasync, SIGIO, POLL_IN);
  if (waitqueue_active(&tty->read_wait))
   wake_up_interruptible(&tty->read_wait);
 }
五、总结

这里有一个相对比较容易混淆的概念:

用户态的/dev/console文件和用户态的struct console结构并不是等同的。用户态的/dev/console是在linux-2.6.21\drivers\char\tty_io.c:tty_init中注册的一个设备

cdev_init(&console_cdev, &console_fops);
 if (cdev_add(&console_cdev, MKDEV(TTYAUX_MAJOR, 1), 1) ||
     register_chrdev_region(MKDEV(TTYAUX_MAJOR, 1), 1, "/dev/console") < 0)
  panic("Couldn't register /dev/console driver\n");

所以它有自己的打开(open)、写入(write)、读出(read)接口实现,而内核的struct console 大部分只是实现了write接口,,或者说内核只要求struct cosnole 实现write接口。然后这个内核的console是在cosole_fops中的tty_open中和内核的console发生联系的,然后使用内核的struc console->device()接口的tty_driver设备来进行具体设备的操作。例如vt_console的实现为vt_console_device(),而8250串口注册的则是uart_console_device()接口。

但是同样的,内核也不要求这个device返回的tty_driver实现read接口,因为对于/dev/console的read接口实现是通过tty_read来实现的,根据前面的解释,这个的读入数据可以不可见驱动的读取,而是由中断来向读取内容中放数据。串口的就是刚才看到的中断,而对于VT控制台则是键盘的中断放数据

串口虽然简单,但是它的工作模式和更为复杂的网口的工作模式本质上是相同的,只是说网口中每次发送的数据量更大,中断的触发也不再是以单个字符(或者更高级的支持FIFO的可以一次中断接受更多个字符)为单位,而是以一个帧为单位,这样的传送效率将会更高,但是明显地,也会带来更多的校验和复杂的结构处理问题。

不论如何,这个简单的串口可以作为网口编程理解的一个模型,它的范式对于网口的编程是比较有借鉴和参考价值的。之后的博客可能会分析网口的流程,所以这里可以作为一个预热。

posted on 2019-03-06 20:46  tsecer  阅读(634)  评论(0编辑  收藏  举报

导航