REMI Lab강의 노트

Wireless Embedded Systems E06

실습 A. Python으로 신호 다루기

컴퓨터 안에서 신호는 숫자 배열 하나이다. 표본화율, 길이, 주파수 세 숫자로 그 배열을 만들고, 시간 파형, I/Q 평면, 스펙트럼 세 가지 그림으로 확인한다. 이 실습에서 익힌 방법을 뒤의 모든 실습이 그대로 쓴다.

이 실습의 자리: 시뮬레이션 — 신호를 배열로 만들고 그리는 법을 익히며, 다음 실습 B에서 그 배열의 DFT를 직접 계산한다.

준비

활동은 셋이다. 정현파를 만드는 함수를 쓰고, 표본화율과 주파수를 바꿔 가며 파형을 보고, 복소 신호를 만들어 세 가지 그림으로 본다. 신호는 모두 코드로 만든 것이며 Pluto는 쓰지 않는다.

회색 상자가 셀이다. ▶ 실행이나 Shift+Enter로 실행한다. 셀들은 변수를 함께 쓰므로, 위 셀에서 만든 것을 아래 셀이 쓴다. 그래서 셀을 실행하면 그 위의 셀 가운데 아직 실행하지 않았거나 고친 뒤 다시 실행하지 않은 셀이 먼저 차례로 실행된다. 위 셀에서 오류가 나면 거기서 멈추고, 실행한 셀에 어느 셀을 고쳐야 하는지 적힌다. np(numpy)와 show(그림 도구)는 이미 준비되어 있다. 코드를 고친 뒤 되돌리기를 누르면 교재의 코드로 돌아간다.

함수를 쓰는 활동에는 함수의 이름, 입력, 출력, 계산 순서가 있다. 그 순서대로 쓰고, 바로 아래 확인 셀로 맞는지 본다. 구현할 함수에는 # TODO 줄과 그 아래 한 줄이 미리 들어 있다. 두 줄을 지우고 그 자리에 함수를 쓴다. 쓰기 전에 확인 셀을 실행하면 '할 일' 안내가 나온다.

그림 옆의 기록에 넣기 단추를 누르면 그 그림이 기록에 들어가고, 글 상자에 쓴 설명도 함께 저장된다. 다 마치면 도구 막대의 저장을 누른다.

활동 1. 정현파를 배열로 만들기

연속 신호 x(t)=cos⁡(2πft)x(t)=\cos(2\pi f t)를 컴퓨터에 담으려면 일정한 간격으로 값을 읽어야 한다. 1초에 읽는 횟수가 표본화율 fsf_s이고, nn번째 표본의 시각은 t[n]=n/fst[n]=n/f_s이다(2장 표본화와 aliasing). 그래서 신호 하나는 배열 두 개로 표현된다. 시각을 담은 t와, 그 시각의 값을 담은 x이다.

numpy 배열은 수식을 그대로 옮겨 쓸 수 있다. 배열에 숫자를 곱하거나 np.cos에 넣으면 원소마다 따로 계산된다. 아래 셀을 실행해 확인한다.

n = np.arange(5)             # 0부터 4까지의 정수 배열
print('n       =', n)
print('n / 10  =', n / 10)   # 원소마다 나눈다
print('표본 수 =', n.size)

쓸 함수 make_tone(frequency, sample_rate, duration)

  • 입력: 주파수 frequency(Hz), 표본화율 sample_rate(Hz), 길이 duration(초)
  • 출력: 시각 배열 t(초)와 신호 배열 x. 두 배열의 길이는 같다.

계산 순서

  1. 표본 수를 구한다. duration * sample_rate를 round로 정수로 만든다.
  2. 시각 배열을 만든다. np.arange(표본 수) / sample_rate.
  3. 신호 배열을 만든다. np.cos(2 * np.pi * frequency * t).
  4. return t, x로 둘을 함께 돌려준다.
def make_tone(frequency, sample_rate, duration):
    """cos(2πft)의 표본. (시각 배열, 신호 배열)을 돌려준다."""
    # TODO: 위의 계산 순서대로 make_tone 함수를 구현하세요.
    raise NotImplementedError('make_tone 함수를 구현하세요.')

아래 셀은 50 Hz 정현파를 1 kHz로 0.1초 동안 만든다. 표본은 100개이고 간격은 1 ms이다. 50 Hz의 한 주기는 20 ms, 곧 표본 20개이므로, 값은 1에서 시작해 5번째 표본에서 0, 10번째 표본에서 −1이 되어야 한다.

sample_rate = 1000    # Hz
duration = 0.1        # 초

t, x = make_tone(50, sample_rate, duration)

print('표본 수:', x.size, ' 표본 간격:', t[1] * 1000, 'ms')
print('x[0], x[5], x[10] =', np.round([x[0], x[5], x[10]], 3) + 0.0)

correct = (
    x.size == 100
    and t.size == 100
    and np.isclose(t[1], 0.001)
    and np.allclose([x[0], x[5], x[10]], [1, 0, -1], atol=1e-9)
)
print('→ 맞음' if correct else '→ 다시 본다')

show.time(x, sample_rate, title='50 Hz 정현파 (합성)')

show.time(신호, 표본화율)은 가로축을 시간으로 바꿔 파형을 그린다. 그림에서 한 주기가 20 ms인지 확인한다.

활동 2. 표본을 점으로 보기

앞의 그림은 표본을 선으로 이어 그려서 매끄러운 곡선처럼 보였다. 실제로 배열에 든 것은 표본 간격마다 하나씩 있는 점이다. 한 주기에 들어가는 점의 수는 fs/ff_s/f이다. 주파수를 올리거나 표본화율을 내리면 한 주기를 그리는 점이 줄어든다.

아래 셀은 같은 함수로 만든 신호를 두 번 그린다. 아주 촘촘한 표본으로 만든 선은 연속 신호를 대신하고, 점은 고른 표본화율로 읽은 표본이다. 축은 고정되어 있으므로 손잡이를 움직여도 같은 50 ms 구간을 보게 된다. 주파수와 표본화율을 바꾸며, 한 주기의 점이 몇 개일 때부터 점만으로는 정현파 모양을 알아보기 어려워지는지 본다.

FINE_RATE = 100_000   # Hz, dense grid that stands in for the continuous signal
WINDOW = 0.05         # s, the stretch of time shown


def draw_samples(frequency, sample_rate):
    t_fine, continuous = make_tone(frequency, FINE_RATE, WINDOW)

    # The samples sit on the dense grid every FINE_RATE / sample_rate points.
    step = FINE_RATE // sample_rate
    samples = np.full(continuous.size, np.nan)
    samples[::step] = continuous[::step]

    show.line(
        continuous,
        samples,
        x=t_fine * 1000,
        labels=['연속 신호', '표본'],
        styles=['line', 'points'],
        title=f'{frequency} Hz를 {sample_rate} Hz로 표본화 (합성)',
        xlabel='시간',
        xunit='ms',
        ylabel='x',
        xlim=(0, 50),
        ylim=(-1.2, 1.2),
    )
    print('한 주기의 표본 수:', sample_rate / frequency)


show.control(
    draw_samples,
    frequency=show.slider(50, 400, step=50, value=50, unit='Hz', label='주파수'),
    sample_rate=show.choice([1000, 2000, 4000], value=1000, unit='Hz', label='표본화율'),
)

표본화율 1000 Hz에서 주파수를 400 Hz까지 올리면 점들의 모양은 어떻게 되는가? 표본화율을 4000 Hz로 바꾸면 무엇이 달라지는가?

활동 3. 복소 신호와 세 가지 그림

SDR이 내놓는 표본은 실수가 아니라 복소수 I+jQI+jQ이다(2장 복소수와 I/Q). 가장 단순한 복소 신호는 복소 지수 z(t)=ej2πftz(t)=e^{j2\pi f t}이다. 실수부 I는 코사인, 허수부 Q는 사인이고, 복소 평면에서는 반지름 1인 원 위를 1초에 ff바퀴 돈다. ff가 양수이면 반시계 방향, 음수이면 시계 방향이다.

쓸 함수 make_complex_tone(frequency, sample_rate, duration)

  • 입력: make_tone과 같다. frequency는 음수일 수 있다.
  • 출력: 시각 배열 t(초)와 복소 신호 배열 z

계산 순서

  1. 표본 수와 시각 배열 t는 make_tone과 같은 방법으로 만든다.
  2. 신호 배열을 만든다. np.exp(2j * np.pi * frequency * t). Python에서 2j는 허수 2j2j이다.
  3. return t, z로 돌려준다.
def make_complex_tone(frequency, sample_rate, duration):
    """exp(j2πft)의 표본. (시각 배열, 복소 신호 배열)을 돌려준다."""
    # TODO: 위의 계산 순서대로 make_complex_tone 함수를 구현하세요.
    raise NotImplementedError('make_complex_tone 함수를 구현하세요.')

아래 셀은 50 Hz 복소 지수를 만든다. 모든 표본의 크기는 1이어야 한다. 5번째 표본은 4분의 1바퀴 돈 자리이므로 jj이고, 실수부는 활동 1의 x와 같아야 한다.

t, z = make_complex_tone(50, sample_rate, duration)

print('z[0] =', np.round(z[0], 3), ' z[5] =', np.round(z[5], 3))
print('크기의 최소, 최대:', np.round([np.abs(z).min(), np.abs(z).max()], 6))

correct = (
    np.iscomplexobj(z)
    and np.allclose(np.abs(z), 1)
    and np.isclose(z[5], 1j)
    and np.allclose(z.real, x)
)
print('→ 맞음' if correct else '→ 다시 본다')

같은 신호를 세 가지 방법으로 그린다. 뒤의 실습에서 받은 신호를 볼 때도 이 세 그림을 쓴다.

  • show.time(z, sample_rate): 시간 파형. 복소 신호이면 I와 Q를 따로 그린다.
  • show.iq(z): I/Q 평면. 가로가 I, 세로가 Q이다.
  • show.spectrum(z, sample_rate): 스펙트럼. 어느 주파수에 얼마나 큰 성분이 있는지 dB로 보인다(2장 DTFT와 DFT).

손잡이로 주파수의 부호를 바꾸고, 신호를 복소 지수에서 코사인으로 바꿔 본다. 세 그림 가운데 무엇이 달라지는지 본다.

def draw_three_views(frequency, kind):
    t, z = make_complex_tone(frequency, sample_rate, duration)

    if kind == '코사인':
        signal = z.real
    else:
        signal = z

    show.time(signal, sample_rate, title=f'시간 파형: {kind} {frequency} Hz (합성)', ylim=(-1.2, 1.2))
    show.iq(signal, title='I/Q 평면 (합성)', limit=1.2)
    show.spectrum(signal, sample_rate, title='스펙트럼 (합성)', ylim=(-120, 5))


show.control(
    draw_three_views,
    frequency=show.slider(-200, 200, step=50, value=50, unit='Hz', label='주파수'),
    kind=show.choice(['복소 지수', '코사인'], value='복소 지수', label='신호'),
)

복소 지수의 주파수를 +50 Hz에서 −50 Hz로 바꾸면 세 그림 가운데 무엇이 바뀌는가? 코사인으로 바꾸면 스펙트럼의 봉우리는 몇 개가 되는가?

복소 지수는 스펙트럼에 봉우리가 하나이고, 주파수의 부호에 따라 봉우리가 0 Hz의 오른쪽이나 왼쪽에 선다. 코사인은 12e+j2πft+12e−j2πft\tfrac12 e^{+j2\pi ft}+\tfrac12 e^{-j2\pi ft}이므로 양쪽에 하나씩 두 개가 선다. I만 보아서는 +f+f와 −f-f를 구별할 수 없고, Q까지 함께 받아야 구별된다. 수신기가 I와 Q를 둘 다 내놓는 까닭이다.