Add pattern library, NTAG5 config check, NDEF writer (M3+M4)
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>
This commit is contained in:
@@ -179,6 +179,7 @@ Typical 3-engine pattern: 16 + 3×34 + 4 = **122 bytes** (fits easily in 2048B E
|
||||
3. On EIC interrupt (FD pin), wake, process SRAM mailbox, return to sleep
|
||||
4. Verify LP5562 keeps running patterns during MCU sleep
|
||||
5. Measure current with multimeter: active vs standby vs total system
|
||||
6. Verify I2C bus releases cleanly on STANDBY entry (SDA/SCL float high via pull-ups, no bus hold). With SRAM passthrough arbiter, an active I2C bus blocks NFC-side reads — confirmed during M4 testing.
|
||||
|
||||
**Power budget reference**:
|
||||
|
||||
|
||||
675
docs/LP5562_ENGINE_REFERENCE.md
Normal file
675
docs/LP5562_ENGINE_REFERENCE.md
Normal file
@@ -0,0 +1,675 @@
|
||||
# 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.
|
||||
@@ -1,6 +1,6 @@
|
||||
# xblink Project Status
|
||||
|
||||
**Current Milestone**: M3 — Hardcoded Pattern Library
|
||||
**Current Milestone**: M5 — Boot-from-EEPROM
|
||||
**Last Updated**: 2026-03-05
|
||||
|
||||
---
|
||||
@@ -34,24 +34,26 @@
|
||||
- [x] Verify LP5562 runs patterns autonomously (MCU idle loop)
|
||||
- [x] Verify MCU can stop and switch patterns (8s cycle between all 3)
|
||||
|
||||
### M3: Hardcoded Pattern Library
|
||||
### M3: Hardcoded Pattern Library (COMPLETE)
|
||||
|
||||
- [ ] Define 4-5 const patterns in firmware
|
||||
- [ ] Implement pattern selector (compile-time or boot-cycle)
|
||||
- [ ] Reduce LED current to 2-5mA/ch (EH-realistic budget)
|
||||
- [ ] Document actual engine program structures (informs M5 format design)
|
||||
- [x] Define 5 const patterns in firmware (breathe, heartbeat, slow_pulse, rgb_cycle, color_wash)
|
||||
- [x] Implement pattern selector (cycle through patterns, 15s each)
|
||||
- [x] Reduce LED current to 2mA/ch (EH-realistic budget)
|
||||
- [x] Document engine program structures (`docs/lp5562-engine-reference.md`)
|
||||
|
||||
## Group B — Add NTAG5Link
|
||||
|
||||
**Hardware needed**: + NTAG5Link Click board + I2C jumper
|
||||
|
||||
### M4: NTAG5 EEPROM Read/Write
|
||||
### M4: NTAG5 EEPROM Read/Write (COMPLETE)
|
||||
|
||||
- [ ] Write `Ntag5Link<I2C>` driver (`src/ntag5/mod.rs`)
|
||||
- [ ] Implement register map (`src/ntag5/registers.rs`)
|
||||
- [ ] Implement EEPROM block read/write
|
||||
- [ ] Implement session register access
|
||||
- [ ] Test round-trip: PCSC writes EEPROM, MCU reads back, displays on LEDs
|
||||
- [x] Write `Ntag5Link<I2C>` driver (`src/ntag5/mod.rs`)
|
||||
- [x] Implement session register access (CONFIG_0, CONFIG_1, EH_CONFIG)
|
||||
- [x] Implement EEPROM block read/write with write-verify
|
||||
- [x] Config check: read session regs, compare against expected, write NDEF result
|
||||
- [x] NDEF Type 5 text record writer for config check output
|
||||
- [x] Verified on hardware: config check passes, NDEF readable via NFC
|
||||
- [ ] Register map module (`src/ntag5/registers.rs`) — deferred, constants in mod.rs for now
|
||||
|
||||
### M5: Boot-from-EEPROM
|
||||
|
||||
|
||||
64
src/main.rs
64
src/main.rs
@@ -15,8 +15,10 @@ use hal::prelude::*;
|
||||
use pac::{CorePeripherals, Peripherals};
|
||||
|
||||
mod led;
|
||||
mod ntag5;
|
||||
mod pattern;
|
||||
use led::lp5562::{ClockSource, Lp5562, DEFAULT_ADDRESS};
|
||||
use ntag5::Ntag5Link;
|
||||
use pattern::LedMode;
|
||||
|
||||
/// Blink the on-board LED n times (active-low: low=on, high=off)
|
||||
@@ -69,45 +71,85 @@ fn main() -> ! {
|
||||
|
||||
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
||||
|
||||
// Try to initialize LP5562 — blink error pattern on failure
|
||||
// 2 mA per channel (20 × 0.1 mA) — realistic for EH power budget
|
||||
const LED_CURRENT: u8 = 20;
|
||||
|
||||
// Verify LP5562 is reachable before entering pattern loop
|
||||
let i2c_ok = (|| -> Result<(), led::lp5562::Error<_>> {
|
||||
lp.enable()?;
|
||||
delay.delay_ms(1u32); // >500us startup
|
||||
lp.init_direct_control(ClockSource::Internal)?;
|
||||
lp.set_all_current(50, 50, 50, 50)?;
|
||||
lp.set_all_current(LED_CURRENT, LED_CURRENT, LED_CURRENT, LED_CURRENT)?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
match i2c_ok {
|
||||
Ok(()) => {
|
||||
// === DIAGNOSTIC: solid LED on = LP5562 init OK ===
|
||||
// === STATUS: solid LED on = LP5562 init OK ===
|
||||
led.set_low().unwrap();
|
||||
delay.delay_ms(500u32);
|
||||
led.set_high().unwrap();
|
||||
delay.delay_ms(1000u32); // 1s gap between status signals
|
||||
|
||||
// LED mode: Rgbw for EVM RGB LED, Mono3 for 3 separate LEDs
|
||||
let mode = LedMode::Rgbw;
|
||||
|
||||
// Cycle through patterns, 8 seconds each
|
||||
let patterns = [
|
||||
pattern::breathe(mode),
|
||||
pattern::heartbeat(mode),
|
||||
pattern::slow_pulse(mode),
|
||||
pattern::rgb_cycle(mode),
|
||||
pattern::color_wash(mode),
|
||||
];
|
||||
|
||||
let mut idx = 0;
|
||||
loop {
|
||||
// Blink pattern index (1, 2, or 3) to indicate which pattern
|
||||
blink(&mut led, &mut delay, (idx + 1) as u8, 150);
|
||||
// Load first pattern — LP5562 engines run autonomously
|
||||
let _ = pattern::load_pattern(&mut lp, &patterns[0], LED_CURRENT, &mut delay);
|
||||
|
||||
// Load pattern — LP5562 runs autonomously
|
||||
if pattern::load_pattern(&mut lp, &patterns[idx], &mut delay).is_err() {
|
||||
// Release I2C from LP5562 (engines keep running) for NTAG5 check
|
||||
let i2c = lp.release();
|
||||
let mut ntag = Ntag5Link::new(i2c, ntag5::DEFAULT_ADDRESS);
|
||||
|
||||
// NTAG5 config check — read session registers, write NDEF result
|
||||
// If NTAG5 isn't connected, this will fail silently (I2C NAK)
|
||||
match ntag.check_and_write_ndef(&mut delay) {
|
||||
Ok(true) => {
|
||||
// Config OK — 2 short blinks
|
||||
blink(&mut led, &mut delay, 2, 100);
|
||||
}
|
||||
Ok(false) => {
|
||||
// Config mismatch — 5 fast blinks (warning)
|
||||
blink(&mut led, &mut delay, 5, 60);
|
||||
}
|
||||
Err(ntag5::Error::VerifyFailed) => {
|
||||
// EEPROM write-verify failed — SOS pattern (3 rapid, pause, 3 rapid)
|
||||
blink(&mut led, &mut delay, 3, 40);
|
||||
delay.delay_ms(300u32);
|
||||
blink(&mut led, &mut delay, 3, 40);
|
||||
}
|
||||
Err(ntag5::Error::I2c(_)) => {
|
||||
// NTAG5 not reachable — 1 long blink (not fatal)
|
||||
led.set_low().unwrap();
|
||||
delay.delay_ms(800u32);
|
||||
led.set_high().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Reclaim I2C for LP5562
|
||||
let i2c = ntag.release();
|
||||
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
||||
|
||||
// Pattern cycle loop — LP5562 engines run autonomously, no MCU LED indication
|
||||
let mut idx = 1; // Already loaded pattern 0 above
|
||||
loop {
|
||||
if pattern::load_pattern(
|
||||
&mut lp, &patterns[idx], LED_CURRENT, &mut delay,
|
||||
).is_err() {
|
||||
// I2C error loading pattern — fast blink
|
||||
blink(&mut led, &mut delay, 10, 50);
|
||||
}
|
||||
|
||||
// MCU idles while LP5562 runs the pattern
|
||||
delay.delay_ms(8000u32);
|
||||
delay.delay_ms(15000u32);
|
||||
|
||||
idx = (idx + 1) % patterns.len();
|
||||
}
|
||||
|
||||
314
src/ntag5/mod.rs
Normal file
314
src/ntag5/mod.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
/// NTAG5Link I2C slave driver for NTP53x2.
|
||||
///
|
||||
/// Provides session register reads, EEPROM block read/write,
|
||||
/// and NDEF Type 5 text record writing.
|
||||
///
|
||||
/// I2C protocol (Section 8.3.1.4 of NTP53x2 datasheet):
|
||||
/// - READ MEMORY: write [BL_AD1, BL_AD0], read N bytes
|
||||
/// - WRITE MEMORY: write [BL_AD1, BL_AD0, D0, D1, D2, D3]
|
||||
/// - READ REGISTER: write [BL_AD1, BL_AD0, REGA], read 1 byte
|
||||
/// - WRITE REGISTER: write [BL_AD1, BL_AD0, REGA, MASK, REGDATA]
|
||||
|
||||
use embedded_hal::i2c::I2c;
|
||||
|
||||
pub const DEFAULT_ADDRESS: u8 = 0x54;
|
||||
|
||||
// Session register I2C block addresses (16-bit)
|
||||
pub const SESSION_CONFIG_REG: u16 = 0x10A1; // CONFIG_0_REG, CONFIG_1_REG, CONFIG_2_REG, RFU
|
||||
pub const SESSION_EH_CONFIG_REG: u16 = 0x10A7; // EH_CONFIG_REG, RFU
|
||||
pub const SESSION_I2C_SLAVE_REG: u16 = 0x10A9; // I2C_SLAVE_ADDR_REG, I2C_SLAVE_CONFIG_REG
|
||||
|
||||
// EEPROM user memory I2C block addresses
|
||||
// Block 0 = CC (capability container), blocks 1+ = NDEF data
|
||||
pub const EEPROM_BLOCK_0: u16 = 0x0000;
|
||||
|
||||
// Expected config values for xblink
|
||||
// CONFIG_0: EH_MODE = low field strength (bits 3:2 = 10b)
|
||||
pub const EXPECTED_CONFIG_0: u8 = 0x08;
|
||||
// CONFIG_1: SRAM_ENABLE (bit 1) + ARBITER_MODE passthrough (bits 3:2 = 10b) + USE_CASE I2C slave (bits 5:4 = 00b)
|
||||
pub const EXPECTED_CONFIG_1: u8 = 0x0A;
|
||||
// EH_CONFIG: skip for now — EH is disabled (0x00) when powered externally.
|
||||
// Set to 0x75 (EH_ENABLE + 3.0V + 12.5mA) when running from energy harvesting.
|
||||
pub const EXPECTED_EH_CONFIG: u8 = 0x00;
|
||||
|
||||
// Masks for checking — only check the bits we care about
|
||||
// CONFIG_0: bits 3:2 (EH_MODE) — ignore SRAM_COPY_EN, AUTO_STANDBY, LOCK_SESSION
|
||||
pub const CONFIG_0_MASK: u8 = 0x0C;
|
||||
// CONFIG_1: bits 5:4 (USE_CASE) + bits 3:2 (ARBITER) + bit 1 (SRAM_EN)
|
||||
pub const CONFIG_1_MASK: u8 = 0x3E;
|
||||
// EH_CONFIG: mask 0x00 — don't check EH config during external power testing
|
||||
pub const EH_CONFIG_MASK: u8 = 0x00;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error<E> {
|
||||
I2c(E),
|
||||
/// EEPROM write-verify failed: read-back didn't match written data
|
||||
VerifyFailed,
|
||||
}
|
||||
|
||||
impl<E> From<E> for Error<E> {
|
||||
fn from(e: E) -> Self {
|
||||
Error::I2c(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ntag5Link<I2C> {
|
||||
i2c: I2C,
|
||||
addr: u8,
|
||||
}
|
||||
|
||||
/// Config check result for a single register
|
||||
pub struct RegCheck {
|
||||
/// Register name abbreviation (e.g., "C0", "C1", "EH")
|
||||
pub name: [u8; 2],
|
||||
/// Actual value read
|
||||
pub actual: u8,
|
||||
/// Expected value (after masking)
|
||||
pub expected: u8,
|
||||
/// Whether it matches
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
/// Full config check result
|
||||
pub struct ConfigResult {
|
||||
pub checks: [RegCheck; 3],
|
||||
/// True if all checks passed
|
||||
pub all_ok: bool,
|
||||
}
|
||||
|
||||
impl<I2C, E> Ntag5Link<I2C>
|
||||
where
|
||||
I2C: I2c<Error = E>,
|
||||
{
|
||||
pub fn new(i2c: I2C, addr: u8) -> Self {
|
||||
Self { i2c, addr }
|
||||
}
|
||||
|
||||
/// Consume the driver and return the I2C bus.
|
||||
pub fn release(self) -> I2C {
|
||||
self.i2c
|
||||
}
|
||||
|
||||
// ---- Low-level I2C commands ----
|
||||
|
||||
/// Read N bytes from a 16-bit block address (READ MEMORY command).
|
||||
/// Used for EEPROM and config memory.
|
||||
pub fn read_memory(&mut self, block: u16, buf: &mut [u8]) -> Result<(), E> {
|
||||
let addr_bytes = block.to_be_bytes();
|
||||
self.i2c.write_read(self.addr, &addr_bytes, buf)
|
||||
}
|
||||
|
||||
/// Write 4 bytes to a 16-bit block address (WRITE MEMORY command).
|
||||
/// Used for EEPROM writes. Each EEPROM block is 4 bytes.
|
||||
pub fn write_memory_block(&mut self, block: u16, data: &[u8; 4]) -> Result<(), E> {
|
||||
let addr_bytes = block.to_be_bytes();
|
||||
let mut buf = [0u8; 6];
|
||||
buf[0] = addr_bytes[0];
|
||||
buf[1] = addr_bytes[1];
|
||||
buf[2] = data[0];
|
||||
buf[3] = data[1];
|
||||
buf[4] = data[2];
|
||||
buf[5] = data[3];
|
||||
self.i2c.write(self.addr, &buf)
|
||||
}
|
||||
|
||||
/// Write 4 bytes then read back and verify. Returns Error::VerifyFailed
|
||||
/// if the read-back doesn't match.
|
||||
pub fn write_verify_block(
|
||||
&mut self,
|
||||
block: u16,
|
||||
data: &[u8; 4],
|
||||
delay: &mut impl embedded_hal::delay::DelayNs,
|
||||
) -> Result<(), Error<E>> {
|
||||
self.write_memory_block(block, data)?;
|
||||
delay.delay_ms(5); // EEPROM write cycle ~5ms
|
||||
let mut readback = [0u8; 4];
|
||||
self.read_memory(block, &mut readback)?;
|
||||
if readback != *data {
|
||||
return Err(Error::VerifyFailed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a single session register byte (READ REGISTER command).
|
||||
/// Protocol: write [BL_AD1, BL_AD0, REGA], then read 1 byte.
|
||||
pub fn read_register(&mut self, block: u16, reg_addr: u8) -> Result<u8, E> {
|
||||
let addr_bytes = block.to_be_bytes();
|
||||
let mut buf = [0u8; 1];
|
||||
self.i2c.write_read(
|
||||
self.addr,
|
||||
&[addr_bytes[0], addr_bytes[1], reg_addr],
|
||||
&mut buf,
|
||||
)?;
|
||||
Ok(buf[0])
|
||||
}
|
||||
|
||||
// ---- Config check ----
|
||||
|
||||
/// Read session registers and compare against expected xblink config.
|
||||
/// Session registers are always readable from I2C, even if config
|
||||
/// memory is password/AES protected.
|
||||
pub fn check_config(&mut self) -> Result<ConfigResult, E> {
|
||||
// Read CONFIG_REG session register: bytes 0,1,2 = CONFIG_0, CONFIG_1, CONFIG_2
|
||||
let c0 = self.read_register(SESSION_CONFIG_REG, 0)?;
|
||||
let c1 = self.read_register(SESSION_CONFIG_REG, 1)?;
|
||||
|
||||
// Read EH_CONFIG_REG session register: byte 0 = EH_CONFIG
|
||||
let eh = self.read_register(SESSION_EH_CONFIG_REG, 0)?;
|
||||
|
||||
let c0_ok = (c0 & CONFIG_0_MASK) == (EXPECTED_CONFIG_0 & CONFIG_0_MASK);
|
||||
let c1_ok = (c1 & CONFIG_1_MASK) == (EXPECTED_CONFIG_1 & CONFIG_1_MASK);
|
||||
let eh_ok = (eh & EH_CONFIG_MASK) == (EXPECTED_EH_CONFIG & EH_CONFIG_MASK);
|
||||
|
||||
let all_ok = c0_ok && c1_ok && eh_ok;
|
||||
|
||||
Ok(ConfigResult {
|
||||
checks: [
|
||||
RegCheck { name: *b"C0", actual: c0, expected: EXPECTED_CONFIG_0, ok: c0_ok },
|
||||
RegCheck { name: *b"C1", actual: c1, expected: EXPECTED_CONFIG_1, ok: c1_ok },
|
||||
RegCheck { name: *b"EH", actual: eh, expected: EXPECTED_EH_CONFIG, ok: eh_ok },
|
||||
],
|
||||
all_ok,
|
||||
})
|
||||
}
|
||||
|
||||
// ---- NDEF Type 5 text record ----
|
||||
|
||||
/// Write an NDEF Type 5 text record to EEPROM user memory.
|
||||
///
|
||||
/// Layout (NFC Forum Type 5 Tag):
|
||||
/// - Block 0: CC (Capability Container) — 4 bytes
|
||||
/// - Block 1+: TLV wrapper + NDEF message
|
||||
///
|
||||
/// The text record uses "en" language code, UTF-8 encoding.
|
||||
pub fn write_ndef_text(&mut self, text: &[u8], delay: &mut impl embedded_hal::delay::DelayNs) -> Result<(), Error<E>> {
|
||||
// CC (block 0) is already present from factory/provisioning — don't overwrite.
|
||||
|
||||
// NDEF message TLV:
|
||||
// [0] 03 = NDEF message TLV type
|
||||
// [1] len = total NDEF message length
|
||||
// NDEF record header:
|
||||
// [2] D1 = MB=1, ME=1, CF=0, SR=1, IL=0, TNF=01 (well-known)
|
||||
// [3] 01 = type length (1 byte: "T")
|
||||
// [4] payload_len = 3 + text.len() (status byte + "en" + text)
|
||||
// [5] 54 = type: "T" (text record)
|
||||
// NDEF payload:
|
||||
// [6] 02 = status byte: UTF-8 (bit 7=0), language code length=2
|
||||
// [7] 65 = 'e'
|
||||
// [8] 6E = 'n'
|
||||
// [9..] = text bytes
|
||||
// After message:
|
||||
// FE = terminator TLV
|
||||
|
||||
let payload_len = 3 + text.len(); // status + "en" + text
|
||||
let ndef_len = 4 + payload_len; // header(3) + type(1) + payload
|
||||
let tlv_len = 2 + ndef_len; // TLV type + TLV len + NDEF message
|
||||
let total_bytes = tlv_len + 1; // + terminator TLV (FE)
|
||||
|
||||
// Build the message into a buffer (max ~80 bytes for our use)
|
||||
let mut msg = [0u8; 80];
|
||||
if total_bytes > msg.len() {
|
||||
// Text too long, truncate silently — shouldn't happen for our short messages
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
msg[i] = 0x03; i += 1; // NDEF TLV type
|
||||
msg[i] = ndef_len as u8; i += 1; // NDEF TLV length
|
||||
msg[i] = 0xD1; i += 1; // NDEF record header: MB|ME|SR, TNF=well-known
|
||||
msg[i] = 0x01; i += 1; // Type length = 1
|
||||
msg[i] = payload_len as u8; i += 1; // Payload length
|
||||
msg[i] = b'T'; i += 1; // Type = "T" (text)
|
||||
msg[i] = 0x02; i += 1; // Status: UTF-8, lang len = 2
|
||||
msg[i] = b'e'; i += 1;
|
||||
msg[i] = b'n'; i += 1;
|
||||
for &b in text {
|
||||
msg[i] = b;
|
||||
i += 1;
|
||||
}
|
||||
msg[i] = 0xFE; i += 1; // Terminator TLV
|
||||
|
||||
// Write to EEPROM in 4-byte blocks starting at block 1
|
||||
let mut block = 1u16;
|
||||
let mut offset = 0;
|
||||
while offset < i {
|
||||
let mut data = [0u8; 4];
|
||||
for j in 0..4 {
|
||||
if offset + j < i {
|
||||
data[j] = msg[offset + j];
|
||||
}
|
||||
}
|
||||
self.write_verify_block(block, &data, delay)?;
|
||||
block += 1;
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Format the config check result as a human-readable NDEF text record.
|
||||
/// Returns the number of bytes written to `buf`.
|
||||
pub fn format_config_result(result: &ConfigResult, buf: &mut [u8]) -> usize {
|
||||
let mut i = 0;
|
||||
|
||||
// Helper: append a byte slice
|
||||
fn append(buf: &mut [u8], i: &mut usize, data: &[u8]) {
|
||||
for &b in data {
|
||||
if *i < buf.len() {
|
||||
buf[*i] = b;
|
||||
*i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: append hex byte as 2 ASCII chars
|
||||
fn hex(buf: &mut [u8], i: &mut usize, val: u8) {
|
||||
const HEX: &[u8; 16] = b"0123456789ABCDEF";
|
||||
if *i + 1 < buf.len() {
|
||||
buf[*i] = HEX[(val >> 4) as usize];
|
||||
*i += 1;
|
||||
buf[*i] = HEX[(val & 0x0F) as usize];
|
||||
*i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
append(buf, &mut i, b"xblink cfg:");
|
||||
|
||||
if result.all_ok {
|
||||
append(buf, &mut i, b" OK ");
|
||||
} else {
|
||||
append(buf, &mut i, b" BAD ");
|
||||
}
|
||||
|
||||
for check in &result.checks {
|
||||
append(buf, &mut i, &check.name);
|
||||
append(buf, &mut i, b":");
|
||||
hex(buf, &mut i, check.actual);
|
||||
if check.ok {
|
||||
append(buf, &mut i, b"ok ");
|
||||
} else {
|
||||
append(buf, &mut i, b"!=");
|
||||
hex(buf, &mut i, check.expected);
|
||||
append(buf, &mut i, b" ");
|
||||
}
|
||||
}
|
||||
|
||||
i
|
||||
}
|
||||
|
||||
/// Run config check and write result as NDEF text record.
|
||||
/// Returns Ok(true) if config matches, Ok(false) if mismatch,
|
||||
/// Err(VerifyFailed) if EEPROM write-verify failed,
|
||||
/// Err(I2c(e)) if I2C communication failed.
|
||||
pub fn check_and_write_ndef(&mut self, delay: &mut impl embedded_hal::delay::DelayNs) -> Result<bool, Error<E>> {
|
||||
let result = self.check_config()?;
|
||||
let all_ok = result.all_ok;
|
||||
|
||||
let mut buf = [0u8; 64];
|
||||
let len = Self::format_config_result(&result, &mut buf);
|
||||
|
||||
self.write_ndef_text(&buf[..len], delay)?;
|
||||
|
||||
Ok(all_ok)
|
||||
}
|
||||
}
|
||||
@@ -34,47 +34,30 @@ pub struct Pattern {
|
||||
// Breathing: smooth ramp up/down, ~2.5s cycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Breathing pattern — one engine, smooth sine-like ramp.
|
||||
/// Breathing pattern — one engine, smooth ramp.
|
||||
///
|
||||
/// Slow prescale (15.6ms/step):
|
||||
/// Ramp up: step_time=1, increment=4 → 64 steps × 15.6ms ≈ 1.0s (0→252)
|
||||
/// Ramp down: step_time=1, increment=4 → 64 steps × 15.6ms ≈ 1.0s (252→0)
|
||||
/// Wait: step_time=32 → 32 × 15.6ms ≈ 0.5s pause at bottom
|
||||
/// Total: ~2.5s per cycle
|
||||
/// LP5562 increment field = number of steps - 1 (max 127 = 128 steps).
|
||||
/// Each step changes PWM by 1 unit. Full 0→255 needs two ramp commands.
|
||||
///
|
||||
/// Slow prescale (15.6ms/step), step_time=1:
|
||||
/// Ramp up: 2 × 128 steps × 15.6ms = ~4.0s (0→128→255)
|
||||
/// Ramp down: 2 × 128 steps × 15.6ms = ~4.0s (255→127→0)
|
||||
/// Wait: step_time=48 → 48 × 15.6ms ≈ 0.75s pause at bottom
|
||||
/// Total: ~8.75s per cycle
|
||||
fn breathe_program() -> EngineProgram {
|
||||
EngineProgram::from_commands(&[
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 0→252, ~1s
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 252→0, ~1s
|
||||
EngineCommand::wait(Prescale::Slow, 32), // ~0.5s pause
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 0→128, ~2s
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 128→255, ~2s
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 255→127, ~2s
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 127→0, ~2s
|
||||
EngineCommand::wait(Prescale::Slow, 48), // ~0.75s pause
|
||||
EngineCommand::branch(0, 0), // loop forever
|
||||
])
|
||||
}
|
||||
|
||||
/// Load breathing pattern. All active channels breathe in sync.
|
||||
pub fn breathe(mode: LedMode) -> Pattern {
|
||||
let prog = breathe_program();
|
||||
match mode {
|
||||
LedMode::Rgbw => Pattern {
|
||||
engine1: Some(prog),
|
||||
engine2: None,
|
||||
engine3: None,
|
||||
// Engine1 drives R, G, B; W = I2C direct (off or static)
|
||||
map_b: LedMapping::Engine1,
|
||||
map_g: LedMapping::Engine1,
|
||||
map_r: LedMapping::Engine1,
|
||||
map_w: LedMapping::I2c,
|
||||
},
|
||||
LedMode::Mono3 => Pattern {
|
||||
engine1: Some(prog),
|
||||
engine2: None,
|
||||
engine3: None,
|
||||
// Engine1 drives all 3 mono LEDs in sync
|
||||
map_b: LedMapping::Engine1,
|
||||
map_g: LedMapping::Engine1,
|
||||
map_r: LedMapping::Engine1,
|
||||
map_w: LedMapping::I2c,
|
||||
},
|
||||
}
|
||||
single_engine_pattern(breathe_program(), mode)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -112,27 +95,7 @@ fn heartbeat_program() -> EngineProgram {
|
||||
|
||||
/// Load heartbeat pattern.
|
||||
pub fn heartbeat(mode: LedMode) -> Pattern {
|
||||
let prog = heartbeat_program();
|
||||
match mode {
|
||||
LedMode::Rgbw => Pattern {
|
||||
engine1: Some(prog),
|
||||
engine2: None,
|
||||
engine3: None,
|
||||
map_b: LedMapping::Engine1,
|
||||
map_g: LedMapping::Engine1,
|
||||
map_r: LedMapping::Engine1,
|
||||
map_w: LedMapping::I2c,
|
||||
},
|
||||
LedMode::Mono3 => Pattern {
|
||||
engine1: Some(prog),
|
||||
engine2: None,
|
||||
engine3: None,
|
||||
map_b: LedMapping::Engine1,
|
||||
map_g: LedMapping::Engine1,
|
||||
map_r: LedMapping::Engine1,
|
||||
map_w: LedMapping::I2c,
|
||||
},
|
||||
}
|
||||
single_engine_pattern(heartbeat_program(), mode)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -141,40 +104,44 @@ pub fn heartbeat(mode: LedMode) -> Pattern {
|
||||
|
||||
/// Phase-offset breathing using triggers for synchronization.
|
||||
///
|
||||
/// Engine 1 (leader): breathe up, send trigger, breathe down, wait, send trigger, loop
|
||||
/// Engine 2 (follower): wait for trigger, breathe up, breathe down, wait, wait for trigger, loop
|
||||
/// Engine 3 (follower): wait for trigger from E2, breathe up, breathe down, wait, loop
|
||||
///
|
||||
/// The trigger chain creates a 3-phase offset: E1 starts, signals E2, E2 signals E3.
|
||||
/// Slow prescale (15.6ms/step), step_time=1, increment=127 (128 steps per command):
|
||||
/// Ramp: 2 × 128 steps × 15.6ms = ~4s per full ramp (0→255 or 255→0)
|
||||
/// E1 cycle: ~4s up + ~4s down + ~1s pause = ~9s
|
||||
/// Trigger chain: E1 triggers E2 at ~4s, E2 triggers E3 at ~8s
|
||||
fn rgb_cycle_engine1() -> EngineProgram {
|
||||
EngineProgram::from_commands(&[
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 0: ramp up ~1s
|
||||
EngineCommand::trigger(0, 0b010), // 1: send to E2
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 2: ramp down ~1s
|
||||
EngineCommand::wait(Prescale::Slow, 32), // 3: pause ~0.5s
|
||||
EngineCommand::wait(Prescale::Slow, 32), // 4: pause ~0.5s
|
||||
EngineCommand::branch(0, 0), // 5: loop forever
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 0: 0→128
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 1: 128→255
|
||||
EngineCommand::trigger(0, 0b010), // 2: send to E2
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 3: 255→127
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 4: 127→0
|
||||
EngineCommand::wait(Prescale::Slow, 63), // 5: pause ~1.0s
|
||||
EngineCommand::branch(0, 0), // 6: loop forever
|
||||
])
|
||||
}
|
||||
|
||||
fn rgb_cycle_engine2() -> EngineProgram {
|
||||
EngineProgram::from_commands(&[
|
||||
EngineCommand::trigger(0b001, 0), // 0: wait for E1
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 1: ramp up ~1s
|
||||
EngineCommand::trigger(0, 0b100), // 2: send to E3
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 3: ramp down ~1s
|
||||
EngineCommand::wait(Prescale::Slow, 32), // 4: pause ~0.5s
|
||||
EngineCommand::branch(0, 0), // 5: loop forever
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 1: 0→128
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 2: 128→255
|
||||
EngineCommand::trigger(0, 0b100), // 3: send to E3
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 4: 255→127
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 5: 127→0
|
||||
EngineCommand::wait(Prescale::Slow, 32), // 6: pause ~0.5s
|
||||
EngineCommand::branch(0, 0), // 7: loop forever
|
||||
])
|
||||
}
|
||||
|
||||
fn rgb_cycle_engine3() -> EngineProgram {
|
||||
EngineProgram::from_commands(&[
|
||||
EngineCommand::trigger(0b010, 0), // 0: wait for E2
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 1: ramp up ~1s
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 2: ramp down ~1s
|
||||
EngineCommand::wait(Prescale::Slow, 32), // 3: pause ~0.5s
|
||||
EngineCommand::branch(0, 0), // 4: loop forever
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 1: 0→128
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 2: 128→255
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 3: 255→127
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 4: 127→0
|
||||
EngineCommand::wait(Prescale::Slow, 32), // 5: pause ~0.5s
|
||||
EngineCommand::branch(0, 0), // 6: loop forever
|
||||
])
|
||||
}
|
||||
|
||||
@@ -205,38 +172,155 @@ pub fn rgb_cycle(mode: LedMode) -> Pattern {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Staggered breathe: 3 engines, same breathe program, phase-offset
|
||||
// Slow pulse: gentle ramp, dimmer peak, ~5s cycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Load staggered breathing — all 3 channels breathe independently with phase offset.
|
||||
pub fn staggered_breathe(mode: LedMode) -> Pattern {
|
||||
// Same trigger-chain structure as rgb_cycle but using the same
|
||||
// visual effect (breathe) on each channel. In RGBW mode this creates
|
||||
// a color-shifting breathe; in mono mode it's a traveling wave.
|
||||
rgb_cycle(mode)
|
||||
/// Slow pulse — very gentle and long.
|
||||
///
|
||||
/// Slow prescale (15.6ms/step), step_time=4:
|
||||
/// Ramp up: 2 × 128 steps × 62.4ms = ~16.0s (0→128→255)
|
||||
/// Wait: step_time=32 → ~0.5s hold at peak
|
||||
/// Ramp down: 2 × 128 steps × 62.4ms = ~16.0s (255→127→0)
|
||||
/// Wait: step_time=63 → ~1.0s pause at bottom
|
||||
/// Total: ~33.5s per cycle
|
||||
fn slow_pulse_program() -> EngineProgram {
|
||||
EngineProgram::from_commands(&[
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 4, RampDirection::Up, 127), // 0→128, ~8s
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 4, RampDirection::Up, 127), // 128→255, ~8s
|
||||
EngineCommand::wait(Prescale::Slow, 32), // hold ~0.5s
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 4, RampDirection::Down, 127), // 255→127, ~8s
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 4, RampDirection::Down, 127), // 127→0, ~8s
|
||||
EngineCommand::wait(Prescale::Slow, 63), // pause ~1.0s
|
||||
EngineCommand::branch(0, 0), // loop forever
|
||||
])
|
||||
}
|
||||
|
||||
/// Load slow pulse pattern.
|
||||
pub fn slow_pulse(mode: LedMode) -> Pattern {
|
||||
single_engine_pattern(slow_pulse_program(), mode)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Color wash: 3 engines, smooth overlapping ramps for blended color transitions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Color wash — free-running engines with different cycle lengths.
|
||||
///
|
||||
/// No triggers. Each engine breathes independently at a slightly different
|
||||
/// rate, causing them to drift in and out of phase. Smooth single-PWM-unit
|
||||
/// increments for clean color blending.
|
||||
///
|
||||
/// Slow prescale (15.6ms/step), step_time=1, increment=127 (128 steps per cmd):
|
||||
/// Ramp: 2 × 128 steps × 15.6ms = ~4s per full ramp
|
||||
///
|
||||
/// E1 (Blue): up ~4s + down ~4s = ~8s cycle (no pause)
|
||||
/// E2 (Green): up ~4s + down ~4s + ~0.5s pause = ~8.5s cycle
|
||||
/// E3 (Red): up ~4s + down ~4s + ~1.0s pause = ~9s cycle
|
||||
///
|
||||
/// Phase drift: ~0.5s per cycle → colors shift noticeably every few cycles.
|
||||
fn color_wash_engine1() -> EngineProgram {
|
||||
// ~8s cycle (no pause)
|
||||
EngineProgram::from_commands(&[
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 0→128
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 128→255
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 255→127
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 127→0
|
||||
EngineCommand::branch(0, 0),
|
||||
])
|
||||
}
|
||||
|
||||
fn color_wash_engine2() -> EngineProgram {
|
||||
// ~8.5s cycle (short pause at bottom)
|
||||
EngineProgram::from_commands(&[
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 0→128
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 128→255
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 255→127
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 127→0
|
||||
EngineCommand::wait(Prescale::Slow, 32), // pause ~0.5s
|
||||
EngineCommand::branch(0, 0),
|
||||
])
|
||||
}
|
||||
|
||||
fn color_wash_engine3() -> EngineProgram {
|
||||
// ~9s cycle (longer pause at bottom)
|
||||
EngineProgram::from_commands(&[
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 0→128
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 128→255
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 255→127
|
||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 127→0
|
||||
EngineCommand::wait(Prescale::Slow, 63), // pause ~1.0s
|
||||
EngineCommand::branch(0, 0),
|
||||
])
|
||||
}
|
||||
|
||||
/// Load color wash (RGBW: smooth hue transitions) or wave (mono: traveling slow pulse).
|
||||
pub fn color_wash(mode: LedMode) -> Pattern {
|
||||
match mode {
|
||||
LedMode::Rgbw => Pattern {
|
||||
engine1: Some(color_wash_engine1()),
|
||||
engine2: Some(color_wash_engine2()),
|
||||
engine3: Some(color_wash_engine3()),
|
||||
map_b: LedMapping::Engine1,
|
||||
map_g: LedMapping::Engine2,
|
||||
map_r: LedMapping::Engine3,
|
||||
map_w: LedMapping::I2c,
|
||||
},
|
||||
LedMode::Mono3 => Pattern {
|
||||
engine1: Some(color_wash_engine1()),
|
||||
engine2: Some(color_wash_engine2()),
|
||||
engine3: Some(color_wash_engine3()),
|
||||
map_b: LedMapping::Engine1,
|
||||
map_g: LedMapping::Engine2,
|
||||
map_r: LedMapping::Engine3,
|
||||
map_w: LedMapping::I2c,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper for single-engine patterns (all RGB channels mapped to Engine1)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn single_engine_pattern(prog: EngineProgram, mode: LedMode) -> Pattern {
|
||||
let _ = mode; // Same mapping for both modes
|
||||
Pattern {
|
||||
engine1: Some(prog),
|
||||
engine2: None,
|
||||
engine3: None,
|
||||
map_b: LedMapping::Engine1,
|
||||
map_g: LedMapping::Engine1,
|
||||
map_r: LedMapping::Engine1,
|
||||
map_w: LedMapping::I2c,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pattern loader
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Load a pattern into the LP5562 and start engines running.
|
||||
/// Software-reset LP5562, re-initialize, and load a pattern.
|
||||
///
|
||||
/// Caller must have already called `lp.enable()` and `lp.init_direct_control()`.
|
||||
/// This function handles the full sequence: stop engines → set LED map → load programs → run.
|
||||
/// Writes 0xFF to the Reset register (0x0D) which resets all registers to
|
||||
/// defaults (PWM=0, engines disabled, current=17.5mA). Then re-initializes
|
||||
/// and loads the new pattern. This guarantees zero residual state.
|
||||
pub fn load_pattern<I2C, E>(
|
||||
lp: &mut Lp5562<I2C>,
|
||||
pattern: &Pattern,
|
||||
current: u8,
|
||||
delay: &mut impl embedded_hal::delay::DelayNs,
|
||||
) -> Result<(), crate::led::lp5562::Error<E>>
|
||||
where
|
||||
I2C: I2c<Error = E>,
|
||||
{
|
||||
// Stop all engines first
|
||||
lp.stop_engine(EngineId::Engine1).map_err(crate::led::lp5562::Error::I2c)?;
|
||||
lp.stop_engine(EngineId::Engine2).map_err(crate::led::lp5562::Error::I2c)?;
|
||||
lp.stop_engine(EngineId::Engine3).map_err(crate::led::lp5562::Error::I2c)?;
|
||||
delay.delay_us(200); // >153us between OP_MODE writes
|
||||
// Software reset: all registers to defaults, device enters STANDBY
|
||||
lp.reset().map_err(crate::led::lp5562::Error::I2c)?;
|
||||
delay.delay_ms(1); // allow reset to complete
|
||||
|
||||
// Re-initialize from clean state
|
||||
lp.enable()?;
|
||||
delay.delay_ms(1); // >500us after enable (datasheet: 500µs typical)
|
||||
lp.init_direct_control(crate::led::lp5562::ClockSource::Internal)?;
|
||||
lp.set_all_current(current, current, current, current)?;
|
||||
|
||||
// Set LED mapping
|
||||
lp.set_led_mapping(Channel::Blue, pattern.map_b).map_err(crate::led::lp5562::Error::I2c)?;
|
||||
|
||||
Reference in New Issue
Block a user