Skip to content

Commit

Permalink
feat!: add VirtIO console device
Browse files Browse the repository at this point in the history
  • Loading branch information
edubart committed Jan 19, 2024
1 parent 3823c2f commit 56b64d6
Show file tree
Hide file tree
Showing 22 changed files with 2,157 additions and 121 deletions.
5 changes: 4 additions & 1 deletion src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ LIBCARTESI_OBJS:= \
clint-factory.o \
plic.o \
plic-factory.o \
virtio-factory.o \
virtio-device.o \
virtio-console.o \
dtb.o \
os.o \
htif.o \
Expand Down Expand Up @@ -481,7 +484,7 @@ test-c-api: c-api remote-cartesi-machine
test-linux-workload: luacartesi
$(LUA) ./cartesi-machine.lua -- "$(COVERAGE_WORKLOAD)"
# Test interactive mode (to cover mcycle overwriting)
echo uname | $(LUA) ./cartesi-machine.lua -i sh
echo uname | $(LUA) ./cartesi-machine.lua -it sh
# Test max mcycle (to cover max mcycle branch)
$(LUA) ./cartesi-machine.lua --max-mcycle=1

Expand Down
13 changes: 4 additions & 9 deletions src/cartesi-machine.lua
Original file line number Diff line number Diff line change
Expand Up @@ -654,18 +654,13 @@ local options = {
function(all)
if not all then return false end
htif_console_getchar = true
-- Switch from HTIF Console (hvc0) to VirtIO console (hvc1)
bootargs = bootargs:gsub("console=hvc0", "console=hvc1")
-- Expose current terminal features to the virtual terminal
local term, lang, lc_all = os.getenv("TERM"), os.getenv("LANG"), os.getenv("LC_ALL")
if term then append_init = append_init .. "export TERM=" .. term .. "\n" end
if lang then append_init = append_init .. "export LANG=" .. lang .. "\n" end
if lc_all then append_init = append_init .. "export LC_ALL=" .. lc_all .. "\n" end
local stty <close> = assert(io.popen("stty size"))
local line = assert(stty:read(), "command failed: stty size")
if line then
local rows, cols = line:match("^([0-9]+) ([0-9]+)$")
if rows and cols then
append_init = append_init .. "busybox stty rows " .. rows .. " cols " .. cols .. "\n"
end
end
return true
end,
},
Expand Down Expand Up @@ -896,7 +891,7 @@ local options = {
flash_length.root = nil
flash_shared.root = nil
table.remove(flash_label_order, 1)
bootargs = "quiet earlycon=sbi console=hvc0"
bootargs = bootargs:gsub(" rootfstype=.*$", "")
return true
end,
},
Expand Down
10 changes: 6 additions & 4 deletions src/device-state-access.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,13 @@ class device_state_access : public i_device_state_access {
return m_a.read_htif_iyield();
}

// LCOV_EXCL_START
void do_write_memory(uint64_t paddr, const unsigned char *data, uint64_t log2_length) override {
return m_a.write_memory(paddr, data, log2_length);
bool do_read_memory(uint64_t paddr, unsigned char *data, uint64_t length) override {
return m_a.read_memory(paddr, data, length);
}

bool do_write_memory(uint64_t paddr, const unsigned char *data, uint64_t length) override {
return m_a.write_memory(paddr, data, length);
}
// LCOV_EXCL_STOP

uint64_t do_read_pma_istart(int p) override {
return m_a.read_pma_istart(p);
Expand Down
10 changes: 10 additions & 0 deletions src/dtb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@ void dtb_init(const machine_config &c, unsigned char *dtb_start, uint64_t dtb_le
fdt.prop_u32_list<2>("interrupts-extended", {INTC_PHANDLE, X_HOST});
fdt.end_node();
}
if (c.htif.console_getchar) { // virtio console
const uint32_t virtio_idx = 0;
const uint64_t virtio_paddr = PMA_FIRST_VIRTIO_START + virtio_idx * PMA_VIRTIO_LENGTH;
const uint32_t plic_irq_id = virtio_idx + 1;
fdt.begin_node_num("virtio", virtio_paddr);
fdt.prop_string("compatible", "virtio,mmio");
fdt.prop_u64_list<2>("reg", {virtio_paddr, PMA_VIRTIO_LENGTH});
fdt.prop_u32_list<2>("interrupts-extended", {PLIC_PHANDLE, plic_irq_id});
fdt.end_node();
}
fdt.end_node();
}

Expand Down
3 changes: 2 additions & 1 deletion src/htif.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ static execute_status htif_console(htif_runtime_config *runtime_config, i_device
// to every participant in a dispute: where would c come from? So if the code reached here in the
// blockchain, there must be some serious bug
// In interactive mode, we just get the next character from the console and send it back in the ack
const int c = os_getchar();
os_poll_tty(0);
const int c = os_getchar() + 1;
a->write_htif_fromhost(HTIF_BUILD(HTIF_DEVICE_CONSOLE, cmd, c));
}
}
Expand Down
27 changes: 19 additions & 8 deletions src/i-device-state-access.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,27 @@ class i_device_state_access {
return do_read_htif_iyield();
}

// LCOV_EXCL_START
/// \brief Reads a chunk of data from a memory PMA range.
/// \param address Target physical address.
/// \param data Receives chunk of memory.
/// \param length Size of chunk.
/// \returns True if PMA was found and memory fully read, false otherwise.
/// \details The entire chunk of data must fit inside the same memory
/// PMA range, otherwise it fails. The search for the PMA range is implicit, and not logged.
bool read_memory(uint64_t paddr, unsigned char *data, uint64_t length) {
return do_read_memory(paddr, data, length);
}

/// \brief Writes a chunk of data to a memory PMA range.
/// \param paddr Target physical address. Must be aligned to data size.
/// \param paddr Target physical address.
/// \param data Pointer to chunk of data.
/// \param log2_size Log 2 of data size. Must be >= 3 and < 64.
/// \param length Size of chunk.
/// \returns True if PMA was found and memory fully written, false otherwise.
/// \details The entire chunk of data must fit inside the same memory
/// PMA range. The search for the PMA range is implicit, and not logged.
void write_memory(uint64_t paddr, const unsigned char *data, uint64_t log2_size) {
return do_write_memory(paddr, data, log2_size);
/// PMA range, otherwise it fails. The search for the PMA range is implicit, and not logged.
bool write_memory(uint64_t paddr, const unsigned char *data, uint64_t length) {
return do_write_memory(paddr, data, length);
}
// LCOV_EXCL_STOP

/// \brief Reads the istart field of a PMA entry
/// \param p Index of PMA
Expand Down Expand Up @@ -213,7 +223,8 @@ class i_device_state_access {
virtual uint64_t do_read_htif_ihalt(void) = 0;
virtual uint64_t do_read_htif_iconsole(void) = 0;
virtual uint64_t do_read_htif_iyield(void) = 0;
virtual void do_write_memory(uint64_t paddr, const unsigned char *data, uint64_t log2_size) = 0;
virtual bool do_read_memory(uint64_t paddr, unsigned char *data, uint64_t length) = 0;
virtual bool do_write_memory(uint64_t paddr, const unsigned char *data, uint64_t length) = 0;
virtual uint64_t do_read_pma_istart(int p) = 0;
virtual uint64_t do_read_pma_ilength(int p) = 0;
};
Expand Down
35 changes: 26 additions & 9 deletions src/i-state-access.h
Original file line number Diff line number Diff line change
Expand Up @@ -602,11 +602,16 @@ class i_state_access { // CRTP
return derived().do_read_htif_iyield();
}

/// \brief Polls console for pending input.
/// \brief Poll for external interrupts.
/// \param mcycle Current machine mcycle.
/// \returns The new machine mcycle advanced by the relative time elapsed while polling.
uint64_t poll_console(uint64_t mcycle) {
return derived().do_poll_console(mcycle);
/// \param mcycle_max Maximum mcycle to wait for interrupts.
/// \returns A pair, the first value is the new machine mcycle advanced by the relative elapsed time while
/// polling, the second value is a boolean that is true when the poll is stopped due do an external interrupt
/// request.
/// \details When mcycle_max is greater than mcycle, this function will sleep until an external interrupt
/// is triggered or mcycle_max relative elapsed time is reached.
std::pair<uint64_t, bool> poll_external_interrupts(uint64_t mcycle, uint64_t mcycle_max) {
return derived().do_poll_external_interrupts(mcycle, mcycle_max);
}

/// \brief Reads PMA at a given index.
Expand All @@ -628,14 +633,26 @@ class i_state_access { // CRTP
return derived().do_read_pma_ilength(p);
}

/// \brief Reads a chunk of data from a memory PMA range.
/// \param paddr Target physical address.
/// \param data Receives chunk of memory.
/// \param length Size of chunk.
/// \returns True if PMA was found and memory fully read, false otherwise.
/// \details The entire chunk of data must fit inside the same memory
/// PMA range, otherwise it fails. The search for the PMA range is implicit, and not logged.
bool read_memory(uint64_t paddr, unsigned char *data, uint64_t length) {
return derived().do_read_memory(paddr, data, length);
}

/// \brief Writes a chunk of data to a memory PMA range.
/// \param paddr Target physical address. Must be aligned to data size.
/// \param paddr Target physical address.
/// \param data Pointer to chunk of data.
/// \param log2_size Log 2 of data length. Must be >= 3 and < 64.
/// \param length Size of chunk.
/// \returns True if PMA was found and memory fully written, false otherwise.
/// \details The entire chunk of data must fit inside the same memory
/// PMA range. The search for the PMA range is implicit, and not logged.
void write_memory(uint64_t paddr, const unsigned char *data, uint64_t log2_size) {
return derived().do_write_memory(paddr, data, log2_size);
/// PMA range, otherwise it fails. The search for the PMA range is implicit, and not logged.
bool write_memory(uint64_t paddr, const unsigned char *data, uint64_t length) {
return derived().do_write_memory(paddr, data, length);
}

/// \brief Reads a word from memory.
Expand Down
41 changes: 29 additions & 12 deletions src/interpret.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -525,12 +525,10 @@ static inline uint64_t raise_interrupt_if_any(STATE_ACCESS &a, uint64_t pc) {
/// \param mcycle Machine current cycle.
template <typename STATE_ACCESS>
static inline void set_rtc_interrupt(STATE_ACCESS &a, uint64_t mcycle) {
if (rtc_is_tick(mcycle)) {
const uint64_t timecmp_cycle = rtc_time_to_cycle(a.read_clint_mtimecmp());
if (timecmp_cycle <= mcycle && timecmp_cycle != 0) {
const uint64_t mip = a.read_mip();
a.write_mip(mip | MIP_MTIP_MASK);
}
const uint64_t timecmp_cycle = rtc_time_to_cycle(a.read_clint_mtimecmp());
if (timecmp_cycle <= mcycle && timecmp_cycle != 0) {
const uint64_t mip = a.read_mip();
a.write_mip(mip | MIP_MTIP_MASK);
}
}

Expand Down Expand Up @@ -912,7 +910,8 @@ static NO_INLINE std::pair<execute_status, uint64_t> write_virtual_memory_slow(S
return {execute_status::success, pc};
} else if (likely(pma.get_istart_IO())) {
const uint64_t offset = paddr - pma.get_start();
auto status = a.write_device(pma, mcycle, offset, val64, log2_size<U>::value);
auto status =
a.write_device(pma, mcycle, offset, static_cast<U>(static_cast<T>(val64)), log2_size<U>::value);
// If we do not know how to write, we treat this as a PMA violation
if (likely(status != execute_status::failure)) {
return {status, pc};
Expand Down Expand Up @@ -2617,9 +2616,19 @@ static FORCE_INLINE execute_status execute_WFI(STATE_ACCESS &a, uint64_t &pc, ui
if (unlikely(priv == PRV_U || (priv < PRV_M && (mstatus & MSTATUS_TW_MASK)))) {
return raise_illegal_insn_exception(a, pc, insn);
}
// Poll console, this may advance mcycle when in interactive mode
mcycle = a.poll_console(mcycle);
return advance_to_next_insn(a, pc);
// We wait for interrupts until the next timer interrupt.
const uint64_t mcycle_max = rtc_time_to_cycle(a.read_clint_mtimecmp());
execute_status status = execute_status::success;
if (mcycle_max > mcycle) {
// Poll for external interrupts (e.g console or network),
// this may advance mcycle only when interactive mode is enabled
const auto [next_mcycle, interrupted] = a.poll_external_interrupts(mcycle, mcycle_max);
mcycle = next_mcycle;
if (interrupted) {
status = execute_status::success_and_serve_interrupts;
}
}
return advance_to_next_insn(a, pc, status);
}

/// \brief Implementation of the FENCE instruction.
Expand Down Expand Up @@ -5535,8 +5544,16 @@ NO_INLINE void interpret_loop(STATE_ACCESS &a, uint64_t mcycle_end, uint64_t mcy
while (mcycle < mcycle_end) {
INC_COUNTER(a.get_statistics(), outer_loop);

// Set interrupt flag for RTC
set_rtc_interrupt(a, mcycle);
if (rtc_is_tick(mcycle)) {
// Set interrupt flag for RTC
set_rtc_interrupt(a, mcycle);

// Polling external interrupts only in WFI instructions is not enough
// because Linux won't execute WFI instructions while under heavy load,
// yet external interrupts still need to be triggered.
// Therefore we poll for external interrupt once a while in the interpreter loop.
a.poll_external_interrupts(mcycle, mcycle);
}

// Raise the highest priority pending interrupt, if any
pc = raise_interrupt_if_any(a, pc);
Expand Down
52 changes: 52 additions & 0 deletions src/machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
#include "uarch-step-state-access.h"
#include "uarch-step.h"
#include "unique-c-ptr.h"
#include "virtio-console.h"
#include "virtio-factory.h"

/// \file
/// \brief Cartesi machine implementation
Expand Down Expand Up @@ -438,6 +440,16 @@ machine::machine(const machine_config &c, const machine_runtime_config &r) :
// Register pma board shadow device
register_pma_entry(make_shadow_pmas_pma_entry(PMA_SHADOW_PMAS_START, PMA_SHADOW_PMAS_LENGTH));

// TODO(edubart): user should be able to configure these devices
if (m_c.htif.console_getchar) {
// Register VirtIO console device
auto vdev_console = std::make_unique<virtio_console>(m_vdevs.size());
register_pma_entry(
make_virtio_pma_entry(PMA_FIRST_VIRTIO_START + vdev_console->get_virtio_index() * PMA_VIRTIO_LENGTH,
PMA_VIRTIO_LENGTH, "VirtIO console device", &virtio_driver, vdev_console.get()));
m_vdevs.push_back(std::move(vdev_console));
}

// Initialize DTB
if (m_c.dtb.image_filename.empty()) {
// Write the FDT (flattened device tree) into DTB
Expand Down Expand Up @@ -494,6 +506,11 @@ machine::machine(const machine_config &c, const machine_runtime_config &r) :
// Sort it by increasing start address
std::sort(m_mrds.begin(), m_mrds.end(),
[](const machine_memory_range_descr &a, const machine_memory_range_descr &b) { return a.start < b.start; });

// Disable SIGPIPE handler, because this signal be raised and terminate the emulator process,
// when calling write() on closed file descriptors.
// This can happen with the stdout console file descriptors or network file descriptors.
os_disable_sigpipe();
}

static void load_hash(const std::string &dir, machine::hash_type &h) {
Expand All @@ -520,6 +537,41 @@ machine::machine(const std::string &dir, const machine_runtime_config &r) : mach
}
}

void machine::prepare_virtio_devices_select(select_fd_sets *fds, uint64_t *timeout_us) {
for (auto &vdev : m_vdevs) {
vdev->prepare_select(fds, timeout_us);
}
}

bool machine::poll_selected_virtio_devices(int select_ret, select_fd_sets *fds, i_device_state_access *da) {
bool interrupt_requested = false;
for (auto &vdev : m_vdevs) {
interrupt_requested |= vdev->poll_selected(select_ret, fds, da);
}
return interrupt_requested;
}

bool machine::poll_virtio_devices(uint64_t *timeout_us, i_device_state_access *da) {
return os_select_fds(
[&](select_fd_sets *fds, uint64_t *timeout_us) -> void { prepare_virtio_devices_select(fds, timeout_us); },
[&](int select_ret, select_fd_sets *fds) -> bool { return poll_selected_virtio_devices(select_ret, fds, da); },
timeout_us);
}

bool machine::has_virtio_devices() const {
return !m_vdevs.empty();
}

bool machine::has_virtio_console() const {
// When present, the console device is guaranteed to be the first VirtIO device,
// therefore we only need to check the first device.
return !m_vdevs.empty() && m_vdevs[0]->get_device_id() == VIRTIO_DEVICE_CONSOLE;
}

bool machine::has_htif_console() const {
return static_cast<bool>(read_htif_iconsole() & (1 << HTIF_CONSOLE_GETCHAR));
}

machine_config machine::get_serialization_config(void) const {
// Initialize with copy of original config
machine_config c = m_c;
Expand Down
Loading

0 comments on commit 56b64d6

Please sign in to comment.