手册行为和编码规范

这一部分我就不过多赘述,比较偏概念

E4.4.1 体验未指定行为

尝试在你的系统中编译运行上述程序, 观察程序的结果.

#include <stdio.h>
void f(int x, int y) {
  printf("x = %d, y = %d\n", x, y);
}
int main() {
  int i = 1;
  f(i ++, i ++);
  return 0;
}
[21:12:30] Ylin@Ylin /home/Ylin/programs/C
> gcc test.c
[21:12:39] Ylin@Ylin /home/Ylin/programs/C
> ./a.out
x = 2, y = 1
[21:12:41] Ylin@Ylin /home/Ylin/programs/C
> clang test.c
test.c:7:7: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
    7 |   f(i ++, i ++);
      |       ^     ~~
1 warning generated.
[21:12:52] Ylin@Ylin /home/Ylin/programs/C
> ./a.out
x = 1, y = 2

E4.4.4 体验未定义行为

尝试在你的系统中编译并多次运行上述程序, 观察程序的结果.

#include <stdio.h>
int main() {
  int a[10] = {0};
  printf("a[10] = %d\n", a[10]);
  return 0;
}
[21:47:02] Ylin@Ylin /home/Ylin/programs/C [2]
> bash -c ' while true;do ./a.out;sleep 1;done'
a[10] = -1541628672
a[10] = 88222160
a[10] = -1754637312
a[10] = 1680474480
a[10] = -1804221760
a[10] = 1444037472
a[10] = -1295021120
a[10] = 1033333600
a[10] = -1406439568

指令集模拟器 - 可以执行程序的程序

我们先前在Logisim中用GUI的方式设计过sISA,现在我们需要用C程序来实现这个指令集模拟器,我们将称之为sEMU,具体的细节参考先前的sISA.

由于C语言中不存在4位的基础数据类型,所以这里设置:

#include <stdint.h>
uint8_t PC = 0;		// 实际位宽为16,所以只能有16条指令
uint8_t R[4];		// 寄存器
uint8_t M[16];		// ROM

要实现这个模拟器也很简单,我们只需要编写一个函数inst_cycle(),实现以下功能并循环:

  • 取指 - 直接根据PC索引内存M, 即可取出一条指令
  • 译码 - 通过C语言的位运算抽取出指令的opcode字段, 并检查属于哪一条指令; 然后根据指令格式抽取出操作数字段, 并获得相应的操作数
  • 执行 - 若执行的指令不是bner0, 则将结果写回目的寄存器; 否则, 根据判断情况决定是否进行跳转
  • 更新PC - 若不跳转, 则让PC加1

E4.5.1 实现sEMU

根据上述思路, 用C代码实现sEMU, 并运行之前的数列求和程序. 由于数列求和程序本身不会结束, 你可以修改while语句的循环条件, 在循环一定的次数后退出循环, 然后检查数列求和的结果是否符合预期.

首先回顾以下sISA的标准:

我们可以编写出以下程序来实现sEMU:

#include <stdint.h>
#include <assert.h>

uint8_t PC = 0;
uint8_t R[4];
uint8_t M[16] = {
    0b10001010,
    0b10010000,
    0b10100000,
    0b10110001,
    0b00010111,
    0b00101001,
    0b11010001,
    0b11011111
};

void inst_cycle(){
    uint8_t inst = *(uint8_t*)&M[PC];
    uint8_t func2 = (inst >> 6) & 0x3;
    uint8_t rd = (inst >> 4) & 0x3;
    uint8_t rs1 = (inst >> 2) & 0x3;
    uint8_t rs2 = inst & 0x3;
    uint8_t imm = inst & 0xF;
    uint8_t addr = (inst >> 2) & 0xF;

    switch (func2){
        case 0b00:
            R[rd] = R[rs1] + R[rs2]; break;
        case 0b10:
            R[rd] = imm;
        case 0b11:
            if(R[0]!=R[rs2]) PC = addr; break;
        default:
            assert("Unknown instruction.\n");
    }
}

int main(){
    for(int i=0;i<100;i++)
        inst_cycle();
    return 0;
}

但是很显然,现在我们无法获取程序运行的结果(虽然可以通过调试得到),这和我们的运行时环境有关,我们的环境并不支持我们查看运行的结果。所以我们需要进一步的完善它。

我们可以添加一个新的指令out rs 以将R[rs]中的数据输出到终端上。

E4.5.2 实现输出功能

根据上文, 在sEMU中实现out指令, 并修改数列求和程序, 使得在计算出结果后通过out指令输出计算结果. 如果你的实现正确, 你应该能看到终端上显示55.

首先需要设置这个指令的格式:

 7  6 5  4 3   2 1   0
+----+----+-----+-----+
| 00 | rd | rs1 | rs2 | R[rd]=R[rs1]+R[rs2]       add指令, 寄存器相加
+----+----+-----+-----+
| 10 | rd |    imm    | R[rd]=imm                 li指令, 装入立即数, 高位补0
+----+----+-----+-----+
| 11 |   addr   | rs2 | if(R[0]!=R[rs2]) PC=addr  bner0指令, 若不等于R[0]则跳转
+----+----------+-----+
| 01 | rd |           | printf(R[rd]) 			 out指令,将R[rd]中的值输出出来
+----+----------+-----+

然后在我们的EMU中实现:

#include <stdint.h>
#include <assert.h>
#include <stdio.h>

uint8_t PC = 0;
uint8_t R[4];
uint8_t M[16] = {
    0b10001010,
    0b10010000,
    0b10100000,
    0b10110001,
    0b00010111,
    0b00101001,
    0b11010001,
    0b11100111,
    0b11100011,
    0b01100000
};

void inst_cycle(){
    uint8_t inst = *(uint8_t*)&M[PC++];
    uint8_t func2 = (inst >> 6) & 0x3;
    uint8_t rd = (inst >> 4) & 0x3;
    uint8_t rs1 = (inst >> 2) & 0x3;
    uint8_t rs2 = inst & 0x3;
    uint8_t imm = inst & 0xF;
    uint8_t addr = (inst >> 2) & 0xF;

    switch (func2){
        case 0b00:
            R[rd] = R[rs1] + R[rs2]; break;
        case 0b01:
            printf("R[%d] = %d\n",rd,R[rd]);break;
        case 0b10:
            R[rd] = imm;break;
        case 0b11:
            if(R[0]!=R[rs2]) PC = addr; break;
        default:
            assert("Unknown instruction.\n");
    }
}

int main(){
    for(int i=0;i<100;i++)
        inst_cycle();
    return 0;
}

得到最终的运行结果:

[13:35:08] Ylin@Ylin /home/Ylin/programs/C
> gcc sEMU.c -g
[13:35:16] Ylin@Ylin /home/Ylin/programs/C
> ./a.out
R[2] = 55

符合我们的预期。现在我们需要进一步强化我们的运行环境,使它支持接受用户的输入。

E4.5.3 实现参数化的数列求和

修改数列求和程序, 使其从r0中获取数列的末项. 然后修改sEMU, 来将用户输入的参数放置在r0中. 实现后, 简单测试你的实现, 例如, 键入./semu 10, 程序应输出55; 键入./semu 15, 程序应输出120. 我们假设用户的输入不会导致计算过程溢出, 因此你不必考虑如何处理结果溢出的情况.

#include <stdint.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>


uint8_t PC = 0;
uint8_t R[4];
uint8_t M[16] = {
    0b10010000, 
    0b10100000,
    0b10110001,
    0b00010111,
    0b00101001,
    0b11001101,
    0b01100000,
    0b11011101
};

void inst_cycle(){
    uint8_t inst = M[PC++];
    uint8_t func2 = (inst >> 6) & 0x3;
    uint8_t rd = (inst >> 4) & 0x3;
    uint8_t rs1 = (inst >> 2) & 0x3;
    uint8_t rs2 = inst & 0x3;
    uint8_t imm = inst & 0xF;
    uint8_t addr = (inst >> 2) & 0xF;

    switch (func2){
        case 0b00:
            R[rd] = R[rs1] + R[rs2]; break;
        case 0b01:
            printf("R[%d] = %d\n",rd,R[rd]);break;
        case 0b10:
            R[rd] = imm;break;
        case 0b11:
            if(R[0]!=R[rs2]) PC = addr; break;
        default:
            assert("Unknown instruction.\n");
    }
}

int main(int argc , char** argv){

    R[0] = (uint8_t)atoi(argv[1]);

    for(int i=0;i<100;i++){
        inst_cycle();
        // printf("R[0]=%d\tR[1]=%d\tR[2]=%d\tR[3]=%d\n",R[0],R[1],R[2],R[3]);
    }
    return 0;
}

运行结果如下:

[14:08:53] Ylin@Ylin /home/Ylin/programs/C
> ./a.out 15
R[2] = 120

minirvEMU的实现

现在我们已经实现了sEMU,大概了解了程序的框架应该是什么样的,我们可以进一步的实现更加复杂的模拟器,参考先前的文档,实现miniRV。

E4.5.4 实现两条指令的minirvEMU

设计支持addijalr指令的minirvEMU, 并让其运行之前在Logisim上运行过的那个两条指令的测试程序.

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>

#define MEM_SIZE 1 << 20
#define REG_SIZE 16

#define DEBUG(MSG,...) fprintf(stderr, "\033[1;32m[DEBUG]: %s:%d -> " MSG "\033[0m\n", __FILE__, __LINE__, ##__VA_ARGS__)
#define REG_STATUS() for(int i = 0; i < REG_SIZE; i++) { \
    fprintf(stderr, "\033[34mreg[%d] = %x\t\033[0m", i, reg[i]);        \
    if(i%4==3) fprintf(stderr,"\n");                     \
}

#define WRITE_RD(val) \
    do { if (inst.I.rd != 0) reg[inst.I.rd] = (val); } while(0)

typedef union{
    struct{
        uint32_t opcode:7;
        uint32_t rd:5;
        uint32_t funct3:3;
        uint32_t rs1:5;
        int32_t immediate:12;
    }I;
    uint32_t bytes;
}Instruction;

uint8_t mem[MEM_SIZE];
uint32_t reg[REG_SIZE];
uint32_t PC = 0;

void disasm_inst(uint32_t pc, Instruction inst) {
    fprintf(stderr, "\033[1;33m[INST] PC=0x%08x: ", pc);
    switch (inst.I.opcode) {
        case 0x13: // ADDI
            fprintf(stderr, "addi x%d, x%d, %d", 
                    inst.I.rd, inst.I.rs1, inst.I.immediate);
            break;
        case 0x67: // JALR
            fprintf(stderr, "jalr x%d, %d(x%d)", 
                    inst.I.rd, inst.I.immediate, inst.I.rs1);
            break;
        default:
            fprintf(stderr, ".word 0x%08x (unknown opcode 0x%02x)", 
                    inst.bytes, inst.I.opcode);
    }
    fprintf(stderr, "\033[0m\n");
}

void inst_cycle(){
    Instruction inst = *(Instruction*)(mem + PC);

    disasm_inst(PC,inst);

    uint32_t nPC = PC + 4;

    switch (inst.I.opcode){
        case 0x13:{// ADDI
            WRITE_RD(reg[inst.I.rs1] + inst.I.immediate);
            break;
        }case 0x67:{ // JALR
            WRITE_RD(PC + 4);
            nPC = reg[inst.I.rs1] + inst.I.immediate;
            break;
        }default:
            DEBUG("Unknown instruction with opcode: %x", inst.I.opcode);
    }
    PC = nPC;
}

int main(){

    uint8_t program[] = {
        // 0x00: addi a0, zero, 20
        0x13, 0x05, 0x40, 0x01,
        // 0x04: jalr ra, 16(zero)  --> 跳转到 0x10 (fun)
        0xe7, 0x00, 0x00, 0x01,
        // 0x08: jalr ra, 12(zero)  --> 跳转到 0x0c (halt)
        0xe7, 0x00, 0xc0, 0x00,
        // 0x0c: halt: jalr zero, 12(zero) --> 无限循环
        0x67, 0x00, 0xc0, 0x00,
        // 0x10: fun: addi a0, a0, -10
        0x13, 0x05, 0x65, 0xff,
        // 0x14: jalr zero, 0(ra) --> 返回调用者(但这里会回到 0x08 后的下一条?注意 ra 设置)
        0x67, 0x80, 0x00, 0x00
    };

    memcpy(mem, program, sizeof(program));

    while (getchar() == '\n'){
        REG_STATUS();
        inst_cycle();
    }
}

看起来比较多,是因为做了一些冗余设计,方便之后添加指令,我们可以查看程序得运行效果如下,循环和加法部分都正确的执行了。

E4.5.5 实现完整的minirvEMU

为minirvEMU添加剩余的6条指令, 并更新加载程序的方式, 然后运行之前在Logisim上运行过的summem两个程序. 为了判断程序是否成功结束运行, 你可以参考之前在Logisim上的判断方式.

全部代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>

#define MEM_SIZE 1 << 20
#define REG_SIZE 16

#define DEBUG(MSG,...) fprintf(stderr, "\033[1;32m[DEBUG]: %s:%d -> " MSG "\033[0m\n", __FILE__, __LINE__, ##__VA_ARGS__)
#define REG_STATUS() for(int i = 0; i < REG_SIZE; i++) { \
    fprintf(stderr, "\033[34mreg[%d] = 0x%x    \t\033[0m", i, reg[i]);        \
    if(i%4==3) fprintf(stderr,"\n");                     \
}

#define WRITE_RD(val) \
    do { if (inst.I.rd != 0) reg[inst.I.rd] = (val); } while(0)

typedef union{
    struct{
        uint32_t opcode:7;
        uint32_t rd:5;
        uint32_t func3:3;
        uint32_t rs1:5;
        int32_t imm:12;
    }I;
    struct{
        uint32_t opcode:7;
        uint32_t rd:5;
        uint32_t imm:20;
    }U;
    struct{
        uint32_t opcode:7;
        uint32_t rd:5;
        uint32_t func3:3;
        uint32_t rs1:5;
        uint32_t rs2:5;
        uint32_t func7:7;
    }R;
    struct{
        uint32_t opcode:7;
        uint32_t imm5:5;
        uint32_t func3:3;
        uint32_t rs1:5;
        uint32_t rs2:5;
        uint32_t imm7:7;
    }S;
    uint32_t bytes;
}Instruction;

uint8_t mem[MEM_SIZE];
uint32_t reg[REG_SIZE];
uint32_t PC = 0;
uint32_t status = 1;

void disasm_inst(uint32_t pc, Instruction inst) {
    fprintf(stderr, "\033[1;33m[INST] PC=0x%08x: ", pc);
    switch (inst.I.opcode) {
        case 0x13: // ADDI
            fprintf(stderr, "addi x%d, x%d, 0x%x", 
                    inst.I.rd, inst.I.rs1, inst.I.imm);
            break;
        case 0x67: // JALR
            fprintf(stderr, "jalr x%d, %d(x%d)", 
                    inst.I.rd, inst.I.imm, inst.I.rs1);
            break;
        case 0x33: // ADD
            fprintf(stderr, "add x%d, x%d, x%d", 
                    inst.I.rd, inst.R.rs1, inst.R.rs2);
            break;
        case 0x37: //LUI
            fprintf(stderr, "lui x%d, 0x%x", 
                    inst.U.rd, inst.U.imm);
            break;
        case 0x23: // Store
            int32_t imm = (inst.S.imm7 << 5) | (inst.S.imm5);
            if(inst.S.func3 == 2){ // SW
                fprintf(stderr, "sw x%d, %d(x%d)", 
                    inst.S.rs2, imm, inst.S.rs1);
            }else if(inst.S.func3 == 0){ // SB
                fprintf(stderr, "sb x%d, %d(x%d)", 
                    inst.S.rs2, imm, inst.S.rs1);
            }else 
                goto fail;
            break;
        case 0x3: // Load
            if(inst.I.func3 == 2){ // LW
                fprintf(stderr, "lw x%d, %d(x%d)", 
                    inst.I.rd , inst.I.imm, inst.I.rs1);
            }else if(inst.I.func3 == 4){ // LBU
                fprintf(stderr, "lbu x%d, %d(x%d)", 
                    inst.I.rd , inst.I.imm, inst.I.rs1);
            }else 
                goto fail;
            break;
        default:
        fail:
            fprintf(stderr, ".word 0x%08x (unknown opcode 0x%02x)", 
                    inst.bytes, inst.I.opcode);
    }
    fprintf(stderr, "\033[0m\n");
}

void inst_cycle(){
    Instruction inst = *(Instruction*)(mem + PC);

    disasm_inst(PC,inst);

    uint32_t nPC = PC + 4;

    switch (inst.I.opcode){
        case 0x13:{ // ADDI
            WRITE_RD(reg[inst.I.rs1] + inst.I.imm);
            break;
        }
        case 0x67:{ // JALR
            nPC = reg[inst.I.rs1] + inst.I.imm;
            WRITE_RD(PC + 4);
            break;
        }
        case 0x33:{ // ADD 
            WRITE_RD(reg[inst.R.rs1] + reg[inst.R.rs2]);
            break;
        }
        case 0x37:{ // LUI
            WRITE_RD((inst.U.imm & 0xFFFFF) << 12);
            break;
        }
        case 0x23:{ // Store
            // int32_t imm = (inst.S.imm7 << 5) | (inst.S.imm5);
            uint32_t imm12 = ((inst.S.imm7 & 0x7F) << 5) | (inst.S.imm5 & 0x1F);
            int32_t imm = (imm12 & 0x800) ? (int32_t)(imm12 | 0xFFFFF000) : (int32_t)imm12;
            uint32_t addr = reg[inst.S.rs1] + imm;
            if(inst.S.func3 == 2){ // SW
                *(uint32_t*)&mem[addr] = reg[inst.S.rs2];
            }else if(inst.S.func3 == 0){ // SB
                mem[addr] = (uint8_t)(reg[inst.S.rs2] & 0xFF);
            }else 
                DEBUG("Unknown Store instruction with func3: %x", inst.S.func3);
            break;
        }
        case 0x3:{ // Load
            uint32_t addr = reg[inst.I.rs1] + inst.I.imm;
            if(inst.I.func3 == 2){ // LW
                WRITE_RD(*(uint32_t*)&mem[addr]);
            }else if(inst.I.func3 == 4){ // LBU
                WRITE_RD((uint32_t)mem[addr]);
            }else 
                DEBUG("Unknown Load instruction with func3: %x", inst.I.func3);
            break;
        }
        case 0x73:{
            if(reg[10] == 0) printf("Hit good trap.\n");
            else printf("Hit bad trap.\n");
            status = 0;
            break;
        }
        default:
            DEBUG("Unknown instruction with opcode: %x", inst.I.opcode);
    }

    PC = nPC;
}

uint8_t* load_bin(const char* filename,size_t* out_size) {
    FILE* file = fopen(filename, "rb");
    if (!file) {
        perror("Failed to open file");
        return NULL;
    }

    fseek(file, 0, SEEK_END);
    long size = ftell(file);
    if (size < 0) {
        perror("ftell failed");
        fclose(file);
        return NULL;
    }
    rewind(file); 

    uint8_t* buffer = (uint8_t*)malloc((size_t)size);
    if (!buffer) {
        perror("malloc failed");
        fclose(file);
        return NULL;
    }

    size_t bytes_read = fread(buffer, 1, (size_t)size, file);
    if (bytes_read != (size_t)size) {
        perror("fread failed");
        free(buffer);
        fclose(file);
        return NULL;
    }

    fclose(file);
    *out_size = (size_t)size;

    return buffer;
}


int main(){
    size_t program_size;
    uint8_t* program = load_bin("sum.bin",&program_size);

    if (program_size > MEM_SIZE) {
        fprintf(stderr, "Error: Program too large (%zu bytes > %d)\n", program_size, MEM_SIZE);
        free(program);
        return 1;
    }
    memset(mem, 0, MEM_SIZE);
    memcpy(mem, program, program_size);

    while (status){
        inst_cycle();
        REG_STATUS();
    }
}

实现了所有的功能且运行测试程序无误。

E4.5.6 实现程序结束的自动判断

根据上述运行时环境的约定, 在minirvEMU中添加并实现ebreak指令, 然后修改程序的指令序列, 使其在结束时执行ebreak指令. 如果你的实现正确, 你会看到程序自动结束并通过minirvEMU输出结束信息.

这个的实现就是上面的代码,这里我就简单讲一件实现思路吧。

设置一个status状态来控制整个模拟器:

while (status){
    inst_cycle();
}

ebreak的实现中,我们通过将status修改来停止模拟器运行,退出循环。效果如下:

但是关键是怎么设置这个程序到bin中:

  • 在txt附件中搜索halt定位相关的指令
  • 然后将附近的指令作为搜索文本,定位halt在hex中的位置
  • 然后修改成0x00100073

这样就可以成功执行了。

支持GUI输入输出的程序

先前在Logisim中我们最终使用RGB显示器运行了渲染图片的程序,现在我们也要想办法在minirvEMU中运行这个程序。所以我们需要通过GUI进行输入输出。

这里我们需要引入一个叫abstract maschine的运行时环境,它以库函数的方式提供API,以实现GUI的输入输出功能,接下来我们需要利用它完成各种任务。

E4.6.1 运行超级玛丽

阅读PA1的在开始愉快的PA之旅之前->NEMU是什么?开头部分的讲义内容, 按照讲义指示尝试运行超级玛丽, 并完成画面, 按键和声音的检查.

先去PA0配置一下PA的环境,运行效果如下:

E4.6.2 体验时钟功能

通过如下命令运行时钟测试程序:

cd am-kernels/tests/am-tests
make ARCH=native mainargs=t run

你会发现程序在终端上每隔1s输出一句话. 此外, 程序还会弹出一个画面全黑的新窗口, 但在当前程序中无任何功能, 目前你不必关心它.

体验上述功能后, 尝试阅读am-kernels/tests/am-test/src/tests/rtc.c, 理解上述功能是如何实现的. 其中, 代码io_read(AM_TIMER_UPTIME).us将获得程序运行以来经过的时间, 单位是us.

> make ARCH=native mainargs=t run
# Building amtest-run [native]
# Building am-archive [native]
# Building klib-archive [native]
/home/Ylin/code/ics2025/am-kernels/tests/am-tests/build/amtest-native.elf
2026-3-17 11:20:26 GMT (1 second).
2026-3-17 11:20:27 GMT (2 seconds).
2026-3-17 11:20:28 GMT (3 seconds).
2026-3-17 11:20:29 GMT (4 seconds).
2026-3-17 11:20:30 GMT (5 seconds).
2026-3-17 11:20:31 GMT (6 seconds).
2026-3-17 11:20:32 GMT (7 seconds).
2026-3-17 11:20:33 GMT (8 seconds).
2026-3-17 11:20:34 GMT (9 seconds).
2026-3-17 11:20:35 GMT (10 seconds).
2026-3-17 11:20:36 GMT (11 seconds).
Exit code = 00h

分析一下实现:

// rtc.c
void rtc_test() {
  // 这个AM定义在include/admdev.h中,通过宏定义来生成设备结构体,以通过io_read来读取虚拟设备
  AM_TIMER_RTC_T rtc;
  int sec = 1;
  while (1) {
    // 重点关注这个io_read,这个函数是虚拟设备工作的关键 
    while(io_read(AM_TIMER_UPTIME).us / 1000000 < sec) ;
    rtc = io_read(AM_TIMER_RTC);
	...
}
    
// 我们进一步跟踪io_read,发现调用了ioe_read, 接着追踪
#define io_read(reg) \
  ({ reg##_T __io_param; \
    ioe_read(reg, &__io_param); \
    __io_param; })
    
// 发现一串调用,发现最终的调用是通过一个lut表实现的
static void *lut[128] = {
  [AM_TIMER_CONFIG] = __am_timer_config,
  [AM_TIMER_RTC   ] = __am_timer_rtc,
  [AM_TIMER_UPTIME] = __am_timer_uptime,
  [AM_INPUT_CONFIG] = __am_input_config,
  [AM_INPUT_KEYBRD] = __am_input_keybrd,
  [AM_GPU_CONFIG  ] = __am_gpu_config,
  [AM_GPU_FBDRAW  ] = __am_gpu_fbdraw,
  [AM_GPU_STATUS  ] = __am_gpu_status,
  [AM_UART_CONFIG ] = __am_uart_config,
  [AM_UART_TX     ] = __am_uart_tx,
  [AM_UART_RX     ] = __am_uart_rx,
  [AM_AUDIO_CONFIG] = __am_audio_config,
  [AM_AUDIO_CTRL  ] = __am_audio_ctrl,
  [AM_AUDIO_STATUS] = __am_audio_status,
  [AM_AUDIO_PLAY  ] = __am_audio_play,
  [AM_DISK_CONFIG ] = __am_disk_config,
  [AM_DISK_STATUS ] = __am_disk_status,
  [AM_DISK_BLKIO  ] = __am_disk_blkio,
  [AM_NET_CONFIG  ] = __am_net_config,
};
    
void __am_ioe_init() {
  for (int i = 0; i < LENGTH(lut); i++)
    if (!lut[i]) lut[i] = fail;
  __am_timer_init();
  __am_gpu_init();
  __am_input_init();
  __am_uart_init();
  __am_audio_init();
  __am_disk_init();
  ioe_init_done = true;
}

static void do_io(int reg, void *buf) {
  if (!ioe_init_done) {
    __am_ioe_init();
  }
  ((handler_t)lut[reg])(buf);
}

void ioe_read (int reg, void *buf) { do_io(reg, buf); }
void ioe_write(int reg, void *buf) { do_io(reg, buf); }

// 可以在lut表中的函数调用找到最终实现计时的底层调用
void __am_timer_rtc(AM_TIMER_RTC_T *rtc) {
  time_t t = time(NULL);
  struct tm *tm = localtime(&t);
  rtc->second = tm->tm_sec;
  rtc->minute = tm->tm_min;
  rtc->hour   = tm->tm_hour;
  rtc->day    = tm->tm_mday;
  rtc->month  = tm->tm_mon + 1;
  rtc->year   = tm->tm_year + 1900;
}

void __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) {
  struct timeval now;
  gettimeofday(&now, NULL);
  long seconds = now.tv_sec - boot_time.tv_sec;
  long useconds = now.tv_usec - boot_time.tv_usec;
  uptime->us = seconds * 1000000 + (useconds + 500);
}

void __am_timer_init() {
  gettimeofday(&boot_time, NULL);
}

这个应该是设备管理中的分层次调用,底层的设备的功能被封装起来,通过io_read来读取功能,上层通过宏定义将其封装为虚拟设备,通过分配设备号,方便调用功能,最终就是我们看到的这样。要想实现类似的功能,我们只需要使用AM给我们提供的API就行了(AM_dev和io_read…)

E4.6.3 体验按键功能

通过如下命令运行按键测试程序:

cd am-kernels/tests/am-tests
make ARCH=native mainargs=k run

你会发现程序弹出一个画面全黑的新窗口, 在新窗口中按下按键, 你将会看到程序在终端输出相应的按键信息, 包括按键名, 键盘码, 以及按键状态.

体验上述功能后, 尝试阅读am-kernels/tests/am-test/src/tests/keyboard.c, 理解上述功能是如何实现的(目前你可以忽略代码中uart相关的功能). 其中, 代码io_read(AM_INPUT_KEYBRD)将获得一个按键事件ev, ev.keycode表示按键的编码, ev.keydown表示按键为按下还是释放. 按键的编码值可查阅abstract-machine/am/include/amdev.h, 它们均以AM_KEY_为前缀, 如A键的编码为AM_KEY_A. 特别地, AM_KEY_NONE表示无按键事件

Got  (kbd): LSHIFT (55) DOWN
Got  (kbd): LSHIFT (55) UP
Got  (kbd): D (45) DOWN
Got  (kbd): D (45) UP
Got  (kbd): S (44) DOWN
Got  (kbd): D (45) DOWN
Got  (kbd): S (44) UP
Got  (kbd): A (43) DOWN
Got  (kbd): D (45) UP
Got  (kbd): A (43) UP
Got  (kbd): D (45) DOWN
Got  (kbd): A (43) DOWN
Got  (kbd): D (45) UP
Got  (kbd): A (43) UP
Got  (kbd): D (45) DOWN
Got  (kbd): D (45) UP
Got  (kbd): LALT (69) DOWN
Got  (kbd): LALT (69) UP
Got  (kbd): LALT (69) DOWN

然后我们来查看一下实现:

// 这一部分是读取的核心实现
static void drain_keys() {
  if (has_uart) {
    while (1) {
      char ch = io_read(AM_UART_RX).data;
      if (ch == (char)-1) break;
      printf("Got (uart): %c (%d)\n", ch, ch & 0xff);
    }
  }

  if (has_kbd) {
    while (1) {
      AM_INPUT_KEYBRD_T ev = io_read(AM_INPUT_KEYBRD);
      if (ev.keycode == AM_KEY_NONE) break;
      printf("Got  (kbd): %s (%d) %s\n", names[ev.keycode], ev.keycode, ev.keydown ? "DOWN" : "UP");
    }
  }
}
// 这一部分是封装起来的底层实现
void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {
  int k = AM_KEY_NONE;
  // 通过互斥锁保证底层资源的安全使用
  SDL_LockMutex(key_queue_lock);
  if (key_f != key_r) {
    k = key_queue[key_f];
    key_f = (key_f + 1) % KEY_QUEUE_LEN;
  }
  SDL_UnlockMutex(key_queue_lock);

  kbd->keydown = (k & KEYDOWN_MASK ? true : false);
  kbd->keycode = k & ~KEYDOWN_MASK;
}

void __am_uart_tx(AM_UART_TX_T *uart) {
  putchar(uart->data);
}

void __am_uart_rx(AM_UART_RX_T *uart) {
  int ret = fgetc(stdin);
  if (ret == EOF) ret = -1;
  uart->data = ret;
}

E4.6.4 体验显示功能

通过如下命令运行显示测试程序:

cd am-kernels/tests/am-tests
make ARCH=native mainargs=v run

你会发现程序弹出一个新窗口并播放动画.

体验上述功能后, 尝试阅读am-kernels/tests/am-test/src/tests/video.c, 理解上述功能是如何实现的. 其中, 代码io_write(AM_GPU_FBDRAW, x * w, y * h, color_buf, w, h, false) 表示向屏幕(x * w, y * h)坐标处绘制w*h的矩形图像. 图像像素按行优先方式存储在color_buf中, 每个像素用32位整数以00RRGGBB的方式描述颜色.

核心的绘制逻辑:

void redraw() {
  int w = io_read(AM_GPU_CONFIG).width / N;
  int h = io_read(AM_GPU_CONFIG).height / N;
  int block_size = w * h;
  assert((uint32_t)block_size <= LENGTH(color_buf));

  int x, y, k;
  for (y = 0; y < N; y ++) {
    for (x = 0; x < N; x ++) {
      for (k = 0; k < block_size; k ++) {
        color_buf[k] = canvas[y][x];
      }
      io_write(AM_GPU_FBDRAW, x * w, y * h, color_buf, w, h, false);
    }
  }
  io_write(AM_GPU_FBDRAW, 0, 0, NULL, 0, 0, true);
}

这里最重要的是io_write(...)这个函数的用法:

  • arg1:作为设备名称,这里我暂时只说明AM_GPU_FBDRAW设备的用法
  • arg2:用来定位横轴,x为个数,w为像素宽度
  • arg3:用来定位纵轴,y为个数,h为像素高度(参数2、3一起定位像素的位置)
  • arg4:用来存放要写入的像素数据
  • arg5/arg6:存放像素的宽高
  • arg7:用来控制同步,开启之后就是画完再显示 ,不开启就是别画边显示。

现在我们利用这个api自己写一个简单的屏幕保护程序,我们可以使用以下框架:

#include <am.h>
#include <klib-macros.h>

void draw(uint32_t color) {
  // add code here
}

int main() {
  ioe_init(); // initialization for GUI
  while (1) {
    draw(0x000000ff);
  }
  return 0;
}

不过为了调用AM,我们需要使用以下符合AM规范的文件:

NAME = screensaver
SRCS = screensaver.c
include $(AM_HOME)/Makefile

# 然后使用 make ARCH=native run 编译执行

E4.6.5 实现单种颜色的显示

实现上述的draw()函数, 它将弹出的窗口填充为参数color所指示的颜色, 窗口的分辨率为400x300.

实现后, 重新编译运行. 如果你的实现正确, 你应该看到弹出的窗口填充了蓝色.

参考先前video.c的实现,我们可以简单的写出:

#include <am.h>
#include <klib-macros.h>

#define W 400
#define H 300

static uint32_t color_buf[W * H];

void draw(uint32_t color) {
    for(int i=0;i < W*H;i++)
        color_buf[i] = color;
    io_write(AM_GPU_FBDRAW, 0, 0, color_buf, W, H, true);    
}

int main() {
  ioe_init(); // initialization for GUI
  while (1) {
    draw(0x000000ff);
  }
  return 0;
}

我们可以还可以进一步通过线性插值的方法实现颜色的渐变。我们选定一个初始颜色(R0,G0,B0)(R_0,G_0,B_0)和目标颜色(Rk,Gk,Bk)(R_k,G_k,B_k),则第i时刻(i=0,1,2,…,k)需要显示的颜色为:

Ri=R0+(RkR0)ikGi=G0+(GkG0)ikBi=B0+(BkB0)ik \begin{aligned} R_i = R_0 + (R_k-R_0)\frac{i}{k} \\ G_i = G_0 + (G_k-G_0)\frac{i}{k} \\ B_i = B_0 + (B_k-B_0)\frac{i}{k} \end{aligned}
我们再以下颜色中选择目标颜色:

0x000000, 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff, 0xffffff

E4.6.6 实现颜色渐变效果

按照上述介绍, 实现颜色渐变效果. 你可以自行决定每轮渐变持续多长时间, 以及一轮渐变中的步数.

实现如下:

#include <am.h>
#include <klib-macros.h>
#include <stdlib.h> 

#define W 400
#define H 300
#define FPS 30
#define k 100

static inline uint32_t pixel(uint8_t r, uint8_t g, uint8_t b) {
  return (r << 16) | (g << 8) | b;
}
static inline uint8_t R(uint32_t p) { return p >> 16; }
static inline uint8_t G(uint32_t p) { return p >> 8; }
static inline uint8_t B(uint32_t p) { return p; }

static uint32_t color_buf[W * H];
static uint32_t color;
static uint32_t colors[8] = {0x000000, 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff, 0xffffff};
static int st,ed;
static uint32_t cur_color,nxt_color;
static bool flag;
static int step;

void draw() {
    for(int i=0;i < W*H;i++)
        color_buf[i] = color;
    io_write(AM_GPU_FBDRAW, 0, 0, color_buf, W, H, true);    
}

void updata(){
    if(flag){
        st = (st==ed) ? rand()%8 : ed;
        do{
            ed = rand()%8;
        }while(st == ed);
        flag = false;
    }
    cur_color = colors[st];
    nxt_color = colors[ed];
    float ratio = (float)step / k;
    color = pixel(
        (uint8_t)(R(cur_color) * (1 - ratio) + R(nxt_color) * ratio),
        (uint8_t)(G(cur_color) * (1 - ratio) + G(nxt_color) * ratio),
        (uint8_t)(B(cur_color) * (1 - ratio) + B(nxt_color) * ratio)
    );
}


int main() {
    ioe_init(); // initialization for GUI
    flag = true;
    st = ed = 0;
    step = 0;

    unsigned long last_update = 0;

    while (1) {
        unsigned long upt = io_read(AM_TIMER_UPTIME).us / 1000;
        if (upt - last_update > 1000 / FPS) {
            if(step<k){
                step++;
                updata();
            }else{
                step = 0;
                flag = true;
            }
            draw();  
            last_update = upt;
        }
    }
    return 0;
}

这个框架我是仿照video.c写的,一开始想不到怎么控制渲染,总之就是各种bug。然后呢main函数的这个帧率控制渲染就是参考了video.cdraw负责渲染图片,update负责实现渐变的效果,通过step和k计算出渐变的颜色,然后通过sted的相关逻辑实现自动选取颜色,并使得渐变更加丝滑。

图片无法显示渐变效果,所以就不展示了。

接下来的话就是进一步的使用按键的功能:

  • 如果按下ESC键(按键编码为AM_KEY_ESCAPE), 则退出程序
  • 如果按下其他任意键, 则加快一轮渐变的时间; 释放按键后, 渐变时间恢复

E4.6.7 添加按键效果

按照上述介绍, 为屏幕保护程序添加按键效果.

这个只需要在之前的基础上添加一个检测键盘输入的函数在循环中就好了:

我们需要current_k来控制当前的所需要的渐变次数,同时需要keydown作为flag,只有在当前有按键按下且之前没有按键按下的情况下才进一步的进入逻辑。

同时需要注意渐变进度中的ratio = step/current_k,这就意味着,为了保证按键按下时的进度一致,你需要在改变current_k的同时改变step,恢复时也同理。这样就可以避免颜色闪色的情况(本人亲身经历)

static int current_k;
static bool keydown;

void detectKey(){
    while (1) {
        AM_INPUT_KEYBRD_T ev = io_read(AM_INPUT_KEYBRD);
        if (ev.keycode == AM_KEY_NONE) break;
        if (ev.keycode == AM_KEY_ESCAPE && ev.keydown) halt(0);
        else if (ev.keydown && !keydown) {
            keydown = 1;
            current_k = k / 2;
            step /= 2;
        }
        else if (!ev.keydown && keydown) {
            keydown = 0;
            current_k = k;
            step *= 2;
        }
    }
}

E4.6.9 添加图形显示功能

根据上述思路, 为minirvEMU添加图形显示功能, 然后运行之前在Logisim上运行过的vga程序.

一些提示:

  • 为了使用AM提供的功能, 你可以参考上文屏幕保护程序的相关代码
  • 为了防止minirvEMU在显示图像后马上退出, 可以让它进入死循环

现在已经熟悉了AM的使用方法,我们现在将用它作为我们先前Logisim中的RBG Video来显示我们的图片,需要注意的地方和以前一样,需要额外定义一个数组用于存放程序将要写入的像素信息, 并在sw指令的实现中判断其地址是否落在[0x20000000, 0x20040000)若是, 则将这次写入操作写入到上述数组中. 等到程序运行结束后, 再通过AM提供的API将这些像素显示到屏幕上。

我们向先前的miniRV中添加以下关键部分:

#define ISVGA(addr) (addr >= 0x20000000 && addr <= 0x20040000)

static uint32_t canvas[256 * 256];

...
    
void inst_cycle(){
	...
    switch (inst.I.opcode){
        ...
        case 0x23:{ // Store
            uint32_t imm12 = ((inst.S.imm7 & 0x7F) << 5) | (inst.S.imm5 & 0x1F);
            int32_t imm = (imm12 & 0x800) ? (int32_t)(imm12 | 0xFFFFF000) : (int32_t)imm12;
            uint32_t addr = reg[inst.S.rs1] + imm; 
            uint32_t index = (addr - 0x20000000) >> 2;

            if(inst.S.func3 == 2){ // SW
                if(!ISVGA(addr))
                    *(uint32_t*)&mem[addr] = reg[inst.S.rs2];
                else if(ISVGA(addr)){
                    canvas[index] = reg[inst.S.rs2];
                    DEBUG("VGA write: addr=0x%x, index=%d, value=0x%x",
                          addr, index, reg[inst.S.rs2]);
                }
            }
    }
	...
}
    
int main(){
    ...
    ioe_init();
    while (status){
        inst_cycle();
        // REG_STATUS();
        io_write(AM_GPU_FBDRAW, 0, 0, canvas, 256, 256, true);
    }
}

然后在makefile中设置编译(这一步我研究了好久,最后发现只需要向其中添加``include $(AM_HOMW)/Makefile),项目会自动构建。

最终的效果如下:

到此为止,E4的任务就完成了