Групповой FEC над GF(256), RAID-6-стиль: 16 кадров данных + P(⊕) + Q(Σgⁱ·dᵢ)
закрывают до 2 стёртых кадров на группу. Потеря (пропуск seq или битый CRC) —
это стирание с известной позицией, поэтому 2 паритета оптимальны (overhead 12.5%,
~99% передач 10 МБ байт-в-байт при PER 0.26%).
- src/group_fec.{c,h}: GF(256) polynom 0x11d, gf_encode/gf_recover + self-test
(make gftest, нативный cc). Восстановление 1 стирания через P или Q, 2 — P+Q.
- transmitter.c: флаг -G, вынесен tx_frame(); копит группу, шлёт k данных +
P/Q с флагами в hdr[11]. Мета паритета: k′ + длина хвостового кадра.
- receiver.c: групповой ассемблер в потоке C, автодетект по hdr[11], финализация
группы на смене базы seq и на выходе потока. Счётчики Rec/GrpF в статистике.
- hdr[11] (был резерв): bit7 режим, bit6 паритет, bit5 Q, bits4-0 idx. RS/CRC
слой прозрачен — геометрия кадра не тронута, PHY-бенчмарк не требуется.
- test_file_link.sh: GROUPFEC=1 по умолчанию (TX -G, RX автодетект).
README §12.7 + перенумерация роадмапа — отдельным коммитом после верификации.
544 lines
25 KiB
C
544 lines
25 KiB
C
// receiver.c — OFDM-приёмник с RS(255,223) FEC и частотной коррекцией
|
||
// Трёхстадийный конвейер (README §12.5, BENCHMARK «Разбивка по стадиям»):
|
||
// поток A (ядро 1): iio_buffer_refill + int16→float → кольцо сэмплов
|
||
// поток B (ядро 0): ТОЛЬКО ofdmflexframesync_execute; callback кладёт сырой
|
||
// кадр в очередь fq (RS с него снят — он насыщал ядро 0)
|
||
// поток C (ядро 1): из fq → seq-детектор + RS-декод + CRC + запись
|
||
// Замер: sync сам 1.65 Msps < 1.92, RS 26% эфира. Вынос RS на ядро 1 роняет
|
||
// p_min ~8 мс → ~1.5–2 мс. -p 0 на этом PHY недостижим (осознанно, §12.5).
|
||
|
||
#define _GNU_SOURCE // pthread_setaffinity_np, cpu_set_t
|
||
#include "common.h"
|
||
#include "group_fec.h"
|
||
#include <pthread.h>
|
||
#include <sched.h>
|
||
|
||
#define BUF_SAMPLES 32768
|
||
#define RING_SLOTS 16 // 16 × 17 мс = 273 мс поглощения всплесков
|
||
#define FQ_SLOTS 32 // очередь кадров B→C, ~147 КБ .bss
|
||
#define FQ_PAY_MAX (18 * RS_ENC) // 4590: ≤18 блоков RS (техдолг #2)
|
||
#define RX_GAIN 50.0
|
||
#define CFO_FILTER_ALPHA 0.95f
|
||
#define CFO_MAX_VALID 0.5f
|
||
|
||
extern volatile sig_atomic_t stop;
|
||
|
||
// --- Кольцо сэмплов A→B (SPSC) ------------------------------------------
|
||
// tail пишет только A, head — только B; count/eof/overruns под мьютексом.
|
||
typedef struct {
|
||
liquid_float_complex data[BUF_SAMPLES];
|
||
int ns; // фактическое число отсчётов (частичный refill)
|
||
} slot_t;
|
||
|
||
static slot_t ring_slots[RING_SLOTS]; // 4 МБ .bss
|
||
static struct {
|
||
int head, tail, count, eof;
|
||
uint64_t overruns; // refill'ы, выброшенные из-за полного кольца
|
||
pthread_mutex_t mtx;
|
||
pthread_cond_t not_empty;
|
||
} ring = {
|
||
.mtx = PTHREAD_MUTEX_INITIALIZER,
|
||
.not_empty = PTHREAD_COND_INITIALIZER,
|
||
};
|
||
|
||
// --- Очередь кадров B→C (SPSC) ------------------------------------------
|
||
// B (producer) копирует сюда сырой кадр и НИКОГДА не блокируется (drop-newest),
|
||
// поэтому C, выйдя раньше, не заклинит B. tail пишет только B, head — только C.
|
||
typedef struct {
|
||
uint8_t hdr[HDR_SIZE]; // 12 Б, парсит C
|
||
uint8_t pay[FQ_PAY_MAX]; // сырой RS-payload
|
||
unsigned pay_len;
|
||
int pay_valid; // liquid-CRC по payload
|
||
} fq_slot_t;
|
||
|
||
static fq_slot_t fq_slots[FQ_SLOTS];
|
||
static struct {
|
||
int head, tail, count, eof;
|
||
uint64_t drops; // переполнение очереди + негабаритные кадры
|
||
pthread_mutex_t mtx;
|
||
pthread_cond_t not_empty;
|
||
} fq = {
|
||
.mtx = PTHREAD_MUTEX_INITIALIZER,
|
||
.not_empty = PTHREAD_COND_INITIALIZER,
|
||
};
|
||
|
||
static struct iio_buffer *g_buf = NULL;
|
||
static ofdmflexframesync g_fs = NULL;
|
||
|
||
// Счётчики B (пишет callback): calls/hdr_ok/crc_raw + телеметрия.
|
||
static uint64_t calls = 0, hdr_ok = 0, crc_raw = 0;
|
||
static float rssi = -100, evm = 0, cfo_est = 0.0f;
|
||
// Счётчики C (пишет process_frame): целостность и вывод.
|
||
static uint64_t rs_saved = 0, crc_final = 0, total_bytes = 0;
|
||
static uint64_t rs_fail = 0, lost = 0;
|
||
static uint64_t grp_recovered = 0, grp_failed = 0; // group-FEC: восст. кадров / провал. групп
|
||
static uint32_t last_seq = 0;
|
||
static int seq_init = 0;
|
||
// Разделяемых на запись счётчиков нет. main читает их раз в секунду для
|
||
// телеметрии без синхронизации: на 32-бит ARM возможен косметический «рваный»
|
||
// показ 64-бит значения — на итоговую строку (печатается после join) не влияет.
|
||
|
||
static fec decoder = NULL;
|
||
static int fec_on = 0;
|
||
|
||
// callback потока B — лёгкий: телеметрия + копия сырого кадра в очередь C.
|
||
static int callback(unsigned char *hdr, int hdr_valid,
|
||
unsigned char *pay, unsigned int pay_len,
|
||
int pay_valid, framesyncstats_s stats,
|
||
void *user) {
|
||
(void)user;
|
||
calls++;
|
||
rssi = stats.rssi;
|
||
evm = stats.evm;
|
||
|
||
// CFO — только телеметрия: ofdmflexframesync сам компенсирует смещение
|
||
// по преамбуле. Внешний NCO-контур убран (техдолг #4).
|
||
if (fabsf(stats.cfo) < CFO_MAX_VALID)
|
||
cfo_est = CFO_FILTER_ALPHA * cfo_est + (1.0f - CFO_FILTER_ALPHA) * stats.cfo;
|
||
|
||
if (!hdr_valid) return 0;
|
||
hdr_ok++;
|
||
if (pay_valid) crc_raw++;
|
||
|
||
// Копия кадра в очередь C. Негабарит/полная очередь → дроп со счётчиком
|
||
// (drop-newest: B не блокируется). Разбор seq/RS/CRC — уже в потоке C.
|
||
int oversize = (pay_len > FQ_PAY_MAX);
|
||
pthread_mutex_lock(&fq.mtx);
|
||
int reject = oversize || (fq.count == FQ_SLOTS);
|
||
if (reject) fq.drops++;
|
||
pthread_mutex_unlock(&fq.mtx);
|
||
if (reject) return 0;
|
||
|
||
fq_slot_t *fs = &fq_slots[fq.tail]; // tail трогает только producer B
|
||
memcpy(fs->hdr, hdr, HDR_SIZE);
|
||
memcpy(fs->pay, pay, pay_len);
|
||
fs->pay_len = pay_len;
|
||
fs->pay_valid = pay_valid;
|
||
|
||
pthread_mutex_lock(&fq.mtx);
|
||
fq.tail = (fq.tail + 1) % FQ_SLOTS;
|
||
fq.count++;
|
||
pthread_cond_signal(&fq.not_empty);
|
||
pthread_mutex_unlock(&fq.mtx);
|
||
return 0;
|
||
}
|
||
|
||
// --- Групповой стирающий FEC (поток C, README §12.7) --------------------
|
||
// Собирает группу из 16 кадров данных + 2 паритетных (P,Q). Пропавший на эфире
|
||
// или битый по CRC кадр = стирание; ≤2 стираний на группу закрываются gf_recover.
|
||
// Всё состояние — только поток C (как остальные C-счётчики), синхронизации нет.
|
||
static struct {
|
||
int active;
|
||
uint32_t base; // seq первого кадра группы (= seq − idx)
|
||
int k; // число кадров данных (из меты паритета)
|
||
int k_known; // k получен из паритета
|
||
int maxidx; // макс. полученный idx данных — оценка k без паритета
|
||
uint8_t data[GF_K][GF_CHUNK];
|
||
uint16_t dlen[GF_K]; // фактическая длина каждого кадра данных
|
||
uint8_t have[GF_K]; // 1 = кадр idx получен и валиден
|
||
uint8_t P[GF_CHUNK], Q[GF_CHUNK];
|
||
int haveP, haveQ;
|
||
uint16_t tail_len; // длина последнего кадра данных (из меты)
|
||
} g;
|
||
|
||
static void group_reset(uint32_t base) {
|
||
memset(g.have, 0, sizeof(g.have));
|
||
memset(g.dlen, 0, sizeof(g.dlen));
|
||
g.active = 1; g.base = base; g.k = 0; g.k_known = 0;
|
||
g.maxidx = -1; g.haveP = g.haveQ = 0; g.tail_len = 0;
|
||
}
|
||
|
||
// Финализировать текущую группу: восстановить стирания и записать данные по idx.
|
||
static void group_flush(void) {
|
||
if (!g.active) return;
|
||
int k = g.k_known ? g.k : (g.maxidx + 1);
|
||
if (k <= 0 || k > GF_K) { g.active = 0; return; }
|
||
|
||
int erasures = 0;
|
||
for (int i = 0; i < k; i++) if (!g.have[i]) erasures++;
|
||
|
||
if (erasures > 0) {
|
||
if (gf_recover(g.data, g.have, k, g.P, g.haveP, g.Q, g.haveQ)) {
|
||
grp_recovered += erasures;
|
||
for (int i = 0; i < k; i++) if (!g.have[i]) {
|
||
// прочие кадры полные 1024, последний — tail_len
|
||
g.dlen[i] = (i == k - 1 && g.tail_len) ? g.tail_len : GF_CHUNK;
|
||
g.have[i] = 1;
|
||
}
|
||
} else {
|
||
grp_failed++; // >2 стираний либо не хватило паритета
|
||
}
|
||
}
|
||
|
||
for (int i = 0; i < k; i++) {
|
||
if (!g.have[i]) continue; // невосстановимая дыра
|
||
uint16_t len = g.dlen[i] > GF_CHUNK ? GF_CHUNK : g.dlen[i];
|
||
fwrite(g.data[i], 1, len, stdout);
|
||
total_bytes += len;
|
||
crc_final++;
|
||
}
|
||
fflush(stdout);
|
||
g.active = 0;
|
||
}
|
||
|
||
// Разложить один декодированный group-FEC кадр по группе.
|
||
static void group_route(uint8_t flags, uint32_t seq,
|
||
const uint8_t *dec, size_t dec_len, int ok) {
|
||
int idx = flags & GF_IDX_MASK;
|
||
int is_par = flags & GF_FLAG_PAR;
|
||
uint32_t base = seq - idx; // idx=k для P, k+1 для Q → та же база
|
||
|
||
if (!g.active || base != g.base) { // новая группа → финализировать прежнюю
|
||
group_flush();
|
||
group_reset(base);
|
||
}
|
||
if (!ok) return; // битый кадр остаётся стиранием
|
||
|
||
if (!is_par) {
|
||
if (idx < GF_K && dec_len <= GF_CHUNK) {
|
||
memcpy(g.data[idx], dec, dec_len);
|
||
if (dec_len < GF_CHUNK) memset(g.data[idx] + dec_len, 0, GF_CHUNK - dec_len);
|
||
g.dlen[idx] = (uint16_t)dec_len;
|
||
g.have[idx] = 1;
|
||
if (idx > g.maxidx) g.maxidx = idx;
|
||
}
|
||
} else if (dec_len >= GF_PAR_PAY) { // паритет: [0..1] мета, [2..] P/Q
|
||
uint16_t meta = ((uint16_t)dec[0] << 8) | dec[1];
|
||
g.k = GF_META_K(meta);
|
||
g.tail_len = GF_META_TAIL(meta);
|
||
g.k_known = 1;
|
||
if (flags & GF_FLAG_Q) { memcpy(g.Q, dec + GF_META, GF_CHUNK); g.haveQ = 1; }
|
||
else { memcpy(g.P, dec + GF_META, GF_CHUNK); g.haveP = 1; }
|
||
}
|
||
}
|
||
|
||
// Разбор одного кадра в потоке C: seq-детектор + RS + CRC-поверх-RS, затем
|
||
// маршрутизация (прямая запись для legacy, сборка группы для group-FEC).
|
||
// Единственный писатель в stdout — этот поток, порядок сохранён (fq FIFO).
|
||
static void process_frame(fq_slot_t *fs) {
|
||
const uint8_t *hdr = fs->hdr;
|
||
uint8_t *pay = fs->pay; // не const: fec_decode() liquid не const-correct
|
||
unsigned pay_len = fs->pay_len;
|
||
int pay_valid = fs->pay_valid;
|
||
|
||
// Детектор пропусков seq (техдолг #6). Кадр, выброшенный переполнением fq,
|
||
// тоже проявится здесь как дыра seq → в `lost`; счётчик FQdrop разделяет
|
||
// причины (эфирная потеря против переполнения очереди). В group-FEC потеря
|
||
// одновременно фиксируется как стирание в group_route по idx.
|
||
uint32_t seq = ((uint32_t)hdr[2] << 24) | ((uint32_t)hdr[3] << 16) |
|
||
((uint32_t)hdr[4] << 8) | hdr[5];
|
||
if (seq_init && seq > last_seq + 1) {
|
||
lost += seq - last_seq - 1;
|
||
fprintf(stderr, "\n[ПОТЕРЯ] seq %u→%u, дыра %u кадров\n",
|
||
last_seq, seq, seq - last_seq - 1);
|
||
}
|
||
last_seq = seq;
|
||
seq_init = 1;
|
||
|
||
uint16_t original_len = (hdr[6] << 8) | hdr[7];
|
||
uint8_t last_block_bytes = hdr[10];
|
||
uint8_t flags = hdr[11];
|
||
|
||
// Декод payload → dec/dec_len, ok=1 при пройденном CRC-поверх-RS.
|
||
static uint8_t recovered[4096]; // поток C один → static безопасен
|
||
const uint8_t *dec = NULL;
|
||
size_t dec_len = 0;
|
||
int ok = 0;
|
||
|
||
if (fec_on && decoder && pay_len >= RS_ENC) {
|
||
size_t nblocks = pay_len / RS_ENC;
|
||
// Защита в глубину: B уже отсёк pay_len>FQ_PAY_MAX, но guard оставлен
|
||
// (recovered[4096] вмещает 4096/RS_DATA = 18 блоков, техдолг #2).
|
||
if (nblocks == 0 || nblocks > 4096 / RS_DATA) goto route;
|
||
|
||
for (size_t b = 0; b < nblocks; b++) {
|
||
// fec_decode() liquid всегда "успешен" — код возврата бесполезен.
|
||
fec_decode(decoder, RS_DATA, pay + b * RS_ENC,
|
||
recovered + b * RS_DATA);
|
||
}
|
||
|
||
// Настоящий критерий целостности — CRC поверх декода (техдолг #3):
|
||
// fec_decode отдаёт мусор молча, поэтому проверяем сами.
|
||
size_t framed_len = (nblocks - 1) * RS_DATA +
|
||
(last_block_bytes > 0 ? last_block_bytes : RS_DATA);
|
||
if (framed_len > nblocks * RS_DATA) framed_len = nblocks * RS_DATA;
|
||
|
||
if (original_len == 0 || (size_t)original_len + 4 > framed_len) {
|
||
rs_fail++;
|
||
goto route;
|
||
}
|
||
unsigned int rx_crc = crc_generate_key(LIQUID_CRC_32, recovered, original_len);
|
||
unsigned int tx_crc = ((uint32_t)recovered[original_len] << 24) |
|
||
((uint32_t)recovered[original_len + 1] << 16) |
|
||
((uint32_t)recovered[original_len + 2] << 8) |
|
||
recovered[original_len + 3];
|
||
if (rx_crc != tx_crc) {
|
||
rs_fail++; // RS не справился, кадр битый — не пишем
|
||
goto route;
|
||
}
|
||
if (!pay_valid) rs_saved++; // liquid CRC по payload не прошёл, а наш
|
||
// CRC поверх RS — да: значит RS реально спас
|
||
dec = recovered; dec_len = original_len; ok = 1;
|
||
} else if (pay_valid && original_len > 0 && original_len <= pay_len) {
|
||
dec = pay; dec_len = original_len; ok = 1;
|
||
}
|
||
|
||
route:
|
||
if (flags & GF_FLAG_MODE) {
|
||
group_route(flags, seq, dec, dec_len, ok);
|
||
} else {
|
||
if (g.active) group_flush(); // TX не мешает режимы, но подстрахуемся
|
||
if (ok) {
|
||
fwrite(dec, 1, dec_len, stdout);
|
||
fflush(stdout);
|
||
total_bytes += dec_len;
|
||
crc_final++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Привязка потока к ядру: ядро 0 — только sync, ядро 1 — захват+FEC+IO
|
||
// (доктрина README §2). Ошибка не фатальна — планировщик разведёт сам.
|
||
static void pin_to_cpu(int cpu) {
|
||
cpu_set_t cs;
|
||
CPU_ZERO(&cs);
|
||
CPU_SET(cpu, &cs);
|
||
if (pthread_setaffinity_np(pthread_self(), sizeof(cs), &cs) != 0)
|
||
fprintf(stderr, "[warn] не удалось привязать поток к ядру %d\n", cpu);
|
||
}
|
||
|
||
// Ждать данных в SPSC-кольце (100 мс страховка от lost wakeup). Возврат:
|
||
// 1 — есть слот (мьютекс m ЗАБЛОКИРОВАН на выходе), 0 — eof/stop и пусто (m разблокирован).
|
||
static int wait_slot(pthread_mutex_t *m, pthread_cond_t *cv, int *count, int *eof) {
|
||
pthread_mutex_lock(m);
|
||
while (*count == 0 && !*eof && !stop) {
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_REALTIME, &ts);
|
||
ts.tv_nsec += 100000000L;
|
||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||
pthread_cond_timedwait(cv, m, &ts);
|
||
}
|
||
if (*count == 0) { pthread_mutex_unlock(m); return 0; }
|
||
return 1;
|
||
}
|
||
|
||
// Поток A (ядро 1): блокирующий refill + конвертация int16→float в слот кольца.
|
||
static void *acq_thread(void *arg) {
|
||
(void)arg;
|
||
pin_to_cpu(1);
|
||
while (!stop) {
|
||
ssize_t n = iio_buffer_refill(g_buf);
|
||
if (n < 0) {
|
||
if (stop) break; // EINTR при сигнале
|
||
usleep(500);
|
||
continue;
|
||
}
|
||
|
||
pthread_mutex_lock(&ring.mtx);
|
||
int full = (ring.count == RING_SLOTS);
|
||
if (full) ring.overruns++;
|
||
pthread_mutex_unlock(&ring.mtx);
|
||
if (full) continue; // drop-newest: кольцо полно — выбрасываем refill
|
||
|
||
slot_t *s = &ring_slots[ring.tail]; // tail трогает только producer A
|
||
int16_t *bb = (int16_t *)iio_buffer_start(g_buf);
|
||
int ns = (iio_buffer_end(g_buf) - iio_buffer_start(g_buf)) / sizeof(int16_t) / 2;
|
||
if (ns > BUF_SAMPLES) ns = BUF_SAMPLES;
|
||
for (int i = 0; i < ns; i++) {
|
||
s->data[i] = ((float)bb[2*i] / 2048.0f) +
|
||
((float)bb[2*i+1] / 2048.0f) * _Complex_I;
|
||
}
|
||
s->ns = ns;
|
||
|
||
pthread_mutex_lock(&ring.mtx);
|
||
ring.tail = (ring.tail + 1) % RING_SLOTS;
|
||
ring.count++;
|
||
pthread_cond_signal(&ring.not_empty);
|
||
pthread_mutex_unlock(&ring.mtx);
|
||
}
|
||
pthread_mutex_lock(&ring.mtx);
|
||
ring.eof = 1;
|
||
pthread_cond_broadcast(&ring.not_empty); // гарантированно будим B
|
||
pthread_mutex_unlock(&ring.mtx);
|
||
return NULL;
|
||
}
|
||
|
||
// Поток B (ядро 0): только синхронизатор. Callback кладёт кадры в fq.
|
||
static void *dsp_thread(void *arg) {
|
||
(void)arg;
|
||
pin_to_cpu(0);
|
||
for (;;) {
|
||
if (!wait_slot(&ring.mtx, &ring.not_empty, &ring.count, &ring.eof))
|
||
break; // eof/stop и пусто
|
||
slot_t *s = &ring_slots[ring.head];
|
||
pthread_mutex_unlock(&ring.mtx); // execute — без мьютекса
|
||
|
||
ofdmflexframesync_execute(g_fs, s->data, s->ns);
|
||
|
||
pthread_mutex_lock(&ring.mtx);
|
||
ring.head = (ring.head + 1) % RING_SLOTS;
|
||
ring.count--;
|
||
pthread_mutex_unlock(&ring.mtx);
|
||
}
|
||
pthread_mutex_lock(&fq.mtx); // сигналим C: кадров больше нет
|
||
fq.eof = 1;
|
||
pthread_cond_broadcast(&fq.not_empty);
|
||
pthread_mutex_unlock(&fq.mtx);
|
||
return NULL;
|
||
}
|
||
|
||
// Поток C (ядро 1): RS-декод + CRC + запись из очереди кадров.
|
||
static void *fec_thread(void *arg) {
|
||
(void)arg;
|
||
pin_to_cpu(1);
|
||
for (;;) {
|
||
if (!wait_slot(&fq.mtx, &fq.not_empty, &fq.count, &fq.eof))
|
||
break;
|
||
fq_slot_t *fs = &fq_slots[fq.head];
|
||
pthread_mutex_unlock(&fq.mtx);
|
||
|
||
process_frame(fs);
|
||
|
||
pthread_mutex_lock(&fq.mtx);
|
||
fq.head = (fq.head + 1) % FQ_SLOTS;
|
||
fq.count--;
|
||
pthread_mutex_unlock(&fq.mtx);
|
||
}
|
||
group_flush(); // дописать последнюю группу файла (нет кадра-триггера)
|
||
return NULL;
|
||
}
|
||
|
||
int main(int argc, char *argv[]) {
|
||
long long freq = DEFAULT_FREQ, rate = DEFAULT_RATE, bw = DEFAULT_BW;
|
||
const char *uri = RX_URI_DEFAULT;
|
||
const char *fec_opt = "rs8";
|
||
|
||
int opt;
|
||
while ((opt = getopt(argc, argv, "f:r:b:u:c:h")) != -1) {
|
||
switch (opt) {
|
||
case 'f': freq = atoll(optarg); break;
|
||
case 'r': rate = atoll(optarg); break;
|
||
case 'b': bw = atoll(optarg); break;
|
||
case 'u': uri = optarg; break;
|
||
case 'c': fec_opt = optarg; break;
|
||
case 'h':
|
||
print_usage(argv[0], "ПРИЁМНИК");
|
||
fprintf(stderr, "\nСПЕЦИФИЧНЫЕ ОПЦИИ RX:\n");
|
||
fprintf(stderr, " Усиление фиксированное: %.0f дБ\n", RX_GAIN);
|
||
fprintf(stderr, " Частотная коррекция: адаптивная (α=%.2f)\n", CFO_FILTER_ALPHA);
|
||
fprintf(stderr, "\nОПТИМАЛЬНЫЙ ЗАПУСК:\n");
|
||
fprintf(stderr, " %s -f %lld -r %lld -b %lld -c rs8 > output.bin\n",
|
||
argv[0], DEFAULT_FREQ, DEFAULT_RATE, DEFAULT_BW);
|
||
return 0;
|
||
default: return 1;
|
||
}
|
||
}
|
||
|
||
fec_on = !strcmp(fec_opt, "rs8");
|
||
setup_signal_handlers();
|
||
gf_init(); // group-FEC автодетектится по hdr[11] первого кадра
|
||
|
||
print_config("ПРИЁМНИК", freq, rate, bw, fec_on);
|
||
fprintf(stderr, "Усиление: %.0f дБ (ручное)\n", RX_GAIN);
|
||
fprintf(stderr, "CFO фильтр: α=%.2f\n", CFO_FILTER_ALPHA);
|
||
fprintf(stderr, "URI: %s\n", uri);
|
||
fprintf(stderr, "Конвейер: 3 стадии (A захват / B sync / C FEC+IO)\n");
|
||
fprintf(stderr, "═══════════════════════════════════════════════════\n");
|
||
|
||
struct iio_context *ctx = NULL;
|
||
struct iio_device *phy = NULL, *rx = NULL;
|
||
if (pluto_init(uri, &ctx, &phy, &rx, 0) != 0) return 1;
|
||
|
||
if (pluto_configure(phy, freq, rate, bw, RX_GAIN, 0) != 0) {
|
||
LOG_ERROR("Настройка радиотракта не удалась");
|
||
iio_context_destroy(ctx);
|
||
return 1;
|
||
}
|
||
|
||
iio_channel_enable(iio_device_find_channel(rx, "voltage0", false));
|
||
iio_channel_enable(iio_device_find_channel(rx, "voltage1", false));
|
||
|
||
// Глубже DMA-очередь ядра (по умолчанию 4): 8 × 17 мс = 137 мс запаса,
|
||
// кроет задержки A от RS-всплесков C на общем ядре 1.
|
||
if (iio_device_set_kernel_buffers_count(rx, 8) != 0)
|
||
fprintf(stderr, "[warn] не удалось увеличить kernel_buffers_count\n");
|
||
|
||
struct iio_buffer *buf = iio_device_create_buffer(rx, BUF_SAMPLES, false);
|
||
if (!buf) {
|
||
fprintf(stderr, "Ошибка создания буфера приёма\n");
|
||
iio_context_destroy(ctx);
|
||
return 1;
|
||
}
|
||
|
||
// Инициализация FEC
|
||
if (fec_on) {
|
||
decoder = fec_create(LIQUID_FEC_RS_M8, NULL);
|
||
if (!decoder) {
|
||
fprintf(stderr, "Ошибка создания FEC-декодера\n");
|
||
}
|
||
}
|
||
|
||
ofdmflexframesync fs = ofdmflexframesync_create(
|
||
OFDM_M, OFDM_CP, OFDM_TAPER, NULL, callback, NULL);
|
||
if (!fs) {
|
||
fprintf(stderr, "Ошибка создания OFDM-синхронизатора\n");
|
||
if (decoder) fec_destroy(decoder);
|
||
iio_buffer_destroy(buf);
|
||
iio_context_destroy(ctx);
|
||
return 1;
|
||
}
|
||
// Тот же 12-байтовый заголовок, что и на TX (иначе байты 8..11 — мусор)
|
||
ofdmflexframesync_set_header_len(fs, HDR_SIZE);
|
||
|
||
fprintf(stderr, "Приёмник готов. Ожидание сигнала...\n\n");
|
||
fprintf(stderr, "Кадры | Загол. | CRC | RS испр | RS бит | Потери | Overr | FQdrp | Rec | GrpF | Вывод | RSSI | EVM | CFO\n");
|
||
fprintf(stderr, "--------+--------+------+---------+--------+--------+--------+--------+--------+--------+--------+-------+-------+----------\n");
|
||
|
||
// Три стадии на двух ядрах. main крутит статистику 1 Гц и ждёт stop, затем
|
||
// join A→B→C: A ставит ring.eof, B дочищает кольцо и ставит fq.eof, C
|
||
// дочищает очередь. Producer'ы (A, B) не блокируются → цепочка без дедлоков.
|
||
g_buf = buf;
|
||
g_fs = fs;
|
||
pthread_t ta, tb, tc;
|
||
pthread_create(&tc, NULL, fec_thread, NULL);
|
||
pthread_create(&tb, NULL, dsp_thread, NULL);
|
||
pthread_create(&ta, NULL, acq_thread, NULL);
|
||
|
||
time_t last = time(NULL);
|
||
while (!stop) {
|
||
struct timespec nap = { .tv_sec = 0, .tv_nsec = 200000000L }; // 200 мс
|
||
nanosleep(&nap, NULL);
|
||
time_t now = time(NULL);
|
||
if (now > last) {
|
||
fprintf(stderr, "\r%6llu | %5llu | %4llu | %6llu | %5llu | %6llu | %6llu | %6llu | %6llu | %6llu | %6llu | %+5.0f | %+5.1f | %+8.4f",
|
||
(unsigned long long)calls, (unsigned long long)hdr_ok,
|
||
(unsigned long long)crc_raw, (unsigned long long)rs_saved,
|
||
(unsigned long long)rs_fail, (unsigned long long)lost,
|
||
(unsigned long long)ring.overruns, (unsigned long long)fq.drops,
|
||
(unsigned long long)grp_recovered, (unsigned long long)grp_failed,
|
||
(unsigned long long)crc_final, rssi, evm, cfo_est);
|
||
fflush(stderr);
|
||
last = now;
|
||
}
|
||
}
|
||
|
||
pthread_join(ta, NULL);
|
||
pthread_join(tb, NULL);
|
||
pthread_join(tc, NULL);
|
||
|
||
fprintf(stderr, "\n\nПРИЁМ ЗАВЕРШЁН\n");
|
||
fprintf(stderr, "Кадров: %llu | Заголовков: %llu | CRC: %llu | RS испр: %llu | RS битых: %llu | Потери seq: %llu | Overrun: %llu | FQ drops: %llu | GF восст: %llu | GF провал: %llu | Вывод: %llu | Байт: %llu\n",
|
||
(unsigned long long)calls, (unsigned long long)hdr_ok,
|
||
(unsigned long long)crc_raw, (unsigned long long)rs_saved,
|
||
(unsigned long long)rs_fail, (unsigned long long)lost,
|
||
(unsigned long long)ring.overruns, (unsigned long long)fq.drops,
|
||
(unsigned long long)grp_recovered, (unsigned long long)grp_failed,
|
||
(unsigned long long)crc_final, (unsigned long long)total_bytes);
|
||
|
||
if (decoder) fec_destroy(decoder);
|
||
ofdmflexframesync_destroy(fs);
|
||
iio_buffer_destroy(buf);
|
||
iio_context_destroy(ctx);
|
||
return 0;
|
||
}
|