1015 lines
36 KiB
Markdown
1015 lines
36 KiB
Markdown
# 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)
|
||
|