프롤로그
저번글에서는 단순히 신호만 캡처해서, 이렇게 신호 모양이 생겼구나, OFDM을 이용하는 구나 정도만 확인했었다.
2026.05.04 - [네트워크] - [WiFi] USRP B210과 MATLAB으로 WiFi 신호 캡처해서 분석하기
[WiFi] USRP B210과 MATLAB으로 WiFi 신호 캡처해서 분석하기
프롤로그저번에 WIFI PHY계층에 대한 이론적인 내용에 대해서 배웠다.2026.04.29 - [네트워크] - [WiFi] IEEE 802.11 WIFI의 PHY 계층에 대해서 알아보자 [WiFi] IEEE 802.11 WIFI의 PHY 계층에 대해서 알아보자프롤로
taesan-smj.tistory.com
이번에는 STF, LTF, SIG 개념을 이전에
2026.04.29 - [네트워크] - [WiFi] IEEE 802.11 WIFI의 PHY 계층에 대해서 알아보자
[WiFi] IEEE 802.11 WIFI의 PHY 계층에 대해서 알아보자
프롤로그2026.03.18 - [네트워크] - [WiFi] IEEE 802.11 계층과 프레임에 대해 알아보자 (Wireshark로 뜯어보는 802.11 Frame의 모든 것) [WiFi] IEEE 802.11 계층과 프레임에 대해 알아보자 (Wireshark로 뜯어보는 802.11 F
taesan-smj.tistory.com
여기 글에서 배웠었으니까, 신호를 하나하나 STF, LTF, SIG대로 파싱하고,
최종적으로 해당 신호에 어떤 데이터가 들어있는지를 개념을 다시 정리하면서 확인해보고자 한다.
신호 자체를 직접적으로 복조해서 할 수도 있기야 하겠지만, 코드가 너무 길어질 거 같고, 나도 그정도 코드 실력은 없으니까 매트랩 WLAN Toolbox에 있는 함수를 그대로 이용해서 진행하려고 한다.
준비물

USRP B210

VERT2450(2.4Ghz, 5GHz 듀얼밴드를 캡처할 수 있다)
를 준비하고
MATLAB 애드온으로

이렇게 준비하였다.
그리고 저번 글에서 작성했던, 매트랩 코드에 덧붙여서 작성할 거기 때문에 코드까지 저번글을 참고해서 준비하도록 하자.
2026.05.04 - [네트워크] - [WiFi] USRP B210과 MATLAB으로 WiFi 신호 캡처해서 분석하기
[WiFi] USRP B210과 MATLAB으로 WiFi 신호 캡처해서 분석하기
프롤로그저번에 WIFI PHY계층에 대한 이론적인 내용에 대해서 배웠다.2026.04.29 - [네트워크] - [WiFi] IEEE 802.11 WIFI의 PHY 계층에 대해서 알아보자 [WiFi] IEEE 802.11 WIFI의 PHY 계층에 대해서 알아보자프롤로
taesan-smj.tistory.com
실험 과정과 흐름
전체적인 실험 흐름은 다음과 같다.

- USRP를 이용해 실제 Wi-Fi 신호를 수신한다.
- 수신된 IQ 샘플에서 패킷의 시작 지점(Packet Detection)을 탐지한다.
- STF(Short Training Field)를 이용해 대략적인 시간 동기화와 주파수 오프셋(Coarse CFO)을 추정한다.
- LTF(Long Training Field)를 이용해 채널 추정(Channel Estimation)과 정밀 동기화(Fine Synchronization)를 수행한다.
- 이후 SIG 필드를 복조하여 패킷의 전송 정보와 프레임 타입을 확인한다.
- 최종적으로 Beacon Frame 내부 정보를 직접 파싱하여 SSID, 채널, Capability 등의 데이터를 추출한다.
즉, 단순히 “Wi-Fi 신호를 잡는다” 수준이 아니라, 실제 PHY 계층의 구조를 따라가며 패킷을 단계적으로 복원하는 과정이라고 볼 수 있다.
이 과정을 통해서 우리는

이렇게 비콘 프레임을 한 번 직접 신호를 파싱해서 데이터를 추출을 해보자.
이 과정을 통해 우리는 다음과 같은 내용을 직접 확인할 수 있다.
- Wi-Fi 패킷이 실제 RF 신호에서는 어떻게 보이는지
- 패킷 시작 지점을 어떤 방식으로 탐지하는지
- STF/LTF가 왜 필요한지
- CFO(Carrier Frequency Offset)가 실제로 얼마나 발생하는지
- 채널 추정이 어떤 역할을 하는지
- PHY 계층의 SIG 정보로 어떤 프레임인지 판별하는 과정
- Beacon Frame 내부에 어떤 정보가 들어있는지
특히 일반적인 Wireshark 분석과 달리, 이번 실험은 “복조 이후의 패킷”이 아니라 “복조 이전의 실제 무선 신호”부터 직접 분석한다는 점에서 의미가 있다.
전체적인 흐름은 결국엔

STF -> LTF -> SIG를 추출하는 거긴 하겠는데, 조금 더 디테일있게 주파수 동기화 , 시간동기화, 그리고 SIG뒤에 어떤 값이 오는지 타입도 확인해보는 식으로 디테일이 추가한다.
결국 이번 실험의 핵심은 단순히 Beacon Frame을 읽는 것이 아니라, “실제 RF 신호에서 PHY 계층을 직접 따라가며 Wi-Fi 패킷이 복원되는 과정” 그 자체를 분석하는 것에 가깝다.
초기 코드 전체 프레임 작성
if exist('rx','var')
release(rx);
clear rx;
end
clear;
clc;
%% 기기 연결 확인
radios = findsdru();
%% 초기 변수 설정
%=====채널=====
frequencyBand = 5;
channelNumber = 36;
%=====실험 변수====
threshold = 0.7; %탐지 임계값 설정
maxLoops = 20; %실험 반복 횟수
captureTime = milliseconds(100); % 1000 = 1초
justOne = false; % 동작 모드 변경
%====패킷 수신 관련====
sampleRate = 20; %샘플레이트 (채널 밴드)
decimationFactor = 1; %다운샘플링 비율
gain = 20; %RF신호를 얼마나 증폭해서 받을 것인가 (dB)
skip = 2000;
%% 초기 initial
radio = radios(1);
fc = wlanChannelFrequency(channelNumber, frequencyBand);
sampleRate = sampleRate * 1e6;
cbw = sampleRateToCBW(sampleRate);
captureTimeSec = seconds(captureTime);
samplePerFrame = round(sampleRate * captureTimeSec); %한 번에 몇 개 샘플을 가져오는가(묶음 크기)
maxBufferLen = round(sampleRate * 0.2); % 200ms
if strcmp(radio.Status, "Success")
rx = comm.SDRuReceiver( ...
Platform = radio.Platform, ... %장비명(b210)
SerialNum = radio.SerialNum, ... %시리얼번호
CenterFrequency = fc, ... %중앙 주파수
MasterClockRate = sampleRate * decimationFactor, ... %내부 샘플 생성 속도
DecimationFactor = decimationFactor, ... %디지털 다운샘플링
Gain = gain, ... %아날로그 증폭
SamplesPerFrame = samplePerFrame, ... %버퍼링
OutputDataType = 'double' ... %매트랩전달형식
);
else
error('Device connection failed. Please check the hardware and try again.');
end
%% 그래프 + 실험 환경 설정
%그래프+실험 세팅
fig = figure(1);
maxHistory = 2; % 최근 패킷 몇 개 볼지
displayLimit = 8000; % 시각화할 신호의 최대 길이
stackView = true; %true시 상하 , false시 좌우 배치
plotHandles = gobjects(maxHistory,1);
specHandles = gobjects(maxHistory,1);
for k = 1:maxHistory
if stackView
% --- Time domain ---
subplot(maxHistory*2, 1, (k-1)*2 + 1);
plotHandles(k) = plot(NaN, NaN);
grid on;
ylabel('Amp');
title(sprintf('Packet #%d - Time Domain', k));
% --- Spectrogram ---
subplot(maxHistory*2, 1, (k-1)*2 + 2);
specHandles(k) = imagesc(NaN);
axis xy;
ylabel('Freq');
title(sprintf('Packet #%d - Spectrogram', k));
else
% 기존 구조 유지
subplot(maxHistory,2,(k-1)*2+1);
plotHandles(k) = plot(NaN,NaN);
grid on;
subplot(maxHistory,2,(k-1)*2+2);
specHandles(k) = imagesc(NaN);
axis xy;
end
end
packetHistory = cell(maxHistory,1);
% 데이터 수신 및 패킷 처리
packetCount = 0;
stopAll = false;
buffer = [];
%% 신호 수신 및 실시간 감지 루프
disp("Scanning for Wi-Fi signals...");
for i = 1:maxLoops
% 데이터 수신
if stopAll, break; end
[data, len] = rx();
if len <=0, continue; end
% buffer에 누적
buffer = [buffer; data];
if length(buffer) > maxBufferLen
buffer = buffer(end-maxBufferLen+1:end);
end
% 신호 수신 및 처리 루프
disp("Receiving signals...");
idx = wlanPacketDetect(buffer, cbw, 0, threshold); %coarse candidate.
%================
%여기서부터 코드 작성%
%================
%-------------
packetCount = packetCount + 1;
% ==== 패킷 저장 (히스토리 유지) ====
packetHistory = [{rxPacket}; packetHistory(1:end-1)];
% ==== 시각화 ====
set(0,'CurrentFigure',fig);
for k = 1:maxHistory
if isempty(packetHistory{k})
continue;
end
pkt = packetHistory{k};
actualLimit = min(length(pkt), displayLimit);
t = (0:actualLimit-1) / sampleRate * 1e6;
sig = abs(pkt(1:actualLimit));
% --- Time domain ---
subplot(maxHistory*2, 1, (k-1)*2 + 1);
set(plotHandles(k), 'XData', t, 'YData', sig);
title(sprintf('Packet #%d (latest-%d)', packetCount-k+1, k-1));
xlim([0 t(end)]);
ylim([0 max(sig)*1.2 + 1e-6]);
% --- Spectrogram ---
subplot(maxHistory*2, 1, (k-1)*2 + 2);
[S,F,T] = spectrogram(pkt(1:actualLimit),256,200,256,sampleRate,'centered');
imagesc(T*1e6, F/1e6, 20*log10(abs(S)));
axis xy;
xlabel('Time (μs)');
ylabel('Freq (MHz)');
end
drawnow limitrate;
% ================================
if justOne, stopAll = true; break; end
if idx + skip < size(buffer,1)
buffer = buffer(idx + skip:end,:);
else
buffer = [];
end
idx = wlanPacketDetect(buffer, sampleRateToCBW(sampleRate), 0, threshold);
end
disp("탐색 종료");
release(rx); % 하드웨어 해제
%% function
function cbw = sampleRateToCBW(sampleRate)
% Hz 기준
if sampleRate == 5e6
cbw = 'CBW5';
elseif sampleRate == 10e6
cbw = 'CBW10';
elseif sampleRate == 20e6
cbw = 'CBW20';
elseif sampleRate == 40e6
cbw = 'CBW40';
elseif sampleRate == 80e6
cbw = 'CBW80';
elseif sampleRate == 160e6
cbw = 'CBW160';
elseif sampleRate == 320e6
cbw = 'CBW320';
else
error('지원하지 않는 sampleRate입니다');
end
end
function [cpLen, symLen] = extractLLTFLengths(sampleRate)
cpDuration = 1.6e-6; % 1.6 us
symDuration = 3.2e-6; % 3.2 us
cpLen = round(cpDuration * sampleRate);
symLen = round(symDuration * sampleRate);
end
function [newBuffer, nextIdx] = advanceBuffer(buffer, processedEnd, cbw, threshold, sampleRate)
% overlap으로 패킷이 겹쳐 있거나 바로 뒤에 붙어있을 경우를 대비 (20us 정도)
overlap = round(20e-6 * sampleRate);
% 다음 시작점 계산
nextStart = processedEnd - overlap;
if nextStart < 1, nextStart = 1; end
if nextStart < size(buffer, 1)
newBuffer = buffer(nextStart:end, :);
% 밀어낸 버퍼에서 바로 다음 패킷 탐지
nextIdx = wlanPacketDetect(newBuffer, cbw, 0, threshold);
else
newBuffer = [];
nextIdx = [];
end
end
이전 글 코드에서 살짝 수정은 된 전체코드이다.
전체적으로 USRP 연결을 하고, 그래프 뽑기. 신호 받을 준비까지는 다 준비가 되었다.
여기서 이번 글은 중간에
idx = wlanPacketDetect(buffer, cbw, 0, threshold);
부터 시작을 한다.
패킷 시작 지점 탐지
여기서부터는 실제로 수신된 신호에서는 Wi-Fi 패킷이 시작되는 위치를 찾아야한다.
idx = wlanPacketDetect(buffer, cbw, 0, threshold);
wlanPacketDetect() 함수는 수신된 IQ 샘플에서 WLAN 패킷의 시작 지점을 탐지하는 함수이다.
즉, buffer 안에는 계속해서 수신된 복소수 IQ 샘플들이 쌓이고 있고, 이 함수는 그 안에서 Wi-Fi 패킷으로 보이는 지점을 찾아 idx로 반환한다.
패킷이 탐지되지 않으면 idx는 비어 있게 된다. 따라서 이 경우에는 분석할 패킷이 없다고 판단하고 다음 루프로 넘어간다.
코드상으로는 저렇게 돌아가는데 실질적으로 탐지되는 원리는 다음과 같다.
코드상으로는 위와 같이 간단하게 처리되지만, 실제 탐지 원리는 PHY 계층의 L-STF 구조와 관련이 있다.
L-STF(Legacy Short Training Field)는 총 8 μs 길이를 가지며, 0.8 μs 길이의 짧은 심볼이 10번 반복되는 구조이다.
이 반복 구조 덕분에 수신기는 자기상관(auto-correlation)을 이용해 “비슷한 패턴이 반복되는 구간”을 찾을 수 있다.
즉, 잡음처럼 불규칙한 신호가 아니라 일정한 주기로 반복되는 신호가 나타나면, 이를 Wi-Fi 패킷의 시작 후보로 판단할 수 있다.
| 항목 | 의미 |
| L-STF | Wi-Fi 패킷의 시작 부분에 위치한 필드 |
| 길이 | 8 μs |
| 구조 | 0.8 μs 짧은 심볼 × 10회 반복 |
| 역할 | 패킷 탐지, AGC 안정화, coarse timing, coarse CFO 추정 |
| 탐지 원리 | 반복 패턴을 이용한 자기상관 기반 탐지 |
따라서 wlanPacketDetect()는 단순히 신호 세기가 커지는 지점을 찾는 것이 아니라, Wi-Fi preamble의 반복 구조를 이용해 패킷 시작 지점을 추정한다고 볼 수 있다.
이렇게 패킷이 탐지하게 되면,
%% 패킷 추출
extractLen = 50000;
if idx(1) + extractLen > size(buffer, 1), break; end
rxPacket = buffer(idx : idx + extractLen - 1);
이후에는 탐지된 위치를 기준으로 실제 패킷 신호를 잘라내는 과정을 수행한다.
아 , 물론 여기가 아직까지는 패킷은 아니다. 패킷 후보인셈이지
idx는 wlanPacketDetect()가 찾아낸 패킷 시작 위치이다. 즉, buffer 안에서 “여기부터 Wi-Fi 패킷이 시작된다”라고 판단한 지점이라고 볼 수 있다.
따라서 위 코드에서는 해당 위치부터 extractLen 길이만큼 신호를 잘라내어 rxPacket에 저장한다.
결과적으로 rxPacket 안에는 다음과 같은 PHY 계층 구조가 포함된다.
- STF
- LTF
- SIG
- DATA 영역
여기서 extractLen은 “얼마나 긴 구간을 패킷으로 간주할 것인가”를 의미한다.
값이 너무 짧으면 뒤쪽의 SIG나 DATA 영역이 잘려나갈 수 있고, 반대로 너무 길면 다음 패킷이나 불필요한 잡음까지 함께 포함될 수 있다.
이번 실험에서는 충분한 길이의 패킷을 안정적으로 확보하기 위해 비교적 큰 값인 50000 샘플을 사용하였다.
만약 현재 버퍼 안에 패킷 전체가 아직 충분히 들어오지 않은 상태라면, 잘못된 메모리 접근이 발생할 수 있기 때문에 해당 루프를 종료하고 다음 수신 데이터를 기다리게 된다.
즉, 이 단계는 단순히 “패킷을 자른다”라기보다는, “탐지된 Wi-Fi 패킷 구간을 PHY 분석 가능한 형태로 추출하는 과정” 이라고 볼 수 있다.
LLTF 기반 정밀 시간 동기화 (Fine Timing Synchronization)
이렇게 패킷 후보를 추출했다고 해서, 바로 정확한 PHY 분석이 가능한 것은 아니다.
현재 rxPacket 안에는 실제 Wi-Fi 패킷이 들어있기는 하지만, 패킷 시작 위치가 샘플 단위로 완벽하게 맞아 있는 상태는 아니다.
즉, coarse packet detection만 완료된 상태라고 볼 수 있다.
따라서 이후에는 보다 정확한 심볼 경계를 찾기 위한 정밀 시간 동기화(Fine Timing Synchronization)를 수행해야 한다.
코드에서는 다음 부분이 해당 과정이다.
%% LLTF 기반 정밀 동기화 (Fine Timing) - 시간 동기화
[cpLen, symLen] = extractLLTFLengths(sampleRate);
cfg = wlanNonHTConfig(ChannelBandwidth=cbw);
% lltf = wlanLLTF(cfg);
% lltfRef = lltf(cpLen+1 : cpLen+symLen); %cp 이후
fineOffset = wlanSymbolTimingEstimate(rxPacket, cbw);
if fineOffset >= 1 && fineOffset < length(rxPacket)
rxPacket = rxPacket(fineOffset:end);
else
% 타이밍 추정 실패 시: 현재 idx에서 조금만 전진해서 다시 찾기
fprintf("Timing Estimate Failed. Advancing buffer...\n");
[buffer, idx] = advanceBuffer(buffer, idx + skip, cbw, threshold, sampleRate);
continue;
end
여기서, 핵심코드는
fineOffset = wlanSymbolTimingEstimate(rxPacket, cbw);
이다.
wlanSymbolTimingEstimate() 함수는 LLTF(Long Legacy Training Field)를 기반으로 보다 정확한 심볼 시작 위치를 추정하는 함수이다.
앞에서 사용했던 wlanPacketDetect()는 L-STF의 반복 구조를 이용해 “대략적인 패킷 시작 위치”를 찾는 역할이었다.
반면 이번 단계에서는 LLTF의 known sequence를 이용해 실제 OFDM 심볼 경계를 더욱 정밀하게 맞춘다.
| 단계 | 역할 |
| STF 기반 탐지 | coarse timing |
| LLTF 기반 탐지 | fine timing |
왜 이런 과정이 필요할까?
OFDM에서는 FFT를 수행할 때 심볼 경계가 정확하게 맞아야 한다.
만약 FFT window 위치가 조금만 틀어져도 다음과 같은 문제가 발생할 수 있다.
- 부반송파(Subcarrier) 간 간섭 증가
- 위상 왜곡
- 채널 추정 오류
- 복조 성능 저하
- SIG 해석 실패
특히 Wi-Fi처럼 다중 경로(Multipath)가 존재하는 환경에서는 coarse timing만으로는 정확한 심볼 위치를 맞추기 어렵다.
따라서 LLTF를 이용해 실제 OFDM symbol boundary를 다시 정렬하게 된다.
정상적으로 timing offset이 추정되면 다음 코드가 수행된다.
rxPacket = rxPacket(fineOffset:end);
즉, 추정된 시작 위치를 기준으로 패킷을 다시 잘라내어, PHY 계층 분석에 적합한 형태로 재정렬하는 것이다.
결과적으로 이 과정을 거치면 이후의:
- CFO 보정
- 채널 추정
- SIG 복조
- 데이터 복조
과정의 안정성이 훨씬 높아진다.
반대로 timing estimation이 실패하는 경우도 존재한다.
예를 들어:
- 잡음이 너무 강한 경우
- 패킷이 중간에 잘린 경우
- STF는 탐지했지만 실제 WLAN 패킷이 아닌 경우
- multipath 영향이 매우 큰 경우
에는 올바른 심볼 경계를 찾지 못할 수 있다. 이 경우 코드에서는 현재 위치를 조금 넘긴 뒤 다시 packet detection을 수행한다.
[buffer, idx] = advanceBuffer(buffer, idx + skip, cbw, threshold, sampleRate);
이것으로 우리는 시간적으로 타이밍을 전부 맞춰서 계산이 끝났다.
CFO - 주파수 동기화
앞에서는 wlanSymbolTimingEstimate()를 이용해 시간 축에서의 위치를 맞췄다.
즉, “OFDM 심볼이 어디서 시작되는가?”를 정렬한 것이다.
하지만 시간 동기화가 끝났다고 해서 바로 복조가 가능한 것은 아니다.
무선 신호에서는 송신기와 수신기의 기준 주파수가 완벽히 일치하지 않기 때문에, 주파수 축에서도 오차가 발생한다.
이 오차를 CFO(Carrier Frequency Offset) 라고 한다.
예를 들어 AP가 5.180 GHz에서 신호를 보냈다고 하더라도, USRP가 수신할 때는 하드웨어 발진기 오차나 주변 환경의 영향으로 인해 약간 틀어진 주파수로 신호를 보게 된다.
이 CFO가 보정되지 않으면 OFDM 부반송파 간 직교성이 깨지고, 이후 LTF 채널 추정이나 SIG 복조 과정에서 오류가 발생할 수 있다.
그래서 다음 단계에서는 주파수 동기화를 수행한다.
%% CFO 보정 (주파수 동기화)
cfoCoarse = wlanCoarseCFOEstimate(rxPacket, cbw); % Coarse : STF 기반
rxPacket = frequencyOffset(rxPacket, sampleRate, -cfoCoarse);
cfoFine = wlanFineCFOEstimate(rxPacket, cbw); % Fine : LTF 기반
rxPacket = frequencyOffset(rxPacket, sampleRate, -cfoFine);
% 전체 오차 합산
totalCFO = cfoCoarse + cfoFine;
fprintf("CFO Details -> Coarse: %.2f Hz| Fine: %.2f Hz | Total: %.2f Hz\n", ...
cfoCoarse, cfoFine, totalCFO);
여기서
cfoCoarse = wlanCoarseCFOEstimate(rxPacket, cbw);
먼저 보면,
wlanCoarseCFOEstimate()는 STF의 반복 구조를 이용해 대략적인 주파수 오차를 추정한다.
앞에서 L-STF는 0.8 μs 길이의 짧은 심볼이 반복되는 구조라고 했다. 이 반복되는 심볼 사이의 위상 변화량을 비교하면, 수신 신호가 주파수 축에서 얼마나 밀려 있는지 추정할 수 있다.
즉, STF는 패킷 시작 지점 탐지뿐만 아니라 coarse CFO 추정에도 사용된다.
추정된 CFO 값은 Hz 단위로 반환된다. 예를 들어 Coarse: -24346.62 Hz 이렇게 값이 나오면
이 값은 수신 신호가 약 -24.3 kHz 정도 주파수 오차를 가지고 있다는 의미이다.
그 다음, 추정된 오차만큼 신호를 반대 방향으로 보정한다.
rxPacket = frequencyOffset(rxPacket, sampleRate, -cfoCoarse);
여기서 중요한 점은 -cfoCoarse를 넣는다는 것이다.
CFO가 -24346 Hz로 추정되었다면, 신호가 그만큼 한쪽 방향으로 밀려 있다는 뜻이다.
따라서 이를 보정하려면 반대 방향인 +24346 Hz만큼 이동시켜야 한다.
즉, frequencyOffset() 함수는 수신 신호의 주파수 오차를 제거하기 위해 사용된다.
하지만 STF 기반 CFO 추정만으로는 충분하지 않을 수 있다.
STF는 패킷 탐지와 대략적인 동기화를 위한 필드이기 때문에, 정확한 보정에는 한계가 있다.
그래서 한 번 더 LTF를 이용해 잔여 주파수 오차를 추정한다.
cfoFine = wlanFineCFOEstimate(rxPacket, cbw);
wlanFineCFOEstimate()는 LTF를 기반으로 더 정밀한 CFO를 추정한다.
즉, 흐름은 다음과 같다.
| 단계 | 사용 | 필드역할 |
| Coarse CFO | STF | 큰 주파수 오차를 대략적으로 보정 |
| Fine CFO | LTF | 남아 있는 작은 주파수 오차를 정밀하게 보정 |
Fine CFO까지 추정한 뒤에는 마찬가지로 반대 방향으로 보정한다.
마지막으로 coarse CFO와 fine CFO를 더해서 전체 CFO를 계산한다.
fprintf("CFO Details -> Coarse: %.2f Hz| Fine: %.2f Hz | Total: %.2f Hz\n", ...
cfoCoarse, cfoFine, totalCFO);
이렇게 하면
필드 인덱스 재설정 및 채널 추정 (CSI)
앞 단계에서는:
- 시간 동기화(Time Synchronization)
- 주파수 동기화(Frequency Synchronization)
를 수행하였다.
즉, 이제는 수신 신호가 PHY 계층 분석이 가능한 수준까지 어느 정도 정렬된 상태라고 볼 수 있다.
다음 단계에서는 실제로 LLTF(Long Legacy Training Field)를 이용하여 채널 추정(Channel Estimation)을 수행한다.
코드는 다음과 같다.
%% 필드 인덱스 재설정 및 채널 추정 (CSI)
ind = wlanFieldIndices(cfg); % L-STF/L-LTF/L-SIG/Data 시작 위치 인덱스
if ind.LSIG(2) > length(rxPacket)
continue;
end
% 채널 추정치 추출
ltfField = rxPacket(ind.LLTF(1):ind.LLTF(2));
demodLTF = wlanLLTFDemodulate(ltfField, cbw);
chanEst = wlanLLTFChannelEstimate(demodLTF, cbw);
ind값을 봐보자.

LSTF, LLTF, LSIG, NonHTData의 시작위치와 끝 위치를 계산한다.
즉, 현재 패킷 안에서:
- L-STF
- L-LTF
- L-SIG
- DATA
가 각각 어디에 위치하는지를 알려주는 역할이다.
이후 다음 코드로 LLTF 부분만 잘라낸다.
ltfField = rxPacket(ind.LLTF(1):ind.LLTF(2));
즉, PHY 프레임 전체 중에서 LLTF 구간만 추출하는 것이다.
왜 LLTF를 사용하는 걸까?
L-LTF는 송신기와 수신기가 모두 알고 있는 known sequence를 사용한다.
즉, 수신기는 원래 어떤 신호가 와야 하는지 알고 있다.
따라서 실제로 수신된 L-LTF와 원래 알고 있는 기준 L-LTF를 비교하면, 무선 채널이 신호를 어떻게 왜곡시켰는지 계산할 수 있다.
이 과정이 바로 Channel Estimation이다.
수신된 L-LTF = 원래 L-LTF × 무선 채널 영향
따라서 수신된 값과 원래 값을 비교하면, 무선 채널의 영향을 추정할 수 있다.
그러고 나서 LLTF 복조를 수행한다.
demodLTF = wlanLLTFDemodulate(ltfField, cbw);
여기서 LTF은 시간영역에서
1.6 μs CP + 3.2 μs OFDM Symbol + 3.2 μs OFDM Symbol
구조를 가진다.
그래서 여기서는:
- CP 제거
- FFT 수행
- 부반송파(Subcarrier) 분리
등이 내부적으로 수행된다.
즉, 시간 영역(Time Domain)에 있던 LLTF를 주파수 영역(Frequency Domain)의 OFDM 심볼로 변환하는 과정이다.
demonLTF값을 보면

52*2 같은 결과값이 있다.
| 값 | 의미 |
| 52 | 사용되는 OFDM subcarrier 개수 |
| 2 | 두 개의 LLTF 심볼 |
| complex | IQ 성분을 가진 복소수 데이터 |
20MHz일 때 OFDM 서브캐리어수는 52개라서 52가 만들어진 거고, symbol값이 2개라서 52*2가 만들어진거다.
따라서 52 × 2라는 것은 “52개의 부반송파에 대해 2개의 LLTF 심볼을 복조했다” 라는 의미가 된다.
복소수인 것은 IQ...라서..
암튼 이후
chanEst = wlanLLTFChannelEstimate(demodLTF, cbw);
ㅇㅣ걸로 채널 추정값을 계산한다. 개념적으로는
채널 응답 = 수신된 L-LTF / 원래 알고 있는 L-LTF계산이다.
즉, 채널이 각 부반송파를 얼마나 감쇠시켰는지, 그리고 얼마나 위상 회전을 발생시켰는지를 추정한다.
다만 실제로는 demodLTF에 두 개의 L-LTF 심볼이 들어 있기 때문에, 각 심볼에서 채널 응답을 계산한 뒤 이를 평균 또는 결합하여 더 안정적인 채널 추정값을 만든다.
즉, 단순히 한 번 측정한 값이 아니라, 반복된 두 개의 L-LTF 심볼을 이용해 잡음 영향을 줄인 CSI를 계산하는 것이다.
결과적으로 chanEst 안에는 각 subcarrier별 CSI(Channel State Information) 값이 저장된다.
L-SIG 복원 및 프레임 포맷 판별
앞 단계에서는 L-LTF를 이용해 채널 추정값인 chanEst를 구했다.
이제 이 채널 추정값을 이용해 L-SIG를 복원한다.
L-SIG는 PHY 계층에서 매우 중요한 필드이다.
이 필드에는 이후 데이터 영역을 어떻게 해석해야 하는지에 대한 정보가 들어 있다.
대표적으로 다음 정보가 포함된다.
| 항목 | 의미 |
| Rate | 전송률 |
| Length | PSDU 길이 |
| Parity | 오류 검사용 비트 |
| Tail | 디코더 종료용 비트 |
%% L-SIG 복원 및 포멧 판별
try
lsig = rxPacket(ind.LSIG(1):ind.LSIG(2));
[lsigBits, fail] = wlanLSIGRecover(lsig, chanEst, 0.1, cbw);
if fail
fprintf("L-SIG Recover Failed. Advancing buffer...\n");
[buffer, idx] = advanceBuffer(buffer, idx + skip, cbw, threshold, sampleRate);
continue;
end
%% Format Detect용 구간 생성
numExtraSymbols = 3;
ofdmSymbolLen = round(4e-6 * sampleRate); % Non-HT OFDM symbol = 4us
formatEnd = ind.LSIG(2) + numExtraSymbols * ofdmSymbolLen;
if formatEnd > length(rxPacket)
fprintf("Format Detected Failed. Advancing buffer...\n");
[buffer, idx] = advanceBuffer(buffer, idx + skip, cbw, threshold, sampleRate);
continue;
end
fmtDetect = rxPacket(ind.LSIG(1) : formatEnd);
format = wlanFormatDetect(fmtDetect, chanEst, 0.2, cbw);
fprintf("Detected Format: %s\n", string(format));
먼저 L-SIG구간을 뽑아온다. 그 다음에
이후 wlanLSIGRecover()를 이용해 L-SIG 비트를 복원한다.
[lsigBits, fail] = wlanLSIGRecover(lsig, chanEst, 0.1, cbw);
여기서 중요한 점은 chanEst가 함께 들어간다는 것이다.
앞에서 추정한 채널 정보를 이용해, 왜곡된 L-SIG 신호를 보정하면서 복원하는 구조이다.
만약 복원에 실패하면 다음 패킷을 찾도록 버퍼를 이동시킨다.
if fail
fprintf("L-SIG Recover Failed. Advancing buffer...\n");
[buffer, idx] = advanceBuffer(buffer, idx + skip, cbw, threshold, sampleRate);
continue;
end
즉, 여기서부터는 단순히 신호 모양을 보는 것이 아니라, 실제 PHY 헤더 정보를 비트 단위로 복원하는 단계라고 볼 수 있다.
L-SIG를 복원한 뒤에는 이 패킷이 어떤 PHY 포맷인지 판별한다.
numExtraSymbols = 3;
ofdmSymbolLen = round(4e-6 * sampleRate); % Non-HT OFDM symbol = 4us
formatEnd = ind.LSIG(2) + numExtraSymbols * ofdmSymbolLen;
여기서는 L-SIG 이후의 OFDM 심볼 몇 개를 추가로 포함하여 포맷 판별용 구간을 만든다.
왜냐하면 L-SIG만으로는 이후 패킷이 정확히 Non-HT인지, HT-Mixed인지, VHT인지 등을 완전히 판별하기 어렵기 때문이다.
그래서 L-SIG 뒤쪽의 일부 심볼까지 함께 보고 포맷을 판별한다.
fmtDetect = rxPacket(ind.LSIG(1) : formatEnd);
format = wlanFormatDetect(fmtDetect, chanEst, 0.2, cbw);
fprintf("Detected Format: %s\n", string(format));
wlanFormatDetect()는 해당 구간을 분석해서 패킷의 PHY 포맷을 판별한다.
상황에 따라 다음과 같은 포맷이 탐지될 수도 있다.
- Non-HT
- HT-Mixed
- VHT
- HE-SU
- EHT
이번 코드에서는 우선 Non-HT 패킷만 실제 MAC 디코딩까지 진행하고, 나머지 포맷은 건너뛰도록 작성하였다.
Non-HT 패킷 디코딩
포맷이 Non-HT로 판별되면, 이제 실제 데이터 영역을 복원한다.
%% 포맷 판별 및 MAC 디코딩
% 기본 전진 거리는 skip으로 설정 (실패 대비)
shift = skip;
switch format
case "Non-HT"
% L-SIG 기반 PSDU 길이 설정
psduLen = double(bit2int(lsigBits(6:17), 12, 0));
cfg.PSDULength = psduLen;
% 데이터 필드 인덱스 계산
indData = wlanFieldIndices(cfg, 'NonHT-Data');
% 버퍼에 데이터 끝까지 들어있는지 확인
if indData(2) <= length(rxPacket)
rxData = rxPacket(indData(1):indData(2));
% 데이터 복조 (MPDU 비트 추출)
bits = wlanNonHTDataRecover(rxData, chanEst, 0.2, cfg);
% MAC 계층 디코딩
[cfgMAC, ~, decodeStatus] = wlanMPDUDecode(bits, cfg, 'SuppressWarnings', true);
if ~decodeStatus
% 성공 시 전진 거리를 패킷 끝으로 업데이트
shift = indData(2);
% 결과 출력
if strcmp(cfgMAC.FrameType, 'Beacon')
fprintf("<strong>[FOUND] SSID: %s | BSSID: %s</strong>\n", ...
string(cfgMAC.ManagementConfig.SSID), string(cfgMAC.Address3));
else
% 비콘이 아닌 다른 프레임 타입 확인용
fprintf("Frame Type: %s (Detected)\n", string(cfgMAC.FrameType));
end
end
end
otherwise
% HT-Mixed, VHT 등은 일단 skip
fprintf("Detected Format: %s (Skipping...)\n", string(format));
shift = skip;
end
% [통합 지점] advanceBuffer 하나로 버퍼 밀기 + 다음 패킷 찾기 완료
[buffer, idx] = advanceBuffer(buffer, idx + shift, cbw, threshold, sampleRate);
먼저
먼저 L-SIG에서 PSDU 길이를 가져온다.
psduLen = double(bit2int(lsigBits(6:17), 12, 0));
cfg.PSDULength = psduLen;
L-SIG의 6번부터 17번 비트는 Length 필드에 해당한다.
즉, 이후 데이터 영역에 몇 바이트의 PSDU가 들어 있는지를 알려준다.
이 값을 cfg.PSDULength에 설정해야 MATLAB이 데이터 영역의 길이를 올바르게 계산할 수 있다.
그 다음 데이터 필드의 위치를 계산한다.
indData = wlanFieldIndices(cfg, 'NonHT-Data');
이제 실제 데이터 영역만 잘라낸다.
rxData = rxPacket(indData(1):indData(2));
그리고 wlanNonHTDataRecover()를 이용해 Non-HT 데이터 영역을 복조한다.
bits = wlanNonHTDataRecover(rxData, chanEst, 0.2, cfg);
이 과정에서는 내부적으로 다음 작업이 수행된다.
- OFDM demodulation
- Channel equalization
- Pilot correction
- Deinterleaving
- Viterbi decoding
- Scrambling 해제
- PSDU 비트 복원
즉, 여기서부터는 RF 신호가 실제 MAC 계층 비트열로 변환되는 단계이다.
복원된 비트열은 아직 단순한 PSDU 비트이다.
따라서 이것을 802.11 MAC 프레임 구조에 맞게 다시 해석해야 한다.
[cfgMAC, ~, decodeStatus] = wlanMPDUDecode(bits, cfg, 'SuppressWarnings', true);
wlanMPDUDecode()는 복원된 비트열을 MAC 프레임으로 디코딩한다.
성공하면 cfgMAC 안에 다음과 같은 MAC 계층 정보가 들어간다.
| 항목 | 의미 |
| FrameType | Beacon, Data, ACK 등 프레임 종류 |
| Address1 | 수신 주소 |
| Address2 | 송신 주소 |
| Address3 | BSSID 등 |
| ManagementConfig | Beacon/Probe 등 관리 프레임 정보 |
이후 코드에서는 디코딩된 프레임이 Beacon인지 확인한다.
if strcmp(cfgMAC.FrameType, 'Beacon')
fprintf("[FOUND] SSID: %s | BSSID: %s\n", ...
string(cfgMAC.ManagementConfig.SSID), string(cfgMAC.Address3));
else
fprintf("Frame Type: %s (Detected)\n", string(cfgMAC.FrameType));
end
수행결과
이것으로 실행시키다 보면

이렇게 값이 잡힌다.
아직 완벽한 코드는 아니라서, 비콘이 잡히다 말락 하는데, 원인은 좀 더 찾아봐야하나, 이론적으로는 코드는 잘 짜였다고 생각한다.
에필로그
원래 이 부분은 지난주에 작성했는데, 너무 바빠서 급하게 쓰느라 대충 휘갈겼다.(95% AI작성..)
다만 코드는 직접 하나하나 비교하면서 짰고, 이론적으로도 정리가 다 된 거 글만 정리시킨것이다...
그람 이만..
'네트워크' 카테고리의 다른 글
| [WiFi] USRP B210과 MATLAB으로 WiFi 신호 캡처해서 분석하기 (2) | 2026.05.04 |
|---|---|
| [WiFi] IEEE 802.11 WIFI의 PHY 계층에 대해서 알아보자 (0) | 2026.04.29 |
| [WiFi] WIFI 7 공유기가 자신의 존재를 알리는 방법(2/2) - Beacon Frame의 MLE + Wireshark로 분석하기 (0) | 2026.04.24 |
| [WiFi] WIFI 7 공유기가 자신의 존재를 알리는 방법(1/2) - Beacon Frame의 RNR (0) | 2026.04.24 |
| [WiFi] 802.11be의 EMLSR 모드에 대해서 알아보자 - 동작과정/표준/관련연구/MATLAB 실습 (0) | 2026.04.19 |