从printXX看tty设备(2)VGA显示模拟

一、虚拟终端模拟的问题

前面曾经说过,所谓控制台是对tty设备的一种模拟。tty和主机之间就一根线,所有的交互都在这条串行线上一个bit一个bit的交互,可以看做是“竹筒倒豆子”--直来直去的模式。进一步说,主机不能(也没有义务)直接控制tty设备上的显示设备(比如显示设备对应的内存、显示控制寄存器等坐落于终端上等组件),虽然主机可以控制自己一端tty设备的数据发送和接收。

现在使用显卡来模拟一个终端,此时为了兼容之前的功能,当我们模拟一个终端的时候,主机要在自己的本地显示器上显示出指定的效果,比如说高亮一些字符,移动光标,滚屏的操作。一个用户态的shell使用的还是终端的协议,就是向一个串口中发送bit流。但是对于主机上的显卡来说,它并不是一个真正的终端命令解释器,甚至可以看到,在VGA显卡中,如果要在屏幕上高亮一个字符,是需要设置这个字符的属性byte。这里的编程模式和tty设备的模式有截然不同的接口和实现,用户需要且只需要将这个属性byte和ascii值写入内存中的指定区域,从而由显示器来自动的显示出来,这个内存区就是PC中著名的“BIOS空洞”。对应于qemu模拟的设备,其地址从0xb8000开始,到0xc0000结束,这么长的地址作为显卡内存。当我们需要显示器显示某个ASCII字符的时候,就向这片内存中写入该字符对应的ASCII码的值,显卡会根据自己ROM中的字模将这个字符显示到显示器上。

总之,当使用显卡模拟终端的时候,需要内核将终端协议转换为显示器内存操作指令,从而相当于将终端的解析和显示功能放在了自己的显卡上来完成。

另外一个问题就是输入的问题,当使用真正的终端的时候,用户的输入来自串口,使用PC模拟终端的时候,此时系统一般只有一个键盘,此时键盘消息需要发送给串口的读入者,从而实现和使用者的交互,这个内核同样需要考虑。

二、显示系统的初始化

linux-2.6.21\arch\i386\kernel\setup.c:setup_arch()#ifdef CONFIG_VT
#if defined(CONFIG_VGA_CONSOLE)
 if (!efi_enabled || (efi_mem_type(0xa0000) != EFI_CONVENTIONAL_MEMORY))
  conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
 conswitchp = &dummy_con;
#endif
#endif

此处初始化了一个全局变量,也就是conswitchp指针,这个指针就是指向了控制台实现(内核成为控制台切换 console switch,因为系统的控制台可以在运行时变化)。当该变量初始化之后,在con_init函数中将会调用者这里注册的指针中对应的start_up实现:

 if (conswitchp)
  display_desc = conswitchp->con_startup();

反过来看上面注册的vga_con中con_startup指针指向的为

static const char *vgacon_startup(void),对于qemu的运行中,此处走的流程为

 } else {
  /* If not, it is color. */
  vga_can_do_color = 1;
  vga_vram_base = 0xb8000;
  vga_video_port_reg = VGA_CRT_IC;
  vga_video_port_val = VGA_CRT_DC;
  if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {
   int i;

   vga_vram_size = 0x8000;这两个大小很重要,将会在这个文件中共享,该文件中很多函数会引用这文件静态变量。

这里设置了两个重要的全局变量,一个是vga内存的起始物理地址,一个是这个显卡区的大小,分别为0xb8000和0x8000,刚好到0xc0000结束。由于这里所说的地址都是物理地址,而内核明显都是使用逻辑地址的,所以同样要把这个物理地址转换为逻辑地址,所以在该函数中有一个转换操作

 vga_vram_base = VGA_MAP_MEM(vga_vram_base, vga_vram_size);这个转换起始比较简单,直接是物理地址加上0xc0000000.
 vga_vram_end = vga_vram_base + vga_vram_size;

例如,在初始化vc结构中显卡地址的时候,将会执行下面的代码

static int vgacon_set_origin(struct vc_data *c)
{
 if (vga_is_gfx || /* We don't play origin tricks in graphic modes */
     (console_blanked && !vga_palette_blanked)) /* Nor we write to blanked screens */
  return 0;
 c->vc_origin = c->vc_visible_origin = vga_vram_base;
 vga_set_mem_top(c);
 vga_rolled_over = 0;
 return 1;
}

三、串口协议模拟

当我们通过printf向标准输出中打印一个字符串的时候,经过的调用连为

#0  do_con_trol (tty=0x296, vc=0xcf986054, c=658) at drivers/char/vt.c:1546
#1  0xc03ea1dd in do_con_write (tty=0xcf986000, 
    buf=0xcfe15df1 "\b\234", <incomplete sequence \317>, count=0) at drivers/char/vt.c:2135
#2  0xc03eab1c in con_put_char (tty=0xcf986000, ch=13 '\r') at drivers/char/vt.c:2449
#3  0xc03d50de in opost (c=10 '\n', tty=0xcf986000) at drivers/char/n_tty.c:277
#4  0xc03d8e75 in write_chan (tty=0xcf986000, file=0xcfea7a80, 
    buf=0xcf9c0800 "\nPlease press Enter to activate this console. ", nr=46)
    at drivers/char/n_tty.c:1468
#5  0xc03d0485 in do_tty_write (count=46, 
    buf=0x81bd854 "\nPlease press Enter to activate this console. ", file=0xcfea7a80, 
    tty=0xcf986000, write=0xc03d8bec <write_chan>) at drivers/char/tty_io.c:1746
#6  tty_write (count=46, buf=0x81bd854 "\nPlease press Enter to activate this console. ", 
    file=0xcfea7a80, tty=0xcf986000, write=0xc03d8bec <write_chan>)
    at drivers/char/tty_io.c:1806
#7  0xc01bf5cd in vfs_write (file=0xcfea7a80, 
    buf=0x81bd854 "\nPlease press Enter to activate this console. ", count=46, pos=0xcfe15f84)
    at fs/read_write.c:330
#8  0xc01bf7d1 in sys_write (fd=1, 
    buf=0x81bd854 "\nPlease press Enter to activate this console. ", count=46)
    at fs/read_write.c:383

模拟一下对于 \e[34;41m这个序列的内核解析过程:

do_con_trol中

case 27:
  vc->vc_state = ESesc; 这只这个控制台当前状态为ESesc。
  return;
……

switch(vc->vc_state) {
 case ESesc:
  vc->vc_state = ESnormal;
  switch (c) {
  case '[':
   vc->vc_state = ESsquare;
   return;

……

case ESsquare:
  for (vc->vc_npar = 0; vc->vc_npar < NPAR; vc->vc_npar++)
   vc->vc_par[vc->vc_npar] = 0;
  vc->vc_npar = 0;
  vc->vc_state = ESgetpars;
  if (c == '[') { /* Function key */
   vc->vc_state=ESfunckey;
   return;
  }
  vc->vc_ques = (c == '?');
  if (vc->vc_ques)
   return;注意:这里并没有返回,根据case的规则,没有break将会继续执行,所以将会执行到接下来的ESgetpars序列
 case ESgetpars:
  if (c == ';' && vc->vc_npar < NPAR - 1) {这里通过分号来区分不同的参数
   vc->vc_npar++;
   return;
  } else if (c>='0' && c<='9') {
   vc->vc_par[vc->vc_npar] *= 10;
   vc->vc_par[vc->vc_npar] += c - '0';均为十进制数,不识别十六进制数。
   return;
  } else
   vc->vc_state = ESgotpars; 这里同样没有返回,继续执行接下来的ESgotpars分支。

case ESgotpars:
  vc->vc_state = ESnormal;
  switch(c) {

……

case 'm':  
   if (vc->vc_ques) {注意:这里我们来说,这个条件并不满足,这个vc_ques是在前面遇到'?'的时候设置的,由于没有这个字符,所以这里是不满足的,不会走这个分支
    clear_selection();
    if (vc->vc_par[0])
     vc->vc_complement_mask = vc->vc_par[0] << 8 | vc->vc_par[1];这里对应的是查询标志
    else
     vc->vc_complement_mask = vc->vc_s_complement_mask;
    return;
   }
   break;这个break将会跳转到下面的位置

……

if (vc->vc_ques) {
   vc->vc_ques = 0;
   return;
  }
  switch(c) {

……

case 'm':
   csi_m(vc);
   return;

在sci_m中

default:
    if (vc->vc_par[i] >= 30 && vc->vc_par[i] <= 37)可以看到,30--37作为前台颜色,
     vc->vc_color = color_table[vc->vc_par[i] - 30]
      | (vc->vc_color & 0xf0);
    else if (vc->vc_par[i] >= 40 && vc->vc_par[i] <= 47)40--47作为后台背景颜色,然后设置到字面的属性中。
     vc->vc_color = (color_table[vc->vc_par[i] - 40] << 4)
      | (vc->vc_color & 0x0f);
    break;
  }
 update_attr(vc);设置入属性成员中。

static void update_attr(struct vc_data *vc)
{
 vc->vc_attr = build_attr(vc, vc->vc_color, vc->vc_intensity, vc->vc_blink, vc->vc_underline, vc->vc_reverse ^ vc->vc_decscnm);
 vc->vc_video_erase_char = (build_attr(vc, vc->vc_color, 1, vc->vc_blink, 0, vc->vc_decscnm) << 8) | ' ';
}

当显示一个字符的时候,在static int do_con_write(struct tty_struct *tty, const unsigned char *buf, int count)中将会向制定位置显示字符,这里的写入操作是通过scr_writew来实现的,从这里可以看到,其中有对vc->vc_attr的使用。从这里我们可以看到的是,对于显存,每个字符本身占用一个字节的ASCII码,然后紧邻的一个byte是这个字符的属性标志。

   scr_writew(himask ?
         ((vc->vc_attr << 8) & ~himask) + ((tc & 0x100) ? himask : 0) + (tc & 0xff) :
         (vc->vc_attr << 8) + tc,
       (u16 *) vc->vc_pos);

最后看一下这个scr_writew的实现

#define scr_writew(val, addr) (*(addr) = (val))


由于前面调用的时候强制转换为了 u16*类型,所以这里的复制是一个short类型的赋值。结合前面的显卡初始化方法就可以知道,当前的VGA显卡显示的时候是将真正希望显示的ASCII码和对应的属性直接写入内存来显示的。

四、显卡编程的一个基础

看来intel是比较喜欢这样的一个硬件编程模型:使用两个寄存器,一个是地址寄存器,专门用来写地址,或者说用来作为寄存器选择寄存器,然后另一个地址作为数据寄存器。编程时首先向地址选择寄存器中写入将要操作的寄存器,然后从另一个数据寄存器中读出这个值。这一点在intel的IOAPIC和PCI系列中均有体现。大家可以理解为C中的指针就好了,虽然这里有点绕。

在VGA中,这两个寄存器分别为

/* VGA index register ports */
#define VGA_CRT_IC   0x3D4 /* CRT Controller Index - color emulation */

/* VGA data register ports */
#define VGA_CRT_DC   0x3D5 /* CRT Controller Data Register - color emulation */
例如

static inline void vga_set_mem_top(struct vc_data *c)
{
 write_vga(12, (c->vc_visible_origin - vga_vram_base) / 2);
}

这里还没有涉及VT的另一个重要部分,就是和显示对应的就是输入,也就是PC的键盘处理,对应的就是tty的read接口,在接下来一篇中讨论。

猜你喜欢

转载自www.cnblogs.com/tsecer/p/10485876.html