之前我发过一个视频,演示的是蜂鸟E203在windows系统上以全图形界面软件开发与逻辑仿真的流程,使用helloworld例程在vivado上仿真。
全图形界面ncleistudi+vivado联合仿真视频
当时这个视频发出来,我还是对一个东西抱有疑问:
为什么c语言中的printf可以把信息输出到逻辑仿真的终端?是如何实现?
经过一段时间的研究,以及论坛上大佬的指点,终于搞懂了整套流程,现在我在这里分享给大家。
在仿真的终端打印信息,是需要软件部分和RTL设计相互配合的。
c语言里,printf函数可以说是人尽皆知。在pc端,由于操作系统提供了标准输出界面,因此pirntf可以直接输出信息。而在嵌入式领域,想要实现printf需要做一点工作。
以蜂鸟e203为例,它没有显示器,没有标准终端,想要实现printf最常见的方式是通过串口打印信息。但是,怎么让编译器知道知道printf应该用串口呢?
(1) 初始化
如果想要使用串口,那么首先需要对串口进行初始化,这一步在系统上电初始化的过程中完成了。参考代码:./hbird_sdk/SoC/hbirdv2/Common/Source/system_hbirdv2.c
其中467-469行代码为:
#if ! defined(SIMULATION_SPIKE) && ! defined(SIMULATION_XLSPIKE)
gpio_iof_config(GPIOA, IOF_UART_MASK);
uart_init(SOC_DEBUG_UART, 115200);
SOC_DEBUG_UART
是一个宏定义,表示UART0
这两行程序的功能显而易见,它配置了GPIO功能复用,对UART0进行了初始化。有了这两句,程序中就能使用UART0了
(2) printf重定向./hbird_sdk/SoC/hbirdv2/Common/Source/Stubs/write.c
里面只有一个函数:
__WEAK ssize_t _write(int fd, const void* ptr, size_t len)
{
if (!isatty(fd)) {
return -1;
}
const uint8_t *writebuf = (const uint8_t *)ptr;
for (size_t i = 0; i < len; i++) {
if (writebuf[i] == '\n') {
uart_write(SOC_DEBUG_UART, '\r');
}
uart_write(SOC_DEBUG_UART, writebuf[i]);
}
return len;
}
从_write()
函数体可以看出,它的作用是通过UART0发送数据。_write
这个函数名是有特殊意义的,它会将printf的字符输出重定向到这里,通过这个函数传递参数、输出数据。__WEAK
表示这个函数是弱定义。如果我们想通过其他方式printf,例如spi,可以在程序中再定义一个_write()
函数,里面写入想要的功能,而write.c
中的函数会被忽略。
至此,printf的信息将由UART0输出
在RTL仿真过程中,想要把信息打印到终端,就需要调用$display、$fwrite等函数,蜂鸟e203能把串口信息打印到仿真器的终端就是用$fwrite实现的。
参考uart的RTL代码:rtl\e203\perips\apb_uart\uart_tx.v
最后几行186-189:
always @(posedge clk_i or negedge rstn_i) begin
if ((tx_valid_i & tx_ready_o) & rstn_i)
$fwrite(32'h80000002, "%c", tx_data_i);
end
它在uart发送信息的同时,调用了$fwrite()
函数。也就是说,串口输出什么,仿真器终端就打印什么。