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
- gettimeofday() 是老接口,通常走系统调用路径
gettimeofday()通常不是 VDSO(Virtual Dynamic Shared Object)支持的函数,每次调用都需要陷入内核态。- 在虚拟机中,系统调用开销更大,因为它需要:从用户态切换到内核态;在虚拟 CPU 和虚拟内核之间完成状态保存与恢复;可能还需和宿主机进行时钟同步。
- clock_gettime(CLOCK_MONOTONIC / CLOCK_REALTIME) 可以使用 VDSO
clock_gettime()新接口,通常是libc内部优先使用 VDSO 实现。- VDSO 是一种在用户态通过共享内存快速访问内核数据的机制,避免了系统调用带来的开销。
- 在现代 Linux(glibc + 内核 >= 2.6)中,
clock_gettime()在很多平台上都走的是 纯用户态路径,非常快。
- 虚拟化优化支持
- 虚拟机内核或 hypervisor(如 KVM)对
clock_gettime()的优化远多于对gettimeofday()。 - Hypervisor 会暴露高效的时钟源(如 TSC、paravirt clock),这些都优先被
clock_gettime()使用。
- 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)
运行演示程序
实现malloc和free。
讲义中提示很多,参考macrobench中bench_alloc和bench_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 显示控制器信息,可读出屏幕大小信息width和height。另外 AM 假设系统在运行过程中,屏幕大小不会发生变化.。AM_GPU_FBDRAW:AM 帧缓冲控制器,可写入绘图信息,向屏幕(x, y)坐标处绘制w*h的矩形图像。图像像素按行优先方式存储在pixels中,每个像素用 32 位整数以00RRGGBB的方式描述颜色。若sync为true,则马上将帧缓冲中的内容同步到屏幕上。
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);
}
// 其实很简单,就是初始化后,不断向缓冲区写入音频数据
freq,channels和samples这三个寄存器可写入相应的初始化参数;init寄存器用于初始化,写入后将根据设置好的freq,channels和samples来对 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 声卡控制寄存器,可根据写入的freq,channels和samples对声卡进行初始化;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 运行马里奥
# 成功运行,但是马里奥极其卡顿,几乎不能响应键盘操作
由于精力有限,作者并不打算优化 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 代码和教程可能并不完全符合学术诚信的要求。然而,考虑到部分网络博客和开源代码中存在较多误导性或错误的实现,本文旨在提供更加准确的参考,但这并不代表本文中所有内容的正确性,请读者自行甄别。请读者在学习过程中保持独立思考,不得照搬照抄用于课程作业或评测提交。本人仅对本文内容的学习参考价值负责,不对由此引发的任何学术或纪律后果承担责任。

本文介绍 PA 2.3 中设备的实现:包括串口,时钟,键盘,VGA和声卡。
浙公网安备 33010602011771号