18주차에는 QT 클라이언트와 서버 스피커 간의 실시간 음성 통신, 보행자 레이저 트래킹을 위한 기본 코드를 구현했습니다.

그렇다면 개발 간의 문제사항과 개발 내용을 정리해보는 시간을 가지도록 하겠습니다.!!!


AUDIO module

초기 목적은 오디오 라이브러리를 직접 구현해보는 것이었지만 사실상 프로젝트의 핵심 기능이 아닌 부가 기능이라 생각되어 개발 속도를 중점으로 완성했습니다.

전체 구조 개요

실시간 마이크 → 스피커오디오 경로는 다음과 같은 파이프라인으로 구현되어 있습니다.

  • 클라이언트(마이크)

    • Qt 클라이언트의 QAudioSource 등에서 16 kHz, mono, 16-bit(S16_LE) 포맷으로 PCM을 뽑아 TCP로 전송 (5556 또는 테스트용 6000 포트).
  • 라즈베리 파이(스피커 유닛) – Audio_Speaker_Unit

    • main.cpp 안에서 TCP 서버(6000 포트)를 열어 RAW/MP3 스트림을 받음.
    • RAW 모드에서는 받은 PCM을 링 버퍼 AudioRingBuffer 에 넣고,
      별도 재생 스레드 AudioPlayback 이 링 버퍼에서 꺼내 ALSA(libasound)로 스피커 출력.

사용 라이브러리/기술

  • ALSA (libasound)

    • 헤더: #include <alsa/asoundlib.h>
    • 주요 API:
    • snd_pcm_open("default", SND_PCM_STREAM_PLAYBACK, 0)
    • snd_pcm_set_params(..., SND_PCM_FORMAT_S16_LE, ..., 16000, 1, ...)
    • snd_pcm_writei() 로 PCM 프레임을 계속 출력
    • XRUN 시 snd_pcm_prepare() 로 복구
    • 역할: 실제 라즈베리 파이 오디오 디바이스에 PCM 데이터를 쓰는 드라이버 레벨 인터페이스
  • POSIX 소켓 (TCP)

    • 헤더: arpa/inet.h, sys/socket.h, netinet/in.h, unistd.h

      서버(audio_speaker_test): socket → bind → listen → accept → read 오디오 스트림 수신
      클라이언트(send_audio): connect → send 오디오 바이트 전송

  • 멀티스레드 & 동기화

    • audio_ring_buffer.*:
      • AudioRingBuffer 내부에서 생산자/소비자 패턴 구현
      • push() / pop() 에서 버퍼가 비었거나 꽉 찼을 때 condition_variable 로 block/wake
    • audio_playback.*:
      • AudioPlayback 가 별도 스레드(playbackThreadFunc)를 돌리며 링 버퍼를 소비

실시간 음성 스트리밍 경로

  1. 공통 오디오 포맷 정의 (audio_common.h)
AUDIO_SAMPLE_RATE = 16000
AUDIO_CHANNELS = 1
AUDIO_FRAME_BYTES = 2 (Int16 * 1ch)
  1. 링 버퍼 (audio_ring_buffer.h/.cpp)

    	클래스: AudioRingBuffer
    
    	내부: std::vector<char> + std::mutex + std::condition_variable
    
    	Producer(네트워크 수신 스레드):
    	- push(const char* data, std::size_t bytes)
    	- 버퍼가 꽉 차면 cv_not_full_ 대기 → 공간 생기면 다시 push
    	Consumer(Playback 스레드):
    	- pop(char* out, std::size_t max_bytes)
    	- 데이터가 없으면 cv_not_empty_ 대기 → 데이터 들어오면 읽기

    역할: 네트워크 지터/속도 차이와 재생 디바이스의 타이밍 차이를 수십~수백 ms 정도 완충.

  2. 재생 스레드 (audio_playback.h/.cpp) – ALSA 사용

    	클래스: AudioPlayback
    
    	initPcm():
    	- snd_pcm_open("default", SND_PCM_STREAM_PLAYBACK, 0)
    	- snd_pcm_set_params(..., SND_PCM_FORMAT_S16_LE, AUDIO_CHANNELS, AUDIO_SAMPLE_RATE, ...)
    	- snd_pcm_get_params() 로 period/buffer 프레임 수 조회
    
    	start():
    	- ALSA 초기화 뒤, std::thread 로 playbackThreadFunc() 시작
    
    	playbackThreadFunc():
    	- 한 번에 period_frames_ * AUDIO_FRAME_BYTES 만큼 ring.pop() 으로 읽어옴
    	- 읽어온 바이트를 프레임 수로 환산 후, 루프 안에서 snd_pcm_writei() 호출
    	-EPIPE(XRUN) 발생 시 snd_pcm_prepare() 후 재시도

    역할: 링 버퍼에 쌓인 오디오를 끊김 없이 ALSA 디바이스로 밀어 넣는 소비자 스레드

    xrun이 무엇인가?는 해당 블로그에 정리해두도록 하겠습니다. ^^😍

  1. TCP 수신 스레드 (main.cpp) – RAW 모드

    run_tcp_receiver_raw(AudioRingBuffer& ring):
    	- 포트 AUDIO_TEST_PORT(6000) 에 socket/bind/listen
    	- accept() 후 루프에서 read() 로 4096 바이트씩 수신
    	- 수신된 바이트를 그대로 ring.push(buf, bytes) 호출
    
    main():
    	AudioRingBuffer ring(RING_CAPACITY_BYTES) – 약 1초 분량 용량
    	AudioPlayback playback(ring); playback.start();

    이후 run_tcp_receiver_raw(ring) 호출 → 수신과 재생이 동시에 진행되는 스트리밍 구조

결과

클라이언트(마이크 또는 send_audio)가 RAW PCM을 보내면,
“TCP 수신 → 링 버퍼 push → Playback 스레드 pop → ALSA(snd_pcm_writei) → 스피커” 흐름으로 거의 실시간에 가깝게 재생된다.

  “네트워크 → mpg123 → ALSA → 스피커” 직결 구조.

이렇게 qt 클라이언트(윈도우)에서 라즈베리파이 서버 audio 통신이 곧바로 스피커로 재생되도록 하는 시스템을 구축했습니다. 비록 디지털 오디오 잭을 이용해 간단하게 구현했지만 전체적인 시스템을 파악할 수 있는 시간이었습니다.

그래도 직접 마이크에서 스피커로 소리가 흘러나옴을 확인한 순간은 보람을 가득 느낄 수 있었습니다.

STM32 Laser Tracking system

한화비전 카메라와 바로 레이저 트레킹 모듈을 연결하려는 시도를 했지만 보안상의 이슈로 이런 저런 접근을 시도했지만 실패했습니다. 그렇지만 유선으로 서버와 Laser 모듈을 연결하는 것은 하드웨어 구성상 어색해 별도의 ESP8266을 활용하여 wifi무선 통신을 구현하였습니다 .

하드웨어 연결 외에는 AT 펌웨어로 초기 세팅을 진행하고 나니 어려움은 없었습니다.

ESP8266 <-> Raspi wifi connect 에 관련된 내용은 해당 블로그에 정리해놓았습니다. !

그렇다면 18주차에 마주친 임베디드 개발 issue 위주로 기록을 해보도록 하겠습니다.

1. MCU / 전체 개요

  • MCU: STM32F401RE (Nucleo-F401RE 보드 기준)
  • 주요 기능
    • TIM1 / TIM2 PWM 으로 2축 서보 모터 제어 (레이저 방향 제어용)
    • USART1 + DMA 로 ESP-8266(WiFi 모듈) 과 통신 (AT 명령, TCP 브리지)
    • USART2PC 디버그/수동 제어 시리얼 제공
    • PC13 버튼 / PA5 LED 로 모드 전환(MANUAL/AUTO) 표시

2. 핀 매핑 정리

2.1 서보 모터 (레이저 방향 제어)

  • Y축 (서보 1)

    • STM32 핀: PA8
    • 기능: TIM1_CH1 (PWM 출력)
    • Cube 설정: S_TIM1_CH1 → PWM Generation CH1
    • 코드 참조
      • PWM 초기화: MX_TIM1_Init() (main.c)
      • 핀 설정: HAL_TIM_MspPostInit()GPIO_AF1_TIM1
  • X축 (서보 2)

    • STM32 핀: PA0 (WKUP)
    • 기능: TIM2_CH1
    • Cube 설정: S_TIM2_CH1_ETR → PWM Generation CH1
    • 코드 참조
      • PWM 초기화: MX_TIM2_Init() (main.c)
      • 핀 설정: HAL_TIM_MspPostInit()GPIO_AF1_TIM2

2.2 ESP-8266 (WiFi 모듈) – USART1

  • UART 신호선

    • ESP TXSTM32 RX
      • STM32 핀: PA10
      • 기능: USART1_RX
      • 설정: GPIO_AF7_USART1
    • ESP RXSTM32 TX
      • STM32 핀: PA9
      • 기능: USART1_TX
      • 설정: GPIO_AF7_USART1
  • DMA (ESP → STM32, RX 전용)

    • DMA2_Stream2, Channel 4 (USART1_RX)
    • 방향: PERIPH_TO_MEMORY
    • 모드: NORMAL
    • PC 쪽 로그는 wifi_rx_pending 플래그를 통해 main() 루프에서 처리.

2.3 PC 디버그 시리얼 – USART2

  • STM32 핀
    • PA2USART2_TX (ST-LINK VCP TX → PC RX)
    • PA3USART2_RX (ST-LINK VCP RX → PC TX, USART_RX_Pin 매크로)
  • 용도
    • 부트 메시지, 모드 변경 로그, 서보 제어 명령 입력, ESP AT 명령 프록시.

2.4 버튼 / LED / 디버그

  • B1 사용자 버튼 (모드 토글)

    • 핀: PC13
    • 기능: GPIO_EXTI13 (외부 인터럽트, Falling edge)
    • 역할: 누를 때마다 MODE_MANUALMODE_AUTO 토글 (button_auto_pending 플래그).
  • LD2 LED / 레이저 테스트 핀

    • 핀: PA5
    • 기능: GPIO Output push-pull
    • 역할:
      • 펌웨어 관점: AUTO 모드 여부를 표시 (Led_SetAutoMode()).
      • 하드웨어 구성: 실제 레이저 모듈의 전원/Enable 신호를 LD2 와 같이 묶어서 연결해 두었기 때문에,
        PA5 를 토글하면 보드 LED 와 레이저가 함께 ON/OFF 되도록 테스트 중.

3. 클럭 설정 정리

PWM 타이머 설정 및 펄스 폭 단위

MX_TIM1_Init(), MX_TIM2_Init() (둘 다 main.c) 에서 설정:

  • TIM1 (PA8, 서보 Y축), TIM2 (PA0, 서보 X축)

    • Prescaler = 83

    • Period(초기) = 19999

    • 결과:

      • 타이머 클럭 = 84 MHz / (83+1) = 1 MHz
      • 1 카운트 = 1 µs
      • 20,000 카운트 = 20 ms (50 Hz, 서보 표준)
    • PWM 출력 모드: TIM_OCMODE_PWM1, OCPolarity = HIGH (서보 일반 규격에 맞게 수정됨).

    • 따라서 두 타이머 모두 1 tick = 1 µs,
      Servo_Set*Us(1500) 이라면 1500 µs 펄스가 됨.


System 전체 Flow

전체 Flow (Closed-Loop Visual Servoing 중심)

  1. CCTV 영상 (RTSP)
  2. Wisenet AI (사람 detect → bbox center)
  3. Laser Dot Detect (opencv) [ camera or raspi ]
  4. Error = dot_pos - bbox_center
  5. PID → Pan/Tilt servo command
  6. (Optional) Ground Plane Distance 계산 → 레이저 세기 조절 or 로그

카메라 레이저 탐지

위는 실제 카메라 영상이고 아래는 빨간 색영역을 기준으로 필터링을 진행한 이미지입니다.

레이저 포인터가 잡히기는 하나 다른 noise(책, 공구)등이 함께 탐지되었습니다.

이를 해결하기 위해서 HSV 이미지 변환 후 밝기 값을 중점으로 임계값을 더욱 세분화 하였고 한화 비전 카메라의 기능을 이용하여 노출 값을 낮춰 카메라에서 빛을 받아들이는 양을 줄였습니다.

카메라 설정
카메라 설정

결과

이는 실제 영상을 어둡게 만드는 단점이 있지만 초기 레이저 포인팅 캘리브레이션 작업은 당분간 해당 설정으로 진행할 예정입니다.

전체 흐름을 따라 서모 모터 제어 그리고 카메라 영상 인식 및 레이저 탐지까지는 구현하는 과정 중 Issue가 발생했습니다.

ISSUE

모터 노이즈 문제

1. 증상

  • 서보가 입력값을 바꾸지 않아도 지지직 소음을 내며 떨림.
  • Servo tester(하드웨어)에서는 정상 동작하고, STM32 PWM 출력에서만 이상 증상 발생.

2. 원인 분석

  • TIM PWM 채널 설정에서 출력 극성이 반대로 설정되어 있었음.

  • 문제 설정:

    sConfigOC.OCPolarity = TIM_OCPOLARITY_LOW;
    sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
  • 위 상태에서는 PWM이 반전되어, 예를 들어 1500us 명령이 실제로는
    거의 전체 주기 High에 가까운 형태로 전달됨.

  • 그 결과 서보 내부 제어 기준에서 비정상 펄스로 해석되어
    엔드스톱 방향으로 과도하게 힘을 주면서 소음/발열/진동이 유발됨.

3. 수정 내용

  • TIM1/TIM2 PWM 채널의 극성을 서보 일반 규격에 맞게 정방향으로 조정:

    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
  • 50Hz(20ms) 기준에서 1000~2000us 펄스 폭이 정상 의미로 전달되도록 정렬.

해당 이슈는 PWM 신호 전압을 상승 시키기 위해서 PNP 트랜지스터를 시도해보며 극성을 바꾼 뒤 설정을 그대로 둔 채 개발을 진행하다 보니 발생하게 되었습니다.

처음에는 AI에게 물어봐도 전압이나 펄스 주기가 맞지 않아 소음이 발생한다는 등의 답변만 들을 수 있었습니다. 오실로스코프로와 같은 계측기가 없는 상황에서 해결하기란 정말 막막했습니다. 그래서 이것 저것 선과 모터를 바꿔끼워가며 테스트를 해보았지만 여전히 해결되지 않았습니다. 하지만 불과 며칠전까지는 진동이 발생하지 않았는데 무엇이 문제일까 고민하며 이틀 전 커밋의 코드와 현재 코드를 비교해본 결과 극성이 변경되어 있음을 확인할 수 있었습니다.

전자 전기 관련 이론적 기반이 여실히 부족함을 느낄 수 있는 경험이었습니다 .
하지만 문제가 해결된 지금 19주차는 다시 앞으로 달려나가 보도록 하겠습니다 !!!!

파이팅

그러면 나의 코딩 토템과 함께 18주차 기록을 마무리하겠습니다.
(피규어 겟또!!)

후훗 ~ 준비됐지요 등?

profile
세상의 어려운 문제를 해결하자

3개의 댓글

comment-user-thumbnail
2026년 3월 3일

쿄쿄쿄

1개의 답글