From 2d2eaa9b48b1c1a557d93711f495131b18fcc82b Mon Sep 17 00:00:00 2001 From: Leon Schuermann Date: Wed, 17 Nov 2021 12:27:13 +0100 Subject: [PATCH 1/5] litex_sim: add inter-module message passing mechanism This introduces a generic, flexible and efficient inter-module message passing mechanism to allow for more advanced module behavior and interactions, as well as outside control of module behavior. Possible applications for this mechanism are outside control of emulated peripherals such as GPIO, simulated hardware attached to multiple independent subsystems (e.g. GPIO + SPI) and integration of the LiteX simulation in automated systems such as CI. The message passing mechanism does not impose any restrictions on the data exchanged between modules and each module is free to define its own interfaces through opcodes >= 256. Opcodes < 256 are reserved for globally defined operations. These global operations include auto-announcement of instantiated module sessions in the simulation for discovery and a mechanism for interacting with outside applications (called "simctrl"). These operations are well documented in `modules.h`. This further introduces a few functions which expose basic information about and control of the simulation, such as querying the current simulation time and state, and requesting the simulation to halt or resume operation. Signed-off-by: Leon Schuermann --- litex/build/sim/core/modules.c | 20 ++ litex/build/sim/core/modules.h | 226 +++++++++++++++++- .../build/sim/core/modules/clocker/clocker.c | 5 +- .../sim/core/modules/ethernet/ethernet.c | 5 +- .../modules/gmii_ethernet/gmii_ethernet.c | 5 +- .../sim/core/modules/jtagremote/jtagremote.c | 5 +- .../modules/serial2console/serial2console.c | 5 +- .../sim/core/modules/serial2tcp/serial2tcp.c | 5 +- .../sim/core/modules/spdeeprom/spdeeprom.c | 7 +- .../modules/xgmii_ethernet/xgmii_ethernet.c | 5 +- litex/build/sim/core/sim.c | 138 ++++++++++- 11 files changed, 385 insertions(+), 41 deletions(-) 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/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/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/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; From 6e3e3b5f3a1ff45a25efb3665407ee8d05c8d432 Mon Sep 17 00:00:00 2001 From: Leon Schuermann Date: Wed, 17 Nov 2021 13:19:25 +0100 Subject: [PATCH 2/5] litex_sim: expose global simulation function symbols to modules This is required for modules to be able to call globally defined symbols to interact with other parts of the simulation, for example to send inter-module messages, query the simulation time or request the simulation to halt / resume. Signed-off-by: Leon Schuermann --- litex/build/sim/core/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litex/build/sim/core/Makefile b/litex/build/sim/core/Makefile index 4866d3a879..fe5c67cf41 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) -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) -rdynamic endif CFLAGS += -Wall -$(OPT_LEVEL) $(if $(COVERAGE), -DVM_COVERAGE) $(if $(TRACE_FST), -DTRACE_FST) From bd02f276922b338f0fc4693921b6d76326d55d00 Mon Sep 17 00:00:00 2001 From: Leon Schuermann Date: Wed, 17 Nov 2021 13:17:02 +0100 Subject: [PATCH 3/5] GitHub actions CI: install ZeroMQ with headers --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b13c4bb204f2a57149a007fe6221546ee8046257 Mon Sep 17 00:00:00 2001 From: Leon Schuermann Date: Wed, 17 Nov 2021 11:33:57 +0100 Subject: [PATCH 4/5] litex_sim: add ZeroMQ/JSON-based simctrl interface module Adds a module implementing the "simctrl"-style control & status interface for the LiteX simulation. It uses a ZeroMQ/JSON-based transport for the control, status and module messages. However, modules are free to define their own format. The passed messages make use of ZeroMQ multipart messages, for instance to carry payloads to simctrl-supported modules in the simulation. Thus, this module takes messages which contain a "simctrl header" and a payload forwarded to the destination module. Furthermore, the module implements some basic standalone status & control functionality. It can be used to query the current wall clock time, simulation time and instantiated module sessions. It can furthermore halt and resume the simulation. Signed-off-by: Leon Schuermann --- litex/build/sim/core/Makefile | 4 +- litex/build/sim/core/modules/Makefile | 2 +- litex/build/sim/core/modules/simctrl/Makefile | 14 + .../build/sim/core/modules/simctrl/simctrl.c | 730 ++++++++++++++++++ litex/tools/litex_sim.py | 5 + 5 files changed, 752 insertions(+), 3 deletions(-) create mode 100644 litex/build/sim/core/modules/simctrl/Makefile create mode 100644 litex/build/sim/core/modules/simctrl/simctrl.c diff --git a/litex/build/sim/core/Makefile b/litex/build/sim/core/Makefile index fe5c67cf41..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) -rdynamic + 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) -rdynamic + 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/Makefile b/litex/build/sim/core/modules/Makefile index d5bb16658d..39d2106e0d 100644 --- a/litex/build/sim/core/modules/Makefile +++ b/litex/build/sim/core/modules/Makefile @@ -1,5 +1,5 @@ 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) .PHONY: $(MODULES) $(EXTRA_MOD_LIST) all: $(MODULES) $(EXTRA_MOD_LIST) 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/tools/litex_sim.py b/litex/tools/litex_sim.py index 63dc738b78..31da48fb13 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,10 @@ 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={}) + # SoC ------------------------------------------------------------------------------------------ soc = SimSoC( with_sdram = args.with_sdram, From 8fb6fc90088dc33404170acf265d74fdd7d8a783 Mon Sep 17 00:00:00 2001 From: Leon Schuermann Date: Wed, 17 Nov 2021 11:45:12 +0100 Subject: [PATCH 5/5] litex_sim: add GPIO module exposed through simctrl Adds a GPIO controller module, which is exposed through simctrl. Supports driving GPIO pins, as well as querying the current pin state (input/output) and signal state. Exposes a JSON-based interface through the simctrl payload. Signed-off-by: Leon Schuermann --- litex/build/sim/core/modules/Makefile | 3 +- litex/build/sim/core/modules/gpio/Makefile | 14 + litex/build/sim/core/modules/gpio/gpio.c | 451 +++++++++++++++++++++ litex/tools/litex_sim.py | 4 + 4 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 litex/build/sim/core/modules/gpio/Makefile create mode 100644 litex/build/sim/core/modules/gpio/gpio.c diff --git a/litex/build/sim/core/modules/Makefile b/litex/build/sim/core/modules/Makefile index 39d2106e0d..749b4f3343 100644 --- a/litex/build/sim/core/modules/Makefile +++ b/litex/build/sim/core/modules/Makefile @@ -1,5 +1,6 @@ include ../variables.mak -MODULES = simctrl 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/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/tools/litex_sim.py b/litex/tools/litex_sim.py index 31da48fb13..d3c7b7ae32 100755 --- a/litex/tools/litex_sim.py +++ b/litex/tools/litex_sim.py @@ -494,6 +494,10 @@ def main(): 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,