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");
}
}