aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKimplul <kimi.h.kuparinen@gmail.com>2025-03-04 20:08:04 +0200
committerKimplul <kimi.h.kuparinen@gmail.com>2025-03-04 20:08:04 +0200
commitf03c2027c3ed7f385842b8583bb1953e04dd9386 (patch)
tree5d5d42e0922fcad989361745778a00d529ff2e48
parent0464b7765aa4d1fc2178e443a3b37d19cfe541a0 (diff)
downloadgran-f03c2027c3ed7f385842b8583bb1953e04dd9386.tar.gz
gran-f03c2027c3ed7f385842b8583bb1953e04dd9386.zip
add fat butterfly
-rw-r--r--include/gran/bfly/fat_bfly.h20
-rw-r--r--include/gran/utils.h18
-rw-r--r--src/bfly/fat_bfly.c271
-rw-r--r--src/bfly/source.mk1
-rw-r--r--tests/simple_fat_bfly/sim.c73
-rw-r--r--tests/simple_fat_bfly/source.mk18
-rw-r--r--tests/simple_fat_bfly/test.c52
7 files changed, 453 insertions, 0 deletions
diff --git a/include/gran/bfly/fat_bfly.h b/include/gran/bfly/fat_bfly.h
new file mode 100644
index 0000000..c3025aa
--- /dev/null
+++ b/include/gran/bfly/fat_bfly.h
@@ -0,0 +1,20 @@
+#ifndef GRAN_FAT_BFLY_H
+#define GRAN_FAT_BFLY_H
+
+#include <gran/component.h>
+
+struct component *create_fat_bfly(uint32_t elems);
+stat fat_bfly_connect(struct component *bfly, struct component *component, uint32_t elem);
+
+static inline void addr_fat_bfly(uint64_t addr, uint32_t *elem, uint32_t *off)
+{
+ if (off) *off = addr & 0xffffffff;
+ if (elem) *elem = (addr >> 32) & 0xffffffff;
+}
+
+static inline uint64_t fat_bfly_addr(uint32_t elem, uint32_t off)
+{
+ return ((uint64_t)elem << 32) | off;
+}
+
+#endif /* GRAN_FAT_BFLY_H */
diff --git a/include/gran/utils.h b/include/gran/utils.h
new file mode 100644
index 0000000..eb76bf5
--- /dev/null
+++ b/include/gran/utils.h
@@ -0,0 +1,18 @@
+#ifndef GRAN_UTILS_H
+#define GRAN_UTILS_H
+
+#include <stdbool.h>
+
+/* assumes x != 0 */
+static inline bool is_powerof2(unsigned long long x)
+{
+ return __builtin_popcountll(x) == 1;
+}
+
+/* assumes x != 0 */
+static inline unsigned long long log2ull(unsigned long long x)
+{
+ return __builtin_ffsll(x) - 1;
+}
+
+#endif /* GRAN_UTILS_H */
diff --git a/src/bfly/fat_bfly.c b/src/bfly/fat_bfly.c
new file mode 100644
index 0000000..39ad657
--- /dev/null
+++ b/src/bfly/fat_bfly.c
@@ -0,0 +1,271 @@
+#include <gran/utils.h>
+#include <gran/bfly/fat_bfly.h>
+
+/*
+ * Essentially, we build up a binary tree of nodes, where each node has two
+ * input 'banks' of width 2^height, and an output 'bank' twice that. The highest
+ * node has an 'output bank' the same width as the number of nodes in the
+ * network, and data flows upwards in this tree such that packets automatically
+ * end up in the correct order in the final output bank. From there, we can just
+ * directly write the output registers to child nodes. Something like the
+ * following:
+ *
+ * ||||
+ * 0123 - height 2, 2^2 nodes with 2*2^1 input registers
+ * // \\
+ * 01 23 - height 1, 2^1 nodes with 2*2^0 input registers
+ * / \ / \
+ * 0 1 2 3 - height 0, input layer
+ *
+ * The cool thing with this approach is that each output register has to look at just
+ * two input registers, since the input is always (partially) sorted.
+ *
+ * The implementation is a bit nontrivial, unfortunately. Another possibility
+ * could be to initialize a plain 2D array with registers and do some index
+ * calculations to directly associate registers with eachother (our nodes are
+ * ultimately just wrappers around some registers, along with some selection
+ * logic) that I tried to go for, but there are some subtleties in how data
+ * flows in the tree that caused my other attempt to deadlock frequently.
+ * Shouldn't be impossible to fix, but there was even more weird bit math than
+ * in this version so I'm sticking with this for now at least.
+ */
+
+struct reg {
+ struct packet pkt;
+ bool busy;
+};
+
+struct fat_node {
+ /* parent in tree */
+ struct fat_node *up;
+
+ /* arrays, length is calculated based on the node height */
+ struct reg *left, *right;
+ struct reg *out;
+};
+
+struct fat_bfly {
+ struct component component;
+
+ uint32_t elems;
+ struct reg *in; /* elems */
+ struct component **send; /* elems */
+
+ size_t layers;
+ /* noe that in this case output is layer 0 and input layers - 1 */
+ struct fat_node **layer;
+};
+
+/* left selects which input bank to use and idx which index within it */
+static stat node_receive(struct fat_node *node, struct packet pkt, size_t idx, bool left)
+{
+ struct reg *rs = left ? node->left : node->right;
+ if (rs[idx].busy)
+ return EBUSY;
+
+ rs[idx].pkt = pkt;
+ rs[idx].busy = true;
+ return OK;
+}
+
+static stat node_clock(struct fat_node *node, size_t count, size_t height)
+{
+ /* can't go higher than the top layer */
+ if (node->up)
+ for (size_t i = 0; i < count; ++i) {
+ if (!node->out[i].busy)
+ continue;
+
+ struct packet *pkt = &node->out[i].pkt;
+
+ uint32_t elem;
+ addr_fat_bfly(pkt->to, &elem, NULL);
+
+ /* here's a pretty tricky spot, note how we select the input
+ * bank based on the address of our target. This increases
+ * hardware complexity, but avoids at least one possible
+ * deadlock situation compared to just having a fixed right/left
+ * selector. */
+ stat ret = node_receive(node->up, *pkt, i, (elem >> height) & 1);
+ if (ret == EBUSY)
+ continue;
+
+ node->out[i].busy = false;
+ }
+
+ /* populate output registers */
+ for (size_t i = 0; i < count; ++i) {
+ /* waiting on something upstream */
+ if (node->out[i].busy)
+ continue;
+
+ /* quick modulo, we know we're dealing with powers of 2 */
+ /* there are twice as few input registers per bank, when
+ * iterating over all output registers we want to loop back to
+ * the start of the input when going over the halway mark */
+ struct reg *l = &node->left[i & (count/2 - 1)];
+ struct reg *r = &node->right[i & (count/2 - 1)];
+
+ uint32_t el;
+ uint32_t er;
+ addr_fat_bfly(l->pkt.to, &el, NULL);
+ addr_fat_bfly(r->pkt.to, &er, NULL);
+
+ /* packet 'wants' to go right
+ * (wl = left packet wants to go right, could be better) */
+ bool wl = (el >> height) & 1;
+ bool wr = (er >> height) & 1;
+
+ /* we accept a packet if the input register is busy and the
+ * packet wants to go in our direction */
+ bool al = l->busy && (i < count / 2 ? !wl : wl);
+ bool ar = r->busy && (i < count / 2 ? !wr : wr);
+
+ /* can't accept either packet */
+ if (!al && !ar)
+ continue;
+
+ /* select packet */
+ struct reg *s = al ? l : r;
+ if (al && ar)
+ s = l->pkt.timestamp < r->pkt.timestamp ? l : r;
+
+ node->out[i].pkt = s->pkt;
+ node->out[i].busy = true;
+ s->busy = false;
+ }
+
+ return OK;
+}
+
+static stat layer_clock(struct fat_node *layer, size_t count, size_t height)
+{
+ for (size_t i = 0; i < count; ++i)
+ node_clock(&layer[i], 2ULL << height, height);
+
+ return OK;
+}
+
+static stat fat_bfly_clock(struct fat_bfly *bfly)
+{
+ struct reg *out = bfly->layer[0]->out;
+ for (size_t i = 0; i < bfly->elems; ++i) {
+ if (!out[i].busy)
+ continue;
+
+ uint32_t elem;
+ addr_fat_bfly(out[i].pkt.to, &elem, NULL);
+ assert(elem == i);
+
+ stat ret = SEND(bfly, bfly->send[i], out[i].pkt);
+ if (ret == EBUSY)
+ continue;
+
+ out[i].busy = false;
+ }
+
+ for (size_t i = 0; i < bfly->layers; ++i)
+ layer_clock(bfly->layer[i], 1ULL << i, bfly->layers - i - 1);
+
+ for (size_t i = 0; i < bfly->elems; ++i) {
+ if (!bfly->in[i].busy)
+ continue;
+
+ struct fat_node *node = &bfly->layer[bfly->layers - 1][i / 2];
+ stat ret = node_receive(node, bfly->in[i].pkt, 0, i % 2);
+ if (ret == EBUSY)
+ continue;
+
+ bfly->in[i].busy = false;
+ }
+
+ return OK;
+}
+
+static stat fat_bfly_receive(struct fat_bfly *bfly, struct component *from, struct packet pkt)
+{
+ uint32_t elem;
+ addr_fat_bfly(pkt.from, &elem, NULL);
+ assert(elem < bfly->elems);
+ assert(bfly->send[elem] == from);
+
+ if (bfly->in[elem].busy)
+ return EBUSY;
+
+ bfly->in[elem].pkt = pkt;
+ bfly->in[elem].busy = true;
+ return OK;
+}
+
+static void fat_bfly_destroy(struct fat_bfly *bfly)
+{
+ for (size_t i = 0; i < bfly->layers; ++i) {
+ size_t count = 1ULL << i;
+ for (size_t j = 0; j < count; ++j) {
+ free(bfly->layer[i][j].out);
+ free(bfly->layer[i][j].left);
+ free(bfly->layer[i][j].right);
+ }
+
+ free(bfly->layer[i]);
+ }
+
+ free(bfly->in);
+ free(bfly->send);
+ free(bfly->layer);
+ free(bfly);
+}
+
+struct component *create_fat_bfly(uint32_t elems)
+{
+ /* initialization of deeply nested pointer hierarchies is a bit of a
+ * pain, so just assert that allocations succeed for now. Might look
+ * into more graceful cleanup in the future. */
+ assert(is_powerof2(elems));
+ struct fat_bfly *bfly = calloc(1, sizeof(struct fat_bfly));
+ assert(bfly);
+
+ bfly->elems = elems;
+ bfly->send = calloc(elems, sizeof(struct component *));
+ assert(bfly->send);
+
+ bfly->in = calloc(elems, sizeof(struct reg));
+ assert(bfly->in);
+
+ unsigned long long layers = log2ull(elems);
+
+ bfly->layers = layers;
+ bfly->layer = calloc(layers, sizeof(bfly->layers));
+ assert(bfly->layers);
+
+ for (size_t i = 0; i < layers; ++i) {
+ size_t count = 1ULL << i;
+ bfly->layer[i] = calloc(count, sizeof(struct fat_node));
+ assert(bfly->layer[i]);
+
+ for (size_t j = 0; j < count; ++j) {
+ /* j / 2 since there are always twice as few nodes per layer */
+ bfly->layer[i][j].up = i == 0 ? NULL : &bfly->layer[i - 1][j / 2];
+ bfly->layer[i][j].left = calloc(elems / count, sizeof(struct reg));
+ bfly->layer[i][j].right = calloc(elems / count, sizeof(struct reg));
+ bfly->layer[i][j].out = calloc(2 * elems / count, sizeof(struct reg));
+ assert(bfly->layer[i][j].left);
+ assert(bfly->layer[i][j].right);
+ assert(bfly->layer[i][j].out);
+ }
+ }
+
+ bfly->component.clock = (clock_callback)fat_bfly_clock;
+ bfly->component.receive = (receive_callback)fat_bfly_receive;
+ bfly->component.destroy = (destroy_callback)fat_bfly_destroy;
+ return (struct component *)bfly;
+}
+
+stat fat_bfly_connect(struct component *bfly, struct component *component, uint32_t elem)
+{
+ struct fat_bfly *b = (struct fat_bfly *)bfly;
+ assert(elem < b->elems);
+ assert(b->send[elem] == NULL);
+ b->send[elem] = component;
+ return OK;
+}
diff --git a/src/bfly/source.mk b/src/bfly/source.mk
new file mode 100644
index 0000000..f09a33d
--- /dev/null
+++ b/src/bfly/source.mk
@@ -0,0 +1 @@
+SOURCES += src/bfly/fat_bfly.c
diff --git a/tests/simple_fat_bfly/sim.c b/tests/simple_fat_bfly/sim.c
new file mode 100644
index 0000000..7d7b166
--- /dev/null
+++ b/tests/simple_fat_bfly/sim.c
@@ -0,0 +1,73 @@
+#include <assert.h>
+
+#include <gran/root.h>
+#include <gran/mem/simple_mem.h>
+#include <gran/bus/simple_bus.h>
+#include <gran/uart/simple_uart.h>
+#include <gran/bfly/fat_bfly.h>
+#include <gran/cpu/riscv/simple_riscv64.h>
+
+#include "../build/tests/simple_fat_bfly/test.inc"
+
+static stat build_ideal_noc(struct clock_domain *clk, uint32_t x)
+{
+ struct component **pes = calloc(x, sizeof(struct component *));
+ assert(pes);
+
+ struct component *noc = create_fat_bfly(x);
+ clock_domain_add(clk, noc);
+
+ for (uint32_t i = 0; i < x; ++i) {
+ if (i == 0 || i == 1)
+ continue;
+
+ struct component *imem = create_simple_mem(4096);
+ init_simple_mem(imem, 0,
+ build_tests_simple_fat_bfly_test_bin_len,
+ build_tests_simple_fat_bfly_test_bin);
+
+ uint64_t rcv = fat_bfly_addr(i, 0);
+ struct component *rv64 = create_simple_riscv64(rcv, 0, imem, noc);
+ simple_riscv64_set_reg(rv64, 10, i); /* a0 */
+ simple_riscv64_set_reg(rv64, 11, x); /* a1 */
+
+ clock_domain_add(clk, rv64);
+ clock_domain_add(clk, imem);
+
+ pes[i] = rv64;
+ }
+
+ struct component *uart = create_simple_uart();
+ clock_domain_add(clk, uart);
+ fat_bfly_connect(noc, uart, 0);
+
+ struct component *dmem = create_simple_mem(4096);
+ clock_domain_add(clk, dmem);
+ fat_bfly_connect(noc, dmem, 1);
+
+ for (size_t i = 0; i < x; ++i) {
+ if (i == 0 || i == 1)
+ continue;
+
+ fat_bfly_connect(noc, pes[i], i);
+ }
+
+ free(pes);
+ return OK;
+}
+
+int main()
+{
+ struct clock_domain *clk = create_clock_domain(NS(1));
+
+ stat r = build_ideal_noc(clk, 64);
+ assert(r == OK);
+
+ struct gran_root *root = create_root();
+ root_add_clock(root, clk);
+
+ r = root_run(root);
+ assert(r == OK);
+
+ destroy_root(root);
+}
diff --git a/tests/simple_fat_bfly/source.mk b/tests/simple_fat_bfly/source.mk
new file mode 100644
index 0000000..9aae373
--- /dev/null
+++ b/tests/simple_fat_bfly/source.mk
@@ -0,0 +1,18 @@
+FAT_BFLY_TEST_OBJ != ./scripts/gen-deps --sources "tests/simple_fat_bfly/sim.c"
+TEST_PROGS += build/tests/simple_fat_bfly/sim
+
+build/tests/simple_fat_bfly/test.inc: tests/simple_fat_bfly/test.c
+ riscv64-unknown-elf-gcc -O2 -Wall -Wextra -ffreestanding -nostdlib \
+ -march=rv64i -mabi=lp64 \
+ -o build/tests/simple_fat_bfly/test \
+ tests/simple_fat_bfly/test.c
+ riscv64-unknown-elf-objcopy -Obinary \
+ build/tests/simple_fat_bfly/test \
+ build/tests/simple_fat_bfly/test.bin
+ xxd -i build/tests/simple_fat_bfly/test.bin \
+ > build/tests/simple_fat_bfly/test.inc
+
+build/tests/simple_fat_bfly/sim.o: build/tests/simple_fat_bfly/test.inc
+
+build/tests/simple_fat_bfly/sim: $(FAT_BFLY_TEST_OBJ) $(OBJS)
+ $(COMPILE) $(FAT_BFLY_TEST_OBJ) $(OBJS) -o $@
diff --git a/tests/simple_fat_bfly/test.c b/tests/simple_fat_bfly/test.c
new file mode 100644
index 0000000..2e4a22c
--- /dev/null
+++ b/tests/simple_fat_bfly/test.c
@@ -0,0 +1,52 @@
+__attribute__((always_inline))
+static inline char hex_char(unsigned x)
+{
+ if (x <= 9)
+ return x + '0';
+
+ return (x - 10) + 'a';
+}
+
+__attribute__((always_inline))
+static inline void print_int32(volatile char *uart, unsigned x)
+{
+ *uart = hex_char((x >> 28) & 0xf);
+ *uart = hex_char((x >> 24) & 0xf);
+ *uart = hex_char((x >> 20) & 0xf);
+ *uart = hex_char((x >> 16) & 0xf);
+ *uart = hex_char((x >> 12) & 0xf);
+ *uart = hex_char((x >> 8) & 0xf);
+ *uart = hex_char((x >> 4) & 0xf);
+ *uart = hex_char((x >> 0) & 0xf);
+}
+
+__attribute__((always_inline))
+static inline void print_addr(volatile char *uart, unsigned x)
+{
+ *uart = '(';
+ print_int32(uart, x);
+ *uart = ')';
+ *uart = '\n';
+}
+
+void _start(unsigned x, unsigned X)
+{
+ volatile char *uart = (char *)4096;
+ volatile unsigned *control = (unsigned *)(1ULL << 32);
+
+ if (x == 2) {
+ goto do_work;
+ } else {
+ while (*control != x) {}
+ }
+
+do_work:
+ print_addr(uart, x);
+ *control = x + 1;
+
+ if (x == X - 1)
+ asm("ebreak");
+
+ /* otherwise just loop */
+ while (1) {}
+}