From 341a5555abb45b45f1ed701a58813aa5d7a22781 Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 16 Jul 2026 11:21:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=202.=20=D0=9D=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D1=82=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/NOTES.md | 26 +++++++++ makefile | 8 +-- scripts/test_fdd_fwd.sh | 17 ++++++ src/feedback.c | 48 +++++++++++++++ src/feedback.h | 45 +++++++++++++++ src/frame_tx.c | 30 ++++++++++ src/frame_tx.h | 10 ++++ src/ofdm_bench.c | 33 +++++++++++ src/receiver.c | 125 ++++++++++++++++++++++++++++++++++++++-- src/transmitter.c | 95 +++++++++++++++++++++++++++++- 10 files changed, 428 insertions(+), 9 deletions(-) create mode 100644 src/feedback.c create mode 100644 src/feedback.h diff --git a/docs/NOTES.md b/docs/NOTES.md index b728aa8..991104b 100644 --- a/docs/NOTES.md +++ b/docs/NOTES.md @@ -46,6 +46,32 @@ RX платы «глохнет», как только её TX активен. Э Поэтому порядок работ: **сначала симплекс** (обе платы 915 МГц, полудуплекс по очереди), FDD — отдельным этапом с замерами деградации RX при активном TX. +### 2.1. Предварительные замеры (этап 1, §10 п.7 — FDD-водопровод) + +Числа сняты на **сконфигурированном**, но молчащем обратном тракте (LO 868 +поднят, `hardwaregain` выставлен, но DMA обратного TX ничего не пушит). +Полноценный gate с реальным STATUS-трафиком — этап 2, числа уйдут в §2.2. + +- **Самоглушения от «просто включённого» обратного тракта не видно.** Форвард + A→B под FDD (1 МБ, `-p 4000`, group-FEC) байт-в-байт и при fb-TX `-g -89` + (≈выкл), и при `-g -20` (штатное). Активный обратный LO 868 в 47 МГц от + RX 915 приёмник данных не глушит — на кабеле+аттенюаторе. Это ответ на + вопрос PER₁ (десенс от включённого тракта), но НЕ на PER₂ (десенс от + реального излучения STATUS) — тот снимается в этапе 2. +- **Диагностика: пустой файл на выходе RX ≠ самоглушение.** Симптом «0 кадров, + RSSI −100» — это НЕ глушение, а мёртвый вход: остаточный процесс держит + RX-DMA (`cf-ad9361-lpc`) от неубитого прошлого запуска. Глушение выглядит + ИНАЧЕ — сильный RSSI (−48) при разрушенном EVM (−2.5). **Различать по RSSI.** + Лечение: `killall -9 receiver transmitter` перед каждым стартом (уже в + `scripts/test_fdd_fwd.sh`). +- **Ридбэк LO округляется PLL.** AD9361 — дробный-N синтезатор с шагом ~2 Гц: + `868000000` читается как `867999998`. Это норма, не ошибка; сверка ридбэка + идёт с допуском `FDD_LO_TOL_HZ`=100 Гц (common.c). +- **Канал деградировал против этапа 0.** Эталон регрессии сдвинут `-p 3000` → + `-p 4000` (на 3000/3500 теперь Overrun+потери). Направление **B→A** флапает + даже на `-p 4000` при СИЛЬНОМ RSSI (−33…−35) — вероятна компрессия входа RX + (усиление 50 дБ избыточно). Проверить снижением RX/TX gain до свипа частот. + ## 3. Регуляторика (868/915 МГц в регионе ETSI) - **868 МГц** — SRD-диапазон: ограничения на полосу канала (сотни кГц), diff --git a/makefile b/makefile index 0982f07..d00dc12 100644 --- a/makefile +++ b/makefile @@ -25,11 +25,11 @@ all: $(BIN)/transmitter $(BIN)/receiver $(BIN)/ofdm_bench $(BIN): mkdir -p $(BIN) -$(BIN)/transmitter: $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c $(SRC)/common.h $(SRC)/group_fec.h $(SRC)/frame_tx.h $(DEPS_LIBS) | $(BIN) - $(CC) $(CFLAGS) $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c -o $@ $(LDFLAGS) -Wl,--start-group -lad9361 -liio $(LDLIBS) -Wl,--end-group +$(BIN)/transmitter: $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c $(SRC)/feedback.c $(SRC)/common.h $(SRC)/group_fec.h $(SRC)/frame_tx.h $(SRC)/feedback.h $(DEPS_LIBS) | $(BIN) + $(CC) $(CFLAGS) $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c $(SRC)/feedback.c -o $@ $(LDFLAGS) -Wl,--start-group -lad9361 -liio $(LDLIBS) -Wl,--end-group -$(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 $(SRC)/group_fec.c -o $@ $(LDFLAGS) -Wl,--start-group -lad9361 -liio $(LDLIBS) -Wl,--end-group +$(BIN)/receiver: $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c $(SRC)/feedback.c $(SRC)/common.h $(SRC)/group_fec.h $(SRC)/frame_tx.h $(SRC)/feedback.h $(DEPS_LIBS) | $(BIN) + $(CC) $(CFLAGS) $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/frame_tx.c $(SRC)/feedback.c -o $@ $(LDFLAGS) -Wl,--start-group -lad9361 -liio $(LDLIBS) -Wl,--end-group $(BIN)/ofdm_bench: $(SRC)/ofdm_bench.c | $(BIN) $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS) diff --git a/scripts/test_fdd_fwd.sh b/scripts/test_fdd_fwd.sh index 8d6f9b7..2955e26 100644 --- a/scripts/test_fdd_fwd.sh +++ b/scripts/test_fdd_fwd.sh @@ -83,6 +83,23 @@ echo "--- FDD-ридбэк ---" "${SSH[@]}" "root@$A" "grep 'FDD ридбэк' /tmp/tx_fdd.log 2>/dev/null | sed 's/^/A: /'" || true "${SSH[@]}" "root@$B" "grep 'FDD ридбэк' /tmp/rx_fdd.log 2>/dev/null | sed 's/^/B: /'" || true +# Обратный канал (этап 2): сколько STATUS B отправил и сколько A декодировал. +# Доля доставки — метрика gate самоглушения (§10 п.7). +echo "--- Обратный канал STATUS (B→A) ---" +SENT=$("${SSH[@]}" "root@$B" "grep -o 'отправлено STATUS: [0-9]*' /tmp/rx_fdd.log 2>/dev/null | grep -o '[0-9]*'" || echo "") +RECV=$("${SSH[@]}" "root@$A" "grep -o 'принято STATUS: [0-9]*' /tmp/tx_fdd.log 2>/dev/null | grep -o '[0-9]*'" || echo "") +SENT=${SENT:-0}; RECV=${RECV:-0} +if [ "$SENT" -gt 0 ]; then + echo "B отправил: $SENT | A принял: $RECV | доставка ≈ $(( RECV * 100 / SENT ))%" +else + echo "B отправил: $SENT | A принял: $RECV" +fi + +# Итоговая RX-статистика — источник PER (Потери seq / Байт), нужна каждый прогон, +# особенно при GROUPFEC=0 (сырой PER без маскировки групповым FEC). +echo "--- RX-статистика (PER) ---" +"${SSH[@]}" "root@$B" "grep '^Кадров:' /tmp/rx_fdd.log 2>/dev/null | tail -1" || true + if [ "$MD5_SRC" = "$MD5_DST" ]; then echo "РЕЗУЛЬТАТ: OK — форвард-линк под FDD цел (FBGAIN=$FBGAIN)" else diff --git a/src/feedback.c b/src/feedback.c new file mode 100644 index 0000000..a1bdafc --- /dev/null +++ b/src/feedback.c @@ -0,0 +1,48 @@ +// feedback.c — упаковка/распаковка STATUS обратного канала (этап 2, §10 п.7). +// Чистая логика без зависимостей от liquid/iio → пригодна к офлайн-юнит-тесту. + +#include "feedback.h" +#include + +static void put_u32(uint8_t *p, uint32_t v) { + p[0] = (v >> 24) & 0xFF; p[1] = (v >> 16) & 0xFF; + p[2] = (v >> 8) & 0xFF; p[3] = v & 0xFF; +} + +static uint32_t get_u32(const uint8_t *p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +size_t fb_status_pack(const fb_status_t *s, uint8_t *out) { + uint8_t cnt = s->nack_cnt > FB_MAX_NACK ? FB_MAX_NACK : s->nack_cnt; + out[0] = FB_TYPE_STATUS; + out[1] = s->flags; + out[2] = cnt; + out[3] = 0; // резерв + put_u32(out + 4, s->highest_seq); + put_u32(out + 8, s->rx_frames); + put_u32(out + 12, s->overruns); + for (uint8_t i = 0; i < cnt; i++) + put_u32(out + FB_STATUS_HDR + (size_t)i * 4, s->nack[i]); + return (size_t)FB_STATUS_HDR + (size_t)cnt * 4; +} + +int fb_status_unpack(const uint8_t *in, size_t len, fb_status_t *s) { + if (len < FB_STATUS_HDR) return -1; + if (in[0] != FB_TYPE_STATUS) return -1; + uint8_t cnt = in[2]; + if (cnt > FB_MAX_NACK) return -1; + if (len < (size_t)FB_STATUS_HDR + (size_t)cnt * 4) return -1; + + memset(s, 0, sizeof(*s)); + s->type = in[0]; + s->flags = in[1]; + s->nack_cnt = cnt; + s->highest_seq = get_u32(in + 4); + s->rx_frames = get_u32(in + 8); + s->overruns = get_u32(in + 12); + for (uint8_t i = 0; i < cnt; i++) + s->nack[i] = get_u32(in + FB_STATUS_HDR + (size_t)i * 4); + return 0; +} diff --git a/src/feedback.h b/src/feedback.h new file mode 100644 index 0000000..08de6e1 --- /dev/null +++ b/src/feedback.h @@ -0,0 +1,45 @@ +// feedback.h — обратный канал STATUS (roadmap §10 п.7, этап 2). +// STATUS — кумулятивный СНИМОК состояния RX, а не поток событий: потеря одного +// STATUS безвредна, следующий несёт то же состояние (ARQ-над-ARQ не нужен). +// Идёт B→A на частоте обратного канала (868); сигнатура кадра F0 55 в hdr[0..1] +// (данные — F0 AA, END — F0 E7). Payload RS(255,223)+CRC32, как у данных. + +#ifndef FEEDBACK_H +#define FEEDBACK_H + +#include +#include + +#define FB_TYPE_STATUS 0x01 +#define FB_MAX_NACK 50 // потолок NACK в одном STATUS (влезает в 1 RS-блок) +#define FB_FLAG_END_SEEN 0x01 // RX видел END-кадр (этап 3) +#define FB_FLAG_COMPLETE 0x02 // все кадры собраны, файл цел (этап 3) + +// Сигнатура STATUS-кадра в hdr[0..1]. +#define FB_SIG_0 0xF0 +#define FB_SIG_1 0x55 + +// Снимок состояния приёмника. nack[] — отсортированные по возрастанию seq +// отсутствующих кадров данных (старейшие первыми), не более FB_MAX_NACK. +typedef struct { + uint8_t type; // FB_TYPE_STATUS + uint8_t flags; // FB_FLAG_END_SEEN | FB_FLAG_COMPLETE + uint8_t nack_cnt; // число валидных элементов nack[] (≤ FB_MAX_NACK) + uint32_t highest_seq; // макс. принятый seq (0xFFFFFFFF = ничего) + uint32_t rx_frames; // кадров данных записано в stdout + uint32_t overruns; // Overrun ядра захвата (диагностика линии) + uint32_t nack[FB_MAX_NACK]; +} fb_status_t; + +// Раскладка payload (сетевой порядок, BE): +// 0 type | 1 flags | 2 nack_cnt | 3 rsv | 4..7 highest | 8..11 rx_frames +// 12..15 overruns | 16.. nack[i] u32 +#define FB_STATUS_HDR 16 // фикс. часть +#define FB_STATUS_MAXLEN (FB_STATUS_HDR + FB_MAX_NACK * 4) // 216 ≤ 219 → 1 RS-блок + +// Упаковка → out (≥ FB_STATUS_MAXLEN). Возврат — длина payload в байтах. +size_t fb_status_pack(const fb_status_t *s, uint8_t *out); +// Распаковка. Возврат 0 при успехе, -1 при кривой длине/типе. +int fb_status_unpack(const uint8_t *in, size_t len, fb_status_t *s); + +#endif diff --git a/src/frame_tx.c b/src/frame_tx.c index d3f17bb..30eaeea 100644 --- a/src/frame_tx.c +++ b/src/frame_tx.c @@ -68,3 +68,33 @@ void phy_tx_frame(ofdmflexframegen fg, struct iio_buffer *buf, int buf_samples, if (idx > 0) iio_buffer_push_partial(buf, idx); if (pause_us > 0) usleep(pause_us); } + +int phy_rx_decode(const uint8_t *hdr, const uint8_t *pay, unsigned pay_len, + fec dec, uint8_t *out, size_t out_cap, size_t *out_len) { + if (!dec || pay_len < RS_ENC) return 0; + size_t nblocks = pay_len / RS_ENC; + if (nblocks == 0 || nblocks * RS_DATA > out_cap) return 0; + + for (size_t b = 0; b < nblocks; b++) + // fec_decode() liquid не const-correct и «всегда успешен» — код возврата + // бесполезен, целостность проверяем CRC ниже (техдолг #3). + fec_decode(dec, RS_DATA, (unsigned char *)pay + b * RS_ENC, + out + b * RS_DATA); + + uint16_t original_len = (uint16_t)((hdr[6] << 8) | hdr[7]); + uint8_t last_block_bytes = hdr[10]; + 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) return 0; + + unsigned int rx_crc = crc_generate_key(LIQUID_CRC_32, out, original_len); + unsigned int tx_crc = ((uint32_t)out[original_len] << 24) | + ((uint32_t)out[original_len + 1] << 16) | + ((uint32_t)out[original_len + 2] << 8) | + (uint32_t)out[original_len + 3]; + if (rx_crc != tx_crc) return 0; + + *out_len = original_len; + return 1; +} diff --git a/src/frame_tx.h b/src/frame_tx.h index ada2bb6..5c476f2 100644 --- a/src/frame_tx.h +++ b/src/frame_tx.h @@ -17,4 +17,14 @@ void phy_tx_frame(ofdmflexframegen fg, struct iio_buffer *buf, int buf_samples, fec enc, const uint8_t sig[2], uint32_t seq, uint8_t hdr11, const uint8_t *pl, size_t n, float amp, int pause_us); +// Обратная операция к phy_tx_frame для кадра, собранного с enc: RS-декод всех +// блоков payload + проверка CRC32-поверх-RS (единственный надёжный критерий, +// техдолг #3). hdr — 12 Б заголовка (нужны байты 6-7 original_len и 10 +// last_block_bytes); pay/pay_len — сырой RS-payload из ofdmflexframesync; dec — +// декодер RS(255,223); out (≥ out_cap) принимает декодированные данные, out_len +// — исходная длина. Возврат 1 при пройденном CRC, 0 иначе. Используется обратным +// каналом (fb_rx) на TX-стороне; RX-данные пока декодит свой инлайн в receiver.c. +int phy_rx_decode(const uint8_t *hdr, const uint8_t *pay, unsigned pay_len, + fec dec, uint8_t *out, size_t out_cap, size_t *out_len); + #endif diff --git a/src/ofdm_bench.c b/src/ofdm_bench.c index 9c41f0d..49c4024 100644 --- a/src/ofdm_bench.c +++ b/src/ofdm_bench.c @@ -165,6 +165,35 @@ static void bench_rs(int nerr, double frame_us) fec_destroy(enc); fec_destroy(dec); } +/* Стоимость генерации короткого кадра обратного канала (STATUS: RS-payload ≈ 1 + * блок = 255 Б). Этап 2 §10 п.7: модуляция фидбэка идёт на ядре 1 приёмника + * (поток C) — убедиться, что assemble+write дёшевы и 10 Гц STATUS не грузят ядро. + * Меряем полную генерацию кадра (assemble + write всех символов). */ +static void bench_framegen(ofdmflexframegen fg, unsigned pay_bytes) +{ + const unsigned sym = OFDM_M + OFDM_CP; + unsigned char hdr[12] = {0xF0, 0x55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + unsigned char pay[512]; + if (pay_bytes > sizeof(pay)) pay_bytes = sizeof(pay); + for (unsigned i = 0; i < pay_bytes; i++) pay[i] = (unsigned char)(i & 0xFF); + float complex buf[OFDM_M + OFDM_CP]; + + unsigned long long frames = 0; + double t0 = tnow(), t1; + do { + ofdmflexframegen_assemble(fg, hdr, pay, pay_bytes); + int done = 0; + while (!done) done = ofdmflexframegen_write(fg, buf, sym); + frames++; + t1 = tnow(); + } while (t1 - t0 < 2.0); + + double us = (t1 - t0) * 1e6 / (double)frames; + /* STATUS 10 Гц → доля ядра = 10·us/1e6, в процентах = us·1e-3 */ + printf("framegen кадр %uБ %7.1f мкс/кадр %8.0f кадров/с (STATUS 10 Гц ≈ %.3f%% ядра)\n", + pay_bytes, us, (double)frames / (t1 - t0), us * 10.0 / 1e6 * 100.0); +} + int main(int argc, char **argv) { int main_cpu = 0, use_hog = 0, opt; @@ -271,6 +300,10 @@ int main(int argc, char **argv) bench_rs(4, frame_us); bench_rs(16, frame_us); + /* обратный канал: генерация короткого STATUS-кадра (§10 п.7, этап 2) */ + printf("\n"); + bench_framegen(fg, 255); /* RS-payload STATUS ≈ 1 блок */ + /* p_min: минимальная TX-пауза для realtime @ 1.92 при чистой sync-only цепочке * (без RS, без контеншна если без -H). d/S + (1-d)/N = 1/1.92; d = frm/(frm+p) */ printf("\nМодель realtime @ %.2f Msps (sync-only%s):\n", diff --git a/src/receiver.c b/src/receiver.c index c10886f..5707601 100644 --- a/src/receiver.c +++ b/src/receiver.c @@ -9,6 +9,8 @@ #include "common.h" // _GNU_SOURCE/pthread/sched — уже подключены здесь #include "group_fec.h" +#include "frame_tx.h" // phy_tx_frame — отправка STATUS обратного канала +#include "feedback.h" // fb_status_t / pack — телеметрия B→A (этап 2, §10 п.7) #define BUF_SAMPLES 32768 #define RING_SLOTS 16 // 16 × 17 мс = 273 мс поглощения всплесков @@ -80,6 +82,75 @@ static int seq_init = 0; static fec decoder = NULL; static int fec_on = 0; +// --- Missing-tracker обратного канала (этап 2, §10 п.7) ----------------- +// Отсортированный по возрастанию набор seq отсутствующих кадров данных. Пишется +// ТОЛЬКО потоком C (в process_frame и maybe_send_status) — синхронизации нет, +// как у прочего C-состояния. Дыры seq → miss_add (по возрастанию), приход кадра +// (в т.ч. будущий ретрансмит) → miss_ack. В STATUS уходят FB_MAX_NACK старейших. +#define MISS_CAP 512 +static uint32_t miss[MISS_CAP]; +static int miss_cnt = 0; + +static void miss_add(uint32_t seq) { + if (miss_cnt >= MISS_CAP) return; // переполнение: старейшие дыры важнее + miss[miss_cnt++] = seq; // highest_seq только растёт → массив сортирован +} + +static void miss_ack(uint32_t seq) { + for (int i = 0; i < miss_cnt; i++) { + if (miss[i] == seq) { + memmove(&miss[i], &miss[i + 1], (size_t)(miss_cnt - i - 1) * sizeof(miss[0])); + miss_cnt--; + return; + } + } +} + +// --- Отправитель STATUS на feedback-TX (только при -F, поток C) ---------- +// Свой framegen + буфер на txfb (868), свой RS-энкодер. Кадр STATUS шлётся не +// чаще FB_PERIOD_MS. Данные RX и обратный TX — независимые cf-девайсы (FDD), +// поэтому push сюда не конфликтует с приёмом. +#define FB_BUF_SAMPLES 16384 +#define FB_PERIOD_MS 100 +#define FB_AMP 0.2f + +static ofdmflexframegen g_fb_fg = NULL; +static struct iio_buffer *g_fb_buf = NULL; +static fec g_fb_enc = NULL; +static uint32_t g_fb_seq = 0; +static int64_t g_fb_last_ms = 0; + +static const uint8_t SIG_STATUS[2] = { FB_SIG_0, FB_SIG_1 }; + +static int64_t now_ms(void) { + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (int64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000; +} + +static void maybe_send_status(void) { + if (!g_fb_fg) return; // симплекс — обратного канала нет + int64_t t = now_ms(); + if (g_fb_last_ms && t - g_fb_last_ms < FB_PERIOD_MS) return; + g_fb_last_ms = t; + + fb_status_t st; + memset(&st, 0, sizeof(st)); + st.type = FB_TYPE_STATUS; + st.flags = 0; // END_SEEN/COMPLETE — этап 3 + st.highest_seq = seq_init ? highest_seq : 0xFFFFFFFFu; + st.rx_frames = (uint32_t)crc_final; + st.overruns = (uint32_t)ring.overruns; // телеметрия, гонка косметическая + int cnt = miss_cnt < FB_MAX_NACK ? miss_cnt : FB_MAX_NACK; + st.nack_cnt = (uint8_t)cnt; + for (int i = 0; i < cnt; i++) st.nack[i] = miss[i]; + + uint8_t pay[FB_STATUS_MAXLEN]; + size_t n = fb_status_pack(&st, pay); + phy_tx_frame(g_fb_fg, g_fb_buf, FB_BUF_SAMPLES, g_fb_enc, + SIG_STATUS, g_fb_seq++, 0, pay, n, FB_AMP, 0); +} + // callback потока B — лёгкий: телеметрия + копия сырого кадра в очередь C. static int callback(unsigned char *hdr, int hdr_valid, unsigned char *pay, unsigned int pay_len, @@ -242,6 +313,8 @@ static void process_frame(fq_slot_t *fs) { seq_init = 1; highest_seq = seq; } else if (seq > highest_seq + 1) { + // Пропавшие seq → NACK-набор обратного канала (этап 2) + счётчик потерь. + for (uint32_t m = highest_seq + 1; m < seq; m++) miss_add(m); lost += seq - highest_seq - 1; fprintf(stderr, "\n[ПОТЕРЯ] seq %u→%u, дыра %u кадров\n", highest_seq, seq, seq - highest_seq - 1); @@ -250,6 +323,7 @@ static void process_frame(fq_slot_t *fs) { highest_seq = seq; } // seq ≤ highest_seq: старый/повторный кадр — highest_seq не трогаем. + miss_ack(seq); // кадр пришёл (в т.ч. ретрансмит старого seq) — снять из NACK uint16_t original_len = (hdr[6] << 8) | hdr[7]; uint8_t last_block_bytes = hdr[10]; @@ -396,13 +470,28 @@ static void *dsp_thread(void *arg) { return NULL; } -// Поток C (ядро 1): RS-декод + CRC + запись из очереди кадров. +// Поток C (ядро 1): RS-декод + CRC + запись из очереди кадров + периодический +// STATUS обратного канала. Ждём кадр таймингованно (50 мс), чтобы STATUS уходил +// и в простое (именно тогда TX и нужно знать про дыры). maybe_send_status — +// no-op в симплексе (g_fb_fg==NULL), поэтому поведение без -F не меняется. 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; + pthread_mutex_lock(&fq.mtx); + while (fq.count == 0 && !fq.eof && !stop) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += 50000000L; // 50 мс + if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; } + pthread_cond_timedwait(&fq.not_empty, &fq.mtx, &ts); + if (fq.count == 0 && !fq.eof && !stop) { // всё ещё пусто → STATUS без кадра + pthread_mutex_unlock(&fq.mtx); + maybe_send_status(); + pthread_mutex_lock(&fq.mtx); + } + } + if (fq.count == 0) { pthread_mutex_unlock(&fq.mtx); break; } // eof/stop fq_slot_t *fs = &fq_slots[fq.head]; pthread_mutex_unlock(&fq.mtx); @@ -412,6 +501,8 @@ static void *fec_thread(void *arg) { fq.head = (fq.head + 1) % FQ_SLOTS; fq.count--; pthread_mutex_unlock(&fq.mtx); + + maybe_send_status(); // под нагрузкой — период гейтит частоту } group_flush(); // дописать последнюю группу файла (нет кадра-триггера) return NULL; @@ -457,7 +548,7 @@ int main(int argc, char *argv[]) { fprintf(stderr, "Усиление: %.0f дБ (ручное)\n", RX_GAIN); fprintf(stderr, "CFO фильтр: α=%.2f\n", CFO_FILTER_ALPHA); fprintf(stderr, "Дуплекс: %s\n", fb_freq - ? "FDD (обратный TX настроен, трафик — этап 2)" : "симплекс"); + ? "FDD (обратный канал: передача STATUS на 868)" : "симплекс"); if (fb_freq) fprintf(stderr, "Обр. TX LO: %.1f МГц, усил. %.0f дБ\n", fb_freq / 1e6, fb_gain); fprintf(stderr, "URI: %s\n", uri); fprintf(stderr, "Конвейер: 3 стадии (A захват / B sync / C FEC+IO)\n"); @@ -519,6 +610,27 @@ int main(int argc, char *argv[]) { // Тот же 12-байтовый заголовок, что и на TX (иначе байты 8..11 — мусор) ofdmflexframesync_set_header_len(fs, HDR_SIZE); + // Обратный канал (этап 2, §10 п.7): framegen + буфер на feedback-TX (868). + // Кадры STATUS (F0 55) шлёт поток C через maybe_send_status. set_header_len + // 12 обязателен и здесь — иначе TX-сторона прочтёт заголовок как мусор. + if (fb_freq && txfb) { + iio_channel_enable(iio_device_find_channel(txfb, "voltage0", true)); + iio_channel_enable(iio_device_find_channel(txfb, "voltage1", true)); + g_fb_buf = iio_device_create_buffer(txfb, FB_BUF_SAMPLES, false); + if (!g_fb_buf) { + fprintf(stderr, "[warn] буфер обратного канала не создан — STATUS отключён\n"); + } else { + ofdmflexframegenprops_s fp; + ofdmflexframegenprops_init_default(&fp); + fp.check = LIQUID_CRC_32; + g_fb_fg = ofdmflexframegen_create(OFDM_M, OFDM_CP, OFDM_TAPER, NULL, &fp); + ofdmflexframegen_set_header_len(g_fb_fg, HDR_SIZE); + g_fb_enc = fec_create(LIQUID_FEC_RS_M8, NULL); + fprintf(stderr, "Обратный канал: STATUS каждые %d мс (F0 55, RS+CRC32)\n", + FB_PERIOD_MS); + } + } + fprintf(stderr, "Приёмник готов. Ожидание сигнала...\n\n"); fprintf(stderr, "Кадры | Загол. | CRC | RS испр | RS бит | SigBd | Потери | Overr | FQdrp | Rec | GrpF | Вывод | RSSI | EVM | CFO\n"); fprintf(stderr, "--------+--------+------+---------+--------+-------+--------+--------+--------+--------+--------+--------+-------+-------+----------\n"); @@ -565,10 +677,15 @@ int main(int argc, char *argv[]) { (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 (fb_freq) + fprintf(stderr, "Обратный канал: отправлено STATUS: %u\n", g_fb_seq); if (decoder) fec_destroy(decoder); ofdmflexframesync_destroy(fs); iio_buffer_destroy(buf); + if (g_fb_fg) ofdmflexframegen_destroy(g_fb_fg); + if (g_fb_enc) fec_destroy(g_fb_enc); + if (g_fb_buf) iio_buffer_destroy(g_fb_buf); iio_context_destroy(ctx); return 0; } diff --git a/src/transmitter.c b/src/transmitter.c index 6b01b0e..f2d91cc 100644 --- a/src/transmitter.c +++ b/src/transmitter.c @@ -8,6 +8,7 @@ #include "common.h" #include "group_fec.h" #include "frame_tx.h" +#include "feedback.h" // приём STATUS обратного канала (этап 2, §10 п.7) // Параметры передатчика по умолчанию #define DEFAULT_GAIN -30.0 // Усиление передатчика: -30 дБ @@ -24,6 +25,63 @@ extern volatile sig_atomic_t stop; // hdr[0..1]; END/STATUS обратного канала используют другие сигнатуры, §10 п.7). static const uint8_t SIG_DATA[2] = { HDR_SIGNATURE_0, HDR_SIGNATURE_1 }; +// --- Обратный канал: приём STATUS на 868 (этап 2, §10 п.7) -------------- +// Отдельный поток на ядре 1 (данные TX — ядро 0). Свой синхронизатор с +// header_len 12 и свой RS-декодер. Пока ТОЛЬКО логирует; ARQ-реакция (ретрансмит +// по NACK) — этап 3. Слушает rxfb — второй cf-девайс, открытый pluto_init_fdd. +#define FB_RX_BUF_SAMPLES 16384 + +static struct iio_buffer *g_fbrx_buf = NULL; +static ofdmflexframesync g_fbrx_fs = NULL; +static fec g_fbrx_dec = NULL; +static volatile sig_atomic_t fb_stop = 0; +static uint64_t g_fb_status_rx = 0; // сколько STATUS декодировано (для итога) + +// callback синхронизатора обратного канала: отсев по сигнатуре F0 55, RS+CRC +// декод, распаковка и лог. Этап 2 — без действий над состоянием передачи. +static int fb_callback(unsigned char *hdr, int hdr_valid, + unsigned char *pay, unsigned int pay_len, + int pay_valid, framesyncstats_s stats, void *user) { + (void)pay_valid; (void)stats; (void)user; + if (!hdr_valid) return 0; + if (hdr[0] != FB_SIG_0 || hdr[1] != FB_SIG_1) return 0; // не STATUS + + static uint8_t dec[RS_ENC * 2]; // STATUS ≤ 1 RS-блок, запас на будущее + size_t dlen = 0; + if (!phy_rx_decode(hdr, pay, pay_len, g_fbrx_dec, dec, sizeof(dec), &dlen)) + return 0; // RS/CRC не прошли — молча дропаем + fb_status_t st; + if (fb_status_unpack(dec, dlen, &st) != 0) return 0; + + g_fb_status_rx++; + fprintf(stderr, "\n[FB] highest=%u rx_frames=%u overruns=%u nack=%u{", + st.highest_seq, st.rx_frames, st.overruns, st.nack_cnt); + for (int i = 0; i < st.nack_cnt && i < 8; i++) + fprintf(stderr, "%s%u", i ? "," : "", st.nack[i]); + if (st.nack_cnt > 8) fprintf(stderr, ",…"); + fprintf(stderr, "}\n"); + return 0; +} + +static void *fb_rx_thread(void *arg) { + (void)arg; + pin_to_cpu(1); + static liquid_float_complex conv[FB_RX_BUF_SAMPLES]; + while (!fb_stop && !stop) { + ssize_t nb = iio_buffer_refill(g_fbrx_buf); + if (nb < 0) { if (fb_stop || stop) break; usleep(500); continue; } + int16_t *bb = (int16_t *)iio_buffer_start(g_fbrx_buf); + int ns = (iio_buffer_end(g_fbrx_buf) - iio_buffer_start(g_fbrx_buf)) + / (int)sizeof(int16_t) / 2; + if (ns > FB_RX_BUF_SAMPLES) ns = FB_RX_BUF_SAMPLES; + for (int i = 0; i < ns; i++) + conv[i] = ((float)bb[2*i] / 2048.0f) + + ((float)bb[2*i+1] / 2048.0f) * _Complex_I; + ofdmflexframesync_execute(g_fbrx_fs, conv, ns); + } + return NULL; +} + int main(int argc, char *argv[]) { // Инициализация параметров long long freq = DEFAULT_FREQ, rate = DEFAULT_RATE, bw = DEFAULT_BW; @@ -74,7 +132,7 @@ int main(int argc, char *argv[]) { fprintf(stderr, "Пауза: %d мкс\n", pause_us); fprintf(stderr, "Group-FEC: %s\n", group_fec ? "16+2 (§10 п.6)" : "выкл"); fprintf(stderr, "Дуплекс: %s\n", fb_freq - ? "FDD (обратный RX настроен, трафик — этап 2)" : "симплекс"); + ? "FDD (обратный канал: приём STATUS на 868)" : "симплекс"); if (fb_freq) fprintf(stderr, "Обр. RX LO: %.1f МГц\n", fb_freq / 1e6); fprintf(stderr, "URI: %s\n", uri); fprintf(stderr, "═══════════════════════════════════════════════════\n"); @@ -122,6 +180,29 @@ int main(int argc, char *argv[]) { // last_block_bytes) без этого в эфир не уходят. RX выставляет то же. ofdmflexframegen_set_header_len(fg, HDR_SIZE); + // Обратный канал (этап 2): поток fb_rx слушает STATUS на 868 (rxfb). Пока + // только логирует. Данные TX уводим на ядро 0, fb_rx — на ядро 1 (§affinity). + pthread_t fb_tid; + int fb_started = 0; + if (fb_freq) { + iio_channel_enable(iio_device_find_channel(rxfb, "voltage0", false)); + iio_channel_enable(iio_device_find_channel(rxfb, "voltage1", false)); + g_fbrx_buf = iio_device_create_buffer(rxfb, FB_RX_BUF_SAMPLES, false); + if (!g_fbrx_buf) { + LOG_ERROR("Буфер обратного канала не создан — STATUS не принимаются"); + } else { + g_fbrx_fs = ofdmflexframesync_create(OFDM_M, OFDM_CP, OFDM_TAPER, + NULL, fb_callback, NULL); + ofdmflexframesync_set_header_len(g_fbrx_fs, HDR_SIZE); + g_fbrx_dec = fec_create(LIQUID_FEC_RS_M8, NULL); + pin_to_cpu(0); // TX-данные — ядро 0 + pthread_create(&fb_tid, NULL, fb_rx_thread, NULL); + fb_started = 1; + fprintf(stderr, "Обратный канал: слушаю STATUS на %.1f МГц (ядро 1)\n", + fb_freq / 1e6); + } + } + // Счётчики статистики uint64_t frames = 0, bytes_tx = 0; uint32_t seq = 0; @@ -207,10 +288,22 @@ int main(int argc, char *argv[]) { usleep(100000); // ~4 kernel-буфера при 1.92 MSPS ≈ 35 мс, с запасом } + // Остановить обратный канал: fb_rx выходит по fb_stop в течение одного + // refill (~8.5 мс на 16384 сэмпл @ 1.92 Msps), затем join. + if (fb_started) { + fb_stop = 1; + pthread_join(fb_tid, NULL); + fprintf(stderr, "Обратный канал: принято STATUS: %llu\n", + (unsigned long long)g_fb_status_rx); + } + // Освобождение ресурсов if (enc) fec_destroy(enc); ofdmflexframegen_destroy(fg); iio_buffer_destroy(buf); + if (g_fbrx_fs) ofdmflexframesync_destroy(g_fbrx_fs); + if (g_fbrx_dec) fec_destroy(g_fbrx_dec); + if (g_fbrx_buf) iio_buffer_destroy(g_fbrx_buf); iio_context_destroy(ctx); return 0; } \ No newline at end of file