전에 쓴 3편을 조금 더 자세하게 보충해봅니다.
오류가 심각하게 안나도 되는게 일어나면 의지가 약화되네요. 오류없이 진행되면 하루에 다섯시간 이상 임하면서 진행을 해두는데, 이상한 오류가 겹치면 해결하느라 시간이 갑니다. 오류가 제작당시에 익히 있을법한 오류라면 그냥 임하면 되는데, 이게 그냥 일어나는 오류가 아니네요.
그래도 임해서 파악한 것들이 있어서 코드를 이리저리 검토해가면서 바꿔도 보고 추가도 해보고 있습니다. 지금까지 파악한 것은
(1) USB 메모리를 쓰려면 라즈베리파이 피코를 USB MSC의 Host로 구현한다. 보조전원을 가능하게 해야 해서, 여러 방법중에 확장 보드격인 익스텐더 쿼드와 같은 장비를 쓴다. 이 장비를 쓰면 보조전원을 USB 케이블로 공급하는게 된다.
(2) USB 관련 구현은 descriptor 설정이 필요한 경우도 있는데 라즈베리파이 피코 C SDK에서도 제공하는 TinyUSB를 이용하면 초기 접근이 쉬워진다. TinyUSB로 마운트, 언마운트, 이후 동작 등을 정의하는 콜백을 만들어 main() 함수에 몇개의 함수를 배치하면 콜백이 작동해서 USB 관련 클래스 구현과 부속 작업을 쉽게 해준다.
(3) 파일 입출력은 FatFS를 주로 이용한다. FatFS는 라즈베리파이 피코에서도 사용이 가능하다. 파일 시스템 처리에 필요한 기능들인 장치 마운트, 파일 생성과 오픈, 파일 읽기, 드라이브 변경, 디렉토리 생성, 파일 기록 등이 된다.
(4) SPI의 경우 라즈베리파이 피코 핀아웃대로 연결해서 정보를 코드에 반영해야 한다. 대부분의 microSD 카드 모듈이 SPI라, SPI 레인을 잘 보고 각각의 CS, SCLK, MOSI, MISO 등에 대응되는 GPIO를 기재해둔다.
(5) 그러나 USB 메모리는 SPI 설정이 필요없다
이정도구요. 일단 실행 순서나 요령만 확인하기 위해 기존 프로젝트를 받아와서 실험중입니다. 우선 파악한 기능은
(a) I2S 초기화를 해서 오디오 재생의 품질도 챙기고, 하드웨어도 초기화하고 다룬다. PIO를 통해서 한다.
(b) WAV 오디오 형식에서 정의한 헤더를 읽어들여 여러 처리를 한다. 헤더의 각각의 영역이 정해져 있고 이 정보를 잘 읽어들여 WAV 파일 처리에 쓴다. 헤더의 영역을 정확하게 알아내서 이 영역 구간을 지나서 오디오 정보가 있는 지점까지 스킵한다.
(c) 큐로 정의된 링버퍼를 써서 오디오 파일을 읽어들인다. FatFS의 f_read()에 갱신되는 오프셋을 넘겨 링버퍼에 저장한다. FIFO로 작동하므로 이에 대한 코드를 구현한다. 보통 pico-extras에 포함된 audio_i2s를 참고하는데, 여러 구조체들로 조합되어 정보를 일관된 코드 구조로 참조한다. 이 audio_i2s 기반 프로젝트들에서도 audio_buffer_t나 audio_buffer_pool_t 같은 여러 정보 구조체가 혼합된 형태로 구현한다. 버퍼의 처리에 순서를 지정하려고 프로듀셔풀이나 컨슈머풀 같은 일종의 디자인 패턴도 쓰인다.
(d) 링버퍼에 저장된 정보를 I2S에 쓰기동작해서 보내면 음악연주가 된다.
일단 이정도가 근간이구요. 나름대로 기존 프로젝트 다섯개 정도를 둘러보면서 파악은 해두었습니다. 지금은 어떤 분이 만드신 TinyUSB MSC를 Host로 제어하는 프로젝트를 채택해서 TinyUSB 구현을 단순화했구요. WAV 파일 재생을 지원하는 프로젝트에서 링버퍼와 음악 재생 코드를 잘 보고 옮겨와서 조합해보고 있습니다.
많은 경우 파이썬으로 하면 패키지를 임포트해서 파일 읽고 I2S에 보내는 코드가 매우 짧은데, C나 C++로 하면 코드 규모가 확 커집니다. 그래서 파일들 내용을 일일히 읽어가면서 함수나 변수 등의 유래한 코드를 찾고 기억해두는게 일거리인데 되네요.
지금은 기존 프로젝트를 보고 조합해서 작동을 시켜보고 흐름을 파악하는 것과, C SDK에서 온 코드인지, 프로젝트 특화인지를 파악해서 어떻게 응용해야 할지를 보고 있네요.
일단은 이 글 위에 정리한대로는 파악했구요. 지금은 코드 실험중입니다.
링버퍼와 FatFS 활용은 WAV 파일 재생을 지원하는 프로젝트가 제일 확실한데요. LCD를 작동시키는 UI 파일을 우선 작동하는 시작점인데, 잘 보면 UI에서 정의한 함수와 변수들이 혼재되어 있어서 집중을 해야 하구요. 파이썬처럼 패키지 임포트해서 파일 읽고 즉시 재생 함수를 실행하는 구조가 아닌 경우가 많아서 IDE를 잘써야 하네요. 우선 참고한 프로젝트는 cpp와 헤더파일로 된 PlayAudio.cpp와 PlayWav.cpp에서 파일 실행을 하는 처리를 하는데요.
주목할 것은 WAV 파일을 읽어들여서 링버퍼에 저장하고 I2S로 보내는 작동인데요. WAV의 헤더를 읽어들이고, RIFF WAVE를 인식하고, FMT, 샘플링 레이트 등을 읽어서 처리에 쓰게 준비하고나서 헤더 영역을 스킵하는 코드가 중요하구요. 링버퍼를 저장하는 변수를 선언해서 left 값을 갱신하고, 버퍼가 FIFO로 작동하게 하는 코드가 중요하네요. WAV 파일을 읽어들일때는 파일이 eof가 되기전에 pos 값을 갱신해서 링버퍼 구조체의 멤버에 저장하는 얼개네요.
그냥 재생 명령을 내리는게 아니고 여러 파일과 함수가 유기적으로 연결되어 있어서, PlayWav.* 구조를 잘 보고 함수를 호출해야 하고, main() 함수를 둔 코드에서도 TinyUSB의 콜백 순서를 잘 파악해서 코드를 추가해야 할 것 같습니다.
대충 정리했는데 이해는 되시죠?^^;; (글이 길게는 썼는데 여러 내용이 들어가 있어서 지루할 것 같습니다)
그리고 도전 과제는 PIO를 정의한 파일에서 pioasm으로 구현한 부분의 코드 이해와 응용인데요. 피코 C SDK 공식문서에서 pioasm 명령어의 일부를 보여주긴 하는데, I2S와 관련해서 활용하자니 정보가 거의 안찾아지네요. 그래서 그냥, PIO로 I2S 동작을 최대한 다양하게 구현해서 C로 바인딩해주는 프로젝트를 골라서 쓰고 있습니다.
잘 해두어야죠.