diff --git a/mongoose.c b/mongoose.c index ef838866..7c4cb987 100644 --- a/mongoose.c +++ b/mongoose.c @@ -874,8 +874,11 @@ static bool bsd_run(struct mg_bsd_cmd *cmd) { cmd->op_setsockopt.optlen != sizeof(int)) { cmd->result = -1, cmd->err = EINVAL; } else if ((cmd->op_setsockopt.level == SOL_SOCKET && - (cmd->op_setsockopt.optname == SO_REUSEADDR || - cmd->op_setsockopt.optname == SO_NOSIGPIPE)) || + (cmd->op_setsockopt.optname == SO_REUSEADDR +#ifdef SO_NOSIGPIPE + || cmd->op_setsockopt.optname == SO_NOSIGPIPE +#endif + )) || (!s->is_udp && cmd->op_setsockopt.level == IPPROTO_TCP && cmd->op_setsockopt.optname == TCP_NODELAY)) { cmd->result = 0; @@ -5994,70 +5997,6 @@ size_t mg_l2_driver_output(struct mg_tcpip_if *ifp, size_t len) { #endif -#ifdef MG_ENABLE_LINES -#line 1 "src/l2_802154.c" -#endif - - - -#if MG_ENABLE_TCPIP - -// Radio frame: 802.15.4 MAC header, 15 bytes: data frame, PAN ID compression, -// broadcast destination, our EUI-64 made from MAC as source. Then 2 bytes: -// tag, fragment index << 4 | last fragment index. Then data. 125 bytes max, -// 127 minus FCS -#define MGI_802154_HDR 17 -#define MGI_802154_DATA (125 - MGI_802154_HDR) - -size_t mg_l2_802154_tx(struct mg_tcpip_if *ifp, const void *buf, size_t len, - size_t (*send_fn)(const uint8_t *, size_t, - struct mg_tcpip_if *)) { - struct mg_l2_802154_data *d = - (struct mg_l2_802154_data *) ifp->driver_data; - const uint8_t *p = (const uint8_t *) buf, *m = ifp->mac; - uint8_t f[MGI_802154_HDR + MGI_802154_DATA]; - size_t i, n, last = (len - 1) / MGI_802154_DATA; - if (len == 0 || last > 15) return 0; - d->tag++; - for (i = 0; i <= last; i++) { - n = i == last ? len - i * MGI_802154_DATA : MGI_802154_DATA; - f[0] = 0x41, f[1] = 0xd8, f[2] = d->seq++; // 2006 frame, see above - f[3] = (uint8_t) d->pan_id, f[4] = (uint8_t) (d->pan_id >> 8); - f[5] = f[6] = 0xff; - f[7] = m[5], f[8] = m[4], f[9] = m[3], f[10] = 0xfe, f[11] = 0xff; - f[12] = m[2], f[13] = m[1], f[14] = m[0]; // Little endian EUI-64 - f[15] = d->tag, f[16] = (uint8_t) ((i << 4) | last); - memcpy(f + MGI_802154_HDR, p + i * MGI_802154_DATA, n); - if (send_fn(f, MGI_802154_HDR + n, ifp) == 0) return 0; - } - return len; -} - -size_t mg_l2_802154_rx(struct mg_tcpip_if *ifp, const uint8_t *f, size_t n, - void *buf, size_t len) { - struct mg_l2_802154_data *d = - (struct mg_l2_802154_data *) ifp->driver_data; - size_t idx, last; - if (n <= MGI_802154_HDR || f[0] != 0x41 || f[1] != 0xd8 || - f[3] != (uint8_t) d->pan_id || f[4] != (uint8_t) (d->pan_id >> 8)) - return 0; // Not ours - idx = f[16] >> 4, last = f[16] & 15; - if (idx == 0) d->rxtag = f[15], d->rxnext = 0, d->rxlen = 0; - if (idx == 0) memcpy(d->rxsrc, f + 7, sizeof(d->rxsrc)); - n -= MGI_802154_HDR; - if (idx != d->rxnext || idx > last || f[15] != d->rxtag || - memcmp(d->rxsrc, f + 7, sizeof(d->rxsrc)) != 0 || - d->rxlen + n > sizeof(d->rxbuf)) - return 0; // Missed a fragment, or from another sender - memcpy(d->rxbuf + d->rxlen, f + MGI_802154_HDR, n); - d->rxlen = (uint16_t) (d->rxlen + n), d->rxnext++; - if (idx < last || d->rxlen > len) return 0; - memcpy(buf, d->rxbuf, d->rxlen); - return d->rxlen; -} - -#endif - #ifdef MG_ENABLE_LINES #line 1 "src/l2_eth.c" #endif @@ -6280,6 +6219,626 @@ uint8_t mg_l2_eth_ip6put(struct mg_l2addr *l2addr, uint8_t *opts) { #endif +#ifdef MG_ENABLE_LINES +#line 1 "src/l2_mesh.c" +#endif +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + + + + + + +#if MG_ENABLE_TCPIP + +#define MGI_ETH_MAX 1514 +#define MGI_TX_DST_LEN 2 + +// Mesh header, after the radio header: type, then burst: the fragment's +// index << 4 | the last fragment's index. Common to every radio +#define MG_MESH_HDR_LEN 2 + +#define MG_MESH_FRAME_HDR_LEN (MG_802154_HEADER_LEN + MG_MESH_HDR_LEN) + +// MAC header, 9 bytes: frame control 0x8841, a data frame, PAN ID +// compression, short destination and source addresses; sequence number, 0, +// as nothing uses it yet; PAN ID; destination; source. Little endian, as +// the standard says +static void mgi_802154_print_hdr(const struct mg_radio *radio, uint8_t *buf, + uint16_t dst, uint16_t src) { + buf[0] = 0x41, buf[1] = 0x88, buf[2] = 0; + buf[3] = (uint8_t) MG_802154_PAN_ID; + buf[4] = (uint8_t) (MG_802154_PAN_ID >> 8); + buf[5] = (uint8_t) dst, buf[6] = (uint8_t) (dst >> 8); + buf[7] = (uint8_t) src, buf[8] = (uint8_t) (src >> 8); + (void) radio; +} + +static size_t mgi_802154_parse_hdr(const struct mg_radio *radio, + const uint8_t *buf, size_t len, + uint16_t *dst, uint16_t *src) { + (void) radio; + if (len < MG_802154_HEADER_LEN || buf[0] != 0x41 || buf[1] != 0x88 || + buf[3] != (uint8_t) MG_802154_PAN_ID || + buf[4] != (uint8_t) (MG_802154_PAN_ID >> 8)) { + return 0; + } + *dst = (uint16_t) (buf[5] | buf[6] << 8); + *src = (uint16_t) (buf[7] | buf[8] << 8); + return MG_802154_HEADER_LEN; +} + +const struct mg_radio mg_radio_802154 = { + "802.15.4", // name + 125, // max_frame_size: 127 minus FCS + 250000, // bitrate + 8, // overhead + 320, // slot_us + 128, // cca_us + 100, // turnaround_us + 640, // ifs_us + 3, // backoff_min + 5, // backoff_max + true, // csma + MG_802154_HEADER_LEN, // header_len + mgi_802154_print_hdr, // print_hdr + mgi_802154_parse_hdr // parse_hdr +}; + +static uint64_t mgi_mesh_b_airtime(size_t len) { + const struct mg_radio *radio = &mg_radio_802154; + return ((uint64_t) (len + radio->overhead) * 8000000 + radio->bitrate - 1) / + radio->bitrate; +} + +// Each queue record holds a destination and a complete burst. +static size_t mgi_mesh_b_queue(struct mg_mesh_node *node, struct mg_queue *q, + const void *buf, size_t len, uint16_t dst, + uint8_t type) { + const struct mg_radio *radio = &mg_radio_802154; + const uint8_t *eth = (const uint8_t *) buf; + size_t hdr = MG_MESH_FRAME_HDR_LEN, cap = radio->max_frame_size - hdr; + size_t frames, size, i; + char *record; + uint8_t *out; + if (len < 14 || len > MGI_ETH_MAX) return 0; + frames = (len + cap - 1) / cap; + size = MGI_TX_DST_LEN + len + frames * (1 + hdr); + if (mg_queue_next(q, NULL) == 0) mg_queue_init(q, q->buf, q->size); + if (mg_queue_book(q, &record, size) < size) return 0; + out = (uint8_t *) record; + *out++ = (uint8_t) (dst >> 8); + *out++ = (uint8_t) dst; + for (i = 0; i < frames; i++) { + size_t chunk = len - i * cap; + if (chunk > cap) chunk = cap; + *out++ = (uint8_t) (hdr + chunk); + radio->print_hdr(radio, out, dst, node->radio_address); + out[radio->header_len] = type; + out[radio->header_len + 1] = (uint8_t) (i << 4 | (frames - 1)); + memcpy(out + hdr, eth + i * cap, chunk); + out += hdr + chunk; + } + mg_queue_add(q, size); + return len; +} + +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#define MGI_JOIN_WINDOW_US 3000 +#define MGI_JOIN_TURN_US 192 +#define MGI_JOIN_JITTER_US 640 +#define MGI_DATA_QUEUE_SIZE 2048 +#define MGI_TX_RETRY_US 32 +// Our frames: the radio header, with the radio addresses, then the mesh +// header MG_MESH_HDR_LEN, then the payload +#define MGI_JOIN_UP_LEN \ + (MG_MESH_FRAME_HDR_LEN + 6) // MGI_JOIN_UP's payload: MAC addr +#define MGI_MAX_FRAGMENTS 14 + +enum { MGI_JOIN_DOWN = 1, MGI_JOIN_UP, MGI_DATA_DOWN, MGI_DATA_UP }; + +enum { MGI_TURN_IDLE, MGI_TURN_JOIN, MGI_TURN_DATA }; + +struct mgi_mesh_b_data { + uint8_t rx_buf[MGI_ETH_MAX]; // Ethernet frame being reassembled + uint8_t rx_expected; // Expected index | last index, 0xff if idle + struct mg_mesh_node *node; // Owner of the TCP/IP interface + struct mg_queue tx; // One record per burst of RF fragments + size_t tx_offset; // Next queued fragment; zero for an empty frame + uint64_t tx_at; // Next fragment or JOIN transmission time + uint64_t deadline; // End of the current JOIN or DATA turn + struct mgi_mesh_b_peer *fanout; // Next broadcast recipient + char tx_buf[MGI_DATA_QUEUE_SIZE]; // Transmit queue storage + uint32_t random; // PRNG state for join timing + struct mgi_mesh_b_peer *members; // Bridge membership list + struct mgi_mesh_b_peer *current; // Member whose turn is active or next + int8_t missed_rounds; // -1 after a grant; otherwise completed rounds missed + uint16_t coordinator; // Coordinator radio address + uint8_t phase; // MGI_TURN_IDLE, MGI_TURN_JOIN, or MGI_TURN_DATA +}; + +struct mgi_mesh_b_peer { + struct mgi_mesh_b_peer *next; // Next member in round-robin order + uint16_t radio_address; // Source address supplied in MGI_JOIN_UP + uint8_t mac[6]; // Ethernet MAC supplied in MGI_JOIN_UP + uint8_t misses; // Turns in a row it did not answer +}; + +static uint32_t mgi_mesh_b_random(struct mg_mesh_node *node, + struct mgi_mesh_b_data *data) { + uint32_t *random = &data->random; + if (*random == 0) { + size_t i; + *random = (uint32_t) mg_now(); + for (i = 0; i < 6; i++) *random = *random * 33u + node->radio->mac[i]; + if (*random == 0) *random = 1; + } + *random ^= *random << 13; + *random ^= *random >> 17; + *random ^= *random << 5; + return *random; +} + +static const struct mg_radio *const s_mesh_default_radio = &mg_radio_802154; + +// Read a frame's radio addresses to dst and src, see mg_radio::parse_hdr(), and +// return its type, from the mesh header. 0: not a frame of ours +static unsigned mgi_mesh_b_frame_parse(const uint8_t *frame, size_t len, + uint16_t *dst, uint16_t *src) { + if (len < MG_MESH_FRAME_HDR_LEN || + s_mesh_default_radio->parse_hdr(s_mesh_default_radio, frame, len, dst, + src) != + s_mesh_default_radio->header_len) { + return 0; + } + return frame[s_mesh_default_radio->header_len]; +} + +// Write a frame's headers: the radio's, to dst from src, then ours: type, +// and burst: high nibble is the index, low nibble is the last index +static void mgi_mesh_b_frame_header(uint8_t *frame, uint16_t dst, uint16_t src, + uint8_t type, uint8_t burst) { + size_t h = s_mesh_default_radio->header_len; + s_mesh_default_radio->print_hdr(s_mesh_default_radio, frame, dst, src); + frame[h] = type, frame[h + 1] = burst; +} + +static bool mgi_mesh_b_control_send(struct mg_mesh_node *node, uint16_t dst, + uint8_t command) { + struct mg_tcpip_if *ifp = node->radio; + uint8_t frame[MGI_JOIN_UP_LEN] = {0}; + size_t len = MG_MESH_FRAME_HDR_LEN; + mgi_mesh_b_frame_header(frame, dst, node->radio_address, command, 0); + if (command == MGI_JOIN_UP) { + memcpy(frame + MG_MESH_FRAME_HDR_LEN, node->radio->mac, 6); + len = MGI_JOIN_UP_LEN; + } + return node->now >= node->tx_at && node->tx(ifp, frame, len) == len; +} + +static size_t mgi_mesh_b_fragment_capacity(void) { + return s_mesh_default_radio->max_frame_size - MG_MESH_FRAME_HDR_LEN; +} + +static uint64_t mgi_mesh_b_burst_time(size_t len) { + size_t cap = mgi_mesh_b_fragment_capacity(); + size_t frames = len == 0 ? 1 : (len + cap - 1) / cap; + return (frames - 1) * + (mgi_mesh_b_airtime(s_mesh_default_radio->max_frame_size) + + s_mesh_default_radio->ifs_us) + + mgi_mesh_b_airtime(MG_MESH_FRAME_HDR_LEN + len - (frames - 1) * cap); +} + +static void mgi_mesh_b_tx_finish(struct mgi_mesh_b_data *data) { + if (data->tx_offset != 0) { + if (data->fanout != NULL) data->fanout = data->fanout->next; + if (data->fanout == NULL) { + size_t len = mg_queue_next(&data->tx, NULL); + mg_queue_del(&data->tx, len); + } + } + data->tx_offset = 0; + data->tx_at = (uint64_t) -1; +} + +static void mgi_mesh_b_tx_step(struct mgi_mesh_b_data *data, uint64_t now) { + struct mg_mesh_node *node = data->node; + struct mg_tcpip_if *radio = node->radio; + uint8_t empty[MG_MESH_FRAME_HDR_LEN], *frame = empty; + size_t len = MG_MESH_FRAME_HDR_LEN, record_len = 0; + uint16_t dst; + uint8_t command; + if (now < data->tx_at) return; + if (now >= data->deadline) { + mgi_mesh_b_tx_finish(data); + return; + } + dst = node->lan == NULL ? data->coordinator : data->current->radio_address; + command = node->lan == NULL ? MGI_DATA_UP : MGI_DATA_DOWN; + if (data->tx_offset != 0) { + char *record; + uint8_t *part; + record_len = mg_queue_next(&data->tx, &record); + part = (uint8_t *) record + data->tx_offset; + len = part[0]; + frame = part + 1; + s_mesh_default_radio->print_hdr(s_mesh_default_radio, frame, dst, + node->radio_address); + } else { + mgi_mesh_b_frame_header(frame, dst, node->radio_address, command, 0); + } + if (now < data->node->tx_at || data->node->tx(radio, frame, len) != len) { + data->tx_at = now + MGI_TX_RETRY_US; + return; + } + if (data->tx_offset == 0 || (data->tx_offset += len + 1) == record_len) { + mgi_mesh_b_tx_finish(data); + } else { + data->tx_at = now + mgi_mesh_b_airtime(len) + s_mesh_default_radio->ifs_us; + } +} + +static void mgi_mesh_b_tx_begin(struct mgi_mesh_b_data *data, bool burst, + uint64_t now, uint64_t deadline) { + data->phase = MGI_TURN_DATA; + data->tx_offset = burst ? MGI_TX_DST_LEN : 0; + data->tx_at = now; + data->deadline = deadline; +} + +// Return the assembled length, zero for an empty frame, or -1 if incomplete +// or invalid. Full non-final fragments make the write offset derivable. +static int mgi_mesh_b_data_receive(struct mgi_mesh_b_data *data, + const uint8_t *frame, size_t len, + uint64_t now) { + unsigned burst = frame[MG_MESH_FRAME_HDR_LEN - 1], index = burst >> 4, + last = burst & 15; + size_t chunk = len - MG_MESH_FRAME_HDR_LEN, + cap = mgi_mesh_b_fragment_capacity(); + size_t offset = index * cap; + if (index == 0) { + data->rx_expected = (uint8_t) burst; + if (data->node->lan == NULL) + data->deadline = now + mgi_mesh_b_burst_time(MGI_ETH_MAX) + + 2 * s_mesh_default_radio->ifs_us + MGI_JOIN_TURN_US; + } + if (last >= MGI_MAX_FRAGMENTS || index > last || burst != data->rx_expected || + chunk > cap || now > data->deadline || (index < last && chunk != cap) || + (last != 0 && chunk == 0) || offset + chunk > sizeof(data->rx_buf)) { + data->rx_expected = 0xff; + return -1; + } + memcpy(data->rx_buf + offset, frame + MG_MESH_FRAME_HDR_LEN, chunk); + if (index < last) { + data->rx_expected = (uint8_t) (burst + 0x10); + return -1; + } + data->rx_expected = 0xff; + return offset + chunk == 0 || offset + chunk >= 14 ? (int) (offset + chunk) + : -1; +} + +// Bridge scheduling and Ethernet forwarding. + +#define MGI_MAX_MISSES 3 // Turns in a row without an answer: remove the member + +static uint16_t mgi_mesh_b_join_field(const uint8_t *p) { + return (uint16_t) ((unsigned) p[0] * 256 + p[1]); +} + +// Return chaddr for an Ethernet DHCP server reply, even when broadcast. +static const uint8_t *mgi_mesh_b_dhcp_client(const uint8_t *frame, size_t len) { + size_t ip_len, ihl, udp_len; + const uint8_t *udp, *dhcp; + if (len < 34 || mgi_mesh_b_join_field(frame + 12) != 0x0800 || + (frame[14] >> 4) != 4 || frame[23] != 17 || + (mgi_mesh_b_join_field(frame + 20) & 0x3fff) != 0) + return NULL; + ip_len = mgi_mesh_b_join_field(frame + 16); + ihl = (size_t) (frame[14] & 15) * 4; + if (ihl < 20 || ip_len > len - 14 || ip_len < ihl + 8 + 240) return NULL; + udp = frame + 14 + ihl; + udp_len = mgi_mesh_b_join_field(udp + 4); + if (mgi_mesh_b_join_field(udp) != 67 || + mgi_mesh_b_join_field(udp + 2) != 68 || udp_len < 8 + 240 || + udp_len > ip_len - ihl) + return NULL; + dhcp = udp + 8; + if (dhcp[0] != 2 || dhcp[1] != 1 || dhcp[2] != 6 || + memcmp(dhcp + 236, "\x63\x82\x53\x63", 4) != 0) + return NULL; + return dhcp + 28; +} + +static size_t mgi_mesh_b_enqueue(struct mg_mesh_node *node, const void *buf, + size_t len) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) node->data; + const uint8_t *frame = (const uint8_t *) buf; + if (node->lan == NULL) + return mgi_mesh_b_queue(node, &data->tx, buf, len, data->coordinator, + MGI_DATA_UP); + if (len >= 14 && len <= MGI_ETH_MAX) { + const uint8_t *client = mgi_mesh_b_dhcp_client(frame, len); + const uint8_t *dst = client == NULL ? frame : client; + bool multicast = client == NULL && (frame[0] & 1) != 0; + struct mgi_mesh_b_peer *peer; + for (peer = data->members; peer != NULL; peer = peer->next) { + if (multicast || memcmp(dst, peer->mac, 6) == 0) { + uint16_t addr = multicast ? 0xffffu : peer->radio_address; + return mgi_mesh_b_queue(node, &data->tx, buf, len, addr, MGI_DATA_DOWN); + } + } + } + return 0; +} + +static void mgi_mesh_b_bridge_queue_lan(struct mg_mesh_node *bridge) { + struct mg_queue *rx = &bridge->lan->recv_queue; + char *buf; + size_t len; + while ((len = mg_queue_next(rx, &buf)) > 0) { + bridge->strategy->tx(bridge, buf, len); + mg_queue_del(rx, len); + } +} + +static void mgi_mesh_b_bridge_rx(uint64_t now, struct mg_mesh_node *b, + const uint8_t *frame, size_t len) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) b->data; + struct mgi_mesh_b_peer *peer; + uint16_t dst = 0, src = 0; + unsigned command = mgi_mesh_b_frame_parse(frame, len, &dst, &src); + if (command == MGI_DATA_UP && dst == b->radio_address && + data->phase == MGI_TURN_DATA && data->current != NULL && + src == data->current->radio_address && now <= data->deadline) { + int received = mgi_mesh_b_data_receive(data, frame, len, now); + if (received < 0) return; + if (received != 0) { + b->lan->driver->tx(data->rx_buf, (size_t) received, b->lan); + } + data->current->misses = 0; + data->current = data->current->next; + data->phase = MGI_TURN_IDLE; + data->deadline = now; + return; + } + if (len != MGI_JOIN_UP_LEN || command != MGI_JOIN_UP || + dst != b->radio_address || data->phase != MGI_TURN_JOIN || + now >= data->deadline) { + return; + } + if (src == 0 || src == 0xffffu) return; + for (peer = data->members; peer != NULL; peer = peer->next) { + if (peer->radio_address == src) break; + } + if (peer == NULL) { + peer = (struct mgi_mesh_b_peer *) calloc(1, sizeof(*peer)); + if (peer == NULL) return; + peer->radio_address = src; + peer->next = data->members; + data->members = peer; + } + memcpy(peer->mac, frame + MG_MESH_FRAME_HDR_LEN, sizeof(peer->mac)); +} + +// Member peer did not answer its turn. After MGI_MAX_MISSES in a row, it is +// gone: remove it, and its queued downlink frames +static void mgi_mesh_b_member_missed(struct mgi_mesh_b_data *data, + struct mgi_mesh_b_peer *peer) { + if (++peer->misses < MGI_MAX_MISSES) return; + MG_INFO(("Member %M removed: no answer %d times", mg_print_mac, peer->mac, + MGI_MAX_MISSES)); + if (data->fanout == peer) { + data->fanout = peer->next; + if (data->fanout == NULL) { + size_t len = mg_queue_next(&data->tx, NULL); + mg_queue_del(&data->tx, len); + } + } + LIST_DELETE(struct mgi_mesh_b_peer, &data->members, peer); + free(peer); +} + +// A broadcast occupies one record and is sent once to each member in list +// order. +static size_t mgi_mesh_b_bridge_burst(struct mgi_mesh_b_data *data, + struct mgi_mesh_b_peer *peer) { + char *record; + size_t len; + while ((len = mg_queue_next(&data->tx, &record)) != 0) { + uint16_t dst = mgi_mesh_b_join_field((const uint8_t *) record); + struct mgi_mesh_b_peer *member; + if (dst == 0xffffu) { + if (data->fanout == NULL) data->fanout = data->members; + return data->fanout == peer ? len : 0; + } + if (dst == peer->radio_address) return len; + for (member = data->members; member != NULL; member = member->next) { + if (member->radio_address == dst) return 0; + } + mg_queue_del(&data->tx, len); // Its destination has left. + } + return 0; +} + +static uint64_t mgi_mesh_b_queued_burst_time(struct mgi_mesh_b_data *data, + size_t len) { + char *record; + size_t frames; + if (len == 0) return mgi_mesh_b_burst_time(0); + mg_queue_next(&data->tx, &record); + // Fragment zero carries the final index. Count overhead and IFS per frame. + frames = + ((uint8_t) record[MGI_TX_DST_LEN + MG_MESH_FRAME_HDR_LEN] & 15u) + 1u; + return mgi_mesh_b_airtime(len - MGI_TX_DST_LEN - frames + + (frames - 1) * s_mesh_default_radio->overhead) + + (frames - 1) * s_mesh_default_radio->ifs_us; +} + +static void mgi_mesh_b_bridge_io(uint64_t now, struct mg_mesh_node *b) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) b->data; + mgi_mesh_b_bridge_queue_lan(b); + if (now >= data->deadline) { + if (data->tx_at != (uint64_t) -1) mgi_mesh_b_tx_finish(data); + if (data->phase == MGI_TURN_DATA && data->current != NULL) { + struct mgi_mesh_b_peer *peer = data->current; + data->current = peer->next; + mgi_mesh_b_member_missed(data, peer); + } else if (data->phase == MGI_TURN_JOIN) { + data->current = data->members; + } + data->phase = MGI_TURN_IDLE; + if (data->current != NULL) { + size_t burst = mgi_mesh_b_bridge_burst(data, data->current); + uint64_t down = mgi_mesh_b_queued_burst_time(data, burst); + data->rx_expected = 0xff; + mgi_mesh_b_tx_begin(data, burst != 0, now, + now + down + mgi_mesh_b_burst_time(MGI_ETH_MAX) + + 2 * s_mesh_default_radio->ifs_us + + MGI_JOIN_TURN_US); + } else if (mgi_mesh_b_control_send(b, 0xffffu, MGI_JOIN_DOWN)) { + data->phase = MGI_TURN_JOIN; + data->deadline = now + MGI_JOIN_WINDOW_US; + } + if (data->deadline <= now) data->deadline = now + MGI_JOIN_TURN_US; + } +} + +static void mgi_mesh_b_bridge_free(struct mgi_mesh_b_data *data) { + while (data->members != NULL) { + struct mgi_mesh_b_peer *peer = data->members; + data->members = peer->next; + free(peer); + } +} + +static void mgi_mesh_b_node_rx(struct mg_mesh_node *n, uint64_t now, + uint8_t *frame, size_t len) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) n->data; + uint16_t dst = 0, src = 0; + unsigned command = mgi_mesh_b_frame_parse(frame, len, &dst, &src); + if (command == MGI_JOIN_DOWN && len == MG_MESH_FRAME_HDR_LEN && + dst == 0xffffu) { + if (n->joined && src == data->coordinator && + ++data->missed_rounds >= MGI_MAX_MISSES) { + n->joined = false; + data->phase = MGI_TURN_IDLE; + data->tx_at = (uint64_t) -1; + data->tx_offset = 0; + data->rx_expected = 0xff; + } + if (!n->joined && (mgi_mesh_b_random(n, data) % 64) == 0) { + // Spread contenders across windows as well as within the reply window. + data->coordinator = src; + data->phase = MGI_TURN_JOIN; + data->tx_at = now + MGI_JOIN_TURN_US + + mgi_mesh_b_random(n, data) % MGI_JOIN_JITTER_US; + } + } else if (command == MGI_DATA_DOWN && data->coordinator != 0 && + dst == n->radio_address && src == data->coordinator) { + int received = mgi_mesh_b_data_receive(data, frame, len, now); + if (received < 0) return; + n->joined = true; + data->missed_rounds = + -1; // This round had a grant; do not count it as missed. + if (received != 0) { + struct mg_tcpip_if *ifp = n->radio; + if (n->rx_len != 0 || (size_t) received > ifp->recv_queue.size) { + ifp->ndrop++; + } else { + memcpy(ifp->recv_queue.buf, data->rx_buf, (size_t) received); + n->rx_len = (size_t) received; + } + } + if (data->tx_at != (uint64_t) -1) mgi_mesh_b_tx_finish(data); + mgi_mesh_b_tx_begin(data, mg_queue_next(&data->tx, NULL) != 0, now, + now + mgi_mesh_b_burst_time(MGI_ETH_MAX) + + 2 * s_mesh_default_radio->ifs_us + + MGI_JOIN_TURN_US); + } +} + +static void mgi_mesh_b_input(struct mg_mesh_node *node, void *buf, size_t len) { + uint64_t ready = node->now + node->strategy->radio->turnaround_us; + if (node->tx_at < ready) node->tx_at = ready; + node->next_tick = node->now; + if (node->lan != NULL) + mgi_mesh_b_bridge_rx(node->now, node, (const uint8_t *) buf, len); + else + mgi_mesh_b_node_rx(node, node->now, (uint8_t *) buf, len); +} + +static void mgi_mesh_b_tick(struct mg_mesh_node *node, uint64_t now) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) node->data; + node->now = now; + if (node->lan != NULL) { + mgi_mesh_b_bridge_io(now, node); + mgi_mesh_b_tx_step(data, now); + node->next_tick = data->deadline; + if (data->tx_at < node->next_tick) node->next_tick = data->tx_at; + return; + } + if (data->phase == MGI_TURN_JOIN) { + if (data->tx_at <= now) { + if (now - data->tx_at <= MGI_JOIN_TURN_US && !node->joined) + mgi_mesh_b_control_send(node, data->coordinator, MGI_JOIN_UP); + data->tx_at = (uint64_t) -1; + data->phase = MGI_TURN_IDLE; + } + } else { + mgi_mesh_b_tx_step(data, now); + } + node->next_tick = data->tx_at; +} + +static bool mgi_mesh_b_init(struct mg_mesh_node *node, + struct mg_tcpip_if *radio, struct mg_tcpip_if *lan, + uint16_t radio_address) { + struct mgi_mesh_b_data *data = + (struct mgi_mesh_b_data *) calloc(1, sizeof(*data)); + memset(node, 0, sizeof(*node)); + node->radio = radio; + node->lan = lan; + node->radio_address = radio_address; + node->next_tick = (uint64_t) -1; + node->ram_used = node->ram_peak = sizeof(*node); + if (data == NULL) return false; + node->ram_used += sizeof(*data); + node->ram_peak = node->ram_used; + data->rx_expected = 0xff; + node->data = data; + data->node = node; + data->tx_at = (uint64_t) -1; + mg_queue_init(&data->tx, data->tx_buf, sizeof(data->tx_buf)); + if (lan != NULL) node->next_tick = 0; + return true; +} + +static void mgi_mesh_b_free(struct mg_mesh_node *node) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) node->data; + if (data == NULL) return; + mgi_mesh_b_bridge_free(data); + free(data); + node->data = NULL; +} + +const struct mg_mesh mg_mesh_default = { + "default", + mgi_mesh_b_init, + mgi_mesh_b_tick, + mgi_mesh_b_free, + mgi_mesh_b_enqueue, + mgi_mesh_b_input, + true, + true, + &mg_radio_802154, +}; + +#endif + #ifdef MG_ENABLE_LINES #line 1 "src/l2_ppp.c" #endif @@ -8904,8 +9463,14 @@ void mg_multicast_restore(struct mg_connection *c, uint8_t *from) { #define MG_TCPIP_KEEPALIVE_MS 45000 // TCP keep-alive period, ms #endif +#ifndef MG_TCPIP_ACK_MS #define MG_TCPIP_ACK_MS 150 // Timeout for ACKing +#endif + +#ifndef MG_TCPIP_ARP_MS #define MG_TCPIP_ARP_MS 100 // Timeout for ARP response +#endif + #define MG_TCPIP_SYN_MS 15000 // Timeout for connection establishment #define MG_TCPIP_FIN_MS 1000 // Timeout for closing connection #define MG_TCPIP_RTX_MS 1000 // Initial retransmission timeout (RFC-6298, 2.1) @@ -31938,6 +32503,8 @@ struct mg_tcpip_driver mg_tcpip_driver_imxrt = {mg_tcpip_driver_imxrt_init, #endif + + #if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_MCXW71) && \ MG_ENABLE_DRIVER_MCXW71 @@ -31947,12 +32514,12 @@ struct mg_zll { volatile uint32_t IRQSTS, PHY_CTRL, EVENT_TMR, TIMESTAMP, T1CMP, T2CMP, T2PRIMECMP, T3CMP, T4CMP, PA_PWR, CHANNEL_NUM0, LQI_AND_RSSI, MACSHORTADDRS0, MACLONGADDRS0_LSB, MACLONGADDRS0_MSB, RX_FRAME_FILTER, - CCA_LQI_CTRL, CCA2_CTRL, RESERVED_0, DSM_CTRL, RESERVED_1, - MACSHORTADDRS1, MACLONGADDRS1_LSB, MACLONGADDRS1_MSB, DUAL_PAN_CTRL, - CHANNEL_NUM1, SAM_CTRL, SAM_TABLE, SAM_MATCH, SAM_FREE_IDX, - SEQ_CTRL_STS, ACKDELAY, FILTERFAIL_CODE, RX_WTR_MARK, RESERVED_2, - SLOT_PRELOAD, SEQ_STATE, TMR_PRESCALE, LENIENCY_LSB, LENIENCY_MSB, - PART_ID, COEX_CTRL, COEX_PRIORITY, ENHACK_CTRL0; + CCA_LQI_CTRL, CCA2_CTRL, RESERVED_0, DSM_CTRL, RESERVED_1, MACSHORTADDRS1, + MACLONGADDRS1_LSB, MACLONGADDRS1_MSB, DUAL_PAN_CTRL, CHANNEL_NUM1, + SAM_CTRL, SAM_TABLE, SAM_MATCH, SAM_FREE_IDX, SEQ_CTRL_STS, ACKDELAY, + FILTERFAIL_CODE, RX_WTR_MARK, RESERVED_2, SLOT_PRELOAD, SEQ_STATE, + TMR_PRESCALE, LENIENCY_LSB, LENIENCY_MSB, PART_ID, COEX_CTRL, + COEX_PRIORITY, ENHACK_CTRL0; }; // TrustZone: code built for the secure state (-mcmse) uses the secure alias @@ -31970,113 +32537,392 @@ struct mg_zll { #define MGI_IRQSTS_SEQIRQ MG_BIT(0) #define MGI_IRQSTS_RXIRQ MG_BIT(2) +#define MGI_IRQSTS_FILTERFAIL MG_BIT(5) // Frame not for us +#define MGI_IRQSTS_CRCVALID MG_BIT(15) #define MGI_IRQSTS_RX_LEN(x) (((x) >> 24) & 0x7f) // Frame length incl. FCS #define MGI_PHY_CTRL_XCVSEQ 7U #define MGI_PHY_CTRL_ALL_IRQ_MASKS 0x8007ff00U // Bits 8..18, 31: mask all IRQs +#define MGI_PHY_CTRL_SEQMSK MG_BIT(8) // Mask the sequence end IRQ +#define MGI_PHY_CTRL_TRCV_MSK MG_BIT(31) // Mask all transceiver IRQs +#define MGI_IRQN 53 // RF_802_15_4_IRQn +#define MGI_NVIC_ISER ((volatile uint32_t *) 0xE000E100) // Enable IRQ +#define MGI_NVIC_ICER ((volatile uint32_t *) 0xE000E180) // Disable IRQ #define MGI_SEQ_STATE_MASK 0x1fU -#define MGI_SEQ_STATE_RX_WU 0x10U // RX warm up +#define MGI_SEQ_STATE_RX_WU 0x10U // RX warm up #define MGI_SEQ_CTRL_NO_RX_RECYCLE MG_BIT(5) #define MGI_FRAME_FILTER 0x70bU // Beacon, data, command; versions 0..2 -enum { MGI_SEQ_IDLE = 0, MGI_SEQ_RX = 1, MGI_SEQ_TX = 2 }; // PHY_CTRL[XCVSEQ] -#define MGI_TX_TIMEOUT 400U // Symbols, 16 us: 127 bytes +// PHY_CTRL[XCVSEQ] +enum { MGI_SEQ_IDLE = 0, MGI_SEQ_RX = 1, MGI_SEQ_TX = 2 }; +#define MGI_SEND_TIMEOUT_MS 60 -// Symbol timer, 16 us, 24 bits -static uint32_t mg_mcxw71_symbols(void) { - return MGI_ZLL->EVENT_TMR >> 8; -} - -// Wait for the sequence to end, up to MGI_TX_TIMEOUT symbols -static bool mg_mcxw71_wait(void) { - uint32_t start = mg_mcxw71_symbols(); - while (((mg_mcxw71_symbols() - start) & 0xffffffU) < MGI_TX_TIMEOUT) { - if (MGI_ZLL->IRQSTS & MGI_IRQSTS_SEQIRQ) return true; - } - return false; -} +static struct mg_tcpip_if *s_mcxw71_ifp; +static struct mg_queue s_mcxw71_rx; +static char s_mcxw71_queue[2048]; +static volatile bool s_mcxw71_txing, s_mcxw71_done; +static uint64_t s_mcxw71_sendstart; // Stop the current sequence, wait for the sequencer to go idle. See PhyAbort() -static void mg_mcxw71_idle(void) { +static void mg_mcxw71_abort(void) { if (MGI_ZLL->PHY_CTRL & MGI_PHY_CTRL_XCVSEQ) { while ((MGI_ZLL->SEQ_STATE & MGI_SEQ_STATE_MASK) == MGI_SEQ_STATE_RX_WU) (void) 0; MGI_ZLL->PHY_CTRL &= ~MGI_PHY_CTRL_XCVSEQ; while (MGI_ZLL->SEQ_STATE & MGI_SEQ_STATE_MASK) (void) 0; } - MGI_ZLL->IRQSTS = MGI_ZLL->IRQSTS; // Clear all flags, write 1 to clear -} - -// Start a new sequence: MGI_SEQ_RX or MGI_SEQ_TX -static void mg_mcxw71_start(uint32_t seq) { MGI_ZLL->IRQSTS = MGI_ZLL->IRQSTS; - MGI_ZLL->PHY_CTRL |= seq; } -static bool mg_mcxw71_init(struct mg_tcpip_if *ifp) { - struct mg_l2_802154_data *d = - (struct mg_l2_802154_data *) ifp->driver_data; - if (d == NULL || d->channel < 11 || d->channel > 26) { - MG_ERROR(("Set driver_data, channel 11..26")); - return false; +// Start receiving, with the sequence end IRQ on, see RF_802_15_4_IRQHandler() +static void mg_mcxw71_listen(void) { + MGI_ZLL->IRQSTS = MGI_ZLL->IRQSTS; + MGI_ZLL->PHY_CTRL |= MGI_SEQ_RX; + MGI_ZLL->PHY_CTRL &= ~(MGI_PHY_CTRL_SEQMSK | MGI_PHY_CTRL_TRCV_MSK); + MGI_NVIC_ISER[MGI_IRQN / 32] = MG_BIT(MGI_IRQN % 32); +} + +// Keep the IRQ handler out while we change the radio. The NVIC disable is +// atomic, so PHY_CTRL updates here and there do not race +static void mg_mcxw71_lock(void) { + MGI_NVIC_ICER[MGI_IRQN / 32] = MG_BIT(MGI_IRQN % 32); + __asm volatile("dsb; isb" ::: "memory"); +} + +// Start the TX sequence for the loaded frame. Its end: see the IRQ handler +static void mg_mcxw71_go(void) { + MGI_ZLL->PHY_CTRL |= MGI_PHY_CTRL_TRCV_MSK | MGI_PHY_CTRL_SEQMSK; + mg_mcxw71_abort(); + MGI_ZLL->IRQSTS = MGI_ZLL->IRQSTS; + MGI_ZLL->PHY_CTRL |= MGI_SEQ_TX; + MGI_ZLL->PHY_CTRL &= ~(MGI_PHY_CTRL_SEQMSK | MGI_PHY_CTRL_TRCV_MSK); +} + +void RF_802_15_4_IRQHandler(void); +void RF_802_15_4_IRQHandler(void) { + uint32_t irqsts = MGI_ZLL->IRQSTS; + if ((irqsts & MGI_IRQSTS_SEQIRQ) != 0) { + mg_mcxw71_abort(); + if (s_mcxw71_txing) { + s_mcxw71_txing = false; + s_mcxw71_done = true; + } else if ((irqsts & (MGI_IRQSTS_RXIRQ | MGI_IRQSTS_FILTERFAIL | + MGI_IRQSTS_CRCVALID)) == + (MGI_IRQSTS_RXIRQ | MGI_IRQSTS_CRCVALID)) { + uint8_t frame[125]; + size_t i, n = MGI_IRQSTS_RX_LEN(irqsts); + n = n >= 2 ? n - 2 : 0; + if (n > 0 && n <= sizeof(frame)) { + for (i = 0; i < n; i++) frame[i] = MGI_RX_PB[i]; + { + char *dst; + if (mg_queue_book(&s_mcxw71_rx, &dst, n) >= n) { + memcpy(dst, frame, n); + mg_queue_add(&s_mcxw71_rx, n); + } else { + s_mcxw71_ifp->ndrop++; + } + } + } + } + mg_mcxw71_listen(); } +} + +static size_t mgi_mcxw71_radio_tx(struct mg_tcpip_if *ifp, const void *buf, + size_t len) { + size_t i; + (void) ifp; + if (len > 125 || s_mcxw71_txing) return 0; + mg_mcxw71_lock(); + MGI_TX_PB[0] = (uint8_t) (len + 2); // FCS is added by hardware + for (i = 0; i < len; i++) + MGI_TX_PB[i + 1] = ((const uint8_t *) buf)[i]; // 8-bit access only + s_mcxw71_done = false; + s_mcxw71_txing = true; + s_mcxw71_sendstart = mg_millis(); + mg_mcxw71_go(); + MGI_NVIC_ISER[MGI_IRQN / 32] = MG_BIT(MGI_IRQN % 32); + return len; +} + +static bool mgi_mcxw71_radio_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mcxw71_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + s_mcxw71_ifp = ifp; MGI_ZLL->SEQ_CTRL_STS |= MGI_SEQ_CTRL_NO_RX_RECYCLE; - MGI_ZLL->PHY_CTRL = MGI_PHY_CTRL_ALL_IRQ_MASKS; // Mask all IRQs, we poll + MGI_ZLL->PHY_CTRL = MGI_PHY_CTRL_ALL_IRQ_MASKS; MGI_ZLL->IRQSTS = MGI_ZLL->IRQSTS; MGI_ZLL->SAM_CTRL = 0; MGI_ZLL->RX_FRAME_FILTER = MGI_FRAME_FILTER; - MGI_ZLL->TMR_PRESCALE = 5; // 16 us timebase, one symbol - MGI_ZLL->CHANNEL_NUM0 = d->channel; - MGI_ZLL->MACSHORTADDRS0 = (0xfffeU << 16) | d->pan_id; // Filter by PAN ID - MGI_ZLL->PA_PWR = 0x3f; // PA slices, max output power - mg_mcxw71_start(MGI_SEQ_RX); + MGI_ZLL->CHANNEL_NUM0 = MG_802154_CHANNEL; + MGI_ZLL->MACSHORTADDRS0 = + ((uint32_t) node->radio_address << 16) | MG_802154_PAN_ID; + MGI_ZLL->PA_PWR = 0x3f; + mg_mcxw71_listen(); return true; } -// Send one radio frame: MAC header and payload, no FCS -static size_t mg_mcxw71_send(const uint8_t *p, size_t len, - struct mg_tcpip_if *ifp) { - size_t i; - bool ok; - mg_mcxw71_idle(); - MGI_TX_PB[0] = (uint8_t) (len + 2); // FCS is added by hardware - for (i = 0; i < len; i++) MGI_TX_PB[i + 1] = p[i]; // 8-bit access only - mg_mcxw71_start(MGI_SEQ_TX); - ok = mg_mcxw71_wait(); - mg_mcxw71_idle(); - mg_mcxw71_start(MGI_SEQ_RX); - (void) ifp; - return ok ? len : 0; +static size_t mgi_mcxw71_tx(const void *buf, size_t len, + struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mcxw71_data *data = ifp->driver_data; + return data->mesh->strategy->tx(data->mesh, buf, len); } -static size_t mg_mcxw71_tx(const void *buf, size_t len, - struct mg_tcpip_if *ifp) { - return mg_l2_802154_tx(ifp, buf, len, mg_mcxw71_send); +static size_t mgi_mcxw71_rx(void *buf, size_t cap, struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mcxw71_data *data = ifp->driver_data; + size_t len = data->mesh->rx_len; + if (len > cap) return 0; + if (len != 0 && buf != ifp->recv_queue.buf) + memcpy(buf, ifp->recv_queue.buf, len); + data->mesh->rx_len = 0; + return len; } -static size_t mg_mcxw71_rx(void *buf, size_t len, struct mg_tcpip_if *ifp) { - uint32_t irqsts = MGI_ZLL->IRQSTS; - uint8_t f[125]; - size_t i, n = 0; - if ((irqsts & MGI_IRQSTS_SEQIRQ) == 0) return 0; // Still receiving - if (irqsts & MGI_IRQSTS_RXIRQ) { - n = MGI_IRQSTS_RX_LEN(irqsts); - n = n >= 2 ? n - 2 : 0; // Strip FCS - if (n > sizeof(f)) n = sizeof(f); - for (i = 0; i < n; i++) f[i] = MGI_RX_PB[i]; +static bool mgi_mcxw71_poll(struct mg_tcpip_if *ifp, bool s1) { + struct mg_tcpip_driver_mcxw71_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + uint64_t now = mg_micros(); + char *buf; + size_t len; + (void) s1; + if (s_mcxw71_txing && + mg_millis() > s_mcxw71_sendstart + MGI_SEND_TIMEOUT_MS) { + mg_mcxw71_lock(); + if (s_mcxw71_txing) { + mg_mcxw71_abort(); + s_mcxw71_txing = false; + s_mcxw71_done = true; + mg_mcxw71_listen(); + } else { + MGI_NVIC_ISER[MGI_IRQN / 32] = MG_BIT(MGI_IRQN % 32); + } } - mg_mcxw71_idle(); - mg_mcxw71_start(MGI_SEQ_RX); - return mg_l2_802154_rx(ifp, f, n, buf, len); + if (s_mcxw71_done) { + s_mcxw71_done = false; + node->tx_at = now + node->strategy->radio->ifs_us; + } + node->now = now; + while (node->rx_len == 0 && (len = mg_queue_next(&s_mcxw71_rx, &buf)) != 0) { + node->strategy->rx(node, buf, len); + mg_queue_del(&s_mcxw71_rx, len); + } + node->strategy->tick(node, now); + return node->joined; } -static bool mg_mcxw71_poll(struct mg_tcpip_if *ifp, bool s1) { - (void) ifp, (void) s1; - return true; // Radio link is always up +static bool mgi_mcxw71_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mcxw71_data *data = ifp->driver_data; + const struct mg_mesh *strategy = data->strategy; + uint16_t address; + address = + (uint16_t) (2 + ((unsigned) ifp->mac[4] * 256 + ifp->mac[5]) % 65532u); + if (strategy == NULL) strategy = &mg_mesh_default; + data->mesh = calloc(1, sizeof(*data->mesh)); + if (data->mesh == NULL) return false; + if (!strategy->init(data->mesh, ifp, NULL, address)) { + free(data->mesh); + data->mesh = NULL; + return false; + } + data->mesh->strategy = strategy; + data->mesh->tx = mgi_mcxw71_radio_tx; + mg_queue_init(&s_mcxw71_rx, s_mcxw71_queue, sizeof(s_mcxw71_queue)); + if (mgi_mcxw71_radio_init(ifp)) return true; + strategy->free(data->mesh); + free(data->mesh); + data->mesh = NULL; + return false; } -struct mg_tcpip_driver mg_tcpip_driver_mcxw71 = {mg_mcxw71_init, mg_mcxw71_tx, - mg_mcxw71_rx, mg_mcxw71_poll}; +struct mg_tcpip_driver mg_tcpip_driver_mcxw71 = { + mgi_mcxw71_init, mgi_mcxw71_tx, mgi_mcxw71_rx, mgi_mcxw71_poll}; +#endif +#ifdef MG_ENABLE_LINES +#line 1 "src/drivers/mg24.c" +#endif + + + + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_MG24) && MG_ENABLE_DRIVER_MG24 + +#include +#include + +// RAIL delivers raw frames and transmit completion from interrupt context. +#define MG_MG24_TX_TIMEOUT_US 60000 + +static struct mg_tcpip_if *s_mg24_ifp; +static RAIL_Handle_t s_mg24_rail; +static struct mg_queue s_mg24_rx; +static char s_mg24_rxbuf[2048]; +static volatile bool s_mg24_txing, s_mg24_tx_done; +static RAIL_Time_t s_mg24_txstart; // Send start, us +static uint8_t s_mg24_txfifo[256] __attribute__((aligned(4))); // Power of 2 + +// Copy a received PSDU, stripping the PHR and any stored FCS. +static void mg_mg24_rx_frame(RAIL_Handle_t h) { + RAIL_RxPacketInfo_t info; + uint8_t f[128]; + size_t n; + char *dst; + if (RAIL_GetRxPacketInfo(h, RAIL_RX_PACKET_HANDLE_NEWEST, &info) == + RAIL_RX_PACKET_HANDLE_INVALID || + info.packetStatus != RAIL_RX_PACKET_READY_SUCCESS || + info.packetBytes <= 1 || info.packetBytes > sizeof(f)) { + return; + } + RAIL_CopyRxPacket(f, &info); + n = info.packetBytes - 1U; // Strip PHR + if (n == f[0]) n -= 2; // FCS stored, strip it + if (mg_queue_book(&s_mg24_rx, &dst, n) >= n) { + memcpy(dst, f + 1, n); + mg_queue_add(&s_mg24_rx, n); + } else { + s_mg24_ifp->ndrop++; + } +} + +// RAIL event callback, runs in interrupt context +static void mg_mg24_events(RAIL_Handle_t h, RAIL_Events_t events) { + if (events & RAIL_EVENT_CAL_NEEDED) { + RAIL_Calibrate(h, NULL, RAIL_CAL_ALL_PENDING); + } + if (events & RAIL_EVENT_RX_PACKET_RECEIVED) mg_mg24_rx_frame(h); + if (events & RAIL_EVENTS_TX_COMPLETION) s_mg24_tx_done = true; +} + +// RAIL adds the FCS. +static size_t mgi_mg24_radio_tx(struct mg_tcpip_if *ifp, const void *buf, + size_t len) { + uint8_t phr = (uint8_t) (len + 2); // FCS is added by hardware + (void) ifp; + if (len > 125 || s_mg24_txing) return 0; + RAIL_WriteTxFifo(s_mg24_rail, &phr, 1, true); // Reset FIFO, write PHR + RAIL_WriteTxFifo(s_mg24_rail, (const uint8_t *) buf, (uint16_t) len, false); + s_mg24_tx_done = false; + s_mg24_txing = true, s_mg24_txstart = RAIL_GetTime(); + if (RAIL_StartTx(s_mg24_rail, MG_802154_CHANNEL, RAIL_TX_OPTIONS_DEFAULT, + NULL) != RAIL_STATUS_NO_ERROR) { + s_mg24_txing = false; + return 0; + } + return len; +} + +static bool mgi_mg24_radio_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mg24_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + RAIL_Config_t cfg = {0}; + RAIL_IEEE802154_Config_t cfg154 = {0}; + RAIL_TxPowerConfig_t pwr; + RAIL_StateTransitions_t to_rx = {RAIL_RF_STATE_RX, RAIL_RF_STATE_RX}; + RAIL_Events_t events = RAIL_EVENT_CAL_NEEDED | RAIL_EVENT_RX_PACKET_RECEIVED | + RAIL_EVENTS_TX_COMPLETION; + cfg.eventsCallback = mg_mg24_events; + if ((s_mg24_rail = RAIL_Init(&cfg, NULL)) == NULL) { + MG_ERROR(("RAIL_Init failed")); + return false; + } + cfg154.ackConfig.enable = false; + cfg154.ackConfig.ackTimeout = 864; + cfg154.ackConfig.rxTransitions = to_rx; + cfg154.ackConfig.txTransitions = to_rx; + cfg154.timings.idleToRx = 100, cfg154.timings.idleToTx = 100; + cfg154.timings.rxToTx = 192, cfg154.timings.txToRx = 182; + cfg154.framesMask = RAIL_IEEE802154_ACCEPT_STANDARD_FRAMES; + cfg154.promiscuousMode = false; // Filter by PAN ID and our short address + pwr.mode = RAIL_TX_POWER_MODE_2P4GIG_HP, pwr.voltage = 3300; + pwr.rampTime = 10; + if (RAIL_ConfigCal(s_mg24_rail, RAIL_CAL_ALL) != RAIL_STATUS_NO_ERROR || + RAIL_ConfigEvents(s_mg24_rail, RAIL_EVENTS_ALL, events) != + RAIL_STATUS_NO_ERROR || + RAIL_IEEE802154_Config2p4GHzRadio(s_mg24_rail) != RAIL_STATUS_NO_ERROR || + RAIL_IEEE802154_Init(s_mg24_rail, &cfg154) != RAIL_STATUS_NO_ERROR || + RAIL_IEEE802154_SetPanId(s_mg24_rail, MG_802154_PAN_ID, 0) != + RAIL_STATUS_NO_ERROR || + RAIL_IEEE802154_SetShortAddress(s_mg24_rail, node->radio_address, 0) != + RAIL_STATUS_NO_ERROR || + RAIL_SetTxTransitions(s_mg24_rail, &to_rx) != RAIL_STATUS_NO_ERROR || + RAIL_SetRxTransitions(s_mg24_rail, &to_rx) != RAIL_STATUS_NO_ERROR || + RAIL_ConfigTxPower(s_mg24_rail, &pwr) != RAIL_STATUS_NO_ERROR || + RAIL_SetTxPower(s_mg24_rail, RAIL_TX_POWER_LEVEL_2P4_HP_MAX) != + RAIL_STATUS_NO_ERROR || + RAIL_SetTxFifo(s_mg24_rail, s_mg24_txfifo, 0, sizeof(s_mg24_txfifo)) != + sizeof(s_mg24_txfifo)) { + MG_ERROR(("RAIL setup failed")); + return false; + } + s_mg24_ifp = ifp; + return RAIL_StartRx(s_mg24_rail, MG_802154_CHANNEL, NULL) == + RAIL_STATUS_NO_ERROR; +} + +static size_t mgi_mg24_tx(const void *buf, size_t len, + struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mg24_data *data = ifp->driver_data; + return data->mesh->strategy->tx(data->mesh, buf, len); +} + +static size_t mgi_mg24_rx(void *buf, size_t cap, struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mg24_data *data = ifp->driver_data; + size_t len = data->mesh->rx_len; + if (len > cap) return 0; + if (len != 0 && buf != ifp->recv_queue.buf) + memcpy(buf, ifp->recv_queue.buf, len); + data->mesh->rx_len = 0; + return len; +} + +static bool mgi_mg24_poll(struct mg_tcpip_if *ifp, bool s1) { + struct mg_tcpip_driver_mg24_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + uint64_t now = mg_micros(); + char *buf; + size_t len; + (void) s1; + node->now = now; + if (s_mg24_txing && (s_mg24_tx_done || RAIL_GetTime() - s_mg24_txstart > + MG_MG24_TX_TIMEOUT_US)) { + if (!s_mg24_tx_done) { + RAIL_Idle(s_mg24_rail, RAIL_IDLE_ABORT, true); + RAIL_StartRx(s_mg24_rail, MG_802154_CHANNEL, NULL); + } + s_mg24_txing = false; + node->tx_at = now + node->strategy->radio->ifs_us; + } + while (node->rx_len == 0 && (len = mg_queue_next(&s_mg24_rx, &buf)) != 0) { + node->strategy->rx(node, buf, len); + mg_queue_del(&s_mg24_rx, len); + } + node->strategy->tick(node, now); + return node->joined; +} +static bool mgi_mg24_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mg24_data *data = ifp->driver_data; + const struct mg_mesh *strategy = data->strategy; + uint16_t address = + (uint16_t) (2 + ((unsigned) ifp->mac[4] * 256 + ifp->mac[5]) % 65532u); + if (strategy == NULL) strategy = &mg_mesh_default; + data->mesh = calloc(1, sizeof(*data->mesh)); + if (data->mesh == NULL) return false; + if (!strategy->init(data->mesh, ifp, NULL, address)) { + free(data->mesh); + data->mesh = NULL; + return false; + } + data->mesh->strategy = strategy; + data->mesh->tx = mgi_mg24_radio_tx; + mg_queue_init(&s_mg24_rx, s_mg24_rxbuf, sizeof(s_mg24_rxbuf)); + if (mgi_mg24_radio_init(ifp)) return true; + data->mesh->strategy->free(data->mesh); + free(data->mesh); + data->mesh = NULL; + return false; +} +struct mg_tcpip_driver mg_tcpip_driver_mg24 = {mgi_mg24_init, mgi_mg24_tx, + mgi_mg24_rx, mgi_mg24_poll}; #endif #ifdef MG_ENABLE_LINES @@ -32551,6 +33397,189 @@ struct mg_tcpip_driver mg_tcpip_driver_netc = {mg_tcpip_driver_netc_init, #endif +#ifdef MG_ENABLE_LINES +#line 1 "src/drivers/nrf52.c" +#endif + + + + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_NRF52) && MG_ENABLE_DRIVER_NRF52 +#include + +static struct mg_tcpip_if *s_nrf52_ifp; +static struct mg_queue s_nrf52_rx; +static char s_nrf52_queue[2048]; +static uint8_t s_nrf52_rxbuf[128] __attribute__((aligned(4))); +static uint8_t s_nrf52_txbuf[128] __attribute__((aligned(4))); +static volatile bool s_nrf52_txing, s_nrf52_done; +static uint64_t s_nrf52_started; + +static void mgi_nrf52_listen(void) { + NRF_RADIO->PACKETPTR = (uint32_t) s_nrf52_rxbuf; + NRF_RADIO->SHORTS = + RADIO_SHORTS_READY_START_Msk | RADIO_SHORTS_END_DISABLE_Msk; + NRF_RADIO->EVENTS_FRAMESTART = 0; + NRF_RADIO->EVENTS_END = 0; + NRF_RADIO->TASKS_RXEN = 1; +} +static void mgi_nrf52_disable(void) { + NRF_RADIO->SHORTS = 0; + NRF_RADIO->EVENTS_DISABLED = 0; + NRF_RADIO->TASKS_DISABLE = 1; + while (!NRF_RADIO->EVENTS_DISABLED) (void) 0; + NRF_RADIO->EVENTS_DISABLED = 0; +} +void RADIO_IRQHandler(void); +void RADIO_IRQHandler(void) { + if (!NRF_RADIO->EVENTS_DISABLED) return; + NRF_RADIO->EVENTS_DISABLED = 0; + if (s_nrf52_txing) { + s_nrf52_txing = false; + s_nrf52_done = true; + } else if (NRF_RADIO->CRCSTATUS) { + struct mg_tcpip_driver_nrf52_data *data = s_nrf52_ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + size_t len = s_nrf52_rxbuf[0] >= 2 ? s_nrf52_rxbuf[0] - 2U : 0; + uint8_t *frame = s_nrf52_rxbuf + 1; + uint16_t dst, src; + if (len <= 125 && + mg_radio_802154.parse_hdr(&mg_radio_802154, frame, len, &dst, &src) != + 0 && + (dst == 0xffffu || dst == node->radio_address)) { + char *dst; + if (mg_queue_book(&s_nrf52_rx, &dst, len) >= len) { + memcpy(dst, frame, len); + mg_queue_add(&s_nrf52_rx, len); + } else { + s_nrf52_ifp->ndrop++; + } + } + } + mgi_nrf52_listen(); +} +static size_t mgi_nrf52_radio_tx(struct mg_tcpip_if *ifp, const void *buf, + size_t len) { + (void) ifp; + if (len > 125) return 0; + NVIC_DisableIRQ(RADIO_IRQn); + if (s_nrf52_txing || NRF_RADIO->EVENTS_FRAMESTART) { + NVIC_EnableIRQ(RADIO_IRQn); + return 0; + } + mgi_nrf52_disable(); + s_nrf52_txbuf[0] = (uint8_t) (len + 2); + memcpy(s_nrf52_txbuf + 1, buf, len); + s_nrf52_done = false; + s_nrf52_txing = true; + s_nrf52_started = mg_micros(); + NRF_RADIO->PACKETPTR = (uint32_t) s_nrf52_txbuf; + NRF_RADIO->SHORTS = + RADIO_SHORTS_READY_START_Msk | RADIO_SHORTS_END_DISABLE_Msk; + NRF_RADIO->TASKS_TXEN = 1; + NVIC_EnableIRQ(RADIO_IRQn); + return len; +} +static bool mgi_nrf52_radio_init(struct mg_tcpip_if *ifp) { + s_nrf52_ifp = ifp; + NRF_RADIO->POWER = 1; + NRF_RADIO->MODE = RADIO_MODE_MODE_Ieee802154_250Kbit; + NRF_RADIO->PCNF0 = (8UL << RADIO_PCNF0_LFLEN_Pos) | + (RADIO_PCNF0_PLEN_32bitZero << RADIO_PCNF0_PLEN_Pos) | + RADIO_PCNF0_CRCINC_Msk; + NRF_RADIO->PCNF1 = 127; + NRF_RADIO->CRCCNF = 0x202; + NRF_RADIO->CRCPOLY = 0x11021; + NRF_RADIO->CRCINIT = 0; + NRF_RADIO->MODECNF0 = (RADIO_MODECNF0_DTX_Center << RADIO_MODECNF0_DTX_Pos) | + RADIO_MODECNF0_RU_Fast; + NRF_RADIO->SFD = 0xa7; + NRF_RADIO->FREQUENCY = 5 * (MG_802154_CHANNEL - 10); + NRF_RADIO->TXPOWER = RADIO_TXPOWER_TXPOWER_Pos8dBm; + // Correlator threshold 45 matches the RADIO reset value. A threshold of + // 20 caused false busy detections and persistent L2 retries. + NRF_RADIO->CCACTRL = + (RADIO_CCACTRL_CCAMODE_CarrierMode << RADIO_CCACTRL_CCAMODE_Pos) | + (2UL << RADIO_CCACTRL_CCACORRCNT_Pos) | + (45UL << RADIO_CCACTRL_CCACORRTHRES_Pos); + NRF_RADIO->INTENSET = RADIO_INTENSET_DISABLED_Msk; + NVIC_SetPriority(RADIO_IRQn, 0); + NVIC_EnableIRQ(RADIO_IRQn); + mgi_nrf52_listen(); + return true; +} +static size_t mgi_nrf52_tx(const void *buf, size_t len, + struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_nrf52_data *data = ifp->driver_data; + return data->mesh->strategy->tx(data->mesh, buf, len); +} + +static size_t mgi_nrf52_rx(void *buf, size_t cap, struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_nrf52_data *data = ifp->driver_data; + size_t len = data->mesh->rx_len; + if (len > cap) return 0; + if (len != 0 && buf != ifp->recv_queue.buf) + memcpy(buf, ifp->recv_queue.buf, len); + data->mesh->rx_len = 0; + return len; +} + +static bool mgi_nrf52_poll(struct mg_tcpip_if *ifp, bool s1) { + struct mg_tcpip_driver_nrf52_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + uint64_t now = mg_micros(); + char *buf; + size_t len; + (void) s1; + if (s_nrf52_txing && mg_micros() - s_nrf52_started > 60000) { + NVIC_DisableIRQ(RADIO_IRQn); + mgi_nrf52_disable(); + s_nrf52_txing = false; + s_nrf52_done = true; + mgi_nrf52_listen(); + NVIC_EnableIRQ(RADIO_IRQn); + } + if (s_nrf52_done) { + s_nrf52_done = false; + node->tx_at = now + node->strategy->radio->ifs_us; + } + node->now = now; + while (node->rx_len == 0 && (len = mg_queue_next(&s_nrf52_rx, &buf)) != 0) { + node->strategy->rx(node, buf, len); + mg_queue_del(&s_nrf52_rx, len); + } + node->strategy->tick(node, now); + return node->joined; +} + +static bool mgi_nrf52_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_nrf52_data *data = ifp->driver_data; + const struct mg_mesh *strategy = data->strategy; + uint16_t address; + address = + (uint16_t) (2 + ((unsigned) ifp->mac[4] * 256 + ifp->mac[5]) % 65532u); + if (strategy == NULL) strategy = &mg_mesh_default; + data->mesh = calloc(1, sizeof(*data->mesh)); + if (data->mesh == NULL) return false; + if (!strategy->init(data->mesh, ifp, NULL, address)) { + free(data->mesh); + data->mesh = NULL; + return false; + } + data->mesh->strategy = strategy; + data->mesh->tx = mgi_nrf52_radio_tx; + mg_queue_init(&s_nrf52_rx, s_nrf52_queue, sizeof(s_nrf52_queue)); + if (mgi_nrf52_radio_init(ifp)) return true; + strategy->free(data->mesh); + free(data->mesh); + data->mesh = NULL; + return false; +} + +struct mg_tcpip_driver mg_tcpip_driver_nrf52 = {mgi_nrf52_init, mgi_nrf52_tx, + mgi_nrf52_rx, mgi_nrf52_poll}; +#endif + #ifdef MG_ENABLE_LINES #line 1 "src/drivers/nxp_wifi.c" #endif @@ -33511,12 +34540,21 @@ struct mg_tcpip_driver mg_tcpip_driver_rw612 = {mg_tcpip_driver_rw612_init, #endif #ifdef MG_ENABLE_LINES -#line 1 "src/drivers/rw612_154.c" +#line 1 "src/drivers/rw612w.c" #endif -#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_RW612_154) && \ - MG_ENABLE_DRIVER_RW612_154 + + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_RW612W) && \ + MG_ENABLE_DRIVER_RW612W + + + +#ifdef ENET +#undef ENET +#endif +#include // The 802.15.4 radio runs NXP combo firmware on CPU2, an OpenThread RCP. We // are CPU3. Messages go through the IMU mailbox: IMU messages carry IMUMC @@ -33531,13 +34569,13 @@ struct mg_rw612_imu { }; #define MGI_IMU ((struct mg_rw612_imu *) 0x443ff804) // CPU3 -> CPU2 #define MGI_IMU_PEER ((struct mg_rw612_imu *) 0x443ff818) // CPU2 -> CPU3 -#define MGI_IMU_LOCKED MG_BIT(0) // STATUS: FIFO locked -#define MGI_IMU_ALMOST_FULL MG_BIT(1) // STATUS: FIFO almost full -#define MGI_IMU_EMPTY MG_BIT(3) // STATUS: FIFO empty +#define MGI_IMU_LOCKED MG_BIT(0) // STATUS: FIFO locked +#define MGI_IMU_ALMOST_FULL MG_BIT(1) // STATUS: FIFO almost full +#define MGI_IMU_EMPTY MG_BIT(3) // STATUS: FIFO empty #define MGI_IMU_WR_PTR(s) (((s) >> 16) & 15U) -#define MGI_IMU_INT_CLR MG_BIT(0) // CNTL: clear message ready IRQ -#define MGI_IMU_FLUSH MG_BIT(16) // CNTL: flush send FIFO -#define MGI_IMU_WAIT_FOR_ACK MG_BIT(17) // CNTL: lock send FIFO +#define MGI_IMU_INT_CLR MG_BIT(0) // CNTL: clear message ready IRQ +#define MGI_IMU_FLUSH MG_BIT(16) // CNTL: flush send FIFO +#define MGI_IMU_WAIT_FOR_ACK MG_BIT(17) // CNTL: lock send FIFO #define MGI_IMU_WATERMARK(x) ((uint32_t) (x) << 20) #define MGI_IMU_WATERMARK_MASK (15U << 20) @@ -33588,8 +34626,10 @@ enum { MGI_PROP_NCP_VERSION = 2, MGI_PROP_PHY_ENABLED = 0x20, MGI_PROP_PHY_CHAN = 0x21, + MGI_PROP_SADDR = 0x35, // Short address, little endian MGI_PROP_PANID = 0x36, MGI_PROP_RAW_STREAM = 0x37, + MGI_PROP_PROMISCUOUS = 0x38, MGI_PROP_STREAM_RAW = 0x71 }; #define MGI_RESET_STACK 2 // RESET argument: reset the RCP stack only @@ -33597,7 +34637,6 @@ enum { #define MGI_STATUS_RESET_MAX 127 #define MGI_TX_TIMEOUT_MS 200 // Wait for transmit done, incl. retries #define MGI_RSP_TIMEOUT_MS 500 -#define MGI_RXQ_CNT 16 // A whole Ethernet frame, 15 radio frames // Driver state, one radio struct mg_rw612 { @@ -33613,11 +34652,13 @@ struct mg_rw612 { bool rsp; // Response to the pending request received uint32_t rsp_status; // Its status, if LAST_STATUS, else 0 char version[64]; // RCP version string - uint8_t rxq[MGI_RXQ_CNT][127]; // Received frames, no FCS - uint8_t rxlen[MGI_RXQ_CNT]; // Their lengths - uint8_t rxhead, rxtail; // Queue indexes + bool txing; // A send is on, see mg_rw612_start() + uint64_t txstart; // Its start, ms + struct mg_tcpip_if *ifp; // Our interface, for the L2 receive pool }; static struct mg_rw612 s_rw612; +static struct mg_queue s_rw612w_rx; +static char s_rw612w_queue[2048]; static void mg_rw612_imu_send(uint8_t type, uint8_t subtype, const uint32_t *payload, uint8_t len) { @@ -33641,12 +34682,15 @@ static void mg_rw612_control(uint8_t subtype, uint32_t a, uint32_t b, } static void mg_rw612_queue_rx(const uint8_t *frame, size_t len) { - struct mg_rw612 *s = &s_rw612; - uint8_t next = (uint8_t) ((s->rxhead + 1) % MGI_RXQ_CNT); - if (next == s->rxtail || len > sizeof(s->rxq[0])) return; // Full: drop - memcpy(s->rxq[s->rxhead], frame, len); - s->rxlen[s->rxhead] = (uint8_t) len; - s->rxhead = next; + { + char *dst; + if (mg_queue_book(&s_rw612w_rx, &dst, len) >= len) { + memcpy(dst, frame, len); + mg_queue_add(&s_rw612w_rx, len); + } else { + s_rw612.ifp->ndrop++; + } + } } // Spinel packed unsigned integer: 7 bits per byte, LSB first @@ -33808,14 +34852,13 @@ static bool mg_rw612_imumc_send(const uint8_t *p, size_t len) { return true; } -// Send a Spinel frame, HDLC encoded, and wait for the response with the -// same transaction ID. Return false on timeout -static bool mg_rw612_request(uint8_t *f, size_t len, uint32_t timeout_ms) { +// Send a Spinel frame, HDLC encoded, with a new transaction ID. Its +// response sets rsp, see mg_rw612_spinel() +static bool mg_rw612_post(uint8_t *f, size_t len) { struct mg_rw612 *s = &s_rw612; uint8_t buf[2 * 160 + 6]; uint16_t fcs = 0xffff; size_t i, n = 0; - uint64_t expire = mg_millis() + timeout_ms; s->tid = (uint8_t) (s->tid % 15 + 1); f[0] = (uint8_t) (0x80 | s->tid); // Header: flag, interface 0, TID s->rsp = false; @@ -33833,9 +34876,15 @@ static bool mg_rw612_request(uint8_t *f, size_t len, uint32_t timeout_ms) { buf[n++] = c; } buf[n++] = 0x7e; - if (!mg_rw612_imumc_send(buf, n)) return false; - while (!s->rsp && mg_millis() < expire) mg_rw612_poll_imu(); - return s->rsp; + return mg_rw612_imumc_send(buf, n); +} + +// Send a Spinel frame and wait for its response. Return false on timeout +static bool mg_rw612_request(uint8_t *f, size_t len, uint32_t timeout_ms) { + uint64_t expire = mg_millis() + timeout_ms; + if (!mg_rw612_post(f, len)) return false; + while (!s_rw612.rsp && mg_millis() < expire) mg_rw612_poll_imu(); + return s_rw612.rsp; } static bool mg_rw612_set(uint32_t prop, const uint8_t *val, size_t len) { @@ -33853,27 +34902,43 @@ static bool mg_rw612_fail(const char *msg) { return false; } -static bool mg_rw612_154_init(struct mg_tcpip_if *ifp) { +// Start one raw transmission. The RCP adds the FCS and reports completion. +static size_t mgi_rw612w_radio_tx(struct mg_tcpip_if *ifp, const void *buf, + size_t len) { + uint8_t f[3 + 2 + 127 + 4]; + size_t n = 3; + if (len > 125 || s_rw612.txing) return 0; + f[1] = MGI_CMD_SET, f[2] = MGI_PROP_STREAM_RAW; + f[n++] = (uint8_t) (len + 2), f[n++] = 0; // PSDU length, incl. FCS + memcpy(f + n, buf, len), n += len; + f[n++] = 0, f[n++] = 0; // FCS, filled in by the radio + (void) ifp; + f[n++] = MG_802154_CHANNEL; + f[n++] = 0; // Max CSMA backoffs + f[n++] = 0; // Max frame retries + f[n++] = 0; // CSMA disabled for scheduled turns + if (!mg_rw612_post(f, n)) return 0; + s_rw612.txing = true, s_rw612.txstart = mg_millis(); + return len; +} + +static bool mgi_rw612w_radio_init(struct mg_tcpip_if *ifp) { struct mg_rw612 *s = &s_rw612; - struct mg_l2_802154_data *d = - (struct mg_l2_802154_data *) ifp->driver_data; - uint8_t f[8], one = 1, pan[2]; + struct mg_tcpip_driver_rw612w_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + uint8_t f[8], one = 1, zero = 0, pan[2], addr[2], channel = MG_802154_CHANNEL; uint64_t expire; - if (d == NULL || d->channel < 11 || d->channel > 26) { - MG_ERROR(("Set driver_data, channel 11..26")); - return false; - } if (MGI_MBOX->magic[0] != MGI_SYNC_MAGIC) { MG_ERROR(("Radio firmware is not running on CPU2")); return false; } memset(s, 0, sizeof(*s)); - s->free_bufs = 0xffff; + s->free_bufs = 0xffff, s->ifp = ifp; // Reset our send FIFO, drop stale messages. See IMU_Init() MGI_IMU->CNTL |= MGI_IMU_FLUSH; MGI_IMU->CNTL &= ~(MGI_IMU_FLUSH | MGI_IMU_WAIT_FOR_ACK); - MGI_IMU->CNTL = (MGI_IMU->CNTL & ~MGI_IMU_WATERMARK_MASK) | - MGI_IMU_WATERMARK(15); + MGI_IMU->CNTL = + (MGI_IMU->CNTL & ~MGI_IMU_WATERMARK_MASK) | MGI_IMU_WATERMARK(15); while ((MGI_IMU_PEER->STATUS & MGI_IMU_EMPTY) == 0) (void) MGI_IMU->RD_MSG; // Synchronise the IMU link, then connect the IMUMC endpoints MGI_MBOX->magic[1] = MGI_SYNC_MAGIC; @@ -33900,10 +34965,15 @@ static bool mg_rw612_154_init(struct mg_tcpip_if *ifp) { if (!mg_rw612_set(MGI_PROP_PHY_ENABLED, &one, 1)) { return mg_rw612_fail("PHY enable failed"); } - pan[0] = (uint8_t) d->pan_id, pan[1] = (uint8_t) (d->pan_id >> 8); - if (!mg_rw612_set(MGI_PROP_PHY_CHAN, &d->channel, 1) || - !mg_rw612_set(MGI_PROP_PANID, pan, 2)) { - return mg_rw612_fail("Channel or PAN ID set failed"); + pan[0] = (uint8_t) MG_802154_PAN_ID; + pan[1] = (uint8_t) (MG_802154_PAN_ID >> 8); + addr[0] = (uint8_t) node->radio_address; + addr[1] = (uint8_t) (node->radio_address >> 8); + if (!mg_rw612_set(MGI_PROP_PHY_CHAN, &channel, 1) || + !mg_rw612_set(MGI_PROP_PANID, pan, 2) || + !mg_rw612_set(MGI_PROP_SADDR, addr, 2) || + !mg_rw612_set(MGI_PROP_PROMISCUOUS, &zero, 1)) { + return mg_rw612_fail("Channel, PAN ID or address set failed"); } if (!mg_rw612_set(MGI_PROP_RAW_STREAM, &one, 1)) { return mg_rw612_fail("Raw stream enable failed"); @@ -33911,57 +34981,79 @@ static bool mg_rw612_154_init(struct mg_tcpip_if *ifp) { return true; } -// Send one radio frame: MAC header and payload, no FCS -static size_t mg_rw612_send(const uint8_t *p, size_t len, +static size_t mgi_rw612w_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) { - struct mg_l2_802154_data *d = - (struct mg_l2_802154_data *) ifp->driver_data; - uint8_t f[3 + 2 + 127 + 4]; - size_t n = 3; - f[1] = MGI_CMD_SET, f[2] = MGI_PROP_STREAM_RAW; - f[n++] = (uint8_t) (len + 2), f[n++] = 0; // PSDU length, incl. FCS - memcpy(f + n, p, len), n += len; - f[n++] = 0, f[n++] = 0; // FCS, filled in by the radio - f[n++] = d->channel; - f[n++] = 4; // Max CSMA backoffs - f[n++] = 0; // Max frame retries: broadcast frames are not acknowledged - f[n++] = 1; // CSMA-CA enabled - if (!mg_rw612_request(f, n, MGI_TX_TIMEOUT_MS)) { - MG_ERROR(("TX timeout")); - return 0; - } - if (s_rw612.rsp_status != 0) { - MG_VERBOSE(("TX failed, status %lu", (unsigned long) s_rw612.rsp_status)); - return 0; - } + struct mg_tcpip_driver_rw612w_data *data = ifp->driver_data; + return data->mesh->strategy->tx(data->mesh, buf, len); +} + +static size_t mgi_rw612w_rx(void *buf, size_t cap, struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_rw612w_data *data = ifp->driver_data; + size_t len = data->mesh->rx_len; + if (len > cap) return 0; + if (len != 0 && buf != ifp->recv_queue.buf) + memcpy(buf, ifp->recv_queue.buf, len); + data->mesh->rx_len = 0; return len; } -static size_t mg_rw612_154_tx(const void *buf, size_t len, - struct mg_tcpip_if *ifp) { - return mg_l2_802154_tx(ifp, buf, len, mg_rw612_send); -} - -static size_t mg_rw612_154_rx(void *buf, size_t len, struct mg_tcpip_if *ifp) { - struct mg_rw612 *s = &s_rw612; - size_t res = 0; +static bool mgi_rw612w_poll(struct mg_tcpip_if *ifp, bool s1) { + struct mg_tcpip_driver_rw612w_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + uint64_t now = mg_micros(); + char *buf; + size_t len; + (void) s1; + bool was_sending = s_rw612.txing; mg_rw612_poll_imu(); - while (s->rxtail != s->rxhead && res == 0) { - res = mg_l2_802154_rx(ifp, s->rxq[s->rxtail], s->rxlen[s->rxtail], buf, - len); - s->rxtail = (uint8_t) ((s->rxtail + 1) % MGI_RXQ_CNT); + if (s_rw612.txing && + (s_rw612.rsp || mg_millis() > s_rw612.txstart + MGI_TX_TIMEOUT_MS)) { + if (!s_rw612.rsp) MG_ERROR(("TX timeout")); + s_rw612.txing = false; } - return res; + if (was_sending && !s_rw612.txing) + node->tx_at = now + node->strategy->radio->ifs_us; + node->now = now; + while (node->rx_len == 0 && (len = mg_queue_next(&s_rw612w_rx, &buf)) != 0) { + node->strategy->rx(node, buf, len); + mg_queue_del(&s_rw612w_rx, len); + } + node->strategy->tick(node, now); + return node->joined; } -static bool mg_rw612_154_poll(struct mg_tcpip_if *ifp, bool s1) { - (void) ifp, (void) s1; - return s_rw612.up; +static bool mgi_rw612w_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_rw612w_data *data = ifp->driver_data; + const struct mg_mesh *strategy = data->strategy; + uint8_t uid[FSL_OCOTP_UID_LENGTH] = {0}, digest[32]; + uint32_t uid_len = sizeof(uid); + uint16_t address; + OCOTP_ReadUniqueID(uid, &uid_len); + mg_sha256(digest, uid, sizeof(uid)); + memcpy(ifp->mac, digest, 6); + ifp->mac[0] = 0x02; + address = + (uint16_t) (2 + ((unsigned) ifp->mac[4] * 256 + ifp->mac[5]) % 65532u); + if (strategy == NULL) strategy = &mg_mesh_default; + data->mesh = calloc(1, sizeof(*data->mesh)); + if (data->mesh == NULL) return false; + if (!strategy->init(data->mesh, ifp, NULL, address)) { + free(data->mesh); + data->mesh = NULL; + return false; + } + data->mesh->strategy = strategy; + data->mesh->tx = mgi_rw612w_radio_tx; + mg_queue_init(&s_rw612w_rx, s_rw612w_queue, sizeof(s_rw612w_queue)); + if (mgi_rw612w_radio_init(ifp)) return true; + strategy->free(data->mesh); + free(data->mesh); + data->mesh = NULL; + return false; } -struct mg_tcpip_driver mg_tcpip_driver_rw612_154 = { - mg_rw612_154_init, mg_rw612_154_tx, mg_rw612_154_rx, mg_rw612_154_poll}; - +struct mg_tcpip_driver mg_tcpip_driver_rw612w = { + mgi_rw612w_init, mgi_rw612w_tx, mgi_rw612w_rx, mgi_rw612w_poll}; #endif #ifdef MG_ENABLE_LINES diff --git a/mongoose.h b/mongoose.h index fb4a9292..0530b414 100644 --- a/mongoose.h +++ b/mongoose.h @@ -4750,41 +4750,11 @@ extern struct mg_tcpip_driver mg_tcpip_driver_nxp_wifi; extern struct mg_tcpip_driver mg_tcpip_driver_st67w6; extern struct mg_tcpip_driver mg_tcpip_driver_atcmd; extern struct mg_tcpip_driver mg_tcpip_driver_netc; -extern struct mg_tcpip_driver mg_tcpip_driver_mcxw71; -extern struct mg_tcpip_driver mg_tcpip_driver_rw612_154; -// IEEE 802.15.4 radio channel and PAN ID. Settable in mongoose_config.h -#ifndef MG_L2_802154_CHANNEL -#define MG_L2_802154_CHANNEL 15 // 11..26 -#endif -#ifndef MG_L2_802154_PAN_ID -#define MG_L2_802154_PAN_ID 0xabcd -#endif -// driver_data for IEEE 802.15.4 radio drivers. The interface is Ethernet: -// each Ethernet frame is split into broadcast radio frames, and reassembled -// on the other side, see mg_l2_802154_tx() and mg_l2_802154_rx() -struct mg_l2_802154_data { - uint8_t channel; // User-set channel, 11..26 - uint16_t pan_id; // User-set PAN ID - uint8_t seq, tag; // MAC sequence number, tag of the last sent frame - uint8_t rxtag, rxnext; // Frame being reassembled: tag, next fragment - uint8_t rxsrc[8]; // Its sender - uint16_t rxlen; // Its bytes received so far - uint8_t rxbuf[1514]; // Reassembly buffer, one Ethernet frame -}; -// Splits an Ethernet frame into radio frames, sends each by calling send(). -// Called by radio drivers' tx(). Return len, or 0 on error -size_t mg_l2_802154_tx(struct mg_tcpip_if *ifp, const void *buf, size_t len, - size_t (*send)(const uint8_t *, size_t, - struct mg_tcpip_if *)); -// Adds a received radio frame f of n bytes to the frame being reassembled. -// Called by radio drivers' rx(). Return the Ethernet frame length, copied to -// buf, when it is complete, otherwise 0 -size_t mg_l2_802154_rx(struct mg_tcpip_if *ifp, const uint8_t *f, size_t n, - void *buf, size_t len); + // SPI bus abstraction for SPI-attached network chips (e.g. W5500). // Populate and assign to the driver's driver_data field. @@ -4838,6 +4808,86 @@ struct mg_tcpip_spi { #endif #endif +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved +// +// Ethernet over radio, through a mesh. Mongoose sees an Ethernet interface, +// the mesh splits Ethernet frames into radio frames. Two parts plug in: +// - struct mg_radio: a radio standard, e.g. mg_radio_802154 +// - struct mg_mesh: how the mesh is built and routed + + + + + +#if MG_ENABLE_TCPIP + +// Monotonic time in microseconds. +uint64_t mg_micros(void); + +// A radio standard +struct mg_radio { + const char *name; // For logs + size_t max_frame_size; // Largest radio frame, bytes + uint32_t bitrate; // Bits per second on air + size_t overhead; // Bytes on air besides the frame: preamble, FCS + uint32_t slot_us; // Random backoff slot duration, us + uint32_t cca_us; // Channel sensing duration, us + uint32_t turnaround_us; // Idle check to transmission start, us + uint32_t ifs_us; // Inter-frame spacing in us: gap between transmits + uint8_t backoff_min; // Initial backoff exponent + uint8_t backoff_max; // Maximum backoff exponent + bool csma; // Use channel sensing and backoff before TX + size_t header_len; // Radio hardware header length + // Write the radio header, with 0xffff as the broadcast address. + void (*print_hdr)(const struct mg_radio *radio, uint8_t *buf, uint16_t dst, + uint16_t src); + // Return the parsed header length, or zero for an invalid header. + size_t (*parse_hdr)(const struct mg_radio *radio, const uint8_t *buf, + size_t len, uint16_t *dst, uint16_t *src); +}; + +#define MG_802154_HEADER_LEN 9 // MAC header, short addresses +#define MG_802154_PAN_ID 0xabcd // Our network's PAN ID + +#ifndef MG_802154_CHANNEL +#define MG_802154_CHANNEL 15 +#endif + +extern const struct mg_radio mg_radio_802154; + +struct mg_mesh_node { + struct mg_tcpip_if *radio; // Owning radio driver interface + struct mg_tcpip_if *lan; // Bridge LAN interface, NULL for a client + const struct mg_mesh *strategy; // Selected strategy + size_t (*tx)(struct mg_tcpip_if *, const void *, size_t); // Radio frame TX + size_t rx_len; // Complete Ethernet frame in the interface RX buffer + uint64_t now; // Current driver poll time, us + uint64_t tx_at; // Earliest next transmission, us + void *data; // Private state owned by the strategy + size_t ram_used; // Live mesh state bytes + size_t ram_peak; // Peak live mesh state bytes + uint64_t next_tick; // Next deadline in us, or UINT64_MAX + uint16_t radio_address; // Local address supplied by the radio + bool joined; // Client is a member of the mesh +}; + +struct mg_mesh { + const char *name; + bool (*init)(struct mg_mesh_node *node, struct mg_tcpip_if *radio, + struct mg_tcpip_if *lan, uint16_t radio_address); + void (*tick)(struct mg_mesh_node *node, uint64_t now_microseconds); + void (*free)(struct mg_mesh_node *node); + size_t (*tx)(struct mg_mesh_node *, const void *, size_t); // ETH frame TX + void (*rx)(struct mg_mesh_node *, void *, size_t); // Radio frame RX + bool single_bridge; // Require exactly one bridge + bool scheduled_tx; // The mesh schedules transmission turns + const struct mg_radio *radio; // Radio configuration for this strategy +}; + +extern const struct mg_mesh mg_mesh_default; + +#endif @@ -5181,30 +5231,74 @@ struct mg_tcpip_driver_imxrt_data { } while (0) #endif +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + #if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_MCXW71) && \ MG_ENABLE_DRIVER_MCXW71 -// NXP MCXW71 IEEE 802.15.4 radio, ZLL link layer, as an Ethernet interface: -// Ethernet frames are split into radio frames, see mg_l2_802154_tx(). -// The transceiver (XCVR) must be set up for 802.15.4 before mg_tcpip_init(), -// see hal_radio_init() in the FRDM-MCXW71 tutorial. -// driver_data: struct mg_l2_802154_data, channel and PAN ID set +struct mg_tcpip_driver_mcxw71_data { + struct mg_mesh_node *mesh; // Driver-owned runtime state + const struct mg_mesh *strategy; // NULL selects mg_mesh_default +}; -#define MG_TCPIP_DRIVER_INIT(mgr) \ - do { \ - static struct mg_l2_802154_data driver_data_; \ - static struct mg_tcpip_if mif_; \ - driver_data_.channel = MG_L2_802154_CHANNEL; \ - driver_data_.pan_id = MG_L2_802154_PAN_ID; \ - mif_.driver = &mg_tcpip_driver_mcxw71; \ - mif_.driver_data = &driver_data_; \ - MG_SET_MAC_ADDRESS(mif_.mac); \ - mg_tcpip_init(mgr, &mif_); \ - MG_INFO(("Driver: mcxw71, channel %d", MG_L2_802154_CHANNEL)); \ +extern struct mg_tcpip_driver mg_tcpip_driver_mcxw71; + +#define MG_TCPIP_DRIVER_MCXW71_INIT(manager) \ + do { \ + static struct mg_tcpip_driver_mcxw71_data driver_data_; \ + static struct mg_tcpip_if mif_; \ + MG_SET_MAC_ADDRESS(mif_.mac); \ + mif_.ip = MG_TCPIP_IP; \ + mif_.mask = MG_TCPIP_MASK; \ + mif_.gw = MG_TCPIP_GW; \ + mif_.mgr = (manager); \ + mif_.driver = &mg_tcpip_driver_mcxw71; \ + mif_.driver_data = &driver_data_; \ + mg_tcpip_init((manager), &mif_); \ + MG_INFO(("Driver: mcxw71, MAC: %M", mg_print_mac, mif_.mac)); \ } while (0) +#ifndef MG_TCPIP_DRIVER_INIT +#define MG_TCPIP_DRIVER_INIT(x) MG_TCPIP_DRIVER_MCXW71_INIT(x) +#endif + +#endif +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + + + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_MG24) && MG_ENABLE_DRIVER_MG24 + +struct mg_tcpip_driver_mg24_data { + struct mg_mesh_node *mesh; // Driver-owned runtime state + const struct mg_mesh *strategy; // NULL selects mg_mesh_default +}; + +extern struct mg_tcpip_driver mg_tcpip_driver_mg24; + +#define MG_TCPIP_DRIVER_MG24_INIT(manager) \ + do { \ + static struct mg_tcpip_driver_mg24_data driver_data_; \ + static struct mg_tcpip_if mif_; \ + MG_SET_MAC_ADDRESS(mif_.mac); \ + mif_.ip = MG_TCPIP_IP; \ + mif_.mask = MG_TCPIP_MASK; \ + mif_.gw = MG_TCPIP_GW; \ + mif_.mgr = (manager); \ + mif_.driver = &mg_tcpip_driver_mg24; \ + mif_.driver_data = &driver_data_; \ + mg_tcpip_init((manager), &mif_); \ + MG_INFO(("Driver: mg24, MAC: %M", mg_print_mac, mif_.mac)); \ + } while (0) + +#ifndef MG_TCPIP_DRIVER_INIT +#define MG_TCPIP_DRIVER_INIT(x) MG_TCPIP_DRIVER_MG24_INIT(x) +#endif + #endif @@ -5255,6 +5349,40 @@ extern struct mg_tcpip_driver mg_tcpip_driver_netc; } while (0) #endif +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + + + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_NRF52) && MG_ENABLE_DRIVER_NRF52 + +struct mg_tcpip_driver_nrf52_data { + struct mg_mesh_node *mesh; // Driver-owned runtime state + const struct mg_mesh *strategy; // NULL selects mg_mesh_default +}; + +extern struct mg_tcpip_driver mg_tcpip_driver_nrf52; + +#define MG_TCPIP_DRIVER_NRF52_INIT(manager) \ + do { \ + static struct mg_tcpip_driver_nrf52_data driver_data_; \ + static struct mg_tcpip_if mif_; \ + MG_SET_MAC_ADDRESS(mif_.mac); \ + mif_.ip = MG_TCPIP_IP; \ + mif_.mask = MG_TCPIP_MASK; \ + mif_.gw = MG_TCPIP_GW; \ + mif_.mgr = (manager); \ + mif_.driver = &mg_tcpip_driver_nrf52; \ + mif_.driver_data = &driver_data_; \ + mg_tcpip_init((manager), &mif_); \ + MG_INFO(("Driver: nrf52, MAC: %M", mg_print_mac, mif_.mac)); \ + } while (0) + +#ifndef MG_TCPIP_DRIVER_INIT +#define MG_TCPIP_DRIVER_INIT(x) MG_TCPIP_DRIVER_NRF52_INIT(x) +#endif + +#endif #if MG_ENABLE_TCPIP && \ @@ -5407,6 +5535,7 @@ struct mg_tcpip_driver_rw612_data { #define MG_DRIVER_MDC_HOLDTIME 3 #endif +#ifndef MG_TCPIP_DRIVER_INIT #define MG_TCPIP_DRIVER_INIT(mgr) \ do { \ static struct mg_tcpip_driver_rw612_data driver_data_; \ @@ -5422,34 +5551,41 @@ struct mg_tcpip_driver_rw612_data { mg_tcpip_init(mgr, &mif_); \ MG_INFO(("Driver: rw612, MAC: %M", mg_print_mac, mif_.mac)); \ } while (0) - #endif +#endif +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved -#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_RW612_154) && \ - MG_ENABLE_DRIVER_RW612_154 -// NXP RW612 IEEE 802.15.4 radio, as an Ethernet interface: Ethernet frames -// are split into radio frames, see mg_l2_802154_tx(). The radio runs on -// CPU2: NXP BLE/802.15.4 combo firmware, an OpenThread radio co-processor -// (RCP). The firmware must be loaded and running before mg_tcpip_init(), see -// the FRDM-RW612 eth_radio_bridge tutorial. The driver talks to it over the -// CPU2 <-> CPU3 IMU mailbox: IMUMC messages carrying HDLC framed Spinel. -// driver_data: struct mg_l2_802154_data, channel and PAN ID set + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_RW612W) && \ + MG_ENABLE_DRIVER_RW612W + +struct mg_tcpip_driver_rw612w_data { + struct mg_mesh_node *mesh; // Driver-owned runtime state + const struct mg_mesh *strategy; // NULL selects mg_mesh_default +}; + +extern struct mg_tcpip_driver mg_tcpip_driver_rw612w; + +#define MG_TCPIP_DRIVER_RW612W_INIT(manager) \ + do { \ + static struct mg_tcpip_driver_rw612w_data driver_data_; \ + static struct mg_tcpip_if mif_; \ + MG_SET_MAC_ADDRESS(mif_.mac); \ + mif_.ip = MG_TCPIP_IP; \ + mif_.mask = MG_TCPIP_MASK; \ + mif_.gw = MG_TCPIP_GW; \ + mif_.mgr = (manager); \ + mif_.driver = &mg_tcpip_driver_rw612w; \ + mif_.driver_data = &driver_data_; \ + mg_tcpip_init((manager), &mif_); \ + MG_INFO(("Driver: rw612w, MAC: %M", mg_print_mac, mif_.mac)); \ + } while (0) #ifndef MG_TCPIP_DRIVER_INIT -#define MG_TCPIP_DRIVER_INIT(mgr) \ - do { \ - static struct mg_l2_802154_data driver_data_; \ - static struct mg_tcpip_if mif_; \ - driver_data_.channel = MG_L2_802154_CHANNEL; \ - driver_data_.pan_id = MG_L2_802154_PAN_ID; \ - mif_.driver = &mg_tcpip_driver_rw612_154; \ - mif_.driver_data = &driver_data_; \ - MG_SET_MAC_ADDRESS(mif_.mac); \ - mg_tcpip_init(mgr, &mif_); \ - MG_INFO(("Driver: rw612_154, channel %d", MG_L2_802154_CHANNEL)); \ - } while (0) +#define MG_TCPIP_DRIVER_INIT(x) MG_TCPIP_DRIVER_RW612W_INIT(x) #endif #endif diff --git a/src/drivers/mcxw71.c b/src/drivers/mcxw71.c index 6cf9913b..0287b234 100644 --- a/src/drivers/mcxw71.c +++ b/src/drivers/mcxw71.c @@ -1,3 +1,5 @@ +#include "l2_mesh.h" +#include "log.h" #include "net_builtin.h" #if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_MCXW71) && \ @@ -9,12 +11,12 @@ struct mg_zll { volatile uint32_t IRQSTS, PHY_CTRL, EVENT_TMR, TIMESTAMP, T1CMP, T2CMP, T2PRIMECMP, T3CMP, T4CMP, PA_PWR, CHANNEL_NUM0, LQI_AND_RSSI, MACSHORTADDRS0, MACLONGADDRS0_LSB, MACLONGADDRS0_MSB, RX_FRAME_FILTER, - CCA_LQI_CTRL, CCA2_CTRL, RESERVED_0, DSM_CTRL, RESERVED_1, - MACSHORTADDRS1, MACLONGADDRS1_LSB, MACLONGADDRS1_MSB, DUAL_PAN_CTRL, - CHANNEL_NUM1, SAM_CTRL, SAM_TABLE, SAM_MATCH, SAM_FREE_IDX, - SEQ_CTRL_STS, ACKDELAY, FILTERFAIL_CODE, RX_WTR_MARK, RESERVED_2, - SLOT_PRELOAD, SEQ_STATE, TMR_PRESCALE, LENIENCY_LSB, LENIENCY_MSB, - PART_ID, COEX_CTRL, COEX_PRIORITY, ENHACK_CTRL0; + CCA_LQI_CTRL, CCA2_CTRL, RESERVED_0, DSM_CTRL, RESERVED_1, MACSHORTADDRS1, + MACLONGADDRS1_LSB, MACLONGADDRS1_MSB, DUAL_PAN_CTRL, CHANNEL_NUM1, + SAM_CTRL, SAM_TABLE, SAM_MATCH, SAM_FREE_IDX, SEQ_CTRL_STS, ACKDELAY, + FILTERFAIL_CODE, RX_WTR_MARK, RESERVED_2, SLOT_PRELOAD, SEQ_STATE, + TMR_PRESCALE, LENIENCY_LSB, LENIENCY_MSB, PART_ID, COEX_CTRL, + COEX_PRIORITY, ENHACK_CTRL0; }; // TrustZone: code built for the secure state (-mcmse) uses the secure alias @@ -32,111 +34,203 @@ struct mg_zll { #define MGI_IRQSTS_SEQIRQ MG_BIT(0) #define MGI_IRQSTS_RXIRQ MG_BIT(2) +#define MGI_IRQSTS_FILTERFAIL MG_BIT(5) // Frame not for us +#define MGI_IRQSTS_CRCVALID MG_BIT(15) #define MGI_IRQSTS_RX_LEN(x) (((x) >> 24) & 0x7f) // Frame length incl. FCS #define MGI_PHY_CTRL_XCVSEQ 7U #define MGI_PHY_CTRL_ALL_IRQ_MASKS 0x8007ff00U // Bits 8..18, 31: mask all IRQs +#define MGI_PHY_CTRL_SEQMSK MG_BIT(8) // Mask the sequence end IRQ +#define MGI_PHY_CTRL_TRCV_MSK MG_BIT(31) // Mask all transceiver IRQs +#define MGI_IRQN 53 // RF_802_15_4_IRQn +#define MGI_NVIC_ISER ((volatile uint32_t *) 0xE000E100) // Enable IRQ +#define MGI_NVIC_ICER ((volatile uint32_t *) 0xE000E180) // Disable IRQ #define MGI_SEQ_STATE_MASK 0x1fU -#define MGI_SEQ_STATE_RX_WU 0x10U // RX warm up +#define MGI_SEQ_STATE_RX_WU 0x10U // RX warm up #define MGI_SEQ_CTRL_NO_RX_RECYCLE MG_BIT(5) #define MGI_FRAME_FILTER 0x70bU // Beacon, data, command; versions 0..2 -enum { MGI_SEQ_IDLE = 0, MGI_SEQ_RX = 1, MGI_SEQ_TX = 2 }; // PHY_CTRL[XCVSEQ] -#define MGI_TX_TIMEOUT 400U // Symbols, 16 us: 127 bytes +// PHY_CTRL[XCVSEQ] +enum { MGI_SEQ_IDLE = 0, MGI_SEQ_RX = 1, MGI_SEQ_TX = 2 }; +#define MGI_SEND_TIMEOUT_MS 60 -// Symbol timer, 16 us, 24 bits -static uint32_t mg_mcxw71_symbols(void) { - return MGI_ZLL->EVENT_TMR >> 8; -} - -// Wait for the sequence to end, up to MGI_TX_TIMEOUT symbols -static bool mg_mcxw71_wait(void) { - uint32_t start = mg_mcxw71_symbols(); - while (((mg_mcxw71_symbols() - start) & 0xffffffU) < MGI_TX_TIMEOUT) { - if (MGI_ZLL->IRQSTS & MGI_IRQSTS_SEQIRQ) return true; - } - return false; -} +static struct mg_tcpip_if *s_mcxw71_ifp; +static struct mg_queue s_mcxw71_rx; +static char s_mcxw71_queue[2048]; +static volatile bool s_mcxw71_txing, s_mcxw71_done; +static uint64_t s_mcxw71_sendstart; // Stop the current sequence, wait for the sequencer to go idle. See PhyAbort() -static void mg_mcxw71_idle(void) { +static void mg_mcxw71_abort(void) { if (MGI_ZLL->PHY_CTRL & MGI_PHY_CTRL_XCVSEQ) { while ((MGI_ZLL->SEQ_STATE & MGI_SEQ_STATE_MASK) == MGI_SEQ_STATE_RX_WU) (void) 0; MGI_ZLL->PHY_CTRL &= ~MGI_PHY_CTRL_XCVSEQ; while (MGI_ZLL->SEQ_STATE & MGI_SEQ_STATE_MASK) (void) 0; } - MGI_ZLL->IRQSTS = MGI_ZLL->IRQSTS; // Clear all flags, write 1 to clear -} - -// Start a new sequence: MGI_SEQ_RX or MGI_SEQ_TX -static void mg_mcxw71_start(uint32_t seq) { MGI_ZLL->IRQSTS = MGI_ZLL->IRQSTS; - MGI_ZLL->PHY_CTRL |= seq; } -static bool mg_mcxw71_init(struct mg_tcpip_if *ifp) { - struct mg_l2_802154_data *d = - (struct mg_l2_802154_data *) ifp->driver_data; - if (d == NULL || d->channel < 11 || d->channel > 26) { - MG_ERROR(("Set driver_data, channel 11..26")); - return false; +// Start receiving, with the sequence end IRQ on, see RF_802_15_4_IRQHandler() +static void mg_mcxw71_listen(void) { + MGI_ZLL->IRQSTS = MGI_ZLL->IRQSTS; + MGI_ZLL->PHY_CTRL |= MGI_SEQ_RX; + MGI_ZLL->PHY_CTRL &= ~(MGI_PHY_CTRL_SEQMSK | MGI_PHY_CTRL_TRCV_MSK); + MGI_NVIC_ISER[MGI_IRQN / 32] = MG_BIT(MGI_IRQN % 32); +} + +// Keep the IRQ handler out while we change the radio. The NVIC disable is +// atomic, so PHY_CTRL updates here and there do not race +static void mg_mcxw71_lock(void) { + MGI_NVIC_ICER[MGI_IRQN / 32] = MG_BIT(MGI_IRQN % 32); + __asm volatile("dsb; isb" ::: "memory"); +} + +// Start the TX sequence for the loaded frame. Its end: see the IRQ handler +static void mg_mcxw71_go(void) { + MGI_ZLL->PHY_CTRL |= MGI_PHY_CTRL_TRCV_MSK | MGI_PHY_CTRL_SEQMSK; + mg_mcxw71_abort(); + MGI_ZLL->IRQSTS = MGI_ZLL->IRQSTS; + MGI_ZLL->PHY_CTRL |= MGI_SEQ_TX; + MGI_ZLL->PHY_CTRL &= ~(MGI_PHY_CTRL_SEQMSK | MGI_PHY_CTRL_TRCV_MSK); +} + +void RF_802_15_4_IRQHandler(void); +void RF_802_15_4_IRQHandler(void) { + uint32_t irqsts = MGI_ZLL->IRQSTS; + if ((irqsts & MGI_IRQSTS_SEQIRQ) != 0) { + mg_mcxw71_abort(); + if (s_mcxw71_txing) { + s_mcxw71_txing = false; + s_mcxw71_done = true; + } else if ((irqsts & (MGI_IRQSTS_RXIRQ | MGI_IRQSTS_FILTERFAIL | + MGI_IRQSTS_CRCVALID)) == + (MGI_IRQSTS_RXIRQ | MGI_IRQSTS_CRCVALID)) { + uint8_t frame[125]; + size_t i, n = MGI_IRQSTS_RX_LEN(irqsts); + n = n >= 2 ? n - 2 : 0; + if (n > 0 && n <= sizeof(frame)) { + for (i = 0; i < n; i++) frame[i] = MGI_RX_PB[i]; + { + char *dst; + if (mg_queue_book(&s_mcxw71_rx, &dst, n) >= n) { + memcpy(dst, frame, n); + mg_queue_add(&s_mcxw71_rx, n); + } else { + s_mcxw71_ifp->ndrop++; + } + } + } + } + mg_mcxw71_listen(); } +} + +static size_t mgi_mcxw71_radio_tx(struct mg_tcpip_if *ifp, const void *buf, + size_t len) { + size_t i; + (void) ifp; + if (len > 125 || s_mcxw71_txing) return 0; + mg_mcxw71_lock(); + MGI_TX_PB[0] = (uint8_t) (len + 2); // FCS is added by hardware + for (i = 0; i < len; i++) + MGI_TX_PB[i + 1] = ((const uint8_t *) buf)[i]; // 8-bit access only + s_mcxw71_done = false; + s_mcxw71_txing = true; + s_mcxw71_sendstart = mg_millis(); + mg_mcxw71_go(); + MGI_NVIC_ISER[MGI_IRQN / 32] = MG_BIT(MGI_IRQN % 32); + return len; +} + +static bool mgi_mcxw71_radio_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mcxw71_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + s_mcxw71_ifp = ifp; MGI_ZLL->SEQ_CTRL_STS |= MGI_SEQ_CTRL_NO_RX_RECYCLE; - MGI_ZLL->PHY_CTRL = MGI_PHY_CTRL_ALL_IRQ_MASKS; // Mask all IRQs, we poll + MGI_ZLL->PHY_CTRL = MGI_PHY_CTRL_ALL_IRQ_MASKS; MGI_ZLL->IRQSTS = MGI_ZLL->IRQSTS; MGI_ZLL->SAM_CTRL = 0; MGI_ZLL->RX_FRAME_FILTER = MGI_FRAME_FILTER; - MGI_ZLL->TMR_PRESCALE = 5; // 16 us timebase, one symbol - MGI_ZLL->CHANNEL_NUM0 = d->channel; - MGI_ZLL->MACSHORTADDRS0 = (0xfffeU << 16) | d->pan_id; // Filter by PAN ID - MGI_ZLL->PA_PWR = 0x3f; // PA slices, max output power - mg_mcxw71_start(MGI_SEQ_RX); + MGI_ZLL->CHANNEL_NUM0 = MG_802154_CHANNEL; + MGI_ZLL->MACSHORTADDRS0 = + ((uint32_t) node->radio_address << 16) | MG_802154_PAN_ID; + MGI_ZLL->PA_PWR = 0x3f; + mg_mcxw71_listen(); return true; } -// Send one radio frame: MAC header and payload, no FCS -static size_t mg_mcxw71_send(const uint8_t *p, size_t len, - struct mg_tcpip_if *ifp) { - size_t i; - bool ok; - mg_mcxw71_idle(); - MGI_TX_PB[0] = (uint8_t) (len + 2); // FCS is added by hardware - for (i = 0; i < len; i++) MGI_TX_PB[i + 1] = p[i]; // 8-bit access only - mg_mcxw71_start(MGI_SEQ_TX); - ok = mg_mcxw71_wait(); - mg_mcxw71_idle(); - mg_mcxw71_start(MGI_SEQ_RX); - (void) ifp; - return ok ? len : 0; +static size_t mgi_mcxw71_tx(const void *buf, size_t len, + struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mcxw71_data *data = ifp->driver_data; + return data->mesh->strategy->tx(data->mesh, buf, len); } -static size_t mg_mcxw71_tx(const void *buf, size_t len, - struct mg_tcpip_if *ifp) { - return mg_l2_802154_tx(ifp, buf, len, mg_mcxw71_send); +static size_t mgi_mcxw71_rx(void *buf, size_t cap, struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mcxw71_data *data = ifp->driver_data; + size_t len = data->mesh->rx_len; + if (len > cap) return 0; + if (len != 0 && buf != ifp->recv_queue.buf) + memcpy(buf, ifp->recv_queue.buf, len); + data->mesh->rx_len = 0; + return len; } -static size_t mg_mcxw71_rx(void *buf, size_t len, struct mg_tcpip_if *ifp) { - uint32_t irqsts = MGI_ZLL->IRQSTS; - uint8_t f[125]; - size_t i, n = 0; - if ((irqsts & MGI_IRQSTS_SEQIRQ) == 0) return 0; // Still receiving - if (irqsts & MGI_IRQSTS_RXIRQ) { - n = MGI_IRQSTS_RX_LEN(irqsts); - n = n >= 2 ? n - 2 : 0; // Strip FCS - if (n > sizeof(f)) n = sizeof(f); - for (i = 0; i < n; i++) f[i] = MGI_RX_PB[i]; +static bool mgi_mcxw71_poll(struct mg_tcpip_if *ifp, bool s1) { + struct mg_tcpip_driver_mcxw71_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + uint64_t now = mg_micros(); + char *buf; + size_t len; + (void) s1; + if (s_mcxw71_txing && + mg_millis() > s_mcxw71_sendstart + MGI_SEND_TIMEOUT_MS) { + mg_mcxw71_lock(); + if (s_mcxw71_txing) { + mg_mcxw71_abort(); + s_mcxw71_txing = false; + s_mcxw71_done = true; + mg_mcxw71_listen(); + } else { + MGI_NVIC_ISER[MGI_IRQN / 32] = MG_BIT(MGI_IRQN % 32); + } } - mg_mcxw71_idle(); - mg_mcxw71_start(MGI_SEQ_RX); - return mg_l2_802154_rx(ifp, f, n, buf, len); + if (s_mcxw71_done) { + s_mcxw71_done = false; + node->tx_at = now + node->strategy->radio->ifs_us; + } + node->now = now; + while (node->rx_len == 0 && (len = mg_queue_next(&s_mcxw71_rx, &buf)) != 0) { + node->strategy->rx(node, buf, len); + mg_queue_del(&s_mcxw71_rx, len); + } + node->strategy->tick(node, now); + return node->joined; } -static bool mg_mcxw71_poll(struct mg_tcpip_if *ifp, bool s1) { - (void) ifp, (void) s1; - return true; // Radio link is always up +static bool mgi_mcxw71_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mcxw71_data *data = ifp->driver_data; + const struct mg_mesh *strategy = data->strategy; + uint16_t address; + address = + (uint16_t) (2 + ((unsigned) ifp->mac[4] * 256 + ifp->mac[5]) % 65532u); + if (strategy == NULL) strategy = &mg_mesh_default; + data->mesh = calloc(1, sizeof(*data->mesh)); + if (data->mesh == NULL) return false; + if (!strategy->init(data->mesh, ifp, NULL, address)) { + free(data->mesh); + data->mesh = NULL; + return false; + } + data->mesh->strategy = strategy; + data->mesh->tx = mgi_mcxw71_radio_tx; + mg_queue_init(&s_mcxw71_rx, s_mcxw71_queue, sizeof(s_mcxw71_queue)); + if (mgi_mcxw71_radio_init(ifp)) return true; + strategy->free(data->mesh); + free(data->mesh); + data->mesh = NULL; + return false; } -struct mg_tcpip_driver mg_tcpip_driver_mcxw71 = {mg_mcxw71_init, mg_mcxw71_tx, - mg_mcxw71_rx, mg_mcxw71_poll}; - +struct mg_tcpip_driver mg_tcpip_driver_mcxw71 = { + mgi_mcxw71_init, mgi_mcxw71_tx, mgi_mcxw71_rx, mgi_mcxw71_poll}; #endif diff --git a/src/drivers/mcxw71.h b/src/drivers/mcxw71.h index 06558ee8..fb4a6abc 100644 --- a/src/drivers/mcxw71.h +++ b/src/drivers/mcxw71.h @@ -1,25 +1,35 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + #pragma once #if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_MCXW71) && \ MG_ENABLE_DRIVER_MCXW71 -// NXP MCXW71 IEEE 802.15.4 radio, ZLL link layer, as an Ethernet interface: -// Ethernet frames are split into radio frames, see mg_l2_802154_tx(). -// The transceiver (XCVR) must be set up for 802.15.4 before mg_tcpip_init(), -// see hal_radio_init() in the FRDM-MCXW71 tutorial. -// driver_data: struct mg_l2_802154_data, channel and PAN ID set +struct mg_tcpip_driver_mcxw71_data { + struct mg_mesh_node *mesh; // Driver-owned runtime state + const struct mg_mesh *strategy; // NULL selects mg_mesh_default +}; -#define MG_TCPIP_DRIVER_INIT(mgr) \ - do { \ - static struct mg_l2_802154_data driver_data_; \ - static struct mg_tcpip_if mif_; \ - driver_data_.channel = MG_L2_802154_CHANNEL; \ - driver_data_.pan_id = MG_L2_802154_PAN_ID; \ - mif_.driver = &mg_tcpip_driver_mcxw71; \ - mif_.driver_data = &driver_data_; \ - MG_SET_MAC_ADDRESS(mif_.mac); \ - mg_tcpip_init(mgr, &mif_); \ - MG_INFO(("Driver: mcxw71, channel %d", MG_L2_802154_CHANNEL)); \ +extern struct mg_tcpip_driver mg_tcpip_driver_mcxw71; + +#define MG_TCPIP_DRIVER_MCXW71_INIT(manager) \ + do { \ + static struct mg_tcpip_driver_mcxw71_data driver_data_; \ + static struct mg_tcpip_if mif_; \ + MG_SET_MAC_ADDRESS(mif_.mac); \ + mif_.ip = MG_TCPIP_IP; \ + mif_.mask = MG_TCPIP_MASK; \ + mif_.gw = MG_TCPIP_GW; \ + mif_.mgr = (manager); \ + mif_.driver = &mg_tcpip_driver_mcxw71; \ + mif_.driver_data = &driver_data_; \ + mg_tcpip_init((manager), &mif_); \ + MG_INFO(("Driver: mcxw71, MAC: %M", mg_print_mac, mif_.mac)); \ } while (0) +#ifndef MG_TCPIP_DRIVER_INIT +#define MG_TCPIP_DRIVER_INIT(x) MG_TCPIP_DRIVER_MCXW71_INIT(x) +#endif + #endif diff --git a/src/drivers/mg24.c b/src/drivers/mg24.c new file mode 100644 index 00000000..a63b808e --- /dev/null +++ b/src/drivers/mg24.c @@ -0,0 +1,183 @@ +#include "net_builtin.h" +#include "l2_mesh.h" +#include "log.h" + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_MG24) && MG_ENABLE_DRIVER_MG24 + +#include +#include + +// RAIL delivers raw frames and transmit completion from interrupt context. +#define MG_MG24_TX_TIMEOUT_US 60000 + +static struct mg_tcpip_if *s_mg24_ifp; +static RAIL_Handle_t s_mg24_rail; +static struct mg_queue s_mg24_rx; +static char s_mg24_rxbuf[2048]; +static volatile bool s_mg24_txing, s_mg24_tx_done; +static RAIL_Time_t s_mg24_txstart; // Send start, us +static uint8_t s_mg24_txfifo[256] __attribute__((aligned(4))); // Power of 2 + +// Copy a received PSDU, stripping the PHR and any stored FCS. +static void mg_mg24_rx_frame(RAIL_Handle_t h) { + RAIL_RxPacketInfo_t info; + uint8_t f[128]; + size_t n; + char *dst; + if (RAIL_GetRxPacketInfo(h, RAIL_RX_PACKET_HANDLE_NEWEST, &info) == + RAIL_RX_PACKET_HANDLE_INVALID || + info.packetStatus != RAIL_RX_PACKET_READY_SUCCESS || + info.packetBytes <= 1 || info.packetBytes > sizeof(f)) { + return; + } + RAIL_CopyRxPacket(f, &info); + n = info.packetBytes - 1U; // Strip PHR + if (n == f[0]) n -= 2; // FCS stored, strip it + if (mg_queue_book(&s_mg24_rx, &dst, n) >= n) { + memcpy(dst, f + 1, n); + mg_queue_add(&s_mg24_rx, n); + } else { + s_mg24_ifp->ndrop++; + } +} + +// RAIL event callback, runs in interrupt context +static void mg_mg24_events(RAIL_Handle_t h, RAIL_Events_t events) { + if (events & RAIL_EVENT_CAL_NEEDED) { + RAIL_Calibrate(h, NULL, RAIL_CAL_ALL_PENDING); + } + if (events & RAIL_EVENT_RX_PACKET_RECEIVED) mg_mg24_rx_frame(h); + if (events & RAIL_EVENTS_TX_COMPLETION) s_mg24_tx_done = true; +} + +// RAIL adds the FCS. +static size_t mgi_mg24_radio_tx(struct mg_tcpip_if *ifp, const void *buf, + size_t len) { + uint8_t phr = (uint8_t) (len + 2); // FCS is added by hardware + (void) ifp; + if (len > 125 || s_mg24_txing) return 0; + RAIL_WriteTxFifo(s_mg24_rail, &phr, 1, true); // Reset FIFO, write PHR + RAIL_WriteTxFifo(s_mg24_rail, (const uint8_t *) buf, (uint16_t) len, false); + s_mg24_tx_done = false; + s_mg24_txing = true, s_mg24_txstart = RAIL_GetTime(); + if (RAIL_StartTx(s_mg24_rail, MG_802154_CHANNEL, RAIL_TX_OPTIONS_DEFAULT, + NULL) != RAIL_STATUS_NO_ERROR) { + s_mg24_txing = false; + return 0; + } + return len; +} + +static bool mgi_mg24_radio_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mg24_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + RAIL_Config_t cfg = {0}; + RAIL_IEEE802154_Config_t cfg154 = {0}; + RAIL_TxPowerConfig_t pwr; + RAIL_StateTransitions_t to_rx = {RAIL_RF_STATE_RX, RAIL_RF_STATE_RX}; + RAIL_Events_t events = RAIL_EVENT_CAL_NEEDED | RAIL_EVENT_RX_PACKET_RECEIVED | + RAIL_EVENTS_TX_COMPLETION; + cfg.eventsCallback = mg_mg24_events; + if ((s_mg24_rail = RAIL_Init(&cfg, NULL)) == NULL) { + MG_ERROR(("RAIL_Init failed")); + return false; + } + cfg154.ackConfig.enable = false; + cfg154.ackConfig.ackTimeout = 864; + cfg154.ackConfig.rxTransitions = to_rx; + cfg154.ackConfig.txTransitions = to_rx; + cfg154.timings.idleToRx = 100, cfg154.timings.idleToTx = 100; + cfg154.timings.rxToTx = 192, cfg154.timings.txToRx = 182; + cfg154.framesMask = RAIL_IEEE802154_ACCEPT_STANDARD_FRAMES; + cfg154.promiscuousMode = false; // Filter by PAN ID and our short address + pwr.mode = RAIL_TX_POWER_MODE_2P4GIG_HP, pwr.voltage = 3300; + pwr.rampTime = 10; + if (RAIL_ConfigCal(s_mg24_rail, RAIL_CAL_ALL) != RAIL_STATUS_NO_ERROR || + RAIL_ConfigEvents(s_mg24_rail, RAIL_EVENTS_ALL, events) != + RAIL_STATUS_NO_ERROR || + RAIL_IEEE802154_Config2p4GHzRadio(s_mg24_rail) != RAIL_STATUS_NO_ERROR || + RAIL_IEEE802154_Init(s_mg24_rail, &cfg154) != RAIL_STATUS_NO_ERROR || + RAIL_IEEE802154_SetPanId(s_mg24_rail, MG_802154_PAN_ID, 0) != + RAIL_STATUS_NO_ERROR || + RAIL_IEEE802154_SetShortAddress(s_mg24_rail, node->radio_address, 0) != + RAIL_STATUS_NO_ERROR || + RAIL_SetTxTransitions(s_mg24_rail, &to_rx) != RAIL_STATUS_NO_ERROR || + RAIL_SetRxTransitions(s_mg24_rail, &to_rx) != RAIL_STATUS_NO_ERROR || + RAIL_ConfigTxPower(s_mg24_rail, &pwr) != RAIL_STATUS_NO_ERROR || + RAIL_SetTxPower(s_mg24_rail, RAIL_TX_POWER_LEVEL_2P4_HP_MAX) != + RAIL_STATUS_NO_ERROR || + RAIL_SetTxFifo(s_mg24_rail, s_mg24_txfifo, 0, sizeof(s_mg24_txfifo)) != + sizeof(s_mg24_txfifo)) { + MG_ERROR(("RAIL setup failed")); + return false; + } + s_mg24_ifp = ifp; + return RAIL_StartRx(s_mg24_rail, MG_802154_CHANNEL, NULL) == + RAIL_STATUS_NO_ERROR; +} + +static size_t mgi_mg24_tx(const void *buf, size_t len, + struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mg24_data *data = ifp->driver_data; + return data->mesh->strategy->tx(data->mesh, buf, len); +} + +static size_t mgi_mg24_rx(void *buf, size_t cap, struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mg24_data *data = ifp->driver_data; + size_t len = data->mesh->rx_len; + if (len > cap) return 0; + if (len != 0 && buf != ifp->recv_queue.buf) + memcpy(buf, ifp->recv_queue.buf, len); + data->mesh->rx_len = 0; + return len; +} + +static bool mgi_mg24_poll(struct mg_tcpip_if *ifp, bool s1) { + struct mg_tcpip_driver_mg24_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + uint64_t now = mg_micros(); + char *buf; + size_t len; + (void) s1; + node->now = now; + if (s_mg24_txing && (s_mg24_tx_done || RAIL_GetTime() - s_mg24_txstart > + MG_MG24_TX_TIMEOUT_US)) { + if (!s_mg24_tx_done) { + RAIL_Idle(s_mg24_rail, RAIL_IDLE_ABORT, true); + RAIL_StartRx(s_mg24_rail, MG_802154_CHANNEL, NULL); + } + s_mg24_txing = false; + node->tx_at = now + node->strategy->radio->ifs_us; + } + while (node->rx_len == 0 && (len = mg_queue_next(&s_mg24_rx, &buf)) != 0) { + node->strategy->rx(node, buf, len); + mg_queue_del(&s_mg24_rx, len); + } + node->strategy->tick(node, now); + return node->joined; +} +static bool mgi_mg24_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_mg24_data *data = ifp->driver_data; + const struct mg_mesh *strategy = data->strategy; + uint16_t address = + (uint16_t) (2 + ((unsigned) ifp->mac[4] * 256 + ifp->mac[5]) % 65532u); + if (strategy == NULL) strategy = &mg_mesh_default; + data->mesh = calloc(1, sizeof(*data->mesh)); + if (data->mesh == NULL) return false; + if (!strategy->init(data->mesh, ifp, NULL, address)) { + free(data->mesh); + data->mesh = NULL; + return false; + } + data->mesh->strategy = strategy; + data->mesh->tx = mgi_mg24_radio_tx; + mg_queue_init(&s_mg24_rx, s_mg24_rxbuf, sizeof(s_mg24_rxbuf)); + if (mgi_mg24_radio_init(ifp)) return true; + data->mesh->strategy->free(data->mesh); + free(data->mesh); + data->mesh = NULL; + return false; +} +struct mg_tcpip_driver mg_tcpip_driver_mg24 = {mgi_mg24_init, mgi_mg24_tx, + mgi_mg24_rx, mgi_mg24_poll}; +#endif diff --git a/src/drivers/mg24.h b/src/drivers/mg24.h new file mode 100644 index 00000000..039e13d2 --- /dev/null +++ b/src/drivers/mg24.h @@ -0,0 +1,34 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#pragma once + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_MG24) && MG_ENABLE_DRIVER_MG24 + +struct mg_tcpip_driver_mg24_data { + struct mg_mesh_node *mesh; // Driver-owned runtime state + const struct mg_mesh *strategy; // NULL selects mg_mesh_default +}; + +extern struct mg_tcpip_driver mg_tcpip_driver_mg24; + +#define MG_TCPIP_DRIVER_MG24_INIT(manager) \ + do { \ + static struct mg_tcpip_driver_mg24_data driver_data_; \ + static struct mg_tcpip_if mif_; \ + MG_SET_MAC_ADDRESS(mif_.mac); \ + mif_.ip = MG_TCPIP_IP; \ + mif_.mask = MG_TCPIP_MASK; \ + mif_.gw = MG_TCPIP_GW; \ + mif_.mgr = (manager); \ + mif_.driver = &mg_tcpip_driver_mg24; \ + mif_.driver_data = &driver_data_; \ + mg_tcpip_init((manager), &mif_); \ + MG_INFO(("Driver: mg24, MAC: %M", mg_print_mac, mif_.mac)); \ + } while (0) + +#ifndef MG_TCPIP_DRIVER_INIT +#define MG_TCPIP_DRIVER_INIT(x) MG_TCPIP_DRIVER_MG24_INIT(x) +#endif + +#endif diff --git a/src/drivers/nrf52.c b/src/drivers/nrf52.c new file mode 100644 index 00000000..9022d483 --- /dev/null +++ b/src/drivers/nrf52.c @@ -0,0 +1,179 @@ +#include "net_builtin.h" +#include "l2_mesh.h" +#include "log.h" + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_NRF52) && MG_ENABLE_DRIVER_NRF52 +#include + +static struct mg_tcpip_if *s_nrf52_ifp; +static struct mg_queue s_nrf52_rx; +static char s_nrf52_queue[2048]; +static uint8_t s_nrf52_rxbuf[128] __attribute__((aligned(4))); +static uint8_t s_nrf52_txbuf[128] __attribute__((aligned(4))); +static volatile bool s_nrf52_txing, s_nrf52_done; +static uint64_t s_nrf52_started; + +static void mgi_nrf52_listen(void) { + NRF_RADIO->PACKETPTR = (uint32_t) s_nrf52_rxbuf; + NRF_RADIO->SHORTS = + RADIO_SHORTS_READY_START_Msk | RADIO_SHORTS_END_DISABLE_Msk; + NRF_RADIO->EVENTS_FRAMESTART = 0; + NRF_RADIO->EVENTS_END = 0; + NRF_RADIO->TASKS_RXEN = 1; +} +static void mgi_nrf52_disable(void) { + NRF_RADIO->SHORTS = 0; + NRF_RADIO->EVENTS_DISABLED = 0; + NRF_RADIO->TASKS_DISABLE = 1; + while (!NRF_RADIO->EVENTS_DISABLED) (void) 0; + NRF_RADIO->EVENTS_DISABLED = 0; +} +void RADIO_IRQHandler(void); +void RADIO_IRQHandler(void) { + if (!NRF_RADIO->EVENTS_DISABLED) return; + NRF_RADIO->EVENTS_DISABLED = 0; + if (s_nrf52_txing) { + s_nrf52_txing = false; + s_nrf52_done = true; + } else if (NRF_RADIO->CRCSTATUS) { + struct mg_tcpip_driver_nrf52_data *data = s_nrf52_ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + size_t len = s_nrf52_rxbuf[0] >= 2 ? s_nrf52_rxbuf[0] - 2U : 0; + uint8_t *frame = s_nrf52_rxbuf + 1; + uint16_t dst, src; + if (len <= 125 && + mg_radio_802154.parse_hdr(&mg_radio_802154, frame, len, &dst, &src) != + 0 && + (dst == 0xffffu || dst == node->radio_address)) { + char *dst; + if (mg_queue_book(&s_nrf52_rx, &dst, len) >= len) { + memcpy(dst, frame, len); + mg_queue_add(&s_nrf52_rx, len); + } else { + s_nrf52_ifp->ndrop++; + } + } + } + mgi_nrf52_listen(); +} +static size_t mgi_nrf52_radio_tx(struct mg_tcpip_if *ifp, const void *buf, + size_t len) { + (void) ifp; + if (len > 125) return 0; + NVIC_DisableIRQ(RADIO_IRQn); + if (s_nrf52_txing || NRF_RADIO->EVENTS_FRAMESTART) { + NVIC_EnableIRQ(RADIO_IRQn); + return 0; + } + mgi_nrf52_disable(); + s_nrf52_txbuf[0] = (uint8_t) (len + 2); + memcpy(s_nrf52_txbuf + 1, buf, len); + s_nrf52_done = false; + s_nrf52_txing = true; + s_nrf52_started = mg_micros(); + NRF_RADIO->PACKETPTR = (uint32_t) s_nrf52_txbuf; + NRF_RADIO->SHORTS = + RADIO_SHORTS_READY_START_Msk | RADIO_SHORTS_END_DISABLE_Msk; + NRF_RADIO->TASKS_TXEN = 1; + NVIC_EnableIRQ(RADIO_IRQn); + return len; +} +static bool mgi_nrf52_radio_init(struct mg_tcpip_if *ifp) { + s_nrf52_ifp = ifp; + NRF_RADIO->POWER = 1; + NRF_RADIO->MODE = RADIO_MODE_MODE_Ieee802154_250Kbit; + NRF_RADIO->PCNF0 = (8UL << RADIO_PCNF0_LFLEN_Pos) | + (RADIO_PCNF0_PLEN_32bitZero << RADIO_PCNF0_PLEN_Pos) | + RADIO_PCNF0_CRCINC_Msk; + NRF_RADIO->PCNF1 = 127; + NRF_RADIO->CRCCNF = 0x202; + NRF_RADIO->CRCPOLY = 0x11021; + NRF_RADIO->CRCINIT = 0; + NRF_RADIO->MODECNF0 = (RADIO_MODECNF0_DTX_Center << RADIO_MODECNF0_DTX_Pos) | + RADIO_MODECNF0_RU_Fast; + NRF_RADIO->SFD = 0xa7; + NRF_RADIO->FREQUENCY = 5 * (MG_802154_CHANNEL - 10); + NRF_RADIO->TXPOWER = RADIO_TXPOWER_TXPOWER_Pos8dBm; + // Correlator threshold 45 matches the RADIO reset value. A threshold of + // 20 caused false busy detections and persistent L2 retries. + NRF_RADIO->CCACTRL = + (RADIO_CCACTRL_CCAMODE_CarrierMode << RADIO_CCACTRL_CCAMODE_Pos) | + (2UL << RADIO_CCACTRL_CCACORRCNT_Pos) | + (45UL << RADIO_CCACTRL_CCACORRTHRES_Pos); + NRF_RADIO->INTENSET = RADIO_INTENSET_DISABLED_Msk; + NVIC_SetPriority(RADIO_IRQn, 0); + NVIC_EnableIRQ(RADIO_IRQn); + mgi_nrf52_listen(); + return true; +} +static size_t mgi_nrf52_tx(const void *buf, size_t len, + struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_nrf52_data *data = ifp->driver_data; + return data->mesh->strategy->tx(data->mesh, buf, len); +} + +static size_t mgi_nrf52_rx(void *buf, size_t cap, struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_nrf52_data *data = ifp->driver_data; + size_t len = data->mesh->rx_len; + if (len > cap) return 0; + if (len != 0 && buf != ifp->recv_queue.buf) + memcpy(buf, ifp->recv_queue.buf, len); + data->mesh->rx_len = 0; + return len; +} + +static bool mgi_nrf52_poll(struct mg_tcpip_if *ifp, bool s1) { + struct mg_tcpip_driver_nrf52_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + uint64_t now = mg_micros(); + char *buf; + size_t len; + (void) s1; + if (s_nrf52_txing && mg_micros() - s_nrf52_started > 60000) { + NVIC_DisableIRQ(RADIO_IRQn); + mgi_nrf52_disable(); + s_nrf52_txing = false; + s_nrf52_done = true; + mgi_nrf52_listen(); + NVIC_EnableIRQ(RADIO_IRQn); + } + if (s_nrf52_done) { + s_nrf52_done = false; + node->tx_at = now + node->strategy->radio->ifs_us; + } + node->now = now; + while (node->rx_len == 0 && (len = mg_queue_next(&s_nrf52_rx, &buf)) != 0) { + node->strategy->rx(node, buf, len); + mg_queue_del(&s_nrf52_rx, len); + } + node->strategy->tick(node, now); + return node->joined; +} + +static bool mgi_nrf52_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_nrf52_data *data = ifp->driver_data; + const struct mg_mesh *strategy = data->strategy; + uint16_t address; + address = + (uint16_t) (2 + ((unsigned) ifp->mac[4] * 256 + ifp->mac[5]) % 65532u); + if (strategy == NULL) strategy = &mg_mesh_default; + data->mesh = calloc(1, sizeof(*data->mesh)); + if (data->mesh == NULL) return false; + if (!strategy->init(data->mesh, ifp, NULL, address)) { + free(data->mesh); + data->mesh = NULL; + return false; + } + data->mesh->strategy = strategy; + data->mesh->tx = mgi_nrf52_radio_tx; + mg_queue_init(&s_nrf52_rx, s_nrf52_queue, sizeof(s_nrf52_queue)); + if (mgi_nrf52_radio_init(ifp)) return true; + strategy->free(data->mesh); + free(data->mesh); + data->mesh = NULL; + return false; +} + +struct mg_tcpip_driver mg_tcpip_driver_nrf52 = {mgi_nrf52_init, mgi_nrf52_tx, + mgi_nrf52_rx, mgi_nrf52_poll}; +#endif diff --git a/src/drivers/nrf52.h b/src/drivers/nrf52.h new file mode 100644 index 00000000..e88a7aeb --- /dev/null +++ b/src/drivers/nrf52.h @@ -0,0 +1,34 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#pragma once + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_NRF52) && MG_ENABLE_DRIVER_NRF52 + +struct mg_tcpip_driver_nrf52_data { + struct mg_mesh_node *mesh; // Driver-owned runtime state + const struct mg_mesh *strategy; // NULL selects mg_mesh_default +}; + +extern struct mg_tcpip_driver mg_tcpip_driver_nrf52; + +#define MG_TCPIP_DRIVER_NRF52_INIT(manager) \ + do { \ + static struct mg_tcpip_driver_nrf52_data driver_data_; \ + static struct mg_tcpip_if mif_; \ + MG_SET_MAC_ADDRESS(mif_.mac); \ + mif_.ip = MG_TCPIP_IP; \ + mif_.mask = MG_TCPIP_MASK; \ + mif_.gw = MG_TCPIP_GW; \ + mif_.mgr = (manager); \ + mif_.driver = &mg_tcpip_driver_nrf52; \ + mif_.driver_data = &driver_data_; \ + mg_tcpip_init((manager), &mif_); \ + MG_INFO(("Driver: nrf52, MAC: %M", mg_print_mac, mif_.mac)); \ + } while (0) + +#ifndef MG_TCPIP_DRIVER_INIT +#define MG_TCPIP_DRIVER_INIT(x) MG_TCPIP_DRIVER_NRF52_INIT(x) +#endif + +#endif diff --git a/src/drivers/rw612.h b/src/drivers/rw612.h index 568284e5..0d2d2e3f 100644 --- a/src/drivers/rw612.h +++ b/src/drivers/rw612.h @@ -27,6 +27,7 @@ struct mg_tcpip_driver_rw612_data { #define MG_DRIVER_MDC_HOLDTIME 3 #endif +#ifndef MG_TCPIP_DRIVER_INIT #define MG_TCPIP_DRIVER_INIT(mgr) \ do { \ static struct mg_tcpip_driver_rw612_data driver_data_; \ @@ -42,5 +43,6 @@ struct mg_tcpip_driver_rw612_data { mg_tcpip_init(mgr, &mif_); \ MG_INFO(("Driver: rw612, MAC: %M", mg_print_mac, mif_.mac)); \ } while (0) +#endif #endif diff --git a/src/drivers/rw612_154.h b/src/drivers/rw612_154.h deleted file mode 100644 index acc67a9d..00000000 --- a/src/drivers/rw612_154.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_RW612_154) && \ - MG_ENABLE_DRIVER_RW612_154 - -// NXP RW612 IEEE 802.15.4 radio, as an Ethernet interface: Ethernet frames -// are split into radio frames, see mg_l2_802154_tx(). The radio runs on -// CPU2: NXP BLE/802.15.4 combo firmware, an OpenThread radio co-processor -// (RCP). The firmware must be loaded and running before mg_tcpip_init(), see -// the FRDM-RW612 eth_radio_bridge tutorial. The driver talks to it over the -// CPU2 <-> CPU3 IMU mailbox: IMUMC messages carrying HDLC framed Spinel. -// driver_data: struct mg_l2_802154_data, channel and PAN ID set - -#ifndef MG_TCPIP_DRIVER_INIT -#define MG_TCPIP_DRIVER_INIT(mgr) \ - do { \ - static struct mg_l2_802154_data driver_data_; \ - static struct mg_tcpip_if mif_; \ - driver_data_.channel = MG_L2_802154_CHANNEL; \ - driver_data_.pan_id = MG_L2_802154_PAN_ID; \ - mif_.driver = &mg_tcpip_driver_rw612_154; \ - mif_.driver_data = &driver_data_; \ - MG_SET_MAC_ADDRESS(mif_.mac); \ - mg_tcpip_init(mgr, &mif_); \ - MG_INFO(("Driver: rw612_154, channel %d", MG_L2_802154_CHANNEL)); \ - } while (0) -#endif - -#endif diff --git a/src/drivers/rw612_154.c b/src/drivers/rw612w.c similarity index 71% rename from src/drivers/rw612_154.c rename to src/drivers/rw612w.c index c37ec5a9..3659b1fe 100644 --- a/src/drivers/rw612_154.c +++ b/src/drivers/rw612w.c @@ -1,7 +1,16 @@ #include "net_builtin.h" +#include "l2_mesh.h" +#include "log.h" -#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_RW612_154) && \ - MG_ENABLE_DRIVER_RW612_154 +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_RW612W) && \ + MG_ENABLE_DRIVER_RW612W + +#include "sha256.h" + +#ifdef ENET +#undef ENET +#endif +#include // The 802.15.4 radio runs NXP combo firmware on CPU2, an OpenThread RCP. We // are CPU3. Messages go through the IMU mailbox: IMU messages carry IMUMC @@ -16,13 +25,13 @@ struct mg_rw612_imu { }; #define MGI_IMU ((struct mg_rw612_imu *) 0x443ff804) // CPU3 -> CPU2 #define MGI_IMU_PEER ((struct mg_rw612_imu *) 0x443ff818) // CPU2 -> CPU3 -#define MGI_IMU_LOCKED MG_BIT(0) // STATUS: FIFO locked -#define MGI_IMU_ALMOST_FULL MG_BIT(1) // STATUS: FIFO almost full -#define MGI_IMU_EMPTY MG_BIT(3) // STATUS: FIFO empty +#define MGI_IMU_LOCKED MG_BIT(0) // STATUS: FIFO locked +#define MGI_IMU_ALMOST_FULL MG_BIT(1) // STATUS: FIFO almost full +#define MGI_IMU_EMPTY MG_BIT(3) // STATUS: FIFO empty #define MGI_IMU_WR_PTR(s) (((s) >> 16) & 15U) -#define MGI_IMU_INT_CLR MG_BIT(0) // CNTL: clear message ready IRQ -#define MGI_IMU_FLUSH MG_BIT(16) // CNTL: flush send FIFO -#define MGI_IMU_WAIT_FOR_ACK MG_BIT(17) // CNTL: lock send FIFO +#define MGI_IMU_INT_CLR MG_BIT(0) // CNTL: clear message ready IRQ +#define MGI_IMU_FLUSH MG_BIT(16) // CNTL: flush send FIFO +#define MGI_IMU_WAIT_FOR_ACK MG_BIT(17) // CNTL: lock send FIFO #define MGI_IMU_WATERMARK(x) ((uint32_t) (x) << 20) #define MGI_IMU_WATERMARK_MASK (15U << 20) @@ -73,8 +82,10 @@ enum { MGI_PROP_NCP_VERSION = 2, MGI_PROP_PHY_ENABLED = 0x20, MGI_PROP_PHY_CHAN = 0x21, + MGI_PROP_SADDR = 0x35, // Short address, little endian MGI_PROP_PANID = 0x36, MGI_PROP_RAW_STREAM = 0x37, + MGI_PROP_PROMISCUOUS = 0x38, MGI_PROP_STREAM_RAW = 0x71 }; #define MGI_RESET_STACK 2 // RESET argument: reset the RCP stack only @@ -82,7 +93,6 @@ enum { #define MGI_STATUS_RESET_MAX 127 #define MGI_TX_TIMEOUT_MS 200 // Wait for transmit done, incl. retries #define MGI_RSP_TIMEOUT_MS 500 -#define MGI_RXQ_CNT 16 // A whole Ethernet frame, 15 radio frames // Driver state, one radio struct mg_rw612 { @@ -98,11 +108,13 @@ struct mg_rw612 { bool rsp; // Response to the pending request received uint32_t rsp_status; // Its status, if LAST_STATUS, else 0 char version[64]; // RCP version string - uint8_t rxq[MGI_RXQ_CNT][127]; // Received frames, no FCS - uint8_t rxlen[MGI_RXQ_CNT]; // Their lengths - uint8_t rxhead, rxtail; // Queue indexes + bool txing; // A send is on, see mg_rw612_start() + uint64_t txstart; // Its start, ms + struct mg_tcpip_if *ifp; // Our interface, for the L2 receive pool }; static struct mg_rw612 s_rw612; +static struct mg_queue s_rw612w_rx; +static char s_rw612w_queue[2048]; static void mg_rw612_imu_send(uint8_t type, uint8_t subtype, const uint32_t *payload, uint8_t len) { @@ -126,12 +138,15 @@ static void mg_rw612_control(uint8_t subtype, uint32_t a, uint32_t b, } static void mg_rw612_queue_rx(const uint8_t *frame, size_t len) { - struct mg_rw612 *s = &s_rw612; - uint8_t next = (uint8_t) ((s->rxhead + 1) % MGI_RXQ_CNT); - if (next == s->rxtail || len > sizeof(s->rxq[0])) return; // Full: drop - memcpy(s->rxq[s->rxhead], frame, len); - s->rxlen[s->rxhead] = (uint8_t) len; - s->rxhead = next; + { + char *dst; + if (mg_queue_book(&s_rw612w_rx, &dst, len) >= len) { + memcpy(dst, frame, len); + mg_queue_add(&s_rw612w_rx, len); + } else { + s_rw612.ifp->ndrop++; + } + } } // Spinel packed unsigned integer: 7 bits per byte, LSB first @@ -293,14 +308,13 @@ static bool mg_rw612_imumc_send(const uint8_t *p, size_t len) { return true; } -// Send a Spinel frame, HDLC encoded, and wait for the response with the -// same transaction ID. Return false on timeout -static bool mg_rw612_request(uint8_t *f, size_t len, uint32_t timeout_ms) { +// Send a Spinel frame, HDLC encoded, with a new transaction ID. Its +// response sets rsp, see mg_rw612_spinel() +static bool mg_rw612_post(uint8_t *f, size_t len) { struct mg_rw612 *s = &s_rw612; uint8_t buf[2 * 160 + 6]; uint16_t fcs = 0xffff; size_t i, n = 0; - uint64_t expire = mg_millis() + timeout_ms; s->tid = (uint8_t) (s->tid % 15 + 1); f[0] = (uint8_t) (0x80 | s->tid); // Header: flag, interface 0, TID s->rsp = false; @@ -318,9 +332,15 @@ static bool mg_rw612_request(uint8_t *f, size_t len, uint32_t timeout_ms) { buf[n++] = c; } buf[n++] = 0x7e; - if (!mg_rw612_imumc_send(buf, n)) return false; - while (!s->rsp && mg_millis() < expire) mg_rw612_poll_imu(); - return s->rsp; + return mg_rw612_imumc_send(buf, n); +} + +// Send a Spinel frame and wait for its response. Return false on timeout +static bool mg_rw612_request(uint8_t *f, size_t len, uint32_t timeout_ms) { + uint64_t expire = mg_millis() + timeout_ms; + if (!mg_rw612_post(f, len)) return false; + while (!s_rw612.rsp && mg_millis() < expire) mg_rw612_poll_imu(); + return s_rw612.rsp; } static bool mg_rw612_set(uint32_t prop, const uint8_t *val, size_t len) { @@ -338,27 +358,43 @@ static bool mg_rw612_fail(const char *msg) { return false; } -static bool mg_rw612_154_init(struct mg_tcpip_if *ifp) { +// Start one raw transmission. The RCP adds the FCS and reports completion. +static size_t mgi_rw612w_radio_tx(struct mg_tcpip_if *ifp, const void *buf, + size_t len) { + uint8_t f[3 + 2 + 127 + 4]; + size_t n = 3; + if (len > 125 || s_rw612.txing) return 0; + f[1] = MGI_CMD_SET, f[2] = MGI_PROP_STREAM_RAW; + f[n++] = (uint8_t) (len + 2), f[n++] = 0; // PSDU length, incl. FCS + memcpy(f + n, buf, len), n += len; + f[n++] = 0, f[n++] = 0; // FCS, filled in by the radio + (void) ifp; + f[n++] = MG_802154_CHANNEL; + f[n++] = 0; // Max CSMA backoffs + f[n++] = 0; // Max frame retries + f[n++] = 0; // CSMA disabled for scheduled turns + if (!mg_rw612_post(f, n)) return 0; + s_rw612.txing = true, s_rw612.txstart = mg_millis(); + return len; +} + +static bool mgi_rw612w_radio_init(struct mg_tcpip_if *ifp) { struct mg_rw612 *s = &s_rw612; - struct mg_l2_802154_data *d = - (struct mg_l2_802154_data *) ifp->driver_data; - uint8_t f[8], one = 1, pan[2]; + struct mg_tcpip_driver_rw612w_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + uint8_t f[8], one = 1, zero = 0, pan[2], addr[2], channel = MG_802154_CHANNEL; uint64_t expire; - if (d == NULL || d->channel < 11 || d->channel > 26) { - MG_ERROR(("Set driver_data, channel 11..26")); - return false; - } if (MGI_MBOX->magic[0] != MGI_SYNC_MAGIC) { MG_ERROR(("Radio firmware is not running on CPU2")); return false; } memset(s, 0, sizeof(*s)); - s->free_bufs = 0xffff; + s->free_bufs = 0xffff, s->ifp = ifp; // Reset our send FIFO, drop stale messages. See IMU_Init() MGI_IMU->CNTL |= MGI_IMU_FLUSH; MGI_IMU->CNTL &= ~(MGI_IMU_FLUSH | MGI_IMU_WAIT_FOR_ACK); - MGI_IMU->CNTL = (MGI_IMU->CNTL & ~MGI_IMU_WATERMARK_MASK) | - MGI_IMU_WATERMARK(15); + MGI_IMU->CNTL = + (MGI_IMU->CNTL & ~MGI_IMU_WATERMARK_MASK) | MGI_IMU_WATERMARK(15); while ((MGI_IMU_PEER->STATUS & MGI_IMU_EMPTY) == 0) (void) MGI_IMU->RD_MSG; // Synchronise the IMU link, then connect the IMUMC endpoints MGI_MBOX->magic[1] = MGI_SYNC_MAGIC; @@ -385,10 +421,15 @@ static bool mg_rw612_154_init(struct mg_tcpip_if *ifp) { if (!mg_rw612_set(MGI_PROP_PHY_ENABLED, &one, 1)) { return mg_rw612_fail("PHY enable failed"); } - pan[0] = (uint8_t) d->pan_id, pan[1] = (uint8_t) (d->pan_id >> 8); - if (!mg_rw612_set(MGI_PROP_PHY_CHAN, &d->channel, 1) || - !mg_rw612_set(MGI_PROP_PANID, pan, 2)) { - return mg_rw612_fail("Channel or PAN ID set failed"); + pan[0] = (uint8_t) MG_802154_PAN_ID; + pan[1] = (uint8_t) (MG_802154_PAN_ID >> 8); + addr[0] = (uint8_t) node->radio_address; + addr[1] = (uint8_t) (node->radio_address >> 8); + if (!mg_rw612_set(MGI_PROP_PHY_CHAN, &channel, 1) || + !mg_rw612_set(MGI_PROP_PANID, pan, 2) || + !mg_rw612_set(MGI_PROP_SADDR, addr, 2) || + !mg_rw612_set(MGI_PROP_PROMISCUOUS, &zero, 1)) { + return mg_rw612_fail("Channel, PAN ID or address set failed"); } if (!mg_rw612_set(MGI_PROP_RAW_STREAM, &one, 1)) { return mg_rw612_fail("Raw stream enable failed"); @@ -396,55 +437,77 @@ static bool mg_rw612_154_init(struct mg_tcpip_if *ifp) { return true; } -// Send one radio frame: MAC header and payload, no FCS -static size_t mg_rw612_send(const uint8_t *p, size_t len, +static size_t mgi_rw612w_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) { - struct mg_l2_802154_data *d = - (struct mg_l2_802154_data *) ifp->driver_data; - uint8_t f[3 + 2 + 127 + 4]; - size_t n = 3; - f[1] = MGI_CMD_SET, f[2] = MGI_PROP_STREAM_RAW; - f[n++] = (uint8_t) (len + 2), f[n++] = 0; // PSDU length, incl. FCS - memcpy(f + n, p, len), n += len; - f[n++] = 0, f[n++] = 0; // FCS, filled in by the radio - f[n++] = d->channel; - f[n++] = 4; // Max CSMA backoffs - f[n++] = 0; // Max frame retries: broadcast frames are not acknowledged - f[n++] = 1; // CSMA-CA enabled - if (!mg_rw612_request(f, n, MGI_TX_TIMEOUT_MS)) { - MG_ERROR(("TX timeout")); - return 0; - } - if (s_rw612.rsp_status != 0) { - MG_VERBOSE(("TX failed, status %lu", (unsigned long) s_rw612.rsp_status)); - return 0; - } + struct mg_tcpip_driver_rw612w_data *data = ifp->driver_data; + return data->mesh->strategy->tx(data->mesh, buf, len); +} + +static size_t mgi_rw612w_rx(void *buf, size_t cap, struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_rw612w_data *data = ifp->driver_data; + size_t len = data->mesh->rx_len; + if (len > cap) return 0; + if (len != 0 && buf != ifp->recv_queue.buf) + memcpy(buf, ifp->recv_queue.buf, len); + data->mesh->rx_len = 0; return len; } -static size_t mg_rw612_154_tx(const void *buf, size_t len, - struct mg_tcpip_if *ifp) { - return mg_l2_802154_tx(ifp, buf, len, mg_rw612_send); -} - -static size_t mg_rw612_154_rx(void *buf, size_t len, struct mg_tcpip_if *ifp) { - struct mg_rw612 *s = &s_rw612; - size_t res = 0; +static bool mgi_rw612w_poll(struct mg_tcpip_if *ifp, bool s1) { + struct mg_tcpip_driver_rw612w_data *data = ifp->driver_data; + struct mg_mesh_node *node = data->mesh; + uint64_t now = mg_micros(); + char *buf; + size_t len; + (void) s1; + bool was_sending = s_rw612.txing; mg_rw612_poll_imu(); - while (s->rxtail != s->rxhead && res == 0) { - res = mg_l2_802154_rx(ifp, s->rxq[s->rxtail], s->rxlen[s->rxtail], buf, - len); - s->rxtail = (uint8_t) ((s->rxtail + 1) % MGI_RXQ_CNT); + if (s_rw612.txing && + (s_rw612.rsp || mg_millis() > s_rw612.txstart + MGI_TX_TIMEOUT_MS)) { + if (!s_rw612.rsp) MG_ERROR(("TX timeout")); + s_rw612.txing = false; } - return res; + if (was_sending && !s_rw612.txing) + node->tx_at = now + node->strategy->radio->ifs_us; + node->now = now; + while (node->rx_len == 0 && (len = mg_queue_next(&s_rw612w_rx, &buf)) != 0) { + node->strategy->rx(node, buf, len); + mg_queue_del(&s_rw612w_rx, len); + } + node->strategy->tick(node, now); + return node->joined; } -static bool mg_rw612_154_poll(struct mg_tcpip_if *ifp, bool s1) { - (void) ifp, (void) s1; - return s_rw612.up; +static bool mgi_rw612w_init(struct mg_tcpip_if *ifp) { + struct mg_tcpip_driver_rw612w_data *data = ifp->driver_data; + const struct mg_mesh *strategy = data->strategy; + uint8_t uid[FSL_OCOTP_UID_LENGTH] = {0}, digest[32]; + uint32_t uid_len = sizeof(uid); + uint16_t address; + OCOTP_ReadUniqueID(uid, &uid_len); + mg_sha256(digest, uid, sizeof(uid)); + memcpy(ifp->mac, digest, 6); + ifp->mac[0] = 0x02; + address = + (uint16_t) (2 + ((unsigned) ifp->mac[4] * 256 + ifp->mac[5]) % 65532u); + if (strategy == NULL) strategy = &mg_mesh_default; + data->mesh = calloc(1, sizeof(*data->mesh)); + if (data->mesh == NULL) return false; + if (!strategy->init(data->mesh, ifp, NULL, address)) { + free(data->mesh); + data->mesh = NULL; + return false; + } + data->mesh->strategy = strategy; + data->mesh->tx = mgi_rw612w_radio_tx; + mg_queue_init(&s_rw612w_rx, s_rw612w_queue, sizeof(s_rw612w_queue)); + if (mgi_rw612w_radio_init(ifp)) return true; + strategy->free(data->mesh); + free(data->mesh); + data->mesh = NULL; + return false; } -struct mg_tcpip_driver mg_tcpip_driver_rw612_154 = { - mg_rw612_154_init, mg_rw612_154_tx, mg_rw612_154_rx, mg_rw612_154_poll}; - +struct mg_tcpip_driver mg_tcpip_driver_rw612w = { + mgi_rw612w_init, mgi_rw612w_tx, mgi_rw612w_rx, mgi_rw612w_poll}; #endif diff --git a/src/drivers/rw612w.h b/src/drivers/rw612w.h new file mode 100644 index 00000000..084e3e7c --- /dev/null +++ b/src/drivers/rw612w.h @@ -0,0 +1,35 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#pragma once + +#if MG_ENABLE_TCPIP && defined(MG_ENABLE_DRIVER_RW612W) && \ + MG_ENABLE_DRIVER_RW612W + +struct mg_tcpip_driver_rw612w_data { + struct mg_mesh_node *mesh; // Driver-owned runtime state + const struct mg_mesh *strategy; // NULL selects mg_mesh_default +}; + +extern struct mg_tcpip_driver mg_tcpip_driver_rw612w; + +#define MG_TCPIP_DRIVER_RW612W_INIT(manager) \ + do { \ + static struct mg_tcpip_driver_rw612w_data driver_data_; \ + static struct mg_tcpip_if mif_; \ + MG_SET_MAC_ADDRESS(mif_.mac); \ + mif_.ip = MG_TCPIP_IP; \ + mif_.mask = MG_TCPIP_MASK; \ + mif_.gw = MG_TCPIP_GW; \ + mif_.mgr = (manager); \ + mif_.driver = &mg_tcpip_driver_rw612w; \ + mif_.driver_data = &driver_data_; \ + mg_tcpip_init((manager), &mif_); \ + MG_INFO(("Driver: rw612w, MAC: %M", mg_print_mac, mif_.mac)); \ + } while (0) + +#ifndef MG_TCPIP_DRIVER_INIT +#define MG_TCPIP_DRIVER_INIT(x) MG_TCPIP_DRIVER_RW612W_INIT(x) +#endif + +#endif diff --git a/src/l2_802154.c b/src/l2_802154.c deleted file mode 100644 index 02b67939..00000000 --- a/src/l2_802154.c +++ /dev/null @@ -1,60 +0,0 @@ -#include "l2.h" -#include "net_builtin.h" - -#if MG_ENABLE_TCPIP - -// Radio frame: 802.15.4 MAC header, 15 bytes: data frame, PAN ID compression, -// broadcast destination, our EUI-64 made from MAC as source. Then 2 bytes: -// tag, fragment index << 4 | last fragment index. Then data. 125 bytes max, -// 127 minus FCS -#define MGI_802154_HDR 17 -#define MGI_802154_DATA (125 - MGI_802154_HDR) - -size_t mg_l2_802154_tx(struct mg_tcpip_if *ifp, const void *buf, size_t len, - size_t (*send_fn)(const uint8_t *, size_t, - struct mg_tcpip_if *)) { - struct mg_l2_802154_data *d = - (struct mg_l2_802154_data *) ifp->driver_data; - const uint8_t *p = (const uint8_t *) buf, *m = ifp->mac; - uint8_t f[MGI_802154_HDR + MGI_802154_DATA]; - size_t i, n, last = (len - 1) / MGI_802154_DATA; - if (len == 0 || last > 15) return 0; - d->tag++; - for (i = 0; i <= last; i++) { - n = i == last ? len - i * MGI_802154_DATA : MGI_802154_DATA; - f[0] = 0x41, f[1] = 0xd8, f[2] = d->seq++; // 2006 frame, see above - f[3] = (uint8_t) d->pan_id, f[4] = (uint8_t) (d->pan_id >> 8); - f[5] = f[6] = 0xff; - f[7] = m[5], f[8] = m[4], f[9] = m[3], f[10] = 0xfe, f[11] = 0xff; - f[12] = m[2], f[13] = m[1], f[14] = m[0]; // Little endian EUI-64 - f[15] = d->tag, f[16] = (uint8_t) ((i << 4) | last); - memcpy(f + MGI_802154_HDR, p + i * MGI_802154_DATA, n); - if (send_fn(f, MGI_802154_HDR + n, ifp) == 0) return 0; - } - return len; -} - -size_t mg_l2_802154_rx(struct mg_tcpip_if *ifp, const uint8_t *f, size_t n, - void *buf, size_t len) { - struct mg_l2_802154_data *d = - (struct mg_l2_802154_data *) ifp->driver_data; - size_t idx, last; - if (n <= MGI_802154_HDR || f[0] != 0x41 || f[1] != 0xd8 || - f[3] != (uint8_t) d->pan_id || f[4] != (uint8_t) (d->pan_id >> 8)) - return 0; // Not ours - idx = f[16] >> 4, last = f[16] & 15; - if (idx == 0) d->rxtag = f[15], d->rxnext = 0, d->rxlen = 0; - if (idx == 0) memcpy(d->rxsrc, f + 7, sizeof(d->rxsrc)); - n -= MGI_802154_HDR; - if (idx != d->rxnext || idx > last || f[15] != d->rxtag || - memcmp(d->rxsrc, f + 7, sizeof(d->rxsrc)) != 0 || - d->rxlen + n > sizeof(d->rxbuf)) - return 0; // Missed a fragment, or from another sender - memcpy(d->rxbuf + d->rxlen, f + MGI_802154_HDR, n); - d->rxlen = (uint16_t) (d->rxlen + n), d->rxnext++; - if (idx < last || d->rxlen > len) return 0; - memcpy(buf, d->rxbuf, d->rxlen); - return d->rxlen; -} - -#endif diff --git a/src/l2_mesh.c b/src/l2_mesh.c new file mode 100644 index 00000000..76c7f733 --- /dev/null +++ b/src/l2_mesh.c @@ -0,0 +1,616 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "l2_mesh.h" +#include "log.h" +#include "printf.h" +#include "util.h" + +#if MG_ENABLE_TCPIP + +#define MGI_ETH_MAX 1514 +#define MGI_TX_DST_LEN 2 + +// Mesh header, after the radio header: type, then burst: the fragment's +// index << 4 | the last fragment's index. Common to every radio +#define MG_MESH_HDR_LEN 2 + +#define MG_MESH_FRAME_HDR_LEN (MG_802154_HEADER_LEN + MG_MESH_HDR_LEN) + +// MAC header, 9 bytes: frame control 0x8841, a data frame, PAN ID +// compression, short destination and source addresses; sequence number, 0, +// as nothing uses it yet; PAN ID; destination; source. Little endian, as +// the standard says +static void mgi_802154_print_hdr(const struct mg_radio *radio, uint8_t *buf, + uint16_t dst, uint16_t src) { + buf[0] = 0x41, buf[1] = 0x88, buf[2] = 0; + buf[3] = (uint8_t) MG_802154_PAN_ID; + buf[4] = (uint8_t) (MG_802154_PAN_ID >> 8); + buf[5] = (uint8_t) dst, buf[6] = (uint8_t) (dst >> 8); + buf[7] = (uint8_t) src, buf[8] = (uint8_t) (src >> 8); + (void) radio; +} + +static size_t mgi_802154_parse_hdr(const struct mg_radio *radio, + const uint8_t *buf, size_t len, + uint16_t *dst, uint16_t *src) { + (void) radio; + if (len < MG_802154_HEADER_LEN || buf[0] != 0x41 || buf[1] != 0x88 || + buf[3] != (uint8_t) MG_802154_PAN_ID || + buf[4] != (uint8_t) (MG_802154_PAN_ID >> 8)) { + return 0; + } + *dst = (uint16_t) (buf[5] | buf[6] << 8); + *src = (uint16_t) (buf[7] | buf[8] << 8); + return MG_802154_HEADER_LEN; +} + +const struct mg_radio mg_radio_802154 = { + "802.15.4", // name + 125, // max_frame_size: 127 minus FCS + 250000, // bitrate + 8, // overhead + 320, // slot_us + 128, // cca_us + 100, // turnaround_us + 640, // ifs_us + 3, // backoff_min + 5, // backoff_max + true, // csma + MG_802154_HEADER_LEN, // header_len + mgi_802154_print_hdr, // print_hdr + mgi_802154_parse_hdr // parse_hdr +}; + +static uint64_t mgi_mesh_b_airtime(size_t len) { + const struct mg_radio *radio = &mg_radio_802154; + return ((uint64_t) (len + radio->overhead) * 8000000 + radio->bitrate - 1) / + radio->bitrate; +} + +// Each queue record holds a destination and a complete burst. +static size_t mgi_mesh_b_queue(struct mg_mesh_node *node, struct mg_queue *q, + const void *buf, size_t len, uint16_t dst, + uint8_t type) { + const struct mg_radio *radio = &mg_radio_802154; + const uint8_t *eth = (const uint8_t *) buf; + size_t hdr = MG_MESH_FRAME_HDR_LEN, cap = radio->max_frame_size - hdr; + size_t frames, size, i; + char *record; + uint8_t *out; + if (len < 14 || len > MGI_ETH_MAX) return 0; + frames = (len + cap - 1) / cap; + size = MGI_TX_DST_LEN + len + frames * (1 + hdr); + if (mg_queue_next(q, NULL) == 0) mg_queue_init(q, q->buf, q->size); + if (mg_queue_book(q, &record, size) < size) return 0; + out = (uint8_t *) record; + *out++ = (uint8_t) (dst >> 8); + *out++ = (uint8_t) dst; + for (i = 0; i < frames; i++) { + size_t chunk = len - i * cap; + if (chunk > cap) chunk = cap; + *out++ = (uint8_t) (hdr + chunk); + radio->print_hdr(radio, out, dst, node->radio_address); + out[radio->header_len] = type; + out[radio->header_len + 1] = (uint8_t) (i << 4 | (frames - 1)); + memcpy(out + hdr, eth + i * cap, chunk); + out += hdr + chunk; + } + mg_queue_add(q, size); + return len; +} + +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#define MGI_JOIN_WINDOW_US 3000 +#define MGI_JOIN_TURN_US 192 +#define MGI_JOIN_JITTER_US 640 +#define MGI_DATA_QUEUE_SIZE 2048 +#define MGI_TX_RETRY_US 32 +// Our frames: the radio header, with the radio addresses, then the mesh +// header MG_MESH_HDR_LEN, then the payload +#define MGI_JOIN_UP_LEN \ + (MG_MESH_FRAME_HDR_LEN + 6) // MGI_JOIN_UP's payload: MAC addr +#define MGI_MAX_FRAGMENTS 14 + +enum { MGI_JOIN_DOWN = 1, MGI_JOIN_UP, MGI_DATA_DOWN, MGI_DATA_UP }; + +enum { MGI_TURN_IDLE, MGI_TURN_JOIN, MGI_TURN_DATA }; + +struct mgi_mesh_b_data { + uint8_t rx_buf[MGI_ETH_MAX]; // Ethernet frame being reassembled + uint8_t rx_expected; // Expected index | last index, 0xff if idle + struct mg_mesh_node *node; // Owner of the TCP/IP interface + struct mg_queue tx; // One record per burst of RF fragments + size_t tx_offset; // Next queued fragment; zero for an empty frame + uint64_t tx_at; // Next fragment or JOIN transmission time + uint64_t deadline; // End of the current JOIN or DATA turn + struct mgi_mesh_b_peer *fanout; // Next broadcast recipient + char tx_buf[MGI_DATA_QUEUE_SIZE]; // Transmit queue storage + uint32_t random; // PRNG state for join timing + struct mgi_mesh_b_peer *members; // Bridge membership list + struct mgi_mesh_b_peer *current; // Member whose turn is active or next + int8_t missed_rounds; // -1 after a grant; otherwise completed rounds missed + uint16_t coordinator; // Coordinator radio address + uint8_t phase; // MGI_TURN_IDLE, MGI_TURN_JOIN, or MGI_TURN_DATA +}; + +struct mgi_mesh_b_peer { + struct mgi_mesh_b_peer *next; // Next member in round-robin order + uint16_t radio_address; // Source address supplied in MGI_JOIN_UP + uint8_t mac[6]; // Ethernet MAC supplied in MGI_JOIN_UP + uint8_t misses; // Turns in a row it did not answer +}; + +static uint32_t mgi_mesh_b_random(struct mg_mesh_node *node, + struct mgi_mesh_b_data *data) { + uint32_t *random = &data->random; + if (*random == 0) { + size_t i; + *random = (uint32_t) mg_now(); + for (i = 0; i < 6; i++) *random = *random * 33u + node->radio->mac[i]; + if (*random == 0) *random = 1; + } + *random ^= *random << 13; + *random ^= *random >> 17; + *random ^= *random << 5; + return *random; +} + +static const struct mg_radio *const s_mesh_default_radio = &mg_radio_802154; + +// Read a frame's radio addresses to dst and src, see mg_radio::parse_hdr(), and +// return its type, from the mesh header. 0: not a frame of ours +static unsigned mgi_mesh_b_frame_parse(const uint8_t *frame, size_t len, + uint16_t *dst, uint16_t *src) { + if (len < MG_MESH_FRAME_HDR_LEN || + s_mesh_default_radio->parse_hdr(s_mesh_default_radio, frame, len, dst, + src) != + s_mesh_default_radio->header_len) { + return 0; + } + return frame[s_mesh_default_radio->header_len]; +} + +// Write a frame's headers: the radio's, to dst from src, then ours: type, +// and burst: high nibble is the index, low nibble is the last index +static void mgi_mesh_b_frame_header(uint8_t *frame, uint16_t dst, uint16_t src, + uint8_t type, uint8_t burst) { + size_t h = s_mesh_default_radio->header_len; + s_mesh_default_radio->print_hdr(s_mesh_default_radio, frame, dst, src); + frame[h] = type, frame[h + 1] = burst; +} + +static bool mgi_mesh_b_control_send(struct mg_mesh_node *node, uint16_t dst, + uint8_t command) { + struct mg_tcpip_if *ifp = node->radio; + uint8_t frame[MGI_JOIN_UP_LEN] = {0}; + size_t len = MG_MESH_FRAME_HDR_LEN; + mgi_mesh_b_frame_header(frame, dst, node->radio_address, command, 0); + if (command == MGI_JOIN_UP) { + memcpy(frame + MG_MESH_FRAME_HDR_LEN, node->radio->mac, 6); + len = MGI_JOIN_UP_LEN; + } + return node->now >= node->tx_at && node->tx(ifp, frame, len) == len; +} + +static size_t mgi_mesh_b_fragment_capacity(void) { + return s_mesh_default_radio->max_frame_size - MG_MESH_FRAME_HDR_LEN; +} + +static uint64_t mgi_mesh_b_burst_time(size_t len) { + size_t cap = mgi_mesh_b_fragment_capacity(); + size_t frames = len == 0 ? 1 : (len + cap - 1) / cap; + return (frames - 1) * + (mgi_mesh_b_airtime(s_mesh_default_radio->max_frame_size) + + s_mesh_default_radio->ifs_us) + + mgi_mesh_b_airtime(MG_MESH_FRAME_HDR_LEN + len - (frames - 1) * cap); +} + +static void mgi_mesh_b_tx_finish(struct mgi_mesh_b_data *data) { + if (data->tx_offset != 0) { + if (data->fanout != NULL) data->fanout = data->fanout->next; + if (data->fanout == NULL) { + size_t len = mg_queue_next(&data->tx, NULL); + mg_queue_del(&data->tx, len); + } + } + data->tx_offset = 0; + data->tx_at = (uint64_t) -1; +} + +static void mgi_mesh_b_tx_step(struct mgi_mesh_b_data *data, uint64_t now) { + struct mg_mesh_node *node = data->node; + struct mg_tcpip_if *radio = node->radio; + uint8_t empty[MG_MESH_FRAME_HDR_LEN], *frame = empty; + size_t len = MG_MESH_FRAME_HDR_LEN, record_len = 0; + uint16_t dst; + uint8_t command; + if (now < data->tx_at) return; + if (now >= data->deadline) { + mgi_mesh_b_tx_finish(data); + return; + } + dst = node->lan == NULL ? data->coordinator : data->current->radio_address; + command = node->lan == NULL ? MGI_DATA_UP : MGI_DATA_DOWN; + if (data->tx_offset != 0) { + char *record; + uint8_t *part; + record_len = mg_queue_next(&data->tx, &record); + part = (uint8_t *) record + data->tx_offset; + len = part[0]; + frame = part + 1; + s_mesh_default_radio->print_hdr(s_mesh_default_radio, frame, dst, + node->radio_address); + } else { + mgi_mesh_b_frame_header(frame, dst, node->radio_address, command, 0); + } + if (now < data->node->tx_at || data->node->tx(radio, frame, len) != len) { + data->tx_at = now + MGI_TX_RETRY_US; + return; + } + if (data->tx_offset == 0 || (data->tx_offset += len + 1) == record_len) { + mgi_mesh_b_tx_finish(data); + } else { + data->tx_at = now + mgi_mesh_b_airtime(len) + s_mesh_default_radio->ifs_us; + } +} + +static void mgi_mesh_b_tx_begin(struct mgi_mesh_b_data *data, bool burst, + uint64_t now, uint64_t deadline) { + data->phase = MGI_TURN_DATA; + data->tx_offset = burst ? MGI_TX_DST_LEN : 0; + data->tx_at = now; + data->deadline = deadline; +} + +// Return the assembled length, zero for an empty frame, or -1 if incomplete +// or invalid. Full non-final fragments make the write offset derivable. +static int mgi_mesh_b_data_receive(struct mgi_mesh_b_data *data, + const uint8_t *frame, size_t len, + uint64_t now) { + unsigned burst = frame[MG_MESH_FRAME_HDR_LEN - 1], index = burst >> 4, + last = burst & 15; + size_t chunk = len - MG_MESH_FRAME_HDR_LEN, + cap = mgi_mesh_b_fragment_capacity(); + size_t offset = index * cap; + if (index == 0) { + data->rx_expected = (uint8_t) burst; + if (data->node->lan == NULL) + data->deadline = now + mgi_mesh_b_burst_time(MGI_ETH_MAX) + + 2 * s_mesh_default_radio->ifs_us + MGI_JOIN_TURN_US; + } + if (last >= MGI_MAX_FRAGMENTS || index > last || burst != data->rx_expected || + chunk > cap || now > data->deadline || (index < last && chunk != cap) || + (last != 0 && chunk == 0) || offset + chunk > sizeof(data->rx_buf)) { + data->rx_expected = 0xff; + return -1; + } + memcpy(data->rx_buf + offset, frame + MG_MESH_FRAME_HDR_LEN, chunk); + if (index < last) { + data->rx_expected = (uint8_t) (burst + 0x10); + return -1; + } + data->rx_expected = 0xff; + return offset + chunk == 0 || offset + chunk >= 14 ? (int) (offset + chunk) + : -1; +} + +// Bridge scheduling and Ethernet forwarding. + +#define MGI_MAX_MISSES 3 // Turns in a row without an answer: remove the member + +static uint16_t mgi_mesh_b_join_field(const uint8_t *p) { + return (uint16_t) ((unsigned) p[0] * 256 + p[1]); +} + +// Return chaddr for an Ethernet DHCP server reply, even when broadcast. +static const uint8_t *mgi_mesh_b_dhcp_client(const uint8_t *frame, size_t len) { + size_t ip_len, ihl, udp_len; + const uint8_t *udp, *dhcp; + if (len < 34 || mgi_mesh_b_join_field(frame + 12) != 0x0800 || + (frame[14] >> 4) != 4 || frame[23] != 17 || + (mgi_mesh_b_join_field(frame + 20) & 0x3fff) != 0) + return NULL; + ip_len = mgi_mesh_b_join_field(frame + 16); + ihl = (size_t) (frame[14] & 15) * 4; + if (ihl < 20 || ip_len > len - 14 || ip_len < ihl + 8 + 240) return NULL; + udp = frame + 14 + ihl; + udp_len = mgi_mesh_b_join_field(udp + 4); + if (mgi_mesh_b_join_field(udp) != 67 || + mgi_mesh_b_join_field(udp + 2) != 68 || udp_len < 8 + 240 || + udp_len > ip_len - ihl) + return NULL; + dhcp = udp + 8; + if (dhcp[0] != 2 || dhcp[1] != 1 || dhcp[2] != 6 || + memcmp(dhcp + 236, "\x63\x82\x53\x63", 4) != 0) + return NULL; + return dhcp + 28; +} + +static size_t mgi_mesh_b_enqueue(struct mg_mesh_node *node, const void *buf, + size_t len) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) node->data; + const uint8_t *frame = (const uint8_t *) buf; + if (node->lan == NULL) + return mgi_mesh_b_queue(node, &data->tx, buf, len, data->coordinator, + MGI_DATA_UP); + if (len >= 14 && len <= MGI_ETH_MAX) { + const uint8_t *client = mgi_mesh_b_dhcp_client(frame, len); + const uint8_t *dst = client == NULL ? frame : client; + bool multicast = client == NULL && (frame[0] & 1) != 0; + struct mgi_mesh_b_peer *peer; + for (peer = data->members; peer != NULL; peer = peer->next) { + if (multicast || memcmp(dst, peer->mac, 6) == 0) { + uint16_t addr = multicast ? 0xffffu : peer->radio_address; + return mgi_mesh_b_queue(node, &data->tx, buf, len, addr, MGI_DATA_DOWN); + } + } + } + return 0; +} + +static void mgi_mesh_b_bridge_queue_lan(struct mg_mesh_node *bridge) { + struct mg_queue *rx = &bridge->lan->recv_queue; + char *buf; + size_t len; + while ((len = mg_queue_next(rx, &buf)) > 0) { + bridge->strategy->tx(bridge, buf, len); + mg_queue_del(rx, len); + } +} + +static void mgi_mesh_b_bridge_rx(uint64_t now, struct mg_mesh_node *b, + const uint8_t *frame, size_t len) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) b->data; + struct mgi_mesh_b_peer *peer; + uint16_t dst = 0, src = 0; + unsigned command = mgi_mesh_b_frame_parse(frame, len, &dst, &src); + if (command == MGI_DATA_UP && dst == b->radio_address && + data->phase == MGI_TURN_DATA && data->current != NULL && + src == data->current->radio_address && now <= data->deadline) { + int received = mgi_mesh_b_data_receive(data, frame, len, now); + if (received < 0) return; + if (received != 0) { + b->lan->driver->tx(data->rx_buf, (size_t) received, b->lan); + } + data->current->misses = 0; + data->current = data->current->next; + data->phase = MGI_TURN_IDLE; + data->deadline = now; + return; + } + if (len != MGI_JOIN_UP_LEN || command != MGI_JOIN_UP || + dst != b->radio_address || data->phase != MGI_TURN_JOIN || + now >= data->deadline) { + return; + } + if (src == 0 || src == 0xffffu) return; + for (peer = data->members; peer != NULL; peer = peer->next) { + if (peer->radio_address == src) break; + } + if (peer == NULL) { + peer = (struct mgi_mesh_b_peer *) calloc(1, sizeof(*peer)); + if (peer == NULL) return; + peer->radio_address = src; + peer->next = data->members; + data->members = peer; + } + memcpy(peer->mac, frame + MG_MESH_FRAME_HDR_LEN, sizeof(peer->mac)); +} + +// Member peer did not answer its turn. After MGI_MAX_MISSES in a row, it is +// gone: remove it, and its queued downlink frames +static void mgi_mesh_b_member_missed(struct mgi_mesh_b_data *data, + struct mgi_mesh_b_peer *peer) { + if (++peer->misses < MGI_MAX_MISSES) return; + MG_INFO(("Member %M removed: no answer %d times", mg_print_mac, peer->mac, + MGI_MAX_MISSES)); + if (data->fanout == peer) { + data->fanout = peer->next; + if (data->fanout == NULL) { + size_t len = mg_queue_next(&data->tx, NULL); + mg_queue_del(&data->tx, len); + } + } + LIST_DELETE(struct mgi_mesh_b_peer, &data->members, peer); + free(peer); +} + +// A broadcast occupies one record and is sent once to each member in list +// order. +static size_t mgi_mesh_b_bridge_burst(struct mgi_mesh_b_data *data, + struct mgi_mesh_b_peer *peer) { + char *record; + size_t len; + while ((len = mg_queue_next(&data->tx, &record)) != 0) { + uint16_t dst = mgi_mesh_b_join_field((const uint8_t *) record); + struct mgi_mesh_b_peer *member; + if (dst == 0xffffu) { + if (data->fanout == NULL) data->fanout = data->members; + return data->fanout == peer ? len : 0; + } + if (dst == peer->radio_address) return len; + for (member = data->members; member != NULL; member = member->next) { + if (member->radio_address == dst) return 0; + } + mg_queue_del(&data->tx, len); // Its destination has left. + } + return 0; +} + +static uint64_t mgi_mesh_b_queued_burst_time(struct mgi_mesh_b_data *data, + size_t len) { + char *record; + size_t frames; + if (len == 0) return mgi_mesh_b_burst_time(0); + mg_queue_next(&data->tx, &record); + // Fragment zero carries the final index. Count overhead and IFS per frame. + frames = + ((uint8_t) record[MGI_TX_DST_LEN + MG_MESH_FRAME_HDR_LEN] & 15u) + 1u; + return mgi_mesh_b_airtime(len - MGI_TX_DST_LEN - frames + + (frames - 1) * s_mesh_default_radio->overhead) + + (frames - 1) * s_mesh_default_radio->ifs_us; +} + +static void mgi_mesh_b_bridge_io(uint64_t now, struct mg_mesh_node *b) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) b->data; + mgi_mesh_b_bridge_queue_lan(b); + if (now >= data->deadline) { + if (data->tx_at != (uint64_t) -1) mgi_mesh_b_tx_finish(data); + if (data->phase == MGI_TURN_DATA && data->current != NULL) { + struct mgi_mesh_b_peer *peer = data->current; + data->current = peer->next; + mgi_mesh_b_member_missed(data, peer); + } else if (data->phase == MGI_TURN_JOIN) { + data->current = data->members; + } + data->phase = MGI_TURN_IDLE; + if (data->current != NULL) { + size_t burst = mgi_mesh_b_bridge_burst(data, data->current); + uint64_t down = mgi_mesh_b_queued_burst_time(data, burst); + data->rx_expected = 0xff; + mgi_mesh_b_tx_begin(data, burst != 0, now, + now + down + mgi_mesh_b_burst_time(MGI_ETH_MAX) + + 2 * s_mesh_default_radio->ifs_us + + MGI_JOIN_TURN_US); + } else if (mgi_mesh_b_control_send(b, 0xffffu, MGI_JOIN_DOWN)) { + data->phase = MGI_TURN_JOIN; + data->deadline = now + MGI_JOIN_WINDOW_US; + } + if (data->deadline <= now) data->deadline = now + MGI_JOIN_TURN_US; + } +} + +static void mgi_mesh_b_bridge_free(struct mgi_mesh_b_data *data) { + while (data->members != NULL) { + struct mgi_mesh_b_peer *peer = data->members; + data->members = peer->next; + free(peer); + } +} + +static void mgi_mesh_b_node_rx(struct mg_mesh_node *n, uint64_t now, + uint8_t *frame, size_t len) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) n->data; + uint16_t dst = 0, src = 0; + unsigned command = mgi_mesh_b_frame_parse(frame, len, &dst, &src); + if (command == MGI_JOIN_DOWN && len == MG_MESH_FRAME_HDR_LEN && + dst == 0xffffu) { + if (n->joined && src == data->coordinator && + ++data->missed_rounds >= MGI_MAX_MISSES) { + n->joined = false; + data->phase = MGI_TURN_IDLE; + data->tx_at = (uint64_t) -1; + data->tx_offset = 0; + data->rx_expected = 0xff; + } + if (!n->joined && (mgi_mesh_b_random(n, data) % 64) == 0) { + // Spread contenders across windows as well as within the reply window. + data->coordinator = src; + data->phase = MGI_TURN_JOIN; + data->tx_at = now + MGI_JOIN_TURN_US + + mgi_mesh_b_random(n, data) % MGI_JOIN_JITTER_US; + } + } else if (command == MGI_DATA_DOWN && data->coordinator != 0 && + dst == n->radio_address && src == data->coordinator) { + int received = mgi_mesh_b_data_receive(data, frame, len, now); + if (received < 0) return; + n->joined = true; + data->missed_rounds = + -1; // This round had a grant; do not count it as missed. + if (received != 0) { + struct mg_tcpip_if *ifp = n->radio; + if (n->rx_len != 0 || (size_t) received > ifp->recv_queue.size) { + ifp->ndrop++; + } else { + memcpy(ifp->recv_queue.buf, data->rx_buf, (size_t) received); + n->rx_len = (size_t) received; + } + } + if (data->tx_at != (uint64_t) -1) mgi_mesh_b_tx_finish(data); + mgi_mesh_b_tx_begin(data, mg_queue_next(&data->tx, NULL) != 0, now, + now + mgi_mesh_b_burst_time(MGI_ETH_MAX) + + 2 * s_mesh_default_radio->ifs_us + + MGI_JOIN_TURN_US); + } +} + +static void mgi_mesh_b_input(struct mg_mesh_node *node, void *buf, size_t len) { + uint64_t ready = node->now + node->strategy->radio->turnaround_us; + if (node->tx_at < ready) node->tx_at = ready; + node->next_tick = node->now; + if (node->lan != NULL) + mgi_mesh_b_bridge_rx(node->now, node, (const uint8_t *) buf, len); + else + mgi_mesh_b_node_rx(node, node->now, (uint8_t *) buf, len); +} + +static void mgi_mesh_b_tick(struct mg_mesh_node *node, uint64_t now) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) node->data; + node->now = now; + if (node->lan != NULL) { + mgi_mesh_b_bridge_io(now, node); + mgi_mesh_b_tx_step(data, now); + node->next_tick = data->deadline; + if (data->tx_at < node->next_tick) node->next_tick = data->tx_at; + return; + } + if (data->phase == MGI_TURN_JOIN) { + if (data->tx_at <= now) { + if (now - data->tx_at <= MGI_JOIN_TURN_US && !node->joined) + mgi_mesh_b_control_send(node, data->coordinator, MGI_JOIN_UP); + data->tx_at = (uint64_t) -1; + data->phase = MGI_TURN_IDLE; + } + } else { + mgi_mesh_b_tx_step(data, now); + } + node->next_tick = data->tx_at; +} + +static bool mgi_mesh_b_init(struct mg_mesh_node *node, + struct mg_tcpip_if *radio, struct mg_tcpip_if *lan, + uint16_t radio_address) { + struct mgi_mesh_b_data *data = + (struct mgi_mesh_b_data *) calloc(1, sizeof(*data)); + memset(node, 0, sizeof(*node)); + node->radio = radio; + node->lan = lan; + node->radio_address = radio_address; + node->next_tick = (uint64_t) -1; + node->ram_used = node->ram_peak = sizeof(*node); + if (data == NULL) return false; + node->ram_used += sizeof(*data); + node->ram_peak = node->ram_used; + data->rx_expected = 0xff; + node->data = data; + data->node = node; + data->tx_at = (uint64_t) -1; + mg_queue_init(&data->tx, data->tx_buf, sizeof(data->tx_buf)); + if (lan != NULL) node->next_tick = 0; + return true; +} + +static void mgi_mesh_b_free(struct mg_mesh_node *node) { + struct mgi_mesh_b_data *data = (struct mgi_mesh_b_data *) node->data; + if (data == NULL) return; + mgi_mesh_b_bridge_free(data); + free(data); + node->data = NULL; +} + +const struct mg_mesh mg_mesh_default = { + "default", + mgi_mesh_b_init, + mgi_mesh_b_tick, + mgi_mesh_b_free, + mgi_mesh_b_enqueue, + mgi_mesh_b_input, + true, + true, + &mg_radio_802154, +}; + +#endif diff --git a/src/l2_mesh.h b/src/l2_mesh.h new file mode 100644 index 00000000..af58fe66 --- /dev/null +++ b/src/l2_mesh.h @@ -0,0 +1,80 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved +// +// Ethernet over radio, through a mesh. Mongoose sees an Ethernet interface, +// the mesh splits Ethernet frames into radio frames. Two parts plug in: +// - struct mg_radio: a radio standard, e.g. mg_radio_802154 +// - struct mg_mesh: how the mesh is built and routed + +#pragma once + +#include "net_builtin.h" + +#if MG_ENABLE_TCPIP + +// Monotonic time in microseconds. +uint64_t mg_micros(void); + +// A radio standard +struct mg_radio { + const char *name; // For logs + size_t max_frame_size; // Largest radio frame, bytes + uint32_t bitrate; // Bits per second on air + size_t overhead; // Bytes on air besides the frame: preamble, FCS + uint32_t slot_us; // Random backoff slot duration, us + uint32_t cca_us; // Channel sensing duration, us + uint32_t turnaround_us; // Idle check to transmission start, us + uint32_t ifs_us; // Inter-frame spacing in us: gap between transmits + uint8_t backoff_min; // Initial backoff exponent + uint8_t backoff_max; // Maximum backoff exponent + bool csma; // Use channel sensing and backoff before TX + size_t header_len; // Radio hardware header length + // Write the radio header, with 0xffff as the broadcast address. + void (*print_hdr)(const struct mg_radio *radio, uint8_t *buf, uint16_t dst, + uint16_t src); + // Return the parsed header length, or zero for an invalid header. + size_t (*parse_hdr)(const struct mg_radio *radio, const uint8_t *buf, + size_t len, uint16_t *dst, uint16_t *src); +}; + +#define MG_802154_HEADER_LEN 9 // MAC header, short addresses +#define MG_802154_PAN_ID 0xabcd // Our network's PAN ID + +#ifndef MG_802154_CHANNEL +#define MG_802154_CHANNEL 15 +#endif + +extern const struct mg_radio mg_radio_802154; + +struct mg_mesh_node { + struct mg_tcpip_if *radio; // Owning radio driver interface + struct mg_tcpip_if *lan; // Bridge LAN interface, NULL for a client + const struct mg_mesh *strategy; // Selected strategy + size_t (*tx)(struct mg_tcpip_if *, const void *, size_t); // Radio frame TX + size_t rx_len; // Complete Ethernet frame in the interface RX buffer + uint64_t now; // Current driver poll time, us + uint64_t tx_at; // Earliest next transmission, us + void *data; // Private state owned by the strategy + size_t ram_used; // Live mesh state bytes + size_t ram_peak; // Peak live mesh state bytes + uint64_t next_tick; // Next deadline in us, or UINT64_MAX + uint16_t radio_address; // Local address supplied by the radio + bool joined; // Client is a member of the mesh +}; + +struct mg_mesh { + const char *name; + bool (*init)(struct mg_mesh_node *node, struct mg_tcpip_if *radio, + struct mg_tcpip_if *lan, uint16_t radio_address); + void (*tick)(struct mg_mesh_node *node, uint64_t now_microseconds); + void (*free)(struct mg_mesh_node *node); + size_t (*tx)(struct mg_mesh_node *, const void *, size_t); // ETH frame TX + void (*rx)(struct mg_mesh_node *, void *, size_t); // Radio frame RX + bool single_bridge; // Require exactly one bridge + bool scheduled_tx; // The mesh schedules transmission turns + const struct mg_radio *radio; // Radio configuration for this strategy +}; + +extern const struct mg_mesh mg_mesh_default; + +#endif diff --git a/src/net_builtin.c b/src/net_builtin.c index ec9882f0..88cdd15b 100644 --- a/src/net_builtin.c +++ b/src/net_builtin.c @@ -10,8 +10,14 @@ #define MG_TCPIP_KEEPALIVE_MS 45000 // TCP keep-alive period, ms #endif +#ifndef MG_TCPIP_ACK_MS #define MG_TCPIP_ACK_MS 150 // Timeout for ACKing +#endif + +#ifndef MG_TCPIP_ARP_MS #define MG_TCPIP_ARP_MS 100 // Timeout for ARP response +#endif + #define MG_TCPIP_SYN_MS 15000 // Timeout for connection establishment #define MG_TCPIP_FIN_MS 1000 // Timeout for closing connection #define MG_TCPIP_RTX_MS 1000 // Initial retransmission timeout (RFC-6298, 2.1) diff --git a/src/net_builtin.h b/src/net_builtin.h index 23aead26..dd0ddf11 100644 --- a/src/net_builtin.h +++ b/src/net_builtin.h @@ -154,41 +154,11 @@ extern struct mg_tcpip_driver mg_tcpip_driver_nxp_wifi; extern struct mg_tcpip_driver mg_tcpip_driver_st67w6; extern struct mg_tcpip_driver mg_tcpip_driver_atcmd; extern struct mg_tcpip_driver mg_tcpip_driver_netc; -extern struct mg_tcpip_driver mg_tcpip_driver_mcxw71; -extern struct mg_tcpip_driver mg_tcpip_driver_rw612_154; -// IEEE 802.15.4 radio channel and PAN ID. Settable in mongoose_config.h -#ifndef MG_L2_802154_CHANNEL -#define MG_L2_802154_CHANNEL 15 // 11..26 -#endif -#ifndef MG_L2_802154_PAN_ID -#define MG_L2_802154_PAN_ID 0xabcd -#endif - -// driver_data for IEEE 802.15.4 radio drivers. The interface is Ethernet: -// each Ethernet frame is split into broadcast radio frames, and reassembled -// on the other side, see mg_l2_802154_tx() and mg_l2_802154_rx() -struct mg_l2_802154_data { - uint8_t channel; // User-set channel, 11..26 - uint16_t pan_id; // User-set PAN ID - uint8_t seq, tag; // MAC sequence number, tag of the last sent frame - uint8_t rxtag, rxnext; // Frame being reassembled: tag, next fragment - uint8_t rxsrc[8]; // Its sender - uint16_t rxlen; // Its bytes received so far - uint8_t rxbuf[1514]; // Reassembly buffer, one Ethernet frame -}; - -// Splits an Ethernet frame into radio frames, sends each by calling send(). -// Called by radio drivers' tx(). Return len, or 0 on error -size_t mg_l2_802154_tx(struct mg_tcpip_if *ifp, const void *buf, size_t len, - size_t (*send)(const uint8_t *, size_t, - struct mg_tcpip_if *)); - -// Adds a received radio frame f of n bytes to the frame being reassembled. -// Called by radio drivers' rx(). Return the Ethernet frame length, copied to -// buf, when it is complete, otherwise 0 -size_t mg_l2_802154_rx(struct mg_tcpip_if *ifp, const uint8_t *f, size_t n, - void *buf, size_t len); +#include "drivers/mg24.h" +#include "drivers/mcxw71.h" +#include "drivers/nrf52.h" +#include "drivers/rw612w.h" // SPI bus abstraction for SPI-attached network chips (e.g. W5500). // Populate and assign to the driver's driver_data field. diff --git a/test/Makefile b/test/Makefile index 55428a0a..c0ad2bb7 100644 --- a/test/Makefile +++ b/test/Makefile @@ -16,7 +16,7 @@ IPV6 ?= 1 MDASH ?= 1 ASAN ?= -fsanitize=address,undefined,alignment -fno-sanitize-recover=all -fno-omit-frame-pointer -fno-common ASAN_OPTIONS ?= detect_leaks=1 -TUTORIALS_EMBEDDED := $(dir $(wildcard ../tutorials/esp32/*/Makefile)) $(dir $(wildcard ../tutorials/infineon/*/Makefile)) $(dir $(wildcard ../tutorials/microchip/*/*/Makefile)) $(dir $(wildcard ../tutorials/nordic/*/Makefile)) $(dir $(wildcard ../tutorials/nxp/*/*/Makefile)) $(dir $(wildcard ../tutorials/pico-sdk/*/Makefile)) $(dir $(wildcard ../tutorials/rp/*/Makefile)) $(dir $(wildcard ../tutorials/renesas/*/Makefile)) $(dir $(wildcard ../tutorials/stm32/*/Makefile)) $(dir $(wildcard ../tutorials/stm32/*/*/Makefile)) $(dir $(wildcard ../tutorials/ti/*/Makefile)) $(dir $(wildcard ../tutorials/wch/*/Makefile)) +TUTORIALS_EMBEDDED := $(dir $(wildcard ../tutorials/esp32/*/Makefile)) $(dir $(wildcard ../tutorials/infineon/*/Makefile)) $(dir $(wildcard ../tutorials/microchip/*/*/Makefile)) $(dir $(wildcard ../tutorials/nordic/*/Makefile)) $(dir $(wildcard ../tutorials/nordic/*/*/Makefile)) $(dir $(wildcard ../tutorials/nxp/*/*/Makefile)) $(dir $(wildcard ../tutorials/pico-sdk/*/Makefile)) $(dir $(wildcard ../tutorials/rp/*/Makefile)) $(dir $(wildcard ../tutorials/renesas/*/Makefile)) $(dir $(wildcard ../tutorials/silabs/*/*/Makefile)) $(dir $(wildcard ../tutorials/stm32/*/Makefile)) $(dir $(wildcard ../tutorials/stm32/*/*/Makefile)) $(dir $(wildcard ../tutorials/ti/*/Makefile)) $(dir $(wildcard ../tutorials/wch/*/Makefile)) TUTORIALS_ZEPHYR := $(dir $(wildcard ../tutorials/zephyr/*/Makefile)) TUTORIALS := ../tutorials/micropython/ $(filter-out $(TUTORIALS_EMBEDDED) $(TUTORIALS_ZEPHYR), $(dir $(wildcard ../tutorials/*/*/Makefile))) TUTORIALS_QUICK := $(filter-out ../tutorials/micropython/ ../tutorials/tcpip/pcap-driver/, $(TUTORIALS)) @@ -98,7 +98,7 @@ mongoose.c: Makefile $(wildcard ../src/*.c) $(wildcard ../src/drivers/*.c) cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#include "mongoose.h"' ; (for F in src/*.c src/drivers/*.c ; do echo; echo '#ifdef MG_ENABLE_LINES'; echo "#line 1 \"$$F\""; echo '#endif'; cat $$F | sed -e 's,#include ".*,,'; done))> $@ mongoose.h: $(HDRS) Makefile - cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#ifndef MONGOOSE_H'; echo '#define MONGOOSE_H'; echo; cat src/version.h ; echo; echo '#ifdef __cplusplus'; echo 'extern "C" {'; echo '#endif'; cat src/arch.h src/arch_*.h src/os_*.h src/net_ft.h src/net_lwip.h src/net_rl.h src/config.h src/profile.h src/str.h src/queue.h src/fmt.h src/printf.h src/log.h src/timer.h src/fs.h src/lfs.h src/util.h src/url.h src/iobuf.h src/base64.h src/md5.h src/sha1.h src/sha256.h src/event.h src/net.h src/http.h src/ssi.h src/tls.h src/tls_aes128.h src/tls_p256.h src/tls_p384.h src/tls_chacha20.h src/tls_rsa.h src/tls_mbed.h src/tls_openssl.h src/ws.h src/sntp.h src/mqtt.h src/dns.h src/modbus.h src/json.h src/jwt.h src/rpc.h src/dash.h src/ota.h src/health.h src/mdash.h src/flash.h src/wifi.h src/l2.h src/net_builtin.h src/bsd.h src/drivers/*.h | sed -e '/keep/! s,#include ".*,,' -e 's,^#pragma once,,'; echo; echo '#ifdef __cplusplus'; echo '}'; echo '#endif'; echo '#endif // MONGOOSE_H')> $@ + cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#ifndef MONGOOSE_H'; echo '#define MONGOOSE_H'; echo; cat src/version.h ; echo; echo '#ifdef __cplusplus'; echo 'extern "C" {'; echo '#endif'; cat src/arch.h src/arch_*.h src/os_*.h src/net_ft.h src/net_lwip.h src/net_rl.h src/config.h src/profile.h src/str.h src/queue.h src/fmt.h src/printf.h src/log.h src/timer.h src/fs.h src/lfs.h src/util.h src/url.h src/iobuf.h src/base64.h src/md5.h src/sha1.h src/sha256.h src/event.h src/net.h src/http.h src/ssi.h src/tls.h src/tls_aes128.h src/tls_p256.h src/tls_p384.h src/tls_chacha20.h src/tls_rsa.h src/tls_mbed.h src/tls_openssl.h src/ws.h src/sntp.h src/mqtt.h src/dns.h src/modbus.h src/json.h src/jwt.h src/rpc.h src/dash.h src/ota.h src/health.h src/mdash.h src/flash.h src/wifi.h src/l2.h src/net_builtin.h src/l2_mesh.h src/bsd.h src/drivers/*.h | sed -e '/keep/! s,#include ".*,,' -e 's,^#pragma once,,'; echo; echo '#ifdef __cplusplus'; echo '}'; echo '#endif'; echo '#endif // MONGOOSE_H')> $@ # Check that all external (exported) symbols have "mg_" prefix mg_prefix: mongoose.c mongoose.h diff --git a/test/mesh/Makefile b/test/mesh/Makefile index 01ff9347..78fb9703 100644 --- a/test/mesh/Makefile +++ b/test/mesh/Makefile @@ -1,11 +1,14 @@ # Mesh simulator. `make` builds and runs an example ROOT = ../.. -SRCS = simulator.c l2_mesh.c l2_802154.c star.c $(ROOT)/mongoose.c -HDRS = simulator.h l2_mesh.h l2_802154.h star.h $(ROOT)/mongoose.h +PROG ?= simulator +SRCS = simulator.c mesh_a.c $(ROOT)/mongoose.c +HDRS = simulator.h $(ROOT)/mongoose.h WARN = -pedantic -W -Wall -Werror -Wshadow -Wdouble-promotion -fno-common \ -Wconversion -Wundef -Wmissing-prototypes -Wstrict-prototypes DEFS = -DMG_ENABLE_TCPIP=1 -DMG_ENABLE_TCPIP_DRIVER_INIT=0 -DMG_ENABLE_IPV6=0 \ - -DMG_ENABLE_CUSTOM_MILLIS=1 + -DMG_ENABLE_CUSTOM_MILLIS=1 -DMG_ENABLE_TCPIP_TCPRTX=1 \ + -DMG_IO_SIZE=512 \ + -DMG_TCPIP_ARP_MS=1000 -DMG_TCPIP_ACK_MS=500 # Slow radio: wait longer CFLAGS ?= -O2 -g0 $(WARN) X ?= 100 Y ?= 100 @@ -18,15 +21,15 @@ GRID = $(shell for r in $$(seq 0 $(ROWS)); do \ echo -node $$((c * $(X) / $(COLS))),$$((r * $(Y) / $(ROWS))); \ done; done) -all: mesh_sim - ./mesh_sim -area $(X),$(Y) \ +all: $(PROG) + $(RUN) ./$(PROG) -area $(X),$(Y) \ -bridge $$(($(X) / 2)),$$(($(Y) / 2)) \ - $(GRID) + $(GRID) $(ARGS) -mesh_sim: $(SRCS) $(HDRS) Makefile +$(PROG): $(SRCS) $(HDRS) Makefile $(CC) $(SRCS) $(CFLAGS) $(DEFS) -I. -I$(ROOT) -o $@ clean: - rm -rf mesh_sim mesh_sim.dSYM + rm -rf $(PROG) $(PROG).dSYM *.exe .PHONY: all clean diff --git a/test/mesh/README.md b/test/mesh/README.md index 991dfe70..395c7854 100644 --- a/test/mesh/README.md +++ b/test/mesh/README.md @@ -6,9 +6,7 @@ Small radio devices, e.g. IEEE 802.15.4, sub-GHz or LoRa boards, should run Mongoose as if they were on an ordinary Ethernet LAN: get an IP address over DHCP, answer HTTP, talk MQTT. No 6LoWPAN, no Thread, no special stack. -To do that, the radio interface looks like Ethernet to Mongoose -(`MG_TCPIP_L2_ETH`). Its driver splits each Ethernet frame into radio -frames, sends them over the air, and the receiver puts them back together. +To do that, the radio mesh looks like an Ethernet switch to Mongoose. One or more bridges connect the radio network to the wired LAN: a bridge has an Ethernet port and a radio. A node in range of a bridge talks to it directly; a node out of range reaches a bridge through other nodes, which @@ -18,28 +16,30 @@ bigger area, and keeps working if a bridge goes down. A PC on the LAN sees the radio nodes as regular hosts on the same LAN. ``` - +---- bridge 1 ~~~ node ~~~ node - | ~ - PC ---- LAN node - | ~ - +---- bridge 2 ~~~ node ~~~ node ~~~ node + PC --- LAN --- bridge ~~~ radio nodes ``` -`~~~` and `~` are radio hops. Nodes far from a bridge send their frames -through nearer nodes. A node in the middle can reach either bridge. - ## Two parts plug in -- `struct mg_radio`, a radio standard: how big a radio frame is, how - addresses are written into it. `mg_radio_802154` is the first one. -- `struct mg_mesh`, a mesh strategy: how nodes find a bridge, and where each - frame goes next. `mg_mesh_star` is the first and simplest one: every node - talks to a bridge directly, one hop. +- `struct mg_radio` describes frame size, bitrate, airtime overhead, signal + parameters and channel-access timings. +- `struct mg_mesh` has `init`, `tick`, `free`, `tx` and `rx` callbacks. The most important + one is the `tick()` callback which is called with a microsecond timestamp, + obtained from `mg_micros()`, and handles routing and mesh management. + The mesh queues and fragments Ethernet frames and reassembles incoming radio + frames. Driver `tx()` queues Ethernet into the mesh; `rx()` returns a + reassembled Ethernet frame. A private callback transmits one radio frame. -Everything else, splitting and reassembling frames, sending again when a -frame is lost, is common code that works the same for every radio and every -mesh. The goal is to swap radios and mesh strategies without touching -anything else. +The default slotted mesh in `../../src/l2_mesh.c` uses the bridge as a coordinator. +It opens windows for new nodes to join and gives joined nodes DATA turns +in round-robin order. + +Mongoose calls driver's `poll()`. Radio driver calls `mg_micros()` to get +the current timestamp and calls `mg_mesh :: tick()`. + +The goal is to swap radios and mesh strategies without changing application +code. The slotted star deliberately omits radio ACKs to avoid +the extra airtime and radio turnaround. Other strategies may use them. ## Why a simulator @@ -51,23 +51,20 @@ takes nodes to join, get an address, and answer a request. ## Virtual time -The simulator has its own clock, `simulator::now`, in microseconds, and -Mongoose reads it through `mg_millis()`. Nothing waits for real time: the -simulator moves the clock forward itself, 250 us per step, and polls every -station once per step. A minute of network traffic takes about a second to -simulate. `-t SEC` sets how long to run, 10 minutes by default. +The simulator exposes its virtual clock through `mg_micros()`. +TCP/IP reads the same clock through `mg_millis()`, rounded down to milliseconds. +Nothing waits for real time. `-t SEC` sets the duration, 10 minutes by default. -The step is a trade-off. Each step polls every station's Mongoose, and even -an idle poll costs something, so a smaller step means a slower run. A -bigger step makes frames wait longer in queues, see below. Measured with -121 nodes and 10 simulated minutes: +Radio channel checks, transmission starts and ends run at their scheduled +microsecond timestamps. The clock jumps to the earliest radio event, mesh +deadline, or periodic stack poll. -| Step | Request time, mean | Run time | -|------|--------------------|----------| -| 1 ms | 6.1 ms | 0.9 s | -| 250 us | 1.5 ms | 3.0 s | -| 100 us | 0.6 ms | 7.2 s | -| 10 us | 0.06 ms | 72 s | +`STEP_MICROSECONDS` is 1000: periodic polling services Mongoose's internal +stack timers every millisecond. Packet delivery triggers immediate work: +the simulator repeatedly polls DHCP, the tester, bridges and nodes at the +same virtual timestamp until that work drains. Ethernet forwarding and +radio receive processing therefore do not wait for the periodic tick. +Internal timer handling can still be delayed by up to about 1 ms. ## The setup @@ -76,100 +73,186 @@ bigger step makes frames wait longer in queues, see below. Measured with 10.0.0.1 | 02:01:00:00:00:00 +-- LAN ---- bridge ~~~ nodes | - test station --------+ + test node --------+ 10.0.0.2 02:02:00:00:00:00 ``` The LAN is assumed infinitely fast, see "How frames move on the LAN" below. -Two stations are on it: +Two nodes are on it: - The DHCP server, 10.0.0.1, MAC 02:01:00:00:00:00, gives each node an address made from its MAC: 02:00:00:X:Y:Z gets 10.X+1.Y.Z, so node - addresses never clash with the LAN stations' 10.0.0.x. -- The test station, 10.0.0.2, MAC 02:02:00:00:00:00, is a Mongoose + addresses never clash with the LAN nodes' 10.0.0.x. It also answers + ARP requests for its advertised gateway IP, 10.0.0.1, so nodes can resolve + the gateway MAC and stop retrying. It does not implement IP routing, + and does not have a full Mongoose instance +- The test node, 10.0.0.2, MAC 02:02:00:00:00:00, is a Mongoose instance. It waits for the mesh to settle, 60 seconds by default (`-w SEC`), then sends `GET /` to each node in turn, one at a time, at the - node's own IP address. A node with no address, or one that doesn't answer - within 10 seconds, counts as failed. At the end it prints how many - requests succeeded and how long they took. + node's own IP address. It wraps back to the first node and keeps querying + until five seconds before the end, when it stops starting new requests. + An in-flight request may still complete or time out during the remaining + time. Runs lasting five seconds or less start no requests. A node with + no address, a connection error, or a request exceeding 10 seconds counts + as failed. The summary reports successful and failed attempts, plus + minimum, mean and maximum response times for successful requests. A + request still pending at the end is not included in those counts. Bridges are on the LAN too, and get every frame, as if in promiscuous mode: a frame for one of their radio nodes isn't addressed to the bridge itself. -A bridge forwards every frame it gets from the LAN to the radio, and every -frame from the radio to the LAN, with no filtering yet. +A bridge forwards every frame from the LAN to the radio and every frame +from the radio to the LAN. Bridges and radio nodes get MACs 02:00:00:xx:xx:xx, numbered from 1 in command-line order: 02:00:00:00:00:01, 02:00:00:00:00:02, and so on. ### How frames move on the LAN -Every LAN station, the DHCP server, the test station and each bridge, has a +Every LAN node, the DHCP server, the test node and each bridge, has a LAN interface, `lif`: a `struct mg_tcpip_if` with a receive queue. All of them use the same driver, `s_lan_driver`. There are no cables and no switch: -- **Sending (tx).** A station sends a frame with its driver's `tx`, which is +- Sending. A node sends a frame with its driver's `tx`, which is `lan_tx()`. It looks at the destination MAC and copies the frame into the - receive queues of the stations it is for: - - a broadcast or multicast goes to every station but the sender; - - a unicast goes to the station with that MAC; + receive queues of the nodes it is for: + - a broadcast or multicast goes to every node but the sender; + - a unicast goes to the node with that MAC; - bridges get every frame, as a real bridge in promiscuous mode would: a frame for a radio node is addressed to the node, not to its bridge. - - That's all `tx` does. Nobody's code runs on the receiving side yet. -- **Receiving (rx).** Each station takes the frames from its own queue when +- Receiving. Each node takes the frames from its own queue when the main loop polls it: - - the test station runs Mongoose, which reads the queue by itself, as it + - the test node runs Mongoose, which reads the queue by itself, as it would with a real Ethernet driver that queues frames from an interrupt; - the DHCP server reads it in `dhcp_poll()`, and replies by sending through its own `lif`; - bridges read it in `bridges_poll()`, and forward it to the radio. -So a frame waits in a queue until its receiver's next poll, at most one -step of virtual time, 250 us. Ethernet is hundreds of times faster than the radio, so that -doesn't matter for what we measure. And since sending never calls the -receiver's code, a reply is never sent from inside another station's send. +Enqueuing marks immediate work pending. The main loop drains that work +without advancing virtual time, so the LAN has no simulated propagation, +serialization or polling delay. Sending never calls receiver code directly; +replies are processed by subsequent polls at the same timestamp. For example, a node asking for an address: 1. The node's DHCP DISCOVER crosses the radio to a bridge, which sends it out of its `lif`. It is a broadcast, so `lan_tx()` puts it into the - queues of the DHCP server, the test station and the other bridges. + queues of the DHCP server, the test node and the other bridges. 2. On its next poll, the DHCP server reads it and sends an OFFER, also a - broadcast, which lands in the test station's and the bridges' queues. + broadcast, which lands in the test node's and the bridges' queues. 3. On their next poll, the bridges read the OFFER and send it over the - radio to the node. The test station's Mongoose drops it: the test station + radio to the node. The test node's Mongoose drops it: the test node has a fixed address and no DHCP client. +### How frames move on the radio + +The simulator keeps one accepted transmission per sender in a singly linked +list. A busy sender's driver returns zero, so the mesh retains the fragment +until the driver accepts it. Different senders can still overlap on air. + +1. A sender waits a random number of backoff slots, then listens for + `cca_us` to check whether the channel was idle throughout that interval. +2. If busy, it increases its backoff exponent up to `backoff_max`, waits + again, and repeats. There is no channel-access attempt limit yet. +3. If idle, it schedules transmission after `turnaround_us`. Another sender + can also pass its check before either starts: the channel check does not + reserve the air. +4. Transmission takes `(len + overhead) * 8 / bitrate` seconds, rounded up + to a microsecond. Overlapping transmissions are both marked collided. + Channel checks at the same timestamp run before transmission starts, + so list order cannot make one simultaneous check win. +5. At the end, a collided frame is dropped. Otherwise it is queued for all + other nodes for broadcast/multicast, or the matching node for unicast. + Every other bridge receives it too. Receive processing happens at the + same virtual timestamp. + +A receiver drops a frame if any part of that frame overlaps its own channel +backoff, sensing, turnaround or transmission interval, even if it is +available again when the frame ends. +The radio summary reports total sent and collided frames, and lost receptions. +Losses include range or random loss, plus missed unicast receptions when the +destination was transmitting. + +The 802.15.4 definition uses 250 kbit/s, 8 bytes overhead, 320 us backoff +slots, 128 us channel sensing and 100 us turnaround. The initial backoff +exponent is 3 (0–7 slots), increasing to 5 (0–31 slots) on busy checks. +After transmitting, a sender waits the radio's inter-frame spacing, +`mg_radio::ifs_us`, 640 us for 802.15.4, before starting its next frame's +backoff. This wait applies even if the frame collided or the sender's +queue was temporarily empty. Reception remains enabled during the +inter-frame wait. +The backoff generator is seeded from the wall clock. + +The mesh limits each transmission to 125 bytes plus the 2-byte FCS. +Overhead and inter-frame spacing apply to each fragment. +A receiver must be within `-range METRES` of the sender (default 185 m). +`-loss PERCENT` adds independent random loss for each in-range reception +(default 0%). The radio summary counts range and random losses together. +Collisions and channel sensing still apply across the whole simulated air. + ## Files | File | What | |------|------| -| `l2_mesh.h`, `l2_mesh.c` | `struct mg_radio`, `struct mg_mesh`, common code | -| `l2_802154.h`, `l2_802154.c` | `mg_radio_802154`, IEEE 802.15.4 | -| `star.h`, `star.c` | `mg_mesh_star`, the star mesh | +| `../../src/l2_mesh.h`, `../../src/l2_mesh.c` | `struct mg_radio`, `struct mg_mesh`, shared node initialization, and IEEE 802.15.4 radio | +| `mesh_a.c` | Alternative unslotted star strategy | | `simulator.h`, `simulator.c` | The simulator | | `Makefile` | Builds and runs the simulator | ## Where we are -It works end to end: nodes get addresses over DHCP, and the test station's -requests reach them through the bridges. But the radio is ideal for now: +The slotted strategy uses direct radio hops between nodes and a bridge. +The simulator models airtime, half-duplex reception and collisions. -- The radio driver works like the LAN's: it puts whole Ethernet frames - straight into receive queues, all of them for a broadcast, else the one - with the destination MAC, and bridges get every frame. No range, no - loss, no airtime. -- Bridges forward everything, with no filtering. -- So use one bridge for now. With two, a broadcast from the LAN goes to the - radio through both, each hears the other's copy and sends it back to the - LAN, and that never ends. Deciding who forwards what, without loops, is - the job of `mg_mesh`. +- Frames outside reception range are not received, so nodes out of a bridge's + range never join. But collisions and channel sensing are still global: + every overlap is a collision, wherever the receiver is, and a busy + channel is busy for everyone. +- Mesh routing remains unimplemented: bridge selection, forwarding through other + nodes and recovery are not implemented. -`mg_radio` and `mg_mesh` are still stubs. Next steps, one at a time: the -air (range, loss, airtime), fragmenting frames into radio frames with -`mg_radio`, choosing a bridge and forwarding through nodes with `mg_mesh`, -and filtering at the bridges. +Next steps include distance-aware collision and channel sensing, mesh +routing and bridge filtering. + +## Strategy A — unslotted (simulator default) + +Each sender contends for the channel using CSMA. The mesh queues radio +fragments and submits them one at a time. The receiver reassembles bursts +per sender, and the bridge forwards Ethernet frames between LAN and radio. + +## Default strategy — slotted mesh + +One bridge coordinates the network and starts with an empty member list. +It broadcasts a JOIN beacon to open a 3 ms window. Unjoined nodes choose +windows at random and add up to 640 µs of jitter before sending their JOIN +request with their Ethernet MAC to the bridge. The bridge adds each new +radio address and its Ethernet MAC to the head of its member list. + +After the JOIN window, the bridge grants each member a DATA turn in list +order, then opens another JOIN window. The bridge advances as soon as it +reassembles a valid uplink. If the reply is missing, the turn times out +after allowing for the downlink and a maximum-size uplink burst, including +inter-frame spacing. A member must answer every turn, +with an empty `DATA_UP` if it has nothing to send. A member that misses 3 +turns in a row is removed. With no +members, the bridge opens JOIN windows back to back. A node recognizes its +first `DATA_DOWN` as +confirmation that it joined. + +Slotted nodes have a separate TCP/IP interface that reports link up after +the first grant. Each mesh interface has one 2 KiB TX queue of split +radio bursts. The bridge shares that queue among members and sends the +head burst in its destination's turn. It directs DHCP replies by the +client MAC in `chaddr` and keeps one copy of other multicast and broadcast +bursts until each member has had its turn. A frame that does not fit is +rejected in full. The hardware and simulator drivers accept one raw frame +at a time. JOIN messages fit in a single radio frame. A DATA +burst carries one Ethernet frame across up to 14 radio frames. Each +fragment has a 9-byte short-address MAC header, a one-byte type, and a +byte containing the fragment index and final index in the high and low +nibbles. Empty DATA messages use a single header-only frame. The node +starts its uplink after receiving the whole downlink, and the bridge +forwards only complete uplinks to the LAN. ## Running @@ -182,8 +265,60 @@ make X=200 Y=50 ROWS=2 COLS=8 # Another area and grid of a `ROWS` x `COLS` grid over it. The simulator itself takes: ``` -./mesh_sim -area X,Y,Z -bridge X,Y,Z -node X,Y,Z ... -t SEC -w SEC +./mesh_sim -area X,Y,Z -bridge X,Y,Z -node X,Y,Z ... -t SEC -w SEC -m A|default -range METRES -loss PERCENT ``` Positions are in metres; `X,Y` means `Z` is 0. Repeat `-bridge` and `-node` for more. + +## Hardware + +The FRDM-RW612 example is the Ethernet bridge. The XIAO MG24 and +FRDM-MCXW71 examples are radio clients. All three use the slotted mesh. + +```sh +make -C tutorials/nxp/frdm-rw612/bridge +make -C tutorials/silabs/xiao-mg24/minimal +make -C tutorials/nxp/frdm-mcxw71/minimal +``` + +Use each tutorial's `flash` target. Connect the +bridge to an Ethernet LAN with a DHCP server. Find the client's assigned +IP in its serial log, then send an HTTP GET to that IP from the LAN. + +All three boards use channel 15 and PAN `0xabcd` by default. Each board derives its radio address from its Ethernet MAC. +MG24 is a node only; its radio address is derived from its MAC. + +The drivers in `src/drivers` send and receive raw 802.15.4 frames. +They use the same slotted mesh implementation as the simulator. +The board driver is initialized by `mg_mgr_init()`. The RW612 bridge +has its own `bridge/main.c`, which initializes and polls the two drivers +directly, without an event manager or local IP stack. + +## Hardware integration + +The default mesh and radio drivers are included in `mongoose.c` and `mongoose.h`. +Radio drivers live in `src/drivers/`: `mg24`, `mcxw71`, `rw612w`, +and `nrf52`. MG24 driver data contains its mesh state and `strategy`; it has +no bridge LAN interface. +A NULL strategy selects `mg_mesh_default`, named `"default"`; assigning another +`const struct mg_mesh *` overrides it. Hardware callbacks transmit and receive +raw radio frames; the selected strategy owns joining, scheduling and Ethernet +fragmentation. Driver initialization goes through `mg_tcpip_init()`. + +Board examples are under `tutorials/nxp/frdm-rw612/bridge`, +`tutorials/nxp/frdm-mcxw71/minimal`, `tutorials/silabs/xiao-mg24/minimal`, and +`tutorials/nordic/xiao-52840/minimal`. Their HALs supply `mg_micros()`. + +The simulator and all radio drivers keep the hardware driver installed throughout the +interface lifetime. Mesh state belongs to `driver_data`. Driver `tx()` queues +Ethernet frames; `poll()` feeds raw received frames to the mesh and calls +strategy `tick()`. The strategy sends scheduled fragments through the driver's +private `radio_tx` callback. Driver `rx()` returns a reassembled Ethernet frame +using the stack's existing receive buffer. There is no mesh polling timer, +copied radio interface, or mesh-specific change in `net_builtin.*`. +The simulator explicitly frees its mesh state after `mg_mgr_free()`; board +drivers keep their state for the firmware lifetime. MG24, MCXW71 and nRF52 are +radio nodes. The RW612 bridge application owns Ethernet initialization, its RX +queue, promiscuous mode and link polling, and attaches the LAN to the mesh. +The RW612W driver owns only the radio interface. diff --git a/test/mesh/l2_802154.c b/test/mesh/l2_802154.c deleted file mode 100644 index fe872f51..00000000 --- a/test/mesh/l2_802154.c +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2026 Cesanta Software Limited -// All rights reserved - -#include "l2_802154.h" - -const struct mg_radio mg_radio_802154 = {"802.15.4"}; diff --git a/test/mesh/l2_802154.h b/test/mesh/l2_802154.h deleted file mode 100644 index 2012ee7d..00000000 --- a/test/mesh/l2_802154.h +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2026 Cesanta Software Limited -// All rights reserved -// -// IEEE 802.15.4 radio - -#pragma once - -#include "l2_mesh.h" - -extern const struct mg_radio mg_radio_802154; diff --git a/test/mesh/l2_mesh.c b/test/mesh/l2_mesh.c deleted file mode 100644 index 3fd64c86..00000000 --- a/test/mesh/l2_mesh.c +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) 2026 Cesanta Software Limited -// All rights reserved - -#include "l2_mesh.h" diff --git a/test/mesh/l2_mesh.h b/test/mesh/l2_mesh.h deleted file mode 100644 index d20f5e0a..00000000 --- a/test/mesh/l2_mesh.h +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2026 Cesanta Software Limited -// All rights reserved -// -// Ethernet over radio, through a mesh. Mongoose sees an Ethernet interface, -// its driver splits Ethernet frames into radio frames. Two parts plug in: -// - struct mg_radio: a radio standard, e.g. mg_radio_802154 -// - struct mg_mesh: how the mesh is built and routed, e.g. mg_mesh_star - -#pragma once - -#include "mongoose.h" - -// A radio standard -struct mg_radio { - const char *name; // For logs -}; - -// A mesh strategy -struct mg_mesh { - const char *name; // For logs -}; diff --git a/test/mesh/mesh_a.c b/test/mesh/mesh_a.c new file mode 100644 index 00000000..e901d8d4 --- /dev/null +++ b/test/mesh/mesh_a.c @@ -0,0 +1,261 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "mongoose.h" + +// Radio header followed by type and fragment index/last index. +#define MGI_FRAME_HDR_LEN (MG_802154_HEADER_LEN + 2) + +#define MGI_ETH_MAX 1514 +#define MGI_TX_DST_LEN 2 + +#define MGI_RX_TIMEOUT_US 1000000 +#define MGI_TX_QUEUE_SIZE 4096 +#define MGI_DATA_TAG \ + 0x80u // High bit identifies DATA, low seven bits tag a burst + +struct mgi_mesh_a_rx { + struct mgi_mesh_a_rx *next; + uint64_t expires; + uint16_t src; + uint8_t tag, index, last; + size_t len; + uint8_t buf[MGI_ETH_MAX]; +}; + +struct mgi_mesh_a_data { + struct mg_mesh_node *node; + struct mg_tcpip_if *ethernet; + struct mgi_mesh_a_rx *rx; + uint8_t tag; + struct mg_queue tx; + char tx_buf[MGI_TX_QUEUE_SIZE]; + size_t tx_offset; + uint64_t tx_at; +}; + +static const struct mg_radio *const s_radio = &mg_radio_802154; + +static uint64_t mgi_mesh_a_airtime(size_t len) { + const struct mg_radio *radio = &mg_radio_802154; + return ((uint64_t) (len + radio->overhead) * 8000000 + radio->bitrate - 1) / + radio->bitrate; +} + +// Each queue record holds a destination and a complete burst. +static size_t mgi_mesh_a_queue(struct mg_mesh_node *node, struct mg_queue *q, + const void *buf, size_t len, uint16_t dst, uint8_t type) { + const struct mg_radio *radio = &mg_radio_802154; + const uint8_t *eth = (const uint8_t *) buf; + size_t hdr = MGI_FRAME_HDR_LEN, cap = radio->max_frame_size - hdr; + size_t frames, size, i; + char *record; + uint8_t *out; + if (len < 14 || len > MGI_ETH_MAX) return 0; + frames = (len + cap - 1) / cap; + size = MGI_TX_DST_LEN + len + frames * (1 + hdr); + if (mg_queue_next(q, NULL) == 0) mg_queue_init(q, q->buf, q->size); + if (mg_queue_book(q, &record, size) < size) return 0; + out = (uint8_t *) record; + *out++ = (uint8_t) (dst >> 8); + *out++ = (uint8_t) dst; + for (i = 0; i < frames; i++) { + size_t chunk = len - i * cap; + if (chunk > cap) chunk = cap; + *out++ = (uint8_t) (hdr + chunk); + radio->print_hdr(radio, out, dst, node->radio_address); + out[radio->header_len] = type; + out[radio->header_len + 1] = (uint8_t) (i << 4 | (frames - 1)); + memcpy(out + hdr, eth + i * cap, chunk); + out += hdr + chunk; + } + mg_queue_add(q, size); + return len; +} + +static size_t mgi_mesh_a_send(struct mgi_mesh_a_data *data, const void *buf, + size_t len) { + uint8_t tag = (uint8_t) ((data->tag + 1) & 0x7f); + size_t queued = mgi_mesh_a_queue(data->node, &data->tx, buf, len, 0xffffu, + (uint8_t) (MGI_DATA_TAG | tag)); + if (queued != 0) data->tag = tag; + return queued; +} + +static void mgi_mesh_a_tx(struct mgi_mesh_a_data *data, uint64_t now) { + struct mg_tcpip_if *radio = data->node->radio; + char *record; + size_t size = mg_queue_next(&data->tx, &record); + if (size == 0) return; + if (now >= data->tx_at) { + const uint8_t *part = (const uint8_t *) record + data->tx_offset; + size_t len = part[0]; + if (now >= data->node->tx_at && + data->node->tx(radio, part + 1, len) == len) { + data->tx_offset += len + 1; + if (data->tx_offset == size) { + mg_queue_del(&data->tx, size); + data->tx_offset = MGI_TX_DST_LEN; + } + data->tx_at = now + mgi_mesh_a_airtime(len) + s_radio->ifs_us; + } else { + data->tx_at = now + s_radio->slot_us; + } + } + if (mg_queue_next(&data->tx, NULL) != 0) data->node->next_tick = data->tx_at; +} + +static size_t mgi_mesh_a_enqueue(struct mg_mesh_node *node, const void *buf, + size_t len) { + return mgi_mesh_a_send(node->data, buf, len); +} + +static void mgi_mesh_a_deliver(struct mgi_mesh_a_data *data, uint8_t *buf, + size_t len) { + struct mg_tcpip_if *lan = data->node->lan; + if (len < 14) return; + if (lan != NULL) { + lan->driver->tx(buf, len, lan); + } else { + struct mg_tcpip_if *ifp = data->node->radio; + if (data->node->rx_len != 0 || len > ifp->recv_queue.size) { + ifp->ndrop++; + } else { + memcpy(ifp->recv_queue.buf, buf, len); + data->node->rx_len = len; + } + } +} + +static void mgi_mesh_a_rx_free(struct mgi_mesh_a_data *data, + struct mgi_mesh_a_rx **p) { + struct mgi_mesh_a_rx *r = *p; + *p = r->next; + data->node->ram_used -= sizeof(*r); + free(r); +} + +static void mgi_mesh_a_receive(struct mgi_mesh_a_data *data, uint8_t *frame, + size_t len, uint64_t now) { + struct mgi_mesh_a_rx **p = &data->rx, *r; + size_t cap = s_radio->max_frame_size - MGI_FRAME_HDR_LEN, chunk; + uint16_t dst, src; + uint8_t tag, index, last; + if (len <= MGI_FRAME_HDR_LEN || len > s_radio->max_frame_size || + s_radio->parse_hdr(s_radio, frame, len, &dst, &src) != + s_radio->header_len || + (dst != 0xffffu && dst != data->node->radio_address)) + return; + tag = frame[MGI_FRAME_HDR_LEN - 2]; + index = frame[MGI_FRAME_HDR_LEN - 1] >> 4; + last = frame[MGI_FRAME_HDR_LEN - 1] & 15; + chunk = len - MGI_FRAME_HDR_LEN; + if ((tag & MGI_DATA_TAG) == 0 || index > last || + last > (MGI_ETH_MAX - 1) / cap || (index < last && chunk != cap)) + return; + if (index == 0 && + (chunk < 14 || + (data->node->lan == NULL && (frame[MGI_FRAME_HDR_LEN] & 1) == 0 && + memcmp(frame + MGI_FRAME_HDR_LEN, data->ethernet->mac, 6) != 0))) + return; + while (*p != NULL && (*p)->src != src) p = &(*p)->next; + if (index == 0) { + if (*p != NULL) mgi_mesh_a_rx_free(data, p); + if (last == 0) { + mgi_mesh_a_deliver(data, frame + MGI_FRAME_HDR_LEN, chunk); + return; + } + r = calloc(1, sizeof(*r)); + if (r == NULL) return; + data->node->ram_used += sizeof(*r); + if (data->node->ram_peak < data->node->ram_used) + data->node->ram_peak = data->node->ram_used; + r->next = *p; + *p = r; + r->src = src; + r->tag = tag; + r->last = last; + } + r = *p; + if (r == NULL || r->tag != tag) return; + if (r->index != index || r->last != last || chunk > sizeof(r->buf) - r->len) { + mgi_mesh_a_rx_free(data, p); + return; + } + memcpy(r->buf + r->len, frame + MGI_FRAME_HDR_LEN, chunk); + r->len += chunk; + r->index++; + r->expires = now + MGI_RX_TIMEOUT_US; + if (index == last) { + mgi_mesh_a_deliver(data, r->buf, r->len); + mgi_mesh_a_rx_free(data, p); + } +} + +static void mgi_mesh_a_input(struct mg_mesh_node *node, void *buf, size_t len) { + uint64_t ready = node->now + node->strategy->radio->turnaround_us; + if (node->tx_at < ready) node->tx_at = ready; + node->next_tick = node->now; + mgi_mesh_a_receive(node->data, buf, len, node->now); +} + +static void mgi_mesh_a_tick(struct mg_mesh_node *node, uint64_t now) { + struct mgi_mesh_a_data *data = node->data; + struct mgi_mesh_a_rx **p = &data->rx; + size_t len; + node->now = now; + node->next_tick = (uint64_t) -1; + while (*p != NULL) { + if ((*p)->expires <= now) + mgi_mesh_a_rx_free(data, p); + else + p = &(*p)->next; + } + if (node->lan != NULL) { + struct mg_queue *q = &node->lan->recv_queue; + char *buf; + while ((len = mg_queue_next(q, &buf)) > 0) { + mgi_mesh_a_send(data, buf, len); + mg_queue_del(q, len); + } + } + mgi_mesh_a_tx(data, now); + for (p = &data->rx; *p != NULL; p = &(*p)->next) { + if ((*p)->expires < node->next_tick) node->next_tick = (*p)->expires; + } +} + +static void mgi_mesh_a_free(struct mg_mesh_node *node) { + struct mgi_mesh_a_data *data = node->data; + if (data == NULL) return; + while (data->rx != NULL) mgi_mesh_a_rx_free(data, &data->rx); + free(data); + node->data = NULL; +} + +static bool mgi_mesh_a_init(struct mg_mesh_node *node, + struct mg_tcpip_if *radio, struct mg_tcpip_if *lan, + uint16_t radio_address) { + struct mgi_mesh_a_data *data = calloc(1, sizeof(*data)); + memset(node, 0, sizeof(*node)); + node->radio = radio; + node->lan = lan; + node->radio_address = radio_address; + node->next_tick = (uint64_t) -1; + node->ram_used = node->ram_peak = sizeof(*node); + if (data == NULL) return false; + node->ram_used += sizeof(*data); + node->ram_peak = node->ram_used; + data->node = node; + data->tx_offset = MGI_TX_DST_LEN; + node->data = data; + mg_queue_init(&data->tx, data->tx_buf, sizeof(data->tx_buf)); + data->ethernet = radio; + node->joined = lan == NULL; + return true; +} + +const struct mg_mesh mg_mesh_a = { + "A", mgi_mesh_a_init, mgi_mesh_a_tick, mgi_mesh_a_free, + mgi_mesh_a_enqueue, mgi_mesh_a_input, false, false, &mg_radio_802154, +}; diff --git a/test/mesh/simulator.c b/test/mesh/simulator.c index 927e4c3f..7afaf4f1 100644 --- a/test/mesh/simulator.c +++ b/test/mesh/simulator.c @@ -1,20 +1,25 @@ // Copyright (c) 2026 Cesanta Software Limited // All rights reserved // -// Radio mesh simulator. Bridges and nodes are stations at positions in an -// area. Time is virtual: see mg_millis(). On the LAN: a DHCP server, and a -// test station that sends HTTP requests to the nodes +// Radio mesh simulator. Bridges and radio nodes have positions in an area. Time +// is virtual: see mg_millis(). On the LAN: a DHCP server, and a test node that +// sends HTTP requests to the nodes #include "simulator.h" -#define STEP_MICROSECONDS 250 // Virtual time per main loop step, see README.md +#define STEP_MICROSECONDS 1000 // Periodic stack timer polling only +static const struct mg_mesh *s_mesh = &mg_mesh_default; static struct simulator s_sim; // The simulator // Mongoose's clock: virtual time. A minute of network traffic takes far less // than a minute to simulate +uint64_t mg_micros(void) { + return s_sim.now; +} + uint64_t mg_millis(void) { - return s_sim.now / 1000; + return mg_micros() / 1000; } // Parse "X,Y,Z", or "X,Y" with Z 0. Return false if it is not that @@ -40,9 +45,14 @@ static void usage(const struct simulator *sim, const char *prog) { " -bridge X,Y,Z - a bridge; repeat for more\n" " -node X,Y,Z - a radio node; repeat for more\n" " -t SEC - simulated time, seconds, default: %g\n" - " -w SEC - mesh settle time before requests, default: %g\n", + " -w SEC - mesh settle time before requests, default: %g\n" + " -range METRES - radio range, default: %g\n" + " -loss PERCENT - random reception loss, default: %g\n" + " -m A|default - mesh strategy, default: default\n", prog, sim->area.x, sim->area.y, sim->area.z, - (double) sim->duration / 1e6, (double) sim->tester.wait / 1e6); + (double) (int64_t) sim->duration / 1e6, + (double) (int64_t) sim->tester.wait / 1e6, sim->radio_range, + sim->radio_loss); exit(EXIT_FAILURE); } @@ -57,7 +67,9 @@ static void next_mac(struct simulator *sim, uint8_t *mac) { static bool add_bridge(struct simulator *sim, const char *s) { struct bridge *b = (struct bridge *) calloc(1, sizeof(*b)); if (b == NULL || !parse_pos(s, &b->pos)) return free(b), false; + if (sim->last_mac >= 0xffffu - 1u) return free(b), false; next_mac(sim, b->rif.mac); + b->radio.address = (uint16_t) sim->last_mac; LIST_ADD_TAIL(struct bridge, &sim->bridges, b); sim->num_bridges++; return true; @@ -67,7 +79,9 @@ static bool add_bridge(struct simulator *sim, const char *s) { static bool add_node(struct simulator *sim, const char *s) { struct radio_node *n = (struct radio_node *) calloc(1, sizeof(*n)); if (n == NULL || !parse_pos(s, &n->pos)) return free(n), false; + if (sim->last_mac >= 0xffffu - 1u) return free(n), false; next_mac(sim, n->rif.mac); + n->radio.address = (uint16_t) sim->last_mac; LIST_ADD_TAIL(struct radio_node, &sim->nodes, n); sim->num_nodes++; return true; @@ -95,15 +109,18 @@ static void check_area(const struct simulator *sim) { // it is not the sender, from static void lan_put(struct mg_tcpip_if *to, struct mg_tcpip_if *from, const void *buf, size_t len) { - if (to != from) mg_tcpip_qwrite((void *) buf, len, to); + if (to != from) { + mg_tcpip_qwrite((void *) buf, len, to); + s_sim.work_pending = true; + } } // LAN driver, all LAN interfaces share it. The LAN is infinitely fast: tx // puts the frame straight into the receive queues of the interfaces it is // for: all, if broadcast; else the one with the destination MAC. Bridges get -// every frame: some are for their radio nodes. Each station takes frames -// from its queue when polled, see dhcp_poll(), bridges_poll(); Mongoose does -// it for the test station +// every frame: some are for their radio nodes. Each node takes frames +// from its queue when polled, see dhcp_poll() and the mesh bridge callback; +// Mongoose does it for the test node static size_t lan_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) { const uint8_t *dst = (const uint8_t *) buf; bool all = dst[0] & 1; // Broadcast, multicast @@ -143,7 +160,7 @@ static uint16_t ip_csum(const uint8_t *buf, size_t len) { } // A node's address, from its MAC 02:00:00:X:Y:Z: 10.X+1.Y.Z, so it never -// clashes with the LAN stations' 10.0.0.x +// clashes with the LAN nodes' 10.0.0.x static void node_ip(const uint8_t *mac, uint8_t *ip) { ip[0] = 10, ip[1] = (uint8_t) (mac[3] + 1), ip[2] = mac[4], ip[3] = mac[5]; } @@ -192,19 +209,42 @@ static void dhcp_rx(struct simulator *sim, const uint8_t *buf, size_t len) { sim->dhcp.lif.driver->tx(r, rlen, &sim->dhcp.lif); } +// The DHCP node also owns the advertised gateway IP, 10.0.0.1. +static bool dhcp_arp(struct simulator *sim, const uint8_t *f, size_t n) { + static const uint8_t header[] = {0, 1, 8, 0, 6, 4, 0, 1}; + static const uint8_t ip[] = {10, 0, 0, 1}; + uint8_t reply[60]; + if (n < 42 || f[12] != 8 || f[13] != 6 || + memcmp(f + 14, header, sizeof(header)) != 0 || + memcmp(f + 38, ip, sizeof(ip)) != 0) + return false; + memset(reply, 0, sizeof(reply)); + memcpy(reply, f + 6, 6); + memcpy(reply + 6, sim->dhcp.lif.mac, 6); + memcpy(reply + 12, f + 12, 8); + reply[21] = 2; + memcpy(reply + 22, sim->dhcp.lif.mac, 6); + memcpy(reply + 28, ip, sizeof(ip)); + memcpy(reply + 32, f + 22, 10); + sim->dhcp.lif.driver->tx(reply, sizeof(reply), &sim->dhcp.lif); + return true; +} + // DHCP server's work: take the frames from its queue static void dhcp_poll(struct simulator *sim) { char *buf; size_t len; while ((len = mg_queue_next(&sim->dhcp.lif.recv_queue, &buf)) > 0) { - dhcp_rx(sim, (const uint8_t *) buf, len); + if (!dhcp_arp(sim, (const uint8_t *) buf, len)) { + dhcp_rx(sim, (const uint8_t *) buf, len); + } mg_queue_del(&sim->dhcp.lif.recv_queue, len); } } // The request on the way finished: ok, or failed static void tester_done(struct lan_tester *t, uint64_t now, bool ok) { - double ms = (double) (now - t->start) / 1000; + double ms = (double) (int64_t) (now - t->start) / 1000; if (ok) { t->ok++, t->sum += ms; if (t->ok == 1 || ms < t->min) t->min = ms; @@ -214,6 +254,7 @@ static void tester_done(struct lan_tester *t, uint64_t now, bool ok) { } if (t->c != NULL) t->c->is_closing = 1, t->c->fn_data = NULL; t->c = NULL; + s_sim.work_pending = true; } // HTTP client: GET /, the reply ends the request @@ -230,8 +271,8 @@ static void tester_fn(struct mg_connection *c, int ev, void *ev_data) { (void) ev_data; } -// Test station's work: Mongoose, then requests, one at a time, once the mesh -// had time to settle. A node without an IP address fails at once +// Test node's work: Mongoose, then requests, one at a time, once the mesh +// had time to settle. Cycle through nodes until five seconds before the end. static void tester_poll(struct simulator *sim) { struct radio_node *n; char url[64]; @@ -241,28 +282,34 @@ static void tester_poll(struct simulator *sim) { tester_done(&sim->tester, sim->now, false); } if (sim->now < sim->tester.wait || sim->tester.c != NULL || - sim->tester.next == NULL) + sim->tester.next == NULL || sim->duration <= 5000000U || + sim->now >= sim->duration - 5000000U) return; n = sim->tester.next; - sim->tester.next = n->next, sim->tester.start = sim->now; - if (n->rif.ip == 0) { - tester_done(&sim->tester, sim->now, false); + sim->tester.next = n->next == NULL ? sim->nodes : n->next; + if (n->mgr.ifp == NULL) return; + sim->tester.start = sim->now; + if (n->mgr.ifp->ip == 0) { + // Wait for the next poll rather than endlessly cycling at this time. + sim->tester.failed++; return; } - mg_snprintf(url, sizeof(url), "http://%M/", mg_print_ip4, &n->rif.ip); + mg_snprintf(url, sizeof(url), "http://%M/", mg_print_ip4, &n->mgr.ifp->ip); if ((sim->tester.c = mg_http_connect(&sim->tester.mgr, url, tester_fn, &sim->tester)) == NULL) { tester_done(&sim->tester, sim->now, false); } } -// Set up sim: defaults, LAN stations +// Set up sim: defaults, LAN nodes static void init(struct simulator *sim) { static const uint8_t dmac[6] = {2, 1, 0, 0, 0, 0}, // DHCP server MAC - tmac[6] = {2, 2, 0, 0, 0, 0}; // Test station MAC + tmac[6] = {2, 2, 0, 0, 0, 0}; // Test node MAC memset(sim, 0, sizeof(*sim)); sim->area.x = sim->area.y = 100; - sim->duration = 600 * 1000000ULL, sim->tester.wait = 60 * 1000000ULL; + sim->radio_range = 185; + sim->loss_random = 1; + sim->duration = 600 * 1000000U, sim->tester.wait = 60 * 1000000U; sim->request_timeout_ms = 10000; memcpy(sim->dhcp.lif.mac, dmac, 6), sim->dhcp.lif.driver = &s_lan_driver; mg_queue_init(&sim->dhcp.lif.recv_queue, (char *) calloc(1, LAN_QUEUE_SIZE), @@ -275,42 +322,258 @@ static void init(struct simulator *sim) { sim->tester.lif.recv_queue.size = 128 * 1024; } -// Radio driver, all radio interfaces share it. An ideal radio for now: -// whole Ethernet frames, no range, no loss, no airtime. It works like the -// LAN, see lan_tx(): a frame goes into the receive queues of all other -// radio interfaces if broadcast, else of the one with the destination MAC. -// Bridges get every frame: some are for the LAN. Who forwards what, and -// loops with several bridges, are for mg_mesh -static size_t radio_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) { - const uint8_t *dst = (const uint8_t *) buf; - bool all = dst[0] & 1; // Broadcast, multicast - struct bridge *b; - struct radio_node *n; - for (b = s_sim.bridges; b != NULL; b = b->next) { - lan_put(&b->rif, ifp, buf, len); +// The driver queues each submitted frame for transmission over the air. +// All nodes hear one channel. CCA and turnaround are separate events: +// two idle checks can succeed before either node starts transmitting. +// Inter-frame spacing is per sender, not a reservation of the shared air. +uint64_t radio_airtime(size_t len) { + const struct mg_radio *radio = s_mesh->radio; + return ((uint64_t) (len + radio->overhead) * 8000000 + radio->bitrate - 1) / + radio->bitrate; +} + +static void radio_schedule(struct simulator *sim, struct radio_frame *f, + bool busy) { + struct radio_port *port = (struct radio_port *) f->from->driver_data; + const struct mg_radio *radio = s_mesh->radio; + bool checked = f->state == RADIO_CCA; + if (s_mesh->scheduled_tx || !radio->csma || (checked && !busy)) { + f->state = RADIO_START; + f->due = sim->now + (checked ? radio->turnaround_us : 0); + return; } - for (n = s_sim.nodes; n != NULL; n = n->next) { - if (all || memcmp(dst, n->rif.mac, 6) == 0) { - lan_put(&n->rif, ifp, buf, len); + if (!checked) { + f->backoff = radio->backoff_min; + } else if (f->backoff < radio->backoff_max) { + f->backoff++; + } + if (port->random == 0) { + size_t i; + port->random = (uint32_t) mg_now(); + for (i = 0; i < 6; i++) { + port->random = port->random * 33u + f->from->mac[i]; } + if (port->random == 0) port->random = 1; } + port->random ^= port->random << 13; + port->random ^= port->random >> 17; + port->random ^= port->random << 5; + f->state = RADIO_BACKOFF; + f->due = sim->now + (port->random % (1U << f->backoff)) * radio->slot_us; + if (f->due > sim->now) port->rx_off_until = f->due; +} + +static size_t radio_send(struct mg_tcpip_if *ifp, const void *buf, size_t len) { + struct radio_port *port = (struct radio_port *) ifp->driver_data; + struct simulator *sim = port->sim; + struct radio_frame *f, **p = &sim->air_head; + const struct mg_radio *radio = s_mesh->radio; + if (len < radio->header_len || len > sizeof(f->buf) || + len > radio->max_frame_size || port->tx_ready_at > sim->now || + radio->bitrate == 0) + return 0; + while (*p != NULL) { + if ((*p)->from == ifp) return 0; + p = &(*p)->next; + } + if ((f = (struct radio_frame *) calloc(1, sizeof(*f))) == NULL) return 0; + f->from = ifp, f->len = len; + memcpy(f->buf, buf, len); + *p = f; + sim->work_pending = true; + radio_schedule(sim, f, false); return len; } +static void radio_receive(struct radio_port *port, + const struct radio_frame *f) { + char *buf; + if (mg_queue_book(&port->rx, &buf, f->len) >= f->len) { + memcpy(buf, f->buf, f->len); + mg_queue_add(&port->rx, f->len); + port->sim->work_pending = true; + } +} + +// Test range and sample independent interference loss for this reception. +static bool visible(struct simulator *sim, const struct pos *a, + const struct pos *b) { + double dx = a->x - b->x, dy = a->y - b->y, dz = a->z - b->z; + double range = sim->radio_range; + if (dx * dx + dy * dy + dz * dz > range * range) return false; + if (sim->radio_loss == 0) return true; + sim->loss_random ^= sim->loss_random << 13; + sim->loss_random ^= sim->loss_random >> 17; + sim->loss_random ^= sim->loss_random << 5; + return (double) sim->loss_random * (100.0 / 4294967296.0) >= sim->radio_loss; +} + +static void radio_deliver_to(struct radio_port *port, + const struct radio_port *from, + const struct radio_frame *f, uint16_t dst) { + bool all = dst == 0xffffu; + if (port == from || (!all && port->address != dst)) return; + if (!visible(port->sim, from->pos, port->pos)) { + port->sim->air_lost++; + } else if (port->tx_until > f->start) { + if (!all) port->sim->air_lost++; + } else if (!f->collided && port->rx_off_until <= f->start) { + radio_receive(port, f); + } +} + +static void radio_deliver(struct simulator *sim, const struct radio_frame *f) { + const struct mg_radio *radio = s_mesh->radio; + const struct radio_port *from = f->from->driver_data; + uint16_t dst, src; + struct bridge *b; + struct radio_node *n; + if (radio->parse_hdr(radio, f->buf, f->len, &dst, &src) == 0) return; + for (b = sim->bridges; b != NULL; b = b->next) + radio_deliver_to(&b->radio, from, f, dst); + for (n = sim->nodes; n != NULL; n = n->next) + radio_deliver_to(&n->radio, from, f, dst); +} + +// Process ends, then all channel checks, then starts at this timestamp. +// Checks therefore see the same air state regardless of list order. +static void radio_poll(struct simulator *sim) { + struct radio_frame **p = &sim->air_head, *f, *g; + while ((f = *p) != NULL) { + if (f->state != RADIO_END || f->due > sim->now) { + p = &f->next; + continue; + } + if (f->collided) { + sim->air_collisions++; + } + radio_deliver(sim, f); + *p = f->next; + free(f); + } + for (f = sim->air_head; f != NULL; f = f->next) { + if (f->state == RADIO_BACKOFF && f->due <= sim->now) { + f->state = RADIO_CCA; + f->due = sim->now + s_mesh->radio->cca_us; + ((struct radio_port *) f->from->driver_data)->rx_off_until = f->due; + } + } + for (f = sim->air_head; f != NULL; f = f->next) { + if (f->state != RADIO_CCA || f->due > sim->now) continue; + radio_schedule(sim, f, + sim->air_busy_until > sim->now - s_mesh->radio->cca_us); + if (f->state == RADIO_START) { + ((struct radio_port *) f->from->driver_data)->rx_off_until = f->due; + } + } + for (f = sim->air_head; f != NULL; f = f->next) { + struct radio_port *port = f->from->driver_data; + if (f->state != RADIO_START || f->due > sim->now) continue; + f->state = RADIO_END; + f->start = sim->now; + f->due = sim->now + radio_airtime(f->len); + port->rx_off_until = port->tx_until = f->due; + port->tx_ready_at = f->due + s_mesh->radio->ifs_us; + for (g = sim->air_head; g != NULL; g = g->next) { + if (g != f && g->state == RADIO_END && g->due > sim->now) { + f->collided = g->collided = true; + } + } + if (f->due > sim->air_busy_until) sim->air_busy_until = f->due; + sim->air_sent++; + } +} + +// Radio events keep microsecond precision between periodic timer polls. +static uint64_t radio_next(const struct simulator *sim, uint64_t next) { + const struct radio_frame *f; + for (f = sim->air_head; f != NULL; f = f->next) { + if (f->due < next) next = f->due; + } + return next; +} + +static size_t radio_receive_frame(void *buf, size_t cap, struct mg_tcpip_if *ifp) { + struct radio_port *port = (struct radio_port *) ifp->driver_data; + char *src; + size_t len; + while ((len = mg_queue_next(&port->rx, &src)) > 0) { + if (len <= cap) memcpy(buf, src, len); + mg_queue_del(&port->rx, len); + if (len <= cap) { + if (mg_queue_next(&port->rx, &src) > 0) port->sim->work_pending = true; + return len; + } + } + return 0; +} + +static size_t radio_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) { + struct radio_port *port = ifp->driver_data; + return port->mesh->strategy->tx(port->mesh, buf, len); +} + +static size_t radio_rx(void *buf, size_t cap, struct mg_tcpip_if *ifp) { + struct radio_port *port = ifp->driver_data; + size_t len = port->mesh->rx_len; + if (len > cap) return 0; + if (len != 0 && buf != ifp->recv_queue.buf) + memcpy(buf, ifp->recv_queue.buf, len); + port->mesh->rx_len = 0; + return len; +} + +static bool radio_driver_poll(struct mg_tcpip_if *ifp, bool s1) { + struct radio_port *port = ifp->driver_data; + uint8_t frame[128]; + size_t len; + (void) s1; + port->mesh->now = mg_micros(); + while (port->mesh->rx_len == 0 && + (len = radio_receive_frame(frame, sizeof(frame), ifp)) != 0) + port->mesh->strategy->rx(port->mesh, frame, len); + s_mesh->tick(port->mesh, mg_micros()); + return port->mesh->joined; +} + static bool radio_init(struct mg_tcpip_if *ifp) { - (void) ifp; + struct radio_port *port = ifp->driver_data; + char *buf = malloc(LAN_QUEUE_SIZE); + if (buf == NULL) return false; + mg_queue_init(&port->rx, buf, LAN_QUEUE_SIZE); + port->mesh = calloc(1, sizeof(*port->mesh)); + if (port->mesh == NULL || + !s_mesh->init(port->mesh, ifp, port->lan, port->address)) { + free(port->mesh); + port->mesh = NULL; + free(buf); + port->rx.buf = NULL; + return false; + } + port->mesh->strategy = s_mesh; + port->mesh->tx = radio_send; return true; } -static bool radio_link(struct mg_tcpip_if *ifp, bool s1) { - (void) ifp, (void) s1; - return true; -} +static struct mg_tcpip_driver s_radio_driver = { + radio_init, radio_tx, radio_rx, radio_driver_poll}; -static struct mg_tcpip_driver s_radio_driver = {radio_init, radio_tx, NULL, - radio_link}; +#define MG_TCPIP_DRIVER_SIM_RADIO_INIT(manager, interface) \ + do { \ + struct mg_tcpip_if *mif_ = (interface); \ + mif_->mgr = (manager); \ + mg_tcpip_init((manager), mif_); \ + MG_INFO(("Driver: sim_radio, MAC: %M", mg_print_mac, mif_->mac)); \ + } while (0) -// A node's HTTP server: the test station requests GET / +#define MG_TCPIP_DRIVER_SIM_LAN_INIT(manager, interface) \ + do { \ + struct mg_tcpip_if *mif_ = (interface); \ + mg_tcpip_init((manager), mif_); \ + MG_INFO(("Driver: sim_lan, MAC: %M", mg_print_mac, mif_->mac)); \ + } while (0) + +// A node's HTTP server: the test node requests GET / static void node_http_ev_handler(struct mg_connection *c, int ev, void *ev_data) { if (ev == MG_EV_HTTP_MSG) { @@ -326,56 +589,38 @@ static void nodes_init(struct simulator *sim) { struct radio_node *n; for (n = sim->nodes; n != NULL; n = n->next) { n->rif.driver = &s_radio_driver; - n->rif.recv_queue.size = LAN_QUEUE_SIZE; + n->rif.driver_data = &n->radio; + n->radio.sim = sim; + n->radio.pos = &n->pos; mg_mgr_init(&n->mgr); - mg_tcpip_init(&n->mgr, &n->rif); - mg_http_listen(&n->mgr, "http://0.0.0.0:80", node_http_ev_handler, - n->rif.mac); + MG_TCPIP_DRIVER_SIM_RADIO_INIT(&n->mgr, &n->rif); + if (n->mgr.ifp != NULL) { + mg_http_listen(&n->mgr, "http://0.0.0.0:80", node_http_ev_handler, + n->mgr.ifp->mac); + } } } -// Start each bridge: its LAN and radio drivers. No Mongoose, no IP +// Start each bridge: its LAN and radio drivers, with no IP stack. static void bridges_init(struct simulator *sim) { struct bridge *b; for (b = sim->bridges; b != NULL; b = b->next) { b->lif.driver = &s_lan_driver, b->rif.driver = &s_radio_driver; + b->rif.driver_data = &b->radio; + b->radio.sim = sim; + b->radio.pos = &b->pos; + mg_mgr_init(&b->mgr); + b->radio.lan = &b->lif; + MG_TCPIP_DRIVER_SIM_RADIO_INIT(&b->mgr, &b->rif); mg_queue_init(&b->lif.recv_queue, (char *) calloc(1, LAN_QUEUE_SIZE), LAN_QUEUE_SIZE); - mg_queue_init(&b->rif.recv_queue, (char *) calloc(1, LAN_QUEUE_SIZE), - LAN_QUEUE_SIZE); - b->lif.driver->init(&b->lif), b->rif.driver->init(&b->rif); + b->lif.driver->init(&b->lif); } } -// Move the frames waiting in the receive queue of interface from out of -// interface to -static void bridge_forward(struct mg_tcpip_if *from, struct mg_tcpip_if *to) { - char *buf; - size_t len; - while ((len = mg_queue_next(&from->recv_queue, &buf)) > 0) { - // MG_DEBUG(("Bridging ETH frame, size %zu", len)); - to->driver->tx(buf, len, to); - mg_queue_del(&from->recv_queue, len); - } -} - -// Bridges' work: forward every frame, LAN to radio and radio to LAN. No -// filtering yet -static void bridges_poll(struct simulator *sim) { - struct bridge *b; - for (b = sim->bridges; b != NULL; b = b->next) { - bridge_forward(&b->lif, &b->rif); - bridge_forward(&b->rif, &b->lif); - } -} - -// Poll each node's Mongoose -static void nodes_poll(struct simulator *sim) { - struct radio_node *n; - for (n = sim->nodes; n != NULL; n = n->next) mg_mgr_poll(&n->mgr, 0); -} - int main(int argc, char *argv[]) { + uint64_t poll_at = 0, mesh_at = (uint64_t) -1; + char *pending; clock_t started; // CPU time, to show how fast the simulation runs int i; @@ -393,32 +638,82 @@ int main(int argc, char *argv[]) { s_sim.duration = (uint64_t) (atof(argv[++i]) * 1e6); } else if (strcmp(argv[i], "-w") == 0) { s_sim.tester.wait = (uint64_t) (atof(argv[++i]) * 1e6); + } else if (strcmp(argv[i], "-range") == 0 || + strcmp(argv[i], "-loss") == 0) { + bool loss = strcmp(argv[i], "-loss") == 0; + const char *arg = argv[++i]; + char *end; + double value = strtod(arg, &end); + if (end == arg || *end != '\0' || + !(value >= 0 && value <= (loss ? 100 : 1e100))) + usage(&s_sim, argv[0]); + if (loss) + s_sim.radio_loss = value; + else + s_sim.radio_range = value; + } else if (strcmp(argv[i], "-m") == 0) { + const char *name = argv[++i]; + if (strcmp(name, mg_mesh_a.name) == 0) + s_mesh = &mg_mesh_a; + else if (strcmp(name, mg_mesh_default.name) == 0) + s_mesh = &mg_mesh_default; + else + usage(&s_sim, argv[0]); } else { usage(&s_sim, argv[0]); } } printf("Area %gx%gx%g m, radio %s, mesh %s, bridges: %d, nodes: %d\n", - s_sim.area.x, s_sim.area.y, s_sim.area.z, mg_radio_802154.name, - mg_mesh_star.name, s_sim.num_bridges, s_sim.num_nodes); + s_sim.area.x, s_sim.area.y, s_sim.area.z, s_mesh->radio->name, + s_mesh->name, s_sim.num_bridges, s_sim.num_nodes); check_area(&s_sim); + if (s_mesh->single_bridge && s_sim.num_bridges != 1) { + fprintf(stderr, "%s requires exactly one bridge\n", s_mesh->name); + return EXIT_FAILURE; + } // mg_log_set(MG_LL_INFO); mg_mgr_init(&s_sim.tester.mgr); - mg_tcpip_init(&s_sim.tester.mgr, &s_sim.tester.lif); + MG_TCPIP_DRIVER_SIM_LAN_INIT(&s_sim.tester.mgr, &s_sim.tester.lif); s_sim.tester.next = s_sim.nodes; bridges_init(&s_sim); nodes_init(&s_sim); - // Main loop: STEP_MICROSECONDS of virtual time per step + // Drain packet processing at the current time. Only idle timer polling + // waits for STEP_MICROSECONDS; radio deadlines remain exact. started = clock(); while (s_sim.now < s_sim.duration) { - dhcp_poll(&s_sim); - tester_poll(&s_sim); - bridges_poll(&s_sim); - nodes_poll(&s_sim); - s_sim.now += STEP_MICROSECONDS; + radio_poll(&s_sim); + if (s_sim.now == poll_at || s_sim.now == mesh_at || s_sim.work_pending) { + do { + s_sim.work_pending = false; + dhcp_poll(&s_sim); + tester_poll(&s_sim); + { + struct bridge *b; + struct radio_node *n; + mesh_at = (uint64_t) -1; + for (b = s_sim.bridges; b != NULL; b = b->next) { + mg_mgr_poll(&b->mgr, 0); + if (b->radio.mesh->next_tick < mesh_at) + mesh_at = b->radio.mesh->next_tick; + } + for (n = s_sim.nodes; n != NULL; n = n->next) { + mg_mgr_poll(&n->mgr, 0); + if (n->radio.mesh->next_tick < mesh_at) + mesh_at = n->radio.mesh->next_tick; + } + } + if (mg_queue_next(&s_sim.tester.lif.recv_queue, &pending) > 0) { + s_sim.work_pending = true; + } + } while (s_sim.work_pending); + if (s_sim.now == poll_at) poll_at += STEP_MICROSECONDS; + } + s_sim.now = radio_next(&s_sim, mesh_at < poll_at ? mesh_at : poll_at); + if (s_sim.now > s_sim.duration) s_sim.now = s_sim.duration; } printf("Requests: %d ok, %d failed, of %d nodes", s_sim.tester.ok, s_sim.tester.failed, s_sim.num_nodes); @@ -426,8 +721,56 @@ int main(int argc, char *argv[]) { printf("; ms: min %.1f, mean %.1f, max %.1f", s_sim.tester.min, s_sim.tester.sum / s_sim.tester.ok, s_sim.tester.max); } - printf("\nSimulated %g s in %.3f s\n", (double) s_sim.now / 1e6, + printf("\nSimulated %g s in %.3f s", (double) (int64_t) s_sim.now / 1e6, (double) (clock() - started) / CLOCKS_PER_SEC); + if (s_sim.nodes != NULL) { + const struct radio_node *n; + size_t min = (size_t) -1, max = 0; + double total = 0; + for (n = s_sim.nodes; n != NULL; n = n->next) { + size_t peak = n->radio.mesh->ram_peak; + if (peak < min) min = peak; + if (peak > max) max = peak; + total += (double) peak; + } + printf("; peak mesh RAM per node (bytes): min %zu, max %zu, avg %.1f", min, + max, total / s_sim.num_nodes); + } + putchar('\n'); + while (s_sim.air_head != NULL) { + struct radio_frame *f = s_sim.air_head; + s_sim.air_head = f->next; + free(f); + } + printf("Radio: %lu sent, %lu collided, %lu lost\n", + s_sim.air_sent, s_sim.air_collisions, s_sim.air_lost); + { + struct radio_node *n; + int count = 0, joined = 0; + for (n = s_sim.nodes; n != NULL; n = n->next) { + if (n->mgr.ifp != NULL && n->mgr.ifp->ip != 0) count++; + if (n->radio.mesh->joined) joined++; + } + printf("Nodes with IP: %d of %d, joined: %d of %d\n", count, + s_sim.num_nodes, joined, s_sim.num_nodes); + } + { + struct bridge *b; + struct radio_node *n; + mg_log_set(MG_LL_NONE); + for (b = s_sim.bridges; b != NULL; b = b->next) { + mg_mgr_free(&b->mgr); + s_mesh->free(b->radio.mesh); + free(b->radio.mesh); + free(b->radio.rx.buf); + } + for (n = s_sim.nodes; n != NULL; n = n->next) { + mg_mgr_free(&n->mgr); + s_mesh->free(n->radio.mesh); + free(n->radio.mesh); + free(n->radio.rx.buf); + } + } return 0; } diff --git a/test/mesh/simulator.h b/test/mesh/simulator.h index e4c79869..2f83cb81 100644 --- a/test/mesh/simulator.h +++ b/test/mesh/simulator.h @@ -5,22 +5,39 @@ #define MG_ENABLE_TCPIP 1 -#include "l2_802154.h" -#include "l2_mesh.h" #include "mongoose.h" -#include "star.h" + +extern const struct mg_mesh mg_mesh_a; // A position in metres struct pos { double x, y, z; // Coordinates }; +struct simulator; + +// Private state of the simulated radio driver. +struct radio_port { + struct simulator *sim; + struct mg_tcpip_if *lan; // Bridge LAN interface, NULL for a node + const struct pos *pos; // Location of the radio + uint64_t tx_until; // Latest transmission end, us + uint64_t tx_ready_at; // Inter-frame wait ends, us + uint64_t rx_off_until; // Latest backoff/sensing/turnaround/TX end, us + struct mg_mesh_node *mesh; // Mesh state owned by the driver + struct mg_queue rx; + uint32_t random; // Driver backoff PRNG + uint16_t address; // Preassigned radio address +}; + // A bridge: forwards frames between the LAN and the radio, no IP stack struct bridge { struct bridge *next; // Next in simulator::bridges struct pos pos; // Where it is + struct mg_mgr mgr; // Polls the raw radio driver struct mg_tcpip_if lif; // LAN side: LAN driver, gets every LAN frame struct mg_tcpip_if rif; // Radio side: radio driver, gets every frame + struct radio_port radio; }; // A radio node: Mongoose on a radio interface @@ -29,31 +46,56 @@ struct radio_node { struct pos pos; // Where it is struct mg_mgr mgr; // Mongoose event manager struct mg_tcpip_if rif; // Radio interface. rif.ip from DHCP, 0: none yet + struct radio_port radio; }; struct simulator; -// LAN station: a DHCP server, 10.0.0.1. Gives each client an address made +// LAN node: a DHCP server, 10.0.0.1. Gives each client an address made // from its MAC, see node_ip() struct lan_dhcp_server { struct mg_tcpip_if lif; // Its LAN interface, no Mongoose on it }; -// LAN station: the test station, 10.0.0.2, a Mongoose instance. Waits for -// the mesh to settle, then sends GET / to each node +// LAN node: the test node, 10.0.0.2, a Mongoose instance. Waits for +// the mesh to settle, then cycles GET / through nodes until 5 s before the end struct lan_tester { struct mg_mgr mgr; // Its Mongoose struct mg_tcpip_if lif; // Its LAN interface uint64_t wait; // Mesh settle time: start requests then, us - struct radio_node *next; // Node to request next, NULL: all done + struct radio_node *next; // Node to request next, wraps at the end struct mg_connection *c; // Request on the way, NULL: none uint64_t start; // When it started, us int ok, failed; // Requests done, by result double min, max, sum; // Times of successful requests, ms }; +enum { RADIO_BACKOFF, RADIO_CCA, RADIO_START, RADIO_END }; + +// One radio frame waiting on the simulated radio channel +struct radio_frame { + struct radio_frame *next; + struct mg_tcpip_if *from; + uint64_t due; // Next radio event, us + uint64_t start; // Transmission start, for receive overlap checks + uint8_t state, backoff; + bool collided; + size_t len; + uint8_t buf[1540]; +}; + +uint64_t radio_airtime(size_t len); + // Simulator state struct simulator { + struct radio_frame *air_head; // Pending and active frames, singly linked + uint64_t air_busy_until; // Last transmission end, for channel sensing + unsigned long air_sent, air_collisions; + unsigned long air_lost; // Receptions lost to range, random loss or TX overlap + double radio_range; // Maximum reception distance, metres + double radio_loss; // In-range reception loss probability, percent + uint32_t loss_random; // Reception-loss PRNG state + bool work_pending; // Drain immediate work before advancing time struct pos area; // A box: all positions inside struct bridge *bridges; // Bridges, a list struct radio_node *nodes; // Radio nodes, a list @@ -63,5 +105,5 @@ struct simulator { uint32_t last_mac; // Low 3 bytes of the last MAC given int request_timeout_ms; // A request that takes longer failed struct lan_dhcp_server dhcp; // DHCP server on the LAN - struct lan_tester tester; // Test station on the LAN + struct lan_tester tester; // Test node on the LAN }; diff --git a/test/mesh/star.c b/test/mesh/star.c deleted file mode 100644 index 4fcd849f..00000000 --- a/test/mesh/star.c +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2026 Cesanta Software Limited -// All rights reserved - -#include "star.h" - -const struct mg_mesh mg_mesh_star = {"star"}; diff --git a/test/mesh/star.h b/test/mesh/star.h deleted file mode 100644 index 3a1f28e7..00000000 --- a/test/mesh/star.h +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2026 Cesanta Software Limited -// All rights reserved -// -// Star mesh: one hop, nodes talk to a bridge - -#pragma once - -#include "l2_mesh.h" - -extern const struct mg_mesh mg_mesh_star; diff --git a/tutorials/mqtt/mqtt-client/mongoose_mqtt.c b/tutorials/mqtt/mqtt-client/mongoose_mqtt.c index 50598c91..b8c50fbe 100644 --- a/tutorials/mqtt/mqtt-client/mongoose_mqtt.c +++ b/tutorials/mqtt/mqtt-client/mongoose_mqtt.c @@ -3,8 +3,12 @@ #include "mongoose.h" +#ifndef MQTT_SERVER_URL #define MQTT_SERVER_URL "mqtt://broker.hivemq.com:1883" +#endif +#ifndef MQTT_CLIENT_ID #define MQTT_CLIENT_ID "d3" +#endif #define MQTT_USER MQTT_CLIENT_ID #define MQTT_PASS "" #define MQTT_PUBLISH_TOPIC "mg/" MQTT_CLIENT_ID "/tx" diff --git a/tutorials/nordic/xiao-52840/minimal/Makefile b/tutorials/nordic/xiao-52840/minimal/Makefile new file mode 100644 index 00000000..121bcf92 --- /dev/null +++ b/tutorials/nordic/xiao-52840/minimal/Makefile @@ -0,0 +1,61 @@ +# Environment setup: https://mongoose.ws/documentation/tutorials/tools/ + +MGDIR = ../../../.. + +# Flash over USB: the board's Adafruit UF2 bootloader does serial DFU. The +# firmware resets into it on a 1200 baud touch, see usb.c. PORT: the board's +# USB serial port. adafruit-nrfutil: pip install adafruit-nrfutil +PORT ?= /dev/cu.usbmodem11301 +NRFUTIL ?= adafruit-nrfutil + +CFLAGS = -W -Wall -Wextra -Werror -Wshadow -Wdouble-promotion +CFLAGS += -Wformat-truncation -fno-common -Wno-conversion -Wno-sign-conversion +CFLAGS += -g3 -Os -ffunction-sections -fdata-sections +CFLAGS += -I. -I$(MGDIR) -Icmsis_core/CMSIS/Core/Include +CFLAGS += -Inrfx/mdk -Inrfx -Inrfx/hal -Inrfx/templates -Inrfx/drivers/src -Itinyusb/src +CFLAGS += -Wno-cast-align -Wno-shadow -Wno-unused-parameter +CFLAGS += -DNRF52840_XXAA -D__STARTUP_CLEAR_BSS -D__START=main -D__HEAP_SIZE=0 +CFLAGS += -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 +CFLAGS += $(CFLAGS_EXTRA) + +LDFLAGS ?= -Tlink.ld -nostdlib -nostartfiles --specs=nano.specs -lc -lm -lgcc -Wl,--gc-sections + +SOURCES = main.c hal.c usb.c $(MGDIR)/mongoose.c +SOURCES += $(MGDIR)/tutorials/mqtt/mqtt-client/mongoose_mqtt.c +SOURCES += nrfx/mdk/system_nrf52840.c nrfx/mdk/gcc_startup_nrf52840.S + +# TinyUSB, for the USB serial console: the board has no USB-UART chip +SOURCES += tinyusb/src/tusb.c tinyusb/src/common/tusb_fifo.c +SOURCES += tinyusb/src/device/usbd.c tinyusb/src/device/usbd_control.c +SOURCES += tinyusb/src/class/cdc/cdc_device.c +SOURCES += tinyusb/src/portable/nordic/nrf5x/dcd_nrf5x.c + +all build: firmware.hex + +firmware.hex: firmware.elf + arm-none-eabi-objcopy -O ihex $< $@ + arm-none-eabi-size --format=berkeley $< + +firmware.elf: cmsis_core nrfx tinyusb $(SOURCES) $(wildcard *.h) link.ld Makefile + arm-none-eabi-gcc $(SOURCES) $(CFLAGS) $(LDFLAGS) -o $@ + +firmware.zip: firmware.hex + $(NRFUTIL) dfu genpkg --dev-type 0x0052 --sd-req 0xFFFE --application $< $@ + +flash: firmware.zip + $(NRFUTIL) dfu serial --package $< -p $(PORT) -b 115200 --singlebank --touch 1200 + +cmsis_core: + git clone -q -c advice.detachedHead=false --depth 1 -b 5.9.0 https://github.com/ARM-software/CMSIS_5 $@ + +# nRF52840 device headers and SystemInit(), nrf_* register helpers for TinyUSB +nrfx: + git clone -q -c advice.detachedHead=false --depth 1 -b v2.11.0 https://github.com/NordicSemiconductor/nrfx $@ + +tinyusb: + git clone -q -c advice.detachedHead=false --depth 1 -b 0.14.0 https://github.com/hathach/tinyusb $@ + +clean: + rm -rf firmware.* *.dSYM tinyusb nrfx cmsis_core + +.PHONY: all build flash clean diff --git a/tutorials/nordic/xiao-52840/minimal/hal.c b/tutorials/nordic/xiao-52840/minimal/hal.c new file mode 100644 index 00000000..cb2cd18f --- /dev/null +++ b/tutorials/nordic/xiao-52840/minimal/hal.c @@ -0,0 +1,141 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "hal.h" + +// Added to minimise the bin size: avoid pulling printf and friends +void __assert_func(const char *s, int line, const char *fn, const char *expr) { + (void) s, (void) line, (void) fn, (void) expr; + for (;;) (void) 0; // or trigger the fault handler +} + +bool hal_timer_expired(volatile uint64_t *t, uint64_t period, uint64_t now) { + uint64_t diff = now - *t; // Wrap-safe elapsed time since last expiry + if (period == 0) return false; // Avoid division by zero + if (diff < period) return false; // Period has not elapsed yet + *t += (diff / period) * period; // Preserve cadence, skip missed periods + return true; +} + +static volatile uint64_t s_ticks; // Milliseconds since boot +void SysTick_Handler(void) { // SyStick IRQ handler, triggered every 1ms + s_ticks++; +} + +uint64_t hal_get_tick(void) { + return s_ticks; +}; + +struct stat; +__attribute__((weak)) int _fstat(int fd, struct stat *st) { + (void) fd, (void) st; + return -1; +} + +extern unsigned char _end[]; // End of data section, start of heap. See link.ld +static unsigned char *s_current_heap_end = _end; + +size_t hal_ram_used(void) { + return (size_t) (s_current_heap_end - _end); +} + +size_t hal_ram_free(void) { + unsigned char endofstack; + return (size_t) (&endofstack - s_current_heap_end); +} + +void *_sbrk(int incr) { + unsigned char *prev_heap; + unsigned char *heap_end = (unsigned char *) ((size_t) &heap_end - 256); + prev_heap = s_current_heap_end; + // Check how much space we got from the heap end to the stack end + if (s_current_heap_end + incr > heap_end) return (void *) -1; + s_current_heap_end += incr; + return prev_heap; +} + +__attribute__((weak)) int _open(const char *path) { + (void) path; + return -1; +} + +__attribute__((weak)) int _close(int fd) { + (void) fd; + return -1; +} + +__attribute__((weak)) int _isatty(int fd) { + (void) fd; + return 1; +} + +__attribute__((weak)) int _lseek(int fd, int ptr, int dir) { + (void) fd, (void) ptr, (void) dir; + return 0; +} + +__attribute__((weak)) void _exit(int status) { + (void) status; + for (;;) asm volatile("BKPT #0"); +} + +__attribute__((weak)) void _kill(int pid, int sig) { + (void) pid, (void) sig; +} + +__attribute__((weak)) int _getpid(void) { + return -1; +} + +__attribute__((weak)) int _write(int fd, char *ptr, int len) { + (void) fd, (void) ptr, (void) len; + return -1; +} + +__attribute__((weak)) int _read(int fd, char *ptr, int len) { + (void) fd, (void) ptr, (void) len; + return -1; +} + +__attribute__((weak)) int _link(const char *a, const char *b) { + (void) a, (void) b; + return -1; +} + +__attribute__((weak)) int _unlink(const char *a) { + (void) a; + return -1; +} + +__attribute__((weak)) int _stat(const char *path, struct stat *st) { + (void) path, (void) st; + return -1; +} + +__attribute__((weak)) int mkdir(const char *path, int mode) { + (void) path, (void) mode; + return -1; +} + +__attribute__((weak)) void _init(void) { +} + +uint64_t mg_micros(void); +// SysTick runs at the core clock and reloads every millisecond. Sample +// the tick counter and the sub-millisecond counter in one critical section. +uint64_t mg_micros(void) { + uint32_t primask = __get_PRIMASK(), val, pending; + uint64_t ms; + __disable_irq(); + ms = hal_get_tick(); + pending = SCB->ICSR & SCB_ICSR_PENDSTSET_Msk; + val = SysTick->VAL; + if (pending != (SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)) { + pending = SCB_ICSR_PENDSTSET_Msk; + val = SysTick->VAL; + } + __set_PRIMASK(primask); + if (pending != 0 && val != 0) ms++; + return ms * 1000 + ((uint64_t) (SysTick->LOAD - val) * 1000) / + (SysTick->LOAD + 1); +} diff --git a/tutorials/nordic/xiao-52840/minimal/hal.h b/tutorials/nordic/xiao-52840/minimal/hal.h new file mode 100644 index 00000000..85157db7 --- /dev/null +++ b/tutorials/nordic/xiao-52840/minimal/hal.h @@ -0,0 +1,68 @@ +#pragma once +#include +#include +#include +#include "nrf.h" +uint64_t hal_get_tick(void); +bool hal_timer_expired(volatile uint64_t *t, uint64_t period, uint64_t now); +size_t hal_ram_used(void); +size_t hal_ram_free(void); +void usb_init(void); +void usb_poll(void); +void usb_log(char ch, void *param); + +#define BIT(x) (1UL << (x)) +#define PIN(port, num) (((port) << 5) | (num)) +#define PINNO(pin) ((pin) & 31) +#define PINPORT(pin) ((pin) >> 5) + +static inline NRF_GPIO_Type *hal_gpio_bank(uint16_t pin) { + return PINPORT(pin) == 0 ? NRF_P0 : NRF_P1; +} + +static inline void hal_gpio_write(uint16_t pin, bool val) { + NRF_GPIO_Type *gpio = hal_gpio_bank(pin); + if (val) { + gpio->OUTSET = BIT(PINNO(pin)); + } else { + gpio->OUTCLR = BIT(PINNO(pin)); + } +} + +static inline void hal_gpio_toggle(uint16_t pin) { + hal_gpio_write(pin, (hal_gpio_bank(pin)->OUT & BIT(PINNO(pin))) == 0); +} + +static inline void hal_gpio_output(uint16_t pin) { + hal_gpio_bank(pin)->PIN_CNF[PINNO(pin)] = + (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos) | + (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos); +} + +static inline void hal_system_init(void) { + extern uint32_t __isr_vector[]; + SCB->VTOR = (uint32_t) __isr_vector; // Application vectors above the bootloader + __DSB(); + __ISB(); + __enable_irq(); + NRF_CLOCK->EVENTS_HFCLKSTARTED = 0; + NRF_CLOCK->TASKS_HFCLKSTART = 1; + while (!NRF_CLOCK->EVENTS_HFCLKSTARTED) (void) 0; + SystemCoreClockUpdate(); + SysTick_Config(SystemCoreClock / 1000); +} + +static inline void hal_rng_init(void) { + NRF_RNG->CONFIG = 1; + NRF_RNG->TASKS_START = 1; +} + +static inline uint32_t hal_rng_read(void) { + uint32_t value = 0; + for (unsigned i = 0; i < 4; i++) { + while (!NRF_RNG->EVENTS_VALRDY) (void) 0; + value |= (uint32_t) NRF_RNG->VALUE << (i * 8); + NRF_RNG->EVENTS_VALRDY = 0; + } + return value; +} diff --git a/tutorials/nordic/xiao-52840/minimal/link.ld b/tutorials/nordic/xiao-52840/minimal/link.ld new file mode 100644 index 00000000..4c9448a2 --- /dev/null +++ b/tutorials/nordic/xiao-52840/minimal/link.ld @@ -0,0 +1,62 @@ +/* nRF52840 on the XIAO nRF52840, with the Adafruit UF2 bootloader. Flash: + * MBR and SoftDevice S140 below 0x27000, the bootloader from 0xED000 up: the + * application in between. The SoftDevice is not enabled, so all RAM is ours, + * except the first 24k it would take: kept free, as the Arduino core does */ +ENTRY(Reset_Handler); +MEMORY { + flash(rx) : ORIGIN = 0x00027000, LENGTH = 0xC6000 + sram(rwx) : ORIGIN = 0x20006000, LENGTH = 0x3A000 +} + +SECTIONS { + .vectors : { KEEP(*(.isr_vector)) . = ALIGN(4); } > flash + .text : { *(.text* .text.*) } > flash + .rodata : { *(.rodata*) . = ALIGN(4); } > flash + + .data : { + _sdata = .; + *(.first_data) + *(.iram .iram.*) /* Mongoose MG_IRAM functions */ + *(.data SORT(.data.*)) + . = ALIGN(4); + _edata = .; + } > sram AT > flash + __data_start = _sdata; + __data_end = _edata; + __data_load_start = LOADADDR(.data); + /* Unused data regions expected by Nordic's standard startup. */ + __sdata_start = _edata; + __sdata_end = _edata; + __sdata_load_start = _edata; + __tdata_start = _edata; + __tdata_end = _edata; + __tdata_load_start = _edata; + __fast_start = _edata; + __fast_end = _edata; + __fast_load_start = _edata; + + .bss : { + _sbss = .; + *(.bss SORT(.bss.*) COMMON) + . = ALIGN(4); + _ebss = .; + } > sram + __bss_start__ = _sbss; + __bss_end__ = _ebss; + __tbss_start__ = _ebss; + __tbss_end__ = _ebss; + __sbss_start__ = _ebss; + __sbss_end__ = _ebss; + + /* Health record, see MG_HEALTH_RAM: not cleared, survives a warm reset */ + .mg_health (NOLOAD) : { KEEP(*(.mg_health .mg_health*)) } > sram + + . = ALIGN(8); + _end = .; /* Start of heap, see _sbrk() in hal.c */ + + /* Nordic startup reserves its default 16 KiB stack at the top of RAM. */ + .stack ORIGIN(sram) + LENGTH(sram) - 0x4000 (NOLOAD) : { + KEEP(*(.stack)) + } > sram + ASSERT(_end <= __StackLimit, "RAM sections overlap the stack") +} diff --git a/tutorials/nordic/xiao-52840/minimal/main.c b/tutorials/nordic/xiao-52840/minimal/main.c new file mode 100644 index 00000000..97e9f4da --- /dev/null +++ b/tutorials/nordic/xiao-52840/minimal/main.c @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "hal.h" +#include "mongoose.h" + +#define LED1 PIN(0, 26) // P0.26, user LED, active low + +static void log_fn(char ch, void *param) { + usb_log(ch, param); +} + +static void blink_task(void) { + static uint64_t blink_timer = 0; + if (hal_timer_expired(&blink_timer, 500, hal_get_tick())) { + hal_gpio_toggle(LED1); + } +} + +uint64_t mg_millis(void) { + return hal_get_tick(); +} + +bool mg_random(void *buf, size_t len) { + for (size_t n = 0; n < len; n += sizeof(uint32_t)) { + uint32_t r = hal_rng_read(); + memcpy((char *) buf + n, &r, n + sizeof(r) > len ? len - n : sizeof(r)); + } + return true; +} + +// Self-signed P-256 certificate and key for the HTTPS listener. Created by: +// openssl ecparam -genkey -name prime256v1 -noout -out key.pem +// openssl req -x509 -key key.pem -out cert.pem -days 3650 -subj /CN=mg +#define TLS_CERT \ + "-----BEGIN CERTIFICATE-----\n" \ + "MIIBgjCCASegAwIBAgIUIJOq77Qn7lHbH6WseMemeqorR50wCgYIKoZIzj0EAwIw\n" \ + "FjEUMBIGA1UEAwwLbnVjbGVvLWg3MjMwHhcNMjYxMDEyMTUwMjE5WhcNMzYxMDA5\n" \ + "MTUwMjE5WjAWMRQwEgYDVQQDDAtudWNsZW8taDcyMzBZMBMGByqGSM49AgEGCCqG\n" \ + "SM49AwEHA0IABB6IQJNmzrsdQL+2BnZYS7GYOaj7sz8f6cjBmz3n8D9Em19XodlU\n" \ + "uy4ybIvyhr766lL0ERoTmMUivb/lGmzx9C+jUzBRMB0GA1UdDgQWBBQqxMuc4gj7\n" \ + "MTMzreWlRMNI3QMqTzAfBgNVHSMEGDAWgBQqxMuc4gj7MTMzreWlRMNI3QMqTzAP\n" \ + "BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQDY4xaJpuxq6b3rHK3b\n" \ + "lOIQyfkQX+orCRqQUkseplqpqwIhAKswZFNn+2zUNRRNY8+cHmdIwmdpsmxtoWYP\n" \ + "BCjX++LI\n" \ + "-----END CERTIFICATE-----\n" + +#define TLS_KEY \ + "-----BEGIN EC PRIVATE KEY-----\n" \ + "MHcCAQEEIJ/GBl0TlD52q9PMg1KWaNIdXqp8b/HyIsABmBpqC+O1oAoGCCqGSM49\n" \ + "AwEHoUQDQgAEHohAk2bOux1Av7YGdlhLsZg5qPuzPx/pyMGbPefwP0SbX1eh2VS7\n" \ + "LjJsi/KGvvrqUvQRGhOYxSK9v+UabPH0Lw==\n" \ + "-----END EC PRIVATE KEY-----\n" + +static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_ACCEPT && c->is_tls) { // Accepted on the https:// listener + struct mg_tls_opts opts = {.cert = mg_str(TLS_CERT), + .key = mg_str(TLS_KEY)}; + mg_tls_init(c, &opts); + } else if (ev == MG_EV_HTTP_MSG) { + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + if (mg_match(hm->uri, mg_str("/api/tick"), NULL)) { + mg_http_reply(c, 200, "", "{%m:%llu}\n", MG_ESC("tick"), hal_get_tick()); + } else if (mg_match(hm->uri, mg_str("/api/crash"), NULL)) { + __asm volatile("udf #0"); // Trigger HardFault + } else { + mg_http_reply(c, 200, "", + "Hi from Mongoose!\n" + "tick: %llu\n" + "ram used: %zu\n" + "ram free: %zu\n", + hal_get_tick(), hal_ram_used(), hal_ram_free()); + } + } +} + +int main(void) { + MG_HEALTH_INIT(); + + hal_system_init(); + usb_init(); + mg_log_set_fn(log_fn, NULL); + hal_rng_init(); + hal_gpio_write(LED1, true); + hal_gpio_output(LED1); + + struct mg_mgr mgr; + mg_mgr_init(&mgr); + mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL); + mg_http_listen(&mgr, "https://0.0.0.0", http_ev_handler, NULL); + mg_mqtt_init(&mgr); + + MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000)); + MG_INFO(("RAM used: %zu, free: %zu", hal_ram_used(), hal_ram_free())); + MG_INFO(("Crash record: %M", mg_print_crash_record)); + + for (;;) { + usb_poll(); + mg_mgr_poll(&mgr, 0); + mg_mqtt_poll(&mgr); + blink_task(); + } + + return 0; +} diff --git a/tutorials/nordic/xiao-52840/minimal/mongoose_config.h b/tutorials/nordic/xiao-52840/minimal/mongoose_config.h new file mode 100644 index 00000000..168d86fc --- /dev/null +++ b/tutorials/nordic/xiao-52840/minimal/mongoose_config.h @@ -0,0 +1,24 @@ +#pragma once +#include "nrf.h" +#define MG_ARCH MG_ARCH_ARMGCC +#define MG_ENABLE_TCPIP 1 +#define MG_ENABLE_DRIVER_NRF52 1 +#define MG_ENABLE_TCPIP_TCPRTX 1 +#define MG_ENABLE_CUSTOM_MILLIS 1 +#define MG_ENABLE_CUSTOM_RANDOM 1 +#define MG_TLS MG_TLS_BUILTIN +#define MQTT_CLIENT_ID "xiao-nrf52840" + +#ifndef MQTT_SERVER_URL +#define MQTT_SERVER_URL "mqtt://broker.emqx.io:1883" +#endif + +// Stable, locally administered MAC derived from the factory device ID. +#define MG_SET_MAC_ADDRESS(mac) \ + do { \ + uint32_t id_[2] = {NRF_FICR->DEVICEID[0], NRF_FICR->DEVICEID[1]}; \ + uint8_t hash_[32]; \ + mg_sha256(hash_, (uint8_t *) id_, sizeof(id_)); \ + memcpy(mac, hash_, 6); \ + (mac)[0] = ((mac)[0] & 0xfc) | 2; \ + } while (0) diff --git a/tutorials/nordic/xiao-52840/minimal/tusb_config.h b/tutorials/nordic/xiao-52840/minimal/tusb_config.h new file mode 100644 index 00000000..4ca0c5cd --- /dev/null +++ b/tutorials/nordic/xiao-52840/minimal/tusb_config.h @@ -0,0 +1,9 @@ +#pragma once +#define CFG_TUSB_MCU OPT_MCU_NRF5X +#define CFG_TUSB_OS OPT_OS_NONE +#define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE +#define CFG_TUD_ENDPOINT0_SIZE 64 +#define CFG_TUD_CDC 1 +#define CFG_TUD_CDC_RX_BUFSIZE 256 +#define CFG_TUD_CDC_TX_BUFSIZE 4096 +#define CFG_TUD_CDC_EP_BUFSIZE 64 diff --git a/tutorials/nordic/xiao-52840/minimal/usb.c b/tutorials/nordic/xiao-52840/minimal/usb.c new file mode 100644 index 00000000..90dd49cc --- /dev/null +++ b/tutorials/nordic/xiao-52840/minimal/usb.c @@ -0,0 +1,99 @@ +#include "hal.h" +#include "tusb.h" +extern void tusb_hal_nrf_power_event(uint32_t event); +void USBD_IRQHandler(void) { + tud_int_handler(0); +} +void POWER_CLOCK_IRQHandler(void) { + if (NRF_POWER->EVENTS_USBDETECTED) { + NRF_POWER->EVENTS_USBDETECTED = 0; + tusb_hal_nrf_power_event(0); + } + if (NRF_POWER->EVENTS_USBREMOVED) { + NRF_POWER->EVENTS_USBREMOVED = 0; + tusb_hal_nrf_power_event(1); + } + if (NRF_POWER->EVENTS_USBPWRRDY) { + NRF_POWER->EVENTS_USBPWRRDY = 0; + tusb_hal_nrf_power_event(2); + } +} +void usb_init(void) { + NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk | + POWER_INTENSET_USBREMOVED_Msk | + POWER_INTENSET_USBPWRRDY_Msk; + NVIC_SetPriority(POWER_CLOCK_IRQn, 3); + NVIC_SetPriority(USBD_IRQn, 3); + if (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) + tusb_hal_nrf_power_event(0); + if (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_OUTPUTRDY_Msk) + tusb_hal_nrf_power_event(2); + NRF_POWER->EVENTS_USBDETECTED = 0; + NRF_POWER->EVENTS_USBPWRRDY = 0; + NRF_POWER->EVENTS_USBREMOVED = 0; + NVIC_ClearPendingIRQ(POWER_CLOCK_IRQn); + tusb_init(); + NVIC_EnableIRQ(POWER_CLOCK_IRQn); +} +void usb_poll(void) { + tud_task(); + tud_cdc_write_flush(); +} +void usb_log(char ch, void *param) { + (void) param; + if (tud_cdc_connected()) tud_cdc_write_char(ch); +} +void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts) { + cdc_line_coding_t lc; + (void) rts; + tud_cdc_n_get_line_coding(itf, &lc); + if (!dtr && lc.bit_rate == 1200) { + NRF_POWER->GPREGRET = 0x57; + NVIC_SystemReset(); + } +} +static const tusb_desc_device_t s_dev = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = 64, + .idVendor = 0xcafe, + .idProduct = 0x4001, + .bcdDevice = 0x0100, + .iManufacturer = 1, + .iProduct = 2, + .iSerialNumber = 3, + .bNumConfigurations = 1}; +uint8_t const *tud_descriptor_device_cb(void) { + return (const uint8_t *) &s_dev; +} +static const uint8_t s_cfg[] = { + TUD_CONFIG_DESCRIPTOR(1, 2, 0, TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN, 0, + 100), + TUD_CDC_DESCRIPTOR(0, 0, 0x81, 8, 0x02, 0x82, 64)}; +uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { + (void) index; + return s_cfg; +} +uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { + static uint16_t buf[32]; + const char *strings[] = {"", "Cesanta", "XIAO nRF52840", "mongoose-nrf52840"}; + size_t n = 0; + (void) langid; + if (index == 0) { + buf[1] = 0x0409; + n = 1; + } else if (index < 4) { + const char *s = strings[index]; + while (s[n] && n < 31) { + buf[n + 1] = s[n]; + n++; + } + } else + return NULL; + buf[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * n + 2)); + return buf; +} diff --git a/tutorials/nxp/frdm-mcxw71/minimal/Makefile b/tutorials/nxp/frdm-mcxw71/minimal/Makefile index b10b19da..9a034cd4 100644 --- a/tutorials/nxp/frdm-mcxw71/minimal/Makefile +++ b/tutorials/nxp/frdm-mcxw71/minimal/Makefile @@ -14,6 +14,7 @@ CFLAGS += -mcpu=cortex-m33 -mthumb -mfloat-abi=hard -mfpu=fpv5-sp-d16 $(CFLAGS_E LDFLAGS ?= -Tlink.ld -nostdlib -nostartfiles --specs=nano.specs -lc -lm -lgcc -Wl,--gc-sections SOURCES = main.c hal.c $(MGDIR)/mongoose.c + SOURCES += $(MGDIR)/tutorials/mqtt/mqtt-client/mongoose_mqtt.c SOURCES += cmsis_mcxw71/MCXW/MCXW716C/gcc/startup_MCXW716C.S @@ -23,9 +24,13 @@ firmware.bin: firmware.elf arm-none-eabi-objcopy -O binary $< $@ arm-none-eabi-size --format=berkeley $< -firmware.elf: cmsis_core cmsis_mcxw71 $(SOURCES) $(wildcard *.h) link.ld Makefile +firmware.elf: cmsis_core cmsis_mcxw71 $(SOURCES) $(wildcard *.h) $(MGDIR)/mongoose.h link.ld Makefile mesh-config arm-none-eabi-gcc $(SOURCES) $(CFLAGS) $(LDFLAGS) -o $@ +# Rebuild when overriding the board address or timing. +.PHONY: mesh-config +mesh-config: + cmsis_core: git clone -q -c advice.detachedHead=false --depth 1 -b 5.9.0 https://github.com/ARM-software/CMSIS_5 $@ diff --git a/tutorials/nxp/frdm-mcxw71/minimal/hal.c b/tutorials/nxp/frdm-mcxw71/minimal/hal.c index 3578b226..3d59098d 100644 --- a/tutorials/nxp/frdm-mcxw71/minimal/hal.c +++ b/tutorials/nxp/frdm-mcxw71/minimal/hal.c @@ -9,7 +9,7 @@ // 802.15.4 register setup. Then select the legacy 802.15.4 demodulator and // CCA from RX digital. See PhyHwInit() in NXP ieee_802_15_4 PHY sources, // PhyPacketProcessor.c -bool hal_radio_init(void) { +void hal_radio_init(void) { uint32_t stat = RFMC_RF2P4GHZ_STAT_RST_STAT_MASK | RFMC_RF2P4GHZ_STAT_LP_ACK_STAT_MASK; size_t i, n = 1000000; @@ -20,7 +20,10 @@ bool hal_radio_init(void) { RFMC_RF2P4GHZ_CTRL_RST_MASK); // Release radio reset, no low power while ((RFMC->RF2P4GHZ_STAT & stat) != 0 || (RFMC->XO_STAT & RFMC_XO_STAT_XTAL_RDY_MASK) == 0) { - if (--n == 0) return false; // Radio did not wake up + if (--n == 0) { + MG_ERROR(("Radio init failed")); // Radio did not wake up + return; + } } RADIO_CTRL->LL_CTRL = RADIO_CTRL_LL_CTRL_ACTIVE_LL_MASK; RFMC->RF2P4GHZ_CTRL |= @@ -31,17 +34,17 @@ bool hal_radio_init(void) { XCVR_MISC->XCVR_CTRL &= ~XCVR_MISC_XCVR_CTRL_DEMOD_SEL_MASK; XCVR_MISC->XCVR_CTRL |= XCVR_MISC_XCVR_CTRL_DEMOD_SEL(2); XCVR_ZBDEMOD->CCA_LQI_SRC |= XCVR_ZBDEMOD_CCA_LQI_SRC_CCA1_FROM_RX_DIG(1); - return true; } -// Random bytes for Mongoose, MG_ENABLE_CUSTOM_RANDOM. Entropy is radio noise: -// receiver RSSI, sampled while the radio receives, mixed with SysTick by -// SHA-256. Good enough for random addresses and nonces, not a certified TRNG -bool mg_random(void *buf, size_t len) { - static uint32_t counter; - uint8_t digest[32], *p = (uint8_t *) buf; +// Random 32 bits, for mg_random(). Entropy is radio noise: receiver RSSI, +// sampled while the radio receives, mixed with SysTick by SHA-256. One hash +// fills a pool of 8 words. Good enough for random addresses and nonces, not +// a certified TRNG +uint32_t hal_rng_read(void) { + static uint32_t counter, pool[8]; + static size_t avail; size_t i, n; - while (len > 0) { + if (avail == 0) { mg_sha256_ctx ctx; mg_sha256_init(&ctx); counter++; @@ -51,11 +54,10 @@ bool mg_random(void *buf, size_t len) { mg_sha256_update(&ctx, (uint8_t *) sample, sizeof(sample)); for (n = 0; n < 50; n++) __NOP(); // Let the RSSI update } - mg_sha256_final(digest, &ctx); - n = len < sizeof(digest) ? len : sizeof(digest); - memcpy(p, digest, n), p += n, len -= n; + mg_sha256_final((uint8_t *) pool, &ctx); + avail = sizeof(pool) / sizeof(pool[0]); } - return true; + return pool[--avail]; } // Added to minimise the bin size: avoid pulling printf and friends @@ -81,6 +83,26 @@ uint64_t hal_get_tick(void) { return s_ticks; } +uint64_t mg_micros(void); +// SysTick runs at the core clock and reloads every millisecond. Sample +// the tick counter and the sub-millisecond counter in one critical section. +uint64_t mg_micros(void) { + uint32_t primask = __get_PRIMASK(), val, pending; + uint64_t ms; + __disable_irq(); + ms = hal_get_tick(); + pending = SCB->ICSR & SCB_ICSR_PENDSTSET_Msk; + val = SysTick->VAL; + if (pending != (SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)) { + pending = SCB_ICSR_PENDSTSET_Msk; + val = SysTick->VAL; + } + __set_PRIMASK(primask); + if (pending != 0 && val != 0) ms++; + return ms * 1000 + ((uint64_t) (SysTick->LOAD - val) * 1000) / + (SysTick->LOAD + 1); +} + uint32_t SystemCoreClock; // Called by the startup code before .data and .bss are initialised, so it diff --git a/tutorials/nxp/frdm-mcxw71/minimal/hal.h b/tutorials/nxp/frdm-mcxw71/minimal/hal.h index 778e78eb..d9b0d494 100644 --- a/tutorials/nxp/frdm-mcxw71/minimal/hal.h +++ b/tutorials/nxp/frdm-mcxw71/minimal/hal.h @@ -17,11 +17,14 @@ uint64_t hal_get_tick(void); bool hal_timer_expired(volatile uint64_t *t, uint64_t period, uint64_t now); size_t hal_ram_used(void); size_t hal_ram_free(void); -bool hal_radio_init(void); // Set up the 2.4 GHz transceiver for 802.15.4 +void hal_radio_init(void); // Set up the 2.4 GHz transceiver for 802.15.4 +uint32_t hal_rng_read(void); // Random 32 bits, radio noise. See hal.c extern uint32_t SystemCoreClock; // CPU clock, Hz. Set by hal_clock_init() - #define BIT(x) (1UL << (x)) +#define PIN(port, num) ((((port) - 'A') << 8) | (num)) +#define PINNO(pin) (pin & 255) +#define PINPORT(pin) (pin >> 8) #define CLRSET(reg, clear, set) ((reg) = ((reg) & ~(clear)) | (set)) #define SIRC_FREQUENCY 6000000UL // Slow IRC (FRO6M), Hz @@ -92,15 +95,53 @@ static inline void hal_periph_clock(volatile uint32_t *mrcc, uint32_t src) { __DSB(); } -// LPUART1 is wired to the MCU-Link debug probe USB serial port. -// TX = PTC3, RX = PTC2, both pin mux alternative 3. See board.h, pin_mux.c -static inline void hal_uart_init(LPUART_Type *uart, unsigned long baud) { +// PORT, the pin multiplexer, of a pin. Ports A..C +static inline PORT_Type *hal_pin_port(uint16_t pin) { + PORT_Type *ports[] = {PORTA, PORTB, PORTC}; + return ports[PINPORT(pin)]; +} + +// GPIO of a pin. Ports A..C +static inline GPIO_Type *hal_pin_gpio(uint16_t pin) { + GPIO_Type *gpios[] = {GPIOA, GPIOB, GPIOC}; + return gpios[PINPORT(pin)]; +} + +// Route a pin to function mux: 1 GPIO, others see the datasheet pinout. +// Clocks its PORT: MRCC_PORTA..C are consecutive +static inline void hal_pin_mux(uint16_t pin, uint32_t mux) { + hal_periph_clock(&MRCC->MRCC_PORTA + PINPORT(pin), 0); + hal_pin_port(pin)->PCR[PINNO(pin)] = PORT_PCR_MUX(mux); +} + +static inline void hal_gpio_output(uint16_t pin) { + hal_pin_mux(pin, 1); + hal_periph_clock(&MRCC->MRCC_GPIOA + PINPORT(pin), 0); // Consecutive, too + hal_pin_gpio(pin)->PDDR |= BIT(PINNO(pin)); +} + +static inline void hal_gpio_write(uint16_t pin, bool val) { + if (val) { + hal_pin_gpio(pin)->PSOR = BIT(PINNO(pin)); + } else { + hal_pin_gpio(pin)->PCOR = BIT(PINNO(pin)); + } +} + +static inline void hal_gpio_toggle(uint16_t pin) { + hal_pin_gpio(pin)->PTOR = BIT(PINNO(pin)); +} + +// LPUART1, on tx and rx pins, pin mux alternative 3. On FRDM-MCXW71 it is +// wired to the MCU-Link debug probe USB serial port: TX PTC3, RX PTC2, see +// board.h, pin_mux.c +static inline void hal_uart_init(LPUART_Type *uart, uint16_t tx, uint16_t rx, + unsigned long baud) { uint32_t src = MRCC_SRC_FRO192M, freq = hal_firc_freq(); uint32_t best_osr = 16, best_sbr = 1, best_err = UINT32_MAX, osr; if (freq == 0) src = MRCC_SRC_FRO6M, freq = SIRC_FREQUENCY; // FIRC off - hal_periph_clock(&MRCC->MRCC_PORTC, 0); - PORTC->PCR[2] = PORT_PCR_MUX(3); // RX - PORTC->PCR[3] = PORT_PCR_MUX(3); // TX + hal_pin_mux(rx, 3); + hal_pin_mux(tx, 3); hal_periph_clock(&MRCC->MRCC_LPUART1, src); // Find the oversampling ratio (4..32) that gives the lowest baud rate error for (osr = 4; osr <= 32; osr++) { @@ -141,6 +182,11 @@ static inline void hal_wdog_disable(void) { while ((WDOG0->CS & WDOG_CS_RCS_MASK) == 0) (void) 0; } +// Random numbers come from radio noise, see hal_rng_read(): nothing to set +// up here. The radio must be running, see hal_radio_init() +static inline void hal_rng_init(void) { +} + static inline void hal_sys_init(void) { SCB->CPACR |= ((3UL << 10 * 2) | (3UL << 11 * 2)); // Enable FPU hal_wdog_disable(); diff --git a/tutorials/nxp/frdm-mcxw71/minimal/link.ld b/tutorials/nxp/frdm-mcxw71/minimal/link.ld index 9c78c8d5..67f669c6 100644 --- a/tutorials/nxp/frdm-mcxw71/minimal/link.ld +++ b/tutorials/nxp/frdm-mcxw71/minimal/link.ld @@ -33,6 +33,9 @@ SECTIONS { _ebss = .; } > sram + /* Health record, see MG_HEALTH_RAM: not cleared, survives a warm reset */ + .mg_health (NOLOAD) : { KEEP(*(.mg_health .mg_health*)) } > sram + . = ALIGN(8); _end = .; /* Start of heap, see _sbrk() in hal.c */ } diff --git a/tutorials/nxp/frdm-mcxw71/minimal/main.c b/tutorials/nxp/frdm-mcxw71/minimal/main.c index f091d1d7..c67b5252 100644 --- a/tutorials/nxp/frdm-mcxw71/minimal/main.c +++ b/tutorials/nxp/frdm-mcxw71/minimal/main.c @@ -1,24 +1,42 @@ // Copyright (c) 2026 Cesanta Software Limited // All rights reserved -// -// IPv4 over IEEE 802.15.4 on FRDM-MCXW71, Mongoose built-in TCP/IP stack. -// The radio carries Ethernet frames, channel 15, PAN ID 0xabcd. With the -// FRDM-RW612 eth_radio_bridge tutorial, gets an address from the LAN's DHCP -// server and is reachable from the LAN. Serves pages on ports 80 and 443 #include "hal.h" #include "mongoose.h" +#ifndef UART_DEBUG #define UART_DEBUG LPUART1 // MCU-Link USB serial port +#define UART_DEBUG_TX_PIN PIN('C', 3) +#define UART_DEBUG_RX_PIN PIN('C', 2) +#endif + +#define LED1 PIN('A', 19) +#define LED2 PIN('A', 20) +#define LED3 PIN('A', 21) static void log_fn(char ch, void *param) { hal_uart_write_buf(param, &ch, 1); } +static void blink_task(void) { + static uint64_t blink_timer = 0; + if (hal_timer_expired(&blink_timer, 500, hal_get_tick())) { + hal_gpio_toggle(LED2); + } +} + uint64_t mg_millis(void) { return hal_get_tick(); } +bool mg_random(void *buf, size_t len) { + for (size_t n = 0; n < len; n += sizeof(uint32_t)) { + uint32_t r = hal_rng_read(); + memcpy((char *) buf + n, &r, n + sizeof(r) > len ? len - n : sizeof(r)); + } + return true; +} + // Self-signed P-256 certificate and key for the HTTPS listener. Created by: // openssl ecparam -genkey -name prime256v1 -noout -out key.pem // openssl req -x509 -key key.pem -out cert.pem -days 3650 -subj /CN=mg @@ -70,16 +88,21 @@ static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) { } int main(void) { - hal_clock_init(); - hal_uart_init(UART_DEBUG, 115200); - mg_log_set_fn(log_fn, UART_DEBUG); + MG_HEALTH_INIT(); - if (!hal_radio_init()) MG_ERROR(("Radio init failed")); + hal_clock_init(); + hal_uart_init(UART_DEBUG, UART_DEBUG_TX_PIN, UART_DEBUG_RX_PIN, 115200); + mg_log_set_fn(log_fn, UART_DEBUG); + hal_rng_init(); + hal_radio_init(); + hal_gpio_output(LED1), hal_gpio_write(LED1, true); // Active low + hal_gpio_output(LED2), hal_gpio_write(LED2, true); // Active low + hal_gpio_output(LED3), hal_gpio_write(LED3, true); // Active low struct mg_mgr mgr; mg_mgr_init(&mgr); - mg_http_listen(&mgr, "http://0.0.0.0:80", http_ev_handler, NULL); - mg_http_listen(&mgr, "https://0.0.0.0:443", http_ev_handler, NULL); + mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL); + mg_http_listen(&mgr, "https://0.0.0.0", http_ev_handler, NULL); mg_mqtt_init(&mgr); MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000)); @@ -89,6 +112,7 @@ int main(void) { for (;;) { mg_mgr_poll(&mgr, 0); mg_mqtt_poll(&mgr); + blink_task(); } return 0; diff --git a/tutorials/nxp/frdm-mcxw71/minimal/mongoose_config.h b/tutorials/nxp/frdm-mcxw71/minimal/mongoose_config.h index 5d236e1e..f7cd37d3 100644 --- a/tutorials/nxp/frdm-mcxw71/minimal/mongoose_config.h +++ b/tutorials/nxp/frdm-mcxw71/minimal/mongoose_config.h @@ -1,9 +1,10 @@ #pragma once #include "MCXW716C.h" -// See https://mongoose.ws/docs/getting-started/build-environment/ +// See https://mongoose.ws/docs/getting-started/build-options/ #define MG_ARCH MG_ARCH_ARMGCC #define MG_TLS MG_TLS_BUILTIN +#define MQTT_CLIENT_ID "w71" // For tutorials/mqtt/mqtt-client #define MG_OTA MG_OTA_MCXW71 #define MG_ENABLE_TCPIP 1 @@ -21,3 +22,12 @@ (mac)[0] = 2, (mac)[1] = b_[1], (mac)[2] = b_[2], (mac)[3] = b_[3]; \ (mac)[4] = b_[4], (mac)[5] = b_[5]; \ } while (0) + +// Crash report support. Health record lives in the .mg_health region, see link.ld +#define MG_ENABLE_HEALTH 1 +#define MG_HEALTH_RAM __attribute__((section(".mg_health"))) + +// mdash.net device management service support +#define MG_ENABLE_MDASH 0 +#define MG_MDASH_KEY "..." // Set this - mDash device token +#define MG_FIRMWARE_VERSION "w71.1.0.0" diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/Makefile b/tutorials/nxp/frdm-rw612/bridge/Makefile similarity index 89% rename from tutorials/nxp/frdm-rw612/eth_radio_bridge/Makefile rename to tutorials/nxp/frdm-rw612/bridge/Makefile index 6b09174d..a6cc066b 100644 --- a/tutorials/nxp/frdm-rw612/eth_radio_bridge/Makefile +++ b/tutorials/nxp/frdm-rw612/bridge/Makefile @@ -17,6 +17,7 @@ CFLAGS += -mcpu=cortex-m33+nodsp -mthumb -mfloat-abi=hard -mfpu=fpv5-sp-d16 $(CF LDFLAGS ?= -Tlink.ld -nostdlib -nostartfiles --specs=nano.specs -lc -lgcc -Wl,--gc-sections SOURCES = main.c hal.c radio.c startup.c flexspi.c flash_config.c $(MGDIR)/mongoose.c + SOURCES += cmsis_rw612/devices/RW612/drivers/fsl_clock.c SOURCES += cmsis_rw612/devices/RW612/drivers/fsl_trng.c SOURCES += cmsis_rw612/devices/RW612/drivers/fsl_reset.c @@ -35,9 +36,13 @@ firmware.bin: firmware.elf arm-none-eabi-objcopy -O binary $< $@ arm-none-eabi-size --format=berkeley $< -firmware.elf: cmsis_core cmsis_rw612 $(RADIO_FW) $(SOURCES) $(wildcard *.h) link.ld Makefile mongoose_config.h +firmware.elf: cmsis_core cmsis_rw612 $(RADIO_FW) $(SOURCES) $(wildcard *.h) $(MGDIR)/mongoose.h link.ld Makefile mongoose_config.h mesh-config arm-none-eabi-gcc $(SOURCES) $(CFLAGS) $(LDFLAGS) -o $@ +# Rebuild when overriding the board address or timing. +.PHONY: mesh-config +mesh-config: + cmsis_core: git clone -q -c advice.detachedHead=false --depth 1 -b 5.9.0 https://github.com/ARM-software/CMSIS_5 $@ @@ -56,7 +61,7 @@ show_symbols_sorted_by_size: firmware.bin arm-none-eabi-readelf -sW firmware.elf | sed 1,3d | sed 's,^.*: ,,' | tr -s ' ' | cut -d ' ' -f 2,7 | sort -nk1 | grep -v '^0 ' flash: firmware.bin - (echo 'loadfile firmware.elf'; echo 'reset'; echo 'exit';) | JLinkExe -device RW612 -if SWD -speed 4000 -autoconnect 1 -NoGui 1 -ExitOnError 1 + (echo 'loadfile firmware.elf'; echo 'reset'; echo 'go'; echo 'exit';) | JLinkExe -device RW612 -if SWD -speed 4000 -autoconnect 1 -NoGui 1 -ExitOnError 1 # JRun --nogui --device RW612 --if SWD firmware.elf clean: diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/flash_config.c b/tutorials/nxp/frdm-rw612/bridge/flash_config.c similarity index 100% rename from tutorials/nxp/frdm-rw612/eth_radio_bridge/flash_config.c rename to tutorials/nxp/frdm-rw612/bridge/flash_config.c diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/flash_config.h b/tutorials/nxp/frdm-rw612/bridge/flash_config.h similarity index 100% rename from tutorials/nxp/frdm-rw612/eth_radio_bridge/flash_config.h rename to tutorials/nxp/frdm-rw612/bridge/flash_config.h diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/flexspi.c b/tutorials/nxp/frdm-rw612/bridge/flexspi.c similarity index 100% rename from tutorials/nxp/frdm-rw612/eth_radio_bridge/flexspi.c rename to tutorials/nxp/frdm-rw612/bridge/flexspi.c diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/hal.c b/tutorials/nxp/frdm-rw612/bridge/hal.c similarity index 83% rename from tutorials/nxp/frdm-rw612/eth_radio_bridge/hal.c rename to tutorials/nxp/frdm-rw612/bridge/hal.c index 7ce75e72..fb50f42b 100644 --- a/tutorials/nxp/frdm-rw612/eth_radio_bridge/hal.c +++ b/tutorials/nxp/frdm-rw612/bridge/hal.c @@ -26,6 +26,26 @@ uint64_t hal_get_tick(void) { return s_ticks; }; +uint64_t mg_micros(void); +// SysTick runs at the core clock and reloads every millisecond. Sample +// the tick counter and the sub-millisecond counter in one critical section. +uint64_t mg_micros(void) { + uint32_t primask = __get_PRIMASK(), val, pending; + uint64_t ms; + __disable_irq(); + ms = hal_get_tick(); + pending = SCB->ICSR & SCB_ICSR_PENDSTSET_Msk; + val = SysTick->VAL; + if (pending != (SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)) { + pending = SCB_ICSR_PENDSTSET_Msk; + val = SysTick->VAL; + } + __set_PRIMASK(primask); + if (pending != 0 && val != 0) ms++; + return ms * 1000 + ((uint64_t) (SysTick->LOAD - val) * 1000) / + (SysTick->LOAD + 1); +} + uint32_t SystemCoreClock = SYS_FREQUENCY; int hal_radio_err; // Result of hal_radio_init() void SystemInit(void) { // Called automatically by startup code diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/hal.h b/tutorials/nxp/frdm-rw612/bridge/hal.h similarity index 96% rename from tutorials/nxp/frdm-rw612/eth_radio_bridge/hal.h rename to tutorials/nxp/frdm-rw612/bridge/hal.h index 57eb3490..5426a7f7 100644 --- a/tutorials/nxp/frdm-rw612/eth_radio_bridge/hal.h +++ b/tutorials/nxp/frdm-rw612/bridge/hal.h @@ -152,16 +152,22 @@ static inline void hal_gpio_toggle(uint16_t pin) { #define MG_HAL_SYSTICK MG_HAL_SYSTICK_NONE #endif -static inline void hal_uart_init(USART_Type *uart, unsigned long baud) { - if (uart == USART0) { +static inline void hal_uart_init(USART_Type *uart, unsigned long baud, + uint16_t tx_pin, uint16_t rx_pin) { + if (uart == USART0 && tx_pin == PIN(0, 3) && + rx_pin == PIN(0, 2)) { IO_MUX_SetPinMux(IO_MUX_FC0_USART_DATA); - } else if (uart == USART1) { + } else if (uart == USART1 && tx_pin == PIN(0, 8) && + rx_pin == PIN(0, 9)) { IO_MUX_SetPinMux(IO_MUX_FC1_USART_DATA); - } else if (uart == USART2) { + } else if (uart == USART2 && tx_pin == PIN(0, 14) && + rx_pin == PIN(0, 13)) { IO_MUX_SetPinMux(IO_MUX_FC2_USART_DATA); - } else if (uart == USART3) { + } else if (uart == USART3 && tx_pin == PIN(0, 26) && + rx_pin == PIN(0, 24)) { IO_MUX_SetPinMux(IO_MUX_FC3_USART_DATA); - } else if (uart == USART14) { + } else if (uart == USART14 && tx_pin == PIN(1, 24) && + rx_pin == PIN(1, 25)) { IO_MUX_SetPinMux(IO_MUX_FC14_USART_DATA); } else { return; diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/link.ld b/tutorials/nxp/frdm-rw612/bridge/link.ld similarity index 100% rename from tutorials/nxp/frdm-rw612/eth_radio_bridge/link.ld rename to tutorials/nxp/frdm-rw612/bridge/link.ld diff --git a/tutorials/nxp/frdm-rw612/bridge/main.c b/tutorials/nxp/frdm-rw612/bridge/main.c new file mode 100644 index 00000000..47f4bf92 --- /dev/null +++ b/tutorials/nxp/frdm-rw612/bridge/main.c @@ -0,0 +1,123 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "hal.h" +#include "mongoose.h" + +#define LED_RADIO PIN(0, 12) // Green, active low +#define LED_LAN PIN(0, 0) // Blue, active low +#define LED_RED PIN(0, 1) // Red, active low + +static uint64_t s_radio_led_until; + +// Cumulative totals. Up: radio -> LAN; down: LAN -> radio queue. +static uint64_t s_frames_up, s_frames_down, s_bytes_up, s_bytes_down; +static uint64_t s_radio_sent, s_radio_received; +static size_t (*s_radio_tx)(struct mg_tcpip_if *, const void *, size_t); + +static size_t radio_tx(struct mg_tcpip_if *ifp, const void *buf, size_t len) { + size_t sent = s_radio_tx(ifp, buf, len); + if (sent != 0) s_radio_sent++; + return sent; +} + +static size_t eth_up(const void *buf, size_t len, struct mg_tcpip_if *ifp) { + size_t sent = mg_tcpip_driver_rw612.tx(buf, len, ifp); + if (sent != 0) s_frames_up++, s_bytes_up += sent; + return sent; +} + +static size_t eth_down(struct mg_mesh_node *node, const void *buf, size_t len) { + size_t queued = mg_mesh_default.tx(node, buf, len); + if (queued != 0) s_frames_down++, s_bytes_down += queued; + return queued; +} + +static void radio_rx(struct mg_mesh_node *node, void *buf, size_t len) { + s_radio_received++; + s_radio_led_until = mg_millis() + 2; + mg_mesh_default.rx(node, buf, len); +} + +static void log_fn(char ch, void *arg) { + hal_uart_write_buf(arg, &ch, 1); +} + +uint64_t mg_millis(void) { + return hal_get_tick(); +} + +int main(void) { + static struct mg_tcpip_driver_rw612_data eth_data; + static struct mg_tcpip_if eth; + static struct mg_tcpip_driver eth_driver; + static char eth_queue[8192]; + static struct mg_tcpip_driver_rw612w_data radio_data; + static struct mg_tcpip_if radio; + static struct mg_mesh mesh; + uint32_t lan_received = 0; + uint64_t lan_led_until = 0; + uint64_t link_timer = 0, stats_timer = 0; + + hal_clock_init(); + hal_rng_init(); + hal_gpio_output(LED_RADIO); + hal_gpio_output(LED_LAN); + hal_gpio_output(LED_RED); + hal_gpio_write(LED_RADIO, true); + hal_gpio_write(LED_LAN, true); + hal_gpio_write(LED_RED, true); + hal_uart_init(USART3, 115200, PIN(0, 26), PIN(0, 24)); + mg_log_set_fn(log_fn, USART3); + hal_ethernet_init(); + + mesh = mg_mesh_default; + mesh.tx = eth_down; + mesh.rx = radio_rx; // Observe radio reception without modifying the driver. + radio_data.strategy = &mesh; + radio.driver = &mg_tcpip_driver_rw612w; + radio.driver_data = &radio_data; + if (!radio.driver->init(&radio)) return 1; + s_radio_tx = radio_data.mesh->tx; + radio_data.mesh->tx = radio_tx; + + eth_data.mdc_cr = MG_DRIVER_MDC_CR; + eth_data.mdc_holdtime = MG_DRIVER_MDC_HOLDTIME; + eth_data.phy_addr = MG_TCPIP_PHY_ADDR; + eth_driver = mg_tcpip_driver_rw612; + eth_driver.tx = eth_up; + eth.driver = ð_driver; + eth.driver_data = ð_data; + memcpy(eth.mac, radio.mac, 6); + mg_queue_init(ð.recv_queue, eth_queue, sizeof(eth_queue)); + if (!eth.driver->init(ð)) return 1; + + ENET->RCR |= ENET_RCR_PROM_MASK; // Receive frames addressed to radio nodes. + radio_data.mesh->lan = ð + radio_data.mesh->next_tick = 0; + + MG_INFO(("Ethernet/radio bridge, channel %u", MG_802154_CHANNEL)); + stats_timer = mg_millis() + 5000; + for (;;) { + uint64_t now = mg_millis(); + if (mg_timer_expired(&link_timer, 100, mg_millis())) { + eth.state = eth.driver->poll(ð, true) ? MG_TCPIP_STATE_UP + : MG_TCPIP_STATE_DOWN; + } + radio.driver->poll(&radio, false); + if (mg_timer_expired(&stats_timer, 5000, now)) { + MG_INFO( + ("ETH frames up/down %llu/%llu, bytes up/down " + "%llu/%llu, radio frames sent/rcvd %llu/%llu", + s_frames_up, s_frames_down, s_bytes_up, s_bytes_down, s_radio_sent, + s_radio_received)); + } + if (eth.nrecv != lan_received) { + lan_received = eth.nrecv; + lan_led_until = now + 2; + } + hal_gpio_write(LED_RADIO, now >= s_radio_led_until); + hal_gpio_write(LED_LAN, now >= lan_led_until); + } + return 0; +} diff --git a/tutorials/nxp/frdm-rw612/bridge/mongoose_config.h b/tutorials/nxp/frdm-rw612/bridge/mongoose_config.h new file mode 100644 index 00000000..d1a1db17 --- /dev/null +++ b/tutorials/nxp/frdm-rw612/bridge/mongoose_config.h @@ -0,0 +1,13 @@ +#pragma once + +// See https://mongoose.ws/docs/getting-started/build-environment/ +#define MG_ARCH MG_ARCH_ARMGCC + +#define MG_ENABLE_TCPIP 1 +#define MG_ENABLE_DRIVER_RW612 1 // Ethernet +#define MG_ENABLE_DRIVER_RW612W 1 // Mesh bridge over raw 802.15.4 +#define MG_ENABLE_CUSTOM_MILLIS 1 + + +// The bridge initializes and polls its two L2 interfaces directly. +#define MG_ENABLE_TCPIP_DRIVER_INIT 0 diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/radio.c b/tutorials/nxp/frdm-rw612/bridge/radio.c similarity index 100% rename from tutorials/nxp/frdm-rw612/eth_radio_bridge/radio.c rename to tutorials/nxp/frdm-rw612/bridge/radio.c diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/startup.c b/tutorials/nxp/frdm-rw612/bridge/startup.c similarity index 100% rename from tutorials/nxp/frdm-rw612/eth_radio_bridge/startup.c rename to tutorials/nxp/frdm-rw612/bridge/startup.c diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/main.c b/tutorials/nxp/frdm-rw612/eth_radio_bridge/main.c deleted file mode 100644 index ca18b830..00000000 --- a/tutorials/nxp/frdm-rw612/eth_radio_bridge/main.c +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2026 Cesanta Software Limited -// All rights reserved -// -// Ethernet <-> IEEE 802.15.4 radio bridge. Radio frames carry Ethernet frames -// split into pieces, see mg_l2_802154_tx(). Radio nodes, e.g. the -// FRDM-MCXW71 minimal tutorial, are on the Ethernet LAN: they get IPv4 -// addresses from its DHCP server. The bridge itself has no IP address. -// Everything from the radio goes to Ethernet. From Ethernet, only broadcasts -// and frames to radio nodes go to the radio, it is much slower - -#include "hal.h" -#include "mongoose.h" - -#define UART_DEBUG USART3 - -static void log_fn(char ch, void *param) { - hal_uart_write_buf(param, &ch, 1); -} - -uint64_t mg_millis(void) { - return hal_get_tick(); -} - -// MAC addresses of radio nodes, learned from frames they send -static uint8_t s_nodes[8][6]; -static size_t s_next; - -// Remember a radio node's MAC address, if new -static void learn(const uint8_t *mac) { - size_t i, n = sizeof(s_nodes) / sizeof(s_nodes[0]); - for (i = 0; i < n && memcmp(s_nodes[i], mac, 6) != 0; i++) (void) 0; - if (i < n) return; - memcpy(s_nodes[s_next], mac, 6); - s_next = (s_next + 1) % n; - MG_INFO(("Radio node %M", mg_print_mac, mac)); -} - -// Is an Ethernet frame for the radio: broadcast, or to a radio node -static bool for_radio(const uint8_t *dst) { - size_t i, n = sizeof(s_nodes) / sizeof(s_nodes[0]); - if (memcmp(dst, "\xff\xff\xff\xff\xff\xff", 6) == 0) return true; - for (i = 0; i < n && memcmp(s_nodes[i], dst, 6) != 0; i++) (void) 0; - return i < n; -} - -int main(void) { - static struct mg_tcpip_driver_rw612_data eth_data = { - .mdc_cr = MG_DRIVER_MDC_CR, .phy_addr = MG_TCPIP_PHY_ADDR}; - static struct mg_l2_802154_data radio_data = { - .channel = MG_L2_802154_CHANNEL, .pan_id = MG_L2_802154_PAN_ID}; - static struct mg_tcpip_if eth = {.driver = &mg_tcpip_driver_rw612, - .driver_data = ð_data}; - static struct mg_tcpip_if radio = {.driver = &mg_tcpip_driver_rw612_154, - .driver_data = &radio_data}; - static char eth_queue[8192]; // Filled by the Ethernet IRQ handler - static uint8_t frame[1540]; - uint8_t uid[FSL_OCOTP_UID_LENGTH], digest[32]; - uint32_t uid_len = sizeof(uid); - uint64_t timer = 0; - char *p; - size_t n; - - hal_clock_init(); - hal_uart_init(UART_DEBUG, 115200); - mg_log_set_fn(log_fn, UART_DEBUG); - if (hal_radio_err) { - MG_ERROR(("Radio firmware failed to start, step %d", hal_radio_err)); - } - - // MAC addresses made from the chip unique ID - memset(uid, 0, sizeof(uid)); - OCOTP_ReadUniqueID(uid, &uid_len); - mg_sha256(digest, uid, sizeof(uid)); - memcpy(eth.mac, digest, 6); - memcpy(radio.mac, digest + 6, 6); - eth.mac[0] = radio.mac[0] = 0x02; // Locally administered, unicast - - hal_ethernet_init(); - mg_queue_init(ð.recv_queue, eth_queue, sizeof(eth_queue)); - if (!eth.driver->init(ð)) MG_ERROR(("Ethernet init failed")); - if (!radio.driver->init(&radio)) MG_ERROR(("Radio init failed")); - ENET->RCR |= ENET_RCR_PROM_MASK; // Receive frames for radio nodes, too - MG_INFO(("Bridge started, radio channel %d", radio_data.channel)); - - for (;;) { - if ((n = mg_queue_next(ð.recv_queue, &p)) > 0) { // Ethernet -> radio - if (n >= 14 && for_radio((uint8_t *) p)) radio.driver->tx(p, n, &radio); - mg_queue_del(ð.recv_queue, n); - } - if ((n = radio.driver->rx(frame, sizeof(frame), &radio)) >= 14) { - learn(frame + 6); // Radio -> Ethernet - eth.driver->tx(frame, n, ð); - } - if (mg_timer_expired(&timer, 1000, mg_millis())) { // Ethernet link - bool up = eth.driver->poll(ð, true); - eth.state = up ? MG_TCPIP_STATE_UP : MG_TCPIP_STATE_DOWN; - } - } - - return 0; -} diff --git a/tutorials/nxp/frdm-rw612/eth_radio_bridge/mongoose_config.h b/tutorials/nxp/frdm-rw612/eth_radio_bridge/mongoose_config.h deleted file mode 100644 index c190ccf5..00000000 --- a/tutorials/nxp/frdm-rw612/eth_radio_bridge/mongoose_config.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -// See https://mongoose.ws/docs/getting-started/build-environment/ -#define MG_ARCH MG_ARCH_ARMGCC - -#define MG_ENABLE_TCPIP 1 -#define MG_ENABLE_DRIVER_RW612 1 // Ethernet -#define MG_ENABLE_DRIVER_RW612_154 1 // 802.15.4 radio -#define MG_ENABLE_TCPIP_DRIVER_INIT 0 // main.c drives both, no TCP/IP stack -#define MG_ENABLE_CUSTOM_MILLIS 1 diff --git a/tutorials/nxp/frdm-rw612/minimal/hal.h b/tutorials/nxp/frdm-rw612/minimal/hal.h index 3d52d4d7..d8ad1554 100644 --- a/tutorials/nxp/frdm-rw612/minimal/hal.h +++ b/tutorials/nxp/frdm-rw612/minimal/hal.h @@ -147,16 +147,22 @@ static inline void hal_gpio_toggle(uint16_t pin) { #define MG_HAL_SYSTICK MG_HAL_SYSTICK_NONE #endif -static inline void hal_uart_init(USART_Type *uart, unsigned long baud) { - if (uart == USART0) { +static inline void hal_uart_init(USART_Type *uart, unsigned long baud, + uint16_t tx_pin, uint16_t rx_pin) { + if (uart == USART0 && tx_pin == PIN(0, 3) && + rx_pin == PIN(0, 2)) { IO_MUX_SetPinMux(IO_MUX_FC0_USART_DATA); - } else if (uart == USART1) { + } else if (uart == USART1 && tx_pin == PIN(0, 8) && + rx_pin == PIN(0, 9)) { IO_MUX_SetPinMux(IO_MUX_FC1_USART_DATA); - } else if (uart == USART2) { + } else if (uart == USART2 && tx_pin == PIN(0, 14) && + rx_pin == PIN(0, 13)) { IO_MUX_SetPinMux(IO_MUX_FC2_USART_DATA); - } else if (uart == USART3) { + } else if (uart == USART3 && tx_pin == PIN(0, 26) && + rx_pin == PIN(0, 24)) { IO_MUX_SetPinMux(IO_MUX_FC3_USART_DATA); - } else if (uart == USART14) { + } else if (uart == USART14 && tx_pin == PIN(1, 24) && + rx_pin == PIN(1, 25)) { IO_MUX_SetPinMux(IO_MUX_FC14_USART_DATA); } else { return; diff --git a/tutorials/nxp/frdm-rw612/minimal/main.c b/tutorials/nxp/frdm-rw612/minimal/main.c index 739f9a2f..0c4d1522 100644 --- a/tutorials/nxp/frdm-rw612/minimal/main.c +++ b/tutorials/nxp/frdm-rw612/minimal/main.c @@ -96,7 +96,7 @@ int main(void) { hal_gpio_write(LED2, true); hal_gpio_output(LED3); hal_gpio_write(LED3, true); - hal_uart_init(UART_DEBUG, 115200); + hal_uart_init(UART_DEBUG, 115200, PIN(0, 26), PIN(0, 24)); mg_log_set_fn(log_fn, UART_DEBUG); hal_ethernet_init(); diff --git a/tutorials/rp/pico-w/Makefile b/tutorials/rp/pico-w/Makefile index 79e32298..efbd455f 100644 --- a/tutorials/rp/pico-w/Makefile +++ b/tutorials/rp/pico-w/Makefile @@ -31,5 +31,5 @@ clean: $(RM) build pico_sdk_import.cmake $(SDK_PATH) $(SDK_PATH): - git clone -c advice.detachedHead=false --quiet --depth 1 -b 2.1.1 https://github.com/raspberrypi/pico-sdk $@ + git clone -c advice.detachedHead=false --quiet --depth 1 -b 2.2.0 https://github.com/raspberrypi/pico-sdk $@ cd $@ && git submodule update --quiet --init diff --git a/tutorials/silabs/xiao-mg24/minimal/Makefile b/tutorials/silabs/xiao-mg24/minimal/Makefile new file mode 100644 index 00000000..1a5bc635 --- /dev/null +++ b/tutorials/silabs/xiao-mg24/minimal/Makefile @@ -0,0 +1,73 @@ +# Environment setup: https://mongoose.ws/documentation/tutorials/tools/ + +MGDIR = ../../../.. +SDK = silabs_sdk/platform +RAIL = $(SDK)/radio/rail_lib + +CFLAGS = -W -Wall -Wextra -Werror -Wshadow # No -Wundef: RAIL headers +CFLAGS += -Wformat-truncation -fno-common -Wno-conversion -Wno-sign-conversion +CFLAGS += -g3 -Os -ffunction-sections -fdata-sections +CFLAGS += -I. -I$(MGDIR) -Icmsis_core/CMSIS/Core/Include +CFLAGS += -I$(SDK)/Device/SiliconLabs/EFR32MG24/Include -I$(SDK)/common/inc +CFLAGS += -I$(RAIL)/common -I$(RAIL)/protocol/ieee802154 +CFLAGS += -I$(RAIL)/chip/efr32/efr32xg2x +CFLAGS += -DEFR32MG24B220F1536IM48 +CFLAGS += -D__START=main # For startup_efr32mg24.c +CFLAGS += -mcpu=cortex-m33 -mthumb -mfloat-abi=hard -mfpu=fpv5-sp-d16 -mcmse +CFLAGS += $(CFLAGS_EXTRA) + +LDFLAGS ?= -Tlink.ld -nostdlib -nostartfiles --specs=nano.specs -lc -lm -lgcc -Wl,--gc-sections + +SOURCES = main.c hal.c $(MGDIR)/mongoose.c + +SOURCES += $(MGDIR)/tutorials/mqtt/mqtt-client/mongoose_mqtt.c +SOURCES += $(SDK)/Device/SiliconLabs/EFR32MG24/Source/startup_efr32mg24.c +SOURCES += $(SDK)/Device/SiliconLabs/EFR32MG24/Source/system_efr32mg24.c + +# RAIL radio library, a binary blob. Simplicity SDK keeps it in Git LFS, +# served by Silicon Labs. The oid is the file's sha256, see the LFS pointer +# platform/radio/rail_lib/autogen/librail_release/librail_efr32xg24_gcc_release.a +SDK_VERSION = v2025.6.3 +LIBRAIL = librail_efr32xg24_gcc_release.a +LIBRAIL_SHA256 = 381b123649b66e9e3bbe6d1363c31ee0457b3274207b837ea49685a3a0a2c387 +LIBRAIL_URL = https://artifacts.silabs.net/api/lfs/gsdk/object/$(LIBRAIL_SHA256) + +all build: firmware.bin + +firmware.bin: firmware.elf + arm-none-eabi-objcopy -O binary $< $@ + arm-none-eabi-size --format=berkeley $< + +firmware.elf: cmsis_core silabs_sdk $(LIBRAIL) $(SOURCES) $(wildcard *.h) $(MGDIR)/mongoose.h link.ld Makefile mesh-config + arm-none-eabi-gcc $(SOURCES) $(LIBRAIL) $(CFLAGS) $(LDFLAGS) -o $@ + +# Rebuild when overriding the board address or timing. +.PHONY: mesh-config +mesh-config: + +cmsis_core: + git clone -q -c advice.detachedHead=false --depth 1 -b 5.9.0 https://github.com/ARM-software/CMSIS_5 $@ + +# Device headers, startup code and RAIL headers from Simplicity SDK +silabs_sdk: + GIT_LFS_SKIP_SMUDGE=1 git clone -q -c advice.detachedHead=false --depth 1 -b $(SDK_VERSION) --filter=blob:none --no-checkout https://github.com/SiliconLabs/simplicity_sdk $@ + cd $@ && git sparse-checkout set --no-cone /platform/Device/SiliconLabs/EFR32MG24/ /platform/common/inc/ /platform/radio/rail_lib/common/ /platform/radio/rail_lib/protocol/ieee802154/ /platform/radio/rail_lib/chip/efr32/efr32xg2x/ + cd $@ && GIT_LFS_SKIP_SMUDGE=1 git checkout -q + +$(LIBRAIL): + curl -sfL $(LIBRAIL_URL) -o $@.tmp + echo "$(LIBRAIL_SHA256) $@.tmp" | shasum -a 256 -c -s + mv $@.tmp $@ + +# Show all symbols sorted by size +show_symbols_sorted_by_size: firmware.bin + arm-none-eabi-readelf -sW firmware.elf | sed 1,3d | sed 's,^.*: ,,' | tr -s ' ' | cut -d ' ' -f 2,7 | sort -nk1 | grep -v '^0 ' + +# Flash over the on-board CMSIS-DAP probe. pyocd needs the EFR32MG24 pack: +# pyocd pack install efr32mg24b220f1536im48 +# With several probes attached, choose one: make flash PYOCD_FLAGS="-u ID" +flash: firmware.bin + pyocd flash -t efr32mg24b220f1536im48 $(PYOCD_FLAGS) firmware.elf + +clean: + rm -rf firmware.* cmsis_* silabs_* librail_* diff --git a/tutorials/silabs/xiao-mg24/minimal/hal.c b/tutorials/silabs/xiao-mg24/minimal/hal.c new file mode 100644 index 00000000..2396454c --- /dev/null +++ b/tutorials/silabs/xiao-mg24/minimal/hal.c @@ -0,0 +1,262 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "hal.h" + +// Secure Engine data descriptor, sli_se_datatransfer_t in Simplicity SDK +struct se_dt { + volatile void *volatile data; // Data buffer + volatile void *volatile next; // Next descriptor, or 1: last one + volatile uint32_t length; // Length in bytes, plus flags +}; + +#define SE_CMD_TRNG_GET_RANDOM 0x07000000UL // SE mailbox commands +#define SE_DT_STOP 1UL // Descriptor flags +#define SE_DT_REALIGN 0x20000000UL +#define SE_RESPONSE_MASK 0x000F0000UL // RX_HEADER response code, 0: OK + +// Fill buf with n random bytes, n a multiple of 4, from the Secure Engine +// TRNG (RM section 11.2.5), through the SE mailbox, as sl_se_get_random() +// and sli_se_mailbox_execute_command() in Simplicity SDK. buf and the +// descriptor are static: on the stack, at the top of RAM, the SE reported a +// bus error. Return false on SE error +static bool se_random(uint32_t *buf, uint32_t n) { + static struct se_dt out; + out.data = buf, out.next = (void *) SE_DT_STOP; + out.length = n | SE_DT_REALIGN; + while ((SEMAILBOX_HOST->TX_STATUS & SEMAILBOX_TX_STATUS_TXINT) == 0) (void) 0; + SEMAILBOX_HOST->TX_HEADER = sizeof(uint32_t) * 5; // 4 words + 1 parameter + SEMAILBOX_HOST->FIFO = SE_CMD_TRNG_GET_RANDOM; + SEMAILBOX_HOST->FIFO = 0; // No input data + SEMAILBOX_HOST->FIFO = (uint32_t) &out; // Output data + SEMAILBOX_HOST->FIFO = n; // Parameter: number of bytes + while ((SEMAILBOX_HOST->RX_STATUS & SEMAILBOX_RX_STATUS_RXINT) == 0) (void) 0; + return (SEMAILBOX_HOST->RX_HEADER & SE_RESPONSE_MASK) == 0; +} + +// Random 32 bits, for mg_random(). One SE request fills a pool of 16 words. +// If the SE fails 3 times in a row, the pool is zeros: see se_random() +uint32_t hal_rng_read(void) { + static uint32_t pool[16]; + static size_t avail; + if (avail == 0) { + int i; + for (i = 0; i < 3 && !se_random(pool, sizeof(pool)); i++) (void) 0; + if (i == 3) memset(pool, 0, sizeof(pool)); + avail = sizeof(pool) / sizeof(pool[0]); + } + return pool[--avail]; +} + +// RAIL library needs these Simplicity SDK functions. Implemented as in the +// SDK: sl_core_cortexm.c, em_cmu.c, em_emu.c, em_system.c, sl_gpio.c, +// pa_conversions_efr32.c. Atomic sections mask IRQs with priority number 3 +// and above, see hal_radio_init() for the IRQ priorities +#define CORE_ATOMIC_BASE_PRIORITY_LEVEL 3 + +uint32_t CORE_EnterCritical(void) { + uint32_t state = __get_PRIMASK(); + __disable_irq(); + return state; +} + +void CORE_ExitCritical(uint32_t state) { + if (state == 0) __enable_irq(), __ISB(); +} + +uint32_t CORE_EnterAtomic(void) { + uint32_t state = __get_BASEPRI(); + __set_BASEPRI(CORE_ATOMIC_BASE_PRIORITY_LEVEL << (8U - __NVIC_PRIO_BITS)); + return state; +} + +void CORE_ExitAtomic(uint32_t state) { + __set_BASEPRI(state); + __ISB(); +} + +// Clock IDs: bits 0..4 bit number, bits 5..6 register, 1 CLKEN0, 2 CLKEN1 +void CMU_ClockEnable(uint32_t clock, bool enable) { + volatile uint32_t *reg = (clock >> 5) == 1 ? &CMU->CLKEN0 + : (clock >> 5) == 2 ? &CMU->CLKEN1 + : NULL; + if (reg == NULL) return; + if (enable) { + *reg |= BIT(clock & 31); + } else { + *reg &= ~BIT(clock & 31); + } +} + +void EMU_DCDCSetPFMXTimeoutMaxCtrl(uint32_t value) { + bool clk_was_on = (CMU->CLKEN0 & CMU_CLKEN0_DCDC) != 0; + bool was_locked; + CMU->CLKEN0_SET = CMU_CLKEN0_DCDC; + was_locked = (DCDC->LOCKSTATUS & DCDC_LOCKSTATUS_LOCK) != 0; + DCDC->LOCK = DCDC_LOCK_LOCKKEY_UNLOCKKEY; + while (DCDC->SYNCBUSY & DCDC_SYNCBUSY_PFMXCTRL) (void) 0; + DCDC->PFMXCTRL = (DCDC->PFMXCTRL & ~_DCDC_PFMXCTRL_IPKTMAXCTRL_MASK) | + (value << _DCDC_PFMXCTRL_IPKTMAXCTRL_SHIFT); + if (was_locked) DCDC->LOCK = ~DCDC_LOCK_LOCKKEY_UNLOCKKEY; + if (!clk_was_on) CMU->CLKEN0_CLR = CMU_CLKEN0_DCDC; +} + +// SYSTEM_ChipRevision_TypeDef, em_system.h +struct chip_rev { + uint8_t minor; // Minor revision number + uint8_t major; // Major revision number + uint8_t family; // Device family number +}; + +void SYSTEM_ChipRevisionGet(struct chip_rev *rev) { + uint32_t chiprev; + CMU->CLKEN0_SET = CMU_CLKEN0_SYSCFG; + chiprev = SYSCFG->CHIPREV; + rev->family = (chiprev & _SYSCFG_CHIPREV_FAMILY_MASK) >> + _SYSCFG_CHIPREV_FAMILY_SHIFT; + rev->major = (chiprev & _SYSCFG_CHIPREV_MAJOR_MASK) >> + _SYSCFG_CHIPREV_MAJOR_SHIFT; + rev->minor = (chiprev & _SYSCFG_CHIPREV_MINOR_MASK) >> + _SYSCFG_CHIPREV_MINOR_SHIFT; +} + +// sl_gpio_t, sl_device_gpio.h +struct sl_gpio { + uint8_t port; // GPIO port number + uint8_t pin; // GPIO pin number +}; + +uint32_t sl_gpio_set_pin(const struct sl_gpio *gpio) { + GPIO->P_SET[gpio->port].DOUT = BIT(gpio->pin); + return 0; +} + +uint32_t sl_gpio_clear_pin(const struct sl_gpio *gpio) { + GPIO->P_CLR[gpio->port].DOUT = BIT(gpio->pin); + return 0; +} + +// We set TX power as a raw PA level and load no PA curves. RAIL calls this to +// check per-channel dBm limits: RAIL_TX_POWER_MAX means no limit, 255. Without +// curves the SDK returns 0, see RAIL_ConvertDbmToRaw(), pa_conversions_efr32.c +uint8_t RAIL_ConvertDbmToRaw(void *rail, int mode, int16_t power) { + (void) rail, (void) mode; + return power >= 0x7fff ? 255 : 0; // 0x7fff: RAIL_TX_POWER_MAX +} + +// Added to minimise the bin size: avoid pulling printf and friends +void __assert_func(const char *s, int line, const char *fn, const char *expr) { + (void) s, (void) line, (void) fn, (void) expr; + for (;;) (void) 0; // or trigger the fault handler +} + +bool hal_timer_expired(volatile uint64_t *t, uint64_t period, uint64_t now) { + uint64_t diff = now - *t; // Wrap-safe elapsed time since last expiry + if (period == 0) return false; // Avoid division by zero + if (diff < period) return false; // Period has not elapsed yet + *t += (diff / period) * period; // Preserve cadence, skip missed periods + return true; +} + +static volatile uint64_t s_ticks; // Milliseconds since boot +void SysTick_Handler(void) { // SyStick IRQ handler, triggered every 1ms + s_ticks++; +} + +uint64_t hal_get_tick(void) { + return s_ticks; +} + +uint64_t mg_micros(void); +// SysTick runs at the core clock and reloads every millisecond. Sample +// the tick counter and the sub-millisecond counter in one critical section. +uint64_t mg_micros(void) { + uint32_t primask = __get_PRIMASK(), val, pending; + uint64_t ms; + __disable_irq(); + ms = hal_get_tick(); + pending = SCB->ICSR & SCB_ICSR_PENDSTSET_Msk; + val = SysTick->VAL; + if (pending != (SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)) { + pending = SCB_ICSR_PENDSTSET_Msk; + val = SysTick->VAL; + } + __set_PRIMASK(primask); + if (pending != 0 && val != 0) ms++; + return ms * 1000 + ((uint64_t) (SysTick->LOAD - val) * 1000) / + (SysTick->LOAD + 1); +} + +struct stat; +__attribute__((weak)) int _fstat(int fd, struct stat *st) { + (void) fd, (void) st; + return -1; +} + +extern unsigned char _end[]; // End of data section, start of heap. See link.ld +static unsigned char *s_current_heap_end = _end; + +size_t hal_ram_used(void) { + return (size_t) (s_current_heap_end - _end); +} + +size_t hal_ram_free(void) { + unsigned char endofstack; + return (size_t) (&endofstack - s_current_heap_end); +} + +void *_sbrk(int incr) { + unsigned char *prev_heap; + unsigned char *heap_end = (unsigned char *) ((size_t) &heap_end - 256); + prev_heap = s_current_heap_end; + // Check how much space we got from the heap end to the stack end + if (s_current_heap_end + incr > heap_end) return (void *) -1; + s_current_heap_end += incr; + return prev_heap; +} + +__attribute__((weak)) int _open(const char *path) { + (void) path; + return -1; +} + +__attribute__((weak)) int _close(int fd) { + (void) fd; + return -1; +} + +__attribute__((weak)) int _isatty(int fd) { + (void) fd; + return 1; +} + +__attribute__((weak)) int _lseek(int fd, int ptr, int dir) { + (void) fd, (void) ptr, (void) dir; + return 0; +} + +__attribute__((weak)) void _exit(int status) { + (void) status; + for (;;) asm volatile("BKPT #0"); +} + +__attribute__((weak)) void _kill(int pid, int sig) { + (void) pid, (void) sig; +} + +__attribute__((weak)) int _getpid(void) { + return -1; +} + +__attribute__((weak)) int _write(int fd, char *ptr, int len) { + (void) fd, (void) ptr, (void) len; + return -1; +} + +__attribute__((weak)) int _read(int fd, char *ptr, int len) { + (void) fd, (void) ptr, (void) len; + return -1; +} + +__attribute__((weak)) void _init(void) { +} diff --git a/tutorials/silabs/xiao-mg24/minimal/hal.h b/tutorials/silabs/xiao-mg24/minimal/hal.h new file mode 100644 index 00000000..068b4175 --- /dev/null +++ b/tutorials/silabs/xiao-mg24/minimal/hal.h @@ -0,0 +1,212 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved +// +// Seeed Studio XIAO MG24 hardware abstraction layer. EFR32MG24B220F1536IM48 +// reference: Simplicity SDK, https://github.com/SiliconLabs/simplicity_sdk, +// platform/Device/SiliconLabs/EFR32MG24. Board: Seeed XIAO MG24 schematic, +// pin names from Silicon Labs Arduino core, variants/xiao_mg24 + +#pragma once + +#include "em_device.h" + +#include +#include +#include +#include + +uint64_t hal_get_tick(void); +bool hal_timer_expired(volatile uint64_t *t, uint64_t period, uint64_t now); +size_t hal_ram_used(void); +size_t hal_ram_free(void); +uint32_t hal_rng_read(void); // Random 32 bits, Secure Engine TRNG. See hal.c + +#define BIT(x) (1UL << (x)) +#define PIN(port, num) ((((port) - 'A') << 8) | (num)) +#define PINNO(pin) (pin & 255) +#define PINPORT(pin) (pin >> 8) + +#define RF_SW_PIN PIN('B', 4) // Antenna switch VCTL: low onboard, high UFL +#define RF_SW_PWR_PIN PIN('B', 5) // Antenna switch power + +// HFXO crystal tuning, XIAO MG24 board value: Silicon Labs Arduino core, +// sl_device_init_hfxo_config.h. XO gets +40, see ctuneDelta in em_cmu.c +#define HFXO_CTUNE 95 +#define HFXO_CTUNE_XO_DELTA 40 + +enum { GPIO_MODE_DISABLED = 0, GPIO_MODE_INPUT = 1, GPIO_MODE_PUSHPULL = 4 }; + +static inline void gpio_set_mode(uint16_t pin, uint32_t mode) { + volatile uint32_t *r = PINNO(pin) < 8 ? &GPIO->P[PINPORT(pin)].MODEL + : &GPIO->P[PINPORT(pin)].MODEH; + uint32_t shift = (PINNO(pin) & 7) * 4; + *r = (*r & ~(15UL << shift)) | (mode << shift); +} + +static inline void gpio_write(uint16_t pin, bool val) { + if (val) { + GPIO->P_SET[PINPORT(pin)].DOUT = BIT(PINNO(pin)); + } else { + GPIO->P_CLR[PINPORT(pin)].DOUT = BIT(PINNO(pin)); + } +} + +static inline void gpio_output(uint16_t pin, bool val) { + gpio_write(pin, val); + gpio_set_mode(pin, GPIO_MODE_PUSHPULL); +} + +// Start the HFXO crystal oscillator, 39 MHz, and wait until it is ready. +// Steps and default values as in CMU_HFXOInit() and CMU_HFXOINIT_DEFAULT, +// emlib em_cmu.c. The radio needs HFXO, and RAIL expects it configured +static inline void hal_hfxo_init(void) { + CMU->CLKEN0_SET = CMU_CLKEN0_HFXO0; + HFXO0->LOCK = HFXO_LOCK_LOCKKEY_UNLOCK; + HFXO0->CTRL_SET = HFXO_CTRL_DISONDEMAND | HFXO_CTRL_DISONDEMANDBUFOUT; + HFXO0->CTRL_CLR = HFXO_CTRL_FORCEEN; + while (HFXO0->STATUS & HFXO_STATUS_ENS) (void) 0; // Wait until disabled + while (HFXO0->STATUS & HFXO_STATUS_SYNCBUSY) (void) 0; + HFXO0->XTALCFG = HFXO_XTALCFG_TIMEOUTCBLSB_T416US | + HFXO_XTALCFG_TIMEOUTSTEADY_T833US | // First lock + (32U << _HFXO_XTALCFG_COREBIASSTARTUP_SHIFT) | + (32U << _HFXO_XTALCFG_COREBIASSTARTUPI_SHIFT); + HFXO0->XTALCTRL = + (HFXO0->XTALCTRL & _HFXO_XTALCTRL_SKIPCOREBIASOPT_MASK) | + HFXO_XTALCTRL_CTUNEFIXANA_XO | // 20 dBm PA parts, see em_cmu.h + ((uint32_t) (HFXO_CTUNE + HFXO_CTUNE_XO_DELTA) + << _HFXO_XTALCTRL_CTUNEXOANA_SHIFT) | + ((uint32_t) HFXO_CTUNE << _HFXO_XTALCTRL_CTUNEXIANA_SHIFT) | + (60U << _HFXO_XTALCTRL_COREBIASANA_SHIFT); + HFXO0->CFG &= ~(_HFXO_CFG_SQBUFSCHTRGANA_MASK | _HFXO_CFG_ENXIDCBIASANA_MASK | + _HFXO_CFG_MODE_MASK); // Crystal mode + HFXO0->CTRL = (HFXO0->CTRL & ~(_HFXO_CTRL_FORCEXO2GNDANA_MASK | + _HFXO_CTRL_FORCEXI2GNDANA_MASK | + _HFXO_CTRL_DISONDEMAND_MASK | + _HFXO_CTRL_EM23ONDEMAND_MASK)) | + HFXO_CTRL_FORCEEN; + uint32_t ready = HFXO_STATUS_RDY | HFXO_STATUS_COREBIASOPTRDY | + HFXO_STATUS_ENS; + while ((HFXO0->STATUS & ready) != ready) (void) 0; + HFXO0->CTRL_SET = HFXO_CTRL_DISONDEMAND; + HFXO0->XTALCFG = (HFXO0->XTALCFG & ~_HFXO_XTALCFG_TIMEOUTSTEADY_MASK) | + HFXO_XTALCFG_TIMEOUTSTEADY_T83US; // Subsequent locks + HFXO0->XTALCTRL_SET = HFXO_XTALCTRL_SKIPCOREBIASOPT; + HFXO0->CTRL_CLR = HFXO_CTRL_DISONDEMAND; // Radio can request it on demand +} + +// Lock HFRCODPLL to HFXO at 78 MHz: 39 MHz * (N + 1) / (M + 1), N 3839, +// M 1919, phase lock, falling edge, auto recover. Values and steps as in +// Simplicity SDK CMU_DPLLLock() and the xG24 clock manager configuration. +// HFRCO band 72.6..80 MHz, tuned with its factory calibration from DEVINFO +static inline bool hal_dpll_78mhz(void) { + uint32_t lock = DPLL_IF_LOCK | DPLL_IF_LOCKFAILLOW | DPLL_IF_LOCKFAILHIGH; + CMU->CLKEN0_SET = CMU_CLKEN0_DPLL0 | CMU_CLKEN0_HFRCO0; + DPLL0->EN_CLR = DPLL_EN_EN; + while (DPLL0->EN & DPLL_EN_DISABLING) (void) 0; + while (HFRCO0->STATUS & (HFRCO_STATUS_SYNCBUSY | HFRCO_STATUS_FREQBSY)) + (void) 0; + HFRCO0->CAL = 0xCCF09F00UL | (DEVINFO->HFRCODPLLCAL[16].HFRCODPLLCAL & + _HFRCO_CAL_TUNING_MASK); + DPLL0->CFG1 = (3839UL << _DPLL_CFG1_N_SHIFT) | (1919UL << _DPLL_CFG1_M_SHIFT); + CMU->DPLLREFCLKCTRL = CMU_DPLLREFCLKCTRL_CLKSEL_HFXO; + DPLL0->CFG = (1UL << _DPLL_CFG_AUTORECOVER_SHIFT) | // Edge: falling, 0 + (_DPLL_CFG_MODE_PLL << _DPLL_CFG_MODE_SHIFT); + DPLL0->IF_CLR = lock; + DPLL0->EN_SET = DPLL_EN_EN; + while ((DPLL0->IF & lock) == 0) (void) 0; + return (DPLL0->IF & lock) == DPLL_IF_LOCK; +} + +// Run the CPU at its maximum, 78 MHz, from HFRCODPLL locked to HFXO. First +// set the limits for 78 MHz, datasheet table 4.2 and em_cmu.c: flash 1 wait +// state above 40 MHz; PCLK max 50 MHz, so SYSCLK / 2; radio HCLK max 40 MHz, +// so SYSCLK / 2. Voltage scaling is VSCALE2 after reset, fine for 78 MHz. +// If the DPLL does not lock, stay on HFXO, 39 MHz +static inline void hal_clock_init(void) { + hal_hfxo_init(); + CMU->CLKEN1_SET = CMU_CLKEN1_MSC; + MSC->LOCK = MSC_LOCK_LOCKKEY_UNLOCK; + MSC->READCTRL = (MSC->READCTRL & ~_MSC_READCTRL_MODE_MASK) | + MSC_READCTRL_MODE_WS1; + CMU->SYSCLKCTRL_SET = CMU_SYSCLKCTRL_PCLKPRESC | CMU_SYSCLKCTRL_RHCLKPRESC; + if (hal_dpll_78mhz()) { + SystemHFRCODPLLClockSet(78000000); + CMU->SYSCLKCTRL = (CMU->SYSCLKCTRL & ~_CMU_SYSCLKCTRL_CLKSEL_MASK) | + CMU_SYSCLKCTRL_CLKSEL_HFRCODPLL; + } else { + CMU->SYSCLKCTRL = (CMU->SYSCLKCTRL & ~_CMU_SYSCLKCTRL_CLKSEL_MASK) | + CMU_SYSCLKCTRL_CLKSEL_HFXO; + } + SystemCoreClock = SystemSYSCLKGet(); // HCLK = SYSCLK, no prescaler + SysTick_Config(SystemCoreClock / 1000); // Sys tick every 1ms +} + +// PCLK, the peripheral clock: SYSCLK, divided by 2 if PCLKPRESC is set +static inline uint32_t hal_pclk(void) { + bool half = (CMU->SYSCLKCTRL & CMU_SYSCLKCTRL_PCLKPRESC) != 0; + return half ? SystemCoreClock / 2 : SystemCoreClock; +} + +static inline void hal_gpio_output(uint16_t pin) { + CMU->CLKEN0_SET = CMU_CLKEN0_GPIO; + gpio_set_mode(pin, GPIO_MODE_PUSHPULL); +} + +static inline void hal_gpio_toggle(uint16_t pin) { + GPIO->P_TGL[PINPORT(pin)].DOUT = BIT(PINNO(pin)); +} + +// USART0, 8N1, on tx and rx pins: any pin, through GPIO routing. On XIAO +// MG24, PA8 TX and PA9 RX go to the SAMD11 USB serial bridge. USART0 is +// clocked by PCLK +static inline void hal_uart_init(USART_TypeDef *uart, uint16_t tx, uint16_t rx, + unsigned long baud) { + uint32_t ovs = 16, div, pclk = hal_pclk(); + CMU->CLKEN0_SET = CMU_CLKEN0_GPIO | CMU_CLKEN0_USART0; + gpio_output(tx, 1); + gpio_set_mode(rx, GPIO_MODE_INPUT); + GPIO->USARTROUTE[0].TXROUTE = + ((uint32_t) PINPORT(tx) << _GPIO_USART_TXROUTE_PORT_SHIFT) | + ((uint32_t) PINNO(tx) << _GPIO_USART_TXROUTE_PIN_SHIFT); + GPIO->USARTROUTE[0].RXROUTE = + ((uint32_t) PINPORT(rx) << _GPIO_USART_RXROUTE_PORT_SHIFT) | + ((uint32_t) PINNO(rx) << _GPIO_USART_RXROUTE_PIN_SHIFT); + GPIO->USARTROUTE[0].ROUTEEN = + GPIO_USART_ROUTEEN_TXPEN | GPIO_USART_ROUTEEN_RXPEN; + uart->EN = USART_EN_EN; + uart->CTRL = 0; // Async, 16x oversampling + uart->FRAME = USART_FRAME_DATABITS_EIGHT | USART_FRAME_STOPBITS_ONE | + USART_FRAME_PARITY_NONE; + // CLKDIV = 256 * (fPCLK / (ovs * baud) - 1), see USART_BaudrateAsyncSet() + div = (32 * pclk + (ovs * baud) / 2) / (ovs * baud) - 32; + uart->CLKDIV = (div * 8) & _USART_CLKDIV_DIV_MASK; + uart->CMD = USART_CMD_TXEN | USART_CMD_RXEN; +} + +static inline void hal_uart_write_byte(USART_TypeDef *uart, uint8_t byte) { + while ((uart->STATUS & USART_STATUS_TXBL) == 0) (void) 0; + uart->TXDATA = byte; +} + +static inline void hal_uart_write_buf(USART_TypeDef *uart, const char *buf, + size_t len) { + while (len-- > 0) hal_uart_write_byte(uart, *(const uint8_t *) buf++); +} + +// Random numbers come from the Secure Engine TRNG, see hal_rng_read(): clock +// its mailbox +static inline void hal_rng_init(void) { + CMU->CLKEN1_SET = CMU_CLKEN1_SEMAILBOXHOST; +} + +// Prepare for RAIL. Set all IRQs to priority 5, as Silicon Labs +// sl_interrupt_manager_init() does: RAIL atomic sections mask priority +// numbers 3 and above. Route the radio to the onboard ceramic antenna. +// FM8625H switch: VCTL low connects ANT to RF1, the chip antenna; high RF2, +// the U.FL socket +static inline void hal_radio_init(void) { + for (int i = 0; i < EXT_IRQ_COUNT; i++) NVIC_SetPriority((IRQn_Type) i, 5); + CMU->CLKEN0_SET = CMU_CLKEN0_GPIO; + gpio_output(RF_SW_PWR_PIN, 1); + gpio_output(RF_SW_PIN, 0); +} diff --git a/tutorials/silabs/xiao-mg24/minimal/link.ld b/tutorials/silabs/xiao-mg24/minimal/link.ld new file mode 100644 index 00000000..ac822361 --- /dev/null +++ b/tutorials/silabs/xiao-mg24/minimal/link.ld @@ -0,0 +1,48 @@ +/* EFR32MG24B220F1536IM48 memory map, see Simplicity SDK linkerfile_base.ld */ +ENTRY(Reset_Handler); +MEMORY { + flash(rx) : ORIGIN = 0x08000000, LENGTH = 1536k + sram(rwx) : ORIGIN = 0x20000000, LENGTH = 256k +} +__StackTop = ORIGIN(sram) + LENGTH(sram); /* Stack points here */ +__StackLimit = __StackTop - 8192; /* MSPLIM, see startup */ + +SECTIONS { + .vectors : { KEEP(*(.vectors)) . = ALIGN(4); } > flash + .text : { *(.text* .text.* text_*) } > flash /* text_*: SDK code */ + .rodata : { *(.rodata*) . = ALIGN(4); } > flash + + /* RAM functions, copied by CopyToRam() in the startup code */ + .ramfuncs : { + . = ALIGN(4); + __ramfuncs_start__ = .; + *(.ram .ram.*) + . = ALIGN(4); + __ramfuncs_end__ = .; + } > sram AT > flash + __lma_ramfuncs_start__ = LOADADDR(.ramfuncs); + __lma_ramfuncs_end__ = LOADADDR(.ramfuncs) + SIZEOF(.ramfuncs); + + .data : { + __data_start__ = .; + *(.first_data) + *(.iram .iram.*) /* Mongoose MG_IRAM functions */ + *(.data SORT(.data.*)) + . = ALIGN(4); + __data_end__ = .; + } > sram AT > flash + __etext = LOADADDR(.data); /* Startup copies .data from here */ + + .bss : { + __bss_start__ = .; + *(.bss SORT(.bss.*) COMMON) + . = ALIGN(4); + __bss_end__ = .; + } > sram + + /* Health record, see MG_HEALTH_RAM: not cleared, survives a warm reset */ + .mg_health (NOLOAD) : { KEEP(*(.mg_health .mg_health*)) } > sram + + . = ALIGN(8); + _end = .; /* Start of heap, see _sbrk() in hal.c */ +} diff --git a/tutorials/silabs/xiao-mg24/minimal/main.c b/tutorials/silabs/xiao-mg24/minimal/main.c new file mode 100644 index 00000000..40a97832 --- /dev/null +++ b/tutorials/silabs/xiao-mg24/minimal/main.c @@ -0,0 +1,113 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "hal.h" +#include "mongoose.h" + +#ifndef UART_DEBUG +#define UART_DEBUG USART0 // SAMD11 USB serial port +#define UART_DEBUG_TX_PIN PIN('A', 8) +#define UART_DEBUG_RX_PIN PIN('A', 9) +#else +#define UART_DEBUG_TX_PIN PIN('C', 6) // D6, D7 header pins +#define UART_DEBUG_RX_PIN PIN('C', 7) +#endif + +#define LED1 PIN('A', 7) // User LED, active low + +static void log_fn(char ch, void *param) { + hal_uart_write_buf(param, &ch, 1); +} + +static void blink_task(void) { + static uint64_t blink_timer = 0; + if (hal_timer_expired(&blink_timer, 500, hal_get_tick())) { + hal_gpio_toggle(LED1); + } +} + +uint64_t mg_millis(void) { + return hal_get_tick(); +} + +bool mg_random(void *buf, size_t len) { + for (size_t n = 0; n < len; n += sizeof(uint32_t)) { + uint32_t r = hal_rng_read(); + memcpy((char *) buf + n, &r, n + sizeof(r) > len ? len - n : sizeof(r)); + } + return true; +} + +// Self-signed P-256 certificate and key for the HTTPS listener. Created by: +// openssl ecparam -genkey -name prime256v1 -noout -out key.pem +// openssl req -x509 -key key.pem -out cert.pem -days 3650 -subj /CN=mg +#define TLS_CERT \ + "-----BEGIN CERTIFICATE-----\n" \ + "MIIBgjCCASegAwIBAgIUIJOq77Qn7lHbH6WseMemeqorR50wCgYIKoZIzj0EAwIw\n" \ + "FjEUMBIGA1UEAwwLbnVjbGVvLWg3MjMwHhcNMjYxMDEyMTUwMjE5WhcNMzYxMDA5\n" \ + "MTUwMjE5WjAWMRQwEgYDVQQDDAtudWNsZW8taDcyMzBZMBMGByqGSM49AgEGCCqG\n" \ + "SM49AwEHA0IABB6IQJNmzrsdQL+2BnZYS7GYOaj7sz8f6cjBmz3n8D9Em19XodlU\n" \ + "uy4ybIvyhr766lL0ERoTmMUivb/lGmzx9C+jUzBRMB0GA1UdDgQWBBQqxMuc4gj7\n" \ + "MTMzreWlRMNI3QMqTzAfBgNVHSMEGDAWgBQqxMuc4gj7MTMzreWlRMNI3QMqTzAP\n" \ + "BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQDY4xaJpuxq6b3rHK3b\n" \ + "lOIQyfkQX+orCRqQUkseplqpqwIhAKswZFNn+2zUNRRNY8+cHmdIwmdpsmxtoWYP\n" \ + "BCjX++LI\n" \ + "-----END CERTIFICATE-----\n" + +#define TLS_KEY \ + "-----BEGIN EC PRIVATE KEY-----\n" \ + "MHcCAQEEIJ/GBl0TlD52q9PMg1KWaNIdXqp8b/HyIsABmBpqC+O1oAoGCCqGSM49\n" \ + "AwEHoUQDQgAEHohAk2bOux1Av7YGdlhLsZg5qPuzPx/pyMGbPefwP0SbX1eh2VS7\n" \ + "LjJsi/KGvvrqUvQRGhOYxSK9v+UabPH0Lw==\n" \ + "-----END EC PRIVATE KEY-----\n" + +static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_ACCEPT && c->is_tls) { // Accepted on the https:// listener + struct mg_tls_opts opts = {.cert = mg_str(TLS_CERT), + .key = mg_str(TLS_KEY)}; + mg_tls_init(c, &opts); + } else if (ev == MG_EV_HTTP_MSG) { + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + if (mg_match(hm->uri, mg_str("/api/tick"), NULL)) { + mg_http_reply(c, 200, "", "{%m:%llu}\n", MG_ESC("tick"), hal_get_tick()); + } else if (mg_match(hm->uri, mg_str("/api/crash"), NULL)) { + __asm volatile("udf #0"); // Trigger HardFault + } else { + mg_http_reply(c, 200, "", + "Hi from Mongoose!\n" + "tick: %llu\n" + "ram used: %zu\n" + "ram free: %zu\n", + hal_get_tick(), hal_ram_used(), hal_ram_free()); + } + } +} + +int main(void) { + MG_HEALTH_INIT(); + + hal_clock_init(); + hal_uart_init(UART_DEBUG, UART_DEBUG_TX_PIN, UART_DEBUG_RX_PIN, 115200); + mg_log_set_fn(log_fn, UART_DEBUG); + hal_rng_init(); + hal_radio_init(); + hal_gpio_output(LED1); + + struct mg_mgr mgr; + mg_mgr_init(&mgr); + mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL); + mg_http_listen(&mgr, "https://0.0.0.0", http_ev_handler, NULL); + mg_mqtt_init(&mgr); + + MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000)); + MG_INFO(("RAM used: %zu, free: %zu", hal_ram_used(), hal_ram_free())); + MG_INFO(("Crash record: %M", mg_print_crash_record)); + + for (;;) { + mg_mgr_poll(&mgr, 0); + mg_mqtt_poll(&mgr); + blink_task(); + } + + return 0; +} diff --git a/tutorials/silabs/xiao-mg24/minimal/mongoose_config.h b/tutorials/silabs/xiao-mg24/minimal/mongoose_config.h new file mode 100644 index 00000000..02715a76 --- /dev/null +++ b/tutorials/silabs/xiao-mg24/minimal/mongoose_config.h @@ -0,0 +1,34 @@ +#pragma once +#include "em_device.h" + +// See https://mongoose.ws/docs/getting-started/build-options/ +#define MG_ARCH MG_ARCH_ARMGCC +#define MG_TLS MG_TLS_BUILTIN +#define MQTT_CLIENT_ID "mg24" // For tutorials/mqtt/mqtt-client + +#define MG_ENABLE_TCPIP 1 +#define MG_ENABLE_DRIVER_MG24 1 +#define MG_ENABLE_CUSTOM_MILLIS 1 +#define MG_ENABLE_CUSTOM_RANDOM 1 // Secure Engine TRNG, see hal.c +#define MG_ENABLE_TCPIP_TCPRTX 1 // Link can be lossy, enable retransmit + +// Stable MAC, so the DHCP address stays the same across reboots: a hash of +// the factory EUI-64, DEVINFO, locally administered +#define MG_SET_MAC_ADDRESS(mac) \ + do { \ + uint32_t eui_[2] = {DEVINFO->EUI64L, DEVINFO->EUI64H}; \ + uint8_t b_[32]; \ + mg_sha256(b_, (uint8_t *) eui_, sizeof(eui_)); \ + (mac)[0] = 2, (mac)[1] = b_[1], (mac)[2] = b_[2], (mac)[3] = b_[3]; \ + (mac)[4] = b_[4], (mac)[5] = b_[5]; \ + } while (0) + +// Crash report support. Health record lives in the .mg_health region, see link.ld +#define MG_ENABLE_HEALTH 1 +#define MG_HEALTH_RAM __attribute__((section(".mg_health"))) + +// mdash.net device management service support +#define MG_ENABLE_MDASH 0 +#define MG_MDASH_KEY "..." // Set this - mDash device token +#define MG_FIRMWARE_VERSION "mg24.1.0.0" +