RISC-V ISA Simulator系列之fesvr<7>

深入解析 FESVR(Front-End Server)

url: https://github.com/riscv/riscv-isa-sim.git
commid: fcbdbe7946079650d0e656fa3d353e3f652d471f

目录

  1. FESVR 概述
  2. FESVR 代码结构分析
  3. 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

  1. 文件概述

fesvr.pc.in 是一个用于模块化 C++ 构建系统的包配置文件模板,其主要作用是为使用 fesvr 库的项目提供必要的配置信息,比如库的路径、版本号、依赖项等。文件中使用了一些占位符(如 @prefix@@PACKAGE_VERSION@ 等),这些占位符会在构建过程中被实际的值替换。

  1. 文件详细分析

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@ 是占位符,会被替换为其他需要链接的库。
  1. 总结

fesvr.pc.in 文件是一个用于模块化 C++ 构建系统的包配置模板,它通过占位符的方式,为不同的构建环境提供了灵活的配置选项。在构建过程中,这些占位符会被替换为实际的值,从而生成最终的包配置文件,供其他项目使用 fesvr 库时参考。

fesvr_dpi.cc

fesvr_dpi.cc 文件主要用于实现与外部硬件接口(如 SystemVerilog DPI)交互的功能,负责加载 ELF 文件并处理其内容,将文件中的段信息和符号信息提供给外部使用。

  1. 头文件包含
#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 接口。

  1. 全局变量定义
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 时记录当前位置。
  1. 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 方法。
  1. 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 映射,将当前段的起始地址和长度存储到 addresslen 指针中,并将 section_index 加 1。
  • 如果还有更多段未处理,返回 1;否则返回 0。
  1. 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 中,若存在则调用 memifread 方法进行读取。
  • 如果 size 为 0,则读取整个段的长度;否则读取指定的 size 字节。
  1. 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 数组的指针,然后将指定地址的段内容逐字节复制到该数组中。
  1. 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。
  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 类实现了内存读写操作,同时使用 sectionssymbolsmems 等映射存储 ELF 文件的相关信息。

fesvr.mk.in

fesvr.mk.in 是一个用于构建系统的 Makefile 模板文件,其中 .in 扩展名通常表示这是一个配置文件模板,在构建过程中会通过工具(如 autoconf)进行处理,生成最终的 Makefile。

该文件主要定义了 FESVR(Front - End Server)相关的安装头文件、源文件、安装配置等信息,这些信息将用于指导 Makefile 进行编译和安装操作。

  1. 占位符
fesvr_subproject_deps = \

这是一个占位符,在后续的配置过程中会被替换为具体的内容,例如一些环境变量的设置或者其他必要的配置信息。

  1. 安装头文件列表
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 模块所需的各种数据结构和函数声明,在编译过程中会被其他源文件引用。
  • 反斜杠 \ 用于换行,使列表更易读。
  1. 安装配置头文件标志
fesvr_install_config_hdr = yes
  • fesvr_install_config_hdr 是一个布尔型变量,设置为 yes 表示需要安装配置头文件。配置头文件通常包含一些编译时的配置信息,如宏定义、常量等。
  1. 安装库文件标志
fesvr_install_lib = yes
fesvr_install_shared_lib = yes
  • fesvr_install_libfesvr_install_shared_lib 分别表示是否安装静态库和共享库。设置为 yes 表示需要安装这两种类型的库文件。
  1. 源文件列表
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 模块的具体实现代码,在编译过程中会被编译成目标文件,最终链接成库文件或可执行文件。
  1. 安装程序源文件列表
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 脚本,帮助软件在不同的系统上进行配置和编译。

  1. 检查 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 库。
  1. 检查 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,同时添加注释说明该宏的用途。
    • 后面两个空参数分别表示如果结构体成员不存在时执行的操作和额外的检查代码,这里为空表示不执行额外操作。
  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。该类提供了一种机制,允许用户定义命令行选项及其对应的处理函数,并解析命令行参数。

  1. 头文件保护
// See LICENSE for license details.

#ifndef _OPTION_PARSER_H
#define _OPTION_PARSER_H
  • 这是典型的头文件保护机制,防止头文件被重复包含。如果 _OPTION_PARSER_H 未被定义,则定义该宏并包含头文件内容;否则跳过。
  1. 头文件包含
#include <vector>
#include <functional>
  • #include <vector>:引入标准库中的 std::vector 容器,用于存储选项信息。
  • #include <functional>:引入 std::function,它是一个通用的多态函数包装器,用于存储、复制和调用任何可调用对象,这里用于存储选项对应的处理函数。
  1. 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 指针初始化为 0helpmsg 用于存储帮助信息的打印函数。
  • 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
  1. 头文件结束
#endif
  • 结束头文件保护块。

总结

option_parser.h 文件定义了一个命令行选项解析器类 option_parser_t,通过该类可以定义选项及其处理函数,并解析命令行参数。它使用 std::vector 存储选项信息,std::function 存储处理函数,同时提供了错误处理和帮助信息打印的接口。

option_parser.cc

option_parser.cc 文件实现了一个命令行选项解析器,用于解析命令行参数。该解析器支持短选项(如 -h)和长选项(如 --help),并且可以为每个选项指定是否需要参数以及对应的处理动作。

  1. 头文件包含
#include "option_parser.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
  • option_parser.h:自定义的头文件,包含 option_parser_t 类的声明。
  • <cstdio><cstdlib><cstring><cassert>:标准库头文件,提供输入输出、内存管理、字符串处理和断言功能。
  1. 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 向量中。
  1. 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:命令行参数数组的起始指针。
  • 实现
    1. 跳过程序名(argv0[0]),从 argv0 + 1 开始解析参数。
    2. 遍历参数数组,直到遇到非选项参数(不以 - 开头)或参数数组结束。
    3. 对于每个选项,遍历 opts 向量,检查是否有匹配的选项。
    4. 如果匹配成功,根据选项类型(短选项或长选项)提取选项的参数值。
    5. 检查参数是否符合要求(是否需要参数),如果不符合则调用 error 函数处理错误。
    6. 调用选项对应的处理动作 it->func(optarg)
    7. 如果没有找到匹配的选项,调用 error 函数处理错误。
    8. 返回未处理的参数数组指针。
  1. 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:出错的选项。
  • 实现
    1. 将错误信息输出到标准错误流。
    2. 如果 helpmsg 函数指针不为空,则调用该函数输出帮助信息。
    3. 调用 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.hcontext.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_READSAI_CMD_WRITE,分别表示读取和写入命令,用于后续的通信协议。
  • 定义了地址和长度的块数 SAI_ADDR_CHUNKSSAI_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_datastd::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)相关的功能。它负责管理主机线程和目标线程之间的切换,以及与目标系统进行数据交互,如读取和写入数据块、发送和接收单词等操作。

  1. 头文件包含
#include <cstdio>
#include <cstdlib>

包含标准输入输出和标准库的头文件,为后续使用 printfmalloc 等标准函数提供支持。

  1. 宏定义
#define NHARTS_MAX 16

定义了最大硬件线程数为 16,用于后续的硬件线程相关操作。

  1. 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();
}
  • 功能:主机线程的入口函数。
  • 步骤
    1. 将传入的参数 arg 转换为 tsi_t 类型的指针。
    2. 调用 tsi 对象的 run 方法。
    3. 进入一个无限循环,不断调用 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 类的构造函数。
  • 步骤
    1. 调用基类 htif_t 的构造函数,传入命令行参数。
    2. 获取当前上下文并赋值给 target 成员变量。
    3. 初始化 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 中的程序。
  • 步骤
    1. 定义一个 uint32_t 类型的变量 one 并初始化为 1
    2. 调用 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 向量中。
  • 步骤
    1. 使用一个循环,循环次数为 SAI_ADDR_CHUNKS
    2. 在每次循环中,将 addr 的低 32 位添加到 in_data 向量中。
    3. 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 缓冲区。
  • 步骤
    1. dst 指针转换为 uint32_t 类型的指针。
    2. 计算需要读取的 uint32_t 类型数据的数量。
    3. 将读取命令 SAI_CMD_READ 添加到 in_data 向量中。
    4. 调用 push_addr 方法将目标地址添加到 in_data 向量中。
    5. 调用 push_len 方法将读取长度添加到 in_data 向量中。
    6. 使用一个循环,依次从 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
  • 步骤
    1. src 指针转换为 const uint32_t 类型的指针。
    2. 计算需要写入的 uint32_t 类型数据的数量。
    3. 将写入命令 SAI_CMD_WRITE 添加到 in_data 向量中。
    4. 调用 push_addr 方法将目标地址添加到 in_data 向量中。
    5. 调用 push_len 方法将写入长度添加到 in_data 向量中。
    6. 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();
}
  • 功能:处理时钟周期事件。
  • 步骤
    1. 如果 out_validtrueout_ready() 函数返回 true,则将 out_bits 添加到 out_data 向量中。
    2. 如果 in_valid() 函数返回 truein_readytrue,则从 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 的部分成员函数,主要用于处理终端的输入输出操作。

  1. 头文件包含
#include <termios.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <stdlib.h>

包含了一些系统头文件,用于处理终端设置、文件操作、轮询机制、信号处理和内存管理等功能。

  1. 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_tiosICANONECHO 标志,从而禁用规范模式和回显功能。
    • 使用 tcsetattr 函数将新的终端设置应用到标准输入。如果设置成功,将 restore_tios 设为 true
  • 析构函数
    • 如果 restore_tiostrue,表示之前成功更改了终端设置,使用 tcsetattr 函数将终端设置恢复为原来的状态。
  1. 全局对象
static canonical_termios_t tios; // exit() will clean up for us

创建一个静态的 canonical_termios_t 对象 tios,用于在程序退出时自动恢复终端设置。

  1. 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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值