深入解析 FESVR(Front-End Server)
url: https://github.com/riscv/riscv-isa-sim.git
commid: fcbdbe7946079650d0e656fa3d353e3f652d471f
目录
- FESVR 概述
- FESVR 代码结构分析
- ELF 加载机制
在RISC-V ISA Simulator系列之fesvr<1-6>中我们已经完成了
1. FESVR 概述
2. FESVR 代码结构分析
1. ELF 相关文件
2. HTIF(Host-Target Interface)
3. 设备模拟
4. 系统调用
5. 调试和仿真管理
内容,下面我们继续完成下列内容。
2. FESVR 代码结构分析
fesvr
的代码主要位于 riscv-isa-sim/fesvr/
目录,主要文件及作用如下:
fesvr
目录下的源码文件清单,涉及 ELF 加载、设备仿真、HTIF 交互、系统调用 等多个模块。下面是各文件的简要介绍:
6. FESVR 相关
fesvr.ac
/fesvr_dpi.cc
/fesvr.mk.in
/fesvr.pc.in
:构建系统相关文件,用于 SystemVerilog DPI(Direct Programming Interface)。option_parser.h
/option_parser.cc
:解析命令行参数。
6.1 fesvr.ac
/ fesvr_dpi.cc
/ fesvr.mk.in
/ fesvr.pc.in
fesvr.pc.in
- 文件概述
fesvr.pc.in
是一个用于模块化 C++ 构建系统的包配置文件模板,其主要作用是为使用 fesvr
库的项目提供必要的配置信息,比如库的路径、版本号、依赖项等。文件中使用了一些占位符(如 @prefix@
、@PACKAGE_VERSION@
等),这些占位符会在构建过程中被实际的值替换。
- 文件详细分析
2.1 文件头部注释
#=========================================================================
# Modular C++ Build System Subproject Package Config
#=========================================================================
# Please read the documenation in 'mcppbs-uguide.txt' for more details
# on how the Modular C++ Build System works.
这部分注释说明了文件的用途,即用于模块化 C++ 构建系统的子项目包配置,同时提示用户阅读 mcppbs-uguide.txt
文档以了解构建系统的详细工作原理。
2.2 通用变量部分
#-------------------------------------------------------------------------
# Generic variables
#-------------------------------------------------------------------------
prefix=@prefix@
include_dir=${prefix}/include/fesvr
lib_dir=${prefix}/lib
prefix
:这是一个占位符,在构建过程中会被替换为实际的安装前缀路径。include_dir
:基于prefix
变量,定义了fesvr
库头文件的包含路径。lib_dir
:同样基于prefix
变量,定义了fesvr
库文件的路径。
2.3 关键字部分
#-------------------------------------------------------------------------
# Keywords
#-------------------------------------------------------------------------
Name : fesvr
Version : @PACKAGE_VERSION@
Description : Frontend Server C/C++ API
Requires : @fesvr_pkcdeps@
Cflags : -I${include_dir} @CPPFLAGS@ @fesvr_extra_cppflags@
Libs : -L${lib_dir} @LDFLAGS@ @fesvr_extra_ldflags@ \
-lfesvr @fesvr_extra_libs@
- Name:指定了包的名称为
fesvr
。 - Version:使用占位符
@PACKAGE_VERSION@
,在构建时会被替换为实际的版本号。 - Description:对
fesvr
库的简要描述,表明它是前端服务器的 C/C++ API。 - Requires:使用占位符
@fesvr_pkcdeps@
,表示fesvr
库所依赖的其他包,构建时会被替换为实际的依赖项。 - Cflags:编译时需要的额外标志。其中
-I${include_dir}
用于指定fesvr
库头文件的包含路径,@CPPFLAGS@
和@fesvr_extra_cppflags@
是占位符,会被替换为实际的编译标志。 - Libs:链接时需要的额外标志。
-L${lib_dir}
用于指定fesvr
库文件的搜索路径,@LDFLAGS@
和@fesvr_extra_ldflags@
是占位符,会被替换为实际的链接标志。-lfesvr
表示链接fesvr
库,@fesvr_extra_libs@
是占位符,会被替换为其他需要链接的库。
- 总结
fesvr.pc.in
文件是一个用于模块化 C++ 构建系统的包配置模板,它通过占位符的方式,为不同的构建环境提供了灵活的配置选项。在构建过程中,这些占位符会被替换为实际的值,从而生成最终的包配置文件,供其他项目使用 fesvr
库时参考。
fesvr_dpi.cc
fesvr_dpi.cc
文件主要用于实现与外部硬件接口(如 SystemVerilog DPI)交互的功能,负责加载 ELF 文件并处理其内容,将文件中的段信息和符号信息提供给外部使用。
- 头文件包含
#include "config.h"
#include "elf.h"
#include "htif.h"
#include "htif_hexwriter.h"
#include "elfloader.h"
#include "memif.h"
#include "byteorder.h"
#include <cstring>
#include <string>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <assert.h>
#include <unistd.h>
#include <stdexcept>
#include <stdlib.h>
#include <stdio.h>
#include <vector>
#include <map>
#include <iostream>
#include <svdpi.h>
包含了项目自定义的头文件和标准库头文件,用于实现 ELF 文件加载、内存操作、错误处理等功能,同时引入了 SystemVerilog DPI 接口。
- 全局变量定义
std::string loaded_binary;
std::map<reg_t, reg_t> sections;
std::map<std::string, uint64_t> symbols;
std::map<reg_t, std::vector<uint8_t>> mems;
memif_t* memif;
htif_hexwriter_t *htif;
reg_t* entry;
int section_index = 0;
loaded_binary
:存储当前加载的 ELF 文件的文件名。sections
:存储 ELF 文件中各个段的起始地址和长度。symbols
:存储 ELF 文件中的符号表,键为符号名,值为符号地址。mems
:存储各个段的内存内容。memif
:内存接口对象指针,用于进行内存读写操作。htif
:硬件接口对象指针,用于与硬件进行交互。entry
:程序入口地址指针。section_index
:用于遍历sections
时记录当前位置。
dpi_memif_t
类
class dpi_memif_t : public memif_t {
public:
dpi_memif_t (htif_t* htif) : memif_t(htif), htif(htif) {}
void write(addr_t taddr, size_t len, const void* src) override
{
memif_t::write(taddr, len, src);
sections[taddr] = len;
uint64_t datum;
uint8_t* buf = (uint8_t*) src;
std::vector<uint8_t> mem;
for (int i = 0; i < len; i++) {
mem.push_back(buf[i]);
}
mems.insert(std::make_pair(taddr, mem));
}
void read(addr_t addr, size_t len, void* bytes) override
{
memif_t::read(addr, len, bytes);
}
private:
htif_t* htif;
};
- 继承自
memif_t
类,用于实现内存读写操作。 write
方法:在调用基类的write
方法后,将段的起始地址和长度记录到sections
中,并将段的内存内容存储到mems
中。read
方法:直接调用基类的read
方法。
get_section
函数
extern "C" char get_section (long long* address, long long* len) {
if (section_index < sections.size()) {
auto it = sections.begin();
for( int i = 0; i < section_index; i++ , it++);
*address = it->first;
*len = it->second;
section_index++;
return 1;
} else return 0;
}
- 用于获取 ELF 文件中各个段的起始地址和长度。
- 通过
section_index
遍历sections
映射,将当前段的起始地址和长度存储到address
和len
指针中,并将section_index
加 1。 - 如果还有更多段未处理,返回 1;否则返回 0。
read_section_void
函数
extern "C" void read_section_void (long long address, void* buffer, uint64_t size = 0) {
assert(mems.count(address) > 0);
auto it = mems.find(address);
if (it == mems.end())
return;
memif->read(address, (size == 0) ? sections[address] : size , buffer);
}
- 用于从指定地址读取段的内存内容到
buffer
中。 - 首先检查指定地址是否存在于
mems
中,若存在则调用memif
的read
方法进行读取。 - 如果
size
为 0,则读取整个段的长度;否则读取指定的size
字节。
read_section_sv
函数
extern "C" void read_section_sv (long long address, const svOpenArrayHandle buffer) {
void* buf = svGetArrayPtr(buffer);
assert(mems.count(address) > 0);
int i = 0;
for (auto &datum : mems.find(address)->second) {
*((char *) buf + i) = datum;
i++;
}
}
- 用于从指定地址读取段的内存内容到 SystemVerilog 数组中。
- 通过
svGetArrayPtr
函数获取 SystemVerilog 数组的指针,然后将指定地址的段内容逐字节复制到该数组中。
read_symbol
函数
extern "C" char read_symbol (const char* symbol_name, long long* address) {
std::string symbol_str(symbol_name);
auto it = symbols.find(symbol_name);
if (it != symbols.end()) {
*address = it->second;
return 0;
}
return 1;
}
- 用于根据符号名查找符号地址。
- 在
symbols
映射中查找指定符号名,如果找到则将符号地址存储到address
指针中,并返回 0;否则返回 1。
read_elf
函数
extern "C" void read_elf(const char* filename) {
std::cout << "Starting read_elf function with filename: " << filename << std::endl;
loaded_binary = filename;
htif = new htif_hexwriter_t(0x0, 1, -1);
entry = new reg_t;
memif = new dpi_memif_t((htif_t*) htif);
symbols = load_elf(filename, memif, entry);
}
- 用于加载 ELF 文件。
- 输出加载信息,将文件名存储到
loaded_binary
中。 - 创建
htif_hexwriter_t
对象和reg_t
对象,并创建dpi_memif_t
对象。 - 调用
load_elf
函数加载 ELF 文件,将符号表存储到symbols
中。
总结
该文件实现了与外部硬件接口交互的功能,包括加载 ELF 文件、处理段信息和符号信息,并提供了相应的接口函数供外部调用。通过 dpi_memif_t
类实现了内存读写操作,同时使用 sections
、symbols
和 mems
等映射存储 ELF 文件的相关信息。
fesvr.mk.in
fesvr.mk.in
是一个用于构建系统的 Makefile 模板文件,其中 .in
扩展名通常表示这是一个配置文件模板,在构建过程中会通过工具(如 autoconf
)进行处理,生成最终的 Makefile。
该文件主要定义了 FESVR(Front - End Server)相关的安装头文件、源文件、安装配置等信息,这些信息将用于指导 Makefile 进行编译和安装操作。
- 占位符
fesvr_subproject_deps = \
这是一个占位符,在后续的配置过程中会被替换为具体的内容,例如一些环境变量的设置或者其他必要的配置信息。
- 安装头文件列表
fesvr_install_hdrs = \
byteorder.h \
elf.h \
elfloader.h \
htif.h \
dtm.h \
memif.h \
syscall.h \
context.h \
htif_pthread.h \
htif_hexwriter.h \
option_parser.h \
term.h \
device.h \
rfb.h \
tsi.h \
fesvr_install_hdrs
是一个变量,用于指定需要安装的头文件列表。- 这些头文件包含了 FESVR 模块所需的各种数据结构和函数声明,在编译过程中会被其他源文件引用。
- 反斜杠
\
用于换行,使列表更易读。
- 安装配置头文件标志
fesvr_install_config_hdr = yes
fesvr_install_config_hdr
是一个布尔型变量,设置为yes
表示需要安装配置头文件。配置头文件通常包含一些编译时的配置信息,如宏定义、常量等。
- 安装库文件标志
fesvr_install_lib = yes
fesvr_install_shared_lib = yes
fesvr_install_lib
和fesvr_install_shared_lib
分别表示是否安装静态库和共享库。设置为yes
表示需要安装这两种类型的库文件。
- 源文件列表
fesvr_srcs = \
elfloader.cc \
fesvr_dpi.cc \
htif.cc \
memif.cc \
dtm.cc \
syscall.cc \
device.cc \
rfb.cc \
context.cc \
htif_pthread.cc \
htif_hexwriter.cc \
dummy.cc \
option_parser.cc \
term.cc \
tsi.cc \
SimDTM.cc \
fesvr_srcs
是一个变量,用于指定 FESVR 模块的源文件列表。- 这些源文件包含了 FESVR 模块的具体实现代码,在编译过程中会被编译成目标文件,最终链接成库文件或可执行文件。
- 安装程序源文件列表
fesvr_install_prog_srcs = \
elf2hex.cc \
fesvr_install_prog_srcs
是一个变量,用于指定需要编译并安装的程序的源文件列表。elf2hex.cc
可能是一个将 ELF 格式文件转换为十六进制文件的程序的源文件。
总结
fesvr.mk.in
文件主要定义了 FESVR 模块的构建和安装相关的信息,包括需要安装的头文件、库文件、源文件以及可执行程序的源文件等。在构建过程中,这些信息将被用于生成最终的 Makefile,从而指导编译和安装操作。
fesvr.ac
fesvr.ac
是一个使用 Autoconf 宏的配置文件,Autoconf 用于生成可移植的 configure
脚本,帮助软件在不同的系统上进行配置和编译。
- 检查
libpthread
库
AC_CHECK_LIB(pthread, pthread_create, [], [AC_MSG_ERROR([libpthread is required])])
- 功能:检查系统中是否存在
pthread
库,并且该库是否包含pthread_create
函数。 - 参数解释:
pthread
:要检查的库名。pthread_create
:用于验证库是否可用的函数名。[]
:如果库和函数都存在时执行的操作,这里为空表示不执行额外操作。[AC_MSG_ERROR([libpthread is required])]
:如果库或函数不存在,打印错误信息并终止配置过程,提示用户需要libpthread
库。
- 检查
struct statx.stx_ino
成员
AC_CHECK_MEMBER(struct statx.stx_ino,
AC_DEFINE_UNQUOTED(HAVE_STATX, 1, [Define to 1 if struct statx exists.]),
,
)
- 功能:检查系统中
struct statx
结构体是否包含stx_ino
成员。 - 参数解释:
struct statx.stx_ino
:要检查的结构体成员。AC_DEFINE_UNQUOTED(HAVE_STATX, 1, [Define to 1 if struct statx exists.])
:如果结构体成员存在,定义一个宏HAVE_STATX
并将其值设为 1,同时添加注释说明该宏的用途。- 后面两个空参数分别表示如果结构体成员不存在时执行的操作和额外的检查代码,这里为空表示不执行额外操作。
- 检查
struct statx.stx_mnt_id
成员
AC_CHECK_MEMBER(struct statx.stx_mnt_id,
AC_DEFINE_UNQUOTED(HAVE_STATX_MNT_ID, 1, [Define to 1 if struct statx has stx_mnt_id.]),
,
)
- 功能:检查系统中
struct statx
结构体是否包含stx_mnt_id
成员。 - 参数解释:
struct statx.stx_mnt_id
:要检查的结构体成员。AC_DEFINE_UNQUOTED(HAVE_STATX_MNT_ID, 1, [Define to 1 if struct statx has stx_mnt_id.])
:如果结构体成员存在,定义一个宏HAVE_STATX_MNT_ID
并将其值设为 1,同时添加注释说明该宏的用途。- 后面两个空参数分别表示如果结构体成员不存在时执行的操作和额外的检查代码,这里为空表示不执行额外操作。
总结
该文件主要完成了两个重要的配置检查:
- 确保系统中存在
libpthread
库,因为项目可能依赖该库来实现多线程功能。 - 检查系统中
struct statx
结构体的特定成员是否存在,并根据检查结果定义相应的宏,以便在后续的代码中根据这些宏来处理不同系统的兼容性问题。
这些检查有助于确保项目在不同的系统环境下能够正确配置和编译。
6.2 option_parser.h
/ option_parser.cc
option_parser.h
option_parser.h
文件定义了一个用于解析命令行选项的类 option_parser_t
。该类提供了一种机制,允许用户定义命令行选项及其对应的处理函数,并解析命令行参数。
- 头文件保护
// See LICENSE for license details.
#ifndef _OPTION_PARSER_H
#define _OPTION_PARSER_H
- 这是典型的头文件保护机制,防止头文件被重复包含。如果
_OPTION_PARSER_H
未被定义,则定义该宏并包含头文件内容;否则跳过。
- 头文件包含
#include <vector>
#include <functional>
#include <vector>
:引入标准库中的std::vector
容器,用于存储选项信息。#include <functional>
:引入std::function
,它是一个通用的多态函数包装器,用于存储、复制和调用任何可调用对象,这里用于存储选项对应的处理函数。
option_parser_t
类定义
类的公共部分
class option_parser_t
{
public:
option_parser_t() : helpmsg(0) {}
void help(void (*helpm)(void)) { helpmsg = helpm; }
void option(char c, const char* s, int arg, std::function<void(const char*)> action);
const char* const* parse(const char* const* argv0);
- 构造函数
option_parser_t()
:初始化类的对象,将helpmsg
指针初始化为0
,helpmsg
用于存储帮助信息的打印函数。 help
方法:接受一个函数指针helpm
,该函数无参数和返回值,将其赋值给helpmsg
,用于后续打印帮助信息。option
方法:- 参数
char c
:表示选项的短名称,例如-h
中的h
。 - 参数
const char* s
:表示选项的长名称,例如--help
。 - 参数
int arg
:表示选项是否需要参数,具体含义可能根据实现而定。 - 参数
std::function<void(const char*)> action
:表示选项对应的处理函数,该函数接受一个const char*
类型的参数。
- 参数
parse
方法:接受一个指向const char*
数组的指针argv0
,通常是main
函数的argv
参数,用于解析命令行参数,并返回未处理的参数。
类的私有部分
private:
struct option_t
{
char chr;
const char* str;
int arg;
std::function<void(const char*)> func;
option_t(char chr, const char* str, int arg, std::function<void(const char*)> func)
: chr(chr), str(str), arg(arg), func(func) {}
};
std::vector<option_t> opts;
void (*helpmsg)(void);
void error(const char* msg, const char* argv0, const char* arg);
option_t
结构体:用于存储每个选项的信息。char chr
:选项的短名称。const char* str
:选项的长名称。int arg
:选项是否需要参数。std::function<void(const char*)> func
:选项对应的处理函数。- 构造函数
option_t
:用于初始化结构体的成员变量。
std::vector<option_t> opts
:存储所有定义的选项。void (*helpmsg)(void)
:指向帮助信息打印函数的指针。error
方法:用于处理解析过程中出现的错误,接受错误信息msg
、程序名称argv0
和出错的参数arg
。
- 头文件结束
#endif
- 结束头文件保护块。
总结
option_parser.h
文件定义了一个命令行选项解析器类 option_parser_t
,通过该类可以定义选项及其处理函数,并解析命令行参数。它使用 std::vector
存储选项信息,std::function
存储处理函数,同时提供了错误处理和帮助信息打印的接口。
option_parser.cc
option_parser.cc
文件实现了一个命令行选项解析器,用于解析命令行参数。该解析器支持短选项(如 -h
)和长选项(如 --help
),并且可以为每个选项指定是否需要参数以及对应的处理动作。
- 头文件包含
#include "option_parser.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
option_parser.h
:自定义的头文件,包含option_parser_t
类的声明。<cstdio>
、<cstdlib>
、<cstring>
和<cassert>
:标准库头文件,提供输入输出、内存管理、字符串处理和断言功能。
option_parser_t::option
函数
void option_parser_t::option(char c, const char* s, int arg, std::function<void(const char*)> action)
{
opts.push_back(option_t(c, s, arg, action));
}
- 功能:向选项解析器中添加一个选项。
- 参数:
c
:短选项字符,如-h
中的h
。s
:长选项字符串,如--help
中的help
。arg
:表示该选项是否需要参数,非零值表示需要参数。action
:一个std::function
对象,用于处理该选项,接受一个const char*
类型的参数,即选项的参数值。
- 实现:创建一个
option_t
对象,并将其添加到opts
向量中。
option_parser_t::parse
函数
const char* const* option_parser_t::parse(const char* const* argv0)
{
assert(argv0);
const char* const* argv = argv0 + 1;
for (const char* opt; (opt = *argv) != NULL && opt[0] == '-'; argv++)
{
bool found = false;
for (auto it = opts.begin(); !found && it != opts.end(); it++)
{
size_t slen = it->str ? strlen(it->str) : 0;
bool chr_match = opt[1] != '-' && it->chr && opt[1] == it->chr;
bool str_match = opt[1] == '-' && slen && strncmp(opt+2, it->str, slen) == 0;
if (chr_match || (str_match && (opt[2+slen] == '=' || opt[2+slen] == '\0')))
{
const char* optarg =
chr_match ? (opt[2] ? &opt[2] : NULL) :
opt[2+slen] ? &opt[3+slen] :
it->arg ? *(++argv) : NULL;
if (optarg && !it->arg)
error("no argument allowed for option", *argv0, opt);
if (!optarg && it->arg)
error("argument required for option", *argv0, opt);
it->func(optarg);
found = true;
}
}
if (!found)
error("unrecognized option", *argv0, opt);
}
return argv;
}
- 功能:解析命令行参数。
- 参数:
argv0
:命令行参数数组的起始指针。
- 实现:
- 跳过程序名(
argv0[0]
),从argv0 + 1
开始解析参数。 - 遍历参数数组,直到遇到非选项参数(不以
-
开头)或参数数组结束。 - 对于每个选项,遍历
opts
向量,检查是否有匹配的选项。 - 如果匹配成功,根据选项类型(短选项或长选项)提取选项的参数值。
- 检查参数是否符合要求(是否需要参数),如果不符合则调用
error
函数处理错误。 - 调用选项对应的处理动作
it->func(optarg)
。 - 如果没有找到匹配的选项,调用
error
函数处理错误。 - 返回未处理的参数数组指针。
- 跳过程序名(
option_parser_t::error
函数
void option_parser_t::error(const char* msg, const char* argv0, const char* arg)
{
fprintf(stderr, "%s: %s %s\n", argv0, msg, arg ? arg : "");
if (helpmsg) helpmsg();
exit(1);
}
- 功能:处理解析过程中出现的错误。
- 参数:
msg
:错误信息。argv0
:程序名。arg
:出错的选项。
- 实现:
- 将错误信息输出到标准错误流。
- 如果
helpmsg
函数指针不为空,则调用该函数输出帮助信息。 - 调用
exit(1)
终止程序。
7. 终端和交互
tsi.h
/tsi.cc
:Target-Server Interface(TSI),用于fesvr
和目标服务器之间的通信。term.h
/term.c
:是终端仿真,处理 RISC-V 目标程序的控制台输入/输出。
7.1 tsi.h
/ tsi.cc
tsi.h
tsi.h
文件主要定义了一个名为 tsi_t
的类,该类继承自 htif_t
类。tsi_t
类用于实现测试接口,负责处理与测试相关的通信和操作。
2.1 头文件保护和头文件包含
#ifndef __SAI_H
#define __SAI_H
#include "htif.h"
#include "context.h"
#include <string>
#include <vector>
#include <deque>
#include <stdint.h>
- 头文件保护:使用
#ifndef
、#define
和#endif
防止头文件被重复包含,避免重复定义错误。 - 头文件包含:
htif.h
和context.h
是自定义的头文件,包含了硬件接口和上下文相关的定义。<string>
、<vector>
、<deque>
是标准库头文件,用于使用字符串、动态数组和双端队列。<stdint.h>
用于使用标准的整数类型,如uint32_t
。
2.2 宏定义
#define SAI_CMD_READ 0
#define SAI_CMD_WRITE 1
#define SAI_ADDR_CHUNKS 2
#define SAI_LEN_CHUNKS 2
- 定义了两个命令常量
SAI_CMD_READ
和SAI_CMD_WRITE
,分别表示读取和写入命令,用于后续的通信协议。 - 定义了地址和长度的块数
SAI_ADDR_CHUNKS
和SAI_LEN_CHUNKS
,用于数据传输时的分块处理。
2.3
tsi_t
类定义
class tsi_t : public htif_t
{
public:
tsi_t(int argc, char** argv);
virtual ~tsi_t();
bool data_available();
void send_word(uint32_t word);
uint32_t recv_word();
void switch_to_host();
uint32_t in_bits() { return in_data.front(); }
bool in_valid() { return !in_data.empty(); }
bool out_ready() { return true; }
void tick(bool out_valid, uint32_t out_bits, bool in_ready);
protected:
void reset() override;
void read_chunk(addr_t taddr, size_t nbytes, void* dst) override;
void write_chunk(addr_t taddr, size_t nbytes, const void* src) override;
void switch_to_target();
size_t chunk_align() override { return 4; }
size_t chunk_max_size() override { return 1024; }
int get_ipi_addrs(addr_t *addrs);
private:
context_t host;
context_t* target;
std::deque<uint32_t> in_data;
std::deque<uint32_t> out_data;
void push_addr(addr_t addr);
void push_len(addr_t len);
static void host_thread(void *tsi);
};
-
公共成员函数:
- 构造函数
tsi_t(int argc, char** argv)
:用于初始化tsi_t
对象,会处理命令行参数。 - 析构函数
virtual ~tsi_t()
:虚析构函数,确保在删除派生类对象时能正确调用析构函数。 data_available()
:检查是否有可用数据。send_word(uint32_t word)
:发送一个 32 位的字。recv_word()
:接收一个 32 位的字。switch_to_host()
:切换到主机模式。in_bits()
:返回输入数据队列的第一个元素。in_valid()
:检查输入数据队列是否为空。out_ready()
:表示输出是否准备好,这里始终返回true
。tick(bool out_valid, uint32_t out_bits, bool in_ready)
:模拟时钟周期,处理输入输出信号。
- 构造函数
-
受保护成员函数:
reset()
:重置对象状态,重写了基类的reset
方法。read_chunk(addr_t taddr, size_t nbytes, void* dst)
:从指定地址读取指定字节数的数据到目标缓冲区,重写了基类的方法。write_chunk(addr_t taddr, size_t nbytes, const void* src)
:将指定缓冲区的数据写入指定地址,重写了基类的方法。switch_to_target()
:切换到目标模式。chunk_align()
:返回数据块的对齐字节数,这里为 4 字节。chunk_max_size()
:返回数据块的最大大小,这里为 1024 字节。get_ipi_addrs(addr_t *addrs)
:获取 IPI(中断处理器间)地址,返回地址数量。
-
私有成员变量和函数:
context_t host
:主机上下文对象。context_t* target
:目标上下文对象指针。std::deque<uint32_t> in_data
和std::deque<uint32_t> out_data
:分别用于存储输入和输出数据的双端队列。push_addr(addr_t addr)
:将地址压入队列。push_len(addr_t len)
:将长度压入队列。static void host_thread(void *tsi)
:静态成员函数,用于启动主机线程。
2.4 头文件结束
#endif
结束头文件保护。
tsi.cc
tsi.cc
文件实现了一个名为 tsi_t
的类,该类主要用于处理目标系统接口(TSI)相关的功能。它负责管理主机线程和目标线程之间的切换,以及与目标系统进行数据交互,如读取和写入数据块、发送和接收单词等操作。
- 头文件包含
#include <cstdio>
#include <cstdlib>
包含标准输入输出和标准库的头文件,为后续使用 printf
、malloc
等标准函数提供支持。
- 宏定义
#define NHARTS_MAX 16
定义了最大硬件线程数为 16,用于后续的硬件线程相关操作。
tsi_t
类成员函数
void tsi_t::host_thread(void *arg)
void tsi_t::host_thread(void *arg)
{
tsi_t *tsi = static_cast<tsi_t*>(arg);
tsi->run();
while (true)
tsi->target->switch_to();
}
- 功能:主机线程的入口函数。
- 步骤:
- 将传入的参数
arg
转换为tsi_t
类型的指针。 - 调用
tsi
对象的run
方法。 - 进入一个无限循环,不断调用
target
对象的switch_to
方法,切换到目标线程。
- 将传入的参数
tsi_t::tsi_t(int argc, char** argv)
tsi_t::tsi_t(int argc, char** argv) : htif_t(argc, argv)
{
target = context_t::current();
host.init(host_thread, this);
}
- 功能:
tsi_t
类的构造函数。 - 步骤:
- 调用基类
htif_t
的构造函数,传入命令行参数。 - 获取当前上下文并赋值给
target
成员变量。 - 初始化
host
对象,将host_thread
作为线程入口函数,并传入this
指针。
- 调用基类
tsi_t::~tsi_t(void)
tsi_t::~tsi_t(void)
{
}
- 功能:
tsi_t
类的析构函数,目前为空,后续需要添加资源释放的代码。
void tsi_t::reset()
#define MSIP_BASE 0x2000000
// Interrupt core 0 to make it start executing the program in DRAM
void tsi_t::reset()
{
uint32_t one = 1;
write_chunk(MSIP_BASE, sizeof(uint32_t), &one);
}
- 功能:重置操作,通过向
MSIP_BASE
地址写入1
来中断核心 0,使其开始执行 DRAM 中的程序。 - 步骤:
- 定义一个
uint32_t
类型的变量one
并初始化为1
。 - 调用
write_chunk
方法,将one
的值写入MSIP_BASE
地址。
- 定义一个
void tsi_t::push_addr(addr_t addr)
void tsi_t::push_addr(addr_t addr)
{
for (int i = 0; i < SAI_ADDR_CHUNKS; i++) {
in_data.push_back(addr & 0xffffffff);
addr = addr >> 32;
}
}
- 功能:将地址
addr
按 32 位为一组拆分成多个块,并依次添加到in_data
向量中。 - 步骤:
- 使用一个循环,循环次数为
SAI_ADDR_CHUNKS
。 - 在每次循环中,将
addr
的低 32 位添加到in_data
向量中。 - 将
addr
右移 32 位,处理下一个 32 位块。
- 使用一个循环,循环次数为
void tsi_t::push_len(addr_t len)
void tsi_t::push_len(addr_t len)
{
for (int i = 0; i < SAI_LEN_CHUNKS; i++) {
in_data.push_back(len & 0xffffffff);
len = len >> 32;
}
}
- 功能:将长度
len
按 32 位为一组拆分成多个块,并依次添加到in_data
向量中。 - 步骤:与
push_addr
方法类似,只是处理的是长度信息。
void tsi_t::read_chunk(addr_t taddr, size_t nbytes, void* dst)
void tsi_t::read_chunk(addr_t taddr, size_t nbytes, void* dst)
{
uint32_t *result = static_cast<uint32_t*>(dst);
size_t len = nbytes / sizeof(uint32_t);
in_data.push_back(SAI_CMD_READ);
push_addr(taddr);
push_len(len - 1);
for (size_t i = 0; i < len; i++) {
while (out_data.empty())
switch_to_target();
result[i] = out_data.front();
out_data.pop_front();
}
}
- 功能:从目标地址
taddr
读取nbytes
字节的数据到dst
缓冲区。 - 步骤:
- 将
dst
指针转换为uint32_t
类型的指针。 - 计算需要读取的
uint32_t
类型数据的数量。 - 将读取命令
SAI_CMD_READ
添加到in_data
向量中。 - 调用
push_addr
方法将目标地址添加到in_data
向量中。 - 调用
push_len
方法将读取长度添加到in_data
向量中。 - 使用一个循环,依次从
out_data
向量中读取数据,并存储到result
数组中。如果out_data
向量为空,则调用switch_to_target
方法切换到目标线程。
- 将
void tsi_t::write_chunk(addr_t taddr, size_t nbytes, const void* src)
void tsi_t::write_chunk(addr_t taddr, size_t nbytes, const void* src)
{
const uint32_t *src_data = static_cast<const uint32_t*>(src);
size_t len = nbytes / sizeof(uint32_t);
in_data.push_back(SAI_CMD_WRITE);
push_addr(taddr);
push_len(len - 1);
in_data.insert(in_data.end(), src_data, src_data + len);
}
- 功能:将
src
缓冲区中的nbytes
字节数据写入到目标地址taddr
。 - 步骤:
- 将
src
指针转换为const uint32_t
类型的指针。 - 计算需要写入的
uint32_t
类型数据的数量。 - 将写入命令
SAI_CMD_WRITE
添加到in_data
向量中。 - 调用
push_addr
方法将目标地址添加到in_data
向量中。 - 调用
push_len
方法将写入长度添加到in_data
向量中。 - 将
src_data
数组中的数据插入到in_data
向量的末尾。
- 将
void tsi_t::send_word(uint32_t word)
void tsi_t::send_word(uint32_t word)
{
out_data.push_back(word);
}
- 功能:将一个 32 位的单词
word
添加到out_data
向量中。
uint32_t tsi_t::recv_word(void)
uint32_t tsi_t::recv_word(void)
{
uint32_t word = in_data.front();
in_data.pop_front();
return word;
}
- 功能:从
in_data
向量中取出第一个 32 位的单词并返回,同时将其从in_data
向量中移除。
bool tsi_t::data_available(void)
bool tsi_t::data_available(void)
{
return !in_data.empty();
}
- 功能:检查
in_data
向量是否为空,如果不为空则返回true
,表示有数据可用。
void tsi_t::switch_to_host(void)
void tsi_t::switch_to_host(void)
{
host.switch_to();
}
- 功能:切换到主机线程。
void tsi_t::switch_to_target(void)
void tsi_t::switch_to_target(void)
{
target->switch_to();
}
- 功能:切换到目标线程。
void tsi_t::tick(bool out_valid, uint32_t out_bits, bool in_ready)
void tsi_t::tick(bool out_valid, uint32_t out_bits, bool in_ready)
{
if (out_valid && out_ready())
out_data.push_back(out_bits);
if (in_valid() && in_ready)
in_data.pop_front();
}
- 功能:处理时钟周期事件。
- 步骤:
- 如果
out_valid
为true
且out_ready()
函数返回true
,则将out_bits
添加到out_data
向量中。 - 如果
in_valid()
函数返回true
且in_ready
为true
,则从in_data
向量中移除第一个元素。
- 如果
tsi.cc
文件实现了一个用于目标系统接口的类 tsi_t
,通过管理主机线程和目标线程之间的切换,以及数据的读写操作,实现了与目标系统的交互。代码中使用了向量来存储数据,并通过一系列的方法来处理地址、长度、命令等信息。
7.2 term.h
/ term.c
term.h
#ifndef _TERM_H
#define _TERM_H
class canonical_terminal_t
{
public:
static int read();
static void write(char);
};
#endif
term.h
是一个头文件,其主要作用是定义一个名为 canonical_terminal_t
的类,该类用于处理规范终端的输入输出操作。
canonical_terminal_t
类的定义
class canonical_terminal_t
{
public:
static int read();
static void write(char);
};
- 类名:
canonical_terminal_t
,从命名来看,这个类是用于处理规范终端(canonical terminal)的相关操作。 - 访问修饰符:
public
,表明类中的成员函数能够被外部代码访问。 - 成员函数:
static int read();
:这是一个静态成员函数,用于从规范终端读取输入。返回值为int
类型,static void write(char);
:同样是静态成员函数,用于向规范终端写入一个字符。参数为char
类型,表示要写入的字符。
term.c
term.cc
文件实现了一个用于管理终端规范模式(canonical mode)的类 canonical_termios_t
,并提供了一个终端类 canonical_terminal_t
的部分成员函数,主要用于处理终端的输入输出操作。
- 头文件包含
#include <termios.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <stdlib.h>
包含了一些系统头文件,用于处理终端设置、文件操作、轮询机制、信号处理和内存管理等功能。
canonical_termios_t
类
class canonical_termios_t
{
public:
canonical_termios_t()
: restore_tios(false)
{
if (tcgetattr(0, &old_tios) == 0)
{
struct termios new_tios = old_tios;
new_tios.c_lflag &= ~(ICANON | ECHO);
if (tcsetattr(0, TCSANOW, &new_tios) == 0)
restore_tios = true;
}
}
~canonical_termios_t()
{
if (restore_tios)
tcsetattr(0, TCSANOW, &old_tios);
}
private:
struct termios old_tios;
bool restore_tios;
};
- 构造函数:
restore_tios
初始化为false
。- 使用
tcgetattr
函数获取标准输入(文件描述符 0)的当前终端设置,并存储在old_tios
中。 - 创建一个新的
termios
结构体new_tios
,并将其初始化为old_tios
的副本。 - 通过按位与操作清除
new_tios
的ICANON
和ECHO
标志,从而禁用规范模式和回显功能。 - 使用
tcsetattr
函数将新的终端设置应用到标准输入。如果设置成功,将restore_tios
设为true
。
- 析构函数:
- 如果
restore_tios
为true
,表示之前成功更改了终端设置,使用tcsetattr
函数将终端设置恢复为原来的状态。
- 如果
- 全局对象
static canonical_termios_t tios; // exit() will clean up for us
创建一个静态的 canonical_termios_t
对象 tios
,用于在程序退出时自动恢复终端设置。
canonical_terminal_t
类的成员函数
read
函数
int canonical_terminal_t::read()
{
struct pollfd pfd;
pfd.fd = 0;
pfd.events = POLLIN;
int ret = poll(&pfd, 1, 0);
if (ret <= 0 || !(pfd.revents & POLLIN))
return -1;
unsigned char ch;
ret = ::read(0, &ch, 1);
return ret <= 0 ? -1 : ch;
}
- 创建一个
pollfd
结构体pfd
,并将其fd
字段设置为标准输入的文件描述符 0,events
字段设置为POLLIN
,表示关注输入事件。 - 使用
poll
函数进行轮询,检查标准输入是否有数据可读。如果poll
失败或没有输入事件,返回 -1。 - 如果有输入事件,使用
::read
函数从标准输入读取一个字节的数据到ch
中。如果读取失败,返回 -1;否则返回读取的字符。
write
函数
void canonical_terminal_t::write(char ch)
{
if (::write(1, &ch, 1) != 1)
abort();
}
- 使用
::write
函数将字符ch
写入标准输出(文件描述符 1)。如果写入失败,调用abort
函数终止程序。
该文件通过 canonical_termios_t
类管理终端的规范模式,确保在程序运行期间禁用规范模式和回显功能,并在程序退出时恢复原来的设置。同时,canonical_terminal_t
类提供了简单的输入输出函数,用于处理终端的读写操作。
下一篇:RISC-V ISA Simulator系列之fesvr<8>