Lab3: Question and Elective

Question 1: Change svc instruction to brk (breakpoint) instruction. See the difference in ELR_EL2(return address). Explain why there is a difference.

  • asm volatile("svc #1");

    1
    2
    3
    4
    Type: 0
    Exception return address 0x81FDC
    Exception class (EC) 0x15 (0b010101)
    Instruction specific synfrome (ISS) 0x1
  • asm volatile("brk #1");

    1
    2
    3
    4
    Type: 0
    Exception return address 0x81FD8
    Exception class (EC) 0x3C (0b111100)
    Instruction specific synfrome (ISS) 0x1

svc 是返回 caller 的下一行指令,brk 是停留在原地

Question 2: Do you need to save floating point SIMD registers in ISRs? Why or why not.

start.S 內我們操作 CPACR_EL1 允許在 EL0 及 EL1 進行操作 floating pointer 及 SIMD register 而不被 trap,在核心內為了效能不使用 floating point 及 SIMD。後續會實作 process 的 content switch,理論上進入 ISR 應該存下這些 register 防止其他 process 改動到。

1
2
3
// Do not cause trap when using Advanced SIMD and floating point register on EL0 & EL1
ldr x1, =CPACR_EL1_VALUE
msr CPACR_EL1, x1

Question 3: What will happen if you don’t clear peripherals’ interrupt signal?

中斷會一直觸發

Elective 1: Pick another timer and implement its handler.

BCM2837 為 4 顆 ARMv8-A 架構的 Cortex-A53 組成,每個 core 有 4 個 timer 可用

位置 名稱 理論上 qemu 可模擬 實際上 qemu 6.2.0可模擬 raspibery pi3b+ pending register
GPU peripheral system timer IRQ_PENDING_1
GPU peripheral arm timer IRQ_BASIC_PENDING
local peripheral local timer COREn_IRQ_SRC
local peripheral core timer COREn_IRQ_SRC

Interrupt handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void irq_handler(unsigned long esr, unsigned long elr)
{
uint32_t basic_irq, local_irq;

#if _ARM_TIMER == 1
basic_irq = *IRQ_BASIC_PENDING;
if (ARM_TIMER_IRQ_0 & basic_irq) {
arm_timer_hanler();
}
#endif

do {
local_irq = *CORE0_IRQ_SRC;
if (local_irq) {
switch (1U << __builtin_ctz(local_irq)) {
#if (_LOCAL_TIMER == 1 || _CORE_TIMER == 1)
case CNTPNSIRQ:
core_timer_handler();
break;
case LOCAL_TIMER_IRQ:
local_timer_handler();
break;
#endif
case GPU_IRQ:
gpu_irq_handler();
break;
default:
}
}
} while (local_irq);
}

void gpu_irq_handler()
{
uint32_t gpu_irq1, gpu_irq2;
do {
gpu_irq1 = *IRQ_PENDING_1;
gpu_irq2 = *IRQ_PENDING_2;
if (gpu_irq1) {
switch (1U << __builtin_ctz(gpu_irq1)) {
#if _SYS_TIMER == 1
case SYSTEM_TIMER_IRQ_1:
sys_timer_handler();
break;
#endif
default:
}
}
if (gpu_irq2) {
switch (1U << __builtin_ctz(gpu_irq2)) {
case UART_IRQ:
uart_handler();
break;
default:
}
}
} while (gpu_irq1 || gpu_irq2);
}

Core timer

Reference: CNTP_CTL_EL0, CNTP_TVAL_EL0, Counter-timer Physical Timer TimerValue register

On a write of this register, CNTP_CVAL_EL0 is set to (CNTPCT_EL0 + TimerValue), where TimerValue is treated as a signed 32-bit integer.

the timer condition is met when (CNTPCT_EL0 - CNTP_CVAL_EL0) is greater than or equal to zero.

If CNTP_CTL_EL0.IMASK is 0, an interrupt is generated.

[CNTP_CTL_EL0](https://developer.arm.com/documentation/ddi0595/2020-12/AArch64-Registers/CNTP-CTL-EL0--Counter-timer-Physical-Timer-Control-register?lang=en).ISTATUS ****is set to 1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

void core_timer_enable()
{
register uint32_t enable = 1, expired_period = EXPIRE_PERIOD >> 4;
// enable timer
asm volatile("msr cntp_ctl_el0, %0" ::"r"(enable));
// set expired time
asm volatile("msr cntp_tval_el0, %0" : : "r"(expired_period));
// enable timer interrupt
*CORE0_TIMER_IRQ_CTRL |= 0x2;
}

void core_timer_disable()
{
register uint32_t enable = 0;
// disable timer
asm volatile("msr cntp_ctl_el0, %0" ::"r"(enable));
// disable timer interrupt
*CORE0_TIMER_IRQ_CTRL &= !0x2;
}

void core_timer_handler()
{
register uint32_t expired_period = EXPIRE_PERIOD;
// set expired time
asm volatile("msr cntp_tval_el0, %0" : : "r"(expired_period));
uart_printf("core timer jiffies: %d\n", core_timer_jiffies++);
}

Elective 2: Implement ISR for either mini UART or PL011 UART.

對照下方資料可得到完整 PL011 使用資訊

UARTRXINTR

receive interrupt 觸發的條件為任一下列事件

  1. FIFO 啟用,receive FIFO 達到 trigger level,直到清除中斷或是讀取資料到低於 trigger level。
  2. FIFO 未啟用,一接收到資料馬上觸發中斷,直到清除中斷或是讀取資料。

UARTTXINTR

transmit interrupt 觸發的條件為任一下列事件

  1. FIFO 啟用,transmit FIFO 等於或低於 trigger level,直到清除中斷或是寫入資料到高於 trigger level。
  2. FIFO 未啟用,在位置上沒有資料要寫入馬上觸發中斷,直到清除中斷或是寫入資料。

更新 transmit FIFO 的方式為在啟用 UART 和 interrupt 前或後,向 transmit FIFO 寫入資料。

transmit interrupt 發生在狀態改變時,也就是當啟用時 transmit FIFO 無資料中斷是不觸發的。只有當寫入後的資料離開 transmit FIFO 該處且 FIFO 為空時觸發。

新增 ring buffer 來處理 uart interrupt,同時改寫原有的 read, write,以下就各實作進行介紹。

Ring buffer

uart 中斷處理 :依據 status 決定資料要往 RX QUEUE 送或是從 TX QUEUE 拉出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void uart_handler()
{
uint32_t status = *UART0_MIS;

if (status & MIS_RXMIS) {
if (!ringbuf_is_full(&PL011_RX_QUEUE)) {
uint8_t data = (uint8_t) *UART0_DR;
ringbuf_push(&PL011_RX_QUEUE, &data);
}
if (ringbuf_is_full(&PL011_RX_QUEUE)) {
uart_disable_rx_interrupt();
}
}

if (status & MIS_TXMIS) {
if (!ringbuf_is_empty(&PL011_TX_QUEUE)) {
uint8_t data;
ringbuf_pop(&PL011_TX_QUEUE, &data);
*UART0_DR = (uint32_t) data;
}
if (ringbuf_is_empty(&PL011_TX_QUEUE)) {
uart_disable_tx_interrupt();
}
}
}

RX QUEUE 與 TX QUEUE 分別各以 ring buffer 進行實作,設計上 ring buffer 資料結構除了儲存資料的 buf[] 外,以 head 表示要讀取的資料位置,tail 表示要寫入檔案的位置。

1
2
3
4
5
typedef struct ringbuf_t {
uint8_t buf[PL011_RINGBUFF_SIZE];
size_t head, tail;
size_t size, mask;
} ringbuf_t;

限制 buf[] 大小為 2 的冪次方,這樣在將 head, tail 累加時,處理邊界問題只需要用 and。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void ringbuf_init(ringbuf_t *rb)
{
// Ensure size is greater than zero and two to the power of n, n > 0.
if (!PL011_RINGBUFF_SIZE || (PL011_RINGBUFF_SIZE & 0x80000000))
return;
rb->size = 1 << (31 - __builtin_clz(PL011_RINGBUFF_SIZE));
rb->mask = rb->size - 1;
ringbuf_reset(rb);
}

void ringbuf_push(ringbuf_t *rb, void *src)
{
// assume ring buffer is not full
rb->buf[rb->tail++] = *((uint8_t *) src);
rb->tail &= rb->mask;
}

void ringbuf_pop(ringbuf_t *rb, void *dst)
{
// assume ring buffer is not empty
*((uint8_t *) dst) = rb->buf[rb->head++];
rb->head &= rb->mask;
}

由於初始化時 head=tail=0,我將 headtail 相等作為判斷 empty。

那如何判斷 full? 試想進到 queue 的資料越來越多,而卻不進行讀取(head=0),tail 變化為 head - 2, head - 1, head…,由於 head==tail 已作為判斷 empty,退而求其次以 (head - 1)==tail 作為 full 條件。

1
2
3
4
5
6
7
8
9
10
11
bool ringbuf_is_empty(const ringbuf_t *rb)
{
__sync_synchronize();
return rb->head == rb->tail;
}

bool ringbuf_is_full(const ringbuf_t *rb)
{
__sync_synchronize();
return rb->head == (rb->tail + 1) & rb->mask;
}

這樣做的後果是若一開始初始化 ring buffer 大小為 PL011_RINGBUFF_SIZE,滿載時 tail 永遠在 head 之後,可使用的空間為 PL011_RINGBUFF_SIZE-1,雖說空間無法完全使用,但隨著 buffer size 增加,這點差異是微不足道的。

read/write

對應 glibc 的 read, write,回傳已讀取/寫入位元組。採用 non-blocking 的方式實作,由於 buffer 不是隨時可讀取/寫入,回傳值可能與預期有差,使用此函式務必檢查回傳值是否符合預期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ssize_t uart_read(ringbuf_t *rb, void *dst, size_t count)
{
// Non-blocking read. Attempts to read up to 'count' bytes.
ssize_t num = 0;
while (!ringbuf_is_empty(rb) && num < count) {
ringbuf_pop(rb, dst++);
num++;
}
uart_enable_rx_interrupt();
return num;
}

ssize_t uart_write(ringbuf_t *rb, void *src, size_t count)
{
// Non-blocking write. Attempts to write up to 'count' bytes.
ssize_t num = 0;
while (!ringbuf_is_full(rb) && num < count) {
ringbuf_push(rb, src++);
num++;
}
uart_enable_tx_interrupt();
return num;
}

putc, getc

基於 read, write 的單一字元操作,會等到 1 byte 字元順利讀取/寫入再回傳,回傳的型態為 int 而非 char,int 負數的部份是保留給 EOF 及其他 errno 使用。

getc, putc會處理 ‘\n’getc_raw 用來讀取 kernel image 不對字元做任何操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int uart_getc()
{
// Blocking read
// Reads a character from ring buffer and returns it as an
// unsigned char cast to an int.
unsigned char buf[1];
while (1 != uart_read(&PL011_RX_QUEUE, buf, 1)) {
}
buf[0] = (buf[0] == '\r') ? '\n' : buf[0];
// Echo back with the character.
uart_putc(buf[0]);
return (int) buf[0];
}

int uart_getc_raw()
{
// Blocking read
// Reads a character from ring buffer and returns it as an
// unsigned char cast to an int.
unsigned char buf[1];
while (1 != uart_read(&PL011_RX_QUEUE, buf, 1)) {
}
return (int) buf[0];
}

int uart_putc(unsigned char c)
{
// Blocking write
// Write a character to ring buffer and return the character
// written as an unsigned char cast to an int.
if (c == '\n') {
while (1 != uart_write(&PL011_TX_QUEUE, &(char[1]){'\r'}, 1)) {
}
}
while (1 != uart_write(&PL011_TX_QUEUE, &(char[1]){c}, 1)) {
}
return (int) c;
}

fputs, fgets

基於 getc, putc 的寫入/讀取字串操作,回傳型態仿造 glibc。fgets 正確回傳 char,錯誤回傳 null pointer,錯誤情況包含傳入的 buffer size 無法除了 null byte 的其他字元。fputs 以 null bytes 做為寫入停止的標記,並回傳寫入的字元數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
char *uart_fgets(char *buf, size_t len)
{
// Attempts to read up to one less than 'len' characters.
// Reading stops after a newline. If a newline is read, it
// is stored into the buffer. A terminating null byte '\0'
// is stored after the last character in the buffer.
// Return 'buf' on success or NULL on error.
size_t idx = 0;
char c;
if (len < 2)
return NULL;
do {
c = uart_getc();
buf[idx++] = c;
} while (c != '\n' && idx < (len - 1));
buf[idx] = '\0';
return buf;
}

ssize_t uart_fputs(const char *buf)
{
// Attempts to write the string 'buf' without its terminating
// null byte. Return a nonnegative number on success, or EOF
// on error.
size_t count = 0;
while (buf[count]) {
uart_putc(buf[count++]);
}
return count;
}