[uartdpi] Accept log file name through plusarg

The UARTDPI module simulates an UART device by creating a
pseudo-terminal (e.g. /dev/pts/N). Additionally, each written character
is also written to a log file, in our case always `uart0.log` in the
current directory.

This patch adds the ability to specify the path of the log file through
a plus argument (plusarg). The defaults remain unchanged: calling the
simulation without special arguents writes an `uart0.log` file.

As a new feature, the log file can now also be given as "-", which
writes UART logs directly to STDOUT. In this case, the running
simulation directly shows all output printed from device software, e.g.
from `LOG()` macros, making the life of software developers much easier.

To write all logs of uart0 (the one and only UART in an Earl Grey
system), use a command like the following:

```
build/lowrisc_systems_top_earlgrey_verilator_0.1/sim-verilator/Vtop_earlgrey_verilator \
  --meminit=rom,build-bin/sw/device/boot_rom/boot_rom_sim_verilator.elf  \
  --meminit=flash,build-bin/sw/device/tests/dif_hmac_sanitytest_sim_verilator.elf \
  +UARTDPI_LOG_uart0=-
```

To implement this functionality, the log writing was refactored from
SystemVerilog to C (DPI) code.

Signed-off-by: Philipp Wagner <phw@lowrisc.org>
diff --git a/doc/ug/getting_started_verilator.md b/doc/ug/getting_started_verilator.md
index d9b61a9..cbfae32 100644
--- a/doc/ug/getting_started_verilator.md
+++ b/doc/ug/getting_started_verilator.md
@@ -93,6 +93,20 @@
 I00005 hello_world.c:46] The LEDs show the ASCII code of the last character.
 ```
 
+Instead of interacting with the UART through a pseudo-terminal, the UART output can be written to a log file, or to STDOUT.
+This is done by passing the `UARTDPI_LOG_uart0` plus argument ("plusarg") to the verilated simulation at runtime.
+To write all UART output to STDOUT, pass `+UARTDPI_LOG_uart0=-` to the simulation.
+To write all UART output to a file called `your-log-file.log`, pass `+UARTDPI_LOG_uart0=your-log-file.log`.
+
+A full command-line invocation of the simulation could then look like that:
+```console
+$ cd $REPO_TOP
+$ build/lowrisc_systems_top_earlgrey_verilator_0.1/sim-verilator/Vtop_earlgrey_verilator \
+  --meminit=rom,build-bin/sw/device/boot_rom/boot_rom_sim_verilator.elf \
+  --meminit=flash,build-bin/sw/device/examples/hello_world/hello_world_sim_verilator.elf \
+  +UARTDPI_LOG_uart0=-
+```
+
 ## Interact with GPIO
 
 The simulation includes a DPI module to map general-purpose I/O (GPIO) pins to two POSIX FIFO files: one for input, and one for output.
diff --git a/hw/dv/dpi/uartdpi/uartdpi.c b/hw/dv/dpi/uartdpi/uartdpi.c
index 6e85aa4..4ef96f2 100644
--- a/hw/dv/dpi/uartdpi/uartdpi.c
+++ b/hw/dv/dpi/uartdpi/uartdpi.c
@@ -11,18 +11,21 @@
 #endif
 
 #include <assert.h>
+#include <errno.h>
 #include <fcntl.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 #include <unistd.h>
 
-void *uartdpi_create(const char *name) {
+void *uartdpi_create(const char *name, const char *log_file_path) {
   struct uartdpi_ctx *ctx =
       (struct uartdpi_ctx *)malloc(sizeof(struct uartdpi_ctx));
   assert(ctx);
 
   int rv;
 
+  // Initialize UART pseudo-terminal
   struct termios tty;
   cfmakeraw(&tty);
 
@@ -43,6 +46,28 @@
       "$ screen %s\n",
       ctx->ptyname, name, ctx->ptyname);
 
+  // Open log file (if requested)
+  ctx->log_file = NULL;
+  bool write_log_file = strlen(log_file_path) != 0;
+  if (write_log_file) {
+    if (strcmp(log_file_path, "-") == 0) {
+      ctx->log_file = stdout;
+      printf("UART: Additionally writing all UART output to STDOUT.\n");
+
+    } else {
+      FILE *log_file;
+      log_file = fopen(log_file_path, "w");
+      if (!log_file) {
+        fprintf(stderr, "UART: Unable to open log file at %s: %s\n",
+                log_file_path, strerror(errno));
+      } else {
+        ctx->log_file = log_file;
+        printf("UART: Additionally writing all UART output to '%s'.\n",
+               log_file_path);
+      }
+    }
+  }
+
   return (void *)ctx;
 }
 
@@ -55,6 +80,15 @@
   close(ctx->host);
   close(ctx->device);
 
+  if (ctx->log_file) {
+    // Always ensure the log file is flushed (most important when writing
+    // to STDOUT)
+    fflush(ctx->log_file);
+    if (ctx->log_file != stdout) {
+      fclose(ctx->log_file);
+    }
+  }
+
   free(ctx);
 }
 
@@ -72,8 +106,15 @@
 }
 
 void uartdpi_write(void *ctx_void, char c) {
+  int rv;
+
   struct uartdpi_ctx *ctx = (struct uartdpi_ctx *)ctx_void;
 
-  int rv = write(ctx->host, &c, 1);
-  assert(rv == 1 && "write() failed.");
+  rv = write(ctx->host, &c, 1);
+  assert(rv == 1 && "Write to pseudo-terminal failed.");
+
+  if (ctx->log_file) {
+    rv = fwrite(&c, sizeof(char), 1, ctx->log_file);
+    assert(rv == 1 && "Write to log file failed.");
+  }
 }
diff --git a/hw/dv/dpi/uartdpi/uartdpi.h b/hw/dv/dpi/uartdpi/uartdpi.h
index 1203fee..29d50a5 100644
--- a/hw/dv/dpi/uartdpi/uartdpi.h
+++ b/hw/dv/dpi/uartdpi/uartdpi.h
@@ -4,16 +4,20 @@
 
 #ifndef OPENTITAN_HW_DV_DPI_UARTDPI_UARTDPI_H_
 #define OPENTITAN_HW_DV_DPI_UARTDPI_UARTDPI_H_
+
 extern "C" {
 
+#include <stdio.h>
+
 struct uartdpi_ctx {
   char ptyname[64];
   int host;
   int device;
   char tmp_read;
+  FILE *log_file;
 };
 
-void *uartdpi_create(const char *name);
+void *uartdpi_create(const char *name, const char *log_file_path);
 void uartdpi_close(void *ctx_void);
 int uartdpi_can_read(void *ctx_void);
 char uartdpi_read(void *ctx_void);
diff --git a/hw/dv/dpi/uartdpi/uartdpi.sv b/hw/dv/dpi/uartdpi/uartdpi.sv
index 203099c..38a7649 100644
--- a/hw/dv/dpi/uartdpi/uartdpi.sv
+++ b/hw/dv/dpi/uartdpi/uartdpi.sv
@@ -13,11 +13,13 @@
   output logic tx_o,
   input  logic rx_i
 );
+  // Path to a log file. Used if none is specified through the `UARTDPI_LOG_<name>` plusarg.
+  localparam string DEFAULT_LOG_FILE = {NAME, ".log"};
 
   localparam int CYCLES_PER_SYMBOL = FREQ / BAUD;
 
   import "DPI-C" function
-    chandle uartdpi_create(input string name);
+    chandle uartdpi_create(input string name, input string log_file_path);
 
   import "DPI-C" function
     void uartdpi_close(input chandle ctx);
@@ -32,13 +34,11 @@
     void uartdpi_write(input chandle ctx, int data);
 
   chandle ctx;
-  int file_handle;
-  string file_name;
+  string log_file_path = DEFAULT_LOG_FILE;
 
   initial begin
-    ctx = uartdpi_create(NAME);
-    $sformat(file_name, "%s.log", NAME);
-    file_handle = $fopen(file_name, "w");
+    $value$plusargs({"UARTDPI_LOG_", NAME, "=%s"}, log_file_path);
+    ctx = uartdpi_create(NAME, log_file_path);
   end
 
   final begin
@@ -119,7 +119,6 @@
             rxactive <= 0;
             if (rx_i) begin
               uartdpi_write(ctx, rxsymbol);
-              $fwrite(file_handle, "%c", rxsymbol);
             end
           end
         end
diff --git a/hw/top_earlgrey/top_earlgrey_verilator.core b/hw/top_earlgrey/top_earlgrey_verilator.core
index 1e818e2..bc1fdd2 100644
--- a/hw/top_earlgrey/top_earlgrey_verilator.core
+++ b/hw/top_earlgrey/top_earlgrey_verilator.core
@@ -57,6 +57,10 @@
     datatype: bool
     paramtype: vlogdefine
     description: Use the OTBN model instead of the RTL implementation (development only)
+  UART_LOG_uart0:
+    datatype: string
+    paramtype: plusarg
+    description: Write a log of output from uart0 to the given log file. Use "-" for stdout.
 
 targets:
   default: &default_target