M3: 5 autonomous LP5562 engine patterns (breathe, heartbeat, slow_pulse, rgb_cycle, color_wash) with dual LED mode support (RGBW/Mono3) and 2mA/ch current for EH-realistic power budget. M4: NTAG5Link I2C slave driver with session register reads, EEPROM write-verify, and NDEF Type 5 text record output. Config check validates CONFIG_0, CONFIG_1, EH_CONFIG against expected values and writes result as NFC-readable text. Verified on hardware: config check passes, NDEF readable via ISO15693 from phone. Key findings from hardware testing: - SRAM passthrough arbiter blocks NFC reads while I2C bus is active (resolves naturally when MCU enters STANDBY — noted in M7 plan) - NTAG5 must be in I2C slave mode (not master) for MCU communication - EH config mask set to 0x00 (skip check during external power testing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
676 lines
28 KiB
Markdown
676 lines
28 KiB
Markdown
# LP5562 Engine Programming Reference
|
||
|
||
Reference for the React Native companion app pattern editor. Covers the LP5562's 16-bit command encoding, timing math, and program construction rules. Everything here is verified against the LP5562 datasheet (SNVS820B, Rev Dec 2016) and tested on hardware with the LP5562EVM.
|
||
|
||
## Hardware Overview
|
||
|
||
The LP5562 has 3 independent execution engines and 4 LED channels (R, G, B, W). Each engine:
|
||
|
||
- Has its own SRAM holding up to **16 commands** (16-bit words, 32 bytes)
|
||
- Runs autonomously on the LP5562's internal 32.768 kHz oscillator
|
||
- Can be mapped to drive any combination of LED channels
|
||
- Continues running even when the host MCU is sleeping
|
||
|
||
The W channel cannot be engine-driven — it is always I2C-direct.
|
||
|
||
### LED Channel Mapping
|
||
|
||
Each of the 4 channels (B, G, R, W) can be independently assigned to:
|
||
|
||
| Mapping | Value | Meaning |
|
||
|------------|-------|----------------------------------------------|
|
||
| `I2c` | 0x00 | Host sets PWM directly via I2C register write |
|
||
| `Engine1` | 0x01 | Engine 1 controls this channel's PWM |
|
||
| `Engine2` | 0x02 | Engine 2 controls this channel's PWM |
|
||
| `Engine3` | 0x03 | Engine 3 controls this channel's PWM |
|
||
|
||
Written to the LED_MAP register (0x70). Each channel gets 2 bits.
|
||
|
||
**Single-engine patterns** (breathe, heartbeat, pulse): map B, G, R all to the same engine — they breathe in unison. W stays I2C-direct.
|
||
|
||
**Multi-engine patterns** (rgb_cycle, color_wash): map each color channel to a different engine — each color animates independently.
|
||
|
||
---
|
||
|
||
## Command Encoding
|
||
|
||
All engine commands are 16-bit words stored in SRAM as big-endian (MSB at lower address). The top 1-3 bits determine the command type.
|
||
|
||
### Command Type Identification
|
||
|
||
| Bits 15:13 | Type | Description |
|
||
|------------|------------|----------------------------------|
|
||
| `0xx` | Ramp/Wait | Bit 15 = 0. Ramp or wait. |
|
||
| `0xx` | Set PWM | Special case: step_time = 0. |
|
||
| `000` | Go to Start| All zeros = reset PC to 0. |
|
||
| `101` | Branch | Jump to step with loop count. |
|
||
| `110` | End | Stop engine, optional interrupt. |
|
||
| `111` | Trigger | Inter-engine synchronization. |
|
||
|
||
### 1. Ramp/Wait Command
|
||
|
||
The most important command. Controls smooth PWM transitions and timed waits.
|
||
|
||
```
|
||
Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
|
||
0 pre- ├──── step_time ────┤ sign ├──── increment ────────────┤
|
||
scale (6 bits) (7 bits)
|
||
```
|
||
|
||
| Field | Bits | Range | Description |
|
||
|------------|-------|--------|-----------------------------------------------------------|
|
||
| (fixed) | 15 | 0 | Always 0 for ramp commands |
|
||
| prescale | 14 | 0-1 | 0 = Fast (0.49ms), 1 = Slow (15.6ms) |
|
||
| step_time | 13:8 | 0-63 | Multiplier for prescale period. **0 = Set PWM command.** |
|
||
| sign | 7 | 0-1 | 0 = ramp up (increase PWM), 1 = ramp down (decrease PWM) |
|
||
| increment | 6:0 | 0-127 | Number of steps - 1. **0 = pure wait (no PWM change).** |
|
||
|
||
**Encoding formula:**
|
||
|
||
```
|
||
word = (prescale << 14) | (step_time << 8) | (sign << 7) | increment
|
||
```
|
||
|
||
#### Timing Math
|
||
|
||
Each ramp/wait command executes `(increment + 1)` steps. Each step takes:
|
||
|
||
```
|
||
step_duration = step_time × prescale_period
|
||
```
|
||
|
||
Where:
|
||
|
||
| Prescale | Period | Source |
|
||
|----------|----------|----------------------------------------|
|
||
| Fast (0) | 0.49 ms | 32768 Hz / 16 = 2048 Hz |
|
||
| Slow (1) | 15.6 ms | 32768 Hz / 512 = 64 Hz |
|
||
|
||
Total command duration:
|
||
|
||
```
|
||
command_duration = (increment + 1) × step_time × prescale_period
|
||
```
|
||
|
||
Maximum single-command duration: 128 steps × 63 × 15.6ms = **125.2 seconds**.
|
||
|
||
Minimum step time (fastest visible change): 1 × 0.49ms = **0.49ms**.
|
||
|
||
#### Critical: The Increment Field
|
||
|
||
**The `increment` field does NOT mean "PWM units per step." It means "total number of steps minus one."**
|
||
|
||
Each step always changes PWM by exactly **1 unit**. The increment field controls **how many** of those 1-unit steps occur.
|
||
|
||
| increment | Actual steps | PWM change | Notes |
|
||
|-----------|-------------|------------|--------------------------|
|
||
| 0 | 0 (wait) | 0 | Pure wait, no PWM change |
|
||
| 1 | 2 | 2 | Almost no visible change |
|
||
| 4 | 5 | 5 | Datasheet example |
|
||
| 63 | 64 | 64 | Quarter of full range |
|
||
| 127 | 128 | 128 | **Maximum: half of 0-255** |
|
||
|
||
**A single ramp command can change PWM by at most 128 units** (half the full range). To ramp from 0 to 255, you need **two consecutive ramp commands**.
|
||
|
||
When PWM reaches the boundary (0 or 255), the command continues executing remaining steps but PWM stays clamped at the boundary.
|
||
|
||
#### Full-Range Ramp Pattern
|
||
|
||
To ramp 0→255 smoothly:
|
||
|
||
```
|
||
ramp_wait(Slow, 1, Up, 127) // 128 steps: PWM 0→128, ~2.0s
|
||
ramp_wait(Slow, 1, Up, 127) // 128 steps: PWM 128→255, ~2.0s
|
||
```
|
||
|
||
To ramp 255→0 smoothly:
|
||
|
||
```
|
||
ramp_wait(Slow, 1, Down, 127) // 128 steps: PWM 255→127, ~2.0s
|
||
ramp_wait(Slow, 1, Down, 127) // 128 steps: PWM 127→0, ~2.0s
|
||
```
|
||
|
||
Total: 256 smooth 1-unit steps in ~4.0s. Uses 2 of 16 command slots per direction.
|
||
|
||
#### Pure Wait (Timed Delay)
|
||
|
||
When increment = 0, the command becomes a wait (no PWM change):
|
||
|
||
```
|
||
word = (prescale << 14) | (step_time << 8) | 0x00
|
||
```
|
||
|
||
Wait duration = `step_time × prescale_period`. Examples:
|
||
|
||
| Prescale | step_time | Duration |
|
||
|----------|-----------|----------|
|
||
| Fast | 20 | 9.8 ms |
|
||
| Fast | 40 | 19.6 ms |
|
||
| Slow | 32 | 499 ms |
|
||
| Slow | 48 | 749 ms |
|
||
| Slow | 63 | 983 ms |
|
||
|
||
For waits longer than ~1s, chain multiple wait commands.
|
||
|
||
### 2. Set PWM Command
|
||
|
||
Instantly sets PWM to an absolute value. This is the ramp command with step_time = 0.
|
||
|
||
```
|
||
Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
|
||
0 1 0 0 0 0 0 0 ├──────── PWM value ────────────┤
|
||
(8 bits, 0-255)
|
||
```
|
||
|
||
**Encoding:** `word = 0x4000 | pwm_value`
|
||
|
||
| PWM value | Word | Effect |
|
||
|-----------|--------|------------------|
|
||
| 0 | 0x4000 | LED off |
|
||
| 128 | 0x4080 | Half brightness |
|
||
| 255 | 0x40FF | Full brightness |
|
||
|
||
Executes in 16 clock cycles (~488µs). Use for instant on/off in heartbeat-style patterns.
|
||
|
||
### 3. Go to Start
|
||
|
||
Resets program counter to 0. All-zero word.
|
||
|
||
```
|
||
word = 0x0000
|
||
```
|
||
|
||
Note: default value for all SRAM slots, so unused slots act as Go-to-Start.
|
||
|
||
### 4. Branch Command
|
||
|
||
Jump to a step number, with optional loop count.
|
||
|
||
```
|
||
Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
|
||
1 0 1 ├──── loop_count ──────────┤ x x ├─ step_num ─┤
|
||
(6 bits, 0-63) (4 bits)
|
||
```
|
||
|
||
| Field | Bits | Range | Description |
|
||
|------------|-------|-------|------------------------------------------|
|
||
| (fixed) | 15:13 | 101 | Branch command identifier |
|
||
| loop_count | 12:7 | 0-63 | 0 = infinite loop, 1-63 = finite count |
|
||
| (don't care)| 6:4 | x | Ignored by hardware |
|
||
| step_number| 3:0 | 0-15 | Target instruction address |
|
||
|
||
**Encoding:** `word = 0xA000 | (loop_count << 7) | step_number`
|
||
|
||
**Infinite loop to start:** `branch(0, 0)` → `0xA000`. This is the standard way to make a pattern repeat forever.
|
||
|
||
Nested loops are supported. The hardware maintains loop counters independently per nesting level.
|
||
|
||
### 5. End Command
|
||
|
||
Stops engine execution, optionally fires interrupt and/or resets PWM.
|
||
|
||
```
|
||
Bit: 15 14 13 12 11 10 ... 0
|
||
1 1 0 int reset x ... x
|
||
```
|
||
|
||
| Field | Bit | Description |
|
||
|--------|-----|--------------------------------------------------|
|
||
| int | 12 | 1 = fire interrupt (sets STATUS register bit) |
|
||
| reset | 11 | 1 = reset PWM to 0, 0 = keep current PWM value |
|
||
|
||
**Encoding:** `word = 0xC000 | (int << 12) | (reset << 11)`
|
||
|
||
After End, the engine returns to Hold state and PC resets to 0. Not typically used in looping patterns (use Branch instead).
|
||
|
||
### 6. Trigger Command
|
||
|
||
Synchronize execution between engines. One engine can send a trigger that another engine waits for.
|
||
|
||
```
|
||
Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
|
||
1 1 1 x x x ├─ wait ──┤ x x x ├── send ──────┤ x
|
||
E3 E2 E1 E3 E2 E1
|
||
```
|
||
|
||
| Field | Bits | Description |
|
||
|--------------|------|------------------------------------------------------|
|
||
| wait_engines | 10:8 | Bitmask: which engines to wait for before continuing |
|
||
| send_engines | 3:1 | Bitmask: which engines to notify |
|
||
|
||
Bitmask mapping: bit 0 = Engine1, bit 1 = Engine2, bit 2 = Engine3.
|
||
|
||
**Encoding:** `word = 0xE000 | (wait_engines << 8) | (send_engines << 1)`
|
||
|
||
A single trigger command can both wait AND send. The engine blocks until all wait conditions are met, then sends its trigger signals.
|
||
|
||
---
|
||
|
||
## Timing Reference Table
|
||
|
||
Pre-computed durations for common configurations.
|
||
|
||
### Ramp durations (full 0-255 sweep = two commands)
|
||
|
||
| Prescale | step_time | Steps per cmd | Duration per cmd | Full 0→255 |
|
||
|----------|-----------|---------------|------------------|------------|
|
||
| Slow | 1 | 128 | 2.0 s | **4.0 s** |
|
||
| Slow | 2 | 128 | 4.0 s | **8.0 s** |
|
||
| Slow | 4 | 128 | 8.0 s | **16.0 s** |
|
||
| Slow | 8 | 128 | 15.9 s | **31.9 s** |
|
||
| Fast | 1 | 128 | 62.7 ms | **125 ms** |
|
||
| Fast | 4 | 128 | 251 ms | **502 ms** |
|
||
| Fast | 10 | 128 | 627 ms | **1.25 s** |
|
||
|
||
### Wait durations
|
||
|
||
| Prescale | step_time | Duration |
|
||
|----------|-----------|------------|
|
||
| Fast | 1 | 0.49 ms |
|
||
| Fast | 63 | 30.9 ms |
|
||
| Slow | 1 | 15.6 ms |
|
||
| Slow | 32 | 499 ms |
|
||
| Slow | 63 | 983 ms |
|
||
|
||
### Maximum durations
|
||
|
||
| Type | Value |
|
||
|-------------------|--------------------------|
|
||
| Single ramp cmd | 128 × 63 × 15.6ms = 125s |
|
||
| Single wait cmd | 63 × 15.6ms = 0.98s |
|
||
| Full 0→255 ramp | 2 × 125s = 250s (max) |
|
||
|
||
---
|
||
|
||
## Program Construction Rules
|
||
|
||
### Hard Limits
|
||
|
||
| Constraint | Value | Notes |
|
||
|-----------------------------------|-------|--------------------------------------------------|
|
||
| Commands per engine | 16 | 16 × 2 bytes = 32 bytes SRAM per engine |
|
||
| Engines | 3 | Engine 1, 2, 3 |
|
||
| Max increment per ramp | 127 | = 128 PWM steps = half of 0-255 range |
|
||
| Max step_time | 63 | 6-bit field |
|
||
| Max loop_count (branch) | 63 | 0 = infinite |
|
||
| Max step_number (branch target) | 15 | 4-bit field |
|
||
| PWM range | 0-255 | 8-bit, linear or logarithmic scale |
|
||
|
||
### Design Guidelines
|
||
|
||
1. **Always use `increment=127`** for smooth ramps. Lower values make the ramp shorter (fewer steps), not smoother.
|
||
|
||
2. **Two ramp commands per direction** for full-range sweeps. One command only covers 128 of the 255 PWM units.
|
||
|
||
3. **End every looping program with `branch(0, 0)`** — infinite loop back to step 0.
|
||
|
||
4. **Budget command slots carefully.** A full breathe cycle needs: 2 (ramp up) + 2 (ramp down) + 1 (wait) + 1 (branch) = 6 slots out of 16.
|
||
|
||
5. **Use Set PWM for instant transitions.** Ramp commands always take time; `set_pwm` is near-instant (~488µs).
|
||
|
||
6. **Chain waits for long pauses.** Max single wait is ~1s (Slow, 63). For longer pauses, use two consecutive waits.
|
||
|
||
7. **Triggers are blocking.** A wait-for-trigger command blocks the engine until the trigger is received. Design trigger chains to avoid deadlocks.
|
||
|
||
8. **Unused SRAM slots default to Go-to-Start (0x0000).** If a branch jumps past the last command, execution will hit a go-to-start and loop from the beginning.
|
||
|
||
### Pattern Switching
|
||
|
||
When switching between patterns, the LP5562 must be fully reset to avoid residual PWM state:
|
||
|
||
1. Write `0xFF` to Reset register (0x0D) — software reset, all registers return to defaults
|
||
2. Wait 1ms
|
||
3. Re-enable the chip (write to Enable register)
|
||
4. Wait 500µs (startup delay)
|
||
5. Reconfigure: clock source, LED current, LED mapping
|
||
6. Load and run new engine programs
|
||
|
||
Without reset, engines that previously held a non-zero PWM value will keep that brightness visible during the new pattern's loading phase.
|
||
|
||
---
|
||
|
||
## Reference Patterns
|
||
|
||
Five verified patterns with their engine programs and timing.
|
||
|
||
### 1. Breathe
|
||
|
||
Single engine, all RGB channels in unison. Smooth triangle wave.
|
||
|
||
| Property | Value |
|
||
|----------------|-----------|
|
||
| Engines used | 1 |
|
||
| Commands | 6 / 16 |
|
||
| Cycle time | ~8.75s |
|
||
| LED mapping | B,G,R → E1; W → I2C |
|
||
|
||
```
|
||
Step Command Hex Duration PWM range
|
||
─────────────────────────────────────────────────────────────────────────
|
||
0 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s 0 → 128
|
||
1 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s 128 → 255
|
||
2 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s 255 → 127
|
||
3 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s 127 → 0
|
||
4 wait(Slow, 48) 0x7000 0.75s (hold at 0)
|
||
5 branch(0, 0) 0xA000 instant (loop)
|
||
```
|
||
|
||
### 2. Heartbeat
|
||
|
||
Single engine, double-pulse with long rest. Mimics cardiac rhythm.
|
||
|
||
| Property | Value |
|
||
|----------------|-----------|
|
||
| Engines used | 1 |
|
||
| Commands | 10 / 16 |
|
||
| Cycle time | ~1.56s |
|
||
| LED mapping | B,G,R → E1; W → I2C |
|
||
|
||
```
|
||
Step Command Hex Duration Effect
|
||
─────────────────────────────────────────────────────────────────────────
|
||
0 set_pwm(255) 0x40FF instant First beat ON
|
||
1 wait(Fast, 20) 0x0A00 9.8ms Hold bright
|
||
2 set_pwm(0) 0x4000 instant First beat OFF
|
||
3 wait(Fast, 40) 0x1400 19.6ms Inter-beat gap
|
||
4 set_pwm(255) 0x40FF instant Second beat ON
|
||
5 wait(Fast, 20) 0x0A00 9.8ms Hold bright
|
||
6 set_pwm(0) 0x4000 instant Second beat OFF
|
||
7 wait(Slow, 63) 0x5F00 983ms Long rest pt.1
|
||
8 wait(Slow, 32) 0x6000 499ms Long rest pt.2
|
||
9 branch(0, 0) 0xA000 instant Loop forever
|
||
```
|
||
|
||
### 3. Slow Pulse
|
||
|
||
Single engine, very gentle ramp. Meditative pace.
|
||
|
||
| Property | Value |
|
||
|----------------|-----------|
|
||
| Engines used | 1 |
|
||
| Commands | 7 / 16 |
|
||
| Cycle time | ~33.5s |
|
||
| LED mapping | B,G,R → E1; W → I2C |
|
||
|
||
```
|
||
Step Command Hex Duration PWM range
|
||
─────────────────────────────────────────────────────────────────────────
|
||
0 ramp_wait(Slow, 4, Up, 127) 0x447F 8.0s 0 → 128
|
||
1 ramp_wait(Slow, 4, Up, 127) 0x447F 8.0s 128 → 255
|
||
2 wait(Slow, 32) 0x6000 499ms Hold at peak
|
||
3 ramp_wait(Slow, 4, Down, 127) 0x44FF 8.0s 255 → 127
|
||
4 ramp_wait(Slow, 4, Down, 127) 0x44FF 8.0s 127 → 0
|
||
5 wait(Slow, 63) 0x5F00 983ms Pause at bottom
|
||
6 branch(0, 0) 0xA000 instant Loop forever
|
||
```
|
||
|
||
### 4. RGB Cycle
|
||
|
||
Three engines, trigger-synchronized phase offset. Colors appear sequentially.
|
||
|
||
| Property | Value |
|
||
|----------------|-----------|
|
||
| Engines used | 3 |
|
||
| Commands | 7 + 8 + 7 = 22 / 48 |
|
||
| Cycle time | ~9s per engine |
|
||
| LED mapping | B → E1; G → E2; R → E3; W → I2C |
|
||
|
||
**Trigger chain:** E1 ramps up, sends trigger → E2 starts, ramps up, sends trigger → E3 starts. Creates a staggered color sequence.
|
||
|
||
**Engine 1 (Blue):**
|
||
```
|
||
Step Command Hex Duration
|
||
─────────────────────────────────────────────────────────────
|
||
0 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
1 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
2 trigger(send=E2) 0xE004 instant
|
||
3 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
4 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
5 wait(Slow, 63) 0x5F00 983ms
|
||
6 branch(0, 0) 0xA000 instant
|
||
```
|
||
|
||
**Engine 2 (Green):**
|
||
```
|
||
Step Command Hex Duration
|
||
─────────────────────────────────────────────────────────────
|
||
0 trigger(wait=E1) 0xE100 blocks
|
||
1 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
2 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
3 trigger(send=E3) 0xE008 instant
|
||
4 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
5 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
6 wait(Slow, 32) 0x6000 499ms
|
||
7 branch(0, 0) 0xA000 instant
|
||
```
|
||
|
||
**Engine 3 (Red):**
|
||
```
|
||
Step Command Hex Duration
|
||
─────────────────────────────────────────────────────────────
|
||
0 trigger(wait=E2) 0xE200 blocks
|
||
1 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
2 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
3 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
4 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
5 wait(Slow, 32) 0x6000 499ms
|
||
6 branch(0, 0) 0xA000 instant
|
||
```
|
||
|
||
### 5. Color Wash
|
||
|
||
Three engines, free-running at different rates. Colors drift in and out of phase, creating evolving blended hues.
|
||
|
||
| Property | Value |
|
||
|----------------|-----------|
|
||
| Engines used | 3 |
|
||
| Commands | 5 + 6 + 6 = 17 / 48 |
|
||
| Cycle time | E1: ~8.0s, E2: ~8.5s, E3: ~9.0s |
|
||
| LED mapping | B → E1; G → E2; R → E3; W → I2C |
|
||
|
||
**Phase drift mechanism:** Each engine has a slightly different cycle length (due to different pause durations at the bottom of the ramp). Over time, the engines desynchronize, causing the color mix to continuously evolve. No triggers needed.
|
||
|
||
**Engine 1 (Blue) — 8.0s cycle, no pause:**
|
||
```
|
||
Step Command Hex Duration
|
||
─────────────────────────────────────────────────────────────
|
||
0 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
1 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
2 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
3 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
4 branch(0, 0) 0xA000 instant
|
||
```
|
||
|
||
**Engine 2 (Green) — 8.5s cycle, 0.5s pause:**
|
||
```
|
||
Step Command Hex Duration
|
||
─────────────────────────────────────────────────────────────
|
||
0 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
1 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
2 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
3 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
4 wait(Slow, 32) 0x6000 499ms
|
||
5 branch(0, 0) 0xA000 instant
|
||
```
|
||
|
||
**Engine 3 (Red) — 9.0s cycle, 1.0s pause:**
|
||
```
|
||
Step Command Hex Duration
|
||
─────────────────────────────────────────────────────────────
|
||
0 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
1 ramp_wait(Slow, 1, Up, 127) 0x417F 2.0s
|
||
2 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
3 ramp_wait(Slow, 1, Down, 127) 0x41FF 2.0s
|
||
4 wait(Slow, 63) 0x5F00 983ms
|
||
5 branch(0, 0) 0xA000 instant
|
||
```
|
||
|
||
---
|
||
|
||
## App Pattern Editor: Implementation Notes
|
||
|
||
### Binary Representation
|
||
|
||
A pattern sent from the app to the implant (via NFC SRAM mailbox) needs:
|
||
|
||
1. **Per-engine program**: Array of up to 16 u16 values (big-endian)
|
||
2. **LED channel mapping**: Which engine drives which channel (4 × 2-bit values)
|
||
3. **LED current**: Per-channel current setting (0-255 maps to 0-25.5mA)
|
||
|
||
Minimum data per pattern: 3 engines × 32 bytes + 1 byte mapping + 4 bytes current = **101 bytes**. Fits easily in the NTAG5Link's 256-byte SRAM mailbox.
|
||
|
||
### Encoding Commands in JavaScript/TypeScript
|
||
|
||
```typescript
|
||
// Prescale constants
|
||
const PRESCALE_FAST = 0; // 0.49ms per step
|
||
const PRESCALE_SLOW = 1; // 15.6ms per step
|
||
|
||
// Ramp/Wait: smooth PWM transition or timed delay
|
||
function rampWait(
|
||
prescale: 0 | 1,
|
||
stepTime: number, // 1-63
|
||
direction: 'up' | 'down',
|
||
increment: number // 0-127 (0 = wait only)
|
||
): number {
|
||
const ps = (prescale & 1) << 14;
|
||
const st = (stepTime & 0x3F) << 8;
|
||
const sign = direction === 'down' ? (1 << 7) : 0;
|
||
const inc = increment & 0x7F;
|
||
return ps | st | sign | inc;
|
||
}
|
||
|
||
// Pure wait (no PWM change)
|
||
function wait(prescale: 0 | 1, stepTime: number): number {
|
||
return rampWait(prescale, stepTime, 'up', 0);
|
||
}
|
||
|
||
// Set PWM to absolute value (instant)
|
||
function setPwm(value: number): number {
|
||
return 0x4000 | (value & 0xFF);
|
||
}
|
||
|
||
// Branch (jump to step, with loop count)
|
||
function branch(loopCount: number, stepNumber: number): number {
|
||
return 0xA000 | ((loopCount & 0x3F) << 7) | (stepNumber & 0x0F);
|
||
}
|
||
|
||
// End program
|
||
function end(interrupt: boolean, resetPwm: boolean): number {
|
||
return 0xC000 | (interrupt ? (1 << 12) : 0) | (resetPwm ? (1 << 11) : 0);
|
||
}
|
||
|
||
// Trigger (inter-engine sync)
|
||
// engineBits: bit0=E1, bit1=E2, bit2=E3
|
||
function trigger(waitEngines: number, sendEngines: number): number {
|
||
return 0xE000 | ((waitEngines & 0x07) << 8) | ((sendEngines & 0x07) << 1);
|
||
}
|
||
```
|
||
|
||
### Duration Calculator
|
||
|
||
```typescript
|
||
const PRESCALE_PERIOD = [0.49, 15.6]; // ms per step for Fast, Slow
|
||
|
||
function rampDurationMs(
|
||
prescale: 0 | 1,
|
||
stepTime: number,
|
||
increment: number
|
||
): number {
|
||
if (increment === 0) {
|
||
// Wait command: single step
|
||
return stepTime * PRESCALE_PERIOD[prescale];
|
||
}
|
||
const steps = increment + 1;
|
||
return steps * stepTime * PRESCALE_PERIOD[prescale];
|
||
}
|
||
|
||
// Full 0→255 ramp using two commands
|
||
function fullRampDurationMs(prescale: 0 | 1, stepTime: number): number {
|
||
return 2 * rampDurationMs(prescale, stepTime, 127);
|
||
}
|
||
```
|
||
|
||
### Pattern Validation
|
||
|
||
Before sending a pattern to the implant, validate:
|
||
|
||
```typescript
|
||
function validateProgram(commands: number[]): string | null {
|
||
if (commands.length > 16) return 'Max 16 commands per engine';
|
||
if (commands.length === 0) return 'Program cannot be empty';
|
||
|
||
// Check for infinite loop or end command
|
||
const hasLoop = commands.some((cmd, i) => {
|
||
const isGoToStart = cmd === 0x0000;
|
||
const isBranch = (cmd >> 13) === 0b101;
|
||
const isEnd = (cmd >> 13) === 0b110;
|
||
return isGoToStart || isBranch || isEnd;
|
||
});
|
||
if (!hasLoop) return 'Program must contain a branch, end, or go-to-start';
|
||
|
||
return null; // valid
|
||
}
|
||
```
|
||
|
||
### Preview Animation
|
||
|
||
To show a real-time preview in the app, simulate engine execution:
|
||
|
||
```typescript
|
||
function simulateEngine(commands: number[]): Generator<number> {
|
||
// Returns PWM values over time, one per step
|
||
// See timing math above for step duration
|
||
// Key: each ramp step changes PWM by exactly 1 unit
|
||
// Key: PWM clamps at 0 and 255 boundaries
|
||
}
|
||
```
|
||
|
||
The simulation should model:
|
||
- Current PWM value (starts at 0 after reset)
|
||
- Ramp direction and step count
|
||
- Boundary clamping (0 and 255)
|
||
- Branch targets and loop counts
|
||
- Set PWM instant jumps
|
||
- Wait pauses (no PWM change)
|
||
|
||
For multi-engine patterns with triggers, simulate all three engines and synchronize on trigger send/wait pairs.
|
||
|
||
---
|
||
|
||
## Pitfalls and Lessons Learned
|
||
|
||
### increment=1 does NOT mean "smooth"
|
||
|
||
The most common mistake. `increment=1` means **2 total steps** — the ramp finishes almost instantly. For smooth ramps, always use `increment=127` (128 steps, the maximum). This is counterintuitive because "increment of 1" sounds like "change by 1 per step," but that's not what the field means. Every step always changes by 1; the field controls how many steps occur.
|
||
|
||
### step_time=0 is Set PWM, not a fast ramp
|
||
|
||
The ramp/wait command and set_pwm command share the same encoding space. When `step_time=0`, the hardware interprets bits 7:0 as an absolute PWM value (8 bits) instead of sign + increment (1 + 7 bits). Never pass `step_time=0` to a ramp function — use the dedicated `set_pwm` function instead.
|
||
|
||
### Software reset is essential between patterns
|
||
|
||
Without a full software reset (0xFF to register 0x0D), residual PWM values from the previous pattern remain visible on LED channels. Toggling the EN pin puts the chip in standby but does NOT clear register state (VDD stays up, no POR). Always use software reset when switching patterns.
|
||
|
||
### Engine load order matters
|
||
|
||
When loading multiple engines, each call to `run_engine` does a read-modify-write on the shared OP_MODE and ENABLE registers. The LP5562 requires:
|
||
- >= 153µs between consecutive OP_MODE register writes
|
||
- >= 488µs between consecutive ENABLE register writes
|
||
|
||
In practice, the I2C transaction overhead (~50-100µs per transfer at 400kHz) plus program data write (~660µs for 33 bytes) provides sufficient delay. A 200µs explicit delay between engine loads adds safety margin.
|
||
|
||
### Trigger deadlocks
|
||
|
||
If Engine A waits for a trigger from Engine B, but Engine B also waits for a trigger from Engine A, both will block forever. Design trigger chains as DAGs — one engine starts freely, triggers the next, which triggers the next. The last engine loops back independently.
|
||
|
||
### W channel is always I2C-direct
|
||
|
||
The LP5562's White channel cannot be mapped to an execution engine. It can only be controlled via direct I2C PWM register writes. For autonomous operation, treat W as static (set once at load time) or ignore it.
|
||
|
||
### LED current and power budget
|
||
|
||
For energy-harvested operation, total LED current must stay within the NFC field's power output (~12.5mA max). With 4 channels at full brightness (25.5mA each), you'd draw 102mA — far beyond what NFC can provide.
|
||
|
||
Current firmware uses 2mA per channel (register value 20, since each unit = 0.1mA). The app should enforce a maximum total current budget and warn users when pattern brightness exceeds what the implant can sustain.
|