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.

    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 мс.

    How to convert to PSG
    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:


    Also popular now: