diff --git a/modules/drivers/gdey037t03/gdey037t03.c b/modules/drivers/gdey037t03/gdey037t03.c new file mode 100644 index 000000000..7307ddf2a --- /dev/null +++ b/modules/drivers/gdey037t03/gdey037t03.c @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 Joshua Jun + * + * This file is part of the Moddable SDK Runtime. + * + * The Moddable SDK Runtime is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Moddable SDK Runtime is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with the Moddable SDK Runtime. If not, see . + * + */ + +#include "xsmc.h" +#include "xsHost.h" + +// Geometry must match gdey037t03.js: logical 416 x 240, native 240 x 416. +#define STRIDE 52 // logical row bytes (416 >> 3) +#define NATIVE_STRIDE 30 // native row bytes (240 >> 3) +#define NATIVE_H 416 + +// transpose(logicalBuffer, outBuffer): native(nx, ny) = logical(ny, nx). +// Each native output byte packs 8 logical columns at one native row. +void xs_gdey037t03_transpose(xsMachine *the) +{ + uint8_t *logical, *out; + xsUnsignedValue logicalLen, outLen; + + xsmcGetBufferReadable(xsArg(0), (void **)&logical, &logicalLen); + xsmcGetBufferWritable(xsArg(1), (void **)&out, &outLen); + if ((logicalLen < STRIDE * 240) || (outLen < NATIVE_STRIDE * NATIVE_H)) + xsUnknownError("buffer too small"); + + for (int ny = 0; ny < NATIVE_H; ny++) { + int lShift = 7 - (ny & 7); + const uint8_t *src = logical + (ny >> 3); + uint8_t *orow = out + ny * NATIVE_STRIDE; + for (int nb = 0; nb < NATIVE_STRIDE; nb++) { + uint8_t byte = 0; + if ((src[0] >> lShift) & 1) byte |= 0x80; + if ((src[STRIDE] >> lShift) & 1) byte |= 0x40; + if ((src[STRIDE * 2] >> lShift) & 1) byte |= 0x20; + if ((src[STRIDE * 3] >> lShift) & 1) byte |= 0x10; + if ((src[STRIDE * 4] >> lShift) & 1) byte |= 0x08; + if ((src[STRIDE * 5] >> lShift) & 1) byte |= 0x04; + if ((src[STRIDE * 6] >> lShift) & 1) byte |= 0x02; + if ((src[STRIDE * 7] >> lShift) & 1) byte |= 0x01; + orow[nb] = byte; + src += STRIDE << 3; // advance nx by 8 + } + } +} diff --git a/modules/drivers/gdey037t03/gdey037t03.js b/modules/drivers/gdey037t03/gdey037t03.js new file mode 100644 index 000000000..5133a162b --- /dev/null +++ b/modules/drivers/gdey037t03/gdey037t03.js @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2026 Joshua Jun + * + * This file is part of the Moddable SDK Runtime. + * + * The Moddable SDK Runtime is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Moddable SDK Runtime is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with the Moddable SDK Runtime. If not, see . + * + */ + +/* + Datasheet: https://v4.cecdn.yun300.cn/100001_1909185148/GDEY037T03-new.pdf + + The panel's pixel array is 240 x 416 with a reversed gate scan. This driver + presents the GDEY037T03's advertised orientation (416 x 240 landscape, upright) + so software uses it at rotation 0 with no flip: the framebuffer is transposed + into the native array on output, which also absorbs the gate-scan reversal. +*/ + +import Timer from "timer"; +import Bitmap from "commodetto/Bitmap"; +import Dither from "commodetto/Dither"; + +const WIDTH = 416; +const HEIGHT = 240; +const STRIDE = WIDTH >> 3; +const NATIVE_W = HEIGHT; +const NATIVE_H = WIDTH; +const NATIVE_STRIDE = NATIVE_W >> 3; +const FRAME_BYTES = STRIDE * HEIGHT; + +// Native transpose (gdey037t03.c): native(nx, ny) = logical(ny, nx). Done in C +// because the per-pixel bit loop over the whole frame is far too slow in script. +function transpose(logical, out) @ "xs_gdey037t03_transpose"; + +class EPD { + constructor() { + const Digital = device.io.Digital; + const SPI = device.io.SPI; + + this.select = new Digital({ + pin: device.pin.epdSelect, + mode: Digital.Output, + initialValue: 1, + }); + this.dc = new Digital({ + pin: device.pin.epdDC, + mode: Digital.Output, + initialValue: 1, + }); + this.reset = new Digital({ + pin: device.pin.epdReset, + mode: Digital.Output, + initialValue: 1, + }); + this.busy = new Digital({ pin: device.pin.epdBusy, mode: Digital.Input }); + + this.spi = new SPI({ ...device.SPI.default, hz: 10_000_000, mode: 0 }); + this.spi.byte = new Uint8Array(1); + } + close() { + this.select?.close(); + this.dc?.close(); + this.reset?.close(); + this.busy?.close(); + this.spi?.close(); + this.select = this.dc = this.reset = this.busy = this.spi = undefined; + } + + writeCMD(cmd) { + this.select.write(0); + this.dc.write(0); + this.spi.byte[0] = cmd; + this.spi.write(this.spi.byte); + this.select.write(1); + } + writeData(value) { + this.select.write(0); + this.dc.write(1); + this.spi.byte[0] = value; + this.spi.write(this.spi.byte); + this.select.write(1); + } + writeBuffer(buffer) { + this.select.write(0); + this.dc.write(1); + this.spi.write(buffer); + this.select.write(1); + } + + isBusy() { + return !this.busy.read(); // LOW (0) = busy + } + waitBusy(ms = 5000) { + const deadline = Date.now() + ms; + do { + if (!this.isBusy()) return; + Timer.delay(1); + } while (Date.now() < deadline); + throw new Error("UC8253 BUSY timeout"); + } + + reset_() { + this.reset.write(0); + Timer.delay(10); + this.reset.write(1); + Timer.delay(10); + } + + initFull() { + // EPD_Init — cleanest, flashes + this.reset_(); + this.writeCMD(0x04); + this.waitBusy(); + this.writeCMD(0x50); + this.writeData(0x97); + } + initFast() { + // EPD_Init_Fast — ~1.5 s + this.reset_(); + this.writeCMD(0x04); + this.waitBusy(); + this.writeCMD(0xe0); + this.writeData(0x02); + this.writeCMD(0xe5); + this.writeData(0x5f); + } + initPart() { + // EPD_Init_Part — no flash + this.reset_(); + this.writeCMD(0x04); + this.waitBusy(); + this.writeCMD(0xe0); + this.writeData(0x02); + this.writeCMD(0xe5); + this.writeData(0x6e); + this.writeCMD(0x50); + this.writeData(0xd7); + } + + #refresh() { + this.writeCMD(0x12); + Timer.delay(1); // >= 200us before polling BUSY + this.waitBusy(); + } + #powerOff() { + this.writeCMD(0x02); + this.waitBusy(); + } + + display(oldFrame, newFrame) { + this.writeCMD(0x10); + this.writeBuffer(oldFrame); + this.writeCMD(0x13); + this.writeBuffer(newFrame); + this.#refresh(); + this.#powerOff(); + } + + displayPartial(x, y, w, h, oldWin, newWin) { + const xEnd = x + w - 1, + yEnd = y + h - 1; + this.writeCMD(0x91); // partial in + this.writeCMD(0x90); // window setting + this.writeData(x); + this.writeData(xEnd); + this.writeData(y >> 8); + this.writeData(y & 0xff); + this.writeData(yEnd >> 8); + this.writeData(yEnd & 0xff); + this.writeData(0x01); + this.writeCMD(0x10); + this.writeBuffer(oldWin); + this.writeCMD(0x13); + this.writeBuffer(newWin); + this.#refresh(); + this.writeCMD(0x92); // partial out + this.#powerOff(); + } + + deepSleep() { + this.writeCMD(0x02); + this.waitBusy(); + this.writeCMD(0x07); + this.writeData(0xa5); + } +} + +class Display { + // Commodetto PixelsOut + #epd; + #current = new Uint8Array(FRAME_BYTES).fill(0xff); // logical frame being drawn (0xFF = white) + #previous = new Uint8Array(FRAME_BYTES).fill(0xff); // last displayed native frame (0x10 "old data") + #dither = new Dither({ width: WIDTH }); + #mode = "full"; // full-refresh waveform: "full" (clean) or "fast" + #wantFull = true; // next update is a full (de-ghosting) refresh + + constructor(options = {}) { + this.#epd = new EPD(); + if (options.mode) this.#mode = options.mode; + } + close() { + if (this.#epd) { + this.#epd.deepSleep(); + this.#epd.close(); + this.#epd = undefined; + } + this.#dither?.close?.(); + this.#dither = this.#current = this.#previous = undefined; + } + configure(options) { + if (undefined !== options?.refresh) this.#wantFull = options.refresh; + if (undefined !== options?.mode) this.#mode = options.mode; + if (undefined !== options?.dither) { + this.#dither.close(); + let algorithm = options.dither; + if (false === algorithm) algorithm = "none"; + else if (true === algorithm) algorithm = undefined; + this.#dither = new Dither({ width: WIDTH, algorithm }); + } + } + + begin(x, y, width, height) { + this.#current.position = 0; + this.#dither.reset(); + } + send(pixels, offset = 0, byteLength = pixels.byteLength) { + const buffer = this.#current; + this.#dither.send( + Math.idiv(byteLength, WIDTH), + pixels, + offset, + buffer, + buffer.position, + ); + buffer.position += byteLength >> 3; + } + end() { + const epd = this.#epd; + const next = this.#toNative(this.#current); + if (this.#wantFull) { + // full refresh: redraws every pixel, clears ghosting, flashes + "fast" === this.#mode ? epd.initFast() : epd.initFull(); + epd.display(this.#previous, next); + this.#wantFull = false; + } else { + // partial waveform over the whole screen: no flash + epd.initPart(); + epd.displayPartial(0, 0, NATIVE_W, NATIVE_H, this.#previous, next); + } + this.#previous = next; + } + continue() { + return this.end(); + } + adaptInvalid(area) { + area.x = 0; + area.y = 0; + area.w = WIDTH; + area.h = HEIGHT; + } + + // Force a full (de-ghosting) refresh of the current image right now. + refresh() { + const epd = this.#epd; + "fast" === this.#mode ? epd.initFast() : epd.initFull(); + epd.display(this.#previous, this.#previous); + this.#wantFull = false; + } + + get width() { + return WIDTH; + } + get height() { + return HEIGHT; + } + get pixelFormat() { + return Bitmap.Gray256; + } + get async() { + return false; + } + pixelsToBytes(count) { + return count; + } + + // Transpose a full logical (W x H) frame into the native (H x W) panel array, + // absorbing the panel's reversed gate scan so the advertised landscape + // orientation comes out upright. The work is done natively (see gdey037t03.c). + #toNative(logical) { + const out = new Uint8Array(NATIVE_STRIDE * NATIVE_H); + transpose(logical.buffer, out.buffer); + return out; + } +} + +export default Display; diff --git a/modules/drivers/gdey037t03/manifest.json b/modules/drivers/gdey037t03/manifest.json new file mode 100644 index 000000000..41ddf0e30 --- /dev/null +++ b/modules/drivers/gdey037t03/manifest.json @@ -0,0 +1,12 @@ +{ + "modules": { + "gdey037t03": "$(MODULES)/drivers/gdey037t03/gdey037t03", + "commodetto/Dither": "$(COMMODETTO)/commodettoDither", + "commodetto/Bitmap": "$(COMMODETTO)/commodettoBitmap" + }, + "preload": [ + "gdey037t03", + "commodetto/Dither", + "commodetto/Bitmap" + ] +}