Pull to refresh

Arduino ZX Spectrum AY Player

Reading time8 min
Views34K
Автономный проигрыватель мелодий с компьютера ZX Spectrum на Arduino с минимальным количеством деталей.




Похоже на то, что спектрумовские мелодии навсегда останутся в моём сердце, так как я регулярно слушаю любимые композиции, используя замечательный бульбовский проигрыватель.



Но не очень удобно быть привязанным к компьютеру. Эту проблему я временно решал, используя не менее замечательный EEE PC. Но хотелось ещё большей миниатюрности.



Поиски в интернете привели на следующих красавцев:




Они восхитительны своей элементной базой, которая вызывает ностальгические воспоминания, но я понимал, что моя лень не позволит мне довести такой проект до конца.

Мне нужно было что-то небольшое. И вот — практически идеальный кандидат:

AVR AY-player
— играет файлы *.PSG
— поддерживаемая файловая система FAT16
— количество каталогов в корне диска 32
— количество файлов в каталоге 42 (итого 32*42=1344 файлов)
— сортировка каталогов и файлов в каталогах по первым двум буквам имени



Схема выглядит весьма приемлемой по размеру:


Конечно же нашёлся фатальный недостаток, который портил идиллию: нет режима случайного выбора композиции. (возможно стоило просто попросить автора добавить эту функцию в прошивку?).

Джва года я искал подходящий вариант, моё терпение кончилось и я решил действовать.

Исходя из моей фантастической лени, я выбрал минимальные телодвижения:
1. Берём Arduino Mini Pro, чтобы не возится с обвязкой.
2. Нужна SD-карта, чтобы где-то хранить музыку. Значит берём SD-shield.
3. Нужен музыкальный сопроцессор. Самый маленький — AY-3-8912.

Был ещё вариант сэмулировать сопроцессор программным путём, но хотелось «тёплого лампового звука», евпочя.

Для воспроизведения будем использовать PSG-формат.

Структура PSG-формата
Offset Number of byte Description
+0 3 Identifier 'PSG'
+3 1 Marker “End of Text” (1Ah)
+4 1 Version number
+5 1 Player frequency (for versions 10+)
+6 10 Data

Data — последовательности пар байтов записи в регистр.
Первый байт — номер регистра (от 0 до 0x0F), второй — значение.
Вместо номера регистра могут быть специальные маркеры: 0xFF, 0xFE или 0xFD
0xFD — конец композиции.
0xFF — ожидание 20 мс.
0xFE — следующий байт показывает сколько раз выждать по 80 мс.

Как конвертировать в PSG
1. Устанавливаем бульбовский проигрыватель.
2. Открываем плейлист кнопкой [PL].
3. Добавляем мелодии в плейлист.
4. Выбираем мелодию в списке, правой кнопкой вызываем меню, в нём Convert to PSG...
5. Сохраняем желательно под именем не длиннее 8 символов, иначе оно будет отображено не полнос~1.тью.

Начнём с подключения SD-карты. Лень подсказала взять стандартное подключение SD-shield и использовать стандартную библиотеку для работы с картой.

Единственное отличие — для удобства использовал 10 вывод в качестве сигнала выбора карты:


Для проверки берём стандартный скетч:

скетч списка файлов на карте
#include <SPI.h>
#include <SD.h>

void setup() {
  Serial.begin(9600);
  Serial.print("Initializing SD card...");

  if (!SD.begin(10)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");

  File root = SD.open("/");
  printDirectory(root);

  Serial.println("done!");
}

void loop() {
}

void printDirectory(File dir) {
  while (true) {
    File entry =  dir.openNextFile();
    if (!entry)  break;
    Serial.print(entry.name());
    if (!entry.isDirectory()) {
      Serial.print("\t\t");
      Serial.println(entry.size(), DEC);
    }
    entry.close();
  }
}

Форматируем карту, пишем туда несколько файлов, запускам… не работает!
Вот у меня так всегда — наистандартнейшая задача — и сразу косяки.

Берём другую флешку — (была старенькая на 32Mb, берём новенькую на 2Gb) — ага, заработало, но через раз. Полчаса чесания лба, перестановка соединений поближе к карте (чтобы проводники были короче), развязочный конденсатор по питанию — и работать стало в 100% случаев. Ладно, едем дальше…

Теперь надо завести сопроцессор — ему нужна тактовая частота 1.75 МГц. Вместо того, чтобы спаять генератор на 14 МГц кварце и поставить делитель, тратим полдня на чтение доков по микроконтроллеру и узнаём, что можно сделать хардовые 1.77(7) МГц, используя быстрый ШИМ:

  pinMode(3, OUTPUT);
  TCCR2A = 0x23;
  TCCR2B = 0x09;
  OCR2A = 8;
  OCR2B = 3; 


Далее, заводим сброс музсопроцессора на пин 2, нижний ниббл шины данных на A0-A3, верхний на 4,5,6,7, BC1 на пин 8, BDIR на пин 9. Аудио выходы для простоты подключим в моно режиме:



На макетке:


Заливаем пробный скетч (откуда утащил массив не помню)
void resetAY(){
  pinMode(A0, OUTPUT); // D0
  pinMode(A1, OUTPUT);
  pinMode(A2, OUTPUT);
  pinMode(A3, OUTPUT); // D3
  
  pinMode(4, OUTPUT); // D4
  pinMode(5, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(7, OUTPUT); // D7
  
  pinMode(8, OUTPUT);  // BC1
  pinMode(9, OUTPUT);  // BDIR
  
  digitalWrite(8,LOW);
  digitalWrite(9,LOW);
  
  pinMode(2, OUTPUT);
  digitalWrite(2, LOW);
  delay(100);
  digitalWrite(2, HIGH);
  delay(100);
  
  for (int i=0;i<16;i++) ay_out(i,0);
}

void setupAYclock(){
  pinMode(3, OUTPUT);
  TCCR2A = 0x23;
  TCCR2B = 0x09;
  OCR2A = 8;
  OCR2B = 3; 
}

void setup() {
  setupAYclock();
  resetAY();
}

void ay_out(unsigned char port, unsigned char data){
  PORTB = PORTB & B11111100;

  PORTC = port & B00001111;
  PORTD = PORTD & B00001111;

  PORTB = PORTB | B00000011;
  delayMicroseconds(1);
  PORTB = PORTB & B11111100;

  PORTC = data & B00001111;
  PORTD = (PORTD & B00001111) | (data & B11110000);

  PORTB = PORTB | B00000010;
  delayMicroseconds(1);
  PORTB = PORTB & B11111100;
}

unsigned int cb = 0;

byte rawData[] = {
    0xFF, 0x00, 0x8E, 0x02, 0x38, 0x03, 0x02, 0x04, 0x0E, 0x05, 0x02, 0x07, 
    0x1A, 0x08, 0x0F, 0x09, 0x10, 0x0A, 0x0E, 0x0B, 0x47, 0x0D, 0x0E, 0xFF, 
    0x00, 0x77, 0x04, 0x8E, 0x05, 0x03, 0x07, 0x3A, 0x08, 0x0E, 0x0A, 0x0D, 
    0xFF, 0x00, 0x5E, 0x04, 0x0E, 0x05, 0x05, 0x0A, 0x0C, 0xFF, 0x04, 0x8E, 
    0x05, 0x06, 0x07, 0x32, 0x08, 0x00, 0x0A, 0x0A, 0xFF, 0x05, 0x08, 0x0A, 
    0x07, 0xFF, 0x04, 0x0E, 0x05, 0x0A, 0x0A, 0x04, 0xFF, 0x00, 0x8E, 0x04, 
    0x8E, 0x05, 0x00, 0x07, 0x1E, 0x08, 0x0F, 0x0A, 0x0B, 0x0D, 0x0E, 0xFF, 
    0x00, 0x77, 0x08, 0x0E, 0x0A, 0x06, 0xFF, 0x00, 0x5E, 0x07, 0x3E, 0x0A, 
    0x00, 0xFF, 0x07, 0x36, 0x08, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x8E, 0x07, 
    0x33, 0x08, 0x0B, 0x0A, 0x0F, 0x0D, 0x0E, 0xFF, 0x04, 0x77, 0x08, 0x06, 
    0x0A, 0x0E, 0xFF, 0x04, 0x5E, 0x07, 0x3B, 0x08, 0x00, 0xFF, 0x07, 0x1B, 
    0x0A, 0x00, 0xFF, 0xFF, 0xFF, 0x02, 0x1C, 0x03, 0x01, 0x04, 0x8E, 0x07, 
    0x33, 0x08, 0x0B, 0x0A, 0x0B, 0x0B, 0x23, 0x0D, 0x0E, 0xFF, 0x04, 0x77, 
    0x08, 0x06, 0x0A, 0x0A, 0xFF, 0x04, 0x5E, 0x07, 0x3B, 0x08, 0x00, 0x0A, 
    0x09, 0xFF, 0x07, 0x1B, 0x0A, 0x00, 0xFF, 0xFF, 0xFF, 0x02, 0x8E, 0x03, 
    0x00, 0x04, 0x0E, 0x05, 0x01, 0x07, 0x18, 0x08, 0x0F, 0x09, 0x0B, 0x0A, 
    0x0E, 0xFF, 0x00, 0x77, 0x02, 0x77, 0x04, 0x8E, 0x06, 0x01, 0x08, 0x0E, 
    0x09, 0x0A, 0x0A, 0x0D, 0xFF, 0x00, 0x5E, 0x02, 0x5E, 0x04, 0x0E, 0x05, 
    0x02, 0x06, 0x02, 0x09, 0x09, 0x0A, 0x0C, 0xFF, 0x02, 0x8E, 0x04, 0x8E, 
    0x07, 0x30, 0x08, 0x00, 0x09, 0x08, 0x0A, 0x0A, 0xFF, 0x02, 0x77, 0xFF,
    0xFF
}

void pseudoInterrupt(){
  while (rawData[cb]<0xFF) {
   ay_out(rawData[cb],rawData[cb+1]);
   cb++;
   cb++;
 }
 if (rawData[cb]==0xff) cb++;
 
 if (cb>20*12)  cb=0;
}

void loop() {
  delay(20);
  pseudoInterrupt();
}

И слышим полсекунды какой-то прекрасной мелодии! (на самом деле я тут ещё два часа ищу как я забыл отпустить ресет после инициализации).

На этом железная часть закончена, а в программной добавляем прерывания 50 Гц, считывание файла и запись в регистры сопроцессора.

Окончательный вариант программы
#include <SPI.h>
#include <SD.h>

void resetAY(){
  pinMode(A0, OUTPUT); // D0
  pinMode(A1, OUTPUT);
  pinMode(A2, OUTPUT);
  pinMode(A3, OUTPUT); // D3
  
  pinMode(4, OUTPUT); // D4
  pinMode(5, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(7, OUTPUT); // D7
  
  pinMode(8, OUTPUT);  // BC1
  pinMode(9, OUTPUT);  // BDIR
  
  digitalWrite(8,LOW);
  digitalWrite(9,LOW);
  
  pinMode(2, OUTPUT);
  digitalWrite(2, LOW);
  delay(100);
  digitalWrite(2, HIGH);
  delay(100);
  
  for (int i=0;i<16;i++) ay_out(i,0);
}


void setupAYclock(){
  pinMode(3, OUTPUT);
  TCCR2A = 0x23;
  TCCR2B = 0x09;
  OCR2A = 8;
  OCR2B = 3; 
}

void setup() {
  Serial.begin(9600);
  randomSeed(analogRead(4)+analogRead(5));

  initFile();

  setupAYclock();
  resetAY();
  setupTimer();
}

void setupTimer(){
  cli();
  
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  OCR1A = 1250;

  TCCR1B |= (1 << WGM12);
  TCCR1B |= (1 << CS12);
  TIMSK1 |= (1 << OCIE1A);

  sei();
}

void ay_out(unsigned char port, unsigned char data){
  PORTB = PORTB & B11111100;

  PORTC = port & B00001111;
  PORTD = PORTD & B00001111;

  PORTB = PORTB | B00000011;
  delayMicroseconds(1);
  PORTB = PORTB & B11111100;

  PORTC = data & B00001111;
  PORTD = (PORTD & B00001111) | (data & B11110000);

  PORTB = PORTB | B00000010;
  delayMicroseconds(1);
  PORTB = PORTB & B11111100;
}

unsigned int playPos = 0;
unsigned int fillPos = 0;
const int bufSize = 200;
byte playBuf[bufSize]; // 31 bytes per frame max, 50*31 = 1550 per sec, 155 per 0.1 sec

File fp;
boolean playFinished = false;

void loop() {
  fillBuffer();
  if (playFinished){
    fp.close();
    openRandomFile();
    playFinished = false;
  }
}

void fillBuffer(){
  int fillSz = 0;
  int freeSz = bufSize;
  if (fillPos>playPos) {
    fillSz = fillPos-playPos;
    freeSz = bufSize - fillSz;
  }
  if (playPos>fillPos) {
    freeSz = playPos - fillPos;
    fillSz = bufSize - freeSz;
  }
  
  freeSz--; // do not reach playPos
  while (freeSz>0){
    byte b = 0xFD;
    if (fp.available()){
      b = fp.read();
    }
    playBuf[fillPos] = b;
    fillPos++;
    if (fillPos==bufSize) fillPos=0;
    freeSz--;
  }
}

void prepareFile(char *fname){
  Serial.print("prepare [");
  Serial.print(fname);
  Serial.println("]...");
  
  fp = SD.open(fname);
  
  if (!fp){
    Serial.println("error opening music file");
    return;
  }  
    
  while (fp.available()) {
    byte b = fp.read();
    if (b==0xFF) break;
  }
 
  fillPos = 0;
  playPos = 0;
  cli();  
  fillBuffer();
  resetAY();
  sei();
}

File root;
int fileCnt = 0;

void openRandomFile(){
  int sel = random(0,fileCnt-1);

  Serial.print("File selection = ");
  Serial.print(sel, DEC);
  Serial.println();
  
  root.rewindDirectory();
  
  int i = 0;
  while (true) {
    File entry =  root.openNextFile();
    if (!entry)  break;

    Serial.print(entry.name());
    if (!entry.isDirectory()) {
      Serial.print("\t\t");
      Serial.println(entry.size(), DEC);
      
      if (i==sel) prepareFile(entry.name());
      i++;
    }
    entry.close();
  }
}

void initFile(){
  Serial.print("Initializing SD card...");
  pinMode(10, OUTPUT);
  digitalWrite(10, HIGH);

  if (!SD.begin(10)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");

  root = SD.open("/");

  // reset AY

  fileCnt = countDirectory(root);
  Serial.print("Files cnt = ");
  Serial.print(fileCnt, DEC);
  Serial.println();
  openRandomFile();

  Serial.print("Buffer size = ");
  Serial.print(bufSize, DEC);
  Serial.println();
  
  Serial.print("fillPos = ");
  Serial.print(fillPos, DEC);
  Serial.println();

  Serial.print("playPos = ");
  Serial.print(playPos, DEC);
  Serial.println();

  for (int i=0; i<bufSize;i++){
    Serial.print(playBuf[i],HEX);
    Serial.print("-");
    if (i%16==15) Serial.println();
  }

  Serial.println("done!");
}

int countDirectory(File dir) {
  int res = 0;
  root.rewindDirectory();
  while (true) {

    File entry =  dir.openNextFile();
    if (!entry)  break;

    Serial.print(entry.name());
    if (!entry.isDirectory()) {
      Serial.print("\t\t");
      Serial.println(entry.size(), DEC);
      res++;
    }
    entry.close();
  }
  
  return res;
}

int skipCnt = 0;

ISR(TIMER1_COMPA_vect){
  if (skipCnt>0){
    skipCnt--;
  } else {
    int fillSz = 0;
    int freeSz = bufSize;
    if (fillPos>playPos) {
      fillSz = fillPos-playPos;
      freeSz = bufSize - fillSz;
    }
    if (playPos>fillPos) {
      freeSz = playPos - fillPos;
      fillSz = bufSize - freeSz;
    }

    boolean ok = false;
    int p = playPos;
    while (fillSz>0){
      byte b = playBuf[p];
      p++; if (p==bufSize) p=0;
      fillSz--;
      
      if (b==0xFF){ ok = true; break; }
      if (b==0xFD){ 
        ok = true; 
        playFinished = true;
        for (int i=0;i<16;i++) ay_out(i,0);
        break; 
      }
      if (b==0xFE){ 
        if (fillSz>0){
          skipCnt = playBuf[p];
          p++; if (p==bufSize) p=0;
          fillSz--;
          
          skipCnt = 4*skipCnt;
          ok = true; 
          break; 
        } 
      }
      if (b<=252){
        if (fillSz>0){
          byte v = playBuf[p];
          p++; if (p==bufSize) p=0;
          fillSz--;
          
          if (b<16) ay_out(b,v);
        } 
      }
    } // while (fillSz>0)
  
    if (ok){
      playPos = p;
    }
  } // else skipCnt 
}

Для полной автономности я ещё добавил усилитель на TDA2822M, сам проигрыватель потребляет 90 мА, вместе с усилителем — около 200 мА, при желании можно питать от аккумуляторов.



Обе макетки вместе:


Вот на этом этапе я пока остановился, музыку слушаю с макетки, раздумываю в каком корпусе я бы хотел это собрать. Думал подключить индикатор, но необходимости как-то не испытываю.

Реализация пока сыровата, т.к. устройство в состоянии разработки, но т.к. я могу его забросить на пару лет в таком состоянии, решил написать статью по горячим следам. Вопросы, предложения, замечания, исправления — приветствую в комментариях.

Использованная литература:

Tags:
Hubs:
Total votes 26: ↑26 and ↓0+26
Comments54

Articles