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;
}