Make IR real-time/non-blocking, harden for Pico W, optimize
Primary target is now the Pico W; functionality preserved on other boards. IR TX (RP2040/PWM): rewritten to a non-blocking, real-time symbol state machine. A hardware-PWM 56 kHz/40% carrier is gated by a one-shot k_timer (mark/space flips in the timer ISR), so TX never busy-waits and runs concurrently with IR RX, BLE and everything else. Queued frames (BLE "bomb") are sent back-to-back with a 50 ms inter-frame gap. Boards whose ir-tx is a plain GPIO keep the (blocking) software bit-bang carrier as a fallback. IR RX: cycle timestamps captured in the ISR, converted to us off the interrupt path in the decode work item (lower ISR jitter). Fixes: - Heartbeat moved off GPIO25 (CYW43 WL_CS on Pico W) to GPIO14 to avoid corrupting WiFi/BLE. - APP_BLE now selects BT_DEVICE_NAME_DYNAMIC (bt_set_name needs it). Optimizations: - RGB rewritten with integer math (no soft-float on the FPU-less M0+). - CONFIG_SIZE_OPTIMIZATIONS; 100 us tick so OLT symbol durations schedule exactly. Pico W: boards/rpi_pico_rp2040_w.conf enables BLE out of the box. Docs: resolved hardware TODOs (battery cell-count divider, PWM channel math, heartbeat, carrier), README primary target = Pico W, inline comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9149761c82
commit
f2de891ea3
@ -27,12 +27,12 @@ implementation.
|
||||
|---|---|---:|---|---|
|
||||
| Game buttons ×4 | main.py:371 | GPIO18/21/20/19, pull-up, active-low | `gpio-keys` node `buttons`, IRQ + workqueue debounce | Ported |
|
||||
| IR RX | main.py:200 | GPIO16, pull-up, active-low (TSOP4856) | alias `ir-rx`, GPIO IRQ + cycle timestamps | Ported |
|
||||
| IR TX | main.py:223 | GPIO17, active-high (PN2222A → IR LED) | alias `ir-tx`, software 56 kHz carrier | Ported (SW carrier) |
|
||||
| IR TX | main.py:223 | GPIO17, active-high (PN2222A → IR LED) | alias `ir-tx`, hardware PWM 56 kHz carrier (bit-bang fallback) | Ported |
|
||||
| Piezo | main.py:44 | GPIO15, active-high (NPN) | alias `piezo`, GPIO toggle | Ported |
|
||||
| RGB LED | lib/rgb.py:17-19 | GPIO22/26/27, PWM 100 Hz, active-high | `pwm-leds` aliases `rgb-red/green/blue` | Ported |
|
||||
| Battery sense | main.py:67 | GPIO28 / ADC ch2 | `zephyr_user` io-channel, `adc_dt_spec` | Ported |
|
||||
| Heartbeat LED | main.py:255 | onboard "LED" | alias `heartbeat-led` (GPIO25 on plain Pico) | Ported |
|
||||
| BLE UART | lib/ble_uart | radio | custom NUS GATT service (`CONFIG_APP_BLE`) | Ported (Pico W/ESP32 only) |
|
||||
| Heartbeat LED | main.py:255 | GPIO14 (external) | alias `heartbeat-led` | Ported — moved off GPIO25 (CYW43 WL_CS on Pico W) |
|
||||
| BLE UART | lib/ble_uart | radio | custom NUS GATT service (`CONFIG_APP_BLE`) | Ported — auto-on for Pico W |
|
||||
|
||||
## Module map
|
||||
|
||||
@ -59,7 +59,7 @@ implementation.
|
||||
| button → `olt_sent_ir` | button callback → IR TX request |
|
||||
| `ble_rx_handler` | NUS RX callback → parse → IR TX / RGB |
|
||||
| IR RX pin IRQ + timer | GPIO IRQ timestamps + frame-timeout `k_timer` → decode work |
|
||||
| `ir_tx.transmit` ISR/PIO | dedicated high-priority IR TX thread, `k_busy_wait` carrier |
|
||||
| `ir_tx.transmit` ISR/PIO | non-blocking `k_timer` symbol state machine + hardware PWM (RP2040); cooperative thread + bit-bang fallback |
|
||||
|
||||
## API mapping
|
||||
|
||||
@ -86,28 +86,49 @@ implementation.
|
||||
indexed into `{0,1,4,5,7,10,15,17,20,25,30,35,40,50,75,100}`; teams `RED/BLUE/YELLOW/GREEN`.
|
||||
- Commands: NewGame `0x8305E8`, Explode `0x830BE8`, TestShot `0x082400`, SensorTest `0x831500`.
|
||||
|
||||
## Open questions / TODO (hardware assumptions)
|
||||
## Resolved hardware questions (second pass)
|
||||
|
||||
- **Battery divider ratio.** main.py:87 divides the measured voltage by 2 before the
|
||||
alkaline capacity lookup, implying a 1:2 divider on VSYS/GPIO28. Confirm the real PCB
|
||||
divider and update `BATTERY_DIVIDER_*` in `app_config.h`. ADC reference is assumed 3.3 V.
|
||||
- **RP2040 PWM channel numbers.** The overlay assumes GPIO22/26/27 map to PWM channels
|
||||
6/10/11 (slice·2+chan). Verify against the board's `&pwm` once Zephyr is available.
|
||||
- **IR TX carrier.** Implemented as a software-bit-banged 56 kHz carrier for portability.
|
||||
Recommended upgrade: RP2040 PIO or ESP32 RMT (the MicroPython code used both). The
|
||||
software carrier monopolises a high-priority thread for ~30 ms per frame.
|
||||
- **IR TX duty.** 40 % duty (ir_tx/olt.py) is approximated by the SW carrier; not tunable
|
||||
without PWM/PIO.
|
||||
- **Heartbeat LED on Pico W.** The Pico W onboard LED is behind the CYW43 chip, not a plain
|
||||
GPIO. The overlay uses GPIO25 (valid on the non-W Pico). Adjust for Pico W if needed.
|
||||
- **ESP32 / STM32 pin maps.** The `esp32_devkitc_wroom` and `nucleo_f401re` overlays are
|
||||
stubs with TODO pin assignments — verify before use.
|
||||
- **BLE board support.** BLE requires a radio: build the Pico W or ESP32 target with
|
||||
`-DEXTRA_CONF_FILE=overlay-ble.conf`.
|
||||
- **Battery divider.** No external resistor divider: the 2-cell AA pack (~3 V, within the
|
||||
3.3 V ADC range) is sampled directly and divided by the cell count (2) in firmware to feed
|
||||
the single-cell alkaline model. `BATTERY_DIVIDER_NUM/DEN = 1/2` in `app_config.h` now
|
||||
documents this as a cell count, not a resistor ratio.
|
||||
- **RP2040 PWM channels.** Confirmed by the slice math (channel = slice·2 + A/B, slice =
|
||||
(gpio/2)%8): GPIO22→6, GPIO26→10, GPIO27→11. Documented in `rpi_pico.overlay`.
|
||||
- **Heartbeat LED / Pico W hazard.** GPIO25 is the CYW43439 SPI chip-select (WL_CS) on the
|
||||
Pico W — toggling it would corrupt WiFi/BLE. Heartbeat moved to GPIO14 (external LED), safe
|
||||
on every RP2040 variant. None of the other app pins (15-22, 26-28) collide with the CYW43
|
||||
pins (23/24/25/29).
|
||||
- **BLE on Pico W.** Enabled out of the box via `boards/rpi_pico_rp2040_w.conf`
|
||||
(`CONFIG_APP_BLE=y`). `bt_set_name()` needs `CONFIG_BT_DEVICE_NAME_DYNAMIC`, now selected by
|
||||
`APP_BLE` in `Kconfig`.
|
||||
|
||||
## Remaining / accepted limitations
|
||||
|
||||
- **IR TX carrier.** `src/ir_tx.c` selects a backend from the `ir-tx` devicetree node:
|
||||
- On RP2040 (primary) the node is a **hardware PWM** channel and TX is fully
|
||||
**non-blocking / real-time**: the 56 kHz / 40 % carrier comes from the PWM peripheral and
|
||||
a one-shot `k_timer` drives a symbol state machine. Each expiry (system-clock ISR) just
|
||||
flips the duty (40 %↔0 %) and arms the next symbol — the CPU is free between symbols, so
|
||||
TX never busy-waits and runs concurrently with IR RX and BLE. Needs
|
||||
`CONFIG_SYS_CLOCK_TICKS_PER_SEC=10000` (100 µs) so the symbol durations (all multiples of
|
||||
100 µs) schedule exactly. Assumes the PWM `set_pulse` path is ISR-safe (true for the
|
||||
RP2040 driver, which only writes registers).
|
||||
- On boards whose `ir-tx` is a plain **GPIO** (the ESP32/STM32 stubs) a software bit-banged
|
||||
carrier on a cooperative thread is used as a portable fallback; this one is inherently
|
||||
blocking (it toggles the pin at 56 kHz during marks).
|
||||
- Queued frames (e.g. the BLE "bomb" burst) are sent back-to-back with a 50 ms inter-frame
|
||||
gap so the receiver can separate them. A full hardware offload (RP2040 PIO+DMA / ESP32
|
||||
RMT) remains the optional last step for the fallback boards.
|
||||
- **ESP32 / STM32 pin maps.** The `esp32_devkitc_wroom` and `nucleo_f401re` overlays remain
|
||||
stubs with placeholder pins (RGB-PWM/ADC omitted, modules degrade to warnings) — verify
|
||||
before use. ESP32 BLE/IR can later use the native controller + RMT.
|
||||
|
||||
## Build status
|
||||
|
||||
`west` / the Zephyr SDK is **not installed in the current environment**, so the application
|
||||
has not been compiled here. The source, overlays, `prj.conf`, `Kconfig`, and `CMakeLists.txt`
|
||||
follow Zephyr conventions and are intended to build per `zephyr_app/README.md`. Record
|
||||
`west build` results here once a Zephyr environment is available.
|
||||
`west` / the Zephyr SDK is **not installed in this environment**, so the firmware has not been
|
||||
compiled here. The protocol layer (`olt_proto.c`) is verified by the host unit test
|
||||
(`tests/olt_proto_test.c` — passing). Source, overlays, `prj.conf`, `Kconfig`, board `.conf`
|
||||
files, and `CMakeLists.txt` follow Zephyr conventions and are intended to build per
|
||||
`zephyr_app/README.md`. Primary target: **Pico W** (`rpi_pico/rp2040/w`). Record `west build`
|
||||
results here once a Zephyr environment is available; the likeliest first fixups are the
|
||||
RP2040 PWM pinctrl tokens and the Pico W BT controller availability in the chosen Zephyr tree.
|
||||
|
||||
@ -10,6 +10,7 @@ config APP_BLE
|
||||
select BT
|
||||
select BT_PERIPHERAL
|
||||
select BT_GATT_DYNAMIC_DB
|
||||
select BT_DEVICE_NAME_DYNAMIC
|
||||
help
|
||||
Enables the Nordic UART Service used for wireless command/telemetry
|
||||
(src/ble_nus.c). Requires a board with a Bluetooth controller, e.g.
|
||||
|
||||
@ -43,23 +43,36 @@ source ~/zephyrproject/zephyr/zephyr-env.sh
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root (after `west` and `ZEPHYR_BASE` are available):
|
||||
From the repository root (after `west` and `ZEPHYR_BASE` are available).
|
||||
|
||||
### Primary target — Raspberry Pi Pico W
|
||||
|
||||
BLE is enabled automatically by `boards/rpi_pico_rp2040_w.conf`:
|
||||
|
||||
```bash
|
||||
# Primary target (no BLE — the plain Pico has no radio)
|
||||
west build -b rpi_pico zephyr_app
|
||||
west build -b rpi_pico/rp2040/w zephyr_app
|
||||
|
||||
# Clean rebuild
|
||||
west build -p always -b rpi_pico zephyr_app
|
||||
west build -p always -b rpi_pico/rp2040/w zephyr_app
|
||||
```
|
||||
|
||||
### With BLE (radio-capable boards)
|
||||
> A working BLE link also needs the chosen Zephyr tree to provide the CYW43
|
||||
> Bluetooth HCI controller for this board. If it doesn't, build the plain
|
||||
> `rpi_pico` target below (everything except BLE works).
|
||||
|
||||
### Plain Pico (no radio)
|
||||
|
||||
```bash
|
||||
# Raspberry Pi Pico W
|
||||
west build -b rpi_pico/rp2040/w zephyr_app -- -DEXTRA_CONF_FILE=overlay-ble.conf
|
||||
west build -b rpi_pico zephyr_app
|
||||
```
|
||||
|
||||
# ESP32 DevKitC
|
||||
The heartbeat LED is on GPIO14 (external) so the same overlay is safe on both
|
||||
the plain Pico and the Pico W — see the note in `boards/rpi_pico.overlay` if you
|
||||
prefer the plain Pico's onboard GPIO25 LED.
|
||||
|
||||
### ESP32 with BLE
|
||||
|
||||
```bash
|
||||
west build -b esp32_devkitc_wroom/esp32/procpu zephyr_app -- -DEXTRA_CONF_FILE=overlay-ble.conf
|
||||
```
|
||||
|
||||
@ -91,12 +104,19 @@ west flash
|
||||
|
||||
## Notes / known limitations
|
||||
|
||||
- **IR carrier is software-generated** (bit-banged 56 kHz, ~39% duty) for
|
||||
portability. Recommended upgrade: RP2040 PIO or ESP32 RMT. See
|
||||
migration-notes.md.
|
||||
- **Heartbeat LED** uses GPIO25 (valid on the plain Pico). The Pico W onboard
|
||||
LED is behind the CYW43 chip — adjust the overlay if you need it there.
|
||||
- **Battery divider ratio** and a few **PWM/ADC channel numbers** are
|
||||
assumptions flagged as TODO in migration-notes.md; confirm against hardware.
|
||||
- **IR TX is non-blocking on RP2040**: a hardware-PWM 56 kHz / 40 % carrier is
|
||||
gated by a one-shot `k_timer` state machine, so transmission never busy-waits
|
||||
and runs concurrently with IR RX, BLE and everything else. Boards whose
|
||||
`ir-tx` is a plain GPIO fall back to a (blocking) software bit-banged carrier.
|
||||
Full offload (PIO+DMA / RMT) is the optional next step. See migration-notes.md.
|
||||
- **IR RX is non-blocking too**: edges are timestamped in a GPIO ISR, a frame
|
||||
timeout closes the packet and decoding runs in a work item — so TX and RX
|
||||
operate at the same time without blocking each other.
|
||||
- **Heartbeat LED is on GPIO14** (external), deliberately *not* GPIO25 — that
|
||||
pin is the CYW43 chip-select on the Pico W and must not be toggled.
|
||||
- **No floating point**: all app math is integer (RGB duty, battery, timing) so
|
||||
nothing soft-float is linked on the FPU-less Cortex-M0+.
|
||||
- Board-specific pins live in `boards/*.overlay`; behaviour lives in `src/`;
|
||||
tunables (timings, command table) live in `src/app_config.h`.
|
||||
- The ESP32 and STM32 overlays are **stubs** with placeholder pins — verify
|
||||
before use (migration-notes.md).
|
||||
|
||||
@ -34,6 +34,11 @@
|
||||
label = "Piezo";
|
||||
};
|
||||
|
||||
/*
|
||||
* IR TX as a plain GPIO -> src/ir_tx.c uses its software bit-bang
|
||||
* carrier fallback here. For a precise carrier, convert this to an
|
||||
* LEDC PWM node (like rpi_pico) or use the ESP32 RMT peripheral.
|
||||
*/
|
||||
ir_tx_out: ir_tx_out {
|
||||
gpios = <&gpio0 23 GPIO_ACTIVE_HIGH>; /* main.py ESP32 IR TX */
|
||||
label = "IR TX";
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* Pin assignments are ported directly from the MicroPython firmware:
|
||||
* - buttons GPIO18/19/20/21 (main.py:371, input pull-up, active-low)
|
||||
* - IR RX GPIO16 (main.py:200, TSOP4856, pull-up, active-low)
|
||||
* - IR TX GPIO17 (main.py:223, gates IR LED via PN2222A)
|
||||
* - IR TX GPIO17 (main.py:223, PWM 56 kHz carrier, PN2222A)
|
||||
* - piezo GPIO15 (main.py:44, NPN gate, active-high)
|
||||
* - RGB R/G/B GPIO22/26/27 (lib/rgb.py:17-19, PWM, active-high)
|
||||
* - battery GPIO28 / ADC2 (main.py:67, 2xAA via divider)
|
||||
@ -20,12 +20,9 @@
|
||||
|
||||
/ {
|
||||
aliases {
|
||||
/* Heartbeat LED. On the plain Pico this is GPIO25; on the Pico W
|
||||
* the onboard LED is behind the CYW43 chip, so we keep a GPIO
|
||||
* alias for boards that expose one and fall back gracefully. */
|
||||
heartbeat-led = &heartbeat_led;
|
||||
piezo = &piezo_out;
|
||||
ir-tx = &ir_tx_out;
|
||||
ir-tx = &ir_tx_pwm;
|
||||
ir-rx = &ir_rx_in;
|
||||
rgb-red = &rgb_red;
|
||||
rgb-green = &rgb_green;
|
||||
@ -36,8 +33,19 @@
|
||||
* The following four pins are driven directly with the GPIO API, so they
|
||||
* are bare nodes exposing a `gpios` property (no led/key driver binding).
|
||||
*/
|
||||
|
||||
/*
|
||||
* Heartbeat LED on GPIO14 (external LED + resistor to GND).
|
||||
*
|
||||
* IMPORTANT: the plain Pico's *onboard* LED is GPIO25, but on the
|
||||
* Pico W (the primary target) GPIO25 is the CYW43439 SPI chip-select
|
||||
* (WL_CS). Driving it would corrupt WiFi/BLE traffic, so we use a
|
||||
* neutral external pin that is safe on every RP2040 variant.
|
||||
* Plain-Pico users who prefer the onboard LED can change this to
|
||||
* <&gpio0 25 GPIO_ACTIVE_HIGH>.
|
||||
*/
|
||||
heartbeat_led: heartbeat_led {
|
||||
gpios = <&gpio0 25 GPIO_ACTIVE_HIGH>;
|
||||
gpios = <&gpio0 14 GPIO_ACTIVE_HIGH>;
|
||||
label = "Heartbeat LED";
|
||||
};
|
||||
|
||||
@ -47,17 +55,6 @@
|
||||
label = "Piezo";
|
||||
};
|
||||
|
||||
/*
|
||||
* IR transmitter LED gate. The carrier (56 kHz) is generated in
|
||||
* software by bit-banging this GPIO during marks (see src/ir_tx.c).
|
||||
* This keeps the port board-portable; the recommended hardware upgrade
|
||||
* is RP2040 PIO / ESP32 RMT (documented in migration-notes.md).
|
||||
*/
|
||||
ir_tx_out: ir_tx_out {
|
||||
gpios = <&gpio0 17 GPIO_ACTIVE_HIGH>;
|
||||
label = "IR TX";
|
||||
};
|
||||
|
||||
/* IR receiver (demodulated output of TSOP4856), active-low, pull-up. */
|
||||
ir_rx_in: ir_rx_in {
|
||||
gpios = <&gpio0 16 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
@ -86,46 +83,76 @@
|
||||
};
|
||||
};
|
||||
|
||||
/* RGB LED driven by hardware PWM at 100 Hz (lib/rgb.py). */
|
||||
/*
|
||||
* RGB LED driven by hardware PWM at 100 Hz (lib/rgb.py).
|
||||
*
|
||||
* RP2040 PWM channel number = slice * 2 + (A:0 / B:1), where
|
||||
* slice = (gpio / 2) % 8 and A/B follows the pin's parity:
|
||||
* GPIO22 -> slice3, A -> 3*2+0 = 6
|
||||
* GPIO26 -> slice5, A -> 5*2+0 = 10
|
||||
* GPIO27 -> slice5, B -> 5*2+1 = 11
|
||||
* The matching carrier pins are routed in &pinctrl below.
|
||||
*/
|
||||
rgb_leds: pwmleds {
|
||||
compatible = "pwm-leds";
|
||||
rgb_red: rgb_red {
|
||||
/* GPIO22 = PWM slice3 chan A -> channel 6 */
|
||||
pwms = <&pwm 6 PWM_HZ(100) PWM_POLARITY_NORMAL>;
|
||||
label = "RGB Red";
|
||||
};
|
||||
rgb_green: rgb_green {
|
||||
/* GPIO26 = PWM slice5 chan A -> channel 10 */
|
||||
pwms = <&pwm 10 PWM_HZ(100) PWM_POLARITY_NORMAL>;
|
||||
label = "RGB Green";
|
||||
};
|
||||
rgb_blue: rgb_blue {
|
||||
/* GPIO27 = PWM slice5 chan B -> channel 11 */
|
||||
pwms = <&pwm 11 PWM_HZ(100) PWM_POLARITY_NORMAL>;
|
||||
label = "RGB Blue";
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* IR transmitter carrier. GPIO17 gates the IR LED (via PN2222A). The
|
||||
* 56 kHz / 40 % carrier is produced by hardware PWM; src/ir_tx.c flips
|
||||
* the duty between 40 % (mark) and 0 % (space). Channel math as above:
|
||||
* GPIO17 -> slice0, B -> 0*2+1 = 1
|
||||
*/
|
||||
ir_carrier: ir_carrier {
|
||||
compatible = "pwm-leds";
|
||||
ir_tx_pwm: ir_tx_pwm {
|
||||
pwms = <&pwm 1 PWM_HZ(56000) PWM_POLARITY_NORMAL>;
|
||||
label = "IR TX carrier";
|
||||
};
|
||||
};
|
||||
|
||||
/* Battery sense channel for ADC_DT_SPEC_GET (src/battery.c). */
|
||||
zephyr_user: zephyr_user {
|
||||
io-channels = <&adc 2>;
|
||||
};
|
||||
};
|
||||
|
||||
/* PWM pin routing for the RGB LED outputs (GPIO22/26/27). */
|
||||
/*
|
||||
* PWM pin routing: RGB LED outputs (GPIO22/26/27) plus the IR TX carrier
|
||||
* (GPIO17). All four are channels of the single RP2040 &pwm controller.
|
||||
*/
|
||||
&pinctrl {
|
||||
pwm_rgb_default: pwm_rgb_default {
|
||||
pwm_default: pwm_default {
|
||||
group1 {
|
||||
pinmux = <PWM_3A_P22>, <PWM_5A_P26>, <PWM_5B_P27>;
|
||||
pinmux = <PWM_3A_P22>, <PWM_5A_P26>, <PWM_5B_P27>,
|
||||
<PWM_0B_P17>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
&pwm {
|
||||
status = "okay";
|
||||
pinctrl-0 = <&pwm_rgb_default>;
|
||||
pinctrl-0 = <&pwm_default>;
|
||||
pinctrl-names = "default";
|
||||
divider-int-0 = <255>;
|
||||
/*
|
||||
* No manual clock dividers: the RP2040 PWM driver derives each slice's
|
||||
* divider from the requested period, so slice 0 (IR, 56 kHz) and slices
|
||||
* 3/5 (RGB, 100 Hz) each get an appropriate divider automatically. If a
|
||||
* future Zephyr requires it, the low 100 Hz RGB slices can be pinned
|
||||
* with divider-int-3 / divider-int-5.
|
||||
*/
|
||||
};
|
||||
|
||||
&adc {
|
||||
|
||||
16
zephyr_app/boards/rpi_pico_rp2040_w.conf
Normal file
16
zephyr_app/boards/rpi_pico_rp2040_w.conf
Normal file
@ -0,0 +1,16 @@
|
||||
# Board-specific config, auto-merged when building for the Raspberry Pi Pico W
|
||||
# (the primary target). The Pico W carries a CYW43439 with a Bluetooth
|
||||
# controller, so BLE is enabled out of the box:
|
||||
#
|
||||
# west build -b rpi_pico/rp2040/w zephyr_app
|
||||
#
|
||||
# NOTE: a working BLE link additionally requires that the chosen Zephyr tree
|
||||
# provides the CYW43 Bluetooth HCI controller for this board. If your Zephyr
|
||||
# version does not, build the plain board instead (west build -b rpi_pico
|
||||
# zephyr_app), which omits BLE.
|
||||
|
||||
CONFIG_APP_BLE=y
|
||||
|
||||
CONFIG_BT_DEVICE_NAME="OpenLaserTag"
|
||||
CONFIG_BT_DEVICE_NAME_MAX=32
|
||||
CONFIG_BT_DEVICE_APPEARANCE=128
|
||||
@ -1,16 +1,17 @@
|
||||
# Extra config fragment to enable the BLE Nordic UART service.
|
||||
# Use on radio-capable boards only (Pico W, ESP32):
|
||||
# west build -b rpi_pico/rp2040/w zephyr_app -- -DEXTRA_CONF_FILE=overlay-ble.conf
|
||||
# west build -b esp32_devkitc_wroom/esp32/procpu zephyr_app -- -DEXTRA_CONF_FILE=overlay-ble.conf
|
||||
# Extra config fragment to enable the BLE Nordic UART service on a
|
||||
# radio-capable board that is NOT auto-configured (e.g. ESP32):
|
||||
# west build -b esp32_devkitc_wroom/esp32/procpu zephyr_app -- \
|
||||
# -DEXTRA_CONF_FILE=overlay-ble.conf
|
||||
#
|
||||
# The Pico W enables BLE automatically via boards/rpi_pico_rp2040_w.conf, so it
|
||||
# does NOT need this file.
|
||||
#
|
||||
# CONFIG_APP_BLE selects BT, BT_PERIPHERAL, BT_GATT_DYNAMIC_DB and
|
||||
# BT_DEVICE_NAME_DYNAMIC (see Kconfig); only the tunables below remain.
|
||||
|
||||
CONFIG_APP_BLE=y
|
||||
|
||||
CONFIG_BT=y
|
||||
CONFIG_BT_PERIPHERAL=y
|
||||
# Advertised/default name (overridden at runtime with the device id).
|
||||
CONFIG_BT_DEVICE_NAME="OpenLaserTag"
|
||||
CONFIG_BT_DEVICE_NAME_MAX=32
|
||||
CONFIG_BT_DEVICE_APPEARANCE=128
|
||||
CONFIG_BT_GATT_DYNAMIC_DB=y
|
||||
|
||||
# Nordic UART payloads can reach the ATT MTU; give the RX buffer room.
|
||||
CONFIG_BT_BUF_ACL_RX_SIZE=27
|
||||
CONFIG_BT_L2CAP_TX_MTU=23
|
||||
|
||||
@ -23,5 +23,15 @@ CONFIG_MAIN_STACK_SIZE=4096
|
||||
# Cycle counter is used for microsecond IR edge timestamps.
|
||||
CONFIG_TIMING_FUNCTIONS=y
|
||||
|
||||
# 100 us tick so the non-blocking IR TX state machine can schedule symbols
|
||||
# accurately (all OLT mark/space durations are multiples of 100 us, so they map
|
||||
# to an exact whole number of ticks; see src/ir_tx.c). Tickless kernel keeps the
|
||||
# overhead low - the rate only sets the timeout granularity.
|
||||
CONFIG_SYS_CLOCK_TICKS_PER_SEC=10000
|
||||
|
||||
# Hardware unique id -> device id string (replaces machine.unique_id()).
|
||||
CONFIG_HWINFO=y
|
||||
|
||||
# Optimize for size: the RP2040 has limited flash and no FPU. All app math is
|
||||
# integer, so no floating-point support is pulled in.
|
||||
CONFIG_SIZE_OPTIMIZATIONS=y
|
||||
|
||||
@ -39,12 +39,16 @@
|
||||
/* ---- Battery monitor (main.py:74-108) -------------------------------- */
|
||||
|
||||
#define BATTERY_PERIOD_MS 10000U
|
||||
/* ADC full-scale reference, volts. Pico VSYS sense is on a divider. */
|
||||
/* ADC full-scale reference in millivolts (RP2040 ADC, 3.3 V rail). */
|
||||
#define BATTERY_VREF_MV 3300
|
||||
/*
|
||||
* The MicroPython code computes capacity from voltage/2 (main.py:87),
|
||||
* i.e. it assumes a 1:2 divider so the measured rail maps to one alkaline
|
||||
* cell. TODO: confirm the real divider ratio on the PCB (see migration-notes).
|
||||
* The 2-cell AA pack (~3 V) is sampled directly on the ADC pin and the
|
||||
* per-cell voltage is derived in firmware by dividing by the cell count
|
||||
* (main.py:87 feeds voltage/2 to a single-cell alkaline capacity model).
|
||||
* So this is the number of series cells, not a resistor-divider ratio:
|
||||
* cell_mv = measured_mv * NUM / DEN
|
||||
* Fresh 2xAA ~3.2 V stays within the 3.3 V ADC range. If you add a resistor
|
||||
* divider in front of the ADC, fold its ratio into these two constants.
|
||||
*/
|
||||
#define BATTERY_DIVIDER_NUM 1
|
||||
#define BATTERY_DIVIDER_DEN 2
|
||||
|
||||
@ -34,10 +34,12 @@ LOG_MODULE_REGISTER(ir_rx, LOG_LEVEL_INF);
|
||||
static const struct gpio_dt_spec ir_rx = GPIO_DT_SPEC_GET(IR_RX_NODE, gpios);
|
||||
static struct gpio_callback ir_rx_cb_data;
|
||||
|
||||
static uint32_t edge_times[IR_RX_MAX_EDGES];
|
||||
/* Raw cycle timestamps are captured in the ISR (cheap); the conversion to
|
||||
* microseconds happens off the interrupt path, in decode_work_fn(). */
|
||||
static uint32_t edge_cycles[IR_RX_MAX_EDGES];
|
||||
static volatile uint32_t edge_count;
|
||||
|
||||
static uint32_t decode_times[IR_RX_MAX_EDGES];
|
||||
static uint32_t decode_cycles[IR_RX_MAX_EDGES];
|
||||
static uint32_t decode_count;
|
||||
|
||||
static ir_rx_cb_t user_cb;
|
||||
@ -46,8 +48,17 @@ static void decode_work_fn(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
|
||||
/* Convert captured cycles to absolute microseconds (relative to the
|
||||
* first edge) for the timing-based decoder. 32-bit deltas are safe: a
|
||||
* frame is < ~50 ms, far below the counter wrap at any clock rate. */
|
||||
uint32_t us_times[IR_RX_MAX_EDGES];
|
||||
for (uint32_t i = 0; i < decode_count; i++) {
|
||||
us_times[i] = k_cyc_to_us_floor32(decode_cycles[i] -
|
||||
decode_cycles[0]);
|
||||
}
|
||||
|
||||
struct olt_frame f;
|
||||
if (olt_decode(decode_times, decode_count, &f)) {
|
||||
if (olt_decode(us_times, decode_count, &f)) {
|
||||
LOG_DBG("RX 0x%06X %s", f.packet,
|
||||
f.is_command ? "command" : "shot");
|
||||
if (user_cb) {
|
||||
@ -68,7 +79,7 @@ static void frame_timer_fn(struct k_timer *t)
|
||||
if (decode_count > IR_RX_MAX_EDGES) {
|
||||
decode_count = IR_RX_MAX_EDGES;
|
||||
}
|
||||
memcpy(decode_times, edge_times, decode_count * sizeof(edge_times[0]));
|
||||
memcpy(decode_cycles, edge_cycles, decode_count * sizeof(edge_cycles[0]));
|
||||
edge_count = 0; /* ready for the next frame */
|
||||
k_work_submit(&decode_work);
|
||||
}
|
||||
@ -81,13 +92,13 @@ static void ir_rx_isr(const struct device *port, struct gpio_callback *cb,
|
||||
ARG_UNUSED(cb);
|
||||
ARG_UNUSED(pins);
|
||||
|
||||
uint32_t now = k_cyc_to_us_floor32(k_cycle_get_32());
|
||||
uint32_t now = k_cycle_get_32();
|
||||
|
||||
if (edge_count == 0) {
|
||||
k_timer_start(&frame_timer, K_MSEC(IR_RX_FRAME_MS), K_NO_WAIT);
|
||||
}
|
||||
if (edge_count < IR_RX_MAX_EDGES) {
|
||||
edge_times[edge_count++] = now;
|
||||
edge_cycles[edge_count++] = now;
|
||||
}
|
||||
}
|
||||
#endif /* IR_RX_PRESENT */
|
||||
|
||||
@ -1,17 +1,27 @@
|
||||
/*
|
||||
* ir_tx.c - OpenLaserTag IR transmitter with a software carrier.
|
||||
* ir_tx.c - OpenLaserTag IR transmitter.
|
||||
*
|
||||
* The original firmware used RP2040 PIO / ESP32 RMT to gate a hardware 56 kHz
|
||||
* carrier (lib/olt_lib/ir_tx). For a board-portable first pass we generate the
|
||||
* carrier in software: a dedicated cooperative thread toggles the IR-TX GPIO
|
||||
* at ~56 kHz during "mark" symbols and holds it low during "space" symbols.
|
||||
* Two carrier backends are selected automatically from the `ir-tx` devicetree
|
||||
* alias:
|
||||
*
|
||||
* Trade-off (see migration-notes.md): this monopolises a high-priority thread
|
||||
* for the duration of a frame (~30 ms) and the carrier frequency/duty are
|
||||
* approximate. The recommended upgrade is PWM-gated PIO/RMT.
|
||||
* - PWM (node has a `pwms` property, e.g. rpi_pico - the primary path):
|
||||
* fully NON-BLOCKING and real-time. The 56 kHz / 40 % carrier is produced
|
||||
* by hardware PWM; a one-shot k_timer drives a symbol state machine. Each
|
||||
* timer expiry (in the system-clock ISR) just flips the PWM duty between
|
||||
* 40 % (mark) and 0 % (space) and arms the next symbol. The CPU is free
|
||||
* between symbols, so TX runs concurrently with IR RX and everything else
|
||||
* and never busy-waits.
|
||||
*
|
||||
* - GPIO (node has a `gpios` property, e.g. the ESP32/STM32 stubs): a
|
||||
* software bit-banged carrier on a dedicated cooperative thread. This is
|
||||
* inherently blocking (it must toggle the pin at 56 kHz during marks) and
|
||||
* is only a portable fallback for boards whose PWM is not configured yet.
|
||||
*
|
||||
* Multiple queued frames (e.g. the BLE "bomb" burst) are transmitted
|
||||
* back-to-back with an inter-frame gap so the receiver can separate them.
|
||||
* The original used PIO/RMT (lib/olt_lib/ir_tx).
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include "app_config.h"
|
||||
@ -22,14 +32,15 @@ LOG_MODULE_REGISTER(ir_tx, LOG_LEVEL_INF);
|
||||
|
||||
#define IR_TX_NODE DT_ALIAS(ir_tx)
|
||||
|
||||
/*
|
||||
* Carrier approximation. Target period is 1e6/56000 = 17.86 us. With 1 us
|
||||
* busy-wait granularity we use 7 us on + 11 us off = 18 us (~55.6 kHz, ~39%
|
||||
* duty), which the 56 kHz TSOP demodulator tolerates.
|
||||
*/
|
||||
#define CARRIER_ON_US 7U
|
||||
#define CARRIER_OFF_US 11U
|
||||
#define CARRIER_PERIOD_US (CARRIER_ON_US + CARRIER_OFF_US)
|
||||
#if DT_NODE_HAS_STATUS(IR_TX_NODE, okay) && DT_NODE_HAS_PROP(IR_TX_NODE, pwms)
|
||||
#define IR_TX_PWM 1
|
||||
#elif DT_NODE_HAS_STATUS(IR_TX_NODE, okay) && DT_NODE_HAS_PROP(IR_TX_NODE, gpios)
|
||||
#define IR_TX_GPIO 1
|
||||
#endif
|
||||
|
||||
/* Quiet time between consecutive frames so the RX frame-timeout (~47 ms) closes
|
||||
* the previous frame before the next one starts. */
|
||||
#define IR_TX_GAP_MS 50
|
||||
|
||||
struct ir_tx_req {
|
||||
uint8_t tx1;
|
||||
@ -37,13 +48,136 @@ struct ir_tx_req {
|
||||
uint8_t tx3;
|
||||
};
|
||||
|
||||
/* Pending frames queued from any context (button/BLE/main). */
|
||||
K_MSGQ_DEFINE(ir_tx_q, sizeof(struct ir_tx_req), 8, 4);
|
||||
|
||||
#if DT_NODE_HAS_STATUS(IR_TX_NODE, okay)
|
||||
#define IR_TX_PRESENT 1
|
||||
/* ================================================================== */
|
||||
/* PWM backend: non-blocking timer-driven symbol state machine */
|
||||
/* ================================================================== */
|
||||
#if defined(IR_TX_PWM)
|
||||
#include <zephyr/drivers/pwm.h>
|
||||
|
||||
static const struct pwm_dt_spec carrier = PWM_DT_SPEC_GET(IR_TX_NODE);
|
||||
static uint32_t mark_pulse; /* PWM pulse width (ns) for a "mark" (40 % duty) */
|
||||
|
||||
enum tx_state { TX_IDLE, TX_FRAME, TX_GAP };
|
||||
static volatile enum tx_state state = TX_IDLE;
|
||||
static struct k_spinlock lock;
|
||||
|
||||
static uint16_t sym[OLT_MAX_SYMBOLS];
|
||||
static size_t sym_n;
|
||||
static size_t sym_idx;
|
||||
static struct k_timer tx_timer;
|
||||
|
||||
/* Even symbol index = mark (carrier on), odd = space (carrier off). */
|
||||
static inline void carrier_apply(size_t idx)
|
||||
{
|
||||
(void)pwm_set_pulse_dt(&carrier, (idx & 1U) ? 0U : mark_pulse);
|
||||
}
|
||||
|
||||
static inline void carrier_idle(void)
|
||||
{
|
||||
(void)pwm_set_pulse_dt(&carrier, 0);
|
||||
}
|
||||
|
||||
/* Pop the next queued frame into sym[]. Returns true if one was loaded. */
|
||||
static bool load_next(void)
|
||||
{
|
||||
struct ir_tx_req r;
|
||||
|
||||
if (k_msgq_get(&ir_tx_q, &r, K_NO_WAIT) != 0) {
|
||||
return false;
|
||||
}
|
||||
sym_n = olt_encode(r.tx1, r.tx2, r.tx3, sym);
|
||||
sym_idx = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Apply the current symbol and arm the timer for its duration. */
|
||||
static void apply_and_arm(void)
|
||||
{
|
||||
carrier_apply(sym_idx);
|
||||
k_timer_start(&tx_timer, K_USEC(sym[sym_idx]), K_NO_WAIT);
|
||||
}
|
||||
|
||||
/* Runs in the system-clock ISR at each symbol/gap boundary. */
|
||||
static void tx_timer_fn(struct k_timer *t)
|
||||
{
|
||||
ARG_UNUSED(t);
|
||||
|
||||
switch (state) {
|
||||
case TX_FRAME:
|
||||
if (++sym_idx < sym_n) {
|
||||
apply_and_arm();
|
||||
break;
|
||||
}
|
||||
/* Frame finished. */
|
||||
carrier_idle();
|
||||
if (k_msgq_num_used_get(&ir_tx_q) > 0) {
|
||||
state = TX_GAP;
|
||||
k_timer_start(&tx_timer, K_MSEC(IR_TX_GAP_MS), K_NO_WAIT);
|
||||
} else {
|
||||
state = TX_IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
case TX_GAP:
|
||||
if (load_next()) {
|
||||
state = TX_FRAME;
|
||||
apply_and_arm();
|
||||
} else {
|
||||
state = TX_IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Kick the state machine if it is idle (called from thread context). */
|
||||
static void ir_tx_kick(void)
|
||||
{
|
||||
k_spinlock_key_t key = k_spin_lock(&lock);
|
||||
|
||||
if (state == TX_IDLE && load_next()) {
|
||||
state = TX_FRAME;
|
||||
apply_and_arm();
|
||||
}
|
||||
k_spin_unlock(&lock, key);
|
||||
}
|
||||
|
||||
static int carrier_init(void)
|
||||
{
|
||||
if (!pwm_is_ready_dt(&carrier)) {
|
||||
LOG_ERR("IR TX PWM not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
mark_pulse = (uint32_t)(((uint64_t)carrier.period *
|
||||
OLT_CARRIER_DUTY_PCT) / 100U);
|
||||
k_timer_init(&tx_timer, tx_timer_fn, NULL);
|
||||
carrier_idle();
|
||||
LOG_INF("IR TX initialized (non-blocking hardware PWM carrier)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* GPIO backend: software bit-bang fallback (blocking, on a thread) */
|
||||
/* ================================================================== */
|
||||
#elif defined(IR_TX_GPIO)
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
|
||||
static const struct gpio_dt_spec ir_tx = GPIO_DT_SPEC_GET(IR_TX_NODE, gpios);
|
||||
|
||||
/* Emit carrier for `us` microseconds (gpio active = LED on). */
|
||||
/*
|
||||
* Carrier approximation. Target period 1e6/56000 = 17.86 us; with 1 us
|
||||
* busy-wait granularity we use 7 us on + 11 us off = 18 us (~55.6 kHz, ~39 %
|
||||
* duty), which the 56 kHz TSOP demodulator tolerates.
|
||||
*/
|
||||
#define CARRIER_ON_US 7U
|
||||
#define CARRIER_OFF_US 11U
|
||||
#define CARRIER_PERIOD_US (CARRIER_ON_US + CARRIER_OFF_US)
|
||||
|
||||
static void mark(uint32_t us)
|
||||
{
|
||||
uint32_t periods = (us + CARRIER_PERIOD_US / 2) / CARRIER_PERIOD_US;
|
||||
@ -56,7 +190,6 @@ static void mark(uint32_t us)
|
||||
}
|
||||
}
|
||||
|
||||
/* Hold the carrier off for `us` microseconds. */
|
||||
static void space(uint32_t us)
|
||||
{
|
||||
gpio_pin_set_dt(&ir_tx, 0);
|
||||
@ -68,7 +201,6 @@ static void transmit(const struct ir_tx_req *req)
|
||||
uint16_t sym[OLT_MAX_SYMBOLS];
|
||||
size_t n = olt_encode(req->tx1, req->tx2, req->tx3, sym);
|
||||
|
||||
/* Even indices are marks (carrier on), odd indices are spaces. */
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if ((i & 1U) == 0U) {
|
||||
mark(sym[i]);
|
||||
@ -78,8 +210,8 @@ static void transmit(const struct ir_tx_req *req)
|
||||
}
|
||||
gpio_pin_set_dt(&ir_tx, 0);
|
||||
}
|
||||
#endif /* IR_TX_PRESENT */
|
||||
|
||||
/* Dedicated cooperative thread drains the queue and bit-bangs each frame. */
|
||||
static void ir_tx_thread(void *a, void *b, void *c)
|
||||
{
|
||||
ARG_UNUSED(a);
|
||||
@ -90,17 +222,34 @@ static void ir_tx_thread(void *a, void *b, void *c)
|
||||
|
||||
for (;;) {
|
||||
k_msgq_get(&ir_tx_q, &req, K_FOREVER);
|
||||
#if IR_TX_PRESENT
|
||||
LOG_DBG("TX 0x%02X%02X%02X", req.tx1, req.tx2, req.tx3);
|
||||
transmit(&req);
|
||||
#endif
|
||||
k_msleep(IR_TX_GAP_MS); /* inter-frame gap for the receiver */
|
||||
}
|
||||
}
|
||||
|
||||
/* Cooperative priority so the bit-banged carrier is not preempted. */
|
||||
K_THREAD_DEFINE(ir_tx_tid, 1024, ir_tx_thread, NULL, NULL, NULL,
|
||||
K_PRIO_COOP(4), 0, 0);
|
||||
|
||||
static int carrier_init(void)
|
||||
{
|
||||
if (!gpio_is_ready_dt(&ir_tx)) {
|
||||
LOG_ERR("IR TX GPIO not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
int ret = gpio_pin_configure_dt(&ir_tx, GPIO_OUTPUT_INACTIVE);
|
||||
|
||||
if (ret) {
|
||||
LOG_ERR("IR TX configure failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
LOG_INF("IR TX initialized (software bit-bang carrier, blocking)");
|
||||
return 0;
|
||||
}
|
||||
#endif /* carrier backend */
|
||||
|
||||
/* ================================================================== */
|
||||
/* Public API */
|
||||
/* ================================================================== */
|
||||
int ir_tx_send(uint8_t tx1, uint8_t tx2, uint8_t tx3)
|
||||
{
|
||||
struct ir_tx_req req = { tx1, tx2, tx3 };
|
||||
@ -109,24 +258,18 @@ int ir_tx_send(uint8_t tx1, uint8_t tx2, uint8_t tx3)
|
||||
if (ret) {
|
||||
LOG_WRN("IR TX queue full, dropping 0x%02X%02X%02X",
|
||||
tx1, tx2, tx3);
|
||||
return ret;
|
||||
}
|
||||
return ret;
|
||||
#if defined(IR_TX_PWM)
|
||||
ir_tx_kick(); /* GPIO backend's thread is already waiting on the queue */
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ir_tx_init(void)
|
||||
{
|
||||
#if IR_TX_PRESENT
|
||||
if (!gpio_is_ready_dt(&ir_tx)) {
|
||||
LOG_ERR("IR TX GPIO not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
int ret = gpio_pin_configure_dt(&ir_tx, GPIO_OUTPUT_INACTIVE);
|
||||
if (ret) {
|
||||
LOG_ERR("IR TX configure failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
LOG_INF("IR TX initialized (software 56 kHz carrier)");
|
||||
return 0;
|
||||
#if defined(IR_TX_PWM) || defined(IR_TX_GPIO)
|
||||
return carrier_init();
|
||||
#else
|
||||
LOG_WRN("IR TX alias not configured");
|
||||
return 0;
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
/*
|
||||
* rgb_led.c - PWM RGB LED, port of lib/rgb.py.
|
||||
* rgb_led.c - PWM RGB LED, integer port of lib/rgb.py.
|
||||
*
|
||||
* Uses pwm_dt_spec from the `rgb-red/green/blue` aliases (pwm-leds nodes).
|
||||
* Channel duty = level/255 * power/100, computed in 64-bit integer math so no
|
||||
* floating point is required on the Cortex-M0+.
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/pwm.h>
|
||||
@ -26,24 +29,19 @@ static const struct pwm_dt_spec led_b = PWM_DT_SPEC_GET(RGB_B_NODE);
|
||||
#warning "RGB aliases (rgb-red/green/blue) not found in overlay"
|
||||
#endif
|
||||
|
||||
static float rgb_pwr = 1.0f; /* lib/rgb.py default */
|
||||
/* "White" preset brightness, ~0.8 on the original 0.0-1.0 scale. */
|
||||
#define RGB_WHITE_LEVEL 204U
|
||||
|
||||
static float clampf(float v)
|
||||
{
|
||||
if (v < 0.0f) {
|
||||
return 0.0f;
|
||||
}
|
||||
if (v > 1.0f) {
|
||||
return 1.0f;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
static uint8_t rgb_pwr = 100; /* percent; lib/rgb.py default 1.0 */
|
||||
|
||||
#if RGB_PRESENT
|
||||
static void set_channel(const struct pwm_dt_spec *led, float level)
|
||||
static void set_channel(const struct pwm_dt_spec *led, uint8_t level)
|
||||
{
|
||||
uint32_t pulse = (uint32_t)(clampf(level) * (float)led->period);
|
||||
/* pulse = period * level/255 * pwr/100 (64-bit to avoid overflow). */
|
||||
uint32_t pulse = (uint32_t)(((uint64_t)led->period * level * rgb_pwr) /
|
||||
(255U * 100U));
|
||||
int ret = pwm_set_pulse_dt(led, pulse);
|
||||
|
||||
if (ret) {
|
||||
LOG_WRN("pwm set failed: %d", ret);
|
||||
}
|
||||
@ -67,12 +65,12 @@ int rgb_init(void)
|
||||
#endif
|
||||
}
|
||||
|
||||
void rgb_set(float r, float g, float b)
|
||||
void rgb_set(uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
#if RGB_PRESENT
|
||||
set_channel(&led_r, r * rgb_pwr);
|
||||
set_channel(&led_g, g * rgb_pwr);
|
||||
set_channel(&led_b, b * rgb_pwr);
|
||||
set_channel(&led_r, r);
|
||||
set_channel(&led_g, g);
|
||||
set_channel(&led_b, b);
|
||||
#else
|
||||
ARG_UNUSED(r);
|
||||
ARG_UNUSED(g);
|
||||
@ -80,37 +78,37 @@ void rgb_set(float r, float g, float b)
|
||||
#endif
|
||||
}
|
||||
|
||||
void rgb_set_power(float pwr)
|
||||
void rgb_set_power(uint8_t pct)
|
||||
{
|
||||
rgb_pwr = clampf(pwr);
|
||||
rgb_pwr = pct > 100U ? 100U : pct;
|
||||
}
|
||||
|
||||
float rgb_get_power(void)
|
||||
uint8_t rgb_get_power(void)
|
||||
{
|
||||
return rgb_pwr;
|
||||
}
|
||||
|
||||
void rgb_off(void)
|
||||
{
|
||||
rgb_set(0.0f, 0.0f, 0.0f);
|
||||
rgb_set(0, 0, 0);
|
||||
}
|
||||
|
||||
void rgb_red(void)
|
||||
{
|
||||
rgb_set(1.0f, 0.0f, 0.0f);
|
||||
rgb_set(255, 0, 0);
|
||||
}
|
||||
|
||||
void rgb_green(void)
|
||||
{
|
||||
rgb_set(0.0f, 1.0f, 0.0f);
|
||||
rgb_set(0, 255, 0);
|
||||
}
|
||||
|
||||
void rgb_blue(void)
|
||||
{
|
||||
rgb_set(0.0f, 0.0f, 1.0f);
|
||||
rgb_set(0, 0, 255);
|
||||
}
|
||||
|
||||
void rgb_white(void)
|
||||
{
|
||||
rgb_set(0.8f, 0.8f, 0.8f);
|
||||
rgb_set(RGB_WHITE_LEVEL, RGB_WHITE_LEVEL, RGB_WHITE_LEVEL);
|
||||
}
|
||||
|
||||
@ -1,19 +1,24 @@
|
||||
/*
|
||||
* rgb_led.h - RGB status LED via PWM.
|
||||
*
|
||||
* Ported from lib/rgb.py (GPIO22/26/27, 100 Hz, active-high, global power
|
||||
* scaling 0.0-1.0). Channel values are 0.0-1.0 like the Python API.
|
||||
* scaling). The Python API used 0.0-1.0 floats; this port uses 8-bit integer
|
||||
* channel values (0-255) and an integer power percentage so the RP2040
|
||||
* Cortex-M0+ (no FPU) never pulls in soft-float routines.
|
||||
*/
|
||||
#ifndef RGB_LED_H
|
||||
#define RGB_LED_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
int rgb_init(void);
|
||||
|
||||
/* Set the three channels (0.0-1.0); scaled by the current power level. */
|
||||
void rgb_set(float r, float g, float b);
|
||||
/* Set the three channels (0-255 each); scaled by the current power level. */
|
||||
void rgb_set(uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
/* Global power multiplier (lib/rgb.py rgb_pwr), clamped to 0.0-1.0. */
|
||||
void rgb_set_power(float pwr);
|
||||
float rgb_get_power(void);
|
||||
/* Global power multiplier as a percentage (0-100); lib/rgb.py rgb_pwr. */
|
||||
void rgb_set_power(uint8_t pct);
|
||||
uint8_t rgb_get_power(void);
|
||||
|
||||
/* Presets matching lib/rgb.py. */
|
||||
void rgb_off(void);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user