mohan-bee/mini-mp3-player

Portable, battery-powered MP3 player hardware with USB charging, microSD storage, an MCU, VS10xx audio decoder, headphone output, status controls, and regulated power rails.

Version
1.2.0
License
unset
Stars
0

firmware/src/main.c

#include "board.h"
#include "buttons.h"
#include "decoder.h"
#include "ff.h"
#include "ch32v20x.h"
#include <string.h>

#define MAX_TRACKS 128
static FATFS filesystem;
static FIL song;
static char tracks[MAX_TRACKS][13];
static uint16_t track_count,track_index;
static uint8_t stream[4096];
static UINT stream_size,stream_position;
static uint16_t trailing_zeros;
static uint8_t attenuation=64; // -32 dB on first power-up.
static bool paused,volume_mode,file_open;
static Buttons buttons;

static bool is_mp3(const char *name) {
    size_t n=strlen(name);
    return n>=5 && name[n-4]=='.' && (name[n-3]=='m'||name[n-3]=='M') &&
        (name[n-2]=='p'||name[n-2]=='P') && name[n-1]=='3';
}
static bool scan_card(void) {
    track_count=0;
    if (f_mount(&filesystem,"",1)!=FR_OK) return false;
    DIR directory; FILINFO info;
    if (f_opendir(&directory,"/")!=FR_OK) return false;
    FRESULT result=FR_OK;
    while(track_count<MAX_TRACKS) {
        result=f_readdir(&directory,&info);
        if (result!=FR_OK || !info.fname[0]) break;
        if (!(info.fattrib & (AM_DIR|AM_HID|AM_SYS)) && is_mp3(info.fname) && info.fsize) {
            memcpy(tracks[track_count],info.fname,strlen(info.fname)+1); track_count++;
        }
    }
    f_closedir(&directory);
    if (result!=FR_OK || !track_count) return false;
    // Deterministic alphabetical order of 8.3 names, independent of FAT order.
    for (unsigned i=1;i<track_count;i++) {
        char name[13]; memcpy(name,tracks[i],sizeof name); unsigned j=i;
        while (j && strcmp(tracks[j-1],name)>0) { memcpy(tracks[j],tracks[j-1],13); j--; }
        memcpy(tracks[j],name,13);
    }
    log_text("MP3 tracks: "); log_number(track_count); log_text("\r\n");
    return true;
}
static bool start_track(void) {
    if (file_open) { f_close(&song); file_open=false; }
    // Mute before resetting the old stream. Fresh hardware reset prevents
    // truncated MP3 state from leaking into the next file.
    if (!decoder_volume(254)) return false;
    decoder_off();
    if (f_open(&song,tracks[track_index],FA_READ)!=FR_OK) return false;
    file_open=true; stream_size=stream_position=0; trailing_zeros=2048;
    if (!decoder_init(paused ? 254 : attenuation)) return false;
    log_text("TRACK "); log_number(track_index+1); log_text(" ");
    log_text(tracks[track_index]); log_text("\r\n");
    return true;
}
static void fault(uint8_t code,const char *message) {
    decoder_off(); if (file_open) { f_close(&song); file_open=false; }
    log_text("ERROR "); log_number(code); log_text(": "); log_text(message); log_text("\r\nPLAY retries; switch off before removing battery.\r\n");
    // Fixed numbered blink groups, with debounced retry; no unbounded SPI waits.
    uint32_t origin=millis();
    for (;;) {
        uint32_t phase=(uint32_t)(millis()-origin)%(code*400u+1600u);
        led_set(phase<code*400u && phase%400u<120u);
        if (buttons_poll(&buttons,buttons_raw(),millis()) & BUTTON_PLAY) return;
        delay_ms(1);
    }
}
int main(void) {
    board_init();
    log_text("\r\nMINI MP3 prototype firmware 0.1.0\r\nClock Hz: ");
    log_number(SystemCoreClock); log_text("\r\n");
    if (SystemCoreClock!=72000000) {
        log_text("FATAL: 8 MHz HSE / 72 MHz PLL clock failed.\r\n");
        for (;;) { led_set(true); delay_ms(100); led_set(false); delay_ms(100); }
    }
    for (;;) {
        if (!decoder_init(254)) { fault(3,"VS1003 not responding / wrong version"); continue; }
        if (!scan_card()) { fault(2,"Use FAT16/FAT32 card with root 8.3 .MP3 files"); continue; }
        track_index=0; paused=false; volume_mode=false;
        if (!start_track()) { fault(4,"Track open or decoder initialization failed"); continue; }
        uint32_t progress=millis(),battery_check=millis();
        unsigned low_samples=0;
        uint8_t error=0;
        while (!error) {
            uint32_t now=millis();
            uint8_t pressed=buttons_poll(&buttons,buttons_raw(),now);
            if (pressed & BUTTON_MODE) { volume_mode=!volume_mode; log_text(volume_mode?"VOLUME MODE\r\n":"TRACK MODE\r\n"); }
            if (pressed & BUTTON_PLAY) {
                paused=!paused; progress=now;
                if (!decoder_volume(paused ? 254 : attenuation)) { error=3; break; }
                log_text(paused?"PAUSE\r\n":"PLAY\r\n");
            }
            if (pressed & (BUTTON_PREV|BUTTON_NEXT)) {
                bool next=(pressed & BUTTON_NEXT)!=0;
                if (volume_mode) {
                    if (next && attenuation>=20) attenuation-=4;
                    if (!next && attenuation<=116) attenuation+=4;
                    if (!decoder_volume(paused?254:attenuation)) { error=3; break; }
                    log_text("Attenuation (half-dB): "); log_number(attenuation); log_text("\r\n");
                } else {
                    track_index=(track_index+track_count+(next?1:-1))%track_count;
                    if (!start_track()) { error=4; break; }
                    progress=millis();
                }
            }
            led_set(volume_mode ? now%400<200 : paused ? now%2000<100 : true);
            if ((uint32_t)(now-battery_check)>=1000) {
                battery_check=now; uint16_t mv=battery_mv();
                if (mv==UINT16_MAX) { error=6; break; }
                log_text("Battery mV: "); log_number(mv); log_text("\r\n");
                // No-cell USB operation may read near zero. Protected packs
                // cannot power this MCU at that voltage. This is not pack protection.
                if (mv>1000 && mv<3550) low_samples++; else low_samples=0;
                if (low_samples>=3) { error=5; break; }
            }
            if (paused) { delay_ms(1); continue; }
            if (!vs_ready()) {
                if ((uint32_t)(now-progress)>1000) error=3;
                continue;
            }
            if (stream_position==stream_size) {
                if (f_read(&song,stream,sizeof stream,&stream_size)!=FR_OK) { error=4; break; }
                stream_position=0;
                if (!stream_size) {
                    if (trailing_zeros) {
                        static const uint8_t zeros[32]={0};
                        if (!decoder_send(zeros,sizeof zeros)) { error=3; break; }
                        trailing_zeros-=sizeof zeros; progress=millis(); continue;
                    }
                    // Let the final PCM FIFO drain before resetting for next file.
                    delay_ms(300);
                    track_index=(track_index+1)%track_count;
                    if (!start_track()) { error=4; break; }
                    progress=millis(); continue;
                }
            }
            unsigned count=stream_size-stream_position;
            if (count>32) count=32;
            if (!decoder_send(stream+stream_position,count)) { error=3; break; }
            stream_position+=count; progress=millis();
        }
        fault(error,error==5?"Battery low: switch OFF and charge":error==6?"Battery ADC failed":error==3?"Decoder timeout":"SD read/track error");
    }
}