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 + перенумерация роадмапа — отдельным коммитом после верификации.
This commit is contained in:
Maxim
2026-07-15 09:18:17 +03:00
parent af370e5443
commit 23b60c6fa2
7 changed files with 495 additions and 117 deletions

View File

@@ -33,7 +33,11 @@ PHY: OFDM через liquid-dsp 1.8.0 + FFTW/NEON, FEC RS(255,223).
## Формат кадра (заголовок 12 Б) ## Формат кадра (заголовок 12 Б)
0-1: 0xF0 0xAA | 2-5: seq u32 BE | 6-7: len u16 BE | 8-9: nblocks | 0-1: 0xF0 0xAA | 2-5: seq u32 BE | 6-7: len u16 BE | 8-9: nblocks |
10: last_block_bytes | 11: резерв 10: last_block_bytes | 11: group-FEC флаги (§10 п.6)
Байт 11: bit7 GF_MODE (группа активна), bit6 GF_PARITY (P/Q-кадр),
bit5 GF_Q (Q иначе P), bits4-0 idx кадра в группе (0..17). 0 = legacy-кадр.
Group-FEC 16+2: 16 кадров данных + P(⊕) + Q(Σgⁱ) над GF(256), закрывает ≤2
стёртых кадра/группу без обратного канала (src/group_fec.c). RX автодетект.
КРИТИЧНО: liquid по умолчанию передаёт 8 Б заголовка. Обязательно КРИТИЧНО: liquid по умолчанию передаёт 8 Б заголовка. Обязательно
`ofdmflexframegen_set_header_len(fg,12)` и `ofdmflexframegen_set_header_len(fg,12)` и
`ofdmflexframesync_set_header_len(fs,12)` на обеих сторонах. `ofdmflexframesync_set_header_len(fs,12)` на обеих сторонах.

View File

@@ -25,18 +25,24 @@ all: $(BIN)/transmitter $(BIN)/receiver $(BIN)/ofdm_bench
$(BIN): $(BIN):
mkdir -p $(BIN) mkdir -p $(BIN)
$(BIN)/transmitter: $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/common.h $(DEPS_LIBS) | $(BIN) $(BIN)/transmitter: $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/common.h $(SRC)/group_fec.h $(DEPS_LIBS) | $(BIN)
$(CC) $(CFLAGS) $(SRC)/transmitter.c $(SRC)/common.c -o $@ $(LDFLAGS) -lad9361 -liio $(LDLIBS) $(CC) $(CFLAGS) $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c -o $@ $(LDFLAGS) -lad9361 -liio $(LDLIBS)
$(BIN)/receiver: $(SRC)/receiver.c $(SRC)/common.c $(SRC)/common.h $(DEPS_LIBS) | $(BIN) $(BIN)/receiver: $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/common.h $(SRC)/group_fec.h $(DEPS_LIBS) | $(BIN)
$(CC) $(CFLAGS) $(SRC)/receiver.c $(SRC)/common.c -o $@ $(LDFLAGS) -lad9361 -liio $(LDLIBS) $(CC) $(CFLAGS) $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c -o $@ $(LDFLAGS) -lad9361 -liio $(LDLIBS)
$(BIN)/ofdm_bench: $(SRC)/ofdm_bench.c | $(BIN) $(BIN)/ofdm_bench: $(SRC)/ofdm_bench.c | $(BIN)
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS) $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS)
tx: $(BIN)/transmitter # Оффлайн-самопроверка GF(256) — нативная сборка (gcc хоста, напр. RPi/aarch64)
rx: $(BIN)/receiver $(BIN)/gftest: $(SRC)/group_fec.c $(SRC)/group_fec.h | $(BIN)
bench: $(BIN)/ofdm_bench cc -O2 -Wall -DGF_SELFTEST $(SRC)/group_fec.c -o $@
tx: $(BIN)/transmitter
rx: $(BIN)/receiver
bench: $(BIN)/ofdm_bench
gftest: $(BIN)/gftest
$(BIN)/gftest
# --- деплой (scp -O: на прошивке v0.38 нет sftp-server) --- # --- деплой (scp -O: на прошивке v0.38 нет sftp-server) ---
deploy: all deploy: all
@@ -60,10 +66,11 @@ clean:
help: help:
@echo "make [all] — собрать transmitter, receiver, ofdm_bench (static armhf)" @echo "make [all] — собрать transmitter, receiver, ofdm_bench (static armhf)"
@echo "make tx|rx|bench — собрать отдельную цель" @echo "make tx|rx|bench — собрать отдельную цель"
@echo "make gftest — оффлайн-самопроверка group-FEC (нативный cc)"
@echo "make deploy — залить бинарники на обе платы (scp -O в /tmp)" @echo "make deploy — залить бинарники на обе платы (scp -O в /tmp)"
@echo "make test — e2e-тест передачи файла с md5-сверкой" @echo "make test — e2e-тест передачи файла с md5-сверкой"
@echo "make check-env — проверить тулчейн и $(XARM)" @echo "make check-env — проверить тулчейн и $(XARM)"
@echo "make clean — удалить build/" @echo "make clean — удалить build/"
@echo "переменные: PLUTO_A=$(PLUTO_A) PLUTO_B=$(PLUTO_B) XARM=$(XARM)" @echo "переменные: PLUTO_A=$(PLUTO_A) PLUTO_B=$(PLUTO_B) XARM=$(XARM)"
.PHONY: all tx rx bench deploy test check-env clean help .PHONY: all tx rx bench gftest deploy test check-env clean help

View File

@@ -18,7 +18,11 @@ TXGAIN="${TXGAIN:--30}" # OTA: переопредели, напр. TXGAIN=-2
FEC="${FEC:-rs8}" # rs8 | none — фаза 3.2 гоняет сначала none, потом rs8 FEC="${FEC:-rs8}" # rs8 | none — фаза 3.2 гоняет сначала none, потом rs8
PAUSE="${PAUSE:-3000}" # пауза TX между кадрами, мкс. Рабочая точка 3-стадийного PAUSE="${PAUSE:-3000}" # пауза TX между кадрами, мкс. Рабочая точка 3-стадийного
# RX (§12.6): чисто, ~120 КБ/с. Ниже ~2500 — Overrun на ядре 0 # RX (§12.6): чисто, ~120 КБ/с. Ниже ~2500 — Overrun на ядре 0
TIMEOUT=$(( SIZE_KB / 80 + 20 )) # ~120 КБ/с на -p 3000 + запас (см. §12.6) GROUPFEC="${GROUPFEC:-1}" # 1 = групповой стирающий FEC 16+2 (§12.7): TX шлёт -G,
# RX автодетектит по hdr[11]. 0 = legacy (потери = дыры)
[ "$GROUPFEC" = 1 ] && GFLAG="-G" || GFLAG=""
# Group-FEC: +12.5% overhead → ~107 КБ/с (SIZE_KB/85); legacy — ~120 (SIZE_KB/95)
if [ "$GROUPFEC" = 1 ]; then TIMEOUT=$(( SIZE_KB / 85 + 20 )); else TIMEOUT=$(( SIZE_KB / 95 + 20 )); fi
if command -v sshpass >/dev/null; then if command -v sshpass >/dev/null; then
SSH=(sshpass -p "$PLUTO_PASS" ssh -o StrictHostKeyChecking=no) SSH=(sshpass -p "$PLUTO_PASS" ssh -o StrictHostKeyChecking=no)
@@ -48,7 +52,7 @@ sleep 3 # дать RX прогреться до старта TX
echo "=== [4/5] Передача с Pluto A ===" echo "=== [4/5] Передача с Pluto A ==="
"${SSH[@]}" "root@$A" \ "${SSH[@]}" "root@$A" \
"cat /tmp/link_test.bin | /tmp/transmitter -f $FREQ -r $RATE -b $BW \ "cat /tmp/link_test.bin | /tmp/transmitter -f $FREQ -r $RATE -b $BW \
-g $TXGAIN -p $PAUSE -c $FEC -u local: 2>/tmp/tx.log" -g $TXGAIN -p $PAUSE -c $FEC $GFLAG -u local: 2>/tmp/tx.log"
sleep 3 # дохлебать хвост эфира sleep 3 # дохлебать хвост эфира
"${SSH[@]}" "root@$B" "killall receiver 2>/dev/null || true" "${SSH[@]}" "root@$B" "killall receiver 2>/dev/null || true"
wait "$RX_PID" 2>/dev/null || true wait "$RX_PID" 2>/dev/null || true

165
src/group_fec.c Normal file
View File

@@ -0,0 +1,165 @@
// group_fec.c — GF(256) для группового стирающего FEC 16+2 (см. group_fec.h)
// Поле GF(2⁸), примитивный полином 0x11d (как в RS/RAID-6), генератор g=2.
// Паритеты: P[j] = ⊕ᵢ dᵢ[j]; Q[j] = ⊕ᵢ gⁱ·dᵢ[j]. Восстановление ≤2 стираний
// данных — прямое решение системы из P и Q (Anvin, «RAID-6»).
#include "group_fec.h"
#include <string.h>
static uint8_t gflog[256];
static uint8_t gfexp[512]; // удвоено: gfexp[a+b] без модуля для a,b≤254
void gf_init(void) {
uint16_t x = 1;
for (int i = 0; i < 255; i++) {
gfexp[i] = (uint8_t)x;
gflog[(uint8_t)x] = (uint8_t)i;
x <<= 1;
if (x & 0x100) x ^= 0x11d;
}
for (int i = 255; i < 512; i++) gfexp[i] = gfexp[i - 255];
gflog[0] = 0; // не определён; умножение на 0 отсекается явно
}
static inline uint8_t gf_mul(uint8_t a, uint8_t b) {
if (a == 0 || b == 0) return 0;
return gfexp[gflog[a] + gflog[b]];
}
static inline uint8_t gf_div(uint8_t a, uint8_t b) { // b != 0
if (a == 0) return 0;
return gfexp[gflog[a] + 255 - gflog[b]];
}
void gf_encode(const uint8_t *const *data, int k, uint8_t *P, uint8_t *Q) {
for (int j = 0; j < GF_CHUNK; j++) {
uint8_t p = 0, q = 0;
for (int i = 0; i < k; i++) {
uint8_t d = data[i][j];
p ^= d;
q ^= gf_mul(gfexp[i], d); // gⁱ·dᵢ
}
P[j] = p;
Q[j] = q;
}
}
int gf_recover(uint8_t data[][GF_CHUNK], const uint8_t *have, int k,
const uint8_t *P, int have_P, const uint8_t *Q, int have_Q) {
int er[3], ne = 0;
for (int i = 0; i < k; i++)
if (!have[i]) { if (ne < 3) er[ne] = i; ne++; }
if (ne == 0) return 1; // нечего восстанавливать
if (ne > 2) return 0; // за пределами 16+2
if (ne == 1) {
int e = er[0];
if (have_P) { // dₑ = P ⊕ (⊕_{i≠e} dᵢ)
for (int j = 0; j < GF_CHUNK; j++) {
uint8_t v = P[j];
for (int i = 0; i < k; i++) if (i != e) v ^= data[i][j];
data[e][j] = v;
}
return 1;
}
if (have_Q) { // dₑ = g^(e) · (Q ⊕ (⊕_{i≠e} gⁱ·dᵢ))
uint8_t inv = gfexp[(255 - e) % 255];
for (int j = 0; j < GF_CHUNK; j++) {
uint8_t v = Q[j];
for (int i = 0; i < k; i++) if (i != e) v ^= gf_mul(gfexp[i], data[i][j]);
data[e][j] = gf_mul(inv, v);
}
return 1;
}
return 0; // потеряны и данные, и оба паритета? (не бывает)
}
// ne == 2: нужны оба паритета.
if (!have_P || !have_Q) return 0;
int e1 = er[0], e2 = er[1];
uint8_t ge1 = gfexp[e1], ge2 = gfexp[e2];
uint8_t denom = ge1 ^ ge2; // ≠ 0: gⁱ различны для i≤254
for (int j = 0; j < GF_CHUNK; j++) {
uint8_t A = P[j], B = Q[j];
for (int i = 0; i < k; i++) if (i != e1 && i != e2) {
A ^= data[i][j];
B ^= gf_mul(gfexp[i], data[i][j]);
}
// dₑ₁ = (B ⊕ ge2·A) / denom ; dₑ₂ = A ⊕ dₑ₁
uint8_t d1 = gf_div(B ^ gf_mul(ge2, A), denom);
data[e1][j] = d1;
data[e2][j] = A ^ d1;
}
return 1;
}
// --- Оффлайн-самопроверка: cc -O2 -DGF_SELFTEST group_fec.c -o gftest -----
#ifdef GF_SELFTEST
#include <stdio.h>
#include <stdlib.h>
int main(void) {
gf_init();
// Проверка поля: g^255 == 1, лог/эксп обратны.
for (int a = 1; a < 256; a++)
if (gfexp[gflog[a]] != a) { printf("FAIL: log/exp a=%d\n", a); return 1; }
srand(12345);
int fails = 0, cases = 0;
for (int trial = 0; trial < 200; trial++) {
for (int k = 1; k <= GF_K; k++) {
uint8_t ref[GF_K][GF_CHUNK];
const uint8_t *ptr[GF_K];
for (int i = 0; i < k; i++) {
for (int j = 0; j < GF_CHUNK; j++) ref[i][j] = rand() & 0xFF;
ptr[i] = ref[i];
}
uint8_t P[GF_CHUNK], Q[GF_CHUNK];
gf_encode(ptr, k, P, Q);
// 0, 1 и 2 стирания во всех комбинациях наличия паритета.
for (int e1 = -1; e1 < k; e1++) {
for (int e2 = e1; e2 < k; e2++) {
// e1==-1: нет стираний; e2==e1: одно; иначе два.
for (int hp = 0; hp <= 1; hp++) {
for (int hq = 0; hq <= 1; hq++) {
uint8_t work[GF_K][GF_CHUNK];
uint8_t have[GF_K];
memcpy(work, ref, sizeof(uint8_t) * k * GF_CHUNK);
for (int i = 0; i < k; i++) have[i] = 1;
int ne = 0;
if (e1 >= 0) { have[e1] = 0; memset(work[e1], 0, GF_CHUNK); ne++; }
if (e2 >= 0 && e2 != e1) { have[e2] = 0; memset(work[e2], 0, GF_CHUNK); ne++; }
int ok = gf_recover(work, have, k, P, hp, Q, hq);
cases++;
// Ожидаемая восстановимость: 0 стираний всегда; 1 — если
// есть хоть один паритет; 2 — если есть оба.
int expect = (ne == 0) || (ne == 1 && (hp || hq)) ||
(ne == 2 && hp && hq);
if (ok != expect) {
printf("FAIL recover-flag k=%d e=%d,%d hp=%d hq=%d ok=%d exp=%d\n",
k, e1, e2, hp, hq, ok, expect);
if (++fails > 10) return 1;
continue;
}
if (ok && memcmp(work, ref, sizeof(uint8_t) * k * GF_CHUNK) != 0) {
printf("FAIL data-mismatch k=%d e=%d,%d hp=%d hq=%d\n",
k, e1, e2, hp, hq);
if (++fails > 10) return 1;
}
}
}
}
}
}
}
printf("gftest: %d кейсов, %d ошибок → %s\n", cases, fails,
fails ? "ПРОВАЛ" : "OK");
return fails ? 1 : 0;
}
#endif

42
src/group_fec.h Normal file
View File

@@ -0,0 +1,42 @@
// group_fec.h — групповой стирающий FEC 16+2 (RAID-6-стиль над GF(256))
// Роадмап §10 п.6: восстановление молчаливых потерь кадров БЕЗ обратного канала.
// Позиции потерь известны из seq (это стирания) → 2 паритетных кадра (P=XOR,
// Q=Σ gⁱ·dᵢ) закрывают до 2 стёртых кадров данных на группу из 16.
// Стоимость GF на 120 КБ/с < 0.2% ядра — считается в потоке C приёмника.
#ifndef GROUP_FEC_H
#define GROUP_FEC_H
#include <stdint.h>
#define GF_K 16 // кадров данных в группе
#define GF_CHUNK 1024 // байт данных на кадр (== MAX_PAYLOAD)
#define GF_META 2 // байт метаданных в начале паритетного payload
#define GF_PAR_PAY (GF_META + GF_CHUNK) // 1026: payload паритетного кадра
// Флаги в резервном байте заголовка hdr[11] (см. CLAUDE.md «Формат кадра»):
#define GF_FLAG_MODE 0x80 // bit7: группа активна (иначе legacy-кадр)
#define GF_FLAG_PAR 0x40 // bit6: паритетный кадр (иначе данные)
#define GF_FLAG_Q 0x20 // bit5: это Q (иначе P) — только для паритета
#define GF_IDX_MASK 0x1F // bits4..0: idx кадра в группе (0..17)
// Метаданные паритета (u16 BE в начале payload): k и длина хвостового кадра.
// (k1) в битах 11..14 (≤15), tail_len в битах 0..10 (≤1024).
#define GF_META_PACK(kprime, tail) (uint16_t)((((kprime) - 1) << 11) | ((tail) & 0x7FF))
#define GF_META_K(meta) (int)((((meta) >> 11) & 0x1F) + 1)
#define GF_META_TAIL(meta) (uint16_t)((meta) & 0x7FF)
void gf_init(void);
// Кодирование: data[i] — указатели на GF_CHUNK-байтовые чанки (короткий
// последний дополняется нулями вызывающим). k ≤ GF_K. P, Q — выход GF_CHUNK.
void gf_encode(const uint8_t *const *data, int k, uint8_t *P, uint8_t *Q);
// Восстановление ≤2 стёртых чанков данных на месте. data[i] валиден при
// have[i]=1 (с нулевым паддингом короткого последнего), иначе будет восстановлен.
// k — число кадров данных. P/Q — паритеты, наличие через have_P/have_Q.
// Возврат: 1 — восстановлено (или нечего восстанавливать), 0 — не хватило паритета.
int gf_recover(uint8_t data[][GF_CHUNK], const uint8_t *have, int k,
const uint8_t *P, int have_P, const uint8_t *Q, int have_Q);
#endif

View File

@@ -9,6 +9,7 @@
#define _GNU_SOURCE // pthread_setaffinity_np, cpu_set_t #define _GNU_SOURCE // pthread_setaffinity_np, cpu_set_t
#include "common.h" #include "common.h"
#include "group_fec.h"
#include <pthread.h> #include <pthread.h>
#include <sched.h> #include <sched.h>
@@ -70,6 +71,7 @@ static float rssi = -100, evm = 0, cfo_est = 0.0f;
// Счётчики C (пишет process_frame): целостность и вывод. // Счётчики C (пишет process_frame): целостность и вывод.
static uint64_t rs_saved = 0, crc_final = 0, total_bytes = 0; static uint64_t rs_saved = 0, crc_final = 0, total_bytes = 0;
static uint64_t rs_fail = 0, lost = 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 uint32_t last_seq = 0;
static int seq_init = 0; static int seq_init = 0;
// Разделяемых на запись счётчиков нет. main читает их раз в секунду для // Разделяемых на запись счётчиков нет. main читает их раз в секунду для
@@ -121,8 +123,98 @@ static int callback(unsigned char *hdr, int hdr_valid,
return 0; return 0;
} }
// Разбор одного кадра в потоке C: seq-детектор + RS + CRC-поверх-RS + запись. // --- Групповой стирающий FEC (поток C, README §12.7) --------------------
// Порядок сохранён (fq FIFO, единственный писатель в stdout — этот поток). // Собирает группу из 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) { static void process_frame(fq_slot_t *fs) {
const uint8_t *hdr = fs->hdr; const uint8_t *hdr = fs->hdr;
uint8_t *pay = fs->pay; // не const: fec_decode() liquid не const-correct uint8_t *pay = fs->pay; // не const: fec_decode() liquid не const-correct
@@ -131,7 +223,8 @@ static void process_frame(fq_slot_t *fs) {
// Детектор пропусков seq (техдолг #6). Кадр, выброшенный переполнением fq, // Детектор пропусков seq (техдолг #6). Кадр, выброшенный переполнением fq,
// тоже проявится здесь как дыра seq → в `lost`; счётчик FQdrop разделяет // тоже проявится здесь как дыра seq → в `lost`; счётчик FQdrop разделяет
// причины (эфирная потеря против переполнения очереди). // причины (эфирная потеря против переполнения очереди). В group-FEC потеря
// одновременно фиксируется как стирание в group_route по idx.
uint32_t seq = ((uint32_t)hdr[2] << 24) | ((uint32_t)hdr[3] << 16) | uint32_t seq = ((uint32_t)hdr[2] << 24) | ((uint32_t)hdr[3] << 16) |
((uint32_t)hdr[4] << 8) | hdr[5]; ((uint32_t)hdr[4] << 8) | hdr[5];
if (seq_init && seq > last_seq + 1) { if (seq_init && seq > last_seq + 1) {
@@ -144,14 +237,20 @@ static void process_frame(fq_slot_t *fs) {
uint16_t original_len = (hdr[6] << 8) | hdr[7]; uint16_t original_len = (hdr[6] << 8) | hdr[7];
uint8_t last_block_bytes = hdr[10]; 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) { if (fec_on && decoder && pay_len >= RS_ENC) {
size_t nblocks = pay_len / RS_ENC; size_t nblocks = pay_len / RS_ENC;
// Защита в глубину: B уже отсёк pay_len>FQ_PAY_MAX, но guard оставлен // Защита в глубину: B уже отсёк pay_len>FQ_PAY_MAX, но guard оставлен
// (recovered[4096] вмещает 4096/RS_DATA = 18 блоков, техдолг #2). // (recovered[4096] вмещает 4096/RS_DATA = 18 блоков, техдолг #2).
if (nblocks == 0 || nblocks > 4096 / RS_DATA) return; if (nblocks == 0 || nblocks > 4096 / RS_DATA) goto route;
uint8_t recovered[4096];
for (size_t b = 0; b < nblocks; b++) { for (size_t b = 0; b < nblocks; b++) {
// fec_decode() liquid всегда "успешен" — код возврата бесполезен. // fec_decode() liquid всегда "успешен" — код возврата бесполезен.
fec_decode(decoder, RS_DATA, pay + b * RS_ENC, fec_decode(decoder, RS_DATA, pay + b * RS_ENC,
@@ -166,7 +265,7 @@ static void process_frame(fq_slot_t *fs) {
if (original_len == 0 || (size_t)original_len + 4 > framed_len) { if (original_len == 0 || (size_t)original_len + 4 > framed_len) {
rs_fail++; rs_fail++;
return; goto route;
} }
unsigned int rx_crc = crc_generate_key(LIQUID_CRC_32, recovered, original_len); unsigned int rx_crc = crc_generate_key(LIQUID_CRC_32, recovered, original_len);
unsigned int tx_crc = ((uint32_t)recovered[original_len] << 24) | unsigned int tx_crc = ((uint32_t)recovered[original_len] << 24) |
@@ -175,19 +274,26 @@ static void process_frame(fq_slot_t *fs) {
recovered[original_len + 3]; recovered[original_len + 3];
if (rx_crc != tx_crc) { if (rx_crc != tx_crc) {
rs_fail++; // RS не справился, кадр битый — не пишем rs_fail++; // RS не справился, кадр битый — не пишем
return; goto route;
} }
if (!pay_valid) rs_saved++; // liquid CRC по payload не прошёл, а наш if (!pay_valid) rs_saved++; // liquid CRC по payload не прошёл, а наш
// CRC поверх RS — да: значит RS реально спас // CRC поверх RS — да: значит RS реально спас
fwrite(recovered, 1, original_len, stdout); dec = recovered; dec_len = original_len; ok = 1;
fflush(stdout);
total_bytes += original_len;
crc_final++;
} else if (pay_valid && original_len > 0 && original_len <= pay_len) { } else if (pay_valid && original_len > 0 && original_len <= pay_len) {
fwrite(pay, 1, original_len, stdout); dec = pay; dec_len = original_len; ok = 1;
fflush(stdout); }
total_bytes += original_len;
crc_final++; 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++;
}
} }
} }
@@ -298,6 +404,7 @@ static void *fec_thread(void *arg) {
fq.count--; fq.count--;
pthread_mutex_unlock(&fq.mtx); pthread_mutex_unlock(&fq.mtx);
} }
group_flush(); // дописать последнюю группу файла (нет кадра-триггера)
return NULL; return NULL;
} }
@@ -329,6 +436,7 @@ int main(int argc, char *argv[]) {
fec_on = !strcmp(fec_opt, "rs8"); fec_on = !strcmp(fec_opt, "rs8");
setup_signal_handlers(); setup_signal_handlers();
gf_init(); // group-FEC автодетектится по hdr[11] первого кадра
print_config("ПРИЁМНИК", freq, rate, bw, fec_on); print_config("ПРИЁМНИК", freq, rate, bw, fec_on);
fprintf(stderr, "Усиление: %.0f дБ (ручное)\n", RX_GAIN); fprintf(stderr, "Усиление: %.0f дБ (ручное)\n", RX_GAIN);
@@ -383,8 +491,8 @@ int main(int argc, char *argv[]) {
ofdmflexframesync_set_header_len(fs, HDR_SIZE); ofdmflexframesync_set_header_len(fs, HDR_SIZE);
fprintf(stderr, "Приёмник готов. Ожидание сигнала...\n\n"); fprintf(stderr, "Приёмник готов. Ожидание сигнала...\n\n");
fprintf(stderr, "Кадры | Загол. | CRC | RS испр | RS бит | Потери | Overr | FQdrp | Вывод | RSSI | EVM | CFO\n"); fprintf(stderr, "Кадры | Загол. | CRC | RS испр | RS бит | Потери | Overr | FQdrp | Rec | GrpF | Вывод | RSSI | EVM | CFO\n");
fprintf(stderr, "--------+--------+------+---------+--------+--------+--------+--------+--------+-------+-------+----------\n"); fprintf(stderr, "--------+--------+------+---------+--------+--------+--------+--------+--------+--------+--------+-------+-------+----------\n");
// Три стадии на двух ядрах. main крутит статистику 1 Гц и ждёт stop, затем // Три стадии на двух ядрах. main крутит статистику 1 Гц и ждёт stop, затем
// join A→B→C: A ставит ring.eof, B дочищает кольцо и ставит fq.eof, C // join A→B→C: A ставит ring.eof, B дочищает кольцо и ставит fq.eof, C
@@ -402,11 +510,12 @@ int main(int argc, char *argv[]) {
nanosleep(&nap, NULL); nanosleep(&nap, NULL);
time_t now = time(NULL); time_t now = time(NULL);
if (now > last) { if (now > last) {
fprintf(stderr, "\r%6llu | %5llu | %4llu | %6llu | %5llu | %6llu | %6llu | %6llu | %6llu | %+5.0f | %+5.1f | %+8.4f", 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)calls, (unsigned long long)hdr_ok,
(unsigned long long)crc_raw, (unsigned long long)rs_saved, (unsigned long long)crc_raw, (unsigned long long)rs_saved,
(unsigned long long)rs_fail, (unsigned long long)lost, (unsigned long long)rs_fail, (unsigned long long)lost,
(unsigned long long)ring.overruns, (unsigned long long)fq.drops, (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); (unsigned long long)crc_final, rssi, evm, cfo_est);
fflush(stderr); fflush(stderr);
last = now; last = now;
@@ -418,11 +527,12 @@ int main(int argc, char *argv[]) {
pthread_join(tc, NULL); pthread_join(tc, NULL);
fprintf(stderr, "\n\nПРИЁМ ЗАВЕРШЁН\n"); fprintf(stderr, "\n\nПРИЁМ ЗАВЕРШЁН\n");
fprintf(stderr, "Кадров: %llu | Заголовков: %llu | CRC: %llu | RS испр: %llu | RS битых: %llu | Потери seq: %llu | Overrun: %llu | FQ drops: %llu | Вывод: %llu | Байт: %llu\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)calls, (unsigned long long)hdr_ok,
(unsigned long long)crc_raw, (unsigned long long)rs_saved, (unsigned long long)crc_raw, (unsigned long long)rs_saved,
(unsigned long long)rs_fail, (unsigned long long)lost, (unsigned long long)rs_fail, (unsigned long long)lost,
(unsigned long long)ring.overruns, (unsigned long long)fq.drops, (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); (unsigned long long)crc_final, (unsigned long long)total_bytes);
if (decoder) fec_destroy(decoder); if (decoder) fec_destroy(decoder);

View File

@@ -6,28 +6,95 @@
*/ */
#include "common.h" #include "common.h"
#include "group_fec.h"
// Параметры передатчика по умолчанию // Параметры передатчика по умолчанию
#define DEFAULT_GAIN -30.0 // Усиление передатчика: -30 дБ #define DEFAULT_GAIN -30.0 // Усиление передатчика: -30 дБ
#define DEFAULT_AMP 0.2f // Амплитуда сигнала #define DEFAULT_AMP 0.2f // Амплитуда сигнала
#define DEFAULT_PAUSE 2000 // Пауза между кадрами: 2000 мкс #define DEFAULT_PAUSE 2000 // Пауза между кадрами: 2000 мкс
#define BUF_SAMPLES 16384 // Размер буфера в отсчётах #define BUF_SAMPLES 16384 // Размер буфера в отсчётах
#define MAX_PAYLOAD 1024 // Максимальный размер полезной нагрузки #define MAX_PAYLOAD GF_CHUNK // 1024: размер чанка данных (== GF_CHUNK)
// Внешняя переменная флага завершения (определена в common.c) // Внешняя переменная флага завершения (определена в common.c)
extern volatile sig_atomic_t stop; extern volatile sig_atomic_t stop;
// Кодирование и отправка одного кадра: CRC-поверх-RS (техдолг #3) → заголовок
// 12 Б → OFDM-сборка → push_partial → пауза. hdr11 несёт флаги group-FEC
// (0 для legacy). pl/n — полезная нагрузка (данные ≤1024 или паритет 1026).
static void tx_frame(ofdmflexframegen fg, struct iio_buffer *buf, fec enc,
uint32_t seq, uint8_t hdr11,
const uint8_t *pl, size_t n, float amp, int pause_us) {
uint8_t *tx_payload = NULL;
size_t tx_len = 0;
uint8_t hdr[HDR_SIZE] = {0};
if (enc) {
// CRC32 поверх исходных данных ДО RS: fec_decode() liquid не сообщает о
// неисправимых блоках — единственный надёжный критерий на RX (техдолг #3).
uint8_t framed[GF_PAR_PAY + 4]; // ≥ данные+4 и ≥ паритет 1026+4
memcpy(framed, pl, n);
unsigned int crc = crc_generate_key(LIQUID_CRC_32, pl, n);
framed[n] = (crc >> 24) & 0xFF;
framed[n + 1] = (crc >> 16) & 0xFF;
framed[n + 2] = (crc >> 8) & 0xFF;
framed[n + 3] = crc & 0xFF;
size_t framed_n = n + 4;
size_t total_blocks = (framed_n + RS_DATA - 1) / RS_DATA;
tx_len = total_blocks * RS_ENC;
tx_payload = malloc(tx_len);
for (size_t b = 0; b < total_blocks; b++) {
uint8_t blk[RS_DATA] = {0};
size_t bytes = (b == total_blocks - 1) ? framed_n - b * RS_DATA : RS_DATA;
memcpy(blk, framed + b * RS_DATA, bytes);
fec_encode(enc, RS_DATA, blk, tx_payload + b * RS_ENC);
}
hdr[8] = (total_blocks >> 8) & 0xFF; hdr[9] = total_blocks & 0xFF;
hdr[10] = framed_n % RS_DATA;
} else {
tx_len = n;
tx_payload = malloc(tx_len);
memcpy(tx_payload, pl, n);
}
hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1;
hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF;
hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF;
hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF;
hdr[11] = hdr11; // group-FEC флаги (0 = legacy)
ofdmflexframegen_assemble(fg, hdr, tx_payload, tx_len);
free(tx_payload);
int16_t *out = (int16_t *)iio_buffer_start(buf);
memset(out, 0, BUF_SAMPLES * 2 * sizeof(int16_t));
int idx = 0, done = 0;
float complex symb[OFDM_M + OFDM_CP];
while (!done && (idx + OFDM_M + OFDM_CP) <= BUF_SAMPLES) {
done = ofdmflexframegen_write(fg, symb, OFDM_M + OFDM_CP);
for (int i = 0; i < OFDM_M + OFDM_CP; i++) {
out[2 * idx] = (int16_t)(crealf(symb[i]) * 32767 * amp);
out[2 * idx + 1] = (int16_t)(cimagf(symb[i]) * 32767 * amp);
idx++;
}
}
// Только фактически заполненные idx сэмплов (техдолг #5): иначе мёртвый эфир
// и сбитый межкадровый тайминг. push_partial даёт ту же длину, что write.
if (idx > 0) iio_buffer_push_partial(buf, idx);
if (pause_us > 0) usleep(pause_us);
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
// Инициализация параметров // Инициализация параметров
long long freq = DEFAULT_FREQ, rate = DEFAULT_RATE, bw = DEFAULT_BW; long long freq = DEFAULT_FREQ, rate = DEFAULT_RATE, bw = DEFAULT_BW;
double gain = DEFAULT_GAIN; double gain = DEFAULT_GAIN;
float amp = DEFAULT_AMP; float amp = DEFAULT_AMP;
const char *uri = TX_URI_DEFAULT; const char *uri = TX_URI_DEFAULT;
int pause_us = DEFAULT_PAUSE, use_fec = 1; int pause_us = DEFAULT_PAUSE, use_fec = 1, group_fec = 0;
// Обработка аргументов командной строки // Обработка аргументов командной строки
int opt; int opt;
while ((opt = getopt(argc, argv, "f:r:b:g:a:p:u:c:h")) != -1) { while ((opt = getopt(argc, argv, "f:r:b:g:a:p:u:c:Gh")) != -1) {
switch (opt) { switch (opt) {
case 'f': freq = atoll(optarg); break; case 'f': freq = atoll(optarg); break;
case 'r': rate = atoll(optarg); break; case 'r': rate = atoll(optarg); break;
@@ -37,12 +104,14 @@ int main(int argc, char *argv[]) {
case 'p': pause_us = atoi(optarg); break; case 'p': pause_us = atoi(optarg); break;
case 'u': uri = optarg; break; case 'u': uri = optarg; break;
case 'c': use_fec = strcmp(optarg, "none") ? 1 : 0; break; case 'c': use_fec = strcmp(optarg, "none") ? 1 : 0; break;
case 'G': group_fec = 1; break;
case 'h': case 'h':
print_usage(argv[0], "ПЕРЕДАТЧИК"); print_usage(argv[0], "ПЕРЕДАТЧИК");
fprintf(stderr, "\nСПЕЦИФИЧНЫЕ ОПЦИИ TX:\n"); fprintf(stderr, "\nСПЕЦИФИЧНЫЕ ОПЦИИ TX:\n");
fprintf(stderr, " -g УСИЛЕНИЕ Усиление в дБ (по умолчанию: %.0f)\n", DEFAULT_GAIN); fprintf(stderr, " -g УСИЛЕНИЕ Усиление в дБ (по умолчанию: %.0f)\n", DEFAULT_GAIN);
fprintf(stderr, " -a АМПЛИТ. Амплитуда сигнала (по умолчанию: %.2f)\n", DEFAULT_AMP); fprintf(stderr, " -a АМПЛИТ. Амплитуда сигнала (по умолчанию: %.2f)\n", DEFAULT_AMP);
fprintf(stderr, " -p ПАУЗА Пауза между кадрами в мкс (по умолчанию: %d)\n", DEFAULT_PAUSE); fprintf(stderr, " -p ПАУЗА Пауза между кадрами в мкс (по умолчанию: %d)\n", DEFAULT_PAUSE);
fprintf(stderr, " -G Групповой стирающий FEC 16+2 (§10 п.6)\n");
fprintf(stderr, "\nОПТИМАЛЬНЫЙ ЗАПУСК:\n"); fprintf(stderr, "\nОПТИМАЛЬНЫЙ ЗАПУСК:\n");
fprintf(stderr, " cat test.bin | %s -f %lld -r %lld -b %lld -c rs8 > /dev/null\n", fprintf(stderr, " cat test.bin | %s -f %lld -r %lld -b %lld -c rs8 > /dev/null\n",
argv[0], DEFAULT_FREQ, DEFAULT_RATE, DEFAULT_BW); argv[0], DEFAULT_FREQ, DEFAULT_RATE, DEFAULT_BW);
@@ -53,12 +122,14 @@ int main(int argc, char *argv[]) {
// Настройка обработчиков сигналов // Настройка обработчиков сигналов
setup_signal_handlers(); setup_signal_handlers();
if (group_fec) gf_init();
// Вывод конфигурации // Вывод конфигурации
print_config("ПЕРЕДАТЧИК", freq, rate, bw, use_fec); print_config("ПЕРЕДАТЧИК", freq, rate, bw, use_fec);
fprintf(stderr, "Усиление: %.0f дБ\n", gain); fprintf(stderr, "Усиление: %.0f дБ\n", gain);
fprintf(stderr, "Амплитуда: %.2f\n", amp); fprintf(stderr, "Амплитуда: %.2f\n", amp);
fprintf(stderr, "Пауза: %d мкс\n", pause_us); fprintf(stderr, "Пауза: %d мкс\n", pause_us);
fprintf(stderr, "Group-FEC: %s\n", group_fec ? "16+2 (§10 п.6)" : "выкл");
fprintf(stderr, "URI: %s\n", uri); fprintf(stderr, "URI: %s\n", uri);
fprintf(stderr, "═══════════════════════════════════════════════════\n"); fprintf(stderr, "═══════════════════════════════════════════════════\n");
@@ -104,95 +175,70 @@ int main(int argc, char *argv[]) {
fprintf(stderr, " Передатчик готов. Ожидание данных...\n\n"); fprintf(stderr, " Передатчик готов. Ожидание данных...\n\n");
// Основной цикл передачи // Основной цикл передачи. Legacy: кадр = чанк ≤1024 Б. Group-FEC: копим до
while (!stop) { // 16 чанков, затем шлём k данных + 2 паритета (P, Q) с флагами в hdr[11].
uint8_t raw[MAX_PAYLOAD]; if (!group_fec) {
size_t n = fread(raw, 1, MAX_PAYLOAD, stdin); while (!stop) {
if (!n) { uint8_t raw[MAX_PAYLOAD];
if (feof(stdin) || stop) break; size_t n = fread(raw, 1, MAX_PAYLOAD, stdin);
usleep(10000); if (!n) {
continue; if (feof(stdin) || stop) break;
} usleep(10000);
continue;
uint8_t *tx_payload = NULL;
size_t tx_len = 0;
uint8_t hdr[HDR_SIZE] = {0};
// Кодирование данных
if (enc) {
// CRC32 поверх исходных данных ДО RS: fec_decode() liquid не
// сообщает о неисправимых блоках, поэтому единственный надёжный
// критерий целостности на RX — этот CRC поверх декода (техдолг #3).
uint8_t framed[MAX_PAYLOAD + 4];
memcpy(framed, raw, n);
unsigned int crc = crc_generate_key(LIQUID_CRC_32, raw, n);
framed[n] = (crc >> 24) & 0xFF;
framed[n + 1] = (crc >> 16) & 0xFF;
framed[n + 2] = (crc >> 8) & 0xFF;
framed[n + 3] = crc & 0xFF;
size_t framed_n = n + 4;
size_t total_blocks = (framed_n + RS_DATA - 1) / RS_DATA;
tx_len = total_blocks * RS_ENC;
tx_payload = malloc(tx_len);
for (size_t b = 0; b < total_blocks; b++) {
uint8_t blk[RS_DATA] = {0};
size_t bytes = (b == total_blocks - 1) ? framed_n - b * RS_DATA : RS_DATA;
memcpy(blk, framed + b * RS_DATA, bytes);
fec_encode(enc, RS_DATA, blk, tx_payload + b * RS_ENC);
} }
tx_frame(fg, buf, enc, seq++, 0, raw, n, amp, pause_us);
// Заголовок: len = исходные n (без 4 байт CRC), nblocks/last по framed frames++; bytes_tx += n;
hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1; if (time(NULL) > last) {
hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF; fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт",
hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF; (unsigned long long)frames, (unsigned long long)bytes_tx);
hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF; fflush(stderr);
hdr[8] = (total_blocks >> 8) & 0xFF; hdr[9] = total_blocks & 0xFF; last = time(NULL);
hdr[10] = framed_n % RS_DATA; hdr[11] = 0;
} else {
tx_len = n;
tx_payload = malloc(tx_len);
memcpy(tx_payload, raw, n);
hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1;
hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF;
hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF;
hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF;
}
// Сборка OFDM-кадра
ofdmflexframegen_assemble(fg, hdr, tx_payload, tx_len);
free(tx_payload);
// Формирование выходного сигнала
int16_t *out = (int16_t *)iio_buffer_start(buf);
memset(out, 0, BUF_SAMPLES * 2 * sizeof(int16_t));
int idx = 0, done = 0;
float complex symb[OFDM_M + OFDM_CP];
while (!done && (idx + OFDM_M + OFDM_CP) <= BUF_SAMPLES) {
done = ofdmflexframegen_write(fg, symb, OFDM_M + OFDM_CP);
for (int i = 0; i < OFDM_M + OFDM_CP; i++) {
out[2 * idx] = (int16_t)(crealf(symb[i]) * 32767 * amp);
out[2 * idx + 1] = (int16_t)(cimagf(symb[i]) * 32767 * amp);
idx++;
} }
} }
// Пушим только фактически заполненные idx сэмплов, а не весь буфер } else {
// 16384 (техдолг #5): иначе ~35% мёртвого эфира на кадр и сбитый static uint8_t gdata[GF_K][GF_CHUNK]; // накопитель группы
// межкадровый тайминг. push_partial даёт ту же длину, что write. const uint8_t *gptr[GF_K];
if (idx > 0) iio_buffer_push_partial(buf, idx); uint16_t glen[GF_K];
int k = 0, done_reading = 0;
// Обновление статистики while (!stop && !done_reading) {
frames++; bytes_tx += n; seq++; size_t r = fread(gdata[k], 1, GF_CHUNK, stdin);
if (time(NULL) > last) { if (r > 0) {
fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт", if (r < GF_CHUNK) memset(gdata[k] + r, 0, GF_CHUNK - r); // паддинг под GF
(unsigned long long)frames, (unsigned long long)bytes_tx); glen[k] = (uint16_t)r;
fflush(stderr); gptr[k] = gdata[k];
last = time(NULL); k++;
}
if (r < GF_CHUNK) done_reading = 1; // fread добирает до конца → короткий = EOF
if (k == GF_K || (done_reading && k > 0)) {
// k кадров данных: idx=0..k-1
for (int i = 0; i < k; i++) {
tx_frame(fg, buf, enc, seq++, GF_FLAG_MODE | i,
gdata[i], glen[i], amp, pause_us);
frames++; bytes_tx += glen[i];
}
// паритеты P (idx=k) и Q (idx=k+1); мета = k + длина хвоста
uint8_t P[GF_CHUNK], Q[GF_CHUNK], par[GF_PAR_PAY];
gf_encode(gptr, k, P, Q);
uint16_t meta = GF_META_PACK(k, glen[k - 1]);
par[0] = meta >> 8; par[1] = meta & 0xFF;
memcpy(par + GF_META, P, GF_CHUNK);
tx_frame(fg, buf, enc, seq++, GF_FLAG_MODE | GF_FLAG_PAR | k,
par, GF_PAR_PAY, amp, pause_us);
memcpy(par + GF_META, Q, GF_CHUNK);
tx_frame(fg, buf, enc, seq++, GF_FLAG_MODE | GF_FLAG_PAR | GF_FLAG_Q | (k + 1),
par, GF_PAR_PAY, amp, pause_us);
frames += 2;
k = 0;
if (time(NULL) > last) {
fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт (group-FEC)",
(unsigned long long)frames, (unsigned long long)bytes_tx);
fflush(stderr);
last = time(NULL);
}
}
} }
if (pause_us > 0) usleep(pause_us);
} }
fprintf(stderr, "\nПередача завершена: %llu кадров, %llu байт\n", fprintf(stderr, "\nПередача завершена: %llu кадров, %llu байт\n",