icE1usb fw: Add multi-port support in USB code
Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
Change-Id: I134410b542461c34339baa8d9120a5c86fc35d46
diff --git a/firmware/ice40-riscv/icE1usb/fw_app.c b/firmware/ice40-riscv/icE1usb/fw_app.c
index 3340f4a..55218b8 100644
--- a/firmware/ice40-riscv/icE1usb/fw_app.c
+++ b/firmware/ice40-riscv/icE1usb/fw_app.c
@@ -102,6 +102,7 @@
/* Start */
e1_init(0, 0, 0);
+ e1_init(1, 0, 0);
led_state(true);
usb_connect();
@@ -151,7 +152,9 @@
usb_poll();
/* E1 poll */
- e1_poll(0);
- usb_e1_run();
+ for (int port=0; port<2; port++) {
+ e1_poll(port);
+ usb_e1_run(port);
+ }
}
}
diff --git a/firmware/ice40-riscv/icE1usb/usb_desc_app.c b/firmware/ice40-riscv/icE1usb/usb_desc_app.c
index 663be0b..22a81d3 100644
--- a/firmware/ice40-riscv/icE1usb/usb_desc_app.c
+++ b/firmware/ice40-riscv/icE1usb/usb_desc_app.c
@@ -39,7 +39,7 @@
struct usb_ep_desc ep_fb;
struct usb_ep_desc ep_interrupt;
} __attribute__ ((packed)) on;
- } __attribute__ ((packed)) e1;
+ } __attribute__ ((packed)) e1[2];
/* CDC */
#if 0
@@ -71,7 +71,7 @@
.bmAttributes = 0x80,
.bMaxPower = 0x32, /* 100 mA */
},
- .e1 = {
+ .e1[0] = {
.off = {
.intf = {
.bLength = sizeof(struct usb_intf_desc),
@@ -103,7 +103,7 @@
.bInterfaceClass = 0xff,
.bInterfaceSubClass = 0xe1,
.bInterfaceProtocol = 0x00,
- .iInterface = 5,
+ .iInterface = 6,
},
.ep_data_in = {
.bLength = sizeof(struct usb_ep_desc),
@@ -139,6 +139,74 @@
},
},
},
+ .e1[1] = {
+ .off = {
+ .intf = {
+ .bLength = sizeof(struct usb_intf_desc),
+ .bDescriptorType = USB_DT_INTF,
+ .bInterfaceNumber = USB_INTF_E1(1),
+ .bAlternateSetting = 0,
+ .bNumEndpoints = 1,
+ .bInterfaceClass = 0xff,
+ .bInterfaceSubClass = 0xe1,
+ .bInterfaceProtocol = 0x00,
+ .iInterface = 7,
+ },
+ .ep_interrupt = {
+ .bLength = sizeof(struct usb_ep_desc),
+ .bDescriptorType = USB_DT_EP,
+ .bEndpointAddress = USB_EP_E1_INT(1),
+ .bmAttributes = 0x03,
+ .wMaxPacketSize = 10,
+ .bInterval = 4, /* every 4 ms */
+ },
+ },
+ .on = {
+ .intf = {
+ .bLength = sizeof(struct usb_intf_desc),
+ .bDescriptorType = USB_DT_INTF,
+ .bInterfaceNumber = USB_INTF_E1(1),
+ .bAlternateSetting = 1,
+ .bNumEndpoints = 4,
+ .bInterfaceClass = 0xff,
+ .bInterfaceSubClass = 0xe1,
+ .bInterfaceProtocol = 0x00,
+ .iInterface = 8,
+ },
+ .ep_data_in = {
+ .bLength = sizeof(struct usb_ep_desc),
+ .bDescriptorType = USB_DT_EP,
+ .bEndpointAddress = USB_EP_E1_IN(1),
+ .bmAttributes = 0x05,
+ .wMaxPacketSize = 292,
+ .bInterval = 1,
+ },
+ .ep_data_out = {
+ .bLength = sizeof(struct usb_ep_desc),
+ .bDescriptorType = USB_DT_EP,
+ .bEndpointAddress = USB_EP_E1_OUT(1),
+ .bmAttributes = 0x05,
+ .wMaxPacketSize = 292,
+ .bInterval = 1,
+ },
+ .ep_fb = {
+ .bLength = sizeof(struct usb_ep_desc),
+ .bDescriptorType = USB_DT_EP,
+ .bEndpointAddress = USB_EP_E1_FB(1),
+ .bmAttributes = 0x11,
+ .wMaxPacketSize = 3,
+ .bInterval = 3, /* every 2^(3-1) = 4 ms */
+ },
+ .ep_interrupt = {
+ .bLength = sizeof(struct usb_ep_desc),
+ .bDescriptorType = USB_DT_EP,
+ .bEndpointAddress = USB_EP_E1_INT(1),
+ .bmAttributes = 0x03,
+ .wMaxPacketSize = 10,
+ .bInterval = 4, /* every 4 ms */
+ },
+ },
+ },
#if 0
.cdc = {
.intf_ctl = {
@@ -150,7 +218,7 @@
.bInterfaceClass = USB_CLS_CDC_CONTROL,
.bInterfaceSubClass = USB_CDC_SCLS_ACM,
.bInterfaceProtocol = 0x00,
- .iInterface = 6,
+ .iInterface = 9,
},
.cdc_hdr = {
.bLength = sizeof(struct usb_cdc_hdr_desc),
@@ -188,7 +256,7 @@
.bInterfaceClass = USB_CLS_CDC_DATA,
.bInterfaceSubClass = 0x00,
.bInterfaceProtocol = 0x00,
- .iInterface = 7,
+ .iInterface = 10,
},
.ep_data_out = {
.bLength = sizeof(struct usb_ep_desc),
@@ -218,7 +286,7 @@
.bInterfaceClass = 0xfe,
.bInterfaceSubClass = 0x01,
.bInterfaceProtocol = 0x01,
- .iInterface = 8,
+ .iInterface = 11,
},
.func = {
.bLength = sizeof(struct usb_dfu_func_desc),
diff --git a/firmware/ice40-riscv/icE1usb/usb_desc_ids.h b/firmware/ice40-riscv/icE1usb/usb_desc_ids.h
index 2247f84..00dbb34 100644
--- a/firmware/ice40-riscv/icE1usb/usb_desc_ids.h
+++ b/firmware/ice40-riscv/icE1usb/usb_desc_ids.h
@@ -8,8 +8,8 @@
#pragma once
#define USB_INTF_E1(p) (0 + (p))
-#define USB_INTF_DFU 1
-#define USB_INTF_NUM 2
+#define USB_INTF_DFU 2
+#define USB_INTF_NUM 3
#define USB_EP_E1_IN(p) (0x82 + (3 * (p)))
#define USB_EP_E1_OUT(p) (0x01 + (3 * (p)))
diff --git a/firmware/ice40-riscv/icE1usb/usb_e1.c b/firmware/ice40-riscv/icE1usb/usb_e1.c
index 75811c2..a18ebe4 100644
--- a/firmware/ice40-riscv/icE1usb/usb_e1.c
+++ b/firmware/ice40-riscv/icE1usb/usb_e1.c
@@ -16,17 +16,20 @@
#include "e1.h"
#include "misc.h"
#include "usb_desc_ids.h"
+#include "utils.h"
#include "ice1usb_proto.h"
-struct {
+struct usb_e1_state {
bool running; /* are we running (transceiving USB data)? */
int out_bdi; /* buffer descriptor index for OUT EP */
int in_bdi; /* buffer descriptor index for IN EP */
struct ice1usb_tx_config tx_cfg;
struct ice1usb_rx_config rx_cfg;
struct e1_error_count last_err;
-} g_usb_e1;
+};
+
+static struct usb_e1_state g_usb_e1[2];
/* default configuration at power-up */
static const struct ice1usb_tx_config tx_cfg_default = {
@@ -47,8 +50,29 @@
return (ep & 0x80) ? &usb_ep_regs[ep & 0x1f].in : &usb_ep_regs[ep & 0x1f].out;
}
+static struct usb_e1_state *
+_get_state(int port)
+{
+ if ((port < 0) || (port > 1))
+ panic("_get_state invalid port %d", port);
+ return &g_usb_e1[port];
+}
+
+static int
+_ifnum2port(uint8_t bInterfaceNumber)
+{
+ switch (bInterfaceNumber) {
+ case USB_INTF_E1(0): return 0;
+ case USB_INTF_E1(1): return 1;
+ default:
+ /* Don't panic since this will be handled as USB STALL */
+ return -1;
+ }
+}
+
+
static void
-_usb_fill_feedback_ep(void)
+_usb_fill_feedback_ep(int port)
{
static uint16_t ticks_prev = 0;
uint16_t ticks;
@@ -57,41 +81,42 @@
volatile struct usb_ep *ep_regs;
/* Compute real E1 tick count (with safety against bad values) */
- ticks = e1_tick_read(0);
+ ticks = e1_tick_read(port);
val = (ticks - ticks_prev) & 0xffff;
ticks_prev = ticks;
if ((val < 7168) | (val > 9216))
val = 8192;
/* Bias depending on TX fifo level */
- level = e1_tx_level(0);
+ level = e1_tx_level(port);
if (level < (3 * 16))
val += 256;
else if (level > (8 * 16))
val -= 256;
/* Prepare buffer */
- ep_regs = _get_ep_regs(USB_EP_E1_FB(0));
+ ep_regs = _get_ep_regs(USB_EP_E1_FB(port));
usb_data_write(ep_regs->bd[0].ptr, &val, 4);
ep_regs->bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(3);
}
void
-usb_e1_run(void)
+usb_e1_run(int port)
{
+ struct usb_e1_state *usb_e1 = _get_state(port);
volatile struct usb_ep *ep_regs;
int bdi;
- if (!g_usb_e1.running)
+ if (!usb_e1->running)
return;
/* Interrupt endpoint */
- ep_regs = _get_ep_regs(USB_EP_E1_INT(0));
+ ep_regs = _get_ep_regs(USB_EP_E1_INT(port));
if ((ep_regs->bd[0].csr & USB_BD_STATE_MSK) != USB_BD_STATE_RDY_DATA) {
- const struct e1_error_count *cur_err = e1_get_error_count(0);
- if (memcmp(cur_err, &g_usb_e1.last_err, sizeof(*cur_err))) {
+ const struct e1_error_count *cur_err = e1_get_error_count(port);
+ if (memcmp(cur_err, &usb_e1->last_err, sizeof(*cur_err))) {
struct ice1usb_irq errmsg = {
.type = ICE1USB_IRQ_T_ERRCNT,
.u = {
@@ -107,13 +132,13 @@
printf("E");
usb_data_write(ep_regs->bd[0].ptr, &errmsg, sizeof(errmsg));
ep_regs->bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(sizeof(errmsg));
- g_usb_e1.last_err = *cur_err;
+ usb_e1->last_err = *cur_err;
}
}
/* Data IN endpoint */
- ep_regs = _get_ep_regs(USB_EP_E1_IN(0));
- bdi = g_usb_e1.in_bdi;
+ ep_regs = _get_ep_regs(USB_EP_E1_IN(port));
+ bdi = usb_e1->in_bdi;
while ((ep_regs->bd[bdi].csr & USB_BD_STATE_MSK) != USB_BD_STATE_RDY_DATA)
{
@@ -126,7 +151,7 @@
puts("Err EP IN\n");
/* Get some data from E1 */
- int n = e1_rx_level(0);
+ int n = e1_rx_level(port);
if (n > 32)
n = 9;
@@ -135,7 +160,7 @@
else if (!n)
break;
- n = e1_rx_need_data(0, (ptr >> 2) + 1, n, &pos);
+ n = e1_rx_need_data(port, (ptr >> 2) + 1, n, &pos);
/* Write header: currently version and pos (mfr/fr number) */
hdr = (0 << 28) | (pos & 0xff);
@@ -146,12 +171,12 @@
/* Next BDI */
bdi ^= 1;
- g_usb_e1.in_bdi = bdi;
+ usb_e1->in_bdi = bdi;
}
/* Data OUT endpoint */
- ep_regs = _get_ep_regs(USB_EP_E1_OUT(0));
- bdi = g_usb_e1.out_bdi;
+ ep_regs = _get_ep_regs(USB_EP_E1_OUT(port));
+ bdi = usb_e1->out_bdi;
while ((ep_regs->bd[bdi].csr & USB_BD_STATE_MSK) != USB_BD_STATE_RDY_DATA)
{
@@ -171,7 +196,7 @@
/* Empty data into the FIFO */
int n = ((int)(csr & USB_BD_LEN_MSK) - 6) / 32;
if (n > 0)
- e1_tx_feed_data(0, (ptr >> 2) + 1, n);
+ e1_tx_feed_data(port, (ptr >> 2) + 1, n);
refill:
/* Refill it */
@@ -179,7 +204,7 @@
/* Next BDI */
bdi ^= 1;
- g_usb_e1.out_bdi = bdi;
+ usb_e1->out_bdi = bdi;
static int x = 0;
if ((x++ & 0xff) == 0xff)
@@ -187,11 +212,11 @@
}
/* Feedback endpoint */
- ep_regs = _get_ep_regs(USB_EP_E1_FB(0));
+ ep_regs = _get_ep_regs(USB_EP_E1_FB(port));
if ((ep_regs->bd[0].csr & USB_BD_STATE_MSK) != USB_BD_STATE_RDY_DATA)
{
- _usb_fill_feedback_ep();
+ _usb_fill_feedback_ep(port);
}
}
@@ -204,24 +229,28 @@
if (!conf)
return USB_FND_SUCCESS;
- intf = usb_desc_find_intf(conf, USB_INTF_E1(0), 0, NULL);
- if (!intf)
- return USB_FND_ERROR;
+ for (int port=0; port<2; port++)
+ {
+ intf = usb_desc_find_intf(conf, USB_INTF_E1(port), 0, NULL);
+ if (!intf)
+ return USB_FND_ERROR;
- printf("e1 set_conf %08x\n", intf);
+ printf("e1 set_conf[%d] %08x\n", port, intf);
- usb_ep_boot(intf, USB_EP_E1_IN(0), true);
- usb_ep_boot(intf, USB_EP_E1_OUT(0), true);
- usb_ep_boot(intf, USB_EP_E1_FB(0), false);
- usb_ep_boot(intf, USB_EP_E1_INT(0), false);
+ usb_ep_boot(intf, USB_EP_E1_IN(port), true);
+ usb_ep_boot(intf, USB_EP_E1_OUT(port), true);
+ usb_ep_boot(intf, USB_EP_E1_FB(port), false);
+ usb_ep_boot(intf, USB_EP_E1_INT(port), false);
+ }
return USB_FND_SUCCESS;
}
-static void _perform_tx_config(void)
+static void _perform_tx_config(int port)
{
- const struct ice1usb_tx_config *cfg = &g_usb_e1.tx_cfg;
- e1_tx_config(0,
+ struct usb_e1_state *usb_e1 = _get_state(port);
+ const struct ice1usb_tx_config *cfg = &usb_e1->tx_cfg;
+ e1_tx_config(port,
((cfg->mode & 3) << 1) |
((cfg->timing & 1) << 3) |
((cfg->alarm & 1) << 4) |
@@ -229,10 +258,11 @@
);
}
-static void _perform_rx_config(void)
+static void _perform_rx_config(int port)
{
- const struct ice1usb_rx_config *cfg = &g_usb_e1.rx_cfg;
- e1_rx_config(0,
+ struct usb_e1_state *usb_e1 = _get_state(port);
+ const struct ice1usb_rx_config *cfg = &usb_e1->rx_cfg;
+ e1_rx_config(port,
(cfg->mode << 1)
);
}
@@ -241,50 +271,60 @@
_e1_set_intf(const struct usb_intf_desc *base, const struct usb_intf_desc *sel)
{
volatile struct usb_ep *ep_regs;
+ struct usb_e1_state *usb_e1;
+ int port;
- /* Validity checks */
+ /* Is it for E1 interface ? */
if ((base->bInterfaceClass != 0xff) || (base->bInterfaceSubClass != 0xe1))
return USB_FND_CONTINUE;
+ /* Get matching port (if any) */
+ port = _ifnum2port(base->bInterfaceNumber);
+ if (port < 0)
+ return USB_FND_ERROR;
+
+ usb_e1 = _get_state(port);
+
+ /* Valid setting ? */
if (sel->bAlternateSetting > 1)
return USB_FND_ERROR;
/* Don't do anything if no change */
- if (g_usb_e1.running == (sel->bAlternateSetting != 0))
+ if (usb_e1->running == (sel->bAlternateSetting != 0))
return USB_FND_SUCCESS;
- g_usb_e1.running = (sel->bAlternateSetting != 0);
+ usb_e1->running = (sel->bAlternateSetting != 0);
/* Reconfigure the endpoints */
- usb_ep_reconf(sel, USB_EP_E1_IN(0));
- usb_ep_reconf(sel, USB_EP_E1_OUT(0));
- usb_ep_reconf(sel, USB_EP_E1_FB(0));
- usb_ep_reconf(sel, USB_EP_E1_INT(0));
+ usb_ep_reconf(sel, USB_EP_E1_IN(port));
+ usb_ep_reconf(sel, USB_EP_E1_OUT(port));
+ usb_ep_reconf(sel, USB_EP_E1_FB(port));
+ usb_ep_reconf(sel, USB_EP_E1_INT(port));
/* Update E1 and USB state */
- switch (g_usb_e1.running) {
+ switch (usb_e1->running) {
case false:
/* Disable E1 rx/tx */
- e1_init(0, 0, 0);
+ e1_init(port, 0, 0);
break;
case true:
/* Reset and Re-Enable E1 */
- e1_init(0, 0, 0);
- _perform_rx_config();
- _perform_tx_config();
+ e1_init(port, 0, 0);
+ _perform_rx_config(port);
+ _perform_tx_config(port);
/* Reset BDI */
- g_usb_e1.in_bdi = 0;
- g_usb_e1.out_bdi = 0;
+ usb_e1->in_bdi = 0;
+ usb_e1->out_bdi = 0;
/* EP OUT: Queue two buffers */
- ep_regs = _get_ep_regs(USB_EP_E1_FB(0));
+ ep_regs = _get_ep_regs(USB_EP_E1_OUT(port));
ep_regs->bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(292);
ep_regs->bd[1].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(292);
/* EP Feedback: Pre-fill */
- _usb_fill_feedback_ep();
+ _usb_fill_feedback_ep(port);
break;
}
@@ -295,10 +335,22 @@
static enum usb_fnd_resp
_e1_get_intf(const struct usb_intf_desc *base, uint8_t *alt)
{
+ struct usb_e1_state *usb_e1;
+ int port;
+
+ /* Is it for E1 interface ? */
if ((base->bInterfaceClass != 0xff) || (base->bInterfaceSubClass != 0xe1))
return USB_FND_CONTINUE;
- *alt = g_usb_e1.running ? 1 : 0;
+ /* Get matching port (if any) */
+ port = _ifnum2port(base->bInterfaceNumber);
+ if (port < 0)
+ return USB_FND_ERROR;
+
+ usb_e1 = _get_state(port);
+
+ /* Return current alt-setting */
+ *alt = usb_e1->running ? 1 : 0;
return USB_FND_SUCCESS;
}
@@ -307,10 +359,13 @@
_set_tx_mode_done(struct usb_xfer *xfer)
{
const struct ice1usb_tx_config *cfg = (const struct ice1usb_tx_config *) xfer->data;
- printf("set_tx_mode %02x%02x%02x%02x\r\n",
+ struct usb_ctrl_req *req = xfer->cb_ctx;
+ int port = _ifnum2port(req->wIndex);
+ struct usb_e1_state *usb_e1 = _get_state(port);
+ printf("set_tx_mode[%d] %02x%02x%02x%02x\r\n", port,
xfer->data[0], xfer->data[1], xfer->data[2], xfer->data[3]);
- g_usb_e1.tx_cfg = *cfg;
- _perform_tx_config();
+ usb_e1->tx_cfg = *cfg;
+ _perform_tx_config(port);
return true;
}
@@ -318,9 +373,12 @@
_set_rx_mode_done(struct usb_xfer *xfer)
{
const struct ice1usb_rx_config *cfg = (const struct ice1usb_rx_config *) xfer->data;
- printf("set_rx_mode %02x\r\n", xfer->data[0]);
- g_usb_e1.rx_cfg = *cfg;
- _perform_rx_config();
+ struct usb_ctrl_req *req = xfer->cb_ctx;
+ int port = _ifnum2port(req->wIndex);
+ struct usb_e1_state *usb_e1 = _get_state(port);
+ printf("set_rx_mode[%d] %02x\r\n", port, xfer->data[0]);
+ usb_e1->rx_cfg = *cfg;
+ _perform_rx_config(port);
return true;
}
@@ -328,8 +386,17 @@
static enum usb_fnd_resp
_e1_ctrl_req_intf(struct usb_ctrl_req *req, struct usb_xfer *xfer)
{
- unsigned int i;
+ struct usb_e1_state *usb_e1;
+ int port;
+ /* Get matching port (if any) */
+ port = _ifnum2port(req->wIndex);
+ if (port < 0)
+ return USB_FND_CONTINUE;
+
+ usb_e1 = _get_state(port);
+
+ /* Process request */
switch (req->bRequest) {
case ICE1USB_INTF_GET_CAPABILITIES:
/* no optional capabilities yet */
@@ -345,7 +412,7 @@
case ICE1USB_INTF_GET_TX_CFG:
if (req->wLength < sizeof(struct ice1usb_tx_config))
return USB_FND_ERROR;
- memcpy(xfer->data, &g_usb_e1.tx_cfg, sizeof(struct ice1usb_tx_config));
+ memcpy(xfer->data, &usb_e1->tx_cfg, sizeof(struct ice1usb_tx_config));
xfer->len = sizeof(struct ice1usb_tx_config);
break;
case ICE1USB_INTF_SET_RX_CFG:
@@ -358,7 +425,7 @@
case ICE1USB_INTF_GET_RX_CFG:
if (req->wLength < sizeof(struct ice1usb_rx_config))
return USB_FND_ERROR;
- memcpy(xfer->data, &g_usb_e1.rx_cfg, sizeof(struct ice1usb_rx_config));
+ memcpy(xfer->data, &usb_e1->rx_cfg, sizeof(struct ice1usb_rx_config));
xfer->len = sizeof(struct ice1usb_rx_config);
break;
default:
@@ -396,8 +463,6 @@
case USB_REQ_RCPT_DEV:
return _e1_ctrl_req_dev(req, xfer);
case USB_REQ_RCPT_INTF:
- if (req->wIndex != USB_INTF_E1(0))
- return USB_FND_CONTINUE;
return _e1_ctrl_req_intf(req, xfer);
case USB_REQ_RCPT_EP:
case USB_REQ_RCPT_OTHER:
@@ -417,8 +482,12 @@
void
usb_e1_init(void)
{
- memset(&g_usb_e1, 0x00, sizeof(g_usb_e1));
- g_usb_e1.tx_cfg = tx_cfg_default;
- g_usb_e1.rx_cfg = rx_cfg_default;
+ for (int i=0; i<2; i++) {
+ struct usb_e1_state *usb_e1 = _get_state(i);
+ memset(usb_e1, 0x00, sizeof(struct usb_e1_state));
+ usb_e1->tx_cfg = tx_cfg_default;
+ usb_e1->rx_cfg = rx_cfg_default;
+ }
+
usb_register_function_driver(&_e1_drv);
}
diff --git a/firmware/ice40-riscv/icE1usb/usb_e1.h b/firmware/ice40-riscv/icE1usb/usb_e1.h
index a573b38..06723ea 100644
--- a/firmware/ice40-riscv/icE1usb/usb_e1.h
+++ b/firmware/ice40-riscv/icE1usb/usb_e1.h
@@ -7,5 +7,5 @@
#pragma once
-void usb_e1_run(void);
+void usb_e1_run(int port);
void usb_e1_init(void);
diff --git a/firmware/ice40-riscv/icE1usb/usb_str_app.txt b/firmware/ice40-riscv/icE1usb/usb_str_app.txt
index 10887d3..3aa6b4a 100644
--- a/firmware/ice40-riscv/icE1usb/usb_str_app.txt
+++ b/firmware/ice40-riscv/icE1usb/usb_str_app.txt
@@ -2,7 +2,10 @@
osmocom
icE1usb
Main
-E1
-Console (control)
-Console (data)
+E1 port 0 (disabled)
+E1 port 0
+E1 port 1 (disabled)
+E1 port 1
+GPS (CDC control)
+GPS (CDC data)
DFU runtime