5.裝置中斷

INnoVation-V2發表於2024-04-21

一.設定中斷

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;
  }
}

相關文章