اردوينو ZX الطيف لاعب AY

مشغل مستقل للإيقاعات من كمبيوتر ZX Spectrum على Arduino بأقل قدر من التفاصيل.




يبدو أن ألحان Spectrum ستبقى في قلبي إلى الأبد ، حيث أستمع بانتظام إلى أغنياتي المفضلة باستخدام مشغل Bulb الرائع .



لكن ليس من السهل جدًا أن تكون مرتبطًا بجهاز كمبيوتر. لقد قمت مؤقتًا بحل هذه المشكلة باستخدام جهاز الكمبيوتر EEE الذي لا يقل عن رائع. لكنني أردت المزيد من المنمنمات.



أدت عمليات البحث على الإنترنت إلى الجمال التالي:




إنهم سعداء بقاعدة عنصريتهم ، التي تثير ذكريات الحنين ، لكنني أدركت أن كسلتي لن تسمح لي بإكمال مثل هذا المشروع.

كنت بحاجة إلى شيء صغير. والآن - مرشح مثالي تقريبًا:

AVR AY-player
- يقوم بتشغيل ملفات * .PSG
- نظام الملفات المدعم FAT16
- عدد الدلائل في جذر القرص 32
- عدد الملفات في الدليل 42 (إجمالي 32 * 42 = 1344 ملفًا)
- فرز الدلائل والملفات في الدلائل بالحرفين الأولين من الاسم



يبدو المخطط مقبولًا تمامًا من حيث الحجم:


بالطبع ، كان هناك خلل قاتل أفسد الخمول: لا يوجد وضع اختيار عشوائي للتكوين. (ربما يجب عليك فقط أن تطلب من المؤلف إضافة هذه الوظيفة إلى البرامج الثابتة؟).

جفا من السنة ، كنت أبحث عن خيار مناسب ، صبرتي انتهت وقررت العمل.

بناءً على كسلتي الرائعة ، اخترت الحد الأدنى من الإيماءات:
1. نأخذ Arduino Mini Pro ، حتى لا نعبث بالحزام.
2. أنت بحاجة إلى بطاقة SD لتخزين الموسيقى في مكان ما. لذا خذ درع SD.
3. بحاجة إلى معالج موسيقي. الأصغر هو AY-3-8912.

كان هناك خيار آخر لمحاكاة المعالج المشترك برمجياً ، لكني أردت "صوت أنبوب دافئ" ، على أي حال تقريباً.

للتشغيل ، سنستخدم تنسيق PSG.

هيكل تنسيق PSG
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 .

كيف يتم التحويل إلى PSG
1. .
2. [PL].
3. .
4. , , Convert to PSG...
5. 8 , ~1..

لنبدأ بتوصيل بطاقة SD. اقترح الكسل أخذ اتصال قياسي SD-shield واستخدام مكتبة قياسية للعمل مع البطاقة .

الفرق الوحيد هو أنه من أجل الراحة ، استخدمت الإخراج العاشر كإشارة لاختيار بطاقة:


للاختبار ، نأخذ رسمًا قياسيًا :

قائمة ملف رسم على الخريطة
#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();
  }
}

نقوم بتهيئة البطاقة ، كتابة العديد من الملفات هناك ، إلى عمليات الإطلاق ... لا يعمل!
هنا لدي دائمًا - أكثر المهام القياسية - وعلى الفور عضادات.

نأخذ محرك أقراص محمول آخر - (كان محركًا قديمًا على 32 ميجابايت ، نأخذ محركًا جديدًا على 2 جيجابايت) - نعم ، لقد نجح ، ولكن بعد فترة. نصف ساعة من خدش الجبين ، وإعادة ترتيب الاتصالات بالقرب من الخريطة (بحيث تكون الموصلات أقصر) ، ومكثف عزل للتغذية - وبدأ العمل في 100 ٪ من الحالات. حسنًا ،

دعنا ننتقل ... الآن نحن بحاجة إلى معالج مشترك - يحتاج إلى تردد ساعة 1.75 ميجاهرتز. بدلاً من لحام مولد على كوارتز 14 ميجا هرتز وتعيين حاجز ، نقضي نصف يوم في قراءة أرصفة على وحدة التحكم الدقيقة ونكتشف أنه من الممكن جعل 1.77 (7) ميجاهرتز صعبة باستخدام PWM سريع :

  pinMode(3, OUTPUT);
  TCCR2A = 0x23;
  TCCR2B = 0x09;
  OCR2A = 8;
  OCR2B = 3; 


بعد ذلك ، نعيد تعيين معالج الموسيقى إلى طرف 2 ، والخط السفلي من ناقل البيانات إلى A0-A3 ، والجزء العلوي إلى 4،5،6،7 ، BC1 إلى pin 8 ، BDIR إلى pin 9. من أجل البساطة ، نقوم بتوصيل مخرجات الصوت في الوضع الأحادي:



على لوحة التوصيل:


املأ الرسم التجريبي (لا أتذكر من أين قمت بسحب المصفوفة)
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();
}

ونسمع نصف ثانية من بعض اللحن الجميل! (في الواقع ، أنا هنا لمدة ساعتين أخريين أبحث عن كيف نسيت إصدار إعادة التعيين بعد التهيئة).

على هذا ، تم الانتهاء من الجزء الحديدي ، وفي البرنامج نضيف مقاطعات 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;
  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 
}

من أجل الاستقلالية الكاملة ، أضفت أيضًا مضخمًا إلى TDA2822M ، ويستهلك المشغل نفسه 90 مللي أمبير ، إلى جانب مكبر الصوت الذي يستهلكه حوالي 200 مللي أمبير ، إذا رغبت في ذلك ، يمكن تشغيله بواسطة البطاريات.



كلا النموذجين معًا:


في هذه المرحلة ، توقفت الآن ، أستمع إلى الموسيقى من النموذج ، أفكر في هذه الحالة التي أود تجميعها. فكرت في توصيل مؤشر ، ولكن بطريقة ما لا أشعر بالحاجة.

التنفيذ لا يزال رطبا ، لأنه الجهاز قيد التطوير ، ولكن بسبب يمكنني التخلي عنه لبضع سنوات في هذه الولاية ، قررت كتابة مقال في المطاردة الساخنة. الأسئلة والاقتراحات والتعليقات والتصحيحات - مرحبا بك في التعليقات.

الأدب المستخدم:

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


All Articles