Arduino ZX Spectrum AY Player

Reprodutor de músicas autônomo do computador ZX Spectrum no Arduino com uma quantidade mínima de detalhes.




Parece que as melodias do Spectrum permanecerão para sempre em meu coração, enquanto eu ouço regularmente minhas músicas favoritas usando o maravilhoso Bulb player .



Mas não é muito conveniente estar ligado a um computador. Resolvi temporariamente esse problema usando o não menos maravilhoso EEE PC. Mas eu queria ainda mais miniaturas.



As pesquisas na Internet levaram às seguintes belezas:




Eles são deliciosos por sua base elementar, que evoca lembranças nostálgicas, mas percebi que minha preguiça não me permitiria concluir um projeto assim.

Eu precisava de algo pequeno. E agora - um candidato quase ideal:

AVR AY-player
- reproduz arquivos * .PSG
- sistema de arquivos suportado FAT16
- número de diretórios na raiz do disco 32
- número de arquivos no diretório 42 (total de 32 * 42 = 1344 arquivos)
- classificando diretórios e arquivos em diretórios pelas duas primeiras letras do nome



O esquema parece bastante aceitável em tamanho:


é claro que houve uma falha fatal que estragou o idílio: não há modo de seleção aleatória para a composição. (talvez você deva pedir ao autor para adicionar essa função ao firmware?).

Jva do ano, eu estava procurando uma opção adequada, minha paciência acabou e resolvi agir.

Com base na minha preguiça fantástica, escolhi os gestos mínimos:
1. Tomamos o Arduino Mini Pro, para não mexer no arnês.
2. Você precisa de um cartão SD para armazenar músicas em algum lugar. Então pegue o escudo SD.
3. Precisa de um coprocessador musical. O menor é AY-3-8912.

Havia outra opção para emular o coprocessador programaticamente, mas eu queria um "som de tubo quente", quase de qualquer maneira.

Para a reprodução, usaremos o formato PSG.

Estrutura de formato 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 .

Como converter para PSG
1. .
2. [PL].
3. .
4. , , Convert to PSG...
5. 8 , ~1..

Vamos começar conectando um cartão SD. A preguiça sugeriu a adoção de uma conexão SD-shield padrão e o uso de uma biblioteca padrão para trabalhar com o cartão .

A única diferença é que, por conveniência, usei a décima saída como sinal para escolher uma placa:


para testar, fazemos um esboço padrão :

lista de arquivos de esboço no mapa
#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();
  }
}

Formaizamos o cartão, escrevemos vários arquivos lá, para os lançamentos ... não funciona!
Aqui eu sempre a tenho - a tarefa mais comum - e imediatamente os batentes.

Tomamos outro flash drive - (era antigo em 32Mb, tomamos um novo em 2Gb) - sim, funcionou, mas depois de um tempo. Meia hora coçando a testa, reorganizando as conexões para mais perto do mapa (para que os condutores sejam mais curtos), um capacitor de isolamento para nutrição - e começou a funcionar em 100% dos casos. Ok,

vamos seguir em frente ... Agora precisamos obter um coprocessador - ele precisa de uma frequência de clock de 1,75 MHz. Em vez de soldar um gerador em quartzo de 14 MHz e configurar um divisor, passamos meio dia lendo docas no microcontrolador e descobrimos que é possível dificultar 1,77 (7) MHz usando PWM rápido :

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


Em seguida, redefinimos o processador de música para o pino 2, a menor mordida do barramento de dados para A0-A3, a superior para 4,5,6,7, BC1 para o pino 8, BDIR para o pino 9. Para simplificar, conectamos as saídas de áudio no modo mono:



Na placa de ensaio:


Preencha o esboço da avaliação (não me lembro de onde arrastei a matriz)
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();
}

E ouvimos meio segundo de uma bela melodia! (na verdade, estou aqui por mais duas horas procurando como esqueci de liberar a redefinição após a inicialização).

Com isso, a parte de ferro é concluída e, no software, adicionamos interrupções de 50 Hz, lendo o arquivo e gravando nos registros do coprocessador.

Versão final do programa
#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 
}

Para total autonomia, também adicionei um amplificador ao TDA2822M, o próprio player consome 90 mA, junto com o amplificador consome cerca de 200 mA, se desejado, pode ser alimentado por baterias.



Ambas as maquetes juntas:


Nesta etapa, parei por enquanto, ouvindo música da maquete, pensando em qual caso eu gostaria de montá-la. Pensei em conectar um indicador, mas de alguma forma não sinto a necessidade.

A implementação ainda está úmida, porque o dispositivo está em desenvolvimento, mas porque Posso abandoná-lo por alguns anos nesse estado, decidi escrever um artigo em perseguição. Perguntas, sugestões, comentários, correções - bem-vindo nos comentários.

Literatura usada:

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


All Articles