RISC_V GPU skybox 系列之rtlsim运行测试(2-3)中,我们介绍了1-5部分内容,这里我们继续介绍。
// 见RISC_V GPU skybox 系列之rtlsim运行测试(2)
// 见RISC_V GPU skybox 系列之rtlsim运行测试(2)
// 见RISC_V GPU skybox 系列之rtlsim运行测试(2)
// 见RISC_V GPU skybox 系列之rtlsim运行测试(3)
// 见RISC_V GPU skybox 系列之rtlsim运行测试(3)
// load program
{
std::string program_ext(fileExtension(program));
if (program_ext == "bin") {
ram.loadBinImage(program, startup_addr);
} else if (program_ext == "hex") {
ram.loadHexImage(program);
} else {
std::cout << "*** error: only *.bin or *.hex images supported." << std::endl;
return -1;
}
}
fileExtension
函数获取程序文件的扩展名。bin
,调用 ram
的 loadBinImage
方法将二进制文件加载到内存中,起始地址为 startup_addr
。hex
,调用 ram
的 loadHexImage
方法将十六进制文件加载到内存中。bin
或 hex
,输出错误信息并返回 -1。loadBinImage
// skybox/sim/common/mem.cpp
void RAM::loadBinImage(const char* filename, uint64_t destination) {
std::ifstream ifs(filename);
if (!ifs) {
std::cout << "error: " << filename << " not found" << std::endl;
std::abort();
}
ifs.seekg(0, ifs.end);
size_t size = ifs.tellg();
std::vector<uint8_t> content(size);
ifs.seekg(0, ifs.beg);
ifs.read((char*)content.data(), size);
this->clear();
this->write(content.data(), destination, size);
}
这个函数属于 RAM 类,用于从二进制文件中加载数据并写入 RAM 的指定地址。其主要步骤如下:
打开文件
使用 std::ifstream ifs(filename)
打开指定文件,如果文件打开失败,则打印错误消息并调用 std::abort()
终止程序。
获取文件大小
利用 ifs.seekg(0, ifs.end)
将文件指针移到文件末尾,再调用 ifs.tellg()
获取文件大小(以字节为单位)。
读取文件内容
创建一个 std::vector
用于存储整个文件内容,并将文件指针重置到起始位置(ifs.seekg(0, ifs.beg)
)。然后调用 ifs.read()
将文件内容读入 vector 中。
清空内存
调用 this->clear()
方法将内存清空,确保写入操作之前内存处于初始状态。
写入内存
调用 this->write(content.data(), destination, size)
将刚读取的内容写入 RAM,从指定的 destination 地址开始,写入 size 字节。
这种实现方式保证了在加载新镜像前清空内存,并将二进制文件的全部内容准确写入指定内存地址,从而常用于仿真器或模拟器中加载程序镜像。
RAM::write
void RAM::write(const void* data, uint64_t addr, uint64_t size) {
if (check_acl_ && acl_mngr_.check(addr, size, 0x2) == false) {
throw BadAddress();
}
const uint8_t* d = (const uint8_t*)data;
for (uint64_t i = 0; i < size; i++) {
*this->get(addr + i) = d[i];
}
}
这个函数实现了在 RAM 对象中写入一段数据,具体步骤如下:
访问控制检查
如果成员变量 check_acl_
为真,则通过 acl_mngr_.check(addr, size, 0x2)
检查写入权限(通常 0x2 表示写权限);如果检查失败,则抛出 BadAddress
异常。
数据写入
将传入的 data
指针转换为字节指针,然后通过循环遍历每个字节:
this->get(addr + i)
获取 RAM 中对应地址的指针,d[i]
写入到该地址。RAM::get
uint8_t *RAM::get(uint64_t address) const {
if (capacity_ != 0 && address >= capacity_) {
throw OutOfRange();
}
uint32_t page_size = 1 << page_bits_;
uint32_t page_offset = address & (page_size - 1);
uint64_t page_index = address >> page_bits_;
uint8_t* page;
if (last_page_ && last_page_index_ == page_index) {
page = last_page_;
} else {
auto it = pages_.find(page_index);
if (it != pages_.end()) {
page = it->second;
} else {
uint8_t *ptr = new uint8_t[page_size];
// set uninitialized data to "baadf00d"
for (uint32_t i = 0; i < page_size; ++i) {
ptr[i] = (0xbaadf00d >> ((i & 0x3) * 8)) & 0xff;
}
pages_.emplace(page_index, ptr);
page = ptr;
}
last_page_ = page;
last_page_index_ = page_index;
}
return page + page_offset;
}
这个函数实现了对模拟 RAM 内存的分页访问,并返回给定地址所在的字节指针。具体来说,其主要功能和步骤如下:
容量检查
页计算
page_bits_
计算页大小:uint32_t page_size = 1 << page_bits_;
uint32_t page_offset = address & (page_size - 1);
uint64_t page_index = address >> page_bits_;
页缓存机制
页查找与分配
如果没有命中缓存,则在 pages_
容器(通常是一个映射)中查找该页索引:
pages_
容器中。更新缓存变量 last_page_
和 last_page_index_
为当前页,以便下次快速访问。
返回地址指针
return page + page_offset;
总之,该函数通过分页机制管理大地址空间,在内存未分配时按需分配,并利用局部缓存加速连续访问,确保安全且高效地返回 RAM 中对应地址的指针。
loadHexImage
void RAM::loadHexImage(const char* filename) {
auto hti = [&](char c)->uint32_t {
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
return c - '0';
};
auto hToI = [&](const char *c, uint32_t size)->uint32_t {
uint32_t value = 0;
for (uint32_t i = 0; i < size; i++) {
value += hti(c[i]) << ((size - i - 1) * 4);
}
return value;
};
std::ifstream ifs(filename);
if (!ifs) {
std::cout << "error: " << filename << " not found" << std::endl;
std::abort();
}
ifs.seekg(0, ifs.end);
size_t size = ifs.tellg();
std::vector<char> content(size);
ifs.seekg(0, ifs.beg);
ifs.read(content.data(), size);
uint32_t offset = 0;
char *line = content.data();
this->clear();
while (true) {
if (line[0] == ':') {
uint32_t byteCount = hToI(line + 1, 2);
uint32_t nextAddr = hToI(line + 3, 4) + offset;
uint32_t key = hToI(line + 7, 2);
switch (key) {
case 0:
for (uint32_t i = 0; i < byteCount; i++) {
uint32_t addr = nextAddr + i;
uint32_t value = hToI(line + 9 + i * 2, 2);
*this->get(addr) = value;
}
break;
case 2:
offset = hToI(line + 9, 4) << 4;
break;
case 4:
offset = hToI(line + 9, 4) << 16;
break;
default:
break;
}
}
while (*line != '\n' && size != 0) {
++line;
--size;
}
if (size <= 1)
break;
++line;
--size;
}
}
这个函数用于从一个 HEX 格式文件中加载数据到 RAM 中,其主要功能和步骤如下:
辅助函数定义
hti
,用于将单个十六进制字符转换为对应的数值(例如 ‘A’ 转换为 10)。hToI
,用于将一个由若干个十六进制字符构成的字符串转换为一个无符号整数。它通过对每个字符调用 hti
并按位移累加实现转换。读取文件内容
std::vector
中。初始化
offset
初始化为 0,用于处理扩展地址记录(HEX 文件中记录地址高位部分)。this->clear()
清空 RAM 中的内容,为加载新数据做准备。解析 HEX 文件记录
:
开头,表示一个 HEX 记录。offset
得到实际地址(nextAddr
)。this->get(addr)
获取对应 RAM 单元,再赋值)。offset
。offset
。逐行遍历文件
line
移到下一行(以换行符为界),同时减少剩余字符数 size
。总之,该函数实现了一个基于 Intel HEX 文件格式的 RAM 镜像加载器,通过解析每条记录(数据记录和扩展地址记录),将文件中描述的数据写入到 RAM 对象的指定地址中,从而为后续仿真或系统启动提供内存镜像数据。
#ifndef NDEBUG
std::cout << "[VXDRV] START: program=" << program << std::endl;
#endif
如果程序不是在调试模式下编译的,这行代码不会被执行。在调试模式下,输出程序开始运行的信息。
// run simulation
processor.run();
调用 processor
的 run
方法,开始运行模拟程序。
void Processor::run() {
impl_->run();
}
Impl类 run函数
void run() {
#ifndef NDEBUG
std::cout << std::dec << timestamp << ": [sim] run()" << std::endl;
#endif
// start execution
running_ = true;
device_->reset = 0;
// wait on device to go busy
while (!device_->busy) {
this->tick();
}
// wait on device to go idle
while (device_->busy) {
this->tick();
}
// reset device
this->reset();
this->cout_flush();
}
这段代码是一个模拟器(或仿真平台)中运行程序的主流程,主要作用是启动并等待设备执行结束,然后复位设备并刷新输出。
调试打印
在非发布(NDEBUG 未定义)模式下,输出当前时间戳和“[sim] run()”消息,方便调试和跟踪仿真运行状态。
启动执行
等待设备进入忙碌状态
等待设备执行完成
复位设备并刷新输出
总的来说,这个 run() 函数启动仿真后,依次推进仿真周期直到设备从空闲变为忙碌,再等待到设备完成执行,最后复位设备并刷新输出信息。这样保证了仿真程序能完整地启动、执行并结束,适合用于单次仿真运行。
这里的tick函数如下:
void tick() {
device_->clk = 0;
this->eval();
this->mem_bus_eval(0);
this->dcr_bus_eval(0);
device_->clk = 1;
this->eval();
this->mem_bus_eval(1);
this->dcr_bus_eval(1);
dram_sim_.tick();
if (!dram_queue_.empty()) {
auto mem_req = dram_queue_.front();
if (dram_sim_.send_request(mem_req->write, mem_req->addr, 0, [](void* arg) {
auto orig_req = reinterpret_cast<mem_req_t*>(arg);
if (orig_req->ready) {
delete orig_req;
} else {
orig_req->ready = true;
}
}, mem_req)) {
dram_queue_.pop();
}
}
#ifndef NDEBUG
fflush(stdout);
#endif
}
dram_queue_更新
在processor.cpp
文件里,dram_queue_
是一个std::queue
类型的队列,主要用于存储待发送到 DRAM 的内存请求。下面说明dram_queue_
的入队逻辑和出队逻辑。
mem_req_t定义,
typedef struct {
bool ready;
std::array<uint8_t, MEM_BLOCK_SIZE> block;
uint64_t addr;
uint64_t tag;
bool write;
} mem_req_t;
push queue逻辑
dram_queue_
的入队操作在内存读写请求处理时进行,以下是具体的入队逻辑:
AXI_BUS
模式)if (device_->m_axi_wvalid[0]) {
// ...
auto mem_req = new mem_req_t();
mem_req->tag = device_->m_axi_awid[0];
mem_req->addr = device_->m_axi_awaddr[0];
mem_req->write = true;
mem_req->ready = false;
pending_mem_reqs_.emplace_back(mem_req);
// send dram request
dram_queue_.push(mem_req);
}
当 device_->m_axi_wvalid[0]
为 true
时,表明有写请求。此时会创建一个 mem_req_t
类型的内存请求对象,对其成员变量进行赋值,将该对象添加到 pending_mem_reqs_
列表中,同时将其入队到 dram_queue_
里。
AXI_BUS
模式)else {
// process reads
auto mem_req = new mem_req_t();
mem_req->tag = device_->m_axi_arid[0];
mem_req->addr = device_->m_axi_araddr[0];
ram_->read(mem_req->block.data(), device_->m_axi_araddr[0], MEM_BLOCK_SIZE);
mem_req->write = false;
mem_req->ready = false;
pending_mem_reqs_.emplace_back(mem_req);
// send dram request
dram_queue_.push(mem_req);
}
当 device_->m_axi_arvalid[0]
为 true
时,表明有读请求。同样会创建一个 mem_req_t
类型的内存请求对象,对其成员变量进行赋值,从 ram_
中读取数据到 mem_req->block
中,将该对象添加到 pending_mem_reqs_
列表中,最后将其入队到 dram_queue_
里。
AXI_BUS
模式)if (device_->mem_req_valid && running_ && device_->mem_req_rw) {
// ...
auto mem_req = new mem_req_t();
mem_req->tag = device_->mem_req_tag;
mem_req->addr = byte_addr;
mem_req->write = true;
mem_req->ready = true;
// send dram request
dram_queue_.push(mem_req);
}
当 device_->mem_req_valid
为 true
且 device_->mem_req_rw
为 true
时,表明有写请求。创建一个 mem_req_t
类型的内存请求对象,对其成员变量进行赋值,然后将该对象入队到 dram_queue_
里。
AXI_BUS
模式)else {
// process reads
auto mem_req = new mem_req_t();
mem_req->tag = device_->mem_req_tag;
mem_req->addr = byte_addr;
mem_req->write = false;
mem_req->ready = false;
ram_->read(mem_req->block.data(), byte_addr, MEM_BLOCK_SIZE);
pending_mem_reqs_.emplace_back(mem_req);
// send dram request
dram_queue_.push(mem_req);
}
当 device_->mem_req_valid
为 true
且 device_->mem_req_rw
为 false
时,表明有读请求。创建一个 mem_req_t
类型的内存请求对象,对其成员变量进行赋值,从 ram_
中读取数据到 mem_req->block
中,将该对象添加到 pending_mem_reqs_
列表中,最后将其入队到 dram_queue_
里。
pop queue逻辑
dram_queue_
的出队操作在 tick
函数中进行,以下是具体的出队逻辑:
void tick() {
// ...
if (!dram_queue_.empty()) {
auto mem_req = dram_queue_.front();
if (dram_sim_.send_request(mem_req->write, mem_req->addr, 0, [](void* arg) {
auto orig_req = reinterpret_cast<mem_req_t*>(arg);
if (orig_req->ready) {
delete orig_req;
} else {
orig_req->ready = true;
}
}, mem_req)) {
dram_queue_.pop();
}
}
// ...
}
在 tick
函数里,若 dram_queue_
不为空,会取出队列的队首元素 mem_req
。接着调用 dram_sim_.send_request
函数尝试将该请求发送到 DRAM 模拟器。若发送成功,dram_sim_.send_request
函数返回 true
,此时将队首元素出队。
mem_req_t
类型的内存请求对象,将其添加到 pending_mem_reqs_
列表中,并将其入队到 dram_queue_
里。tick
函数中,若 dram_queue_
不为空,会尝试将队首元素发送到 DRAM 模拟器,若发送成功则将其出队。// read exitcode from @MPM.1
ram.read(&exitcode, (IO_MPM_ADDR + 8), 4);
从内存地址 IO_MPM_ADDR + 8
处读取 4 个字节的数据,并将其存储到 exitcode
变量中。
IO_MPM_ADDR
// skybox/hw/rtl/VX_config.vh
`ifndef IO_MPM_ADDR
`define IO_MPM_ADDR (`IO_COUT_ADDR + `IO_COUT_SIZE)
`endif
`ifndef IO_COUT_ADDR
`define IO_COUT_ADDR `IO_BASE_ADDR
`endif
`define IO_COUT_SIZE 64
`ifndef IO_BASE_ADDR
`define IO_BASE_ADDR 32'h00000040
`endif
要计算 IO_MPM_ADDR
的值,需要根据代码中定义的宏逐步推导。下面是推导过程:
确定 IO_BASE_ADDR
的值:
`ifndef IO_BASE_ADDR
`define IO_BASE_ADDR 32'h00000040
`endif
由此可知,IO_BASE_ADDR
的值为 32'h00000040
。
确定 IO_COUT_ADDR
的值:
`ifndef IO_COUT_ADDR
`define IO_COUT_ADDR `IO_BASE_ADDR
`endif
因为 IO_COUT_ADDR
被定义为 IO_BASE_ADDR
,所以 IO_COUT_ADDR
的值为 32'h00000040
。
确定 IO_COUT_SIZE
的值:
`define IO_COUT_SIZE 64
可知 IO_COUT_SIZE
的值为 64
。
计算 IO_MPM_ADDR
的值:
`ifndef IO_MPM_ADDR
`define IO_MPM_ADDR (`IO_COUT_ADDR + `IO_COUT_SIZE)
`endif
将 IO_COUT_ADDR
(32'h00000040
)和 IO_COUT_SIZE
(64
)代入计算:
IO_MPM_ADDR = IO_COUT_ADDR + IO_COUT_SIZE
= 32'h00000040 + 64
= 32'h00000040 + 32'h00000040
= 32'h00000080
IO_MPM_ADDR
的值为 32'h00000080
。
return exitcode;
返回 exitcode
作为程序的退出码。
综上所述,main
函数的主要执行流程是解析命令行参数、创建内存模块和处理器、设置寄存器、加载程序、运行模拟并最终返回退出码。