实现完整的文件系统

完整文件系统只需要实现下面的几个函数:

  1. 打开文件 fs_open:

    • 遍历文件表, 查找与 pathname 匹配的文件.
    • 如果找到匹配的文件, 初始化文件的打开偏移量为0, 并返回文件描述符.
    • 如果未找到匹配的文件, 返回错误.
  2. 读取文件 fs_read:

    • 检查文件描述符是否有效.
    • 根据文件的读取函数指针调用相应的读取函数.
    • 更新文件的打开偏移量.
  3. 写入文件 fs_write:

    • 检查文件描述符是否有效.
    • 根据文件的写入函数指针调用相应的写入函数.
    • 更新文件的打开偏移量.
  4. 修改文件偏移量 fs_lseek:

    • 检查文件描述符是否有效.
    • 根据 whence 参数(SEEK_SET, SEEK_CUR, SEEK_END)更新文件的打开偏移量.
  5. 关闭文件 fs_close:

    • 检查文件描述符是否有效.
    • 重置文件的打开偏移量.

打开关闭操作只需要进行检查与初始化. 读写普通文件可以直接复用ramdisk_xxx函数, 如果是虚拟文件则调用专属的读写函数.

稍微复杂一点点的只有对文件偏移量的维护, 这里就需要仔细阅读文档, 知道偏移量在什么情况下会改变, 以及偏移量的作用.

部分代码

typedef struct {
  size_t open_offset; // 文件的打开偏移量
} SEEKinfo; // 偏移量信息结构体

static SEEKinfo file_offset[ARRLEN(file_table)] = {0}; // 初始化文件偏移量数组

int fs_open(const char *pathname, int flags, int mode) {
  Debug("%s", pathname); // 输出调试信息
  for (int i = 0; i < ARRLEN(file_table); i++) { // 遍历文件表
    if (strcmp(pathname, file_table[i].name) == 0) { // 查找匹配的文件
      Debug("file fd:%s(%d, at %d) found. Openning...", pathname, i, (int)(file_table[i].disk_offset)); // 输出调试信息
      file_offset[i].open_offset = 0; // 初始化文件的打开偏移量
      return i; // 返回文件描述符
    }
  }
  panic("file:%s not found", pathname); // 文件未找到, 抛出错误
  return -1; // 返回错误码
}

size_t fs_read(int fd, void *buf, size_t len) {
  Assert(buf != NULL, "File operation error: read the NULL"); // 检查缓冲区是否为空
  Assert(fd < ARRLEN(file_table), "file fd:%d not found", fd); // 检查文件描述符是否有效
  Debug("file fd:%d found. Reading (%d)...", fd, len); // 输出调试信息
  size_t read_len = 0; // 初始化读取长度
  if (file_offset[fd].open_offset >= file_table[fd].size && fd >= FD_END) {return 0;} // 检查是否超出文件大小
  if(file_table[fd].read != NULL) { // 如果文件有读取函数
    read_len = file_table[fd].read(buf, file_table[fd].disk_offset + file_offset[fd].open_offset, len); // 调用读取函数
  } else { // 否则使用默认读取函数
    read_len = ramdisk_read(buf, file_table[fd].disk_offset + file_offset[fd].open_offset, len); // 调用默认读取函数
    file_offset[fd].open_offset += read_len; // 更新文件的打开偏移量
  }
  return read_len; // 返回读取长度
}

size_t fs_write(int fd, const void *buf, size_t len) {
  Assert(fd < ARRLEN(file_table), "file fd:%d not found", fd); // 检查文件描述符是否有效
  Debug("file fd:%d found. Writing (%d)...", fd, len); // 输出调试信息
  size_t write_len = 0; // 初始化写入长度
  if (file_offset[fd].open_offset >= file_table[fd].size && fd >= FD_END) {assert(0); return 0;} // 检查是否超出文件大小
  if(file_table[fd].write != NULL) { // 如果文件有写入函数
    write_len = file_table[fd].write(buf, file_table[fd].disk_offset + file_offset[fd].open_offset, len); // 调用写入函数
  } else { // 否则使用默认写入函数
    write_len = ramdisk_write(buf, file_table[fd].disk_offset + file_offset[fd].open_offset, len); // 调用默认写入函数
    file_offset[fd].open_offset += write_len; // 更新文件的打开偏移量
  }
  return write_len; // 返回写入长度
}

size_t fs_lseek(int fd, size_t offset, int whence) {
  Assert(fd < ARRLEN(file_table), "file fd:%d not found", fd); // 检查文件描述符是否有效
  Debug("file fd:%d found. Seting...", fd); // 输出调试信息
  switch (whence) { // 根据 whence 参数更新文件的打开偏移量
    case SEEK_SET: file_offset[fd].open_offset = offset; break; // 从文件开头定位
    case SEEK_CUR: file_offset[fd].open_offset += offset; break; // 从当前位置定位
    case SEEK_END: file_offset[fd].open_offset = file_table[fd].size + offset; break; // 从文件末尾定位
    default: break;
  }
  Debug("Offset:%d", (int)(file_offset[fd].open_offset)); // 输出调试信息
  return file_offset[fd].open_offset; // 返回新的文件偏移量
}

int fs_close(int fd) {
  Assert(fd < ARRLEN(file_table), "file fd:%d not found", fd); // 检查文件描述符是否有效
  Debug("file fd:%d found. Closing...", fd); // 输出调试信息
  file_offset[fd].open_offset = 0; // 重置文件的打开偏移量
  return 0; // 返回成功
}

把串口抽象成文件

文档中已经提到了, 我们只需要提供相应的接口, 就能使文件系统能操控设备, 在框架代码中则是一个特殊的读写函数, 被保存在device.c中.

实现下面的函数后, 我们只需要让文件系统写入时, 不使用ramdisk_write, 改为serial_write即可.

size_t serial_write(const void *buf, size_t offset, size_t len) {
  int i = 0;
  while (i != len) {
    putch(((char *)buf)[i++]);
  }
  return len;
}

上文已经实现了自动调用虚拟文件接口的功能了. 我们只需要将其写入file_table中即可:

static Finfo file_table[] __attribute__((used)) = {
  [FD_STDIN   ] = {"stdin",           0, 0, invalid_read,  invalid_write },
  [FD_STDOUT  ] = {"stdout",          0, 0, invalid_read,  serial_write  },
  [FD_STDERR  ] = {"stderr",          0, 0, invalid_read,  serial_write  },
#include "files.h"
};

实现gettimeofday

使用io_read读取AM_TIMER_UPTIME寄存器获得时间, 按照手册的描述存入struct timeval *tv, struct timezone *tz.

int mygettimeofday(struct timeval *tv, struct timezone *tz) {
  AM_TIMER_UPTIME_T t = io_read(AM_TIMER_UPTIME);
  if (tv) {
    tv->tv_usec = t.us % (1000 * 1000);
    tv->tv_sec = t.us / (1000 * 1000);
  }
  if (tz) {
    tz->tz_minuteswest = -480;
    tz->tz_dsttime = DST_NONE;
  }
  return 0;
}

实现NDL的时钟

PA3噩梦开始, 写不完的lib

static uint64_t NDL_start_time = 0;
static struct timeval tv;
static struct timezone tz;

uint32_t NDL_GetTicks() {
  gettimeofday(&tv, &tz);
  return tv.tv_usec / 1000 + tv.tv_sec * 1000 - NDL_start_time;
}

把按键输入抽象成文件

规定按键events的格式为:

k[d/u] [$keyname]\n

k表示按键事件, d代表按下, u代表抬起, keyname是按键名.

提示

如果你想追求效率, 可以不用字符表示, 直接使用二进制编码.

使用字符只是方便调试.

我们从寄存器读取状态后, 即可写到指针对应的地址内:

size_t events_read(void *buf, size_t offset, size_t len) {
  AM_INPUT_KEYBRD_T key = io_read(AM_INPUT_KEYBRD);
  if (key.keycode == AM_KEY_NONE) {
    return -1;
  }
  return snprintf(buf, len, "k%c %s\n", key.keydown ? 'd' : 'u', keyname[key.keycode]);
}

完成后添加新的虚拟文件:

static Finfo file_table[] __attribute__((used)) = {
  [FD_STDIN   ] = { "stdin",           0, 0, invalid_read,  invalid_write },
  [FD_STDOUT  ] = { "stdout",          0, 0, invalid_read,  serial_write  },
  [FD_STDERR  ] = { "stderr",          0, 0, invalid_read,  serial_write  },
  [FD_EVENTS  ] = { "/dev/events",     0, 0, events_read,   invalid_write }, // new
#include "files.h"
};

VGA

在NDL中获取屏幕大小

与按键输入基本一直, 自己定义传输的信息格式, 然后完成对应的读取函数.

size_t dispinfo_read(void *buf, size_t offset, size_t len) {
  AM_GPU_CONFIG_T gpu = io_read(AM_GPU_CONFIG);
  return snprintf(buf, len, "WIDTH : %d\nHEIGHT : %d\n", gpu.width, gpu.height);
}

提示

记得向file_table添加新的文件

把VGA显存抽象成文件

fb_write实现

首先明确一点, fb实际上是am中是一块连续的内存空间, 映射到二位空间组成图像, 也就是说想要绘制矩形空间, 就得“一行一行”的写入, 换行时得重新计算偏移量, 那么我们可以规定: fb只支持单行写入, width、offset与len必须满足条件: (offset / sizeof(uint32_t) % width + len / sizeof(uint32_t)) < width.

size_t fb_write(const void *buf, size_t offset, size_t len) {
  AM_GPU_CONFIG_T cfg = io_read(AM_GPU_CONFIG);
  int x = offset / sizeof(uint32_t) % cfg.width;
  int y = offset / sizeof(uint32_t) / cfg.width;
  assert(x + len/sizeof(uint32_t) < cfg.width);
  io_write(AM_GPU_FBDRAW, x, y, (void*)buf, len/sizeof(uint32_t), 1, true);
  return len;
}

提示

记得向file_table添加新的文件

NDL_DrawRect & 居中显示

编程题, 直接贴代码

static int screen_w = 0, screen_h = 0;
static int cave_w = 0, cave_h = 0;

void NDL_OpenCanvas(int *w, int *h) {
  if (*w == 0 || *h == 0) {
    *w = screen_w;
    *h = screen_h;
  }
  cave_h = *h;
  cave_w = *w;
  ...
}

void NDL_DrawRect(uint32_t *pixels, int x, int y, int w, int h) {
  x = (screen_w - cave_w) / 2 + x;
  y = (screen_h - cave_h) / 2 + y;
  int fd = open("/dev/fb", 0);
  for (int i = 0; i < h; i ++) {
    lseek(fd, (x + (y + i) * screen_w) * 4, SEEK_SET);
    write(fd, &(pixels[w * i]), w * 4);
  }
  close(fd);
}

实现更多的fixedptc API

给ai秒解

fixedpt类型乘除int类型

这种类型很简单, 因为本身就是定点运算, 乘除整数直接乘除即可.

static inline fixedpt fixedpt_muli(fixedpt A, int B) {
	return (A * B);
}

static inline fixedpt fixedpt_divi(fixedpt A, int B) {
	return (A / B);
}

fixedpt类型之间乘除

我们可以先假设被乘/除数的小数部分都是0, 也就是没有小数部分.

那么与上方fixedpt类型乘除int类型相比, 仅仅只是被乘/除数位移了8位, 考虑小数部分的话实际也是成立的, 那么我们只需要在乘除运算中插入位移保证“小数点”位置就行.

提示

乘除法位移的先后不同为了保证精度不丢失

// 先乘后位移
static inline fixedpt fixedpt_mul(fixedpt A, fixedpt B) {
	return ((fixedpt)(((fixedptd)A * (fixedptd)B) >> FIXEDPT_FBITS));
}

// 先位移后除
static inline fixedpt fixedpt_div(fixedpt A, fixedpt B) {
	return ((fixedpt)(((fixedptd)A << FIXEDPT_FBITS) / (fixedptd)B));
}

其他运算

  • abs: fixedpt本身就是int类型, 可以直接使用比较判断, 然后进行反转
  • floor: 向下取整, 直接清零小数部分即可
  • ceil: 判断小数部分是否为零, 不是的话清零小数部分后加1
static inline fixedpt fixedpt_abs(fixedpt A) {
	return (A < 0 ? -A : A);
}

static inline fixedpt fixedpt_floor(fixedpt A) {
	return (A & ~FIXEDPT_FMASK);
}

static inline fixedpt fixedpt_ceil(fixedpt A) {
	return ((A & FIXEDPT_FMASK) ? ((A + FIXEDPT_ONE) & ~FIXEDPT_FMASK) : A);
}

miniSDL

直接贴代码了, 没修改的代码省略, 有时间补上解析, 标记一下先

// TODO: Documentation to be improved

// navy-apps/libs/libminiSDL/src/video.c
void SDL_BlitSurface(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect) {
  assert(dst && src);
  assert(dst->format->BitsPerPixel == src->format->BitsPerPixel);
  SDL_Rect dst_r = { .x = 0, .y = 0, .w = dst->w, .h = dst->h }, 
    src_r = { .x = 0, .y = 0, .w = src->w, .h = src->h };
  if (srcrect) {
    src_r = *srcrect;
  }
  if (dstrect) {
    dst_r = *dstrect;
  }
  switch (dst->format->BitsPerPixel) {
    case 32:
      for (uint32_t i = 0; i < src_r.h; i ++) {
        for (uint32_t j = 0; j < src_r.w; j ++) {
          ((uint32_t *)dst->pixels)[(i + dst_r.y) * dst->w + j + dst_r.x] = 
            ((uint32_t *)src->pixels)[(i + src_r.y) * src->w + j + src_r.x];
        }
      }
      break;
    case 8:
      for (uint32_t i = 0; i < src_r.h; i ++) {
        for (uint32_t j = 0; j < src_r.w; j ++) {
          dst->pixels[(i + dst_r.y) * dst->w + j + dst_r.x] = 
            src->pixels[(i + src_r.y) * src->w + j + src_r.x];
        }
      }
      break;
    default: break;
  }
}

void SDL_FillRect(SDL_Surface *dst, SDL_Rect *dstrect, uint32_t color) {
  SDL_Rect dst_r = { .x = 0, .y = 0, .w = dst->w, .h = dst->h };
  if (dstrect) {
    dst_r = *dstrect;
  }
  switch (dst->format->BitsPerPixel) {
    case 32:
      for (int i = 0; i < dst_r.h; i ++) {
        for (int j = 0; j < dst_r.w; j ++) {
          ((uint32_t *)dst->pixels)[(i + dst_r.y) * dst->w + j + dst_r.x] = color;
        }
      }
      break;
    case 8:
      for (int i = 0; i < dst_r.h; i ++) {
        for (int j = 0; j < dst_r.w; j ++) {
          dst->pixels[(i + dst_r.y) * dst->w + j + dst_r.x] = color;
        }
      }
      break;
    default: break;
  }
}

void SDL_UpdateRect(SDL_Surface *s, int x, int y, int w, int h) {
  if (w == 0 || h == 0) {
    w = s->w;
    h = s->h;
  }
  uint32_t local_pixels[WINDOW_W * WINDOW_H];
  switch (s->format->BitsPerPixel) {
    case 32: NDL_DrawRect((uint32_t *)(s->pixels), x, y, w, h); break;
    case 8: 
      for (int i = 0; i < h; i ++) {
        for (int j = 0; j < w; j ++) {
          local_pixels[i * w + j] = s->format->palette->colors[s->pixels[(i + y) * s->w + j + x]].val;
        }
      }
      NDL_DrawRect(local_pixels, x, y, w, h);
      break;
    default: break;
  }
}

// navy-apps/libs/libminiSDL/src/timer.c
uint32_t SDL_GetTicks() {
  return NDL_GetTicks();
}

// navy-apps/libs/libminiSDL/src/event.c
#define KEYNUM sizeof(keyname) / sizeof(keyname[0])
uint8_t key_status[KEYNUM] = {0};

int SDL_PollEvent(SDL_Event *ev) {
  char buf[64];
  if (NDL_PollEvent(buf, sizeof(buf))) {
    if (buf[0] == 'k') {
      char key_type;
      char key_buf[32];
      assert(sscanf(buf, "k%c %s", &key_type, key_buf) == 2);
      switch (key_type) {
        case 'u': ev->type = SDL_KEYUP; break;
        case 'd': ev->type = SDL_KEYDOWN; break;
        default: assert(0); break;
      }
      for (int i = 1; i < KEYNUM; i ++) {
        if (strcmp(keyname[i], key_buf) == 0) {
          ev->key.keysym.sym = i;
          key_status[i] = !ev->type;
          return 1;
        }
      }
    }
  }
}

int SDL_WaitEvent(SDL_Event *ev) {
  while (true) {
    if (SDL_PollEvent(ev)) {
      return 1;
    }
  }
  return 0;
}

uint8_t* SDL_GetKeyState(int *numkeys) {
  return key_status;
}

// navy-apps/libs/libSDL_image/src/image.c
SDL_Surface* IMG_Load(const char *filename) {
  FILE* fp = fopen(filename, "r");
  assert(fp);
  fseek(fp, 0, SEEK_END);
  size_t size = ftell(fp);
  fseek(fp, 0, SEEK_SET);
  char *img_mem = SDL_malloc(size * sizeof(char));
  assert(img_mem);
  fread(img_mem, size, 1, fp);
  SDL_Surface * IMG = STBIMG_LoadFromMemory(img_mem, size);
  SDL_free(img_mem);
  fclose(fp);
  assert(IMG);
  return IMG;
}