Автономный проигрыватель мелодий с компьютера 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("tt");
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("tt");
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("tt");
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 мА, при желании можо питать от аккумуляторов.
Обе макетки вместе:
Вот на этом этапе я пока остановился, музыку слушаю с макетки, раздумываю в каком корпусе я бы хотел это собрать. Думал подключить индикатор, но необходимости как-то не испытываю.
Реализация пока сыровата, т.к. устройство в состоянии разработки, но т.к. я могу его забросить на пару лет в таком состоянии, решил написать статью по горячим следам. Вопросы, предложения, замечания, исправления — приветствую в комментариях.
Использованная литература:
- 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
- Робкие попытки сделать проигрыватель на Arduino
Автор: Z80A