ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MATLAB实现LoRa接收机解调:从SDR基带到Payload解码

MATLAB实现LoRa接收机解调:从SDR基带到Payload解码 简介这是一份MATLAB实现的LoRa接收机解调与解码SDR工程面向通信工程、物联网及软件无线电方向的学习者与开发者。LoRa基于Chirp扩频调制通过线性调频啁啾实现远距离低功耗通信项目完整展示了从射频信号采集、预处理、解调到解码的流程并结合Communications Toolbox与Signal Processing Toolbox完成数字信号处理。压缩包共9个文件以MATLAB脚本.m、示例I/Q波形.wav为主另含PDF报告、Markdown说明与备份文件整体约49.28MB。其中两个主脚本分别处理录制信号与示例信号可直接运行以观察解调效果配套报告和演示文稿还系统梳理了LoRa调制原理、SDR接收链路及工程实现要点。目前已有42人学习对理解LoRa物理层、SDR实现方式及MATLAB在信号处理中的实际应用具有参考价值。1. 为什么要在 MATLAB 里做 LoRa 接收机解调LoRa 接收机最难的地方不是“把无线报文收下来”而是把一组基带 I/Q 数据从同步、Demodulation 一直推到 Decoding最后拿到真正的 payload。用 MATLAB 和一台 RTL-SDR 做这件事的理由很直接LoRa 用的是 Chirp Spread Spectrum符号级的载波同步比 OFDM 宽松核心算法可以用不到一百行脚本复现而且每个中间结果都能画出来看。对正在调协议栈或者做反向分析的人来说这套链路的价值在于你能把标准里含糊的交织、Hamming 编码和 CRC 校验拆到波形级别验证。本文会顺着一个 MATLAB SDR LoRa Receiver 的典型实现顺序把解调和解码做成你改一改参数就能跑起来的代码。2. LoRa 信号结构与 SDR 接收链路选型2.1 CSS 调制在 I/Q 上到底是怎么排布的LoRa 的每一个符号是一个线性调频信号频率在带宽 BW 内从低到高扫一遍。扩频因子 SF 决定每个符号携带多少 bit也决定一个符号有多长时间。符号周期 T_sym 满足T_sym 2^SF / BW这是 LoRa 物理层最要紧的一个公式。接收机采样率 fs 确定后一个符号对应的样本数是N fs * T_sym fs * 2^SF / BWN 就是后面做 dechirp 和 FFT 的长度。下面是 SF 变化时 N 的变化假设 BW125 kHzfs250 kHzSFT_sym (ms)N fs/BW * 2^SF71.02425682.04851294.0961024108.19220481232.7688192从表里能直接看出为什么 SF 越高空中传输时间成倍增加。实际 SDR 接收时我一般把 fs 设成至少 2 倍 BW一是留出抗混叠余量二是让 N 变成整数FFT bin 正好对到符号值。LoRa 调制本身可以理解成对基础 up-chirp 做循环移位。发送符号值 s 时信号不是换一个频率而是把基础 chirp 在时域上向左循环移位 s 个样本。接收端把接收样本乘以基础 chirp 的共轭也就是 dechirp循环移位就变成了一个单音FFT 峰值出现在第 s 个 bin。这一步是后面所有解调代码的数学依据。2.2 为什么 dechirp FFT 是默认解调手法如果直接拿接收波形和模板 chirp 做相关也不是不行但每个符号都要算一次二维相关计算量太大而且对载波频率偏移不友好。dechirp 加 FFT 的做法把匹配滤波转化为一次 FFTbin 位置直接对应符号值频率偏移只表现为峰值在 FFT 谱上平移后续可以用前导码把它估计出来。一条完整的 SDR 接收链路是SDR 下变频出复数基带 I/Q用滤波器把 LoRa 带宽外的噪声和镜像去掉检测前导码估计符号起点利用前导码估计载波频率偏移 CFO 并校正从 payload 起点开始逐符号 dechirp做 FFT 取峰值对符号索引做 Gray 解码然后去交织、Hamming 解码、去白化最后看 CRC。标题里的 Demodulation and Decoding 在这里被拆成了两个独立模块中间只传整数符号索引。这个边界很值得保留调同步和调编解码是两个完全不同的调试界面。2.3 先造一个已知 LoRa 帧再把接收算法跑通写接收机之前我先在 MATLAB 里造一个基带帧这样每一步输出都能和发射端对比。下面这个函数会生成标准的 preamble、down-chirp 同步符、up-chirp 同步符和一组 Gray 编码的 payload 符号。function base lora_base_chirp(SF, BW, fs) % 生成一个基础 up-chirp长度为 N N round(fs * 2^SF / BW); t (0:N-1). / fs; f0 -BW/2; K BW / (N / fs); % 扫频斜率 base exp(1j * 2 * pi * (f0 * t 0.5 * K * t.^2)); endfunction [samples, payloadBits, graySymbols] lora_tx_frame(payloadBits, SF, BW, fs, preambleLen) base lora_base_chirp(SF, BW, fs); N length(base); % 把 bit 向量补成 SF 的整数倍每 SF bit 构成一个符号 nPkt ceil(length(payloadBits) / SF); if length(payloadBits) nPkt * SF payloadBits(end1:nPkt*SF, 1) 0; end bm reshape(payloadBits, SF, []); n sum(bm .* (2.^(SF-1:-1:0)), 2); % 无 toolbox 的 bi2de 替代写法 % LoRa 符号做 Gray 编码g n xor (n 1) graySymbols bitxor(n, bitshift(n, -1)); % 每个 payload 符号是基础 chirp 的循环移位 chirps zeros(N * nPkt, 1); for k 1:nPkt chirps((k-1)*N1 : k*N) circshift(base, -graySymbols(k)); end % 帧结构前导码 down-chirp up-chirp payload preamble repmat(base, preambleLen, 1); down conj(base); samples [preamble; down; base; chirps]; end这段代码里circshift(base, -s)的左移对应了 LoRa 的循环调制接收端 dechirp 后峰值 bin 恰好就是 Gray 符号值 s。n sum(...)这一段是为了不依赖 Communications Toolbox 的bi2de直接把每行 SF 个 bit 按 MSB 权重累加成整数。3. 用 MATLAB 实现 LoRa 解调同步、去啁啾与符号判决3.1 用匹配滤波和前导码对齐符号起点SDR 采样不会自动落在符号边界上所以接收机第一步是找 preamble。LoRa preamble 是连续重复的基础 up-chirp最稳妥的检测方式是把接收信号通过一个匹配滤波器滤波器的系数是基础 chirp 的时反共轭。当整个 chirp 进入滤波器时输出会形成一个峰值峰值的间距正好是一个符号长度。function [graySymbols, startIdx] lora_sync_demod(iq, SF, BW, fs, preambleLen) base lora_base_chirp(SF, BW, fs); N length(base); % 匹配滤波器base chirp 的时反 共轭 b conj(base(end:-1:1)); r filter(b, 1, iq); rMag abs(r); % 找周期性峰峰间距至少 N [~, locs] findpeaks(rMag, MinPeakHeight, 0.6 * max(rMag), ... MinPeakDistance, N); if isempty(locs) error(Preamble not detected); end % 匹配滤波峰值位置是某个 chirp 的最后一个样本 startIdx locs(1) - N 1; if startIdx 1 startIdx 1; end % 跳过 preamble 和两个同步符号down-chirp up-chirp syncLen 2; payloadStart startIdx (preambleLen syncLen) * N; % 从 payload 起点逐符号 dechirp availSym floor((length(iq) - payloadStart 1) / N); graySymbols zeros(availSym, 1); for m 0:availSym-1 block iq(payloadStart m*N : payloadStart (m1)*N - 1); y block .* conj(base); [~, bin] max(abs(fft(y, N))); graySymbols(m1) bin - 1; end end这里有几个参数值得说明。MinPeakHeight我取的是 0.6 倍全局峰值这在信噪比较高时够用如果环境噪声很大应该改成固定绝对门限或者用中位数做自适应门限。MinPeakDistance必须设成至少 N否则同一帧内相邻 preamble 峰会被合并或重复触发导致startIdx偏差。filter的输出位置和输入信号长度相同但初始 N-1 个点是瞬态不能作为稳定峰值。如果前导码很短建议在调用函数前先多采一段空白信号或者用conv(b, iq)后手动做延迟补偿。3.2 逐符号 dechirp 与 FFT 峰值提取核心的下标处理是取一段长度为 N 的样本逐点乘以conj(base)对结果做 N 点 FFT找到峰值位置bin记录 Gray 符号索引bin-1。为什么峰值位置减一因为 MATLAB 索引从 1 开始FFT 的第 1 个 bin 对应频率 0符号值 0 的解调结果会落在那里。符号值 127 对应第 128 个 bin。这个偏移虽然小但如果你直接在后面接 Gray 解码所有符号值都会错 1解码结果会是一堆乱码。fft(y, N)里的 N 其实可以更大。把 N 扩展成L * N比如 4 倍补零 FFT可以让峰值定位更细。补零不会提高真实分辨率但对低信噪比下估峰值有好处。代价是计算量变大实时处理时不如直接用 N 点。3.3 SF/BW/fs 和 FFT 点数怎么搭配在实际 SDR 工程里参数组合不是随便选的。以下是我常用的几个组合场景SFBWfsN说明快速测试7125 kHz250 kHz256采样率最小整数倍对信号有干扰7125 kHz1 MHz1024过采样 8 倍滤波余量大长距离低速1062.5 kHz250 kHz4096高频偏下需要校准低功耗传感器1231.25 kHz250 kHz32768FFT 点数大建议用 GPU 或做分段fs 必须大于 BW否则 LoRa 信号的上边带会被混叠回来。最好把 fs 设成 BW 的整数倍这样 N 是整数。如果 fs 不是整数倍lora_base_chirp里的round会引入一个小的频差长时间累积后会表现为符号间相位旋转最终影响峰值位置。注意 SF 高到 10 以上时N 可能到几千甚至上万逐符号 FFT 在纯 MATLAB 里仍然可以跑但实时接收建议把 FFT 改成fft(y, N)的 N 只保留信号实际带宽内的 bin不必全算。更激进的优化是只做 FFT 的一部分输出点因为 LoRa 调制符号的峰值只会出现在 binshift 后的一个窄范围内。4. 解码链路从符号索引恢复有效载荷4.1 Gray 解映射和字节序处理解调模块拿到的是整数符号索引这些索引在发射端做了 Gray 编码。Gray 编码的优点是相邻符号索引之间只有一位 bit 翻转这样解调器如果判到相邻符号只错一个比特交给 FEC 后更容易纠正。Gray 码的逆变换不能直接用一个bitxor完成因为编码时是g n xor (n 1)解码时要把所有高位的影响逐级迭代回去。function bits gray_decode_symbols(graySymbols, SF) n graySymbols; shift bitshift(n, -1); while any(shift(:)) n bitxor(n, shift); shift bitshift(shift, -1); end bits zeros(length(n) * SF, 1); for k 1:length(n) % 从 bitget 的第 SF 位往第 1 位取得到 MSB-first 的 bit 数组 bits((k-1)*SF1 : k*SF) bitget(n(k), SF:-1:1); end endbitget(n(k), SF:-1:1)返回的是长度为 SF 的逻辑数组第一个值是最高位。这里不要用de2bi(n(k), SF, left-msb)也行但bitget不依赖 Communications Toolbox放在调试验证脚本里更省事。4.2 交织、Hamming 解码和去白化的顺序不能变标准 LoRa 解码链路中Gray 解出来的是交织后的编码比特。完整的顺序是对每个符号的 SF bit 做 Gray 逆变换按 SF 的网格做去交织把每 6 bit 一组做 Hamming (4,6) 解码恢复 4 bit 数据对恢复出的数据做 LoRa 白化解码最后查 CRC。如果只需要把代码跑通可以先跳过交织和 Hamming直接用发射端未加 FEC 的帧做 loopback 验证。但如果你拿到的 IQ 数据来自真实 SX127x 设备上面五步一步都不能省。顺序错了最典型的症状是单看每个符号的 Gray 解码都正确但组合出来的 bytes 完全不对而且错误分布有明显整块性。去交织在 MATLAB 里最常见做法是reshape 转置。LoRa 的交织宽度和 SF 绑定SF7时按 7 列做SF10时按 10 列做。每列字节顺序还要和 bit 顺序对齐建议在代码里保留原始 bit 序只做 reshape不要先把多个符号拼成 int。Hamming 解码则强烈建议做成查表而不是写奇偶校验位判断。因为 LoRa 用的 (4,6) 码有固定 16 种合法码字把 6 bit 输入映射到 4 bit 输出一张表就够运行效率比循环纠错高一个量级。查表法的另一个好处是输入错误图样可以直接列出来方便定位是同步误差还是频率偏移导致的单符号错误。4.3 CRC 校验和失败定位解码的最后一关是 CRC。MATLAB 自带的 Communications Toolbox 提供comm.CRCDetector可以避免自己实现多项式除法。crcDet comm.CRCDetector( ... Polynomial, z^16z^12z^51, ... InitialConditions, 0); % decBits 是包含 CRC 的完整解码输出 [decData, errFlag] crcDet(decBits); if errFlag error(CRC check failed); else payload decData(1:end-16); end这段代码假定发射端在 payload 后追加了 CRC-16-CCITT 校验值。errFlag是逻辑 true/falseMATLAB 里也可以直接放进assert但我习惯用 if 分支因为后续还要打印错误统计。如果 CRC 一直失败不要先怀疑 CRC 多项式先回查解调出来的 Gray 符号索引是否连续。最常见的问题是 CFO 估计不准导致符号值整体偏移一个常数。这个偏移不会影响 Gray 解码的单 bit 错误但会让交织和解码全部错位最终 CRC 必挂。先把解调符号序列和发射端 Gray 符号做一次整数对齐再往下追交织问题是最快的定位路径。5. 用 RTL-SDR 实测 LoRa 报文中心频偏校准与验证把前面几个函数接到 RTL-SDR 上才算是完整的 SDR Implementation。新版本 MATLAB 推荐用sdrrx对象配置一次后可以连续抓帧fs 250e3; rx sdrrx(RTL-SDR, ... RadioName, 0, ... CenterFrequency, 868e6, ... BasebandSampleRate, fs, ... SamplesPerFrame, 2^15, ... OutputDataType, double); [iq, settings] rx();SamplesPerFrame要根据你的 Len 和前导码长度设。2^15 个样本在 fs250 kHz 时是 131 ms足够抓一个 SF7、几十字节 payload 的包。如果抓不到前导码优先检查settings.RadioFrequencies看实际 center frequency 和下发的参数是否一致。实测中最容易踩的坑是中心频率偏移。RTL-SDR 的 TCXO 误差可能在几千赫兹这个偏差会直接变成 dechirp 后 FFT 峰值整体的 bin 偏移。我习惯先用前导码的一个符号粗估 CFO再把整个帧乘一个数字振荡器校正function [iqCorr, cfoHz] correct_cfo_from_preamble(iq, SF, BW, fs) base lora_base_chirp(SF, BW, fs); N length(base); block iq(1:N); y block .* conj(base); [~, bin] max(abs(fft(y, N))); binShift bin - 1; if binShift N / 2 binShift binShift - N; end % 每个 bin 代表 BW / 2^SF Hz 的频偏 cfoHz binShift * BW / 2^SF; t (0:length(iq)-1). / fs; iqCorr iq .* exp(-1j * 2 * pi * cfoHz * t); end这段代码假设第一个符号确实是 preamble 的 up-chirp而且信噪比足够高。如果频率偏移超过半个符号带宽bin 会出现模糊所以先用if binShift N/2做了环绕处理。更稳的做法是连续多看几个 preamble 符号对 bin 改变量取平均值而不是只依赖一个符号。把校正后的iqCorr传给lora_sync_demod再走 Gray 解码和 CRC 验证。实际验证时我一般会在发射端固定发一份已知字节序列而不是随机比特比如0x2B 0x7E 0x88 0x01。这样抓包后直接肉眼对比前四个字节就能知道链路是否已经通不需要等 CRC 统计结果。等这四个字节稳定后再关掉固定序列做随机载荷压力测试。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进