REMI Lab강의 노트

Wireless Embedded Systems E16

실습 6. 실제 신호에서 주파수 오차 재기: LTE와 BLE

알려진 LTE 동기 신호(PSS)와의 상관으로 내 수신기의 주파수 오차를 찾고, 반송파 주파수에 대한 상대 오차를 ppm으로 잰다.

이 실습의 자리: Pluto 실습: 실제 신호 — 실습 5에서 찾은 LTE 반송파를 기준으로 수신기의 주파수 오차를 잰다. BLE는 수업 시연으로 본다.

준비

상관을 구하는 함수를 쓰고 PSS의 반복을 찾는다. 이어서 위상차를 Hz로 바꾸는 함수를 쓴다. 그림과 표 옆의 기록에 넣기 단추로 결과를 모으고, 글 상자에 설명을 쓴 뒤 도구 막대의 저장을 누른다.

  • RX에 안테나를 달고 수신만 한다. 강의실 송신 Pluto는 필요 없으며 내 TX도 멈추고 감쇠를 최대로 둔다.
  • 실습 5에서 확인한 LTE 하향 반송파의 정확한 중심을 쓴다. 근처 주파수에 맞추면 중심과 LO의 차이를 발진기 오차로 잘못 잰다. 기본값은 합성 예제용이며 실제 중심·이득·수신 상태는 실습실에서 확인한다.
  • ?device=fake에서는 도우미가 만든 합성 LTE를 쓴다. 제목에 합성을 붙인다. 합성 모델에는 다중 경로와 표본 시계의 속도 차이가 없다.
  • 배경은 3장 표본화와 주파수 축, 4장 주파수 변환, 수신기의 DC 오프셋이다.
from sdrlab import pluto, show, dsp, ltelab
import numpy as np

LTE_CENTER = 1842.5e6  # Hz. 실습실에서 확인한 정확한 LTE 중심으로 바꾼다.
sdr = await pluto.connect()
await sdr.setup('lte-pss', mode='receive', rx_lo=LTE_CENTER)

활동 1. PSS의 상관 봉우리와 주파수 오차

알려진 신호와 비교하면 내 발진기의 오차도 알 수 있을까? PSS(primary synchronization signal)는 LTE 수신기가 시각과 주파수를 맞추려고 찾는 알려진 62개 값의 수열이다. 이 실습에서 받는 LTE 신호는 PSS를 5 ms마다 보낸다.

도우미는 알려진 수열을 수신 표본과 비교할 시간 파형 reference로 바꿔 준다. 수열 후보와 주파수를 바꾸는 일도 도우미가 맡는다. 학생은 reference를 수신 배열 x의 각 위치에 놓고 상관 크기를 구한다.

수업 시간에 LTE 실측 화면을 함께 본다. 연구실의 HURA EdgeRF 장비가 954.3 MHz LTE 하향 신호에서 동기를 찾고 셀의 기본 정보를 읽는 화면이다. 이 실습은 그 첫 단계인 동기 신호(PSS)를 찾아 주파수 오차를 읽는다.

쓸 함수 correlate(x, reference)

  • 입력: 길이 N의 수신 복소 배열 x, 길이 M의 기준 복소 배열 reference. N은 M 이상이다.
  • 출력: 길이 N−M+1의 실수 배열. 위치 ll의 값은 ∣∑n=0M−1x[l+n]p∗[n]∣|\sum_{n=0}^{M-1}x[l+n]p^*[n]|이다.

계산 순서

  1. np.asarray로 두 입력을 배열로 받는다.
  2. np.correlate(x, reference, mode='valid')로 기준이 완전히 들어가는 위치마다 합을 구한다. 이 함수가 두 번째 입력에 켤레를 취하므로 reference에 따로 켤레를 취하지 않는다.
  3. np.abs로 각 합의 크기를 돌려준다.
def correlate(x, reference):
    """수신 복소 배열과 기준 복소 배열을 받아 valid 상관 크기 배열을 돌려준다."""
    # TODO: 위의 계산 순서대로 correlate 함수를 구현하세요.
    raise NotImplementedError('correlate 함수를 구현하세요.')

아래 확인은 복소 기준 파형을 영 표본 사이에 넣는다. 직접 합한 값과 같으면 맞게 쓴 것이다.

test_reference = np.array([1 + 1j, 2 - 1j, -1 + 2j])
test_signal = np.r_[0j, 0j, test_reference, 0j]
expected_correlation = []

for k in range(len(test_signal) - len(test_reference) + 1):
    segment = test_signal[k:k + len(test_reference)]
    correlation = abs(np.sum(segment * np.conj(test_reference)))
    expected_correlation.append(correlation)

expected_correlation = np.array(expected_correlation)
measured_correlation = correlate(test_signal, test_reference)
correct = (
    measured_correlation.shape == expected_correlation.shape
    and np.allclose(measured_correlation, expected_correlation)
)
print(
    '상관 크기:',
    np.round(measured_correlation, 2),
    '→ 맞음' if correct else '→ 다시 본다',
)

도우미는 내 함수를 써서 시간과 주파수를 함께 찾는다. 주파수가 어긋나면 상관을 더하는 동안 위상이 돌아 합의 크기가 줄기 때문이다. 탐색 범위 끝에 봉우리가 있거나 반복을 확인하지 못하면 값을 내지 않는다.

먼저 상관 봉우리의 반복을 보고, 그다음 각 봉우리에서 잰 Δf\Delta f의 부호와 퍼짐을 본다. 합성 기본값에서는 봉우리 간격이 5 ms이고 평균 오차는 약 −31.4 kHz, 상대 오차는 약 +17.04 ppm이다. LTE_CENTER에 확인한 정확한 값을 넣고 받기를 누른다.

async def draw_pss():
    global pss_capture
    pss_capture = await ltelab.observe(sdr, LTE_CENTER, correlate)


await show.control(draw_pss, capture=True)

기지국 중심에서 수신 LO를 빼면 기저대역 주파수가 된다. 기지국이 정확한 기준이라는 근사에서 내 LO가 높으면 Δf\Delta f는 음수이고, 수신기의 상대 오차는 ε=−Δf/fLO\varepsilon=-\Delta f/f_{\rm LO}이다. ppm(parts per million)은 이 비율에 10610^6을 곱한 단위이다.

표준편차와 평균의 표준오차는 반복값의 흔들림을 나타낸다. 중심 선택, 기지국 자체 오차, 다중 경로의 편향은 포함하지 않는다. 일치도가 낮으면 중심과 수신 상태를 확인한다.

Δf\Delta f와 수신기 ppm의 부호가 반대인 이유는 무엇인가? 반복값이 좁게 모여도 남을 수 있는 오차를 설명한다.

활동 2. 위상차를 Hz로 바꾸기

거친 주파수 가설 사이에 남은 오차는 어떻게 읽을까? 주파수 가설 fhf_h를 적용한 기준을 php_h라 하자. PSS의 알려진 위상을 없앤 값 z[n]=x[k+n]ph∗[n]z[n]=x[k+n]p_h^*[n]에서 남은 오차가 위상을 돌린다. 같은 평탄 채널이라는 근사에서 기준 파형의 두 절반 사이, M표본 떨어진 쌍을 비교하면

ϕ=arg⁡∑n=0M−1z[n+M]z∗[n],Δt=M/fs,δf=ϕ2πΔt.\phi=\arg\sum_{n=0}^{M-1}z[n+M]z^*[n],\qquad \Delta t=M/f_s,\qquad \delta f=\frac{\phi}{2\pi\Delta t}.

최종 오차는 Δf=fh+δf\Delta f=f_h+\delta f이다. 두 절반의 합을 따로 비교하지 않고 일정한 간격의 표본 쌍을 쓰는 것은 PSS의 크기가 시간에 따라 달라 두 합의 평균 시각이 다르기 때문이다. np.angle의 범위 밖으로 위상이 돌면 접히므로 ∣δf∣<1/(2Δt)|\delta f|<1/(2\Delta t)인 가까운 가설을 먼저 찾아야 한다. 다중 경로로 채널 위상이 남으면 이 식에도 편향이 생긴다.

쓸 함수 phase_to_hz(phase_difference, time_interval)

  • 입력: 위상차 phase_difference(rad, 실수 또는 실수 배열), 표본 쌍의 시간 간격 time_interval(s, 양의 실수 또는 phase_difference와 같은 모양의 양의 실수 배열).
  • 출력: 남은 주파수 오차(Hz). 배열 입력이면 같은 모양의 배열이다.

계산 순서

  1. np.asarray(phase_difference)로 위상차를 배열로 받는다.
  2. 2 * np.pi로 나누어 각을 회전 수로 바꾼다.
  3. 시간 time_interval로 나누어 초당 회전 수인 Hz를 돌려준다. 부호를 그대로 둔다.
def phase_to_hz(phase_difference, time_interval):
    """위상차(rad)와 시간 간격(s)을 받아 주파수 오차(Hz)를 돌려준다."""
    # TODO: 위의 계산 순서대로 phase_to_hz 함수를 구현하세요.
    raise NotImplementedError('phase_to_hz 함수를 구현하세요.')

아래 확인은 부호가 다른 주파수와 시간 간격을 쓴다. 각각 −1400, 4200, −7300 Hz가 복원되어야 한다. 이어서 앞서 받은 PSS를 내 함수로 다시 계산한다. 새로 받지 않으므로 관찰 그림과 같은 오차가 나와야 한다.

expected_frequencies = np.array([-1400., 4200., -7300.])  # Hz
time_interval = np.array([1 / 30e3, 1 / 30e3, 1 / 60e3])  # s
phase_difference = np.angle(np.exp(2j * np.pi * expected_frequencies * time_interval))
converted_frequencies = phase_to_hz(phase_difference, time_interval)

correct = np.allclose(converted_frequencies, expected_frequencies, atol=1e-6, rtol=0)
print(
    '복원 주파수 (Hz):',
    np.round(converted_frequencies, 1),
    '→ 맞음' if correct else '→ 다시 본다',
)

converted_pss = ltelab.compare_conversion(pss_capture, phase_to_hz)

시간 간격이 같을 때 위상차의 크기와 부호는 주파수에서 각각 무엇을 뜻하는가? 거친 주파수 탐색 없이 위상차만 쓰면 어떤 문제가 생기는가?

다른 대역의 정확한 LTE 중심도 확인했다면 같은 활동을 그 중심에서 반복해 볼 수 있다. 공통 클럭의 상대 오차는 반송파 주파수에 비례해 Hz 오차를 만들므로 ppm을 비교한다. 중심을 일정한 Hz만큼 잘못 맞춘 경우에는 이 비례 관계가 깨진다. 중심 선택과 채널의 편향은 별도로 확인한다.

수업 시연. BLE

BLE 광고 패킷은 수업 시간에 시연으로 본다. 앞의 LTE 실측 화면을 함께 보는 것처럼, BLE의 짧은 버스트와 주파수 오차 분포를 관찰한다. LTE로 구한 수신기 오차를 빼고 송신기 쪽 오차의 분포를 본다. 약한 신호나 충돌로 놓친 패킷은 포함되지 않는다. 기기 주소·광고 내용·복조 비트·BLE 원본 I/Q는 표시하거나 저장하지 않는다. 오차 분포만으로 기기를 식별하지 않는다.