diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e27b6a23d2..aa9b1826e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: run: | sudo apt-get install wget build-essential ninja-build sudo apt-get install libevent-dev libjson-c-dev flex bison - sudo apt-get install libfl-dev libfl2 zlib1g-dev + sudo apt-get install libfl-dev libfl2 zlib1g-dev libzmq3-dev - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/litex/build/sim/core/Makefile b/litex/build/sim/core/Makefile index 4866d3a879..d035193551 100644 --- a/litex/build/sim/core/Makefile +++ b/litex/build/sim/core/Makefile @@ -10,11 +10,11 @@ ifeq ($(UNAME_S),Darwin) CFLAGS += -I/opt/homebrew/include LDFLAGS += -L/opt/homebrew/lib endif - LDFLAGS += -lpthread -ljson-c -lz -lm -lstdc++ -ldl -levent $(if $(VIDEO), -lSDL2) + LDFLAGS += -lpthread -ljson-c -lz -lm -lstdc++ -ldl -levent $(if $(VIDEO), -lSDL2) -lzmq -rdynamic else CC ?= gcc CFLAGS += -ggdb - LDFLAGS += -lpthread -Wl,--no-as-needed -ljson-c -lz -lm -lstdc++ -Wl,--no-as-needed -ldl -levent $(if $(VIDEO), -lSDL2) + LDFLAGS += -lpthread -Wl,--no-as-needed -ljson-c -lz -lm -lstdc++ -Wl,--no-as-needed -ldl -levent $(if $(VIDEO), -lSDL2) -lzmq -rdynamic endif CFLAGS += -Wall -$(OPT_LEVEL) $(if $(COVERAGE), -DVM_COVERAGE) $(if $(TRACE_FST), -DTRACE_FST) diff --git a/litex/build/sim/core/modules.c b/litex/build/sim/core/modules.c index 7c7a464300..a159c75304 100644 --- a/litex/build/sim/core/modules.c +++ b/litex/build/sim/core/modules.c @@ -157,3 +157,23 @@ int litex_sim_find_module(struct module_s *first, char *name , struct module_s * *found = list; return ret; } + +const char* msg_return_strerror(msg_return_t err) { + switch (err) { + case MSGRET_SUCCESS: + return "success"; + break; + case MSGRET_FAIL: + return "fail"; + break; + case MSGRET_MODSESSION_NOT_FOUND: + return "modsession_not_found"; + break; + case MSGRET_INVALID_OP: + return "invalid_op"; + break; + default: + return "unknown"; + break; + } +} diff --git a/litex/build/sim/core/modules.h b/litex/build/sim/core/modules.h index 76dba43083..0c32e6e974 100644 --- a/litex/build/sim/core/modules.h +++ b/litex/build/sim/core/modules.h @@ -7,12 +7,6 @@ #include #include "pads.h" -typedef enum clk_edge { - CLK_EDGE_NONE, - CLK_EDGE_RISING, - CLK_EDGE_FALLING, -} clk_edge_t; - struct interface_s { char *name; int index; @@ -27,13 +21,217 @@ struct module_s { struct module_s *next; }; +/** + * Inter-module messaging. + * + * The LiteX simulator provides a way for modules to exchange messages. This can + * be used for a variety of things, such as emulating hardware which is + * connected two two independent bus subsystems, controlling the simulation, + * etc. The messaging system does not define the content and types of messages + * transmitted. + * + * While modules are free to implement their own message handlers, opcodes < 256 + * are reserved and must be defined in this file below. This is to enable + * implementation of global, non module-specific behavior. + * + * An example of this is the simctrl interface. It is a generic component which + * can be used to allow external programs to interface with the simulation and + * is a bridge for messages to specific modules in the simulation. Thus it's + * interface and it's opcodes are specified here. + * + * To discover other modules in the simulation, each module with a registered + * module handler will retrieve a message of opcode MODMSG_OP_NEWMODSESSION with + * a `data` value of type `modmsg_newmodsession_payload_t` for each module + * session in the simulation. The returned `*retdata` must be NULL. + */ + +// Global inter-module messaging opcodes +#define MODMSG_OP_NEWMODSESSION 0 +#define MODMSG_OP_SIMCTRL_REQ 1 +#define MODMSG_OP_SIMCTRL_RETFREE 2 + +/** + * Error codes for inter-module messaging. + * + * Modules must never return MSGRET_MODSESSION_NOT_FOUND. + */ +typedef enum { + MSGRET_SUCCESS = 0, + MSGRET_FAIL = -1, + MSGRET_MODSESSION_NOT_FOUND = -2, + MSGRET_INVALID_OP = -3, +} msg_return_t; + +/** + * Convert a msg_return_t to a string representation. + * + * This performs no allocations. + */ +const char* msg_return_strerror(msg_return_t err); + + +/** + * A simctrl message targeted towards a specific module. + * + * The MODMSG_OP_SIMCTRL_REQ and MODMSG_OP_SIMCTRL_RETFREE messages use this + * data type as their `data` parameter. The MODMSG_OP_SIMCTRL_REQ messages + * accept an optional `retdata` pointer to point towards a valid instance of + * this data type. + * + * The simctrl_msg_t pointer passed into `data` on a MODMSG_OP_SIMCTRL_REQ, + * along with the contained `data` field, is allocated by the caller and only + * valid for the duration of the method invocation. + * + * On a MODMSG_OP_SIMCTRL_REQ the receiving module may wish to return a response + * to the caller. It can allocate a `simctrl_msg_t` and make `retdata` point to + * this allocated struct, which will be recognized as a response by the message + * sender. If `retdata` is non-NULL, to allow freeing the allocated memory, the + * sender must subsequently send a MODMSG_OP_SIMCTRL_RETFREE message, with the + * `data` pointer set to the value written to `retdata`. This can be used by the + * receiving module to free the previously allocated module. To keep additional + * state between sending the response and freeing the response memory, the + * receiver is free to use the `retdata_private` field. + * + * Upon a MODMSG_OP_SIMCTRL_RETFREE, the `retdata` field MUST be set to NULL. No + * additional response may be returned. + * + * Visually, the message flow looks like the following: + * + * /----------------\ /--------------\ + * | simctrl | MODMSG_OP_SIMCTRL_REQ | other module | + * | implementation | -> `data` stack allocated | | + * | |---------------------------------->| | + * | | `*retdata` heap allocated <- | | + * | | or NULL | | + * | | | | + * -- if `*retdata` is not NULL ---------------------------------------- + * | | | | + * | | MODMSG_OP_SIMCTRL_RETFREE | | + * | | -> `data` = previous `*retdata` | | + * | |---------------------------------->| | + * | | `*retdata` MUST be NULL <- | | + * \----------------/ \--------------/ + */ +typedef struct { + size_t len; + void* data; + void* retdata_private; +} simctrl_msg_t; + +/** + * Identifier of a specific module session. + * + * Modules must not rely on or access the contents of this struct. + */ +typedef struct { + void *sptr; +} litex_sim_msid_t; + +/** + * Payload of the MODMSG_OP_NEWMODSESSION message. + * + * Indicates that a specific module session has been registered with the + * simulation. `mod_name` will contain the name of the registered + * module. `mod_session_id` can be used with `litex_sim_send_msg` to send + * messages to the respective module session. + */ +typedef struct { + char *mod_name; + litex_sim_msid_t mod_session_id; +} modmsg_newmodsession_payload_t; + +/** + * Send an inter-module message. + * + * Callers must pass in a valid sim_handle, typically announced to them using a + * parameter on the `start` method of the `ext_module_s` interface. + * + * `mod_session_id` determines to which module session this message will be + * routed. This value must have been provided to the caller through the + * MODMSG_OP_NEWMODSESSION message. + * + * `msg_op`, `data` and `retdata` are forwarded to the call to `module_msg` on + * the `ext_module_s` interface. + * + * In case the module session could not be found, this method will return + * MSGRET_MODSESSION_NOT_FOUND. In case the module has no message handler + * defined, this method will return MSGRET_INVALID_OP. If the module itself + * returns MSGRET_MODSESSION_NOT_FOUND, this will be converted into a + * MSGRET_FAIL. + */ +msg_return_t litex_sim_send_msg( + void *sim_handle, + litex_sim_msid_t mod_session_id, + uint32_t msg_op, + void* data, + void** retdata +); + +/** + * Retrieve the current simulation time in picoseconds. + */ +uint64_t litex_sim_current_time_ps(void *sim_handle); + +/** + * Query the current state of the simulation. + * + * Returns: + * - `true` if the simulation is currently halted. + * - `false` if the simulation is currently running. + */ +bool litex_sim_halted(void *sim_handle); + +/** + * Request the simulation to halt or resume. + * + * Parameters: + * `halt`: - if `true`, halt the simulation + * - if `false`, resume the simulation + * + * If the simulation is already in the requested state, this does nothing. + * + * Beware that halting the simulation will eventually cause calls to `tick` + * methods on `ext_module_s` interfaces to cease. The module should have a way + * to resume the simulation at some point, possibly through a scheduled event on + * the libevent base. + */ +void litex_sim_halt(bool halt); + struct ext_module_s { char *name; - int (*start)(void *); + int (*start)(void *, void *sim_handle); int (*new_sess)(void **, char *); int (*add_pads)(void *, struct pad_list_s *); int (*close)(void*); int (*tick)(void*, uint64_t); + + /** Generic interface for inter-module communication + * + * This method is invoked whenever a message for a module with a matching name + * is received. + * + * It is legal for a module to not implement this function. If this function + * pointer is set to NULL, every received message will automatically return an + * MSGRET_INVALID_OP. + * + * The return code must indicate whether the message received and processed + * successfully. Implementations must never return + * MSGRET_MODSESSION_NOT_FOUND. + * + * `msg_op` hints at the type of a message. The API contract per `msg_op` is + * defined by the module itself. Thus `msg_op` must be unique within a given + * module only. + * + * Any message payload will be passed as `data`. If no payload is provided, + * this pointer can be NULL. + * + * A pointer to a return payload, if any, can be placed in `retdata`. How + * allocated memory for return payload is freed is not specified in this + * interface. One possible implementation is to define an additional `msg_op` + * per `msg_op`, invoked with the pointer to `retdata` passed in such that the + * module implementation can decide whether memory needs to be freed. + */ + msg_return_t (*module_msg)(void* state, uint32_t msg_op, void *data, void **retdata); }; struct ext_module_list_s { @@ -41,14 +239,20 @@ struct ext_module_list_s { struct ext_module_list_s *next; }; -typedef struct clk_edge_state { - int last_clk; -} clk_edge_state_t; - int litex_sim_file_parse(char *filename, struct module_s **mod, uint64_t *timebase); int litex_sim_load_ext_modules(struct ext_module_list_s **mlist); int litex_sim_find_ext_module(struct ext_module_list_s *first, char *name , struct ext_module_list_s **found); +typedef enum clk_edge { + CLK_EDGE_NONE, + CLK_EDGE_RISING, + CLK_EDGE_FALLING, +} clk_edge_t; + +typedef struct clk_edge_state { + int last_clk; +} clk_edge_state_t; + inline bool clk_pos_edge(clk_edge_state_t *edge_state, int new_clk) { bool is_edge = edge_state->last_clk == 0 && new_clk == 1; edge_state->last_clk = new_clk; diff --git a/litex/build/sim/core/modules/Makefile b/litex/build/sim/core/modules/Makefile index d5bb16658d..749b4f3343 100644 --- a/litex/build/sim/core/modules/Makefile +++ b/litex/build/sim/core/modules/Makefile @@ -1,5 +1,6 @@ include ../variables.mak -MODULES = xgmii_ethernet ethernet serial2console serial2tcp clocker spdeeprom gmii_ethernet jtagremote $(if $(VIDEO), video) +MODULES = simctrl xgmii_ethernet ethernet serial2console serial2tcp clocker spdeeprom gmii_ethernet jtagremote $(if $(VIDEO), video) gpio + .PHONY: $(MODULES) $(EXTRA_MOD_LIST) all: $(MODULES) $(EXTRA_MOD_LIST) diff --git a/litex/build/sim/core/modules/clocker/clocker.c b/litex/build/sim/core/modules/clocker/clocker.c index 92115a4130..87334d2f2b 100644 --- a/litex/build/sim/core/modules/clocker/clocker.c +++ b/litex/build/sim/core/modules/clocker/clocker.c @@ -85,7 +85,7 @@ static int clocker_parse_args(struct session_s *s, const char *args) return ret; } -static int clocker_start() +static int clocker_start(void *b, void *sh) { printf("[clocker] loaded\n"); return RC_OK; @@ -164,7 +164,8 @@ static struct ext_module_s ext_mod = { clocker_new, clocker_add_pads, NULL, - clocker_tick + clocker_tick, + NULL, }; int litex_sim_ext_module_init(int (*register_module)(struct ext_module_s *)) diff --git a/litex/build/sim/core/modules/ethernet/ethernet.c b/litex/build/sim/core/modules/ethernet/ethernet.c index 196790ff8c..e9621f4bc9 100644 --- a/litex/build/sim/core/modules/ethernet/ethernet.c +++ b/litex/build/sim/core/modules/ethernet/ethernet.c @@ -97,7 +97,7 @@ static int litex_sim_module_pads_get(struct pad_s *pads, char *name, void **sign return ret; } -static int ethernet_start(void *b) +static int ethernet_start(void *b, void *sh) { base = (struct event_base *) b; printf("[ethernet] loaded (%p)\n", base); @@ -248,7 +248,8 @@ static struct ext_module_s ext_mod = { ethernet_new, ethernet_add_pads, NULL, - ethernet_tick + ethernet_tick, + NULL, }; int litex_sim_ext_module_init(int (*register_module)(struct ext_module_s *)) diff --git a/litex/build/sim/core/modules/gmii_ethernet/gmii_ethernet.c b/litex/build/sim/core/modules/gmii_ethernet/gmii_ethernet.c index e773f6b7bd..c28ebd08bc 100644 --- a/litex/build/sim/core/modules/gmii_ethernet/gmii_ethernet.c +++ b/litex/build/sim/core/modules/gmii_ethernet/gmii_ethernet.c @@ -498,7 +498,7 @@ static int gmii_ethernet_add_pads(void *state, struct pad_list_s *plist) { return ret; } -static int gmii_ethernet_start(void *b) { +static int gmii_ethernet_start(void *b, void *sh) { base = (struct event_base *) b; printf("[gmii_ethernet] loaded (%p)\n", base); return RC_OK; @@ -555,7 +555,8 @@ static struct ext_module_s ext_mod = { gmii_ethernet_new, gmii_ethernet_add_pads, NULL, - gmii_ethernet_tick + gmii_ethernet_tick, + NULL, }; int litex_sim_ext_module_init(int (*register_module)(struct ext_module_s *)) { diff --git a/litex/build/sim/core/modules/gpio/Makefile b/litex/build/sim/core/modules/gpio/Makefile new file mode 100644 index 0000000000..dc81b943c0 --- /dev/null +++ b/litex/build/sim/core/modules/gpio/Makefile @@ -0,0 +1,14 @@ +include ../../variables.mak +UNAME_S := $(shell uname -s) + +include $(SRC_DIR)/modules/rules.mak + +CFLAGS += -I$(TAPCFG_DIRECTORY)/src/include +OBJS = $(MOD).o + +$(MOD).so: $(OBJS) +ifeq ($(UNAME_S),Darwin) + $(CC) $(LDFLAGS) -o $@ $^ +else + $(CC) $(LDFLAGS) -Wl,-soname,$@ -o $@ $^ +endif diff --git a/litex/build/sim/core/modules/gpio/gpio.c b/litex/build/sim/core/modules/gpio/gpio.c new file mode 100644 index 0000000000..b2b7e31a8c --- /dev/null +++ b/litex/build/sim/core/modules/gpio/gpio.c @@ -0,0 +1,451 @@ +/** + * GPIO module for interfacing with external applications through generic I/O + * signals. + * + * This module does little on its own, but can be controlled through the + * simctrl-style interface. This interface allows to drive pins and query their + * configuration (input/output) as well as output value. + * + * Copyright (c) 2021 Leon Schuermann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * Copyright (c) 2021 Leon Schuermann + */ + + +#include +#include +#include +#include +#include "error.h" + +#include +#include "modules.h" + +typedef struct { + // Simulation signals and signal attributes + uint64_t *sim_gpio_oe; + uint64_t *sim_gpio_o; + uint64_t *sim_gpio_i; + size_t sim_gpio_length; + uint8_t *sim_sys_clk; +} gpio_state_t; + +static struct event_base *base = NULL; +static void *sim_handle = NULL; + +int litex_sim_module_get_args(char *args, char *arg, char **val) +{ + int ret = RC_OK; + json_object *jsobj = NULL; + json_object *obj = NULL; + char *value = NULL; + int r; + + jsobj = json_tokener_parse(args); + if(NULL == jsobj) { + fprintf(stderr, "Error parsing json arg: %s \n", args); + ret = RC_JSERROR; + goto out; + } + + if(!json_object_is_type(jsobj, json_type_object)) { + fprintf(stderr, "Arg must be type object! : %s \n", args); + ret = RC_JSERROR; + goto out; + } + + obj=NULL; + r = json_object_object_get_ex(jsobj, arg, &obj); + if(!r) { + fprintf(stderr, "Could not find object: \"%s\" (%s)\n", arg, args); + ret = RC_JSERROR; + goto out; + } + value = strdup(json_object_get_string(obj)); + +out: + *val = value; + return ret; +} + +static int gpio_start(void *b, void* sh) { + base = (struct event_base *) b; + fprintf(stderr, "[gpio] loaded (libevent base: %p, sim_handle: %p)\n", + base, sim_handle); + return RC_OK; +} + +static int gpio_new(void **state, char *args) { + int res = RC_OK; + gpio_state_t *s = NULL; + + if (!state) { + res = RC_INVARG; + goto out; + } + + s = malloc(sizeof(gpio_state_t)); + if (!s) { + res = RC_NOENMEM; + goto out; + } + + // Initialize all fields of the gpio_state + + // Register this session + *state = (void*) s; + + // Everything worked, keep all allocated structures + goto out; + +//free_gpio_state: + free(s); + +out: + return res; +} + +static int gpio_add_pads(void *state, struct pad_list_s *plist) { + int ret = RC_OK; + gpio_state_t *s = state; + bool length_set = false; + + if(!state || !plist) { + ret = RC_INVARG; + goto out; + } + + if(!strcmp(plist->name, "gpio")) { + for (int i = 0; plist->pads[i].name; i++) { + if (strcmp(plist->pads[i].name, "oe") == 0) { + s->sim_gpio_oe = plist->pads[i].signal; + } else if (strcmp(plist->pads[i].name, "o") == 0) { + s->sim_gpio_o = plist->pads[i].signal; + } else if (strcmp(plist->pads[i].name, "i") == 0) { + s->sim_gpio_i = plist->pads[i].signal; + } else { + // If we match neither, don't execute the code below + continue; + } + + if (length_set) { + // Alright, we've set the length in the state + // once. Assert that all other signals have the same + // length, otherwise print an error. + if (plist->pads[i].len != s->sim_gpio_length) { + fprintf(stderr, "[gpio]: GPIO signals have different " + "lengths: %ld vs %ld. Can't reasonably handle " + "this, expect weird behavior!\n", + plist->pads[i].len, s->sim_gpio_length); + } + } else { + // The GPIO signal length has not been set. Check that + // it's below 64 (otherwise cap it and print an error) + // and set it. + if (plist->pads[i].len > 64) { + fprintf(stderr, "[gpio]: can't handle GPIO wider than 64 " + "bits. capping at 64 controllable IOs.\n"); + s->sim_gpio_length = 64; + } else { + s->sim_gpio_length = plist->pads[i].len; + } + } + } + } + + if (!strcmp(plist->name, "sys_clk")) { + for (int i = 0; plist->pads[i].name; i++) { + if (strcmp(plist->pads[i].name, "sys_clk") == 0) { + s->sim_sys_clk = (uint8_t*) plist->pads[i].signal; + } + } + } + + out: + return ret; +} + +static int gpio_tick(void *state, uint64_t time_ps) { + return RC_OK; +} + +static void gpio_simctrl_report_error(json_object *response_obj, + const char *err, + json_object *additional_information) { + // Top-level response-type field + json_object *response_type = json_object_new_string("error"); + json_object_object_add(response_obj, "_type", response_type); + + // Error code to report to the client + json_object *response_error = json_object_new_string(err); + json_object_object_add(response_obj, "error", response_error); + + // If we have additional information, also add that to the error + // response + if (additional_information != NULL) { + // Increase the reference count for the passed in additional + // information such that it will survive the round trip + // through simctrl and can be freed on the + // MODMSG_SIMCTRL_RETFREE. + json_object_get(additional_information); + json_object_object_add(response_obj, "additional_information", + additional_information); + } +} + +static msg_return_t gpio_msg(void *state, uint32_t msg_op, void *data, + void **retdata) { + msg_return_t msg_ret = MSGRET_SUCCESS; + gpio_state_t *s = state; + + if (msg_op == MODMSG_OP_SIMCTRL_REQ) { + // For this message type, we expect a simctrl_msg_t in + // data. This contains an opaque data pointer and length. The + // sim control module has done bounds checking on the + // underlying array for us, we can thus trust the + // length. However, it might not be a null-terminated string + // and we can't necessarily just set one byte past the last + // one to null, as it may overrun the underlying buffer. + simctrl_msg_t *message = data; + + // We always expect JSON for this module, thus try to parse it + json_tokener *tokener = json_tokener_new(); + json_object *request_obj = + json_tokener_parse_ex(tokener, message->data, message->len); + + // The parsing will not succeed without a null byte. If the + // string just didn't contain that but was otherwise valid, + // throw it in as well. + if (request_obj == NULL + && json_tokener_get_error(tokener) == json_tokener_continue) { + const char null = '\0'; + request_obj = json_tokener_parse_ex(tokener, &null, 1); + } + + // Generic response object for every request type + json_object *response_obj = json_object_new_object(); + + // Now check whether we've got a valid object + if (request_obj == NULL) { + enum json_tokener_error err = json_tokener_get_error(tokener); + const char* errdesc = json_tokener_error_desc(err); + json_object *addinfo = json_object_new_object(); + json_object_object_add(addinfo, "description", + json_object_new_string(errdesc)); + gpio_simctrl_report_error(response_obj, "payload_parse_error", + addinfo); + msg_ret = MSGRET_FAIL; + // Safe to remove this reference, we've increased the + // reference count in the report_error function + json_object_put(addinfo); + goto report_error; + } + + // Okay, parsing worked, let's see whether we know the type + json_object *request_type; + json_bool type_present = json_object_object_get_ex(request_obj, "_type", + &request_type); + if (!type_present) { + gpio_simctrl_report_error(response_obj, "payload_missing_type", + NULL); + msg_ret = MSGRET_FAIL; + goto report_error; + } + if (!json_object_is_type(request_type, json_type_string)) { + gpio_simctrl_report_error(response_obj, "payload_type_not_a_string", + NULL); + msg_ret = MSGRET_FAIL; + goto report_error; + } + const char *request_type_str = json_object_get_string(request_type); + + if (strcmp(request_type_str, "gpio_count") == 0) { + json_object_object_add(response_obj, "_type", + json_object_new_string("gpio_count")); + json_object_object_add(response_obj, "gpio_count", + json_object_new_int64(s->sim_gpio_length)); + } else if (strcmp(request_type_str, "set_input") == 0) { + // Extract the GPIO index + json_object *gpio_index; + json_bool gpio_index_present = + json_object_object_get_ex(request_obj, "gpio_index", + &gpio_index); + if (!gpio_index_present) { + gpio_simctrl_report_error(response_obj, "gpio_index_missing", + NULL); + msg_ret = MSGRET_FAIL; + goto report_error; + } + if (!json_object_is_type(gpio_index, json_type_int)) { + gpio_simctrl_report_error(response_obj, "gpio_index_not_an_int", + NULL); + msg_ret = MSGRET_FAIL; + goto report_error; + } + uint64_t gpio_index_int = json_object_get_int64(gpio_index); + + // Check whether the gpio_index is within the GPIO count bounds + if (gpio_index_int > s->sim_gpio_length) { + gpio_simctrl_report_error(response_obj, + "gpio_index_out_of_bounds", NULL); + msg_ret = MSGRET_FAIL; + goto report_error; + } + + // Extract the target GPIO input state + json_object *input_state; + json_bool input_state_present = + json_object_object_get_ex(request_obj, "state", &input_state); + if (!input_state_present) { + gpio_simctrl_report_error(response_obj, "input_state_missing", + NULL); + msg_ret = MSGRET_FAIL; + goto report_error; + } + if (!json_object_is_type(input_state, json_type_boolean)) { + gpio_simctrl_report_error(response_obj, + "input_state_not_a_bool", + NULL); + msg_ret = MSGRET_FAIL; + goto report_error; + } + json_bool input_state_bool = json_object_get_boolean(input_state); + + // Set the input state in the simulation IOs + *s->sim_gpio_i &= ~(1 << gpio_index_int); + *s->sim_gpio_i |= input_state_bool << gpio_index_int; + + // Report a success, without a payload + json_object_put(response_obj); + response_obj = NULL; + } else if (strcmp(request_type_str, "get_state") == 0) { + // Extract the GPIO index + json_object *gpio_index; + json_bool gpio_index_present = + json_object_object_get_ex(request_obj, "gpio_index", + &gpio_index); + if (!gpio_index_present) { + gpio_simctrl_report_error(response_obj, "gpio_index_missing", + NULL); + msg_ret = MSGRET_FAIL; + goto report_error; + } + if (!json_object_is_type(gpio_index, json_type_int)) { + gpio_simctrl_report_error(response_obj, "gpio_index_not_an_int", + NULL); + msg_ret = MSGRET_FAIL; + goto report_error; + } + uint64_t gpio_index_int = json_object_get_int64(gpio_index); + + // Check whether the gpio_index is within the GPIO count bounds + if (gpio_index_int > s->sim_gpio_length) { + gpio_simctrl_report_error(response_obj, + "gpio_index_out_of_bounds", NULL); + msg_ret = MSGRET_FAIL; + goto report_error; + } + + // Extract the current GPIO state + json_bool output_enabled = + (*s->sim_gpio_oe >> gpio_index_int) & 0b1; + json_bool gpio_state = (output_enabled) + ? (*s->sim_gpio_o >> gpio_index_int) & 0b1 + : (*s->sim_gpio_i >> gpio_index_int) & 0b1; + const char *driven_by = (output_enabled) ? "output" : "input"; + + // Add the appropriate fields to the JSON + json_object_object_add(response_obj, "_type", + json_object_new_string("get_state")); + json_object_object_add(response_obj, "gpio_index", + json_object_new_int64(gpio_index_int)); + json_object_object_add(response_obj, "driven_by", + json_object_new_string(driven_by)); + json_object_object_add(response_obj, "state", + json_object_new_boolean(gpio_state)); + } else { + gpio_simctrl_report_error(response_obj, "payload_unknown_type", + NULL); + msg_ret = MSGRET_FAIL; + } + + report_error: + + if (response_obj != NULL) { + // Allocate a new simctrl_msg_t to contain a pointer to the data, + // the length and a our reference to the top-level json-c object + // for freeing later. + simctrl_msg_t *retmsg = malloc(sizeof(simctrl_msg_t)); + const char *encoded = json_object_to_json_string(response_obj); + retmsg->data = (void*) encoded; + retmsg->len = strlen(encoded); + // By convention, all of the gpio retdata objects contain the + // top-level json-c object as their retdata_private field. + retmsg->retdata_private = (void*) response_obj; + *retdata = (void*) retmsg; + } + + if (request_obj != NULL) { + json_object_put(request_obj); + } + + if (tokener != NULL) { + json_tokener_free(tokener); + } + } else if (msg_op == MODMSG_OP_SIMCTRL_RETFREE) { + // This message is sent whenever we've passed back some non-NULL retdata + // to the simctrl module. The retdata is then passed in as data. By + // convention, the data.data will always contain the JSON response + // string, and data.retdata_private is a handle of the json-c object + // holding that string. Thus we just need to free that. + simctrl_msg_t *retmsg = data; + json_object_put((json_object *) retmsg->retdata_private); + + // Now, free the container holding this data + free(retmsg); + } else { + msg_ret = MSGRET_INVALID_OP; + } + + return msg_ret; +} + +static struct ext_module_s ext_mod = { + "gpio", + gpio_start, + gpio_new, + gpio_add_pads, + NULL, + gpio_tick, + gpio_msg, +}; + +int litex_sim_ext_module_init(int (*register_module)(struct ext_module_s *)) { + int ret = RC_OK; + ret = register_module(&ext_mod); + return ret; +} diff --git a/litex/build/sim/core/modules/jtagremote/jtagremote.c b/litex/build/sim/core/modules/jtagremote/jtagremote.c index 1b0fa4e254..c9880acb52 100644 --- a/litex/build/sim/core/modules/jtagremote/jtagremote.c +++ b/litex/build/sim/core/modules/jtagremote/jtagremote.c @@ -84,7 +84,7 @@ static int litex_sim_module_pads_get( struct pad_s *pads, char *name, void **sig return ret; } -static int jtagremote_start(void *b) +static int jtagremote_start(void *b, void *sh) { base = (struct event_base *)b; printf("[jtagremote] loaded (%p)\n", base); @@ -254,7 +254,8 @@ static struct ext_module_s ext_mod = { jtagremote_new, jtagremote_add_pads, NULL, - jtagremote_tick + jtagremote_tick, + NULL, }; int litex_sim_ext_module_init(int (*register_module)(struct ext_module_s *)) diff --git a/litex/build/sim/core/modules/serial2console/serial2console.c b/litex/build/sim/core/modules/serial2console/serial2console.c index 824662170e..b5d7210de4 100644 --- a/litex/build/sim/core/modules/serial2console/serial2console.c +++ b/litex/build/sim/core/modules/serial2console/serial2console.c @@ -59,7 +59,7 @@ void set_conio_terminal_mode(void) tcsetattr(0, TCSANOW, &new_termios); } -static int serial2console_start(void *b) +static int serial2console_start(void *b, void *sh) { base = (struct event_base *)b; set_conio_terminal_mode(); @@ -171,7 +171,8 @@ static struct ext_module_s ext_mod = { serial2console_new, serial2console_add_pads, NULL, - serial2console_tick + serial2console_tick, + NULL, }; int litex_sim_ext_module_init(int (*register_module) (struct ext_module_s *)) diff --git a/litex/build/sim/core/modules/serial2tcp/serial2tcp.c b/litex/build/sim/core/modules/serial2tcp/serial2tcp.c index 0d173471ec..7153aa91fe 100644 --- a/litex/build/sim/core/modules/serial2tcp/serial2tcp.c +++ b/litex/build/sim/core/modules/serial2tcp/serial2tcp.c @@ -97,7 +97,7 @@ static int litex_sim_module_pads_get( struct pad_s *pads, char *name, void **sig return ret; } -static int serial2tcp_start(void *b) +static int serial2tcp_start(void *b, void *sh) { base = (struct event_base *)b; printf("[serial2tcp] loaded (%p)\n", base); @@ -272,7 +272,8 @@ static struct ext_module_s ext_mod = { serial2tcp_new, serial2tcp_add_pads, NULL, - serial2tcp_tick + serial2tcp_tick, + NULL, }; int litex_sim_ext_module_init(int (*register_module)(struct ext_module_s *)) diff --git a/litex/build/sim/core/modules/simctrl/Makefile b/litex/build/sim/core/modules/simctrl/Makefile new file mode 100644 index 0000000000..dc81b943c0 --- /dev/null +++ b/litex/build/sim/core/modules/simctrl/Makefile @@ -0,0 +1,14 @@ +include ../../variables.mak +UNAME_S := $(shell uname -s) + +include $(SRC_DIR)/modules/rules.mak + +CFLAGS += -I$(TAPCFG_DIRECTORY)/src/include +OBJS = $(MOD).o + +$(MOD).so: $(OBJS) +ifeq ($(UNAME_S),Darwin) + $(CC) $(LDFLAGS) -o $@ $^ +else + $(CC) $(LDFLAGS) -Wl,-soname,$@ -o $@ $^ +endif diff --git a/litex/build/sim/core/modules/simctrl/simctrl.c b/litex/build/sim/core/modules/simctrl/simctrl.c new file mode 100644 index 0000000000..dcb790ede9 --- /dev/null +++ b/litex/build/sim/core/modules/simctrl/simctrl.c @@ -0,0 +1,730 @@ +/** + * simctrl module for interacting with the simulation through a + * ZeroMQ/JSON-based protocol. + * + * This module provides a ZeroMQ/JSON-based protocol for basic interaction with + * the LiteX simulation, as well as interacting with other modules through the + * simctrl interface. + * + * Copyright (c) 2021 Leon Schuermann + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * Copyright (c) 2021 Leon Schuermann + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include "error.h" +#include "modules.h" + +#define SIMCTRL_ZMQ_RECV_ENVELOPE_BUF_LEN 1024 +#define SIMCTRL_ZMQ_RECV_PAYLOAD_BUF_LEN 64 * 1024 +#define SIMCTRL_ZMQ_SEND_ENVELOPE_BUF_LEN 1024 +#define SIMCTRL_ZMQ_SEND_PAYLOAD_BUF_LEN 64 * 1024 + +typedef struct simctrl_modsession_list { + char *mod_name; + litex_sim_msid_t mod_session_id; + size_t zmq_mod_session_id; + time_t registered_at; + struct simctrl_modsession_list *next; +} simctrl_modsession_list_t; + +typedef struct { + // Instantiated module sessions of the current simulation, + // collected through incoming messages + simctrl_modsession_list_t *modsession_list; + // Counter used to assign IDs to the modsession list + // elements. These values are exposed via ZeroMQ, we don't really + // want to encode and expose the internal litex_sim_msid_t type to + // ZeroMQ clients. + size_t modsession_count; + + // ZeroMQ communication socket and libevent events + void *zmq_context; + void *zmq_socket_handle; + void *zmq_fd_event; + + // Incoming ZeroMQ message buffer + char zmq_recv_envelope_buffer[SIMCTRL_ZMQ_RECV_ENVELOPE_BUF_LEN]; + size_t zmq_recv_envelope_len; + char zmq_recv_payload_buffer[SIMCTRL_ZMQ_RECV_PAYLOAD_BUF_LEN]; + size_t zmq_recv_payload_len; +} simctrl_state_t; + +static struct event_base *base = NULL; +static void *sim_handle = NULL; + +// Instance counter to make sure we don't violate the singleton pattern +static size_t ninstances = 0; + +int litex_sim_module_get_args(char *args, char *arg, char **val) +{ + int ret = RC_OK; + json_object *jsobj = NULL; + json_object *obj = NULL; + char *value = NULL; + int r; + + jsobj = json_tokener_parse(args); + if(NULL == jsobj) { + fprintf(stderr, "Error parsing json arg: %s \n", args); + ret = RC_JSERROR; + goto out; + } + + if(!json_object_is_type(jsobj, json_type_object)) { + fprintf(stderr, "Arg must be type object! : %s \n", args); + ret = RC_JSERROR; + goto out; + } + + obj=NULL; + r = json_object_object_get_ex(jsobj, arg, &obj); + if(!r) { + fprintf(stderr, "Could not find object: \"%s\" (%s)\n", arg, args); + ret = RC_JSERROR; + goto out; + } + value = strdup(json_object_get_string(obj)); + +out: + *val = value; + return ret; +} + +static void simctrl_zmq_report_error(simctrl_state_t *s, + const char *err, + json_object *additional_information) { + int res; + + // Top-level response object + json_object *response_obj = json_object_new_object(); + + // Top-level response-type field + json_object *response_type = json_object_new_string("error"); + json_object_object_add(response_obj, "_type", response_type); + + // Error code to report to the client + json_object *response_error = json_object_new_string(err); + json_object_object_add(response_obj, "error", response_error); + + // If we have additional information, also add that to the error + // response + if (additional_information != NULL) { + // Increase the reference count for the passed in additional + // information such that it is not freed through our call + // below. + json_object_get(additional_information); + json_object_object_add(response_obj, "additional_information", + additional_information); + } + + // Send the response via ZeroMQ. Don't need to free the encoded + // string manually, it is stored within the json_object + const char *encoded = json_object_to_json_string(response_obj); + res = zmq_send(s->zmq_socket_handle, encoded, strlen(encoded), ZMQ_DONTWAIT); + if (res < 0) { + fprintf(stderr, "[simctrl]: responding to ZeroMQ request with error " + "failed: %s\n", strerror(errno)); + } + + // Free all allocated memory. This will not free the passed in + // additional_information. + json_object_put(response_obj); +} + +static void simctrl_zmq_process_message(simctrl_state_t *s) { + int res; + msg_return_t msg_res; + + // When we reach this function, we know that we have at least + // received some envelope and maybe some payload via + // ZeroMQ. However, we may have received zero bytes of data or + // truncated data (the length being larger than the buffer + // capacity). While the payload will potentially be passed + // unmodified to a different subsystem, validate and + // NULL-terminate the envelope so we can parse it as a JSON + // string. + if (s->zmq_recv_envelope_len > sizeof(s->zmq_recv_envelope_buffer)) { + json_object *addinfo = json_object_new_object(); + json_object_object_add(addinfo, "actual", + json_object_new_int64(s->zmq_recv_envelope_len)); + json_object_object_add(addinfo, "maximum", + json_object_new_int64( + sizeof(s->zmq_recv_envelope_buffer))); + simctrl_zmq_report_error(s, "envelope_too_large", addinfo); + json_object_put(addinfo); + goto out; + } + + if (s->zmq_recv_payload_len > sizeof(s->zmq_recv_payload_buffer)) { + json_object *addinfo = json_object_new_object(); + json_object_object_add(addinfo, "actual", + json_object_new_int64(s->zmq_recv_envelope_len)); + json_object_object_add(addinfo, "maximum", + json_object_new_int64( + sizeof(s->zmq_recv_envelope_buffer))); + simctrl_zmq_report_error(s, "payload_too_large", addinfo); + json_object_put(addinfo); + goto out; + } + + // Use the _ex parse method with a tokener to be able to limit the + // string length. If the JSON is incomplete we treat it as invalid + // json. + json_tokener *tokener = json_tokener_new(); + // It appears we must still null-terminate the received string, + // even though we pass a length to json_tokener_parse_ex. + s->zmq_recv_envelope_buffer[s->zmq_recv_envelope_len] = '\0'; + json_object *request_envelope = + json_tokener_parse_ex(tokener, s->zmq_recv_envelope_buffer, + s->zmq_recv_envelope_len); + if (request_envelope == NULL) { + enum json_tokener_error err = json_tokener_get_error(tokener); + const char* errdesc = json_tokener_error_desc(err); + json_object *addinfo = json_object_new_object(); + json_object_object_add(addinfo, "description", + json_object_new_string(errdesc)); + simctrl_zmq_report_error(s, "envelope_parse_error", addinfo); + json_object_put(addinfo); + goto free_tokener; + } + + // Okay, parsing worked, let's see whether we know the type + json_object *request_type; + json_bool type_present = json_object_object_get_ex(request_envelope, + "_type", &request_type); + if (!type_present) { + simctrl_zmq_report_error(s, "envelope_missing_type", NULL); + goto free_parsed; + } + if (!json_object_is_type(request_type, json_type_string)) { + simctrl_zmq_report_error(s, "envelope_type_not_a_string", NULL); + goto free_parsed; + } + + const char* request_type_str = json_object_get_string(request_type); + + if (strcmp(request_type_str, "sim_info") == 0) { + // Encode the current date and time into the object + time_t timep = time(NULL); + if (timep == -1) { + simctrl_zmq_report_error(s, "get_time_failed", NULL); + goto free_parsed; + } + + // Store values in a JSON object + json_object *sim_info = json_object_new_object(); + json_object_object_add(sim_info, "_type", + json_object_new_string("sim_info")); + char *timestr = ctime(&timep); + timestr[24] = '\0'; // Hack, to strip off the trailing newline + json_object_object_add(sim_info, "system_time", + json_object_new_string(timestr)); + bool sim_halted = litex_sim_halted(sim_handle); + json_object_object_add(sim_info, "sim_halted", + json_object_new_boolean(sim_halted)); + + // Send the response via ZeroMQ + const char *encoded = json_object_to_json_string(sim_info); + res = zmq_send(s->zmq_socket_handle, encoded, strlen(encoded), + ZMQ_DONTWAIT); + if (res < 0) { + fprintf(stderr, "[simctrl]: responding to ZeroMQ request failed: " + "%s\n", strerror(errno)); + } + + // Free the allocated object + json_object_put(sim_info); + } else if (strcmp(request_type_str, "sim_time") == 0) { + // Encode the current simulation time into the response + json_object *sim_time = json_object_new_object(); + json_object_object_add(sim_time, "_type", + json_object_new_string("sim_time")); + // TODO: handle overflows properly + int64_t sim_time_ps = (int64_t) litex_sim_current_time_ps(sim_handle); + json_object_object_add(sim_time, "sim_time", + json_object_new_int64(sim_time_ps)); + bool sim_halted = litex_sim_halted(sim_handle); + json_object_object_add(sim_time, "sim_halted", + json_object_new_boolean(sim_halted)); + + // Send the response via ZeroMQ + const char *encoded = json_object_to_json_string(sim_time); + res = zmq_send(s->zmq_socket_handle, encoded, strlen(encoded), + ZMQ_DONTWAIT); + if (res < 0) { + fprintf(stderr, "[simctrl]: responding to ZeroMQ request failed: " + "%s\n", strerror(errno)); + } + + // Free the allocated object + json_object_put(sim_time); + } else if (strcmp(request_type_str, "halt") == 0) { + // For a module message, assert that we know of an instance + // with the given session_id. + json_object *halt_val; + json_bool halt_present = json_object_object_get_ex(request_envelope, + "halt", &halt_val); + if (!halt_present) { + simctrl_zmq_report_error(s, "missing_halt", NULL); + goto free_parsed; + } + + json_type halt_type = json_object_get_type(halt_val); + if (halt_type != json_type_boolean) { + simctrl_zmq_report_error(s, "invalid_halt_type", NULL); + json_object_put(halt_val); + goto free_parsed; + } + + json_bool halt = json_object_get_boolean(halt_val); + //json_object_put(halt_val); + + litex_sim_halt(halt); + + // Encode the current simulation time into the response + json_object *sim_halt = json_object_new_object(); + json_object_object_add(sim_halt, "_type", + json_object_new_string("halt")); + // TODO: handle overflows properly + int64_t sim_time_ps = (int64_t) litex_sim_current_time_ps(sim_handle); + json_object_object_add(sim_halt, "sim_time", + json_object_new_int64(sim_time_ps)); + bool sim_halted = litex_sim_halted(sim_handle); + json_object_object_add(sim_halt, "sim_halted", + json_object_new_boolean(sim_halted)); + + // Send the response via ZeroMQ + const char *encoded = json_object_to_json_string(sim_halt); + res = zmq_send(s->zmq_socket_handle, encoded, strlen(encoded), + ZMQ_DONTWAIT); + if (res < 0) { + fprintf(stderr, "[simctrl]: responding to ZeroMQ request failed: " + "%s\n", strerror(errno)); + } + + // Free the allocated object + json_object_put(sim_halt); + } else if (strcmp(request_type_str, "module_session_list") == 0) { + // Top-level object containing type info + json_object *module_sessions_obj = json_object_new_object(); + json_object_object_add(module_sessions_obj, "_type", + json_object_new_string("module_session_list")); + + // List of individual module sessions + json_object *module_sessions_arr = json_object_new_array(); + for (simctrl_modsession_list_t *ms = s->modsession_list; + ms != NULL; ms = ms->next) { + json_object *module_session = json_object_new_object(); + json_object_object_add(module_session, "module_name", + json_object_new_string(ms->mod_name)); + json_object_object_add(module_session, "session_id", + json_object_new_int64( + ms->zmq_mod_session_id)); + char *timestr = ctime(&ms->registered_at); + timestr[24] = '\0'; // Hack, to strip off the trailing newline + json_object_object_add(module_session, "registered_at", + json_object_new_string(timestr)); + json_object_array_add(module_sessions_arr, module_session); + } + json_object_object_add(module_sessions_obj, "module_sessions", + module_sessions_arr); + + // Send the response via ZeroMQ + const char *encoded = json_object_to_json_string(module_sessions_obj); + res = zmq_send(s->zmq_socket_handle, encoded, strlen(encoded), + ZMQ_DONTWAIT); + if (res < 0) { + fprintf(stderr, "[simctrl]: responding to ZeroMQ request failed: " + "%s\n", strerror(errno)); + } + + // Free the allocated object + json_object_put(module_sessions_obj); + } else if (strcmp(request_type_str, "module_msg") == 0) { + // For a module message, assert that we know of an instance + // with the given session_id. + json_object *session_id; + json_bool session_id_present = + json_object_object_get_ex(request_envelope, "session_id", + &session_id); + if (!session_id_present) { + simctrl_zmq_report_error(s, "missing_session_id", NULL); + goto free_parsed; + } + if (!json_object_is_type(session_id, json_type_int)) { + simctrl_zmq_report_error(s, "session_id_not_an_integer", NULL); + goto free_parsed; + } + long zmq_session_id = json_object_get_int64(session_id); + + // Search for the session ID in the mod session list + simctrl_modsession_list_t *ms = s->modsession_list; + for (; ms != NULL; ms = ms->next) { + if (ms->zmq_mod_session_id == zmq_session_id) { + break; + } + } + + // Check whether we've reached the end of the list without finding the + // module session + if (ms == NULL) { + simctrl_zmq_report_error(s, "session_not_found", NULL); + goto free_parsed; + } + + // Okay, we've found the session in question. Deliver the message. + simctrl_msg_t data; + data.len = s->zmq_recv_payload_len; + data.data = s->zmq_recv_payload_buffer; + simctrl_msg_t *retdata = NULL; + msg_res = litex_sim_send_msg(sim_handle, ms->mod_session_id, + MODMSG_OP_SIMCTRL_REQ, &data, + (void*) &retdata); + + if (msg_res == MSGRET_MODSESSION_NOT_FOUND) { + // Don't need to free retdata, module was not invoked + fprintf(stderr, "[simctrl]: internal inconsistency in modsessions\n"); + simctrl_zmq_report_error(s, "internal_error", NULL); + } else if (msg_res == MSGRET_INVALID_OP) { + // Don't need to free retdata, module reported that the op does not + // exist and thus it shouldn't have written to retdata + simctrl_zmq_report_error(s, "module_does_not_support_simctrl", NULL); + } else { + // Either success or fail, in both cases report to the client + + // Top-level object containing type info + json_object *module_sessions_obj = json_object_new_object(); + json_object_object_add(module_sessions_obj, "_type", + json_object_new_string("module_msg")); + + // Encode the module return code as a string + json_object_object_add(module_sessions_obj, "module_return_code", + json_object_new_string( + msg_return_strerror(msg_res))); + + // Send the envelope response via ZeroMQ + const char *encoded = + json_object_to_json_string(module_sessions_obj); + res = zmq_send(s->zmq_socket_handle, encoded, strlen(encoded), + ZMQ_DONTWAIT + | ((retdata != NULL) ? ZMQ_SNDMORE : 0)); + if (res < 0) { + fprintf(stderr, "[simctrl]: responding to ZeroMQ request " + "failed: %s\n", strerror(errno)); + goto free_parsed; + } + + // When there is data returned by the module + if (retdata != NULL) { + res = zmq_send(s->zmq_socket_handle, retdata->data, + retdata->len, ZMQ_DONTWAIT); + if (res < 0) { + fprintf(stderr, "[simctrl]: sending retdata via ZeroMQ " + "failed: %s\n", strerror(errno)); + } + + // Regardless of whether the transmission succeeded, we must + // pass the retdata back to the module such that it can be + // freed. + void* dummy_retdata_free = NULL; + msg_res = litex_sim_send_msg(sim_handle, ms->mod_session_id, + MODMSG_OP_SIMCTRL_RETFREE, + retdata, &dummy_retdata_free); + if (msg_res != MSGRET_SUCCESS) { + fprintf(stderr, "[simctrl]: module %s passed back some " + "retdata, but does not accept the RETFREE call: " + "%d\n", ms->mod_name, msg_res); + } + } + } + } else { + simctrl_zmq_report_error(s, "envelope_unknown_type", NULL); + } + +free_parsed: + json_object_put(request_envelope); + +free_tokener: + json_tokener_free(tokener); + +out: + return; +} + +static bool simctrl_zmq_recv_nonblock(simctrl_state_t *s) { + int received_len; + int rcvmore; + size_t rcvmore_len = sizeof(rcvmore); + + // Try to receive the envelope first + received_len = zmq_recv( + s->zmq_socket_handle, + s->zmq_recv_envelope_buffer, + sizeof(s->zmq_recv_envelope_buffer), + ZMQ_DONTWAIT + ); + if (received_len >= 0) { + s->zmq_recv_envelope_len = received_len; + + // Also, reset the payload length so if we don't receive any, + // we won't use the previous buffer contents + s->zmq_recv_payload_len = 0; + + // Now, check whether there is an additional payload + // available. All parts of a message must arrive atomically, + // thus it's fine to check this immediately here as well. + zmq_getsockopt(s->zmq_socket_handle, ZMQ_RCVMORE, &rcvmore, + &rcvmore_len); + if (rcvmore) { + // There is an additional payload, try to receive it. + received_len = zmq_recv( + s->zmq_socket_handle, + s->zmq_recv_payload_buffer, + sizeof(s->zmq_recv_payload_buffer), + ZMQ_DONTWAIT + ); + if (received_len >= 0) { + // There was a payload, set the length accordingly + s->zmq_recv_payload_len = received_len; + } + } + + // There might be even more message parts, which we don't want + // to handle. To avoid receiving them in the next invocation + // of this function, process them here in a loop. + do { + zmq_getsockopt(s->zmq_socket_handle, ZMQ_RCVMORE, &rcvmore, + &rcvmore_len); + if (rcvmore) { + fprintf(stderr, "[simctrl]: received additional unexpected " + "ZeroMQ message parts\n"); + zmq_recv(s->zmq_socket_handle, NULL, 0, ZMQ_DONTWAIT); + } + } while (rcvmore); + + // In any case we've now got a valid envelope (and maybe also + // payload) which we must process and respond to. + simctrl_zmq_process_message(s); + + return true; + } else { + return false; + } +} + +static void simctrl_zmq_event_cb(int fd, short event, void *arg) { + simctrl_state_t *s = arg; + + uint32_t events = 0; + size_t events_len = sizeof(events); + if (zmq_getsockopt(s->zmq_socket_handle, ZMQ_EVENTS, &events, &events_len) + != 0) { + fprintf(stderr, "[simctrl]: error retrieving information ZeroMQ socket " + "event state: %s\n", strerror(errno)); + return; + } + + if (events & ZMQ_POLLIN) { + while (simctrl_zmq_recv_nonblock(s)) { + // Receive pending messages + } + } +} + +static int simctrl_start(void *b, void* sh) { + base = (struct event_base *) b; + sim_handle = sh; + fprintf(stderr, "[simctrl] loaded (libevent base: %p, sim_handle: %p)\n", + base, sim_handle); + return RC_OK; +} + +static int simctrl_new(void **state, char *args) { + int res = RC_OK; + //char *listen_port = NULL; + simctrl_state_t *s = NULL; + + if (!state) { + res = RC_INVARG; + goto out; + } + + // There should only ever be a single simctrl instance in a + // simulation. It doesn't make sense to have two. + if (ninstances > 0) { + fprintf(stderr, "[simctrl]: refusing to create instance %ld\n", + ninstances + 1); + res = RC_ERROR; + goto out; + } + + /* ret = litex_sim_module_get_args(args, "listen_port", &listen_port); */ + /* if (ret != RC_OK) { */ + /* return ret; */ + /* } */ + + s = malloc(sizeof(simctrl_state_t)); + if (!s) { + res = RC_NOENMEM; + goto out; + } + + // Set the ZMQ receive buffer lengths + s->zmq_recv_envelope_len = 0; + s->zmq_recv_payload_len = 0; + + // Create a new ZeroMQ context, create and bind a socket so that + // clients can connect + s->zmq_context = zmq_ctx_new(); + if (s->zmq_context == NULL) { + fprintf(stderr, "[simctrl]: failed to create ZeroMQ context: %s\n", + strerror(errno)); + res = RC_ERROR; + goto free_simctrl_state; + } + + s->zmq_socket_handle = zmq_socket(s->zmq_context, ZMQ_REP); + if (s->zmq_socket_handle == NULL) { + fprintf(stderr, "[simctrl]: failed to create ZeroMQ socket: %s\n", + strerror(errno)); + res = RC_ERROR; + goto free_zmq_ctx; + } + + res = zmq_bind(s->zmq_socket_handle, "tcp://*:7173"); + if (res != 0) { + fprintf(stderr, "[simctrl]: failed to bind ZeroMQ socket: %s\n", + strerror(errno)); + res = RC_ERROR; + goto free_zmq_ctx; + } + res = RC_OK; + + // Add ZMQ socket pull to event dispatcher + int zmq_socket_fd; + size_t zmq_socket_fd_len = sizeof(zmq_socket_fd); + if (zmq_getsockopt(s->zmq_socket_handle, ZMQ_FD, &zmq_socket_fd, + &zmq_socket_fd_len) != 0) { + fprintf(stderr, "[simctrl]: failed to determine the ZeroMQ socket file " + "descriptor: %s\n", strerror(errno)); + res = RC_ERROR; + goto free_zmq_ctx; + } + s->zmq_fd_event = event_new(base, zmq_socket_fd, EV_READ | EV_PERSIST, + simctrl_zmq_event_cb, s); + struct timeval tv = {10, 0}; + event_add(s->zmq_fd_event, &tv); + + // Initialize all fields of the simctrl_state + s->modsession_list = NULL; + s->modsession_count = 0; + + // Register our session state and mark that we have created one + // instance + *state = (void*) s; + ninstances += 1; + + // Everything worked, keep all allocated structures + goto out; + +free_zmq_ctx: + zmq_ctx_destroy(s->zmq_context); + +free_simctrl_state: + free(s); + +out: + return res; +} + +static int simctrl_add_pads(void *state, struct pad_list_s *plist) { + return RC_OK; +} + +static int simctrl_tick(void *state, uint64_t time_ps) { + return RC_OK; +} + +static msg_return_t simctrl_msg(void *state, uint32_t msg_op, void *data, + void **retdata) { + simctrl_state_t *s = state; + + time_t timep = time(NULL); + if (timep == -1) { + fprintf(stderr, "[simctrl]: getting current time failed\n"); + return MSGRET_FAIL; + } + + + if (msg_op == MODMSG_OP_NEWMODSESSION) { + // A new module session is being announced. Add it to the + // internal list of module sessions. + modmsg_newmodsession_payload_t *msg_payload = data; + + // New list element to append + simctrl_modsession_list_t *modsession = + malloc(sizeof(simctrl_modsession_list_t)); + modsession->mod_name = strdup(msg_payload->mod_name); + modsession->mod_session_id = msg_payload->mod_session_id; + modsession->zmq_mod_session_id = s->modsession_count++; + modsession->registered_at = timep; + + // Append it to the front of the state list (keeping the + // ordering does not matter). + // TODO: possibly lock this beforehand + modsession->next = s->modsession_list; + s->modsession_list = modsession; + } + + return MSGRET_SUCCESS; + +} + +static struct ext_module_s ext_mod = { + "simctrl", + simctrl_start, + simctrl_new, + simctrl_add_pads, + NULL, + simctrl_tick, + simctrl_msg, +}; + +int litex_sim_ext_module_init(int (*register_module)(struct ext_module_s *)) { + int ret = RC_OK; + ret = register_module(&ext_mod); + return ret; +} diff --git a/litex/build/sim/core/modules/spdeeprom/spdeeprom.c b/litex/build/sim/core/modules/spdeeprom/spdeeprom.c index 932b51b803..6712b63008 100644 --- a/litex/build/sim/core/modules/spdeeprom/spdeeprom.c +++ b/litex/build/sim/core/modules/spdeeprom/spdeeprom.c @@ -62,7 +62,7 @@ struct session_s { }; // Module interface -static int spdeeprom_start(); +static int spdeeprom_start(void *b, void *sh); static int spdeeprom_new(void **sess, char *args); static int spdeeprom_add_pads(void *sess, struct pad_list_s *plist); static int spdeeprom_tick(void *sess, uint64_t time_ps); @@ -81,7 +81,8 @@ static struct ext_module_s ext_mod = { spdeeprom_new, spdeeprom_add_pads, NULL, - spdeeprom_tick + spdeeprom_tick, + NULL, }; int litex_sim_ext_module_init(int (*register_module)(struct ext_module_s *)) @@ -91,7 +92,7 @@ int litex_sim_ext_module_init(int (*register_module)(struct ext_module_s *)) return ret; } -static int spdeeprom_start() +static int spdeeprom_start(void *b, void *sh) { printf("[spdeeprom] loaded (addr = 0x%01x)\n", SPD_EEPROM_ADDR); return RC_OK; diff --git a/litex/build/sim/core/modules/xgmii_ethernet/xgmii_ethernet.c b/litex/build/sim/core/modules/xgmii_ethernet/xgmii_ethernet.c index c2ec7a96a2..8e04031d23 100644 --- a/litex/build/sim/core/modules/xgmii_ethernet/xgmii_ethernet.c +++ b/litex/build/sim/core/modules/xgmii_ethernet/xgmii_ethernet.c @@ -882,7 +882,7 @@ static int xgmii_ethernet_add_pads(void *state, struct pad_list_s *plist) { return ret; } -static int xgmii_ethernet_start(void *b) { +static int xgmii_ethernet_start(void *b, void *sh) { base = (struct event_base *) b; printf("[xgmii_ethernet] loaded (%p)\n", base); return RC_OK; @@ -939,7 +939,8 @@ static struct ext_module_s ext_mod = { xgmii_ethernet_new, xgmii_ethernet_add_pads, NULL, - xgmii_ethernet_tick + xgmii_ethernet_tick, + NULL, }; int litex_sim_ext_module_init(int (*register_module)(struct ext_module_s *)) { diff --git a/litex/build/sim/core/sim.c b/litex/build/sim/core/sim.c index 33e6edb346..201216b803 100644 --- a/litex/build/sim/core/sim.c +++ b/litex/build/sim/core/sim.c @@ -1,4 +1,9 @@ -/* Copyright (C) 2017 LambdaConcept */ +/** + * LiteX Simulator (verilated simulation) + * + * Copyright (c) 2017 LambdaConcept + * Copyright (c) 2021 Leon Schuermann + */ #include #include @@ -33,6 +38,10 @@ struct session_list_s { uint64_t timebase_ps = 1; uint64_t sim_time_ps = 0; + +// TODO: introduce parameter which enables the simulation to halt on start. +bool sim_halt = false; + struct session_list_s *sesslist=NULL; struct event_base *base=NULL; @@ -45,11 +54,22 @@ static int litex_sim_initialize_all(void **sim, void *base) //struct ext_module_list_s *mlisti=NULL; struct pad_list_s *plist=NULL; struct pad_list_s *pplist=NULL; + struct session_list_s *fslist=NULL; struct session_list_s *slist=NULL; void *vsim=NULL; int i; int ret = RC_OK; + // Initialize the first entry of the session list early on, as we'd + // like to give a pointer to that to the individual modules. This + // pointer is going to be passed back by the modules for + // inter-module communication. + fslist = malloc(sizeof(struct session_list_s)); + if (NULL == fslist) { + ret = RC_NOENMEM; + goto out; + } + /* Load external modules */ ret = litex_sim_load_ext_modules(&mlist); if(RC_OK != ret) @@ -60,7 +80,7 @@ static int litex_sim_initialize_all(void **sim, void *base) { if(pmlist->module->start) { - pmlist->module->start(base); + pmlist->module->start(base, fslist); } } @@ -82,7 +102,6 @@ static int litex_sim_initialize_all(void **sim, void *base) for(mli = ml; mli; mli=mli->next) { - /* Find the module in the external module */ pmlist = NULL; ret = litex_sim_find_ext_module(mlist, mli->name, &pmlist ); @@ -96,11 +115,10 @@ static int litex_sim_initialize_all(void **sim, void *base) continue; } - slist=(struct session_list_s *)malloc(sizeof(struct session_list_s)); - if(NULL == slist) - { - ret = RC_NOENMEM; - goto out; + slist = malloc(sizeof(struct session_list_s)); + if (NULL == slist) { + ret = RC_NOENMEM; + goto out; } memset(slist, 0, sizeof(struct session_list_s)); @@ -122,20 +140,45 @@ static int litex_sim_initialize_all(void **sim, void *base) ret = litex_sim_pads_find(plist, mli->iface[i].name, mli->iface[i].index, &pplist); if(RC_OK != ret) { - goto out; + goto out; } if(NULL == pplist) { - eprintf("Could not find interface %s with index %d\n", mli->iface[i].name, mli->iface[i].index); - continue; + eprintf("Could not find interface %s with index %d\n", mli->iface[i].name, mli->iface[i].index); + continue; } + if (pmlist->module->add_pads != NULL) { ret = pmlist->module->add_pads(slist->session, pplist); if(RC_OK != ret) { - goto out; + goto out; + }} + } + } + + // Move the list head to the already allocated memory location + memcpy(fslist, slist, sizeof(struct session_list_s)); + + // Try to send a message to every module session, announcing every + // other module session. + struct session_list_s *slist_iter_a; + struct session_list_s *slist_iter_b; + for (slist_iter_a = fslist; slist_iter_a != NULL; slist_iter_a = slist_iter_a->next) { + litex_sim_msid_t dst_session_id; + dst_session_id.sptr = slist_iter_a->session; + for (slist_iter_b = fslist; slist_iter_b != NULL; slist_iter_b = slist_iter_b->next) { + if (slist_iter_a != slist_iter_b) { + litex_sim_msid_t mod_session_id; + mod_session_id.sptr = slist_iter_b->session; + modmsg_newmodsession_payload_t data; + data.mod_name = slist_iter_b->module->name; + data.mod_session_id = mod_session_id; + void* retdata; + litex_sim_send_msg(fslist, dst_session_id, MODMSG_OP_NEWMODSESSION, &data, &retdata); } } } + *sim = vsim; out: return ret; @@ -178,8 +221,13 @@ static void cb(int sock, short which, void *arg) tv.tv_usec = 0; int i; + for(i = 0; i < 1000; i++) { + if (sim_halt) { + break; + } + for(s = sesslist; s; s=s->next) { if(s->tickfirst) @@ -203,12 +251,76 @@ static void cb(int sock, short which, void *arg) } } - if (!evtimer_pending(ev, NULL)) { + if (!evtimer_pending(ev, NULL) && !sim_halt) { event_del(ev); evtimer_add(ev, &tv); } } +/** + * Send a message to a module session in the simulation. + * + * Prototype and behavior defined in `modules.h`. + */ +msg_return_t litex_sim_send_msg( + void *sim_handle, + litex_sim_msid_t mod_session_id, + uint32_t msg_op, + void* data, + void** retdata +) { + struct session_list_s *slist = sim_handle; + + // Find the matching session + while (slist != NULL) { + if (slist->session == mod_session_id.sptr) { + break; + } + slist = slist->next; + } + + // Check whether we finished the loop without finding a matching module + if (slist == NULL) { + return MSGRET_MODSESSION_NOT_FOUND; + } + + // Check whether the module has defined a handler for messages + if (slist->module->module_msg == NULL) { + return MSGRET_INVALID_OP; + } + + // Finally, pass the message to the module + msg_return_t msg_ret = (slist->module->module_msg)(slist->session, msg_op, data, retdata); + + if (msg_ret == MSGRET_MODSESSION_NOT_FOUND) { + fprintf(stderr, "[litex_sim]: module %s reported MSGRET_MODSESSION_NOT_FOUND, which is illegal. replacing with MSGRET_FAIL.\n", slist->module->name); + msg_ret = MSGRET_FAIL; + } + + return msg_ret; +}; + +uint64_t litex_sim_current_time_ps(void *sim_handle) { + return sim_time_ps; +} + +bool litex_sim_halted(void *sim_handle) { + return sim_halt; +} + +void litex_sim_halt(bool halt) { + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + + sim_halt = halt; + + if (!evtimer_pending(ev, NULL) && !sim_halt) { + event_del(ev); + evtimer_add(ev, &tv); + } +} + int main(int argc, char *argv[]) { void *vsim=NULL; diff --git a/litex/tools/litex_sim.py b/litex/tools/litex_sim.py index 63dc738b78..d3c7b7ae32 100755 --- a/litex/tools/litex_sim.py +++ b/litex/tools/litex_sim.py @@ -413,6 +413,7 @@ def sim_args(parser): parser.add_argument("--sim-debug", action="store_true", help="Add simulation debugging modules.") parser.add_argument("--gtkwave-savefile", action="store_true", help="Generate GTKWave savefile.") parser.add_argument("--non-interactive", action="store_true", help="Run simulation without user input.") + parser.add_argument("--with-simctrl", action="store_true", help="Enable a ZeroMQ JSON control interface") def main(): from litex.build.parser import LiteXArgumentParser @@ -489,6 +490,14 @@ def main(): if args.with_video_framebuffer or args.with_video_terminal: sim_config.add_module("video", "vga") + # Sim control (using ZMQ JSON RPC) + if args.with_simctrl: + sim_config.add_module("simctrl", [], args={}) + + # GPIO simulation module, controllable through simctrl interface + if args.with_gpio: + sim_config.add_module("gpio", "gpio", args={}) + # SoC ------------------------------------------------------------------------------------------ soc = SimSoC( with_sdram = args.with_sdram,