Этап 2. Новый модуль обратного канала

This commit is contained in:
Maxim
2026-07-16 11:21:14 +03:00
parent b5bf0c7b18
commit 341a5555ab
10 changed files with 428 additions and 9 deletions

View File

@@ -46,6 +46,32 @@ RX платы «глохнет», как только её TX активен. Э
Поэтому порядок работ: **сначала симплекс** (обе платы 915 МГц, полудуплекс Поэтому порядок работ: **сначала симплекс** (обе платы 915 МГц, полудуплекс
по очереди), FDD — отдельным этапом с замерами деградации RX при активном TX. по очереди), 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) ## 3. Регуляторика (868/915 МГц в регионе ETSI)
- **868 МГц** — SRD-диапазон: ограничения на полосу канала (сотни кГц), - **868 МГц** — SRD-диапазон: ограничения на полосу канала (сотни кГц),

View File

@@ -25,11 +25,11 @@ 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)/group_fec.c $(SRC)/frame_tx.c $(SRC)/common.h $(SRC)/group_fec.h $(SRC)/frame_tx.h $(DEPS_LIBS) | $(BIN) $(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 -o $@ $(LDFLAGS) -Wl,--start-group -lad9361 -liio $(LDLIBS) -Wl,--end-group $(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) $(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 -o $@ $(LDFLAGS) -Wl,--start-group -lad9361 -liio $(LDLIBS) -Wl,--end-group $(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) $(BIN)/ofdm_bench: $(SRC)/ofdm_bench.c | $(BIN)
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS) $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS)

View File

@@ -83,6 +83,23 @@ echo "--- FDD-ридбэк ---"
"${SSH[@]}" "root@$A" "grep 'FDD ридбэк' /tmp/tx_fdd.log 2>/dev/null | sed 's/^/A: /'" || true "${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 "${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 if [ "$MD5_SRC" = "$MD5_DST" ]; then
echo "РЕЗУЛЬТАТ: OK — форвард-линк под FDD цел (FBGAIN=$FBGAIN)" echo "РЕЗУЛЬТАТ: OK — форвард-линк под FDD цел (FBGAIN=$FBGAIN)"
else else

48
src/feedback.c Normal file
View File

@@ -0,0 +1,48 @@
// feedback.c — упаковка/распаковка STATUS обратного канала (этап 2, §10 п.7).
// Чистая логика без зависимостей от liquid/iio → пригодна к офлайн-юнит-тесту.
#include "feedback.h"
#include <string.h>
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;
}

45
src/feedback.h Normal file
View File

@@ -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 <stdint.h>
#include <stddef.h>
#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

View File

@@ -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 (idx > 0) iio_buffer_push_partial(buf, idx);
if (pause_us > 0) usleep(pause_us); 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;
}

View File

@@ -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, 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); 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 #endif

View File

@@ -165,6 +165,35 @@ static void bench_rs(int nerr, double frame_us)
fec_destroy(enc); fec_destroy(dec); 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(int argc, char **argv)
{ {
int main_cpu = 0, use_hog = 0, opt; 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(4, frame_us);
bench_rs(16, 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 цепочке /* p_min: минимальная TX-пауза для realtime @ 1.92 при чистой sync-only цепочке
* (без RS, без контеншна если без -H). d/S + (1-d)/N = 1/1.92; d = frm/(frm+p) */ * (без RS, без контеншна если без -H). d/S + (1-d)/N = 1/1.92; d = frm/(frm+p) */
printf("\nМодель realtime @ %.2f Msps (sync-only%s):\n", printf("\nМодель realtime @ %.2f Msps (sync-only%s):\n",

View File

@@ -9,6 +9,8 @@
#include "common.h" // _GNU_SOURCE/pthread/sched — уже подключены здесь #include "common.h" // _GNU_SOURCE/pthread/sched — уже подключены здесь
#include "group_fec.h" #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 BUF_SAMPLES 32768
#define RING_SLOTS 16 // 16 × 17 мс = 273 мс поглощения всплесков #define RING_SLOTS 16 // 16 × 17 мс = 273 мс поглощения всплесков
@@ -80,6 +82,75 @@ static int seq_init = 0;
static fec decoder = NULL; static fec decoder = NULL;
static int fec_on = 0; 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. // callback потока B — лёгкий: телеметрия + копия сырого кадра в очередь C.
static int callback(unsigned char *hdr, int hdr_valid, static int callback(unsigned char *hdr, int hdr_valid,
unsigned char *pay, unsigned int pay_len, unsigned char *pay, unsigned int pay_len,
@@ -242,6 +313,8 @@ static void process_frame(fq_slot_t *fs) {
seq_init = 1; seq_init = 1;
highest_seq = seq; highest_seq = seq;
} else if (seq > highest_seq + 1) { } 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; lost += seq - highest_seq - 1;
fprintf(stderr, "\n[ПОТЕРЯ] seq %u→%u, дыра %u кадров\n", fprintf(stderr, "\n[ПОТЕРЯ] seq %u→%u, дыра %u кадров\n",
highest_seq, seq, seq - highest_seq - 1); highest_seq, seq, seq - highest_seq - 1);
@@ -250,6 +323,7 @@ static void process_frame(fq_slot_t *fs) {
highest_seq = seq; highest_seq = seq;
} }
// seq ≤ highest_seq: старый/повторный кадр — highest_seq не трогаем. // seq ≤ highest_seq: старый/повторный кадр — highest_seq не трогаем.
miss_ack(seq); // кадр пришёл (в т.ч. ретрансмит старого seq) — снять из NACK
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];
@@ -396,13 +470,28 @@ static void *dsp_thread(void *arg) {
return NULL; 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) { static void *fec_thread(void *arg) {
(void)arg; (void)arg;
pin_to_cpu(1); pin_to_cpu(1);
for (;;) { for (;;) {
if (!wait_slot(&fq.mtx, &fq.not_empty, &fq.count, &fq.eof)) pthread_mutex_lock(&fq.mtx);
break; 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]; fq_slot_t *fs = &fq_slots[fq.head];
pthread_mutex_unlock(&fq.mtx); pthread_mutex_unlock(&fq.mtx);
@@ -412,6 +501,8 @@ static void *fec_thread(void *arg) {
fq.head = (fq.head + 1) % FQ_SLOTS; fq.head = (fq.head + 1) % FQ_SLOTS;
fq.count--; fq.count--;
pthread_mutex_unlock(&fq.mtx); pthread_mutex_unlock(&fq.mtx);
maybe_send_status(); // под нагрузкой — период гейтит частоту
} }
group_flush(); // дописать последнюю группу файла (нет кадра-триггера) group_flush(); // дописать последнюю группу файла (нет кадра-триггера)
return NULL; return NULL;
@@ -457,7 +548,7 @@ int main(int argc, char *argv[]) {
fprintf(stderr, "Усиление: %.0f дБ (ручное)\n", RX_GAIN); fprintf(stderr, "Усиление: %.0f дБ (ручное)\n", RX_GAIN);
fprintf(stderr, "CFO фильтр: α=%.2f\n", CFO_FILTER_ALPHA); fprintf(stderr, "CFO фильтр: α=%.2f\n", CFO_FILTER_ALPHA);
fprintf(stderr, "Дуплекс: %s\n", fb_freq 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); if (fb_freq) fprintf(stderr, "Обр. TX LO: %.1f МГц, усил. %.0f дБ\n", fb_freq / 1e6, fb_gain);
fprintf(stderr, "URI: %s\n", uri); fprintf(stderr, "URI: %s\n", uri);
fprintf(stderr, "Конвейер: 3 стадии (A захват / B sync / C FEC+IO)\n"); fprintf(stderr, "Конвейер: 3 стадии (A захват / B sync / C FEC+IO)\n");
@@ -519,6 +610,27 @@ int main(int argc, char *argv[]) {
// Тот же 12-байтовый заголовок, что и на TX (иначе байты 8..11 — мусор) // Тот же 12-байтовый заголовок, что и на TX (иначе байты 8..11 — мусор)
ofdmflexframesync_set_header_len(fs, HDR_SIZE); 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, "Приёмник готов. Ожидание сигнала...\n\n");
fprintf(stderr, "Кадры | Загол. | CRC | RS испр | RS бит | SigBd | Потери | Overr | FQdrp | Rec | GrpF | Вывод | RSSI | EVM | CFO\n"); fprintf(stderr, "Кадры | Загол. | CRC | RS испр | RS бит | SigBd | Потери | Overr | FQdrp | Rec | GrpF | Вывод | RSSI | EVM | CFO\n");
fprintf(stderr, "--------+--------+------+---------+--------+-------+--------+--------+--------+--------+--------+--------+-------+-------+----------\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)ring.overruns, (unsigned long long)fq.drops,
(unsigned long long)grp_recovered, (unsigned long long)grp_failed, (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 (fb_freq)
fprintf(stderr, "Обратный канал: отправлено STATUS: %u\n", g_fb_seq);
if (decoder) fec_destroy(decoder); if (decoder) fec_destroy(decoder);
ofdmflexframesync_destroy(fs); ofdmflexframesync_destroy(fs);
iio_buffer_destroy(buf); 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); iio_context_destroy(ctx);
return 0; return 0;
} }

View File

@@ -8,6 +8,7 @@
#include "common.h" #include "common.h"
#include "group_fec.h" #include "group_fec.h"
#include "frame_tx.h" #include "frame_tx.h"
#include "feedback.h" // приём STATUS обратного канала (этап 2, §10 п.7)
// Параметры передатчика по умолчанию // Параметры передатчика по умолчанию
#define DEFAULT_GAIN -30.0 // Усиление передатчика: -30 дБ #define DEFAULT_GAIN -30.0 // Усиление передатчика: -30 дБ
@@ -24,6 +25,63 @@ extern volatile sig_atomic_t stop;
// hdr[0..1]; END/STATUS обратного канала используют другие сигнатуры, §10 п.7). // hdr[0..1]; END/STATUS обратного канала используют другие сигнатуры, §10 п.7).
static const uint8_t SIG_DATA[2] = { HDR_SIGNATURE_0, HDR_SIGNATURE_1 }; 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[]) { 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;
@@ -74,7 +132,7 @@ int main(int argc, char *argv[]) {
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, "Group-FEC: %s\n", group_fec ? "16+2 (§10 п.6)" : "выкл");
fprintf(stderr, "Дуплекс: %s\n", fb_freq fprintf(stderr, "Дуплекс: %s\n", fb_freq
? "FDD (обратный RX настроен, трафик — этап 2)" : "симплекс"); ? "FDD (обратный канал: приём STATUS на 868)" : "симплекс");
if (fb_freq) fprintf(stderr, "Обр. RX LO: %.1f МГц\n", fb_freq / 1e6); if (fb_freq) fprintf(stderr, "Обр. RX LO: %.1f МГц\n", fb_freq / 1e6);
fprintf(stderr, "URI: %s\n", uri); fprintf(stderr, "URI: %s\n", uri);
fprintf(stderr, "═══════════════════════════════════════════════════\n"); fprintf(stderr, "═══════════════════════════════════════════════════\n");
@@ -122,6 +180,29 @@ int main(int argc, char *argv[]) {
// last_block_bytes) без этого в эфир не уходят. RX выставляет то же. // last_block_bytes) без этого в эфир не уходят. RX выставляет то же.
ofdmflexframegen_set_header_len(fg, HDR_SIZE); 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; uint64_t frames = 0, bytes_tx = 0;
uint32_t seq = 0; uint32_t seq = 0;
@@ -207,10 +288,22 @@ int main(int argc, char *argv[]) {
usleep(100000); // ~4 kernel-буфера при 1.92 MSPS ≈ 35 мс, с запасом 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); if (enc) fec_destroy(enc);
ofdmflexframegen_destroy(fg); ofdmflexframegen_destroy(fg);
iio_buffer_destroy(buf); 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); iio_context_destroy(ctx);
return 0; return 0;
} }