Cześć,
pod tą niecodzienną nazwą tematu prezentuję projekt małego, automatycznego odtwarzacza muzyki do umilania posiedzeń na toalecie. Oczywiście to jedno zastosowanie, w innych sytuacjach pewnie też może się przydać
Głównym założeniem była niewielka złożoność, banalny interfejs, bezobsługowość po wstępnej konfiguracji i pomieszczenie niskim kosztem setek albo tysięcy godzin muzyki.
Cechy:
- Obsługa kart MicroSD z systemem plików Fat32.
- Dekodowanie plików SBC Stereo, z pełnym bitpool, 44.1kHz 10bit.
- Bardzo prosty interfejs w postaci 2 przycisków i diody stanu.
- Złącza: USB type C (zasilanie). USB A (źródło zasilania głośników), Mini-Jack (dźwięk).
- Czujnik światła - automatyczne uruchamianie i zatrzymywanie odtwarzania w zależności od jasności.
- Obsługa katalogów: zapętlanie utworów wewnątrz katalogu albo przeskakiwanie do następnego.
- Funkcja odtwarzania losowego bez powtórzeń: każdy utwór z katalogu będzie odtwarzany tylko raz, chyba że odtworzą się już wszystkie inne.
- Przeskakiwanie do następnego/poprzedniego utworu, zachowując wylosowaną kolejność (po cofnięciu się i przeskoczeniu do przodu wrócimy do tego samego utworu).
- Pełna konfigurowalność za pomocą pliku na karcie SD.
- Zapisywanie aktualnego stanu na karcie SD na wypadek zaniku zasilania, opcjonalne.
- Opcjonalny efekt fade in / fade out z regulowaną prędkością.
Projekt powstał w dużej mierze z ciekawości, ile można wycisnąć z mikrokontrolera za 8 centów.
Ten użyty tutaj to Pyua PY32F002AF15P6TU. O tych chińskich układach za grosze przeczytamy chociażby tutaj: https://www.elektroda.pl/rtvforum/topic3946116.html albo ostatnio tutaj: https://www.elektroda.pl/rtvforum/topic4144976.html
Mikrokontroler posiada 32kB pamięci flash i jedynie 4kB pamięci SRAM.
Dekodowanie MP3 w takich warunkach nie jest możliwe. Mamy tu kilkukrotnie za mało ramu i prawdopodobnie mocy obliczeniowej.
Z drugiej strony odtwarzanie nieskompresowanych plików wav byłoby marnotractwem przestrzeni i cały projekt nie wniósłby niczego ciekawego. Zewnętrznego dekodera też nie chciałem użyć, bo zwielokrotniłoby to cenę całości i znowu, nie byłoby w tym nic ciekawego.
Możliwie lekkim, jakościowo wystarczającym i całkiem wydajnym kodekiem okazał się SBC, powszechnie używany w urządzeniach bluetooth jako podstawowy kodek.
Nośnikiem danych została karta MicroSD. Obecnie za mniej niż 8 złotych można zakupić kartę o pojemności 64GB, która przy kodeku SBC zmieści ponad 400 godzin muzyki.
Tak wypchana karta w połączeniu z losowym odtwarzaniem naprawdę może dać efekt idealnego "radia" - bez reklam i bez powtórek.
Firmware jest napisany trochę w C, trochę w C++. Obsługa sprzetu zrealizowana jest na gołych rejestrach (HAL Puya jest bazowany na halu STM32 i paskudny), za wyjątkiem konfiguracji zegarów, której nie chciało mi się pisać od zera.
Dekoder SBC znalazłem na githubie: http://github.com/google/libsbc/ . Zrobiłem kilka lekkich optymalizacji takich jak zinlinowanie czego się da i wywalenie liczenia sumy kontrolnej.
Na początku było za wolno, zwłaszcza w stereo, ale na szczęście po tych zmianach dekoder okazał się mieścić "na styk" i ostatecznie więcej pary poszło w system plików i optymalne użycie karty SD.
Wykorzystana Puya ma tylko 4kB pamięci ram. Musi się tu zmieścić całkiem spory stos dla dekodera SBC, bufory dla lewego i prawego kanału, bufor dla danych i cache sektora karty SD.
Z powodu tycich buforów nie ma mowy o żadnych opóźnieniach: po rozpoczęciu odtwarzania pliku transmisja z karty musi być możliwie szybka i nieprzerywana.
Mikrokontroler taktuję z częstotliwością 48MHz z użyciem PLLa, którego teoretycznie nie powinno być
Na początku ustawiłem PWM do generowania przebiegu na 176,4kHz, co miało wyciągnąć potencjalny pisk daleko poza zakres słyszenia. Nowa próbka była pobierana co 4 przebiegi, co dawało ostateczną częstotliwość próbkowania 44,1kHz. Niestety taka konfiugracja PWM ograniczała odtwarzanie do 8 bitów - więcej osiągnąć przy taktowaniu 48MHz się po prostu nie dało.
W ramach eksperymetu zrezygnowałem z pobierania próbki co 4 przebiegi i zwiększyłem zakres do 10 bitów. Pisku na 44,1kHz uszy i tak nie słyszą, ale 10bitów już zdecydowanie tak.
Na początku planowałem dodać jakiś podstawowy dithering, ale 10bit okazało się brzmieć na tyle lepiej, że na razie zrezygnowałem z tego pomysłu.
Do próbek wyjściowych stworzyłem 2 bufory cykliczne (dla lewego kanału i prawego) i dekodowanie synchronizuję za pomocą przerwań DMA o połowie transferu i jego końcu.
DMA "odtwarza" połówkę bufora, gdzie kolejna połówka jest wypełniana przez dekoder. Transfer jest cykliczny, więc po jego wykonaniu od razu startuje kolejny.
Karta jest obsługiwana za pomocą sprzetowego SPI o najszybszym możliwym taktowaniu. Przeprowadziłem testy na kilkunastu kartach MicroSD i każda współpracuje, o ile jest to MicroSDHC albo MicroSDXC.
Na szybko dodałem też obsługę starych kart, więc powinno działać praktycznie wszystko. Trzeci kanał DMA jest obecnie nieużywany - rozważam użyć go do przyspieszenia transferów z karty, ale ciężko ogarnąć dwukierunkowe SPI za pomocą jednego transferu.
Jako czujnik światła wykorzystuję dzielnik napięcia z fotorezystorem spięty z ADC, używając ciekawej funkcjonalności "window watchdoga". ADC działa w tle autonomicznie i pilnuje, czy wartość napięcia mieści się w ustalonym przedziale. Jeżeli nie - generuje przerwanie.
W przerwaniu zmieniam stan odtwarzania i ustawiam nowy przedział - inny dla światła i mroku. Dzięki czemu podczas normalnego odtwarzania nie marnuję żadnych cykli procesora na pilnowanie ADC.
Urządzenie zostało zaprojektowane do wspołpracy z tanimi głośniczkami komputerowymi zasilanymi z USB, dlatego posiada wbudowane wyjście zasilania USB, które może się wyłączać razem z odtwarzaniem (drobna oszczędność energii), albo działać cały czas.
Do obsługi systemu plików wybrałem bibliotekę Petit FAT. Nie był to chyba najtrafniejszy wybór, bo ostatecznie musiałem ją trochę rozbudować.
Dodałem:
- Cachowanie zakresów sektorów dla aktualnie otwieranego pliku, co umożliwia streamowanie zawartości pliku bez zerkania co chwilę do FATu. Najoptymistyczniej, bez fragmentacji jedną komendą CMD18 można odczytać cały plik.
- Liczenie elementów w katalogu
- Poruszanie się w katalogu do tyłu
- Stosunkowo szybkie przeskakiwanie do n-tego elementu w katalogu.
- Zapis i przywrócenie aktualnego wewnętrznego stanu Petit FAT, żeby móc w razie potrzeby zapisywać plik stanu bez zamykania odtwarzanego pliku.
Początkowo nie wiedziałem jak zabrać się za implementację losowania utworów. Bardzo chciałem uniknąć powtórek utworów podczas odtwarzania, ale naiwne implementacje (np. trzymanie bitmapy rzeczy już odtworzonych) nie mogły dobrze działać przy tak ograniczonej pamięci RAM.
Ostatecznie, po konsultacji z AI doszedłem do kodu, który można zobaczyć w pliku feistel.cpp. Algorytm robi permutację zbioru możliwych utworów 1...N na zbiór utworów przemieszanych 1...N na podstawie klucza.
Klucz jest albo stały - dostarczony przez użytkownika w pliku konfiguracyjnym (jako seed), albo generowany na podstawie wartości timera kręcącego się cały czas w tle + crc32. Na losowość wpływają opóźnienia karty i czasy wciśnięć przycisku przez użytkownika.
Jako że kolejność odtwarzania jest z góry ustalona przez permutację, można swobodnie nawigować po utworach bez powtórzeń. Oczywiście losowanie można wyłączyć i wtedy utwory odtwarzają się w kolejności skopiowania ich do katalogu. Na sortowanie alfabetyczne zabrakłoby ramu.
W końcowych etapach zabawy dodałem efekt płynnego zgłaśniania i ściszania dźwięku przy zapalaniu i zgaszaniu światła - aby odtwarzanie się muzyki nie było nagłym zaskoczeniem.
W konfiguracji można ustalić jak szybko muzyka ma się zgłaśniać i ściszać (i czy w ogóle) oraz czy ma to się też dziać przy zmianie trybu za pomocą przycisku.
Podczas projektu dużo przesiedziałem na jak optymalniejszym wykorzystaniu karty SD. Na początku spodziewałem się, że czytanie pojedynczego sektora za pomocą CMD17 i sprytne "proszenie" karty o kolejny sektor z wyprzedzeniem, będzie porównywalne do stosowania CMD18 (odczyt wielu sektorów).
W rzeczywistości owszem, na niektórych kartach to działało, ale zaskakująco wiele "złośliwych" kart wykonywało periodycznie jakieś operacje w tle, co ogromnie zwiększało czas oczekiwania na każdy sektor.
No i miałem efekt w postaci 2 minut bezproblemowego odtwarzania, po których następowało 20 sekund kompletnej "sieczki". Wnioskuję, że karty otrzymują swoją klasę wydajności przy komendzie CMD18 i żadnej innej.
Jest też możliwe, że tryb SPI nie gwarantuje niczego i po prostu miałem szczęście, że przy CMD18 karty okazały się wystarczająco szybkie.
Interfejs jest prosty:
- Lewy przycisk, wciśnięcie: zmiana trybu (Auto - Sensor światła/Włączony/Wyłączony). Przytrzymanie: następny folder.
Jeżeli sensor światła jest wyłączony to trybu auto nie ma.
- Prawy przycisk, wciśnięcie: następny utwór. Przytrzymanie: poprzedni utwór.
- Dioda świeci się kiedy utwór powinien być odtwarzany.
Konfiguracja jest możliwa przez utworzenie pliku config.ini na karcie pamięci:
; LooTunes config file
; Enable random playback mode. 0: disabled, 1: enabled
; This affects playback order in subdirectories. Directory selection order is not randomized.
random_mode=1
; Randomization key (32-bit unsigned integer). Change this value to get a different order.
; Set to 0 to generate a random key based on time each time a new directory is being opened.
seed=0
; Light intensity auto power on / power off feature
; Light feature enabled, 0: disabled, 1: enabled, normal, 2: enabled, reversed
; In normal mode, light value higher than on_threshold will turn on the music
; and light value lower than off_threshold will turn off the music.
; In reversed mode, light value lower than on_threshold will turn on music
; and value higher than off_threshold will turn it off.
; Light sensor range is from 0 to 4095, where 0 is the darkest value and 4095 is the brightest.
; When light feature is disabled, player will only have two modes: forced on and forced off.
light_mode=1
on_threshold=2000
off_threshold=500
; USB port power. 0: port always powered off; 1: port always powered on; 2: port powered on during playback.
usb_mode=2
; Fade in / fade out effect
; When turning music on and off, the player can slowly fade in or fade out music in 8 steps.
; This value specifies how long (in ms) each step should take. Set to 0 to start/stop instantly.
fade_in=50
fade_out=100
; If instant_mode_change is set to 1, fade in / fade out will be skipped when changing mode (left button press).
instant_mode_change=0
; Current state saving (on SD). Requires state.bin file to be present in the root directory.
; Save current directory on directory change. Generate write cycle when changing directory.
save_directory=1
; Save current track. Can wear card a little more. Saving track will enable saving directory as well.
save_track=1
; Save current playback mode (light sensor / forced on / forced off). Generate write cycle when changing mode.
save_mode=1
; Jump to next directory after current one is finished
; 0: loop current directory, 1: jump to next directory
jump_next_dir=1Aktualny stan zapisuje się w pliku state.bin. Co jest tam zapisywane można ustawić w konfiguracji. Więcej rzeczy bardzej obciąży kartę, ale przy wspołczesnych kartach z wear levelingiem to wciąż powinny być śladowe ilości zapisów.
Zapis stanu to zawsze nadpisanie jednego sektora na karcie. Brak pliku state.bin powoduje wyłączenie zapisywania stanu.
Schemat i PCB:
Schemat ideowy oraz PCB zostały zaprojektowane w KiCAD, a PCB wyprodukowane w taniej płytkarni. PCB jest dwustronne, każdy normalny producent PCB powinien bezproblemowo je wytworzyć w najtańszej opcji.
Główny mikrokontroler to Puya PY32F002AF15P6TU, używana jako PY32F030. Wszystkie sztuki jakie sprawdziłem działały, ale producent nie gwarantuje istnienia peryferiów których używam.
Całość jest zasilana napięciem 3,3V przez dwa stabilizatory typu "662K" - bardzo tanie i powszechne klony XC6206P332MR. Drugi stabilizator jest wykorzystywany zasilania bufora do dźwięku na 74LVC2G14GW.
Schemat całego wyjścia audio jest niemalże w 100% skopiowany z Raspberry Pi, tylko tam jest używany droższy NC7WZ16, którego tutaj też można bez żadnych przeróbek wstawić. To tylko bufor, więc brzmi tak samo.
W pierwszej wersji nie było bufora a jedynie filtr dolnoprzepustowy i niestety praca karta pracy pamięci była bardzo wyraźnie słyszalna. Dodanie oddzielnego stabilizatora 3,3v i bufora ZNACZNIE poprawiło jakość dźwięku.
Na PCB zrobiłem zwory które można zlutować, żeby pominąć drugi stabilizator i bufor - będzie działać, tylko gorzej.
Jest też zwora umożliwiająca odcięcie masy od mini jacka - chciałem uniknąć pętli masy przy zasilaniu głośników z gniazda USB A urządzenia, ale w praktyce raczej nie słychać róznicy.
Zdaje sobie sprawę, że zastosowany filtr dolnoprzepustowy absolutnie nie pomoże na nośną PWM: 44100Hz. W załączonych próbkach wyraźnie ją widać, jednak w normalnych warunkach nie jest słyszalna.
Z tego co mi wiadomo nie da się skonstruować prostego filtru dolnoprzepustowego na 44100Hz, który nie zmasakrowałby wysokich tonów.
Sterowanie gniazda USB A zrealizowane jest na 2 standardowych mosfetach Si2302 i Si2301. Nie przesadzałbym z obciązeniem tego portu, ale do głośniczków na USB starczy.
Jest zwora, która pozwala pominąć również tę część układu i nie obsadzać tranzystorów - wtedy port będzie zawsze włączony. Można oczywiście w ogóle nie obsadzać tego portu, jeżeli nie jest potrzebny.
Zastosowane układy są na tyle tanie, że gniazda mogą okazać się najdroższym elementem całości.
Programowanie odbywa się przez gniazdo SWD, a do programowania i całego developmentu użyłem chyba najtańszego możliwego programatora: debugprobe opartego na Raspberry Pi Pico. Klona płytki można kupić u Chińczyka już za dolara, a oryginalna też jest bardzo tania.
Jest szansa, że zadziałają inne programatory jak J-Link albo może nawet ST-Link. Programowałem całość za pomocą PyOCD, wcześniej wspomagając się moim "brudnym" forkiem OpenOCD z dodaną obsługą Puya.
Myślałem, że możliwe będzie programowanie płytki przez zwykły adapter USB-TTL, ale wyprowadzone na padach RX i TX zostały podłączone do pinów innych, niż używanych przez wbudowany bootloader...
Jeżeli ktoś porwie się na odtworzenie konstrukcji to chętnie pomogę przy programowaniu.
Obudowa:
Obudowa została zaprojektowana w Blenderze i wykonana w druku 3D. Dzieli się na trzy stosunkowo proste do wydrukowania kawałki, które ciasno szczepiają się za pomocą zatrzasków.
Na PCB zostawiłem 2 otwory montażowe które w ostatecznie się nie przydały - wszystko siedzi stabilnie bez śrubek.
Całość jest prosta w druku, wymagane jest tylko działające chłodzenie, aby wykonać kilka łuków. Ponadto adhezja warstw musi być dobra na tyle, żeby haczyki nie połamały się podczas montażu.
Drukować można zupełnie bez supportów, ustawiając obiekty płasko, aby nic - poza wcięciom na porty - nie wisiało w powietrzu. Dzięki temu, że trzy elementy drukuje się oddzielnie, można uzyskać różnorodne warianty kolorystyczne bez specjalnej drukarki
Całość waży ok. 8,5g i na szybszej drukarce drukuje się w mniej niż 30min.
Chyba najlepszy efekt daje drukowanie na chropowatej podstawie PEI - nadaje to lekkiej tekstury, która ukrywa niedoskonałości druku.
Montaż:
PCB lutowałem samodzielnie, chociaż za pewną opłatą możnaby poprosić producenta PCB o obsadzenie elementów.
Całość zacząłem od posmarowania wszystkiego topnikiem i pocynowania padów mniejszych elementów.
Następnie przylutowałem MCU, pozycjonowanie nie było idealne ale stwierdziłem że poprawię je przy kolejnym etapie.
Umieściłem PCB na małym hotplate i włączyłem grzanie. Cyna roztopiła się i z pewnymi korektami przy pomocy pęsety, elementy osadziły się na swoich miejscach.
Na końcu przylutowałem grotem elementy przewlekane, port USB, przyciski i slot na kartę pamięci.
Po montażu elementów warto sprawdzić czy jest generowane poprawnie napięcie 3,3V. Jeżeli tak jest, można podłączyć programator/debugger i wgrać oprogramowanie.
Po włożeniu odpowiednio przygotowanej karty (i włączeniu odpowiedniego trybu) muzyka powinna zacząć grać.
Montaż obudowy zaczynam od włożenia PCB do środkowego elementu (body). PCB musi satysfakcjonująco wskoczyć w swoją szczelinę, a przyciski zacząć klikać.
Następnie wciskam "kapsel" na górze oraz ostrożnie podstawę, co kończy montaż urządzenia.
Koszty:
Koszty jakie poniosłem były mniejsze, bo kupowałem części w większych ilościach.
Drobnicy takiej jak rezystory i kondensatory SMD nie rozpisuję - wyszłyby centy, a kupowałem je do wielu róznych projektów.
Podobnie jest ze zużytym filamentem - filamenty już miałem, a kilogramową szpulę da się kupić nawet za 20zł. Kompletna obudowa waży 8,5g.
Przeliczając, za 5 sztuk grajków:
- PCB $4.17 (JLCPCB: $2 PCB, transport i podatki: $2.17) = ~15zł
- MCU: PY32F002AF15P6TU (LCSC, kupiłem swego czasu 300 sztuk): $0.08 * 5 = $0.40 = ~1.46zł
- Gniazdo USB A (na aliexpress) $1 za 10 sztuk = $0.50 = 1.82zł
- Gniazdo USB C (LCSC) $0.06*5 = $0.30 = 1,09zł
- Gniazdo Mini-jack $0.08*5 = $0.40 = ~1,46zł
- Bufor 74LVC2G14GW $0.03 * 5 = $0.15 = 0.55zł
- Przyciski (LCSC) $0.02 szt *2 *5 = $0.20 = 0.73zł
- Czas (inaczej i tak grałbym w grę) $0 = 0zł
Suma: 22,11zł za 5szt = 4,43zł za 1szt.
Oczywiście tanie głośniki do komputera zasilane na USB to dodatkowy koszt: ok. 20zł, a karta SD np. 64GB to 8zł.
Wszystko co potrzebne do stworzenia i rozbudowy projektu: schemat, projekt w KiCAD, obudowę jak i cały firmware udostępniam na swoim githubie:
https://github.com/l0ud/LooTunes
Licencja to MIT.
Dla osób chcących po prostu odtworzyć projekt zapraszam do katalogu assets, gdzie jest już zbudowany FW oraz Gerbery do produkcji PCB:
https://github.com/l0ud/LooTunes/tree/master/assets
W załącznikach udostępniam to samo, pliki najnowsze w momencie pisania wiadomości.
Dodatkowo dodaje 2 próbki dźwięku: nowy utwór typu "kiełbasa", oraz coś o wiekszej dynamice, gdzie będzie bardziej słychać niedoskonałości dźwięku 10bit.
Pierwotnie nagrałem je specjalnie bezstratnie z większym próbkowaniem, żeby pokazać "nośną" pwm na ~44100Hz. Na spektrogramie wyraźnie ją widać:
Niestety forum nie zmieści takich dużych plików. Więc ostatecznie sample umieszczam w mp3 44100Hz do oceny pasma słyszalnego
Pliki do SBC można skonwertować za pomocą FFmpeg, komenda to:
ffmpeg -n -i "plik_wejsciowy.mp3" -ac 2 -c:a sbc -b:a 328k "plik_wyjsciowy.sbc"Na repozytorium umieściłem skrypt bash, który konwertuje wszystkie pliki mp3 oraz flac z katalogu "in" do katalogu "out":
https://github.com/l0ud/LooTunes/blob/master/assets/convert.sh
W głównym katalogu powinnien się znaleźć plik config.ini oraz state.bin (jeżeli chcemy przechowywać aktualny stan). Pliki .sbc muszą znajdywać się w podkatalogach. Jeżeli nie chcemy dzielić muzyki na foldery, należy i tak utworzyć co najmniej jeden folder i tam umieścić te pliki.
Krótkie wideo demonstrujące działanie:
Zapraszam do komentowania ale nie zjedzcie mnie, bo to tutaj moje pierwsze DIY
Fajne? Ranking DIY