Skip to content
April 9, 2012 / Mr Megahertz

YM2149 sound generator, Arduino and fast pin switching

I spent my childhood with an Atari STe and its bleepy sounds. Recently, I purchased its sound generator on eBay: the YM2149 chip (made by Yamaha) or the AY-3-8910 (made by General Instruments).

I would like to make a chiptune MIDI synthesizer with it, but I thought it was a good start to play existing tunes.
There is a lot of .ym files available on the Internet. They are dumps of the sixteen YM registers at 50Hz, so it’s quite simple to stream the frames to an Arduino which will update the state of the YM2149. I wrote a short program in C# which sends registers data to the Arduino every 20 milliseconds. The only tricky part was to reorder interleaved data, you can find more details on Leonard’s website.

On the hardware side, the circuit is straightforward: a $3 USB to Serial CP2102 module sending raw register data to a Boarduino clone. Every 16 bytes received (8 registers of 8 bits), data is written to the YM2149. I used a 8 MHz crystal TTL/CMOS oscillator with a 74LS93 divider to generate a 2 MHz clock signal for the YM.

The YM2149 is connected to a Boarduino.

Raw data comes from serial port, then the Arduino generates the timing signals for the YM2149.

The Arduino code is quite simple too, it’s all about reading serial port data and writing it on eight pins. But I had to face an annoying problem: I heard a click sound every frame of data. After some investigations, I read in the YM datasheet that the BDIR pin and the BC1 pin of the control bus must be switched within a 50 nanoseconds time window. So, I had a look to these pins with my oscilloscope: the classic Arduino digitalwrite() method is really slow for a control bus!

Pin switching with digitalwrite(): there is a 5μs delay between the two pins, this is a hundred times too slow!

Then, I remembered a post dealing with this issue on Bill Porter’s blog. Let’s look at these lovely low-level tricks! Bill provides two macros to set or clear a specific bit of a given port.

#define CLR(x,y) (x&=(~(1<<y)))
#define SET(x,y) (x|=(1<<y))

These macros read the “x” port state, put its value in memory, change the “y” pin value to 1 or 0 and finally writes the new port value in the port register. I used SET(__BCPORT__,__BDIR__) and SET(__BCPORT__,__BC1__) where __BDIR__ and __BC1__ are the bit number of BDIR and BC1 ports on the port B. Bingo: no more audible clicks!

Pin switching with low-level SET/CLR method: 124 ns of delay, that's better!

But things weren’t perfect, 124ns of delay is not less than 50ns. Why not going a bit further in port manipulation ? The compiler replaces SET(__BCPORT__,__BDIR__) with ( __BCPORT__ |= (1<< __BDIR__ )). I used this form and added __BC__ to this expression:

__BCPORT__ |=   (1 << __BDIR__) + (1 << __BC1__)

The opposite instruction to set BDIR and BC1 to the low state is:

__BCPORT__ &= ~((1 << __BDIR__) + (1 << __BC1__))

As expected, the pin switching is now perfectly simultaneous.

Switching bits with direct port manipulation

Now I know how to talk with this chip, the next step is to make a MIDI synth with it. I can’t wait to hear those three oscillators in a fat unison lead sound. Stay tuned! :)


Update: Here are the schematics, made for a breadboard.

YM2149 + Breadboard Ardiuno

About these ads

32 Comments

Leave a Comment
  1. jumpingpolarbear / Apr 9 2012 11:12 pm

    The word “Atari” has old school and nostalgia written all over it :).

  2. vindolin / Apr 10 2012 11:51 am

    upvote for jochen hippel! :P

  3. ScottInNH / Apr 10 2012 7:54 pm

    Well done sir! I may have to borrow the YM chip from my old Atari ST and try this.

    • Oryx / Apr 10 2012 9:29 pm

      Thanks!
      There is a lot of these chips on eBay too. ;)

      • ScottInNH / Apr 10 2012 9:55 pm

        Will you be posting a schematic for this? I can almost make it out from the overhead photo, but the circuit is still somewhat ambiguous to me.

    • Oryx / Apr 11 2012 7:48 pm

      Schematics added. :)

  4. arnaudcarre / Apr 10 2012 11:51 pm

    Nice stuff! Good to see my STSound / YM file format stuff is still alive :)

    • Coda / Apr 11 2012 4:24 pm

      Nice to see you’re still alive and kicking Leonard! :)

  5. Coda / Apr 11 2012 4:22 pm

    I don’t like the arduino platform myself (played with it for a while, found it too clunky and the IDE pointless), but I did get into AVR programming with AVR-GCC on MacOSX. My sole motivation was to build a YM bedside alarm clock (just you wait it will appear on hackaday from someone else now :/), but instead I got inspired and designed and built a working floppy drive emulator for my Atari ST. Sidetrackeded lol.

    BTW – those of you looking for YM chips; don’t overlook the AY-3-8912, which is the same chip minus one of the parallel ports. It was in the 8-bit Sinclair computers from the 128k and up. Some of the 128k Spectrum games (Glider Rider 128, some Monty games etc) have some nice tunes too.

    • Oryx / Apr 11 2012 4:31 pm

      I plan to use the IO ports to control a VCF in my future YM synth. ;)

  6. Luke Dunwell / Apr 15 2012 5:40 pm

    I have some AY-3-8910 chips here, which will be used for a MIDI synth. As I had no way to test them so far (I didn’t even know if they were genuine) your post really helped out!

    After testing them for a first time I had a closer look at the code, and made some optimisations.
    1. Arduino code rewrite and load from PROGMEM (this was because the C# streaming thing was too slow and I’m quicker with C than C#) (the changes might be interesting for the uart-streamer anyway)
    2. A C# program to create hex-files from YM source, to fill your arduino mem.

    IMPORTANT:
    – Don’t use arrays greater than blah[32767]: it won’t work.
    – Need a YM-Arduino-Player? Connect a FAT16 SD to it and combine my 2 pastebin posts.
    – No ducks harmed in this production.

    Links:

    http://pastebin.com/Bs3nyEQh

    http://pastebin.com/REKuwzsy

    • Oryx / Apr 16 2012 10:17 am

      Interesting work, thank you!

  7. asselinpaul / May 5 2012 3:57 pm

    Hi, I have two questions,
    Firstly, would a smaller 8MHz crystal be ok for such a build.
    Additionally, where is it connected, the schematic just shows one connection?
    Thanks a lot
    Paul

  8. Pancra / Jul 19 2012 7:59 pm

    Sorry I sent you a private message, will ask you here instead (so everybody can see it)

    Hi! I am trying to do some kind of synth with the AY-3-8910 IC but I am having some problems.
    I am somewhat experienced in programming but not in electronics, and new in Arduino.

    I know you say here that there is a small click sound when writing information to the IC with digitalWrite(), but at this point I want to get ANY tone from it, just to start somewhere

    On the main loop, just to test for sound on Channel A I do this:

    PIECE OF CODE:

    void loop() {
    delay(3000);
    writeICFun(7,62);
    writeICFun(1,50);
    writeICFun(8,15);
    }

    //to write a value to a register I use writeICFun(Reg,Value)

    void writeICFun(int reg,int valor) {
    //address to R01 is 11110001. If reg=1, then we need to add: 11110000 -> 240(dec)
    reg=reg+240;

    inactiveModeFun();
    Serial.println(“Write on Reg number: “);
    writeBusFun(reg); //sends address of register
    latchModeFun();
    inactiveModeFun();
    Serial.println(“Value to write :”);
    writeBusFun(valor); //send value
    writeModeFun();
    inactiveModeFun();
    }

    void writeBusFun(int value) {
    Serial.println(value,BIN);
    for (int x=0;x<8;x++) {
    digitalWrite(dataPins[x],bitRead(value,x));
    }
    Serial.println("-FIN Write-");
    }
    void inactiveModeFun(){
    digitalWrite(bdirPin,0);
    digitalWrite(bc1Pin,0);
    }
    void readModeFun(){
    digitalWrite(bdirPin,0);
    digitalWrite(bc1Pin,1);
    }
    void writeModeFun(){
    digitalWrite(bdirPin,1);
    digitalWrite(bc1Pin,0);
    }
    void latchModeFun() {
    digitalWrite(bdirPin,1);
    digitalWrite(bc1Pin,1);
    }

    Thanks a lot!

    • Luke Dunwell / Jul 20 2012 7:39 am

      in this order, it will not compile.
      Got your function protos at the top? You’re calling functions top to bottom, without protos this cannot be done. please paste full sourcecode, use pastebin or whatever and i offer to check

      • Pancra / Jul 20 2012 9:30 am

        Thanks for offering to check the code! But fortunately it does work now!!! YAY!

        I will paste the entire code in case someone needs it, right now it’s very basic but it does work and you can change the pitch of the note with a pot and play the note with a button.

        For fast pin switching I used a library called digitalWriteFast that facilitates this.

        FULL CODE:

        #include
        //then you can use digitalWriteFast(pin,HIGH/LOW),
        //digitalReadFast(pin), pinMode(pin,INPUT/OUTPUT),
        //digitalWriteFast2(pin,HIGH/LOW), digitalReadFast2(pin),
        //pinMode2(pin,INPUT/OUTPUT)very much as you have used the built-in commands.
        //The object code will not only be faster, but actually smaller as well.

        //—-Para clock en pin 3
        const int freqOutputPin = 3;
        const int ocr2aval = 3;
        const int prescale = 1;
        const float period = 2.0 * prescale * (ocr2aval+1) / (F_CPU/1.0e6);
        const float freq = 1.0e6 / period;
        //———

        //Pins de control del chip:
        const int dataPins[] = {4,5,6,7,8,9,10,11};
        const int bc1Pin=12;
        const int bdirPin=13;
        //——-

        const int pitchPin=A0;
        float oldPitch=-1;
        int oldPitchDec=-1;
        const int playPin=A1;

        void setup() {
        //—para clock
        pinMode(freqOutputPin, OUTPUT);
        Serial.begin(9600);
        TCCR2A = ((1 << WGM21) | (1 << COM2B0));
        TCCR2B = (1 << CS20);
        TIMSK2 = 0;
        OCR2A = ocr2aval;

        Serial.print("Period = ");
        Serial.print(period);
        Serial.println(" microseconds");
        Serial.print("Frequency = ");
        Serial.print(freq);
        Serial.println(" Hz");
        //—-

        pinMode (bc1Pin,OUTPUT);
        pinMode (bdirPin,OUTPUT);
        pinMode (pitchPin,INPUT);
        Serial.begin(9600);
        inactiveModeFun();
        setDataOutFun();
        resetICFun();
        testSoundA();
        }

        void loop() {
        float pitch = analogRead(pitchPin);
        pitch=(pitch/1023)*15;
        int pitchDec=(pitch-int(pitch))*255;

        if (analogRead(playPin) < 16) {
        writeICFun(13,0);
        }

        delay(100);
        if (int(pitch)!=int(oldPitch)) {
        oldPitch=pitch;
        Serial.print("Course: ");
        Serial.println(int(pitch));
        writeICFun(1,int(pitch));
        writeICFun(3,int(pitch));
        writeICFun(5,int(pitch));
        }
        if (pitchDec-oldPitchDec-10){
        oldPitchDec=pitchDec;
        //Serial.print(“Fine: “);
        //Serial.println(pitchDec);
        writeICFun(0,int(pitchDec));
        writeICFun(2,int(pitchDec));
        writeICFun(4,int(pitchDec));
        }

        }

        void writeICFun(int reg,int valor) { //maneja valor integer, lo convierte y lo pasa a la otra

        //inactiveModeFun();
        //Serial.println(“Grabar registro:”);
        writeBusFun(reg); //buscar dir
        latchModeFun();
        inactiveModeFun();
        //Serial.println(“Valor grabado:”);
        writeBusFun(valor); //darle varlor
        writeModeFun();
        inactiveModeFun();
        }
        void writeBusFun(int valor) {
        //Serial.println(valor,BIN);
        for (int x=0;x<8;x++) {
        digitalWriteFast2(dataPins[x],bitRead(valor,x));
        }
        //Serial.println("-FIN Write-");
        }

        byte readICFun(int reg) {
        //inactiveModeFun();
        //Serial.println("Leer registro:");
        writeBusFun(reg); //buscar dir
        latchModeFun();
        inactiveModeFun();
        //Serial.print("Valor leido: ");
        readModeFun();
        byte valorByte=readBusFun(); //mira valores
        inactiveModeFun();
        //Serial.println(valorByte,BIN);
        return valorByte;

        }
        byte readBusFun() {
        setDataInFun();
        byte valorByte=0;
        for (int x=0;x<8;x++) {
        bitWrite(valorByte,x,digitalRead(dataPins[x]));
        //Serial.print("digito: ");
        //Serial.println(bitRead(valorByte,x));
        }
        //Serial.println("-FIN Read-");
        setDataOutFun();
        return valorByte;
        }

        void inactiveModeFun(){
        digitalWriteFast2(bdirPin,LOW);
        digitalWriteFast2(bc1Pin,LOW)
        }
        void readModeFun(){
        digitalWriteFast2(bdirPin,LOW);
        digitalWriteFast2(bc1Pin,HIGH)
        }
        void writeModeFun(){
        digitalWriteFast2(bdirPin,HIGH);
        digitalWriteFast2(bc1Pin,LOW)
        }
        void latchModeFun() {
        digitalWriteFast2(bdirPin,HIGH);
        digitalWriteFast2(bc1Pin,HIGH)
        }
        void setDataOutFun(){
        for (int x=0;x<8;x++) {
        pinMode(dataPins[x],OUTPUT);
        }
        }
        void setDataInFun(){
        for (int x=0;x<8;x++) {
        pinMode(dataPins[x],INPUT);
        }
        }
        void testSoundA(){
        writeICFun(0,0);
        writeICFun(1,6);
        //delay(6);
        writeICFun(2,0);
        writeICFun(3,6);
        writeICFun(4,0);
        writeICFun(5,6);
        writeICFun(6,1);
        writeICFun(7,int(B11111000));
        writeICFun(8,16);
        writeICFun(9,16);
        writeICFun(10,16);
        writeICFun(11,0);
        writeICFun(12,50);
        writeICFun(13,0);
        }
        void resetICFun(){
        for (int x=0;x<6;x++){
        writeICFun(x,0);
        }
        }

  9. chrz / Aug 12 2013 1:17 am

    Hi,

    First of all, very nice project :)

    Unfortunately, I’ve got problem with C# code. There is a problem with parsing ym file how I think.

    ———————————————————-
    YMSerial, simple streamer for YM2149.
    Opening serial port
    Opening file

    Type: -l
    Check string: h5-?
    Unhandled Exception:
    OutOfMemoryException
    [ERROR] FATAL UNHANDLED EXCEPTION: System.OutOfMemoryException: Out of memory
    at (wrapper managed-to-native) object:__icall_wrapper_mono_array_new_specific (intptr,int)
    at System.IO.BinaryReader.ReadBytes (Int32 count) [0x00000] in :0
    at YMSerial.MainClass.ReadAllFrames (System.IO.BinaryReader reader, Int32 frameCount) [0x00000] in :0
    at YMSerial.MainClass.Main (System.String[] args) [0x00000] in :0
    ———————————————————-

    I compiled this code on OSX with mono and on windows with C# express 2010, results are similar….

    File is rather fine:
    ———————————————————-
    ymTool.exe i bubble_bobble1.ym

    Working on “bubble_bobble1.ym”…
    ———————————
    YM File Version.:”YM5!”
    Song Name…….:Bubble Bobble
    Author……….:Tim & Mike Follin
    Comment………:Converted by Frederic Gidouin
    Length……….:0:46 sec
    Player rate…..:50 Hz
    YM Frequency….:2000000 Hz
    Nb VBL……….:2305
    Loop VBL……..:0
    Flags………..:$00000001

    Total file(s) …….:1
    Total command success:1
    Total music time…..:0:00:46
    ———————————————————-

    Any suggestion what can be wrong ?

    • Oryx / Sep 24 2013 10:46 am

      Hi,

      Did you manually unpack the ym file?
      My app doesn’t do it. :)

    • LPDunwell / Sep 24 2013 10:58 am

      suggestions:
      – try a different File (format?)
      – post windows error (this is mono, just for comparison)
      – debug to find out the value of frameCount ehrn calling ReadAllFrames

  10. Makoto Schoppert / Sep 26 2014 11:33 pm

    I realize this is an old post but thought I’d comment. I’ve built pretty much the same thing and the sound quality issues that you mentioned. I always thought this was an issue with the latency from the serial port but now that you pointed out the latency from digitalWrite(), I’m going to have to try modifying my Arduino code.

    Also, you can do away with the crystal oscillator and use the Arduino to provide the clock pulse.

    You have to use Pin 11 (pin 10 or Arduino mega) to get the clock pulse. There’s a code snippet below on how to achieve this.

    The constant “ocr2aval” is the value that controls the frequency. On a 16MHz Arduino, using a value of “3” will generate a 2MHz clock. There’s code that calculates the frequency from the ocr2aval value included as well, which isn’t actually used as part of the program.

    const int prescale = 1;
    const int ocr2aval = 3;

    const float period = 2.0 * prescale * (ocr2aval+1) / (F_CPU/1.0e6);

    const float freq = 1.0e6 / period;

    pinMode(11, OUTPUT);

    // Set Timer 2 CTC mode with no prescaling. OC2A toggles on compare match
    //
    // WGM22:0 = 010: CTC Mode, toggle OC
    // WGM2 bits 1 and 0 are in TCCR2A,
    // WGM2 bit 2 is in TCCR2B
    // COM2A0 sets OC2A (arduino pin 11 on Uno or Duemilanove) to toggle on compare match
    //
    TCCR2A = ((1 << WGM21) | (1 << COM2A0));

    // Set Timer 2 No prescaling (i.e. prescale division = 1)
    //
    // CS22:0 = 001: Use CPU clock with no prescaling
    // CS2 bits 2:0 are all in TCCR2B
    TCCR2B = (1 << CS20);

    // Make sure Compare-match register A interrupt for timer2 is disabled
    TIMSK2 = 0;
    // This value determines the output frequency
    OCR2A = ocr2aval;

Trackbacks

  1. Audio, Arduino and fast pin switching | Arduino Focus | Scoop.it
  2. Playing chiptunes with a YM2149 and optimizing an Arduino - Hack a Day
  3. Génération de son avec le YM2149 (AY-3-8910)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 25 other followers

%d bloggers like this: