This commit is contained in:
2026-05-13 11:45:58 +02:00
commit c9551aa663
21 changed files with 158068 additions and 0 deletions
+480
View File
@@ -0,0 +1,480 @@
# 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