5.设备中断

一.设置中断

1.start.c\start()

// 默认所有trap在机器模式下处理
// 这里将trap的处理托管给Supervisor mode
w_medeleg(0xffff);
w_mideleg(0xffff);
// 设置SIE寄存器接收外部,软件和定时器中断,
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

// 初始化定时器
timerinit();

2.main.c\main()

void main(){
  if(cpuid() == 0){
    consoleinit();    
    ...
    plicinit();      // set up interrupt controller
    plicinithart();  // ask PLIC for device interrupts
    ...
  } else {
    ...
    plicinithart();   // ask PLIC for device interrupts
  }
  scheduler();        
}

3.console.c\consoleinit()

void consoleinit(void)
{
  initlock(&cons.lock, "cons");
	
  uartinit();

  // connect read and write system calls
  // to consoleread and consolewrite.
  devsw[CONSOLE].read = consoleread;
  devsw[CONSOLE].write = consolewrite;
}

4.uart.c\uartinit()

运行完这个函数,UART就可以产生中断了。但是因为还没有对PLIC编程,所以中断不能被CPU感知, PLIC的初始化函数plicinit()main()中被调用

//初始化UART
void uartinit(void)
{
  // 关闭中断
  WriteReg(IER, 0x00);
  
  // 设置波特率
  WriteReg(LCR, LCR_BAUD_LATCH);
  WriteReg(0, 0x03);
  WriteReg(1, 0x00);
  // 设置字符长度为8bit
  WriteReg(LCR, LCR_EIGHT_BITS);

  // 重置并打开FIFO
  WriteReg(FCR, FCR_FIFO_ENABLE | FCR_FIFO_CLEAR);
  // 重新打开中断
  WriteReg(IER, IER_TX_ENABLE | IER_RX_ENABLE);
  
  initlock(&uart_tx_lock, "uart");
}

5.plic.c\plicinit()

PLIC与外设一样,也占用了一个I/O地址0xC000_0000

void plicinit(void)
{
  //启动UART的中断,设置好PLIC接收哪些中断,将中断路由到CPU
  *(uint32*)(PLIC + UART0_IRQ*4) = 1;
  //设置PLIC接收来自I/O磁盘的中断,这节课不会介绍这部分内容。
  *(uint32*)(PLIC + VIRTIO0_IRQ*4) = 1;
}

6.plic.c\plicinithart()

plicinit()只由0号CPU运行,但是每个CPU核都需要运行plicinithart(),表明自己可响应哪些外设中断。

void plicinithart(void)
{
  int hart = cpuid();
	
  //每个CPU核都表明对来自UART和VIRTIO的中断感兴趣
  *(uint32*)PLIC_SENABLE(hart) = (1 << UART0_IRQ) | (1 << VIRTIO0_IRQ);

 	//因为我们忽略中断的优先级,所以将优先级设置为0
  *(uint32*)PLIC_SPRIORITY(hart) = 0;
}

到目前为止,有了生成中断的外部设备,有了PLIC可以传递中断到单个CPU。但是CPU本身还不能接收中断,因为没有设置SSTATUS寄存器。在main函数的最后,程序调用了scheduler()函数

7.riscv.h\intr_on()

scheduler()中每个进程都会执行intr_on(),这个函数设置sstatus接收中断

static inline void intr_on(){
  w_sstatus(r_sstatus() | SSTATUS_SIE);
}

到这里,中断被完全打开了。如果PLIC正好有pending的中断,那么会有CPU核收到中断

二.UART驱动的top部分

以打印$到Console为例

1.init.c\main()

这是系统启动后运行的第一个进程

int
main(void)
{
  int pid, wpid;

  if(open("console", O_RDWR) < 0){
    //创建console设备文件
    mknod("console", CONSOLE, 0);
    open("console", O_RDWR);
  }
  
  //dup(): 创建一个文件描述符,和给定fd指向同一个文件
  //通过dup创建stdout和stderr
  //复制文件描述符0,得到了另外两个文件描述符1,2
  //最终文件描述符0,1,2都指向Console。
  dup(0);  // stdout
  dup(0);  // stderr

  for(;;){
    ...
    if(pid == 0){
      exec("sh", argv);
      printf("init: exec sh failed\n");
      exit(1);
    }
    ...
  }
}

2. sh.c

Shell程序首先打开文件描述符0,1,2。之后向文件描述符2写入**$ ** 。

int getcmd(char *buf, int nbuf){
  // write(int fd, char *buf, int n)
  // 系统调用,将buf中的n byte写到fd中,返回n
  write(2, "$ ", 2);
  ...
}

3.sysfile.c\sys_write()

uint64 sys_write(void){
  struct file *f;
  int n;
  uint64 p;
  
  argaddr(1, &p);
  argint(2, &n);
  if(argfd(0, 0, &f) < 0)
    return -1;

  return filewrite(f, p, n);
}

4.file.c\filewrite()

int filewrite(struct file *f, uint64 addr, int n) {
  int r, ret = 0;

  if(f->writable == 0)
    return -1;

  if(f->type == FD_PIPE){
    ret = pipewrite(f->pipe, addr, n);
  } else if(f->type == FD_DEVICE){
    // mknod生成的文件描述符属于设备(FD_DEVICE)
    if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
      return -1;
    // 根据设备类型调用不同的write()函数
    ret = devsw[f->major].write(1, addr, n);
  } else if(f->type == FD_INODE){
    ...
  return ret;
}

我们现在的设备是Console,所以这里会调用console.c中的consolewrite函数

5.console.c\consolewrite()

consolewrite()是一个UART驱动的top部分

uart.c\uartputc()函数完成实际打印字符操作。

// user_src 指明地址是否来自用户空间
int consolewrite(int user_src, uint64 src, int n)
{
  int i;

  for(i = 0; i < n; i++)
  {
    char c;
    // 通过either_copyin将字符拷入,每次拷入一个字符
    if(either_copyin(&c, user_src, src+i, 1) == -1)
      break;
    // uartputc()将字符写入给UART设备
    uartputc(c);
  }
  return i;
}

6. proc.c\either_copyin()

int either_copyin(void *dst, int user_src, uint64 src, uint64 len)
{
  struct proc *p = myproc();
  if(user_src){
    // 来自用户空间的地址需要使用用户页表拷贝
    return copyin(p->pagetable, dst, src, len);
  } else {
    memmove(dst, (char*)src, len);
    return 0;
  }
}

7.uart.c\uartputc()

#define UART_TX_BUF_SIZE 32
// uart_tx_buf是一个大小为32的环形队列
char uart_tx_buf[UART_TX_BUF_SIZE];
uint64 uart_tx_w; // 写指针
uint64 uart_tx_r; // 读指针

void uartputc(int c)
{
  acquire(&uart_tx_lock);

  if(panicked){
    for(;;) ;
  }
  //判断环形队列是否满
  while(uart_tx_w == uart_tx_r + UART_TX_BUF_SIZE){
    // 满了就sleep一会
    // wait for uartstart() to open up space in the buffer.
    sleep(&uart_tx_r, &uart_tx_lock);
  }
  uart_tx_buf[uart_tx_w % UART_TX_BUF_SIZE] = c;
  uart_tx_w += 1;
  uartstart();
  release(&uart_tx_lock);
}

8. uart.c\uartstart()

将数据写入到THR(Transmission Holding Register)发送寄存器。相当于告诉设备,有一个字节需要发送。一旦数据送到了设备,系统调用会返回,用户应用程序Shell继续执行。这里从内核返回到用户空间的机制与lec06的trap机制是一样的。

#define THR 0 //transmit holding register(for output bytes)
void uartstart()
{
  while(1)
  {
    // buffer is empty.
    if(uart_tx_w == uart_tx_r) return;
    // IDLE状态就返回
    if((ReadReg(LSR) & LSR_TX_IDLE) == 0) return;
    
    int c = uart_tx_buf[uart_tx_r % UART_TX_BUF_SIZE];
    uart_tx_r += 1;
    // maybe uartputc() is waiting for space in the buffer.
    wakeup(&uart_tx_r);
    
    WriteReg(THR, c);
  }
}

与此同时,UART设备会将数据送出。在某个时间点,我们会收到中断,因为我们之前设置了要处理UART设备中断。接下来我们看一下,当发生中断时的实际处理流程

三.UART驱动的bottom部分

bottom部分就是来自UART的中断

1.trap.c\usertrap()

void usertrap(void)
{
  ...
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
...
}

2.trap.c\devintr()

// check if it's an external interrupt or
// software interrupt,and handle it. 
// returns 2 if timer interrupt,
// 1 if other device,
// 0 if not recognized.
int devintr()
{
  uint64 scause = r_scause();

  //通过SCAUSE寄存器判断当前中断是否是外设中断
  if((scause & 0x8000000000000000L) &&
     (scause & 0xff) == 9){
    // this is a supervisor external interrupt, via PLIC.

    // 获取中断, irq指明中断类型
    int irq = plic_claim();

    // UART中断
    if(irq == UART0_IRQ){
      uartintr();
    } else if(irq == VIRTIO0_IRQ){
      virtio_disk_intr();
    } else if(irq){
      printf("unexpected interrupt irq=%d\n", irq);
    }
...
}

3.plic.c\plic_claim()

// ask the PLIC what interrupt we should serve.
// 获取并返回中断号,UART的中断号是10。
int plic_claim(void)
{
  int hart = cpuid();
  int irq = *(uint32*)PLIC_SCLAIM(hart);
  return irq;
}

4.uart.c\uartintr()

我们现在讨论的是向UART发送数据,还没有通过键盘输入任何数据,所以UART的接受寄存器现在为空,uartgetc()返回-1,代码运行到uartstart(),之前已经分析过

void uartintr(void)
{
  // read and process incoming characters.
  while(1){
    int c = uartgetc();
    if(c == -1) break;
    consoleintr(c);
  }

  // send buffered characters.
  acquire(&uart_tx_lock);
  uartstart();
  release(&uart_tx_lock);
}

5.uart.c\uartgetc()

int uartgetc(void)
{
  if(ReadReg(LSR) & 0x01){
    // input data is ready.
    return ReadReg(RHR);
  } else {
    return -1;
  }
}
posted @ 2024-04-21 00:18  INnoVation-V2  阅读(8)  评论(0编辑  收藏  举报