PA-2.3-NEMU 中的设备

PA-2.3-NEMU 中的设备

本文介绍 PA 2.3 中设备的实现。

所有的 NEMU 设备(7种):串口,时钟,键盘,VGA,声卡,磁盘,SD卡。

串口

IOE 提供 3 个 API:

bool ioe_init();
void ioe_read(int reg, void *buf);
void ioe_write(int reg, void *buf);

运行 hello world:

make ARCH=$ISA-nemu run
make ARCH=$ISA-nemu run mainargs=I-love-PA

理解mainargs

// 找到以下这些内容后就很容易理解 mainargs 和脚本 tools/insert-arg.py 的行为了
// $ISA-NEMU
// 在文件 am/src/platform/nemu/trm.c 中
Area heap = RANGE(&_heap_start, PMEM_END);
static const char mainargs[MAINARGS_MAX_LEN] = TOSTRING(MAINARGS_PLACEHOLDER); // defined in CFLAGS
// ...
void _trm_init() {
  int ret = main(mainargs);
  halt(ret);
}
// 其中 klib/include/klib-macros.h
#define STRINGIFY(s)        #s
#define TOSTRING(s)         STRINGIFY(s)

// native
// 文件 am/src/native/platform.c
const char *args = getenv("mainargs");
halt(main(args ? args : "")); // call main here!

实现printf

// 通过缓冲区实现,也许有更标准的实现
int printf(const char *fmt, ...) {
    char buffer[1024]; // temp buffer
    va_list ap;
    va_start(ap, fmt);
    int len = vsnprintf(buffer, sizeof(buffer), fmt, ap);
    va_end(ap);

    for (int i = 0; i < len; ++i) {
        putch(buffer[i]);
    }

    return len;
}

完成alu-tests

时钟

AM_TIMER_UPTIME,AM系统启动时间,可读出系统启动后的微秒数。

// nemu/src/device/timer.c
// get_time() 在 nemu/src/utils/timer.c
// 注意 offset == 4 时才会访问,必须先访问高字节
static void rtc_io_handler(uint32_t offset, int len, bool is_write) {
  assert(offset == 0 || offset == 4);
  if (!is_write && offset == 4) {
    uint64_t us = get_time();
    rtc_port_base[0] = (uint32_t)us;
    rtc_port_base[1] = us >> 32;
  }
}
// am/src/platform/nemu/include/nemu.h
#define RTC_ADDR        (DEVICE_BASE + 0x0000048)
// 读写的操作在 am/src/riscv/riscv.h
static inline uint8_t  inb(uintptr_t addr) { return *(volatile uint8_t  *)addr; }
static inline uint16_t inw(uintptr_t addr) { return *(volatile uint16_t *)addr; }
static inline uint32_t inl(uintptr_t addr) { return *(volatile uint32_t *)addr; }

static inline void outb(uintptr_t addr, uint8_t  data) { *(volatile uint8_t  *)addr = data; }
static inline void outw(uintptr_t addr, uint16_t data) { *(volatile uint16_t *)addr = data; }
static inline void outl(uintptr_t addr, uint32_t data) { *(volatile uint32_t *)addr = data; }
// am/src/platform/nemu/ioe/timer.c
void __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) {
    uint64_t huint32 = inl(RTC_ADDR + 4); // first read high 32-bits
    uint64_t luint32 = inl(RTC_ADDR);
    uptime->us = (huint32 << 32) | luint32;
}
// 这里的访问顺序错误可能会导致 coremark 的一些错误
// See blog: https://www.cnblogs.com/cilinmengye/p/18127248

注意 timer 用到的printf需要支持位宽功能:

// %md:m为指定的输出宽度。如果数据的位数小于m,则左端补空格;若大于m,则按实际位数输出;
// %0md:同上,但这里如果数据的位数小于m,则左端补0;若大于m,则按实际位数输出。
// 具体见代码

运行real-time clock test测试:

make ARCH=$ISA-nemu run mainargs=t

跑分测试

benchmarks/
├── coremark     # 工业标准的嵌入式 CPU 性能测试程序,测试整数处理、指针操作等
├── dhrystone    # 经典的整数基准测试,用于估算 MIPS,适合轻量快速测试
└── microbench   # 自定义微基准测试集合,用于精细评估单条指令或功能单元性能
# microbench
# 运行方法
make ARCH=native run mainargs=test
make ARCH=native run mainargs=ref
# native ref 的跑分
==================================================
MicroBench PASS        33746 Marks
                   vs. 100000 Marks (i9-9900K @ 3.60GHz)
Scored time: 351.021 ms
Total  time: 416.150 ms
Exit code = 00h

# 注意关闭所有的 debug 信息和 tracer
make ARCH=$ISA-nemu run mainargs=test
make ARCH=$ISA-nemu run mainargs=ref
# riscv32-nemu ref 的跑分
==================================================
MicroBench PASS        144 Marks
                   vs. 100000 Marks (i9-9900K @ 3.60GHz)
Scored time: 129258.527 ms
Total  time: 149249.188 ms
# 使用 clock_gettime() 后
# 参考 https://stackoverflow.com/questions/42622427/gettimeofday-not-using-vdso
==================================================
MicroBench PASS        254 Marks
                   vs. 100000 Marks (i9-9900K @ 3.60GHz)
Scored time: 71681.885 ms
Total  time: 82638.324 ms
  1. gettimeofday() 是老接口,通常走系统调用路径
  • gettimeofday() 通常不是 VDSO(Virtual Dynamic Shared Object)支持的函数,每次调用都需要陷入内核态
  • 在虚拟机中,系统调用开销更大,因为它需要:从用户态切换到内核态;在虚拟 CPU 和虚拟内核之间完成状态保存与恢复;可能还需和宿主机进行时钟同步。
  1. clock_gettime(CLOCK_MONOTONIC / CLOCK_REALTIME) 可以使用 VDSO
  • clock_gettime() 新接口,通常是 libc 内部优先使用 VDSO 实现。
  • VDSO 是一种在用户态通过共享内存快速访问内核数据的机制,避免了系统调用带来的开销。
  • 在现代 Linux(glibc + 内核 >= 2.6)中,clock_gettime() 在很多平台上都走的是 纯用户态路径,非常快。
  1. 虚拟化优化支持
  • 虚拟机内核或 hypervisor(如 KVM)对 clock_gettime() 的优化远多于对 gettimeofday()
  • Hypervisor 会暴露高效的时钟源(如 TSC、paravirt clock),这些都优先被 clock_gettime() 使用。
  1. glibc 的实现差异
  • glibc 中 gettimeofday() 通常会调用 __vdso_gettimeofday,但并非所有平台都支持;
  • clock_gettime() 更常见地通过 __vdso_clock_gettime 实现,并广泛支持。
# coremark
# native
==================================================
CoreMark PASS       42961 Marks
                vs. 100000 Marks (i7-7700K @ 4.20GHz)
Exit code = 00h
# make ARCH=$ISA-nemu run
# riscv32-nemu 的跑分
Running CoreMark for 1000 iterations
2K performance run parameters for coremark.
CoreMark Size    : 666
Total time (ms)  : 13460
Iterations       : 1000
Compiler version : GCC11.4.0
seedcrc          : 0xe9f5
[0]crclist       : 0xe714
[0]crcmatrix     : 0x1fd7
[0]crcstate      : 0x8e3a
[0]crcfinal      : 0xd340
Finised in 13460 ms.
==================================================
CoreMark PASS       217 Marks
                vs. 100000 Marks (i7-7700K @ 4.20GHz)
// 实现 coremark 需要 printf 支持 %x
#define putnum(base)                                 \
    do {                                             \
        int val = va_arg(ap, int);                   \
        char numbuf[20];                             \
        itoa(val, numbuf, base);                     \
                                                     \
        int len = strlen(numbuf);                    \
        int pad = (width > len) ? (width - len) : 0; \
                                                     \
        /* pad left */                               \
        while (pad-- > 0 && pos + 1 < n) {           \
            out[pos++] = pad_char;                   \
        }                                            \
                                                     \
        for (char *p = numbuf; *p != '\0'; p++) {    \
            if (pos + 1 < n)                         \
                out[pos++] = *p;                     \
        }                                            \
    } while (0)

// vsnprintf
case 'x': {
            putnum(16);
            break;
        }
// 其实也需要 %u,但仅在报错的条件下会使用
# dhrystone
# native
Dhrystone Benchmark, Version C, Version 2.2
Trying 500000 runs through Dhrystone.
Finished in 19 ms
==================================================
Dhrystone PASS         46363 Marks
                   vs. 100000 Marks (i7-7700K @ 4.20GHz)
Exit code = 00h
# riscv32-nemu 的跑分
Dhrystone Benchmark, Version C, Version 2.2
Trying 500000 runs through Dhrystone.
Finished in 10400 ms
==================================================
Dhrystone PASS         84 Marks
                   vs. 100000 Marks (i7-7700K @ 4.20GHz)

运行演示程序

实现mallocfree

讲义中提示很多,参考macrobenchbench_allocbench_free的实现。

// klib/include/klib-macros.h
#define ROUNDUP(a, sz)      ((((uintptr_t)a) + (sz) - 1) & ~((sz) - 1))
// klib/src/stdlib.c
static char *_malloc_addr;
static bool _malloc_init = false;

void *malloc(size_t size) {
    // On native, malloc() will be called during initializaion of C runtime.
    // Therefore do not call panic() here, else it will yield a dead recursion:
    //   panic() -> putchar() -> (glibc) -> malloc() -> panic()
#if !(defined(__ISA_NATIVE__) && defined(__NATIVE_USE_KLIB__))
    // panic("Not implemented");
    if (!_malloc_init) {
        _malloc_addr = (void *)ROUNDUP(heap.start, 8);
        _malloc_init = true;
    }

    size = (size_t)ROUNDUP(size, 8);
    char *old = _malloc_addr;
    _malloc_addr += size;
    assert((uintptr_t)heap.start <= (uintptr_t)_malloc_addr && (uintptr_t)_malloc_addr < (uintptr_t)heap.end);
    for (uint64_t *p = (uint64_t *)old; p != (uint64_t *)_malloc_addr; p++) {
        *p = 0;
    }
    return old;
#endif
    return NULL;
}

修改am-kernels/kernels/demo/include/io.h中的代码,把HAS_GUI宏注释掉,演示程序就会将画图通过字符输出到终端(不必实现键盘的功能)。

为了支持aclock,需要完成AM_TIMER_RTC的功能(暂未实现)。

# 运行方法一样 RTFSC
make ARCH=$ISA-nemu run mainargs=*
demo/src/
├── aclock  # 5
├── ant     # 1
├── bf      # 8
├── cmatrix # 6
├── donut   # 7
├── galton  # 2
├── hanoi   # 3
├── life    # 4
└── main.c
# bad-apple
make ARCH=native run
make ARCH=$ISA-nemu run

关于__NATIVE_USE_KLIB__的小技巧:

这里挖个小坑,后面填上😘

dtrace

# Makefile
CFLAGS_TRACE += -DDTRACE_COND=$(if $(CONFIG_DTRACE_COND),$(call remove_quote,$(CONFIG_DTRACE_COND)),true)
# Kconfig
config DTRACE
  depends on TRACE && TARGET_NATIVE_ELF && ENGINE_INTERPRETER
  bool "Enable divices tracer"
  default y

config DTRACE_COND
  depends on DTRACE
  string "Only trace divices when the condition is true"
  default "true"
// NEMU 的 IO 读写代码在 src/device/io/map.c
word_t map_read(paddr_t addr, int len, IOMap *map) {...}
// 添加 IFDEF(CONFIG_DTRACE, dtrace_read(addr, len, map));
void map_write(paddr_t addr, int len, word_t data, IOMap *map) {...}
// 添加 IFDEF(CONFIG_DTRACE, dtrace_write(addr, len, data, map));
// include/device/map.h
typedef struct {
  const char *name;
  // we treat ioaddr_t as paddr_t here
  paddr_t low;
  paddr_t high;
  void *space;
  io_callback_t callback;
} IOMap;
// dtrace 的实现
// include/tracer.h
// ------------ dtrace ------------
#include <device/map.h>
#define DTRACE_MSG_LEN 256

typedef struct {
    char str[DTRACE_MSG_LEN];
} DtraceNode;

// src/utils/tracer/dtrace.c
#include <tracer.h>

static DtraceNode dtracer;

void dtrace_read(paddr_t addr, int len, IOMap *map) {
    snprintf(dtracer.str, DTRACE_MSG_LEN,
             "Device Name = %s : read address = " FMT_PADDR " at pc = " FMT_WORD " with byte = %d",
             map->name, addr, cpu.pc, len);
    puts(dtracer.str);
#ifdef CONFIG_DTRACE_COND
    if (DTRACE_COND) {
        log_write("%s\n", dtracer.str);
    }
#endif
}

void dtrace_write(paddr_t addr, int len, word_t data, IOMap *map) {
    snprintf(dtracer.str, DTRACE_MSG_LEN,
             "Drive Name = %s : write address = " FMT_PADDR " at pc = " FMT_WORD " with byte = %d and data = " FMT_WORD,
             map->name, addr, cpu.pc, len, data);
    puts(dtracer.str);
#ifdef CONFIG_DTRACE_COND
    if (DTRACE_COND) {
        log_write("%s\n", dtracer.str);
    }
#endif
}

键盘

abstract-machine/am/src/platform/nemu/ioe/input.c中实现AM_INPUT_KEYBRD的功能。

多个键同时被按下:使用缓冲栈。

keyboard 用到了 SDL:SDL 是一套“面向裸机硬件的统一接口层”,它帮你屏蔽不同操作系统的差异,让你用统一代码处理图形/声音/输入/计时等功能。

RTFM:SDL2 wiki,或者直接问 AI。

// src/device/device.c
case SDL_KEYDOWN:
      case SDL_KEYUP: {
        uint8_t k = event.key.keysym.scancode;
        bool is_keydown = (event.key.type == SDL_KEYDOWN);
        send_key(k, is_keydown);
// src/device/keyboard.c
#define KEYDOWN_MASK 0x8000
void send_key(uint8_t scancode, bool is_keydown) {
  if (nemu_state.state == NEMU_RUNNING && keymap[scancode] != NEMU_KEY_NONE) {
    uint32_t am_scancode = keymap[scancode] | (is_keydown ? KEYDOWN_MASK : 0);
    key_enqueue(am_scancode);
  }
}
// am/include/amdev.h
// keydown 1 bit; keycode 32 bits
AM_DEVREG( 8, INPUT_KEYBRD, RD, bool keydown; int keycode);
// am/src/platform/nemu/ioe/input.c
// keymap 的长度为 256
void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {
    uint32_t scancode = inl(KBD_ADDR);
    kbd->keydown = scancode & KEYDOWN_MASK;
    kbd->keycode = scancode & 0x00ff;
}
# 运行测试 readkey test
# am-kernels/tests/am-tests
# 注意关闭 dtrace
make ARCH=$ISA-nemu run mainargs=k

VGA

  • AM_GPU_CONFIG:AM 显示控制器信息,可读出屏幕大小信息widthheight。另外 AM 假设系统在运行过程中,屏幕大小不会发生变化.。
  • AM_GPU_FBDRAW:AM 帧缓冲控制器,可写入绘图信息,向屏幕(x, y)坐标处绘制w*h的矩形图像。图像像素按行优先方式存储在pixels中,每个像素用 32 位整数以00RRGGBB的方式描述颜色。若synctrue,则马上将帧缓冲中的内容同步到屏幕上。
vgactl_port_base = (uint32_t *)new_space(8);
// 这里的 new_space 返回 8 Bytes,vgaclt_port_base 有 0 和 1 两个。
// 从已有的代码中知道,vgactl_port_base[0] 高位存 w,低位存 h
vgactl_port_base[0] = (screen_width() << 16) | screen_height();
// 阅读 am-tests 中的 display test 测试,理解它如何获取正确的屏幕大小
// am-kernels/tests/am-tests/tests/video.c
#define N   32
int w = io_read(AM_GPU_CONFIG).width / N;
int h = io_read(AM_GPU_CONFIG).height / N;
int block_size = w * h;
// 其中 io_read() 的定义在 klib/include/klib-macros.h
#define io_read(reg) \
  ({ reg##_T __io_param; \
    ioe_read(reg, &__io_param); \
    __io_param; })
// 一处语法点
/* 在 GNU C 中,语句块表达式的最后一行表达式(即 __io_param)的值会作为整个 block 的值。
 */

// am/src/platform/nemu/ioe/gpu.c
void __am_gpu_init() {
    int i;
    int w = (inl(VGACTL_ADDR) >> 16);    // get the correct width
    int h = (inl(VGACTL_ADDR) & 0xffff); // get the correct height
    uint32_t *fb = (uint32_t *)(uintptr_t)FB_ADDR;
    for (i = 0; i < w * h; i++) fb[i] = i;
    outl(SYNC_ADDR, 1);
}
# 运行 display test 测试
make ARCH=native run mainargs=k
make ARCH=$ISA-nemu run mainargs=k

现在可以注释调__am_gpu_init()中的内容了,并实现完整的 VGA 功能。

// am/include/amdev.h
AM_DEVREG( 9, GPU_CONFIG,   RD, bool present, has_accel; int width, height, vmemsz);
AM_DEVREG(11, GPU_FBDRAW,   WR, int x, y; void *pixels; int w, h; bool sync);

// am/src/platform/nemu/ioe/gpu.c
void __am_gpu_config(AM_GPU_CONFIG_T *cfg) {
    int w = (inl(VGACTL_ADDR) >> 16);
    int h = (inl(VGACTL_ADDR) & 0xffff);

    *cfg = (AM_GPU_CONFIG_T){
        .present = true,
        .has_accel = false,
        .width = w,
        .height = h,
        .vmemsz = w * h * sizeof(uint32_t),
    };
}

void __am_gpu_fbdraw(AM_GPU_FBDRAW_T *ctl) {
    int w = ctl->w, h = ctl->h;
    int x = ctl->x, y = ctl->y;
    uint32_t *pixels = ctl->pixels;
    int width = (inl(VGACTL_ADDR) >> 16);

    int i, j;
    for (j = y; j < y + h; j++) {
        for (i = x; i < x + w; i++) {
            outl(FB_ADDR + 4 * (j * width + i), pixels[(j - y) * w + (i - x)]); // Attention: 4 *
        }
    }
    if (ctl->sync) {
        outl(SYNC_ADDR, 1);
    }
}
# 运行红白模拟机
make ARCH=native run mainargs=mario
make ARCH=$ISA-nemu run mainargs=mario
# 操作方式
* U — SELECT
* I — START
* J — A键
* K — B键
* W/S/A/D — UP/DOWN/LEFT/RIGHT
* Q — 退出
# 重新运行 kernels/demo 中的演示小程序
# 取消 HAS_GUI 的注释
# 结束运行后 Q 退出,可以用 hanoi 3 测试,运行时间较短

声卡

# 理解声卡的运行 am-kernels/tests/am-tests
make ARCH=native run mainargs=a
#include <amtest.h>

void audio_test() {
  if (!io_read(AM_AUDIO_CONFIG).present) {
    printf("WARNING: %s does not support audio\n", TOSTRING(__ARCH__));
    return;
  }

  io_write(AM_AUDIO_CTRL, 8000, 1, 1024); // 写入 freq,channel,samples

  extern uint8_t audio_payload, audio_payload_end;
  uint32_t audio_len = &audio_payload_end - &audio_payload;
  int nplay = 0;
  Area sbuf;
  sbuf.start = &audio_payload;
  while (nplay < audio_len) {
    int len = (audio_len - nplay > 4096 ? 4096 : audio_len - nplay);
    sbuf.end = sbuf.start + len;
    io_write(AM_AUDIO_PLAY, sbuf);
    sbuf.start += len;
    nplay += len;
    printf("Already play %d/%d bytes of data\n", nplay, audio_len);
  }

  // wait until the audio finishes
  while (io_read(AM_AUDIO_STATUS).count > 0);
}
// 其实很简单,就是初始化后,不断向缓冲区写入音频数据
  • freqchannelssamples这三个寄存器可写入相应的初始化参数;
  • init寄存器用于初始化,写入后将根据设置好的freqchannelssamples来对 SDL 的音频子系统进行初始化;
  • 流缓冲区STREAM_BUF是一段 MMIO 空间,用于存放来自程序的音频数据,这些音频数据会在将来写入到 SDL 库中;
  • sbuf_size寄存器可读出流缓冲区的大小;
  • count寄存器可以读出当前流缓冲区已经使用的大小。

SDL_AudioSpec

字段 是否必填 说明
freq ✅ 是 设置采样率(如 44100)
format ✅ 是 设置音频格式(如 AUDIO_S16SYS
channels ✅ 是 声道数(1单声道,2立体声)
samples ✅ 是 设置缓冲区采样点数(影响延迟)
callback ✅ 是 或设为 NULL 用 SDL_QueueAudio()
userdata ✅ 可选 一般设为 NULL
silence ❌ 否 由 SDL 自动填充,不需要手动设置
padding ❌ 否 内部使用,可以忽略
size ❌ 否 SDL 填充后表示缓冲区大小
// 初始化
// SDL_audio.h
typedef struct SDL_AudioSpec
{
    int freq;                   /**< DSP frequency -- samples per second */
    SDL_AudioFormat format;     /**< Audio data format */
    Uint8 channels;             /**< Number of channels: 1 mono, 2 stereo */
    Uint8 silence;              /**< Audio buffer silence value (calculated) */
    Uint16 samples;             /**< Audio buffer size in sample FRAMES (total samples divided by channel count) */
    Uint16 padding;             /**< Necessary for some compile environments */
    Uint32 size;                /**< Audio buffer size in bytes (calculated) */
    SDL_AudioCallback callback; /**< Callback that feeds the audio device (NULL to use SDL_QueueAudio()). */
    void *userdata;             /**< Userdata passed to callback (ignored for NULL callbacks). */
} SDL_AudioSpec;

typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream,
                                            int len);
/*
 *  \param userdata An application-specific parameter saved in
 *                  the SDL_AudioSpec structure
 *  \param stream   A pointer to the audio data buffer.
 *  \param len      The length of that buffer in bytes.
 */
// SDLCALL 是修饰符宏,为了确保跨平台的兼容性,可以忽略
// autoconfig.h
#define CONFIG_SB_SIZE 0x10000
// src/device/audio.c
#include <common.h>
#include <device/map.h>
#include <SDL2/SDL.h>

enum {
    reg_freq,      // Sampling rate (Hz), e.g., 44100
    reg_channels,  // Number of channels (1 = mono, 2 = stereo)
    reg_samples,   // Number of samples per transfer (used for SDL)
    reg_sbuf_size, // Size of the stream buffer can be read by reg (in bytes)
    reg_init,      // Write 1 to initialize, write 0 to ignore
    reg_count,     // Number of audio data bytes written
    nr_reg         // Number of registers (6 in total)
};

static uint8_t *sbuf = NULL;        // sound buffer (ringbuf)
static uint32_t sbuf_start = 0;     // first byte of sbuf
static uint32_t *audio_base = NULL; // sound regs

static void audio_callback(void *userdata, Uint8 *stream, int len) {
    int write_len = (audio_base[reg_count] < len) ? audio_base[reg_count] : len;

    SDL_LockAudio();
    if (write_len < len) { // Avoid noise!
        SDL_memset(stream + write_len, 0, len - write_len);
    }
    if (sbuf_start + write_len < CONFIG_SB_SIZE) {
        SDL_memcpy(stream, sbuf + sbuf_start, write_len);
        sbuf_start += write_len;
    } else { // This is ringbuf!
        SDL_memcpy(stream, sbuf + sbuf_start, CONFIG_SB_SIZE - sbuf_start);
        SDL_memcpy(stream + (CONFIG_SB_SIZE - sbuf_start), sbuf, write_len - (CONFIG_SB_SIZE - sbuf_start));
        sbuf_start = write_len - (CONFIG_SB_SIZE - sbuf_start);
    }
    SDL_UnlockAudio();

    audio_base[reg_count] -= write_len;
}

static void init_sound() {
    SDL_AudioSpec s = {};    // See SDL_audio.h
    s.format = AUDIO_S16SYS; // Assume the audio data format is always 16-bit signed integer
    s.userdata = NULL;       // Not used
    s.freq = audio_base[reg_freq];
    s.channels = audio_base[reg_channels];
    s.samples = audio_base[reg_samples];
    s.callback = audio_callback;

    audio_base[reg_count] = 0;
    audio_base[reg_sbuf_size] = CONFIG_SB_SIZE;
    audio_base[reg_init] = 0;

    SDL_InitSubSystem(SDL_INIT_AUDIO);
    SDL_OpenAudio(&s, NULL);
    SDL_PauseAudio(0);
}

static void audio_io_handler(uint32_t offset, int len, bool is_write) {
    assert((offset >= 0) && (offset <= 4 * nr_reg) && (offset % 4 == 0));
    if (audio_base[reg_init] == 1 && is_write && offset == reg_init * sizeof(uint32_t)) {
        audio_base[reg_init] = 0;
        init_sound();
        puts("Init success");
    }
}

void init_audio() {
    uint32_t space_size = sizeof(uint32_t) * nr_reg;
    audio_base = (uint32_t *)new_space(space_size);
#ifdef CONFIG_HAS_PORT_IO
    add_pio_map("audio", CONFIG_AUDIO_CTL_PORT, audio_base, space_size, audio_io_handler);
#else
    add_mmio_map("audio", CONFIG_AUDIO_CTL_MMIO, audio_base, space_size, audio_io_handler);
#endif

    sbuf = (uint8_t *)new_space(CONFIG_SB_SIZE);
    add_mmio_map("audio-sbuf", CONFIG_SB_ADDR, sbuf, CONFIG_SB_SIZE, NULL);
}

其中,解决并发相关问题的方法为SDL_LockAudio():其作用是在使用 SDL 音频子系统时,防止主线程和音频回调线程同时访问音频共享资源(例如缓冲区)而引起数据竞争。它是一个线程同步机制,用于在多线程环境中保护音频数据的一致性和正确性。

但我并没有感觉加上这个之后有什么区别🤣🤣🤣,可能是我实现的问题。讲义中说,需要再 PA 的多个抽象层中进行修改,但事实上我仅仅修改了 NEMU 的硬件层。

另外,SDL_memcpy也可以用SDL_MixAudio代替,后者支持一些更高级的特性。

  • AM_AUDIO_CONFIG:AM 声卡控制器信息,可读出存在标志present以及流缓冲区的大小bufsize。另外 AM 假设系统在运行过程中,流缓冲区的大小不会发生变化;
  • AM_AUDIO_CTRL:AM 声卡控制寄存器,可根据写入的freqchannelssamples对声卡进行初始化;
  • AM_AUDIO_STATUS:AM声卡状态寄存器,可读出当前流缓冲区已经使用的大小count
  • AM_AUDIO_PLAY:AM 声卡播放寄存器,可将[buf.start, buf.end)区间的内容作为音频数据写入流缓冲。若当前流缓冲区的空闲空间少于即将写入的音频数据,此次写入将会一直等待,直到有足够的空闲空间将音频数据完全写入流缓冲区才会返回。
// am/src/platform/nemu/ioe/audio.c
void __am_audio_config(AM_AUDIO_CONFIG_T *cfg) {
    cfg->bufsize = inl(AUDIO_SBUF_SIZE_ADDR);
    cfg->present = true;
}

void __am_audio_ctrl(AM_AUDIO_CTRL_T *ctrl) {
    outl(AUDIO_FREQ_ADDR, ctrl->freq);
    outl(AUDIO_CHANNELS_ADDR, ctrl->channels);
    outl(AUDIO_SAMPLES_ADDR, ctrl->samples);
    outl(AUDIO_INIT_ADDR, true);
}

void __am_audio_status(AM_AUDIO_STATUS_T *stat) {
    stat->count = inl(AUDIO_COUNT_ADDR);
}

static int _sbuf_start = 0;

void __am_audio_play(AM_AUDIO_PLAY_T *ctl) {
    int data_len = ctl->buf.end - ctl->buf.start; // in bytes
    int sbuf_size = inl(AUDIO_SBUF_SIZE_ADDR);
    // assert(data_len < sbuf_size);

    while (inl(AUDIO_COUNT_ADDR) + data_len > sbuf_size);
    if (data_len + _sbuf_start < sbuf_size) {
        for (int i = 0; i < data_len; i += 2) {
            outw(AUDIO_SBUF_ADDR + i + _sbuf_start, *(uint16_t *)(ctl->buf.start + i));
        }
        _sbuf_start += data_len;
    } else {
        for (int i = 0; i < sbuf_size - _sbuf_start; i += 2) {
            outw(AUDIO_SBUF_ADDR + i + _sbuf_start, *(uint16_t *)(ctl->buf.start + i));
        }
        for (int i = sbuf_size - _sbuf_start; i < data_len; i += 2) {
            outw(AUDIO_SBUF_ADDR + i - (sbuf_size - _sbuf_start), *(uint16_t *)(ctl->buf.start + i));
        }
        _sbuf_start = data_len - (sbuf_size - _sbuf_start);
    }

    int count = inl(AUDIO_COUNT_ADDR);
    outl(AUDIO_COUNT_ADDR, count + data_len);
}

注意软硬件要同时维护的sbuf是个环形缓冲区即可。

展示我们的计算机系统

# slider      ysyx 幻灯片
# typing-game 打字小游戏
# demo        演示程序,之前已经测试过了
# bad-apple   观看 PV,略有杂音,不知道怎么解决
# snake       贪吃蛇
# litenes     简易的红白模拟机
# fceux-am    运行马里奥
# 成功运行,但是马里奥极其卡顿,几乎不能响应键盘操作
mario

由于精力有限,作者并不打算优化 LiteNES,但我在 nemu 上运行了 nemu,这也是一件有趣的事。

# 运行打字小游戏
make ARCH=$ISA-nemu mainargs=/home/knight/ysyx/ysyx-workbench/am-kernels/kernels/typing-game/build/typing-game-$ISA-nemu.bin

声明

请读者务必遵守学术诚信原则严格来说,公开 NEMU 代码和教程可能并不完全符合学术诚信的要求。然而,考虑到部分网络博客和开源代码中存在较多误导性或错误的实现,本文旨在提供更加准确的参考,但这并不代表本文中所有内容的正确性,请读者自行甄别。请读者在学习过程中保持独立思考,不得照搬照抄用于课程作业或评测提交。本人仅对本文内容的学习参考价值负责,不对由此引发的任何学术或纪律后果承担责任。

posted @ 2025-05-31 02:34  Knight112357  阅读(258)  评论(0)    收藏  举报