From 530964d9d300d2a4da5e1e432c6f9f3b12288db7 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Mon, 21 Sep 2026 16:02:39 +0800 Subject: [PATCH 1/6] feat: add RC32 and RC52 rotary encoder example --- README.md | 5 + docs/CONTEXT.md | 10 + examples/Rotary_Encoder/Rotary_Encoder.ino | 317 +++++++++++++++++++++ src/RadioCoreBoardConfig.h | 2 + src/boards/heltec_rc32.h | 10 + src/boards/heltec_rc52.h | 10 + src/boards/heltec_rcc6.h | 3 + 7 files changed, 357 insertions(+) create mode 100644 examples/Rotary_Encoder/Rotary_Encoder.ino diff --git a/README.md b/README.md index 37369ca..cb7badb 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ compile time. | `VO01_Read` | Estimate VO01 gas concentration and trend | [Guide](examples/VO01_Read/README.md) | | `Soil_Moisture_Read` | Read and calibrate an analog soil moisture sensor | [Guide](examples/Soil_Moisture_Read/README.md) | | `NV3001B_Display` | Exercise the 128-by-220 color TFT | [Guide](examples/NV3001B_Display/README.md) | +| `Rotary_Encoder` | Report rotary direction, signed count, and user-button presses on RC32 and RC52 | [Sketch](examples/Rotary_Encoder/Rotary_Encoder.ino) | | `WS2812` | Run a three-color breathing animation | [Sketch](examples/WS2812/WS2812.ino) | | `Buzzer_Control` | Play and repeat an RTTTL melody through the board-designated buzzer output | [Sketch](examples/Buzzer_Control/Buzzer_Control.ino) | | `Motor_Control` | Drive two direction inputs on an external motor driver | [Sketch](examples/Motor_Control/Motor_Control.ino) | @@ -85,6 +86,10 @@ A new board declares only the capabilities it supports: `CO01_Read`, and `Soil_Moisture_Read`; CO01 also requires its control pin. - NV3001B pins, active levels, SPI frequency, and one supported display transport for `NV3001B_Display`. +- A rotary encoder I2C address and phase masks for `Rotary_Encoder`, plus an + independently declared user-button pin, active level, and input mode. The + current RC32 and RC52 mappings share the configured Sensor I2C bus and power + rail. - A data pin for `WS2812`, a buzzer output pin and any shared peripheral power control required by `Buzzer_Control`, two motor-driver inputs for `Motor_Control`, a relay output and active level for `Relay_Control`, and two diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index aa3fba0..1ade116 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -90,6 +90,16 @@ The board-designated digital signal connected to the logic input of an external relay module or driver. _Avoid_: relay coil output, load power output +**Rotary encoder input**: +The board-designated two-channel user control whose rotation produces previous +or next steps. +_Avoid_: rotary button, generic GPIO input + +**User button input**: +The board-designated momentary digital input reported independently from +rotary encoder movement. +_Avoid_: rotary encoder press, enter key + **Servo control interface**: The board-designated pair of servo PWM signals and enable signal used to connect an external dual-servo driver such as the RS-SV01. diff --git a/examples/Rotary_Encoder/Rotary_Encoder.ino b/examples/Rotary_Encoder/Rotary_Encoder.ino new file mode 100644 index 0000000..501b5a6 --- /dev/null +++ b/examples/Rotary_Encoder/Rotary_Encoder.ino @@ -0,0 +1,317 @@ +/* + * Reads the onboard rotary encoder on Heltec RC32 and RC52 through the + * TCA6408 I2C expander. RC32 uses Sensor I2C on GPIO21/GPIO18 with shared + * peripheral power on GPIO46; RC52 uses P1.11/P0.02 with shared peripheral + * power on P0.12. Turning the encoder prints Previous/Next and a signed count. + * + * The board-designated user button is reported independently from rotation: + * GPIO0 on RC32 and P1.10 on RC52. It is not treated as an encoder push event. + * Avoid holding RC32 GPIO0 low while resetting or uploading because it is also + * a boot-strapping pin. + */ +#include +#include +#include + +#if !RADIOCORE_HAS_ROTARY_ENCODER +#error "Rotary_Encoder requires a RadioCore rotary encoder board configuration." +#elif !RADIOCORE_HAS_SENSOR_I2C +#error "Rotary_Encoder requires a RadioCore Sensor I2C board configuration." +#else + +namespace { + +constexpr uint8_t kInputRegister = 0x00; +constexpr uint8_t kPolarityRegister = 0x02; +constexpr uint8_t kConfigRegister = 0x03; +constexpr uint8_t kRotaryMask = + RADIOCORE_ROTARY_ENCODER_PHASE_A_MASK | + RADIOCORE_ROTARY_ENCODER_PHASE_B_MASK; +constexpr uint32_t kRotaryEventIntervalMs = 5; +constexpr uint32_t kEncoderRetryIntervalMs = 1000; +constexpr uint32_t kButtonDebounceMs = 20; + +enum class RotaryEvent : uint8_t { + None, + Previous, + Next, +}; + +bool encoderReady = false; +bool encoderWasReady = false; +bool activeLowPhase = false; +uint8_t inputState = kRotaryMask; +uint32_t lastRotaryEventTime = 0; +uint32_t nextEncoderRetryTime = 0; +int32_t rotaryCount = 0; + +#if RADIOCORE_HAS_USER_BUTTON +uint8_t buttonRawState = HIGH; +uint8_t buttonStableState = HIGH; +uint32_t buttonChangedAt = 0; +#endif + +bool timeReached(uint32_t now, uint32_t deadline) +{ + return static_cast(now - deadline) >= 0; +} + +void enableSharedPower() +{ +#if RADIOCORE_HAS_SENSOR_POWER_CTRL + pinMode(RADIOCORE_SENSOR_POWER_CTRL_PIN, OUTPUT); + digitalWrite( + RADIOCORE_SENSOR_POWER_CTRL_PIN, + RADIOCORE_SENSOR_POWER_ON); + delay(RADIOCORE_SENSOR_POWER_WARMUP_MS); +#endif +} + +bool beginSensorI2C() +{ +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_WIFI_KIT_SERIES) + return RADIOCORE_SENSOR_I2C_INSTANCE.begin( + RADIOCORE_SENSOR_I2C_SDA, + RADIOCORE_SENSOR_I2C_SCL, + RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY); +#elif defined(ARDUINO_ARCH_HELTEC_NRF52) + RADIOCORE_SENSOR_I2C_INSTANCE.setPins( + RADIOCORE_SENSOR_I2C_SDA, + RADIOCORE_SENSOR_I2C_SCL); + RADIOCORE_SENSOR_I2C_INSTANCE.begin(); + RADIOCORE_SENSOR_I2C_INSTANCE.setClock( + RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY); + return true; +#else +#error "Rotary_Encoder supports only the configured ESP32 and nRF52 I2C API types." +#endif +} + +bool writeExpanderRegister(uint8_t reg, uint8_t value) +{ + RADIOCORE_SENSOR_I2C_INSTANCE.beginTransmission( + RADIOCORE_ROTARY_ENCODER_I2C_ADDRESS); + RADIOCORE_SENSOR_I2C_INSTANCE.write(reg); + RADIOCORE_SENSOR_I2C_INSTANCE.write(value); + return RADIOCORE_SENSOR_I2C_INSTANCE.endTransmission() == 0; +} + +bool readExpanderRegister(uint8_t reg, uint8_t& value) +{ + RADIOCORE_SENSOR_I2C_INSTANCE.beginTransmission( + RADIOCORE_ROTARY_ENCODER_I2C_ADDRESS); + RADIOCORE_SENSOR_I2C_INSTANCE.write(reg); + if (RADIOCORE_SENSOR_I2C_INSTANCE.endTransmission(false) != 0) { + return false; + } + + if (RADIOCORE_SENSOR_I2C_INSTANCE.requestFrom( + RADIOCORE_ROTARY_ENCODER_I2C_ADDRESS, + static_cast(1)) != 1) { + return false; + } + + value = RADIOCORE_SENSOR_I2C_INSTANCE.read(); + return true; +} + +bool updateExpanderBits(uint8_t reg, uint8_t setMask, uint8_t clearMask) +{ + uint8_t currentValue = 0; + if (!readExpanderRegister(reg, currentValue)) { + return false; + } + + const uint8_t newValue = (currentValue | setMask) & ~clearMask; + return newValue == currentValue || writeExpanderRegister(reg, newValue); +} + +bool beginEncoder() +{ + encoderReady = false; + activeLowPhase = false; + inputState = kRotaryMask; + + if (!updateExpanderBits(kPolarityRegister, 0, kRotaryMask) || + !updateExpanderBits(kConfigRegister, kRotaryMask, 0)) { + return false; + } + + uint8_t state = 0; + if (!readExpanderRegister(kInputRegister, state)) { + return false; + } + + inputState = state & kRotaryMask; + encoderReady = true; + return true; +} + +bool tryBeginEncoder(uint32_t now) +{ + if (!beginSensorI2C() || !beginEncoder()) { + encoderReady = false; + nextEncoderRetryTime = now + kEncoderRetryIntervalMs; + Serial.println(F("Rotary encoder unavailable; retrying in 1 s.")); + return false; + } + + Serial.println( + encoderWasReady ? F("Rotary encoder recovered.") + : F("Rotary encoder ready.")); + encoderWasReady = true; + return true; +} + +RotaryEvent handleTransition(uint8_t newState, uint32_t now) +{ + const uint8_t changed = (inputState ^ newState) & kRotaryMask; + const bool aLow = + (newState & RADIOCORE_ROTARY_ENCODER_PHASE_A_MASK) == 0; + const bool bLow = + (newState & RADIOCORE_ROTARY_ENCODER_PHASE_B_MASK) == 0; + RotaryEvent event = RotaryEvent::None; + + if (!aLow && !bLow) { + activeLowPhase = false; + } + + if (!activeLowPhase && + (changed & RADIOCORE_ROTARY_ENCODER_PHASE_A_MASK) && + aLow && !bLow) { + event = RotaryEvent::Previous; + activeLowPhase = true; + } else if (!activeLowPhase && + (changed & RADIOCORE_ROTARY_ENCODER_PHASE_B_MASK) && + bLow && !aLow) { + event = RotaryEvent::Next; + activeLowPhase = true; + } + + if (event == RotaryEvent::None && !activeLowPhase && + (changed & RADIOCORE_ROTARY_ENCODER_PHASE_A_MASK) && + !aLow && bLow) { + event = RotaryEvent::Previous; + } + + if (event == RotaryEvent::None && !activeLowPhase && + (changed & RADIOCORE_ROTARY_ENCODER_PHASE_B_MASK) && + !bLow && aLow) { + event = RotaryEvent::Next; + } + + if (event == RotaryEvent::None || + now - lastRotaryEventTime < kRotaryEventIntervalMs) { + return RotaryEvent::None; + } + + lastRotaryEventTime = now; + return event; +} + +RotaryEvent pollEncoder(uint32_t now, bool& readSucceeded) +{ + uint8_t newState = 0; + readSucceeded = readExpanderRegister(kInputRegister, newState); + if (!readSucceeded) { + return RotaryEvent::None; + } + + newState &= kRotaryMask; + const RotaryEvent event = handleTransition(newState, now); + inputState = newState; + return event; +} + +void printRotaryEvent(RotaryEvent event) +{ + if (event == RotaryEvent::Previous) { + --rotaryCount; + Serial.print(F("Previous, count: ")); + } else if (event == RotaryEvent::Next) { + ++rotaryCount; + Serial.print(F("Next, count: ")); + } else { + return; + } + + Serial.println(rotaryCount); +} + +#if RADIOCORE_HAS_USER_BUTTON +void beginUserButton(uint32_t now) +{ + pinMode( + RADIOCORE_USER_BUTTON_PIN, + RADIOCORE_USER_BUTTON_PIN_MODE); + buttonRawState = digitalRead(RADIOCORE_USER_BUTTON_PIN); + buttonStableState = buttonRawState; + buttonChangedAt = now; +} + +void pollUserButton(uint32_t now) +{ + const uint8_t state = digitalRead(RADIOCORE_USER_BUTTON_PIN); + if (state != buttonRawState) { + buttonRawState = state; + buttonChangedAt = now; + } + + if (buttonRawState == buttonStableState || + now - buttonChangedAt < kButtonDebounceMs) { + return; + } + + buttonStableState = buttonRawState; + if (buttonStableState == RADIOCORE_USER_BUTTON_ACTIVE_LEVEL) { + Serial.println(F("User button pressed")); + } +} +#endif + +} // namespace + +void setup() +{ + Serial.begin(115200); + delay(100); + + Serial.println(); + Serial.println(F("RadioCore rotary encoder example")); + + const uint32_t now = millis(); +#if RADIOCORE_HAS_USER_BUTTON + beginUserButton(now); +#endif + enableSharedPower(); + tryBeginEncoder(millis()); +} + +void loop() +{ + const uint32_t now = millis(); + +#if RADIOCORE_HAS_USER_BUTTON + pollUserButton(now); +#endif + + if (!encoderReady) { + if (timeReached(now, nextEncoderRetryTime)) { + tryBeginEncoder(now); + } + return; + } + + bool readSucceeded = false; + const RotaryEvent event = pollEncoder(now, readSucceeded); + if (!readSucceeded) { + encoderReady = false; + nextEncoderRetryTime = now + kEncoderRetryIntervalMs; + Serial.println(F("Rotary encoder read failed; retrying in 1 s.")); + return; + } + + printRotaryEvent(event); +} + +#endif // RADIOCORE_HAS_ROTARY_ENCODER diff --git a/src/RadioCoreBoardConfig.h b/src/RadioCoreBoardConfig.h index 35300f7..bc8ebbc 100644 --- a/src/RadioCoreBoardConfig.h +++ b/src/RadioCoreBoardConfig.h @@ -27,6 +27,8 @@ #define RADIOCORE_HAS_MOTOR_CONTROL 0 #define RADIOCORE_HAS_RELAY_CONTROL 0 #define RADIOCORE_HAS_SERVO_CONTROL 0 +#define RADIOCORE_HAS_ROTARY_ENCODER 0 +#define RADIOCORE_HAS_USER_BUTTON 0 #endif #endif // RADIOCORE_BOARD_CONFIG_H diff --git a/src/boards/heltec_rc32.h b/src/boards/heltec_rc32.h index 5f01046..3e23c23 100644 --- a/src/boards/heltec_rc32.h +++ b/src/boards/heltec_rc32.h @@ -16,6 +16,16 @@ #define RADIOCORE_SENSOR_POWER_ON HIGH #define RADIOCORE_SENSOR_POWER_WARMUP_MS 100UL +#define RADIOCORE_HAS_ROTARY_ENCODER 1 +#define RADIOCORE_ROTARY_ENCODER_I2C_ADDRESS 0x20 +#define RADIOCORE_ROTARY_ENCODER_PHASE_A_MASK 0x01 +#define RADIOCORE_ROTARY_ENCODER_PHASE_B_MASK 0x02 + +#define RADIOCORE_HAS_USER_BUTTON 1 +#define RADIOCORE_USER_BUTTON_PIN 0 +#define RADIOCORE_USER_BUTTON_ACTIVE_LEVEL LOW +#define RADIOCORE_USER_BUTTON_PIN_MODE INPUT_PULLUP + #define RADIOCORE_HAS_CH01_ADC 1 #define RADIOCORE_CH01_ADC_PIN 2 #define RADIOCORE_HAS_CH01_POWER_CTRL 1 diff --git a/src/boards/heltec_rc52.h b/src/boards/heltec_rc52.h index d4a5b55..5e8a28e 100644 --- a/src/boards/heltec_rc52.h +++ b/src/boards/heltec_rc52.h @@ -16,6 +16,16 @@ #define RADIOCORE_SENSOR_POWER_ON HIGH #define RADIOCORE_SENSOR_POWER_WARMUP_MS 100UL +#define RADIOCORE_HAS_ROTARY_ENCODER 1 +#define RADIOCORE_ROTARY_ENCODER_I2C_ADDRESS 0x20 +#define RADIOCORE_ROTARY_ENCODER_PHASE_A_MASK 0x01 +#define RADIOCORE_ROTARY_ENCODER_PHASE_B_MASK 0x02 + +#define RADIOCORE_HAS_USER_BUTTON 1 +#define RADIOCORE_USER_BUTTON_PIN (32 + 10) // P1.10 +#define RADIOCORE_USER_BUTTON_ACTIVE_LEVEL LOW +#define RADIOCORE_USER_BUTTON_PIN_MODE INPUT_PULLUP + #define RADIOCORE_HAS_CH01_ADC 1 #define RADIOCORE_CH01_ADC_PIN 2 #define RADIOCORE_HAS_CH01_POWER_CTRL 0 diff --git a/src/boards/heltec_rcc6.h b/src/boards/heltec_rcc6.h index 401ead5..4719c6e 100644 --- a/src/boards/heltec_rcc6.h +++ b/src/boards/heltec_rcc6.h @@ -12,6 +12,9 @@ #define RADIOCORE_HAS_SENSOR_POWER_CTRL 0 +#define RADIOCORE_HAS_ROTARY_ENCODER 0 +#define RADIOCORE_HAS_USER_BUTTON 0 + #define RADIOCORE_HAS_CH01_ADC 0 #define RADIOCORE_HAS_CH01_POWER_CTRL 0 #define RADIOCORE_HAS_VO01_ADC 0 From 5fbcf79235ea1f42d5f1e9843becc41d89e0e6b6 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Mon, 21 Sep 2026 16:45:06 +0800 Subject: [PATCH 2/6] feat: add RC32 and RC52 motion sensor examples --- README.md | 8 + docs/CONTEXT.md | 12 +- examples/ICM42607P_Read/ICM42607P_Read.ino | 240 +++++++++++++++++++++ examples/ICM42607P_Read/README.md | 35 +++ examples/MMC5983MA_Read/MMC5983MA_Read.ino | 162 ++++++++++++++ examples/MMC5983MA_Read/README.md | 35 +++ library.properties | 2 +- src/RadioCoreBoardConfig.h | 3 + src/boards/heltec_rc32.h | 7 + src/boards/heltec_rc52.h | 7 + src/boards/heltec_rcc6.h | 4 + 11 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 examples/ICM42607P_Read/ICM42607P_Read.ino create mode 100644 examples/ICM42607P_Read/README.md create mode 100644 examples/MMC5983MA_Read/MMC5983MA_Read.ino create mode 100644 examples/MMC5983MA_Read/README.md diff --git a/README.md b/README.md index cb7badb..6525c38 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ The library declares these Arduino dependencies: - `Adafruit BME280 Library` - `BH1750` version 1.3.0 or newer from [claws/BH1750](https://github.com/claws/BH1750) +- `SparkFun MMC5983MA Magnetometer Arduino Library` version 1.1.5 or newer + for the onboard MMC5983MA magnetometer +- `ICM42670P` version 1.0.8 or newer from TDK InvenSense as the compatible + driver used by the onboard ICM42607P example - `Adafruit NeoPixel` - `ESP32Servo` for ESP32-based RadioCore boards - `NonBlockingRTTTL` version 1.4.0 or newer for non-blocking buzzer melodies @@ -56,6 +60,8 @@ compile time. | Example | Purpose | Documentation | | --- | --- | --- | | `BH1750_Read` | Read ambient light over Sensor I2C | [Guide](examples/BH1750_Read/README.md) | +| `MMC5983MA_Read` | Read the onboard three-axis magnetic field in gauss on RC32 and RC52 | [Guide](examples/MMC5983MA_Read/README.md) | +| `ICM42607P_Read` | Read onboard acceleration in g and angular rate in degrees per second on RC32 and RC52 | [Guide](examples/ICM42607P_Read/README.md) | | `CH01_Read` | Estimate CH01 gas concentration and trend | [Guide](examples/CH01_Read/README.md) | | `CO01_Read` | Run the CO01 warm-up cycle and estimate concentration | [Guide](examples/CO01_Read/README.md) | | `VO01_Read` | Estimate VO01 gas concentration and trend | [Guide](examples/VO01_Read/README.md) | @@ -82,6 +88,8 @@ A new board declares only the capabilities it supports: - Sensor I2C instance, pins, frequency, and optional sensor-power control for `BH1750_Read`. +- Onboard MMC5983MA and ICM42607P capability flags plus any shared sensor-reset + pin and release level required by their examples. - ADC pins and optional power controls for `CH01_Read`, `VO01_Read`, `CO01_Read`, and `Soil_Moisture_Read`; CO01 also requires its control pin. - NV3001B pins, active levels, SPI frequency, and one supported display diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index 1ade116..9eeaffa 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -10,7 +10,7 @@ The shared Arduino package for the RadioCore hardware family. _Avoid_: RadioCore Library, sensor driver bundle, full board support package **Sensor I2C**: -The board-designated I2C bus used by RadioCore examples for external sensors. +The board-designated I2C bus used by RadioCore onboard and external sensors. It may map to different `TwoWire` instances or pins on different boards. _Avoid_: default Wire, I2C0 @@ -19,6 +19,16 @@ The board-controlled power domain that supplies external sensors and may be shared with other peripherals. _Avoid_: sensor VCC pin, sensor enable pin, Vext +**MMC5983MA magnetometer**: +The board-integrated three-axis magnetic-field sensor on Sensor I2C in RC32 +and RC52. +_Avoid_: MMC5983, compass module, external magnetometer + +**ICM42607P motion sensor**: +The board-integrated inertial sensor on Sensor I2C in RC32 and RC52; its +compatible Arduino driver dependency is named ICM42670P. +_Avoid_: ICM42670P sensor, external IMU + **Board configuration**: Compile-time hardware facts for one RadioCore board, without peripheral initialization or example behavior. diff --git a/examples/ICM42607P_Read/ICM42607P_Read.ino b/examples/ICM42607P_Read/ICM42607P_Read.ino new file mode 100644 index 0000000..925616e --- /dev/null +++ b/examples/ICM42607P_Read/ICM42607P_Read.ino @@ -0,0 +1,240 @@ +#include +#include +#include + +#if !RADIOCORE_HAS_ICM42607P +#error "ICM42607P_Read requires an onboard RadioCore ICM42607P configuration." +#elif !RADIOCORE_HAS_SENSOR_I2C +#error "ICM42607P_Read requires a RadioCore Sensor I2C board configuration." +#else + +#include + +namespace { + +constexpr uint8_t kIcm42607pAddresses[] = {0x68, 0x69}; +constexpr uint8_t kWhoAmIRegister = 0x75; +constexpr uint8_t kIcm42607pWhoAmI = 0x60; +constexpr uint16_t kAccelerometerFrequencyHz = 50; +constexpr uint16_t kAccelerometerRangeG = 2; +constexpr uint16_t kGyroscopeFrequencyHz = 50; +constexpr uint16_t kGyroscopeRangeDps = 2000; +constexpr float kSignedSensorCounts = 32768.0F; +constexpr uint32_t kPrintIntervalMs = 100; + +ICM42670 imuAtPrimaryAddress( + RADIOCORE_SENSOR_I2C_INSTANCE, + false, + RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY); +ICM42670 imuAtAlternateAddress( + RADIOCORE_SENSOR_I2C_INSTANCE, + true, + RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY); +ICM42670* imu = nullptr; +bool sensorReady = false; +uint8_t detectedAddress = 0; +uint32_t lastPrintTime = 0; + +void enableSensorHardware() +{ +#if RADIOCORE_HAS_SENSOR_POWER_CTRL + pinMode(RADIOCORE_SENSOR_POWER_CTRL_PIN, OUTPUT); + digitalWrite( + RADIOCORE_SENSOR_POWER_CTRL_PIN, + RADIOCORE_SENSOR_POWER_ON); +#endif + +#if RADIOCORE_HAS_SENSOR_RESET + pinMode(RADIOCORE_SENSOR_RESET_PIN, OUTPUT); + digitalWrite( + RADIOCORE_SENSOR_RESET_PIN, + RADIOCORE_SENSOR_RESET_RELEASE_LEVEL); +#endif + +#if RADIOCORE_HAS_SENSOR_POWER_CTRL + delay(RADIOCORE_SENSOR_POWER_WARMUP_MS); +#endif +} + +bool beginSensorI2C() +{ +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_WIFI_KIT_SERIES) + return RADIOCORE_SENSOR_I2C_INSTANCE.begin( + RADIOCORE_SENSOR_I2C_SDA, + RADIOCORE_SENSOR_I2C_SCL, + RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY); +#elif defined(ARDUINO_ARCH_HELTEC_NRF52) + RADIOCORE_SENSOR_I2C_INSTANCE.setPins( + RADIOCORE_SENSOR_I2C_SDA, + RADIOCORE_SENSOR_I2C_SCL); + RADIOCORE_SENSOR_I2C_INSTANCE.begin(); + RADIOCORE_SENSOR_I2C_INSTANCE.setClock( + RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY); + return true; +#else +#error "ICM42607P_Read supports only the configured ESP32 and nRF52 I2C API types." +#endif +} + +bool readRegister(uint8_t address, uint8_t reg, uint8_t& value) +{ + RADIOCORE_SENSOR_I2C_INSTANCE.beginTransmission(address); + RADIOCORE_SENSOR_I2C_INSTANCE.write(reg); + if (RADIOCORE_SENSOR_I2C_INSTANCE.endTransmission(false) != 0) { + return false; + } + + if (RADIOCORE_SENSOR_I2C_INSTANCE.requestFrom( + address, + static_cast(1)) != 1) { + return false; + } + + value = RADIOCORE_SENSOR_I2C_INSTANCE.read(); + return true; +} + +bool detectImu() +{ + for (size_t index = 0; + index < sizeof(kIcm42607pAddresses) / + sizeof(kIcm42607pAddresses[0]); + ++index) { + const uint8_t address = kIcm42607pAddresses[index]; + uint8_t whoAmI = 0; + if (!readRegister(address, kWhoAmIRegister, whoAmI) || + whoAmI != kIcm42607pWhoAmI) { + continue; + } + + detectedAddress = address; + imu = address == kIcm42607pAddresses[0] + ? &imuAtPrimaryAddress + : &imuAtAlternateAddress; + return true; + } + + return false; +} + +bool beginImu() +{ + if (!detectImu()) { + Serial.println( + F("Onboard ICM42607P was not found at address 0x68 or 0x69.")); + return false; + } + + const int beginStatus = imu->begin(); + // ICM42670P 1.0.8 returns -3 for the ICM42607P WHO_AM_I value. The + // register map remains compatible, and the identity was verified above. + if (beginStatus != 0 && beginStatus != -3) { + Serial.print(F("ICM42607P initialization failed with status ")); + Serial.println(beginStatus); + return false; + } + + const int accelStatus = imu->startAccel( + kAccelerometerFrequencyHz, + kAccelerometerRangeG); + if (accelStatus != 0) { + Serial.print(F("ICM42607P accelerometer start failed with status ")); + Serial.println(accelStatus); + return false; + } + + const int gyroStatus = imu->startGyro( + kGyroscopeFrequencyHz, + kGyroscopeRangeDps); + if (gyroStatus != 0) { + Serial.print(F("ICM42607P gyroscope start failed with status ")); + Serial.println(gyroStatus); + return false; + } + + return true; +} + +float accelerationToG(int16_t rawValue) +{ + return static_cast(rawValue) * kAccelerometerRangeG / + kSignedSensorCounts; +} + +float angularRateToDps(int16_t rawValue) +{ + return static_cast(rawValue) * kGyroscopeRangeDps / + kSignedSensorCounts; +} + +} // namespace + +void setup() +{ + Serial.begin(115200); + delay(100); + + Serial.println(); + Serial.println(F("RadioCore onboard ICM42607P read example")); + + enableSensorHardware(); + + if (!beginSensorI2C()) { + Serial.println(F("Sensor I2C initialization failed.")); + return; + } + + sensorReady = beginImu(); + if (!sensorReady) { + return; + } + + Serial.print(F("ICM42607P initialized at address 0x")); + Serial.println(detectedAddress, HEX); +} + +void loop() +{ + if (!sensorReady) { + return; + } + + const uint32_t now = millis(); + if (now - lastPrintTime < kPrintIntervalMs) { + return; + } + lastPrintTime = now; + + inv_imu_sensor_event_t event = {}; + const int readStatus = imu->getDataFromRegisters(event); + if (readStatus != 0) { + Serial.print(F("ICM42607P read failed with status ")); + Serial.println(readStatus); + return; + } + + if (event.accel[0] == 0 && + event.accel[1] == 0 && + event.accel[2] == 0 && + event.gyro[0] == 0 && + event.gyro[1] == 0 && + event.gyro[2] == 0) { + return; + } + + Serial.print(F("Acceleration (g) X:")); + Serial.print(accelerationToG(event.accel[0]), 3); + Serial.print(F(" Y:")); + Serial.print(accelerationToG(event.accel[1]), 3); + Serial.print(F(" Z:")); + Serial.println(accelerationToG(event.accel[2]), 3); + + Serial.print(F("Angular rate (deg/s) X:")); + Serial.print(angularRateToDps(event.gyro[0]), 2); + Serial.print(F(" Y:")); + Serial.print(angularRateToDps(event.gyro[1]), 2); + Serial.print(F(" Z:")); + Serial.println(angularRateToDps(event.gyro[2]), 2); +} + +#endif // RADIOCORE_HAS_ICM42607P diff --git a/examples/ICM42607P_Read/README.md b/examples/ICM42607P_Read/README.md new file mode 100644 index 0000000..a6d08a6 --- /dev/null +++ b/examples/ICM42607P_Read/README.md @@ -0,0 +1,35 @@ +# ICM42607P Read + +## Purpose + +This example reads the onboard ICM42607P accelerometer and gyroscope on a RadioCore RC32 or RC52 and prints both three-axis measurements in physical units ten times per second. + +## Requirements + +- RadioCore RC32 or RC52 board configuration +- [TDK InvenSense ICM42670P](https://github.com/tdk-invn-oss/motion.arduino.ICM42670P) version 1.0.8 or newer + +The physical sensor is an ICM42607P. The ICM42670P library is a compatible driver dependency used by the reference firmware; it is not the detected sensor model. The dependency is declared in `library.properties` and can be installed automatically by dependency-aware Arduino tools. + +## Wiring and board differences + +The ICM42607P is integrated into the supported boards, so no external wiring is required. + +| Board | Sensor I2C SDA | Sensor I2C SCL | Sensor power control | Sensor reset | +| --- | ---: | ---: | --- | --- | +| Heltec RC32 | GPIO21 | GPIO18 | GPIO46, active high | GPIO2, high to release | +| Heltec RC52 | P1.11 (43) | P0.02 (2) | P0.12 (12), active high | P1.15 (47), high to release | + +Both boards wait 100 ms after enabling the Sensor power rail and releasing reset before starting I2C. The sketch detects the ICM42607P at `0x68` or `0x69` and verifies that register `0x75` contains its `0x60` identity value. + +## Behavior + +After identity verification, the sketch accepts driver initialization status `0` or `-3`. Version 1.0.8 of the ICM42670P driver returns `-3` because it expects the ICM42670P identity value, while the ICM42607P has a compatible register map and reports `0x60`. Other initialization errors stop the example. + +The accelerometer runs at 50 Hz with a +/-2 g full-scale range, and the gyroscope runs at 50 Hz with a +/-2000 degrees-per-second full-scale range. Every 100 ms, the sketch polls the latest event, converts signed 16-bit readings with `raw * full_scale / 32768`, and prints acceleration in g and angular rate in degrees per second at 115200 baud. + +## Limitations + +The values use the configured full-scale ranges but do not include offset calibration or temperature compensation. The example does not read temperature, calculate orientation, or enable wake-on-motion. + +The board Sensor interrupt is deliberately unused because RC32 and RC52 share that signal with the onboard rotary-encoder expander. The example intentionally supports only the onboard ICM42607P configurations declared by RC32 and RC52. diff --git a/examples/MMC5983MA_Read/MMC5983MA_Read.ino b/examples/MMC5983MA_Read/MMC5983MA_Read.ino new file mode 100644 index 0000000..2007c90 --- /dev/null +++ b/examples/MMC5983MA_Read/MMC5983MA_Read.ino @@ -0,0 +1,162 @@ +#include +#include +#include + +#if !RADIOCORE_HAS_MMC5983MA +#error "MMC5983MA_Read requires an onboard RadioCore MMC5983MA configuration." +#elif !RADIOCORE_HAS_SENSOR_I2C +#error "MMC5983MA_Read requires a RadioCore Sensor I2C board configuration." +#else + +#include + +namespace { + +constexpr float kZeroFieldCounts = 131072.0F; +constexpr float kCountsPerGauss = 16384.0F; +constexpr uint16_t kContinuousFrequencyHz = 50; +constexpr uint32_t kPrintIntervalMs = 100; + +SFE_MMC5983MA magnetometer; +bool sensorReady = false; +bool continuousMode = false; +uint32_t lastPrintTime = 0; + +void enableSensorHardware() +{ +#if RADIOCORE_HAS_SENSOR_POWER_CTRL + pinMode(RADIOCORE_SENSOR_POWER_CTRL_PIN, OUTPUT); + digitalWrite( + RADIOCORE_SENSOR_POWER_CTRL_PIN, + RADIOCORE_SENSOR_POWER_ON); +#endif + +#if RADIOCORE_HAS_SENSOR_RESET + pinMode(RADIOCORE_SENSOR_RESET_PIN, OUTPUT); + digitalWrite( + RADIOCORE_SENSOR_RESET_PIN, + RADIOCORE_SENSOR_RESET_RELEASE_LEVEL); +#endif + +#if RADIOCORE_HAS_SENSOR_POWER_CTRL + delay(RADIOCORE_SENSOR_POWER_WARMUP_MS); +#endif +} + +bool beginSensorI2C() +{ +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_WIFI_KIT_SERIES) + return RADIOCORE_SENSOR_I2C_INSTANCE.begin( + RADIOCORE_SENSOR_I2C_SDA, + RADIOCORE_SENSOR_I2C_SCL, + RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY); +#elif defined(ARDUINO_ARCH_HELTEC_NRF52) + RADIOCORE_SENSOR_I2C_INSTANCE.setPins( + RADIOCORE_SENSOR_I2C_SDA, + RADIOCORE_SENSOR_I2C_SCL); + RADIOCORE_SENSOR_I2C_INSTANCE.begin(); + RADIOCORE_SENSOR_I2C_INSTANCE.setClock( + RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY); + return true; +#else +#error "MMC5983MA_Read supports only the configured ESP32 and nRF52 I2C API types." +#endif +} + +bool beginMagnetometer() +{ + if (!magnetometer.begin(RADIOCORE_SENSOR_I2C_INSTANCE)) { + return false; + } + + magnetometer.softReset(); + magnetometer.setFilterBandwidth(100); + magnetometer.performSetOperation(); + magnetometer.enableAutomaticSetReset(); + + continuousMode = + magnetometer.setContinuousModeFrequency(kContinuousFrequencyHz); + continuousMode &= magnetometer.enableContinuousMode(); + if (!continuousMode) { + magnetometer.disableContinuousMode(); + Serial.println( + F("MMC5983MA continuous mode unavailable; using single-shot reads.")); + } + + return true; +} + +bool readMagneticField(float& xGauss, float& yGauss, float& zGauss) +{ + uint32_t rawX = 0; + uint32_t rawY = 0; + uint32_t rawZ = 0; + + const bool readSucceeded = continuousMode + ? magnetometer.readFieldsXYZ(&rawX, &rawY, &rawZ) + : magnetometer.getMeasurementXYZ(&rawX, &rawY, &rawZ); + if (!readSucceeded) { + return false; + } + + xGauss = (static_cast(rawX) - kZeroFieldCounts) / kCountsPerGauss; + yGauss = (static_cast(rawY) - kZeroFieldCounts) / kCountsPerGauss; + zGauss = (static_cast(rawZ) - kZeroFieldCounts) / kCountsPerGauss; + return true; +} + +} // namespace + +void setup() +{ + Serial.begin(115200); + delay(100); + + Serial.println(); + Serial.println(F("RadioCore onboard MMC5983MA read example")); + + enableSensorHardware(); + + if (!beginSensorI2C()) { + Serial.println(F("Sensor I2C initialization failed.")); + return; + } + + sensorReady = beginMagnetometer(); + if (!sensorReady) { + Serial.println(F("Onboard MMC5983MA was not found at address 0x30.")); + return; + } + + Serial.println(F("MMC5983MA initialized at address 0x30.")); +} + +void loop() +{ + if (!sensorReady) { + return; + } + + const uint32_t now = millis(); + if (now - lastPrintTime < kPrintIntervalMs) { + return; + } + lastPrintTime = now; + + float xGauss = 0.0F; + float yGauss = 0.0F; + float zGauss = 0.0F; + if (!readMagneticField(xGauss, yGauss, zGauss)) { + Serial.println(F("MMC5983MA read failed.")); + return; + } + + Serial.print(F("Magnetic field (G) X:")); + Serial.print(xGauss, 4); + Serial.print(F(" Y:")); + Serial.print(yGauss, 4); + Serial.print(F(" Z:")); + Serial.println(zGauss, 4); +} + +#endif // RADIOCORE_HAS_MMC5983MA diff --git a/examples/MMC5983MA_Read/README.md b/examples/MMC5983MA_Read/README.md new file mode 100644 index 0000000..0637076 --- /dev/null +++ b/examples/MMC5983MA_Read/README.md @@ -0,0 +1,35 @@ +# MMC5983MA Read + +## Purpose + +This example reads the onboard MMC5983MA three-axis magnetometer on a RadioCore RC32 or RC52 and prints the magnetic field in gauss ten times per second. + +## Requirements + +- RadioCore RC32 or RC52 board configuration +- [SparkFun MMC5983MA Magnetometer Arduino Library](https://github.com/sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library) version 1.1.5 or newer + +The dependency is declared in `library.properties` and can be installed automatically by dependency-aware Arduino tools. + +## Wiring and board differences + +The MMC5983MA is integrated into the supported boards, so no external wiring is required. + +| Board | Sensor I2C SDA | Sensor I2C SCL | Sensor power control | Sensor reset | +| --- | ---: | ---: | --- | --- | +| Heltec RC32 | GPIO21 | GPIO18 | GPIO46, active high | GPIO2, high to release | +| Heltec RC52 | P1.11 (43) | P0.02 (2) | P0.12 (12), active high | P1.15 (47), high to release | + +Both boards wait 100 ms after enabling the Sensor power rail and releasing reset before starting I2C. The MMC5983MA uses address `0x30`. + +## Behavior + +The sketch configures the MMC5983MA with a 100 Hz filter bandwidth, performs a SET operation, enables automatic SET/RESET, and requests 50 Hz continuous measurements. If continuous mode cannot be enabled, it reports the condition and uses single-shot measurements instead. + +Every 100 ms, the sketch converts each 18-bit unsigned reading to gauss using `(raw - 131072) / 16384` and prints the X, Y, and Z fields at 115200 baud. Initialization or read failures are reported over Serial. + +## Limitations + +The output is an uncalibrated magnetic-field vector. The example does not calculate a compass heading, remove hard-iron bias, correct soft-iron scaling, or compensate for board tilt. + +The example intentionally supports only the onboard MMC5983MA configurations declared by RC32 and RC52. A board that merely provides a Sensor I2C bus is not considered compatible. diff --git a/library.properties b/library.properties index 411457a..4d19958 100644 --- a/library.properties +++ b/library.properties @@ -8,4 +8,4 @@ category=Device Control url=https://github.com/HelTecAutomation/RadioCore_Library architectures=* includes=RadioCore_Kit.h -depends=Adafruit BME280 Library, BH1750 (>=1.3.0), Adafruit NeoPixel, ESP32Servo, NonBlockingRTTTL (>=1.4.0) +depends=Adafruit BME280 Library, BH1750 (>=1.3.0), SparkFun MMC5983MA Magnetometer Arduino Library (>=1.1.5), ICM42670P (>=1.0.8), Adafruit NeoPixel, ESP32Servo, NonBlockingRTTTL (>=1.4.0) diff --git a/src/RadioCoreBoardConfig.h b/src/RadioCoreBoardConfig.h index bc8ebbc..12bc6d3 100644 --- a/src/RadioCoreBoardConfig.h +++ b/src/RadioCoreBoardConfig.h @@ -9,7 +9,10 @@ #include "boards/heltec_rcc6.h" #else #define RADIOCORE_HAS_SENSOR_I2C 0 +#define RADIOCORE_HAS_MMC5983MA 0 +#define RADIOCORE_HAS_ICM42607P 0 #define RADIOCORE_HAS_SENSOR_POWER_CTRL 0 +#define RADIOCORE_HAS_SENSOR_RESET 0 #define RADIOCORE_HAS_CH01_ADC 0 #define RADIOCORE_HAS_CH01_POWER_CTRL 0 #define RADIOCORE_HAS_VO01_ADC 0 diff --git a/src/boards/heltec_rc32.h b/src/boards/heltec_rc32.h index 3e23c23..0ca6076 100644 --- a/src/boards/heltec_rc32.h +++ b/src/boards/heltec_rc32.h @@ -11,11 +11,18 @@ #define RADIOCORE_SENSOR_I2C_SCL 18 #define RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY 100000UL +#define RADIOCORE_HAS_MMC5983MA 1 +#define RADIOCORE_HAS_ICM42607P 1 + #define RADIOCORE_HAS_SENSOR_POWER_CTRL 1 #define RADIOCORE_SENSOR_POWER_CTRL_PIN 46 #define RADIOCORE_SENSOR_POWER_ON HIGH #define RADIOCORE_SENSOR_POWER_WARMUP_MS 100UL +#define RADIOCORE_HAS_SENSOR_RESET 1 +#define RADIOCORE_SENSOR_RESET_PIN 2 +#define RADIOCORE_SENSOR_RESET_RELEASE_LEVEL HIGH + #define RADIOCORE_HAS_ROTARY_ENCODER 1 #define RADIOCORE_ROTARY_ENCODER_I2C_ADDRESS 0x20 #define RADIOCORE_ROTARY_ENCODER_PHASE_A_MASK 0x01 diff --git a/src/boards/heltec_rc52.h b/src/boards/heltec_rc52.h index 5e8a28e..0d8121a 100644 --- a/src/boards/heltec_rc52.h +++ b/src/boards/heltec_rc52.h @@ -11,11 +11,18 @@ #define RADIOCORE_SENSOR_I2C_SCL (0 + 2) // P0.02 #define RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY 100000UL +#define RADIOCORE_HAS_MMC5983MA 1 +#define RADIOCORE_HAS_ICM42607P 1 + #define RADIOCORE_HAS_SENSOR_POWER_CTRL 1 #define RADIOCORE_SENSOR_POWER_CTRL_PIN (0 + 12) #define RADIOCORE_SENSOR_POWER_ON HIGH #define RADIOCORE_SENSOR_POWER_WARMUP_MS 100UL +#define RADIOCORE_HAS_SENSOR_RESET 1 +#define RADIOCORE_SENSOR_RESET_PIN (32 + 15) // P1.15 +#define RADIOCORE_SENSOR_RESET_RELEASE_LEVEL HIGH + #define RADIOCORE_HAS_ROTARY_ENCODER 1 #define RADIOCORE_ROTARY_ENCODER_I2C_ADDRESS 0x20 #define RADIOCORE_ROTARY_ENCODER_PHASE_A_MASK 0x01 diff --git a/src/boards/heltec_rcc6.h b/src/boards/heltec_rcc6.h index 4719c6e..0bd9d13 100644 --- a/src/boards/heltec_rcc6.h +++ b/src/boards/heltec_rcc6.h @@ -10,7 +10,11 @@ #define RADIOCORE_SENSOR_I2C_SCL SCL #define RADIOCORE_SENSOR_I2C_DEFAULT_FREQUENCY 100000UL +#define RADIOCORE_HAS_MMC5983MA 0 +#define RADIOCORE_HAS_ICM42607P 0 + #define RADIOCORE_HAS_SENSOR_POWER_CTRL 0 +#define RADIOCORE_HAS_SENSOR_RESET 0 #define RADIOCORE_HAS_ROTARY_ENCODER 0 #define RADIOCORE_HAS_USER_BUTTON 0 From 36d8043ff0141a2e27d0f98152dc537fb76a1529 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 23 Sep 2026 16:05:19 +0800 Subject: [PATCH 3/6] feat: add RC32 and RC52 e-paper examples --- README.md | 10 ++ docs/CONTEXT.md | 24 +++- ...p-e-paper-drivers-outside-radiocore-kit.md | 7 ++ .../DEPG1020BNS770F1_Display.ino | 104 ++++++++++++++++++ examples/DEPG1020BNS770F1_Display/README.md | 61 ++++++++++ .../E0213A367_Display/E0213A367_Display.ino | 101 +++++++++++++++++ examples/E0213A367_Display/README.md | 55 +++++++++ src/RadioCoreBoardConfig.h | 2 + src/boards/heltec_rc32.h | 27 +++++ src/boards/heltec_rc52.h | 27 +++++ 10 files changed, 416 insertions(+), 2 deletions(-) create mode 100644 docs/adr/0001-keep-e-paper-drivers-outside-radiocore-kit.md create mode 100644 examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino create mode 100644 examples/DEPG1020BNS770F1_Display/README.md create mode 100644 examples/E0213A367_Display/E0213A367_Display.ino create mode 100644 examples/E0213A367_Display/README.md diff --git a/README.md b/README.md index 6525c38..0d8b14b 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,11 @@ that fork manually in the Arduino sketchbook `libraries` directory. It is not listed in `library.properties` because NV3001B support has not been merged into the Arduino Library Manager version of `GFX Library for Arduino`. +`DEPG1020BNS770F1_Display` and `E0213A367_Display` require the development +version of `heltec-eink-modules` containing the external RD02E constructors and +RC52 platform support. Install that version manually until the changes are +available in a published Library Manager release. + ## Examples Open the examples from `File > Examples > RadioCore_Kit` in the Arduino @@ -67,6 +72,8 @@ compile time. | `VO01_Read` | Estimate VO01 gas concentration and trend | [Guide](examples/VO01_Read/README.md) | | `Soil_Moisture_Read` | Read and calibrate an analog soil moisture sensor | [Guide](examples/Soil_Moisture_Read/README.md) | | `NV3001B_Display` | Exercise the 128-by-220 color TFT | [Guide](examples/NV3001B_Display/README.md) | +| `DEPG1020BNS770F1_Display` | Refresh the 960-by-640 monochrome e-paper panel on RC32 or RC52 | [Guide](examples/DEPG1020BNS770F1_Display/README.md) | +| `E0213A367_Display` | Refresh the 122-by-250 drawable E0213A367 e-paper panel on RC32 or RC52 | [Guide](examples/E0213A367_Display/README.md) | | `Rotary_Encoder` | Report rotary direction, signed count, and user-button presses on RC32 and RC52 | [Sketch](examples/Rotary_Encoder/Rotary_Encoder.ino) | | `WS2812` | Run a three-color breathing animation | [Sketch](examples/WS2812/WS2812.ino) | | `Buzzer_Control` | Play and repeat an RTTTL melody through the board-designated buzzer output | [Sketch](examples/Buzzer_Control/Buzzer_Control.ino) | @@ -94,6 +101,9 @@ A new board declares only the capabilities it supports: `CO01_Read`, and `Soil_Moisture_Read`; CO01 also requires its control pin. - NV3001B pins, active levels, SPI frequency, and one supported display transport for `NV3001B_Display`. +- RD02E driver-board pins and active levels for the shared paged + DEPG1020BNS770F1 and E0213A367 examples. Document the connector mapping and + shared-pin conflicts. - A rotary encoder I2C address and phase masks for `Rotary_Encoder`, plus an independently declared user-button pin, active level, and input mode. The current RC32 and RC52 mappings share the configured Sensor I2C bus and power diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index 9eeaffa..6812502 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -39,10 +39,30 @@ The 128-by-220 color TFT panel configured by RadioCore display examples. _Avoid_: OLED, generic screen **Display transport**: -The board-selected SPI mechanism used to send commands and pixels to the -NV3001B display. +The board-selected SPI mechanism used to send commands and pixels to a +configured display device, including the NV3001B display or an e-paper driver +board. _Avoid_: default SPI, display bus +**DEPG1020BNS770F1 e-paper panel**: +The 960-by-640 monochrome e-paper panel driven by an SSD1677 controller. +_Avoid_: generic 10.2-inch display, TFT, grayscale panel + +**E0213A367 e-paper panel**: +The 128-by-250 monochrome e-paper panel with a 122-by-250 drawable area, +connected through the RD02E e-paper driver board in RadioCore examples. +_Avoid_: generic 2.13-inch display, Wireless Paper display + +**RD02E e-paper driver board**: +The external board that supplies switched power and high-voltage drive circuits +for compatible e-paper panels, including DEPG1020BNS770F1 and E0213A367, and +exposes their controller logic interface. +_Avoid_: bare panel, passive adapter, TFT connector + +**Full-screen e-paper refresh**: +A monochrome update that refreshes the complete e-paper image in one operation. +_Avoid_: partial refresh, fast refresh, grayscale refresh + **Analog gas sensor**: A CH01, VO01, or CO01 device that represents gas concentration as an analog output voltage. diff --git a/docs/adr/0001-keep-e-paper-drivers-outside-radiocore-kit.md b/docs/adr/0001-keep-e-paper-drivers-outside-radiocore-kit.md new file mode 100644 index 0000000..bb106ce --- /dev/null +++ b/docs/adr/0001-keep-e-paper-drivers-outside-radiocore-kit.md @@ -0,0 +1,7 @@ +# Keep e-paper panel drivers outside RadioCore_Kit + +RadioCore_Kit owns RadioCore board configuration and board-adaptive examples, +while reusable e-paper panel drivers remain in `heltec-eink-modules`. This keeps +the e-paper library independently usable and avoids coupling panel-controller +behavior to RadioCore hardware, at the cost of coordinating compatible releases +across the two repositories. diff --git a/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino b/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino new file mode 100644 index 0000000..3ecec40 --- /dev/null +++ b/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino @@ -0,0 +1,104 @@ +/* + * RadioCore DEPG1020BNS770F1 e-paper example + * + * Requires a heltec-eink-modules version that provides the + * DEPG1020BNS770F1 display class. + */ +#include +#include + +#if !RADIOCORE_HAS_DEPG1020BNS770F1_DISPLAY +#error "DEPG1020BNS770F1_Display requires a supported RadioCore board configuration." +#else + +#include + +namespace { + +constexpr uint16_t kPageHeight = 32; +constexpr uint32_t kRefreshRestMs = 60000UL; + +DEPG1020BNS770F1 display( + RADIOCORE_DEPG1020BNS770F1_DC, + RADIOCORE_DEPG1020BNS770F1_CS, + RADIOCORE_DEPG1020BNS770F1_BUSY, + RADIOCORE_DEPG1020BNS770F1_RST, + RADIOCORE_DEPG1020BNS770F1_ENABLE, + RADIOCORE_DEPG1020BNS770F1_ENABLE_ACTIVE, + RADIOCORE_DEPG1020BNS770F1_MOSI, + RADIOCORE_DEPG1020BNS770F1_SCK, + kPageHeight); + +uint32_t refreshCount = 0; + +void drawTestPage() +{ + display.drawRect(0, 0, display.width(), display.height(), BLACK); + display.drawRect(8, 8, display.width() - 16, display.height() - 16, BLACK); + + display.setTextColor(BLACK); + display.setTextSize(4); + display.setCursor(40, 48); + display.println(F("RadioCore")); + + display.setTextSize(2); + display.setCursor(40, 112); + display.print(F("Board: ")); + display.println(F(RADIOCORE_DEPG1020BNS770F1_BOARD_NAME)); + display.setCursor(40, 148); + display.println(F("DEPG1020BNS770F1 / SSD1677")); + display.setCursor(40, 184); + display.println(F("960 x 640 monochrome")); + display.setCursor(40, 220); + display.print(F("Refresh count: ")); + display.println(refreshCount); + + constexpr int16_t blockTop = 300; + constexpr int16_t blockHeight = 180; + constexpr int16_t blockWidth = 180; + display.fillRect(60, blockTop, blockWidth, blockHeight, BLACK); + display.drawRect(260, blockTop, blockWidth, blockHeight, BLACK); + display.fillCircle(570, blockTop + blockHeight / 2, 90, BLACK); + display.drawCircle(790, blockTop + blockHeight / 2, 90, BLACK); + + display.drawLine(40, 540, display.width() - 40, 540, BLACK); + display.setCursor(40, 568); + display.println(F("Full refresh; then 60 s rest")); +} + +} // namespace + +void setup() +{ + Serial.begin(115200); + delay(100); + + Serial.println(); + Serial.print(F("RadioCore DEPG1020BNS770F1 example: ")); + Serial.println(F(RADIOCORE_DEPG1020BNS770F1_BOARD_NAME)); +} + +void loop() +{ + ++refreshCount; + Serial.print(F("Starting full refresh ")); + Serial.println(refreshCount); + + // Full-refresh mode also powers, resets, and initializes the driver board. + display.fastmodeOff(); + if (!display.timedOut()) { + DRAW(display) { + drawTestPage(); + } + } + + if (display.timedOut()) { + Serial.println(F("E-paper BUSY timeout; the driver board was powered down.")); + } else { + Serial.println(F("Refresh complete; the panel is asleep and power is off.")); + } + + delay(kRefreshRestMs); +} + +#endif // RADIOCORE_HAS_DEPG1020BNS770F1_DISPLAY diff --git a/examples/DEPG1020BNS770F1_Display/README.md b/examples/DEPG1020BNS770F1_Display/README.md new file mode 100644 index 0000000..aabd915 --- /dev/null +++ b/examples/DEPG1020BNS770F1_Display/README.md @@ -0,0 +1,61 @@ +# DEPG1020BNS770F1 E-Paper Display + +## Purpose + +This example drives a DEPG1020BNS770F1 960-by-640 monochrome e-paper panel +through an RD02E SSD1677 driver board. It uses 32-row paged drawing, performs one +full-screen refresh, powers the driver board down, rests for 60 seconds, and +then repeats with an incremented refresh counter. + +## Requirements + +- RadioCore RC32 or RC52 board configuration +- A DEPG1020BNS770F1 driver board that includes the panel power and high-voltage + circuitry; do not connect the bare 24-pin panel directly to the MCU +- A `heltec-eink-modules` version that provides the `DEPG1020BNS770F1` class + +Install the updated `heltec-eink-modules` library manually in the Arduino +sketchbook `libraries` directory. The currently published Library Manager +version does not contain this panel driver. + +## RD02E wiring + +RD02E P1 mates pin-for-pin with RC32 P3 or RC52 P1. The signal assignment comes +from the RD02E and RadioCore schematics; it is not the RadioCore TFT SPI mapping. + +| Signal | RD02E P1 | Heltec RC32 P3 | Heltec RC52 P1 | +| --- | ---: | ---: | ---: | +| RST/RES | 1 | GPIO4 | P0.10 (10) | +| BUSY | 2 | GPIO6 | P1.13 (45) | +| DC | 3 | GPIO5 | P0.09 (9) | +| EN/VEINK_Ctrl | 4 | GPIO21 | P1.11 (43) | +| CS | 5 | GPIO2 | P1.15 (47) | +| SCK/SCL | 11 | GPIO48 | P0.05 (5) | +| MOSI/SDA | 13 | GPIO47 | P0.15 (15) | + +`VEINK_Ctrl` is active low: pulling it low turns on the RD02E P-channel power +switch. BUSY is active high, while RST and CS are active low. BS1 is tied to +ground on RD02E, fixing the panel in 4-wire, 8-bit SPI mode. There is no separate +MISO signal; SDA is only used as MOSI by this write-only driver. + +RC52 uses `SPI1`, explicitly remapped to SCK D5 and MOSI D15 by the display +constructor. Do not rely on the board variant's TFT SPI defaults. + +The example owns these connector pins while it runs. Do not simultaneously use +the conflicting sensor reset/power, soil moisture, WS2812, buzzer, relay, TFT, +or other connector functions. Check the PCB and RD02E silkscreen pin-1 marks +before applying power so the board-to-board connector is not inserted mirrored. + +## Behavior and limitations + +The sketch draws a static identification page, test geometry, and an increasing +refresh counter. A complete image is rendered through a 3,840-byte page buffer. +The driver waits at most 60 seconds for BUSY to return low. After a successful +refresh it sends the supplied-demo deep-sleep sequence and disables driver-board +power; the next cycle performs a complete power-on and initialization sequence. + +Only monochrome full-screen refresh is supported. Partial refresh, fast mode, +grayscale, controller readback, and hardware operation have not been verified. +RD02E has no isolation between the MCU control signals and the powered-down +SSD1677 domain. This implementation does not place the SPI and control pins in +high impedance after disabling `VDD_EINK`; hardware backfeed remains unverified. diff --git a/examples/E0213A367_Display/E0213A367_Display.ino b/examples/E0213A367_Display/E0213A367_Display.ino new file mode 100644 index 0000000..99e67bf --- /dev/null +++ b/examples/E0213A367_Display/E0213A367_Display.ino @@ -0,0 +1,101 @@ +/* + * RadioCore E0213A367 e-paper example + * + * Uses the RD02E driver board and the E0213A367 display class from + * heltec-eink-modules. The example performs a full refresh, keeps the driver + * powered for 60 seconds, then repeats with a new refresh count. + */ +#include +#include + +#if !RADIOCORE_HAS_E0213A367_DISPLAY +#error "E0213A367_Display requires a supported RadioCore board configuration." +#else + +#include + +namespace { + +constexpr uint16_t kPageHeight = 32; +constexpr uint32_t kRefreshRestMs = 60000UL; + +E0213A367 display( + RADIOCORE_E0213A367_DC, + RADIOCORE_E0213A367_CS, + RADIOCORE_E0213A367_BUSY, + RADIOCORE_E0213A367_RST, + RADIOCORE_E0213A367_ENABLE, + RADIOCORE_E0213A367_ENABLE_ACTIVE, + RADIOCORE_E0213A367_MOSI, + RADIOCORE_E0213A367_SCK, + kPageHeight); + +uint32_t refreshCount = 0; + +void drawTestPage() +{ + display.drawRect(0, 0, display.width(), display.height(), BLACK); + display.drawRect(3, 3, display.width() - 6, display.height() - 6, BLACK); + + display.setTextColor(BLACK); + display.setTextSize(2); + display.setCursor(10, 14); + display.println(F("RadioCore")); + + display.setTextSize(1); + display.setCursor(10, 46); + display.println(F(RADIOCORE_E0213A367_BOARD_NAME)); + display.setCursor(10, 62); + display.println(F("E0213A367")); + display.setCursor(10, 78); + display.println(F("122 x 250 BW")); + display.setCursor(10, 94); + display.print(F("Refresh: ")); + display.println(refreshCount); + + display.fillRect(12, 122, 38, 38, BLACK); + display.drawRect(70, 122, 38, 38, BLACK); + display.fillCircle(31, 190, 19, BLACK); + display.drawCircle(89, 190, 19, BLACK); + + display.drawLine(10, 222, display.width() - 10, 222, BLACK); + display.setCursor(10, 232); + display.println(F("Full refresh")); +} + +} // namespace + +void setup() +{ + Serial.begin(115200); + delay(100); + + Serial.println(); + Serial.print(F("RadioCore E0213A367 example: ")); + Serial.println(F(RADIOCORE_E0213A367_BOARD_NAME)); +} + +void loop() +{ + ++refreshCount; + Serial.print(F("Starting full refresh ")); + Serial.println(refreshCount); + + // Reasserts RD02E power, hard-resets the panel and loads full-refresh mode. + display.fastmodeOff(); + if (!display.timedOut()) { + DRAW(display) { + drawTestPage(); + } + } + + if (display.timedOut()) { + Serial.println(F("E-paper BUSY timeout; RD02E power is off.")); + } else { + Serial.println(F("Refresh complete; RD02E power remains on.")); + } + + delay(kRefreshRestMs); +} + +#endif // RADIOCORE_HAS_E0213A367_DISPLAY diff --git a/examples/E0213A367_Display/README.md b/examples/E0213A367_Display/README.md new file mode 100644 index 0000000..0240c25 --- /dev/null +++ b/examples/E0213A367_Display/README.md @@ -0,0 +1,55 @@ +# E0213A367 E-Paper Display + +This example drives the monochrome E0213A367 panel through the RD02E e-paper +driver board on Heltec RC32 and RC52. It renders a changing test page using a +32-row page buffer, performs a full-screen refresh, then rests for 60 seconds. + +## Requirements + +- A Heltec RC32 or RC52. +- An RD02E e-paper driver board and compatible E0213A367 panel. +- A development version of `heltec-eink-modules` that provides the external + E0213A367 constructor and RC52 platform support. + +## Wiring + +RD02E P1 mates pin-for-pin with RC32 P3 or RC52 P1. Confirm the connector pin 1 +orientation against the PCB silkscreen before applying power. + +| Signal | RD02E P1 | RC32 P3 | RC52 P1 | +| --- | ---: | ---: | ---: | +| RST/RES | 1 | GPIO4 | P0.10 / D10 | +| BUSY | 2 | GPIO6 | P1.13 / D45 | +| DC | 3 | GPIO5 | P0.09 / D9 | +| EN/VEINK_Ctrl | 4 | GPIO21 | P1.11 / D43 | +| CS | 5 | GPIO2 | P1.15 / D47 | +| SCK/SCL | 11 | GPIO48 | P0.05 / D5 | +| MOSI/SDA | 13 | GPIO47 | P0.15 / D15 | + +`VEINK_Ctrl` is active low, BUSY is active high, and RST and CS are active low. +BS1 is tied to ground on RD02E, selecting 4-wire, 8-bit SPI. The interface has +no independent MISO signal. RC52 uses `SPI1`, explicitly remapped to SCK D5 and +MOSI D15; CS remains the separate D47 GPIO. + +## Runtime behavior + +Each loop asserts RD02E power, waits 100 ms, hard-resets the panel, selects full +refresh mode and draws a new refresh count. The 128-pixel controller width and +32-row page height use a 512-byte image buffer; the visible drawing area is +122 by 250 pixels. After a successful refresh, RD02E remains powered during the +60-second rest period. + +BUSY waits time out after 60 seconds. A timeout switches `VEINK_Ctrl` inactive, +prints an error, rests for 60 seconds and retries the complete power/reset +sequence on the next loop. + +## Shared-pin restrictions + +Run this as a standalone example. Its GPIOs overlap sensor reset or power, +Sensor I2C, soil-moisture ADC or power, WS2812, buzzer, relay and other display +functions. Do not operate those features at the same time. + +RD02E does not isolate MCU logic signals when its switched e-paper supply is +off. After a BUSY timeout, control signals may still create a reverse-current +path; this example does not stop SPI or place every control line in a high- +impedance state. diff --git a/src/RadioCoreBoardConfig.h b/src/RadioCoreBoardConfig.h index 12bc6d3..1fcc6ef 100644 --- a/src/RadioCoreBoardConfig.h +++ b/src/RadioCoreBoardConfig.h @@ -25,6 +25,8 @@ #define RADIOCORE_NV3001B_USE_ESP32_SPI 0 #define RADIOCORE_NV3001B_USE_HARDWARE_SPI 0 #define RADIOCORE_NV3001B_USE_SOFTWARE_SPI 0 +#define RADIOCORE_HAS_DEPG1020BNS770F1_DISPLAY 0 +#define RADIOCORE_HAS_E0213A367_DISPLAY 0 #define RADIOCORE_HAS_WS2812 0 #define RADIOCORE_HAS_BUZZER 0 #define RADIOCORE_HAS_MOTOR_CONTROL 0 diff --git a/src/boards/heltec_rc32.h b/src/boards/heltec_rc32.h index 0ca6076..16ab404 100644 --- a/src/boards/heltec_rc32.h +++ b/src/boards/heltec_rc32.h @@ -80,6 +80,33 @@ #define RADIOCORE_NV3001B_BACKLIGHT_ACTIVE HIGH #define RADIOCORE_NV3001B_SPI_FREQUENCY 8000000UL +// RD02E P1 mates pin-for-pin with RC32 P3. The RD02E schematic assigns its +// SSD1677 interface to P1 pins 1-5, 11, and 13. +#define RADIOCORE_HAS_DEPG1020BNS770F1_DISPLAY 1 +#define RADIOCORE_DEPG1020BNS770F1_BOARD_NAME "Heltec RC32" +#define RADIOCORE_DEPG1020BNS770F1_SCK 48 +#define RADIOCORE_DEPG1020BNS770F1_MOSI 47 +#define RADIOCORE_DEPG1020BNS770F1_CS 2 +#define RADIOCORE_DEPG1020BNS770F1_DC 5 +#define RADIOCORE_DEPG1020BNS770F1_RST 4 +#define RADIOCORE_DEPG1020BNS770F1_ENABLE 21 +#define RADIOCORE_DEPG1020BNS770F1_ENABLE_ACTIVE LOW +#define RADIOCORE_DEPG1020BNS770F1_BUSY 6 +#define RADIOCORE_DEPG1020BNS770F1_BUSY_ACTIVE HIGH + +// E0213A367 uses the same RD02E host interface as DEPG1020BNS770F1. +#define RADIOCORE_HAS_E0213A367_DISPLAY 1 +#define RADIOCORE_E0213A367_BOARD_NAME RADIOCORE_DEPG1020BNS770F1_BOARD_NAME +#define RADIOCORE_E0213A367_SCK RADIOCORE_DEPG1020BNS770F1_SCK +#define RADIOCORE_E0213A367_MOSI RADIOCORE_DEPG1020BNS770F1_MOSI +#define RADIOCORE_E0213A367_CS RADIOCORE_DEPG1020BNS770F1_CS +#define RADIOCORE_E0213A367_DC RADIOCORE_DEPG1020BNS770F1_DC +#define RADIOCORE_E0213A367_RST RADIOCORE_DEPG1020BNS770F1_RST +#define RADIOCORE_E0213A367_ENABLE RADIOCORE_DEPG1020BNS770F1_ENABLE +#define RADIOCORE_E0213A367_ENABLE_ACTIVE RADIOCORE_DEPG1020BNS770F1_ENABLE_ACTIVE +#define RADIOCORE_E0213A367_BUSY RADIOCORE_DEPG1020BNS770F1_BUSY +#define RADIOCORE_E0213A367_BUSY_ACTIVE RADIOCORE_DEPG1020BNS770F1_BUSY_ACTIVE + #define RADIOCORE_HAS_WS2812 1 #define RADIOCORE_WS2812_DATA_PIN 5 diff --git a/src/boards/heltec_rc52.h b/src/boards/heltec_rc52.h index 0d8121a..17a8cff 100644 --- a/src/boards/heltec_rc52.h +++ b/src/boards/heltec_rc52.h @@ -71,6 +71,33 @@ #define RADIOCORE_NV3001B_BACKLIGHT_ACTIVE HIGH #define RADIOCORE_NV3001B_SPI_FREQUENCY 8000000UL +// RD02E P1 mates pin-for-pin with RC52 P1. The RD02E schematic assigns its +// SSD1677 interface to P1 pins 1-5, 11, and 13. +#define RADIOCORE_HAS_DEPG1020BNS770F1_DISPLAY 1 +#define RADIOCORE_DEPG1020BNS770F1_BOARD_NAME "Heltec RC52" +#define RADIOCORE_DEPG1020BNS770F1_SCK (0 + 5) // P0.05, RD02E P1 pin 11 +#define RADIOCORE_DEPG1020BNS770F1_MOSI (0 + 15) // P0.15, RD02E P1 pin 13 +#define RADIOCORE_DEPG1020BNS770F1_CS (32 + 15) // P1.15, RD02E P1 pin 5 +#define RADIOCORE_DEPG1020BNS770F1_DC (0 + 9) // P0.09, RD02E P1 pin 3 +#define RADIOCORE_DEPG1020BNS770F1_RST (0 + 10) +#define RADIOCORE_DEPG1020BNS770F1_ENABLE (32 + 11) // P1.11, RD02E P1 pin 4 +#define RADIOCORE_DEPG1020BNS770F1_ENABLE_ACTIVE LOW +#define RADIOCORE_DEPG1020BNS770F1_BUSY (32 + 13) // P1.13, RD02E P1 pin 2 +#define RADIOCORE_DEPG1020BNS770F1_BUSY_ACTIVE HIGH + +// E0213A367 uses the same RD02E host interface as DEPG1020BNS770F1. +#define RADIOCORE_HAS_E0213A367_DISPLAY 1 +#define RADIOCORE_E0213A367_BOARD_NAME RADIOCORE_DEPG1020BNS770F1_BOARD_NAME +#define RADIOCORE_E0213A367_SCK RADIOCORE_DEPG1020BNS770F1_SCK +#define RADIOCORE_E0213A367_MOSI RADIOCORE_DEPG1020BNS770F1_MOSI +#define RADIOCORE_E0213A367_CS RADIOCORE_DEPG1020BNS770F1_CS +#define RADIOCORE_E0213A367_DC RADIOCORE_DEPG1020BNS770F1_DC +#define RADIOCORE_E0213A367_RST RADIOCORE_DEPG1020BNS770F1_RST +#define RADIOCORE_E0213A367_ENABLE RADIOCORE_DEPG1020BNS770F1_ENABLE +#define RADIOCORE_E0213A367_ENABLE_ACTIVE RADIOCORE_DEPG1020BNS770F1_ENABLE_ACTIVE +#define RADIOCORE_E0213A367_BUSY RADIOCORE_DEPG1020BNS770F1_BUSY +#define RADIOCORE_E0213A367_BUSY_ACTIVE RADIOCORE_DEPG1020BNS770F1_BUSY_ACTIVE + #define RADIOCORE_HAS_WS2812 1 #define RADIOCORE_WS2812_DATA_PIN (0 + 9) // P0.09 From e1a2737c559387f65966385e84a78b0e194b6155 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 8 Oct 2026 15:56:30 +0800 Subject: [PATCH 4/6] feat: add RadioCore e-paper partial refresh support --- README.md | 2 +- docs/CONTEXT.md | 10 +++ .../DEPG1020BNS770F1_Display.ino | 77 ++++++++++++++---- examples/DEPG1020BNS770F1_Display/README.md | 81 ++++++++++++++----- examples/E0213A367_Display/README.md | 25 +++--- src/boards/heltec_rc32.h | 17 ++-- src/boards/heltec_rc52.h | 17 ++-- 7 files changed, 160 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 0d8b14b..5d29dd9 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ compile time. | `VO01_Read` | Estimate VO01 gas concentration and trend | [Guide](examples/VO01_Read/README.md) | | `Soil_Moisture_Read` | Read and calibrate an analog soil moisture sensor | [Guide](examples/Soil_Moisture_Read/README.md) | | `NV3001B_Display` | Exercise the 128-by-220 color TFT | [Guide](examples/NV3001B_Display/README.md) | -| `DEPG1020BNS770F1_Display` | Refresh the 960-by-640 monochrome e-paper panel on RC32 or RC52 | [Guide](examples/DEPG1020BNS770F1_Display/README.md) | +| `DEPG1020BNS770F1_Display` | Full baseline and partial-window updates on the 960-by-640 panel (RC32/RC52) | [Guide](examples/DEPG1020BNS770F1_Display/README.md) | | `E0213A367_Display` | Refresh the 122-by-250 drawable E0213A367 e-paper panel on RC32 or RC52 | [Guide](examples/E0213A367_Display/README.md) | | `Rotary_Encoder` | Report rotary direction, signed count, and user-button presses on RC32 and RC52 | [Sketch](examples/Rotary_Encoder/Rotary_Encoder.ino) | | `WS2812` | Run a three-color breathing animation | [Sketch](examples/WS2812/WS2812.ino) | diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index 6812502..c848bbb 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -63,6 +63,16 @@ _Avoid_: bare panel, passive adapter, TFT connector A monochrome update that refreshes the complete e-paper image in one operation. _Avoid_: partial refresh, fast refresh, grayscale refresh +**Partial-window e-paper refresh**: +A monochrome update that uses the panel's partial-refresh waveform to update +an explicitly selected region while preserving the surrounding displayed image. +_Avoid_: full-screen refresh, automatic change detection, grayscale refresh + +**E-paper reference image**: +Controller-side image data that provides a baseline for subsequent partial +refreshes, distinct from the MCU drawing buffer and the physically visible image. +_Avoid_: MCU framebuffer, visible image + **Analog gas sensor**: A CH01, VO01, or CO01 device that represents gas concentration as an analog output voltage. diff --git a/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino b/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino index 3ecec40..b540f55 100644 --- a/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino +++ b/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino @@ -1,8 +1,8 @@ /* * RadioCore DEPG1020BNS770F1 e-paper example * - * Requires a heltec-eink-modules version that provides the - * DEPG1020BNS770F1 display class. + * Requires a heltec-eink-modules version with DEPG1020BNS770F1 + * partial-window refresh support. */ #include #include @@ -16,7 +16,13 @@ namespace { constexpr uint16_t kPageHeight = 32; -constexpr uint32_t kRefreshRestMs = 60000UL; +constexpr uint32_t kRefreshRestMs = 2000UL; +constexpr uint8_t kPartialRefreshesBeforeFull = 10; +// Byte-aligned, entirely inside 960 x 640; keep static content outside it. +constexpr uint16_t kWindowLeft = 40; +constexpr uint16_t kWindowTop = 216; +constexpr uint16_t kWindowWidth = 600; +constexpr uint16_t kWindowHeight = 56; DEPG1020BNS770F1 display( RADIOCORE_DEPG1020BNS770F1_DC, @@ -30,8 +36,26 @@ DEPG1020BNS770F1 display( kPageHeight); uint32_t refreshCount = 0; +uint8_t partialRefreshCount = 0; +bool hasBaseline = false; -void drawTestPage() +void drawUpdateRegion(uint32_t count) +{ + // DRAW runs twice in partial mode. Never advance state inside this function. + display.fillRect(kWindowLeft, kWindowTop, kWindowWidth, kWindowHeight, WHITE); + display.setTextColor(BLACK); + display.setTextSize(2); + display.setCursor(40, 220); + display.print(F("Refresh count: ")); + display.println(count); + if (count & 1UL) { + display.fillRect(576, 220, 32, 32, BLACK); + } else { + display.drawRect(576, 220, 32, 32, BLACK); + } +} + +void drawTestPage(uint32_t count) { display.drawRect(0, 0, display.width(), display.height(), BLACK); display.drawRect(8, 8, display.width() - 16, display.height() - 16, BLACK); @@ -49,9 +73,7 @@ void drawTestPage() display.println(F("DEPG1020BNS770F1 / SSD1677")); display.setCursor(40, 184); display.println(F("960 x 640 monochrome")); - display.setCursor(40, 220); - display.print(F("Refresh count: ")); - display.println(refreshCount); + drawUpdateRegion(count); constexpr int16_t blockTop = 300; constexpr int16_t blockHeight = 180; @@ -63,7 +85,7 @@ void drawTestPage() display.drawLine(40, 540, display.width() - 40, 540, BLACK); display.setCursor(40, 568); - display.println(F("Full refresh; then 60 s rest")); + display.println(F("Window refresh; 2 s rest; full after 10 partials")); } } // namespace @@ -80,22 +102,45 @@ void setup() void loop() { - ++refreshCount; - Serial.print(F("Starting full refresh ")); - Serial.println(refreshCount); - - // Full-refresh mode also powers, resets, and initializes the driver board. - display.fastmodeOff(); + const bool fullRefresh = !hasBaseline || + partialRefreshCount >= kPartialRefreshesBeforeFull; + const uint32_t nextCount = refreshCount + 1; + Serial.print(fullRefresh ? F("Starting full refresh ") : F("Starting window refresh ")); + Serial.println(nextCount); + + if (fullRefresh) { + // Always restore the full window, including after a failed partial update. + display.fastmodeOff(); + display.fullscreen(); + } else { + // The first full image must succeed before selecting partial mode. + display.fastmodeOn(false); + display.setWindow(kWindowLeft, kWindowTop, kWindowWidth, kWindowHeight); + } if (!display.timedOut()) { DRAW(display) { - drawTestPage(); + if (fullRefresh) { + drawTestPage(nextCount); + } else { + drawUpdateRegion(nextCount); + } } } if (display.timedOut()) { + hasBaseline = false; + partialRefreshCount = 0; Serial.println(F("E-paper BUSY timeout; the driver board was powered down.")); + Serial.println(F("The next cycle will rebuild the image with a full refresh.")); } else { - Serial.println(F("Refresh complete; the panel is asleep and power is off.")); + refreshCount = nextCount; + hasBaseline = true; + if (fullRefresh) { + partialRefreshCount = 0; + } else { + ++partialRefreshCount; + } + Serial.println(F("Refresh complete; driver-board power remains on. Resting 2 s.")); } delay(kRefreshRestMs); diff --git a/examples/DEPG1020BNS770F1_Display/README.md b/examples/DEPG1020BNS770F1_Display/README.md index aabd915..bb246cf 100644 --- a/examples/DEPG1020BNS770F1_Display/README.md +++ b/examples/DEPG1020BNS770F1_Display/README.md @@ -3,16 +3,17 @@ ## Purpose This example drives a DEPG1020BNS770F1 960-by-640 monochrome e-paper panel -through an RD02E SSD1677 driver board. It uses 32-row paged drawing, performs one -full-screen refresh, powers the driver board down, rests for 60 seconds, and -then repeats with an incremented refresh counter. +through an RD02E SSD1677 driver board. It uses 32-row paged drawing, builds a +full-screen baseline, then updates a small window with the manufacturer's +partial-refresh waveform. It rests for 2 seconds after each completed refresh +and performs a full-screen refresh after every 10 successful partial updates. ## Requirements - RadioCore RC32 or RC52 board configuration - A DEPG1020BNS770F1 driver board that includes the panel power and high-voltage circuitry; do not connect the bare 24-pin panel directly to the MCU -- A `heltec-eink-modules` version that provides the `DEPG1020BNS770F1` class +- A `heltec-eink-modules` version with DEPG1020BNS770F1 partial-refresh support Install the updated `heltec-eink-modules` library manually in the Arduino sketchbook `libraries` directory. The currently published Library Manager @@ -20,42 +21,78 @@ version does not contain this panel driver. ## RD02E wiring -RD02E P1 mates pin-for-pin with RC32 P3 or RC52 P1. The signal assignment comes -from the RD02E and RadioCore schematics; it is not the RadioCore TFT SPI mapping. +RD02E P1 mates pin-for-pin with RC32 P3 or RC52 P1. The table uses the updated +RD02E signal assignment and the RadioCore connector mappings; it is not the +RadioCore TFT SPI mapping. | Signal | RD02E P1 | Heltec RC32 P3 | Heltec RC52 P1 | | --- | ---: | ---: | ---: | -| RST/RES | 1 | GPIO4 | P0.10 (10) | -| BUSY | 2 | GPIO6 | P1.13 (45) | -| DC | 3 | GPIO5 | P0.09 (9) | -| EN/VEINK_Ctrl | 4 | GPIO21 | P1.11 (43) | -| CS | 5 | GPIO2 | P1.15 (47) | -| SCK/SCL | 11 | GPIO48 | P0.05 (5) | -| MOSI/SDA | 13 | GPIO47 | P0.15 (15) | +| RST/RES | 10 | GPIO16 | P0.28 (28) | +| BUSY | 24 | GPIO38 | P1.02 (34) | +| DC | 8 | GPIO17 | P0.30 (30) | +| EN/VEINK_Ctrl | 26 | GPIO39 | P1.04 (36) | +| CS | 2 | GPIO6 | P1.13 (45) | +| SCK/SCL | 1 | GPIO4 | P0.10 (10) | +| MOSI/SDA | 3 | GPIO5 | P0.09 (9) | `VEINK_Ctrl` is active low: pulling it low turns on the RD02E P-channel power switch. BUSY is active high, while RST and CS are active low. BS1 is tied to ground on RD02E, fixing the panel in 4-wire, 8-bit SPI mode. There is no separate MISO signal; SDA is only used as MOSI by this write-only driver. -RC52 uses `SPI1`, explicitly remapped to SCK D5 and MOSI D15 by the display +RC52 uses `SPI1`, explicitly remapped to SCK D10 and MOSI D9 by the display constructor. Do not rely on the board variant's TFT SPI defaults. The example owns these connector pins while it runs. Do not simultaneously use -the conflicting sensor reset/power, soil moisture, WS2812, buzzer, relay, TFT, +the conflicting soil-moisture ADC/power, WS2812, TFT, or other connector functions. Check the PCB and RD02E silkscreen pin-1 marks before applying power so the board-to-board connector is not inserted mirrored. ## Behavior and limitations -The sketch draws a static identification page, test geometry, and an increasing -refresh counter. A complete image is rendered through a 3,840-byte page buffer. -The driver waits at most 60 seconds for BUSY to return low. After a successful -refresh it sends the supplied-demo deep-sleep sequence and disables driver-board -power; the next cycle performs a complete power-on and initialization sequence. +The first refresh draws the identification page and test geometry over the +complete 960-by-640 screen. Subsequent partial updates select the byte-aligned +rectangle `(40, 216, 600, 56)`, clear that rectangle to white, and redraw only the +refresh counter and an alternating black/white test block. The border, board +name, and other geometry remain unchanged. Coordinates are absolute screen +coordinates, not relative to the window. -Only monochrome full-screen refresh is supported. Partial refresh, fast mode, -grayscale, controller readback, and hardware operation have not been verified. +Drawing uses a 3,840-byte page buffer (`960 * 32 / 8`), not a 76,800-byte full +framebuffer. Partial mode runs the `DRAW` body twice through the shared library +paging loop; the DEPG1020 driver skips hardware writes on the second pass. The +counter and block state stay constant throughout both passes and advance only +after a successful refresh. Every 10 partial updates, the sketch explicitly +restores `fullscreen()` and refreshes the full image before continuing. + +Both full and partial refreshes now keep RD02E powered, with active-low +`VEINK_Ctrl` held low. Normal completion does not send deep sleep or turn power +off. This is a driver-wide behavior change, not an example-only option, and +increases standby power consumption. No public sleep/power-off API is added. +The driver waits at most 60 seconds for BUSY to return low. On timeout it +disables driver-board power and stops subsequent transfers. After the 2-second +rest, the next cycle powers on, resets, and rebuilds a full-screen baseline; +failed updates do not advance the displayed counter or the partial-update count. + +The port uses the supplied top-level SSD1677 demo's `WF_PARTIAL` (105 LUT bytes +plus five voltage parameters), `0x37` configuration, border `0x80`, and `0xCF` +activation. Full-refresh drawing writes the same paged image to both `0x24` +and `0x26`, restoring the page address counters before the second write to +initialize a consistent full-image reference. Partial windows still use +only `0x24`, with the second hardware pass skipped. The sketch does not detect +changed pixels automatically or use the nested SSD1685 project or OTP readback. +On 2026-10-08, the user confirmed normal display after adding the full-refresh +`0x26` write to resolve the reported window-external noise on the RC32 setup. +Temporary diagnostic logging and its API have been removed. +The existing Y-decreasing address mapping +is retained: the vendor window example has inconsistent Y direction and an +out-of-bounds demonstration rectangle, so its coordinates are not copied. + +Partial refresh remains experimental beyond this initial user-confirmed RC32 +test. RC52 hardware, long-term residual images, BUSY duration, and timeout +recovery still need validation; check window position, unchanged surrounding +content, and both black-to-white and white-to-black transitions on each setup. +Ten partial updates between full refreshes is an adjustable example policy, not +a manufacturer-guaranteed limit. Grayscale and Turbo mode are not implemented. RD02E has no isolation between the MCU control signals and the powered-down SSD1677 domain. This implementation does not place the SPI and control pins in high impedance after disabling `VDD_EINK`; hardware backfeed remains unverified. diff --git a/examples/E0213A367_Display/README.md b/examples/E0213A367_Display/README.md index 0240c25..0eacd55 100644 --- a/examples/E0213A367_Display/README.md +++ b/examples/E0213A367_Display/README.md @@ -14,22 +14,23 @@ driver board on Heltec RC32 and RC52. It renders a changing test page using a ## Wiring RD02E P1 mates pin-for-pin with RC32 P3 or RC52 P1. Confirm the connector pin 1 -orientation against the PCB silkscreen before applying power. +orientation against the PCB silkscreen before applying power. The table uses +the updated RD02E host signal assignment. | Signal | RD02E P1 | RC32 P3 | RC52 P1 | | --- | ---: | ---: | ---: | -| RST/RES | 1 | GPIO4 | P0.10 / D10 | -| BUSY | 2 | GPIO6 | P1.13 / D45 | -| DC | 3 | GPIO5 | P0.09 / D9 | -| EN/VEINK_Ctrl | 4 | GPIO21 | P1.11 / D43 | -| CS | 5 | GPIO2 | P1.15 / D47 | -| SCK/SCL | 11 | GPIO48 | P0.05 / D5 | -| MOSI/SDA | 13 | GPIO47 | P0.15 / D15 | +| RST/RES | 10 | GPIO16 | P0.28 / D28 | +| BUSY | 24 | GPIO38 | P1.02 / D34 | +| DC | 8 | GPIO17 | P0.30 / D30 | +| EN/VEINK_Ctrl | 26 | GPIO39 | P1.04 / D36 | +| CS | 2 | GPIO6 | P1.13 / D45 | +| SCK/SCL | 1 | GPIO4 | P0.10 / D10 | +| MOSI/SDA | 3 | GPIO5 | P0.09 / D9 | `VEINK_Ctrl` is active low, BUSY is active high, and RST and CS are active low. BS1 is tied to ground on RD02E, selecting 4-wire, 8-bit SPI. The interface has -no independent MISO signal. RC52 uses `SPI1`, explicitly remapped to SCK D5 and -MOSI D15; CS remains the separate D47 GPIO. +no independent MISO signal. RC52 uses `SPI1`, explicitly remapped to SCK D10 and +MOSI D9; CS remains the separate D45 GPIO. ## Runtime behavior @@ -45,8 +46,8 @@ sequence on the next loop. ## Shared-pin restrictions -Run this as a standalone example. Its GPIOs overlap sensor reset or power, -Sensor I2C, soil-moisture ADC or power, WS2812, buzzer, relay and other display +Run this as a standalone example. Its GPIOs overlap +soil-moisture ADC or power, WS2812 and other display functions. Do not operate those features at the same time. RD02E does not isolate MCU logic signals when its switched e-paper supply is diff --git a/src/boards/heltec_rc32.h b/src/boards/heltec_rc32.h index 16ab404..35fe74d 100644 --- a/src/boards/heltec_rc32.h +++ b/src/boards/heltec_rc32.h @@ -80,18 +80,17 @@ #define RADIOCORE_NV3001B_BACKLIGHT_ACTIVE HIGH #define RADIOCORE_NV3001B_SPI_FREQUENCY 8000000UL -// RD02E P1 mates pin-for-pin with RC32 P3. The RD02E schematic assigns its -// SSD1677 interface to P1 pins 1-5, 11, and 13. +// Updated RD02E P1 host mapping, pin-for-pin with RC32 P3. #define RADIOCORE_HAS_DEPG1020BNS770F1_DISPLAY 1 #define RADIOCORE_DEPG1020BNS770F1_BOARD_NAME "Heltec RC32" -#define RADIOCORE_DEPG1020BNS770F1_SCK 48 -#define RADIOCORE_DEPG1020BNS770F1_MOSI 47 -#define RADIOCORE_DEPG1020BNS770F1_CS 2 -#define RADIOCORE_DEPG1020BNS770F1_DC 5 -#define RADIOCORE_DEPG1020BNS770F1_RST 4 -#define RADIOCORE_DEPG1020BNS770F1_ENABLE 21 +#define RADIOCORE_DEPG1020BNS770F1_SCK 4 // RD02E P1 pin 1 +#define RADIOCORE_DEPG1020BNS770F1_MOSI 5 // RD02E P1 pin 3 +#define RADIOCORE_DEPG1020BNS770F1_CS 6 // RD02E P1 pin 2 +#define RADIOCORE_DEPG1020BNS770F1_DC 17 // RD02E P1 pin 8 +#define RADIOCORE_DEPG1020BNS770F1_RST 16 // RD02E P1 pin 10 +#define RADIOCORE_DEPG1020BNS770F1_ENABLE 39 // RD02E P1 pin 26 #define RADIOCORE_DEPG1020BNS770F1_ENABLE_ACTIVE LOW -#define RADIOCORE_DEPG1020BNS770F1_BUSY 6 +#define RADIOCORE_DEPG1020BNS770F1_BUSY 38 // RD02E P1 pin 24 #define RADIOCORE_DEPG1020BNS770F1_BUSY_ACTIVE HIGH // E0213A367 uses the same RD02E host interface as DEPG1020BNS770F1. diff --git a/src/boards/heltec_rc52.h b/src/boards/heltec_rc52.h index 17a8cff..e172109 100644 --- a/src/boards/heltec_rc52.h +++ b/src/boards/heltec_rc52.h @@ -71,18 +71,17 @@ #define RADIOCORE_NV3001B_BACKLIGHT_ACTIVE HIGH #define RADIOCORE_NV3001B_SPI_FREQUENCY 8000000UL -// RD02E P1 mates pin-for-pin with RC52 P1. The RD02E schematic assigns its -// SSD1677 interface to P1 pins 1-5, 11, and 13. +// Updated RD02E P1 host mapping, pin-for-pin with RC52 P1. #define RADIOCORE_HAS_DEPG1020BNS770F1_DISPLAY 1 #define RADIOCORE_DEPG1020BNS770F1_BOARD_NAME "Heltec RC52" -#define RADIOCORE_DEPG1020BNS770F1_SCK (0 + 5) // P0.05, RD02E P1 pin 11 -#define RADIOCORE_DEPG1020BNS770F1_MOSI (0 + 15) // P0.15, RD02E P1 pin 13 -#define RADIOCORE_DEPG1020BNS770F1_CS (32 + 15) // P1.15, RD02E P1 pin 5 -#define RADIOCORE_DEPG1020BNS770F1_DC (0 + 9) // P0.09, RD02E P1 pin 3 -#define RADIOCORE_DEPG1020BNS770F1_RST (0 + 10) -#define RADIOCORE_DEPG1020BNS770F1_ENABLE (32 + 11) // P1.11, RD02E P1 pin 4 +#define RADIOCORE_DEPG1020BNS770F1_SCK (0 + 10) // P0.10, RD02E P1 pin 1 +#define RADIOCORE_DEPG1020BNS770F1_MOSI (0 + 9) // P0.09, RD02E P1 pin 3 +#define RADIOCORE_DEPG1020BNS770F1_CS (32 + 13) // P1.13, RD02E P1 pin 2 +#define RADIOCORE_DEPG1020BNS770F1_DC (0 + 30) // P0.30, RD02E P1 pin 8 +#define RADIOCORE_DEPG1020BNS770F1_RST (0 + 28) // P0.28, RD02E P1 pin 10 +#define RADIOCORE_DEPG1020BNS770F1_ENABLE (32 + 4) // P1.04, RD02E P1 pin 26 #define RADIOCORE_DEPG1020BNS770F1_ENABLE_ACTIVE LOW -#define RADIOCORE_DEPG1020BNS770F1_BUSY (32 + 13) // P1.13, RD02E P1 pin 2 +#define RADIOCORE_DEPG1020BNS770F1_BUSY (32 + 2) // P1.02, RD02E P1 pin 24 #define RADIOCORE_DEPG1020BNS770F1_BUSY_ACTIVE HIGH // E0213A367 uses the same RD02E host interface as DEPG1020BNS770F1. From 5be823df8d10e9edf10106876d194bec58770d12 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 8 Oct 2026 17:47:42 +0800 Subject: [PATCH 5/6] feat: add DEPG1020 full-screen startup test --- README.md | 2 +- .../DEPG1020BNS770F1_Display.ino | 35 +++++++++++++++ examples/DEPG1020BNS770F1_Display/README.md | 45 +++++++++++++------ 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5d29dd9..4672219 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ compile time. | `VO01_Read` | Estimate VO01 gas concentration and trend | [Guide](examples/VO01_Read/README.md) | | `Soil_Moisture_Read` | Read and calibrate an analog soil moisture sensor | [Guide](examples/Soil_Moisture_Read/README.md) | | `NV3001B_Display` | Exercise the 128-by-220 color TFT | [Guide](examples/NV3001B_Display/README.md) | -| `DEPG1020BNS770F1_Display` | Full baseline and partial-window updates on the 960-by-640 panel (RC32/RC52) | [Guide](examples/DEPG1020BNS770F1_Display/README.md) | +| `DEPG1020BNS770F1_Display` | Startup black/white test, full baseline, and partial-window updates on the 960-by-640 panel (RC32/RC52) | [Guide](examples/DEPG1020BNS770F1_Display/README.md) | | `E0213A367_Display` | Refresh the 122-by-250 drawable E0213A367 e-paper panel on RC32 or RC52 | [Guide](examples/E0213A367_Display/README.md) | | `Rotary_Encoder` | Report rotary direction, signed count, and user-button presses on RC32 and RC52 | [Sketch](examples/Rotary_Encoder/Rotary_Encoder.ino) | | `WS2812` | Run a three-color breathing animation | [Sketch](examples/WS2812/WS2812.ino) | diff --git a/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino b/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino index b540f55..e6c1e78 100644 --- a/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino +++ b/examples/DEPG1020BNS770F1_Display/DEPG1020BNS770F1_Display.ino @@ -16,6 +16,7 @@ namespace { constexpr uint16_t kPageHeight = 32; +constexpr uint32_t kStartupFrameHoldMs = 1000UL; constexpr uint32_t kRefreshRestMs = 2000UL; constexpr uint8_t kPartialRefreshesBeforeFull = 10; // Byte-aligned, entirely inside 960 x 640; keep static content outside it. @@ -39,6 +40,18 @@ uint32_t refreshCount = 0; uint8_t partialRefreshCount = 0; bool hasBaseline = false; +bool showStartupFrame(uint16_t color) +{ + display.fastmodeOff(); + display.fullscreen(); + if (!display.timedOut()) { + DRAW(display) { + display.fillScreen(color); + } + } + return !display.timedOut(); +} + void drawUpdateRegion(uint32_t count) { // DRAW runs twice in partial mode. Never advance state inside this function. @@ -66,6 +79,8 @@ void drawTestPage(uint32_t count) display.println(F("RadioCore")); display.setTextSize(2); + display.setCursor(656, 56); + display.println(F("PARTIAL-WINDOW REFRESH")); display.setCursor(40, 112); display.print(F("Board: ")); display.println(F(RADIOCORE_DEPG1020BNS770F1_BOARD_NAME)); @@ -98,6 +113,26 @@ void setup() Serial.println(); Serial.print(F("RadioCore DEPG1020BNS770F1 example: ")); Serial.println(F(RADIOCORE_DEPG1020BNS770F1_BOARD_NAME)); + + display.setFlip(Flip::VERTICAL); + + Serial.println(F("Startup self-test: full-screen black.")); + if (!showStartupFrame(BLACK)) { + Serial.println(F("E-paper BUSY timeout during the black startup frame.")); + Serial.println(F("Startup self-test aborted; the next cycle will try a normal full refresh.")); + delay(kRefreshRestMs); + return; + } + delay(kStartupFrameHoldMs); + + Serial.println(F("Startup self-test: full-screen white.")); + if (!showStartupFrame(WHITE)) { + Serial.println(F("E-paper BUSY timeout during the white startup frame.")); + Serial.println(F("Startup self-test aborted; the next cycle will try a normal full refresh.")); + delay(kRefreshRestMs); + return; + } + delay(kStartupFrameHoldMs); } void loop() diff --git a/examples/DEPG1020BNS770F1_Display/README.md b/examples/DEPG1020BNS770F1_Display/README.md index bb246cf..b39d3a0 100644 --- a/examples/DEPG1020BNS770F1_Display/README.md +++ b/examples/DEPG1020BNS770F1_Display/README.md @@ -5,7 +5,8 @@ This example drives a DEPG1020BNS770F1 960-by-640 monochrome e-paper panel through an RD02E SSD1677 driver board. It uses 32-row paged drawing, builds a full-screen baseline, then updates a small window with the manufacturer's -partial-refresh waveform. It rests for 2 seconds after each completed refresh +partial-refresh waveform. At cold startup it first shows full-screen black and +white test frames. It rests for 2 seconds after each completed normal refresh and performs a full-screen refresh after every 10 successful partial updates. ## Requirements @@ -50,19 +51,35 @@ before applying power so the board-to-board connector is not inserted mirrored. ## Behavior and limitations -The first refresh draws the identification page and test geometry over the -complete 960-by-640 screen. Subsequent partial updates select the byte-aligned -rectangle `(40, 216, 600, 56)`, clear that rectangle to white, and redraw only the -refresh counter and an alternating black/white test block. The border, board -name, and other geometry remain unchanged. Coordinates are absolute screen -coordinates, not relative to the window. - -Drawing uses a 3,840-byte page buffer (`960 * 32 / 8`), not a 76,800-byte full -framebuffer. Partial mode runs the `DRAW` body twice through the shared library -paging loop; the DEPG1020 driver skips hardware writes on the second pass. The -counter and block state stay constant throughout both passes and advance only -after a successful refresh. Every 10 partial updates, the sketch explicitly -restores `fullscreen()` and refreshes the full image before continuing. +At cold startup, the sketch performs one full-screen black refresh and one +full-screen white refresh, holding each completed frame for 1 second. It then +draws the identification page and test geometry over the complete 960-by-640 +screen. The startup frames do not increment the displayed refresh counter and +are not repeated by periodic full refreshes or timeout recovery. If either +startup frame reaches the 60-second BUSY timeout, the driver board is powered +down, the remaining startup frame is skipped, and the next loop cycle attempts +the normal identification page as a full refresh. + +The normal page keeps `RadioCore` at the upper left and displays +`PARTIAL-WINDOW REFRESH` at the upper right so the top edge is visibly exercised. +Both labels are static full-refresh content. Subsequent partial updates select +the byte-aligned rectangle `(40, 216, 600, 56)`, clear that rectangle to white, +and redraw only the refresh counter and an alternating black/white test block. +The border, board name, header, and other geometry remain unchanged. Coordinates +are absolute screen coordinates, not relative to the window. + +The sketch selects `Flip::VERTICAL` before the startup test. With the verified +panel scan direction, this keeps text readable while rotating the physical +layout by 180 degrees, so the former upper-left content appears at the lower +right. The same mapping is applied to full-screen and partial-window updates. + +All startup and normal drawing uses a 3,840-byte page buffer +(`960 * 32 / 8`), not a 76,800-byte full framebuffer. Partial mode runs the +`DRAW` body twice through the shared library paging loop; the DEPG1020 driver +skips hardware writes on the second pass. The counter and block state stay +constant throughout both passes and advance only after a successful refresh. +Every 10 partial updates, the sketch explicitly restores `fullscreen()` and +refreshes the full image before continuing. Both full and partial refreshes now keep RD02E powered, with active-low `VEINK_Ctrl` held low. Normal completion does not send deep sleep or turn power From e6d22991571651f6ac85bb3ec17a6583259c04e5 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 8 Oct 2026 18:25:28 +0800 Subject: [PATCH 6/6] feat: add DEPG0290 e-paper example --- README.md | 25 ++- docs/CONTEXT.md | 15 +- .../DEPG0290BNS800_Display.ino | 174 ++++++++++++++++++ examples/DEPG0290BNS800_Display/README.md | 106 +++++++++++ src/RadioCoreBoardConfig.h | 1 + src/boards/heltec_rc32.h | 13 ++ src/boards/heltec_rc52.h | 13 ++ 7 files changed, 339 insertions(+), 8 deletions(-) create mode 100644 examples/DEPG0290BNS800_Display/DEPG0290BNS800_Display.ino create mode 100644 examples/DEPG0290BNS800_Display/README.md diff --git a/README.md b/README.md index 4672219..1f94941 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,22 @@ that fork manually in the Arduino sketchbook `libraries` directory. It is not listed in `library.properties` because NV3001B support has not been merged into the Arduino Library Manager version of `GFX Library for Arduino`. -`DEPG1020BNS770F1_Display` and `E0213A367_Display` require the development -version of `heltec-eink-modules` containing the external RD02E constructors and -RC52 platform support. Install that version manually until the changes are -available in a published Library Manager release. +`DEPG1020BNS770F1_Display`, `E0213A367_Display`, and +`DEPG0290BNS800_Display` require the development version of +`heltec-eink-modules` containing the external RD02E constructors and RC52 +platform support. The public Library Manager `4.6.0` package does not contain +these interfaces. From the Arduino sketchbook `libraries` directory, install +the verified revision with: + +```powershell +git clone https://github.com/Quency-D/heltec-eink-modules.git +git -C heltec-eink-modules checkout 7f5f0dafe3bce0a39350901ae3cc8c745463045f +``` + +`GFX_Root` is bundled with `heltec-eink-modules` and does not need a separate +installation. The Heltec Arduino core used to select RC32 or RC52 is a board +environment requirement, not an Arduino library dependency; no minimum core +version is asserted here. ## Examples @@ -74,6 +86,7 @@ compile time. | `NV3001B_Display` | Exercise the 128-by-220 color TFT | [Guide](examples/NV3001B_Display/README.md) | | `DEPG1020BNS770F1_Display` | Startup black/white test, full baseline, and partial-window updates on the 960-by-640 panel (RC32/RC52) | [Guide](examples/DEPG1020BNS770F1_Display/README.md) | | `E0213A367_Display` | Refresh the 122-by-250 drawable E0213A367 e-paper panel on RC32 or RC52 | [Guide](examples/E0213A367_Display/README.md) | +| `DEPG0290BNS800_Display` | Test full refresh and fast-window updates on the 296-by-128 landscape panel (RC32/RC52) | [Guide](examples/DEPG0290BNS800_Display/README.md) | | `Rotary_Encoder` | Report rotary direction, signed count, and user-button presses on RC32 and RC52 | [Sketch](examples/Rotary_Encoder/Rotary_Encoder.ino) | | `WS2812` | Run a three-color breathing animation | [Sketch](examples/WS2812/WS2812.ino) | | `Buzzer_Control` | Play and repeat an RTTTL melody through the board-designated buzzer output | [Sketch](examples/Buzzer_Control/Buzzer_Control.ino) | @@ -102,8 +115,8 @@ A new board declares only the capabilities it supports: - NV3001B pins, active levels, SPI frequency, and one supported display transport for `NV3001B_Display`. - RD02E driver-board pins and active levels for the shared paged - DEPG1020BNS770F1 and E0213A367 examples. Document the connector mapping and - shared-pin conflicts. + DEPG1020BNS770F1, E0213A367, and DEPG0290BNS800 examples. Document the + connector mapping and shared-pin conflicts. - A rotary encoder I2C address and phase masks for `Rotary_Encoder`, plus an independently declared user-button pin, active level, and input mode. The current RC32 and RC52 mappings share the configured Sensor I2C bus and power diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index c848bbb..4419e57 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -53,10 +53,15 @@ The 128-by-250 monochrome e-paper panel with a 122-by-250 drawable area, connected through the RD02E e-paper driver board in RadioCore examples. _Avoid_: generic 2.13-inch display, Wireless Paper display +**DEPG0290BNS800 e-paper panel**: +The 128-by-296 monochrome e-paper panel identified by its FPC-7519 rev.b +connector marking and supported by the matching heltec-eink-modules driver. +_Avoid_: generic 2.9-inch display, DEPG0290BNS75A, Vision Master display + **RD02E e-paper driver board**: The external board that supplies switched power and high-voltage drive circuits -for compatible e-paper panels, including DEPG1020BNS770F1 and E0213A367, and -exposes their controller logic interface. +for compatible e-paper panels, including DEPG1020BNS770F1, E0213A367, and +DEPG0290BNS800, and exposes their controller logic interface. _Avoid_: bare panel, passive adapter, TFT connector **Full-screen e-paper refresh**: @@ -68,6 +73,12 @@ A monochrome update that uses the panel's partial-refresh waveform to update an explicitly selected region while preserving the surrounding displayed image. _Avoid_: full-screen refresh, automatic change detection, grayscale refresh +**Fast-window e-paper update**: +A differential fast refresh whose image-RAM writes are restricted to a selected +window while the surrounding displayed image is preserved; it does not imply +that the controller drives only the selected gate region. +_Avoid_: proven local gate scan, full-screen refresh, grayscale refresh + **E-paper reference image**: Controller-side image data that provides a baseline for subsequent partial refreshes, distinct from the MCU drawing buffer and the physically visible image. diff --git a/examples/DEPG0290BNS800_Display/DEPG0290BNS800_Display.ino b/examples/DEPG0290BNS800_Display/DEPG0290BNS800_Display.ino new file mode 100644 index 0000000..8e3fd08 --- /dev/null +++ b/examples/DEPG0290BNS800_Display/DEPG0290BNS800_Display.ino @@ -0,0 +1,174 @@ +/* + * RadioCore DEPG0290BNS800 e-paper example + * + * Uses an RD02E driver board and the existing DEPG0290BNS800 display class + * from heltec-eink-modules. A successful full refresh establishes the image + * baseline before repeated fast-window updates. + */ +#include +#include + +#if !RADIOCORE_HAS_DEPG0290BNS800_DISPLAY +#error "DEPG0290BNS800_Display requires a supported RadioCore board configuration." +#else + +#include + +namespace { + +constexpr uint16_t kPageHeight = 32; +constexpr uint32_t kStartupFrameHoldMs = 1000UL; +constexpr uint32_t kRefreshRestMs = 2000UL; +constexpr uint8_t kFastUpdatesBeforeFull = 10; +// In landscape orientation, logical Y maps to the controller's byte-addressed +// X axis. This window is byte aligned and stays inside the 296 x 128 canvas. +constexpr uint16_t kWindowLeft = 8; +constexpr uint16_t kWindowTop = 72; +constexpr uint16_t kWindowWidth = 280; +constexpr uint16_t kWindowHeight = 48; + +DEPG0290BNS800 display( + RADIOCORE_DEPG0290BNS800_DC, + RADIOCORE_DEPG0290BNS800_CS, + RADIOCORE_DEPG0290BNS800_BUSY, + RADIOCORE_DEPG0290BNS800_RST, + RADIOCORE_DEPG0290BNS800_ENABLE, + static_cast(RADIOCORE_DEPG0290BNS800_ENABLE_ACTIVE), + RADIOCORE_DEPG0290BNS800_MOSI, + RADIOCORE_DEPG0290BNS800_SCK, + kPageHeight); + +uint32_t refreshCount = 0; +uint8_t fastUpdateCount = 0; +bool hasBaseline = false; + +bool showStartupFrame(uint16_t color) +{ + display.fastmodeOff(); + display.fullscreen(); + if (!display.timedOut()) { + DRAW(display) { + display.fillScreen(color); + } + } + return !display.timedOut(); +} + +void drawUpdateRegion(uint32_t count) +{ + // DRAW runs twice in fast mode. Never advance state inside this function. + display.fillRect(kWindowLeft, kWindowTop, kWindowWidth, kWindowHeight, WHITE); + display.setTextColor(BLACK); + display.setTextSize(2); + display.setCursor(12, 84); + display.print(F("Refresh: ")); + display.println(count); + + if (count & 1UL) { + display.fillRect(248, 80, 32, 32, BLACK); + } else { + display.drawRect(248, 80, 32, 32, BLACK); + } +} + +void drawTestPage(uint32_t count) +{ + display.drawRect(0, 0, display.width(), display.height(), BLACK); + display.drawRect(4, 4, display.width() - 8, display.height() - 8, BLACK); + + display.setTextColor(BLACK); + display.setTextSize(2); + display.setCursor(12, 12); + display.println(F("RadioCore")); + + display.setTextSize(1); + display.setCursor(176, 16); + display.println(F("FAST WINDOW UPDATE")); + display.setCursor(12, 40); + display.print(F("Board: ")); + display.println(F(RADIOCORE_DEPG0290BNS800_BOARD_NAME)); + display.setCursor(12, 54); + display.println(F("DEPG0290BNS800 296 x 128 BW")); + display.drawLine(8, 68, display.width() - 9, 68, BLACK); + + drawUpdateRegion(count); +} + +} // namespace + +void setup() +{ + Serial.begin(115200); + delay(100); + + Serial.println(); + Serial.print(F("RadioCore DEPG0290BNS800 example: ")); + Serial.println(F(RADIOCORE_DEPG0290BNS800_BOARD_NAME)); + + display.landscape(); + + Serial.println(F("Startup self-test: full-screen black.")); + if (!showStartupFrame(BLACK)) { + Serial.println(F("E-paper BUSY timeout during the black startup frame.")); + Serial.println(F("Startup self-test aborted; the next cycle will try a normal full refresh.")); + delay(kRefreshRestMs); + return; + } + delay(kStartupFrameHoldMs); + + Serial.println(F("Startup self-test: full-screen white.")); + if (!showStartupFrame(WHITE)) { + Serial.println(F("E-paper BUSY timeout during the white startup frame.")); + Serial.println(F("Startup self-test aborted; the next cycle will try a normal full refresh.")); + delay(kRefreshRestMs); + return; + } + delay(kStartupFrameHoldMs); +} + +void loop() +{ + const bool fullRefresh = !hasBaseline || + fastUpdateCount >= kFastUpdatesBeforeFull; + const uint32_t nextCount = refreshCount + 1; + Serial.print(fullRefresh ? F("Starting full refresh ") : F("Starting fast window update ")); + Serial.println(nextCount); + + if (fullRefresh) { + display.fastmodeOff(); + display.fullscreen(); + } else { + display.fastmodeOn(false); + display.setWindow(kWindowLeft, kWindowTop, kWindowWidth, kWindowHeight); + } + + if (!display.timedOut()) { + DRAW(display) { + if (fullRefresh) { + drawTestPage(nextCount); + } else { + drawUpdateRegion(nextCount); + } + } + } + + if (display.timedOut()) { + hasBaseline = false; + fastUpdateCount = 0; + Serial.println(F("E-paper BUSY timeout; the driver board was powered down.")); + Serial.println(F("The next cycle will rebuild the image with a full refresh.")); + } else { + refreshCount = nextCount; + hasBaseline = true; + if (fullRefresh) { + fastUpdateCount = 0; + } else { + ++fastUpdateCount; + } + Serial.println(F("Refresh complete; driver-board power remains on. Resting 2 s.")); + } + + delay(kRefreshRestMs); +} + +#endif // RADIOCORE_HAS_DEPG0290BNS800_DISPLAY diff --git a/examples/DEPG0290BNS800_Display/README.md b/examples/DEPG0290BNS800_Display/README.md new file mode 100644 index 0000000..ad15417 --- /dev/null +++ b/examples/DEPG0290BNS800_Display/README.md @@ -0,0 +1,106 @@ +# DEPG0290BNS800 E-Paper Display + +This example drives the monochrome DEPG0290BNS800 panel through an RD02E +e-paper driver board on Heltec RC32 and RC52. It performs cold-start coverage +tests, builds a full-screen image baseline, then repeatedly updates a selected +window with the driver's differential fast-refresh mode. + +## Requirements + +- A Heltec RC32 or RC52. +- An RD02E e-paper driver board and an electrically compatible + DEPG0290BNS800 panel marked FPC-7519 rev.b. + +## Library dependency + +This example requires `heltec-eink-modules` from the Quency-D development fork +at commit `7f5f0dafe3bce0a39350901ae3cc8c745463045f`. The public Library Manager +`4.6.0` package does not contain the required DEPG0290BNS800 external-driver +constructor or RC52 platform support. + +From the Arduino sketchbook `libraries` directory, install the verified +revision with: + +```powershell +git clone https://github.com/Quency-D/heltec-eink-modules.git +git -C heltec-eink-modules checkout 7f5f0dafe3bce0a39350901ae3cc8c745463045f +``` + +`GFX_Root` is included inside `heltec-eink-modules`; do not install it +separately. The Heltec Arduino core that provides the RC32 or RC52 board target +is a board environment requirement rather than an Arduino library dependency. +This example does not claim a minimum core version. + +The host-side connector mapping is shared with the other RD02E examples, but +that does not by itself prove panel-side compatibility. Before first power-up, +confirm the FPC pinout, panel voltage and high-voltage waveform requirements +against the hardware supplied for the panel. Do not connect the panel FPC +directly to RC32 or RC52. + +## Wiring + +RD02E P1 mates pin-for-pin with RC32 P3 or RC52 P1. Confirm connector pin 1 +against the PCB silkscreen before applying power. + +| Signal | RD02E P1 | RC32 P3 | RC52 P1 | +| --- | ---: | ---: | ---: | +| SCK/SCL | 1 | GPIO4 | P0.10 / D10 | +| CS | 2 | GPIO6 | P1.13 / D45 | +| MOSI/SDA | 3 | GPIO5 | P0.09 / D9 | +| DC | 8 | GPIO17 | P0.30 / D30 | +| RST/RES | 10 | GPIO16 | P0.28 / D28 | +| BUSY | 24 | GPIO38 | P1.02 / D34 | +| EN/VEINK_Ctrl | 26 | GPIO39 | P1.04 / D36 | + +`VEINK_Ctrl` is active low, BUSY is active high, and RST and CS are active low. +BS1 is tied to ground on RD02E, selecting 4-wire, 8-bit SPI. There is no +independent MISO signal. RC52 uses `SPI1`, explicitly remapped through the full +display constructor to SCK D10 and MOSI D9; CS remains D45. + +## Runtime behavior + +The sketch uses the driver's native 128-by-296 panel definition and selects a +296-by-128 landscape canvas. Cold startup displays full-screen black and white, +holding each successful frame for 1 second. These coverage frames do not change +the refresh counter. A BUSY timeout aborts the remaining startup test, powers +off RD02E and lets the next loop attempt the normal page as a full refresh. + +The normal page contains static identification text above a logical window at +`(8, 72, 280, 48)`. After a successful full-screen baseline, only this window +is rewritten with the refresh count and an alternating black/white block. After +10 successful fast-window updates, the sketch performs another full refresh. +Each completed operation is followed by a 2-second rest. + +`FAST WINDOW UPDATE` means that image-RAM writes are restricted to the selected +window while differential fast-refresh processing preserves the surrounding +displayed image. Available local documentation does not prove that the panel +controller drives only the selected gate region during this operation. + +All drawing uses a 32-row page buffer: `128 * 32 / 8 = 512` bytes. The paging +loop clips the final page to the remaining 8 rows and does not allocate the +4,736-byte full framebuffer. The existing driver retains its panel-specific +one-byte X RAM offset and custom 153-byte fast-refresh LUT. + +Each full or fast-mode setup asserts RD02E power, waits 100 ms, pulses reset low +for 10 ms and high for 10 ms, then sends software reset. Successful operations +leave RD02E powered so the controller image reference remains available. BUSY +waits time out after 60 seconds; timeout switches `VEINK_Ctrl` inactive, stops +later transfers and invalidates the example's baseline so the next cycle starts +with a complete full refresh. + +## Shared-pin restrictions + +Run this as a standalone example. Its GPIOs overlap soil-moisture ADC or power, +WS2812 and other display functions. Do not operate those features at the same +time. + +RD02E does not isolate MCU logic signals when its switched e-paper supply is +off. After a BUSY timeout, control signals may still create a reverse-current +path; this example does not stop SPI or place every control line in a high- +impedance state. + +The 10-update full-refresh interval, 2-second rest and supplied custom fast LUT +are example policies, not manufacturer guarantees for ghosting, temperature, +lifetime or safe update frequency. Validate full-screen coverage, orientation, +unchanged surrounding content and both black-to-white and white-to-black window +transitions on the actual RC32 and RC52 hardware. diff --git a/src/RadioCoreBoardConfig.h b/src/RadioCoreBoardConfig.h index 1fcc6ef..14df15f 100644 --- a/src/RadioCoreBoardConfig.h +++ b/src/RadioCoreBoardConfig.h @@ -27,6 +27,7 @@ #define RADIOCORE_NV3001B_USE_SOFTWARE_SPI 0 #define RADIOCORE_HAS_DEPG1020BNS770F1_DISPLAY 0 #define RADIOCORE_HAS_E0213A367_DISPLAY 0 +#define RADIOCORE_HAS_DEPG0290BNS800_DISPLAY 0 #define RADIOCORE_HAS_WS2812 0 #define RADIOCORE_HAS_BUZZER 0 #define RADIOCORE_HAS_MOTOR_CONTROL 0 diff --git a/src/boards/heltec_rc32.h b/src/boards/heltec_rc32.h index 35fe74d..705cea7 100644 --- a/src/boards/heltec_rc32.h +++ b/src/boards/heltec_rc32.h @@ -106,6 +106,19 @@ #define RADIOCORE_E0213A367_BUSY RADIOCORE_DEPG1020BNS770F1_BUSY #define RADIOCORE_E0213A367_BUSY_ACTIVE RADIOCORE_DEPG1020BNS770F1_BUSY_ACTIVE +// DEPG0290BNS800 uses the same RD02E host interface as DEPG1020BNS770F1. +#define RADIOCORE_HAS_DEPG0290BNS800_DISPLAY 1 +#define RADIOCORE_DEPG0290BNS800_BOARD_NAME RADIOCORE_DEPG1020BNS770F1_BOARD_NAME +#define RADIOCORE_DEPG0290BNS800_SCK RADIOCORE_DEPG1020BNS770F1_SCK +#define RADIOCORE_DEPG0290BNS800_MOSI RADIOCORE_DEPG1020BNS770F1_MOSI +#define RADIOCORE_DEPG0290BNS800_CS RADIOCORE_DEPG1020BNS770F1_CS +#define RADIOCORE_DEPG0290BNS800_DC RADIOCORE_DEPG1020BNS770F1_DC +#define RADIOCORE_DEPG0290BNS800_RST RADIOCORE_DEPG1020BNS770F1_RST +#define RADIOCORE_DEPG0290BNS800_ENABLE RADIOCORE_DEPG1020BNS770F1_ENABLE +#define RADIOCORE_DEPG0290BNS800_ENABLE_ACTIVE RADIOCORE_DEPG1020BNS770F1_ENABLE_ACTIVE +#define RADIOCORE_DEPG0290BNS800_BUSY RADIOCORE_DEPG1020BNS770F1_BUSY +#define RADIOCORE_DEPG0290BNS800_BUSY_ACTIVE RADIOCORE_DEPG1020BNS770F1_BUSY_ACTIVE + #define RADIOCORE_HAS_WS2812 1 #define RADIOCORE_WS2812_DATA_PIN 5 diff --git a/src/boards/heltec_rc52.h b/src/boards/heltec_rc52.h index e172109..6d28ec6 100644 --- a/src/boards/heltec_rc52.h +++ b/src/boards/heltec_rc52.h @@ -97,6 +97,19 @@ #define RADIOCORE_E0213A367_BUSY RADIOCORE_DEPG1020BNS770F1_BUSY #define RADIOCORE_E0213A367_BUSY_ACTIVE RADIOCORE_DEPG1020BNS770F1_BUSY_ACTIVE +// DEPG0290BNS800 uses the same RD02E host interface as DEPG1020BNS770F1. +#define RADIOCORE_HAS_DEPG0290BNS800_DISPLAY 1 +#define RADIOCORE_DEPG0290BNS800_BOARD_NAME RADIOCORE_DEPG1020BNS770F1_BOARD_NAME +#define RADIOCORE_DEPG0290BNS800_SCK RADIOCORE_DEPG1020BNS770F1_SCK +#define RADIOCORE_DEPG0290BNS800_MOSI RADIOCORE_DEPG1020BNS770F1_MOSI +#define RADIOCORE_DEPG0290BNS800_CS RADIOCORE_DEPG1020BNS770F1_CS +#define RADIOCORE_DEPG0290BNS800_DC RADIOCORE_DEPG1020BNS770F1_DC +#define RADIOCORE_DEPG0290BNS800_RST RADIOCORE_DEPG1020BNS770F1_RST +#define RADIOCORE_DEPG0290BNS800_ENABLE RADIOCORE_DEPG1020BNS770F1_ENABLE +#define RADIOCORE_DEPG0290BNS800_ENABLE_ACTIVE RADIOCORE_DEPG1020BNS770F1_ENABLE_ACTIVE +#define RADIOCORE_DEPG0290BNS800_BUSY RADIOCORE_DEPG1020BNS770F1_BUSY +#define RADIOCORE_DEPG0290BNS800_BUSY_ACTIVE RADIOCORE_DEPG1020BNS770F1_BUSY_ACTIVE + #define RADIOCORE_HAS_WS2812 1 #define RADIOCORE_WS2812_DATA_PIN (0 + 9) // P0.09