# AiP650E Protocol Proxy Firmware

**Version:** 1.0  
**Date:** April 2026  
**Microcontroller:** ATmega328P (28-pin DIP)  
**Protocol:** I2C Slave @ 0x50  
**Purpose:** Capture AiP650E display protocol, memory keys, and expose via I2C to ESP32-S3

---

## 1. Overview

This firmware runs on an ATmega328P microcontroller to act as a protocol bridge. It captures:
- Original microcontroller's AiP650E display commands via CLK/DIO lines
- Memory key button states (KEY_1, KEY_2, KEY_3 with common)
- Exposes all data to the ESP32-S3 via I2C

This isolates timing-critical protocol capture from the ESP32's rendering and connectivity tasks. The ATmega328P provides ample GPIO for all sensors and debug capability via UART.

---

## 2. Pin Assignments

```
        ┌──────────┐
    PD0 ├───●───●──┤ PD1
    PD2 ├──────────┤ GND
    PD3 ├──────────┤ AREF
    PD4 ├──────────┤ AV
    VCC ├──────────┤ GND
    GND ├──────────┤ PB6
    PB7 ├──────────┤ PB5
    PD5 ├──────────┤ PB4
    PD6 ├──────────┤ PB3
    PD7 ├──────────┤ PB2
    PB0 ├──────────┤ PB1
    PC0 ├──────────┤ PC1
        └──────────┘
         ATmega328P (top view)
```

| Pin # | Port | Function | Connect To | Notes |
|-------|------|----------|-----------|-------|
| 1 | PC6 | RESET | ISP Header Pin 6 | Reset signal (ISP programming) |
| 2 | PD0 | RXD | USB-UART adapter TX | Serial debugging (optional) |
| 3 | PD1 | TXD | USB-UART adapter RX | 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 (KEY_1) | Memory key 1 button 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 (KEY_2) | Memory key 2 button input |
| 12 | PD6 | KEY_3 Input | Display Pin 8 (KEY_3) | Memory key 3 button input |
| 13 | PD7 | KEY_COMMON | Display Pin 7 (KEY_COMMON) | Common line for all memory keys |
| 14 | PB0 | SCL (I2C) | ESP32 GPIO21 | I2C Clock, with 4.7k pull-up to 3.3V |
| 15 | PB1 | SDA (I2C) | ESP32 GPIO20 | I2C Data, with 4.7k pull-up to 3.3V |
| 16 | PB2 | CLK Input | Display Pin 2 (SEGM_CLK) | Interrupt input for protocol capture |
| 17 | PB3 | MOSI (ISP) | ISP Header Pin 1 | SPI Data In (ISP programmer) |
| 18 | PB4 | MISO (ISP) | ISP Header Pin 5 | SPI Data Out (ISP programmer) |
| 19 | PB5 | SCK (ISP) | ISP Header Pin 3 | SPI Clock (ISP programmer) |
| 20 | AVCC | Analog Ref | VCC | Tie to VCC with 0.1uF cap |
| 21 | PC1 | LED Output | LED Anode (via 330Ω resistor) | Activity LED indicator (active HIGH) |
| 22 | GND | Ground | Common GND | Ground reference |
| 23 | PC0 | DIO Input | Display Pin 3 (SEGM_DIO) | Data line (open-drain output) |
| 24 | PC2 | UP Button | Button to GND | UP button input (pull-up enabled) |
| 25 | PC3 | DOWN Button | Button to GND | DOWN button input (pull-up enabled) |

**Advantages of ATmega328P:**
- ✅ More GPIO pins (can route buttons directly)
- ✅ Hardware UART (easy serial debugging via `Serial.println()`)
- ✅ Larger flash (4x more code space)
- ✅ Serial bootloader (reprogram via serial instead of ISP)
- ✅ Better for custom PCB with level shifter

### 2.2a ISP Programming Header (ATmega328P)

**Standard 6-pin ISP Header Layout:**
```
    ┌─────────────┐
    │ 1  2  3  4  │
    │ 5  6       │
    └─────────────┘
     ISP Header (top view)
```

| Header Pin | Signal | ATmega328P Pin | Arduino Pin | Connect |
|------------|--------|----------------|-------------|---------|
| 1 | MOSI | PB3 (pin 17) | 11 | SPI Data In |
| 2 | VCC | Pin 7 | Power | Power supply |
| 3 | SCK | PB5 (pin 19) | 13 | SPI Clock |
| 4 | GND | Pin 8/22 | GND | Ground |
| 5 | MISO | PB4 (pin 18) | 12 | SPI Data Out |
| 6 | RESET | PC6 (pin 1) | RESET | Reset signal |

**Advantages for ATmega328P:**
- ✅ ISP pins separate from I2C (no conflicts!)
- ✅ Can keep I2C running during ISP programming
- ✅ Serial bootloader option (program via UART instead of ISP)
- ✅ Larger PCB footprint allows better ISP header access

### 2.1a ISP Programming Header

**Standard 6-pin ISP Header Layout:**
```
    ┌─────────────┐
    │ 1  2  3  4  │
    │ 5  6       │
    └─────────────┘
     ISP Header (top view)
```

| Header Pin | Signal | ATmega328P Pin | Arduino Pin | Connect |
|------------|--------|----------------|-------------|---------|
| 1 | MOSI | PB3 (pin 17) | 11 | SPI Data In |
| 2 | VCC | Pin 7 | Power | Power supply |
| 3 | SCK | PB5 (pin 19) | 13 | SPI Clock |
| 4 | GND | Pin 8/22 | GND | Ground |
| 5 | MISO | PB4 (pin 18) | 12 | SPI Data Out |
| 6 | RESET | PC6 (pin 1) | RESET | Reset signal |

**ISP Pin Notes:**
- ISP pins separate from I2C (no conflicts!)
- Can keep I2C running during ISP programming
- Serial bootloader option (program via UART instead of ISP)

**Recommended PCB Layout:**
- Add 6-pin ISP header (standard 2×3 pin spacing: 100 mil)
- Add 6-pin FTDI serial header for bootloader reprogramming (optional)
- Mount headers on PCB edge for easy access
- Label pins clearly to match programmer cable

### 2.1b Memory Key & Button Input Configuration

**Display Driver Memory Keys (Direct Connection to ATmega328P):**

```
Display Driver:           ATmega328P:
Pin 2 (CLK)      ------→  PB2 (pin 16)  [CLK]
Pin 3 (DIO)      ------→  PC0 (pin 23)  [DIO]
Pin 7 (KEY_COMMON) -----→  PD7 (pin 13)  [Key Common]
Pin 8 (KEY_3)    ------→  PD6 (pin 12)  [Key 3]
Pin 9 (KEY_2)    ------→  PD5 (pin 11)  [Key 2]
Pin 13 (KEY_1)   ------→  PD4 (pin 6)   [Key 1]
```

**Desk Control Buttons (Direct Connection to ATmega328P):**

```
UP Button (desk)   ----→  PC2 (pin 24)  [UP]
DOWN Button (desk) ----→  PC3 (pin 25)  [DOWN]
```

**All button inputs use internal pull-ups** (configured via `INPUT_PULLUP` in setup).

**Button State Register (0x04):**
```
Bit 0: KEY_1 (from memory matrix)
Bit 1: KEY_2 (from memory matrix)
Bit 2: KEY_3 (from memory matrix)
Bit 3: UP Button (direct connection)
Bit 4: DOWN Button (direct connection)
Bits 7-5: Reserved
```

**Key Debouncing:**
- Software debounce: 20ms minimum between reads
- Optional: Add 100nF capacitor between each button pin and GND for hardware debounce

**Input Logic (Active LOW):**
```cpp
// Memory keys: KEY_COMMON pulled LOW enables the matrix
// When a key is pressed, that KEY_X line goes LOW
if (digitalRead(KEY_COMMON) == LOW) {
    if (digitalRead(KEY_1) == LOW) button_state |= 0x01;  // Bit 0
    if (digitalRead(KEY_2) == LOW) button_state |= 0x02;  // Bit 1
    if (digitalRead(KEY_3) == LOW) button_state |= 0x04;  // Bit 2
}

// UP/DOWN buttons: Direct connections, also active LOW
if (digitalRead(UP_BTN) == LOW) button_state |= 0x08;    // Bit 3
if (digitalRead(DOWN_BTN) == LOW) button_state |= 0x10;  // Bit 4
```

**Pin Definitions (C++):**
```cpp
#define CLK_PIN PB2           // Pin 16
#define DIO_PIN PC0           // Pin 23
#define KEY_1 PD4             // Pin 6
#define KEY_2 PD5             // Pin 11
#define KEY_3 PD6             // Pin 12
#define KEY_COMMON PD7        // Pin 13
#define UP_BTN PC2            // Pin 24
#define DOWN_BTN PC3          // Pin 25
#define LED_PIN PC1           // Pin 21
```

### 2.1c Minimal Wiring Diagram

```
┌────────────────────────────────────────────────────────────────┐
│                 Display Driver Board                           │
│   Pin 2 (CLK)     Pin 3 (DIO)                                  │
│   Pin 7,8,9,13 (KEY matrix)                                    │
│        │               │               │ │ │ │                │
│        │               │               │ │ │ └─┐              │
│        │               │               │ │ └──┬┤              │
│        │               │               │ └────┼┤              │
│        │               │               └──────┼┤              │
│        ▼               ▼                       ▼ ▼ ▼ ▼         │
│     ATmega328P (28-pin QFN)                                    │
│     ┌──────────────────────────────────┐                      │
│     │ Pin 16 PB2 (CLK)                 │                      │
│     │ Pin 23 PC0 (DIO)                 │                      │
│     │ Pin 6  PD4 (KEY_1)               │                      │
│     │ Pin 11 PD5 (KEY_2)               │                      │
│     │ Pin 12 PD6 (KEY_3)               │                      │
│     │ Pin 13 PD7 (KEY_COMMON)          │                      │
│     │ Pin 21 PC1 (LED) ───[330Ω]────→ │ ●  LED (active HIGH) │
│     │ Pin 24 PC2 (UP)   ──→ ○ UP BTN   │                      │
│     │ Pin 25 PC3 (DOWN) ──→ ○ DOWN BTN │                      │
│     │ Pin 14 PB0 (I2C SCL) ┬──────┐    │                      │
│     │ Pin 15 PB1 (I2C SDA) │      │    │                      │
│     │ [4.7k pull-ups]      │      │    │                      │
│     │ Pin 7 VCC ◄──────────┴──┬───────→ 3.3V                  │
│     │ Pin 8 GND ────────────┬─┴────→ GND                       │
│     │ Pin 17 PB3 ──┐   [ISP]│                                 │
│     │ Pin 18 PB4 ──┼─────┤  │                                 │
│     │ Pin 19 PB5 ──┤     │  │                                 │
│     │ Pin 1  PC6 ──┘     │  │                                 │
│     └─────────────────────┼──┼─────────┘                       │
│                           │  │                                 │
│                           │  └────────→ ESP32-S3 GPIO21 (SCL) │
│                           └──────────→ ESP32-S3 GPIO20 (SDA)   │
│                                                                 │
│                      [ISP Programmer]                           │
│                      ├─ Pin 1: MOSI                            │
│                      ├─ Pin 2: VCC                             │
│                      ├─ Pin 3: SCK                             │
│                      ├─ Pin 4: GND                             │
│                      ├─ Pin 5: MISO                            │
│                      └─ Pin 6: RESET                           │
└────────────────────────────────────────────────────────────────┘
```

**Connections Summary:**
- **Display Protocol:** CLK (Pin 2) → PB2, DIO (Pin 3) → PC0
- **Memory Keys:** KEY_COMMON (Pin 7) → PD7, KEY_1/2/3 (Pins 13/9/8) → PD4/5/6
- **UP/DOWN Buttons:** Desk buttons → PC2/PC3 (active LOW, pull-ups enabled)
- **Activity LED:** PC1 → 330Ω resistor → LED anode (cathode to GND)
- **I2C to ESP32:** PB0/PB1 → GPIO21/20 with 4.7k pull-ups
- **ISP Programmer:** 6-pin header for initial firmware flashing
- **Serial Debug:** PD0/PD1 optional for USB-UART debugging

---

## 3. ATmega328P Firmware Architecture

### 3.1 Software Structure

```
┌─────────────────────────────────┐
│   Main Loop (State Machine)     │
│  - Monitor CLK interrupts       │
│  - Decode protocol frames       │
│  - Update display registers     │
│  - Poll memory keys             │
│  - Service I2C requests         │
└────────────┬────────────────────┘
             │
      ┌──────┴──────┬──────────┬──────────┐
      │             │          │          │
      ▼             ▼          ▼          ▼
   ┌──────┐  ┌─────────┐ ┌───────┐ ┌────────┐
   │ CLK  │  │ DIO     │ │ Frame │ │ I2C    │
   │ISR   │  │Sampling │ │Parser │ │Handler │
   └──────┘  └─────────┘ └───────┘ └────────┘
```

### 3.2 I2C Register Map

**Slave Address:** 0x50 (0xA0 write, 0xA1 read in standard notation)

| Addr | Name | Type | Value | Description |
|------|------|------|-------|-------------|
| 0x00 | DIG1 | byte | 0-9 | Display digit 1 (hundreds) - decoded value |
| 0x01 | DIG2 | byte | 0-9 | Display digit 2 (tens) - decoded value |
| 0x02 | DIG3 | byte | 0-9 | Display digit 3 (ones) - decoded value |
| 0x03 | STAT | byte | bits | Display status (power, brightness, mode) |
| 0x04 | BTNS | byte | bits | Button states via AiP650E (if available) |
| 0x10 | VERSION | byte | 0x10 | Firmware version (v1.0) |
| 0x11 | ERRORS | byte | bits | Error/status flags |

**STAT Byte (0x03):**
```
Bit 7: Display Power (1=on, 0=off)
Bit 6-4: Brightness (0-7)
Bit 3: SEG Mode (1=7-segment, 0=8-segment)
Bit 2: Sleep Mode (1=enabled, 0=disabled)
Bit 1-0: Reserved
```

**ERRORS Byte (0x11):**
```
Bit 7: I2C Comm Error (1=error, 0=ok)
Bit 6: Frame Timeout (1=no CLK for >1s, 0=ok)
Bit 5: Protocol Error (1=malformed frame, 0=ok)
Bit 4: Checksum Mismatch (1=error, 0=ok)
Bit 3-0: Reserved
```

### 3.3 Protocol Capture State Machine

```
IDLE
  ↓
  [Wait for START condition: CLK high, DIO falling edge]
  ↓
RECEIVING
  ├─ Clock bit counter = 0
  ├─ Byte buffer = 0
  ├─ [For each CLK rising edge]
  │  ├─ Sample DIO
  │  ├─ Shift bit into buffer
  │  ├─ Increment counter
  │  └─ [If counter = 8]
  │     ├─ Store byte
  │     ├─ Reset counter
  │     └─ [Decode command if 2 bytes received]
  │        ├─ Check if display data write (RAM addr 0x68/0x6A/0x6C)
  │        ├─ If yes: Update shadow RAM (NEW data from original micro)
  │        └─ If no (brightness/mode/status): Just track state
  │
  └─ [Wait for STOP condition: CLK high, DIO rising edge]
  ↓
IDLE
```

### 3.4 Shadow RAM & Display Blanking Logic

**Problem:** When the original micro blanks the display (e.g., sleep timeout), it may:
- Reduce brightness to 0 (segment data still valid but not displayed)
- Clear the segment RAM entirely (segment data becomes 0x00 or noise)

**Solution:** ATtiny85 maintains a "shadow" copy of the last VALID display data:

```cpp
// Persistent shadow RAM (only updated on ACTUAL data writes, not blanking)
volatile uint8_t shadow_dig1 = 0xFF;
volatile uint8_t shadow_dig2 = 0xFF;
volatile uint8_t shadow_dig3 = 0xFF;

// Exposed to ESP32 (never shows blanked/corrupt data)
uint8_t dig1 = 0xFF;
uint8_t dig2 = 0xFF;
uint8_t dig3 = 0xFF;

// Last known state for detecting changes
volatile uint8_t last_display_stat = 0x00;
volatile uint8_t last_brightness = 0xFF;
```

**Detection Algorithm:**

1. When a write to RAM address 0x68/0x6A/0x6C is detected:
   - Extract segment data
   - Check if it's different from ALL ZEROS (0x00)
   - If non-zero: It's real data → update shadow RAM
   - If zero: Could be blanking → ignore (keep shadow RAM)

2. If brightness drops to 0 (display going dark):
   - Don't clear shadow RAM
   - Keep last known display value available

3. ESP32 always reads from shadow RAM:
   - Gets last actual displayed value
   - Not affected by display blanking
   - Shows "250" even after display goes dark

**Pseudocode:**
```cpp
void on_display_data_write(uint8_t ram_addr, uint8_t segment_byte) {
    uint8_t decoded_digit = decode_segment(segment_byte);
    
    if (ram_addr == 0x68) {
        // DIG1 write
        if (decoded_digit != 0xFF) {  // Valid digit (not all zeros/noise)
            shadow_dig1 = decoded_digit;
        }
        // If 0xFF or 0x00, don't update shadow (keep last value)
    }
    else if (ram_addr == 0x6A) {
        // DIG2 write
        if (decoded_digit != 0xFF) {
            shadow_dig2 = decoded_digit;
        }
    }
    else if (ram_addr == 0x6C) {
        // DIG3 write
        if (decoded_digit != 0xFF) {
            shadow_dig3 = decoded_digit;
        }
    }
}

void i2c_request() {
    // Always return shadow RAM values (not affected by display blanking)
    switch (reg_ptr) {
        case 0x00: TinyWireS.send(shadow_dig1); break;
        case 0x01: TinyWireS.send(shadow_dig2); break;
        case 0x02: TinyWireS.send(shadow_dig3); break;
        // ... etc
    }
}
```

---

## 4. Implementation Notes

### 4.1 Display Blanking Behavior (Important!)

The ATtiny85 implements **shadow RAM** to handle display blanking gracefully:

**Scenario:** Original desk controller blanks the display after timeout (power saving):
- What happens: Brightness reduced to 0 or segment RAM cleared
- Without shadow RAM: ESP32 would read 0xFF or corrupted data
- With shadow RAM: **ESP32 always sees the last valid display value**

**Example:**
```
Time 0:    Display shows "250" → shadow_dig1=2, shadow_dig2=5, shadow_dig3=0
Time 60s:  Display blanks (sleep timeout) → brightness=0
Time 60s:  ESP32 reads registers → gets "250" (from shadow RAM, not blanked display!)
Time 120s: User presses button, display wakes → shows "250" again
Time 120s: ESP32 still reads "250" (no interruption)
```

**Shadow RAM Logic:**
1. When a display data write is detected (RAM addr 0x68/0x6A/0x6C):
   - Decode the segment data to a digit (0-9)
   - If valid digit: Update shadow RAM
   - If invalid (0xFF or 0x00): Skip update, keep existing shadow value

2. ESP32 always reads from shadow RAM (not live display data)
   - Immune to display blanking
   - Shows persistent height value
   - Updates only when original micro writes new data

**Benefits:**
- ✅ No "NaN" or 0xFF errors on display blanking
- ✅ ESP32 touchscreen shows last height even when original display is dark
- ✅ Seamless user experience during sleep mode

### 4.2 Microcontroller Configuration

#### ATmega328P Fuses (Internal 8 MHz option):
```
lfuse: 0xE2 (8 MHz internal oscillator, ~5% accuracy)
hfuse: 0xDA (BOD disabled, reset enabled)
efuse: 0xFF (default)
```
- **Oscillator:** Internal 8 MHz (no crystal needed for I2C/serial debug)
- **RESET:** Enabled (required for ISP programming)
- **Bootloader:** Optional (if you want serial reprogramming)

**OR with External 16 MHz Crystal (higher precision):**
```
lfuse: 0xFF (external crystal)
hfuse: 0xDE (BOD disabled, reset enabled, bootloader space)
efuse: 0xFF (default)
```
- Add 16 MHz crystal between XTAL1/XTAL2 with 22pF caps to GND
- Allows precise serial baud rates and higher clock speed

### 4.3 CLK Interrupt Handler

```cpp
// Interrupt on CLK rising edge (PB2 = PCINT2)
ISR(PCINT0_vect) {
    if (digitalRead(CLK_PIN) == HIGH && prev_clk == LOW) {
        // Rising edge detected
        uint8_t dio_bit = digitalRead(DIO_PIN);
        
        // Store bit in shift register
        bit_buffer <<= 1;
        bit_buffer |= (dio_bit & 1);
        
        bit_count++;
        
        if (bit_count == 8) {
            // Full byte received
            byte_buffer[byte_count++] = bit_buffer;
            bit_count = 0;
            
            if (byte_count == 2) {
                // Frame complete, parse it
                parse_command(byte_buffer);
                byte_count = 0;
            }
        }
    }
    prev_clk = digitalRead(CLK_PIN);
}
```

### 4.4 I2C Slave Handler

```cpp
// ATmega328P I2C handler using Wire library
#include <Wire.h>

#define I2C_ADDR 0x50

void setup() {
    Wire.begin(I2C_ADDR);
    Wire.onReceive(i2c_receive);
    Wire.onRequest(i2c_request);
}

void i2c_receive(uint8_t count) {
    if (Wire.available() >= 1) {
        reg_ptr = Wire.read();  // Get register address
    }
}

void i2c_request() {
    uint8_t value;
    
    switch (reg_ptr) {
        case 0x00: value = shadow_dig1; break;
        case 0x01: value = shadow_dig2; break;
        case 0x02: value = shadow_dig3; break;
        case 0x03: value = display_stat; break;
        case 0x04: value = button_state; break;  // Memory key state
        case 0x10: value = 0x10; break;  // VERSION
        case 0x11: value = error_flags; break;
        default: value = 0xFF; break;
    }
    
    Wire.write(value);
    reg_ptr++;  // Auto-increment for sequential reads
}
```

### 4.5 7-Segment Decoder

```cpp
// Decode segment byte to digit value
uint8_t decode_segment(uint8_t segments) {
    // segments format: bit0=A, bit1=B, ... bit7=DP
    const uint8_t digit_patterns[10] = {
        0x3F,  // 0: A B C D E F (no G, no DP)
        0x06,  // 1: B C
        0x5B,  // 2: A B D E G
        0x4F,  // 3: A B C D G
        0x66,  // 4: B C F G
        0x6D,  // 5: A C D F G
        0x7D,  // 6: A C D E F G
        0x07,  // 7: A B C
        0x7F,  // 8: A B C D E F G
        0x6F   // 9: A B C D F G
    };
    
    uint8_t seg_no_dp = segments & 0x7F;  // Mask off DP
    
    for (uint8_t i = 0; i < 10; i++) {
        if (digit_patterns[i] == seg_no_dp) {
            return i;
        }
    }
    
    return 0xFF;  // Unknown pattern
}
```

### 4.5 Memory Usage

```
ATtiny85 Resources:
- Flash: 8 KB total
  - Used: ~2-3 KB (protocol + I2C handler)
  - Available: ~5-6 KB
  
- SRAM: 512 bytes total
  - Used: ~150-200 bytes
  - Available: ~300+ bytes
  
- EEPROM: 512 bytes (unused)
```

---

## 5. Build & Programming

### 5.1 Arduino IDE Setup

1. Install standard AVR support (pre-installed with Arduino IDE)
2. Board: "Arduino Uno" (uses same ATmega328P)
3. Clock: "16 MHz" (with external oscillator) OR "8 MHz" (internal, if you add fuse change)
4. **Programmer:** Select your ISP programmer or use serial bootloader
   - ISP: USbasp, Arduino as ISP, AVRISP mkII
   - **Serial:** "Arduino" (if bootloader pre-programmed)

**Required Libraries:**
```cpp
#include <Wire.h>  // Standard I2C for ATmega328P
#include <HardwareSerial.h>  // Serial for debugging (optional)
```

### 5.2 ISP Programmer Setup

**Common ISP Programmers:**

| Programmer | Cost | Speed | Notes |
|------------|------|-------|-------|
| USbasp | $2-5 | ~370 kHz | Most affordable, widely available |
| Arduino as ISP | $0 | ~125 kHz | Use spare Arduino Uno/Nano |
| AVRISP mkII | $40-50 | ~375 kHz | Official Atmel programmer |
| USBISP | $5-10 | ~375 kHz | Similar to USbasp |

**Connection (Standard 6-pin ISP Header):**

```
        ISP Programmer
        (USB connector)
              │
         ┌────┴────┐
         │ ISP Cable │ 6-wire ribbon
         └────┬────┘
              │
    ┌─────────────────────┐
    │  Interposer PCB     │
    │ ┌───────────────┐   │
    │ │ ISP Header    │   │
    │ │ (6-pin box)   │   │
    │ │               │   │
    │ │ 1  2  3  4    │   │
    │ │ 5  6         │   │
    │ └───────────────┘   │
    │                     │
    │ [Microcontroller]   │
    └─────────────────────┘
```

### 5.3 Programming Method

#### ISP Programming (Initial Setup)

```bash
# Option 1: Arduino IDE (easiest)
# 1. Connect USbasp/Arduino to ISP header
# 2. Tools → Programmer → [select your programmer]
# 3. Tools → Burn Bootloader (sets fuses + installs bootloader)
# 4. Sketch → Upload Using Programmer (or Ctrl+Shift+U)

# Option 2: Command line with avrdude
avrdude -c usbasp -p m328p -U flash:w:firmware.hex:i \
        -U lfuse:w:0xe2:m -U hfuse:w:0xda:m

# Option 3: Verify ISP connection first
avrdude -c usbasp -p m328p -v
```

#### Serial Bootloader Reprogramming (After Initial ISP)

```bash
# After initial ISP programming with bootloader:
# 1. Connect USB-UART adapter to PD0 (RXD) and PD1 (TXD)
# 2. Select Tools → Programmer → "Arduino"
# 3. Sketch → Upload (standard upload, not "Upload Using Programmer")
# 4. Baud rate should auto-detect

# Much faster than ISP! Reuses Arduino serial bootloader.
```

**⚠️ Important: ISP Header Labels**

Always label your ISP header clearly on the PCB:
```
[ISP Header]
┌─────────────────┐
│  MOSI  VCC      │  (Pin 1: MOSI, Pin 2: VCC)
│  SCK   GND      │  (Pin 3: SCK, Pin 4: GND)
│  MISO  RESET    │  (Pin 5: MISO, Pin 6: RESET)
└─────────────────┘
   Standard 6-pin ISP pinout
```

### 5.4 Troubleshooting ISP

| Problem | Cause | Solution |
|---------|-------|----------|
| "Cannot find programmer" | Wrong programmer selected | Tools → Programmer → check selection |
| ISP connection fails | Bad cable or header | Test with multimeter, reseat header |
| "Fuse mismatch" | Fuses different than expected | Use correct fuses for 8 MHz or 16 MHz |
| Extremely slow programming | ISP speed too fast | Add -B flag: `-B 10` to avrdude |
| "Timeout talking to programmer" | USB/serial issue | Try different USB port, different cable |

---

## 6. Firmware Code Template

```cpp
#include <avr/interrupt.h>
#include <avr/io.h>
#include <Wire.h>

// Pin definitions (ATmega328P 28-pin QFN)
#define CLK_PIN PB2           // Pin 16 (PCINT2)
#define DIO_PIN PC0           // Pin 23 (input)
#define KEY_1 PD4             // Pin 6 (input)
#define KEY_2 PD5             // Pin 11 (input)
#define KEY_3 PD6             // Pin 12 (input)
#define KEY_COMMON PD7        // Pin 13 (input)
#define LED_PIN PC1           // Pin 21 (output, activity indicator)
#define UP_BTN PC2            // Pin 24 (input, UP button)
#define DOWN_BTN PC3          // Pin 25 (input, DOWN button)

// I2C configuration
#define I2C_ADDR 0x50

// Display data (exposed via I2C to ESP32)
volatile uint8_t dig1 = 0xFF, dig2 = 0xFF, dig3 = 0xFF;

// Shadow RAM - persistent storage of last VALID display data
// Only updated when original micro writes NEW data, NOT when display blanks
volatile uint8_t shadow_dig1 = 0xFF, shadow_dig2 = 0xFF, shadow_dig3 = 0xFF;

volatile uint8_t display_stat = 0x00;
volatile uint8_t button_state = 0x00;
volatile uint8_t error_flags = 0x00;

// Protocol state
volatile uint8_t bit_buffer = 0;
volatile uint8_t bit_count = 0;
volatile uint8_t byte_buffer[2];
volatile uint8_t byte_count = 0;
volatile uint8_t prev_clk = 0;
volatile uint32_t last_clk_time = 0;

// Display write tracking (to distinguish data writes from display blanking)
volatile uint8_t last_ram_addr = 0xFF;
volatile uint8_t last_segment_data = 0xFF;

uint8_t reg_ptr = 0;

// 7-segment decoder lookup
const uint8_t digit_patterns[10] = {
    0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};

// ===== 7-Segment Decoder =====
uint8_t decode_segment(uint8_t segments) {
    uint8_t seg_no_dp = segments & 0x7F;  // Mask off DP bit
    
    for (uint8_t i = 0; i < 10; i++) {
        if (digit_patterns[i] == seg_no_dp) {
            return i;  // Return digit 0-9
        }
    }
    
    return 0xFF;  // Unknown/invalid pattern
}

// ===== ISR: CLK Pin Change =====
ISR(PCINT0_vect) {
    if (digitalRead(CLK_PIN) && !prev_clk) {
        // CLK rising edge
        uint8_t dio_bit = digitalRead(DIO_PIN);
        bit_buffer = (bit_buffer << 1) | (dio_bit & 1);
        bit_count++;
        last_clk_time = millis();
        
        if (bit_count == 8) {
            byte_buffer[byte_count++] = bit_buffer;
            bit_count = 0;
            
            if (byte_count == 2) {
                parse_command();
                byte_count = 0;
            }
        }
    }
    prev_clk = digitalRead(CLK_PIN);
}

// ===== Parse AiP650E Command =====
void parse_command() {
    uint8_t sys_instr = byte_buffer[0];
    uint8_t disp_instr = byte_buffer[1];
    
    if (sys_instr == 0x48) {  // System instruction
        // Parse display instruction
        if (bit_is_clear(disp_instr, 0)) {
            display_stat &= ~0x80;  // Display OFF
        } else {
            display_stat |= 0x80;   // Display ON
        }
        
        // Brightness (bits 5-3)
        uint8_t brightness = (disp_instr >> 3) & 0x07;
        display_stat = (display_stat & 0x8F) | (brightness << 4);
        
        // SEG mode (bit 3 of disp_instr -> bit 3 of stat)
        if (bit_is_set(disp_instr, 3)) {
            display_stat |= 0x08;   // 7-segment
        } else {
            display_stat &= ~0x08;  // 8-segment
        }
    }
    else if (sys_instr >= 0x68 && sys_instr <= 0x6E) {
        // Display data write to RAM (0x68=DIG1, 0x6A=DIG2, 0x6C=DIG3)
        uint8_t segment_data = disp_instr;
        uint8_t decoded = decode_segment(segment_data);
        
        // SHADOW RAM LOGIC:
        // Only update if we get a valid digit (0-9)
        // Ignore if we get 0xFF (unknown) or 0x00 (blanking/noise)
        
        if (sys_instr == 0x68) {
            if (decoded != 0xFF) {  // Valid digit
                shadow_dig1 = decoded;
                dig1 = decoded;  // Also update exposed register
            }
            // If 0xFF or blank, keep shadow_dig1 unchanged
        }
        else if (sys_instr == 0x6A) {
            if (decoded != 0xFF) {
                shadow_dig2 = decoded;
                dig2 = decoded;
            }
        }
        else if (sys_instr == 0x6C) {
            if (decoded != 0xFF) {
                shadow_dig3 = decoded;
                dig3 = decoded;
            }
        }
        
        last_ram_addr = sys_instr;
        last_segment_data = segment_data;
    }
}

// ===== I2C Receive Handler =====
void i2c_receive(uint8_t count) {
    if (Wire.available() >= 1) {
        reg_ptr = Wire.read();  // Get register address
    }
}

// ===== I2C Request Handler =====
void i2c_request() {
    uint8_t value = 0xFF;
    
    switch (reg_ptr) {
        // Return shadow RAM values (persistent across display blanking)
        case 0x00: value = shadow_dig1; break;
        case 0x01: value = shadow_dig2; break;
        case 0x02: value = shadow_dig3; break;
        
        // Status always returns current state
        case 0x03: value = display_stat; break;
        case 0x04: value = button_state; break;  // Memory key state
        case 0x10: value = 0x10; break;  // VERSION
        case 0x11: value = error_flags; break;
        default: value = 0xFF; break;
    }
    
    Wire.write(value);
    reg_ptr++;  // Auto-increment for sequential reads
}

// ===== Setup =====
void setup() {
    // Configure input pins
    pinMode(CLK_PIN, INPUT);
    pinMode(DIO_PIN, INPUT);
    pinMode(KEY_1, INPUT_PULLUP);
    pinMode(KEY_2, INPUT_PULLUP);
    pinMode(KEY_3, INPUT_PULLUP);
    pinMode(KEY_COMMON, INPUT_PULLUP);
    pinMode(UP_BTN, INPUT_PULLUP);   // UP button input
    pinMode(DOWN_BTN, INPUT_PULLUP); // DOWN button input
    
    // Configure output pins
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);      // LED off at startup
    
    // Enable pin change interrupt on CLK (PB2 = PCINT2)
    // PCINT group 0 covers PB0-PB7
    PCMSK0 |= (1 << PCINT2);
    PCIFR |= (1 << PCIF0);
    PCICR |= (1 << PCIE0);
    
    // Initialize I2C slave
    Wire.begin(I2C_ADDR);
    Wire.onReceive(i2c_receive);
    Wire.onRequest(i2c_request);
    
    // Enable serial for debugging (optional)
    Serial.begin(9600);
    
    sei();  // Enable interrupts
}

// ===== Main Loop =====
void loop() {
    // Check for bus timeout
    if ((millis() - last_clk_time) > 1000) {
        error_flags |= 0x40;  // Frame timeout
    }
    
    // Poll memory keys (debounce: read every 20ms)
    static unsigned long last_key_read = 0;
    if (millis() - last_key_read > 20) {
        last_key_read = millis();
        
        uint8_t new_key_state = 0;
        if (digitalRead(KEY_COMMON) == LOW) {
            if (digitalRead(KEY_1) == LOW) new_key_state |= 0x01;
            if (digitalRead(KEY_2) == LOW) new_key_state |= 0x02;
            if (digitalRead(KEY_3) == LOW) new_key_state |= 0x04;
        }
        
        // Read UP/DOWN buttons (direct connections)
        if (digitalRead(UP_BTN) == LOW) new_key_state |= 0x08;    // Bit 3: UP
        if (digitalRead(DOWN_BTN) == LOW) new_key_state |= 0x10;  // Bit 4: DOWN
        
        // Only update on state change
        if (new_key_state != button_state) {
            button_state = new_key_state;
            // Optional: log key press to serial
            // Serial.print("Button state: 0x"); Serial.println(button_state, HEX);
        }
    }
    
    // Activity LED blinking (200ms toggle = visual indicator of protocol activity)
    static unsigned long last_led_toggle = 0;
    if (millis() - last_led_toggle > 200) {
        digitalWrite(LED_PIN, !digitalRead(LED_PIN));
        last_led_toggle = millis();
    }
}
```

---

## 7. Debugging & Troubleshooting

### 7.1 Common Issues

| Problem | Cause | Solution |
|---------|-------|----------|
| I2C not responding | Address wrong or I2C not initialized | Check I2C address (0x50), verify Wire.h setup |
| LED not blinking | LED pin (PC1) not configured or LED polarity wrong | Verify pin 21 is configured as OUTPUT, check LED polarity (anode to PC1) |
| CLK interrupt never fires | Pin change interrupt not enabled | Check PCMSK0 |= (1<<PCINT2) and PCICR setup |
| Display values always 0xFF | Protocol not being decoded | Check CLK/DIO connections, verify PCINT interrupt fires |
| I2C reads return 0xFF | Register not implemented | Ensure i2c_request() switch statement covers all register addresses |
| Memory keys not detected | KEY pins not connected or pull-ups not enabled | Verify KEY_COMMON/1/2/3 pull-ups enabled, check connections to display board |
| UP/DOWN buttons not working | Button pins (PC2/PC3) not configured or buttons not connected | Verify pins 24/25 are INPUT_PULLUP, check button wiring and polarity |
| Buttons register as always pressed | Pull-ups not working or button stuck | Check PORTC pull-up settings, verify buttons work mechanically |
| ATmega328P won't program via ISP | Fuses incorrect or ISP speed too fast | Reduce ISP speed, verify ISP header connections, use -B flag in avrdude |

### 7.2 Testing Checklist

- [ ] ATmega328P powers on and LED blinks (visual indicator)
- [ ] I2C responds at address 0x50 with correct VERSION (0x10)
- [ ] CLK pin shows activity on oscilloscope (protocol capture working)
- [ ] Registers 0x00-0x02 update when desk display shows new heights
- [ ] Register 0x03 updates with display brightness/status changes
- [ ] Register 0x04 (BTNS) reflects button state:
  - [ ] Memory keys (bits 0-2) change when pressed via matrix
  - [ ] UP button (bit 3) toggles when pressed
  - [ ] DOWN button (bit 4) toggles when pressed
- [ ] Error flags (0x11) set when protocol bus inactive >1 second
- [ ] **Shadow RAM test:** After display blanks (sleep timeout), ESP32 still reads last height value
  - [ ] Original desk display goes dark, but ESP32 I2C reads show "250" (not 0xFF)
  - [ ] Display wakes up, shows "250" again
  - [ ] Values persist across blanking events
- [ ] LED blinks continuously at ~2.5Hz (200ms toggle rate) during normal operation
- [ ] ISP programming works for firmware updates
- [ ] Serial debug output shows key presses (optional)

---

## 8. Hardware Bill of Materials

| Component | Value | Qty | Notes |
|-----------|-------|-----|-------|
| ATmega328P | 28-pin QFN | 1 | Microchip ATmega328P (primary microcontroller) |
| LED | Any color (RED/GREEN) | 1 | Activity indicator (common cathode) |
| Resistor | 330Ω | 1 | LED current limiting (for 3.3V) |
| Resistor | 4.7k | 2 | I2C pull-ups (SCL/SDA to 3.3V) |
| Resistor | 10k | 2 | Optional: button pull-ups (if weak internal pull-ups needed) |
| Capacitor | 100nF | 4 | Decoupling (VCC), optional key input filtering |
| Button | Tactile 6mm | 2 | UP and DOWN buttons (momentary, normally open) |
| ISP Header | 6-pin | 1 | ISP programming connector (standard 2×3) |
| Serial Header | 6-pin FTDI | 1 | Optional: USB-UART debugging (PD0/PD1) |
| PCB | — | 1 | Breadboard prototype or custom PCB interposer |

**Breadboard Prototype Cost:** ~$3-5  
**Custom PCB Interposer Cost:** ~$15-25 (including ISP header, serial header, resistors, capacitors)

---

## 9. Future Enhancements

- [ ] Handle memory button states via AiP650E key matrix
- [ ] Add CRC8 checksum for data integrity
- [ ] Implement watchdog timer for crash recovery
- [ ] Add EEPROM storage for statistics
- [ ] Support multiple display protocols (not just AiP650E)

