From 42fedca6f5186b1ad2415c5073e8ffbfa5386cfd Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Fri, 4 Sep 2026 16:52:24 +0200 Subject: [PATCH 1/2] Initial part of IR implementation --- doxygen/modules_diagram.dox.inc | 2 + include/picofuse/hw.h | 1 + include/picofuse/hw/infrared.h | 181 ++++++++++++++++++++++++++ src/picofuse/hw/darwin/CMakeLists.txt | 1 + src/picofuse/hw/linux/CMakeLists.txt | 1 + src/picofuse/hw/pico/CMakeLists.txt | 4 + src/picofuse/hw/pico/ir_tx.pio | 66 ++++++++++ src/picofuse/hw/stub/infrared.c | 49 +++++++ 8 files changed, 305 insertions(+) create mode 100644 include/picofuse/hw/infrared.h create mode 100644 src/picofuse/hw/pico/ir_tx.pio create mode 100644 src/picofuse/hw/stub/infrared.c diff --git a/doxygen/modules_diagram.dox.inc b/doxygen/modules_diagram.dox.inc index 2de3771..ab567a6 100644 --- a/doxygen/modules_diagram.dox.inc +++ b/doxygen/modules_diagram.dox.inc @@ -37,6 +37,7 @@ digraph modules { LED [label="LED", URL="\ref LED"]; WiFi [label="WiFi", URL="\ref WiFi"]; UART [label="UART", URL="\ref UART"]; + Infrared [label="Infrared", URL="\ref Infrared"]; DeviceIO [label="Device I/O", URL="\ref DeviceIO"]; SPI [label="SPI", URL="\ref SPI"]; I2C [label="I2C", URL="\ref I2C"]; @@ -75,6 +76,7 @@ digraph modules { Hardware -> LED; Hardware -> WiFi; Hardware -> UART; + Hardware -> Infrared; Hardware -> DeviceIO; DeviceIO -> SPI; DeviceIO -> I2C; diff --git a/include/picofuse/hw.h b/include/picofuse/hw.h index 1ce6133..443fd6f 100644 --- a/include/picofuse/hw.h +++ b/include/picofuse/hw.h @@ -12,6 +12,7 @@ #include "hw/deviceio.h" #include "hw/gpio.h" #include "hw/i2c.h" +#include "hw/infrared.h" #include "hw/init.h" #include "hw/led.h" #include "hw/pwm.h" diff --git a/include/picofuse/hw/infrared.h b/include/picofuse/hw/infrared.h new file mode 100644 index 0000000..bc42665 --- /dev/null +++ b/include/picofuse/hw/infrared.h @@ -0,0 +1,181 @@ +/** + * @file infrared.h + * @brief Infrared (IR) receiver and transmitter interface + * @defgroup Infrared Infrared + * @ingroup Hardware + * + * Infrared interface for capturing and generating the raw MARK/SPACE + * timing that consumer IR remotes use - not any particular protocol. + * NEC, RC5, SIRC, and so on are all just a specific sequence of MARK + * (carrier on) and SPACE (carrier off) durations; decoding or encoding + * a specific protocol from/to that sequence is a separate "codec" layer + * built on top of this module (see hw_infrared_set_callback()), not + * something this header knows about. + * + * A single hw_infrared_t can receive, transmit, or both, since a remote + * receiver and an IR LED are physically independent hardware - pass + * NULL for whichever side (pin or device) you don't want. + */ +#pragma once +#include "gpio.h" +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +/** + * @brief Infrared receiver/transmitter handle. + * @ingroup Infrared + * @headerfile infrared.h picofuse/hw.h + */ +typedef struct hw_infrared_t hw_infrared_t; + +/** + * @brief Infrared receive event types. + * @ingroup Infrared + */ +typedef enum { + hw_infrared_event_mark, ///< The carrier was on for duration_us. + hw_infrared_event_space, ///< The carrier was off for duration_us. + hw_infrared_event_timeout, ///< duration_us exceeded hw_infrared_config_t's + ///< timeout_us - treat any in-progress frame as + ///< abandoned and start decoding fresh. +} hw_infrared_event_t; + +/** + * @brief Infrared receive callback. + * @ingroup Infrared + * @param ir The handle the event occurred on. + * @param event Which kind of event this is. + * @param duration_us How long the mark or space lasted, or how long the + * receiver had been idle when a timeout was declared. + * @param userdata User-defined data pointer provided to + * hw_infrared_set_callback(). + */ +typedef void (*hw_infrared_callback_t)(hw_infrared_t *ir, + hw_infrared_event_t event, + uint32_t duration_us, void *userdata); + +/** + * @brief Infrared initialization configuration. + * @ingroup Infrared + * + * When NULL is passed to hw_infrared_init()/hw_infrared_init_device(), + * every field here defaults as documented. + */ +typedef struct { + /** + * TX carrier frequency in Hz. 0 uses the default, 38000 (38kHz) - the + * most common consumer IR carrier. Ignored if tx_pin/tx_device is + * NULL. + */ + uint32_t carrier_freq; + /** + * RX duration, in microseconds, above which an event is reported as + * hw_infrared_event_timeout instead of hw_infrared_event_space. 0 + * uses the default, 50000 (50ms) - comfortably longer than the + * inter-frame gap of any common protocol's individual frame, but + * short enough to promptly notice a remote has stopped transmitting. + * Ignored if rx_pin/rx_device is NULL. + */ + uint32_t timeout_us; +} hw_infrared_config_t; + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +/** @name Lifecycle + * @{ */ + +/** + * @brief Initialize an Infrared receiver and/or transmitter. + * @ingroup Infrared + * @param rx_pin GPIO pin wired to an IR receiver module's output (e.g. a + * TSOP38238), or NULL to not enable receiving. + * @param tx_pin GPIO pin wired to an IR LED (directly or via a driver + * transistor), or NULL to not enable transmitting. + * @param config Optional pointer to extended configuration. Pass NULL to + * use default carrier frequency and receive timeout. + * @return An initialized handle, or NULL if both @p rx_pin and @p tx_pin + * are NULL, initialization fails, or the receiver/transmitter pool is + * exhausted. Release it with hw_infrared_deinit(). + * + * Receive events are not reported anywhere until a callback is attached + * with hw_infrared_set_callback() - that's a separate step so that a + * protocol codec can own the callback without hw_infrared_init() itself + * needing to know codecs exist. + */ +hw_infrared_t *hw_infrared_init(const hw_gpio_t *rx_pin, const hw_gpio_t *tx_pin, + const hw_infrared_config_t *config); + +/** + * @brief Initialize an Infrared receiver and/or transmitter by device path. + * @ingroup Infrared + * @param rx_device Device path for a real IR receiver, or NULL to not + * enable receiving. + * @param tx_device Device path for a real IR transmitter, or NULL to not + * enable transmitting. + * @param config Optional pointer to extended configuration. Pass NULL to + * use default carrier frequency and receive timeout. + * @return An initialized handle, or NULL if both @p rx_device and + * @p tx_device are NULL, initialization fails, or the receiver/ + * transmitter pool is exhausted. Release it with hw_infrared_deinit(). + * + * Host platforms have no GPIO pins for this - hw_infrared_init() is + * Pico-only, mirroring hw_uart_init()/hw_uart_init_device()'s own split. + */ +hw_infrared_t *hw_infrared_init_device(const char *rx_device, + const char *tx_device, + const hw_infrared_config_t *config); + +/** + * @brief Deinitialize and release an Infrared handle. + * @ingroup Infrared + * @param ir Pointer to the handle to deinitialize, or NULL (a no-op). + */ +void hw_infrared_deinit(hw_infrared_t *ir); + +/** @} */ + +/////////////////////////////////////////////////////////////////////////////// +// METHODS + +/** @name Methods + * @{ */ + +/** + * @brief Set or clear the receive callback. + * @ingroup Infrared + * @param ir The handle to observe. + * @param callback Callback to invoke on each receive event, or NULL to + * remove the current callback. + * @param userdata User-defined data pointer passed to @p callback. + * @return true if the callback was registered, false if @p ir is NULL or + * has no receiver configured. + */ +bool hw_infrared_set_callback(hw_infrared_t *ir, hw_infrared_callback_t callback, + void *userdata); + +/** + * @brief Transmit a sequence of IR mark/space durations. + * @ingroup Infrared + * @param ir The handle to transmit on. + * @param durations_us Alternating mark/space durations in microseconds, + * starting with a mark - the same raw representation LIRC uses, and what + * a protocol codec builds from a decoded button press. + * @param count Number of entries in @p durations_us. + * @return true if the whole sequence was transmitted, false if @p ir is + * NULL, has no transmitter configured, or @p durations_us is NULL with a + * nonzero @p count. + * + * Blocks until transmission completes - a full frame takes anywhere from + * a few milliseconds to a few tens of milliseconds, comparable to + * hw_led_neopixel's own blocking flush, and asynchronous transmission + * isn't something callers have needed so far. + */ +bool hw_infrared_transmit(hw_infrared_t *ir, const uint32_t *durations_us, + size_t count); + +/** @} */ diff --git a/src/picofuse/hw/darwin/CMakeLists.txt b/src/picofuse/hw/darwin/CMakeLists.txt index 69114e9..8f9116f 100644 --- a/src/picofuse/hw/darwin/CMakeLists.txt +++ b/src/picofuse/hw/darwin/CMakeLists.txt @@ -19,6 +19,7 @@ picofuse_library( ../stub/adc.c ../stub/gpio.c ../stub/i2c.c + ../stub/infrared.c ../stub/pwm.c ../stub/spi.c ../posix/uart.c diff --git a/src/picofuse/hw/linux/CMakeLists.txt b/src/picofuse/hw/linux/CMakeLists.txt index 59b4209..6d91261 100644 --- a/src/picofuse/hw/linux/CMakeLists.txt +++ b/src/picofuse/hw/linux/CMakeLists.txt @@ -16,6 +16,7 @@ picofuse_library( ../led/led_neopixel.c led_device.c ../stub/adc.c + ../stub/infrared.c ../posix/uart.c # @todo No real Linux hw_wifi_* backend yet (wpa_supplicant control # socket - see hw_wifi_init_device()'s doc) to gate behind diff --git a/src/picofuse/hw/pico/CMakeLists.txt b/src/picofuse/hw/pico/CMakeLists.txt index 787f958..8817649 100644 --- a/src/picofuse/hw/pico/CMakeLists.txt +++ b/src/picofuse/hw/pico/CMakeLists.txt @@ -20,6 +20,10 @@ picofuse_library( pwm.c spi.c uart.c + # @todo No real Pico hw_infrared_* backend yet (ir_tx.pio is + # written but not wired into a driver, and RX has no PIO backend + # here) - always the stub for now. + ../stub/infrared.c ../deviceio/deviceio.c ../led/led.c ../led/led_gpio.c diff --git a/src/picofuse/hw/pico/ir_tx.pio b/src/picofuse/hw/pico/ir_tx.pio new file mode 100644 index 0000000..2cec52b --- /dev/null +++ b/src/picofuse/hw/pico/ir_tx.pio @@ -0,0 +1,66 @@ +; Consumer-IR transmitter: gates a fixed carrier on ("mark") and off +; ("space") for durations fed one at a time over the TX FIFO, starting +; with a mark and strictly alternating - the natural representation for +; every common IR protocol (NEC, RC5, SIRC, ...) and for LIRC's own raw +; timing format, just in units of whole carrier periods here rather than +; microseconds (ir_tx_program_init()'s caller converts, since it also +; knows the carrier frequency). +; +; A two-state-machine split (a generic "burst" generator triggered by a +; separate "control" sequencer, as in the official pico-examples +; nec_transmit_library) only pays for itself when the burst has a fixed +; length per trigger. Supporting arbitrary durations means the trigger +; would have to fire once per carrier cycle regardless, at which point +; control is already doing all the real work and a second, independently +; clocked state machine (plus the IRQ handshake and clock-divider +; coordination between them) buys nothing - so this does it all in one. +; +; Each FIFO word is a period count: how many full carrier periods to +; hold that state for. A mark toggles the pin at ~33% duty cycle (T_HIGH +; high, T_LOW low per period - the usual recommendation for driving an +; IR LED, trading a little detection range for a lot less average +; current than 50%); a space just holds the pin low for the same +; per-period duration, so the same period count means the same +; wall-clock time either way. +.program ir_tx +.side_set 1 opt + +.define public T_HIGH 1 +.define public T_LOW 2 + +.wrap_target +mark: + pull block + mov x, osr +mark_loop: + nop side 1 [T_HIGH - 1] + jmp x-- mark_loop side 0 [T_LOW - 1] + +space: + pull block + mov x, osr +space_loop: + jmp x-- space_loop side 0 [T_HIGH + T_LOW - 1] +.wrap + +% c-sdk { +static inline void ir_tx_program_init(PIO pio, + uint sm, + uint offset, + uint pin, + float carrier_freq) { + pio_sm_config c = ir_tx_program_get_default_config(offset); + + sm_config_set_sideset_pins(&c, pin); + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); + + const float cycles_per_period = (float)(ir_tx_T_HIGH + ir_tx_T_LOW); + float div = (float)clock_get_hz(clk_sys) / (carrier_freq * cycles_per_period); + sm_config_set_clkdiv(&c, div); + + pio_gpio_init(pio, pin); + pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); + pio_sm_init(pio, sm, offset, &c); + pio_sm_set_enabled(pio, sm, true); +} +%} diff --git a/src/picofuse/hw/stub/infrared.c b/src/picofuse/hw/stub/infrared.c new file mode 100644 index 0000000..3411995 --- /dev/null +++ b/src/picofuse/hw/stub/infrared.c @@ -0,0 +1,49 @@ +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +/** Stub implementation: no Infrared hardware on this platform. */ +hw_infrared_t *hw_infrared_init(const hw_gpio_t *rx_pin, + const hw_gpio_t *tx_pin, + const hw_infrared_config_t *config) { + (void)rx_pin; + (void)tx_pin; + (void)config; + return NULL; +} + +/** Stub implementation: no Infrared hardware on this platform. */ +hw_infrared_t *hw_infrared_init_device(const char *rx_device, + const char *tx_device, + const hw_infrared_config_t *config) { + (void)rx_device; + (void)tx_device; + (void)config; + return NULL; +} + +/** Stub implementation: no Infrared hardware on this platform. */ +void hw_infrared_deinit(hw_infrared_t *ir) { (void)ir; } + +/////////////////////////////////////////////////////////////////////////////// +// METHODS + +/** Stub implementation: no Infrared hardware on this platform. */ +bool hw_infrared_set_callback(hw_infrared_t *ir, hw_infrared_callback_t callback, + void *userdata) { + (void)ir; + (void)callback; + (void)userdata; + return false; +} + +/** Stub implementation: no Infrared hardware on this platform. */ +bool hw_infrared_transmit(hw_infrared_t *ir, const uint32_t *durations_us, + size_t count) { + (void)ir; + (void)durations_us; + (void)count; + return false; +} From 850cf2118c13bed85af150ebea676bd79f4e7cee Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sat, 5 Sep 2026 08:18:35 +0200 Subject: [PATCH 2/2] Updated --- include/picofuse/hw/infrared.h | 17 +- src/picofuse/hw/pico/CMakeLists.txt | 9 +- src/picofuse/hw/pico/infrared.c | 197 ++++++++++++++++++ .../hw/pico/{ir_tx.pio => infrared_tx.pio} | 20 +- 4 files changed, 226 insertions(+), 17 deletions(-) create mode 100644 src/picofuse/hw/pico/infrared.c rename src/picofuse/hw/pico/{ir_tx.pio => infrared_tx.pio} (78%) diff --git a/include/picofuse/hw/infrared.h b/include/picofuse/hw/infrared.h index bc42665..c56f3cd 100644 --- a/include/picofuse/hw/infrared.h +++ b/include/picofuse/hw/infrared.h @@ -22,6 +22,16 @@ #include #include +/** + * @def HW_INFRARED_CAPACITY + * @ingroup Infrared + * @brief Maximum number of simultaneously open Infrared instances. One + instance can be either a receiver, a transmitter, or both. + */ +#ifndef HW_INFRARED_CAPACITY +#define HW_INFRARED_CAPACITY 1 +#endif + /////////////////////////////////////////////////////////////////////////////// // TYPES @@ -107,7 +117,8 @@ typedef struct { * protocol codec can own the callback without hw_infrared_init() itself * needing to know codecs exist. */ -hw_infrared_t *hw_infrared_init(const hw_gpio_t *rx_pin, const hw_gpio_t *tx_pin, +hw_infrared_t *hw_infrared_init(const hw_gpio_t *rx_pin, + const hw_gpio_t *tx_pin, const hw_infrared_config_t *config); /** @@ -155,8 +166,8 @@ void hw_infrared_deinit(hw_infrared_t *ir); * @return true if the callback was registered, false if @p ir is NULL or * has no receiver configured. */ -bool hw_infrared_set_callback(hw_infrared_t *ir, hw_infrared_callback_t callback, - void *userdata); +bool hw_infrared_set_callback(hw_infrared_t *ir, + hw_infrared_callback_t callback, void *userdata); /** * @brief Transmit a sequence of IR mark/space durations. diff --git a/src/picofuse/hw/pico/CMakeLists.txt b/src/picofuse/hw/pico/CMakeLists.txt index 8817649..bde9488 100644 --- a/src/picofuse/hw/pico/CMakeLists.txt +++ b/src/picofuse/hw/pico/CMakeLists.txt @@ -20,10 +20,10 @@ picofuse_library( pwm.c spi.c uart.c - # @todo No real Pico hw_infrared_* backend yet (ir_tx.pio is - # written but not wired into a driver, and RX has no PIO backend - # here) - always the stub for now. - ../stub/infrared.c + # infrared.c only wires up TX (infrared_tx.pio) so far - RX + # (hw_infrared_init()'s rx_pin) is rejected until infrared_rx.pio + # and its IRQ-driven decode loop exist - see its own comment. + infrared.c ../deviceio/deviceio.c ../led/led.c ../led/led_gpio.c @@ -36,6 +36,7 @@ picofuse_library( ) pico_generate_pio_header(picofuse-hw ${CMAKE_CURRENT_LIST_DIR}/led_neopixel.pio) +pico_generate_pio_header(picofuse-hw ${CMAKE_CURRENT_LIST_DIR}/infrared_tx.pio) target_link_libraries(picofuse-hw PRIVATE picofuse-sys) diff --git a/src/picofuse/hw/pico/infrared.c b/src/picofuse/hw/pico/infrared.c new file mode 100644 index 0000000..63c5dae --- /dev/null +++ b/src/picofuse/hw/pico/infrared.c @@ -0,0 +1,197 @@ +#include "../../sys/pico/sync.h" +#include "hardware/clocks.h" +#include "hardware/pio.h" +#include "infrared_tx.pio.h" +#include "pico/time.h" +#include +#include +#include +#include + +#define HW_INFRARED_DEFAULT_CARRIER_FREQ 38000u +#define HW_INFRARED_DEFAULT_TIMEOUT_US 50000u + +// How long to wait for the TX FIFO to fully drain before giving up +#define HW_INFRARED_FIFO_TIMEOUT_US 3000u + +/////////////////////////////////////////////////////////////////////////////// +// TYPES + +// A pool slot with no dedicated hardware instance to index by (unlike +// e.g. NUM_PWM_SLICES/NUM_UARTS) - any GPIO pin reachable by a free PIO +// state machine can serve, so this is just a small fixed-size pool of +// handles instead, matching hw_pwm_t/hw_uart_t's own "define the real +// struct here, hand out pointers into a static array" pattern. +struct hw_infrared_t { + bool active; + bool has_tx; + PIO tx_pio; + uint tx_sm; + uint tx_offset; + uint32_t tx_carrier_freq; // periods-per-second, for hw_infrared_transmit()'s + // microseconds -> whole-periods conversion + bool has_rx; // @todo always false - see hw_infrared_init()'s own comment + hw_infrared_callback_t callback; + void *userdata; +}; + +/////////////////////////////////////////////////////////////////////////////// +// GLOBALS + +static hw_infrared_t _hw_infrared_pool[HW_INFRARED_CAPACITY] = {0}; + +/////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +static hw_infrared_t *_hw_infrared_alloc(void) { + _sys_sync_pool_lock(); + for (size_t i = 0; i < HW_INFRARED_CAPACITY; i++) { + if (!_hw_infrared_pool[i].active) { + _hw_infrared_pool[i].active = true; + _sys_sync_pool_unlock(); + return &_hw_infrared_pool[i]; + } + } + _sys_sync_pool_unlock(); + return NULL; +} + +static void _hw_infrared_free(hw_infrared_t *ir) { + _sys_sync_pool_lock(); + memset(ir, 0, sizeof(*ir)); + _sys_sync_pool_unlock(); +} + +/////////////////////////////////////////////////////////////////////////////// +// LIFECYCLE + +hw_infrared_t *hw_infrared_init(const hw_gpio_t *rx_pin, + const hw_gpio_t *tx_pin, + const hw_infrared_config_t *config) { + sys_debugf("hw", "infrared_init: rx=%p tx=%p config=%p", (void *)rx_pin, + (void *)tx_pin, (void *)config); + + if (rx_pin == NULL && tx_pin == NULL) { + return NULL; + } + if (rx_pin != NULL) { + // @todo No real RX backend wired in yet (infrared_rx.pio + an IRQ- + // driven decode loop) - reject outright rather than accepting a pin + // that would silently never deliver an event, which + // hw_infrared_set_callback() returning true for it would imply. + sys_debugf("hw", "infrared_init: RX not yet supported on this backend"); + return NULL; + } + + uint32_t carrier_freq = (config != NULL && config->carrier_freq != 0) + ? config->carrier_freq + : HW_INFRARED_DEFAULT_CARRIER_FREQ; + + hw_infrared_t *ir = _hw_infrared_alloc(); + if (ir == NULL) { + return NULL; + } + + uint pin = hw_gpio_pin(tx_pin); + PIO pio; + uint sm, offset; + if (!pio_claim_free_sm_and_add_program_for_gpio_range( + &infrared_tx_program, &pio, &sm, &offset, pin, 1, true)) { + _hw_infrared_free(ir); + return NULL; + } + infrared_tx_program_init(pio, sm, offset, pin, (float)carrier_freq); + + ir->has_tx = true; + ir->tx_pio = pio; + ir->tx_sm = sm; + ir->tx_offset = offset; + ir->tx_carrier_freq = carrier_freq; + + return ir; +} + +hw_infrared_t *hw_infrared_init_device(const char *rx_device, + const char *tx_device, + const hw_infrared_config_t *config) { + sys_debugf("hw", + "infrared_init_device: unsupported on this target (rx=%s " + "tx=%s config=%p)", + rx_device != NULL ? rx_device : "(null)", + tx_device != NULL ? tx_device : "(null)", (void *)config); + (void)rx_device; + (void)tx_device; + (void)config; + return NULL; +} + +void hw_infrared_deinit(hw_infrared_t *ir) { + sys_debugf("hw", "infrared_deinit: ir=%p", (void *)ir); + if (ir == NULL || !ir->active) { + return; + } + + if (ir->has_tx) { + pio_sm_set_enabled(ir->tx_pio, ir->tx_sm, false); + pio_remove_program_and_unclaim_sm(&infrared_tx_program, ir->tx_pio, + ir->tx_sm, ir->tx_offset); + } + + _hw_infrared_free(ir); +} + +/////////////////////////////////////////////////////////////////////////////// +// METHODS + +bool hw_infrared_set_callback(hw_infrared_t *ir, + hw_infrared_callback_t callback, void *userdata) { + if (ir == NULL || !ir->has_rx) { + return false; + } + ir->callback = callback; + ir->userdata = userdata; + return true; +} + +bool hw_infrared_transmit(hw_infrared_t *ir, const uint32_t *durations_us, + size_t count) { + if (ir == NULL || !ir->has_tx || (durations_us == NULL && count > 0)) { + return false; + } + + for (size_t i = 0; i < count; i++) { + // infrared_tx.pio counts whole carrier periods, not microseconds - + // convert, rounding to the nearest period (IR receivers tolerate + // small timing error far better than a systematic short-bias + // compounding over a whole frame would). + uint64_t periods = + ((uint64_t)durations_us[i] * ir->tx_carrier_freq + 500000ull) / + 1000000ull; + if (periods == 0) { + periods = 1; + } + + // The PIO program's own loop counter is "periods - 1": with the + // counter at 0, its jmp x-- instructions don't jump, but they - like + // every other instruction - still execute their own side-set/delay + // once before falling through, so a counter of 0 already plays out + // exactly one period, not zero. + uint32_t word = (uint32_t)(periods - 1); + + uint64_t start = time_us_64(); + while (pio_sm_is_tx_fifo_full(ir->tx_pio, ir->tx_sm)) { + if (time_us_64() - start > HW_INFRARED_FIFO_TIMEOUT_US) { + return false; + } + } + pio_sm_put(ir->tx_pio, ir->tx_sm, word); + } + + uint64_t start = time_us_64(); + while (!pio_sm_is_tx_fifo_empty(ir->tx_pio, ir->tx_sm)) { + if (time_us_64() - start > HW_INFRARED_FIFO_TIMEOUT_US) { + return false; + } + } + return true; +} diff --git a/src/picofuse/hw/pico/ir_tx.pio b/src/picofuse/hw/pico/infrared_tx.pio similarity index 78% rename from src/picofuse/hw/pico/ir_tx.pio rename to src/picofuse/hw/pico/infrared_tx.pio index 2cec52b..af0dc33 100644 --- a/src/picofuse/hw/pico/ir_tx.pio +++ b/src/picofuse/hw/pico/infrared_tx.pio @@ -3,8 +3,8 @@ ; with a mark and strictly alternating - the natural representation for ; every common IR protocol (NEC, RC5, SIRC, ...) and for LIRC's own raw ; timing format, just in units of whole carrier periods here rather than -; microseconds (ir_tx_program_init()'s caller converts, since it also -; knows the carrier frequency). +; microseconds (infrared_tx_program_init()'s caller converts, since it +; also knows the carrier frequency). ; ; A two-state-machine split (a generic "burst" generator triggered by a ; separate "control" sequencer, as in the official pico-examples @@ -22,7 +22,7 @@ ; current than 50%); a space just holds the pin low for the same ; per-period duration, so the same period count means the same ; wall-clock time either way. -.program ir_tx +.program infrared_tx .side_set 1 opt .define public T_HIGH 1 @@ -44,17 +44,17 @@ space_loop: .wrap % c-sdk { -static inline void ir_tx_program_init(PIO pio, - uint sm, - uint offset, - uint pin, - float carrier_freq) { - pio_sm_config c = ir_tx_program_get_default_config(offset); +static inline void infrared_tx_program_init(PIO pio, + uint sm, + uint offset, + uint pin, + float carrier_freq) { + pio_sm_config c = infrared_tx_program_get_default_config(offset); sm_config_set_sideset_pins(&c, pin); sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); - const float cycles_per_period = (float)(ir_tx_T_HIGH + ir_tx_T_LOW); + const float cycles_per_period = (float)(infrared_tx_T_HIGH + infrared_tx_T_LOW); float div = (float)clock_get_hz(clk_sys) / (carrier_freq * cycles_per_period); sm_config_set_clkdiv(&c, div);