#include /* Copyright (c) 2026 Devine Lu Linvega Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE. The m5 Stick C Plus, requires m5stack(2.1.4) */ static const uint8_t ROM_DATA[] = { 0xa0, 0x4c, 0xfd, 0x80, 0x08, 0x37, 0xa0, 0x4c, 0xf3, 0x80, 0x0a, 0x37, 0xa0, 0xdc, 0xf2, 0x80, 0x0c, 0x37, 0xa0, 0x01, 0x38, 0x80, 0x20, 0x37, 0x80, 0x22, 0x36, 0x26, 0xa0, 0x00, 0x20, 0x39, 0x80, 0x22, 0x33, 0x80, 0x01, 0x3f, 0x80, 0x24, 0x36, 0x26, 0xa0, 0x00, 0x10, 0x39, 0x80, 0x2f, 0x33, 0x80, 0x01, 0x3f, 0x60, 0x00, 0x40, 0x00, 0x80, 0x4f, 0x32, 0x9d, 0x20, 0x00, 0x04, 0xa0, 0x38, 0x0f, 0x13, 0x26, 0xa0, 0x00, 0x00, 0x29, 0x20, 0x00, 0x04, 0xa0, 0x39, 0x03, 0x13, 0xa0, 0x00, 0x01, 0x38, 0x80, 0x3a, 0x32, 0x9d, 0x20, 0x00, 0x04, 0xa0, 0x38, 0x0f, 0x13, 0x26, 0xa0, 0x00, 0x00, 0x29, 0x20, 0x00, 0x04, 0xa0, 0x39, 0x03, 0x13, 0xa0, 0x00, 0x01, 0x38, 0x60, 0x00, 0x01, 0x00, 0x80, 0x00, 0x60, 0x00, 0x08, 0x80, 0x16, 0x33, 0x80, 0x0d, 0x33, 0x80, 0x01, 0xa0, 0x36, 0x26, 0x17, 0xa0, 0x01, 0x9a, 0x80, 0x2c, 0x37, 0xa0, 0x00, 0x00, 0x80, 0x28, 0x37, 0xa0, 0x00, 0x00, 0x80, 0x2a, 0x37, 0x80, 0x2f, 0x97, 0x17, 0x6c, 0x00, 0x1f, 0x3f, 0x38, 0x38, 0x38, 0x78, 0x7f, 0x00, 0xfe, 0xfe, 0x7e, 0x77, 0x77, 0xe3, 0xc3, 0x00, 0x0f, 0x1f, 0x3b, 0x7b, 0x77, 0xe7, 0xc7, 0x00, 0xfc, 0xfe, 0x8f, 0x87, 0x07, 0x0e, 0xfc, 0x7f, 0x00, 0x00, 0x0f, 0xff, 0x7f, 0x07, 0x00, 0x03, 0x01, 0x00, 0xff, 0xf0, 0xf8, 0xff, 0x00, 0x87, 0x00, 0x00, 0xff, 0x7f, 0x7f, 0xff, 0x00, 0xf0, 0x00, 0x00, 0xe0, 0xfc, 0xfc, 0x80, 0x00 }; #define SCREEN_W 240 #define SCREEN_H 135 static uint8_t ram[0x10000], dev[0x100], stk[2][0x100], ptr[2]; static uint16_t screen_palette[16]; static unsigned int uxn_eval(uint16_t pc); static void screen_change(int x1, int y1, int x2, int y2); static inline uint16_t peek2(const uint8_t *d) { return ((uint16_t)d[0] << 8) | d[1]; } /* @|System ------------------------------------------------------------ */ static void system_deo_colorize(void) { unsigned int i, shift; uint16_t colors[4]; for(i = 0, shift = 4; i < 4; ++i, shift ^= 4) { uint8_t r4 = (dev[0x8 + i/2] >> shift) & 0xf, r8 = (r4 << 4) | r4; uint8_t g4 = (dev[0xa + i/2] >> shift) & 0xf, g8 = (g4 << 4) | g4; uint8_t b4 = (dev[0xc + i/2] >> shift) & 0xf, b8 = (b4 << 4) | b4; colors[i] = ((r8 & 0xF8) << 8) | ((g8 & 0xFC) << 3) | (b8 >> 3); } for(i = 0; i < 16; i++) screen_palette[i] = colors[i >> 2 ? i >> 2 : i & 3]; screen_change(0, 0, SCREEN_W, SCREEN_H); } /* @|Screen ------------------------------------------------------------ */ static int screen_vector = 0; static int rX, rY, rA, rMX, rMY, rMA, rML, rDX, rDY; static int screen_x1, screen_y1, screen_x2, screen_y2, screen_reqdraw; static uint8_t screen_layers[SCREEN_W * SCREEN_H]; static const uint8_t alpha_lut[16] = {0,1,2,3,4,0,1,2,3,4,0,1,2,3,4,0}; static const uint8_t blend_lut[16][2][4] = { {{0,0,1,2},{0,0,4,8}}, {{0,1,2,3},{0,4,8,12}}, {{0,2,3,1},{0,8,12,4}}, {{0,3,1,2},{0,12,4,8}}, {{1,0,1,2},{4,0,4,8}}, {{1,1,2,3},{4,4,8,12}}, {{1,2,3,1},{4,8,12,4}}, {{1,3,1,2},{4,12,4,8}}, {{2,0,1,2},{8,0,4,8}}, {{2,1,2,3},{8,4,8,12}}, {{2,2,3,1},{8,8,12,4}}, {{2,3,1,2},{8,12,4,8}}, {{3,0,1,2},{12,0,4,8}}, {{3,1,2,3},{12,4,8,12}}, {{3,2,3,1},{12,8,12,4}}, {{3,3,1,2},{12,12,4,8}} }; static void screen_change(int x1, int y1, int x2, int y2) { if(!screen_reqdraw) { screen_x1=x1; screen_y1=y1; screen_x2=x2; screen_y2=y2; screen_reqdraw=1; return; } if(x1 < screen_x1) screen_x1 = x1; if(y1 < screen_y1) screen_y1 = y1; if(x2 > screen_x2) screen_x2 = x2; if(y2 > screen_y2) screen_y2 = y2; } static void screen_deo_pixel(void) { const int ctrl = dev[0x2e]; const int hi = ctrl & 0x40; const uint8_t mask = hi ? 0x03 : 0x0c; const uint8_t color = hi ? ((ctrl & 0x3) << 2) : (ctrl & 0x3); if(ctrl & 0x80) { int px, py; const int x1 = (ctrl & 0x10) ? 0 : rX; const int x2 = (ctrl & 0x10) ? rX : SCREEN_W; const int y1 = (ctrl & 0x20) ? 0 : rY; const int y2 = (ctrl & 0x20) ? rY : SCREEN_H; for(py = y1; py < y2; py++) for(px = x1; px < x2; px++) { uint8_t *d = &screen_layers[py * SCREEN_W + px]; *d = (*d & mask) | color; } screen_change(x1, y1, x2, y2); } else { const int x = rX, y = rY; if(x >= 0 && x < SCREEN_W && y >= 0 && y < SCREEN_H) { uint8_t *d = &screen_layers[y * SCREEN_W + x]; *d = (*d & mask) | color; screen_change(x, y, x+1, y+1); } if(rMX) rX++; if(rMY) rY++; } } static void screen_deo_sprite(void) { int i, j, x = rX, y = rY; const int ctrl = dev[0x2f]; const int flipx = ctrl & 0x10, dx = flipx ? -rDY : rDY; const int flipy = ctrl & 0x20, dy = flipy ? -rDX : rDX; const int row_start = flipx ? 0 : 7, row_delta = flipx ? 1 : -1; const int col_start = flipy ? 7 : 0, col_delta = flipy ? -1 : 1; const int layer = ctrl & 0x40, layer_mask = layer ? 0x3 : 0xc; const int mode_2bpp = ctrl >> 7, addr_2bpp = mode_2bpp ? rMA << 2 : rMA << 1; const int blend = ctrl & 0xf; const uint8_t opaque = alpha_lut[blend]; const uint8_t *table = blend_lut[blend][layer >> 6]; for(i = 0; i <= rML; i++, x += dx, y += dy, rA += addr_2bpp) { if(x < -8 || x >= SCREEN_W || y < -8 || y >= SCREEN_H) continue; const uint8_t *col = &ram[rA + col_start]; for(j = 0; j < 8; j++, col += col_delta) { const int ch1 = *col, ch2 = mode_2bpp ? col[8] : 0; for(int k = 0, row = row_start; k < 8; k++, row += row_delta) { int px = x + k, py = y + j; if(px < 0 || px >= SCREEN_W || py < 0 || py >= SCREEN_H) continue; const int color = ((ch1 >> row) & 1) | (((ch2 >> row) & 1) << 1); if(opaque || color) { uint8_t *d = &screen_layers[py * SCREEN_W + px]; *d = (*d & layer_mask) | table[color]; } } } } { int x1 = flipx ? x : rX, x2 = flipx ? rX : x; int y1 = flipy ? y : rY, y2 = flipy ? rY : y; screen_change(x1-8, y1-8, x2+8, y2+8); } if(rMX) rX += flipx ? -rDX : rDX; if(rMY) rY += flipy ? -rDY : rDY; } static void screen_flush(void) { int x1 = max(0, screen_x1), y1 = max(0, screen_y1); int x2 = min(SCREEN_W, screen_x2), y2 = min(SCREEN_H, screen_y2); int w = x2 - x1, h = y2 - y1; if (w <= 0 || h <= 0) return; static uint16_t row[SCREEN_W]; M5.Lcd.startWrite(); M5.Lcd.setAddrWindow(x1, y1, w, h); for (int y = y1; y < y2; y++) { for (int x = x1; x < x2; x++) row[x - x1] = screen_palette[screen_layers[y * SCREEN_W + x] & 0xf]; M5.Lcd.pushColors(row, w, true); } M5.Lcd.endWrite(); screen_x1 = screen_y1 = screen_x2 = screen_y2 = screen_reqdraw = 0; } static void screen_deo_auto(void) { rMX = dev[0x26] & 0x1; rMY = dev[0x26] & 0x2; rMA = dev[0x26] & 0x4; rML = dev[0x26] >> 4; rDX = rMX << 3; rDY = rMY << 2; } /* @|Controller -------------------------------------------------------- */ static int controller_vector = 0; static void controller_down(uint8_t mask) { if (!mask) return; dev[0x82] |= mask; if (controller_vector) uxn_eval(controller_vector); } static void controller_up(uint8_t mask) { if (!mask) return; dev[0x82] &= ~mask; if (controller_vector) uxn_eval(controller_vector); } static inline uint8_t emu_dei(const uint8_t port) { switch (port) { case 0x22: return SCREEN_W >> 8; case 0x23: return SCREEN_W; case 0x24: return SCREEN_H >> 8; case 0x25: return SCREEN_H; case 0x28: return rX >> 8; case 0x29: return rX; case 0x2a: return rY >> 8; case 0x2b: return rY; case 0x2c: return rA >> 8; case 0x2d: return rA; default: return dev[port]; } } static inline void emu_deo(const uint8_t port, const uint8_t value) { dev[port] = value; switch (port) { case 0x08: case 0x09: case 0x0a: case 0x0b: case 0x0c: case 0x0d: system_deo_colorize(); break; case 0x21: screen_vector = peek2(&dev[0x20]); break; case 0x26: screen_deo_auto(); break; case 0x28: case 0x29: rX = (short)peek2(&dev[0x28]); break; case 0x2a: case 0x2b: rY = (short)peek2(&dev[0x2a]); break; case 0x2c: case 0x2d: rA = peek2(&dev[0x2c]); break; case 0x2e: screen_deo_pixel(); break; case 0x2f: screen_deo_sprite(); break; case 0x81: controller_vector = peek2(&dev[0x80]); break; } } /* @|Core -------------------------------------------------------------- */ #define OPC(opc, A, B) {\ case 0x00|opc: {const uint8_t d=0,r=0;A B} break;\ case 0x20|opc: {const uint8_t d=1,r=0;A B} break;\ case 0x40|opc: {const uint8_t d=0,r=1;A B} break;\ case 0x60|opc: {const uint8_t d=1,r=1;A B} break;\ case 0x80|opc: {const uint8_t d=0,r=0,k=ptr[0];A ptr[0]=k;B} break;\ case 0xa0|opc: {const uint8_t d=1,r=0,k=ptr[0];A ptr[0]=k;B} break;\ case 0xc0|opc: {const uint8_t d=0,r=1,k=ptr[1];A ptr[1]=k;B} break;\ case 0xe0|opc: {const uint8_t d=1,r=1,k=ptr[1];A ptr[1]=k;B} break;} #define DEC(m) stk[m][--ptr[m]] #define INC(m) stk[m][ptr[m]++] #define IMM a = ram[pc++] << 8, a |= ram[pc++]; #define MOV pc = d ? (uint16_t)a : pc + (int8_t)a; #define POx(o,m) o = DEC(r); if(m) o |= DEC(r) << 8; #define PUx(i,m,s) if(m) c = (i), INC(s) = c >> 8, INC(s) = c; else INC(s) = i; #define GOT(o) if(d) o[1] = DEC(r); o[0] = DEC(r); #define PUT(i,s) INC(s) = i[0]; if(d) INC(s) = i[1]; #define DEO(o,v) emu_deo(o, v[0]); if(d) emu_deo(o + 1, v[1]); #define DEI(i,v) v[0] = emu_dei(i); if(d) v[1] = emu_dei(i + 1); PUT(v,r) #define POK(o,v,m) ram[o] = v[0]; if(d) ram[(o + 1) & m] = v[1]; #define PEK(i,v,m) v[0] = ram[i]; if(d) v[1] = ram[(i + 1) & m]; PUT(v,r) static unsigned int uxn_eval(uint16_t pc) { unsigned int a, b, c; uint16_t x[2], y[2], z[2]; for(;;) switch(ram[pc++]) { case 0x00: return 1; case 0x20: if(DEC(0)) { IMM pc += a; } else pc += 2; break; case 0x40: IMM pc += a; break; case 0x60: IMM PUx(pc, 1, 1) pc += a; break; case 0xa0: INC(0) = ram[pc++]; /* fall-through */ case 0x80: INC(0) = ram[pc++]; break; case 0xe0: INC(1) = ram[pc++]; /* fall-through */ case 0xc0: INC(1) = ram[pc++]; break; OPC(0x01,POx(a,d),PUx(a+1,d,r)) OPC(0x02,ptr[r] -= 1+d;,{}) OPC(0x03,GOT(x) ptr[r] -= 1+d;,PUT(x,r)) OPC(0x04,GOT(x) GOT(y),PUT(x,r) PUT(y,r)) OPC(0x05,GOT(x) GOT(y) GOT(z),PUT(y,r) PUT(x,r) PUT(z,r)) OPC(0x06,GOT(x),PUT(x,r) PUT(x,r)) OPC(0x07,GOT(x) GOT(y),PUT(y,r) PUT(x,r) PUT(y,r)) OPC(0x08,POx(a,d) POx(b,d),PUx(b==a,0,r)) OPC(0x09,POx(a,d) POx(b,d),PUx(b!=a,0,r)) OPC(0x0a,POx(a,d) POx(b,d),PUx(b>a,0,r)) OPC(0x0b,POx(a,d) POx(b,d),PUx(b>(a&0xf)<<(a>>4),d,r)) } return 0; } void setup(void) { M5.begin(); M5.Lcd.setRotation(1); M5.Lcd.fillScreen(TFT_BLACK); memcpy(&ram[0x100], ROM_DATA, sizeof(ROM_DATA)); uxn_eval(0x100); } static uint8_t btn_prev = 0; void loop(void) { static uint32_t next_frame = 0; uint32_t now = millis(); if(now < next_frame) return; next_frame = now + 16; /* 60fps */ M5.update(); /* Controller */ uint8_t btn_now = 0; if(M5.BtnA.isPressed()) btn_now |= 0x01; if(M5.BtnB.isPressed()) btn_now |= 0x02; uint8_t changed = btn_prev ^ btn_now; if(changed) { uint8_t pressed = changed & btn_now; uint8_t released = changed & ~btn_now; if(pressed) controller_down(pressed); if(released) controller_up(released); btn_prev = btn_now; } /* Screen */ if(screen_vector) uxn_eval(screen_vector); if(screen_reqdraw) screen_flush(); }