DIY MP3 player. How to assemble and program a gadget at home.

Hacker

Professional
Messages
1,048
Reputation
9
Reaction score
714
Points
113
The content of the article
  • Components
  • Layout
  • Future program template
  • SD card driver
  • A few words about debugging
  • FatFs
  • Audio codec VS1011E
  • Player implementation
  • Interface
  • Sequence of tracks
  • Finished device
  • What is worth adding

INFO
I remember how in 2004 I got an MP3 player and gave me a complete delight. True, he had only 128 MB of memory, which at that time was already considered modest. In addition, the player had a very bad feature to distort the files recorded on it. As explained in the instructions, this is not a bug, but a "feature", that is, copy protection.
Now, of course, MP3 players are no longer popular and everyone listens to music from their phones, but as a goal for their project, this is a good choice - not trivial, but quite feasible.

So, from my project, I wanted to:
  • the device was (obviously) playing MP3;
  • modern SD cards supported;
  • FAT was used as the file system;
  • the sound quality was acceptable;
  • energy consumption was as low as possible.

Components
I took the inexpensive VS1011E MP3 codec as the basis for the device. In fact, it would be wiser to choose the more advanced VS1053 or VS1063 or the updated version of VS1011 - VS1003 (it has a higher clock speed), they all cost about the same.
However, I did not delve into these subtleties and settled on the first microcircuit that came across. As a controller, I took an STM32F103C8T6 so that you can make a breadboard using a ready-made Blue Pill board, and only then collect everything in a serious way. I chose the TFT screen, the resolution is 128x160 (ST7735). I already have libraries written earlier for it.
The code, as in the case of the phone, we will write in C using the libopencm3 and FatFs libraries.
The device will work simply: read data from a file on a USB flash drive and feed it to the codec, and the codec will do the rest itself.

Layout
Before moving on to the code, it makes sense to assemble a device mockup (I'm generally a fan of debugging programs on real hardware). We take the Blue Pill board and solder the display module with the card holder to it. Soldering allows us not to face the problem of bouncing connections, which can cause a lot of troubles during the debugging stage.
I assembled a test module for VS1011 on a breadboard using an adapter from QNF48 to DIP, the circuit of which I looked at in the datasheet. In fact, it is not necessary to bother like this - you can take a ready-made module. But I didn't have it, and I didn't want to wait.
In the end, I put it all together in a few hours and was ready to move on to the code.

Future program template
Before writing basic functions, it is helpful to initialize the display and keyboard. I already spoke about the display above, and the four-by-four keypad remained from the phone's layout.
The source below contains standard header files, peripheral initialization functions, display and keyboard initialization functions, and at the end the output of the Hello world line.

sd.c
Code:
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/spi.h>
#include "st7735_128x160.h"
#include "st_printf.h"
#include "4x4key.h"

static void key_init(void){
  // Initialize the keyboard four by four
}
static void spi2_init(void){
  //spi2 - vs1011
  /* Configure GPIOs:
   *
   * SCK = PB13
   * MOSI = PB15
   * MISO = PB14
   *
   * for vs1011e
   * VS_CCS PB6
   * VS_RST PB9
   * VS_DREQ PB8
   * VS_DCS PB5
   *
   * /
  ...
}

static void spi1_init(void){
  //spi1 - display
  /* Configure GPIOs:
   *
   * SCK = PA5
   * MOSI = PA7
   * MISO = PA6
   *
   * for st7735
   * STCS PA1
   * DC=PA3
   * RST PA2
   *
   * for SD card
   * SDCS PA0
   * /
}

void main(){
  rcc_clock_setup_in_hse_8mhz_out_72mhz();
  spi1_init();
  spi2_init();
  st7735_init();
  st7735_clear(BLACK);
  st7735_set_printf_color(GREEN,BLACK);
  stprintf("Display is OK!\r\n");
  key_init();
  while(1) __asm__("nop");
}

Also in the makefile you need to add the directory with the library sources and the libraries themselves. Below is a snippet Makefile:
Code:
...
SHARED_DIR = ./inc ./fatfs
CFILES = sd.c
CFILES += st7735_128x160.c st_printf.c
CFILES += 4x4key.c
...

SD card driver
Without a driver, you won't be able to work with SD cards, so let's start with it. Reading and writing SDHC discs occurs in blocks of 512 bytes. Our driver should be able to: write a block to disk, read a block from disk and initialize the disk.

WWW
Finding documentation on working with SD cards via SPI is not a problem.

Nevertheless, there are several important and not very obvious points, the knowledge of which will greatly speed up the writing and debugging of the driver. Firstly, if other devices are sitting on the SPI bus along with SD, then SD must be initialized first, otherwise it will not start.
Secondly, initialization must be done at a sufficiently low bus frequency (about 500 kHz), otherwise the SD does not respond. Then you can turn the frequency to the maximum (I have 36 MHz, which is about 4 Mbit / s).
Third, there are several types of SD cards, and each type has its own initialization. I was guided by the most modern and common SDHC cards now, and my version of the initialization function was written specifically for them.
In the examples on Elm Chan's site, you can find a generic initialization function. Actually, I tried to write the minimum required driver, so it only supports one type of cards, as well as writing and reading one sector at a time. However, during debugging, it became clear that multi-sector read and write is not needed.
Please note that before sending initialization commands on the bus, 80 clock pulses must be transmitted with a high level on the CS pin of the card. This is necessary to switch SD to SPI mode (normal card mode - SDIO). After that, CS is lowered and initialization begins, which I will not dwell on.

sdcard.c
Code:
uint8_t sd_init(){
  uint8_t n, cmd, ty, ocr [4];
  uint16_t i;
  for(n=10; n; n--) spi_xfer(SDSPI,0xff); // 80 dummy clocks
  ty = 0;

  SDCS_DOWN();
  // Enter Idle state
  send_cmd(CMD0, 0);
  // SDHC

  if (send_cmd(CMD8, 0x1AA) == 1){
    // Get trailing return value of R7 response
    for (n = 0; n < 4; n++) ocr[n] = spi_xfer(SDSPI,0xff);
    // The card can work at VDD range of 2.7-3.6V
    if (ocr[2] == 0x01 && ocr[3] == 0xAA){
      // Wait for leaving idle state (ACMD41 with HCS bit)
      i = 0xfff;
      while (--i && send_cmd(ACMD41, 1UL << 30));
      if (i && send_cmd(CMD58, 0) == 0){
        // Check CCS bit in the OCR
        for (n = 0; n < 4; n++) ocr[n] = spi_xfer(SDSPI,0xff);
        ty = (ocr[0] & 0x40) ? CT_SD2 | CT_BLOCK : CT_SD2;
      }
    }
  }
  SDCS_UP();
  return ty;
}

SD cards have an uncomfortable tendency to keep MISO high for a few more CLK cycles after applying low to CS. This is treated by transmitting the 0xFF byte on the bus with a high level on CS. However, in my case, this is not critical.

Below - readand writefrom the file sdcard.c.
Code:
uint8_t sd_read_block(uint8_t *buf, uint32_t lba){
  uint8_t result;
  uint16_t cnt=0xffff;
  SDCS_DOWN(); 
  result = send_cmd (CMD17, lba); // CMD17 datasheet with. 50 and 96
  if (result) {SDCS_UP (); return 5;} // Exit If The Result Is Not 0x00

  spi_xfer (SDSPI, 0xff);
  cnt=0;
  do result=spi_xfer(SDSPI,0xff); while ((result!=0xFE)&&--cnt);
  if(!cnt){SDCS_UP(); return 5;}

  for(cnt=0;cnt<512;cnt++) *buf++=spi_xfer(SDSPI,0xff);
  // Get the bytes of the block from the bus into the buffer
  spi_xfer (SDSPI, 0xff); // Skip the checksum
  spi_xfer (SDSPI, 0xff);
  SDCS_UP();
  spi_xfer (SDSPI, 0xff);
  return 0;
}

uint8_t sd_write_block (uint8_t *buf, uint32_t lba){
  uint8_t result;
  uint16_t cnt=0xffff;
  SDCS_DOWN();
  result = send_cmd (CMD24, lba); // CMD24 datasheet pp. 51 and 97–98
  if (result) {SDCS_UP (); return 6;} // Exit If The Result Is Not 0x00

  spi_xfer (SDSPI, 0xff);
  spi_xfer (SDSPI, 0xfe); // Start of buffer
  for (cnt=0;cnt<512;cnt++) spi_xfer(SDSPI,buf[cnt]); // Данные
  spi_xfer (SDSPI, 0xff);
  spi_xfer (SDSPI, 0xff);
  result=spi_xfer(SDSPI,0xff);
  // result=wait_ready();
  if((result&0x05)!=0x05){SDCS_UP(); return 6;}
  // spi_xfer (SDSPI, 0xff);
  WSPI ();

  // Exit if the result is not 0x05 (Datasheet p. 111)
  // if(wait_ready()==0xFF){SDCS_UP(); return 6;}
  SDCS_UP();
  spi_xfer (SDSPI, 0xff);
  return 0;
}

After I had been digging around with the logic analyzer for a couple of days, this thing began to initialize, read and write.

Now you need to add the library sdcard.cand its header file to the project, and to the function main()- initialize the SD card. And then we remember that SPI1 is configured for a low speed for successful initialization (FCPU / 128 ~ 500 kHz), and it is inconvenient to work with a screen at such a speed. Therefore, we add a function spi1_forsage(void)that, in fact, reinitializes SPI1, but with an increased frequency (FCPU / 2 36 MHz).
Code:
...
static void spi1_forsage(void){
  ...
}
...
void main(){
  ...
  spi1_init();
  ...
  sd_init();
  spi1_forsage();
  ...
}

We now have an SD card, but we also need a file system to work.

A few words about debugging
Previously, I often used the UART output for debugging, but when the device has its own display and the standard output is directed to it, then you don't even need to connect the UART, just use the function stprintf(). It was with her help that I analyzed the calls discio.c.

Below is an example of debug messages in discio.h(debug commands are commented out).
Code:
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count){
  //SDCS_UP();
  //stprintf("d_w(pdrv=%d,sec=%d,cnt=%d\r\n",pdrv,sector,count);
  //SDCS_DOWN();
  uint8_t ret = 0;
  //if(count==1){
    //ret=sd_write_block(buff,sector);
    //stprintf("w_ret_code=%d",ret);
    //if(ret==6) return RES_ERROR;
  //} else return RES_ERROR;

  while(count){
    if(sd_write_block(buff,sector)) return RES_ERROR;
    --count;
    ++sector;
    buff+=512;
  }
  //stprintf("WresOK\r\n");
  return RES_OK;
}

As a result, each time the function is called, the disk_write()arguments passed to it and the return value are displayed on the screen sd_write_block(). For a long time I could not understand why writing to disk via FatFs does not work, although the logic analyzer says that everything is going as it should: both the direct function call sd_write_block()and the subsequent call sd_read_block()showed that writing and reading are working.
It turned out that the function sd_write_block()was writing successfully, but did not return 0 and FatFs considered it a write error. I fixed the error, and the debug messages are commented out.
Also in debugging, the Saleae Logic logic analyzer (more precisely, its Chinese clone) and the program of the same name, which works fine in Linux and helps a lot when debugging protocols, are extremely useful in debugging.

FatFs
To read a file from a card, you must first write it there somehow. And it is most convenient to do this when there is a file system on the card. So we connect it to the computer, format and copy the necessary files.
Writing your own file system driver for the sake of the player is still a bit too much even for me, but there is a FatFs driver written in C and easily portable to anything.

WWW
You can download the source code of FatFs and see a detailed description on the same Elm Chan website.

In order to add FatFs to a project, you need to do a few things. The first is making changes to the file ffconf.h.
Code:
#define FF_CODE_PAGE 866 // 866 - Cyrillic code page
#define FF_USE_LFN 1 // Long name support
#define FF_MAX_LFN 255 // Maximum length of the name, we still have a lot of memory
#define FF_LFN_UNICODE 2 // UTF-8 encoding
#define FF_STRF_ENCODE 3 // UTF-8 encoding
#define FF_FS_NORTC 1 // Stub for the real time function

It's enough. Without Cyrillic support, we will be sad, but I chose UTF-8 encoding, since I use it on the desktop and it greatly simplifies file operations.

Now you need to edit the file diskio.c. The functions in it bind the FatFs to the SD card driver we discussed above. Making the necessary changes.
Code:
...
#include "sdcard.h"
#include "st_printf.h"
...
DSTATUS disk_initialize(BYTE pdrv){
  return 0;
}

This is just a stub, since we initialize the disk manually elsewhere. All this mess is due to the fact that the SD card needs to be initialized first on the bus.
Code:
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count){
  //stprintf("d_r(pdrv=%d,sec=%d,cnt=%d\r\n",pdrv,sector,count);
  while(count){
    if (sd_read_block(buff,sector)) return RES_ERROR;
    --count;
    ++sector;
    buff+=512;
  }
  //stprintf("resOK\r\n");
  return RES_OK;
}

Here it would be possible to add multi-sector reading, this will increase the performance, but for this project this is not so important. The same can be said about recording.
Code:
DRESULT disk_write (
  BYTE pdrv, // Physical Disk Number
  const BYTE * buff, // Data to be written
  LBA_t sector, // Starting sector in LBA
  UINT count // Number of sectors to write
) {
  ...
  while(count){
    if(sd_write_block(buff,sector)) return RES_ERROR;
    --count;
    ++sector;
    buff+=512;
  }
  //stprintf("WresOK\r\n");
  return RES_OK;
}

And the last function that needs to be tweaked is also a stub.
Code:
DRESULT disk_ioctl (
  BYTE pdrv, // Physical Disk Number
  BYTE cmd, // Control code
  void * buff // Buffer for sending and receiving control code
) {
  if(cmd == GET_SECTOR_SIZE) {
    *(WORD*)buff = 512;
    return RES_OK;
  }
  return RES_OK;
}

Now we add the header files ( ff.h) to the project, and the source code ( ff.c, diskio.cand ffunicode.c) to the Makefile. Ready! We now have support for FAT12, 16 and 32 file systems.

WWW
The "Programmer's Notes" blog has a nice article about working with the FatFs library.

Audio codec VS1011E
The audio codec is fairly easy to use. Its interface (SPI) has two modes: command mode (turned on low on CCS) and data mode (turned on low on DCS). That is, from the outside it looks like two independent SPI devices on the bus.
In addition, two more pins, DREQ and RST, are used. With RST, everything is clear - a low level on it causes the chip to reboot. DREQ, on the other hand, indicates the readiness of the chip to receive 32 bytes of data on the bus.
This is a seven-wire connection of the chip, which allows it to be put on the same SPI bus with other devices. However, when assembling and adjusting the layout, it turned out that keeping the display, SD card and VS1011E on the same bus is inconvenient. This is primarily due to the VS1011 bus speed limit. The datasheet indicates that the maximum bus frequency is FCPU / 6, that is, in my case 12 * 2/6 = 4 MHz. This is too slow for the display and memory card, and as a result, the sound will be laggy, which is unacceptable.
Of course, it was possible to dynamically change the bus speed, but I decided to just transfer the codec to the second SPI, since my STM has two of them.

INFO
There is a separate VS1011e SPI AN application note about the connection and communication protocol with the VS1011E, there are even examples of communication functions for different connection options. And the VS1011E Play AN application note will help us in writing the VS1011 driver.
So, in order for our player to play, data must be sent to the codec in batches of 32 bytes and take into account the readiness of the chip to receive data. Conveniently, it will skip MP3 titles, so the file can be transferred in its entirety, which makes things easier.

Let's get started. This is how the functions for reading and writing control registers begin.
Code:
#define VS_CCS_DOWN() gpio_clear(VS_PORT, VS_CCS)
#define VS_CCS_UP() gpio_set(VS_PORT, VS_CCS)
#define DREQ() gpio_get(VS_PORT, VS_DREQ)
#define VS_W_SPI() while(SPI_SR(VS_SPI) & SPI_SR_BSY)

...
// Write to register
void vs_write_sci(uint8_t addr, uint16_t data){
  while (! DREQ ()); // We are waiting for the chip to receive data
  VS_CCS_DOWN (); // Command mode
  spi_xfer (VS_SPI, 2); // 2 - write command
  spi_xfer (VS_SPI, addr); // Register address
  spi_xfer(VS_SPI, (uint8_t)(data>>8));
  spi_xfer(VS_SPI, (uint8_t)(data&0xff));
  VS_CCS_UP();
}

// Read from register
uint16_t vs_read_sci(uint8_t addr){
  uint16_t business;
  while (! DREQ ()); // We are waiting for the chip to receive data
  VS_CCS_DOWN (); // Command mode
  spi_xfer (VS_SPI, 3); // 3 - read command
  spi_xfer (VS_SPI, addr); // Register address
  res=spi_xfer(VS_SPI, 0xff);
  res<<=8;
  res|=spi_xfer(VS_SPI, 0xff);
  VS_CCS_UP();
  return res;
}

Now we need a function for sending data, data is transferred in arrays of up to 32 bytes. Everything here is similar to the previous function, but the chip is switched to data mode, and there is no need to send a special command, you can send data immediately.
Code:
#define VS_DCS_DOWN() gpio_clear(VS_PORT, VS_DCS)
#define VS_DCS_UP() gpio_set(VS_PORT, VS_DCS)
#define DREQ() gpio_get(VS_PORT, VS_DREQ)
#define VS_W_SPI() while(SPI_SR(VS_SPI) & SPI_SR_BSY)

...
uint8_t vs_write_sdi(const uint8_t *data, uint8_t count){
  if(count>32) return 1;
  while(!DREQ());
  VS_DCS_DOWN();
  while(count--) spi_xfer(VS_SPI, *data++);
  VS_W_SPI();
  VS_DCS_UP();
  return 0;
}

Now we can initialize the chip. To do this, he needs to briefly drop RST, and then set the bits SM_SDINEWand SM_RESETthe register SCI_MODE. Finally, you need to set the correct value of the quartz frequency in the register SCI_CLOCKF, for which a convenient macro is used HZ_TO_SCI_CLOCKF(hz). This is important for correct playback speed.
Code:
// This macro for VS1011 will automatically install
// frequency doubling if XTALI <16 MHz
#define HZ_TO_SCI_CLOCKF(hz) ((((hz)<16000000)?0x8000:0)+((hz)+1000)/2000)
#define SCI_MODE    0x00
#define SCI_CLOCKF  0x03
#define SCI_VOL     0x0B
#define SM_RESET    (1<< 2)

uint8_t vs_init(){
  gpio_clear (VS_PORT, VS_RST); // Omit the reset for a while
  VS_CCS_UP (); // Just in case, raise CCS and DCS
  VS_DCS_UP();
  gpio_set (VS_PORT, VS_RST); // Raise the reset

  vs_write_sci(SCI_MODE, SM_SDINEW|SM_RESET); // Устанавливаем
  // data exchange mode and do a software reset,
  // as recommended in datasheet and apnot,
  // specify the frequency of the quartz, since we have a non-standard 12 MHz
  vs_write_sci(SCI_CLOCKF, HZ_TO_SCI_CLOCKF(12000000));

  // Set the volume 6 dB below maximum
  // The maximum volume is 0x0000, the minimum is 0xfefe,
  // high and low bytes are set independently
  // channel volume
  vs_write_sci(SCI_VOL, 0x3f3f);
  return 0;
}

Now you can go directly to playing files.

Player implementation
In the above-mentioned application note VS1011 AN Play there is an example of the player implementation - I was guided by it.
Let's consider how the function works play_file(char *name). We open the MP3 file with the FatFs functions, read 512 bytes from there into the buffer and start sending data from the buffer to the codec in groups of 32 bytes as the chip is ready to receive them. However, waiting for readiness is already in the function vs_write_sdi(), so you don't have to think about it here.

After sending several such packets, you can poll the keyboard and the interface (to add a progress bar, for example). When the buffer is empty, read another 512 bytes and repeat again. If the file ends before the buffer is full, it's not scary, we will give 32 bytes each while there is such an opportunity, and the last packet will be shorter than 32 bytes. To define such cases, we use the macro function min(a,b).
Code:
#define FILE_BUFFER_SIZE 512
#define SDI_MAX_TRANSFER_SIZE 32
#define SDI_END_FILL_BYTES 512 // Any value can be here
#define min(a,b) (((a)<(b))?(a):(b))

uint8_t play_file(char *name){
  ...
  FIL file;
  uint8_t playBuf[512];
  uint16_t bytes_in_buffer, bytes_read, t; // How many bytes are left in the buffer
  uint32_t pos = 0, cnt = 0, fsize = 0; // Position in the file
  uint32_t nread;
  uint16_t sr, dt, min, sec, hdat0;
  uint8_t key,bar=0,bitrate=8;
  ...
  if(f_open(&file, name, FA_READ)) stprintf("open file error!\r\n");
  ...

  do{
    f_read(&file, playBuf, FILE_BUFFER_SIZE, &bytes_read);
    uint8_t *bufP = playBuf;
    bytes_in_buffer=bytes_read;

    do{
      t = min(SDI_MAX_TRANSFER_SIZE, bytes_in_buffer);
      vs_write_sdi(bufP, t);
      bufP += t;
      bytes_in_buffer -= t;
      pos += t;
    } while (bytes_in_buffer);

    cnt++;
    if(cnt>bitrate){
      cnt=0;
      // Poll the keyboard here and draw the interface
    }
  } while(bytes_read==FILE_BUFFER_SIZE);

  return CODE;
}

This is essentially a simple function, if we remove the little things and everything related to the interface from it, but in this form the function is inconvenient, so we will implement the interface.

Interface
I already wrote about the output to the 128 by 160 display on the ST7735 board in the article about the phone. However, for this project, it was necessary to implement support for UTF-8, albeit in a stripped down form. Latin and Cyrillic characters are supported (without the letter ё). This simplified the conversion from CP866 - I just rearranged the symbols in the tables a little, corrected the search for the symbol and added ignoring the codes with the symbols 0xD0 and 0xD1 - the prefixes of the Cyrillic page.

st7735_128x160.c
Code:
oid st7735_drawchar(unsigned char x,unsigned char y,char chr,
                    uint16_t color, uint16_t bg_color){
  ...
  // Added support for Cyrillic UTF-8
  unsigned char c=(chr<0xe0) ? chr - 0x20 : chr - 0x50;
  ...
}

void st7735_string_at(unsigned char x,unsigned char y,
                      unsigned char *chr, uint16_t color,
                      uint16_t bg_color){
  ...
  while(*chr){
#ifdef UTF8ASCII
    if(*chr==0xd0||*chr==0xd1) chr++;
#endif
  }
  ...
}

void st7735_sendchar(char ch){
#ifdef UTF8ASCII
  if (ch == 0xd0 || ch == 0xd1) return; // Ignore prefixes
#endif
  ...
}

Thus, codes up to 0x7F are perceived as ASCII, and others as symbols of a Cyrillic page. The solution, of course, is not universal, and when we meet the letter ё we will see artifacts, but this is the easiest way to ensure compatibility with the locale on the desktop.

For the sake of simplicity, we will also draw a progress bar using text symbols.
Code:
void st7735_progress_bar(uint8_t y,uint8_t value,
                         uint16_t color,uint16_t bgcolor){
  // It looks like this: =====> -------
  char bar [27];
  uint8_t i, count=value*26/256;
  for(i=0;i<count;i++)bar[i]='=';
  bar[count]='>';
  for(i=count+1;i<26;i++)bar[i]='-';
  bar[26]=0;
  st7735_string_at(0,y,bar,color,bgcolor);
}

In addition, stprintf.cI added an output function to the library with formatting to a given string, so it's easier to draw the interface.
Code:
int stprintf_at(uint8_t x, uint8_t y,uint16_t color,
                uint16_t bgcolor, uint8_t size,
                const char *format, ...){
  va_list arg;
  char buffer[128];
  SPRINTF_buffer=buffer;
  va_start (arg, format);
  stprintf_((&putc_strg), format, arg);
  va_end (arg);
  *SPRINTF_buffer ='\0';
  if(size==1)
    st7735_string_at(x,y,buffer,color,bgcolor);
  else if(size==2)
    st7735_string_x2_at(x,y,buffer,color,bgcolor);
  else if(size==3)
    st7735_string_x2_at(x,y,buffer,color,bgcolor);
  return 0;
}

The screen is divided into three sections. The first part - the top 14 text lines, is used to display messages (the name of the current track, errors, and so on). The second part is the 15th line, where the progress bar is located, and the last, 16th line with information about the current track.

The bottom line displays the following data: "KB read / total KB, time from the beginning of the track, mode, track number, total tracks." In the code, it looks like this:
Code:
// Global variables
uint8_t zanuda_mode = 0, rand_mode = 0;
char mode[3]="  ";

...
cnt++;
if(cnt>bitrate){
  //report
  cnt=0;
  dt = vs_read_sci (SCI_DECODE_TIME); // Play time
  hdat0=vs_read_sci(SCI_HDAT0);
  bitrate=(hdat0>>12)&0xF;
  min = dt / 60;
  sec=dt%60;
  bar=255*pos/fsize;
  if(zanuda_mode) st7735_progress_bar(112,bar,GREEN,BLACK);
  else st7735_progress_bar(112,bar,MAGENTA,BLACK);
  if(zanuda_mode) mode[1]='Z';
  else mode[1]=' ';
  if(rand_mode) mode[0]='R';
  else mode[0]='S';
  stprintf_at(0, 120,RED,BLACK,1, "%4d/%dK %d:%02d %s %d/%d",
              pos/1024,fsize/1024, min, sec, mode, track,
              files_count);
  ...
}

After drawing the interface, the keyboard handler goes, the keyboard itself is assembled on the 74HC165D shift register and works similarly to the phone's keyboard from the previous article. The register is polled using software emulation of the SPI protocol. There are no subtleties here.
Code:
uint8_t read_key(void){
  uint8_t data, cnt = 0;
  gpio_clear (HC165_PORT, HC165_CS); // Enable clocking
  gpio_clear (HC165_PORT, HC165_PL); // Write value to shift register
  gpio_set(HC165_PORT, HC165_PL);
  for(uint8_t i=0;i<8;i++){
    data<<=1;
    if(gpio_get(HC165_PORT, HC165_Q7)) data|=1;
    gpio_set(HC165_PORT,HC165_CLK);
    gpio_clear(HC165_PORT,HC165_CLK);
  }
  gpio_set(HC165_PORT,HC165_CS);
  data = ~ data;
  return data;
}

The keyboard press handler reads the state of the keyboard from the register and, depending on the value received, performs the required action. At the moment, the following functions are implemented: quieter / louder, next track / previous track, pause, random play / sequential play, play the current track ( zanuda_mode).

Since the keyboard handler is inside the function play_file(), and the track is selected inside the function loop main(), it becomes necessary to pass the command to the function loop main(). This can be done using the return play_file()values of the function :
  • 0 - next track or next random track;
  • 2 - next track;
  • 1 - previous track.

Sequence of tracks
The above function play_file()requires the full path to the file as input. It is not very convenient to operate with file names, in addition, it can require a significant amount of memory. Therefore, it is reasonable to assign them some numbers.

The f_readdir(&dir, &fileInfo)FatFs library function allows you to get the names of files in a directory . This function reads a directory, writing fileInfoinformation about the file to the structure . Its field fnameis the name of the file. Using it, we can, for example, display a list of files and subdirectories in a directory.
Code:
uint8_t ls(char *path){
  YOU you;
  FILINFO fileInfo;
  if(f_opendir(&dir, path)) return 1;

  stprintf("\a%s\r\n",path);
  for(;;){
    if(f_readdir(&dir, &fileInfo)) return 2;
    if(fileInfo.fname[0] == 0) break;
    if(fileInfo.fattrib & AM_DIR) stprintf("+DIR  %s\r\n", fileInfo.fname);
    else stprintf("+ %s\r\n", fileInfo.fname);
  }
  return 0;
}

It is needed rather for debugging. For our purpose, however, we need a function is_mp3()that determines whether the file actually has a MP3. If successful, it returns zero.

Now we can easily count the MP3 files in the directory and get the file name number N (functions cnt_mp3_in_dir()and get_name_mp3()).
Code:
uint8_t ismp3(char *name){
  uint16_t len;
  len = strlen (name);
  if(!strncmp(name+len-4,".mp3",3)) return 0;
  else return 1;
}

uint16_t cnt_mp3_in_dir (char * path) {
  YOU you;
  FILINFO fileInfo;
  uint16_t count=0;
  if(f_opendir(&dir, path)) return 1;

  //stprintf("\a%s\r\n",path);
  for(;;){
    if (f_readdir(&dir, &fileInfo)) return 2;
    if(fileInfo.fname[0] == 0) break;
    if(!(fileInfo.fattrib & AM_DIR))
    if(!ismp3(fileInfo.fname)) count++;
  }
  return count;
}

uint8_t get_name_mp3(char *path, uint16_t n, char *name){
  YOU you;
  FILINFO fileInfo;
  uint16_t count=0;
  if(f_opendir(&dir, path)) return 1;

  //stprintf("\a%s\r\n",path);
  while(count<n){
    if(f_readdir(&dir, &fileInfo)) return 2;
    if(fileInfo.fname[0] == 0) return 3;
    if(!(fileInfo.fattrib & AM_DIR))
      if(!ismp3(fileInfo.fname))
        count++;
  }
  strcpy(name,fileInfo.fname);
  return 0;
}

It is unreasonable to write a separate function to play a file by number; it is better to make a wrapper over the function play_file().
Code:
uint8_t play_mp3_n (char * path, uint16_t n) {
  char fname[257];
  uint8_t code=0;
  get_name_mp3("/",n,fname);
  code=play_file(fname);
  return code;
}

Random playback deserves special mention. Getting pseudo-random numbers is a very special topic, but it has nothing to do with things like an MP3 player. We'll just use a function rand()from the library stdlib.h, but to get a sequence of random numbers, we need to pass one random number to it. For identical seeds, the sequence will always be the same.
Where to get a random number on the microcontroller? You can take the value from the real-time clock counter, or you can read the signal from the ADC. The first option, in my opinion, is better, but the clock in this project has not yet been implemented. Therefore, it remains to read the signal from the ADC.
The sequence of actions is as follows: turn on the ADC in the fastest and most inaccurate mode and measure the potential at the unused leg of the controller. It is better to connect a conductor of short length to it so that it works as an antenna and catches interference. But this is not necessary, because we are just shuffling the tracks in the player.

Then we turn off the ADC as unnecessary, and transfer the resulting value to the function srand()that will configure the PRNG.
Code:
static uint16_t get_random(void){
  // Get a random number from the ADC
  uint8_t channel=4;
  uint16_t adc = 0;
  rcc_periph_clock_enable(RCC_GPIOA);
  /* Configure GPIOs:
   * sensor PA1
   * /
  gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_ANALOG, GPIO4);
  rcc_periph_clock_enable(RCC_ADC1);
  rcc_set_adcpre(RCC_CFGR_ADCPRE_PCLK2_DIV2);
  / * Make sure the ADC is not working during setup * /
  adc_power_off(ADC1);
  / * Configuring * /
  adc_disable_scan_mode(ADC1);
  adc_set_single_conversion_mode(ADC1);
  adc_disable_external_trigger_regular(ADC1);
  adc_set_right_aligned(ADC1);
  / * We will read the temperature sensor, so we include it * /
  //adc_enable_temperature_sensor();
  adc_set_sample_time_on_all_channels(ADC1, ADC_SMPR_SMP_1DOT5CYC);
  adc_power_on(ADC1);
  / * We are waiting for the start of the ADC * /
  for(uint32_t i = 0; i < 800000; i++) __asm__("nop");
  //adc_reset_calibration(ADC1);
  //adc_calibrate(ADC1);

  adc_set_regular_sequence(ADC1, 1, &channel);
  adc_start_conversion_direct(ADC1);
  / * We are waiting for the end of the conversion * /
  while(!(ADC_SR(ADC1) & ADC_SR_EOC));
  adc=ADC_DR(ADC1);
  adc_power_off(ADC1);
  rcc_periph_clock_disable(RCC_ADC1);
  return adc;
}

main(){
  ...
  init_random=get_random();
  stprintf("ADC random is %d\r\n",init_random);
  srand (init_random); // Initialize PRNG
  ...
}

Finished device
When everything or almost everything that you want has been tested on a mock-up, you can assemble a prototype. For this, two printed circuit boards were made.
In principle, all the details could fit on one double-sided board, but I was too lazy.
Next, the display module was assembled and tested on a breadboard.
Then the codec board was unsoldered and connected to the display board.
And finally, it was all placed in a plexiglass case, which turned out to be too big.
The player sounds pretty decent, but, unfortunately, it consumes a lot (about 60 mA). However, this is not so scary.

What is worth adding
In the future, I plan to add support for ID3 tags, recursive file system search, and support for playlists. Well, now you can assemble your own player and attach whatever your heart desires to it!
 
Top