当前位置: 首页 > news >正文

一生一芯学习:PA2:输入输出

一生一芯学习:PA2:输入输出

输入输出是计算机与外界交互的基本手段,只需要向设备发送一些有意义的数字信号,设备就会按照这些信号来工作。设备有自己的专属寄存器(如CPU的通用寄存器),也有自己的功能部件(如CPU的ALU)。以键盘外设为例,键盘有一个把按键的模拟信号转换成扫描码的部件,然后CPU根据扫描码就知道真实世界的用户按下了键盘的哪个按键了。除了纯粹的数据读写之外,我们还需要对设备进行控制,比如查看键盘是否有按键被按下。现在有个问题,CPU是如何访问设备的寄存器呢?答案是MMIO(內存映射I/O)

MMIO我个人理解就是把内存中一段固定的地址作为访问寄存器的接口,需要有控制判断访问的是否是这段地址,是的话就等价于访问对应的IO。这样的话CPU就可以通过普通的访存指令来访问设备。

map.h中定义了设备映射的结构体

typedef struct {const char *name;   // 设备名称paddr_t low;        // 映射区起始地址paddr_t high;       // 映射区结束地址void *space;        // 设备实际存储空间指针io_callback_t callback; // 设备回调函数
} IOMap;

map.c中,实现了映射的管理,包括I/O空间的分配和映射,还有映射的访问接口。
在源码中定义了这样两边静态变量。

static uint8_t *io_space = NULL;
static uint8_t *p_space = NULL;

其中里面的io_space是指向整个IO设备映射空间的起始地址。
p_space是指向当前可分配空间的位置,每次分配设备空间后向后移动指针,类似堆指针。

paddr_read()和paddr_write()会判断地址addr落在物理内存空间还是设备空间, 若落在物理内存空间, 就会通过pmem_read()和pmem_write()来访问真正的物理内存; 否则就通过map_read()和map_write()来访问相应的设备. 从这个角度来看, 内存和外设在CPU来看并没有什么不同, 只不过都是一个字节编址的对象而已.map_read 和 map_write 可以用统一的方式模拟各种设备的寄存器访问,并通过回调函数实现设备的特殊行为,适合单线程仿真环境,非常方便地支持各种 I/O 设备的模拟。

设备

NEMU使用SDL库来实现设备的模拟, nemu/src/device/device.c含有和SDL库相关的代码. init_device()函数主要进行以下工作:

调用init_map()进行初始化.
对上述设备进行初始化, 其中在初始化VGA时还会进行一些和SDL相关的初始化工作, 包括创建窗口, 设置显示模式等;
然后会进行定时器(alarm)相关的初始化工作. 定时器的功能在PA4最后才会用到, 目前可以忽略它.

将输入输出抽象成IOE
IOE(抽象机 I/O 设备层)提供了三个统一的 API:

bool ioe_init();
用于初始化 IOE 相关的内容。void ioe_read(int reg, void *buf);
用于从编号为 reg 的“抽象寄存器”读取内容到 buf。void ioe_write(int reg, void *buf);
用于把 buf 的内容写入编号为 reg 的“抽象寄存器”。
void ioe_read (int reg, void *buf) { ((handler_t)lut[reg])(buf); }
void ioe_write(int reg, void *buf) { ((handler_t)lut[reg])(buf); }

可以看到ioe_read``ioe_write函数都调用了lut这个函数。

static inline void screen_refresh() {io_write(AM_GPU_FBDRAW, 0, 0, NULL, 0, 0, true);
}static inline int screen_tile_height() {return io_read(AM_GPU_CONFIG).height / TILE_W;
}static inline int screen_tile_width() {return io_read(AM_GPU_CONFIG).width / TILE_W;
}

一般用法就是这样,根据amdev.h中定义的特殊寄存器及其该寄存器结构体中带的元素进行读取与写入等操作

AM_DEVREG( 1, UART_CONFIG,  RD, bool present);
AM_DEVREG( 2, UART_TX,      WR, char data);
AM_DEVREG( 3, UART_RX,      RD, char data);
AM_DEVREG( 4, TIMER_CONFIG, RD, bool present, has_rtc);
AM_DEVREG( 5, TIMER_RTC,    RD, int year, month, day, hour, minute, second);
AM_DEVREG( 6, TIMER_UPTIME, RD, uint64_t us);
AM_DEVREG( 7, INPUT_CONFIG, RD, bool present);
AM_DEVREG( 8, INPUT_KEYBRD, RD, bool keydown; int keycode);
AM_DEVREG( 9, GPU_CONFIG,   RD, bool present, has_accel; int width, height, vmemsz);
AM_DEVREG(10, GPU_STATUS,   RD, bool ready);
AM_DEVREG(11, GPU_FBDRAW,   WR, int x, y; void *pixels; int w, h; bool sync);
AM_DEVREG(12, GPU_MEMCPY,   WR, uint32_t dest; void *src; int size);
AM_DEVREG(13, GPU_RENDER,   WR, uint32_t root);
AM_DEVREG(14, AUDIO_CONFIG, RD, bool present; int bufsize);
AM_DEVREG(15, AUDIO_CTRL,   WR, int freq, channels, samples);
AM_DEVREG(16, AUDIO_STATUS, RD, int count);
AM_DEVREG(17, AUDIO_PLAY,   WR, Area buf);
AM_DEVREG(18, DISK_CONFIG,  RD, bool present; int blksz, blkcnt);
AM_DEVREG(19, DISK_STATUS,  RD, bool ready);
AM_DEVREG(20, DISK_BLKIO,   WR, bool write; void *buf; int blkno, blkcnt);
AM_DEVREG(21, NET_CONFIG,   RD, bool present);
AM_DEVREG(22, NET_STATUS,   RD, int rx_len, tx_len);
AM_DEVREG(23, NET_TX,       WR, Area buf);
AM_DEVREG(24, NET_RX,       WR, Area buf);

串口
serial.c函数中模拟了串口的功能。

static void serial_putc(char ch) {MUXDEF(CONFIG_TARGET_AM, putch(ch), putc(ch, stderr)); //如果没用am那就用标准io库,如果用了am那就用自己实现的putch
}

调用了putch的函数。
putchtrm.c中定义了函数,

void putch(char ch) { //输出一个字符outb(SERIAL_PORT, ch);
}
static inline void outb(uintptr_t addr, uint8_t  data) { *(volatile uint8_t  *)addr = data; }

时钟

timer.c模拟了i8253计时器的功能. 计时器的大部分功能都被简化, 只保留了"发起时钟中断"的功能(目前我们不会用到). 同时添加了一个自定义的时钟. i8253计时器初始化时会分别注册0x48处长度为8个字节的端口, 以及0xa0000048处长度为8字节的MMIO空间, 它们都会映射到两个32位的RTC寄存器. CPU可以访问这两个寄存器来获得用64位表示的当前时间.

amdev.h为时钟定义了两个特殊寄存器,分别叫做AM_TIMER_RTC``AM_TIMER_UPTIME分别用于读出AM实时时钟和AM系统启动时间可以用来读出系统启动的秒数。

dtrace-设备访问的痕迹

word_t map_read(paddr_t addr, int len, IOMap *map) {assert(len >= 1 && len <= 8);check_bound(map, addr);paddr_t offset = addr - map->low;  //将物理地址转换为映射区域内的相对偏移量invoke_callback(map->callback, offset, len, false); // 如果map->callback存在,调用它并传入参数(offset、len、false 表示读操作)。//callback用于模拟硬件设备的副作用(例如,读取某个寄存器可能自动清除状态位)。//map->space + offset:定位到映射区域中的目标地址。//host_read:从指针处读取 len 字节并返回 word_t 类型的地址。word_t ret = host_read(map->space + offset, len);//如果启用调试(CONFIG_DTRACE),记录读取操作的设备名、地址和长度。IFDEF(CONFIG_DTRACE, Log("read device %s : address in  = " FMT_PADDR ", len = %d\n", map->name , addr, len));return ret;
}
//其中map_read()和map_write()用于将地址addr映射到map所指示的目标空间, 并进行访问. 
//每次进行I/O读写的时候, 才会调用设备提供的回调函数(callback).
void map_write(paddr_t addr, int len, word_t data, IOMap *map) {assert(len >= 1 && len <= 8);check_bound(map, addr);paddr_t offset = addr - map->low;host_write(map->space + offset, len, data);invoke_callback(map->callback, offset, len, true);IFDEF(CONFIG_DTRACE, Log("write device %s : address in = " FMT_PADDR ", len = %d\n",  map->name , addr, len));
}

在KCONFIG中定义变量然后在读写的时候LOG出设备名字即可。

键盘
学习native的写法即可。

#define KEYDOWN_MASK 0x8000void __am_input_keybrd(AM_INPUT_KEYBRD_T *kbd) {uint32_t kc = inl(KBD_ADDR);kbd->keydown = kc & KEYDOWN_MASK ? true : false;kbd->keycode = kc & ~KEYDOWN_MASK;
}

VGA
abstract-machine/am/include/amdev.h中为GPU定义了五个抽象寄存器, 在NEMU中只会用到其中的两个:

AM_GPU_CONFIG, AM显示控制器信息, 可读出屏幕大小信息width和height. 另外AM假设系统在运行过程中, 屏幕大小不会发生变化.
AM_GPU_FBDRAW, AM帧缓冲控制器, 可写入绘图信息, 向屏幕(x, y)坐标处绘制w*h的矩形图像. 图像像素按行优先方式存储在pixels中, 每个像素用32位整数以00RRGGBB的方式描述颜色. 若sync为true, 则马上将帧缓冲中的内容同步到屏幕上.

也就是这两个寄存器,他带了一下这些参数。

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);
http://www.hskmm.com/?act=detail&tid=26152

相关文章:

  • vector使用中的一个小问题
  • OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering() - 指南
  • 2025.10.7——2绿
  • 完整教程:无人机避障——感知部分(Ubuntu 20.04 复现Vins Fusion跑数据集)胎教级教程
  • 我真的博了
  • 2025.10.6——1绿1蓝
  • 深入解析:人工智能-Chain of Thought Prompting(思维链提示,简称CoT)
  • 年龄排序
  • 二分图最大匹配 输出具体方案
  • 我的联想小新潮7000笔记本的优化
  • Go语言之接口与多态 -《Go语言实战指南》 - 指南
  • 地球科学概论
  • 2025多校冲刺CSP模拟赛4 总结
  • 多路归并、败者树、置换-选择排序、最佳归并树
  • 看vue文档记录(未整理)
  • Spring5笔记
  • 50天50个前端项目 - HTML/CSS和JavaScript实战合集
  • [BalticOI 2002] Tennis Club (Day1) 解题报告
  • 党徽
  • ZKEACMS:基于ASP.Net Core开发的开源免费内容管理系统
  • MySQL面试题汇总
  • 穷人的中国象棋打谱程序
  • 文件系统的层次结构
  • oracle 19c学习笔记2
  • 文件保护
  • 一些数数杂题
  • AI元人文:规则与人文的统一之路
  • 10.7
  • qmd 模拟赛的一道题
  • 四元数:从理论基础到实际应用的深度探索 - 教程