sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s

This commit is contained in:
2026-02-16 13:31:49 +01:00
parent 3febb6411e
commit 6376185622
5 changed files with 397 additions and 152 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -3,6 +3,28 @@
#include <zephyr/device.h>
// 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).
*

View File

@@ -1,12 +1,345 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <string.h>
#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;
}

View File

@@ -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