Автономный проигрыватель мелодий с компьютера ZX Spectrum на Arduino с минимальным количеством деталей.
Похоже на то, что спектрумовские мелодии навсегда останутся в моём сердце, так как я регулярно слушаю любимые композиции, используя замечательный бульбовский проигрыватель.
Но не очень удобно быть привязанным к компьютеру. Эту проблему я временно решал, использую не менее замечательный EEE PC. Но хотелось ещё большей миниатюрности.
Поиски в интернете привели на следующих красавцев:
AY-player «XZ-80».
Музыкальный дверной звонок на звуковом сопроцессоре AY8910
Они восхитительны своей элементной базой, которая вызывает ностальгические воспоминания, но я понимал, что моя лень не позволит мне довести такой проект до конца.
Мне нужно было что-то небольшое. И вот — практически идеальный кандидат:
AVR AY-player
-Играет файлы *.PSG
-поддерживаемая файловая система FAT16 (FAT12 в процессе 🙂
-количество каталогов в корне диска 32
-количество файлов в каталоге 42 (итого 32*42=1344 файлов)
-сортировка каталогов и файлов в каталогах по первым двум буквам имени
Схема выглядит весьма приемлемой по размеру:
Конечно же нашлёлся фатальный недостаток, который портил иддилию: нет режима случайного выбора композиции. (возможно стоило просто попросить автора добавить эту функцию в прошивку?).
Джва года я искал подходящий вариант и вот терпение моё кончилось и я решил действовать.
Исходя из моей фантастической лени, я выбрал минимальные телодвижения:
1. Берём Arduino Mini Pro, чтобы не возится с обвязкой.
2. Нужна SD-карта, чтобы где-то хранить музыку. Значит берём SD-shield.
3. Нужен музыкальный сопроцессор. Самый маленький — AY-3-8912.
Был ещё вариант сэмулировать сопроцессор программным путём, но хотелось «тёплого лампового звука», евпочя.
Для воспроизведения будем использовать PSG-формат.
+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 мс.
2. Открываем плейлист кнопкой [PL].
3. Добавляем мелодии в плейлист.
4. Выбираем мелодию в списке, правой кнопкой вызываем меню, в нём Convert to PSG…
5. Сохраняем желательно под именем не длиннее 8 символов, иначе оно будет отображено не полнстью.
Начнём с подключения 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. Аудио выходы для простоты подключим в моно режиме:
На макетке:
#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)); 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){ Serial.print("AY["); Serial.print(rawData[cb],DEC); Serial.print("] = "); Serial.print(rawData[cb+1],HEX); Serial.println(); ay_out(rawData[cb],rawData[cb+1]); cb++; cb++; } if(rawData[cb]==0xff)cb++; if (cb>20*12) { Serial.println("==================================================== ZERO =================================================="); 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;// set entire TCCR1A register to 0 TCCR1B = 0;// same for TCCR1B TCNT1 = 0;//initialize counter value to 0 OCR1A = 1250; TCCR1B |= (1 << WGM12); TCCR1B |= (1 << CS12); // Set CS12 bit for 256 prescaler TIMSK1 |= (1 << OCIE1A); // enable timer compare interrupt 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, всё устройство потребляет около 200 мА, при желании можо питать от аккумуляторов.
Обе макетки вместе:
Вот на этом этапе я пока остановился, музыку слушаю с макетки, раздумываю в каком корпусе я бы хотел это собрать. Думал подключить индикатор, но необходимости как-то не испытываю.
Реализация пока сыровата, т.к. устройство в состоянии разработки, но т.к. я могу его забросить на пару лет в таком состоянии, решил написать статью по горячим следам. Вопросы, предложения, замечания, исправления — приветствую в комментариях.
Использованная литература:
ссылка на оригинал статьи https://geektimes.ru/post/273968/
Добавить комментарий