| // Copyright lowRISC contributors. |
| // Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| #include "sw/device/lib/runtime/print.h" |
| |
| #include <stdarg.h> |
| #include <stdbool.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include "sw/device/lib/base/macros.h" |
| #include "sw/device/lib/base/memory.h" |
| #include "sw/device/lib/base/status.h" |
| |
| // This is declared as an enum to force the values to be |
| // compile-time constants, but the type is otherwise not |
| // used for anything. |
| enum { |
| // Standard format specifiers. |
| kPercent = '%', |
| kCharacter = 'c', |
| kString = 's', |
| kSignedDec1 = 'd', |
| kSignedDec2 = 'i', |
| kUnsignedOct = 'o', |
| kUnsignedHexLow = 'x', |
| kUnsignedHexHigh = 'X', |
| kUnsignedDec = 'u', |
| kPointer = 'p', |
| |
| // Verilog-style format specifiers. |
| kSvBinary = 'b', |
| kSvHexLow = 'h', |
| kSvHexHigh = 'H', |
| |
| // Other non-standard specifiers. |
| kHexLeLow = 'y', |
| kHexLeHigh = 'Y', |
| kStatusResult = 'r', |
| }; |
| |
| // NOTE: all of the lengths of the strings below are given so that the NUL |
| // terminator is left off; that way, `sizeof(kConst)` does not include it. |
| static const char kDigitsLow[16] = "0123456789abcdef"; |
| static const char kDigitsHigh[16] = "0123456789ABCDEF"; |
| |
| static const char kErrorNul[17] = "%<unexpected nul>"; |
| static const char kUnknownSpec[15] = "%<unknown spec>"; |
| static const char kErrorTooWide[12] = "%<bad width>"; |
| |
| static size_t base_dev_null(void *data, const char *buf, size_t len) { |
| return len; |
| } |
| static buffer_sink_t base_stdout = { |
| .data = NULL, |
| // Note: Using `&base_dev_null` causes this variable to be placed in the |
| // .data section and triggers the assertion in rom.ld. |
| .sink = NULL, |
| }; |
| |
| void base_set_stdout(buffer_sink_t out) { |
| if (out.sink == NULL) { |
| out.sink = &base_dev_null; |
| } |
| base_stdout = out; |
| } |
| |
| static size_t base_dev_uart(void *data, const char *buf, size_t len) { |
| const dif_uart_t *uart = (const dif_uart_t *)data; |
| for (size_t i = 0; i < len; ++i) { |
| if (dif_uart_byte_send_polled(uart, (uint8_t)buf[i]) != kDifOk) { |
| return i; |
| } |
| } |
| return len; |
| } |
| |
| void base_uart_stdout(const dif_uart_t *uart) { |
| base_set_stdout( |
| (buffer_sink_t){.data = (void *)uart, .sink = &base_dev_uart}); |
| } |
| |
| size_t base_printf(const char *format, ...) { |
| va_list args; |
| va_start(args, format); |
| size_t bytes_left = base_vprintf(format, args); |
| va_end(args); |
| return bytes_left; |
| } |
| |
| size_t base_vprintf(const char *format, va_list args) { |
| return base_vfprintf(base_stdout, format, args); |
| } |
| |
| typedef struct snprintf_captures_t { |
| char *buf; |
| size_t bytes_left; |
| } snprintf_captures_t; |
| |
| static size_t snprintf_sink(void *data, const char *buf, size_t len) { |
| snprintf_captures_t *captures = (snprintf_captures_t *)data; |
| if (captures->bytes_left == 0) { |
| return 0; |
| } |
| |
| if (len > captures->bytes_left) { |
| len = captures->bytes_left; |
| } |
| memcpy(captures->buf, buf, len); |
| captures->buf += len; |
| captures->bytes_left -= len; |
| return len; |
| } |
| |
| size_t base_snprintf(char *buf, size_t len, const char *format, ...) { |
| va_list args; |
| va_start(args, format); |
| |
| snprintf_captures_t data = { |
| .buf = buf, |
| .bytes_left = len, |
| }; |
| buffer_sink_t out = { |
| .data = &data, |
| .sink = &snprintf_sink, |
| }; |
| size_t bytes_left = base_vfprintf(out, format, args); |
| va_end(args); |
| return bytes_left; |
| } |
| |
| size_t base_fprintf(buffer_sink_t out, const char *format, ...) { |
| va_list args; |
| va_start(args, format); |
| size_t bytes_left = base_vfprintf(out, format, args); |
| va_end(args); |
| return bytes_left; |
| } |
| |
| /** |
| * Consumes characters from `format` until a '%' or NUL is reached. All |
| * characters seen before that are then sinked into `out`. |
| * |
| * @param out the sink to write bytes to. |
| * @param format a pointer to the format string to consume a prefix of. |
| * @param[out] bytes_written out param for the number of bytes writen to `out`. |
| * @return true if an unprocessed '%' was found. |
| */ |
| static bool consume_until_percent(buffer_sink_t out, const char **format, |
| size_t *bytes_written) { |
| size_t text_len = 0; |
| while (true) { |
| char c = (*format)[text_len]; |
| if (c == '\0' || c == kPercent) { |
| if (text_len > 0) { |
| *bytes_written += out.sink(out.data, *format, text_len); |
| } |
| *format += text_len; |
| return c != '\0'; |
| } |
| ++text_len; |
| } |
| } |
| |
| /** |
| * Represents a parsed format specifier. |
| */ |
| typedef struct format_specifier { |
| char type; |
| size_t width; |
| char padding; |
| bool is_nonstd; |
| } format_specifier_t; |
| |
| /** |
| * Consumes characters from `format` until a complete format specifier is |
| * parsed. See the documentation in `print.h` for full syntax. |
| * |
| * @param out the sink to write bytes to. |
| * @param format a pointer to the format string to consume a prefix of. |
| * @param[out] spec out param for the specifier. |
| * @return whether the parse succeeded. |
| */ |
| static bool consume_format_specifier(buffer_sink_t out, const char **format, |
| size_t *bytes_written, |
| format_specifier_t *spec) { |
| *spec = (format_specifier_t){0}; |
| |
| // Consume the percent sign. |
| ++(*format); |
| |
| // Parse a ! to detect an OpenTitan-specific extension (that isn't one |
| // of the Verilog ones...). |
| if ((*format)[0] == '!') { |
| spec->is_nonstd = true; |
| ++(*format); |
| } |
| |
| // Attempt to parse out an unsigned, decimal number, a "width", |
| // after the percent sign; the format specifier is the character |
| // immediately after this width. |
| // |
| // `spec->padding` is used to indicate whether we've seen a width; |
| // if nonzero, we have an actual width. |
| size_t spec_len = 0; |
| while (true) { |
| char c = (*format)[spec_len]; |
| if (c == '\0') { |
| *bytes_written += out.sink(out.data, kErrorNul, sizeof(kErrorNul)); |
| return false; |
| } |
| if (c < '0' || c > '9') { |
| break; |
| } |
| if (spec->padding == 0) { |
| if (c == '0') { |
| spec->padding = '0'; |
| ++spec_len; |
| continue; |
| } |
| spec->padding = ' '; |
| } |
| spec->width *= 10; |
| spec->width += (c - '0'); |
| ++spec_len; |
| } |
| |
| if ((spec->width == 0 && spec->padding != 0) || spec->width > 32) { |
| *bytes_written += out.sink(out.data, kErrorTooWide, sizeof(kErrorTooWide)); |
| return false; |
| } |
| |
| spec->type = (*format)[spec_len]; |
| *format += spec_len + 1; |
| return true; |
| } |
| |
| /** |
| * Write the digits of `value` onto `out`. |
| * |
| * @param out the sink to write bytes to. |
| * @param value the value to "stringify". |
| * @param width the minimum width to print; going below will result in writing |
| * out zeroes. |
| * @param base the base to express `value` in. |
| * @param glyphs an array of characters to use as the digits of a number, which |
| * should be at least ast long as `base`. |
| * @return the number of bytes written. |
| */ |
| static size_t write_digits(buffer_sink_t out, uint32_t value, uint32_t width, |
| char padding, uint32_t base, const char *glyphs) { |
| // All allocations are done relative to a buffer that could hold the longest |
| // textual representation of a number: ~0x0 in base 2, i.e., 32 ones. |
| static const int kWordBits = sizeof(uint32_t) * 8; |
| char buffer[kWordBits]; |
| |
| size_t len = 0; |
| if (value == 0) { |
| buffer[kWordBits - 1] = glyphs[0]; |
| ++len; |
| } |
| while (value > 0) { |
| uint32_t digit = value % base; |
| value /= base; |
| buffer[kWordBits - 1 - len] = glyphs[digit]; |
| ++len; |
| } |
| width = width == 0 ? 1 : width; |
| width = width > kWordBits ? kWordBits : width; |
| while (len < width) { |
| buffer[kWordBits - len - 1] = padding; |
| ++len; |
| } |
| return out.sink(out.data, buffer + (kWordBits - len), len); |
| } |
| |
| static size_t write_status(buffer_sink_t out, status_t value, bool as_json) { |
| // The module id is defined to be 3 chars long. |
| char mod[] = {'"', 0, 0, 0, '"', ','}; |
| int32_t arg; |
| const char *start; |
| bool err = status_extract(value, &start, &arg, &mod[1]); |
| |
| // strlen of the status code. |
| const char *end = start; |
| while (*end) |
| end++; |
| size_t len = 0; |
| |
| len += out.sink(out.data, "{\"", as_json ? 2 : 0); |
| len += out.sink(out.data, start, end - start); |
| len += out.sink(out.data, "\"", as_json ? 1 : 0); |
| |
| len += out.sink(out.data, ":", 1); |
| if (err) { |
| // All error codes include the module identifier. |
| len += out.sink(out.data, "[", 1); |
| len += out.sink(out.data, mod, sizeof(mod)); |
| len += write_digits(out, arg, 0, 0, 10, kDigitsLow); |
| len += out.sink(out.data, "]", 1); |
| } else { |
| // Ok codes include only the arg |
| len += write_digits(out, arg, 0, 0, 10, kDigitsLow); |
| } |
| len += out.sink(out.data, "}", as_json ? 1 : 0); |
| return len; |
| } |
| |
| /** |
| * Hexdumps `bytes` onto `out`. |
| * |
| * @param out the sink to write bytes to. |
| * @param bytes the bytes to dump. |
| * @param len the number of bytes to dump. |
| * @param width the minimum width to print; going below will result in writing |
| * out zeroes. |
| * @param padding the character to use for padding. |
| * @param big_endian whether to print in big-endian order (i.e. as %x would). |
| * @param glyphs an array of characters to use as the digits of a number, which |
| * should be at least ast long as `base`. |
| * @return the number of bytes written. |
| */ |
| static size_t hex_dump(buffer_sink_t out, const char *bytes, size_t len, |
| uint32_t width, char padding, bool big_endian, |
| const char *glyphs) { |
| size_t bytes_written = 0; |
| char buf[32]; |
| size_t buffered = 0; |
| if (len < width) { |
| width -= len; |
| memset(buf, padding, sizeof(buf)); |
| while (width > 0) { |
| size_t to_write = width > ARRAYSIZE(buf) ? 32 : width; |
| bytes_written += out.sink(out.data, buf, to_write); |
| width -= to_write; |
| } |
| } |
| |
| for (size_t i = 0; i < len; ++i) { |
| size_t idx = big_endian ? len - i - 1 : i; |
| buf[buffered] = glyphs[(bytes[idx] >> 4) & 0xf]; |
| buf[buffered + 1] = glyphs[bytes[idx] & 0xf]; |
| buffered += 2; |
| |
| if (buffered == ARRAYSIZE(buf)) { |
| bytes_written += out.sink(out.data, buf, buffered); |
| buffered = 0; |
| } |
| } |
| |
| if (buffered != 0) { |
| bytes_written += out.sink(out.data, buf, buffered); |
| } |
| return bytes_written; |
| } |
| |
| /** |
| * Prints out the next entry in `args` according to `spec`. |
| * |
| * This function assumes that `spec` accurately describes the next entry in |
| * `args`. |
| * |
| * @param out the sink to write bytes to. |
| * @param spec the specifier to use for stringifying. |
| * @param[out] bytes_written out param for the number of bytes writen to `out`. |
| * @param args the list to pull an entry from. |
| */ |
| static void process_specifier(buffer_sink_t out, format_specifier_t spec, |
| size_t *bytes_written, va_list *args) { |
| // Switch on the specifier. At this point, we assert that there is |
| // an initialized value of correct type in the VA list; if it is |
| // missing, the caller has caused UB. |
| switch (spec.type) { |
| case kPercent: { |
| if (spec.is_nonstd) { |
| goto bad_spec; |
| } |
| *bytes_written += out.sink(out.data, "%", 1); |
| break; |
| } |
| case kCharacter: { |
| if (spec.is_nonstd) { |
| goto bad_spec; |
| } |
| char value = (char)va_arg(*args, uint32_t); |
| *bytes_written += out.sink(out.data, &value, 1); |
| break; |
| } |
| case kString: { |
| size_t len = 0; |
| if (spec.is_nonstd) { |
| // This implements %!s. |
| len = va_arg(*args, size_t); |
| } |
| |
| char *value = va_arg(*args, char *); |
| while (!spec.is_nonstd && value[len] != '\0') { |
| // This implements %s. |
| ++len; |
| } |
| |
| *bytes_written += out.sink(out.data, value, len); |
| break; |
| } |
| case kSignedDec1: |
| case kSignedDec2: { |
| if (spec.is_nonstd) { |
| goto bad_spec; |
| } |
| uint32_t value = va_arg(*args, uint32_t); |
| if (((int32_t)value) < 0) { |
| *bytes_written += out.sink(out.data, "-", 1); |
| value = -value; |
| } |
| *bytes_written += |
| write_digits(out, value, spec.width, spec.padding, 10, kDigitsLow); |
| break; |
| } |
| case kUnsignedOct: { |
| if (spec.is_nonstd) { |
| goto bad_spec; |
| } |
| uint32_t value = va_arg(*args, uint32_t); |
| *bytes_written += |
| write_digits(out, value, spec.width, spec.padding, 8, kDigitsLow); |
| break; |
| } |
| case kPointer: { |
| if (spec.is_nonstd) { |
| goto bad_spec; |
| } |
| // Pointers are formatted as 0x<hex digits>, where the width is always |
| // set to the number necessary to represent a pointer on the current |
| // platform, that is, the size of uintptr_t in nybbles. For example, on |
| // different architecutres the null pointer prints as |
| // - rv32imc: 0x00000000 (four bytes, eight nybbles). |
| // - amd64: 0x0000000000000000 (eight bytes, sixteen nybbles). |
| *bytes_written += out.sink(out.data, "0x", 2); |
| uintptr_t value = va_arg(*args, uintptr_t); |
| *bytes_written += |
| write_digits(out, value, sizeof(uintptr_t) * 2, '0', 16, kDigitsLow); |
| break; |
| } |
| case kUnsignedHexLow: |
| if (spec.is_nonstd) { |
| size_t len = va_arg(*args, size_t); |
| char *value = va_arg(*args, char *); |
| *bytes_written += hex_dump(out, value, len, spec.width, spec.padding, |
| /*big_endian=*/true, kDigitsLow); |
| break; |
| } |
| OT_FALLTHROUGH_INTENDED; |
| case kSvHexLow: { |
| uint32_t value = va_arg(*args, uint32_t); |
| *bytes_written += |
| write_digits(out, value, spec.width, spec.padding, 16, kDigitsLow); |
| break; |
| } |
| case kUnsignedHexHigh: |
| if (spec.is_nonstd) { |
| size_t len = va_arg(*args, size_t); |
| char *value = va_arg(*args, char *); |
| *bytes_written += hex_dump(out, value, len, spec.width, spec.padding, |
| /*big_endian=*/true, kDigitsHigh); |
| break; |
| } |
| OT_FALLTHROUGH_INTENDED; |
| case kSvHexHigh: { |
| uint32_t value = va_arg(*args, uint32_t); |
| *bytes_written += |
| write_digits(out, value, spec.width, spec.padding, 16, kDigitsHigh); |
| break; |
| } |
| case kHexLeLow: { |
| if (!spec.is_nonstd) { |
| goto bad_spec; |
| } |
| size_t len = va_arg(*args, size_t); |
| char *value = va_arg(*args, char *); |
| *bytes_written += hex_dump(out, value, len, spec.width, spec.padding, |
| /*big_endian=*/false, kDigitsLow); |
| break; |
| } |
| case kHexLeHigh: { |
| if (!spec.is_nonstd) { |
| goto bad_spec; |
| } |
| size_t len = va_arg(*args, size_t); |
| char *value = va_arg(*args, char *); |
| *bytes_written += hex_dump(out, value, len, spec.width, spec.padding, |
| /*big_endian=*/false, kDigitsHigh); |
| break; |
| } |
| case kUnsignedDec: { |
| if (spec.is_nonstd) { |
| goto bad_spec; |
| } |
| uint32_t value = va_arg(*args, uint32_t); |
| *bytes_written += |
| write_digits(out, value, spec.width, spec.padding, 10, kDigitsLow); |
| break; |
| } |
| case kSvBinary: { |
| if (spec.is_nonstd) { |
| // Bools passed into a ... function will be automatically promoted |
| // to int; va_arg(..., bool) is actually UB! |
| if (va_arg(*args, int) != 0) { |
| *bytes_written += out.sink(out.data, "true", 4); |
| } else { |
| *bytes_written += out.sink(out.data, "false", 5); |
| } |
| break; |
| } |
| uint32_t value = va_arg(*args, uint32_t); |
| *bytes_written += |
| write_digits(out, value, spec.width, spec.padding, 2, kDigitsLow); |
| break; |
| } |
| case kStatusResult: { |
| status_t value = va_arg(*args, status_t); |
| *bytes_written += write_status(out, value, spec.is_nonstd); |
| break; |
| } |
| bad_spec: // Used with `goto` to bail out early. |
| default: { |
| *bytes_written += out.sink(out.data, kUnknownSpec, sizeof(kUnknownSpec)); |
| } |
| } |
| } |
| |
| size_t base_vfprintf(buffer_sink_t out, const char *format, va_list args) { |
| if (out.sink == NULL) { |
| out.sink = &base_dev_null; |
| } |
| |
| // NOTE: This copy is necessary on amd64 and other platforms, where |
| // `va_list` is a fixed array type (and, as such, decays to a pointer in |
| // an argument list). On PSABI RV32IMC, however, `va_list` is a `void*`, so |
| // this is a copy of the pointer, not the array. |
| va_list args_copy; |
| va_copy(args_copy, args); |
| |
| size_t bytes_written = 0; |
| while (format[0] != '\0') { |
| if (!consume_until_percent(out, &format, &bytes_written)) { |
| break; |
| } |
| format_specifier_t spec; |
| if (!consume_format_specifier(out, &format, &bytes_written, &spec)) { |
| break; |
| } |
| |
| process_specifier(out, spec, &bytes_written, &args_copy); |
| } |
| |
| va_end(args_copy); |
| return bytes_written; |
| } |
| |
| const char kBaseHexdumpDefaultFmtAlphabet[256] = |
| // clang-format off |
| // First 32 characters are not printable. |
| "................................" |
| // Printable ASCII. |
| " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" |
| // The rest of the range is also not printable (129 characters). |
| "................................................................" |
| "................................................................" |
| "."; |
| // clang-format on |
| |
| static const base_hexdump_fmt_t kBaseHexdumpDefaultFmt = { |
| .bytes_per_word = 2, |
| .words_per_line = 8, |
| .alphabet = &kBaseHexdumpDefaultFmtAlphabet, |
| }; |
| |
| size_t base_hexdump(const char *buf, size_t len) { |
| return base_hexdump_with(kBaseHexdumpDefaultFmt, buf, len); |
| } |
| |
| size_t base_snhexdump(char *out, size_t out_len, const char *buf, size_t len) { |
| return base_snhexdump_with(out, out_len, kBaseHexdumpDefaultFmt, buf, len); |
| } |
| |
| size_t base_fhexdump(buffer_sink_t out, const char *buf, size_t len) { |
| return base_fhexdump_with(out, kBaseHexdumpDefaultFmt, buf, len); |
| } |
| |
| size_t base_hexdump_with(base_hexdump_fmt_t fmt, const char *buf, size_t len) { |
| return base_fhexdump_with(base_stdout, fmt, buf, len); |
| } |
| |
| size_t base_snhexdump_with(char *out, size_t out_len, base_hexdump_fmt_t fmt, |
| const char *buf, size_t len) { |
| snprintf_captures_t data = { |
| .buf = out, |
| .bytes_left = out_len, |
| }; |
| buffer_sink_t sink = { |
| .data = &data, |
| .sink = &snprintf_sink, |
| }; |
| return base_fhexdump_with(sink, fmt, buf, len); |
| } |
| |
| size_t base_fhexdump_with(buffer_sink_t out, base_hexdump_fmt_t fmt, |
| const char *buf, size_t len) { |
| size_t bytes_written = 0; |
| size_t bytes_per_line = fmt.bytes_per_word * fmt.words_per_line; |
| |
| for (size_t line = 0; line < len; line += bytes_per_line) { |
| bytes_written += base_fprintf(out, "%08x:", line); |
| |
| size_t chars_per_line = bytes_per_line * 2 + fmt.words_per_line; |
| size_t line_bytes_written = 0; |
| for (size_t word = 0; word < bytes_per_line; word += fmt.bytes_per_word) { |
| if (len < line + word) { |
| char spaces[16] = " "; |
| while (line_bytes_written < chars_per_line) { |
| size_t to_print = chars_per_line - line_bytes_written; |
| if (to_print > sizeof(spaces)) { |
| to_print = sizeof(spaces); |
| } |
| line_bytes_written += base_fprintf(out, "%!s", to_print, spaces); |
| } |
| break; |
| } |
| |
| size_t bytes_left = len - line - word; |
| if (bytes_left > fmt.bytes_per_word) { |
| bytes_left = fmt.bytes_per_word; |
| } |
| line_bytes_written += base_fprintf(out, " "); |
| line_bytes_written += |
| hex_dump(out, buf + line + word, bytes_left, bytes_left, '0', |
| /*big_endian=*/false, kDigitsLow); |
| } |
| bytes_written += line_bytes_written; |
| |
| bytes_written += base_fprintf(out, " "); |
| size_t buffered = 0; |
| char glyph_buffer[16]; |
| for (size_t byte = 0; byte < bytes_per_line; ++byte) { |
| if (buffered == sizeof(glyph_buffer)) { |
| bytes_written += base_fprintf(out, "%!s", buffered, glyph_buffer); |
| buffered = 0; |
| } |
| if (line + byte >= len) { |
| break; |
| } |
| glyph_buffer[buffered++] = |
| (*fmt.alphabet)[(size_t)(uint8_t)buf[line + byte]]; |
| } |
| if (buffered > 0) { |
| bytes_written += base_fprintf(out, "%!s", buffered, glyph_buffer); |
| } |
| bytes_written += base_fprintf(out, "\n"); |
| } |
| |
| return bytes_written; |
| } |