1148 lines
22 KiB
Python
1148 lines
22 KiB
Python
"""
|
||
Lab018. Формирование BPSK-радиокадра.
|
||
|
||
Программа:
|
||
|
||
1. Формирует внутренний пакет SDR Rover Link.
|
||
2. Добавляет радиопреамбулу.
|
||
3. Добавляет внешнее синхрослово.
|
||
4. Добавляет длину внутреннего пакета.
|
||
5. Преобразует радиокадр в биты.
|
||
6. Модулирует биты методом BPSK.
|
||
7. Увеличивает частоту дискретизации.
|
||
8. Применяет Root Raised Cosine-фильтр.
|
||
9. Формирует комплексные IQ-сэмплы.
|
||
10. Сохраняет IQ в форматах NPY и CF32.
|
||
11. Строит графики кадра, сигнала и спектра.
|
||
|
||
Передача через SDR в этой лабораторной не выполняется.
|
||
"""
|
||
|
||
from pathlib import Path
|
||
import struct
|
||
|
||
import matplotlib.pyplot as plt
|
||
import numpy as np
|
||
|
||
from protocol.packet import (
|
||
MESSAGE_TYPE_TEXT,
|
||
build_packet,
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Настройки содержимого
|
||
# ============================================================
|
||
|
||
MESSAGE = "ПРИВЕТ SDR"
|
||
|
||
SEQUENCE_NUMBER = 18
|
||
|
||
|
||
# ============================================================
|
||
# Настройки радиокадра
|
||
# ============================================================
|
||
|
||
# Радиосинхрослово физического уровня.
|
||
#
|
||
# Не следует путать с SYNC_WORD = 0xAA55,
|
||
# который находится внутри packet.py.
|
||
RADIO_SYNC_WORD = 0xD391
|
||
|
||
# Преамбула 10101010...
|
||
PREAMBLE_BIT_COUNT = 64
|
||
|
||
# Пустой участок до и после радиокадра.
|
||
GUARD_SYMBOL_COUNT = 16
|
||
|
||
|
||
# ============================================================
|
||
# Настройки BPSK
|
||
# ============================================================
|
||
|
||
# BPSK передаёт один бит одним символом.
|
||
SYMBOL_RATE = 20_000
|
||
|
||
# На один BPSK-символ приходится 32 IQ-сэмпла.
|
||
SAMPLES_PER_SYMBOL = 32
|
||
|
||
SAMPLE_RATE = (
|
||
SYMBOL_RATE
|
||
* SAMPLES_PER_SYMBOL
|
||
)
|
||
|
||
# Коэффициент скругления RRC-фильтра.
|
||
RRC_ROLLOFF = 0.35
|
||
|
||
# Длина фильтра в символах.
|
||
#
|
||
# Используем чётное значение.
|
||
RRC_SPAN_SYMBOLS = 10
|
||
|
||
# Амплитудный запас.
|
||
#
|
||
# Не используем полную шкалу ±1,
|
||
# чтобы позднее не перегружать передатчик.
|
||
TX_AMPLITUDE = 0.70
|
||
|
||
|
||
# ============================================================
|
||
# Выходные файлы
|
||
# ============================================================
|
||
|
||
OUTPUT_DIRECTORY = Path(
|
||
"data/processed/lab018"
|
||
)
|
||
|
||
OUTPUT_DIRECTORY.mkdir(
|
||
parents=True,
|
||
exist_ok=True,
|
||
)
|
||
|
||
NPY_PATH = (
|
||
OUTPUT_DIRECTORY
|
||
/ "lab018_bpsk_tx_iq.npy"
|
||
)
|
||
|
||
CF32_PATH = (
|
||
OUTPUT_DIRECTORY
|
||
/ "lab018_bpsk_tx_iq.cf32"
|
||
)
|
||
|
||
GRAPH_PATH = (
|
||
OUTPUT_DIRECTORY
|
||
/ "lab018_bpsk_radio_frame.png"
|
||
)
|
||
|
||
INFO_PATH = (
|
||
OUTPUT_DIRECTORY
|
||
/ "lab018_frame_info.txt"
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Преобразование байтов в биты
|
||
# ============================================================
|
||
|
||
def bytes_to_bits(
|
||
data: bytes,
|
||
) -> np.ndarray:
|
||
"""
|
||
Преобразовать bytes в массив битов 0 и 1.
|
||
|
||
Используется порядок от старшего бита к младшему.
|
||
"""
|
||
|
||
if not isinstance(data, bytes):
|
||
raise TypeError(
|
||
"data должен иметь тип bytes"
|
||
)
|
||
|
||
byte_array = np.frombuffer(
|
||
data,
|
||
dtype=np.uint8,
|
||
)
|
||
|
||
return np.unpackbits(
|
||
byte_array
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# BPSK-модулятор
|
||
# ============================================================
|
||
|
||
def bpsk_modulate(
|
||
bits: np.ndarray,
|
||
) -> np.ndarray:
|
||
"""
|
||
Преобразовать биты в BPSK-символы.
|
||
|
||
0 → -1
|
||
1 → +1
|
||
"""
|
||
|
||
bits = np.asarray(
|
||
bits,
|
||
dtype=np.uint8,
|
||
)
|
||
|
||
if bits.ndim != 1:
|
||
raise ValueError(
|
||
"bits должен быть одномерным массивом"
|
||
)
|
||
|
||
if not np.all(
|
||
(bits == 0) | (bits == 1)
|
||
):
|
||
raise ValueError(
|
||
"bits должен содержать только 0 и 1"
|
||
)
|
||
|
||
symbols = (
|
||
2.0 * bits.astype(np.float64)
|
||
- 1.0
|
||
)
|
||
|
||
return symbols.astype(
|
||
np.complex128
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Root Raised Cosine-фильтр
|
||
# ============================================================
|
||
|
||
def root_raised_cosine_taps(
|
||
rolloff: float,
|
||
samples_per_symbol: int,
|
||
span_symbols: int,
|
||
) -> np.ndarray:
|
||
"""
|
||
Рассчитать коэффициенты Root Raised Cosine-фильтра.
|
||
|
||
Параметры
|
||
----------
|
||
rolloff:
|
||
Коэффициент скругления beta.
|
||
|
||
samples_per_symbol:
|
||
Количество сэмплов на символ.
|
||
|
||
span_symbols:
|
||
Полная длина фильтра в символах.
|
||
|
||
Возвращает
|
||
----------
|
||
Одномерный массив коэффициентов фильтра.
|
||
"""
|
||
|
||
if not 0.0 < rolloff <= 1.0:
|
||
raise ValueError(
|
||
"rolloff должен находиться в диапазоне 0...1"
|
||
)
|
||
|
||
if samples_per_symbol <= 0:
|
||
raise ValueError(
|
||
"samples_per_symbol должен быть положительным"
|
||
)
|
||
|
||
if span_symbols <= 0:
|
||
raise ValueError(
|
||
"span_symbols должен быть положительным"
|
||
)
|
||
|
||
if span_symbols % 2 != 0:
|
||
raise ValueError(
|
||
"span_symbols должен быть чётным"
|
||
)
|
||
|
||
half_sample_count = (
|
||
span_symbols
|
||
* samples_per_symbol
|
||
// 2
|
||
)
|
||
|
||
sample_indexes = np.arange(
|
||
-half_sample_count,
|
||
half_sample_count + 1,
|
||
dtype=np.float64,
|
||
)
|
||
|
||
# Время нормировано к длительности одного символа.
|
||
time_values = (
|
||
sample_indexes
|
||
/ samples_per_symbol
|
||
)
|
||
|
||
taps = np.zeros_like(
|
||
time_values
|
||
)
|
||
|
||
beta = rolloff
|
||
|
||
for index, time_value in enumerate(
|
||
time_values
|
||
):
|
||
|
||
# Особая точка t = 0.
|
||
if np.isclose(
|
||
time_value,
|
||
0.0,
|
||
):
|
||
taps[index] = (
|
||
1.0
|
||
- beta
|
||
+ (
|
||
4.0
|
||
* beta
|
||
/ np.pi
|
||
)
|
||
)
|
||
|
||
continue
|
||
|
||
# Особые точки t = ±1/(4 beta).
|
||
if np.isclose(
|
||
abs(time_value),
|
||
1.0 / (4.0 * beta),
|
||
):
|
||
taps[index] = (
|
||
beta
|
||
/ np.sqrt(2.0)
|
||
* (
|
||
(
|
||
1.0
|
||
+ 2.0 / np.pi
|
||
)
|
||
* np.sin(
|
||
np.pi
|
||
/ (4.0 * beta)
|
||
)
|
||
+ (
|
||
1.0
|
||
- 2.0 / np.pi
|
||
)
|
||
* np.cos(
|
||
np.pi
|
||
/ (4.0 * beta)
|
||
)
|
||
)
|
||
)
|
||
|
||
continue
|
||
|
||
numerator = (
|
||
np.sin(
|
||
np.pi
|
||
* time_value
|
||
* (1.0 - beta)
|
||
)
|
||
+ (
|
||
4.0
|
||
* beta
|
||
* time_value
|
||
* np.cos(
|
||
np.pi
|
||
* time_value
|
||
* (1.0 + beta)
|
||
)
|
||
)
|
||
)
|
||
|
||
denominator = (
|
||
np.pi
|
||
* time_value
|
||
* (
|
||
1.0
|
||
- (
|
||
4.0
|
||
* beta
|
||
* time_value
|
||
) ** 2
|
||
)
|
||
)
|
||
|
||
taps[index] = (
|
||
numerator
|
||
/ denominator
|
||
)
|
||
|
||
# Нормируем энергию фильтра.
|
||
taps /= np.sqrt(
|
||
np.sum(
|
||
taps ** 2
|
||
)
|
||
)
|
||
|
||
return taps
|
||
|
||
|
||
# ============================================================
|
||
# Увеличение частоты дискретизации
|
||
# ============================================================
|
||
|
||
def upsample_symbols(
|
||
symbols: np.ndarray,
|
||
samples_per_symbol: int,
|
||
) -> np.ndarray:
|
||
"""
|
||
Вставить между символами нулевые сэмплы.
|
||
|
||
Пример при SPS = 4:
|
||
|
||
[1, -1]
|
||
↓
|
||
[1, 0, 0, 0, -1, 0, 0, 0]
|
||
"""
|
||
|
||
symbols = np.asarray(
|
||
symbols,
|
||
dtype=np.complex128,
|
||
)
|
||
|
||
upsampled = np.zeros(
|
||
len(symbols)
|
||
* samples_per_symbol,
|
||
dtype=np.complex128,
|
||
)
|
||
|
||
upsampled[
|
||
::samples_per_symbol
|
||
] = symbols
|
||
|
||
return upsampled
|
||
|
||
|
||
# ============================================================
|
||
# Спектр
|
||
# ============================================================
|
||
|
||
def calculate_normalized_spectrum(
|
||
iq_samples: np.ndarray,
|
||
sample_rate: float,
|
||
) -> tuple[np.ndarray, np.ndarray]:
|
||
"""
|
||
Рассчитать нормированный амплитудный спектр.
|
||
"""
|
||
|
||
fft_size = 2 ** int(
|
||
np.ceil(
|
||
np.log2(
|
||
max(
|
||
len(iq_samples),
|
||
1024,
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
spectrum = np.fft.fftshift(
|
||
np.fft.fft(
|
||
iq_samples,
|
||
n=fft_size,
|
||
)
|
||
)
|
||
|
||
frequencies = np.fft.fftshift(
|
||
np.fft.fftfreq(
|
||
fft_size,
|
||
d=1.0 / sample_rate,
|
||
)
|
||
)
|
||
|
||
magnitude = np.abs(
|
||
spectrum
|
||
)
|
||
|
||
maximum_magnitude = np.max(
|
||
magnitude
|
||
)
|
||
|
||
magnitude_db = 20.0 * np.log10(
|
||
magnitude / maximum_magnitude
|
||
+ 1e-12
|
||
)
|
||
|
||
return frequencies, magnitude_db
|
||
|
||
|
||
# ============================================================
|
||
# Формирование внутреннего пакета
|
||
# ============================================================
|
||
|
||
payload = MESSAGE.encode(
|
||
"utf-8"
|
||
)
|
||
|
||
protocol_packet = build_packet(
|
||
payload=payload,
|
||
message_type=MESSAGE_TYPE_TEXT,
|
||
sequence_number=SEQUENCE_NUMBER,
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Формирование внешнего заголовка радиокадра
|
||
# ============================================================
|
||
|
||
if len(protocol_packet) > 65535:
|
||
raise ValueError(
|
||
"Внутренний пакет слишком велик "
|
||
"для 16-битного поля длины"
|
||
)
|
||
|
||
radio_header = struct.pack(
|
||
">HH",
|
||
RADIO_SYNC_WORD,
|
||
len(protocol_packet),
|
||
)
|
||
|
||
radio_header_bits = bytes_to_bits(
|
||
radio_header
|
||
)
|
||
|
||
protocol_packet_bits = bytes_to_bits(
|
||
protocol_packet
|
||
)
|
||
|
||
# Преамбула 101010...
|
||
preamble_bits = np.tile(
|
||
np.array(
|
||
[1, 0],
|
||
dtype=np.uint8,
|
||
),
|
||
PREAMBLE_BIT_COUNT // 2,
|
||
)
|
||
|
||
radio_frame_bits = np.concatenate(
|
||
[
|
||
preamble_bits,
|
||
radio_header_bits,
|
||
protocol_packet_bits,
|
||
]
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# BPSK-модуляция
|
||
# ============================================================
|
||
|
||
radio_frame_symbols = bpsk_modulate(
|
||
radio_frame_bits
|
||
)
|
||
|
||
upsampled_symbols = upsample_symbols(
|
||
radio_frame_symbols,
|
||
SAMPLES_PER_SYMBOL,
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Guard interval
|
||
# ============================================================
|
||
|
||
guard_samples = np.zeros(
|
||
GUARD_SYMBOL_COUNT
|
||
* SAMPLES_PER_SYMBOL,
|
||
dtype=np.complex128,
|
||
)
|
||
|
||
guarded_upsampled_symbols = np.concatenate(
|
||
[
|
||
guard_samples,
|
||
upsampled_symbols,
|
||
guard_samples,
|
||
]
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# RRC-фильтрация
|
||
# ============================================================
|
||
|
||
rrc_taps = root_raised_cosine_taps(
|
||
rolloff=RRC_ROLLOFF,
|
||
samples_per_symbol=SAMPLES_PER_SYMBOL,
|
||
span_symbols=RRC_SPAN_SYMBOLS,
|
||
)
|
||
|
||
filtered_iq = np.convolve(
|
||
guarded_upsampled_symbols,
|
||
rrc_taps,
|
||
mode="full",
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Нормирование амплитуды
|
||
# ============================================================
|
||
|
||
maximum_amplitude = np.max(
|
||
np.abs(
|
||
filtered_iq
|
||
)
|
||
)
|
||
|
||
if maximum_amplitude == 0.0:
|
||
raise RuntimeError(
|
||
"Сформирован нулевой IQ-сигнал"
|
||
)
|
||
|
||
normalized_iq = (
|
||
filtered_iq
|
||
/ maximum_amplitude
|
||
* TX_AMPLITUDE
|
||
)
|
||
|
||
tx_iq = normalized_iq.astype(
|
||
np.complex64
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Параметры сигнала
|
||
# ============================================================
|
||
|
||
frame_duration_seconds = (
|
||
len(tx_iq)
|
||
/ SAMPLE_RATE
|
||
)
|
||
|
||
theoretical_baseband_bandwidth_hz = (
|
||
SYMBOL_RATE
|
||
* (1.0 + RRC_ROLLOFF)
|
||
)
|
||
|
||
average_power = float(
|
||
np.mean(
|
||
np.abs(tx_iq) ** 2
|
||
)
|
||
)
|
||
|
||
peak_power = float(
|
||
np.max(
|
||
np.abs(tx_iq) ** 2
|
||
)
|
||
)
|
||
|
||
papr_db = 10.0 * np.log10(
|
||
peak_power / average_power
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Сохранение IQ
|
||
# ============================================================
|
||
|
||
# Формат NPY сохраняет тип и форму массива.
|
||
np.save(
|
||
NPY_PATH,
|
||
tx_iq,
|
||
)
|
||
|
||
# complex64 записывается как чередующиеся:
|
||
#
|
||
# float32 I, float32 Q, float32 I, float32 Q...
|
||
tx_iq.tofile(
|
||
CF32_PATH
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Проверка сохранённых данных
|
||
# ============================================================
|
||
|
||
loaded_npy_iq = np.load(
|
||
NPY_PATH
|
||
)
|
||
|
||
loaded_cf32_iq = np.fromfile(
|
||
CF32_PATH,
|
||
dtype=np.complex64,
|
||
)
|
||
|
||
assert np.array_equal(
|
||
loaded_npy_iq,
|
||
tx_iq,
|
||
)
|
||
|
||
assert np.array_equal(
|
||
loaded_cf32_iq,
|
||
tx_iq,
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Спектр
|
||
# ============================================================
|
||
|
||
frequencies_hz, spectrum_db = (
|
||
calculate_normalized_spectrum(
|
||
iq_samples=tx_iq,
|
||
sample_rate=SAMPLE_RATE,
|
||
)
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Графики
|
||
# ============================================================
|
||
|
||
figure, axes = plt.subplots(
|
||
4,
|
||
1,
|
||
figsize=(13, 13),
|
||
)
|
||
|
||
|
||
# ------------------------------------------------------------
|
||
# 1. Структура битового радиокадра
|
||
# ------------------------------------------------------------
|
||
|
||
bit_indexes = np.arange(
|
||
len(radio_frame_bits)
|
||
)
|
||
|
||
axes[0].step(
|
||
bit_indexes,
|
||
radio_frame_bits,
|
||
where="post",
|
||
)
|
||
|
||
preamble_end = (
|
||
PREAMBLE_BIT_COUNT
|
||
)
|
||
|
||
sync_end = (
|
||
preamble_end + 16
|
||
)
|
||
|
||
length_end = (
|
||
sync_end + 16
|
||
)
|
||
|
||
axes[0].axvline(
|
||
preamble_end,
|
||
linestyle="--",
|
||
)
|
||
|
||
axes[0].axvline(
|
||
sync_end,
|
||
linestyle="--",
|
||
)
|
||
|
||
axes[0].axvline(
|
||
length_end,
|
||
linestyle="--",
|
||
)
|
||
|
||
axes[0].text(
|
||
PREAMBLE_BIT_COUNT / 2,
|
||
1.08,
|
||
"PREAMBLE",
|
||
ha="center",
|
||
)
|
||
|
||
axes[0].text(
|
||
preamble_end + 8,
|
||
1.08,
|
||
"SYNC",
|
||
ha="center",
|
||
)
|
||
|
||
axes[0].text(
|
||
sync_end + 8,
|
||
1.08,
|
||
"LENGTH",
|
||
ha="center",
|
||
)
|
||
|
||
axes[0].text(
|
||
length_end
|
||
+ len(protocol_packet_bits) / 2,
|
||
1.08,
|
||
"SDR ROVER PACKET",
|
||
ha="center",
|
||
)
|
||
|
||
axes[0].set_ylim(
|
||
-0.2,
|
||
1.3,
|
||
)
|
||
|
||
axes[0].set_xlabel(
|
||
"Номер бита"
|
||
)
|
||
|
||
axes[0].set_ylabel(
|
||
"Бит"
|
||
)
|
||
|
||
axes[0].set_title(
|
||
"Структура радиокадра"
|
||
)
|
||
|
||
axes[0].grid(
|
||
True
|
||
)
|
||
|
||
|
||
# ------------------------------------------------------------
|
||
# 2. Первые BPSK-символы
|
||
# ------------------------------------------------------------
|
||
|
||
symbol_plot_count = min(
|
||
100,
|
||
len(radio_frame_symbols),
|
||
)
|
||
|
||
axes[1].step(
|
||
np.arange(
|
||
symbol_plot_count
|
||
),
|
||
radio_frame_symbols[
|
||
:symbol_plot_count
|
||
].real,
|
||
where="post",
|
||
)
|
||
|
||
axes[1].set_xlabel(
|
||
"Номер символа"
|
||
)
|
||
|
||
axes[1].set_ylabel(
|
||
"Амплитуда BPSK"
|
||
)
|
||
|
||
axes[1].set_title(
|
||
"Первые BPSK-символы"
|
||
)
|
||
|
||
axes[1].set_ylim(
|
||
-1.3,
|
||
1.3,
|
||
)
|
||
|
||
axes[1].grid(
|
||
True
|
||
)
|
||
|
||
|
||
# ------------------------------------------------------------
|
||
# 3. Отфильтрованный IQ-сигнал
|
||
# ------------------------------------------------------------
|
||
|
||
filter_delay_samples = (
|
||
len(rrc_taps) - 1
|
||
) // 2
|
||
|
||
frame_start_sample = (
|
||
GUARD_SYMBOL_COUNT
|
||
* SAMPLES_PER_SYMBOL
|
||
+ filter_delay_samples
|
||
)
|
||
|
||
waveform_plot_symbol_count = 20
|
||
|
||
waveform_plot_start = (
|
||
frame_start_sample
|
||
)
|
||
|
||
waveform_plot_end = (
|
||
waveform_plot_start
|
||
+ waveform_plot_symbol_count
|
||
* SAMPLES_PER_SYMBOL
|
||
)
|
||
|
||
waveform_plot_samples = tx_iq[
|
||
waveform_plot_start:
|
||
waveform_plot_end
|
||
]
|
||
|
||
time_axis_ms = (
|
||
np.arange(
|
||
len(waveform_plot_samples)
|
||
)
|
||
/ SAMPLE_RATE
|
||
* 1000.0
|
||
)
|
||
|
||
axes[2].plot(
|
||
time_axis_ms,
|
||
waveform_plot_samples.real,
|
||
label="I",
|
||
)
|
||
|
||
axes[2].plot(
|
||
time_axis_ms,
|
||
waveform_plot_samples.imag,
|
||
label="Q",
|
||
)
|
||
|
||
axes[2].set_xlabel(
|
||
"Время, мс"
|
||
)
|
||
|
||
axes[2].set_ylabel(
|
||
"Амплитуда"
|
||
)
|
||
|
||
axes[2].set_title(
|
||
"BPSK после Root Raised Cosine-фильтра"
|
||
)
|
||
|
||
axes[2].grid(
|
||
True
|
||
)
|
||
|
||
axes[2].legend()
|
||
|
||
|
||
# ------------------------------------------------------------
|
||
# 4. Спектр
|
||
# ------------------------------------------------------------
|
||
|
||
axes[3].plot(
|
||
frequencies_hz / 1000.0,
|
||
spectrum_db,
|
||
)
|
||
|
||
axes[3].axvline(
|
||
theoretical_baseband_bandwidth_hz
|
||
/ 2.0
|
||
/ 1000.0,
|
||
linestyle="--",
|
||
)
|
||
|
||
axes[3].axvline(
|
||
-theoretical_baseband_bandwidth_hz
|
||
/ 2.0
|
||
/ 1000.0,
|
||
linestyle="--",
|
||
)
|
||
|
||
axes[3].set_xlim(
|
||
-100,
|
||
100,
|
||
)
|
||
|
||
axes[3].set_ylim(
|
||
-100,
|
||
5,
|
||
)
|
||
|
||
axes[3].set_xlabel(
|
||
"Частота относительно несущей, кГц"
|
||
)
|
||
|
||
axes[3].set_ylabel(
|
||
"Нормированный уровень, дБ"
|
||
)
|
||
|
||
axes[3].set_title(
|
||
"Спектр сформированного IQ-сигнала"
|
||
)
|
||
|
||
axes[3].grid(
|
||
True
|
||
)
|
||
|
||
|
||
figure.tight_layout()
|
||
|
||
figure.savefig(
|
||
GRAPH_PATH,
|
||
dpi=160,
|
||
)
|
||
|
||
plt.close(
|
||
figure
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Текстовый отчёт
|
||
# ============================================================
|
||
|
||
report_lines = [
|
||
"Lab018. BPSK radio frame",
|
||
"",
|
||
f"Message: {MESSAGE}",
|
||
f"Protocol packet bytes: {len(protocol_packet)}",
|
||
f"Preamble bits: {len(preamble_bits)}",
|
||
f"Radio header bits: {len(radio_header_bits)}",
|
||
f"Protocol packet bits: {len(protocol_packet_bits)}",
|
||
f"Total radio frame bits: {len(radio_frame_bits)}",
|
||
f"Symbol rate: {SYMBOL_RATE} symbols/s",
|
||
f"Samples per symbol: {SAMPLES_PER_SYMBOL}",
|
||
f"Sample rate: {SAMPLE_RATE} samples/s",
|
||
f"RRC rolloff: {RRC_ROLLOFF}",
|
||
f"RRC taps: {len(rrc_taps)}",
|
||
f"IQ sample count: {len(tx_iq)}",
|
||
f"Frame duration: {frame_duration_seconds:.6f} s",
|
||
(
|
||
"Approximate occupied bandwidth: "
|
||
f"{theoretical_baseband_bandwidth_hz:.1f} Hz"
|
||
),
|
||
f"Average normalized power: {average_power:.6f}",
|
||
f"Peak normalized power: {peak_power:.6f}",
|
||
f"PAPR: {papr_db:.2f} dB",
|
||
f"NPY file: {NPY_PATH}",
|
||
f"CF32 file: {CF32_PATH}",
|
||
]
|
||
|
||
INFO_PATH.write_text(
|
||
"\n".join(
|
||
report_lines
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Вывод результатов
|
||
# ============================================================
|
||
|
||
print(
|
||
"=== Lab018. Формирование BPSK-радиокадра ==="
|
||
)
|
||
|
||
print("\nИсходное сообщение:")
|
||
|
||
print(MESSAGE)
|
||
|
||
print("\nВнутренний пакет SDR Rover Link:")
|
||
|
||
print(
|
||
len(protocol_packet),
|
||
"байт",
|
||
)
|
||
|
||
print("\nРадиосинхрослово:")
|
||
|
||
print(
|
||
f"0x{RADIO_SYNC_WORD:04X}"
|
||
)
|
||
|
||
print("\nСтруктура радиокадра:")
|
||
|
||
print(
|
||
"Преамбула:",
|
||
len(preamble_bits),
|
||
"бит",
|
||
)
|
||
|
||
print(
|
||
"Радиозаголовок:",
|
||
len(radio_header_bits),
|
||
"бит",
|
||
)
|
||
|
||
print(
|
||
"Внутренний пакет:",
|
||
len(protocol_packet_bits),
|
||
"бит",
|
||
)
|
||
|
||
print(
|
||
"Всего:",
|
||
len(radio_frame_bits),
|
||
"битов и BPSK-символов",
|
||
)
|
||
|
||
print("\nПараметры дискретизации:")
|
||
|
||
print(
|
||
"Символьная скорость:",
|
||
SYMBOL_RATE,
|
||
"символов/с",
|
||
)
|
||
|
||
print(
|
||
"Сэмплов на символ:",
|
||
SAMPLES_PER_SYMBOL,
|
||
)
|
||
|
||
print(
|
||
"Частота дискретизации:",
|
||
SAMPLE_RATE,
|
||
"сэмплов/с",
|
||
)
|
||
|
||
print("\nRRC-фильтр:")
|
||
|
||
print(
|
||
"Rolloff:",
|
||
RRC_ROLLOFF,
|
||
)
|
||
|
||
print(
|
||
"Количество коэффициентов:",
|
||
len(rrc_taps),
|
||
)
|
||
|
||
print("\nIQ-сигнал:")
|
||
|
||
print(
|
||
"Количество сэмплов:",
|
||
len(tx_iq),
|
||
)
|
||
|
||
print(
|
||
"Длительность:",
|
||
f"{frame_duration_seconds * 1000:.2f}",
|
||
"мс",
|
||
)
|
||
|
||
print(
|
||
"Максимальная амплитуда:",
|
||
f"{np.max(np.abs(tx_iq)):.3f}",
|
||
)
|
||
|
||
print(
|
||
"PAPR:",
|
||
f"{papr_db:.2f}",
|
||
"дБ",
|
||
)
|
||
|
||
print("\nОценочная ширина спектра:")
|
||
|
||
print(
|
||
f"{theoretical_baseband_bandwidth_hz / 1000:.1f}",
|
||
"кГц",
|
||
)
|
||
|
||
print("\nФайл NumPy:")
|
||
|
||
print(NPY_PATH)
|
||
|
||
print("\nФайл raw complex float32:")
|
||
|
||
print(CF32_PATH)
|
||
|
||
print("\nГрафик:")
|
||
|
||
print(GRAPH_PATH)
|
||
|
||
print("\nОтчёт:")
|
||
|
||
print(INFO_PATH)
|
||
|
||
|
||
# ============================================================
|
||
# Автоматические проверки
|
||
# ============================================================
|
||
|
||
assert len(preamble_bits) == PREAMBLE_BIT_COUNT
|
||
|
||
assert radio_header[:2] == struct.pack(
|
||
">H",
|
||
RADIO_SYNC_WORD,
|
||
)
|
||
|
||
assert len(radio_header) == 4
|
||
|
||
assert len(radio_frame_symbols) == len(
|
||
radio_frame_bits
|
||
)
|
||
|
||
assert np.iscomplexobj(
|
||
tx_iq
|
||
)
|
||
|
||
assert tx_iq.dtype == np.complex64
|
||
|
||
assert np.max(
|
||
np.abs(tx_iq)
|
||
) <= TX_AMPLITUDE + 1e-6
|
||
|
||
assert NPY_PATH.exists()
|
||
assert CF32_PATH.exists()
|
||
assert GRAPH_PATH.exists()
|
||
assert INFO_PATH.exists()
|
||
|
||
print(
|
||
"\nПроверка пройдена: "
|
||
"реальный BPSK-радиокадр и IQ-сэмплы сформированы."
|
||
) |