Arduino ZX Spectrum AY Player
Standalone melody player from the ZX Spectrum computer on the Arduino with a minimum of details.

It seems that the Spectrum melodies will forever remain in my heart, as I regularly listen to my favorite songs using the wonderful Bulbovsky player .

But it is not very convenient to be tied to a computer. I have temporarily solved this problem using an equally remarkable EEE PC. But I wanted even more miniature.

Searches on the Internet resulted in the following beauties:

They are delightful with their element base, which causes nostalgic memories, but I understood that my laziness would not allow me to bring such a project to the end.
I needed something small. And now - almost an ideal candidate:
AVR AY-player
- plays * .PSG files
- supported FAT16 file system
- the number of directories in the root of the disk is 32
- the number of files in the directory is 42 (total 32 * 42 = 1344 files)
- the sorting of directories and files in directories by the first two letters of the name
The scheme looks very acceptable in size:

Of course, there was a fatal flaw that spoiled the idyll: there is no random selection mode. (maybe it was enough just to ask the author to add this feature to the firmware?).
Jwa, I was looking for a suitable option, my patience was over and I decided to act.
Based on my fantastic laziness, I chose the minimum gestures:
1. Take the Arduino Mini Pro, so as not to tinker with the strapping.
2. You need an SD card to store music somewhere. So take the SD-shield.
3. Need a music coprocessor. The smallest is AY-3-8912.
There was another option to emulate the coprocessor programmatically, but I wanted “warm tube sound”, evropochya.
To play we will use the PSG-format.
Let's start by connecting the SD card. Laziness prompted to take the standard connection SD-shield and use the standard library to work with the card .
The only difference is that for convenience I used the 10 output as the map selection signal:

To test, we take the standard sketch :
Format the card, write there some files, launches ... does not work!
Here I have always - the most standard task - and immediately jambs.
We take another flash drive - (it was old for 32Mb, we take a new one for 2Gb) - yeah, it worked, but through time. Half an hour of scratching the forehead, swapping the connections closer to the map (so that the conductors were shorter), the isolation capacitor on the power supply - and it began to work 100% of the time. Okay,
let's go further ... Now we need to start a coprocessor - it needs a clock frequency of 1.75 MHz. Instead of soldering the generator at 14 MHz quartz and putting a divider, spend half a day reading the docks on the microcontroller and find out what can be done hard 1.77 (7) MHz using fast PWM :
Next, we start resetting the music processor to pin 2, lower data bus nibble to A0-A3, upper to 4,5,6,7, BC1 to pin 8, BDIR to pin 9. For simplicity, we connect the audio outputs in mono mode:

On the breadboard:

And we hear half a second of some beautiful melody! (in fact, I'm still looking for two more hours here, as I forgot to release the reset after initialization).
On this, the iron part is finished, and in the software we add interrupts of 50 Hz, reading the file and writing to the coprocessor registers.
For complete autonomy, I also added an amplifier to the TDA2822M, the player itself consumes 90 mA, together with the amplifier - about 200 mA, if desired, it can be powered from batteries.

Both layouts together:

At this stage I stopped for the time being, listening to music from the layout, wondering in which building I would like to collect it. I thought to connect the indicator, but somehow I do not feel the need.
The implementation is still damp, because the device is in development, but since I can throw him for a couple of years in this state, I decided to write an article without delay. Questions, suggestions, comments, corrections - welcome in the comments.
References:

It seems that the Spectrum melodies will forever remain in my heart, as I regularly listen to my favorite songs using the wonderful Bulbovsky player .

But it is not very convenient to be tied to a computer. I have temporarily solved this problem using an equally remarkable EEE PC. But I wanted even more miniature.

Searches on the Internet resulted in the following beauties:

They are delightful with their element base, which causes nostalgic memories, but I understood that my laziness would not allow me to bring such a project to the end.
I needed something small. And now - almost an ideal candidate:
AVR AY-player
- plays * .PSG files
- supported FAT16 file system
- the number of directories in the root of the disk is 32
- the number of files in the directory is 42 (total 32 * 42 = 1344 files)
- the sorting of directories and files in directories by the first two letters of the name
The scheme looks very acceptable in size:

Of course, there was a fatal flaw that spoiled the idyll: there is no random selection mode. (maybe it was enough just to ask the author to add this feature to the firmware?).
Jwa, I was looking for a suitable option, my patience was over and I decided to act.
Based on my fantastic laziness, I chose the minimum gestures:
1. Take the Arduino Mini Pro, so as not to tinker with the strapping.
2. You need an SD card to store music somewhere. So take the SD-shield.
3. Need a music coprocessor. The smallest is AY-3-8912.
There was another option to emulate the coprocessor programmatically, but I wanted “warm tube sound”, evropochya.
To play we will use the PSG-format.
PSG format structure
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 мс.
How to convert to PSG
1. Устанавливаем бульбовский проигрыватель.
2. Открываем плейлист кнопкой [PL].
3. Добавляем мелодии в плейлист.
4. Выбираем мелодию в списке, правой кнопкой вызываем меню, в нём Convert to PSG...
5. Сохраняем желательно под именем не длиннее 8 символов, иначе оно будет отображено не полнос~1.тью.
2. Открываем плейлист кнопкой [PL].
3. Добавляем мелодии в плейлист.
4. Выбираем мелодию в списке, правой кнопкой вызываем меню, в нём Convert to PSG...
5. Сохраняем желательно под именем не длиннее 8 символов, иначе оно будет отображено не полнос~1.тью.
Let's start by connecting the SD card. Laziness prompted to take the standard connection SD-shield and use the standard library to work with the card .
The only difference is that for convenience I used the 10 output as the map selection signal:

To test, we take the standard sketch :
sketch of the file list on the card
#include<SPI.h>#include<SD.h>voidsetup(){
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!");
}
voidloop(){
}
voidprintDirectory(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();
}
}
Format the card, write there some files, launches ... does not work!
Here I have always - the most standard task - and immediately jambs.
We take another flash drive - (it was old for 32Mb, we take a new one for 2Gb) - yeah, it worked, but through time. Half an hour of scratching the forehead, swapping the connections closer to the map (so that the conductors were shorter), the isolation capacitor on the power supply - and it began to work 100% of the time. Okay,
let's go further ... Now we need to start a coprocessor - it needs a clock frequency of 1.75 MHz. Instead of soldering the generator at 14 MHz quartz and putting a divider, spend half a day reading the docks on the microcontroller and find out what can be done hard 1.77 (7) MHz using fast PWM :
pinMode(3, OUTPUT);
TCCR2A = 0x23;
TCCR2B = 0x09;
OCR2A = 8;
OCR2B = 3;
Next, we start resetting the music processor to pin 2, lower data bus nibble to A0-A3, upper to 4,5,6,7, BC1 to pin 8, BDIR to pin 9. For simplicity, we connect the audio outputs in mono mode:

On the breadboard:

Fill in the trial sketch (from where I dragged the array I do not remember)
voidresetAY(){
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);
}
voidsetupAYclock(){
pinMode(3, OUTPUT);
TCCR2A = 0x23;
TCCR2B = 0x09;
OCR2A = 8;
OCR2B = 3;
}
voidsetup(){
setupAYclock();
resetAY();
}
voiday_out(unsignedchar port, unsignedchar 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;
}
unsignedint 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;
}
voidloop(){
delay(20);
pseudoInterrupt();
}
And we hear half a second of some beautiful melody! (in fact, I'm still looking for two more hours here, as I forgot to release the reset after initialization).
On this, the iron part is finished, and in the software we add interrupts of 50 Hz, reading the file and writing to the coprocessor registers.
The final version of the program
#include<SPI.h>#include<SD.h>voidresetAY(){
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);
}
voidsetupAYclock(){
pinMode(3, OUTPUT);
TCCR2A = 0x23;
TCCR2B = 0x09;
OCR2A = 8;
OCR2B = 3;
}
voidsetup(){
Serial.begin(9600);
randomSeed(analogRead(4)+analogRead(5));
initFile();
setupAYclock();
resetAY();
setupTimer();
}
voidsetupTimer(){
cli();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 1250;
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS12);
TIMSK1 |= (1 << OCIE1A);
sei();
}
voiday_out(unsignedchar port, unsignedchar 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;
}
unsignedint playPos = 0;
unsignedint fillPos = 0;
constint 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;
voidloop(){
fillBuffer();
if (playFinished){
fp.close();
openRandomFile();
playFinished = false;
}
}
voidfillBuffer(){
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 playPoswhile (freeSz>0){
byte b = 0xFD;
if (fp.available()){
b = fp.read();
}
playBuf[fillPos] = b;
fillPos++;
if (fillPos==bufSize) fillPos=0;
freeSz--;
}
}
voidprepareFile(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;
voidopenRandomFile(){
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();
}
}
voidinitFile(){
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!");
}
intcountDirectory(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
}
For complete autonomy, I also added an amplifier to the TDA2822M, the player itself consumes 90 mA, together with the amplifier - about 200 mA, if desired, it can be powered from batteries.

Both layouts together:

At this stage I stopped for the time being, listening to music from the layout, wondering in which building I would like to collect it. I thought to connect the indicator, but somehow I do not feel the need.
The implementation is still damp, because the device is in development, but since I can throw him for a couple of years in this state, I decided to write an article without delay. Questions, suggestions, comments, corrections - welcome in the comments.
References:
- 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
- Old school, hardcore, AY-3-8912. "Iron" chiptune with sequential input
- The sound on the chip AY-3-8910 (or Yamaha YM2149F) comes from the ZX Spectrum on the PC via LPT-port
- Homemade SD Card Shield for Arduino
- Software ay players
- AY38910 controlled by Arduino - Basic connections
- AY emulator on Arduino
- AY Player on AVR Atmega8 [ASM / Algorithm Builder]