GNU USRP动态调制发射
grc脚本

Embedded Python block代码:
import numpy as np
from gnuradio import gr
import time
# --- 各调制函数 ---
def mod_BPSK(bits, freq, t):
return np.exp(1j*(2*np.pi*freq*t + np.pi*bits))
def mod_QPSK(bits, freq, t):
# 保证比特数为偶数
n_sym = len(bits) // 2
bits = bits[:n_sym*2]
idx = bits[0::2]*2 + bits[1::2]
phases = np.pi/4 + np.pi/2*idx
# 生成每个符号的复数值
sym = np.exp(1j*phases)
# 重复符号以匹配采样点数量
out = np.repeat(sym, int(np.ceil(len(t)/len(sym))))[:len(t)]
return out * np.exp(1j*2*np.pi*freq*t)
def mod_GFSK(freq, t, bt=0.35):
N = len(t)
data = np.random.choice([-1,1], N)
samples_per_bit = max(2, int(len(t)/N))
gauss = np.exp(-0.5*(np.linspace(-2,2,samples_per_bit*2)**2)/bt**2)
phase_dev = np.convolve(data, gauss, mode="same")
phase = np.cumsum(phase_dev)*(np.pi/samples_per_bit)
return np.exp(1j*(2*np.pi*freq*t + phase))
def mod_16QAM(bits, freq, t):
# 4 bit 一个符号
n_sym = len(bits) // 4
bits = bits[:n_sym*4]
b = bits.reshape(-1,4)
mI = (2*b[:,0]+b[:,1])*2-3
mQ = (2*b[:,2]+b[:,3])*2-3
sym = (mI + 1j*mQ)/np.sqrt(10)
out = np.repeat(sym, int(np.ceil(len(t)/len(sym))))[:len(t)]
return out * np.exp(1j*2*np.pi*freq*t)
# -------- 主 Block --------
class blk(gr.sync_block):
def __init__(self):
gr.sync_block.__init__(self,
name="Simple Scheduler",
in_sig=None,
out_sig=[np.complex64]
)
self.samp_rate = 1e6
self.freq_carrier = 1e6
self.last_switch = time.time()
self.state = "GFSK" # 当前模式
def work(self, input_items, output_items):
out = output_items[0]
N = len(out)
t = np.arange(N)/self.samp_rate
# --- 限制符号速率,插值平滑 ---
symbol_rate = 1e3
samples_per_symbol = max(1, int(self.samp_rate / symbol_rate))
num_symbols = int(np.ceil(N / samples_per_symbol))
bits = np.random.randint(0, 2, num_symbols)
bits = np.repeat(bits, samples_per_symbol)[:N]
elapsed = time.time() - self.last_switch
# -------- 显式调度逻辑 --------
if elapsed < 3:
self.state = "GFSK"
elif elapsed < 6:
self.state = "BPSK"
elif elapsed < 8:
self.state = "QPSK"
elif elapsed < 9:
self.state = "16QAM"
else:
self.last_switch = time.time() # 重新开始
self.state = "GFSK"
# -------- 调制调用 --------
if self.state == "GFSK":
out[:] = mod_GFSK(self.freq_carrier, t)
elif self.state == "QPSK":
out[:] = mod_QPSK(bits, self.freq_carrier, t)
elif self.state == "16QAM":
out[:] = mod_16QAM(bits, self.freq_carrier, t)
else:
out[:] = mod_BPSK(bits, self.freq_carrier, t)
return len(out)
大致效果:
更多调制方式的版本
import numpy as np
from gnuradio import gr
import time
# --- 各调制函数 ---
def mod_BPSK(bits, freq, t):
return np.exp(1j*(2*np.pi*freq*t + np.pi*bits))
def mod_QPSK(bits, freq, t):
# 保证比特数为偶数
n_sym = len(bits) // 2
bits = bits[:n_sym*2]
idx = bits[0::2]*2 + bits[1::2]
phases = np.pi/4 + np.pi/2*idx
# 生成每个符号的复数值
sym = np.exp(1j*phases)
# 重复符号以匹配采样点数量
out = np.repeat(sym, int(np.ceil(len(t)/len(sym))))[:len(t)]
return out * np.exp(1j*2*np.pi*freq*t)
def mod_GFSK(freq, t, bt=0.35):
N = len(t)
data = np.random.choice([-1,1], N)
samples_per_bit = max(2, int(len(t)/N))
gauss = np.exp(-0.5*(np.linspace(-2,2,samples_per_bit*2)**2)/bt**2)
phase_dev = np.convolve(data, gauss, mode="same")
phase = np.cumsum(phase_dev)*(np.pi/samples_per_bit)
return np.exp(1j*(2*np.pi*freq*t + phase))
def mod_16QAM(bits, freq, t):
# 4 bit 一个符号
n_sym = len(bits) // 4
bits = bits[:n_sym*4]
b = bits.reshape(-1,4)
mI = (2*b[:,0]+b[:,1])*2-3
mQ = (2*b[:,2]+b[:,3])*2-3
sym = (mI + 1j*mQ)/np.sqrt(10)
out = np.repeat(sym, int(np.ceil(len(t)/len(sym))))[:len(t)]
return out * np.exp(1j*2*np.pi*freq*t)
def mod_2FSK(bits, freq, t, df=5e3):
"""二进制频移键控(2-FSK)"""
# bits=0 → freq - df, bits=1 → freq + df
freq_inst = freq + (2*bits-1)*df
phase = np.cumsum(2*np.pi*freq_inst/len(freq_inst))
return np.exp(1j*phase)
def mod_OFDM(bits, freq, t, n_subcarriers=8, sub_spacing=1e3):
"""简化版 OFDM:多载波正交叠加"""
# 生成多个子载波信号叠加
carriers = []
for k in range(n_subcarriers):
sub_freq = freq + (k - n_subcarriers/2) * sub_spacing
phase = 2*np.pi*sub_freq*t + np.pi*bits[k % len(bits)]
carriers.append(np.exp(1j*phase))
sig = np.sum(carriers, axis=0) / n_subcarriers
return sig
# -------- 主 Block --------
class blk(gr.sync_block):
def __init__(self):
gr.sync_block.__init__(self,
name="Simple Scheduler",
in_sig=None,
out_sig=[np.complex64]
)
self.samp_rate = 1e6
self.freq_carrier = 1e6
self.last_switch = time.time()
self.state = "GFSK" # 当前模式
def work(self, input_items, output_items):
out = output_items[0]
N = len(out)
t = np.arange(N)/self.samp_rate
# --- 限制符号速率,插值平滑 ---
symbol_rate = 1e3
samples_per_symbol = max(1, int(self.samp_rate / symbol_rate))
num_symbols = int(np.ceil(N / samples_per_symbol))
bits = np.random.randint(0, 2, num_symbols)
bits = np.repeat(bits, samples_per_symbol)[:N]
elapsed = time.time() - self.last_switch
# -------- 显式调度逻辑 --------
if elapsed < 0.2:
self.state = "GFSK"
elif elapsed < 0.5:
self.state = "BPSK"
elif elapsed < 0.5:
self.state = "QPSK"
elif elapsed < 0.8:
self.state = "16QAM"
elif elapsed < 1.1:
self.state = "2FSK"
elif elapsed < 1.4:
self.state = "OFDM"
elif elapsed < 1.7:
self.state = "IDLE"
else:
self.last_switch = time.time() # 重新开始
self.state = "GFSK"
# -------- 调制调用 --------
if self.state == "GFSK":
out[:] = mod_GFSK(self.freq_carrier, t,bt=0.1)
elif self.state == "QPSK":
out[:] = mod_QPSK(bits, self.freq_carrier+5e4, t)
elif self.state == "16QAM":
out[:] = mod_16QAM(bits, self.freq_carrier, t)
elif self.state == "BPSK":
out[:] = mod_BPSK(bits, self.freq_carrier, t)
elif self.state == "2FSK":
out[:] = mod_2FSK(bits, self.freq_carrier, t, df=2e3)
elif self.state == "OFDM":
out[:] = mod_OFDM(bits, self.freq_carrier, t, n_subcarriers=8, sub_spacing=2e3)
elif self.state == "IDLE":
out[:] = 0
else:
out[:] = 0
return len(out)
复杂调度方式的实现:
基础跳频通信(单信号)
-
调制:GFSK(bt=0.2~0.35)或 2FSK(Δf 固定)。
-
采样率:
samp_rate = 1e6 -
符号率:
sym_rate = 100e3(samples/symbol=10) -
跳频节拍:
hop_period = 5 ms(可做 1/2/5/10ms 四档) -
频点栅格:
grid = 200 kHz;中心f0,范围±2 MHz→ 21 个栅格。 -
频偏选择:每个 hop 从 {f0 + k*grid} 随机选(避免相邻重复)
-
时长:每段 2–5 s
-
功率:常数或轻微抖动±2 dB
-
标注(每 hop 一条):
{mode:'GFSK', hop_id, f_offset, bt/Δf, sym_rate, SNR, t_start, t_end} -
实现要点:在
GFSK/2FSK分支里每到 hop_period 随机更新self.freq_carrier(±2 MHz 栅格)。
代码:
# epy_block.py —— 随机跳频的窄带 FH-GFSK
FS = 10e6 # 采样率
FRAME_MS = 3.0 # 10 ms 调度
DUTY = 0.8 # 80% 发射 + 20% 静默
FH_GRID_HZ = 2e6 # 跳频栅格
FH_SPAN_HZ = 6e6 # ±6 MHz 范围(去掉 DC)
GFSK_SYM = 250e3 # Rs,窄带
H = 0.35 # 调制指数
BT = 0.3 # 高斯 BT
FADE_MAX_MS = 0.5 # 渐入/出上限
W_FH = 1.0
RNG_SEED = None # 设成整数则复现同一随机序列
import numpy as np
from gnuradio import gr
def raised_cosine_envelope(tau, on_s, frame_s, fade_s):
e = np.zeros_like(tau, dtype=np.float64)
on = tau < on_s
if not np.any(on): return e
x = tau[on]
y = np.ones_like(x)
m = x < fade_s
if np.any(m): y[m] = 0.5*(1 - np.cos(np.pi*x[m]/fade_s))
m2 = x > (on_s - fade_s)
if np.any(m2): y[m2] = 0.5*(1 - np.cos(np.pi*(on_s - x[m2])/fade_s))
e[on] = y
return e
def gaussian_taps(sps, BT, span_syms=6):
t = np.arange(-span_syms*sps, span_syms*sps + 1, dtype=np.float64)/float(sps)
taps = np.exp(-2.0*(np.pi*BT*t)**2)
taps /= np.sum(taps)
return taps.astype(np.float64)
def gfsk_baseband(N, Fs, Rs, h, BT, phase0=0.0, span_syms=6):
if N == 0: return np.zeros(0, np.complex64), float(phase0)
sps = max(2, int(round(Fs/float(Rs))))
taps = gaussian_taps(sps, BT, span_syms=span_syms)
pad = span_syms + 2
n_sym = int(np.ceil((N + pad*sps*2)/sps)) + 1
bits = np.random.randint(0, 2, n_sym)*2 - 1 # ±1
up = np.zeros(n_sym*sps, dtype=np.float64); up[::sps] = bits
m = np.convolve(up, taps, mode="same")
start = pad*sps
m = m[start:start+N]
df = h*Rs/2.0
w = 2*np.pi*df * m / Fs
phase = phase0 + np.cumsum(w)
bb = np.exp(1j*phase).astype(np.complex64)
return bb, float(phase[-1])
class blk(gr.sync_block):
"""
随机 FH-GFSK(无宽带背景):
- 每帧随机选一条栅格信道
- 禁止与上帧相同,尽量避开相邻信道
- 10 ms 帧,80% 占空,窄带 GFSK,边界清晰
"""
def __init__(self):
gr.sync_block.__init__(self, name="Random FH-GFSK", in_sig=None, out_sig=[np.complex64])
self.Fs = float(FS)
self.frame_s = float(FRAME_MS)*1e-3
self.on_s = float(DUTY)*self.frame_s
self.fade_s = min(0.01*self.frame_s, float(FADE_MAX_MS)*1e-3)
grid = np.arange(-FH_SPAN_HZ, FH_SPAN_HZ + 1, FH_GRID_HZ, dtype=np.float64)
grid = grid[np.abs(grid) > 1e-6] # 去 DC
lim = 0.45*self.Fs
grid = np.clip(grid, -lim, lim)
assert grid.size >= 2, "FH 栅格不足"
self.fh_grid = grid
self.n_chan = len(grid)
# 随机源与逐帧缓存
self.rng = np.random.default_rng(RNG_SEED)
self._frame2idx = {} # frame -> channel index
self._last_idx = None
self._last_assigned = -1
# GFSK
self.Rs = float(GFSK_SYM)
self.h = float(H)
self.bt = float(BT)
self._phi = 0.0
self.w_fh = float(W_FH)
self._n0 = 0
def _pick_next_idx(self, last_idx):
cand = np.arange(self.n_chan)
if last_idx is not None:
# 尽量避开相邻;若通道过少则只避相同
ban = {last_idx}
if self.n_chan >= 4:
if last_idx - 1 >= 0: ban.add(last_idx - 1)
if last_idx + 1 < self.n_chan: ban.add(last_idx + 1)
cand = np.array([i for i in cand if i not in ban], dtype=int)
if cand.size == 0:
cand = np.array([i for i in range(self.n_chan) if i != last_idx], dtype=int)
return int(self.rng.choice(cand))
def _idx_for_frame(self, k):
if k in self._frame2idx:
return self._frame2idx[k]
# 逐帧补齐到 k
while self._last_assigned < k:
next_idx = self._pick_next_idx(self._last_idx)
self._last_assigned += 1
self._frame2idx[self._last_assigned] = next_idx
self._last_idx = next_idx
return self._frame2idx[k]
def work(self, input_items, output_items):
out = output_items[0]; N = len(out)
if N == 0: return 0
dt = 1.0/self.Fs
n = self._n0 + np.arange(N, dtype=np.float64)
t = n*dt
frame_idx = np.floor(t/self.frame_s).astype(np.int64)
tau = t - frame_idx*self.frame_s
env = raised_cosine_envelope(tau, self.on_s, self.frame_s, self.fade_s)
bb, self._phi = gfsk_baseband(N, self.Fs, self.Rs, self.h, self.bt, self._phi, span_syms=6)
k_unique = np.unique(frame_idx)
idx_map = {int(k): self._idx_for_frame(int(k)) for k in k_unique}
f0 = np.array([ self.fh_grid[idx_map[int(k)]] for k in frame_idx ], dtype=np.float64)
sig = bb * np.exp(1j*2*np.pi*f0*t).astype(np.complex64)
sig *= env.astype(np.float64)
peak = np.max(np.abs(sig))
if peak > 0.99: sig = sig/peak*0.95
out[:] = (self.w_fh*sig).astype(np.complex64)
self._n0 += N
return N
效果:

(加噪声至snr=30db后)
双信号共存(静态+跳频)
-
信号A(静态“锚点”):QPSK,
sym_rate = 200e3,f_offset = +500 kHz,持续全程。 -
信号B(跳频“入侵者”):GFSK 或 2FSK,参数同 Level 1。
-
叠加方式:A 与 B 相加(用两个发生器更干净;或一块中在不同时段切换两组参数并加噪)
-
功率关系:
P_B - P_A ∈ { -10, -5, 0, +5 } dB -
时长:每样本 5–10 s
-
标注:为每条流各自产一条 meta,或统一为区间列表:
-
A:
{mode:'QPSK', f_offset:+500k, sym_rate, SNR_A} -
B:
{mode:'GFSK', hop_id, f_offset, hop_period, SNR_B}
-
-
实现要点:若只用现在的一块 block,做时间复用:例如 10 ms 内 8 ms 生成 A,2 ms 生成 B 并与 A 相加(把 B 的输出单独缓存在数组后再相加)。更简单的做法是在 flowgraph 里加第二个 block + Add。
代码
import numpy as np
from gnuradio import gr
import time
# --- 各调制函数 ---
def mod_BPSK(bits, freq, t):
return np.exp(1j * (2 * np.pi * freq * t + np.pi * bits))
def mod_QPSK(bits, freq, t):
# 保证比特数为偶数
n_sym = len(bits) // 2
bits = bits[:n_sym * 2]
idx = bits[0::2] * 2 + bits[1::2]
phases = np.pi / 4 + np.pi / 2 * idx
# 生成每个符号的复数值
sym = np.exp(1j * phases)
# 重复符号以匹配采样点数量
out = np.repeat(sym, int(np.ceil(len(t) / len(sym))))[:len(t)]
return out * np.exp(1j * 2 * np.pi * freq * t)
def mod_GFSK(freq, t, bt=0.35):
N = len(t)
data = np.random.choice([-1, 1], N)
samples_per_bit = max(2, int(len(t) / N))
gauss = np.exp(-0.5 * (np.linspace(-2, 2, samples_per_bit * 2) ** 2) / bt ** 2)
phase_dev = np.convolve(data, gauss, mode="same")
phase = np.cumsum(phase_dev) * (np.pi / samples_per_bit)
return np.exp(1j * (2 * np.pi * freq * t + phase))
def mod_16QAM(bits, freq, t):
# 4 bit 一个符号
n_sym = len(bits) // 4
bits = bits[:n_sym * 4]
b = bits.reshape(-1, 4)
mI = (2 * b[:, 0] + b[:, 1]) * 2 - 3
mQ = (2 * b[:, 2] + b[:, 3]) * 2 - 3
sym = (mI + 1j * mQ) / np.sqrt(10)
out = np.repeat(sym, int(np.ceil(len(t) / len(sym))))[:len(t)]
return out * np.exp(1j * 2 * np.pi * freq * t)
def mod_2FSK(bits, freq, t, df=5e3):
"""二进制频移键控(2-FSK)"""
# bits=0 → freq - df, bits=1 → freq + df
freq_inst = freq + (2 * bits - 1) * df
phase = np.cumsum(2 * np.pi * freq_inst / len(freq_inst))
return np.exp(1j * phase)
def mod_OFDM(bits, freq, t, n_subcarriers=8, sub_spacing=1e3):
"""简化版 OFDM:多载波正交叠加"""
# 生成多个子载波信号叠加
carriers = []
for k in range(n_subcarriers):
sub_freq = freq + (k - n_subcarriers / 2) * sub_spacing
phase = 2 * np.pi * sub_freq * t + np.pi * bits[k % len(bits)]
carriers.append(np.exp(1j * phase))
sig = np.sum(carriers, axis=0) / n_subcarriers
return sig
# -------- 主 Block --------
class blk(gr.sync_block):
def __init__(self):
gr.sync_block.__init__(self,
name="Simple Scheduler",
in_sig=None,
out_sig=[np.complex64]
)
self.samp_rate = 1e6
self.freq_carrier = 1e6
self.last_switch = time.time()
self.state = "GFSK" # 当前模式
def work(self, input_items, output_items):
out = output_items[0]
N = len(out)
t = np.arange(N) / self.samp_rate
# --- 比特序列 ---
symbol_rate = 1e3
samples_per_symbol = max(1, int(self.samp_rate / symbol_rate))
num_symbols = int(np.ceil(N / samples_per_symbol))
bits = np.random.randint(0, 2, num_symbols)
bits = np.repeat(bits, samples_per_symbol)[:N]
elapsed = (time.time() - self.last_switch) * 1000 # 毫秒级
# -------- 跳频信号参数 --------
hop_period = 10.0 # 每 10 ms 跳一次
hop_range = 2e6
hop_step = 2e5
if elapsed >= hop_period:
f_list = np.arange(-hop_range, hop_range + hop_step, hop_step)
self.freq_carrier = float(np.random.choice(f_list))
self.last_switch = time.time()
print(f"[Hop] GFSK carrier = {self.freq_carrier / 1e3:.1f} kHz")
# -------- 跳频 GFSK 信号 --------
sig_hop = mod_GFSK(self.freq_carrier, t, bt=0.1)
# -------- 静态 QPSK 信号 --------
static_freq = 0
sig_static = mod_QPSK(bits, static_freq, t)
# -------- 叠加两路信号 --------
power_ratio = 0.5 # 控制相对功率(0.5 ≈ –6 dB)
out[:] = sig_static + power_ratio * sig_hop
return len(out)
工业帧式调度(周期 100 ms)
-
时间轴(例):
-
0–3 ms:同步/前导 BPSK,
f_offset = 0 -
3–10 ms:信标 GFSK,
f_offset = +50 kHz -
10–60 ms:业务数据 QPSK 或 16QAM,
f_offset = +200 kHz -
60–65 ms:跳频控制 GFSK,
f_offset ∈ ±{100, 300, 500} kHz -
65–75 ms:应答窗口 BPSK(可置静默以模拟无应答)
-
75–100 ms:空口(IDLE,纯零)
-
-
速率建议:
-
BPSK:
sym_rate=50e3 -
GFSK:
sym_rate=100e3, bt=0.15 -
QPSK/16QAM:
sym_rate=250e3
-
-
SNR 级别:{5, 10, 20} dB(在输出末端加 AWGN)
-
标注(每帧一条,内含段落列表):
{ frame_id, segments:[ {type:'SYNC', mode:'BPSK', t:[0,3ms], f:+0}, {type:'BEACON', mode:'GFSK', t:[3,10ms], f:+50k, bt:0.15}, {type:'DATA', mode:'QPSK', t:[10,60ms], f:+200k, sym:250k}, {type:'FH_CTRL', mode:'GFSK', t:[60,65ms], f:+random}, {type:'ACK', mode:'BPSK', t:[65,75ms], present:0/1}, {type:'IDLE', t:[75,100ms]} ], SNR } -
实现要点:
elapsed判断直接照此表写。IDLE 段out[:] = 0。FH 段内随机换freq_carrier1–2 次(子跳)。
代码
import numpy as np
from gnuradio import gr
import time
# --- 各调制函数 ---
def mod_BPSK(bits, freq, t):
return np.exp(1j * (2 * np.pi * freq * t + np.pi * bits))
def mod_QPSK(bits, freq, t):
# 保证比特数为偶数
n_sym = len(bits) // 2
bits = bits[:n_sym * 2]
idx = bits[0::2] * 2 + bits[1::2]
phases = np.pi / 4 + np.pi / 2 * idx
# 生成每个符号的复数值
sym = np.exp(1j * phases)
# 重复符号以匹配采样点数量
out = np.repeat(sym, int(np.ceil(len(t) / len(sym))))[:len(t)]
return out * np.exp(1j * 2 * np.pi * freq * t)
def mod_GFSK(freq, t, bt=0.35):
N = len(t)
data = np.random.choice([-1, 1], N)
samples_per_bit = max(2, int(len(t) / N))
gauss = np.exp(-0.5 * (np.linspace(-2, 2, samples_per_bit * 2) ** 2) / bt ** 2)
phase_dev = np.convolve(data, gauss, mode="same")
phase = np.cumsum(phase_dev) * (np.pi / samples_per_bit)
return np.exp(1j * (2 * np.pi * freq * t + phase))
def mod_16QAM(bits, freq, t):
# 4 bit 一个符号
n_sym = len(bits) // 4
bits = bits[:n_sym * 4]
b = bits.reshape(-1, 4)
mI = (2 * b[:, 0] + b[:, 1]) * 2 - 3
mQ = (2 * b[:, 2] + b[:, 3]) * 2 - 3
sym = (mI + 1j * mQ) / np.sqrt(10)
out = np.repeat(sym, int(np.ceil(len(t) / len(sym))))[:len(t)]
return out * np.exp(1j * 2 * np.pi * freq * t)
def mod_2FSK(bits, freq, t, df=5e3):
"""二进制频移键控(2-FSK)"""
# bits=0 → freq - df, bits=1 → freq + df
freq_inst = freq + (2 * bits - 1) * df
phase = np.cumsum(2 * np.pi * freq_inst / len(freq_inst))
return np.exp(1j * phase)
def mod_OFDM(bits, freq, t, n_subcarriers=8, sub_spacing=1e3):
"""简化版 OFDM:多载波正交叠加"""
# 生成多个子载波信号叠加
carriers = []
for k in range(n_subcarriers):
sub_freq = freq + (k - n_subcarriers / 2) * sub_spacing
phase = 2 * np.pi * sub_freq * t + np.pi * bits[k % len(bits)]
carriers.append(np.exp(1j * phase))
sig = np.sum(carriers, axis=0) / n_subcarriers
return sig
# -------- 主 Block --------
class blk(gr.sync_block):
def __init__(self):
gr.sync_block.__init__(self,
name="Simple Scheduler",
in_sig=None,
out_sig=[np.complex64]
)
self.samp_rate = 1e6
self.freq_carrier = 1e6
self.last_switch = time.time()
self.state = "GFSK" # 当前模式
def work(self, input_items, output_items):
out = output_items[0]
N = len(out)
t = np.arange(N) / self.samp_rate
# --- 生成比特序列 ---
symbol_rate = 1e3
samples_per_symbol = max(1, int(self.samp_rate / symbol_rate))
num_symbols = int(np.ceil(N / samples_per_symbol))
bits = np.random.randint(0, 2, num_symbols)
bits = np.repeat(bits, samples_per_symbol)[:N]
# --- 周期计时(毫秒)---
elapsed = (time.time() - self.last_switch) * 1000
frame_period = 100.0 # 100 ms 周期
# -------- 段落判断 --------
if elapsed < 3: # 同步/前导
out[:] = mod_BPSK(bits, 0, t)
elif elapsed < 10: # 信标
out[:] = mod_GFSK(+5e4, t, bt=0.15)
elif elapsed < 60: # 业务数据
# 可切换 QPSK / 16QAM
if np.random.rand() < 0.5:
out[:] = mod_QPSK(bits, +2e5, t)
else:
out[:] = mod_16QAM(bits, +2e5, t)
elif elapsed < 65: # 跳频控制
f = np.random.choice([+1e5, -1e5, +3e5, -3e5, +5e5, -5e5])
out[:] = mod_GFSK(f, t, bt=0.2)
elif elapsed < 75: # 应答窗口(可置静默)
if np.random.rand() < 0.7: # 70 % 有应答
out[:] = mod_BPSK(bits, 0, t)
else:
out[:] = 0
elif elapsed < 100: # 空口
out[:] = 0
else: # 周期重启
self.last_switch = time.time()
out[:] = 0
return len(out)
多业务混合
-
三路并行概念(推荐用三块发生器+Add;若坚持单块,做时分叠加):
-
FH 短突发:2FSK,
hop_period=2 ms,占空 50%,f_offset在 ±1 MHz 栅格随机。 -
宽带数据:16QAM,
sym_rate=400e3,固定f_offset=+300 kHz,间歇 20 ms ON / 10 ms OFF。 -
周期信标:GFSK,
sym_rate=100e3,f_offset=-150 kHz,每 50 ms 发 5 ms。
-
-
功率:三路功率比 {0 dB, -6 dB, -12 dB} 的排列组合。
-
SNR:全局加噪,或对单路先缩放再相加。
-
标注:按路记录,再加总体混合表。
代码:
import numpy as np
from gnuradio import gr
import time
# ======== 工具:RRC 滤波器 ========
def rrc_taps(beta=0.35, sps=10, span=6):
N = span * sps
t = np.arange(-N / 2, N / 2 + 1) / sps
taps = np.zeros_like(t)
for i, x in enumerate(t):
if np.isclose(x, 0.0):
taps[i] = 1.0 - beta + 4 * beta / np.pi
elif np.isclose(abs(x), 1 / (4 * beta)):
taps[i] = (beta / np.sqrt(2)) * (
(1 + 2 / np.pi) * np.sin(np.pi / (4 * beta)) +
(1 - 2 / np.pi) * np.cos(np.pi / (4 * beta))
)
else:
num = np.sin(np.pi * x * (1 - beta)) + 4 * beta * x * np.cos(np.pi * x * (1 + beta))
den = np.pi * x * (1 - (4 * beta * x) ** 2)
taps[i] = num / den
taps /= np.sqrt(np.sum(taps ** 2))
return taps
# ======== 各调制函数 ========
def mod_2FSK(bits, freq, t, df=5e3):
N = len(t)
if N == 0:
return np.zeros(0, np.complex64)
dt = (t[1] - t[0]) if N > 1 else 0.0
freq_inst = freq + (2 * bits.astype(np.float64) - 1) * df
phase = 2 * np.pi * np.cumsum(freq_inst) * dt
return np.exp(1j * phase).astype(np.complex64)
def mod_GFSK(freq, t, samp_rate, bt=0.35, sym_rate=1e5):
N = len(t)
if N == 0:
return np.zeros(0, np.complex64)
# 用实际采样率计算每符号采样点
sps = max(2, int(round(samp_rate / float(sym_rate))))
# 生成符号并上采样到 >= N
n_sym = max(1, int(np.ceil(N / sps)))
data = np.random.choice([-1, 1], n_sym)
up = np.repeat(data, sps)
if len(up) < N:
up = np.pad(up, (0, N - len(up)), mode="edge")
up = up[:N] # 保证长度 = N
# 高斯脉冲成形
L = 5
gauss = np.exp(-0.5 * (np.linspace(-2, 2, L) ** 2) / (bt ** 2))
phase_dev = np.convolve(up, gauss, mode="same")
# 双保险:长度对齐
if len(phase_dev) > N:
phase_dev = phase_dev[:N]
elif len(phase_dev) < N:
phase_dev = np.pad(phase_dev, (0, N - len(phase_dev)), mode="edge")
phase = np.cumsum(phase_dev) * (np.pi / sps)
sig = np.exp(1j * (2 * np.pi * freq * t + phase))
return sig.astype(np.complex64)
def mod_16QAM_rrc(freq, t, samp_rate, sym_rate=4e5, beta=0.4):
"""16QAM + RRC 成形,去掉周期伪谱"""
N = len(t)
if N == 0:
return np.zeros(0, np.complex64)
sps = max(2, int(round(samp_rate / sym_rate)))
n_sym = int(np.ceil(N / sps)) + 8
bits = np.random.randint(0, 2, size=(n_sym, 4))
mI = (2 * bits[:, 0] + bits[:, 1]) * 2 - 3
mQ = (2 * bits[:, 2] + bits[:, 3]) * 2 - 3
sym = (mI + 1j * mQ) / np.sqrt(10)
sym *= np.exp(1j * 2 * np.pi * np.random.rand(n_sym)) # 打散周期
up = np.zeros(n_sym * sps, dtype=np.complex128)
up[::sps] = sym
taps = rrc_taps(beta=beta, sps=sps, span=6)
shaped = np.convolve(up, taps, mode="full")
if len(shaped) < N:
shaped = np.pad(shaped, (0, N - len(shaped)))
baseband = shaped[:N]
# 微弱相位噪声防固定纹理
phi_noise = np.cumsum(np.random.randn(N) * 1e-3)
baseband *= np.exp(1j * phi_noise)
return (baseband * np.exp(1j * 2 * np.pi * freq * t)).astype(np.complex64)
class blk(gr.sync_block):
"""
FH 2FSK 远跳 + 相位连续 + 慢速AGC(提清晰度)+ 仅溢出保护
"""
def __init__(self, samp_rate=10e6):
gr.sync_block.__init__(
self,
name="Scheduler (farther FH + phase continuity + slow AGC)",
in_sig=None,
out_sig=[np.complex64],
)
self.samp_rate = float(samp_rate)
self.t0 = time.time()
# --- FH 2FSK ---
self.fh_hop_ms = 2.0
self.fh_duty = 0.5
self.fh_grid = np.arange(-1_000_000, 1_000_000 + 1, 200_000, dtype=np.int64)
self.fh_guard_hz = 250e3
self.fh_far_bias = 5.0
self.fh_min_step_hz = 800e3
self.fh_curr = np.inf
self._fh_last_win = -1
self._phi_fh = 0.0
self._df_fh = 5e3
# --- 宽带 16QAM ---
self.wide_sym_rate = 4e5
self.wide_offset = +300e3
self.wide_beta = 0.5
self.wide_period_ms = 30.0
self.wide_on_ms = 20.0
# --- 周期信标 GFSK ---
self.bcn_sym_rate = 1e5
self.bcn_offset = -150e3
self.bcn_period_ms = 50.0
self.bcn_on_ms = 5.0
# 幅度权重与总增益
self.w_fh, self.w_wide, self.w_bcn = 0.25, 2.0, 0.2
self.master_gain = 1.0 # 可按需微调整体电平
# --- 慢速 AGC(跨块RMS恒定) ---
self.target_rms = 0.35 # 目标均方根幅度,给足余量避免削波
self.agc_tau = 0.25 # 时间常数,秒;越大越慢
self._ema_power = (self.target_rms ** 2) # 初始化到目标功率
self._last_gain = 1.0
# ---------- 工具 ----------
def _safe_off(self, f_off):
limit = 0.45 * self.samp_rate
return float(np.clip(f_off, -limit, limit))
def _apply_fade(self, sig, fade_ratio=0.06):
N = len(sig)
if N < 8:
return sig
edge = max(4, int(fade_ratio * N))
edge = min(edge, N // 2)
if edge <= 0:
return sig
win = np.ones(N, dtype=np.float64)
taper = 0.5 * (1 - np.cos(np.linspace(0, np.pi, edge)))
win[:edge] = taper
win[-edge:] = taper[::-1]
return (sig * win).astype(sig.dtype)
def _fh_pick_away_from(self, avoid_center, avoid_bw):
lo = avoid_center - 0.5 * avoid_bw - self.fh_guard_hz
hi = avoid_center + 0.5 * avoid_bw + self.fh_guard_hz
base_allowed = self.fh_grid[(self.fh_grid < lo) | (self.fh_grid > hi)]
if base_allowed.size == 0:
base_allowed = self.fh_grid
allowed = base_allowed
if np.isfinite(self.fh_curr):
m = np.abs(allowed - self.fh_curr) >= self.fh_min_step_hz
if np.any(m):
allowed = allowed[m]
else:
allowed = base_allowed
if allowed.size == 1:
return float(allowed[0])
d_avoid = np.where(allowed < lo, lo - allowed, allowed - hi).astype(np.float64)
d_prev = (np.abs(allowed - (self.fh_curr if np.isfinite(self.fh_curr) else 0.0))
.astype(np.float64))
d_prev = np.maximum(d_prev, 1.0)
w = (d_avoid ** max(self.fh_far_bias, 1.0)) * d_prev
p = w / np.sum(w)
return float(np.random.choice(allowed, p=p))
def _slow_agc(self, sig):
"""
跨块RMS保持:对平均功率做指数滑动估计,再把当前块缩放到 target_rms。
设软限幅只在临近满刻度时启动。
"""
# 指数平均的alpha由块长度和时间常数决定
N = sig.size
if N == 0:
return sig
alpha = 1.0 - np.exp(-N / (self.samp_rate * max(self.agc_tau, 1e-3)))
p_hat = np.mean(np.real(sig)**2 + np.imag(sig)**2)
self._ema_power = (1 - alpha) * self._ema_power + alpha * p_hat
# 目标增益
eps = 1e-9
gain = self.target_rms / np.sqrt(max(self._ema_power, eps))
# 抑制突变:对增益做小范围限速
max_step = 1.25
min_step = 0.8
gain = np.clip(gain, self._last_gain * min_step, self._last_gain * max_step)
self._last_gain = gain
y = sig * (gain * self.master_gain)
# 溢出保护:仅当幅度>0.98时做轻度压缩
mag = np.abs(y)
over = mag > 0.98
if np.any(over):
k = 0.5 # 压缩强度
y[over] = y[over] / (1 + k * (mag[over] - 0.98))
return y
# ---------- 主工作 ----------
def work(self, input_items, output_items):
out = output_items[0]
N = len(out)
if N == 0:
return 0
t = np.arange(N, dtype=np.float64) / self.samp_rate
dt = 1.0 / self.samp_rate
now = time.time()
elap_ms = (now - self.t0) * 1000.0
# === FH 2FSK 调度 ===
fh_win = int(elap_ms // self.fh_hop_ms)
if fh_win != self._fh_last_win:
self._fh_last_win = fh_win
wide_bw = (1.0 + self.wide_beta) * self.wide_sym_rate
self.fh_curr = self._safe_off(self._fh_pick_away_from(self.wide_offset, wide_bw))
fh_phase_ms = elap_ms % self.fh_hop_ms
fh_on = fh_phase_ms < (self.fh_hop_ms * self.fh_duty)
sr_fh = 1e3
sps_fh = max(1, int(self.samp_rate // sr_fh))
n_fh = int(np.ceil(N / sps_fh))
bits_fh = np.repeat(np.random.randint(0, 2, n_fh), sps_fh)[:N]
if fh_on:
# 基带相位跨块连续:先生成相对相位,再乘累计相位
sig_fh_rel = mod_2FSK(bits_fh, self.fh_curr, t, df=self._df_fh)
sig_fh = (sig_fh_rel * np.exp(1j * self._phi_fh)).astype(np.complex64)
# 更新累计相位(本块内积分)
freq_inst = self.fh_curr + (2 * bits_fh.astype(np.float64) - 1) * self._df_fh
self._phi_fh = (self._phi_fh + 2 * np.pi * dt * np.sum(freq_inst)) % (2 * np.pi)
# 渐入渐出与 hop 匹配
hop_samples = max(1, int(self.fh_hop_ms * 1e-3 * self.samp_rate))
fade_ratio_fh = float(np.clip(0.12 * hop_samples / max(N, 1), 0.04, 0.18))
sig_fh = self._apply_fade(sig_fh, fade_ratio=fade_ratio_fh)
else:
sig_fh = np.zeros(N, np.complex64)
# === 宽带 16QAM ===
wide_phase_ms = elap_ms % self.wide_period_ms
wide_on = (wide_phase_ms < self.wide_on_ms)
wide_off = self._safe_off(self.wide_offset)
if wide_on:
sig_wide = mod_16QAM_rrc(
wide_off, t, self.samp_rate,
sym_rate=self.wide_sym_rate, beta=self.wide_beta
)
sig_wide = self._apply_fade(sig_wide, fade_ratio=0.05)
else:
sig_wide = np.zeros(N, np.complex64)
# === 周期信标 GFSK ===
bcn_phase_ms = elap_ms % self.bcn_period_ms
bcn_on = (bcn_phase_ms < self.bcn_on_ms)
bcn_off = self._safe_off(self.bcn_offset)
if bcn_on:
sig_bcn = mod_GFSK(bcn_off, t, self.samp_rate, bt=0.2, sym_rate=self.bcn_sym_rate)
sig_bcn = self._apply_fade(sig_bcn, fade_ratio=0.05)
else:
sig_bcn = np.zeros(N, np.complex64)
# 相位随机化仅对非 FH 分量
rot = np.exp(1j * 2 * np.pi * np.random.rand(2))
sig_wide *= rot[0]
sig_bcn *= rot[1]
# 叠加
sig = self.w_fh * sig_fh + self.w_wide * sig_wide + self.w_bcn * sig_bcn
# 慢速AGC提升清晰度,软限幅只做溢出保护
sig = self._slow_agc(sig)
out[:] = sig.astype(np.complex64)
return N
效果:
BLE广播
# =============================================================================
# 名称
# BLE Advertising(主广播 37→38→39 轮发)— GFSK 合成(低占空、干净边沿)
#
# 生成的是什么信号?
# - 只模拟 BLE 主广播事件:按 37→38→39 的次序在三个主广播信道各发一帧短突发;
# - 帧间插固定静默 IFG(默认 10 ms);事件之间按 ADV_INTERVAL_MS 加 0..ADV_JITTER_MS 抖动;
# - 物理近似:GFSK(BT≈0.5,调制指数 h≈0.5);符号率默认 1 Msym/s(可改 2 Msym/s);
# - 频率映射:三信道中心为 2402/2426/2480 MHz。为兼容低采样率,带宽按比例压缩至 ±0.45·Fs 内,
# 只保持三者的相对间隔(24 MHz 与 78 MHz 的比例)。需要“真跨度”,提高 SAMP_RATE。
#
# 行为与时序
# - 一个“广播事件”包含 3 帧:ch37 → ch38 → ch39;
# - 每帧持续 ADV_PDU_MS_RANGE 内的随机时长;帧间固定 IFG_MS 静默;
# - 下一事件开始时间 = 上一事件开始时间 + ADV_INTERVAL_MS + U[0, ADV_JITTER_MS];
# - 广播占空很低;FFT/瀑布上表现为三条稀疏、短促的 2 MHz 级窄带突发。
#
# 旁瓣与“横纹”控制
# - 所有突发用余弦渐入/出(EDGE_MS);无按块 RMS 归一化,只用固定线性增益 OUT_LEVEL;
# - 频点在静默中切换,避免频率阶跃引起的宽带横纹;跨块相位连续。
#
# 使用
# 1) 作为 epy_block 放入 GNU Radio 流图。仿真接 Throttle,外发接 USRP Sink。采样率用 SAMP_RATE;
# 2) 若要把三信道搬到某 RF 中心,设 F_SHIFT_HZ(例:USRP=2.440 GHz,要把 ch37≈2.402 GHz
# 落在此处,可设 F_SHIFT_HZ ≈ 2.402e9-2.440e9;其余两信道会按比例相对分布);
# 3) 想要更低占空:增大 ADV_INTERVAL_MS 或缩短 ADV_PDU_MS_RANGE 上限;想 2 Msym/s,将 BLE_SYM_RATE 设 2e6。
#
# 限制
# - 非真实 BLE 协议栈:不生成接入地址、包头、CRC、跳频图或白化;仅复现“GFSK + 三信道 + 时序”的外观。
# =============================================================================
import numpy as np
from gnuradio import gr
# ====================== 顶部参数(无需 GUI 配置) ======================
SAMP_RATE = 1e6 # 采样率;低 Fs 下三信道总跨度会按比例压缩
BLE_SYM_RATE = 1.0e6 # 1e6 或 2e6(符号率);实际会被 sps≥SPS_MIN 约束
BT_GAUSS = 0.50 # 高斯滤波 BT(BLE 规范约 0.5)
H_INDEX = 0.50 # 调制指数 h(≈0.5 → 峰偏 df=0.25*Rs)
SPS_MIN = 8 # 每符号最小采样点,保证成形质量
GAUSS_SPAN_SYM = 4 # 高斯脉冲跨越的符号数(时域滤波长度)
# —— 广播事件时序(单位 ms) ——
ADV_INTERVAL_MS = 30.0 # 广播事件基础间隔(20 ms..10240 ms 合理)
ADV_JITTER_MS = 1 # 事件间隔叠加的 [0, JITTER] 抖动
ADV_PDU_MS_RANGE = (1.2, 1.6) # 单帧持续(BLE 广播包近似)
IFG_MS = 3.0 # 帧间静默(ch37→ch38→ch39 之间)
EDGE_MS = 0.20 # 每帧渐入/出时长(每侧)
# —— 三个主广播信道(真实差值:24 MHz、78 MHz) → 按 Fs 压缩 ——
AVOID_DC = True # 避开 0 Hz:整体加半个最小间隔的偏置
F_SHIFT_HZ = 0.0 # 全局统一移频(限制在 ±0.45·Fs)
OUT_LEVEL = 0.5 # 固定线性增益(不做按块 RMS)
SEED = 37 # 随机种子(保证复现)
# ====================== 工具:高斯 GFSK 成形(流式) ======================
def _gauss_taps(bt=0.5, sps=8, span=4):
"""单位面积的离散高斯脉冲(σ 按 BT 推导,单位=符号时间)"""
sigma_ts = np.sqrt(np.log(2.0)) / (2*np.pi*bt)
L = span * sps
t = (np.arange(-L/2, L/2 + 1, dtype=np.float64)) / sps
g = np.exp(-(t**2) / (2*sigma_ts**2))
g /= np.sum(g)
return g
class GFSKStreamer:
"""
连续 GFSK:NRZ±1 → 高斯滤波 → 频偏积分为相位。保持跨块滤波/相位/符号状态。
"""
def __init__(self, fs, Rs_target, bt, h, sps_min=SPS_MIN, span_sym=GAUSS_SPAN_SYM, seed=0):
self.fs = float(fs)
Rs = min(Rs_target, self.fs / float(sps_min))
self.sps = max(sps_min, int(round(self.fs / Rs)))
self.Rs = self.fs / self.sps
self.bt = float(bt)
self.h = float(h)
self.span = int(span_sym)
self.Ts = 1.0 / self.fs
self.df = 0.5 * self.h * self.Rs # 峰频偏
self.taps = _gauss_taps(self.bt, self.sps, self.span)
self.L = len(self.taps)
self.prev_up = np.zeros(self.L-1, dtype=np.float64)
self.phase = 0.0
self.rng = np.random.default_rng(seed)
self.cur_sym = 1.0 if self.rng.random() < 0.5 else -1.0
self.rep_left = 0
def _make_up(self, N):
up = np.empty(N, dtype=np.float64)
i = 0
while i < N:
if self.rep_left == 0:
self.cur_sym = 1.0 if self.rng.random() < 0.5 else -1.0
self.rep_left = self.sps
take = min(self.rep_left, N - i)
up[i:i+take] = self.cur_sym
self.rep_left -= take
i += take
return up
def next(self, N):
up = self._make_up(N)
arr = np.concatenate([self.prev_up, up])
filt= np.convolve(arr, self.taps, mode='valid') # 长度恰为 N
self.prev_up = arr[-(self.L-1):]
dphi = 2*np.pi * (self.df * filt) * self.Ts
phi = self.phase + np.cumsum(dphi)
self.phase = float(phi[-1] % (2*np.pi))
return np.exp(1j * phi).astype(np.complex64)
# ====================== 突发包络工具 ======================
def _add_burst(env, start, end, fs, edge_samp, amp=1.0):
"""
在 env 上叠加 [start,end) 的包络,边缘使用余弦渐入/出;amp 为峰值。
env 为 0..1 浮点数组。
"""
if end <= start:
return
N = len(env)
i0 = int(np.floor(start * fs))
i1 = int(np.ceil(end * fs))
if i1 <= 0 or i0 >= N:
return
i0c = max(0, i0); i1c = min(N, i1)
L = i1c - i0c
if L <= 0:
return
e = int(edge_samp)
if 2*e >= L:
# 极短突发:用 Hann
w = 0.5*(1 - np.cos(2*np.pi*np.arange(L)/(L-1 if L>1 else 1)))
else:
w = np.ones(L, dtype=np.float64)
w[:e] = 0.5*(1 - np.cos(np.linspace(0, np.pi, e, endpoint=False)))
w[-e:] = 0.5*(1 - np.cos(np.linspace(np.pi, 0, e, endpoint=False)))
env[i0c:i1c] = np.maximum(env[i0c:i1c], amp*np.clip(w,0,1))
# ====================== epy_block:BLE 广播 ======================
class blk(gr.sync_block):
"""
BLE_Advertising_Approx
输出:complex64 基带;三主广播信道轮发的低占空 GFSK 突发。
"""
def __init__(self):
gr.sync_block.__init__(
self, name="BLE_Advertising_Approx",
in_sig=None, out_sig=[np.complex64],
)
self.fs = float(SAMP_RATE)
self.rng = np.random.default_rng(SEED)
self.t = 0.0
# —— GFSK 调制器 ——(自适应保证 sps≥SPS_MIN)
self.mod = GFSKStreamer(self.fs, BLE_SYM_RATE, BT_GAUSS, H_INDEX, seed=SEED+1)
# —— 三信道等比例压缩到 ±0.45·Fs 内 ——
# 真实中心差:ch37→ch38 = 24 MHz;ch37→ch39 = 78 MHz
span_real = 78e6
scale = min(1.0, 0.90*self.fs / span_real)
off_min = 24e6 * scale
off_max = 78e6 * scale
dc_off = 0.5*off_min if AVOID_DC else 0.0
# 基带内中心(Hz)
self.ch_freqs = np.array([dc_off + 0.0,
dc_off + off_min,
dc_off + off_max], dtype=np.float64)
self.f_shift = float(np.clip(F_SHIFT_HZ, -0.45*self.fs, 0.45*self.fs))
# —— 广播事件调度 ——
self.edge_samp = int(max(1, round(float(EDGE_MS)*1e-3*self.fs)))
self.ifg_s = float(IFG_MS) * 1e-3
self.pdu_min_s = float(ADV_PDU_MS_RANGE[0]) * 1e-3
self.pdu_max_s = float(ADV_PDU_MS_RANGE[1]) * 1e-3
self.T_ev_base = float(ADV_INTERVAL_MS) * 1e-3
self.T_ev_jit = float(ADV_JITTER_MS) * 1e-3
self.next_event = 0.0 # t=0 立即来一组
self.mix_phase = 0.0 # 混频相位连续
# 固定线性增益(不做按块 RMS)
self.fixed_gain = float(OUT_LEVEL)
# 记录(可用作标注):真实中心 MHz
self.channel_MHz = [2402.0, 2426.0, 2480.0]
def _schedule_event_into(self, t0, t1, env, fseq):
"""
把所有落入 [t0,t1) 的广播事件叠加到 env,并在对应段落设置 fseq。
事件:ch37→ch38→ch39;帧间隔 IFG;下一事件从“上一事件开始时刻”加抖动间隔计算。
"""
Fs = self.fs
while self.next_event < t1 + 1e-12:
ev_start = self.next_event
# 三帧的起止
t_cur = ev_start
for ch_i in (0, 1, 2): # 37→38→39
dur = self.rng.uniform(self.pdu_min_s, self.pdu_max_s)
# 叠加包络
_add_burst(env, t_cur - t0, t_cur + dur - t0, Fs, self.edge_samp, amp=1.0)
# 在该段填充对应中心频率(静默段 fseq 无关)
i0 = int(max(0, np.floor((t_cur - t0)*Fs)))
i1 = int(min(len(fseq), np.ceil((t_cur + dur - t0)*Fs)))
if i1 > i0:
fseq[i0:i1] = self.ch_freqs[ch_i]
t_cur += dur + self.ifg_s
# 下一事件:从当前事件“开始时刻”起按间隔+抖动
self.next_event = ev_start + self.T_ev_base + self.rng.uniform(0.0, self.T_ev_jit)
def work(self, input_items, output_items):
out = output_items[0]
N = len(out)
if N == 0:
return 0
fs = self.fs
t0 = self.t
t1 = t0 + N/fs
# 1) 连续 GFSK 基底
base = self.mod.next(N) # complex64,幅度≈1
# 2) 事件包络与频率序列
env = np.zeros(N, dtype=np.float64)
fseq = np.zeros(N, dtype=np.float64) # 仅在突发段赋值;静默段无所谓
self._schedule_event_into(t0, t1, env, fseq)
# 3) 逐样混频(跨块相位连续)
dphi = 2*np.pi * (fseq + self.f_shift) / fs
ph = self.mix_phase + np.cumsum(dphi)
self.mix_phase = float(ph[-1] % (2*np.pi))
mix = np.exp(1j * ph).astype(np.complex64)
# 4) 合成与固定增益
sig = base * mix * env.astype(np.float64)
sig *= self.fixed_gain
out[:] = sig.astype(np.complex64)
self.t = t1
return N

Bluetooth Classic FHSS 持续链路
# =============================================================================
# Bluetooth Classic (BR/EDR) FHSS — 稀疏版(慢跳频 + 低占空 + 子集信道)— 修复横纹
# 关键修复:
# [FIX-1] 驻留幅度在跳变处做余弦交叉渐变(不再整段阶跃),避免全频横纹
# [FIX-2] 取消按块 RMS 归一化,使用固定线性增益,消除块间幅度跳步
# =============================================================================
import numpy as np
from gnuradio import gr
# ---------------------- 顶部参数 ----------------------
SAMP_RATE = 1e6
MODE = "BR" # "BR" | "EDR2" | "EDR3"
SEED = 11
# BR(GFSK)
BR_SYM_RATE = 1.0e6
BR_BT = 0.50
BR_H = 0.32
# EDR(DPSK 近似)
EDR2_SYM_RATE = 2.0e6
EDR3_SYM_RATE = 3.0e6
RRC_BETA = 0.35
RRC_SPAN_SYM = 12
# 跳频与频栅格
N_CHANNELS = 79
GRID_HZ_TARGET = 1.0e6
HOP_DWELL_US = 2500.0 # 慢跳频(可调)
HOP_EDGE_FRAC = 0.25 # 跳变渐变宽度(每侧占驻留比例)
HOP_DIP_DB = 12.0 # 跳变“凹口”深度(dB)
AVOID_DC = True
# 稀疏信道
ACTIVE_CH_COUNT = 20
CHANNEL_SELECTION= "SPACED" # "SPACED" | "RANDOM"
# 业务占空(两层)
ONOFF_ENABLE = True
ON_MS_RANGE = (5.0, 15.0)
OFF_MS_RANGE = (5.0, 50.0)
ONOFF_EDGE_MS = 1.0
IDLE_PROB_PER_HOP= 0.30 # ON 状态下,每个驻留段静音的概率
SILENCE_FLOOR_DB = 60.0 # 静音地板幅度
# 统一移频与电平
F_SHIFT_HZ = 0.0
OUT_LEVEL = 0.5 # [FIX-2] 固定线性增益(不做按块RMS)
# 数值稳定
SPS_MIN = 8
GAUSS_SPAN_SYM = 4
# ---------------------- 工具函数 ----------------------
def _clip_off(f_off, fs):
return float(np.clip(f_off, -0.45*fs, 0.45*fs))
def _rcos_taps(beta=0.35, sps=8, span=12):
N = span * sps
t = np.arange(-N/2, N/2 + 1, dtype=np.float64) / sps
h = np.zeros_like(t)
for i, x in enumerate(t):
ax = abs(x)
if np.isclose(x, 0.0):
h[i] = 1.0 - beta + 4*beta/np.pi
elif np.isclose(ax, 1.0/(4*beta)):
h[i] = (beta/np.sqrt(2.0))*((1+2/np.pi)*np.sin(np.pi/(4*beta)) + (1-2/np.pi)*np.cos(np.pi/(4*beta)))
else:
h[i] = (np.sin(np.pi*x*(1-beta)) + 4*beta*x*np.cos(np.pi*x*(1+beta))) / (np.pi*x*(1-(4*beta*x)**2))
h /= np.sqrt(np.sum(h**2) + 1e-15)
return h
def _gauss_taps(bt=0.5, sps=8, span=4):
sigma_ts = np.sqrt(np.log(2.0)) / (2*np.pi*bt)
L = span * sps
t = (np.arange(-L/2, L/2 + 1, dtype=np.float64)) / sps
g = np.exp(-(t**2) / (2*sigma_ts**2))
g /= np.sum(g)
return g
def _apply_valley(env, idx, edge, floor_amp):
"""跳变中心 idx 处乘一个对称‘凹口’,抑制频率阶跃泄露。"""
N = len(env)
floor_amp = float(np.clip(floor_amp, 0.0, 1.0))
i0 = max(0, idx - edge)
i1 = min(N, idx + edge)
if i1 <= i0 or edge <= 1:
return
left_L = min(edge, idx - i0)
right_L = min(edge, i1 - idx)
if left_L > 0:
k = np.arange(left_L)
w = floor_amp + (1.0 - floor_amp) * 0.5 * (1 + np.cos(np.pi * (k+1)/left_L))
env[idx-left_L:idx] *= w
if right_L > 0:
k = np.arange(right_L)
w = floor_amp + (1.0 - floor_amp) * 0.5 * (1 + np.cos(np.pi * (right_L-k)/right_L))
env[idx:idx+right_L] *= w
def _apply_fade(mask, edge_samp):
if edge_samp <= 1:
return mask.astype(np.float64)
N = len(mask)
out = mask.astype(np.float64).copy()
up = np.flatnonzero(np.diff(np.pad(mask.astype(np.int8),(1,0)))==1)
for i0 in up:
i1 = min(N, i0 + edge_samp)
ramp = 0.5*(1 - np.cos(np.linspace(0, np.pi, i1-i0)))
out[i0:i1] *= ramp
dn = np.flatnonzero(np.diff(np.pad(mask.astype(np.int8),(0,1)))==-1)
for j1 in dn:
j0 = max(0, j1 - edge_samp)
ramp = 0.5*(1 - np.cos(np.linspace(np.pi, 0, j1-j0)))
out[j0:j1] *= ramp
return out
def _crossfade_amp(env_amp, idx, edge, a_prev, a_next):
"""
[FIX-1] 在跳变中心 idx 两侧各 edge 样本内,将幅度从 a_prev 余弦平滑过渡到 a_next。
只写绝对幅度包络(非乘法),避免整段阶跃导致的‘横纹’。
"""
N = len(env_amp)
i0 = max(0, idx - edge)
i1 = min(N, idx + edge)
if i1 <= i0 or edge <= 0:
return
L = i1 - i0
# 0..1 的余弦权重
r = 0.5*(1 - np.cos(2*np.pi*np.arange(L)/(L-1 if L>1 else 1)))
env_amp[i0:idx] = a_prev*(1 - r[:idx-i0]) + a_next*r[:idx-i0]
env_amp[idx:i1] = a_prev*(1 - r[idx-i0:]) + a_next*r[idx-i0:]
# ---------------------- 调制器 ----------------------
class GFSKStreamer:
def __init__(self, fs, Rs_target, bt, h, sps_min=SPS_MIN, span_sym=GAUSS_SPAN_SYM, seed=0):
self.fs = float(fs)
Rs = min(Rs_target, self.fs / float(sps_min))
self.sps = max(sps_min, int(round(self.fs / Rs)))
self.Rs = self.fs / self.sps
self.bt = float(bt); self.h = float(h)
self.span = int(span_sym)
self.Ts = 1.0 / self.fs
self.df = 0.5 * self.h * self.Rs
self.taps = _gauss_taps(self.bt, self.sps, self.span)
self.L = len(self.taps)
self.prev_up = np.zeros(self.L-1, dtype=np.float64)
self.phase = 0.0
self.rng = np.random.default_rng(seed)
self.cur_sym = 1.0 if self.rng.random() < 0.5 else -1.0
self.rep_left = 0
def _make_up(self, N):
up = np.empty(N, dtype=np.float64)
i = 0
while i < N:
if self.rep_left == 0:
self.cur_sym = 1.0 if self.rng.random() < 0.5 else -1.0
self.rep_left = self.sps
take = min(self.rep_left, N - i)
up[i:i+take] = self.cur_sym
self.rep_left -= take
i += take
return up
def next(self, N):
up = self._make_up(N)
arr = np.concatenate([self.prev_up, up])
filt = np.convolve(arr, self.taps, mode='valid')
self.prev_up = arr[-(self.L-1):]
dphi = 2*np.pi * (self.df * filt) * self.Ts
phi = self.phase + np.cumsum(dphi)
self.phase = float(phi[-1] % (2*np.pi))
return np.exp(1j * phi).astype(np.complex64)
class DPSKStreamer:
def __init__(self, fs, Rs_target, M, beta=0.35, sps_min=SPS_MIN, span_sym=RRC_SPAN_SYM, seed=0):
self.fs = float(fs)
Rs = min(Rs_target, self.fs / float(sps_min))
self.sps = max(sps_min, int(round(self.fs / Rs)))
self.Rs = self.fs / self.sps
self.M = int(M); self.beta = float(beta)
self.span = int(span_sym)
self.taps = _rcos_taps(self.beta, self.sps, self.span)
self.L = len(self.taps)
self.prev_up = np.zeros(self.L-1, dtype=np.complex128)
self.rng = np.random.default_rng(seed)
self.phase = 0.0
def _symbol_stream(self, Nsym):
k = self.rng.integers(0, self.M, size=Nsym, dtype=np.int32)
dtheta = 2*np.pi * k / self.M
theta = np.cumsum(dtheta) + self.phase
self.phase = float(theta[-1] % (2*np.pi))
return np.exp(1j * theta)
def next(self, N):
Nsym = int(np.ceil(N / self.sps)) + self.span
sym = self._symbol_stream(Nsym)
up = np.zeros(Nsym * self.sps, dtype=np.complex128)
up[::self.sps] = sym
arr = np.concatenate([self.prev_up, up])
shaped = np.convolve(arr, self.taps, mode='valid')[:N]
self.prev_up = arr[-(self.L-1):]
return shaped.astype(np.complex64)
# ---------------------- epy_block ----------------------
class blk(gr.sync_block):
"""
BT_Classic_FHSS_Sparse_Fixed
修复‘横纹’后的稀疏 FHSS。
"""
def __init__(self):
gr.sync_block.__init__(
self, name="BT_Classic_FHSS_Sparse_Fixed",
in_sig=None, out_sig=[np.complex64],
)
self.fs = float(SAMP_RATE)
self.rng = np.random.default_rng(SEED)
self.t = 0.0
# 调制器
mode = str(MODE).upper()
if mode == "BR":
self.mod = GFSKStreamer(self.fs, BR_SYM_RATE, BR_BT, BR_H, seed=SEED+1)
elif mode == "EDR2":
self.mod = DPSKStreamer(self.fs, EDR2_SYM_RATE, M=4, beta=RRC_BETA, seed=SEED+2)
elif mode == "EDR3":
self.mod = DPSKStreamer(self.fs, EDR3_SYM_RATE, M=8, beta=RRC_BETA, seed=SEED+3)
else:
raise ValueError("MODE must be 'BR' | 'EDR2' | 'EDR3'")
# 栅格(自动压缩)
full_grid = min(GRID_HZ_TARGET, 0.90*self.fs / (N_CHANNELS-1))
self.grid_hz = full_grid
dc_off = 0.5*full_grid if AVOID_DC else 0.0
k = np.arange(N_CHANNELS) - (N_CHANNELS-1)/2.0
all_freqs = k * full_grid + dc_off
# 子集信道
M = int(np.clip(ACTIVE_CH_COUNT, 1, N_CHANNELS))
if CHANNEL_SELECTION.upper() == "SPACED":
idx = np.round(np.linspace(0, N_CHANNELS-1, M)).astype(int)
else:
idx = self.rng.choice(N_CHANNELS, size=M, replace=False); idx.sort()
self.chan_idx = idx
self.chan_freqs = all_freqs[self.chan_idx]
# 跳频时序与包络参数
self.dwell = float(HOP_DWELL_US) * 1e-6
self.edge_samp = max(2, int(self.dwell * self.fs * float(HOP_EDGE_FRAC)))
self.valley = 10**(-float(HOP_DIP_DB)/20.0)
self.cur_ch_i = int(self.rng.integers(0, len(self.chan_freqs)))
self.next_hop = self.dwell
# [FIX-1] 驻留幅度状态,用于跨块与跨跳的平滑
self.silence_floor = 10**(-float(SILENCE_FLOOR_DB)/20.0)
self.cur_amp = 1.0 # 当前驻留幅度
# 在 ON 状态下,下一驻留目标幅度按概率为 1 或 floor
self.next_amp = 1.0
# ON/OFF
self.onoff_enable = bool(ONOFF_ENABLE)
self.onoff_state = 1
self.onoff_edge = int(max(1, ONOFF_EDGE_MS*1e-3*self.fs))
self.onoff_next = self.t + self.rng.uniform(*ON_MS_RANGE)*1e-3
# 混频相位与平移
self.mix_phase = 0.0
self.f_shift = _clip_off(F_SHIFT_HZ, self.fs)
# [FIX-2] 固定线性增益
self.fixed_gain = float(OUT_LEVEL)
def _draw_next_index(self, prev_i):
M = len(self.chan_freqs)
j = int(self.rng.integers(0, M))
if j == prev_i:
j = (j + 1) % M
return j
def _onoff_env(self, t0, N):
Fs = self.fs
t = t0 + np.arange(N)/Fs
mask = np.ones(N, dtype=np.int8)
if not self.onoff_enable:
return mask.astype(np.float64)
state = self.onoff_state
next_flip = self.onoff_next
cur = t0
while True:
seg_end = min(t[-1], next_flip)
if state == 0:
k0 = int(max(0, np.floor((cur - t0)*Fs)))
k1 = int(min(N, np.ceil((seg_end - t0)*Fs)))
if k1 > k0:
mask[k0:k1] = 0
if seg_end >= t[-1] - 1e-12:
break
state = 1 - state
cur = seg_end
dur_ms = self.rng.uniform(*(ON_MS_RANGE if state==1 else OFF_MS_RANGE))
next_flip = seg_end + dur_ms*1e-3
self.onoff_state = state
self.onoff_next = next_flip
return _apply_fade(mask, self.onoff_edge)
def work(self, input_items, output_items):
out = output_items[0]; N = len(out)
if N == 0: return 0
fs = self.fs
t0 = self.t
t1 = t0 + N/fs
# 调制
base = self.mod.next(N) # 幅度≈1
# 频率序列、跳变凹口、幅度包络(含交叉渐变)
fseq = np.empty(N, dtype=np.float64); fseq[:] = self.chan_freqs[self.cur_ch_i]
env_valley = np.ones(N, dtype=np.float64)
env_amp = np.empty(N, dtype=np.float64); env_amp[:] = float(self.cur_amp)
# 处理本块内的所有跳变(按时间顺序)
while self.next_hop < t1 + 1e-12:
idx = int(np.floor((self.next_hop - t0) * fs))
if 0 <= idx < N:
# 跳变:频率切换 + 凹口
_apply_valley(env_valley, idx, self.edge_samp, self.valley)
prev_i = self.cur_ch_i
self.cur_ch_i = self._draw_next_index(prev_i)
new_f = self.chan_freqs[self.cur_ch_i]
fseq[idx:] = new_f
# [FIX-1] 幅度交叉渐变:从 self.cur_amp → amp_next
# 仅在 ON 状态下按概率静音;OFF 时强制静音地板
if (not self.onoff_enable) or (self.onoff_state == 1):
amp_next = 1.0 if (self.rng.random() >= float(IDLE_PROB_PER_HOP)) else self.silence_floor
else:
amp_next = self.silence_floor
_crossfade_amp(env_amp, idx, self.edge_samp, float(self.cur_amp), float(amp_next))
self.cur_amp = amp_next
self.next_hop += self.dwell
# ON/OFF 大包络
env_onoff = self._onoff_env(t0, N)
# 混频(跨块相位连续)
dphi = 2*np.pi * (fseq + self.f_shift) / fs
ph = self.mix_phase + np.cumsum(dphi)
self.mix_phase = float(ph[-1] % (2*np.pi))
mix = np.exp(1j * ph).astype(np.complex64)
# 合成:基带 * 混频 * 三层包络(凹口 × 驻留幅度 × ON/OFF)
env = env_valley * env_amp * env_onoff
sig = base * mix * env.astype(np.float64)
# [FIX-2] 固定线性增益(不做按块 RMS)
sig *= self.fixed_gain
out[:] = sig.astype(np.complex64)
self.t = t1
return N

Wi-Fi “空闲+信标"
# =============================================================================
# 名称
# Wi-Fi 2.4 GHz “空闲 + 信标” 频谱占用近似(低占空、窄突发、低旁瓣)
#
# 生成的是什么信号?
# - 固定信道(不跳频)的“空闲 AP”外观:大部分时间静默,仅周期性信标突发;
# - 偶尔插入“探测/管理帧”类的短包,使占空上到 1–3% 以内;
# - 非真实 802.11 PHY:用 16QAM + RRC 的“宽带随机符号流”近似 OFDM 的频域占用,仅复现宏观节律。
#
# 行为与节律
# - 信标周期:100 TU ≈ 102.4 ms(可加抖动);每次信标为 0.3–1.0 ms 的短突发;
# - 探测/管理帧:按概率在相邻信标之间随机插入 0.8–2.0 ms 的短突发,电平可与信标不同;
# - 占空目标:默认 ~0.5%(仅信标)到 ~2%(含偶发管理帧)。
#
# 旁瓣控制
# - 先成形(RRC,sps≥8,span≥12)后移频;
# - 所有突发边缘做余弦渐入/出(避免硬门控泄露);
# - 可选 Kaiser 窗 FIR 低通进一步压带外(~80 dB 级,取决于 Fs 与参数)。
#
# 使用
# 1) 将本文件作为 epy_block 接到 Throttle(仿真)或 USRP Sink(外发),采样率用 SAMP_RATE;
# 2) 设 F_SHIFT_HZ 把基带搬到目标中心(例:USRP=2.440 GHz,要合成信道1中心→F_SHIFT_HZ=2.412e9-2.440e9);
# 3) 想更低占空:缩短 BEACON_LEN_MS 或调低 MGMT_PROB、拉大 MGMT_GAP_MS_RANGE。
# =============================================================================
import numpy as np
from gnuradio import gr
# ====================== 固定参数(无需 GUI 配置) ======================
SAMP_RATE = 1e6 # 采样率;低 Fs 运行轻快;升高可得更宽合成占用
CHANNEL = 6 # 仅记录:1..13 → 2412+5*(ch-1) MHz
BETA = 0.35 # RRC 滚降
TARGET_BW_HZ = 20e6 # 目标带宽;实际带宽≈(1+β)*R_s,会按 sps 约束自适应
SPS_TARGET = 8 # 成形过采样下限;≥8 抑制泄露更稳
RRC_SPAN_SYM = 12 # RRC 脉冲长度(符号数)
# —— 信标(Beacon)设置 ——
BEACON_INTERVAL_MS = 102.4 # 周期 100 TU
BEACON_JITTER_MS = 2.0 # 周期抖动(±)上限,0 表示无抖动
BEACON_LEN_MS_RANGE = (0.4, 0.8) # 单次信标时长范围
BEACON_EDGE_MS = 0.20 # 渐入/出时长(每侧)
# —— 偶发探测/管理帧(低占空突发)设置 ——
MGMT_ENABLE = True
MGMT_PROB_PER_BEACON = 0.20 # 每个信标周期后插入一次管理帧的概率
MGMT_DELAY_MS_RANGE = (4.0, 40.0) # 管理帧相对上次信标的延迟
MGMT_LEN_MS_RANGE = (0.8, 2.0) # 管理帧时长
MGMT_EDGE_MS = 0.30 # 管理帧渐入/出时长
MGMT_REL_AMP_DB = -3.0 # 相对信标的电平(dB)
# 频率与电平
DATA_CFO_HZ = 0.0 # 轻微 CFO(可 0)
F_SHIFT_HZ = 0.0 # 统一移频(限制在 ±0.45*Fs 内)
OUT_LEVEL = 0.5 # 输出 RMS 归一化目标
SEED = 2025 # 随机种子
# 带外抑制 FIR(Kaiser 低通,可选)
LPF_ENABLE = True
LPF_ATTEN_DB = 80.0 # 阻带衰减指标
LPF_PASS_KEEP = 0.90 # 通过带截止=0.5*(1+β)*R_s*PASS_KEEP
LPF_TRANS_FRAC = 0.20 # 过渡带宽占目标带宽的比例
# ====================== 滤波与调制工具 ======================
def rrc_taps(beta=0.35, sps=8, span=12):
N = span * sps
t = np.arange(-N/2, N/2 + 1, dtype=np.float64) / sps
taps = np.zeros_like(t, dtype=np.float64)
for i, x in enumerate(t):
ax = abs(x)
if np.isclose(x, 0.0):
taps[i] = 1.0 - beta + 4*beta/np.pi
elif np.isclose(ax, 1.0/(4*beta)):
taps[i] = (beta/np.sqrt(2.0)) * (
(1 + 2/np.pi) * np.sin(np.pi/(4*beta)) +
(1 - 2/np.pi) * np.cos(np.pi/(4*beta))
)
else:
num = (np.sin(np.pi * x * (1 - beta))
+ 4*beta*x*np.cos(np.pi * x * (1 + beta)))
den = np.pi * x * (1 - (4*beta*x)**2)
taps[i] = num / den
taps /= np.sqrt(np.sum(taps**2) + 1e-15)
return taps
def fir_lpf_kaiser(fs, f_pass, f_trans, atten_db):
dw = 2*np.pi * (f_trans / fs)
if atten_db > 50:
beta = 0.1102*(atten_db - 8.7)
elif atten_db >= 21:
beta = 0.5842*(atten_db - 21)**0.4 + 0.07886*(atten_db - 21)
else:
beta = 0.0
M = int(np.ceil((atten_db - 8.0) / (2.285 * dw))) | 1
n = np.arange(M) - (M-1)/2
h = 2*(f_pass/fs) * np.sinc(2*f_pass*n/fs)
w = np.kaiser(M, beta)
taps = h * w
taps /= np.sum(taps)
return taps.astype(np.float64)
def mod_16QAM_rrc(freq, t, samp_rate, sym_rate, beta, rng, sps, span):
"""16QAM + RRC,上采样后先成形再移频;相位随 t 连续。"""
N = len(t)
if N == 0:
return np.zeros(0, np.complex64)
n_sym = int(np.ceil(N / sps)) + span
bits = rng.integers(0, 2, size=(n_sym, 4), dtype=np.int8)
I = (2*bits[:,0]+bits[:,1]) - 1.5
Q = (2*bits[:,2]+bits[:,3]) - 1.5
symbols = (I + 1j*Q) / np.sqrt(10)
up = np.zeros(n_sym * sps, dtype=np.complex128)
up[::sps] = symbols
taps = rrc_taps(beta=beta, sps=sps, span=span)
shaped = np.convolve(up, taps, mode="same")[:N]
return (shaped * np.exp(1j * 2*np.pi * freq * t)).astype(np.complex64)
# ====================== 突发包络工具 ======================
def _add_burst(env, start, end, fs, edge_samp, amp=1.0):
"""在 env 上叠加一个 [start,end) 的突发,边缘余弦渐入/出,峰值 amp。"""
if end <= start:
return
N = len(env)
i0 = int(np.floor(start * fs))
i1 = int(np.ceil(end * fs))
if i1 <= 0 or i0 >= N:
return
i0_clip = max(0, i0); i1_clip = min(N, i1)
L = i1_clip - i0_clip
if L <= 0:
return
e = int(edge_samp)
if 2*e >= L:
# 短包:用 Hann(单周期余弦)整形
w = 0.5*(1 - np.cos(2*np.pi*np.arange(L)/(L-1 if L>1 else 1)))
else:
w = np.ones(L, dtype=np.float64)
# 上升沿
w[:e] = 0.5*(1 - np.cos(np.linspace(0, np.pi, e, endpoint=False)))
# 下降沿
w[-e:] = 0.5*(1 - np.cos(np.linspace(np.pi, 0, e, endpoint=False)))
env[i0_clip:i1_clip] = np.maximum(env[i0_clip:i1_clip], amp*np.clip(w,0,1))
# ====================== epy_block 实现 ======================
class blk(gr.sync_block):
"""
WiFi_IdleWithBeacon_Approx
输出:低占空的“空闲+信标”基带信号(complex64)。
"""
def __init__(self):
gr.sync_block.__init__(
self, name="WiFi_IdleWithBeacon_Approx",
in_sig=None, out_sig=[np.complex64],
)
# 采样与成形自适应
self.samp_rate = float(SAMP_RATE)
self.beta = float(BETA)
self.rng = np.random.default_rng(SEED)
self.t = 0.0
Rs_need = TARGET_BW_HZ / (1.0 + self.beta)
Rs_sps = self.samp_rate / float(SPS_TARGET)
self.Rs = min(Rs_need, Rs_sps)
self.sps = max(SPS_TARGET, int(round(self.samp_rate / self.Rs)))
self.Rs = self.samp_rate / self.sps
self.span = int(RRC_SPAN_SYM)
# 记录用中心频率
self.channel = int(CHANNEL)
self.fc_MHz = 2412 + 5*(self.channel-1)
# 事件调度状态
self.beacon_T = float(BEACON_INTERVAL_MS) * 1e-3
self.beacon_j = float(BEACON_JITTER_MS) * 1e-3
self.next_beacon = self.beacon_T # 从 t=0 起,先来一次
self.data_cfo = float(DATA_CFO_HZ)
self.f_shift = float(F_SHIFT_HZ)
self.out_level = float(OUT_LEVEL)
# FIR 状态
self.lpf_taps = None
self.lpf_state = np.zeros(0, np.complex64)
if LPF_ENABLE:
tgt_bw = (1.0 + self.beta) * self.Rs
f_pass = 0.5 * tgt_bw * float(LPF_PASS_KEEP)
f_trans = max(200.0, float(LPF_TRANS_FRAC) * tgt_bw)
self.lpf_taps = fir_lpf_kaiser(self.samp_rate, f_pass, f_trans, LPF_ATTEN_DB)
self.lpf_state = np.zeros(len(self.lpf_taps)-1, np.complex64)
# 预先把管理帧相对幅度转为线性
self.mgmt_amp = 10**(float(MGMT_REL_AMP_DB)/20.0)
def _schedule_events(self, t0, t1, env):
"""在 [t0,t1) 内向 env 叠加信标与管理帧包络。"""
Fs = self.samp_rate
# 迭代添加跨度内的所有信标
while self.next_beacon < t1 + 1e-12:
b_start = self.next_beacon
b_len = self.rng.uniform(*BEACON_LEN_MS_RANGE) * 1e-3
b_edge = int(round(float(BEACON_EDGE_MS) * 1e-3 * Fs))
_add_burst(env, b_start - t0, b_start + b_len - t0, Fs, b_edge, amp=1.0)
# 可能插入一帧管理帧
if MGMT_ENABLE and (self.rng.random() < float(MGMT_PROB_PER_BEACON)):
delay = self.rng.uniform(*MGMT_DELAY_MS_RANGE) * 1e-3
m_start = b_start + delay
m_len = self.rng.uniform(*MGMT_LEN_MS_RANGE) * 1e-3
m_edge = int(round(float(MGMT_EDGE_MS) * 1e-3 * Fs))
_add_burst(env, m_start - t0, m_start + m_len - t0, Fs, m_edge, amp=self.mgmt_amp)
# 下一次信标时间(带对称抖动)
jitter = self.rng.uniform(-self.beacon_j, self.beacon_j)
# 保证不低于 50% 周期,避免极端重叠
self.next_beacon += max(0.5*self.beacon_T, self.beacon_T + jitter)
def work(self, input_items, output_items):
out = output_items[0]
N = len(out)
if N == 0:
return 0
Fs = self.samp_rate
t_vec = self.t + np.arange(N)/Fs
# 生成“宽带占用近似”的基底
base = mod_16QAM_rrc(self.data_cfo, t_vec, Fs,
sym_rate=self.Rs, beta=self.beta,
rng=self.rng, sps=self.sps, span=self.span)
# 事件包络(0..1),只在突发时非零
env = np.zeros(N, dtype=np.float64)
self._schedule_events(self.t, self.t + N/Fs, env)
# 施加包络
sig = base * env.astype(np.float64)
# RMS 归一化
p = np.sqrt(np.mean(np.abs(sig)**2) + 1e-12)
if p > 0:
sig *= (self.out_level / p)
# 带外抑制 FIR(有状态卷积)
if self.lpf_taps is not None:
buf = np.concatenate([self.lpf_state, sig.astype(np.complex64)])
full = np.convolve(buf, self.lpf_taps, mode='full')
sig = full[len(self.lpf_taps)-1 : len(self.lpf_taps)-1 + N]
self.lpf_state = buf[-(len(self.lpf_taps)-1):]
# 统一移频
if F_SHIFT_HZ != 0.0:
sig *= np.exp(1j * 2*np.pi * np.clip(F_SHIFT_HZ, -0.45*Fs, 0.45*Fs) * t_vec)
out[:] = sig.astype(np.complex64)
self.t += N / Fs
return N

Wi-Fi 20 MHz OFDM“忙业务”
# =============================================================================
# 名称
# Wi-Fi 2.4 GHz “20 MHz OFDM 忙业务” 频谱占用近似(带 ON-OFF 业务门控,低旁瓣版)
#
# 生成的是什么信号?
# - 固定信道(不跳频)的“Wi-Fi 类宽带占用”外观,用于干扰/占用检测的数据集合成。
# - 用 16QAM + RRC 成形的宽带随机符号流来近似 20 MHz OFDM 的带宽与“丰满”度。
# - 行为特征:
# 1) 数据流近似“持续帧 + 短 ACK 缝”(ACK 缝为浅凹并做长余弦斜坡,避免硬门控展宽)。
# 2) 每 102.4 ms 产生一个“信标”短突发(独立包络,OFF 时仍可保留以模拟 AP 信标)。
# 3) 业务 ON-OFF 门控:模拟 CSMA/CA 下“有时发、有时不发”的忙闲交替(非周期,区间随机)。
# - 非真实 802.11:不包含 STF/LTF/导频/编码/CSMA 过程,仅复现“频域占用 + 时域节律”的宏观特征。
#
# 为什么不是跳频?
# - Wi-Fi 在 2.4/5/6 GHz 使用固定信道(DSSS/CCK 或 OFDM),不做 FHSS。你若需要跳频,请改用 BT/BLE FHSS 合成块。
#
# 旁瓣控制策略
# - 先成形(RRC,sps≥8,span≥12 符号)后移频,保证过渡带陡峭。
# - ACK/信标边界做余弦渐入/出;ACK 采用“浅凹地板”(非硬 0),显著降低门控泄露。
# - 可选的 Kaiser 窗 FIR 低通(砖墙化),把带外能量压到 ~−80 dB 级别(取决于 Fs 与参数)。
# - 全流程线性(无软限幅),避免非线性再生旁瓣。
#
# 关键参数(全部在下方常量中设定,GUI 无需配置)
# - SAMP_RATE:采样率。低 Fs 运行轻,带宽会按 sps 约束自动缩小;要“更宽”,升 Fs。
# - TARGET_BW_HZ:目标带宽(如 20e6)。实际带宽≈(1+β)*R_s;本块会在“sps≥SPS_TARGET”前提下自适应 R_s。
# - BETA、SPS_TARGET、RRC_SPAN_SYM:决定成形滚降与过采样品质。
# - ACK_*、BEACON_*:ACK 凹槽与信标的节律与长度。
# - ONOFF_*:业务门控的 ON/OFF 随机区间与渐入/出时长;OFF 时可仅保留信标。
# - F_SHIFT_HZ:最终统一移频。例:USRP 设 2.440 GHz,要合成“信道 1 中心” → F_SHIFT_HZ=2.412e9-2.440e9。
# - LPF_*:Kaiser 低通开关与指标;提高 LPF_ATTEN_DB 或减小 LPF_PASS_KEEP 可进一步压低带外。
#
# 建议可视化设置(GNU Radio QT GUI Sink / FFT)
# - Window=Hann 或 Blackman;FFT Size≥4096;Average=20-50;关闭 Autoscale,手动 −120…0 dB。
#
# 典型用法
# 1) 将本文件作为 epy_block 加入流图,采样率用 SAMP_RATE。仿真接 Throttle,外发接 USRP Sink。
# 2) 需要 1/6/11 三信道并发:复制本块 3 次,分别设 F_SHIFT_HZ 到 2412/2437/2462 MHz 的相对偏移。
# 3) 需要“更断断续续”:增大 OFF_MS_RANGE 上限,或缩短 ON_MS_RANGE。
# =============================================================================
import numpy as np
from gnuradio import gr
# ====================== 固定参数(无需 GUI 配置) ======================
SAMP_RATE = 1e6 # 采样率;升高可得到更宽的合成占用
CHANNEL = 6 # 仅记录:1..13 → 2412+5*(ch-1) MHz
BETA = 0.35 # RRC 滚降因子 β
TARGET_BW_HZ = 20e6 # 目标“20 MHz”外观;实际带宽由 R_s 和 β 决定
SPS_TARGET = 8 # 成形过采样比下限(≥8 有利于抑制泄露)
RRC_SPAN_SYM = 12 # RRC 脉冲长度(符号数)
# ACK/信标:模拟“持续帧 + 短 ACK 缝 + 周期信标”
ACK_GAP_US = 30.0 # 每个 ACK 凹槽名义宽度(微秒)
ACK_PERIOD_MS = 1.2 # ACK 凹槽平均周期(毫秒)
ACK_DIP_DB = 12.0 # ACK 凹槽深度(dB),越大越“浅凹”
ACK_EDGE_FRAC = 0.6 # ACK 渐入/出相对凹槽宽度的比例
BEACON_INTERVAL_MS = 102.4 # 信标周期(100 TU)
BEACON_LEN_MS = 0.5 # 信标突发长度(毫秒)
# 业务 ON-OFF 门控(OFF 时可仅保留信标,近似“空闲信道”)
ONOFF_ENABLE = True
ON_MS_RANGE = (2.0, 8.0) # 每次“忙”持续时长范围(毫秒,均匀分布)
OFF_MS_RANGE = (0.2, 15.0) # 每次“闲”持续时长范围(毫秒,均匀分布)
ONOFF_EDGE_MS = 1.0 # ON/OFF 渐入出时长(毫秒)
# 频率与电平
DATA_CFO_HZ = 0.0 # 给数据流一个极小 CFO(可为 0)
F_SHIFT_HZ = 0.0 # 最终统一移频(搬到目标中心)
OUT_LEVEL = 0.5 # 线性电平目标(RMS 归一化)
SEED = 1 # 随机种子(保证可复现)
# 带外抑制 FIR(Kaiser 低通,可选)
LPF_ENABLE = True
LPF_ATTEN_DB = 80.0 # 阻带衰减指标(dB)
LPF_PASS_KEEP = 0.90 # 通过带截止=0.5*(1+β)*R_s*PASS_KEEP(略留余量防过削)
LPF_TRANS_FRAC = 0.20 # 过渡带宽占目标带宽的比例(越小越陡,阶数越高)
# ====================== 滤波与调制工具 ======================
def rrc_taps(beta: float = 0.35, sps: int = 8, span: int = 12) -> np.ndarray:
"""
生成 RRC(Root-Raised-Cosine)脉冲的离散抽头。
参数:
beta: RRC 滚降因子 β
sps: 每符号采样点数(过采样比)
span: 脉冲跨越的符号数(总长=span*sps+1)
返回:
实系数 1D float64 数组,能量归一化(∑h^2=1)
"""
N = span * sps
t = np.arange(-N/2, N/2 + 1, dtype=np.float64) / sps
taps = np.zeros_like(t, dtype=np.float64)
for i, x in enumerate(t):
ax = abs(x)
if np.isclose(x, 0.0):
taps[i] = 1.0 - beta + 4*beta/np.pi
elif np.isclose(ax, 1.0/(4*beta)):
taps[i] = (beta/np.sqrt(2.0)) * (
(1 + 2/np.pi) * np.sin(np.pi/(4*beta)) +
(1 - 2/np.pi) * np.cos(np.pi/(4*beta))
)
else:
num = (np.sin(np.pi * x * (1 - beta))
+ 4*beta*x*np.cos(np.pi * x * (1 + beta)))
den = np.pi * x * (1 - (4*beta*x)**2)
taps[i] = num / den
taps /= np.sqrt(np.sum(taps**2) + 1e-15)
return taps
def fir_lpf_kaiser(fs: float, f_pass: float, f_trans: float, atten_db: float) -> np.ndarray:
"""
Kaiser 窗低通 FIR 设计(实系数)。
参数:
fs: 采样率
f_pass: 通带截止频率(Hz)
f_trans: 过渡带宽(Hz)
atten_db: 阻带衰减(dB)
返回:
FIR 抽头,单位增益(∑h=1)
"""
dw = 2*np.pi * (f_trans / fs) # 归一化过渡宽度
if atten_db > 50:
beta = 0.1102*(atten_db - 8.7)
elif atten_db >= 21:
beta = 0.5842*(atten_db - 21)**0.4 + 0.07886*(atten_db - 21)
else:
beta = 0.0
M = int(np.ceil((atten_db - 8.0) / (2.285 * dw))) | 1 # 阶数(取奇数)
n = np.arange(M) - (M-1)/2
# 理想低通 sinc,注意 np.sinc(x)=sin(pi x)/(pi x)
h = 2*(f_pass/fs) * np.sinc(2*f_pass*n/fs)
w = np.kaiser(M, beta)
taps = h * w
taps /= np.sum(taps) # 单位增益
return taps.astype(np.float64)
def mod_16QAM_rrc(freq: float, t: np.ndarray, samp_rate: float, sym_rate: float,
beta: float, rng: np.random.Generator, sps: int, span: int) -> np.ndarray:
"""
16QAM 基带符号 → RRC 成形上采样 → 频移到 freq,保持跨块相位连续。
参数:
freq: 基带频移(Hz),常用于注入轻微 CFO
t: 绝对时间戳数组(长度=输出长度)
samp_rate: 采样率
sym_rate: 符号率 R_s
beta: RRC 滚降
rng: 随机源
sps/span: 成形参数
返回:
complex64 基带信号段(与 t 等长)
"""
N = len(t)
if N == 0:
return np.zeros(0, np.complex64)
n_sym = int(np.ceil(N / sps)) + span # 尾部冗余,避免截断泄露
bits = rng.integers(0, 2, size=(n_sym, 4), dtype=np.int8)
I = (2*bits[:,0] + bits[:,1]) - 1.5
Q = (2*bits[:,2] + bits[:,3]) - 1.5
symbols = (I + 1j*Q) / np.sqrt(10) # 单位平均功率
up = np.zeros(n_sym * sps, dtype=np.complex128)
up[::sps] = symbols
taps = rrc_taps(beta=beta, sps=sps, span=span)
shaped = np.convolve(up, taps, mode="same")[:N]
# 先成形后移频;相位随 t 自然连续
return (shaped * np.exp(1j * 2*np.pi * freq * t)).astype(np.complex64)
# ====================== 辅助:门控与地板 ======================
def _apply_fade(mask: np.ndarray, edge_samp: int) -> np.ndarray:
"""
对 0/1 掩码边缘做余弦渐入/出,返回 [0,1] 浮点包络。
edge_samp:渐变样本数(越大越平滑,旁瓣越低)。
"""
if edge_samp <= 1:
return mask.astype(np.float64)
N = len(mask)
out = mask.astype(np.float64).copy()
# 上升沿
up = np.flatnonzero(np.diff(np.pad(mask.astype(np.int8),(1,0)))==1)
for i0 in up:
i1 = min(N, i0 + edge_samp)
ramp = 0.5*(1 - np.cos(np.linspace(0, np.pi, i1-i0)))
out[i0:i1] *= ramp
# 下降沿
dn = np.flatnonzero(np.diff(np.pad(mask.astype(np.int8),(0,1)))==-1)
for j1 in dn:
j0 = max(0, j1 - edge_samp)
ramp = 0.5*(1 - np.cos(np.linspace(np.pi, 0, j1-j0)))
out[j0:j1] *= ramp
return out
def _with_floor(env01: np.ndarray, floor_amp: float) -> np.ndarray:
"""
将 [0,1] 包络映射到 [floor_amp, 1],用于“浅凹”(非硬 0)。
floor_amp=10^(-D/20),如 D=12 dB → floor≈0.25。
"""
f = float(np.clip(floor_amp, 0.0, 1.0))
return f + (1.0 - f) * env01
# ====================== epy_block 实现 ======================
class blk(gr.sync_block):
"""
WiFi20MHzBusyApprox_ONOFF
- 输出:complex64 基带信号,包含数据宽带占用、短 ACK 凹槽、周期信标,以及 ON-OFF 业务门控。
- 采样率与带宽会在“保证 sps≥SPS_TARGET”的前提下自适应,低 Fs 时自动缩小实际带宽。
"""
def __init__(self):
gr.sync_block.__init__(
self, name="WiFi20MHzBusyApprox_ONOFF",
in_sig=None, out_sig=[np.complex64],
)
# —— 基本配置与速率自适应 ——
self.samp_rate = float(SAMP_RATE)
self.beta = float(BETA)
self.rng = np.random.default_rng(SEED)
self.t = 0.0
Rs_need = TARGET_BW_HZ / (1.0 + self.beta) # 期望 R_s
Rs_sps = self.samp_rate / float(SPS_TARGET) # sps 约束下的最大 R_s
self.Rs = min(Rs_need, Rs_sps)
self.sps = max(SPS_TARGET, int(round(self.samp_rate / self.Rs)))
self.Rs = self.samp_rate / self.sps # 回写保证 sps 精确
self.span = int(RRC_SPAN_SYM)
# —— 元数据记录(不直接改变频移) ——
self.channel = int(CHANNEL)
self.fc_MHz = 2412 + 5*(self.channel-1)
# —— 时序参数(ACK/信标) ——
self.ack_gap = float(ACK_GAP_US) * 1e-6
self.ack_period = float(ACK_PERIOD_MS) * 1e-3
self.beacon_T = float(BEACON_INTERVAL_MS) * 1e-3
self.beacon_len = float(BEACON_LEN_MS) * 1e-3
self.next_ack = 0.5*self.ack_period
self.next_beacon = self.beacon_T
self.data_cfo = float(DATA_CFO_HZ)
self.f_shift = float(F_SHIFT_HZ)
self.out_level = float(OUT_LEVEL)
# —— 渐变与浅凹设置 ——
self.edge_ack = max(int(self.ack_gap * self.samp_rate * ACK_EDGE_FRAC),
self.sps * self.span)
self.edge_beacon = max(int(self.beacon_len * self.samp_rate * 0.3),
self.sps * self.span)
self.ack_floor = 10**(-float(ACK_DIP_DB)/20.0)
# —— ON-OFF 业务门控(OFF 时可仅保留信标) ——
self.onoff_enable = bool(ONOFF_ENABLE)
self.onoff_edge = int(max(1, ONOFF_EDGE_MS * 1e-3 * self.samp_rate))
self.onoff_state = 1 # 1=ON, 0=OFF
# 下一次翻转的绝对时间戳(初始化为“从当前时刻起”的一段 ON)
self.onoff_next = self.t + self.rng.uniform(*ON_MS_RANGE)*1e-3
# —— 带外抑制 FIR(Kaiser)状态 ——
self.lpf_taps = None
self.lpf_state = np.zeros(0, np.complex64)
if LPF_ENABLE:
tgt_bw = (1.0 + self.beta) * self.Rs
f_pass = 0.5 * tgt_bw * float(LPF_PASS_KEEP)
f_trans = max(200.0, float(LPF_TRANS_FRAC) * tgt_bw)
self.lpf_taps = fir_lpf_kaiser(self.samp_rate, f_pass, f_trans, LPF_ATTEN_DB)
self.lpf_state = np.zeros(len(self.lpf_taps)-1, np.complex64)
# —— 生成 ACK/信标/ON-OFF 包络(返回 0..1 的浮点包络) ——
def _make_masks(self, t0: float, N: int):
Fs = self.samp_rate
t = t0 + np.arange(N)/Fs
# ACK 凹槽:置 0,再做渐变并映射为浅凹
on = np.ones(N, dtype=np.int8)
i = 0
while self.next_ack < t[-1] + 1e-12:
g0 = self.next_ack
g1 = g0 + max(10e-6, self.ack_gap * (0.8 + 0.4*self.rng.random()))
self.next_ack += max(0.2e-3, self.ack_period * (0.6 + 0.8*self.rng.random()))
i0 = int(max(0, np.floor((g0 - t0)*Fs)))
i1 = int(min(N, np.ceil((g1 - t0)*Fs)))
if i1 > i0:
on[i0:i1] = 0
i += 1
if i > 64: # 防止极端情况下过多事件
break
# 信标:独立短突发
beacon = np.zeros(N, dtype=np.int8)
j = 0
while self.next_beacon < t[-1] + 1e-12:
b0 = self.next_beacon
b1 = b0 + self.beacon_len
self.next_beacon += self.beacon_T
j0 = int(max(0, np.floor((b0 - t0)*Fs)))
j1 = int(min(N, np.ceil((b1 - t0)*Fs)))
if j1 > j0:
beacon[j0:j1] = 1
j += 1
if j > 8:
break
# 业务 ON-OFF:仅作用在“数据”上,不屏蔽信标
if self.onoff_enable:
onoff = np.ones(N, dtype=np.int8)
state = self.onoff_state
next_flip = self.onoff_next
cur = t0
while True:
seg_end = min(t[-1], next_flip)
if state == 0:
k0 = int(max(0, np.floor((cur - t0)*Fs)))
k1 = int(min(N, np.ceil((seg_end - t0)*Fs)))
if k1 > k0:
onoff[k0:k1] = 0
if seg_end >= t[-1] - 1e-12:
break
# 翻转并抽下一段
state = 1 - state
cur = seg_end
dur_ms = self.rng.uniform(*(ON_MS_RANGE if state==1 else OFF_MS_RANGE))
next_flip = seg_end + dur_ms*1e-3
# 更新到块末状态
self.onoff_state = state
self.onoff_next = next_flip
onoff_env = _apply_fade(onoff, self.onoff_edge)
else:
onoff_env = np.ones(N, dtype=np.float64)
# ACK/信标平滑 + ACK 浅凹
on_env = _apply_fade(on, max(self.edge_ack, 16))
on_env = _with_floor(on_env, self.ack_floor)
be_env = _apply_fade(beacon, max(self.edge_beacon, 16))
return on_env, be_env, onoff_env
# —— 主工作函数:生成一段基带并施加包络、带外抑制、频移 ——
def work(self, input_items, output_items):
out = output_items[0]
N = len(out)
if N == 0:
return 0
Fs = self.samp_rate
t = self.t + np.arange(N)/Fs
# 生成两路宽带占用(数据与信标),相位随 t 连续
sig_data = mod_16QAM_rrc(DATA_CFO_HZ, t, Fs,
sym_rate=self.Rs, beta=self.beta,
rng=self.rng, sps=self.sps, span=self.span)
sig_beac = mod_16QAM_rrc(0.0, t, Fs,
sym_rate=self.Rs, beta=self.beta,
rng=self.rng, sps=self.sps, span=self.span)
# 包络:数据受 ACK 与 ON-OFF 共同约束;信标仅受自身包络
on_env, be_env, onoff_env = self._make_masks(self.t, N)
sig = sig_data * (on_env * onoff_env) + 0.6 * sig_beac * be_env
# 线性电平(RMS 归一化到 OUT_LEVEL)
sig *= self.out_level / (np.sqrt(np.mean(np.abs(sig)**2) + 1e-12))
# 可选:带外抑制 FIR(Kaiser),简单的“有状态卷积”
if LPF_ENABLE and self.lpf_taps is not None:
buf = np.concatenate([self.lpf_state, sig.astype(np.complex64)])
full = np.convolve(buf, self.lpf_taps, mode='full')
sig = full[len(self.lpf_taps)-1 : len(self.lpf_taps)-1 + N]
self.lpf_state = buf[-(len(self.lpf_taps)-1):]
# 统一移频到目标中心(限制在 ±0.45*Fs 内避免混叠)
if F_SHIFT_HZ != 0.0:
sig *= np.exp(1j * 2*np.pi * np.clip(F_SHIFT_HZ, -0.45*Fs, 0.45*Fs) * t)
out[:] = sig.astype(np.complex64)
self.t += N / Fs
return N

Zigbee[IEEE 802.15.4](2.4 GHz)
# =============================================================================
# 名称
# Zigbee / IEEE 802.15.4 (2.4 GHz) 近似:QPSK+RRC 窄带占用 + 成组短帧(非跳频)
#
# 生成的是什么信号?
# - 固定信道(默认不跳频)的 Zigbee 类外观:窄而干净的 ~2 MHz 带宽占用;
# - 时序:短帧、成组 burst,组与组之间随机间隔(类似 CSMA/CA 业务突发);
# - 物理近似:用 QPSK + RRC 成形代替 O-QPSK DSSS(2 Mchip/s,净 250 kb/s),仅复现频域宽度与突发节律。
#
# 频道映射(记录用途)
# - 2.4 GHz 有 16 个信道:CH=11..26,中心频率 f_c(MHz)=2405 + 5*(CH-11)
# - 本块不直接设绝对射频频点;通过 F_BASE_HZ 与 CH_NUM 计算“基带内中心”,再用 F_SHIFT_HZ 做统一平移。
#
# 旁瓣控制
# - 先成形(RRC,sps≥8,span≥12)后移频;突发边缘余弦渐入/出;可选 Kaiser 低通压带外。
#
# 使用
# 1) 作为 epy_block 接到 Throttle(仿真)或 USRP Sink(外发),采样率用 SAMP_RATE;
# 2) 设 CH_NUM 选择频道;如需把频道中心搬到你的射频中心,设 F_SHIFT_HZ(例:USRP=2.440 GHz,
# 要发 CH15=2425 MHz → F_SHIFT_HZ=2.425e9-2.440e9);或直接调 USRP 载频;
# 3) 想切换频道:把 CH_SWITCH_ENABLE=True(默认 False),频点按 5 MHz 栅格在组间切换。
# =============================================================================
import numpy as np
from gnuradio import gr
# ====================== 固定参数(无需 GUI 配置) ======================
SAMP_RATE = 1e6 # 采样率;低 Fs 下会自动缩窄带宽以保证 sps≥8
BETA = 0.35 # RRC 滚降
TARGET_BW_HZ = 2.0e6 # 目标“~2 MHz”占用(实际≈(1+β)*R_s,自适应)
SPS_TARGET = 8 # 成形过采样下限;≥8 抑制泄露更稳
RRC_SPAN_SYM = 12 # RRC 脉冲跨越符号数
# —— 频道设置 ——
CH_NUM = 15 # 11..26(记录与混频用)
F_BASE_HZ = 0.0 # 基带内“CH11”中心相对 0 Hz 的偏移;通常为 0
F_GRID_HZ = 5.0e6 # 频道间隔(5 MHz)
CH_SWITCH_ENABLE = False # True=按组在频道间切换(默认关闭)
CH_SWITCH_PROB = 0.25 # 每组结束后切换频道的概率
CH_ALLOWED = list(range(11, 27)) # 允许的频道集合(11..26)
# —— 成组短帧时序(单位 ms) ——
GROUP_GAP_MS_RANGE = (20.0, 120.0) # 组与组之间的空闲
FRAMES_PER_GROUP = (3, 8) # 每组帧数(含起止)
FRAME_LEN_MS_RANGE = (0.4, 1.2) # 单帧持续时间
INTER_FRAME_MS_RANGE = (0.2, 2.0) # 帧间隔
EDGE_MS = 0.20 # 每帧渐入/出时长(每侧)
JITTER_CFO_HZ = 0.0 # 可选极小 CFO 抖动(0 关闭)
# —— 统一移频与电平 ——
F_SHIFT_HZ = 0.0 # 把整体搬到目标中心(限制在 ±0.45·Fs)
OUT_LEVEL = 0.5 # 固定线性增益(不做按块 RMS)
# —— 带外抑制 FIR(Kaiser 低通,可选) ——
LPF_ENABLE = True
LPF_ATTEN_DB = 80.0
LPF_PASS_KEEP = 0.90 # 通带截止=0.5*(1+β)*R_s*PASS_KEEP
LPF_TRANS_FRAC = 0.25 # 过渡带宽占目标带宽的比例
SEED = 26
# ====================== 滤波与调制工具 ======================
def rrc_taps(beta=0.35, sps=8, span=12):
N = span * sps
t = np.arange(-N/2, N/2 + 1, dtype=np.float64) / sps
taps = np.zeros_like(t, dtype=np.float64)
for i, x in enumerate(t):
ax = abs(x)
if np.isclose(x, 0.0):
taps[i] = 1.0 - beta + 4*beta/np.pi
elif np.isclose(ax, 1.0/(4*beta)):
taps[i] = (beta/np.sqrt(2.0)) * (
(1 + 2/np.pi) * np.sin(np.pi/(4*beta)) +
(1 - 2/np.pi) * np.cos(np.pi/(4*beta))
)
else:
num = (np.sin(np.pi * x * (1 - beta))
+ 4*beta*x*np.cos(np.pi * x * (1 + beta)))
den = np.pi * x * (1 - (4*beta*x)**2)
taps[i] = num / den
taps /= np.sqrt(np.sum(taps**2) + 1e-15)
return taps
def fir_lpf_kaiser(fs, f_pass, f_trans, atten_db):
dw = 2*np.pi * (f_trans / fs)
if atten_db > 50:
beta = 0.1102*(atten_db - 8.7)
elif atten_db >= 21:
beta = 0.5842*(atten_db - 21)**0.4 + 0.07886*(atten_db - 21)
else:
beta = 0.0
M = int(np.ceil((atten_db - 8.0) / (2.285 * dw))) | 1
n = np.arange(M) - (M-1)/2
h = 2*(f_pass/fs) * np.sinc(2*f_pass*n/fs)
w = np.kaiser(M, beta)
taps = h * w
taps /= np.sum(taps)
return taps.astype(np.float64)
def mod_qpsk_rrc(freq, t, samp_rate, sym_rate, beta, rng, sps, span):
"""
QPSK + RRC:生成与 t 等长的基带段,先成形后移频;相位随 t 连续。
"""
N = len(t)
if N == 0:
return np.zeros(0, np.complex64)
n_sym = int(np.ceil(N / sps)) + span
# 均匀随机符号,Gray 映射等价:±1/±1
bits = rng.integers(0, 2, size=(n_sym, 2), dtype=np.int8)
I = 2*bits[:,0] - 1
Q = 2*bits[:,1] - 1
symbols = (I + 1j*Q) / np.sqrt(2)
up = np.zeros(n_sym * sps, dtype=np.complex128)
up[::sps] = symbols
taps = rrc_taps(beta=beta, sps=sps, span=span)
shaped = np.convolve(up, taps, mode="same")[:N]
return (shaped * np.exp(1j * 2*np.pi * freq * t)).astype(np.complex64)
# ====================== 突发包络工具 ======================
def _add_burst(env, start, end, fs, edge_samp, amp=1.0):
"""在 env 上叠加一个 [start,end) 的突发,边缘余弦渐入/出,峰值 amp。"""
if end <= start:
return
N = len(env)
i0 = int(np.floor(start * fs))
i1 = int(np.ceil(end * fs))
if i1 <= 0 or i0 >= N:
return
i0c = max(0, i0); i1c = min(N, i1)
L = i1c - i0c
if L <= 0:
return
e = int(edge_samp)
if 2*e >= L:
w = 0.5*(1 - np.cos(2*np.pi*np.arange(L)/(L-1 if L>1 else 1)))
else:
w = np.ones(L, dtype=np.float64)
w[:e] = 0.5*(1 - np.cos(np.linspace(0, np.pi, e, endpoint=False)))
w[-e:] = 0.5*(1 - np.cos(np.linspace(np.pi, 0, e, endpoint=False)))
env[i0c:i1c] = np.maximum(env[i0c:i1c], amp*np.clip(w,0,1))
# ====================== epy_block 实现 ======================
class blk(gr.sync_block):
"""
Zigbee_802154_QPSK_Approx
输出:complex64 基带;窄带占用 + 成组短帧;默认不跳频。
"""
def __init__(self):
gr.sync_block.__init__(
self, name="Zigbee_802154_QPSK_Approx",
in_sig=None, out_sig=[np.complex64],
)
self.fs = float(SAMP_RATE)
self.beta= float(BETA)
self.rng = np.random.default_rng(SEED)
self.t = 0.0
# —— 符号率自适应(保证 sps≥SPS_TARGET) ——
Rs_need = TARGET_BW_HZ / (1.0 + self.beta)
Rs_sps = self.fs / float(SPS_TARGET)
self.Rs = min(Rs_need, Rs_sps)
self.sps = max(SPS_TARGET, int(round(self.fs / self.Rs)))
self.Rs = self.fs / self.sps
self.span= int(RRC_SPAN_SYM)
# —— 频道中心(基带内) ——
self.ch = int(CH_NUM)
self.f_base = float(F_BASE_HZ)
self.f_grid = float(F_GRID_HZ)
self.f_center = self._center_of(self.ch) # 基带内中心
self.f_shift = float(np.clip(F_SHIFT_HZ, -0.45*self.fs, 0.45*self.fs))
# —— 事件调度:成组短帧 ——
self.next_group = self._now() + self.rng.uniform(*GROUP_GAP_MS_RANGE)*1e-3
# —— FIR 状态(可选带外抑制) ——
self.lpf_taps = None
self.lpf_state = np.zeros(0, np.complex64)
if LPF_ENABLE:
tgt_bw = (1.0 + self.beta) * self.Rs
f_pass = 0.5 * tgt_bw * float(LPF_PASS_KEEP)
f_trans = max(200.0, float(LPF_TRANS_FRAC) * tgt_bw)
self.lpf_taps = fir_lpf_kaiser(self.fs, f_pass, f_trans, LPF_ATTEN_DB)
self.lpf_state = np.zeros(len(self.lpf_taps)-1, np.complex64)
# —— 固定线性增益(避免块间幅度跳步) ——
self.fixed_gain = float(OUT_LEVEL)
# —— 可选极小 CFO 抖动 ——
self.cfo_rng = self.rng.random() # 初始偏置
self.cfo = float(JITTER_CFO_HZ)
def _now(self):
return self.t
def _center_of(self, ch):
# CH=11..26 映射到基带:F_BASE_HZ + (ch-11)*5 MHz
return self.f_base + (int(ch) - 11) * self.f_grid
def _schedule_group(self, t0, t1, env):
"""在 [t0,t1) 内把所有落入的‘组 + 帧’叠加到 env。"""
Fs = self.fs
edge = int(round(float(EDGE_MS)*1e-3*Fs))
while self.next_group < t1 + 1e-12:
g_start = self.next_group
# 抽一组帧
n_frames = self.rng.integers(FRAMES_PER_GROUP[0], FRAMES_PER_GROUP[1]+1)
t_cur = g_start
for _ in range(int(n_frames)):
dur = self.rng.uniform(*FRAME_LEN_MS_RANGE) * 1e-3
_add_burst(env, t_cur - t0, t_cur + dur - t0, Fs, edge, amp=1.0)
gap = self.rng.uniform(*INTER_FRAME_MS_RANGE) * 1e-3
t_cur += dur + gap
# 下一组起始时间
self.next_group = t_cur + self.rng.uniform(*GROUP_GAP_MS_RANGE) * 1e-3
# 可选:组间切换频道(按概率)
if CH_SWITCH_ENABLE and (self.rng.random() < float(CH_SWITCH_PROB)):
ch_list = list(CH_ALLOWED)
if int(self.ch) in ch_list and len(ch_list) > 1:
ch_list.remove(int(self.ch))
self.ch = int(self.rng.choice(ch_list))
self.f_center = self._center_of(self.ch)
def work(self, input_items, output_items):
out = output_items[0]
N = len(out)
if N == 0:
return 0
Fs = self.fs
t_vec = self.t + np.arange(N)/Fs
# 基底:QPSK+RRC;可叠加极小 CFO 抖动
freq_cfo = self.cfo * (0.5 - (0.5 - self.cfo_rng)) # 常值;默认 0
base = mod_qpsk_rrc(freq_cfo, t_vec, Fs,
sym_rate=self.Rs, beta=self.beta,
rng=self.rng, sps=self.sps, span=self.span)
# 组/帧包络(0..1)
env = np.zeros(N, dtype=np.float64)
self._schedule_group(self.t, self.t + N/Fs, env)
# 施加包络
sig = base * env.astype(np.float64)
# 可选带外抑制 FIR(有状态)
if self.lpf_taps is not None:
buf = np.concatenate([self.lpf_state, sig.astype(np.complex64)])
full = np.convolve(buf, self.lpf_taps, mode='full')
sig = full[len(self.lpf_taps)-1 : len(self.lpf_taps)-1 + N]
self.lpf_state = buf[-(len(self.lpf_taps)-1):]
# 统一混频到“频道中心 + 全局平移”处;跨块相位连续
# 逐样相位增量
dphi = 2*np.pi * (self.f_center + self.f_shift) / Fs
# 用累加器保持相位连续
if not hasattr(self, "mix_phase"):
self.mix_phase = 0.0
ph = self.mix_phase + dphi * np.arange(1, N+1)
self.mix_phase = float(ph[-1] % (2*np.pi))
sig *= np.exp(1j * ph).astype(np.complex64)
# 固定线性增益
sig *= self.fixed_gain
out[:] = sig.astype(np.complex64)
self.t += N / Fs
return N


浙公网安备 33010602011771号