Автономный проигрыватель мелодий с компьютера 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]