Pivot from LP5562 to GPIO-direct PWM LEDs, add NTAG5 EH provisioning
LP5562 requires 2.7V min but NFC energy harvesting produces only 1.8V. New architecture: SAMD21 drives 6 red LEDs directly via TCC0/TCC1 hardware PWM through current-limiting resistors. Key changes: - Add TCC PWM driver (src/led/pwm.rs) for 6 GPIO-direct LED channels - Rewrite pattern engine: software waveform LUTs (sine/triangle/square/ heartbeat) replace LP5562 hardware execution engines - Add XBLK v2 EEPROM format with 16-byte pattern entries + playlist - Add TC4 50Hz ISR for animation, power governor for current budget - Add NTAG5 EH provisioning: persistent config + session trigger - Critical finding: SRAM passthrough in persistent EEPROM blocks all NFC access when MCU unpowered — CONFIG_1 must be session-only - Add provision_eh.py PCSC tool for ACR1552 reader - Update CLAUDE.md and STATUS.md for new architecture Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
72
CLAUDE.md
72
CLAUDE.md
@@ -1,6 +1,6 @@
|
|||||||
# xBlink Development Guide
|
# xBlink Development Guide
|
||||||
|
|
||||||
Embedded Rust firmware for a battery-free, NFC-powered LED implant. The NTAG5Link harvests energy from an NFC field and provides EEPROM storage for LED patterns. A SAMD21E MCU reads patterns from EEPROM, programs LP5562 execution engines, then sleeps while the LP5562 autonomously drives RGBW LEDs.
|
Embedded Rust firmware for a battery-free, NFC-powered LED implant. The NTAG5Link harvests energy from an NFC field (1.8V) and provides EEPROM storage for LED patterns. A SAMD21E MCU reads patterns from EEPROM, drives 6 red LEDs directly via TCC hardware PWM through current-limiting resistors, and updates duty cycles from a 50Hz timer ISR. The MCU stays in IDLE sleep during animation.
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
@@ -19,8 +19,9 @@ cargo hf2 --release
|
|||||||
|
|
||||||
- Architecture: `thumbv6m-none-eabi` (ARM Cortex-M0+)
|
- Architecture: `thumbv6m-none-eabi` (ARM Cortex-M0+)
|
||||||
- Dev board: Seeed XIAO M0 (SAMD21G18A) — will port to bare SAMD21E18A later
|
- Dev board: Seeed XIAO M0 (SAMD21G18A) — will port to bare SAMD21E18A later
|
||||||
- LED driver: LP5562EVM (TI evaluation module, RGBW, I2C addr 0x30)
|
- LEDs: 6x low-Vf red LEDs (Kingbright APTD1608, Vf ~1.7V) driven via TCC PWM + resistors
|
||||||
- NFC: NTAG5Link Click board (MikroElektronika)
|
- NFC: NTAG5Link Click board (MikroElektronika)
|
||||||
|
- LP5562: removed from design (requires 2.7V min, incompatible with 1.8V EH)
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@@ -37,12 +38,13 @@ When porting to bare SAMD21E: replace `xiao_m0` with `atsamd-hal = { version = "
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
main.rs # Entry point: boot, pattern load, sleep/wake loop
|
main.rs # Entry point: boot, EEPROM load, TC4 ISR animation, sleep/wake loop
|
||||||
led/
|
led/
|
||||||
mod.rs # LP5562 re-exports (LedController trait added later, M10)
|
mod.rs # LED module re-exports
|
||||||
lp5562.rs # LP5562 driver (from ../ntag5-samd21-lp562/)
|
pwm.rs # TCC0/TCC1 hardware PWM driver for 6 GPIO-direct LEDs
|
||||||
ntag5/ # NTAG5Link I2C slave driver (M4+)
|
lp5562.rs # LP5562 driver (legacy, kept for reference)
|
||||||
pattern/ # Pattern format and engine builder (M5+)
|
ntag5/ # NTAG5Link I2C slave driver
|
||||||
|
pattern/ # XBLK v2 format, software pattern engine, waveform LUTs
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture Conventions
|
## Architecture Conventions
|
||||||
@@ -55,23 +57,32 @@ src/
|
|||||||
- **`const fn`**: Prefer `const fn` where possible (see `EngineCommand` builder in LP5562 driver).
|
- **`const fn`**: Prefer `const fn` where possible (see `EngineCommand` builder in LP5562 driver).
|
||||||
- **No `unsafe`**: Avoid unless absolutely necessary for hardware access.
|
- **No `unsafe`**: Avoid unless absolutely necessary for hardware access.
|
||||||
|
|
||||||
## LP5562 Timing Constraints
|
## LED PWM Architecture
|
||||||
|
|
||||||
The LP5562 driver does NOT enforce timing delays internally — the caller is responsible:
|
6 LEDs driven directly by SAMD21 TCC hardware PWM through current-limiting resistors (10-47 ohm).
|
||||||
|
|
||||||
- **>= 500 us** after `enable()` before any other commands
|
| LED | TCC | Channel | SAMD21E Pin | XIAO Pin |
|
||||||
- **>= 488 us** between consecutive ENABLE register writes (engine exec changes)
|
|-----|------|---------|-------------|----------|
|
||||||
- **>= 153 us** between consecutive OP_MODE register writes (engine mode changes)
|
| 0 | TCC0 | WO[0] | PA04 | A1 |
|
||||||
- When transitioning from Run, set exec to Hold first, then change mode
|
| 1 | TCC0 | WO[1] | PA05 | A2 |
|
||||||
|
| 2 | TCC0 | WO[2] | PA06 | A3* |
|
||||||
|
| 3 | TCC0 | WO[3] | PA07 | A4* |
|
||||||
|
| 4 | TCC1 | WO[0] | PA10 | D2 |
|
||||||
|
| 5 | TCC1 | WO[1] | PA11 | D3 |
|
||||||
|
|
||||||
## Key I2C Addresses
|
*PA06/PA07 conflict with I2C on XIAO M0. Dev board uses 4 LEDs (0,1,4,5).
|
||||||
|
|
||||||
Both devices share the same I2C bus (A4/A5). Non-conflicting addresses — no bus arbitration issues.
|
- **Animation**: TC4 ISR at 50Hz computes brightness from PatternEngine, writes TCC CC registers
|
||||||
|
- **Power governor**: Proportional scaling ensures total LED current stays within NTAG5 EH budget
|
||||||
|
- **Sleep**: IDLE mode during animation (~0.5mA MCU), STANDBY when no pattern active (~2uA)
|
||||||
|
|
||||||
| Device | Address | Notes |
|
## Key I2C Address
|
||||||
| --------- | ------- | ------------------------------------------ |
|
|
||||||
| LP5562 | 0x30 | ADDR_SEL pins both low (LP5562EVM default) |
|
I2C bus (A4/A5) is used for NTAG5 only (LP5562 removed from design).
|
||||||
| NTAG5Link | 0x54 | Default NTP53x2 I2C slave address |
|
|
||||||
|
| Device | Address | Notes |
|
||||||
|
| --------- | ------- | --------------------------------- |
|
||||||
|
| NTAG5Link | 0x54 | Default NTP53x2 I2C slave address |
|
||||||
|
|
||||||
## NTAG5Link I2C Register Map (MCU-side)
|
## NTAG5Link I2C Register Map (MCU-side)
|
||||||
|
|
||||||
@@ -93,24 +104,25 @@ Key config constants (from `../ntag5sensor/vicinity/ntag5link.py`):
|
|||||||
|
|
||||||
## Hardware Wiring (Dev Board)
|
## Hardware Wiring (Dev Board)
|
||||||
|
|
||||||
| XIAO Pin | Connection | Function |
|
| XIAO Pin | Connection | Function |
|
||||||
| --------- | --------------------------- | ----------------------------------------------- |
|
| -------- | --------------------- | ------------------------------------------- |
|
||||||
| A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data |
|
| A1 (PA04)| LED 0 + resistor | TCC0/WO[0] PWM output |
|
||||||
| A5 (SCL) | LP5562 SCL, NTAG5 SCL | Shared I2C clock |
|
| A2 (PA05)| LED 1 + resistor | TCC0/WO[1] PWM output |
|
||||||
| D0/A0 | LP5562 EN line (open-drain) | Wired-AND with hall sensor, pull-up to VCC |
|
| A4 (SDA) | NTAG5 SDA | I2C data |
|
||||||
| TBD (EIC) | Hall sensor output | EIC wake + wired-AND to LP5562 EN line |
|
| A5 (SCL) | NTAG5 SCL | I2C clock |
|
||||||
| TBD (EIC) | NTAG5 FD pin | Field detect / SRAM write indication (EIC wake) |
|
| D2 (PA10)| LED 4 + resistor | TCC1/WO[0] PWM output |
|
||||||
|
| D3 (PA11)| LED 5 + resistor | TCC1/WO[1] PWM output |
|
||||||
**LP5562 EN wired-AND**: D0/A0 (open-drain) and hall sensor (open-drain, active-low) both connect to LP5562 EN with a 1M pull-up. Either can force EN low. Magnet kills LEDs at hardware level regardless of MCU state.
|
| A1 (PA04)| NTAG5 FD pin | Field detect / SRAM write indication (EIC) |
|
||||||
|
| TBD | Hall sensor output | EIC wake, pattern cycling |
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- **Hardware**: XIAO M0 + LP5562EVM + NTAG5Link Click (when jumpers available)
|
- **Hardware**: XIAO M0 + NTAG5Link Click + 4 LEDs with resistors on A1/A2/D2/D3
|
||||||
- **PCSC reader**: Use `../ntag5sensor/` Python tooling with ACR1552 reader
|
- **PCSC reader**: Use `../ntag5sensor/` Python tooling with ACR1552 reader
|
||||||
- **Phone NFC**: VivoKey RawNFC app for SRAM mailbox testing
|
- **Phone NFC**: VivoKey RawNFC app for SRAM mailbox testing
|
||||||
- **Phone app**: DT NFC Identifier for basic tag info
|
- **Phone app**: DT NFC Identifier for basic tag info
|
||||||
|
|
||||||
## Sibling Projects
|
## Sibling Projects
|
||||||
|
|
||||||
- `../ntag5-samd21-lp562/` — Original LP5562 driver + smoke test (xBlink's `src/led/lp5562.rs` is synced from here)
|
- `../ntag5-samd21-lp562/` — Original LP5562 driver (legacy reference)
|
||||||
- `../ntag5sensor/` — NTAG5Link command reference (`vicinity/ntag5link.py`), I2C patterns (`vicinity/i2cbase.py`)
|
- `../ntag5sensor/` — NTAG5Link command reference (`vicinity/ntag5link.py`), I2C patterns (`vicinity/i2cbase.py`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# xblink Project Status
|
# xblink Project Status
|
||||||
|
|
||||||
**Current Milestone**: M7 — Sleep/Wake
|
**Current Milestone**: GPIO-Direct LED Pivot (replacing LP5562)
|
||||||
**Last Updated**: 2026-03-05
|
**Last Updated**: 2026-03-07
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -76,16 +76,37 @@
|
|||||||
- [x] I2C bus swapping for LP5562 reprogramming after pattern updates
|
- [x] I2C bus swapping for LP5562 reprogramming after pattern updates
|
||||||
- [ ] Hardware test: flash and verify with PCSC reader / phone app (pending FD pin wiring)
|
- [ ] Hardware test: flash and verify with PCSC reader / phone app (pending FD pin wiring)
|
||||||
|
|
||||||
|
## GPIO-Direct LED Pivot (2026-03-07)
|
||||||
|
|
||||||
|
LP5562 requires VDD >= 2.7V, incompatible with 1.8V NFC energy harvesting.
|
||||||
|
Replaced with SAMD21E GPIO-direct PWM driving 6 red LEDs.
|
||||||
|
See `docs/plans/2026-03-06-gpio-led-pivot.md` for full design.
|
||||||
|
|
||||||
|
### LED Pivot Implementation
|
||||||
|
|
||||||
|
- [x] Design GPIO-direct PWM architecture (`docs/plans/2026-03-06-gpio-led-pivot.md`)
|
||||||
|
- [x] Implement software pattern engine with waveform LUTs (`src/pattern/mod.rs`)
|
||||||
|
- [x] Implement XBLK v2 EEPROM format (16-byte entries, playlist support)
|
||||||
|
- [x] Implement TCC PWM driver for 6 LED channels (`src/led/pwm.rs`)
|
||||||
|
- [x] Implement TC4 50Hz animation timer ISR
|
||||||
|
- [x] Implement power governor (proportional brightness scaling)
|
||||||
|
- [x] Update main.rs for GPIO-direct boot flow
|
||||||
|
- [x] Update SRAM mailbox protocol for v2 format (`src/ntag5/sram.rs`)
|
||||||
|
- [x] Update Python serializer (`tools/xblk_serialize.py`)
|
||||||
|
- [x] Verify `cargo build --release` compiles
|
||||||
|
- [ ] Hardware test: wire LEDs to A1/A2/D2/D3, flash and verify animations
|
||||||
|
- [ ] Measure power consumption with multimeter
|
||||||
|
|
||||||
## Group C — Power + Recovery
|
## Group C — Power + Recovery
|
||||||
|
|
||||||
**Hardware needed**: + Hall sensor + multimeter
|
**Hardware needed**: + Hall sensor + multimeter
|
||||||
|
|
||||||
### M7: Sleep/Wake
|
### M7: Sleep/Wake
|
||||||
|
|
||||||
- [ ] Configure SAMD21 EIC for FD pin wake
|
- [x] Configure SAMD21 EIC for FD pin wake
|
||||||
- [ ] Implement STANDBY sleep after LP5562 programming
|
- [x] Implement IDLE sleep during animation (TC4 ISR drives LEDs)
|
||||||
- [ ] Verify LP5562 keeps running during MCU sleep
|
- [x] Implement STANDBY sleep when no pattern active
|
||||||
- [ ] Measure current: active vs standby vs total system
|
- [ ] Measure current: IDLE (animating) vs STANDBY vs total system
|
||||||
|
|
||||||
### M8: Recovery Mode
|
### M8: Recovery Mode
|
||||||
|
|
||||||
@@ -96,28 +117,17 @@
|
|||||||
|
|
||||||
### M9: Power Characterization
|
### M9: Power Characterization
|
||||||
|
|
||||||
- [ ] Measure current at various LED current settings
|
- [ ] Measure current at various LED brightness / resistor values
|
||||||
- [ ] Test NTAG5Link EH output with phone NFC
|
- [ ] Test NTAG5Link EH output with phone NFC at 1.8V
|
||||||
- [ ] Find optimal brightness vs EH budget balance
|
- [ ] Tune power governor budget for optimal brightness
|
||||||
- [ ] Document real power numbers
|
- [ ] Document real power numbers
|
||||||
|
|
||||||
## Group D — Abstraction + Polish
|
## Group D — Future
|
||||||
|
|
||||||
### M10: LedController Trait
|
|
||||||
|
|
||||||
- [ ] Define trait based on proven usage patterns from Groups A-C
|
|
||||||
- [ ] Implement for LP5562 wrapper
|
|
||||||
- [ ] Refactor main.rs to trait-based API
|
|
||||||
|
|
||||||
### M11: SimplePwm Driver
|
|
||||||
|
|
||||||
- [ ] Implement only if single-color LED hardware is available
|
|
||||||
|
|
||||||
## Group E — Future
|
|
||||||
|
|
||||||
### Custom PCB (SAMD21E)
|
### Custom PCB (SAMD21E)
|
||||||
|
|
||||||
- [ ] Port to `atsamd-hal` with `samd21e` feature
|
- [ ] Port to `atsamd-hal` with `samd21e` feature
|
||||||
|
- [ ] Add LEDs 2-5 on PA06/PA07 (freed from I2C conflict on custom PCB)
|
||||||
- [ ] Custom `memory.x` linker script
|
- [ ] Custom `memory.x` linker script
|
||||||
- [ ] PCB design and fabrication
|
- [ ] PCB design and fabrication
|
||||||
|
|
||||||
@@ -135,23 +145,21 @@
|
|||||||
|
|
||||||
| Question | Status | Notes |
|
| Question | Status | Notes |
|
||||||
|----------|--------|-------|
|
|----------|--------|-------|
|
||||||
| Energy harvesting power budget | TBD | Multimeter measurements in M9 |
|
| Energy harvesting power budget | TBD | Multimeter measurements at 1.8V |
|
||||||
| Optimal LED current per channel | TBD | Probably 2-5mA/ch, verify in M9 |
|
| Optimal LED resistor value | TBD | 10-47 ohm, affects brightness vs power |
|
||||||
|
| Power governor budget default | Set to 50 | Configurable via companion app |
|
||||||
| Firmware update strategy | Deferred | UF2 for dev, NFC OTA evaluated later |
|
| Firmware update strategy | Deferred | UF2 for dev, NFC OTA evaluated later |
|
||||||
| NTAG5Link I2C slave address | Assumed 0x54 | Verify in M4 with Click board |
|
| Hall sensor part selection | Decided | DRV5032FB (SOT-23, 8.4mT, prototype) |
|
||||||
| Pattern binary format | Deferred to M5 | Design after M2-M3 engine experience |
|
|
||||||
| Hall sensor + EN circuit | Designed | Wired-AND: hall + MCU open-drain on EN with 1M pull-up. See `docs/plans/2026-03-03-hall-en-design.md` |
|
|
||||||
| Hall sensor part selection | Decided | DRV5032FB (SOT-23, 8.4mT, prototype) → DRV5032FE (X2SON 1x1mm, final PCB) |
|
|
||||||
| EN pull-up value | Decided | 1M — zero steady-state draw, 3µA when EN low, ~10µs rise time |
|
|
||||||
|
|
||||||
## Hardware Inventory
|
## Hardware Inventory
|
||||||
|
|
||||||
| Item | Status | Notes |
|
| Item | Status | Notes |
|
||||||
|------|--------|-------|
|
|------|--------|-------|
|
||||||
| Seeed XIAO M0 | Available | Dev board MCU (SAMD21G18A) |
|
| Seeed XIAO M0 | Available | Dev board MCU (SAMD21G18A) |
|
||||||
| LP5562EVM | Available | TI eval module, RGBW LEDs, I2C addr 0x30 |
|
| LP5562EVM | Available (unused) | Removed from design (VDD > 2.7V) |
|
||||||
| Mini-USB cable | Available | USB-C, used for flashing XIAO M0 |
|
|
||||||
| NTAG5 Link Click | Available, partially wired | Missing I2C jumper to XIAO |
|
| NTAG5 Link Click | Available, partially wired | Missing I2C jumper to XIAO |
|
||||||
| Hall effect sensor | Not available | Need to source — TI DRV5032FB (SOT-23) for prototype |
|
| Low-Vf red LEDs | Need to source | Kingbright APTD1608 or similar, Vf ~1.7V |
|
||||||
|
| Resistors (10-47 ohm) | Need to source | Current limiting for GPIO-direct LEDs |
|
||||||
|
| Hall effect sensor | Not available | Need to source — TI DRV5032FB (SOT-23) |
|
||||||
| ACR1552 PCSC reader | Available | For ntag5sensor Python tooling |
|
| ACR1552 PCSC reader | Available | For ntag5sensor Python tooling |
|
||||||
| Multimeter | Available | For power budget measurements |
|
| Multimeter | Available | For power budget measurements |
|
||||||
|
|||||||
144
docs/plans/2026-03-06-gpio-led-pivot.md
Normal file
144
docs/plans/2026-03-06-gpio-led-pivot.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# GPIO-Direct LED Pivot
|
||||||
|
|
||||||
|
**Date**: 2026-03-06
|
||||||
|
**Status**: Design complete, implementation pending
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The LP5562 LED driver IC requires VDD >= 2.7V. Our NTAG5Link NFC energy harvesting antenna produces only 1.8V. No workaround exists -- a boost converter would exceed the power budget. We're replacing the LP5562 with SAMD21E GPIO-direct PWM driving 6 low-Vf red LEDs through current-limiting resistors.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **Remove**: LP5562 from BOM, `src/led/lp5562.rs` driver, LP5562 engine opcode pattern format
|
||||||
|
- **Add**: TCC hardware PWM driver (`src/led/pwm.rs`), software pattern engine, XBLK v2 EEPROM format
|
||||||
|
- **Keep**: NTAG5Link driver, SRAM mailbox protocol, EEPROM storage, EIC wake, hall sensor design
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
**LEDs**: 6x Kingbright APTD1608 (0603, red, Vf ~1.7V at 1mA). 0.1V headroom at 1.8V supply. Arranged in a line.
|
||||||
|
|
||||||
|
**PWM outputs**: TCC0 (4 channels) + TCC1 (2 channels) = 6 independent PWM channels.
|
||||||
|
|
||||||
|
**Pin assignments** (SAMD21E, TCC-capable pins):
|
||||||
|
|
||||||
|
| LED | TCC | Channel | SAMD21E Pin | XIAO Pin |
|
||||||
|
|-----|------|---------|-------------|----------|
|
||||||
|
| 0 | TCC0 | WO[0] | PA04 | A1 |
|
||||||
|
| 1 | TCC0 | WO[1] | PA05 | A2 |
|
||||||
|
| 2 | TCC0 | WO[2] | PA06 | A3 |
|
||||||
|
| 3 | TCC0 | WO[3] | PA07 | A4* |
|
||||||
|
| 4 | TCC1 | WO[0] | PA10 | D2 |
|
||||||
|
| 5 | TCC1 | WO[1] | PA11 | D3 |
|
||||||
|
|
||||||
|
*PA06/PA07 conflict with I2C (A4/A5) on XIAO M0. Dev board testing uses 4 LEDs on non-conflicting pins. Custom PCB (SAMD21E) has no conflict.
|
||||||
|
|
||||||
|
**Resistors**: 10-47 ohm series per LED. At 1.8V supply, Vf=1.7V: I = 0.1V/47ohm = ~2mA (safe). Lower resistance = brighter but more current. Governor handles the budget.
|
||||||
|
|
||||||
|
## Power Governor
|
||||||
|
|
||||||
|
The NTAG5 drops out entirely (hard power loss) when current budget is exceeded. The governor prevents this by running every ISR tick before writing TCC registers:
|
||||||
|
|
||||||
|
```
|
||||||
|
raw[6] = pattern engine computes brightness 0-255 per LED
|
||||||
|
total = sum(raw[0..6])
|
||||||
|
if total > BUDGET:
|
||||||
|
scale = BUDGET * 256 / total // fixed-point
|
||||||
|
for i in 0..6:
|
||||||
|
raw[i] = raw[i] * scale / 256
|
||||||
|
write raw values to TCC CC registers
|
||||||
|
```
|
||||||
|
|
||||||
|
**BUDGET**: Stored in XBLK v2 header (1 byte, units of ~0.1mA steps). Default ~50 (5mA total LED budget). Configurable via companion app or SRAM mailbox command.
|
||||||
|
|
||||||
|
**Phase splitting** is the key optimization: stagger LED phase offsets so peaks don't align. A 6-LED sine wave with 60-degree phase offsets has roughly constant total brightness, maximizing perceived brightness within the budget.
|
||||||
|
|
||||||
|
## Software Pattern Engine
|
||||||
|
|
||||||
|
Replaces LP5562's hardware execution engines. Runs in TC4 ISR at 50Hz.
|
||||||
|
|
||||||
|
**PatternState** (per-pattern, loaded from EEPROM):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct PatternState {
|
||||||
|
waveform: Waveform, // sine, triangle, square, heartbeat
|
||||||
|
cycle_len: u8, // ticks per full cycle (1-255 = 20ms-5.1s)
|
||||||
|
phase: [u8; 6], // phase offset per LED (0-255 = 0-360 degrees)
|
||||||
|
envelope: [u8; 6], // max brightness per LED (governor input)
|
||||||
|
tick: u16, // current animation tick (runtime, not stored)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Waveform lookup tables** (const, in flash):
|
||||||
|
- `SINE_LUT[256]`: 8-bit sine quarter-wave, mirrored at runtime
|
||||||
|
- `TRIANGLE_LUT[256]`: linear ramp up/down
|
||||||
|
- `HEARTBEAT_LUT[256]`: double-pulse cardiac shape
|
||||||
|
|
||||||
|
**ISR flow** (~40us at 8MHz):
|
||||||
|
1. Increment `tick`, wrap at `cycle_len * 256`
|
||||||
|
2. For each LED: `phase_pos = (tick + phase[i] * cycle_len) % (cycle_len * 256)`
|
||||||
|
3. Look up `waveform[phase_pos]`, scale by `envelope[i]`
|
||||||
|
4. Apply governor scaling
|
||||||
|
5. Write 6 TCC CC registers
|
||||||
|
|
||||||
|
**MCU sleep**: IDLE mode (not STANDBY) during animation. CPU halts between interrupts, peripherals (TCC, TC4) keep running. ~0.5mA at 1.8V/8MHz. CPU active only ~40us per 20ms tick = 0.2% duty cycle.
|
||||||
|
|
||||||
|
## XBLK v2 EEPROM Format
|
||||||
|
|
||||||
|
**Storage region**: Last 1KB of NTAG5 EEPROM (blocks 0x100-0x1FF), beyond normal NFC/NDEF access.
|
||||||
|
|
||||||
|
### Header (16 bytes)
|
||||||
|
|
||||||
|
```
|
||||||
|
Offset Size Field
|
||||||
|
0x00 4 magic "XBLK"
|
||||||
|
0x04 1 version 0x02
|
||||||
|
0x05 1 flags bit 0: has_playlist
|
||||||
|
0x06 1 pattern_count number of pattern entries
|
||||||
|
0x07 1 active_index pattern to load on boot
|
||||||
|
0x08 1 budget power governor budget
|
||||||
|
0x09 1 playlist_count number of playlist entries (0 if no playlist)
|
||||||
|
0x0A 4 reserved
|
||||||
|
0x0E 2 crc16 over bytes 0x00-0x0D
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern Entry (16 bytes each, immediately after header)
|
||||||
|
|
||||||
|
```
|
||||||
|
Offset Size Field
|
||||||
|
0x00 1 waveform (0=sine, 1=triangle, 2=square, 3=heartbeat)
|
||||||
|
0x01 1 cycle_len (in 50Hz ticks, 1-255 = 20ms-5.1s)
|
||||||
|
0x02 6 phase[6] (0-255 phase offset per LED, maps to 0-360 degrees)
|
||||||
|
0x08 6 envelope[6] (max brightness per LED, 0-255)
|
||||||
|
0x0E 1 repeat_count (times to play before advancing playlist, 0xFF=forever)
|
||||||
|
0x0F 1 reserved
|
||||||
|
```
|
||||||
|
|
||||||
|
### Playlist Table (after all pattern entries, if flags bit 0 set)
|
||||||
|
|
||||||
|
Each playlist entry is 1 byte = pattern index. Sequence plays in order, loops back to start. Max 32 entries.
|
||||||
|
|
||||||
|
Example: patterns [breathe=0, chase=1, flash=2] with playlist [1, 2, 2, 0] plays: chase, flash, flash, breathe, chase, ...
|
||||||
|
|
||||||
|
**Capacity**: 1024B - 16B header - 32B playlist = 976B for patterns. At 16B each: 61 patterns.
|
||||||
|
|
||||||
|
## Sleep and Power-on Behavior
|
||||||
|
|
||||||
|
Three MCU states:
|
||||||
|
|
||||||
|
1. **STANDBY** (~2uA) -- No animation. Wakes on FD pin (EIC) for SRAM commands or hall sensor tap.
|
||||||
|
2. **ANIMATING** (~0.5mA MCU + LED current) -- IDLE sleep, TC4 ISR at 50Hz updates TCC duty cycles.
|
||||||
|
3. **COMMAND PROCESSING** -- Fully awake, handling SRAM mailbox. Returns to ANIMATING or STANDBY.
|
||||||
|
|
||||||
|
**Power-on sequence**:
|
||||||
|
1. NTAG5 EH powers up, VOUT rises, SAMD21 boots
|
||||||
|
2. Read EEPROM header -- if valid XBLK v2, load pattern at `active_index`
|
||||||
|
3. If playlist exists, start playlist from entry 0
|
||||||
|
4. Configure TC4 (50Hz), TCC0/TCC1 (PWM), start animation
|
||||||
|
5. Enter IDLE sleep (animation runs via ISR)
|
||||||
|
6. On FD interrupt, wake fully, process SRAM command, resume
|
||||||
|
|
||||||
|
**Playlist advancement**: When `repeat_count` cycles complete for current pattern, ISR loads next playlist entry. If playlist wraps, loop from start. Hall sensor tap: advance to next playlist entry (or next pattern if no playlist).
|
||||||
|
|
||||||
|
**Pattern cycling (no playlist)**: Hall sensor increments `active_index`, loads next pattern. Wrapping past last pattern goes to STANDBY (LEDs off).
|
||||||
@@ -1 +1,2 @@
|
|||||||
pub mod lp5562;
|
pub mod lp5562;
|
||||||
|
pub mod pwm;
|
||||||
|
|||||||
139
src/led/pwm.rs
Normal file
139
src/led/pwm.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
//! TCC hardware PWM driver for 6 GPIO-direct LEDs.
|
||||||
|
//!
|
||||||
|
//! Uses TCC0 (4 channels: WO[0]-WO[3]) and TCC1 (2 channels: WO[0]-WO[1])
|
||||||
|
//! to drive 6 LEDs via current-limiting resistors.
|
||||||
|
//!
|
||||||
|
//! Pin assignments (SAMD21E target, XIAO M0 dev board):
|
||||||
|
//! LED 0: TCC0/WO[0] on PA04 (XIAO A1)
|
||||||
|
//! LED 1: TCC0/WO[1] on PA05 (XIAO A2)
|
||||||
|
//! LED 2: TCC0/WO[2] on PA06 (XIAO A3) — conflicts with I2C on XIAO
|
||||||
|
//! LED 3: TCC0/WO[3] on PA07 (XIAO A4) — conflicts with I2C on XIAO
|
||||||
|
//! LED 4: TCC1/WO[0] on PA10 (XIAO D2)
|
||||||
|
//! LED 5: TCC1/WO[1] on PA11 (XIAO D3)
|
||||||
|
//!
|
||||||
|
//! For dev board: only LEDs 0, 1, 4, 5 are usable (4 LEDs).
|
||||||
|
|
||||||
|
use crate::pattern::NUM_LEDS;
|
||||||
|
|
||||||
|
/// Initialize TCC0 and TCC1 for PWM output.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// Must be called once during init. Caller must ensure PM and GCLK are configured.
|
||||||
|
pub unsafe fn init() {
|
||||||
|
let pm = &*crate::pac::PM::ptr();
|
||||||
|
let gclk = &*crate::pac::GCLK::ptr();
|
||||||
|
let port = &*crate::pac::PORT::ptr();
|
||||||
|
let tcc0 = &*crate::pac::TCC0::ptr();
|
||||||
|
let tcc1 = &*crate::pac::TCC1::ptr();
|
||||||
|
|
||||||
|
// Enable GCLK0 for TCC0/TCC1 (generic clock ID 0x1A = 26)
|
||||||
|
gclk.clkctrl.write(|w| {
|
||||||
|
w.id().bits(0x1A)
|
||||||
|
.gen().gclk0()
|
||||||
|
.clken().set_bit()
|
||||||
|
});
|
||||||
|
while gclk.status.read().syncbusy().bit_is_set() {}
|
||||||
|
|
||||||
|
// Enable TCC0 and TCC1 in Power Manager
|
||||||
|
pm.apbcmask.modify(|_, w| {
|
||||||
|
w.tcc0_().set_bit()
|
||||||
|
.tcc1_().set_bit()
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Pin muxing: function E (0x04) for TCC ---
|
||||||
|
// PA04 (even in group 2)
|
||||||
|
port.pmux0_[2].modify(|_, w| w.pmuxe().bits(0x04));
|
||||||
|
port.pincfg0_[4].modify(|_, w| w.pmuxen().set_bit());
|
||||||
|
// PA05 (odd in group 2)
|
||||||
|
port.pmux0_[2].modify(|_, w| w.pmuxo().bits(0x04));
|
||||||
|
port.pincfg0_[5].modify(|_, w| w.pmuxen().set_bit());
|
||||||
|
// PA10 (even in group 5)
|
||||||
|
port.pmux0_[5].modify(|_, w| w.pmuxe().bits(0x04));
|
||||||
|
port.pincfg0_[10].modify(|_, w| w.pmuxen().set_bit());
|
||||||
|
// PA11 (odd in group 5)
|
||||||
|
port.pmux0_[5].modify(|_, w| w.pmuxo().bits(0x04));
|
||||||
|
port.pincfg0_[11].modify(|_, w| w.pmuxen().set_bit());
|
||||||
|
|
||||||
|
// --- Configure TCC0 ---
|
||||||
|
tcc0.ctrla.modify(|_, w| w.enable().clear_bit());
|
||||||
|
while tcc0.syncbusy.read().enable().bit_is_set() {}
|
||||||
|
|
||||||
|
tcc0.ctrla.write(|w| {
|
||||||
|
w.prescaler().div1()
|
||||||
|
.prescsync().presc()
|
||||||
|
});
|
||||||
|
|
||||||
|
tcc0.wave.write(|w| w.wavegen().npwm());
|
||||||
|
while tcc0.syncbusy.read().wave().bit_is_set() {}
|
||||||
|
|
||||||
|
// Period = 255 (8-bit resolution, ~31.4 kHz at 8 MHz)
|
||||||
|
tcc0.per().write(|w| w.bits(255));
|
||||||
|
while tcc0.syncbusy.read().per().bit_is_set() {}
|
||||||
|
|
||||||
|
// All channels start at 0
|
||||||
|
tcc0.cc()[0].write(|w| w.bits(0));
|
||||||
|
tcc0.cc()[1].write(|w| w.bits(0));
|
||||||
|
tcc0.cc()[2].write(|w| w.bits(0));
|
||||||
|
tcc0.cc()[3].write(|w| w.bits(0));
|
||||||
|
while tcc0.syncbusy.read().cc0().bit_is_set() {}
|
||||||
|
while tcc0.syncbusy.read().cc1().bit_is_set() {}
|
||||||
|
while tcc0.syncbusy.read().cc2().bit_is_set() {}
|
||||||
|
while tcc0.syncbusy.read().cc3().bit_is_set() {}
|
||||||
|
|
||||||
|
tcc0.ctrla.modify(|_, w| w.enable().set_bit());
|
||||||
|
while tcc0.syncbusy.read().enable().bit_is_set() {}
|
||||||
|
|
||||||
|
// --- Configure TCC1 ---
|
||||||
|
tcc1.ctrla.modify(|_, w| w.enable().clear_bit());
|
||||||
|
while tcc1.syncbusy.read().enable().bit_is_set() {}
|
||||||
|
|
||||||
|
tcc1.ctrla.write(|w| {
|
||||||
|
w.prescaler().div1()
|
||||||
|
.prescsync().presc()
|
||||||
|
});
|
||||||
|
|
||||||
|
tcc1.wave.write(|w| w.wavegen().npwm());
|
||||||
|
while tcc1.syncbusy.read().wave().bit_is_set() {}
|
||||||
|
|
||||||
|
tcc1.per().write(|w| w.bits(255));
|
||||||
|
while tcc1.syncbusy.read().per().bit_is_set() {}
|
||||||
|
|
||||||
|
tcc1.cc()[0].write(|w| w.bits(0));
|
||||||
|
tcc1.cc()[1].write(|w| w.bits(0));
|
||||||
|
while tcc1.syncbusy.read().cc0().bit_is_set() {}
|
||||||
|
while tcc1.syncbusy.read().cc1().bit_is_set() {}
|
||||||
|
|
||||||
|
tcc1.ctrla.modify(|_, w| w.enable().set_bit());
|
||||||
|
while tcc1.syncbusy.read().enable().bit_is_set() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set duty cycle for a single LED channel (0-255).
|
||||||
|
#[inline]
|
||||||
|
pub fn set_duty(channel: u8, duty: u8) {
|
||||||
|
unsafe {
|
||||||
|
match channel {
|
||||||
|
0..=3 => {
|
||||||
|
let tcc0 = &*crate::pac::TCC0::ptr();
|
||||||
|
tcc0.cc()[channel as usize].write(|w| w.bits(duty as u32));
|
||||||
|
}
|
||||||
|
4..=5 => {
|
||||||
|
let tcc1 = &*crate::pac::TCC1::ptr();
|
||||||
|
tcc1.cc()[(channel - 4) as usize].write(|w| w.bits(duty as u32));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set duty cycles for all 6 LEDs at once.
|
||||||
|
pub fn set_all(duties: &[u8; NUM_LEDS]) {
|
||||||
|
for (i, &d) in duties.iter().enumerate() {
|
||||||
|
set_duty(i as u8, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn all LEDs off.
|
||||||
|
pub fn all_off() {
|
||||||
|
let zeros = [0u8; NUM_LEDS];
|
||||||
|
set_all(&zeros);
|
||||||
|
}
|
||||||
607
src/main.rs
607
src/main.rs
@@ -9,18 +9,32 @@ use bsp::entry;
|
|||||||
use bsp::hal;
|
use bsp::hal;
|
||||||
use bsp::pac;
|
use bsp::pac;
|
||||||
|
|
||||||
use hal::clock::GenericClockController;
|
use hal::clock::{ClockGenId, GenericClockController};
|
||||||
use hal::delay::Delay;
|
use hal::delay::Delay;
|
||||||
|
use hal::eic::pin::{ExtInt4, Sense};
|
||||||
|
use hal::eic::EIC;
|
||||||
|
use hal::gpio::PullUpInterrupt;
|
||||||
use hal::prelude::*;
|
use hal::prelude::*;
|
||||||
use pac::{CorePeripherals, Peripherals};
|
use pac::{interrupt, CorePeripherals, Peripherals};
|
||||||
|
|
||||||
|
use cortex_m::peripheral::NVIC;
|
||||||
|
|
||||||
|
use core::cell::RefCell;
|
||||||
|
use cortex_m::interrupt::Mutex;
|
||||||
|
|
||||||
mod led;
|
mod led;
|
||||||
mod ntag5;
|
mod ntag5;
|
||||||
mod pattern;
|
mod pattern;
|
||||||
use led::lp5562::{ClockSource, Lp5562, DEFAULT_ADDRESS};
|
|
||||||
use ntag5::Ntag5Link;
|
use ntag5::Ntag5Link;
|
||||||
use ntag5::sram::MailboxState;
|
use ntag5::sram::MailboxState;
|
||||||
use pattern::LedMode;
|
use ntag5::SESSION_CONFIG_REG;
|
||||||
|
use pattern::{PatternEngine, PatternDef, NUM_LEDS};
|
||||||
|
|
||||||
|
/// Global pattern engine, accessed from TC4 ISR and main loop.
|
||||||
|
static ENGINE: Mutex<RefCell<Option<PatternEngine>>> = Mutex::new(RefCell::new(None));
|
||||||
|
|
||||||
|
/// Flag set by TC4 ISR: pattern repeats are done, main loop should advance playlist.
|
||||||
|
static REPEATS_DONE: Mutex<RefCell<bool>> = Mutex::new(RefCell::new(false));
|
||||||
|
|
||||||
/// Blink the on-board LED n times (active-low: low=on, high=off)
|
/// Blink the on-board LED n times (active-low: low=on, high=off)
|
||||||
fn blink<D: embedded_hal::delay::DelayNs>(led: &mut bsp::Led0, delay: &mut D, times: u8, ms: u16) {
|
fn blink<D: embedded_hal::delay::DelayNs>(led: &mut bsp::Led0, delay: &mut D, times: u8, ms: u16) {
|
||||||
@@ -32,10 +46,59 @@ fn blink<D: embedded_hal::delay::DelayNs>(led: &mut bsp::Led0, delay: &mut D, ti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configure TC4 for 50Hz interrupt (animation tick).
|
||||||
|
unsafe fn init_tc4() {
|
||||||
|
let pm = &*pac::PM::ptr();
|
||||||
|
let gclk = &*pac::GCLK::ptr();
|
||||||
|
let tc4 = &*pac::TC4::ptr();
|
||||||
|
|
||||||
|
// Enable GCLK0 for TC4/TC5 (generic clock ID 0x1C = 28)
|
||||||
|
gclk.clkctrl.write(|w| {
|
||||||
|
w.id().bits(0x1C) // GCLK_TC4_TC5
|
||||||
|
.gen().gclk0()
|
||||||
|
.clken().set_bit()
|
||||||
|
});
|
||||||
|
while gclk.status.read().syncbusy().bit_is_set() {}
|
||||||
|
|
||||||
|
// Enable TC4 in Power Manager
|
||||||
|
pm.apbcmask.modify(|_, w| w.tc4_().set_bit());
|
||||||
|
|
||||||
|
// Disable TC4 first
|
||||||
|
tc4.count16().ctrla.modify(|_, w| w.enable().clear_bit());
|
||||||
|
while tc4.count16().status.read().syncbusy().bit_is_set() {}
|
||||||
|
|
||||||
|
// Software reset
|
||||||
|
tc4.count16().ctrla.modify(|_, w| w.swrst().set_bit());
|
||||||
|
while tc4.count16().status.read().syncbusy().bit_is_set() {}
|
||||||
|
|
||||||
|
// Configure: 16-bit mode, prescaler /256, match frequency mode
|
||||||
|
// 8 MHz / 256 = 31250 Hz. For 50 Hz: 31250 / 50 = 625 counts.
|
||||||
|
tc4.count16().ctrla.write(|w| {
|
||||||
|
w.mode().count16()
|
||||||
|
.prescaler().div256()
|
||||||
|
.wavegen().mfrq()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set compare value for 50Hz
|
||||||
|
tc4.count16().cc[0].write(|w| unsafe { w.cc().bits(624) }); // 625 - 1
|
||||||
|
|
||||||
|
// Enable MC0 interrupt
|
||||||
|
tc4.count16().intenset.write(|w| w.mc0().set_bit());
|
||||||
|
|
||||||
|
// Enable TC4
|
||||||
|
tc4.count16().ctrla.modify(|_, w| w.enable().set_bit());
|
||||||
|
while tc4.count16().status.read().syncbusy().bit_is_set() {}
|
||||||
|
}
|
||||||
|
|
||||||
#[entry]
|
#[entry]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
let mut peripherals = Peripherals::take().unwrap();
|
let mut peripherals = Peripherals::take().unwrap();
|
||||||
let core = CorePeripherals::take().unwrap();
|
let mut core = CorePeripherals::take().unwrap();
|
||||||
|
|
||||||
|
// Boot on internal oscillator first — minimal current draw (~0.3mA at 1MHz).
|
||||||
|
// This gives EH VOUT time to stabilize before we switch to 8MHz and init peripherals.
|
||||||
|
// Busy-wait ~50ms for VOUT ramp-up before doing anything.
|
||||||
|
cortex_m::asm::delay(50_000); // ~50ms at 1MHz internal oscillator
|
||||||
|
|
||||||
let mut clocks = GenericClockController::with_external_32kosc(
|
let mut clocks = GenericClockController::with_external_32kosc(
|
||||||
peripherals.GCLK,
|
peripherals.GCLK,
|
||||||
@@ -51,19 +114,40 @@ fn main() -> ! {
|
|||||||
let mut led: bsp::Led0 = pins.led0.into_push_pull_output();
|
let mut led: bsp::Led0 = pins.led0.into_push_pull_output();
|
||||||
led.set_high().unwrap(); // LED off (active low)
|
led.set_high().unwrap(); // LED off (active low)
|
||||||
|
|
||||||
// === DIAGNOSTIC: 3 slow blinks = firmware is alive ===
|
// === DIAGNOSTIC: 3 blinks = firmware is alive ===
|
||||||
blink(&mut led, &mut delay, 3, 200);
|
blink(&mut led, &mut delay, 3, 250);
|
||||||
delay.delay_ms(500u32);
|
delay.delay_ms(1500u32);
|
||||||
|
|
||||||
// LP5562 hardware enable on D0/A0 — drive high to power on
|
// --- EIC setup for FD pin wake from STANDBY ---
|
||||||
let mut lp_en = pins.a0.into_push_pull_output();
|
let gclk2 = clocks
|
||||||
lp_en.set_high().unwrap();
|
.configure_gclk_divider_and_source(
|
||||||
delay.delay_ms(10u32); // Let LP5562 power stabilize
|
ClockGenId::GCLK2,
|
||||||
|
1,
|
||||||
|
pac::gclk::genctrl::SRCSELECT_A::OSC8M,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let eic_clock = clocks.eic(&gclk2).unwrap();
|
||||||
|
let mut eic = EIC::init(&mut peripherals.PM, eic_clock, peripherals.EIC);
|
||||||
|
|
||||||
// NTAG5 FD pin on A1 (PA04) — input with pull-up (FD is open-drain)
|
// FD pin on A1 (PA04) → ExtInt4, pull-up (FD is open-drain)
|
||||||
let fd_pin = pins.a1.into_pull_up_input();
|
let fd_pin: hal::gpio::Pin<_, PullUpInterrupt> = pins.a1.into();
|
||||||
|
let mut extint4 = ExtInt4::new(fd_pin);
|
||||||
|
extint4.sense(&mut eic, Sense::FALL);
|
||||||
|
extint4.filter(&mut eic, true);
|
||||||
|
extint4.enable_interrupt(&mut eic);
|
||||||
|
extint4.enable_interrupt_wake(&mut eic);
|
||||||
|
|
||||||
// I2C on A4 (SDA) / A5 (SCL) at 400 kHz
|
// Enable EIC interrupt in NVIC
|
||||||
|
unsafe {
|
||||||
|
core.NVIC.set_priority(interrupt::EIC, 2);
|
||||||
|
NVIC::unmask(interrupt::EIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Initialize TCC PWM for LEDs ---
|
||||||
|
unsafe { led::pwm::init(); }
|
||||||
|
|
||||||
|
// I2C on A4 (SDA) / A5 (SCL) at 400 kHz — for NTAG5 only
|
||||||
let i2c = bsp::i2c_master(
|
let i2c = bsp::i2c_master(
|
||||||
&mut clocks,
|
&mut clocks,
|
||||||
400u32.kHz(),
|
400u32.kHz(),
|
||||||
@@ -73,269 +157,276 @@ fn main() -> ! {
|
|||||||
pins.a5,
|
pins.a5,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
let mut ntag = Ntag5Link::new(i2c, ntag5::DEFAULT_ADDRESS);
|
||||||
|
|
||||||
// Default LED current: 2 mA per channel (20 × 0.1 mA)
|
// Default power budget
|
||||||
let mut led_current: u8 = 20;
|
let mut budget: u8 = pattern::DEFAULT_BUDGET;
|
||||||
|
|
||||||
// Verify LP5562 is reachable before continuing
|
// Write all xblink config to persistent EEPROM (CONFIG + EH + ED).
|
||||||
let i2c_ok = (|| -> Result<(), led::lp5562::Error<_>> {
|
// Only needs to succeed once — subsequent boots will already have it.
|
||||||
lp.enable()?;
|
let provision_ok = ntag.provision_all_config(&mut delay).is_ok();
|
||||||
delay.delay_ms(1u32); // >500us startup
|
|
||||||
lp.init_direct_control(ClockSource::Internal)?;
|
|
||||||
lp.set_all_current(led_current, led_current, led_current, led_current)?;
|
|
||||||
Ok(())
|
|
||||||
})();
|
|
||||||
|
|
||||||
match i2c_ok {
|
// Set session registers for this boot (persistent CONFIG stays at safe defaults).
|
||||||
Ok(()) => {
|
let eh_ok = ntag.configure_eh_session().is_ok();
|
||||||
// === STATUS: solid LED on = LP5562 init OK ===
|
let cfg_ok = ntag.configure_config_session().is_ok();
|
||||||
led.set_low().unwrap();
|
|
||||||
delay.delay_ms(500u32);
|
|
||||||
led.set_high().unwrap();
|
|
||||||
delay.delay_ms(1000u32);
|
|
||||||
|
|
||||||
// --- Try to load pattern library from NTAG5 EEPROM ---
|
// Write full diagnostic to NDEF text record (readable via phone NFC)
|
||||||
let i2c = lp.release();
|
{
|
||||||
let mut ntag = Ntag5Link::new(i2c, ntag5::DEFAULT_ADDRESS);
|
let mut buf = [0u8; 128];
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
// Config check (LED blinks only, no NDEF write)
|
fn append(buf: &mut [u8], i: &mut usize, data: &[u8]) {
|
||||||
match ntag.check_config() {
|
for &b in data {
|
||||||
Ok(result) => {
|
if *i < buf.len() { buf[*i] = b; *i += 1; }
|
||||||
if result.all_ok {
|
}
|
||||||
blink(&mut led, &mut delay, 2, 100);
|
}
|
||||||
} else {
|
fn hex(buf: &mut [u8], i: &mut usize, val: u8) {
|
||||||
blink(&mut led, &mut delay, 5, 60);
|
const H: &[u8; 16] = b"0123456789ABCDEF";
|
||||||
|
if *i + 1 < buf.len() {
|
||||||
|
buf[*i] = H[(val >> 4) as usize]; *i += 1;
|
||||||
|
buf[*i] = H[(val & 0x0F) as usize]; *i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config check results
|
||||||
|
match ntag.check_config() {
|
||||||
|
Ok(result) => {
|
||||||
|
append(&mut buf, &mut i, b"CFG:");
|
||||||
|
if result.all_ok { append(&mut buf, &mut i, b"OK "); }
|
||||||
|
else { append(&mut buf, &mut i, b"BAD "); }
|
||||||
|
for c in &result.checks {
|
||||||
|
append(&mut buf, &mut i, &c.name);
|
||||||
|
append(&mut buf, &mut i, b":");
|
||||||
|
hex(&mut buf, &mut i, c.actual);
|
||||||
|
if !c.ok {
|
||||||
|
append(&mut buf, &mut i, b"!=");
|
||||||
|
hex(&mut buf, &mut i, c.expected);
|
||||||
}
|
}
|
||||||
}
|
append(&mut buf, &mut i, b" ");
|
||||||
Err(_) => {
|
|
||||||
// NTAG5 not reachable — 1 long blink (not fatal)
|
|
||||||
led.set_low().unwrap();
|
|
||||||
delay.delay_ms(800u32);
|
|
||||||
led.set_high().unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delay.delay_ms(500u32);
|
Err(_) => { append(&mut buf, &mut i, b"CFG:I2C_ERR "); }
|
||||||
|
}
|
||||||
|
|
||||||
// Configure FD pin for SRAM-write-by-RF indication
|
// Config provision result
|
||||||
let _ = ntag.configure_fd_sram_write(); // Best-effort, non-fatal
|
append(&mut buf, &mut i, b"PROV:");
|
||||||
|
if provision_ok { append(&mut buf, &mut i, b"OK "); }
|
||||||
|
else { append(&mut buf, &mut i, b"FAIL "); }
|
||||||
|
|
||||||
// Try reading XBLK library header from upper 1K
|
// EH session register readback
|
||||||
let mut header_buf = [0u8; pattern::HEADER_SIZE];
|
append(&mut buf, &mut i, b"EH:");
|
||||||
let eeprom_ok = ntag.read_memory(pattern::LIBRARY_BASE_BLOCK, &mut header_buf).ok()
|
if eh_ok {
|
||||||
.and_then(|()| pattern::parse_header(&header_buf));
|
if let Ok(v) = ntag.read_register(ntag5::SESSION_EH_CONFIG_REG, 0) {
|
||||||
|
hex(&mut buf, &mut i, v);
|
||||||
|
// Also read EH_LOAD_OK status (bit 7)
|
||||||
|
if v & 0x80 != 0 { append(&mut buf, &mut i, b"/LOAD_OK"); }
|
||||||
|
else { append(&mut buf, &mut i, b"/no_load"); }
|
||||||
|
} else {
|
||||||
|
append(&mut buf, &mut i, b"RD_ERR");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
append(&mut buf, &mut i, b"WR_ERR");
|
||||||
|
}
|
||||||
|
append(&mut buf, &mut i, b" ");
|
||||||
|
|
||||||
// Read active pattern if header is valid
|
// ED/FD config readback
|
||||||
let eeprom_pattern = eeprom_ok.as_ref().and_then(|hdr| {
|
append(&mut buf, &mut i, b"ED:");
|
||||||
led_current = hdr.current;
|
if let Ok(v) = ntag.read_register(ntag5::SESSION_EH_CONFIG_REG, 2) {
|
||||||
let pat_block = pattern::LIBRARY_BASE_BLOCK
|
hex(&mut buf, &mut i, v);
|
||||||
+ (pattern::HEADER_SIZE as u16 / 4)
|
} else {
|
||||||
+ (hdr.active_index as u16 * (pattern::PATTERN_ENTRY_SIZE as u16 / 4));
|
append(&mut buf, &mut i, b"ERR");
|
||||||
let mut pat_buf = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
}
|
||||||
ntag.read_memory(pat_block, &mut pat_buf).ok()?;
|
|
||||||
pattern::parse_pattern_entry(&pat_buf)
|
|
||||||
});
|
|
||||||
|
|
||||||
match eeprom_pattern {
|
// Write as NDEF text record
|
||||||
Some(pat) => {
|
let _ = ntag.write_ndef_text(&buf[..i], &mut delay);
|
||||||
// EEPROM pattern loaded — 3 fast blinks
|
}
|
||||||
blink(&mut led, &mut delay, 3, 80);
|
|
||||||
|
|
||||||
// Load pattern into LP5562 (swap I2C to LP5562 then back)
|
blink(&mut led, &mut delay, 1, 250);
|
||||||
{
|
delay.delay_ms(500u32);
|
||||||
let i2c = ntag.release();
|
|
||||||
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
|
||||||
if pattern::load_pattern(&mut lp, &pat, led_current, &mut delay).is_err() {
|
|
||||||
blink(&mut led, &mut delay, 10, 50);
|
|
||||||
}
|
|
||||||
let i2c = lp.release();
|
|
||||||
ntag = Ntag5Link::new(i2c, ntag5::DEFAULT_ADDRESS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mailbox polling loop — LP5562 runs autonomously,
|
// --- Try loading pattern from EEPROM ---
|
||||||
// MCU polls FD pin for NFC commands
|
let mut header_buf = [0u8; pattern::HEADER_SIZE];
|
||||||
let mut mbox = MailboxState::new();
|
let eeprom_ok = ntag.read_memory(pattern::LIBRARY_BASE_BLOCK, &mut header_buf).ok()
|
||||||
loop {
|
.and_then(|()| pattern::parse_header(&header_buf));
|
||||||
delay.delay_ms(200u32);
|
|
||||||
if fd_pin.is_low().unwrap_or(false) {
|
let mut active_pattern: Option<PatternDef> = None;
|
||||||
let cmd_ok = ntag5::sram::process_command(
|
|
||||||
&mut ntag, &mut mbox, &mut delay,
|
if let Some(ref hdr) = eeprom_ok {
|
||||||
);
|
budget = hdr.budget;
|
||||||
if matches!(cmd_ok, Ok(true)) {
|
let pat_block = pattern::LIBRARY_BASE_BLOCK
|
||||||
// Reload LP5562 from current EEPROM state
|
+ (pattern::HEADER_SIZE as u16 / 4)
|
||||||
let mut hdr_buf = [0u8; pattern::HEADER_SIZE];
|
+ (hdr.active_index as u16 * (pattern::PATTERN_ENTRY_SIZE as u16 / 4));
|
||||||
if ntag.read_memory(pattern::LIBRARY_BASE_BLOCK, &mut hdr_buf).is_ok() {
|
let mut pat_buf = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
||||||
if let Some(hdr) = pattern::parse_header(&hdr_buf) {
|
if ntag.read_memory(pat_block, &mut pat_buf).is_ok() {
|
||||||
led_current = hdr.current;
|
active_pattern = pattern::parse_pattern_entry(&pat_buf);
|
||||||
let pat_block = pattern::LIBRARY_BASE_BLOCK
|
}
|
||||||
+ (pattern::HEADER_SIZE as u16 / 4)
|
}
|
||||||
+ (hdr.active_index as u16
|
|
||||||
* (pattern::PATTERN_ENTRY_SIZE as u16 / 4));
|
if active_pattern.is_none() {
|
||||||
let mut pat_buf = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
// No XBLK in EEPROM — self-provision default patterns
|
||||||
if ntag.read_memory(pat_block, &mut pat_buf).is_ok() {
|
blink(&mut led, &mut delay, 2, 350);
|
||||||
if let Some(new_pat) = pattern::parse_pattern_entry(&pat_buf) {
|
|
||||||
// Swap I2C to LP5562 for reprogramming
|
let patterns = [
|
||||||
let i2c = ntag.release();
|
pattern::breathe(),
|
||||||
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
pattern::heartbeat(),
|
||||||
let _ = pattern::load_pattern(
|
pattern::wave_chase(),
|
||||||
&mut lp, &new_pat, led_current, &mut delay,
|
pattern::slow_pulse(),
|
||||||
);
|
pattern::alternating_blink(),
|
||||||
let i2c = lp.release();
|
];
|
||||||
ntag = Ntag5Link::new(i2c, ntag5::DEFAULT_ADDRESS);
|
|
||||||
}
|
let pat_base_block = pattern::LIBRARY_BASE_BLOCK
|
||||||
}
|
+ (pattern::HEADER_SIZE as u16 / 4);
|
||||||
}
|
let mut provision_ok = true;
|
||||||
}
|
|
||||||
}
|
for (i, pat) in patterns.iter().enumerate() {
|
||||||
}
|
let mut entry_buf = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
||||||
}
|
pattern::serialize_pattern_entry(pat, &mut entry_buf);
|
||||||
|
|
||||||
|
let entry_block = pat_base_block
|
||||||
|
+ (i as u16 * (pattern::PATTERN_ENTRY_SIZE as u16 / 4));
|
||||||
|
for blk in 0..(pattern::PATTERN_ENTRY_SIZE as u16 / 4) {
|
||||||
|
let bo = (blk as usize) * 4;
|
||||||
|
let chunk = [entry_buf[bo], entry_buf[bo+1], entry_buf[bo+2], entry_buf[bo+3]];
|
||||||
|
if ntag.write_verify_block(entry_block + blk, &chunk, &mut delay).is_err() {
|
||||||
|
provision_ok = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
None => {
|
}
|
||||||
// No XBLK in EEPROM — self-provision hardcoded patterns
|
if !provision_ok { break; }
|
||||||
// 2 long blinks = provisioning
|
}
|
||||||
blink(&mut led, &mut delay, 2, 300);
|
|
||||||
|
|
||||||
let mode = LedMode::Rgbw;
|
if provision_ok {
|
||||||
let patterns = [
|
let mut hdr_buf = [0u8; pattern::HEADER_SIZE];
|
||||||
pattern::breathe(mode),
|
pattern::serialize_header(
|
||||||
pattern::heartbeat(mode),
|
5, 0, budget, false, 0, &mut hdr_buf,
|
||||||
pattern::slow_pulse(mode),
|
);
|
||||||
pattern::rgb_cycle(mode),
|
for blk in 0..4u16 {
|
||||||
pattern::color_wash(mode),
|
let bo = (blk as usize) * 4;
|
||||||
];
|
let chunk = [hdr_buf[bo], hdr_buf[bo+1], hdr_buf[bo+2], hdr_buf[bo+3]];
|
||||||
|
if ntag.write_verify_block(
|
||||||
// Serialize all pattern entries and write to EEPROM
|
pattern::LIBRARY_BASE_BLOCK + blk, &chunk, &mut delay,
|
||||||
let pat_base_block = pattern::LIBRARY_BASE_BLOCK
|
).is_err() {
|
||||||
+ (pattern::HEADER_SIZE as u16 / 4);
|
provision_ok = false;
|
||||||
let mut all_pat_data = [0u8; pattern::PATTERN_ENTRY_SIZE * 5];
|
break;
|
||||||
let mut provision_ok = true;
|
|
||||||
|
|
||||||
for (i, pat) in patterns.iter().enumerate() {
|
|
||||||
let mut entry_buf = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
|
||||||
pattern::serialize_pattern_entry(pat, &mut entry_buf);
|
|
||||||
|
|
||||||
// Copy into combined buffer for CRC
|
|
||||||
let offset = i * pattern::PATTERN_ENTRY_SIZE;
|
|
||||||
all_pat_data[offset..offset + pattern::PATTERN_ENTRY_SIZE]
|
|
||||||
.copy_from_slice(&entry_buf);
|
|
||||||
|
|
||||||
// Write 112 bytes as 28 x 4-byte blocks
|
|
||||||
let entry_block = pat_base_block
|
|
||||||
+ (i as u16 * (pattern::PATTERN_ENTRY_SIZE as u16 / 4));
|
|
||||||
for blk in 0..28u16 {
|
|
||||||
let bo = (blk as usize) * 4;
|
|
||||||
let chunk = [entry_buf[bo], entry_buf[bo+1], entry_buf[bo+2], entry_buf[bo+3]];
|
|
||||||
if ntag.write_verify_block(entry_block + blk, &chunk, &mut delay).is_err() {
|
|
||||||
provision_ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !provision_ok { break; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if provision_ok {
|
|
||||||
// Write header last (so partial writes don't look valid)
|
|
||||||
let mut hdr_buf = [0u8; pattern::HEADER_SIZE];
|
|
||||||
pattern::serialize_header(
|
|
||||||
5, 0, led_current, 0x00,
|
|
||||||
&all_pat_data,
|
|
||||||
&mut hdr_buf,
|
|
||||||
);
|
|
||||||
for blk in 0..4u16 {
|
|
||||||
let bo = (blk as usize) * 4;
|
|
||||||
let chunk = [hdr_buf[bo], hdr_buf[bo+1], hdr_buf[bo+2], hdr_buf[bo+3]];
|
|
||||||
if ntag.write_verify_block(
|
|
||||||
pattern::LIBRARY_BASE_BLOCK + blk, &chunk, &mut delay,
|
|
||||||
).is_err() {
|
|
||||||
provision_ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if provision_ok {
|
|
||||||
// Success — 4 fast blinks, then load pattern 0 from EEPROM
|
|
||||||
blink(&mut led, &mut delay, 4, 80);
|
|
||||||
|
|
||||||
// Re-read active pattern from what we just wrote
|
|
||||||
let mut pat_buf = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
|
||||||
let loaded = ntag.read_memory(pat_base_block, &mut pat_buf).ok()
|
|
||||||
.and_then(|()| pattern::parse_pattern_entry(&pat_buf));
|
|
||||||
|
|
||||||
// Load into LP5562 (swap I2C), then swap back for mailbox
|
|
||||||
{
|
|
||||||
let i2c = ntag.release();
|
|
||||||
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
|
||||||
if let Some(pat) = loaded {
|
|
||||||
if pattern::load_pattern(&mut lp, &pat, led_current, &mut delay).is_err() {
|
|
||||||
blink(&mut led, &mut delay, 10, 50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let i2c = lp.release();
|
|
||||||
ntag = Ntag5Link::new(i2c, ntag5::DEFAULT_ADDRESS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mailbox polling loop (same as EEPROM-loaded path)
|
|
||||||
let mut mbox = MailboxState::new();
|
|
||||||
loop {
|
|
||||||
delay.delay_ms(200u32);
|
|
||||||
if fd_pin.is_low().unwrap_or(false) {
|
|
||||||
let cmd_ok = ntag5::sram::process_command(
|
|
||||||
&mut ntag, &mut mbox, &mut delay,
|
|
||||||
);
|
|
||||||
if matches!(cmd_ok, Ok(true)) {
|
|
||||||
let mut hdr_buf = [0u8; pattern::HEADER_SIZE];
|
|
||||||
if ntag.read_memory(pattern::LIBRARY_BASE_BLOCK, &mut hdr_buf).is_ok() {
|
|
||||||
if let Some(hdr) = pattern::parse_header(&hdr_buf) {
|
|
||||||
led_current = hdr.current;
|
|
||||||
let pat_block = pattern::LIBRARY_BASE_BLOCK
|
|
||||||
+ (pattern::HEADER_SIZE as u16 / 4)
|
|
||||||
+ (hdr.active_index as u16
|
|
||||||
* (pattern::PATTERN_ENTRY_SIZE as u16 / 4));
|
|
||||||
let mut pat_buf2 = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
|
||||||
if ntag.read_memory(pat_block, &mut pat_buf2).is_ok() {
|
|
||||||
if let Some(new_pat) = pattern::parse_pattern_entry(&pat_buf2) {
|
|
||||||
let i2c = ntag.release();
|
|
||||||
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
|
||||||
let _ = pattern::load_pattern(
|
|
||||||
&mut lp, &new_pat, led_current, &mut delay,
|
|
||||||
);
|
|
||||||
let i2c = lp.release();
|
|
||||||
ntag = Ntag5Link::new(i2c, ntag5::DEFAULT_ADDRESS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Provisioning failed — fall back to hardcoded cycle
|
|
||||||
blink(&mut led, &mut delay, 8, 50);
|
|
||||||
|
|
||||||
let i2c = ntag.release();
|
|
||||||
let mut lp = Lp5562::new(i2c, DEFAULT_ADDRESS);
|
|
||||||
|
|
||||||
let mut idx = 0;
|
|
||||||
loop {
|
|
||||||
if pattern::load_pattern(
|
|
||||||
&mut lp, &patterns[idx], led_current, &mut delay,
|
|
||||||
).is_err() {
|
|
||||||
blink(&mut led, &mut delay, 10, 50);
|
|
||||||
}
|
|
||||||
delay.delay_ms(15000u32);
|
|
||||||
idx = (idx + 1) % patterns.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
|
||||||
// === DIAGNOSTIC: fast blink forever = I2C error ===
|
if provision_ok {
|
||||||
loop {
|
blink(&mut led, &mut delay, 4, 250);
|
||||||
blink(&mut led, &mut delay, 5, 80);
|
active_pattern = Some(pattern::breathe());
|
||||||
delay.delay_ms(500u32);
|
} else {
|
||||||
|
blink(&mut led, &mut delay, 8, 200);
|
||||||
|
// Fall back to hardcoded breathe
|
||||||
|
active_pattern = Some(pattern::breathe());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blink(&mut led, &mut delay, 3, 250); // EEPROM pattern loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Start animation ---
|
||||||
|
if let Some(pat) = active_pattern {
|
||||||
|
cortex_m::interrupt::free(|cs| {
|
||||||
|
ENGINE.borrow(cs).replace(Some(PatternEngine::new(pat, budget)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start TC4 (50Hz animation timer) and enable its interrupt
|
||||||
|
unsafe {
|
||||||
|
init_tc4();
|
||||||
|
core.NVIC.set_priority(interrupt::TC4, 1); // Higher priority than EIC
|
||||||
|
NVIC::unmask(interrupt::TC4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === DIAGNOSTIC: blink arbiter mode before entering idle loop ===
|
||||||
|
if let Ok(c1) = ntag.read_register(SESSION_CONFIG_REG, 1) {
|
||||||
|
let arbiter = (c1 >> 2) & 0x03;
|
||||||
|
blink(&mut led, &mut delay, arbiter + 1, 150);
|
||||||
|
delay.delay_ms(500u32);
|
||||||
|
let sram_en = (c1 >> 1) & 0x01;
|
||||||
|
blink(&mut led, &mut delay, sram_en + 1, 400);
|
||||||
|
} else {
|
||||||
|
blink(&mut led, &mut delay, 9, 100);
|
||||||
|
}
|
||||||
|
delay.delay_ms(1500u32);
|
||||||
|
|
||||||
|
led.set_high().unwrap(); // LED off
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Sleep/Wake loop — MCU in IDLE, TC4 ISR drives LEDs.
|
||||||
|
// Wake fully on EIC (FD pin) for SRAM mailbox commands.
|
||||||
|
// ============================================================
|
||||||
|
let mut mbox = MailboxState::new();
|
||||||
|
loop {
|
||||||
|
// IDLE sleep (not STANDBY) — TC4 and TCC keep running
|
||||||
|
cortex_m::asm::wfi();
|
||||||
|
|
||||||
|
// Check if woken by EIC (FD pin = SRAM write by RF)
|
||||||
|
let eic_reg = unsafe { &*pac::EIC::ptr() };
|
||||||
|
if eic_reg.intflag.read().extint4().bit_is_set() {
|
||||||
|
extint4.clear_interrupt();
|
||||||
|
|
||||||
|
// Process SRAM mailbox command
|
||||||
|
let cmd_ok = ntag5::sram::process_command(
|
||||||
|
&mut ntag, &mut mbox, &mut delay,
|
||||||
|
);
|
||||||
|
|
||||||
|
if matches!(cmd_ok, Ok(true)) {
|
||||||
|
// Pattern library was modified — reload from EEPROM
|
||||||
|
let mut hdr_buf = [0u8; pattern::HEADER_SIZE];
|
||||||
|
if ntag.read_memory(pattern::LIBRARY_BASE_BLOCK, &mut hdr_buf).is_ok() {
|
||||||
|
if let Some(hdr) = pattern::parse_header(&hdr_buf) {
|
||||||
|
budget = hdr.budget;
|
||||||
|
let pat_block = pattern::LIBRARY_BASE_BLOCK
|
||||||
|
+ (pattern::HEADER_SIZE as u16 / 4)
|
||||||
|
+ (hdr.active_index as u16
|
||||||
|
* (pattern::PATTERN_ENTRY_SIZE as u16 / 4));
|
||||||
|
let mut pat_buf = [0u8; pattern::PATTERN_ENTRY_SIZE];
|
||||||
|
if ntag.read_memory(pat_block, &mut pat_buf).is_ok() {
|
||||||
|
if let Some(new_pat) = pattern::parse_pattern_entry(&pat_buf) {
|
||||||
|
cortex_m::interrupt::free(|cs| {
|
||||||
|
if let Some(ref mut eng) = *ENGINE.borrow(cs).borrow_mut() {
|
||||||
|
eng.budget = budget as u16;
|
||||||
|
eng.load(new_pat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TC4 interrupt handler — 50Hz animation tick.
|
||||||
|
/// Computes LED brightness from pattern engine and writes TCC duty cycles.
|
||||||
|
#[interrupt]
|
||||||
|
fn TC4() {
|
||||||
|
let tc4 = unsafe { &*pac::TC4::ptr() };
|
||||||
|
// Clear MC0 interrupt flag
|
||||||
|
tc4.count16().intflag.write(|w| w.mc0().set_bit());
|
||||||
|
|
||||||
|
cortex_m::interrupt::free(|cs| {
|
||||||
|
if let Some(ref mut engine) = *ENGINE.borrow(cs).borrow_mut() {
|
||||||
|
let mut output = [0u8; NUM_LEDS];
|
||||||
|
engine.tick(&mut output);
|
||||||
|
led::pwm::set_all(&output);
|
||||||
|
|
||||||
|
if engine.repeats_done() {
|
||||||
|
*REPEATS_DONE.borrow(cs).borrow_mut() = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EIC interrupt handler — clears flag, main loop checks and processes.
|
||||||
|
#[interrupt]
|
||||||
|
fn EIC() {
|
||||||
|
// Don't clear here — let main loop detect it via intflag read
|
||||||
|
// Just need the handler to exist so WFI returns
|
||||||
|
let eic = unsafe { &*pac::EIC::ptr() };
|
||||||
|
if eic.intflag.read().extint4().bit_is_set() {
|
||||||
|
eic.intflag.modify(|_, w| w.extint4().set_bit());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
104
src/ntag5/mod.rs
104
src/ntag5/mod.rs
@@ -25,11 +25,17 @@ pub const SRAM_BASE_BLOCK: u16 = 0x10F8;
|
|||||||
pub const SRAM_BLOCK_COUNT: u16 = 64;
|
pub const SRAM_BLOCK_COUNT: u16 = 64;
|
||||||
pub const SRAM_SIZE: usize = 256;
|
pub const SRAM_SIZE: usize = 256;
|
||||||
|
|
||||||
// Session register for ED/FD pin configuration
|
// Config EEPROM I2C block addresses (NFC block 0x3D → I2C 0x103D)
|
||||||
pub const SESSION_ED_FD_PIN_CFG: u16 = 0x10A3;
|
pub const CONFIG_EH_BLOCK: u16 = 0x103D; // EH_CONFIG block (persistent)
|
||||||
|
pub const CONFIG_DEV_SEC_BLOCK: u16 = 0x103F; // DEV_SEC_CONFIG block
|
||||||
|
|
||||||
// FD pin mode: active on SRAM write by RF, cleared on I2C read
|
// ED/FD pin config is byte 2 within the EH_CONFIG block (0x103D / session 0x10A7)
|
||||||
pub const FD_MODE_SRAM_RF_WRITE: u8 = 0x04;
|
// ED_CONFIG values (from NTP53x2 datasheet / ntag5link.py):
|
||||||
|
// 0x00 = disabled
|
||||||
|
// 0x01 = NFC field detect
|
||||||
|
// 0x04 = NFC-to-I2C pass-through (SRAM write by RF)
|
||||||
|
// 0x0C = write to synch block
|
||||||
|
pub const ED_CONFIG_NFC_TO_I2C_PASS_THROUGH: u8 = 0x04;
|
||||||
|
|
||||||
// EEPROM user memory I2C block addresses
|
// EEPROM user memory I2C block addresses
|
||||||
// Block 0 = CC (capability container), blocks 1+ = NDEF data
|
// Block 0 = CC (capability container), blocks 1+ = NDEF data
|
||||||
@@ -40,17 +46,22 @@ pub const EEPROM_BLOCK_0: u16 = 0x0000;
|
|||||||
pub const EXPECTED_CONFIG_0: u8 = 0x08;
|
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)
|
// 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;
|
pub const EXPECTED_CONFIG_1: u8 = 0x0A;
|
||||||
// EH_CONFIG: skip for now — EH is disabled (0x00) when powered externally.
|
// EH_CONFIG: EH_ENABLE + VOUT_V_SEL_1_8V + DISABLE_POWER_CHECK + VOUT_I_SEL_6_5mA = 0x59
|
||||||
// Set to 0x75 (EH_ENABLE + 3.0V + 12.5mA) when running from energy harvesting.
|
// Bit 0: EH_ENABLE = 1
|
||||||
pub const EXPECTED_EH_CONFIG: u8 = 0x00;
|
// Bits 2:1: VOUT_V_SEL = 00 (1.8V)
|
||||||
|
// Bit 3: DISABLE_POWER_CHECK = 1 (skip power check, VOUT comes up immediately)
|
||||||
|
// Bits 6:4: VOUT_I_SEL = 101 (6.5mA)
|
||||||
|
pub const EXPECTED_EH_CONFIG: u8 = 0x59;
|
||||||
|
// Session EH trigger bit (bit 3 in session register = EH_TRIGGER, different from persistent bit 3)
|
||||||
|
pub const EH_SESSION_TRIGGER: u8 = 0x08;
|
||||||
|
|
||||||
// Masks for checking — only check the bits we care about
|
// 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
|
// CONFIG_0: bits 3:2 (EH_MODE) — ignore SRAM_COPY_EN, AUTO_STANDBY, LOCK_SESSION
|
||||||
pub const CONFIG_0_MASK: u8 = 0x0C;
|
pub const CONFIG_0_MASK: u8 = 0x0C;
|
||||||
// CONFIG_1: bits 5:4 (USE_CASE) + bits 3:2 (ARBITER) + bit 1 (SRAM_EN)
|
// CONFIG_1: bits 5:4 (USE_CASE) + bits 3:2 (ARBITER) + bit 1 (SRAM_EN)
|
||||||
pub const CONFIG_1_MASK: u8 = 0x3E;
|
pub const CONFIG_1_MASK: u8 = 0x3E;
|
||||||
// EH_CONFIG: mask 0x00 — don't check EH config during external power testing
|
// EH_CONFIG: check EH_ENABLE (bit 0) + VOUT_V_SEL (bits 2:1), ignore current limit + status bits
|
||||||
pub const EH_CONFIG_MASK: u8 = 0x00;
|
pub const EH_CONFIG_MASK: u8 = 0x07;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error<E> {
|
pub enum Error<E> {
|
||||||
@@ -189,12 +200,79 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure FD pin for SRAM-write-by-RF indication.
|
/// Configure ED/FD pin for SRAM-write-by-RF indication.
|
||||||
/// FD goes low when phone writes to SRAM, returns high when MCU reads SRAM.
|
/// FD goes low when phone writes to SRAM, returns high when MCU reads SRAM.
|
||||||
|
///
|
||||||
|
/// Sets ED_CONFIG via session register (volatile, resets on power cycle).
|
||||||
pub fn configure_fd_sram_write(&mut self) -> Result<(), E> {
|
pub fn configure_fd_sram_write(&mut self) -> Result<(), E> {
|
||||||
// ED_FD_PIN_CFG register index 1 within the session block
|
self.write_register(
|
||||||
// Bits 2:0 control FD output mode
|
SESSION_EH_CONFIG_REG, 2, 0xFF,
|
||||||
self.write_register(SESSION_ED_FD_PIN_CFG, 0x01, 0x07, FD_MODE_SRAM_RF_WRITE)
|
ED_CONFIG_NFC_TO_I2C_PASS_THROUGH,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable EH via session register (volatile, must run each boot).
|
||||||
|
///
|
||||||
|
/// Config EEPROM (block 0x3D) is NOT writable from I2C — only from NFC
|
||||||
|
/// using WRITE CONFIG (0xC1) via PCSC reader. Session register writes
|
||||||
|
/// work immediately but reset on power cycle.
|
||||||
|
///
|
||||||
|
/// Session register 0x10A7: byte 0 = EH_CONFIG, byte 2 = ED_CONFIG
|
||||||
|
pub fn configure_eh_session(&mut self) -> Result<(), E> {
|
||||||
|
// Write EH_CONFIG + EH_TRIGGER to session register byte 0
|
||||||
|
// Session bit 3 = EH_TRIGGER (different from persistent bit 3 = DISABLE_POWER_CHECK)
|
||||||
|
self.write_register(SESSION_EH_CONFIG_REG, 0, 0xFF, EXPECTED_EH_CONFIG | EH_SESSION_TRIGGER)?;
|
||||||
|
// Write ED_CONFIG to session register byte 2 (FD pin = SRAM pass-through)
|
||||||
|
self.write_register(SESSION_EH_CONFIG_REG, 2, 0xFF, ED_CONFIG_NFC_TO_I2C_PASS_THROUGH)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write EH_CONFIG + ED_CONFIG to persistent config EEPROM (block 0x103D).
|
||||||
|
/// This must be done via I2C when no NFC field is present (USB-powered).
|
||||||
|
/// After writing, the NTAG5 will automatically enable EH on subsequent power-ups.
|
||||||
|
/// Block format: [EH_CONFIG, 0x00, ED_CONFIG, 0x00]
|
||||||
|
pub fn provision_eh_persistent(
|
||||||
|
&mut self,
|
||||||
|
delay: &mut impl embedded_hal::delay::DelayNs,
|
||||||
|
) -> Result<(), Error<E>> {
|
||||||
|
let data = [EXPECTED_EH_CONFIG, 0x00, ED_CONFIG_NFC_TO_I2C_PASS_THROUGH, 0x00];
|
||||||
|
self.write_verify_block(CONFIG_EH_BLOCK, &data, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write EH config to persistent EEPROM and reset CONFIG to defaults.
|
||||||
|
///
|
||||||
|
/// NOTE: CONFIG_0/CONFIG_1 must NOT be persisted with SRAM passthrough —
|
||||||
|
/// it breaks NFC EEPROM access when I2C bus is floating (MCU unpowered).
|
||||||
|
/// Use configure_config_session() to set CONFIG at boot instead.
|
||||||
|
pub fn provision_all_config(
|
||||||
|
&mut self,
|
||||||
|
delay: &mut impl embedded_hal::delay::DelayNs,
|
||||||
|
) -> Result<(), Error<E>> {
|
||||||
|
// Write EH + ED config FIRST (most important for automatic power-up)
|
||||||
|
let eh_data = [EXPECTED_EH_CONFIG, 0x00, ED_CONFIG_NFC_TO_I2C_PASS_THROUGH, 0x00];
|
||||||
|
self.write_memory_block(CONFIG_EH_BLOCK, &eh_data)?;
|
||||||
|
delay.delay_ms(5); // EEPROM write cycle
|
||||||
|
|
||||||
|
// Then CONFIG_0 (EH_MODE=low field strength), clear CONFIG_1
|
||||||
|
let config_data = [EXPECTED_CONFIG_0, 0x00, 0x00, 0x00];
|
||||||
|
self.write_memory_block(0x1037, &config_data)?;
|
||||||
|
delay.delay_ms(5);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set CONFIG_1 via session register (volatile, safe).
|
||||||
|
/// Enables SRAM passthrough only while MCU is active on the I2C bus.
|
||||||
|
pub fn configure_config_session(&mut self) -> Result<(), E> {
|
||||||
|
self.write_register(SESSION_CONFIG_REG, 1, 0xFF, EXPECTED_CONFIG_1)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read DEV_SEC_CONFIG block (0x103F) to check security/protection status.
|
||||||
|
/// Returns the 4 raw bytes: [DEV_SEC_CONFIG, SRAM_CONF_PROT, PP_AREA1_LSB, PP_AREA1_MSB]
|
||||||
|
pub fn read_dev_sec_config(&mut self) -> Result<[u8; 4], E> {
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
self.read_memory(CONFIG_DEV_SEC_BLOCK, &mut buf)?;
|
||||||
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Config check ----
|
// ---- Config check ----
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use embedded_hal::i2c::I2c;
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Firmware version reported in GET_STATUS response.
|
/// Firmware version reported in GET_STATUS response.
|
||||||
pub const FIRMWARE_VERSION: u8 = 0x01;
|
pub const FIRMWARE_VERSION: u8 = 0x02;
|
||||||
|
|
||||||
// Command IDs (phone -> MCU)
|
// Command IDs (phone -> MCU)
|
||||||
pub const CMD_WRITE_PATTERN: u8 = 0x01;
|
pub const CMD_WRITE_PATTERN: u8 = 0x01;
|
||||||
@@ -49,10 +49,8 @@ pub struct MailboxState {
|
|||||||
pub syncing: bool,
|
pub syncing: bool,
|
||||||
/// Pattern count supplied by SYNC_START.
|
/// Pattern count supplied by SYNC_START.
|
||||||
pub sync_count: u8,
|
pub sync_count: u8,
|
||||||
/// Current (mA setting) supplied by SYNC_START.
|
/// Budget supplied by SYNC_START.
|
||||||
pub sync_current: u8,
|
pub sync_budget: u8,
|
||||||
/// LED mode supplied by SYNC_START.
|
|
||||||
pub sync_mode: u8,
|
|
||||||
/// Next pattern index for READ_NEXT.
|
/// Next pattern index for READ_NEXT.
|
||||||
pub read_index: u8,
|
pub read_index: u8,
|
||||||
/// Total patterns available for READ_NEXT iteration.
|
/// Total patterns available for READ_NEXT iteration.
|
||||||
@@ -64,8 +62,7 @@ impl MailboxState {
|
|||||||
Self {
|
Self {
|
||||||
syncing: false,
|
syncing: false,
|
||||||
sync_count: 0,
|
sync_count: 0,
|
||||||
sync_current: 0,
|
sync_budget: pattern::DEFAULT_BUDGET,
|
||||||
sync_mode: 0,
|
|
||||||
read_index: 0,
|
read_index: 0,
|
||||||
read_count: 0,
|
read_count: 0,
|
||||||
}
|
}
|
||||||
@@ -113,13 +110,14 @@ fn build_response(buf: &mut [u8], seq: u8, status: u8, payload: &[u8]) -> usize
|
|||||||
// EEPROM helpers
|
// EEPROM helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Block address for pattern N in the EEPROM library.
|
/// Block address for pattern N in the EEPROM library (v2: 16-byte entries = 4 blocks each).
|
||||||
fn pattern_block(index: u8) -> u16 {
|
fn pattern_block(index: u8) -> u16 {
|
||||||
pattern::LIBRARY_BASE_BLOCK + 4 + (index as u16) * 28
|
pattern::LIBRARY_BASE_BLOCK
|
||||||
|
+ (pattern::HEADER_SIZE as u16 / 4)
|
||||||
|
+ (index as u16) * (pattern::PATTERN_ENTRY_SIZE as u16 / 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the XBLK library header from EEPROM.
|
/// Read the XBLK library header from EEPROM.
|
||||||
/// Returns None if magic/version don't match.
|
|
||||||
fn read_library_header<I2C, E>(
|
fn read_library_header<I2C, E>(
|
||||||
ntag: &mut Ntag5Link<I2C>,
|
ntag: &mut Ntag5Link<I2C>,
|
||||||
) -> Result<Option<pattern::LibraryHeader>, ntag5::Error<E>>
|
) -> Result<Option<pattern::LibraryHeader>, ntag5::Error<E>>
|
||||||
@@ -149,46 +147,21 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read all pattern data from EEPROM for the given count (for CRC computation).
|
/// Recalculate the header CRC and write the updated header back to EEPROM.
|
||||||
/// Returns the number of bytes read into `all_data`.
|
|
||||||
fn read_all_pattern_data<I2C, E>(
|
|
||||||
ntag: &mut Ntag5Link<I2C>,
|
|
||||||
count: u8,
|
|
||||||
all_data: &mut [u8],
|
|
||||||
) -> Result<usize, ntag5::Error<E>>
|
|
||||||
where
|
|
||||||
I2C: I2c<Error = E>,
|
|
||||||
{
|
|
||||||
let total = count as usize * pattern::PATTERN_ENTRY_SIZE;
|
|
||||||
// Read in chunks — read_memory can handle arbitrary lengths
|
|
||||||
// but we read per-pattern for clarity
|
|
||||||
for i in 0..count as usize {
|
|
||||||
let block = pattern_block(i as u8);
|
|
||||||
let off = i * pattern::PATTERN_ENTRY_SIZE;
|
|
||||||
ntag.read_memory(block, &mut all_data[off..off + pattern::PATTERN_ENTRY_SIZE])?;
|
|
||||||
}
|
|
||||||
Ok(total)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recalculate the header CRC based on current EEPROM pattern data,
|
|
||||||
/// then write the updated header back.
|
|
||||||
fn recalculate_header_crc<I2C, E>(
|
fn recalculate_header_crc<I2C, E>(
|
||||||
ntag: &mut Ntag5Link<I2C>,
|
ntag: &mut Ntag5Link<I2C>,
|
||||||
count: u8,
|
count: u8,
|
||||||
active: u8,
|
active: u8,
|
||||||
current: u8,
|
budget: u8,
|
||||||
led_mode: u8,
|
has_playlist: bool,
|
||||||
|
playlist_count: u8,
|
||||||
delay: &mut impl embedded_hal::delay::DelayNs,
|
delay: &mut impl embedded_hal::delay::DelayNs,
|
||||||
) -> Result<(), ntag5::Error<E>>
|
) -> Result<(), ntag5::Error<E>>
|
||||||
where
|
where
|
||||||
I2C: I2c<Error = E>,
|
I2C: I2c<Error = E>,
|
||||||
{
|
{
|
||||||
// Read all pattern data for CRC
|
|
||||||
let mut all_data = [0u8; pattern::MAX_PATTERNS * pattern::PATTERN_ENTRY_SIZE];
|
|
||||||
let data_len = read_all_pattern_data(ntag, count, &mut all_data)?;
|
|
||||||
|
|
||||||
let mut hdr = [0u8; pattern::HEADER_SIZE];
|
let mut hdr = [0u8; pattern::HEADER_SIZE];
|
||||||
pattern::serialize_header(count, active, current, led_mode, &all_data[..data_len], &mut hdr);
|
pattern::serialize_header(count, active, budget, has_playlist, playlist_count, &mut hdr);
|
||||||
write_header_to_eeprom(ntag, &hdr, delay)
|
write_header_to_eeprom(ntag, &hdr, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,14 +176,15 @@ where
|
|||||||
{
|
{
|
||||||
let hdr = match read_library_header(ntag)? {
|
let hdr = match read_library_header(ntag)? {
|
||||||
Some(h) => h,
|
Some(h) => h,
|
||||||
None => return Ok(()), // no valid header, nothing to update
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
recalculate_header_crc(
|
recalculate_header_crc(
|
||||||
ntag,
|
ntag,
|
||||||
hdr.pattern_count,
|
hdr.pattern_count,
|
||||||
hdr.active_index,
|
hdr.active_index,
|
||||||
hdr.current,
|
hdr.budget,
|
||||||
hdr.led_mode,
|
hdr.has_playlist,
|
||||||
|
hdr.playlist_count,
|
||||||
delay,
|
delay,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -228,23 +202,20 @@ fn handle_get_status<I2C, E>(
|
|||||||
where
|
where
|
||||||
I2C: I2c<Error = E>,
|
I2C: I2c<Error = E>,
|
||||||
{
|
{
|
||||||
let mut payload = [0u8; 5];
|
let mut payload = [0u8; 4];
|
||||||
payload[0] = FIRMWARE_VERSION;
|
payload[0] = FIRMWARE_VERSION;
|
||||||
match read_library_header(ntag)? {
|
match read_library_header(ntag)? {
|
||||||
Some(h) => {
|
Some(h) => {
|
||||||
payload[1] = h.pattern_count;
|
payload[1] = h.pattern_count;
|
||||||
payload[2] = h.active_index;
|
payload[2] = h.active_index;
|
||||||
payload[3] = h.current;
|
payload[3] = h.budget;
|
||||||
payload[4] = h.led_mode;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// No valid header — return zeros
|
|
||||||
}
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
Ok(build_response(rsp_buf, seq, STATUS_OK, &payload))
|
Ok(build_response(rsp_buf, seq, STATUS_OK, &payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// WRITE_PATTERN (0x01): Write a 112-byte pattern entry to EEPROM.
|
/// WRITE_PATTERN (0x01): Write a 16-byte pattern entry to EEPROM.
|
||||||
fn handle_write_pattern<I2C, E>(
|
fn handle_write_pattern<I2C, E>(
|
||||||
ntag: &mut Ntag5Link<I2C>,
|
ntag: &mut Ntag5Link<I2C>,
|
||||||
state: &MailboxState,
|
state: &MailboxState,
|
||||||
@@ -264,10 +235,11 @@ where
|
|||||||
return Ok(build_response(rsp_buf, seq, STATUS_INVALID_INDEX, &[]));
|
return Ok(build_response(rsp_buf, seq, STATUS_INVALID_INDEX, &[]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write 112 bytes = 28 blocks
|
// Write 16 bytes = 4 blocks
|
||||||
let base_block = pattern_block(index);
|
let base_block = pattern_block(index);
|
||||||
let pattern_data = &payload[1..1 + pattern::PATTERN_ENTRY_SIZE];
|
let pattern_data = &payload[1..1 + pattern::PATTERN_ENTRY_SIZE];
|
||||||
for i in 0..28u16 {
|
let blocks = pattern::PATTERN_ENTRY_SIZE / 4;
|
||||||
|
for i in 0..blocks as u16 {
|
||||||
let off = (i as usize) * 4;
|
let off = (i as usize) * 4;
|
||||||
let mut chunk = [0u8; 4];
|
let mut chunk = [0u8; 4];
|
||||||
chunk.copy_from_slice(&pattern_data[off..off + 4]);
|
chunk.copy_from_slice(&pattern_data[off..off + 4]);
|
||||||
@@ -280,7 +252,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not syncing, update header CRC to reflect changed pattern data
|
// If not syncing, update header CRC
|
||||||
if !state.syncing {
|
if !state.syncing {
|
||||||
match update_header_after_write(ntag, delay) {
|
match update_header_after_write(ntag, delay) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
@@ -319,9 +291,10 @@ where
|
|||||||
return Ok(build_response(rsp_buf, seq, STATUS_INVALID_INDEX, &[]));
|
return Ok(build_response(rsp_buf, seq, STATUS_INVALID_INDEX, &[]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate header with new active index
|
match recalculate_header_crc(
|
||||||
match recalculate_header_crc(ntag, hdr.pattern_count, index, hdr.current, hdr.led_mode, delay)
|
ntag, hdr.pattern_count, index, hdr.budget,
|
||||||
{
|
hdr.has_playlist, hdr.playlist_count, delay,
|
||||||
|
) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(ntag5::Error::VerifyFailed) => {
|
Err(ntag5::Error::VerifyFailed) => {
|
||||||
return Ok(build_response(rsp_buf, seq, STATUS_EEPROM_FAIL, &[]));
|
return Ok(build_response(rsp_buf, seq, STATUS_EEPROM_FAIL, &[]));
|
||||||
@@ -339,7 +312,7 @@ fn handle_sync_start(
|
|||||||
payload: &[u8],
|
payload: &[u8],
|
||||||
rsp_buf: &mut [u8],
|
rsp_buf: &mut [u8],
|
||||||
) -> usize {
|
) -> usize {
|
||||||
if payload.len() < 3 {
|
if payload.len() < 2 {
|
||||||
return build_response(rsp_buf, seq, STATUS_BAD_CMD, &[]);
|
return build_response(rsp_buf, seq, STATUS_BAD_CMD, &[]);
|
||||||
}
|
}
|
||||||
let count = payload[0];
|
let count = payload[0];
|
||||||
@@ -348,8 +321,7 @@ fn handle_sync_start(
|
|||||||
}
|
}
|
||||||
state.syncing = true;
|
state.syncing = true;
|
||||||
state.sync_count = count;
|
state.sync_count = count;
|
||||||
state.sync_current = payload[1];
|
state.sync_budget = payload[1];
|
||||||
state.sync_mode = payload[2];
|
|
||||||
build_response(rsp_buf, seq, STATUS_OK, &[])
|
build_response(rsp_buf, seq, STATUS_OK, &[])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,14 +340,8 @@ where
|
|||||||
return Ok(build_response(rsp_buf, seq, STATUS_BAD_CMD, &[]));
|
return Ok(build_response(rsp_buf, seq, STATUS_BAD_CMD, &[]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active index defaults to 0
|
|
||||||
match recalculate_header_crc(
|
match recalculate_header_crc(
|
||||||
ntag,
|
ntag, state.sync_count, 0, state.sync_budget, false, 0, delay,
|
||||||
state.sync_count,
|
|
||||||
0, // active_index = 0
|
|
||||||
state.sync_current,
|
|
||||||
state.sync_mode,
|
|
||||||
delay,
|
|
||||||
) {
|
) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(ntag5::Error::VerifyFailed) => {
|
Err(ntag5::Error::VerifyFailed) => {
|
||||||
@@ -402,13 +368,12 @@ fn handle_read_library<I2C, E>(
|
|||||||
where
|
where
|
||||||
I2C: I2c<Error = E>,
|
I2C: I2c<Error = E>,
|
||||||
{
|
{
|
||||||
let mut payload = [0u8; 4];
|
let mut payload = [0u8; 3];
|
||||||
match read_library_header(ntag)? {
|
match read_library_header(ntag)? {
|
||||||
Some(h) => {
|
Some(h) => {
|
||||||
payload[0] = h.pattern_count;
|
payload[0] = h.pattern_count;
|
||||||
payload[1] = h.active_index;
|
payload[1] = h.active_index;
|
||||||
payload[2] = h.current;
|
payload[2] = h.budget;
|
||||||
payload[3] = h.led_mode;
|
|
||||||
state.read_index = 0;
|
state.read_index = 0;
|
||||||
state.read_count = h.pattern_count;
|
state.read_count = h.pattern_count;
|
||||||
}
|
}
|
||||||
@@ -420,7 +385,7 @@ where
|
|||||||
Ok(build_response(rsp_buf, seq, STATUS_OK, &payload))
|
Ok(build_response(rsp_buf, seq, STATUS_OK, &payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// READ_NEXT (0x08): Return the next pattern entry (112 bytes).
|
/// READ_NEXT (0x08): Return the next pattern entry (16 bytes).
|
||||||
fn handle_read_next<I2C, E>(
|
fn handle_read_next<I2C, E>(
|
||||||
ntag: &mut Ntag5Link<I2C>,
|
ntag: &mut Ntag5Link<I2C>,
|
||||||
state: &mut MailboxState,
|
state: &mut MailboxState,
|
||||||
@@ -447,11 +412,8 @@ where
|
|||||||
|
|
||||||
/// Read SRAM, dispatch the command, and write the response back.
|
/// Read SRAM, dispatch the command, and write the response back.
|
||||||
///
|
///
|
||||||
/// Returns `Ok(true)` if a command was processed, `Ok(false)` if no valid
|
/// Returns `Ok(true)` if a command was processed that modifies the pattern library,
|
||||||
/// command was found in SRAM (e.g., empty buffer, bad header, bad CRC).
|
/// `Ok(false)` if no valid command or no library change.
|
||||||
///
|
|
||||||
/// I2C errors propagate as `Err`. EEPROM verify failures are reported via
|
|
||||||
/// a STATUS_EEPROM_FAIL response (not as Err).
|
|
||||||
pub fn process_command<I2C, E>(
|
pub fn process_command<I2C, E>(
|
||||||
ntag: &mut Ntag5Link<I2C>,
|
ntag: &mut Ntag5Link<I2C>,
|
||||||
state: &mut MailboxState,
|
state: &mut MailboxState,
|
||||||
@@ -494,5 +456,11 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
ntag.write_sram_blocks(0, &rsp[..rsp_len])?;
|
ntag.write_sram_blocks(0, &rsp[..rsp_len])?;
|
||||||
Ok(true)
|
|
||||||
|
// Return true for commands that modify pattern library
|
||||||
|
let library_changed = matches!(
|
||||||
|
cmd,
|
||||||
|
CMD_WRITE_PATTERN | CMD_SET_ACTIVE | CMD_SYNC_END
|
||||||
|
);
|
||||||
|
Ok(library_changed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,366 +1,306 @@
|
|||||||
//! Predefined LP5562 engine patterns for xblink.
|
//! Software pattern engine for GPIO-direct PWM LED control.
|
||||||
//!
|
//!
|
||||||
//! Each pattern is a set of engine programs + LED_MAP configuration.
|
//! Replaces LP5562 hardware execution engines. The MCU drives 6 LEDs via
|
||||||
//! The LP5562 runs these autonomously — the MCU can sleep after loading.
|
//! TCC hardware PWM, updating duty cycles from a TC4 ISR at 50Hz.
|
||||||
|
//!
|
||||||
|
//! XBLK v2 EEPROM format: 16-byte header + 16-byte pattern entries + playlist.
|
||||||
|
|
||||||
use crate::led::lp5562::{
|
/// Number of LED channels.
|
||||||
Channel, EngineCommand, EngineId, EngineProgram, LedMapping, Lp5562, Prescale, RampDirection,
|
pub const NUM_LEDS: usize = 6;
|
||||||
|
|
||||||
|
/// Animation tick rate in Hz.
|
||||||
|
pub const TICK_RATE_HZ: u32 = 50;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Waveform types and lookup tables
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Available waveform shapes.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Waveform {
|
||||||
|
Sine = 0,
|
||||||
|
Triangle = 1,
|
||||||
|
Square = 2,
|
||||||
|
Heartbeat = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Waveform {
|
||||||
|
pub fn from_u8(v: u8) -> Option<Self> {
|
||||||
|
match v {
|
||||||
|
0 => Some(Waveform::Sine),
|
||||||
|
1 => Some(Waveform::Triangle),
|
||||||
|
2 => Some(Waveform::Square),
|
||||||
|
3 => Some(Waveform::Heartbeat),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 64-entry quarter-wave sine table (0-255 output range).
|
||||||
|
/// Full wave is reconstructed by mirroring: indices 0..63 = rising first quarter,
|
||||||
|
/// 64..127 = falling second quarter (mirror), 128..191 = negative third (zero),
|
||||||
|
/// 192..255 = negative fourth (zero). For unipolar: mirror to get full 0-255-0 cycle.
|
||||||
|
const SINE_QUARTER: [u8; 64] = {
|
||||||
|
// Approximate sin(x) for x in [0, pi/2], scaled to 0-255.
|
||||||
|
// Generated from: round(255 * sin(i * pi / 128)) for i in 0..64
|
||||||
|
let mut table = [0u8; 64];
|
||||||
|
let mut i = 0;
|
||||||
|
while i < 64 {
|
||||||
|
// Fixed-point sine approximation using Taylor series:
|
||||||
|
// sin(x) ~ x - x^3/6 + x^5/120, where x = i * pi / 128
|
||||||
|
// We use a precomputed table for accuracy.
|
||||||
|
// These values are: round(255 * sin(i * pi / 128))
|
||||||
|
table[i] = SINE_VALUES[i];
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
table
|
||||||
};
|
};
|
||||||
use embedded_hal::i2c::I2c;
|
|
||||||
|
|
||||||
/// LED hardware configuration.
|
const SINE_VALUES: [u8; 64] = [
|
||||||
#[derive(Clone, Copy, Debug)]
|
0, 6, 12, 19, 25, 31, 37, 43, 49, 56, 62, 68, 74, 80, 86, 91,
|
||||||
pub enum LedMode {
|
97, 103, 109, 114, 120, 125, 131, 136, 141, 146, 151, 156, 161, 166, 170, 175,
|
||||||
/// Single RGBW LED (e.g., LP5562EVM D1). Engines map to R, G, B; W is I2C-direct.
|
179, 183, 187, 191, 195, 199, 202, 206, 209, 212, 215, 218, 220, 223, 225, 228,
|
||||||
Rgbw,
|
230, 232, 233, 235, 237, 238, 239, 241, 242, 243, 243, 244, 245, 245, 245, 245,
|
||||||
/// 3 independent monochrome LEDs. Engines map to B, G, R channels (one each).
|
];
|
||||||
Mono3,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A complete pattern: up to 3 engine programs + LED mapping.
|
/// Heartbeat waveform: 256-entry full cycle.
|
||||||
pub struct Pattern {
|
/// Double-pulse cardiac shape: two sharp peaks with a rest period.
|
||||||
pub engine1: Option<EngineProgram>,
|
const HEARTBEAT_LUT: [u8; 256] = {
|
||||||
pub engine2: Option<EngineProgram>,
|
let mut table = [0u8; 256];
|
||||||
pub engine3: Option<EngineProgram>,
|
// First beat: indices 0-31 (sharp rise/fall)
|
||||||
/// LED_MAP: which engine (or I2C direct) drives each channel.
|
let mut i = 0;
|
||||||
/// Index: [B, G, R, W] → LedMapping value.
|
while i < 16 {
|
||||||
pub map_b: LedMapping,
|
table[i] = (i as u8) * 16; // 0 → 240
|
||||||
pub map_g: LedMapping,
|
i += 1;
|
||||||
pub map_r: LedMapping,
|
}
|
||||||
pub map_w: LedMapping,
|
while i < 32 {
|
||||||
/// Next pattern index after this one completes (0xFF = loop forever).
|
table[i] = (31 - i as u8) * 16; // 240 → 0
|
||||||
pub next_pattern: u8,
|
i += 1;
|
||||||
/// Number of full cycles before chaining (0 = chain immediately on engine stop).
|
}
|
||||||
pub loop_count: u8,
|
// Gap: 32-63
|
||||||
}
|
// Second beat: indices 64-95 (slightly weaker)
|
||||||
|
i = 64;
|
||||||
|
while i < 80 {
|
||||||
|
table[i] = ((i - 64) as u8) * 12; // 0 → 180
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
while i < 96 {
|
||||||
|
table[i] = ((95 - i) as u8) * 12; // 180 → 0
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
// Rest: indices 96-255 = 0 (already zeroed)
|
||||||
|
table
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
/// Sample a waveform at position `pos` (0-255 maps to one full cycle, 0-360 degrees).
|
||||||
// Breathing: smooth ramp up/down, ~2.5s cycle
|
fn sample_waveform(waveform: Waveform, pos: u8) -> u8 {
|
||||||
// ---------------------------------------------------------------------------
|
match waveform {
|
||||||
|
Waveform::Sine => {
|
||||||
/// Breathing pattern — one engine, smooth ramp.
|
// Unipolar sine: 0 at pos=0, 255 at pos=64, 0 at pos=128, stays 0 for 128-255
|
||||||
///
|
// Actually for LED breathing we want: 0→255→0 over the full cycle.
|
||||||
/// LP5562 increment field = number of steps - 1 (max 127 = 128 steps).
|
// Map pos 0-255 to a full sine period (0 → peak → 0 → peak → 0) ... no.
|
||||||
/// Each step changes PWM by 1 unit. Full 0→255 needs two ramp commands.
|
// Better: simple 0→255→0 breathing shape over 256 steps.
|
||||||
///
|
// pos 0..63: rising (quarter 1)
|
||||||
/// Slow prescale (15.6ms/step), step_time=1:
|
// pos 64..127: falling from peak (quarter 2, mirror)
|
||||||
/// Ramp up: 2 × 128 steps × 15.6ms = ~4.0s (0→128→255)
|
// pos 128..191: rising again (quarter 3 = same as 1)
|
||||||
/// Ramp down: 2 × 128 steps × 15.6ms = ~4.0s (255→127→0)
|
// pos 192..255: falling again (quarter 4 = same as 2)
|
||||||
/// Wait: step_time=48 → 48 × 15.6ms ≈ 0.75s pause at bottom
|
// No — that's two cycles. For one full breath cycle:
|
||||||
/// Total: ~8.75s per cycle
|
// pos 0..127: 0→255 (half sine, rising)
|
||||||
fn breathe_program() -> EngineProgram {
|
// pos 128..255: 255→0 (half sine, falling)
|
||||||
EngineProgram::from_commands(&[
|
let half = pos as u16;
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 0→128, ~2s
|
if half < 128 {
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 128→255, ~2s
|
// Rising: sample quarter sine and mirror
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 255→127, ~2s
|
let idx = if half < 64 {
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 127→0, ~2s
|
SINE_QUARTER[half as usize]
|
||||||
EngineCommand::wait(Prescale::Slow, 48), // ~0.75s pause
|
} else {
|
||||||
EngineCommand::branch(0, 0), // loop forever
|
SINE_QUARTER[127 - half as usize]
|
||||||
])
|
};
|
||||||
}
|
// Scale: quarter sine peaks at 245, we want 255
|
||||||
|
let scaled = (idx as u16 * 255) / 245;
|
||||||
/// Load breathing pattern. All active channels breathe in sync.
|
if scaled > 255 { 255 } else { scaled as u8 }
|
||||||
pub fn breathe(mode: LedMode) -> Pattern {
|
} else {
|
||||||
single_engine_pattern(breathe_program(), mode)
|
// Falling: mirror of rising
|
||||||
}
|
let mirror = 255 - pos;
|
||||||
|
let half_m = mirror as u16;
|
||||||
// ---------------------------------------------------------------------------
|
let idx = if half_m < 64 {
|
||||||
// Heartbeat: double-pulse with long pause, ~1.6s cycle
|
SINE_QUARTER[half_m as usize]
|
||||||
// ---------------------------------------------------------------------------
|
} else {
|
||||||
|
SINE_QUARTER[127 - half_m as usize]
|
||||||
/// Heartbeat pattern — fast double-pulse, long rest.
|
};
|
||||||
///
|
let scaled = (idx as u16 * 255) / 245;
|
||||||
/// Fast prescale (0.49ms/step):
|
if scaled > 255 { 255 } else { scaled as u8 }
|
||||||
/// set_pwm 255 → snap on
|
}
|
||||||
/// wait fast, st=20 → 20 × 0.49ms ≈ 10ms hold
|
}
|
||||||
/// set_pwm 0 → snap off
|
Waveform::Triangle => {
|
||||||
/// wait fast, st=40 → 40 × 0.49ms ≈ 20ms gap
|
// 0→255→0 linear triangle
|
||||||
/// set_pwm 255 → second beat
|
if pos < 128 {
|
||||||
/// wait fast, st=20 → 10ms hold
|
(pos as u16 * 2) as u8
|
||||||
/// set_pwm 0 → snap off
|
} else {
|
||||||
/// Slow prescale for the long rest:
|
((255 - pos as u16) * 2) as u8
|
||||||
/// wait slow, st=63 → 63 × 15.6ms ≈ 1.0s
|
}
|
||||||
/// wait slow, st=32 → 32 × 15.6ms ≈ 0.5s (total rest ~1.5s)
|
}
|
||||||
/// branch 0 → loop
|
Waveform::Square => {
|
||||||
fn heartbeat_program() -> EngineProgram {
|
// On for first half, off for second half
|
||||||
EngineProgram::from_commands(&[
|
if pos < 128 { 255 } else { 0 }
|
||||||
EngineCommand::set_pwm(255), // 0: first beat ON
|
}
|
||||||
EngineCommand::wait(Prescale::Fast, 20), // 1: hold ~10ms
|
Waveform::Heartbeat => {
|
||||||
EngineCommand::set_pwm(0), // 2: first beat OFF
|
HEARTBEAT_LUT[pos as usize]
|
||||||
EngineCommand::wait(Prescale::Fast, 40), // 3: gap ~20ms
|
}
|
||||||
EngineCommand::set_pwm(255), // 4: second beat ON
|
|
||||||
EngineCommand::wait(Prescale::Fast, 20), // 5: hold ~10ms
|
|
||||||
EngineCommand::set_pwm(0), // 6: second beat OFF
|
|
||||||
EngineCommand::wait(Prescale::Slow, 63), // 7: rest ~1.0s
|
|
||||||
EngineCommand::wait(Prescale::Slow, 32), // 8: rest ~0.5s
|
|
||||||
EngineCommand::branch(0, 0), // 9: loop forever
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load heartbeat pattern.
|
|
||||||
pub fn heartbeat(mode: LedMode) -> Pattern {
|
|
||||||
single_engine_pattern(heartbeat_program(), mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// RGB cycle / staggered chase: 3 engines, trigger-synced phase offset
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Phase-offset breathing using triggers for synchronization.
|
|
||||||
///
|
|
||||||
/// 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, 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, 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, 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
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load RGB cycle (RGBW mode) or staggered chase (mono mode).
|
|
||||||
pub fn rgb_cycle(mode: LedMode) -> Pattern {
|
|
||||||
match mode {
|
|
||||||
LedMode::Rgbw => Pattern {
|
|
||||||
engine1: Some(rgb_cycle_engine1()),
|
|
||||||
engine2: Some(rgb_cycle_engine2()),
|
|
||||||
engine3: Some(rgb_cycle_engine3()),
|
|
||||||
// Each engine drives one color channel
|
|
||||||
map_b: LedMapping::Engine1,
|
|
||||||
map_g: LedMapping::Engine2,
|
|
||||||
map_r: LedMapping::Engine3,
|
|
||||||
map_w: LedMapping::I2c,
|
|
||||||
next_pattern: 0xFF,
|
|
||||||
loop_count: 0,
|
|
||||||
},
|
|
||||||
LedMode::Mono3 => Pattern {
|
|
||||||
engine1: Some(rgb_cycle_engine1()),
|
|
||||||
engine2: Some(rgb_cycle_engine2()),
|
|
||||||
engine3: Some(rgb_cycle_engine3()),
|
|
||||||
// Each engine drives one physical LED
|
|
||||||
map_b: LedMapping::Engine1,
|
|
||||||
map_g: LedMapping::Engine2,
|
|
||||||
map_r: LedMapping::Engine3,
|
|
||||||
map_w: LedMapping::I2c,
|
|
||||||
next_pattern: 0xFF,
|
|
||||||
loop_count: 0,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Slow pulse: gentle ramp, dimmer peak, ~5s cycle
|
// Pattern state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Slow pulse — very gentle and long.
|
/// A single pattern definition, loaded from EEPROM or hardcoded.
|
||||||
///
|
#[derive(Clone, Copy)]
|
||||||
/// Slow prescale (15.6ms/step), step_time=4:
|
pub struct PatternDef {
|
||||||
/// Ramp up: 2 × 128 steps × 62.4ms = ~16.0s (0→128→255)
|
pub waveform: Waveform,
|
||||||
/// Wait: step_time=32 → ~0.5s hold at peak
|
pub cycle_len: u8, // ticks per full cycle (1-255)
|
||||||
/// Ramp down: 2 × 128 steps × 62.4ms = ~16.0s (255→127→0)
|
pub phase: [u8; NUM_LEDS], // phase offset per LED (0-255 = 0-360 degrees)
|
||||||
/// Wait: step_time=63 → ~1.0s pause at bottom
|
pub envelope: [u8; NUM_LEDS], // max brightness per LED
|
||||||
/// Total: ~33.5s per cycle
|
pub repeat_count: u8, // playlist: times to play before advancing (0xFF=forever)
|
||||||
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.
|
/// Runtime pattern engine state.
|
||||||
pub fn slow_pulse(mode: LedMode) -> Pattern {
|
pub struct PatternEngine {
|
||||||
single_engine_pattern(slow_pulse_program(), mode)
|
pub pattern: PatternDef,
|
||||||
|
pub tick: u16, // current tick within cycle
|
||||||
|
pub cycle_count: u16, // completed cycles (for repeat_count tracking)
|
||||||
|
pub budget: u16, // power governor budget (sum of all LED values must not exceed this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
impl PatternEngine {
|
||||||
// Color wash: 3 engines, smooth overlapping ramps for blended color transitions
|
pub fn new(pattern: PatternDef, budget: u8) -> Self {
|
||||||
// ---------------------------------------------------------------------------
|
PatternEngine {
|
||||||
|
pattern,
|
||||||
|
tick: 0,
|
||||||
|
cycle_count: 0,
|
||||||
|
budget: budget as u16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Color wash — free-running engines with different cycle lengths.
|
/// Compute brightness for all LEDs at the current tick, applying governor.
|
||||||
///
|
/// Call this from the TC4 ISR at 50Hz.
|
||||||
/// No triggers. Each engine breathes independently at a slightly different
|
pub fn tick(&mut self, output: &mut [u8; NUM_LEDS]) {
|
||||||
/// rate, causing them to drift in and out of phase. Smooth single-PWM-unit
|
let cycle = self.pattern.cycle_len as u16;
|
||||||
/// increments for clean color blending.
|
if cycle == 0 {
|
||||||
///
|
for v in output.iter_mut() { *v = 0; }
|
||||||
/// Slow prescale (15.6ms/step), step_time=1, increment=127 (128 steps per cmd):
|
return;
|
||||||
/// 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 {
|
// Compute raw brightness per LED
|
||||||
// ~8.5s cycle (short pause at bottom)
|
for i in 0..NUM_LEDS {
|
||||||
EngineProgram::from_commands(&[
|
// Map tick to 0-255 position within the waveform cycle
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 0→128
|
// tick ranges from 0 to cycle_len-1
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 128→255
|
// phase[i] offsets in units of 1/256 of a cycle
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 255→127
|
let pos = ((self.tick as u32 * 256 / cycle as u32)
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 127→0
|
+ self.pattern.phase[i] as u32) % 256;
|
||||||
EngineCommand::wait(Prescale::Slow, 32), // pause ~0.5s
|
let raw = sample_waveform(self.pattern.waveform, pos as u8);
|
||||||
EngineCommand::branch(0, 0),
|
// Scale by envelope (max brightness for this LED)
|
||||||
])
|
output[i] = ((raw as u16 * self.pattern.envelope[i] as u16) / 255) as u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn color_wash_engine3() -> EngineProgram {
|
// Power governor: scale down if total exceeds budget
|
||||||
// ~9s cycle (longer pause at bottom)
|
if self.budget > 0 {
|
||||||
EngineProgram::from_commands(&[
|
let total: u16 = output.iter().map(|&v| v as u16).sum();
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 0→128
|
if total > self.budget {
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 127), // 128→255
|
let scale = (self.budget * 256) / total;
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 255→127
|
for v in output.iter_mut() {
|
||||||
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 127), // 127→0
|
*v = ((*v as u16 * scale) / 256) as u8;
|
||||||
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).
|
// Advance tick
|
||||||
pub fn color_wash(mode: LedMode) -> Pattern {
|
self.tick += 1;
|
||||||
match mode {
|
if self.tick >= cycle {
|
||||||
LedMode::Rgbw => Pattern {
|
self.tick = 0;
|
||||||
engine1: Some(color_wash_engine1()),
|
if self.cycle_count < u16::MAX {
|
||||||
engine2: Some(color_wash_engine2()),
|
self.cycle_count += 1;
|
||||||
engine3: Some(color_wash_engine3()),
|
}
|
||||||
map_b: LedMapping::Engine1,
|
}
|
||||||
map_g: LedMapping::Engine2,
|
}
|
||||||
map_r: LedMapping::Engine3,
|
|
||||||
map_w: LedMapping::I2c,
|
/// Check if this pattern's repeat count has been reached.
|
||||||
next_pattern: 0xFF,
|
pub fn repeats_done(&self) -> bool {
|
||||||
loop_count: 0,
|
if self.pattern.repeat_count == 0xFF {
|
||||||
},
|
return false; // loop forever
|
||||||
LedMode::Mono3 => Pattern {
|
}
|
||||||
engine1: Some(color_wash_engine1()),
|
self.cycle_count >= self.pattern.repeat_count as u16
|
||||||
engine2: Some(color_wash_engine2()),
|
}
|
||||||
engine3: Some(color_wash_engine3()),
|
|
||||||
map_b: LedMapping::Engine1,
|
/// Load a new pattern, resetting tick and cycle count.
|
||||||
map_g: LedMapping::Engine2,
|
pub fn load(&mut self, pattern: PatternDef) {
|
||||||
map_r: LedMapping::Engine3,
|
self.pattern = pattern;
|
||||||
map_w: LedMapping::I2c,
|
self.tick = 0;
|
||||||
next_pattern: 0xFF,
|
self.cycle_count = 0;
|
||||||
loop_count: 0,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helper for single-engine patterns (all RGB channels mapped to Engine1)
|
// Predefined patterns
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn single_engine_pattern(prog: EngineProgram, mode: LedMode) -> Pattern {
|
/// Breathing: smooth sine, all LEDs in phase, ~2.5s cycle
|
||||||
let _ = mode; // Same mapping for both modes
|
pub fn breathe() -> PatternDef {
|
||||||
Pattern {
|
PatternDef {
|
||||||
engine1: Some(prog),
|
waveform: Waveform::Sine,
|
||||||
engine2: None,
|
cycle_len: 125, // 125 ticks = 2.5s at 50Hz
|
||||||
engine3: None,
|
phase: [0, 0, 0, 0, 0, 0],
|
||||||
map_b: LedMapping::Engine1,
|
envelope: [255, 255, 255, 255, 255, 255],
|
||||||
map_g: LedMapping::Engine1,
|
repeat_count: 0xFF,
|
||||||
map_r: LedMapping::Engine1,
|
}
|
||||||
map_w: LedMapping::I2c,
|
}
|
||||||
next_pattern: 0xFF,
|
|
||||||
loop_count: 0,
|
/// Heartbeat: double-pulse cardiac, ~1.6s cycle
|
||||||
|
pub fn heartbeat() -> PatternDef {
|
||||||
|
PatternDef {
|
||||||
|
waveform: Waveform::Heartbeat,
|
||||||
|
cycle_len: 80, // 80 ticks = 1.6s
|
||||||
|
phase: [0, 0, 0, 0, 0, 0],
|
||||||
|
envelope: [255, 255, 255, 255, 255, 255],
|
||||||
|
repeat_count: 0xFF,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wave chase: sine with 60-degree phase offsets between LEDs, ~2s cycle
|
||||||
|
pub fn wave_chase() -> PatternDef {
|
||||||
|
PatternDef {
|
||||||
|
waveform: Waveform::Sine,
|
||||||
|
cycle_len: 100, // 2s
|
||||||
|
phase: [0, 43, 85, 128, 170, 213], // ~60 degree spacing
|
||||||
|
envelope: [255, 255, 255, 255, 255, 255],
|
||||||
|
repeat_count: 0xFF,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slow pulse: very gentle and long, ~5s cycle
|
||||||
|
pub fn slow_pulse() -> PatternDef {
|
||||||
|
PatternDef {
|
||||||
|
waveform: Waveform::Triangle,
|
||||||
|
cycle_len: 250, // 5s
|
||||||
|
phase: [0, 0, 0, 0, 0, 0],
|
||||||
|
envelope: [200, 200, 200, 200, 200, 200],
|
||||||
|
repeat_count: 0xFF,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alternating blink: odds and evens alternate, ~1s cycle
|
||||||
|
pub fn alternating_blink() -> PatternDef {
|
||||||
|
PatternDef {
|
||||||
|
waveform: Waveform::Square,
|
||||||
|
cycle_len: 50, // 1s
|
||||||
|
phase: [0, 128, 0, 128, 0, 128],
|
||||||
|
envelope: [255, 255, 255, 255, 255, 255],
|
||||||
|
repeat_count: 0xFF,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Pattern loader
|
// XBLK v2 EEPROM format
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Software-reset LP5562, re-initialize, and load a pattern.
|
|
||||||
///
|
|
||||||
/// 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>,
|
|
||||||
{
|
|
||||||
// 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)?;
|
|
||||||
lp.set_led_mapping(Channel::Green, pattern.map_g).map_err(crate::led::lp5562::Error::I2c)?;
|
|
||||||
lp.set_led_mapping(Channel::Red, pattern.map_r).map_err(crate::led::lp5562::Error::I2c)?;
|
|
||||||
lp.set_led_mapping(Channel::White, pattern.map_w).map_err(crate::led::lp5562::Error::I2c)?;
|
|
||||||
|
|
||||||
// Load and run each engine that has a program
|
|
||||||
if let Some(ref prog) = pattern.engine1 {
|
|
||||||
lp.run_engine(EngineId::Engine1, prog)?;
|
|
||||||
delay.delay_us(200);
|
|
||||||
}
|
|
||||||
if let Some(ref prog) = pattern.engine2 {
|
|
||||||
lp.run_engine(EngineId::Engine2, prog)?;
|
|
||||||
delay.delay_us(200);
|
|
||||||
}
|
|
||||||
if let Some(ref prog) = pattern.engine3 {
|
|
||||||
lp.run_engine(EngineId::Engine3, prog)?;
|
|
||||||
delay.delay_us(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// XBLK EEPROM pattern library format
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// EEPROM base block for the pattern library (upper 1K, block 256).
|
/// EEPROM base block for the pattern library (upper 1K, block 256).
|
||||||
@@ -370,26 +310,33 @@ pub const LIBRARY_BASE_BLOCK: u16 = 0x0100;
|
|||||||
pub const XBLK_MAGIC: [u8; 4] = *b"XBLK";
|
pub const XBLK_MAGIC: [u8; 4] = *b"XBLK";
|
||||||
|
|
||||||
/// Format version.
|
/// Format version.
|
||||||
pub const XBLK_VERSION: u8 = 0x01;
|
pub const XBLK_VERSION: u8 = 0x02;
|
||||||
|
|
||||||
/// Header size in bytes.
|
/// Header size in bytes.
|
||||||
pub const HEADER_SIZE: usize = 16;
|
pub const HEADER_SIZE: usize = 16;
|
||||||
|
|
||||||
/// Pattern entry size in bytes (fixed for direct seeking).
|
/// Pattern entry size in bytes.
|
||||||
pub const PATTERN_ENTRY_SIZE: usize = 112;
|
pub const PATTERN_ENTRY_SIZE: usize = 16;
|
||||||
|
|
||||||
/// Maximum patterns that fit in 1024 bytes: (1024 - 16) / 112 = 9.
|
/// Maximum playlist entries.
|
||||||
pub const MAX_PATTERNS: usize = 9;
|
pub const MAX_PLAYLIST: usize = 32;
|
||||||
|
|
||||||
/// Parsed XBLK library header.
|
/// Maximum patterns that fit: (1024 - 16 header - 32 playlist) / 16 = 61.
|
||||||
|
pub const MAX_PATTERNS: usize = 61;
|
||||||
|
|
||||||
|
/// Default power governor budget (sum of all LED brightness values).
|
||||||
|
pub const DEFAULT_BUDGET: u8 = 50;
|
||||||
|
|
||||||
|
/// Parsed XBLK v2 library header.
|
||||||
pub struct LibraryHeader {
|
pub struct LibraryHeader {
|
||||||
pub pattern_count: u8,
|
pub pattern_count: u8,
|
||||||
pub active_index: u8,
|
pub active_index: u8,
|
||||||
pub current: u8,
|
pub budget: u8,
|
||||||
pub led_mode: u8,
|
pub has_playlist: bool,
|
||||||
|
pub playlist_count: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a 16-byte XBLK header. Returns None if magic or version is invalid.
|
/// Parse a 16-byte XBLK v2 header. Returns None if magic or version is invalid.
|
||||||
pub fn parse_header(buf: &[u8; HEADER_SIZE]) -> Option<LibraryHeader> {
|
pub fn parse_header(buf: &[u8; HEADER_SIZE]) -> Option<LibraryHeader> {
|
||||||
if buf[0..4] != XBLK_MAGIC {
|
if buf[0..4] != XBLK_MAGIC {
|
||||||
return None;
|
return None;
|
||||||
@@ -397,147 +344,82 @@ pub fn parse_header(buf: &[u8; HEADER_SIZE]) -> Option<LibraryHeader> {
|
|||||||
if buf[4] != XBLK_VERSION {
|
if buf[4] != XBLK_VERSION {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let count = buf[5];
|
let flags = buf[5];
|
||||||
|
let count = buf[6];
|
||||||
if count == 0 || count as usize > MAX_PATTERNS {
|
if count == 0 || count as usize > MAX_PATTERNS {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
// Verify CRC
|
||||||
|
let stored_crc = (buf[14] as u16) << 8 | buf[15] as u16;
|
||||||
|
let computed_crc = crc16(&buf[0..14]);
|
||||||
|
if stored_crc != computed_crc {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
Some(LibraryHeader {
|
Some(LibraryHeader {
|
||||||
pattern_count: count,
|
pattern_count: count,
|
||||||
active_index: buf[6] % count, // wrap if out of range
|
active_index: buf[7] % count,
|
||||||
current: buf[7],
|
budget: buf[8],
|
||||||
led_mode: buf[8],
|
has_playlist: flags & 0x01 != 0,
|
||||||
|
playlist_count: buf[9],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a 112-byte pattern entry into a Pattern struct.
|
/// Parse a 16-byte pattern entry into a PatternDef.
|
||||||
/// Returns None if the data is malformed.
|
pub fn parse_pattern_entry(buf: &[u8; PATTERN_ENTRY_SIZE]) -> Option<PatternDef> {
|
||||||
pub fn parse_pattern_entry(buf: &[u8; PATTERN_ENTRY_SIZE]) -> Option<Pattern> {
|
let waveform = Waveform::from_u8(buf[0])?;
|
||||||
let engine_count = buf[0];
|
let cycle_len = buf[1];
|
||||||
if engine_count > 3 {
|
if cycle_len == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let mut phase = [0u8; NUM_LEDS];
|
||||||
let led_map_reg = buf[1];
|
phase.copy_from_slice(&buf[2..8]);
|
||||||
// Decode LED_MAP register: 2 bits per channel [W(7:6), R(5:4), G(3:2), B(1:0)]
|
let mut envelope = [0u8; NUM_LEDS];
|
||||||
let map_b = led_map_byte_to_mapping(led_map_reg & 0x03)?;
|
envelope.copy_from_slice(&buf[8..14]);
|
||||||
let map_g = led_map_byte_to_mapping((led_map_reg >> 2) & 0x03)?;
|
Some(PatternDef {
|
||||||
let map_r = led_map_byte_to_mapping((led_map_reg >> 4) & 0x03)?;
|
waveform,
|
||||||
let map_w = led_map_byte_to_mapping((led_map_reg >> 6) & 0x03)?;
|
cycle_len,
|
||||||
|
phase,
|
||||||
// Direct PWM values at bytes 2-5 (unused for now, engines override)
|
envelope,
|
||||||
// let _direct_pwm = [buf[2], buf[3], buf[4], buf[5]];
|
repeat_count: buf[14],
|
||||||
|
|
||||||
let engine1 = parse_engine_program(&buf[6..40])?;
|
|
||||||
let engine2 = parse_engine_program(&buf[40..74])?;
|
|
||||||
let engine3 = parse_engine_program(&buf[74..108])?;
|
|
||||||
|
|
||||||
Some(Pattern {
|
|
||||||
engine1,
|
|
||||||
engine2,
|
|
||||||
engine3,
|
|
||||||
map_b,
|
|
||||||
map_g,
|
|
||||||
map_r,
|
|
||||||
map_w,
|
|
||||||
next_pattern: buf[108],
|
|
||||||
loop_count: buf[109],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a 34-byte engine section: 2 bytes command count (BE) + 32 bytes commands.
|
/// Serialize a PatternDef into a 16-byte buffer.
|
||||||
/// Returns Some(None) for unused engines, Some(Some(prog)) for valid, None for malformed.
|
pub fn serialize_pattern_entry(p: &PatternDef, buf: &mut [u8; PATTERN_ENTRY_SIZE]) {
|
||||||
fn parse_engine_program(buf: &[u8]) -> Option<Option<EngineProgram>> {
|
buf[0] = p.waveform as u8;
|
||||||
let cmd_count = ((buf[0] as u16) << 8 | buf[1] as u16) as usize;
|
buf[1] = p.cycle_len;
|
||||||
if cmd_count == 0 {
|
buf[2..8].copy_from_slice(&p.phase);
|
||||||
return Some(None);
|
buf[8..14].copy_from_slice(&p.envelope);
|
||||||
}
|
buf[14] = p.repeat_count;
|
||||||
if cmd_count > 16 {
|
buf[15] = 0; // reserved
|
||||||
return None; // malformed
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut commands = [0u16; 16];
|
|
||||||
for i in 0..cmd_count {
|
|
||||||
let offset = 2 + i * 2;
|
|
||||||
commands[i] = (buf[offset] as u16) << 8 | buf[offset + 1] as u16;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut prog = EngineProgram::new();
|
|
||||||
for i in 0..cmd_count {
|
|
||||||
let _ = prog.push(commands[i]);
|
|
||||||
}
|
|
||||||
Some(Some(prog))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a 2-bit LED_MAP field to a LedMapping enum value.
|
/// Serialize the XBLK v2 header into a 16-byte buffer.
|
||||||
fn led_map_byte_to_mapping(val: u8) -> Option<LedMapping> {
|
pub fn serialize_header(
|
||||||
match val {
|
pattern_count: u8,
|
||||||
0b00 => Some(LedMapping::I2c),
|
active_index: u8,
|
||||||
0b01 => Some(LedMapping::Engine1),
|
budget: u8,
|
||||||
0b10 => Some(LedMapping::Engine2),
|
has_playlist: bool,
|
||||||
0b11 => Some(LedMapping::Engine3),
|
playlist_count: u8,
|
||||||
_ => None,
|
buf: &mut [u8; HEADER_SIZE],
|
||||||
}
|
) {
|
||||||
|
buf[0..4].copy_from_slice(&XBLK_MAGIC);
|
||||||
|
buf[4] = XBLK_VERSION;
|
||||||
|
buf[5] = if has_playlist { 0x01 } else { 0x00 };
|
||||||
|
buf[6] = pattern_count;
|
||||||
|
buf[7] = active_index;
|
||||||
|
buf[8] = budget;
|
||||||
|
buf[9] = playlist_count;
|
||||||
|
buf[10] = 0; // reserved
|
||||||
|
buf[11] = 0;
|
||||||
|
buf[12] = 0;
|
||||||
|
buf[13] = 0;
|
||||||
|
let crc = crc16(&buf[0..14]);
|
||||||
|
buf[14] = (crc >> 8) as u8;
|
||||||
|
buf[15] = crc as u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the raw LED_MAP register byte from a Pattern's mapping fields.
|
/// CRC-16/CCITT-FALSE.
|
||||||
pub fn pattern_to_led_map_byte(p: &Pattern) -> u8 {
|
|
||||||
(p.map_b as u8)
|
|
||||||
| ((p.map_g as u8) << 2)
|
|
||||||
| ((p.map_r as u8) << 4)
|
|
||||||
| ((p.map_w as u8) << 6)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// XBLK serializer (MCU-side, for self-provisioning)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Serialize a Pattern into a 112-byte XBLK entry buffer.
|
|
||||||
pub fn serialize_pattern_entry(p: &Pattern, buf: &mut [u8; PATTERN_ENTRY_SIZE]) {
|
|
||||||
// Zero the buffer
|
|
||||||
for b in buf.iter_mut() {
|
|
||||||
*b = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Engine count
|
|
||||||
let mut count = 0u8;
|
|
||||||
if p.engine1.is_some() { count += 1; }
|
|
||||||
if p.engine2.is_some() { count += 1; }
|
|
||||||
if p.engine3.is_some() { count += 1; }
|
|
||||||
buf[0] = count;
|
|
||||||
|
|
||||||
// LED_MAP register byte
|
|
||||||
buf[1] = pattern_to_led_map_byte(p);
|
|
||||||
|
|
||||||
// Direct PWM [B, G, R, W] — bytes 2-5, leave as 0 for engine patterns
|
|
||||||
|
|
||||||
// Engine programs
|
|
||||||
serialize_engine(&p.engine1, &mut buf[6..40]);
|
|
||||||
serialize_engine(&p.engine2, &mut buf[40..74]);
|
|
||||||
serialize_engine(&p.engine3, &mut buf[74..108]);
|
|
||||||
|
|
||||||
// Chaining
|
|
||||||
buf[108] = p.next_pattern;
|
|
||||||
buf[109] = p.loop_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize an optional engine program into a 34-byte section.
|
|
||||||
fn serialize_engine(eng: &Option<EngineProgram>, buf: &mut [u8]) {
|
|
||||||
match eng {
|
|
||||||
None => {
|
|
||||||
buf[0] = 0;
|
|
||||||
buf[1] = 0;
|
|
||||||
}
|
|
||||||
Some(prog) => {
|
|
||||||
let len = prog.len();
|
|
||||||
buf[0] = (len >> 8) as u8;
|
|
||||||
buf[1] = len as u8;
|
|
||||||
let bytes = prog.as_bytes(); // 32-byte SRAM image, big-endian
|
|
||||||
buf[2..34].copy_from_slice(&bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CRC-16/CCITT-FALSE (matching Python serializer).
|
|
||||||
pub fn crc16(data: &[u8]) -> u16 {
|
pub fn crc16(data: &[u8]) -> u16 {
|
||||||
let mut crc: u16 = 0xFFFF;
|
let mut crc: u16 = 0xFFFF;
|
||||||
for &b in data {
|
for &b in data {
|
||||||
@@ -553,40 +435,3 @@ pub fn crc16(data: &[u8]) -> u16 {
|
|||||||
}
|
}
|
||||||
crc
|
crc
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize the XBLK header into a 16-byte buffer.
|
|
||||||
/// CRC is computed over header bytes 0-13 + pattern data.
|
|
||||||
pub fn serialize_header(
|
|
||||||
pattern_count: u8,
|
|
||||||
active_index: u8,
|
|
||||||
current: u8,
|
|
||||||
led_mode: u8,
|
|
||||||
pattern_data_crc_input: &[u8],
|
|
||||||
buf: &mut [u8; HEADER_SIZE],
|
|
||||||
) {
|
|
||||||
buf[0..4].copy_from_slice(&XBLK_MAGIC);
|
|
||||||
buf[4] = XBLK_VERSION;
|
|
||||||
buf[5] = pattern_count;
|
|
||||||
buf[6] = active_index;
|
|
||||||
buf[7] = current;
|
|
||||||
buf[8] = led_mode;
|
|
||||||
for i in 9..14 {
|
|
||||||
buf[i] = 0;
|
|
||||||
}
|
|
||||||
// CRC over header[0..14] + pattern data
|
|
||||||
let mut crc = crc16(&buf[0..14]);
|
|
||||||
// Continue CRC over pattern data
|
|
||||||
for &b in pattern_data_crc_input {
|
|
||||||
crc ^= (b as u16) << 8;
|
|
||||||
for _ in 0..8 {
|
|
||||||
if crc & 0x8000 != 0 {
|
|
||||||
crc = (crc << 1) ^ 0x1021;
|
|
||||||
} else {
|
|
||||||
crc <<= 1;
|
|
||||||
}
|
|
||||||
crc &= 0xFFFF;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buf[14] = (crc >> 8) as u8;
|
|
||||||
buf[15] = crc as u8;
|
|
||||||
}
|
|
||||||
|
|||||||
166
tools/provision_eh.py
Normal file
166
tools/provision_eh.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Configure NTAG5Link energy harvesting for 1.8V automatic VOUT.
|
||||||
|
|
||||||
|
Writes EH_CONFIG to persistent EEPROM (block 0x3D) so the NTAG5
|
||||||
|
automatically outputs 1.8V when an NFC field is present — powering
|
||||||
|
the SAMD21 MCU without any firmware intervention.
|
||||||
|
|
||||||
|
Also sets ED_CONFIG (FD pin) for NFC-to-I2C SRAM pass-through so
|
||||||
|
the MCU can detect SRAM writes from the phone.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# Read current EH config:
|
||||||
|
python provision_eh.py --read
|
||||||
|
|
||||||
|
# Write 1.8V / 6.5mA EH config:
|
||||||
|
python provision_eh.py --write
|
||||||
|
|
||||||
|
# Write with custom current limit:
|
||||||
|
python provision_eh.py --write --current 4.0
|
||||||
|
|
||||||
|
# Also configure CONFIG_0 and CONFIG_1 for xblink:
|
||||||
|
python provision_eh.py --write --full
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add ntag5sensor to path
|
||||||
|
ntag5sensor_path = os.path.join(os.path.dirname(__file__), "..", "..", "ntag5sensor")
|
||||||
|
sys.path.insert(0, ntag5sensor_path)
|
||||||
|
|
||||||
|
from reader.acr1552 import ACR1552
|
||||||
|
from vicinity.iso15693 import ISO15693
|
||||||
|
from vicinity.ntag5link import (
|
||||||
|
Ntag5Link,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_V_SEL_1_8,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_V_SEL_2_4,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_V_SEL_3_0,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_I_SEL_0_4,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_I_SEL_0_6,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_I_SEL_1_4,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_I_SEL_2_7,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_I_SEL_4_0,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_I_SEL_6_5,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_I_SEL_9_0,
|
||||||
|
NXP_EH_CONFIG_EH_VOUT_I_SEL_12_5,
|
||||||
|
NXP_ED_CONFIG_NFC_TO_I2C_PASS_THROUGH,
|
||||||
|
NXP_CONFIG_0_EH_MODE_LOW_FIELD_STRENGTH,
|
||||||
|
NXP_CONFIG_1_ARBITER_MODE_SRAM_PASSTHROUGH,
|
||||||
|
NXP_CONFIG_1_USE_CASE_CONF_I2C_SLAVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Current limit lookup: string -> constant
|
||||||
|
CURRENT_MAP = {
|
||||||
|
"0.4": NXP_EH_CONFIG_EH_VOUT_I_SEL_0_4,
|
||||||
|
"0.6": NXP_EH_CONFIG_EH_VOUT_I_SEL_0_6,
|
||||||
|
"1.4": NXP_EH_CONFIG_EH_VOUT_I_SEL_1_4,
|
||||||
|
"2.7": NXP_EH_CONFIG_EH_VOUT_I_SEL_2_7,
|
||||||
|
"4.0": NXP_EH_CONFIG_EH_VOUT_I_SEL_4_0,
|
||||||
|
"6.5": NXP_EH_CONFIG_EH_VOUT_I_SEL_6_5,
|
||||||
|
"9.0": NXP_EH_CONFIG_EH_VOUT_I_SEL_9_0,
|
||||||
|
"12.5": NXP_EH_CONFIG_EH_VOUT_I_SEL_12_5,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(chip):
|
||||||
|
"""Read and display current EH and general config."""
|
||||||
|
print("=== NTAG5 Configuration ===\n")
|
||||||
|
|
||||||
|
info = chip.get_system_info()
|
||||||
|
print(f"UID: {info['uid'].hex()}")
|
||||||
|
|
||||||
|
config = chip.get_config_info()
|
||||||
|
print(f"\nCONFIG_0:")
|
||||||
|
print(f" EH mode: {config.get('energy_harvesting_mode', '?')}")
|
||||||
|
print(f" SRAM copy: {config.get('sram_copy_enabled', '?')}")
|
||||||
|
print(f" Auto standby: {config.get('auto_standby_mode', '?')}")
|
||||||
|
|
||||||
|
print(f"\nCONFIG_1:")
|
||||||
|
print(f" SRAM enable: {config.get('sram_enabled', '?')}")
|
||||||
|
print(f" Arbiter mode: {config.get('arbiter_mode', '?')}")
|
||||||
|
print(f" Use case: {config.get('use_case', '?')}")
|
||||||
|
print(f" EH arbiter: {config.get('eh_arbiter_mode_enabled', '?')}")
|
||||||
|
|
||||||
|
eh = chip.get_eh_ed_config_info()
|
||||||
|
print(f"\nEH_CONFIG (block 0x3D):")
|
||||||
|
print(f" EH enable: {eh.get('eh_enable', '?')}")
|
||||||
|
print(f" VOUT voltage: {eh.get('eh_vout_v_sel', '?')}V")
|
||||||
|
print(f" VOUT current: {eh.get('eh_vout_i_sel', '?')}mA")
|
||||||
|
print(f" Power check disabled: {eh.get('disable_power_check', '?')}")
|
||||||
|
print(f" ED/FD config: {eh.get('ed_config', '?')}")
|
||||||
|
|
||||||
|
|
||||||
|
def write_eh(chip, current_sel, full_config=False):
|
||||||
|
"""Write EH config for 1.8V automatic come-up."""
|
||||||
|
|
||||||
|
print("Writing EH config: 1.8V, current limit = "
|
||||||
|
f"{[k for k,v in CURRENT_MAP.items() if v == current_sel][0]}mA")
|
||||||
|
print(f" ED/FD pin: NFC-to-I2C pass-through (SRAM write detect)")
|
||||||
|
|
||||||
|
chip.write_eh_ed_config(
|
||||||
|
enable=True,
|
||||||
|
disable_power_check=False,
|
||||||
|
current=current_sel,
|
||||||
|
voltage=NXP_EH_CONFIG_EH_VOUT_V_SEL_1_8,
|
||||||
|
ed_config=NXP_ED_CONFIG_NFC_TO_I2C_PASS_THROUGH,
|
||||||
|
)
|
||||||
|
print(" EH_CONFIG written.")
|
||||||
|
|
||||||
|
if full_config:
|
||||||
|
print("\nWriting CONFIG_0: EH mode = low field strength")
|
||||||
|
chip.write_config0(
|
||||||
|
eh_mode=NXP_CONFIG_0_EH_MODE_LOW_FIELD_STRENGTH,
|
||||||
|
)
|
||||||
|
print(" CONFIG_0 written.")
|
||||||
|
|
||||||
|
print("Writing CONFIG_1: SRAM enable, arbiter=passthrough, use_case=I2C slave")
|
||||||
|
chip.write_config1(
|
||||||
|
sram_enable=True,
|
||||||
|
arbiter_mode=NXP_CONFIG_1_ARBITER_MODE_SRAM_PASSTHROUGH,
|
||||||
|
use_case=NXP_CONFIG_1_USE_CASE_CONF_I2C_SLAVE,
|
||||||
|
)
|
||||||
|
print(" CONFIG_1 written.")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
print("\n--- Verify ---")
|
||||||
|
read_config(chip)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Configure NTAG5Link energy harvesting for 1.8V")
|
||||||
|
parser.add_argument("--read", action="store_true",
|
||||||
|
help="Read current config (no writes)")
|
||||||
|
parser.add_argument("--write", action="store_true",
|
||||||
|
help="Write EH config for 1.8V automatic VOUT")
|
||||||
|
parser.add_argument("--current", default="6.5",
|
||||||
|
choices=list(CURRENT_MAP.keys()),
|
||||||
|
help="VOUT current limit in mA (default: 6.5)")
|
||||||
|
parser.add_argument("--full", action="store_true",
|
||||||
|
help="Also write CONFIG_0 and CONFIG_1 for xblink")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.read and not args.write:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
reader = ACR1552()
|
||||||
|
reader.connect()
|
||||||
|
iso = ISO15693(reader)
|
||||||
|
chip = Ntag5Link(iso)
|
||||||
|
|
||||||
|
if args.read:
|
||||||
|
read_config(chip)
|
||||||
|
|
||||||
|
if args.write:
|
||||||
|
current_sel = CURRENT_MAP[args.current]
|
||||||
|
write_eh(chip, current_sel, full_config=args.full)
|
||||||
|
|
||||||
|
reader.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
XBLK pattern library serializer for xblink.
|
XBLK v2 pattern library serializer for xblink.
|
||||||
|
|
||||||
Converts pattern definitions to the XBLK binary format and writes them
|
Converts pattern definitions to the XBLK v2 binary format (GPIO-direct PWM,
|
||||||
to NTAG5 EEPROM blocks 256+ (upper 1K) via ntag5sensor ISO15693 commands.
|
16-byte pattern entries) and writes them to NTAG5 EEPROM blocks 256+
|
||||||
|
(upper 1K) via ntag5sensor ISO15693 commands.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
# Serialize built-in patterns to binary file:
|
# Serialize built-in patterns to binary file:
|
||||||
@@ -22,27 +23,23 @@ import struct
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# XBLK format constants (must match src/pattern/mod.rs)
|
# XBLK v2 format constants (must match src/pattern/mod.rs)
|
||||||
XBLK_MAGIC = b"XBLK"
|
XBLK_MAGIC = b"XBLK"
|
||||||
XBLK_VERSION = 0x01
|
XBLK_VERSION = 0x02
|
||||||
HEADER_SIZE = 16
|
HEADER_SIZE = 16
|
||||||
PATTERN_ENTRY_SIZE = 112
|
PATTERN_ENTRY_SIZE = 16
|
||||||
MAX_PATTERNS = 9
|
MAX_PATTERNS = 61
|
||||||
MAX_COMMANDS_PER_ENGINE = 16
|
MAX_PLAYLIST = 32
|
||||||
|
NUM_LEDS = 6
|
||||||
|
|
||||||
# EEPROM block offset for pattern library (upper 1K)
|
# EEPROM block offset for pattern library (upper 1K)
|
||||||
LIBRARY_BASE_BLOCK = 256
|
LIBRARY_BASE_BLOCK = 256
|
||||||
|
|
||||||
# LED_MAP encoding helpers
|
# Waveform IDs
|
||||||
LED_MAP_LOOKUP = {
|
WAVEFORM_LOOKUP = {
|
||||||
"direct": 0b00, "i2c": 0b00,
|
"sine": 0, "triangle": 1, "square": 2, "heartbeat": 3,
|
||||||
"engine1": 0b01, "e1": 0b01,
|
|
||||||
"engine2": 0b10, "e2": 0b10,
|
|
||||||
"engine3": 0b11, "e3": 0b11,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LED_MODE_LOOKUP = {"rgbw": 0x00, "mono3": 0x01}
|
|
||||||
|
|
||||||
|
|
||||||
def crc16(data: bytes) -> int:
|
def crc16(data: bytes) -> int:
|
||||||
"""CRC-16/CCITT-FALSE."""
|
"""CRC-16/CCITT-FALSE."""
|
||||||
@@ -58,215 +55,131 @@ def crc16(data: bytes) -> int:
|
|||||||
return crc
|
return crc
|
||||||
|
|
||||||
|
|
||||||
def encode_led_map(mapping: dict) -> int:
|
|
||||||
"""Encode {"b": "engine1", "g": "engine1", ...} to LP5562 LED_MAP register byte."""
|
|
||||||
b = LED_MAP_LOOKUP.get(mapping.get("b", "direct"), 0)
|
|
||||||
g = LED_MAP_LOOKUP.get(mapping.get("g", "direct"), 0)
|
|
||||||
r = LED_MAP_LOOKUP.get(mapping.get("r", "direct"), 0)
|
|
||||||
w = LED_MAP_LOOKUP.get(mapping.get("w", "direct"), 0)
|
|
||||||
return b | (g << 2) | (r << 4) | (w << 6)
|
|
||||||
|
|
||||||
|
|
||||||
def encode_pattern(pat: dict) -> bytes:
|
def encode_pattern(pat: dict) -> bytes:
|
||||||
"""Encode a single pattern dict to PATTERN_ENTRY_SIZE bytes."""
|
"""Encode a single pattern dict to PATTERN_ENTRY_SIZE bytes."""
|
||||||
engines = pat.get("engines", [[], [], []])
|
waveform = WAVEFORM_LOOKUP.get(pat.get("waveform", "sine"), 0)
|
||||||
while len(engines) < 3:
|
cycle_len = pat.get("cycle_len", 125)
|
||||||
engines.append([])
|
phase = pat.get("phase", [0] * NUM_LEDS)
|
||||||
|
envelope = pat.get("envelope", [255] * NUM_LEDS)
|
||||||
|
repeat_count = pat.get("repeat_count", 0xFF)
|
||||||
|
|
||||||
engine_count = sum(1 for e in engines if len(e) > 0)
|
# Pad/truncate to NUM_LEDS
|
||||||
led_map_reg = encode_led_map(pat.get("led_map", {}))
|
phase = (phase + [0] * NUM_LEDS)[:NUM_LEDS]
|
||||||
direct_pwm = pat.get("direct_pwm", [0, 0, 0, 0])
|
envelope = (envelope + [255] * NUM_LEDS)[:NUM_LEDS]
|
||||||
while len(direct_pwm) < 4:
|
|
||||||
direct_pwm.append(0)
|
|
||||||
|
|
||||||
buf = bytearray(PATTERN_ENTRY_SIZE)
|
buf = bytearray(PATTERN_ENTRY_SIZE)
|
||||||
buf[0] = engine_count
|
buf[0] = waveform & 0xFF
|
||||||
buf[1] = led_map_reg
|
buf[1] = cycle_len & 0xFF
|
||||||
buf[2:6] = bytes(direct_pwm[:4])
|
buf[2:8] = bytes(phase)
|
||||||
|
buf[8:14] = bytes(envelope)
|
||||||
for eng_idx, cmds in enumerate(engines[:3]):
|
buf[14] = repeat_count & 0xFF
|
||||||
if len(cmds) > MAX_COMMANDS_PER_ENGINE:
|
buf[15] = 0 # reserved
|
||||||
raise ValueError(f"Engine {eng_idx+1} has {len(cmds)} commands (max {MAX_COMMANDS_PER_ENGINE})")
|
|
||||||
base = 6 + eng_idx * 34 # 2 bytes count + 32 bytes commands
|
|
||||||
struct.pack_into(">H", buf, base, len(cmds))
|
|
||||||
for i, cmd in enumerate(cmds):
|
|
||||||
struct.pack_into(">H", buf, base + 2 + i * 2, cmd & 0xFFFF)
|
|
||||||
|
|
||||||
return bytes(buf)
|
return bytes(buf)
|
||||||
|
|
||||||
|
|
||||||
def encode_library(config: dict) -> bytes:
|
def encode_library(config: dict) -> bytes:
|
||||||
"""Encode a full XBLK library (header + patterns) to bytes."""
|
"""Encode a full XBLK v2 library (header + patterns + playlist) to bytes."""
|
||||||
patterns = config.get("patterns", [])
|
patterns = config.get("patterns", [])
|
||||||
if len(patterns) == 0:
|
if len(patterns) == 0:
|
||||||
raise ValueError("No patterns defined")
|
raise ValueError("No patterns defined")
|
||||||
if len(patterns) > MAX_PATTERNS:
|
if len(patterns) > MAX_PATTERNS:
|
||||||
raise ValueError(f"Too many patterns: {len(patterns)} (max {MAX_PATTERNS})")
|
raise ValueError(f"Too many patterns: {len(patterns)} (max {MAX_PATTERNS})")
|
||||||
|
|
||||||
current = config.get("current", 20)
|
budget = config.get("budget", 50)
|
||||||
mode = LED_MODE_LOOKUP.get(config.get("mode", "rgbw"), 0x00)
|
|
||||||
active = config.get("active", 0) % len(patterns)
|
active = config.get("active", 0) % len(patterns)
|
||||||
|
playlist = config.get("playlist", [])
|
||||||
|
has_playlist = len(playlist) > 0
|
||||||
|
|
||||||
# Build header (without CRC)
|
if len(playlist) > MAX_PLAYLIST:
|
||||||
|
raise ValueError(f"Playlist too long: {len(playlist)} (max {MAX_PLAYLIST})")
|
||||||
|
|
||||||
|
# Build header
|
||||||
header = bytearray(HEADER_SIZE)
|
header = bytearray(HEADER_SIZE)
|
||||||
header[0:4] = XBLK_MAGIC
|
header[0:4] = XBLK_MAGIC
|
||||||
header[4] = XBLK_VERSION
|
header[4] = XBLK_VERSION
|
||||||
header[5] = len(patterns)
|
header[5] = 0x01 if has_playlist else 0x00 # flags
|
||||||
header[6] = active
|
header[6] = len(patterns)
|
||||||
header[7] = current & 0xFF
|
header[7] = active
|
||||||
header[8] = mode
|
header[8] = budget & 0xFF
|
||||||
# bytes 9-13 reserved
|
header[9] = len(playlist) & 0xFF
|
||||||
# bytes 14-15 CRC (filled below)
|
# bytes 10-13 reserved
|
||||||
|
|
||||||
|
# Compute CRC over header bytes 0-13
|
||||||
|
crc = crc16(bytes(header[:14]))
|
||||||
|
struct.pack_into(">H", header, 14, crc)
|
||||||
|
|
||||||
# Build pattern data
|
# Build pattern data
|
||||||
pat_data = b""
|
pat_data = b""
|
||||||
for pat in patterns:
|
for pat in patterns:
|
||||||
pat_data += encode_pattern(pat)
|
pat_data += encode_pattern(pat)
|
||||||
|
|
||||||
# Compute CRC over header (bytes 0-13) + all pattern data
|
# Build playlist data
|
||||||
crc = crc16(bytes(header[:14]) + pat_data)
|
playlist_data = bytes(playlist + [0] * (MAX_PLAYLIST - len(playlist)))
|
||||||
struct.pack_into(">H", header, 14, crc)
|
|
||||||
|
|
||||||
return bytes(header) + pat_data
|
result = bytes(header) + pat_data
|
||||||
|
if has_playlist:
|
||||||
|
result += playlist_data[:MAX_PLAYLIST]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Built-in patterns (matching src/pattern/mod.rs)
|
# Built-in patterns (matching src/pattern/mod.rs)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# LP5562 EngineCommand helpers (matching lp5562.rs encoding)
|
|
||||||
def ramp_wait(prescale_slow: bool, step_time: int, up: bool, increment: int) -> int:
|
|
||||||
prescale_bit = 0x4000 if prescale_slow else 0
|
|
||||||
sign_bit = 0 if up else 0x0080
|
|
||||||
return prescale_bit | ((step_time & 0x3F) << 8) | sign_bit | (increment & 0x7F)
|
|
||||||
|
|
||||||
def wait(prescale_slow: bool, step_time: int) -> int:
|
|
||||||
prescale_bit = 0x4000 if prescale_slow else 0
|
|
||||||
return prescale_bit | ((step_time & 0x3F) << 8)
|
|
||||||
|
|
||||||
def set_pwm(value: int) -> int:
|
|
||||||
return 0x4000 | value # Actually: 0x40xx format
|
|
||||||
# Wait, let me check the actual encoding...
|
|
||||||
|
|
||||||
def branch(step: int, loop_count: int) -> int:
|
|
||||||
return 0xA000 | ((loop_count & 0x3F) << 7) | (step & 0x7F)
|
|
||||||
|
|
||||||
def trigger(wait_mask: int, send_mask: int) -> int:
|
|
||||||
return 0xE000 | ((wait_mask & 0x07) << 7) | (send_mask & 0x07)
|
|
||||||
|
|
||||||
|
|
||||||
def builtin_patterns() -> dict:
|
def builtin_patterns() -> dict:
|
||||||
"""Return the 5 built-in patterns as a config dict."""
|
"""Return the 5 built-in patterns as a config dict."""
|
||||||
# Breathe: 1 engine, all RGB channels
|
|
||||||
breathe_cmds = [
|
|
||||||
ramp_wait(True, 1, True, 127), # 0→128
|
|
||||||
ramp_wait(True, 1, True, 127), # 128→255
|
|
||||||
ramp_wait(True, 1, False, 127), # 255→127
|
|
||||||
ramp_wait(True, 1, False, 127), # 127→0
|
|
||||||
wait(True, 48), # pause
|
|
||||||
branch(0, 0), # loop
|
|
||||||
]
|
|
||||||
|
|
||||||
# Heartbeat: 1 engine, double-pulse
|
|
||||||
heartbeat_cmds = [
|
|
||||||
0x40FF, # set_pwm(255)
|
|
||||||
wait(False, 20), # hold ~10ms
|
|
||||||
0x4000, # set_pwm(0)
|
|
||||||
wait(False, 40), # gap ~20ms
|
|
||||||
0x40FF, # set_pwm(255)
|
|
||||||
wait(False, 20), # hold ~10ms
|
|
||||||
0x4000, # set_pwm(0)
|
|
||||||
wait(True, 63), # rest ~1.0s
|
|
||||||
wait(True, 32), # rest ~0.5s
|
|
||||||
branch(0, 0),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Slow pulse: 1 engine, very gentle
|
|
||||||
slow_pulse_cmds = [
|
|
||||||
ramp_wait(True, 4, True, 127), # 0→128
|
|
||||||
ramp_wait(True, 4, True, 127), # 128→255
|
|
||||||
wait(True, 32), # hold
|
|
||||||
ramp_wait(True, 4, False, 127), # 255→127
|
|
||||||
ramp_wait(True, 4, False, 127), # 127→0
|
|
||||||
wait(True, 63), # pause
|
|
||||||
branch(0, 0),
|
|
||||||
]
|
|
||||||
|
|
||||||
# RGB cycle: 3 engines with trigger sync
|
|
||||||
rgb_e1 = [
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
trigger(0, 0b010), # send to E2
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
wait(True, 63),
|
|
||||||
branch(0, 0),
|
|
||||||
]
|
|
||||||
rgb_e2 = [
|
|
||||||
trigger(0b001, 0), # wait for E1
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
trigger(0, 0b100), # send to E3
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
wait(True, 32),
|
|
||||||
branch(0, 0),
|
|
||||||
]
|
|
||||||
rgb_e3 = [
|
|
||||||
trigger(0b010, 0), # wait for E2
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
wait(True, 32),
|
|
||||||
branch(0, 0),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Color wash: 3 engines, free-running with different periods
|
|
||||||
wash_e1 = [
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
branch(0, 0),
|
|
||||||
]
|
|
||||||
wash_e2 = [
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
wait(True, 32),
|
|
||||||
branch(0, 0),
|
|
||||||
]
|
|
||||||
wash_e3 = [
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
ramp_wait(True, 1, True, 127),
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
ramp_wait(True, 1, False, 127),
|
|
||||||
wait(True, 63),
|
|
||||||
branch(0, 0),
|
|
||||||
]
|
|
||||||
|
|
||||||
single_rgb_map = {"b": "engine1", "g": "engine1", "r": "engine1", "w": "direct"}
|
|
||||||
triple_map = {"b": "engine1", "g": "engine2", "r": "engine3", "w": "direct"}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"current": 20,
|
"budget": 50,
|
||||||
"mode": "rgbw",
|
|
||||||
"active": 0,
|
"active": 0,
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{"name": "breathe", "led_map": single_rgb_map, "engines": [breathe_cmds, [], []]},
|
{
|
||||||
{"name": "heartbeat", "led_map": single_rgb_map, "engines": [heartbeat_cmds, [], []]},
|
"name": "breathe",
|
||||||
{"name": "slow_pulse", "led_map": single_rgb_map, "engines": [slow_pulse_cmds, [], []]},
|
"waveform": "sine",
|
||||||
{"name": "rgb_cycle", "led_map": triple_map, "engines": [rgb_e1, rgb_e2, rgb_e3]},
|
"cycle_len": 125, # 2.5s
|
||||||
{"name": "color_wash", "led_map": triple_map, "engines": [wash_e1, wash_e2, wash_e3]},
|
"phase": [0, 0, 0, 0, 0, 0],
|
||||||
|
"envelope": [255, 255, 255, 255, 255, 255],
|
||||||
|
"repeat_count": 0xFF,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "heartbeat",
|
||||||
|
"waveform": "heartbeat",
|
||||||
|
"cycle_len": 80, # 1.6s
|
||||||
|
"phase": [0, 0, 0, 0, 0, 0],
|
||||||
|
"envelope": [255, 255, 255, 255, 255, 255],
|
||||||
|
"repeat_count": 0xFF,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wave_chase",
|
||||||
|
"waveform": "sine",
|
||||||
|
"cycle_len": 100, # 2s
|
||||||
|
"phase": [0, 43, 85, 128, 170, 213],
|
||||||
|
"envelope": [255, 255, 255, 255, 255, 255],
|
||||||
|
"repeat_count": 0xFF,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slow_pulse",
|
||||||
|
"waveform": "triangle",
|
||||||
|
"cycle_len": 250, # 5s
|
||||||
|
"phase": [0, 0, 0, 0, 0, 0],
|
||||||
|
"envelope": [200, 200, 200, 200, 200, 200],
|
||||||
|
"repeat_count": 0xFF,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "alternating_blink",
|
||||||
|
"waveform": "square",
|
||||||
|
"cycle_len": 50, # 1s
|
||||||
|
"phase": [0, 128, 0, 128, 0, 128],
|
||||||
|
"envelope": [255, 255, 255, 255, 255, 255],
|
||||||
|
"repeat_count": 0xFF,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def write_to_ntag5(data: bytes):
|
def write_to_ntag5(data: bytes):
|
||||||
"""Write binary data to NTAG5 EEPROM blocks 256+ via ntag5sensor."""
|
"""Write binary data to NTAG5 EEPROM blocks 256+ via ntag5sensor."""
|
||||||
# Add ntag5sensor to path
|
|
||||||
ntag5sensor_path = os.path.join(os.path.dirname(__file__), "..", "..", "ntag5sensor")
|
ntag5sensor_path = os.path.join(os.path.dirname(__file__), "..", "..", "ntag5sensor")
|
||||||
sys.path.insert(0, ntag5sensor_path)
|
sys.path.insert(0, ntag5sensor_path)
|
||||||
|
|
||||||
@@ -278,20 +191,16 @@ def write_to_ntag5(data: bytes):
|
|||||||
|
|
||||||
print(f"Writing {len(data)} bytes to EEPROM blocks {LIBRARY_BASE_BLOCK}-{LIBRARY_BASE_BLOCK + len(data)//4 - 1}")
|
print(f"Writing {len(data)} bytes to EEPROM blocks {LIBRARY_BASE_BLOCK}-{LIBRARY_BASE_BLOCK + len(data)//4 - 1}")
|
||||||
|
|
||||||
# Write in 4-byte blocks
|
|
||||||
for i in range(0, len(data), 4):
|
for i in range(0, len(data), 4):
|
||||||
block = LIBRARY_BASE_BLOCK + i // 4
|
block = LIBRARY_BASE_BLOCK + i // 4
|
||||||
chunk = data[i:i+4]
|
chunk = data[i:i+4]
|
||||||
if len(chunk) < 4:
|
if len(chunk) < 4:
|
||||||
chunk = chunk + b'\x00' * (4 - len(chunk))
|
chunk = chunk + b'\x00' * (4 - len(chunk))
|
||||||
|
|
||||||
# Use ISO15693 WRITE SINGLE BLOCK (unaddressed)
|
flags = ISO_FLAG_DATA_RATE | 0x08
|
||||||
# Block address needs protocol extension for blocks > 255
|
|
||||||
flags = ISO_FLAG_DATA_RATE | 0x08 # data rate + protocol extension
|
|
||||||
cmd = bytes([flags, 0x21]) + struct.pack("<H", block) + chunk
|
cmd = bytes([flags, 0x21]) + struct.pack("<H", block) + chunk
|
||||||
reader.transmit_iso15693(cmd, True)
|
reader.transmit_iso15693(cmd, True)
|
||||||
|
|
||||||
# EEPROM write cycle delay
|
|
||||||
import time
|
import time
|
||||||
time.sleep(0.006)
|
time.sleep(0.006)
|
||||||
|
|
||||||
@@ -302,11 +211,10 @@ def write_to_ntag5(data: bytes):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="XBLK pattern library serializer")
|
parser = argparse.ArgumentParser(description="XBLK v2 pattern library serializer")
|
||||||
parser.add_argument("--json", help="JSON pattern definition file")
|
parser.add_argument("--json", help="JSON pattern definition file")
|
||||||
parser.add_argument("--output", "-o", help="Output binary file")
|
parser.add_argument("--output", "-o", help="Output binary file")
|
||||||
parser.add_argument("--write", action="store_true", help="Write to NTAG5 via PCSC")
|
parser.add_argument("--write", action="store_true", help="Write to NTAG5 via PCSC")
|
||||||
parser.add_argument("--builtin", action="store_true", help="Use built-in patterns (default if no --json)")
|
|
||||||
parser.add_argument("--dump", action="store_true", help="Hex dump the binary")
|
parser.add_argument("--dump", action="store_true", help="Hex dump the binary")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user