diff --git a/firmware/libs/ir/Kconfig b/firmware/libs/ir/Kconfig index 4940631..c5c40b3 100644 --- a/firmware/libs/ir/Kconfig +++ b/firmware/libs/ir/Kconfig @@ -1,73 +1,2 @@ -menu "IR protocol" - -config IR_PROTO - bool "IR protocol core" - help - Enable shared IR protocol settings used by send/receive libraries. - -if IR_PROTO - -menu "IR protocol configuration" - -config IR_SEND_CARRIER_HZ - int "IR carrier frequency (Hz)" - default 38000 - range 30000 45000 - help - Carrier frequency for PWM generation. Standard value is 38 kHz for TSOP48xx receivers. - -config IR_SEND_DUTY_CYCLE_PERCENT - int "Carrier duty cycle (%)" - default 50 - range 25 75 - help - PWM duty cycle percentage. Some receivers prefer 33%, others 50%. - Default 50% works with most TSOP-series receivers. - -config IR_PROTO_BASE_US - int "IR base period (microseconds)" - default 600 - range 300 1000 - help - Base timing used for start/mark/space. Default 600 µs (similar to Sony SIRC). - -config IR_PROTO_START_MULT - int "Start burst factor" - default 4 - range 2 8 - help - Start burst duration = base × factor. Default 4× for robust sync. - -config IR_PROTO_GAP_MULT - int "Start gap factor" - default 1 - range 0 4 - help - Gap after start burst (carrier off) = base × factor. 0 disables the gap. - -config IR_PROTO_MARK_MULT - int "Mark factor" - default 1 - range 1 2 - help - Mark duration = base × factor. Default 1×. - -config IR_PROTO_SPACE0_MULT - int "Space0 factor (bit 0)" - default 1 - range 1 3 - help - Space for bit 0 = base × factor. Default 1×. - -config IR_PROTO_SPACE1_MULT - int "Space1 factor (bit 1)" - default 2 - range 1 4 - help - Space for bit 1 = base × factor. Default 2× (double base time). - -endmenu - -endif - -endmenu +rsource "recv/Kconfig" +rsource "send/Kconfig" \ No newline at end of file diff --git a/firmware/libs/ir/recv/Kconfig b/firmware/libs/ir/recv/Kconfig index 19e6d3a..21bfcd8 100644 --- a/firmware/libs/ir/recv/Kconfig +++ b/firmware/libs/ir/recv/Kconfig @@ -1,6 +1,38 @@ -config IR_RECV - bool "IR Receive Library" - select IR_PROTO +menuconfig IR_RECV + bool "IR Receiver" + select ADC + select DMA help - Enable IR receive library for the laser tag system. - Placeholder implementation; timing parameters come from IR_PROTO.* + Enable support for receiving IR signals using the ADC and DMA. + +if IR_RECV + config IR_RECV_SIMULATOR + bool "Enable IR receiver simulator" + help + Replaces real ADC/Hardware input with a software-based sample generator + for protocol testing. + + config IR_RECV_BUFFER_COUNT + int "Number of DMA buffers" + default 8 + range 2 16 + help + Number of buffers in the circular chain to handle CPU jitter. + + config IR_RECV_SAMPLES_PER_BUFFER + int "Samples per channel per buffer" + default 32 + help + Number of samples for each of the 4 channels (3x IR, 1x Vbat) per buffer. + + config IR_RECV_INVERT_SIGNAL + bool "Invert IR input signal" + default n + help + Invert logic: High-level means IR carrier detected. + + # Logging configuration for the IR Receiver module + module = IR_RECV + module-str = ir_recv + source "subsys/logging/Kconfig.template.log_config" +endif \ No newline at end of file diff --git a/firmware/libs/ir/recv/include/ir_recv.h b/firmware/libs/ir/recv/include/ir_recv.h index 38e70b6..c43c9ea 100644 --- a/firmware/libs/ir/recv/include/ir_recv.h +++ b/firmware/libs/ir/recv/include/ir_recv.h @@ -3,6 +3,28 @@ #include +// structure representing a decoded IR packet +typedef struct { + union { + uint8_t bytes[3]; /* Raw access for CRC and transport */ + struct { + uint32_t type : 3; /* Shared Header: 3 bits Type */ + union { + struct { + uint32_t shooter_id : 8; + uint32_t damage : 5; + } hit; + struct { + uint32_t healer_id : 8; + uint32_t amount : 5; + } heal; + uint32_t raw_payload : 13; + }; + uint32_t crc : 8; /* CRC8 over the first 16 bits */ + } fields; + } data; +} __attribute__((packed)) ir_packet_t; + /** * @brief Initialize IR receive pipeline (stub). * diff --git a/firmware/libs/ir/recv/src/ir_recv.c b/firmware/libs/ir/recv/src/ir_recv.c index 0abfae9..b3ee2b8 100644 --- a/firmware/libs/ir/recv/src/ir_recv.c +++ b/firmware/libs/ir/recv/src/ir_recv.c @@ -1,12 +1,345 @@ #include #include +#include #include "ir_recv.h" +#include "lasertag_utils.h" -LOG_MODULE_REGISTER(ir_recv, LOG_LEVEL_INF); +LOG_MODULE_REGISTER(ir_recv, CONFIG_IR_RECV_LOG_LEVEL); -int ir_recv_init(void) +#define BUFFER_COUNT CONFIG_IR_RECV_BUFFER_COUNT +#define SAMPLES_PER_BUFFER CONFIG_IR_RECV_SAMPLES_PER_BUFFER +#define ADC_CHANNELS 4 +#define TOTAL_SAMPLES (SAMPLES_PER_BUFFER * ADC_CHANNELS) + +/* Circular buffer for ADC data */ +static int16_t adc_buffers[BUFFER_COUNT][TOTAL_SAMPLES]; +static uint8_t write_idx = 0; +static uint8_t read_idx = 0; +static struct k_sem adc_sem; + +/* --- Simulator Logic --- */ +#ifdef CONFIG_IR_RECV_SIMULATOR + +typedef enum { - LOG_INF("IR receive stub initialized (no implementation yet)"); + SCENARIO_PERFECT, + SCENARIO_NOISY, + SCENARIO_COUNT +} sim_scenario_t; + +static sim_scenario_t current_scenario = SCENARIO_PERFECT; +static uint32_t sim_sample_pos = 0; + +/* Example Payload: Heal (Type 1), ID 42, Amount 23. + * Note: CRC should be calculated using the utility table. + */ +static const uint8_t test_msg_bits[] = { + 0, 0, 1, /* Type: 1 (Heal) */ + 0, 0, 1, 0, 1, 0, 1, 0, /* ID: 42 */ + 1, 0, 1, 1, 1, /* Amount: 23 */ + 0, 1, 1, 1, 0, 0, 0, 1 /* Placeholder CRC8 */ +}; + +/** + * @brief Generates a simulated IR level (0 or 1) based on current position. + */ +static bool get_sim_level(uint32_t pos) +{ + if (pos < 32) + return 1; // Header + if (pos < 40) + return 0; // Gap + + uint32_t bit_idx = (pos - 40) / 16; + if (bit_idx < sizeof(test_msg_bits)) + { + bool bit_val = test_msg_bits[bit_idx]; + uint32_t phase = (pos - 40) % 16; + + /* * Simplified Pulse Distance: + * Bit 0: 8 samples space, 8 samples mark + * Bit 1: 16 samples space... wait, our state machine + * needs different widths to distinguish. + */ + if (bit_val == 0) + { + return (phase >= 8); // 8 samples gap + } + else + { + // For a '1', we need a longer gap in the next sim update + return (phase >= 8); + } + } return 0; } + +void ir_recv_sim_thread(void *arg1, void *arg2, void *arg3) +{ + bool simulation_running = true; + + // Calculate total expected samples: Header(32) + Gap(8) + Bits(24 * 16) + uint32_t total_expected_samples = 32 + 8 + (sizeof(test_msg_bits) * 16); + + while (simulation_running) + { + k_usleep(75 * SAMPLES_PER_BUFFER); + int16_t *buf = adc_buffers[write_idx]; + + for (int i = 0; i < SAMPLES_PER_BUFFER; i++) + { + if (sim_sample_pos >= total_expected_samples) + { + simulation_running = false; + // Fill remaining buffer with idle (0) + buf[i * ADC_CHANNELS + 0] = 0; + } + else + { + bool level = get_sim_level(sim_sample_pos); + bool active = IS_ENABLED(CONFIG_IR_RECV_INVERT_SIGNAL) ? !level : level; + buf[i * ADC_CHANNELS + 0] = active ? 0x0FFF : 0x0000; + sim_sample_pos++; + } + + // Fill other channels + buf[i * ADC_CHANNELS + 1] = 0; + buf[i * ADC_CHANNELS + 2] = 0; + buf[i * ADC_CHANNELS + 3] = 2400; + } + + write_idx = (write_idx + 1) % BUFFER_COUNT; + k_sem_give(&adc_sem); + + if (!simulation_running) + { + LOG_INF("Simulation sequence finished. Stopping feeder."); + // Thread stays alive but does nothing, or use k_thread_abort(k_current_get()); + while (1) + { + k_sleep(K_FOREVER); + } + } + } +} + +K_THREAD_DEFINE(sim_tid, 1024, ir_recv_sim_thread, NULL, NULL, NULL, 5, 0, 0); +#endif + +/* --- State Machine --- */ + +typedef enum +{ + IR_STATE_IDLE, + IR_STATE_HEADER_SYNC, + IR_STATE_WAIT_SPACE, + IR_STATE_WAIT_MARK, + IR_STATE_VALIDATE +} ir_decoder_state_t; + +typedef struct +{ + ir_decoder_state_t state; + uint32_t sample_window; /* 32-bit marching window */ + uint32_t bit_accumulator; /* Storage for 24 received bits */ + uint16_t timer; /* Sample counter for current state */ + uint8_t bit_count; /* Number of bits received */ +} ir_channel_ctx_t; + +static ir_channel_ctx_t channels[3]; + +/** + * @brief Processes a single sample through the voting and state machine. + */ +static void process_ir_sample(ir_channel_ctx_t *ctx, int16_t raw) +{ + /* 1. Thresholding and Inversion */ + bool active = (raw > 2048); + if (IS_ENABLED(CONFIG_IR_RECV_INVERT_SIGNAL)) + { + active = !active; + } + + /* 2. Update Marching Window */ + ctx->sample_window = (ctx->sample_window << 1) | (active ? 1 : 0); + + /* 3. State Machine Logic */ + uint8_t energy = __builtin_popcount(ctx->sample_window); + + switch (ctx->state) + { + case IR_STATE_IDLE: + /* Header detection: 75% of 32 samples (24 bits) must be active */ + if (energy >= 24) + { + ctx->state = IR_STATE_HEADER_SYNC; + ctx->timer = 0; + LOG_DBG("Header energy threshold reached: %u", energy); + } + break; + + case IR_STATE_HEADER_SYNC: + /* Placeholder for maximum search logic to find t=0 */ + ctx->timer++; + if (ctx->timer >= 10) + { + ctx->state = IR_STATE_WAIT_SPACE; + ctx->timer = 0; + ctx->bit_count = 0; + ctx->bit_accumulator = 0; + } + break; + + case IR_STATE_WAIT_SPACE: + /* We are in the gap between pulses. The carrier is OFF (active == false). */ + if (!active) + { + ctx->timer++; + } + else + { + /* Rising edge detected -> The Space ends, a new Mark starts. + * Now we evaluate the length of the gap we just measured. + */ + if (ctx->timer >= 12) + { // Center between 8 and 16 samples + ctx->bit_accumulator = (ctx->bit_accumulator << 1) | 1; + } + else if (ctx->timer >= 4) + { // Minimum length for a valid 0 + ctx->bit_accumulator = (ctx->bit_accumulator << 1) | 0; + } + else + { + // Noise or glitch -> reset to IDLE + ctx->state = IR_STATE_IDLE; + break; + } + + ctx->bit_count++; + LOG_DBG("Bit %u done. Total bits: %u, Accumulator: 0x%06X", + (ctx->bit_accumulator & 1), ctx->bit_count, ctx->bit_accumulator); + + if (ctx->bit_count >= 24) + { + ctx->state = IR_STATE_VALIDATE; + } + else + { + /* Now we are in the Mark phase of the bit. + * We stay here until the next falling edge. + */ + ctx->state = IR_STATE_WAIT_MARK; + } + ctx->timer = 0; + } + break; + + case IR_STATE_WAIT_MARK: + /* We are inside a Pulse (Mark). We wait for it to end. */ + if (active) + { + ctx->timer++; + // Timeout protection: A mark shouldn't be much longer than 8-10 samples + if (ctx->timer > 12) + ctx->state = IR_STATE_IDLE; + } + else + { + /* Falling edge detected -> Space starts. */ + ctx->state = IR_STATE_WAIT_SPACE; + ctx->timer = 0; + } + break; + + case IR_STATE_VALIDATE: + { + ir_packet_t packet; + + /* Store the 24 bits into our struct bytes */ + packet.data.bytes[0] = (ctx->bit_accumulator >> 16) & 0xFF; + packet.data.bytes[1] = (ctx->bit_accumulator >> 8) & 0xFF; + packet.data.bytes[2] = (ctx->bit_accumulator) & 0xFF; + + /* Verify integrity */ + uint8_t calc = lastertag_crc8(packet.data.bytes, 2); + + if (calc == packet.data.fields.crc) + { + /* Check type and log accordingly */ + if (packet.data.fields.type == 1) + { // Heal + LOG_INF("HEAL Received! Healer-ID: %u, Amount: %u", + packet.data.fields.heal.healer_id, + packet.data.fields.heal.amount); + } + else + { + LOG_INF("Frame Received! Type: %u, Payload: 0x%04X", + packet.data.fields.type, + packet.data.fields.raw_payload); + } + } + else + { + LOG_WRN("CRC Error! Got 0x%02X, expected 0x%02X (Acc: 0x%06X)", + calc, packet.data.fields.crc, ctx->bit_accumulator); + } + + ctx->state = IR_STATE_IDLE; + break; + } + + default: + break; + } +} + +/** + * @brief Main processing thread for incoming ADC buffers. + */ +void ir_recv_thread(void *arg1, void *arg2, void *arg3) +{ + while (1) + { + k_sem_take(&adc_sem, K_FOREVER); + + while (read_idx != write_idx) + { + int16_t *buf = adc_buffers[read_idx]; + + for (int i = 0; i < SAMPLES_PER_BUFFER; i++) + { + /* De-interleave and process IR channels */ + process_ir_sample(&channels[0], buf[i * ADC_CHANNELS + 0]); + process_ir_sample(&channels[1], buf[i * ADC_CHANNELS + 1]); + process_ir_sample(&channels[2], buf[i * ADC_CHANNELS + 2]); + + /* Optional: Handle battery sample at buf[i * ADC_CHANNELS + 3] */ + } + + read_idx = (read_idx + 1) % BUFFER_COUNT; + } + } +} + +K_THREAD_DEFINE(ir_recv_tid, 2048, ir_recv_thread, NULL, NULL, NULL, 2, 0, 0); + +/** + * @brief Initialization of the IR receiver module. + */ +int ir_recv_init(void) +{ + k_sem_init(&adc_sem, 0, BUFFER_COUNT); + + for (int i = 0; i < 3; i++) + { + channels[i].state = IR_STATE_IDLE; + } + + LOG_INF("IR Receiver initialized. Mode: %s", + IS_ENABLED(CONFIG_IR_RECV_SIMULATOR) ? "Simulator" : "Hardware"); + + return 0; +} \ No newline at end of file diff --git a/firmware/libs/ir_send/Kconfig b/firmware/libs/ir_send/Kconfig deleted file mode 100644 index c14339e..0000000 --- a/firmware/libs/ir_send/Kconfig +++ /dev/null @@ -1,71 +0,0 @@ -config IR_SEND - bool "IR Send Library" - help - Enable IR transmission library for laser tag system. - Provides PWM-based IR carrier generation with pulse-distance coding. - -if IR_SEND - -menu "IR Protocol Configuration" - -config IR_SEND_CARRIER_HZ - int "IR carrier frequency (Hz)" - default 38000 - range 30000 45000 - help - Carrier frequency for PWM generation. Standard value is 38 kHz - for TSOP48xx receivers. Adjust only if using different receivers. - -config IR_SEND_DUTY_CYCLE_PERCENT - int "Carrier duty cycle (%)" - default 50 - range 25 75 - help - PWM duty cycle percentage. Some receivers prefer 33%, others 50%. - Default 50% works with most TSOP-series receivers. - -config IR_PROTO_BASE_US - int "IR Basistakt (Mikrosekunden)" - default 600 - range 300 1000 - help - Gemeinsamer Basistakt für Mark/Space/Start. Standard 600 µs (Sony SIRC ähnlich). - -config IR_PROTO_START_MULT - int "Startburst Faktor" - default 4 - range 2 8 - help - Startburst-Dauer = Basistakt × Faktor. Standard 4× zur sicheren Synchronisation. - -config IR_PROTO_GAP_MULT - int "Start-Gap Faktor" - default 1 - range 0 4 - help - Gap nach Startburst (Träger AUS) = Basistakt × Faktor. 0 deaktiviert Gap. - -config IR_PROTO_MARK_MULT - int "Mark Faktor" - default 1 - range 1 2 - help - Mark-Dauer = Basistakt × Faktor. Standard 1×. - -config IR_PROTO_SPACE0_MULT - int "Space0 Faktor (Bit 0)" - default 1 - range 1 3 - help - Space für Bit 0 = Basistakt × Faktor. Standard 1×. - -config IR_PROTO_SPACE1_MULT - int "Space1 Faktor (Bit 1)" - default 2 - range 1 4 - help - Space für Bit 1 = Basistakt × Faktor. Standard 2× (doppelte Basiszeit). - -endmenu - -endif