# 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 #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 // Standard I2C for ATmega328P #include // 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 #include #include // 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<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)