# ESPHome Standing Desk Display Component ## ATmega328P Proxy Architecture **Version:** 3.1 **Date:** April 2026 **Platform:** ESP32-S3 + ATmega328P proxy **Protocol:** I2C (ATmega328P slave at 0x50) --- ## Executive Summary This project uses an **ATmega328P microcontroller (28-pin) as a protocol proxy** to bridge the original desk controller's AiP650E display communication and memory keys to an ESP32-S3 running ESPHome. **Architecture:** - **Original Desk Controller** → Sends CLK/DIO protocol + memory key signals - **ATmega328P Proxy** → Listens to CLK/DIO, reads memory keys, exposes I2C registers - **ESP32-S3** → Reads I2C registers, renders touchscreen UI, connects to Home Assistant **Benefits:** - ✅ Timing-critical protocol capture isolated from ESP32 (no WiFi jitter) - ✅ Handles all 3 memory keys + common line (no ESP32 GPIO needed) - ✅ Serial debugging via UART (faster development) - ✅ Easy reprogramming via bootloader (after initial ISP) - ✅ Hardware I2C support (not bit-banged) - ✅ Minimal wiring (CLK/DIO + 3 keys + I2C + power) - ✅ Original motor control completely unaffected --- ## 1. Hardware Setup ### 1.1 ATmega328P Pin Assignment (28-pin QFN) ``` ┌─────────────┐ PC6 ├───●───────┤ AVCC (pin 20) PD0 ├───────────┤ GND (pin 22) PD1 ├───────────┤ PC5 PD2 ├───────────┤ PC4 PD3 ├───────────┤ PC3 PD4 ├───────────┤ PC2 PD5 ├───────────┤ PC1 PD6 ├───────────┤ PC0 PD7 ├───────────┤ GND PB0 ├───────────┤ VCC PB1 ├───────────┤ PB2 PB3 ├───────────┤ PB4 PB5 ├───────────┤ PB6 └─────────────┘ ``` | Pin # | Port | Function | Connect To | Notes | |-------|------|----------|-----------|-------| | 1 | PC6 | RESET | ISP Header (or button) | ISP programming reset | | 2 | PD0 | RXD | USB-UART TX (optional) | Serial debugging input | | 3 | PD1 | TXD | USB-UART RX (optional) | Serial debugging output | | 4 | PD2 | (spare) | — | Available for future use | | 5 | PD3 | (spare) | — | Available for future use | | 6 | PD4 | KEY_1 Input | Display Pin 13 | Memory key 1 input | | 7 | VCC | Power | ESP32 3.3V | Main power supply | | 8 | GND | Ground | Common GND | Ground reference | | 11 | PD5 | KEY_2 Input | Display Pin 9 | Memory key 2 input | | 12 | PD6 | KEY_3 Input | Display Pin 8 | Memory key 3 input | | 13 | PD7 | KEY_COMMON | Display Pin 7 | Memory key common line | | 14 | PB0 | SCL (I2C) | ESP32 GPIO21 | I2C Clock (4.7k pull-up to 3.3V) | | 15 | PB1 | SDA (I2C) | ESP32 GPIO20 | I2C Data (4.7k pull-up to 3.3V) | | 16 | PB2 | CLK Input | Display Pin 2 | Display protocol clock | | 17 | PB3 | MOSI (ISP) | ISP Header Pin 1 | SPI Data In (ISP only) | | 18 | PB4 | MISO (ISP) | ISP Header Pin 5 | SPI Data Out (ISP only) | | 19 | PB5 | SCK (ISP) | ISP Header Pin 3 | SPI Clock (ISP only) | | 20 | AVCC | Analog Ref | VCC + 0.1µF cap | Analog voltage reference | | 21 | PC1 | LED Output | LED Anode | Activity LED indicator (active HIGH) | | 22 | GND | Ground | Common GND | Ground reference | | 23 | PC0 | DIO Input | Display Pin 3 | Display protocol data | | 24 | PC2 | UP Button | Button to GND | UP button input (pull-up) | | 25 | PC3 | DOWN Button | Button to GND | DOWN button input (pull-up) | **Key Layout Advantages:** - ✅ Protocol pins (CLK/DIO) on PB2/PC0 (safe from ISP) - ✅ Memory keys on PD4/5/6/7 (safe from ISP) - ✅ ISP pins (PB3/4/5) isolated and only used during programming - ✅ Serial debug on PD0/1 (useful for development) ### 1.2 Wiring Diagram ``` ┌──────────────────────────────┐ ┌──────────────────┐ ┌──────────────┐ │ Display Driver Board │ │ ATmega328P │ │ ESP32-S3 │ ├──────────────────────────────┤ ├──────────────────┤ ├──────────────┤ │ Pin 2 (CLK) ────────────────→├─→ │ Pin 16 PB2 │ │ │ │ Pin 3 (DIO) ────────────────→├─→ │ Pin 23 PC0 │ │ │ │ Pin 7 (KEY_COMMON) ─────────→├─→ │ Pin 13 PD7 │ │ │ │ Pin 8 (KEY_3) ──────────────→├─→ │ Pin 12 PD6 │ │ │ │ Pin 9 (KEY_2) ──────────────→├─→ │ Pin 11 PD5 │ │ │ │ Pin 13 (KEY_1) ─────────────→├─→ │ Pin 6 PD4 │ │ │ │ GND ────────────────────────→├─→ │ Pin 8 GND ──┬───→├──→ GND │ │ │ │ Pin 21 PC1 ─┼──┐ │ │ │ │ │ (LED → R ├─→└─┼→ +3.3V via R │ │ │ │ Pin 24 PC2 ─┼──→├──→ UP Button │ │ │ │ Pin 25 PC3 ─┼──→├──→ DOWN Button │ │ │ │ Pin 14 PB0 ─┼──→├──→ GPIO21 (SCL) │ │ │ │ (4.7k pull) │ │ │ │ │ │ Pin 15 PB1 ─┼──→├──→ GPIO20 (SDA) │ │ │ │ (4.7k pull) │ │ │ │ │ │ Pin 7 VCC ◄─┴──→├──← GPIO 3.3V │ │ │ │ │ │ │ │ │ [ISP Header] │ │ │ │ │ Pin 17 (MOSI) │ │ │ │ │ Pin 18 (MISO) │ │ │ │ │ Pin 19 (SCK) ────→ ISP Programmer │ │ │ │ Pin 1 (RESET) │ │ └──────────────────────────────┘ └──────────────────┘ └──────────────┘ ``` ### 1.3 Bill of Materials #### For Breadboard Prototype: | Part | Value | Qty | Cost | Notes | |------|-------|-----|------|-------| | ATmega328P | 28-pin QFN | 1 | $2-3 | Microchip or compatible | | Resistor | 4.7k | 2 | $0.20 | I2C pull-ups (3.3V side) | | Resistor | 330Ω | 1 | $0.05 | LED current limiting | | Resistor | 10k | 2 | $0.10 | Button pull-ups (optional) | | Capacitor | 100nF | 1 | $0.10 | Decoupling on VCC | | LED | Any color | 1 | $0.10 | Activity indicator | | Button | Tactile 6mm | 2 | $0.20 | UP and DOWN buttons | | **Total** | | | **$2.70-3.75** | Breadboard prototype | | Breadboard | — | 1 | $2-5 | For prototyping | | **Total** | | | **$5-15** | Estimated cost | #### For Custom PCB Interposer: | Part | Value | Qty | Cost | Notes | |------|-------|-----|------|-------| | ATmega328P | 28-pin DIP/TQFP | 1 | $2-3 | Better for production | | Resistor | 4.7k | 2 | $0.10 | I2C pull-ups | | Resistor | 10k | 2 | $0.10 | LED resistors (optional) | | Capacitor | 100nF | 2 | $0.20 | Decoupling + AVCC filter | | **Connectors:** | | | | | | JST-SH4 (QWIIC) | — | 1 | $0.50 | I2C port (standard) | | 2×3 pin header | — | 1 | $0.20 | ISP programming header | | 1×6 pin header | — | 1 | $0.20 | Serial debug (optional) | | **Level Shifter** | TXB0104 | 1 | $0.80 | Shift 5V ↔ 3.3V (if needed) | | PCB | — | 1 | $5-15 | Small custom PCB | | **Total** | | | **$12-25** | Estimated cost | **Notes:** - ISP header is required for initial firmware programming - Serial header enables easy reprogramming (after bootloader installed) - Level shifter needed only if running ATmega at 5V with 3.3V ESP32 - QWIIC port provides standardized I2C connector ### 1.4 Interposer PCB Design Checklist When designing the custom PCB, include: - ✅ **Main Components:** - [ ] ATmega328P in 28-pin DIP or TQFP-32 package - [ ] 100nF decoupling capacitor across VCC/GND - [ ] 4.7k pull-up resistors on I2C lines (if not on ESP32 board) - ✅ **Programming Headers:** - [ ] 6-pin ISP header (2×3 pin spacing, labeled clearly) - [ ] 6-pin serial FTDI header (for bootloader reprogramming) - [ ] Label each header with pin numbers (1-6) - ✅ **External Connectors:** - [ ] JST-SH4 connector for QWIIC I2C (front-facing) - [ ] 4-pin header for CLK/DIO from original micro - [ ] 4-pin header for UP/DOWN buttons (optional) - ✅ **Layout:** - [ ] ISP header on edge of board for programmer access - [ ] Serial header nearby ISP for easy access - [ ] QWIIC port on opposite edge from ISP - [ ] Clear silk-screen labeling - ✅ **Routing:** - [ ] Keep ISP traces short (< 1 inch) - [ ] 4.7k pull-ups on I2C, close to connector - [ ] Ground plane if possible (connects all GNDs together) - [ ] No high-speed traces near ISP/serial headers --- ## 2. I2C Protocol (ATtiny85 → ESP32-S3) ### 2.1 Register Map **Address:** 0x50 (1010_0000 in 8-bit format) | Offset | Name | Type | Access | Description | |--------|------|------|--------|-------------| | 0x00 | DIG1 | uint8 | R | Digit 1 (hundreds): 0-9 or 0xFF | | 0x01 | DIG2 | uint8 | R | Digit 2 (tens): 0-9 or 0xFF | | 0x02 | DIG3 | uint8 | R | Digit 3 (ones): 0-9 or 0xFF | | 0x03 | STAT | uint8 | R | Display status (power, brightness, mode) | | 0x10 | VERSION | uint8 | R | Firmware version (0x10 = v1.0) | | 0x11 | ERROR | uint8 | R | Error flags | ### 2.2 Register Details **DIG1, DIG2, DIG3 (0x00, 0x01, 0x02) - Shadow RAM:** - **Range:** 0-9 (valid digit) or 0xFF (invalid/not available) - **Combined Value:** `display_mm = DIG1*100 + DIG2*10 + DIG3` - **Example:** If DIG1=2, DIG2=5, DIG3=0 → display shows "250" ⚠️ **Important: Shadow RAM Behavior** These registers contain **persistent** display values, maintained by the ATtiny85 even when the original desk display blanks (power saving mode). This means: - **Display is awake:** Registers show current desk height - **Display goes to sleep (blanks after timeout):** Registers continue showing last known height - Original display might show brightness=0 or blank segments - ESP32 still reads the height value (e.g., "250") - This prevents "NaN" or error readings on your touchscreen UI - **Display wakes up:** Registers update with any new height changes **Example Timeline:** ``` Time 0: Desk at 250mm → DIG1=2, DIG2=5, DIG3=0 Time 60s: Sleep timeout → Original display blanks Time 60s: ESP32 reads DIG1/2/3 → Still gets "250" (shadow RAM) Time 120s: User presses UP button → Display wakes to 275mm Time 120s: DIG1=2, DIG2=7, DIG3=5 (updated) ``` **Benefits of Shadow RAM:** - ✅ No stale data on ESP32 touchscreen - ✅ Display blanking doesn't disrupt UI - ✅ Seamless height display during sleep mode **STAT Byte (0x03):** ``` Bit 7 (MSB): Display Power (1=on, 0=off) Bit 6-4: Brightness Level (0-7, 7=brightest) Bit 3: SEG Mode (1=7-segment, 0=8-segment) Bit 2: Sleep Mode (1=enabled, 0=disabled) Bit 1-0: Reserved (always 0) ``` **VERSION Byte (0x10):** - `0x10` = ATtiny85 firmware v1.0 **ERROR Byte (0x11):** ``` Bit 7: I2C Communication Error (1=error, 0=ok) Bit 6: Frame Timeout (1=no CLK >1sec, 0=ok) Bit 5: Malformed Protocol Frame (1=error, 0=ok) Bit 4: Segment Data Corruption (1=error, 0=ok) Bit 3-0: Reserved ``` ### 2.3 Example I2C Read (C++) ```cpp #include #define PROXY_ADDR 0x50 void setup() { Wire.begin(); Serial.begin(115200); } void loop() { uint8_t dig1, dig2, dig3, stat, err; // Read digit 1 Wire.beginTransmission(PROXY_ADDR); Wire.write(0x00); Wire.endTransmission(); Wire.requestFrom(PROXY_ADDR, 1); dig1 = Wire.read(); // Read digit 2 Wire.beginTransmission(PROXY_ADDR); Wire.write(0x01); Wire.endTransmission(); Wire.requestFrom(PROXY_ADDR, 1); dig2 = Wire.read(); // Read digit 3 Wire.beginTransmission(PROXY_ADDR); Wire.write(0x02); Wire.endTransmission(); Wire.requestFrom(PROXY_ADDR, 1); dig3 = Wire.read(); // Combine into height value if (dig1 != 0xFF && dig2 != 0xFF && dig3 != 0xFF) { uint16_t height = dig1 * 100 + dig2 * 10 + dig3; Serial.printf("Desk Height: %d mm\n", height); } delay(100); // Poll every 100ms } ``` ### 2.4 Example ESPHome Config ```yaml esphome: name: standing-desk esp32_s3: board: esp32-s3-devkitc-1 i2c: sda: GPIO20 scl: GPIO21 frequency: 100kHz sensor: - platform: i2c name: "Desk Height (from proxy)" address: 0x50 register: 0x00 value_type: U8 id: proxy_dig1 unit_of_measurement: "" - platform: i2c name: "Desk Height Digit 2" address: 0x50 register: 0x01 value_type: U8 id: proxy_dig2 - platform: i2c name: "Desk Height Digit 3" address: 0x50 register: 0x02 value_type: U8 id: proxy_dig3 - platform: template name: "Desk Height (mm)" unit_of_measurement: "mm" icon: "mdi:ruler" update_interval: 100ms lambda: |- uint16_t d1 = (uint8_t)id(proxy_dig1).state; uint16_t d2 = (uint8_t)id(proxy_dig2).state; uint16_t d3 = (uint8_t)id(proxy_dig3).state; if (d1 != 0xFF && d2 != 0xFF && d3 != 0xFF) { return d1 * 100 + d2 * 10 + d3; } return {}; binary_sensor: - platform: gpio name: "UP Button" pin: GPIO14 icon: "mdi:arrow-up" - platform: gpio name: "DOWN Button" pin: GPIO13 icon: "mdi:arrow-down" ``` --- ## 3. ATtiny85 Firmware See: **ATTINY85_PROXY_FIRMWARE.md** for complete firmware source code **Key functions:** - CLK interrupt handler: Capture protocol bits - Frame parser: Decode AiP650E commands - I2C slave: Expose registers 0x00-0x11 - Timeout detection: Set error flags on bus inactivity **Compilation:** - Arduino IDE + ATtinyCore - 8 MHz internal clock - 3-4 KB flash used --- ## 4. Expected Display Values The ATtiny85 decodes AiP650E segment data and converts to digits. ### 4.1 7-Segment Character Mapping | Digit | Hex | Segments | Pattern | |-------|-----|----------|---------| | 0 | 0x3F | A,B,C,D,E,F | `█████▓` | | 1 | 0x06 | B,C | `░░█████` | | 2 | 0x5B | A,B,D,E,G | `█▓█▓██` | | 3 | 0x4F | A,B,C,D,G | `█▓███▓` | | 4 | 0x66 | B,C,F,G | `░░██████` | | 5 | 0x6D | A,C,D,F,G | `██▓███` | | 6 | 0x7D | A,C,D,E,F,G | `██▓███` | | 7 | 0x07 | A,B,C | `░░█████` | | 8 | 0x7F | A,B,C,D,E,F,G | `███████` | | 9 | 0x6F | A,B,C,D,F,G | `██████▓` | ### 4.2 Typical Display Values - **Minimum height:** 000 (all zeros: 0 mm) - **Normal range:** 600-1200 (standing desk typical) - **Maximum height:** 999 (3 digits max) --- ## 5. Troubleshooting ### I2C Issues | Problem | Cause | Solution | |---------|-------|----------| | I2C not responding | Wrong address or not started | Check address 0x50, verify Wire.begin() in ESP32 sketch | | Reads return 0xFF | Protocol not capturing | Check CLK/DIO connections, verify ATtiny85 power | | Intermittent I2C | Weak pull-ups | Use external 4.7k resistors on SCL/SDA | | ATtiny85 won't program | ISP speed too fast | Reduce ISP clock to <500kHz in programmer | ### Protocol Issues | Problem | Cause | Solution | |---------|-------|----------| | Display values always 0xFF | CLK interrupt not firing | Check PB3 connected to original pin 16 | | Display values wrong | Segment decoder bug | Check 7-segment lookup table in firmware | | ERROR byte shows bit 6 (timeout) | Original micro not running | Power-cycle original control board | ### Expected Behavior - Display Blanking This is **not** a problem - it's expected behavior! **Scenario:** Your desk display goes dark after timeout (sleep mode) - **Original desk display:** Blank/dark (brightness=0) - **ESP32 touchscreen:** Still shows the height value (e.g., "250mm") - **Explanation:** Shadow RAM keeps the last valid value even when the physical display blanks **Why this is good:** - ✅ Your touchscreen UI remains responsive and shows current height - ✅ No "NaN" or error messages on screen - ✅ User always knows the desk position, even at night with display off - ✅ When desk wakes up, both displays sync automatically **If you DON'T see height on touchscreen while original display is blank:** - Check I2C pull-ups (4.7k resistors on SCL/SDA) - Verify ESP32 is polling DIG1/DIG2/DIG3 registers - Check ATtiny85 power supply (should be 3.3V) --- ## 6. Performance - **I2C Read Time:** ~5-10ms per register - **Display Update Rate:** 10-100ms (based on original micro) - **ESP32 Poll Interval:** 100ms recommended - **Power Consumption (ATtiny85):** ~10-50mA depending on load --- ## 7. Future Enhancements - [ ] Decode memory buttons from AiP650E key matrix - [ ] Add CRC8 checksum for data validation - [ ] Support dual-display setup (multiple ATtiny85 proxies) - [ ] Energy monitoring (motor current sense) - [ ] Watchdog timer for crash recovery --- ## 8. References - **ATtiny85 Datasheet:** Microchip ATtiny85 (8-bit AVR) - **AiP650E Datasheet:** Wuxi I-CORE Electronics (attached in spec) - **ESPHome Documentation:** https://esphome.io - **Arduino ATtinyCore:** https://github.com/SpenceKonde/ATTinyCore --- **Document Version:** 3.0 **Last Updated:** April 2026