其中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函数。
标准输入函数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);
}
在SOC仿真环境中,虽然同样可以用UART输出重定向实现printf,并通过UART monitor采样和打印信息,但这样仿真速度很慢,因此通过以下方式实现了printf:
在某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
在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控制外层仿真环境。