В общем, не стоим на месте -- вот новая версия:

Версия 0

Кроме библиотеки TimerOne и SdFat, для работы теперь ещё требуется библиотека LiquidCrystal из стандартного комплекта IDE для ардуино.
Тут постарался исправить все найденные ошибки, добавил экран и кнопки. Кроме того, теперь в wav-ке в заголовках передаётся имя воспроизводимого файла.
Код:
/*
  SD-картридер подключяется к выводам ардуино:
 * MOSI  - D11
 * MISO  - D12
 * CLK   - D13
 * CS    - D10
 
 Выход - D3
 
 Подключение экрана 1602А (LCD Keypad Shield):
 * LCD RS pin     - D8
 * LCD Enable pin - D9
 * LCD D4 pin     - D4
 * LCD D5 pin     - D5
 * LCD D6 pin     - D6
 * LCD D7 pin     - D7
 * LCD R/W pin    - GND
 * LCD 5V pin     - 5V
 
 Кнопки подключаются на вывод A0 (схема LCD Keypad Shield)
 */

// include the library code:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile romFile;

#define filenameLength    100      // максимальная длина имени файла
char fileName[filenameLength + 1]; // имя текущего файла
char sfileName[13];                // короткое имя текущего файла
int currentFile = 1;               // текущая позиция в директории
int maxFile = 0;                   // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0;                    // признак того, что текущая позиция -- это директория

volatile byte BUFF[256];       // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15;       // индекс читаемого полубита
unsigned int CWB = 0;          // индекс записи в буфер
unsigned int CRB_temp = 0;     // временная переменная для CRB

// Заголовок
byte SB[27] = { 
  0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
  0x31, 0x34, 0x30, 0x32, 0x31, 0x38,             // дата: 140218
  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
  0x20, 0x20, 0x20, 0x00, 0x00 };

int Tpp = 256; // Начальная длительность задержки сигнала в микросекундах (один полупериод)

const int p = 3; // номер пина, на который будет вывод сигнала

const int BT_none   = 0; // константы -- коды нажатой кнопки
const int BT_right  = 1;
const int BT_up     = 2;
const int BT_down   = 3;
const int BT_left   = 4;
const int BT_select = 5;

void setup() {
  pinMode(p, OUTPUT);  // объявляем пин как выход
  pinMode(10, OUTPUT); // CS для SD-картридера
  lcd.begin(16, 2);    // объявляем размер экрана 16 символов и 2 строки

  Timer1.initialize(Tpp);               // инициализировать timer1, и установить период Tpp мкс.
  Timer1.attachInterrupt(SendHalfBit);  // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
  Timer1.stop();

  while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
    printtext("SD-card failed!",0);
    tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
    delay(3000);       // ждем 3 с
  }
  printtext("SD-card is OK.",0);

  sd.chdir();            // устанавливаем корневую директорию SD
  getMaxFile();          // получаем количество файлов в директории
  seekFile(currentFile); // переходим к первому файлу в директории
}

void loop() {
  lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
  lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины -- это для теста...

  int button = getPressedButton(); // какая кнопка нажата?
  switch (button)
  {
  case BT_right: // вход в директорию, или запуск файла на воспроизведение
    if(isDir==1) { //Если это директория, то переход в неё
      sd.chdir(fileName, true);
      getMaxFile();
      currentFile=1;
      seekFile(currentFile);
    } 
    else {         // если не директория -- пробуем воспроизвести файл
      if(romFile.cwd()->exists(sfileName)) {
        printtext("Not ROM-file",1);
        for (int i=0;12;i++){
          if (sfileName[i]=='.'){                 // ищем точку в имени файла
            if ((sfileName[i+1]|0x20)=='r') {     // проверяем следующий символ (расширения) == 'r'|'R'
              if ((sfileName[i+2]|0x20)=='o') {   // == 'o'|'O'
                if ((sfileName[i+3]|0x20)=='m') { // == 'm'|'M'
                  printtext("Playing...",1);
                  if (PlayROM(sfileName, i)==0) { // Передаём короткое имя файла и позицию точки в качестве параметров
                    printtext("Done.",1); // Всё закончилось успешно.
                  }
                  else {
                    printtext("ERROR!",1); // Сообщение об ошибке
                  }
                }
              }
            }
            break;
          }
        }
      } 
      else {
        printtext("No File Selected",1);
      }
      delay(1000);           // ждем 1 с
      printtext(" ",1);      // очищаем строку 1
    }
    break;
  case BT_left: // возврат в корневую директорию, ремарка ниже:
    //SDFat has no easy way to move up a directory, so returning to root is the easiest way. 
    //each directory (except the root) must have a file called ROOT (no extension)
    sd.chdir(true);
    getMaxFile();
    currentFile=1;
    seekFile(currentFile);
    break;
  case BT_up: // вверх по файлам в директории
    currentFile--;
    if(currentFile<1) {
      getMaxFile();
      currentFile = maxFile;
    }
    seekFile(currentFile);
    break;
  case BT_down: // вниз по файлам в директории
    currentFile++;
    if(currentFile>maxFile) { 
      currentFile=1; 
    }
    seekFile(currentFile);
    break;
  case BT_select: // пока что просто сообщение о нажатой кнопке, потом будет выход в настройки...
    printtext("<Select>",0);
    break;
  case BT_none: // ничего не нажато
    break;
  }    
  while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
    delay(50); 
  }
}

//=================================================================
int getPressedButton()  // функция проверки нажатой кнопки
{
  int buttonValue = analogRead(0);
  if (buttonValue < 100) return BT_right;
  else if (buttonValue < 200) return BT_up;
  else if (buttonValue < 400) return BT_down;
  else if (buttonValue < 600) return BT_left;
  else if (buttonValue < 800) return BT_select;
  return BT_none;
}

void printtext(char* text, int l) {  // Вывод текста на экран в строке l с очисткой строки
  lcd.setCursor(0,l);
  lcd.print("                ");
  lcd.setCursor(0,l);
  lcd.print(text);
}

void getMaxFile() { // считаем файлы в текущей директории и сохраняем в maxFile
  romFile.cwd()->rewind();
  maxFile=0;
  while(romFile.openNext(romFile.cwd(),O_READ)) {
    romFile.getName(fileName,filenameLength);
    romFile.close();
    maxFile++;
  }
  romFile.cwd()->rewind();
}

void seekFile(int pos) { // переход на позицию в директории, сохранение имени файла и показ его на экране
  romFile.cwd()->rewind();
  for(int i=1;i<=currentFile;i++) {
    romFile.openNext(romFile.cwd(),O_READ);
    romFile.getName(fileName,filenameLength);
    romFile.getSFN(sfileName);
    isDir = romFile.isDir();
    romFile.close();
  }
  printtext(sfileName,0);       // вывод имени текущего файла
  if (isDir==1) lcd.print('>'); // если это директория, добавляем в конце символ '>'
}

int PlayROM(char FName[], int pnt) // функция вывода файла
{
  tone(p, 500, 100);     // включаем на 500 Гц на 100 мс
  delay(1000);           // ждем 1 с

  if (romFile.open(FName,O_READ)) { // открываем файл. Открылся?
    byte BLs = 0x01;    // начальный блок
    unsigned int Nbt = romFile.fileSize();  // всего байт
    byte BLe = Nbt/256; // всего блоков
    byte BLt;           // осталось блоков
    byte Nst;           // номер строки
    byte St;            // выводимый байт

    byte CSz = 0x00;    // контрольная сумма заголовка
    byte CSs = 0x00;    // контрольная сумма строки
    byte i;
    byte j;

    if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

    int StName = 0;
    for (i=14; i<=21; i++){              // заносим в SB имя файла
      if (StName < pnt) {                // имя ещё не закончилось?
        SB[i] = FName[StName];
        StName++;
      }
      else SB[i] = 0x20;                 // пробелы
    }
    StName = pnt;
    for (i=22; i<=24; i++){              // заносим в SB расширение файла
      StName++;
      SB[i] = FName[StName];
    }

    // Начинаем наполнять буфер
    for (i=0; i<=3; i++){           // преамбула (4*(00H*25+55H*25))
      for (j=0; j<=24; j++){
        BUFF[lowByte(CWB)] = 0x00;
        CWB++;
      }
      for (j=0; j<=24; j++){
        BUFF[lowByte(CWB)] = 0x55;
        CWB++;
      }
    }  

    Timer1.start();    // Запускаем таймер............

    for (BLt=BLe; BLt>=1; BLt--){   // Вывод блоков данных в цикле
      CSz = BLs;
      CSz += BLe;
      CSz += BLt;

      for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
      for (j=0; j<=3; j++)  ToBUFF(0x55);// 55h*4
      ToBUFF(0xE6);                      // E6h*1
      for (j=0; j<=3; j++)  ToBUFF(0x00);// 00h*4

      for (j=0; j<=26; j++){             // заголовок блока
        CSz += SB[j];
        ToBUFF(SB[j]);
      }
      ToBUFF(BLs);                       // начальный блок
      ToBUFF(BLe);                       // конечный блок
      ToBUFF(BLt);                       // осталось блоков

      lcd.setCursor(12, 1);              // выводим на экран кол-во оставшихся блоков
      lcd.print(BLt);
      lcd.print(" ");

      ToBUFF(CSz);                       // контр.сумма заголовка

      for (Nst=0x80; Nst<=0x87; Nst++){   // вывод строк (8 шт.)
        for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
        ToBUFF(0xE6);                     // E6h*1
        CSs = Nst;
        ToBUFF(Nst);                      // номер строки
        CSs += CSz;
        ToBUFF(CSz);                      // контр.сумма заголовка

        // начинаем вывод строки данных
        for (j=0; j<=31; j++){      // цикл на 32 байта
          if (Nbt > 0){             // ещё есть данные?
            St = romFile.read();    // читаем очередной байт из файла
            Nbt--;
          }
          else {                    // нет -- дополняем нулями
            St = 0x00;
          }
          ToBUFF(St);               // передаём считанный байт
          CSs += St;
          if (CRB_temp > CWB) {     // проверка -- не обогнало ли чтение запись?
            Timer1.stop();          // Останавливаем таймер
            CRB = 0;                // Сбрасываем индексы.
            CWB = 0;
            bBit = 15;
            return 1;               // выход из ПП с ошибкой 1.
          }
        }
        ToBUFF(CSs);                // контр.сумма строки
      }  
    }
    romFile.close(); // закрываем файл

    for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
  }
  else tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс

  noInterrupts();              // запрет прерываний
  CRB_temp = CRB;              // сохраняем CRB во временную переменную
  interrupts();                // разрешение прерываний
  while (CRB_temp < CWB) {     // Ждём опустошения буфера
    //delay(Tpp); // попробуем отключить этот delay... :-)
    noInterrupts();            // запрет прерываний
    CRB_temp = CRB;            // обновляем значение CRB
    interrupts();              // разрешение прерываний
  }
  Timer1.stop();  // Останавливаем таймер............
  CRB = 0;        // Сбрасываем индексы.
  CWB = 0;
  bBit = 15;
  return 0;       // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){     // Подпрограмма записи байта в буфер
  noInterrupts();               // запрет прерываний
  CRB_temp = CRB;               // сохраняем CRB во временную переменную
  interrupts();                 // разрешение прерываний
  while (CWB > CRB_temp + 255) {// Если позиция записи больше, чем позиция чтения + размер буфера - 1
    delay(Tpp);            // Задержка (Tpp*1000 мкс = Tpp мс)
    noInterrupts();
    CRB_temp = CRB;        // обновляем значение CRB
    interrupts();
  }
  BUFF[lowByte(CWB)] = SBb;
  CWB++;
}

void SendHalfBit() {  // Подпрограмма вывода полубита по циклу таймера
  byte Pd=PORTD;
  if (bBit & 1){      // проверка индекса полубитов на чётность
    if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
      Pd ^= (1 << p); // инвертируем бит в позиции p
    }
  }
  else{               // нечётный -- просто инвертируем порт
    Pd ^= (1 << p);   // инвертируем бит в позиции p(=3)
  }
  PORTD = Pd;         // вывод в порт p
  if (bBit > 0) {     // правим счётчики полубитов и байтов
    bBit--;
  }
  else{
    bBit = 15;
    CRB++;
  }
}
[свернуть]

Из трёх экранов для ардуин, завалявшихся у меня, решил использовать более-менее стандартный 1602, но вот только он у меня работает не по IIC-интерфейсу... Да и вообще это комплект из экрана и кнопок, именуемый "LCD Keypad Shield", поэтому пришлось разрисовать схему подключения (как смог).

Схема ROM-player

Нажмите на изображение для увеличения. 

Название:	ROM-player.jpg 
Просмотров:	350 
Размер:	47.7 Кб 
ID:	64310
На этой схеме подключение SD-карты смог изобразить только условно, уточнить по подключению карты можно на рисунке zebest или в самом скетче. Кроме того, не расписаны номиналы сопротивлений (они стандартны для LCD Keypad Shield).
[свернуть]

Если надо будет переделать на другое подключение кнопок, то это легко будет сделать минимальной правкой подпрограммы getPressedButton, для другого экрана править скетч понадобится немного больше...

Управление кнопками такое:
- кнопки "вверх" и "вниз" -- выбор файла/директории на SD
- кнопка "влево" -- перейти в выбранную директорию или запустить на воспроизведение файл
- кнопка "вправо" -- перейти в корневую директорию
- кнопка "select" пока не задействована, думаю потом добавить туда вызов настройки, например, изменение скорости воспроизведения.

На экране в верхне строке пишется имя выбранного файла, а в нижней идёт отсчёт секунд от включения ардуинки (для теста делал, потом можно будет убрать). Во время воспроизведения в нижней строке экрана пишется "Playing...", а справа от надписи выводится количество оставшихся блоков.

Скетч с библиотеками в одном архиве: ROM-player_0.zip
Пример вывода: TestWAV38.7z