📜 ⬆️ ⬇️

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 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 here is an almost perfect candidate:

AVR AY-player
- plays * .PSG files
- supported file system FAT16
- 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)
- Sort directories and files in directories by the first two letters of the name



The scheme looks quite 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 minimal gestures:
1. Take the Arduino Mini Pro, so as not to mess 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 - a sequence of byte pairs of entries in the register.
The first byte is the register number (from 0 to 0x0F), the second is the value.
Instead of a register number there may be special markers: 0xFF, 0xFE or 0xFD
0xFD - the end of the composition.
0xFF - waiting for 20 ms.
0xFE - the next byte shows how many times to wait for 80 ms.

How to convert to PSG
1. Install Bulbovsky player .
2. Open the playlist with the [PL] button.
3. Add melodies to the playlist.
4. Select a melody in the list, right-click on the menu, in it Convert to PSG ...
5. Preserve preferably under the name no longer than 8 characters, otherwise it will not be displayed fully.

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 for convenience I used the 10 output as the map selection signal:


To check, we take a standard sketch :

sketch of the file list on the card
#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(); } } 

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 have a coprocessor - it needs a clock frequency of 1.75 MHz. Instead of soldering the generator on 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 nibble data bus to A0-A3, upper to 4,5,6,7, BC1 to pin 8, BDIR to pin 9. For simplicity, we connect mono mode to:



On the breadboard:


Fill in the trial sketch (from where I dragged the array I do not remember)
 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(); } 

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).

This completes the iron part, 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> 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 } 

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:


Here at this stage I have stopped, I listen to music from the layout, I reflect on which building I would like to collect. 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:

Source: https://habr.com/ru/post/392625/


All Articles