18 KiB
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++)
#include <Wire.h>
#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
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