Автономный проигрыватель мелодий с компьютера 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-формат.
Начнём с подключения SD-карты. Лень подсказала взять стандартное подключение SD-shield и использовать стандартную библиотеку для работы с картой.
Единственное отличие — для удобства использовал 10 вывод в качестве сигнала выбора карты:

Для проверки берём стандартный скетч:
Форматируем карту, пишем туда несколько файлов, запускам… не работает!
Вот у меня так всегда — наистандартнейшая задача — и сразу косяки.
Берём другую флешку — (была старенькая на 32Mb, берём новенькую на 2Gb) — ага, заработало, но через раз. Полчаса чесания лба, перестановка соединений поближе к карте (чтобы проводники были короче), развязочный конденсатор по питанию — и работать стало в 100% случаев. Ладно, едем дальше…
Теперь надо завести сопроцессор — ему нужна тактовая частота 1.75 МГц. Вместо того, чтобы спаять генератор на 14 МГц кварце и поставить делитель, тратим полдня на чтение доков по микроконтроллеру и узнаём, что можно сделать хардовые 1.77(7) МГц, используя быстрый ШИМ:
Далее, заводим сброс музсопроцессора на пин 2, нижний ниббл шины данных на A0-A3, верхний на 4,5,6,7, BC1 на пин 8, BDIR на пин 9. Аудио выходы для простоты подключим в моно режиме:

На макетке:

И слышим полсекунды какой-то прекрасной мелодии! (на самом деле я тут ещё два часа ищу как я забыл отпустить ресет после инициализации).
На этом железная часть закончена, а в программной добавляем прерывания 50 Гц, считывание файла и запись в регистры сопроцессора.
Для полной автономности я ещё добавил усилитель на TDA2822M, сам проигрыватель потребляет 90 мА, вместе с усилителем — около 200 мА, при желании можно питать от аккумуляторов.

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

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

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

Но не очень удобно быть привязанным к компьютеру. Эту проблему я временно решал, используя не менее замечательный 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 мс.
+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.тью.
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 мА, при желании можно питать от аккумуляторов.

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

Вот на этом этапе я пока остановился, музыку слушаю с макетки, раздумываю в каком корпусе я бы хотел это собрать. Думал подключить индикатор, но необходимости как-то не испытываю.
Реализация пока сыровата, т.к. устройство в состоянии разработки, но т.к. я могу его забросить на пару лет в таком состоянии, решил написать статью по горячим следам. Вопросы, предложения, замечания, исправления — приветствую в комментариях.
Использованная литература:
- Generating 1-4 MHz clock on Arduino
- Playing chiptunes with a YM2149 and optimizing an Arduino
- YM2149 sound generator, Arduino and fast pin switching
- ZX Spectrum AY adapter
- Олдскул, хардкор, AY-3-8912. «Железный» чиптюн с последовательным входом
- Звук на чипе AY-3-8910 (или Yamaha YM2149F) родом с ZX Spectrum на PC через LPT-порт
- Самодельный SD Card Shield для Arduino
- Software AY players
- AY38910 controlled by Arduino — Basic connections
- Эмулятор AY на Arduino
- Проигрываель AY на AVR Atmega8 [ASM/Algorithm Builder]
