输入输出

实现IOE: 时钟

按照文档完成am/src/platform/nemu/ioe/timer.c即可, 代码本身很简单, 直接贴出如下:

static uint64_t boot_time = 0;

void __am_timer_init() {
  uint32_t high = inl(RTC_ADDR+4);
  uint32_t low = inl(RTC_ADDR);
  boot_time = (uint64_t)low + (((uint64_t)high) << 32);
}

void __am_timer_uptime(AM_TIMER_UPTIME_T *uptime) {
  uint32_t high = inl(RTC_ADDR+4);
  uint32_t low = inl(RTC_ADDR);
  uptime->us = (uint64_t)low + (((uint64_t)high) << 32) - boot_time;
}

int is_leap_year(int year) {
  return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

int days_in_month(int year, int month) {
  static const int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  if (month == 2 && is_leap_year(year)) {
    return 29;
  }
  return days[month - 1];
}

void __am_timer_rtc(AM_TIMER_RTC_T *rtc) {
  uint32_t high         = inl(RTC_ADDR+4);
  uint32_t low          = inl(RTC_ADDR);
  uint64_t total_second = ((uint64_t)low + (((uint64_t)high) << 32)) / 1000000;
  total_second += 8 * 3600; // Asia/Shanghai UTC+8
  uint32_t days          = total_second / 86400;
  uint32_t second_in_day = total_second % 86400;

  rtc->year = 1970;

  while(true) {
    int days_in_year = is_leap_year(rtc->year) ? 366 : 365;
    if (days >= days_in_year) {
      days -= days_in_year;
      rtc->year++;
    } else {
      break;
    }
  }

  rtc->month = 1;

  while (1) {
    int dim = days_in_month(rtc->year, rtc->month);
    if (days >= dim) {
      days -= dim;
      rtc->month++;
    } else {
      break;
    }
  }

  rtc->day    = days + 1;
  rtc->hour   = second_in_day / 3600;
  second_in_day %= 3600;
  rtc->minute = second_in_day / 60;
  rtc->second = second_in_day % 60;
}

若想查看实时时间, 可修改nemu/src/utils/timer.c中的get_time函数如下:

uint64_t get_time() {
  uint64_t now = get_time_internal();
  return now;
}

实现IOE: 按键

查看nemu的src/device/keyboard.c, 按键数据的格式实际上为:

位置意义
[7]按键状态
[6:0]按键号码

代码实现如下:

#define KEYDOWN_MASK 0x8000

void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {
  uint32_t key_code = inl(KBD_ADDR);
  kbd->keydown = key_code & KEYDOWN_MASK ? true : false;
  kbd->keycode = key_code & ~KEYDOWN_MASK;
}

实现IOE: VGA

文档提供的初始化代码提示很明确, 照着修改即可:

void __am_gpu_fbdraw(AM_GPU_FBDRAW_T *ctl) {
  uint32_t *pixels = ctl->pixels;
  for (uint32_t i = ctl->y; i < ctl->y + ctl->h; i++) {
    for (uint32_t j = ctl->x; j < ctl->x + ctl->w; j++) {
      config.fb[config.w * i + j] = pixels[ctl->w * (i - ctl->y) + (j - ctl->x)];
    }
  }

  if (ctl->sync) {
    outl(SYNC_ADDR, 1);
  }
}

实现IOE: 声卡

这是一个重量级任务, 刚开始我也一头雾水. 前面的内容都是基础教学, 到这里难度突然上升, 既要完成硬件(nemu)又要完成驱动(am).

仔细阅读文档后, 发现基本逻辑没有太大变化, 但仍然感到困惑. 于是, 我查阅了SDL声音库的使用案例, 简单了解了声卡原理.

接着, 我查看了am-testaudio_test, 发现测试只是在配置完成后向一个缓冲区不断写入数据, 与其他IOE没有太大区别. 配置过程也只是将音频参数传输给硬件.

于是, 我先完成了驱动配置部分:

/*
由下面两段代码分析__am_audio_ctrl实际功能
am-kernels/tests/am-tests/src/tests/audio.c
io_write(AM_AUDIO_CTRL, 8000, 1, 1024);

abstract-machine/am/include/amdev.h
AM_DEVREG(15, AUDIO_CTRL,   WR, int freq, channels, samples);
*/
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);
  // 未完
}

在硬件方面, 我们知道驱动已经将音频参数传输过来, 根据SDL手册说明, 我们需要进行初始化才能播放音乐. 首先创建一个audio_init函数用于初始化:

static void audio_init(void) {
  SDL_AudioSpec s = {}; // 
  s.freq = audio_base[reg_freq]; // 频率
  s.channels = audio_base[reg_channels]; // 频道
  s.samples = audio_base[reg_samples]; // 采样率
  s.callback = ...; // 播放回调函数
  ...
  SDL_InitSubSystem(SDL_INIT_AUDIO);
  SDL_OpenAudio(&s, NULL);
  SDL_PauseAudio(0);
}

这里有一个名为callback的参数, 即SDL用于定期读取音频数据的函数, 由我们定义. 其函数原型为void audio_callback(void *userdata, uint8_t *stream, int len). 若存在新的音频数据, 就将其写入stream内.

现在思路开始清晰了, 我们需要在软件完成参数传递后, 由NEMU完成SDL音频初始化, 然后软件一直向缓冲区写数据, SDL则会定时调用audio_callback读数据.

这里会产生一个问题, 我们如何知道音频是否被播放了?假如不管不顾地往里面写, 一定有概率将部分数据覆盖, 造成音频功能故障.

回看文档就会发现, 实际上已经给过提示了, 使用流缓冲区剩余大小reg_count, 来限制写入, audio_callback播放完成后才会让reg_count增加. 我们仅需要让双方都知道当前有效数据的起始位置及大小, 就能轻松保证不冲突.

首先, 在nemu这边, 定义一个变量用于标记位置, 然后根据audio_callback要求的长度len与现有数据的长度reg_count的不同:

static int audio_sbuf_flag = 0;

static void audio_callback(void *userdata, uint8_t *stream, int len) {
  int len_ctrl = len;
  if(audio_base[reg_count] < len) { // 需求数据长度大于有效数据长度
    len_ctrl = audio_base[reg_count];
  }

  if(len_ctrl + audio_sbuf_flag < CONFIG_SB_SIZE) { // 回环读取处理
    memcpy(stream, sbuf + audio_sbuf_flag, len_ctrl);
    audio_sbuf_flag += len_ctrl;
  } else {
    memcpy(stream, sbuf + audio_sbuf_flag, CONFIG_SB_SIZE - audio_sbuf_flag);
    memcpy(stream + CONFIG_SB_SIZE - audio_sbuf_flag, sbuf, len_ctrl - (CONFIG_SB_SIZE - audio_sbuf_flag));
    audio_sbuf_flag = len_ctrl - (CONFIG_SB_SIZE - audio_sbuf_flag);
  }
  audio_base[reg_count] -= len_ctrl; // 更新有效数据长度
  if (len_ctrl < len) { // 长度不足, 清除后续数据, 避免杂音
    memset(stream + len_ctrl, 0, len - len_ctrl);
  }
}

对应的, 驱动方面可以完成, 这里如同上方一样采用回环写入:

static int audio_sbuf_flag = 0;

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

  while (data_len > sbuf_size - inl(AUDIO_COUNT_ADDR)) {};
  uint8_t *buf = (uint8_t*)AUDIO_SBUF_ADDR;
  if (data_len + audio_sbuf_flag < sbuf_size) {
    memcpy(buf + audio_sbuf_flag, ctl->buf.start, data_len);
    audio_sbuf_flag += data_len;
  } else {
    memcpy(buf + audio_sbuf_flag, ctl->buf.start, sbuf_size - audio_sbuf_flag);
    memcpy(buf, ctl->buf.start + (sbuf_size - audio_sbuf_flag), data_len - (sbuf_size - audio_sbuf_flag));
    audio_sbuf_flag = data_len - (sbuf_size - audio_sbuf_flag);
  }
  int count = inl(AUDIO_COUNT_ADDR);
  count += data_len;
  outl(AUDIO_COUNT_ADDR, count);
}

双方都定义了audio_sbuf_flag, 属实没想到什么好办法, 如果你有什么好想法, 欢迎发邮件.

最后, 我们只需要知道什么时候进行初始化即可, 根据整个的音频逻辑, 在进行配置参数后, 硬件就应该完成初始化工作, 所以我们直接在audio_io_handler中这样写:

static void audio_io_handler(uint32_t offset, int len, bool is_write) {
  if (audio_base[reg_init] && !audio_init_finish && is_write) {
    audio_init_finish = true;
    audio_init();
  }
}

然后补全__am_audio_ctrl并修改__am_audio_config:

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);
  audio_sbuf_flag = 0;
}

声卡完整代码(有修改)

nemu:

static int audio_sbuf_flag = 0;

static void audio_callback(void *userdata, uint8_t *stream, int len) {
  int len_ctrl = len;
  if(audio_base[reg_count] < len) {
    len_ctrl = audio_base[reg_count];
  }
  SDL_LockAudio();
  if (len_ctrl < len) {
    memset(stream + len_ctrl, 0, len - len_ctrl);
  }
  if(len_ctrl + audio_sbuf_flag < CONFIG_SB_SIZE) {
    memcpy(stream, sbuf + audio_sbuf_flag, len_ctrl);
    audio_sbuf_flag += len_ctrl;
  } else {
    memcpy(stream, sbuf + audio_sbuf_flag, CONFIG_SB_SIZE - audio_sbuf_flag);
    memcpy(stream + CONFIG_SB_SIZE - audio_sbuf_flag, sbuf, len_ctrl - (CONFIG_SB_SIZE - audio_sbuf_flag));
    audio_sbuf_flag = len_ctrl - (CONFIG_SB_SIZE - audio_sbuf_flag);
  }
  SDL_UnlockAudio();
  audio_base[reg_count] -= len_ctrl;
}

static void audio_init(void) {
  SDL_AudioSpec s = {};
  s.freq = audio_base[reg_freq];
  s.channels = audio_base[reg_channels];
  s.samples = audio_base[reg_samples];
  s.callback = audio_callback;
  s.userdata = NULL;
  audio_base[reg_count] = 0;
  audio_base[reg_sbuf_size] = CONFIG_SB_SIZE;
  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) {
  if (audio_base[reg_init] && !audio_init_finish && is_write) {
    audio_init_finish = true;
    audio_init();
  }
}

am:

static int audio_sbuf_flag = 0;

void __am_audio_init() {
}

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);
  audio_sbuf_flag = 0;
}

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

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

  while (data_len > sbuf_size - inl(AUDIO_COUNT_ADDR)) {};
  uint8_t *buf = (uint8_t*)AUDIO_SBUF_ADDR;
  if (data_len + audio_sbuf_flag < sbuf_size) {
    memcpy(buf + audio_sbuf_flag, ctl->buf.start, data_len);
    audio_sbuf_flag += data_len;
  } else {
    memcpy(buf + audio_sbuf_flag, ctl->buf.start, sbuf_size - audio_sbuf_flag);
    memcpy(buf, ctl->buf.start + (sbuf_size - audio_sbuf_flag), data_len - (sbuf_size - audio_sbuf_flag));
    audio_sbuf_flag = data_len - (sbuf_size - audio_sbuf_flag);
  }
  int count = inl(AUDIO_COUNT_ADDR);
  count += data_len;
  outl(AUDIO_COUNT_ADDR, count);
}