Files
deskscreen/FW/ATTINY85_PROXY_FIRMWARE.md
T
2026-05-13 11:45:58 +02:00

1015 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)