메뉴 전체보기

회원메뉴

StackFM > 게시판

본문 바로가기

쇼핑몰 검색

NO. 3153
제목 : StackFM
2026-09-02 11:12

소개: StackFM


소개: StackFM


소개: StackFM


소개: StackFM


여러분, 안녕하세요. 다시 찾아주셔서 반갑습니다.

여기는 StackFM입니다. M5Stack AtomS3, MAX98357A I2S 오디오 앰프, 스피커를 중심으로 만든 소형 Wi-Fi 라디오입니다.

이 프로젝트의 목적은 작은 AtomS3를 제대로 된 탁상용 라디오로 만드는 것이었습니다. 인터넷 라디오 스트림에 연결하고, 자체 스피커로 재생하며, 간단한 물리적 인터페이스도 갖추는 것이 목표였습니다.

AtomS3의 128 × 128 디스플레이에 모든 조작부를 넣으려 하지 않고, 라디오용 사용자 지정 웹페이지를 만들었습니다. 같은 Wi-Fi 네트워크에 연결된 휴대폰이나 컴퓨터에서 웹페이지를 열 수 있습니다. 여기서 스트림 URL을 추가하고, 방송국을 저장하고, 듣고 싶은 방송을 선택하고, 재생을 제어하며, 볼륨을 조절할 수 있습니다.

중요한 점은 휴대폰이 무선 컨트롤러로만 사용된다는 것입니다. 휴대폰에서 오디오를 재생하는 것이 아닙니다. 방송국을 선택하면 AtomS3가 스트림에 직접 연결하고, 오디오를 MAX98357A 앰프로 보내며, 앰프가 스피커를 구동합니다.

실제로는 Wi-Fi를 통해 방송을 수신하지만, 기기가 오래된 FM 라디오처럼 느껴지기를 원했습니다. 그래서 라디오 디스플레이와 브라우저 컨트롤러 모두에 레트로 스타일의 인터페이스를 적용했습니다.

이 글에서는 이 프로젝트의 전체 제작 과정을 다룹니다. 그럼 바로 시작해 보겠습니다!




STEP 1. 준비물


필요한 부품

  1. 내장 128 × 128 디스플레이가 있는 M5Stack AtomS3
  2. MAX98357A I2S 디지털 오디오 앰프
  3. 앰프와 호환되는 외부 스피커
  4. 사용자 지정 StackFM PCB
  5. IP5306 전원 관리 IC
  6. 10 µF 커패시터, SMD 1206 패키지
  7. 10 kΩ 저항, SMD 0805 패키지
  8. 1 µH SMD 인덕터
  9. 0805 표시 LED
  10. USB Type-C 포트
  11. 푸시 스위치
  12. 3D 프린팅 StackFM 케이스 및 인클로저 부품
  13. 점퍼 와이어 또는 짧은 연결 전선




STEP 2. ATOMS3 및 오디오 설정


ATOMS3 및 오디오 설정


ATOMS3 및 오디오 설정


M5Stack AtomS3는 StackFM의 두뇌입니다. Wi-Fi에 연결하고, 웹페이지를 호스팅하고, 명령을 수신하고, 오디오 스트림을 열고, 디스플레이를 업데이트하며, 디지털 오디오를 앰프로 보냅니다.

오디오는 I2S를 사용해 전송되며, I2S에는 세 가지 주요 신호가 있습니다.

  1. BCLK — 오디오 비트 클록
  2. LRC/WS — 워드 선택
  3. DIN — 디지털 오디오 데이터

MAX98357A는 이 신호를 수신해 디지털 오디오를 변환하고 증폭한 다음 스피커를 구동합니다.




STEP 3. MAX98357A 및 스피커


MAX98357A 및 스피커


MAX98357A 및 스피커


MAX98357A는 ESP32-S3에서 I2S 오디오를 직접 수신하므로 별도의 DAC와 앰프가 필요하지 않습니다.

스피커는 SPK+와 SPK- 사이에 연결합니다. 이 모듈은 브리지 출력 방식을 사용하므로 스피커 단자 어느 쪽도 GND에 연결해서는 안 됩니다.

테스트하는 동안 I2S 전선을 짧게 유지했고, AtomS3와 앰프가 동일한 GND를 공유하도록 했습니다.




STEP 4. 설계


설계


설계


설계


설계


설계


Stack FM을 설계할 때 일반적인 전자기기 인클로저보다는 작은 탁상용 라디오처럼 보이게 만들고 싶었습니다.

모든 부품이 안에 제대로 들어가도록 본체는 작고 각진 상자 형태로 유지했으며, 전면은 프로젝트의 주요 시각적 면으로 설계했습니다.

전면 패널에는 돌출된 STACK FM 로고가 위에 있는 검은색 페이스플레이트를 사용했습니다. 로고는 파란색/시안색으로 인쇄해 검은색 배경에서 선명하게 돋보이도록 했습니다. 외부 본체와 버튼에도 같은 파란색을 사용해 기기 전체에 깔끔한 투톤 느낌을 주었습니다.

전면 패널 하단에는 원형 버튼 세 개를 추가했습니다. 이 버튼 덕분에 기기가 웹 앱으로만 조작되는 장치가 아니라 제대로 된 라디오처럼 느껴집니다.

인클로저의 한쪽 면에는 스피커 그릴 부분도 있어 내부 스피커의 소리가 제대로 밖으로 나올 수 있습니다. 이 프로젝트는 기기 자체에서 오디오가 나오는 독립형 라디오처럼 작동하도록 만든 것이므로 중요한 부분이었습니다.

전체적인 디자인은 단순합니다:

  1. 파란색 외부 셸
  2. 검은색 전면 플레이트
  3. 돌출된 Stack FM 로고
  4. 전면 버튼 3개
  5. 측면 스피커 그릴
  6. 라디오와 비슷한 컴팩트한 폼 팩터

여기서 가장 중요한 목표는 단순히 전자 부품을 숨기는 것이 아니라, 프로젝트가 완성된 기기처럼 보이게 만드는 것이었습니다. 작은 AtomS3 보드를 기반으로 제작했지만, 인클로저 덕분에 제대로 된 미니 FM 라디오 같은 느낌을 줍니다.




STEP 5. 인클로저 3D 프린팅


인클로저 3D 프린팅


인클로저 3D 프린팅


인클로저 3D 프린팅


인클로저 3D 프린팅


설계를 마무리한 후 인클로저의 모든 부품을 3D 프린팅했습니다. 본체 전체를 하나의 부품으로 출력하는 대신, 출력·조립·마감이 더 쉽도록 설계를 여러 개의 부품으로 나누었습니다.

사진에서 볼 수 있듯이 주요 부품은 파란색 외부 인클로저, 검은색 전면 패널, 버튼 3개, 돌출된 STACK FM 로고, 스피커 그릴입니다. 이 부품들을 따로 출력하면 복잡한 후처리나 도색 없이도 서로 다른 색상을 사용할 수 있습니다.

본체는 Aqua PLA로 출력했으며, 라디오의 기본 색상을 결정합니다. 전면 패널은 검은색으로 별도 출력하여 컨트롤과 돌출된 로고가 강한 대비를 이루도록 했습니다.

STACK FM 로고 역시 돌출된 별도 부품으로 출력했습니다. 덕분에 전면 패널에서 로고가 돋보이고, 인클로저가 더욱 완성도 높고 거의 상용 제품처럼 보입니다.

원형 버튼 3개도 별도로 출력했습니다. 버튼을 개별 부품으로 구성하면 설치가 더 쉽고, 눌렀을 때 제대로 움직일 수 있습니다.

스피커 그릴도 별도로 출력했습니다. 작은 구멍으로 이루어진 단순한 패턴으로, 인클로저 내부의 스피커를 보호하면서도 소리가 통과할 수 있도록 합니다.

이와 같은 방식으로 인클로저를 설계하고 출력하면 전체 인클로저를 다시 출력하지 않고도 개별 부품을 조금씩 수정할 수 있다는 장점이 있습니다. 또한 다양한 부품의 색상과 맞춤 상태를 시험하기도 훨씬 쉬웠습니다.




STEP 6. PCB 설계


PCB 설계


PCB 설계


PCB 설계


이 프로젝트에서는 푸시 버튼과 전원 관리 회로를 하나의 컴팩트한 보드에 통합한 커스텀 PCB 하나를 사용했습니다. 이 PCB는 기존 Waveform 프로젝트 중 하나를 바탕으로 수정했으며, Stack FM에서는 마이크 부분을 사용하지 않았습니다.

보드에는 푸시 버튼 3개와 IP5306 전원 관리 IC가 포함되어 있습니다. Li-Po 셀은 IP5306 전원 관리 IC에 연결되며, 매트릭스와 Pico에 전원을 공급할 안정적인 5V 2A 출력을 제공합니다. 버튼 3개는 프로젝트의 물리적 컨트롤로 사용됩니다. 하나는 IP5306에 연결된 주 전원 스위치이고, 나머지 두 개는 Pico의 GPIO0과 GPIO1에 연결됩니다.

기존 Waveform PCB의 마이크 부분은 이 프로젝트에서 사용하지 않으므로 MAX9814와 관련 연결부는 미사용 상태로 두었습니다.

또한 PCB를 프로젝트의 나머지 부분과 연결하는 데 필요한 커넥터도 추가했습니다. HUB75 커넥터(CON-16)는 64×32 LED 매트릭스를 Pico에 연결하는 데 사용되며, 연결은 다음과 같습니다: A는 GPIO19, B는 GPIO16, C는 GPIO18, D는 GPIO20, E는 GPIO22, CLK는 GPIO11, LAT/STB는 GPIO12, OE는 GPIO13, R1은 GPIO2, G1은 GPIO3, B1은 GPIO4, R2는 GPIO5, G2는 GPIO8, B2는 GPIO9입니다.

5V와 GND용 CON2 커넥터와 GPIO0, GPIO1, GPIO26, GPIO28을 브레이크아웃하는 CON4 커넥터도 포함했습니다.

PCB는 3D CAD 모델의 PCB 외곽선을 사용해 설계했습니다. 이를 통해 인클로저의 치수에 맞춰 푸시 버튼, USB Type-C 포트, Pico 풋프린트, 장착 홀, 커넥터를 정확히 배치할 수 있었습니다.




STEP 7. NextPCB PCB 서비스


NextPCB PCB 서비스


PCB 설계를 완료한 후 두 PCB의 Gerber 데이터를 다음 업체로 보냈습니다: HQ NextPCB, 그리고 흰색 실크스크린과 녹색 솔더 마스크 사양으로 주문했습니다.

주문 후 일주일 이내에 PCB를 받았으며, PCB 품질은 상당히 훌륭했습니다.

또한 여러 프로젝트에서 큰 도움을 받은 HQDFM도 소개해야겠습니다. Huaqiu의 사내 엔지니어들이 무료 Design for Manufacturing 소프트웨어인 HQDFM을 개발했으며, PCB 설계자가 설계를 시각화하고 검증하는 방식을 혁신했습니다.

NextPCB의 Accelerator 캠페인을 활용하여 혁신적인 프로젝트에 사용할 RP2040 기반 조립 PCB 2개를 무료로 받아 보세요.

https://www.nextpcb.com/blog/rp2040-free-pcba-prototypes-nextpcb-accelerator

이 혜택은 물류비를 포함한 모든 비용을 부담하므로 아이디어를 더 쉽고 저렴하게 실현할 수 있습니다. SMT 서비스는 비용이 많이 들 수 있지만 NextPCB가 그 장벽을 극복할 수 있도록 도와드립니다. 관련 프로젝트를 공유하기만 하면 나머지는 NextPCB가 처리합니다. 기술 창작물을 한 단계 발전시킬 수 있는 이 놀라운 기회를 놓치지 마세요!




STEP 8. HQDFM: 무료 온라인 Gerber 뷰어 및 DFM 분석 도구


HQDFM: 무료 온라인 Gerber 뷰어 및 DFM 분석 도구


또한 NextPCB에는 자체 Gerber 뷰어와 DFM 분석 소프트웨어가 있습니다.

HQDFM 소프트웨어의 DFM 서비스가 설계를 개선해 줍니다. 제조업체의 DFM 보고서를 기다리는 것이 번거롭다고 느끼는 저에게는, HQDFM 사전 자체 점검을 수행하는 가장 효율적인 방법입니다.

온라인 Gerber 뷰어에서 확인되는 화면은 다음과 같습니다. 빠르게 살펴보기에는 괜찮지만 완전히 명확하지는 않습니다. PCBA의 상세한 DFM 분석과 같은 모든 기능을 사용하려면 데스크톱 소프트웨어를 다운로드해야 합니다. 웹 버전에서는 기본적인 DFM 보고서만 제공합니다.

종합적인 Design for Manufacture(DFM) 분석 기능을 갖춘 HQDFM 은 무료로 사용할 수 있는 정교한 온라인 PCB Gerber 파일 뷰어입니다.

15년 이상의 업계 경험을 바탕으로 고급 제조 공정에 대한 유용한 통찰을 제공합니다. 합리적인 가격의 신뢰할 수 있는 PCB 서비스를 찾고 있다면 HQ NextPCB를 확인해 볼 가치가 있습니다.




STEP 9. PCB 조립


PCB 조립


PCB 조립


PCB 조립


PCB 조립


  1. 이 프로젝트에는 서로 다른 회로가 2개 있습니다. 먼저 Switch–MIC–Power 보드 조립 공정부터 시작합니다. 솔더 페이스트 디스펜싱 니들을 사용해 각 SMD 부품 패드에 솔더 페이스트(63/37 Sn/Pb)를 도포합니다.
  2. 그다음 ESD 트위저를 사용해 각 SMD 부품을 집어 PCB 위에 배치합니다.
  3. 실장 후 PCB를 리플로우 핫플레이트 위로 옮깁니다. 기판이 아래에서 가열되면서 온도가 서서히 상승하고, 솔더 페이스트의 녹는점에 도달합니다. PCB의 온도가 약 200°C에 도달하면 솔더 페이스트가 녹고 모든 SMD 부품이 제자리에 고정됩니다.





STEP 10. M5 ATOM용 HAT 제작


M5 ATOM용 HAT 제작


M5 ATOM용 HAT 제작


M5 ATOM용 HAT 제작


M5 ATOM용 HAT 제작


M5 ATOM용 HAT 제작


배선을 더 깔끔하게 정리하기 위해 M5Stack AtomS3용 소형 커스텀 HAT도 제작했습니다.

HAT는 AtomS3에 함께 장착되는 소형 애드온 보드처럼 작동하며, MAX98357A 모듈, 스피커 배선, 외부 버튼에 필요한 기본 연결부를 제공합니다. 모든 배선을 점퍼 와이어로 느슨하게 연결하는 대신 HAT를 사용하면 최종 구성이 더 작고 안정적입니다.

이 보드의 연결은 코드에서 사용하는 핀에 맞춰 배선했습니다. MAX98357A의 I2S 라인은 선택한 GPIO 핀에 연결했고, 두 개의 외부 버튼은 GND와 함께 지정된 핀에 연결했습니다.

Main HAT connections:

MAX98357A BCLK -> GPIO 5
MAX98357A LRC -> GPIO 6
MAX98357A DIN -> GPIO 7

Button 1 -> GPIO 8
Button 2 -> Last available GPIO pin

Common GND line for buttons
5V and GND lines for MAX98357A

그런 다음 이 HAT를 AtomS3에 장착하여 전체 전자 부품 부분을 3D 프린팅 본체 안에 더 쉽게 넣을 수 있도록 했습니다.

이후 최종 조립 상태가 훨씬 깔끔해졌고, 장치를 인클로저 안에 넣을 준비가 되었습니다.




STEP 11. 회로 및 배선


M5Stack AtomS3 | MAX98357A | Purpose
----------------------------------------------------------------------
5V | VIN | Amplifier power
GND | GND | Common ground
GPIO 5 | BCLK | I2S bit clock
GPIO 6 | LRC / WS | I2S word select
GPIO 7 | DIN | I2S audio data

스피커는 SPK+와 SPK- 사이에 직접 연결합니다.

회로에 전원을 공급하기 전에 5V와 GND 연결을 확인하고, 세 개의 I2S 와이어를 점검했으며, 스피커 단자 어느 쪽도 GND에 연결되지 않았는지 확인했습니다.




STEP 12. 브레드보드 구성


브레드보드 구성


모든 부품을 3D 프린팅 본체 안에 넣기 전에 먼저 브레드보드에서 전체 회로를 테스트했습니다.

이 단계는 AtomS3, MAX98357A 앰프, 스피커, 물리 버튼 등 여러 부품이 함께 작동해야 했기 때문에 중요했습니다. 인클로저 밖에서 모든 부품을 테스트하면 배선을 확인하고 최종 조립 전에 문제를 수정하기가 훨씬 쉬워집니다.

I2S 핀을 사용해 MAX98357A 모듈을 AtomS3에 연결했습니다. 스피커는 MAX98357A 모듈의 출력 단자에 연결했습니다.

그 후 브레드보드에 푸시 버튼을 추가하고 GPIO 핀에 연결했습니다. 버튼은 누르면 GPIO 핀이 GND에 연결되는 간단한 방식으로 배선했습니다. 코드에서 내부 풀업을 사용하므로 별도의 풀업 저항은 필요하지 않았습니다.

배선이 완료되면 AtomS3에 첫 번째 테스트 코드를 업로드하고, 장치가 Wi-Fi에 연결되는지와 로컬 웹 앱에 응답하는지 확인했습니다.

브레드보드 구성으로 다음 항목을 확인할 수 있었습니다.

  1. MAX98357A 오디오 출력
  2. 스피커 연결
  3. I2S 배선
  4. 버튼 입력
  5. Wi-Fi 연결
  6. 로컬 웹 앱과의 통신

또한 최종 인클로저로 옮기기 전에 기본 라디오 시스템이 정상적으로 작동하는지도 확인할 수 있었습니다.




STEP 13. 최종 연결 및 조립


최종 연결 및 조립


최종 연결 및 조립


브레드보드에서 회로를 테스트한 후 3D 프린팅 본체 내부에서 최종 배선과 조립을 진행했습니다.

먼저 인클로저 안에 AtomS3를 배치하고 전면 개구부에 화면이 맞게 정렬되는지 확인했습니다. 그 후 MAX98357A 앰프와 스피커를 본체 안에 배치했습니다.

소리가 선명하게 나올 수 있도록 스피커를 측면 그릴 근처에 배치했습니다. 장치가 제대로 된 소형 라디오처럼 작동하고 인클로저 안에 막힌 듯한 소리가 나지 않도록 하는 것이 중요했습니다.

다음으로 앞에서 테스트한 것과 동일한 I2S 연결을 사용해 MAX98357A 모듈을 AtomS3에 영구적으로 연결했습니다.

MAX98357A VIN -> AtomS3 5V
MAX98357A GND -> AtomS3 GND
MAX98357A BCLK -> GPIO 5
MAX98357A LRC -> GPIO 6
MAX98357A DIN -> GPIO 7

그 후 전면 버튼 세 개를 슬롯에 설치했습니다. 버튼 캡은 전면에서 끼우고, 실제 푸시 버튼은 그 뒤에 맞춰 정렬했습니다.

버튼 배선에는 코드에서 선택한 GPIO 핀을 사용했습니다.

Previous button -> GPIO 8 to GND
Next button -> GPIO 38 to GND

버튼은 내부 풀업을 사용하므로 별도의 풀업 저항은 필요하지 않았습니다.

모든 전자 부품을 내부에 넣은 후, 어떤 와이어도 스피커 콘을 누르거나 버튼을 막고 있지 않은지 확인했습니다. 또한 프로그래밍과 전원 공급을 위해 AtomS3의 USB-C 포트에 계속 접근할 수 있는지도 확인했습니다.

마지막으로 전면 패널을 제자리에 고정하고 인클로저를 닫았습니다.

이 단계에서 Stack FM은 디스플레이, 물리 버튼, 스피커 출력을 갖춘 소형 3D 프린팅 라디오라는 최종 형태를 갖추었습니다.




STEP 14. 코드


코드


Stack FM의 코드는 두 가지 주요 부분으로 나뉩니다.

첫 번째 부분은 M5Stack AtomS3에서 실행되고, 두 번째 부분은 휴대폰에서 실행되는 로컬 웹 앱입니다.

Stack-FM/
StackFM/
StackFM.ino
DisplayUI.h

Phone_Web_App/
index.html

AtomS3 코드는 프로젝트의 하드웨어 측면을 처리합니다. Wi-Fi에 연결하고, 로컬 웹 앱에서 명령을 수신하며, I2S를 사용해 MAX98357A 앰프로 오디오를 전송하고, 디스플레이를 업데이트하며, 물리 버튼을 읽습니다.

로컬 웹 앱은 제어 인터페이스로 작동합니다. AtomS3의 IP 주소를 사용해 Stack FM에 연결하고, 저장된 항목을 선택하며, 재생을 제어하고, 볼륨을 조절하는 데 사용됩니다.

따라서 코드의 작동 흐름은 다음과 같습니다.

Local Web App
WebSocket connection
AtomS3
I2S audio output
MAX98357A speaker

AtomS3 측에는 Arduino IDE와 함께 M5Unified 라이브러리 및 WebSockets 라이브러리를 사용했습니다. M5Unified 라이브러리는 AtomS3의 디스플레이와 버튼을 처리하고, WebSockets 라이브러리는 로컬 웹 앱과 AtomS3 간의 통신에 사용됩니다.

주요 Arduino 스케치는 다음과 같습니다.

StackFM.ino

디스플레이 코드는 다음 파일에 별도로 작성했습니다.

DisplayUI.h

이렇게 하면 메인 스케치가 작동 로직을 처리하고 디스플레이 파일이 라디오 스타일 화면 UI를 처리하므로 프로젝트를 더 깔끔하게 유지할 수 있습니다.

로컬 웹 앱은 HTML, CSS, JavaScript로 작성했습니다.

Phone_Web_App/index.html

이 파일에는 TUNE, SAVED, DEVICE 섹션을 포함한 전체 라디오 인터페이스가 들어 있습니다. 또한 오디오 선택, 저장 항목, 연결, 재생 제어도 처리합니다.

다음 섹션에서는 AtomS3 코드와 로컬 웹 앱 코드를 각각 살펴보겠습니다.




STEP 15. M5 ATOM 펌웨어


Stack FM의 주요 코드는 StackFM.ino 파일에 작성되어 있습니다.

이 파일은 라디오의 작동 부분을 처리합니다. AtomS3를 Wi-Fi에 연결하고, WebSocket 서버를 시작하며, 로컬 웹 앱에서 오디오 및 제어 데이터를 수신하고, MAX98357A 앰프로 오디오를 전송하며, 물리 버튼을 읽습니다.

디스플레이 UI는 이 메인 파일 안에 직접 작성하지 않았습니다. 디스플레이 부분을 DisplayUI.h에 별도로 분리하여 메인 코드를 더 쉽게 이해하고 나중에 수정할 수 있도록 했습니다.

메인 스케치는 먼저 필요한 라이브러리를 포함하는 것부터 시작합니다.

#include <Arduino.h>
#include <WiFi.h>
#include <WebSocketsServer.h>
#include <M5Unified.h>
#include "driver/i2s_std.h"
#include "DisplayUI.h"

여기서는 WiFi.h를 사용해 AtomS3를 로컬 네트워크에 연결합니다.

WebSocketsServer.h는 로컬 웹 앱이 AtomS3로 오디오와 명령을 보낼 수 있도록 사용됩니다.

M5Unified.h는 AtomS3의 디스플레이와 버튼을 지원하는 데 사용됩니다.

I2S 드라이버는 오디오 출력을 MAX98357A 앰프로 보내는 데 사용됩니다.

Wi-Fi 자격 증명은 코드 상단에 추가합니다.

const char *WIFI_SSID = "YOUR_WIFI_NAME";
const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

MAX98357A 핀도 코드에 정의되어 있습니다.

#define I2S_BCLK 5
#define I2S_LRC 6
#define I2S_DOUT 7

이 핀은 AtomS3와 MAX98357A 모듈 간의 I2S 오디오 연결에 사용됩니다.

물리적 버튼에는 GPIO 핀 2개를 사용했습니다.

#define BUTTON_PREV_PIN 8
#define BUTTON_NEXT_PIN 38

이 버튼을 사용하면 장치 자체에서 저장된 항목을 변경할 수 있습니다.

코드에서는 오디오 샘플 레이트도 설정합니다.

static const uint32_t SAMPLE_RATE = 22050;

이 샘플 레이트는 로컬 웹 앱과 동일하게 유지되므로 양쪽에서 같은 형식으로 오디오를 송수신할 수 있습니다.

WebSocket 서버는 포트 81에서 시작됩니다.

WebSocketsServer webSocket(81);

이는 로컬 웹 앱과 Stack FM 간의 주요 통신 경로입니다.

웹 앱이 오디오 데이터를 보내면 AtomS3는 이를 바이너리 데이터로 수신하고 I2S를 사용해 MAX98357A로 씁니다.

void writePcmToI2S(uint8_t *payload, size_t length) {
if (!tx_handle || length == 0) return;

size_t written = 0;
i2s_channel_write(tx_handle, payload, length, &written, 50);
bytesReceived += written;
}

웹 앱의 텍스트 명령은 디스플레이 상태를 업데이트하는 데 사용됩니다. 예를 들어 웹 앱은 현재 미디어 이름, 재생 상태, 버퍼링 상태 또는 중지 상태를 보낼 수 있습니다.

물리적 버튼은 메인 루프 안에서 읽습니다. 버튼이 눌리면 AtomS3가 웹 앱으로 명령을 다시 보냅니다.

webSocket.broadcastTXT("button:prev");
webSocket.broadcastTXT("button:next");

이를 통해 물리적 버튼으로 웹 앱 내부의 저장 목록을 제어할 수 있습니다.

메인 루프가 모든 기능을 계속 실행합니다.

void loop() {
M5.update();
webSocket.loop();
handleButtons();

if (millis() - lastStatsMs > 2000) {
lastStatsMs = millis();
drawAtomRadioDisplay();
}
}

간단히 말해 StackFM.ino는 다음을 담당합니다:

  1. Wi-Fi 연결
  2. WebSocket 통신
  3. 로컬 웹 앱에서 오디오 수신
  4. I2S를 통해 MAX98357A로 오디오 전송
  5. 물리적 버튼 읽기
  6. 장치 상태 업데이트

디스플레이 디자인 자체는 다음 파일인 DisplayUI.h에서 별도로 처리합니다.




STEP 16. 디스플레이 UI 코드


디스플레이 UI 코드


Stack FM의 디스플레이 UI는 DisplayUI.h 파일에 별도로 작성되어 있습니다.

프로젝트를 더 쉽게 관리할 수 있도록 이 부분을 메인 Arduino 스케치와 분리했습니다. 메인 StackFM.ino 파일은 Wi-Fi, 오디오, WebSocket 통신 및 버튼 입력을 처리하고, DisplayUI.h는 AtomS3 화면에 표시되는 내용만 처리합니다.

디스플레이 UI는 물리적 인클로저와 로컬 웹 앱에 적용된 것과 동일한 시각적 방향을 따릅니다. 현재 미디어 이름, 연결 상태, 재생 상태와 작은 튜너 스타일 섹션을 보여 주는 간단한 라디오 스타일 화면을 사용합니다.

디스플레이 상단에는 라디오가 준비 상태인지 오프라인인지 표시됩니다. 또한 휴대폰/로컬 웹 앱의 연결 여부도 표시합니다.

현재 미디어 또는 방송국 이름은 화면 중앙에 표시됩니다. AtomS3 화면이 작기 때문에 이름이 너무 길면 텍스트를 줄입니다.

String fitDisplayText(String text, uint8_t maxChars) {
text.trim();
if (text.length() > maxChars) {
text = text.substring(0, maxChars - 1) + "~";
}
return text;
}

이 작은 함수는 긴 이름이 디스플레이 영역 안에 표시되도록 도와줍니다.

메인 디스플레이 함수의 이름은 drawAtomRadioDisplay입니다.

void drawAtomRadioDisplay(const char *line1 = nullptr, const char *line2 = nullptr)

이 함수는 AtomS3 화면 전체를 업데이트합니다.

이 함수 안에서는 먼저 라디오 UI에 사용할 주요 색상을 정의합니다. 이 색상은 프로젝트에서 사용한 부드러운 녹색/검은색 라디오 스타일과 어울립니다.

const uint16_t sage = M5.Display.color565(166, 182, 147);
const uint16_t ink = M5.Display.color565(8, 10, 8);
const uint16_t muted = M5.Display.color565(126, 143, 113);
const uint16_t glow = M5.Display.color565(183, 216, 140);

그런 다음 세이지 색상을 사용해 화면을 지우고, 텍스트 색상을 검은색으로 설정합니다.

M5.Display.fillScreen(sage);
M5.Display.setTextWrap(false);
M5.Display.setTextSize(1);
M5.Display.setTextColor(ink, sage);

화면 상단에는 라디오 연결 상태가 표시됩니다.

M5.Display.fillCircle(7, 8, 2, WiFi.status() == WL_CONNECTED ? ink : muted);
M5.Display.setCursor(12, 4);
M5.Display.print(WiFi.status() == WL_CONNECTED ? "FM READY" : "OFFLINE");

오른쪽 상단에는 휴대폰 웹 앱의 연결 여부가 표시됩니다.

M5.Display.setCursor(82, 4);
M5.Display.print(clientConnected ? "PHONE" : "WAIT");

그다음 미디어 이름이 중앙 영역에 표시됩니다. 이름이 길면 작은 AtomS3 디스플레이에서도 읽기 쉽도록 두 줄로 나눕니다.

이름 아래에는 현재 상태가 표시됩니다:

M5.Display.print(deviceBuffering ? "BUFFERING" : (devicePlaying ? "NOW PLAYING" : currentDeviceStatus));

오디오가 재생되는 동안 화면이 더 생동감 있게 보이도록 작은 도트 매트릭스 애니메이션도 추가했습니다. 이 애니메이션은 실제 오디오 레벨을 기반으로 하지 않지만, 라디오와 같은 멋진 시각 효과를 제공합니다.

화면 하단 영역은 작은 튜너 블록으로 표시됩니다. 여기에는 FM/RADIO와 현재 재생 상태가 표시됩니다.

M5.Display.fillRoundRect(4, 84, 120, 42, 7, ink);
M5.Display.setTextColor(glow, ink);
M5.Display.setTextSize(2);
M5.Display.setCursor(10, 90);
M5.Display.print("FM");
M5.Display.setTextSize(1);
M5.Display.setCursor(68, 92);
M5.Display.print("RADIO");

이 블록의 아래쪽에는 라디오가 방송 중인지 또는 준비 상태인지 표시됩니다.

M5.Display.setCursor(10, 114);
M5.Display.print(devicePlaying ? "ON AIR" : "READY");

로컬 웹 앱에 입력할 수 있도록 IP 주소도 장치 디스플레이에 표시됩니다.

전체적으로 DisplayUI.h는 AtomS3 화면의 전체 레이아웃을 처리하며, 디스플레이 부분을 메인 라디오 로직과 분리해 둡니다.




STEP 17. 로컬 웹 앱 코드


로컬 웹 앱 코드


로컬 웹 앱 코드


로컬 웹 앱 코드


1. 장치 연결 로직

let socket;

function connectDevice() {
const ip = document.getElementById("deviceIp").value.trim();

socket = new WebSocket(`ws://${ip}:81`);

socket.onopen = () => {
console.log("Stack FM connected");
};

socket.onmessage = (event) => {
console.log("Device:", event.data);
};

socket.onerror = () => {
console.log("Connection error");
};
}

이 부분은 로컬 웹 앱을 AtomS3에 연결합니다.

사용자는 AtomS3 디스플레이에 표시된 IP 주소를 입력합니다. 그러면 웹 앱은 해당 IP를 사용해 WebSocket 연결을 엽니다.

연결 형식은 다음과 같습니다:

ws://AtomS3-IP:81
ws://AtomS3-IP:81

AtomS3 WebSocket 서버가 해당 포트에서 실행되므로 포트 81을 사용합니다.

2. 저장된 미디어 저장 로직

let savedMedia = JSON.parse(localStorage.getItem("stackFmMedia")) || [];

function saveMedia() {
const name = document.getElementById("mediaName").value.trim();
const url = document.getElementById("mediaUrl").value.trim();

if (!name || !url) return;

savedMedia.push({ name, url });
localStorage.setItem("stackFmMedia", JSON.stringify(savedMedia));

renderSavedMedia();
}

이 섹션은 저장된 미디어 목록을 처리합니다.

사용자는 웹 앱에서 미디어 이름과 오디오 URL을 추가할 수 있습니다. 저장하면 데이터는 localStorage를 사용해 브라우저에 저장됩니다.

따라서 페이지를 닫았다가 다시 열어도 저장된 항목을 계속 사용할 수 있습니다.

3. 저장 목록 표시 로직

function renderSavedMedia() {
const list = document.getElementById("savedList");
list.innerHTML = "";

savedMedia.forEach((item, index) => {
const button = document.createElement("button");
button.innerText = item.name;
button.onclick = () => playMedia(index);
list.appendChild(button);
});
}

이 함수는 웹 페이지에 저장된 모든 미디어 항목을 표시합니다.

저장된 각 항목은 버튼으로 변환됩니다. 사용자가 해당 버튼을 누르면 선택한 미디어 항목이 Stack FM에서 재생됩니다.

4. 선택한 미디어 재생 로직

function playMedia(index) {
const item = savedMedia[index];

sendMessage({
type: "playUrl",
name: item.name,
url: item.url
});
}

이 부분은 선택한 미디어 URL을 AtomS3로 전송합니다.

웹 앱 자체에서 사운드를 재생하지는 않습니다. 미디어 이름과 URL만 장치로 전송합니다.

이 데이터를 수신한 후 AtomS3는 MAX98357A 스피커를 통해 선택한 소스의 재생을 시작합니다.

5. 재생 버튼 로직

function sendCommand(command) {
sendMessage({
type: "command",
value: command
});
}

이 로직은 다음과 같은 버튼에 사용됩니다:

Play
Pause
Stop
Previous
Next

이 버튼 중 하나를 누르면 웹 앱이 AtomS3로 명령을 전송합니다. 그러면 AtomS3가 실제 오디오 출력에서 해당 동작을 수행합니다.

6. 볼륨 제어 로직

function setVolume(value) {
sendMessage({
type: "volume",
value: Number(value)
});
}

이 부분은 Stack FM의 볼륨을 제어합니다.

볼륨 슬라이더를 움직이면 웹 앱이 새로운 볼륨 값을 AtomS3로 전송합니다. AtomS3는 이 값을 사용해 스피커 출력 레벨을 변경합니다.

7. 공통 메시지 전송 함수

function sendMessage(data) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
console.log("Stack FM not connected");
return;
}

socket.send(JSON.stringify(data));
}

이 함수는 모든 컨트롤에서 사용하는 공통 함수입니다.

명령을 전송하기 전에 먼저 웹 앱이 AtomS3에 연결되어 있는지 확인합니다. 연결이 활성 상태이면 명령을 JSON 형식으로 변환해 장치로 전송합니다.




STEP 18. ATOMS3에 코드 업로드


ESP32 메인 코드와 디스플레이 UI 코드를 완성한 후, 다음 단계는 M5Stack AtomS3에 펌웨어를 업로드하는 것입니다.

이를 위해 Arduino IDE를 사용했습니다. 전체 펌웨어 폴더에는 메인 .ino 파일과 디스플레이 UI 헤더 파일이 동일한 스케치 폴더에 들어 있습니다.

StackFM/
StackFM.ino
DisplayUI.h

StackFM.ino 파일은 Arduino IDE에서 열고, DisplayUI.h 파일은 메인 코드에서 제대로 포함할 수 있도록 같은 폴더에 둡니다.

업로드하기 전에 Arduino IDE에 필요한 보드와 라이브러리를 설치해야 합니다.

Board used:
M5Stack AtomS3

Main libraries used:
M5Unified
WiFi
WebSocketsServer
ESP32-audioI2S / Audio_nopsram

업로드에 사용한 보드 설정은 다음과 같습니다:

Board: M5Stack-AtomS3
USB CDC On Boot: Enabled
PSRAM: Disabled
Flash Mode: DIO
Upload Speed: 921600 or 115200
Core Debug Level: None

올바른 보드와 COM 포트를 선택한 후 코드를 컴파일하고 AtomS3에 업로드합니다.

업로드가 완료되면 AtomS3는 저장된 Wi-Fi 네트워크에 연결하면서 시작합니다. 연결에 성공하면 디스플레이에 Stack FM 인터페이스와 장치 IP 주소가 오른쪽 상단에 표시됩니다.

이 IP 주소는 로컬 웹 앱에서 브라우저 인터페이스와 라디오 장치를 연결하는 데 사용되므로 중요합니다.

Example:

AtomS3 shows IP:
192.168.1.44

The same IP is entered in the Stack FM local web app.

이 단계가 끝나면 Stack FM의 하드웨어 측면이 준비됩니다. 이제 AtomS3는 로컬 웹 앱에서 명령을 수신하고 MAX98357A 스피커를 통해 오디오 재생을 제어할 수 있습니다.




STEP 19. 음악 재생


음악 재생


음악을 재생하려면 먼저 Stack FM 로컬 웹 앱을 열고 AtomS3 디스플레이에 표시된 IP 주소를 입력합니다.

연결한 후 저장된 항목 섹션으로 이동해 음악 링크를 선택하고 재생을 누릅니다. AtomS3가 명령을 수신하고 MAX98357A 스피커를 통해 오디오를 재생합니다.




STEP 20. 뉴스 재생


뉴스 재생


뉴스를 재생하려면 같은 과정을 따르면 됩니다. 웹 앱에 AtomS3 IP 주소를 입력하고 장치를 연결한 다음 저장된 뉴스 링크를 선택하고 재생을 누릅니다.

선택한 뉴스 스트림이 AtomS3로 전송되고 스피커에서 재생됩니다.




STEP 21. 결론


결론


이렇게 해서 Stack FM이 완성되었습니다.

이 프로젝트는 M5Stack AtomS3를 사용해 작은 FM 스타일 라디오를 만들어 보자는 아이디어에서 시작했습니다. 하지만 큰 화면이나 복잡한 컨트롤을 추가하는 대신 로컬 웹 앱을 제어 패널로 사용했습니다.

AtomS3는 Wi-Fi에 연결되고 디스플레이에 IP 주소를 표시하며 웹 앱에서 명령을 수신하고 MAX98357A 스피커를 통해 오디오를 재생합니다. 저장된 목록에서 음악과 뉴스 스트림을 선택할 수 있어 소형 데스크톱 라디오처럼 작동합니다.

3D 프린트 본체, 전면 버튼, 디스플레이 UI, 스피커 출력이 모두 어우러져 하나의 완성된 작은 라디오 장치를 이룹니다.

전반적으로 재미있는 제작 과정이었고, AtomS3를 물리적 하드웨어와 브라우저 기반 인터페이스를 모두 갖춘 실용적인 라디오 스타일 프로젝트로 바꿀 수 있었던 좋은 방법이었습니다.

현재 프로젝트는 완성되었습니다. 프로젝트와 관련해 도움이 필요하다면 댓글을 남겨 주세요!

특별히 감사드릴 곳은 HQ NextPCB로,이 프로젝트에서 사용한 부품을 제공해 주셨습니다. 저렴한 비용으로 다양한 PCB 또는 PCBA 관련 서비스를 이용하려면 확인해 보세요.

평화를 빕니다.


원문: https://www.instructables.com/StackFM/

좋아요 0
게시판

회사명 공방유니언 주소 각 공방 주소 참조
사업자 등록번호 0000 대표 공방유니언 전화 019-60105846 팩스 0000 이메일 gongbangunion@gmail.com
통신판매업신고번호 0000 개인정보관리책임자 공방유니언
Copyright © 2017 공방유니언. All Rights Reserved.