Files
Pluto-SDR/src/receiver.c
Maxim 23b60c6fa2 group-FEC 16+2: стирающее восстановление кадров без обратного канала (§10 п.6)
Групповой 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 + перенумерация роадмапа — отдельным коммитом после верификации.
2026-07-15 09:18:17 +03:00

544 lines
25 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.52 мс. -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;
}