CPU仿真环境中的printf实现

CPU仿真环境中的printf实现,第1张

文章目录
  • Linux系统的打印实现
  • 嵌入式系统的打印实现
  • RTL仿真环境的打印实现
    • CPU的打印实现
    • Memory中数据的打印方式

在包含CPU的仿真环境中,如果要在C程序测试中通过打印做一些调试,通常需要重新实现 printf函数。在介绍CPU仿真环境中的printf实现之前,首先简单了解一下Linux系统和嵌入式系统中的printf实现。

Linux系统的打印实现
  1. GCC编译器链接glibc标准库
  2. *** 作系统为程序创建进程并分配内存空间
  3. 调用sys_write等系统调用函数
  4. 数据格式化输出至标准I/O的缓存区
  5. 输出映射至console,转换输出到显示器或终端

其中sys_write函数声明在/usr/src/kernels/<内核版本>/include/linux/syscalls.h文件中,感兴趣的同学可以自己研究下。

asmlinkage long sys_write(unsigned int fd, const char __user *buf, size_t count);
嵌入式系统的打印实现

在嵌入式系统中,由于存储器资源有限,需要小型运行库(如newlib),且一般没有显示终端,所以需要将输出重定向以支持printf函数。

  1. 调用newlib或其他嵌入式C运行库中的printf库函数
  2. 实现newlib的桩函数write();或在其他系统中重写printf调用的库函数
  3. 调用UART驱动并通过串口将打印信息重定向至PC主机的COM接口
  4. 在PC主机的串口调试助手中显示输出信息

标准输入函数scanf函数同样可由UART接口获取输入实现,而printf和scanf最终会调用fputc和fgetc进行字符串输出/输入。在某板级验证运行库中,重定向了fputc和fgetc,并通过UartPutc和UartGetc实现:

// retarget
int fputc(int ch, FILE *f) {
    return (UartPutc(ch));
}
int fgetc(FILE *f) {
    return (UartPutc(UartGetc()));
}

// uart stdout
unsigned char UartPutc(unsigned char my_ch) {
    while ((CMSDK_UART0->STATE & 1)); // Wait if Transmit Holding register is full
    CMSDK_UART0->DATA = my_ch; // write to transmit holding register
    return (my_ch);
}
unsigned char UartGetc(void) {
    while ((CMSDK_UART0->STATE & 2)==0); // Wait if Receive Holding register is empty
    return (CMSDK_UART0->DATA);
}
RTL仿真环境的打印实现

在SOC仿真环境中,虽然同样可以用UART输出重定向实现printf,并通过UART monitor采样和打印信息,但这样仿真速度很慢,因此通过以下方式实现了printf:

  1. 调用仿真C运行库
  2. 重定向fputc,将字符串ASCII码写入指定地址
  3. 在仿真testbench/monitor中实现对指定地址写入的监测
  4. 将写入字符串用系统打印函数输出到终端或保存到log文件
CPU的打印实现

在某RISC-V CPU的仿真环境中,重新实现了io lib,printf函数最终会写到MSCRATCH(机器模式中断数据备份寄存器)并在仿真tb中监测和打印,其具体实现如下:

#include 
#include 

int printf(const char *format,...) {
    int n;
    va_list arg_ptr;
    va_start(arg_ptr, format);
    n=vprintf(format, arg_ptr);
    va_end(arg_ptr);
    return n;
}

通过va_start和va_end可以获取可变参数列表,然后调用vfprintf发送格式化输出到流stream中。在这之后还会调用 __v_printf等多个函数,对stream进行格式化处理,计算字符串长度,并通过函数指针调用 __stdio_outs函数。

int __stdio_outs(const char *s,size_t len) {
    os_critical_enter();
    for(int i = 0; i < len; i++) {
        fputc(*(s+i), stdout);
    }
    os_critical_exit();
    return 1;
}

最后调用fputc完成字符串输出。这里通过write_csr宏内联汇编重定向实现了fputc:

#define write_csr(reg, val) ({ \
  if (__builtin_constant_p(val) && (unsigned long)(val) < 32) \
    asm volatile ("csrw " #reg ", %0" :: "i"(val)); \
  else \
    asm volatile ("csrw " #reg ", %0" :: "r"(val)); }

int fputc(int ch, FILE *stream) {
    write_csr(mscratch, ch);
}

在tb中,实现了一个CPU monitor,监测对MSCRATCH寄存器的写入,然后由仿真工具调用系统打印函数 $fwrite() 将打印字符逐个输出到log文件并更新流。

initial begin
    file_cpu = $fopen("cpu_printf.log", "w");
end
always @(posedge `U_CPU.pll_core_cpuclk) begin
    if(`CPU_S_EN == 1'b1 ) begin  // mscratch_local_en
        $fwrite(file_cpu, "%c", `CPU_S_VAL);  // mscratch_value[31:0]
        $fflush(file_cpu);
    end
end
Memory中数据的打印方式

在RTL仿真环境中,虽然支持printf,但调用打印函数后生成的程序代码较大。实现printf的目的是为了支持C语言标准库和方便写C测试程序。如果需要连续打印一些memory内的数据进行debug,考虑到eda仿真的效率,可以通过SystemVerilog后门访问取代printf函数调用的方式。
在验证环境中,用SV实现了对SOC中各ram的后门读写方法,其特点在于不通过总线而是直接根据hierarchy路径对DUT内部寄存器或存储器进行读写 *** 作,不消耗仿真时间。以CPU ram的后门读取方法为例:

task cpu_tcm_bkdoor_read(bit[31:0] raddr, ref bit[31:0] rdata);
    rdata = `U_CPU_RAM[raddr[11:4]][raddr[3:2]];
    $display("TCM MEMORY[%h][%h] read data %h\n",raddr[11:4],raddr[3:2],rdata);
endtask

然后,自定义了一段地址空间LOG_PRINT_ADDR ,通过对指定地址写入数据实现传参和仿真方法调用,以下是C调用仿真接口的函数实现。

void print_mem(uint32_t start_addr, int byte_num); {
    OUTP32(LOG_PRINT_ADDR + 0x4, start_addr);
    OUTP32(LOG_PRINT_ADDR + 0x8, byte_num);
    OUTP32(LOG_PRINT_ADDR, 1);  // write 1 at last to trigger
}

在仿真环境中,实现了一个monitor以监视对这段地址空间的写入,根据写入数据调用对应的SV task并传参。对于print_mem方法,会解析地址参数并选择对应的后门读取方法进行调用,然后将读取数据打印至run目录下的一个log文件中,这里的SV实现可以参考上节cpu monitor实现。参照以上方法,仿真环境还可提供许多其他仿真控制接口给C,例如仿真结束控制、外设VIP控制等,以此实现仿真环境内CPU控制外层仿真环境。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/676217.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)

保存