481 lines
18 KiB
Markdown
481 lines
18 KiB
Markdown
# 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 <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
|
||
|
||
```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
|
||
|