diff --git a/src/boards/boards.c b/src/boards/boards.c index f12e459bd..fe319760a 100644 --- a/src/boards/boards.c +++ b/src/boards/boards.c @@ -159,7 +159,7 @@ void board_teardown(void) { neopixel_teardown(); #endif -#ifdef DISPLAY_PIN_SCK +#ifdef BOARD_HAS_DISPLAY board_display_teardown(); #endif @@ -238,7 +238,7 @@ static void tft_cmd(uint8_t cmd, uint8_t const* data, size_t narg) { tft_cs(true); } -void board_display_init(void) { +bool board_display_init(void) { //------------- SPI init -------------// // highspeed SPIM should set SCK and MOSI to high drive nrf_gpio_cfg(DISPLAY_PIN_SCK, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_CONNECT, @@ -277,6 +277,9 @@ void board_display_init(void) { #endif tft_controller_init(); + + // A soldered-on TFT is always present; nothing here can fail. + return true; } void board_display_teardown(void) { @@ -928,4 +931,215 @@ static void tft_controller_init(void) { } } -#endif \ No newline at end of file +#endif +//--------------------------------------------------------------------+ +// Display controller - I2C mono OLED (SSD1306 / SH1106) +//--------------------------------------------------------------------+ +// Unlike the SPI TFTs above, an I2C OLED is frequently a plug-in module +// (the RAK1921 in a WisBlock IO slot, for instance) that may simply not be +// fitted. Every transfer here is therefore bounded and failure-tolerant: a +// bootloader that spins forever waiting on an absent panel is far worse +// than one that shows no picture. + +#ifdef DISPLAY_PIN_SDA +#include "nrf_twim.h" + +#ifndef DISPLAY_I2C_ADDR +#define DISPLAY_I2C_ADDR 0x3C +#endif + +#ifndef DISPLAY_COL_OFFSET +// SH1106 and friends have 132 columns of GDDRAM behind a 128-column glass, +// centred, so the visible window starts at column 2. +#define DISPLAY_COL_OFFSET 0 +#endif + +// Total addressable columns, which may exceed the visible width. Only the +// blanking pass cares: columns outside the glass still hold undefined data +// at power-up, and on a panel whose offset is slightly off they show. +#ifndef DISPLAY_GDDRAM_WIDTH +#define DISPLAY_GDDRAM_WIDTH (DISPLAY_COL_OFFSET + DISPLAY_WIDTH) +#endif + +#ifndef DISPLAY_CONTRAST +#define DISPLAY_CONTRAST 0xCF +#endif + +// Milliseconds to let the panel's rail come up before we talk to it. +#ifndef DISPLAY_POWER_ON_MS +#define DISPLAY_POWER_ON_MS 100 +#endif + +// TWIM0 is the same peripheral ID as SPIM0, which the TFT path uses; boards.h +// rejects a board.h that asks for both. +#define _twim NRF_TWIM0 + +// I2C control bytes: Co=0, D/C# selects the command or data stream. +#define OLED_CTRL_CMD 0x00 +#define OLED_CTRL_DATA 0x40 + +// Bound every wait. At 400 kHz a 129-byte page is ~2.9 ms; this is a couple +// of orders of magnitude beyond that, so it only ever fires on a dead bus. +#define TWI_GUARD_LOOPS 2000000UL + +static bool _display_present = false; + +static void oled_write_page(uint8_t page, uint8_t col, uint8_t const* buf, size_t nbytes); + +// Blocking write of a whole I2C transaction. Returns false on NACK, bus +// error, or timeout, leaving the peripheral idle and ready for the next try. +static bool twi_write(uint8_t const* buf, size_t len) { + nrf_twim_event_clear(_twim, NRF_TWIM_EVENT_STOPPED); + nrf_twim_event_clear(_twim, NRF_TWIM_EVENT_ERROR); + (void) nrf_twim_errorsrc_get_and_clear(_twim); + + nrf_twim_tx_buffer_set(_twim, buf, len); + nrf_twim_shorts_set(_twim, NRF_TWIM_SHORT_LASTTX_STOP_MASK); + nrf_twim_task_trigger(_twim, NRF_TWIM_TASK_STARTTX); + + uint32_t guard = TWI_GUARD_LOOPS; + while (!nrf_twim_event_check(_twim, NRF_TWIM_EVENT_STOPPED) && + !nrf_twim_event_check(_twim, NRF_TWIM_EVENT_ERROR) && + --guard) {} + + bool const ok = (guard != 0) && !nrf_twim_event_check(_twim, NRF_TWIM_EVENT_ERROR); + + if (!ok) { + // An address NACK or a stuck bus can leave the transfer half-done. + nrf_twim_task_trigger(_twim, NRF_TWIM_TASK_STOP); + guard = TWI_GUARD_LOOPS; + while (!nrf_twim_event_check(_twim, NRF_TWIM_EVENT_STOPPED) && --guard) {} + (void) nrf_twim_errorsrc_get_and_clear(_twim); + } + + return ok; +} + +// EasyDMA can only read from RAM, so command lists must live on the stack, +// never in flash. +static bool oled_cmds(uint8_t* buf, size_t len) { + buf[0] = OLED_CTRL_CMD; + return twi_write(buf, len); +} + +bool board_display_init(void) { + _display_present = false; + +#if defined(DISPLAY_VSENSOR_PIN) && DISPLAY_VSENSOR_PIN >= 0 + nrf_gpio_cfg_output(DISPLAY_VSENSOR_PIN); + nrf_gpio_pin_write(DISPLAY_VSENSOR_PIN, DISPLAY_VSENSOR_ON); + NRFX_DELAY_MS(DISPLAY_POWER_ON_MS); +#endif + + // Open-drain with the internal pull-ups engaged. Boards that carry their + // own pull-ups are unaffected; boards with an empty socket get a bus that + // idles high, so a missing panel NACKs cleanly instead of hanging. + nrf_gpio_cfg(DISPLAY_PIN_SDA, NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_INPUT_CONNECT, + NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_S0D1, NRF_GPIO_PIN_NOSENSE); + nrf_gpio_cfg(DISPLAY_PIN_SCL, NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_INPUT_CONNECT, + NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_S0D1, NRF_GPIO_PIN_NOSENSE); + + nrf_twim_pins_set(_twim, DISPLAY_PIN_SCL, DISPLAY_PIN_SDA); + nrf_twim_frequency_set(_twim, NRF_TWIM_FREQ_400K); + nrf_twim_address_set(_twim, DISPLAY_I2C_ADDR); + nrf_twim_enable(_twim); + + // Display-off doubles as the presence probe: if nobody ACKs the address + // there is no panel, and we leave the bus alone from here on. + uint8_t probe[] = { OLED_CTRL_CMD, 0xAE }; + if (!oled_cmds(probe, sizeof(probe))) { + nrf_twim_disable(_twim); + return false; + } + + uint8_t init[] = { + OLED_CTRL_CMD, + 0xD5, 0x80, // clock divide / oscillator frequency + 0xA8, (uint8_t)(DISPLAY_HEIGHT - 1), // multiplex ratio + 0xD3, 0x00, // display offset + 0x40, // start line 0 +#ifdef DISPLAY_CONTROLLER_SH1106 + 0xAD, 0x8B, // SH1106 DC-DC converter on +#else + 0x8D, 0x14, // SSD1306 charge pump on +#endif + // Page addressing mode. Both parts support it, and it lets one + // draw-page routine cover the SH1106, which has no horizontal mode. + 0x20, 0x02, + 0xA1, // segment remap: column 127 -> SEG0 + 0xC8, // COM scan direction: reversed +#if DISPLAY_HEIGHT >= 64 + 0xDA, 0x12, // COM pins: alternative, no remap +#else + 0xDA, 0x02, // COM pins: sequential, no remap +#endif + 0x81, DISPLAY_CONTRAST, + 0xD9, 0xF1, // pre-charge period + 0xDB, 0x40, // VCOMH deselect level + 0xA4, // resume from RAM contents + 0xA6, // non-inverted + 0x2E, // scrolling off + }; + if (!oled_cmds(init, sizeof(init))) { + nrf_twim_disable(_twim); + return false; + } + + _display_present = true; + + // GDDRAM contents are undefined at power-up, and the panel is still off + // from the probe. Blank the RAM first so switching on cannot flash noise. + uint8_t blank[DISPLAY_GDDRAM_WIDTH]; + memset(blank, 0, sizeof(blank)); + for (uint8_t page = 0; page < DISPLAY_HEIGHT / 8; ++page) { + oled_write_page(page, 0, blank, sizeof(blank)); + } + + // Display on. The probe above sent 0xAE; without this the panel stays + // dark no matter what we write to it. + uint8_t on[] = { OLED_CTRL_CMD, 0xAF }; + if (!oled_cmds(on, sizeof(on))) { + _display_present = false; + nrf_twim_disable(_twim); + return false; + } + + return true; +} + +void board_display_teardown(void) { + if (_display_present) { + uint8_t off[] = { OLED_CTRL_CMD, 0xAE }; + (void) oled_cmds(off, sizeof(off)); + _display_present = false; + } + nrf_twim_disable(_twim); +} + +static void oled_write_page(uint8_t page, uint8_t col, uint8_t const* buf, size_t nbytes) { + if (!_display_present || nbytes > DISPLAY_GDDRAM_WIDTH) return; + + // Page addressing: set the page, then the low and high nibbles of the + // starting column, then stream the row. + uint8_t addr[] = { + OLED_CTRL_CMD, + (uint8_t)(0xB0 | (page & 0x0F)), + (uint8_t)(0x00 | (col & 0x0F)), + (uint8_t)(0x10 | ((col >> 4) & 0x0F)), + }; + if (!oled_cmds(addr, sizeof(addr))) return; + + // The control byte has to be contiguous with the pixels for EasyDMA, and + // the caller's buffer has no room in front of it. + static uint8_t page_buf[1 + DISPLAY_GDDRAM_WIDTH]; + page_buf[0] = OLED_CTRL_DATA; + memcpy(page_buf + 1, buf, nbytes); + (void) twi_write(page_buf, nbytes + 1); +} + +void board_display_draw_page(uint8_t page, uint8_t const* buf, size_t nbytes) { + if (nbytes > DISPLAY_WIDTH) return; + oled_write_page(page, DISPLAY_COL_OFFSET, buf, nbytes); +} + +#endif // DISPLAY_PIN_SDA diff --git a/src/boards/boards.h b/src/boards/boards.h index e85f76d33..4c1d91f77 100644 --- a/src/boards/boards.h +++ b/src/boards/boards.h @@ -117,12 +117,38 @@ bool is_ota(void); //--------------------------------------------------------------------+ // Display //--------------------------------------------------------------------+ -#ifdef DISPLAY_PIN_SCK -void board_display_init(void); +// A board opts into the DFU screens by naming a display bus in board.h: +// DISPLAY_PIN_SCK for the SPI TFT path, DISPLAY_PIN_SDA for the I2C mono +// path. They are mutually exclusive: TWIM0 and SPIM0 are the same +// peripheral ID and cannot both be enabled. +#if defined(DISPLAY_PIN_SCK) && defined(DISPLAY_PIN_SDA) +#error "board.h defines both DISPLAY_PIN_SCK and DISPLAY_PIN_SDA - pick one display bus" +#endif + +#if defined(DISPLAY_PIN_SCK) || defined(DISPLAY_PIN_SDA) +#define BOARD_HAS_DISPLAY 1 +#endif + +// Mono (1bpp paged) panels vs 16-bit colour TFTs. +#if defined(DISPLAY_CONTROLLER_SSD1306) || defined(DISPLAY_CONTROLLER_SH1106) +#define DISPLAY_MONO 1 +#endif + +#ifdef BOARD_HAS_DISPLAY +// Returns false if no panel answered, in which case nothing else here may be +// called. Only the I2C path can fail: an OLED is a plug-in module on some +// boards, so its absence is a normal condition, not an error. +bool board_display_init(void); void board_display_teardown(void); -void board_display_draw_line(uint16_t y, uint8_t const* buf, size_t nbytes); void screen_draw_drag(void); void screen_draw_ble(void); + +#ifdef DISPLAY_MONO +// One 8-row page, DISPLAY_WIDTH bytes, LSB = topmost row of the page. +void board_display_draw_page(uint8_t page, uint8_t const* buf, size_t nbytes); +#else +void board_display_draw_line(uint16_t y, uint8_t const* buf, size_t nbytes); +#endif #endif //--------------------------------------------------------------------+ diff --git a/src/boards/heltec_t096/board.h b/src/boards/heltec_t096/board.h index cf52e1ae6..62eeae709 100644 --- a/src/boards/heltec_t096/board.h +++ b/src/boards/heltec_t096/board.h @@ -100,6 +100,9 @@ #define BLE_OTA_Y 38 #define DRAG 34 #define DRAGX 4 +// Three 32px icons have to fit in 160px: the default pendriveLogo_X of +// 129 puts the last one at x=133..164, off the right edge of the panel. +#define pendriveLogo_X 124 //--------------------------------------------------------------------+ // BLE OTA diff --git a/src/boards/heltec_t1/board.h b/src/boards/heltec_t1/board.h index a8ba1be2e..1bd71e146 100644 --- a/src/boards/heltec_t1/board.h +++ b/src/boards/heltec_t1/board.h @@ -99,6 +99,9 @@ #define BLE_OTA_Y 38 #define DRAG 34 #define DRAGX 4 +// Three 32px icons have to fit in 160px: the default pendriveLogo_X of +// 129 puts the last one at x=133..164, off the right edge of the panel. +#define pendriveLogo_X 124 //--------------------------------------------------------------------+ // BLE OTA diff --git a/src/boards/promicro_nrf52840/board.h b/src/boards/promicro_nrf52840/board.h index 2a85aaea6..3c968f264 100644 --- a/src/boards/promicro_nrf52840/board.h +++ b/src/boards/promicro_nrf52840/board.h @@ -44,6 +44,30 @@ #define BUTTON_2 _PINNUM(1, 0) #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - optional SSD1306/SH1106 OLED (commonly user-wired) + * + * Pins follow the default Meshtastic promicro variant. The EASYPROMICRO + * arrangement puts SCL on P1.06 instead; on such a board the probe simply + * finds nothing and the screen stays blank, which is harmless. + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 +#define DISPLAY_I2C_ADDR 0x3C + +#define DISPLAY_PIN_SDA _PINNUM(1, 4) +#define DISPLAY_PIN_SCL _PINNUM(0, 11) + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// Not yet verified on hardware. Offset 0 is right for a true 128-column +// SSD1306; a 132-column SH1106 will sit 2px left of centre, in which case set +// DISPLAY_COL_OFFSET to 2. Blanking spans 132 either way so no uncovered +// column can show power-up garbage. +#define DISPLAY_COL_OFFSET 0 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "ProMicro" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/boards/wio_tracker_l1/board.h b/src/boards/wio_tracker_l1/board.h index 67670f749..ba0c566d1 100644 --- a/src/boards/wio_tracker_l1/board.h +++ b/src/boards/wio_tracker_l1/board.h @@ -44,6 +44,26 @@ #define BUTTON_2 _PINNUM(0, 8) #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - SSD1306 OLED (variant declares HAS_SCREEN 1, USE_SSD1306 1) + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 +#define DISPLAY_I2C_ADDR 0x3C + +#define DISPLAY_PIN_SDA _PINNUM(0, 9) +#define DISPLAY_PIN_SCL _PINNUM(0, 10) + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// Not yet verified on hardware. Offset 0 is right for a true 128-column +// SSD1306; a 132-column SH1106 will sit 2px left of centre, in which case set +// DISPLAY_COL_OFFSET to 2. Blanking spans 132 either way so no uncovered +// column can show power-up garbage. +#define DISPLAY_COL_OFFSET 0 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "TRACKER L1" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/boards/wiscore_rak3401/board.h b/src/boards/wiscore_rak3401/board.h index ae5c86bee..0224b920d 100644 --- a/src/boards/wiscore_rak3401/board.h +++ b/src/boards/wiscore_rak3401/board.h @@ -46,6 +46,32 @@ #define BUTTON_2 _PINNUM(0, 8) // change default 0.0 to 0.8, add by Michael. This pin is not connected on 4631. #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - optional SSD1306 OLED on a WisBlock IO slot + * + * Pin- and rail-identical to the RAK4631; the panel is a plug-in module, so + * board_display_init() probes for it and a board with an empty slot behaves + * exactly as it did before. + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 +#define DISPLAY_I2C_ADDR 0x3C + +// WB_I2C1, shared by the sensor and IO slots. +#define DISPLAY_PIN_SDA _PINNUM(0, 13) +#define DISPLAY_PIN_SCL _PINNUM(0, 14) + +// Switchable 3V3_S rail (WB_IO2), off out of reset. +#define DISPLAY_VSENSOR_PIN _PINNUM(0, 34) +#define DISPLAY_VSENSOR_ON 1 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// Untested here, but the RAK4631's WisBlock OLED measured as 132-column. +#define DISPLAY_COL_OFFSET 2 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "RAK3401" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/boards/wiscore_rak4631_board/board.h b/src/boards/wiscore_rak4631_board/board.h index 7886cc334..880d27e84 100644 --- a/src/boards/wiscore_rak4631_board/board.h +++ b/src/boards/wiscore_rak4631_board/board.h @@ -46,6 +46,36 @@ #define BUTTON_2 _PINNUM(0, 8) // change default 0.0 to 0.8, add by Michael. This pin is not connected on 4631. #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - optional SSD1306 OLED (RAK1921) on a WisBlock IO slot + * + * The panel is a plug-in module, so this is best-effort: board_display_init() + * probes the I2C address and reports absence rather than blocking, and a + * board with an empty slot behaves exactly as it did before. + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 + +// WB_I2C1, shared by the sensor and IO slots. +#define DISPLAY_PIN_SDA _PINNUM(0, 13) +#define DISPLAY_PIN_SCL _PINNUM(0, 14) +#define DISPLAY_I2C_ADDR 0x3C + +// The slots are fed from the switchable 3V3_S rail (WB_IO2), which is off +// out of reset - the bootloader has to raise it before the panel answers. +#define DISPLAY_VSENSOR_PIN _PINNUM(0, 34) +#define DISPLAY_VSENSOR_ON 1 + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// This panel has 132 columns of GDDRAM behind 128px of glass, so the visible +// window starts at column 2. Confirmed on hardware: with an offset of 0 the +// image sat 2px left of the glass and the two uncovered columns on the right +// showed power-up garbage. +#define DISPLAY_COL_OFFSET 2 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "RAK4631" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/boards/xiao_nrf52840_ble/board.h b/src/boards/xiao_nrf52840_ble/board.h index 9f4c45e6e..4b9b9c94d 100644 --- a/src/boards/xiao_nrf52840_ble/board.h +++ b/src/boards/xiao_nrf52840_ble/board.h @@ -46,6 +46,30 @@ #define BUTTON_2 _PINNUM(0, 3) #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - optional SSD1306 OLED (user-wired) + * + * The XIAO variant offers three I2C pinouts depending on build flags; these + * are the kit default (NFC pins). On a board wired to D4/D5 or D6/D7 the + * probe finds nothing and the screen stays blank, which is harmless. + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 +#define DISPLAY_I2C_ADDR 0x3C + +#define DISPLAY_PIN_SDA _PINNUM(0, 30) +#define DISPLAY_PIN_SCL _PINNUM(0, 31) + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// Not yet verified on hardware. Offset 0 is right for a true 128-column +// SSD1306; a 132-column SH1106 will sit 2px left of centre, in which case set +// DISPLAY_COL_OFFSET to 2. Blanking spans 132 either way so no uncovered +// column can show power-up garbage. +#define DISPLAY_COL_OFFSET 0 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "XIAO" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/boards/xiao_nrf52840_ble_sense/board.h b/src/boards/xiao_nrf52840_ble_sense/board.h index e512c1b78..408bf2576 100644 --- a/src/boards/xiao_nrf52840_ble_sense/board.h +++ b/src/boards/xiao_nrf52840_ble_sense/board.h @@ -46,6 +46,30 @@ #define BUTTON_2 _PINNUM(0, 3) #define BUTTON_PULL NRF_GPIO_PIN_PULLUP +/*------------------------------------------------------------------*/ +/* DISPLAY - optional SSD1306 OLED (user-wired) + * + * The XIAO variant offers three I2C pinouts depending on build flags; these + * are the kit default (NFC pins). On a board wired to D4/D5 or D6/D7 the + * probe finds nothing and the screen stays blank, which is harmless. + *------------------------------------------------------------------*/ +#define DISPLAY_CONTROLLER_SSD1306 +#define DISPLAY_I2C_ADDR 0x3C + +#define DISPLAY_PIN_SDA _PINNUM(0, 30) +#define DISPLAY_PIN_SCL _PINNUM(0, 31) + +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 64 +// Not yet verified on hardware. Offset 0 is right for a true 128-column +// SSD1306; a 132-column SH1106 will sit 2px left of centre, in which case set +// DISPLAY_COL_OFFSET to 2. Blanking spans 132 either way so no uncovered +// column can show power-up garbage. +#define DISPLAY_COL_OFFSET 0 +#define DISPLAY_GDDRAM_WIDTH 132 + +#define DISPLAY_TITLE "XIAO Sense" + //--------------------------------------------------------------------+ // BLE OTA //--------------------------------------------------------------------+ diff --git a/src/images.c b/src/images.c index 2f2767fdf..a634f2b2c 100644 --- a/src/images.c +++ b/src/images.c @@ -1,6 +1,6 @@ #include "boards.h" -#if defined(DISPLAY_PIN_SCK) +#ifdef BOARD_HAS_DISPLAY #include diff --git a/src/main.c b/src/main.c index bd9971379..3c8f5a41e 100644 --- a/src/main.c +++ b/src/main.c @@ -293,9 +293,10 @@ static void check_dfu_mode(void) { // Enter DFU mode accordingly to input if (dfu_start || !valid_app) { if (_ota_dfu) { - #ifdef DISPLAY_PIN_SCK - board_display_init(); - screen_draw_ble(); + #ifdef BOARD_HAS_DISPLAY + if (board_display_init()) { + screen_draw_ble(); + } #endif led_state(STATE_BLE_DISCONNECTED); if (!_sd_inited) mbr_init_sd(); diff --git a/src/screen.c b/src/screen.c index a1f82e44f..fb637997b 100644 --- a/src/screen.c +++ b/src/screen.c @@ -24,7 +24,58 @@ #include "boards.h" -#if defined(DISPLAY_PIN_SCK) +#ifdef BOARD_HAS_DISPLAY + +// Defaults for 1bpp mono panels. The stock geometry below is sized for a +// 240x135 TFT and does not fit a typical 128x64 OLED, so give mono boards a +// four-line layout (title / mode / version / banner) out of the box. A board +// only has to declare its bus and title; anything here can still be +// overridden per board. +#ifdef DISPLAY_MONO +#if DISPLAY_HEIGHT < 96 +// Three 32x32 icons need 96x32; that plus four text lines does not fit. +#define DISPLAY_NO_DRAG_ICONS +#define NOLABELS +#endif + +#ifndef FONT_SIZE_LARGE +#define FONT_SIZE_LARGE 2 +#endif +#ifndef BANNER_TEXT +#define BANNER_TEXT "meshtastic.org" +#endif + +// The colour bars are dark in mono, so bar 1 simply spans the panel and the +// other two collapse; this also keeps drawBar inside the frame buffer. +#ifndef SCREEN_BAR1_Y +#define SCREEN_BAR1_Y 0 +#endif +#ifndef SCREEN_BAR1_H +#define SCREEN_BAR1_H DISPLAY_HEIGHT +#endif +#ifndef SCREEN_BAR2_H +#define SCREEN_BAR2_H 0 +#endif +#ifndef SCREEN_BAR3_H +#define SCREEN_BAR3_H 0 +#endif + +#ifndef DISPLAY_TITLE_Y +#define DISPLAY_TITLE_Y 0 +#endif +#ifndef DRAG_TEXT_Y +#define DRAG_TEXT_Y 22 +#endif +#ifndef BLE_OTA_Y +#define BLE_OTA_Y 22 +#endif +#ifndef UF2_VERSION_BASE_Y +#define UF2_VERSION_BASE_Y 44 +#endif +#ifndef BANNER_TEXT_Y +#define BANNER_TEXT_Y 55 +#endif +#endif // DISPLAY_MONO #ifndef BANNER_TEXT #define BANNER_TEXT "meshtastic.org - OTAFIX by oltaco" @@ -121,6 +172,13 @@ const uint16_t palette[] = { #ifndef BLE_OTA_Y #define BLE_OTA_Y 65 #endif +// Mode line on the drag screen, for panels too short for the 32px icon row. +#ifndef DRAG_TEXT_Y +#define DRAG_TEXT_Y BLE_OTA_Y +#endif +#ifndef DRAG_TEXT +#define DRAG_TEXT "USB UF2" +#endif #ifndef FONT_SIZE_LARGE #define FONT_SIZE_LARGE 4 #endif @@ -129,6 +187,11 @@ const uint16_t palette[] = { // TODO only buffer partial screen to save SRAM // ESP32s2 can only statically allocated DRAM up to 160KB. // the remaining 160KB can only be allocated at runtime as heap. +#ifdef DISPLAY_MONO +_Static_assert(DISPLAY_HEIGHT % 8 == 0, + "mono panels are addressed in 8-row pages; DISPLAY_HEIGHT must be a multiple of 8"); +#endif + static uint8_t frame_buf[DISPLAY_WIDTH * DISPLAY_HEIGHT]; //static uint8_t* frame_buf; @@ -155,12 +218,19 @@ static void printch(int x, int y, int color, const uint8_t* fnt, int size) { } } +#ifndef DISPLAY_NO_DRAG_ICONS // print icon static void printicon(int x, int y, int color, const uint8_t* icon) { int w = *icon++; int h = *icon++; int sz = *icon++; + // The decode loop below writes w*h bytes straight into frame_buf with no + // clipping of its own, so anything that would land outside is dropped. + if (x < 0 || y < 0 || x + w > DISPLAY_WIDTH || y + h > DISPLAY_HEIGHT) { + return; + } + uint8_t mask = 0x80; int runlen = 0; int runbit = 0; @@ -201,6 +271,7 @@ static void printicon(int x, int y, int color, const uint8_t* icon) { } } } +#endif // DISPLAY_NO_DRAG_ICONS static void print(int x, int y, int color, const char* text, int size) { // Glyphs are written straight into frame_buf with no bounds check of @@ -231,6 +302,39 @@ static inline void print_centered(int y, int color, const char* text, int size) // //--------------------------------------------------------------------+ +#ifdef DISPLAY_MONO + +// One bit per pixel, so the 16-entry palette collapses to "lights a pixel or +// does not". The layout above only ever draws foreground in white (text, +// icons) and purple (the version string); the three background bars use +// colours that stay dark. That is what lets the colour layout survive the +// conversion without being redrawn for mono. +#define MONO_LIT_MASK ((1u << COLOR_WHITE) | (1u << COLOR_PURPLE)) + +static void draw_screen(uint8_t const* fb) { + // SSD1306/SH1106 GDDRAM is paged: one byte spans 8 rows of a single + // column, LSB topmost. frame_buf is already column-major, so each output + // byte gathers 8 contiguous bytes of it. + for (int page = 0; page < DISPLAY_HEIGHT / 8; ++page) { + uint8_t line[DISPLAY_WIDTH]; + + for (int x = 0; x < DISPLAY_WIDTH; ++x) { + uint8_t const* col = fb + x * DISPLAY_HEIGHT + page * 8; + uint8_t bits = 0; + for (int bit = 0; bit < 8; ++bit) { + if (MONO_LIT_MASK & (1u << (col[bit] & 0xf))) { + bits |= (uint8_t) (1u << bit); + } + } + line[x] = bits; + } + + board_display_draw_page((uint8_t) page, line, sizeof(line)); + } +} + +#else + static void draw_screen(uint8_t const* fb) { uint8_t const* p = fb; for (int y = 0; y < DISPLAY_WIDTH; ++y) { @@ -246,8 +350,23 @@ static void draw_screen(uint8_t const* fb) { } } +#endif + // draw color bar static void drawBar(int y, int h, int color) { + // Clamp to the panel: the default bar geometry is sized for a 240x135 + // screen and would run off the end of a column on a shorter one. + if (y < 0) { + h += y; + y = 0; + } + if (y >= DISPLAY_HEIGHT || h <= 0) { + return; + } + if (y + h > DISPLAY_HEIGHT) { + h = DISPLAY_HEIGHT - y; + } + for (int x = 0; x < DISPLAY_WIDTH; ++x) { memset(frame_buf + x * DISPLAY_HEIGHT + y, color, h); } @@ -264,6 +383,12 @@ void screen_draw_drag(void) { print_centered(UF2_VERSION_BASE_Y, COLOR_PURPLE, UF2_VERSION_BASE, 1); print_centered(BANNER_TEXT_Y, COLOR_WHITE, BANNER_TEXT, 1); +#ifdef DISPLAY_NO_DRAG_ICONS + // The icon row is three 32x32 glyphs, 96px wide and 32px tall before any + // spacing. A 128x64 panel cannot carry that and the four text lines, so + // name the mode instead. + print_centered(DRAG_TEXT_Y, COLOR_WHITE, DRAG_TEXT, FONT_SIZE_LARGE); +#else #ifndef DRAG #define DRAG 70 #endif @@ -277,6 +402,7 @@ void screen_draw_drag(void) { print(22, DRAG - 12, COLOR_WHITE, "firmware.uf2", 1); print(160, DRAG - 12, COLOR_WHITE, UF2_VOLUME_LABEL, 1); #endif // NOLABELS +#endif // DISPLAY_NO_DRAG_ICONS draw_screen(frame_buf); } diff --git a/src/usb/usb.c b/src/usb/usb.c index 30d91c0d4..5fa008077 100644 --- a/src/usb/usb.c +++ b/src/usb/usb.c @@ -97,9 +97,10 @@ void usb_init(bool cdc_only) { uf2_init(); tusb_init(); - #ifdef DISPLAY_PIN_SCK - board_display_init(); - screen_draw_drag(); + #ifdef BOARD_HAS_DISPLAY + if (board_display_init()) { + screen_draw_drag(); + } #endif }