close-circle
Close
0%
0%

Project 72 - Korg DW-6000 wave memory expansion

An attempt to reverse engineer and modify Korg DW-6000s firmware in order to expand its wave memory.

Similar projects worth following
close
One day I stumbled upon Chris Strellis' Korg DW-6000 256 Waveforms Mod. What a great mod, I thought, but unfortunately Chris didn't want to share his overengineered (at least that's what I was thinking back then...) CPLD design. Furthermore, I like the factory look of my gear, so cutting holes is definitely a no-go. My DW-6000 was broken, so I spent a reasonable amount of time fixing it. Reading schematics gave me this crazy idea, that if I could drive the unused half of upd7810's port B, I could address 16x more waveform memory than the DW does just by soldering 4 wires from the CPU to the ROM chips. That sounds way more simple than using a CPLD chip.

Well, let's see if I manage to reverse-engineer this thing and squeeze some code in 72 remaining bytes...

Korg DW-6000 is a 6 voice polyphonic synthesizer from 1985. It's hybrid, because its tone generators are digital (sample based), but amplifiers and filters are analog. DW-6000 has been superseded by his bigger brother, DW-8000 which featured more waveforms (16 instead of 8), more voices (8 instead of 6), digital effects section (well, I think that the DW-6000's analog chorus is way better than DW-8000's digital delay, but you know, back then - c'mon - it's digital dude, and digital is better).

Internally, DW-6000 consists of 3 main boards:

  • KLM-653 - programmer / assigner board - (this is the board I'm working on)
  • KLM-654 - generator (here be voice ROMs)
  • KLM-655 - analog board

This is how it looks like on a block diagram:

DW-6000 block diagram

The idea (at least from electrical point of view) is fairly simple:

  • replace the stock ROMs (KLM-654, IC29 and IC30 - HN613256, 256 kb) with bigger ones (4 Mb) and fill them with some new waveforms
  • connect four unused address lines (A15-A18) to PB4-PB7 of IC1 (uPD7811, KLM-653)
  • modify DW-6000s firmware to implement bank switching mechanism - add a new parameter (14) with 4 bit resolution (1-16), which will tell the CPU to set a value on PB4-PB7

more paint masteryP.S. I have started this project some time ago and I didn't pay much attention to logging activities, that's why I'm adding a whole bunch of stuff right now and that's also why I'm missing some dates. Once I'm happy with my backlog, I will make this project public and start logging things as they appear.

Adobe Portable Document Format - 2.65 MB - 03/14/2016 at 06:06

eye
Preview
download-circle
Download

Adobe Portable Document Format - 3.16 MB - 03/12/2016 at 18:38

eye
Preview
download-circle
Download

Adobe Portable Document Format - 1.35 MB - 03/12/2016 at 18:38

eye
Preview
download-circle
Download

Nec_UCOM-87AD_family_8-Bit_Microcomputers_UPD78C1X_Users_manual.pdf

The ultimate source of information about uPD78(C)1x

Adobe Portable Document Format - 7.77 MB - 03/12/2016 at 18:38

eye
Preview
download-circle
Download

  • RTFM

    mateusz.kolanski09/27/2017 at 10:57 0 comments

    Just as I thought, there must be a reason behind every piece of code. I don't know MIDI specification by heart, but I had a feeling, that it might be the reason. And (unfortunately) I was right.

    This is an excerpt from the official MIDI specification. The MSB destinguishes between status and data bytes. That's why one does not simply use 8 bits for patch data. Bummer. Using only 2 bits for bank switching doesn't sound good enough to me, so I will probably rewrite the code and use 2 subsequent bytes to carry 2 bits each. Pros? Moar banks. Cons? I won't be able to reuse as much code as currently. Back to square 2.

  • Hello, world!

    mateusz.kolanski09/26/2017 at 20:40 0 comments

    When you're developing something that lives only inside of an emulator, there's a big chance that the code, which seems to work just fine in a debugger, will fail to work on real hardware. That's exactely why I wanted to hook up a logic analyzer and see what my code is doing outside of a silicon chip. Unfortunately due to my non existing safety precautions I have released the magic smoke from the CPU and didn't have a chance to see my code on real hardware. You know the story.

    This time I have taken a slightly different approach. No darn logic analyzers. Real hardware only. This is what I ended up with:

    It's pretty simple: just an octal edge-triggered latch and some LEDs. The latches are connected to PB4..6 and the clock input goes to PC2. As you can see this time I have used some heavy duty tape, just in case :) OK, but will it work?

    Nothing happens so far. Maybe I should change the bank number?

    Nice, looks like a binary five! So it works. Kinda. I have cheated a little bit. You see, the code I wrote gets executed only if I explicitely change the bank number, but if the patch is loaded from the memory (e.g. on power on or patch change), nothing happens. I have to dig a little deeper and find another place to call my bank switching routine. But I think that I already earned a beer:)

    Oh, there's one more thing that can complicate a thing or two. This piece of code:

    	LXI	EA, 2680H   ; EA = $2680 (working patch)
    	LXI	H, 2000H    ; HL = $2000 (patch memory)
    .1AEEH:	LDAX	H	    ; A = (HL)
    	ANI	A, 7FH	    ; highest bit = 0
    	STAX	H	    ; save back to (HL)
    	INX	H	    ; HL++
    	DEQ	EA, H	    ; loop until EA==HL
    	JR	.1AEEH		

    For some strange reason the memory region which contains patches gets ANDed with 0x7F to set the highest bit to zero. If you look at the DW6000 bit map, you can see that no parameter uses the highest byte. Unfortunately my parameter violates this rule:

    Is there any reason behind it? Well, there are two ways to find out: a) analyze the source code or b) disable it and see what happens. Guess which path I will take:)

  • Back on track

    mateusz.kolanski09/19/2017 at 12:25 0 comments

    After over one month, DW6k is working again. Yay! Fortunately only the main CPU was broken and after quick (sorta) replacement, everything seems to work just fine. It took so long, because I have ordered the ICs from China and they stuck at customs.

    The replacement took longer than I expected, mostly because I wanted to install the IC in a machined socket (you know, just in case), but I couldn't fit all the pins at once, so after more than one hour long struggle, I have soldered the chip directly to the PCB.

    For safety's sake I have poured a ton of hot snot on all exposed mains connectors :)

    After replacing the fuse I have connected the board to the PSU, powered it on and... success! It blinks again.

    Another few moments later I have connected other boards and it looks like this lil one is tougher than I thought - works like back in the day.

    More to come soon...

  • We're not dead - errata

    mateusz.kolanski08/09/2017 at 10:50 0 comments

    Some time ago I told you, that we're not dead yet, but.. that's not entirely true. Long story short - I was sniffing the GPIOs with a logic analyzer to see how the new code works and suddenly I have shorted out mains to the chassis. Boom, flash, circuit breaker engaged, but it was too late. Logic analyzer is partially vaporized, my laptop doesn't start anymore (at least my hard drive seems to be intact) and the logic board of DW6k is dead as well... I don't know yet if the analog boards are fine, but first I will try to fix the digital one (mostly off the shelf low cost chips) and try to estimate the damage. upd7810 is fried, no clue about other logic chips. Fail of the week, huh?

  • History of 1-4

    mateusz.kolanski07/30/2017 at 14:27 0 comments

    So, it's been a long time (almost a year), but I finally got back to my little project and done some progress. If you know DW6000, or if your perception level is at least somewhere round 7, you will see that on the picture I posted yesterday I dialed in the new, non existant parameter 14. So, here's the story:

    Before I even started further reverse engineering, I got in touch with Alfred Arnold (creator of the assembler I'm using) and asked him a few questions, because I wanted to get rid of any hardcoded addresses in jump tables. At the beginning I wanted to write a macro which would convert any label into either high or low byte and replace it with a nice DB. Long story short - the answer was pretty simple - all I needed to do was to use DWs instead. I didn't think about it first, because it seemed too easy:)

    before:

    .TBL03:	TABLE
    	JB	
    	DB    29H
            DB    14H

    after: 

    .TBL03:	TABLE
    	JB	
    	DW    .1429H    ; that's an autogenerated label pointing 
                            ; (originally) to 0x1429h
    

    Then I fired up my emulator again (I didn't change anything in it, it just works) and started with setting a watchpoint which would break the execution if anything tries to read the data from any of both of the tables (more on them later). That brought me somewhat closer to what I wanted to do - I ended up knowing more or less where's the code which reads the data from the tables and does the stuff with it.

    Next I just started to change parameter numbers and/or parameter values (incrementing or decrementing them) and just looking into memory window searching for some patterns. And I finally found something: two offsets which are always taking the parameter's value. Another watchpoint set and... bingo! With some backtracking I have finally found the code I was looking for.

    Now, a word about the data tables. There are two tables (related to synthesizer's parameters) and in order to modify the code I had to understand precisely what kind of data is stored in them. The first one is 144 bytes (48x3) long. Each entry represents a parameter number (11..16, 21..26 and so on). 

    The 1st byte holds the information about parameter offset (bits 3-7) relative to the beginning of each patch and its beginning bit (bits 0-2). That makes more sense if you just take a look at the DW6k's service manual (page 6, DW-6000 bit map).

    The high nibble (4-7) of the second byte is a numeric value (0-9) which multiplied by 4 (size of an entry in the second table) gives an offset to the value from the second table. Just a relative pointer to say so. The low nibble holds a value between 0 and 3 which selects an appropriate display subroutine:
    - 0: "normal" 2 digit (max) value (e.g. 0-31)
    - 1: "normal" 2 digit (max) value, incremented (e.g. 0-31 displayed as 1-32)
    - 2: value from 0 to 2 with translation -> 0=16, 1=8, 2=4 (octave selection)
    - 3: value from 0 to 4 with translation -> 0=1, 1=-3, 2=3, 3=4, 4=5 (interval selection)

    The last byte is kind of an index of each parameter with a small twist - "local" (per pach) parameters go from 0x00 to 0x21, "global" parameters (81-83) from 0xF0 to 0xF2 and invalid parameters (like our 14)  are marked with 0xFF.

    The 2nd table isn't so exciting - the 1st byte is the maximum parameter's value, the second one is the bit mask to be applied on the value (after bit shifting) to get the desired value (hope you know what I mean:)). The 3rd one - I have no idea whatsoever, but it hasn't been used in any code which looks interesting to me, so let's just skip it. The last byte is another bitmask used to get or set the value.

    Knowing all of that I took a look at the bit map again to find out where I could store my new parameter. Unfortunately there's no continuous 4 bit space, so I had to use only 3 bits and extend the memory by 8 banks (not 16 as I planned before). The second byte which holds the value of portamento time consumes...

    Read more »

  • A teaser

    mateusz.kolanski08/18/2016 at 21:11 0 comments

    I told you, we're not dead :)

    More to come soon!

  • We're not dead (yet...)

    mateusz.kolanski08/14/2016 at 20:35 0 comments

    It's been a long time, but I needed a break to stop going in circles, gain some distance, energy and new ideas to continue my struggle.

    As you probably now, I've been using a modified MAME/MESS driver written for a handheld console called Game Master. As I started to play with it, I just wanted to have a way to see the DW-6000 firmware running, but as you know, the more you have, the more you want.

    Yesterday I started to write my own MESS driver from scratch. It's not that complicated, once you know what are you doing. After a few hours I ended up with a working skeleton emulator with working (well, sort of) keyboard support and display.

    I'm not going to show you anything now, because the keyboard works (very unreliably and somewhat random) just because of my mistake in coding (what do you know, sometimes it's a good thing) and after I fixed it, it stopped working. As I stated before it's probably because of the debouncing code in DW6000's firmware and that's something I need to investigate and understand now.

    Apart from that I have a nice debugging tool which still needs some work to be fully useful, but right now it can do a thing or two.

    So here's the plan for the nearest future:

    • investigate how the debouncing routine works and 'fix' the keyboard support problem
    • write a function that will be triggered by a press of a numeric key (1-8) and then it will watch the memory for a write of that value - that should help me track down the memory address which holds the number of a parameter or value we want to select

    Keep the fingers crossed!

  • Also sprach DW6000

    mateusz.kolanski03/30/2016 at 11:06 1 comment

    Finally, after numerous attempts I made this thing speak to me!

    But first things first. Currently I'm trying to find the code responsible for scanning and analyzing the keypresses. And I want to do it the easy way (i.e. using the debugger, not by reading /and understanding/ the whole source code). How hard can it be? Harder than I thought.

    Let's take a look at the schematics (I have merged two pages and removed some irrelevant connections).

    The keyboard matrix is driven by port A and port B of uPD7810. The CPU sets a 4 bit value on PB[0..3] which then gets decoded by IC11 and IC12. As you can see, both IC11 and IC12 enable lines are tied together, but IC11 is enabled when PB3 goes high and IC12, when it goes low. So if we set a PB value between 0 and 3, we will put SW lines 8-11 low, while values 8-F are enabling SW lines 0-7. SW lines 0-7 are connected to the keyboard and 8-11 are connected to the switch matrix.

    SW columns are connected to PA through non inverting buffers. They are pulled up, so if no switch is pressed, PA should read 0xFF. DW6000 stores switch states in the external RAM ($27E9-$27EC). Ok, so far so good. Now, I thought that if I modify those addresses (e.g. change $27E9 from 0xFF to 0xEF) I should see some changes on the displays (= display memory - $27E1-$27E6 for 6 digits and $21E7 for the LEDs), right? Wrong. Each time I modified that value, it came back to 0xFF after some time with no change on displays whatsoever. I tried to set some watch- and registerpoints in the debugger, but it wasn't useful, cause the debugger was halting code execution too often.

    I started to browse the source code looking for the places where PA is being read, but there was still too many gaps (i.e. addresses in RAM that contain something that gets analyzed, etc.). OK, so I need some help. Maybe I could force MAME to emulate those keypresses somehow?

    Looking at the sources I found, that there are special handlers for both reading and writing to the memory and ports. I have modified the gmaster_state::gmaster_port_r handler to return a fixed value (0xFE) each time PA is being read and PB is set to 3. That should emulate the up key. The code compiled and as expected I saw one FE among FFs. Nice. Unfortunately that didn't work either. I was confused. After numerous attempts to make this damn thing speak to me I modified the code to return 0x00 (all keys pressed) no matter what. And this time... nothing happened. But at some point I changed one of the stored PA values and after some struggle I saw some values in the display memory saying "TAPE". Great success! OK, now I get it. There must be some sort of debouncing code that samples PA value, holds it, compares, yada yada yada and finally does what it should. I don't want to bore you to death describing my failed attempts to make this thing work, so I just show you what I ended with:

    READ8_MEMBER(gmaster_state::gmaster_port_r)
    {
    	UINT8 data = 0xff;
    	static __attribute__((__unused__)) UINT8 cnt = 0;
    	switch (offset)
    	{
    	case UPD7810_PORTA:
    		if (m_ports[UPD7810_PORTB] == 0x00)
    		{
    			if (++cnt % 3)
    				data = 0xEF;
    		}
    		logerror("%.4x port %d read %.2x\n", 
                             m_maincpu->pc(), offset, data);
    		break;
    	}
    	
    	return data;
    }

    And that made me very happy. Looking at the memory window I finally saw some predictable changes: the value on the 1st display changed to 5- (1st keypress) and then to 55 (and so on).

    Now I will try to "program" a sequence of keypresses which should bring me to an unsupported menu value. Wish me luck, I hope I'm getting closer.

  • Going live

    mateusz.kolanski03/23/2016 at 15:15 0 comments

    Today's a big day for my little project - I decided to make it public. I still have some backlog (put a photo here and there, rephrase some sentences, etc), but I can probably take care about it later.

    As for today's activities I set up a local GIT repository and committed some code. I'm not planning to go to github, because I'm reverse engineering proprietary code. I know that DW-6000 is an ancient piece of gear and I bet nobody would care, but still I don't want to turn my little project into a lawsuit :) *Maybe* someday I will contact KORG via korgforums, but not yet.

    I created a custom Notepad++ style to highlight upd7810 syntax properly. It's far from being perfect (it doesn't color some hexadecimal values and doesn't support labels at the moment) and some colors are well.. at least questionable, but it's still a nice thing to have.

    I managed to understand/comment some more source code. I focused on the main loop and timer ISR just to see what it does when it's "idling". It's still incomplete and somewhat convoluted and I got a feeling, that I'm nowhere near the end - but that's the beauty of assembly language - jump, branch, call and interrupt in the middle :)

  • 09-11 march 2016

    mateusz.kolanski03/12/2016 at 18:32 0 comments

    I started replacing some constants with nice labels, to make the code look more readable:

    That's my IDE

    I made a simple excel sheet that converts hexadecimal values into 7 segment characters to be able to decipher some values at the end of the file.

    Seeing "words" like TAPE, LOAD, etc gave me a few hints about how the code works. I also added some constants pointing to known places in external RAM.

    Now it's time to spend some time with the MAME debugger. I have to look carefully at the routines doing something with PA/PB (keyboard reading) and those which use the corresponding data saved in RAM. I'm pretty sure that I have a rough idea about how they work, but I wasn't able to change anything on the displays (well... make the display memory to change to be exact) just by entering some values to where the switch memory is supposed to be. Long way to go.

    The more I know about the inner workings of DW6000, the more worried I am about the available space in ROM (whopping 72 bytes - I think, that "project 72" would make a good name for this one). Right now I think (or better said I expect, because I didn't have a chance to verify this guess), that the parameter selection is performed on a base of jumps to subroutines (that's what the data at the end of the file is for?), where each combination generates an offset to a function pointer. If a given combination is invalid, the resulting pointer will invoke this same subroutine which just waits for another, valid keypress. If it worked this way (i.e. one jump address per combination), it would be possible to modify some jump addresses without wasting precious bytes. If not (i.e. the code check if the keypress is in a fixed range), things will get more complicated.s We'll see. My other concern is, that since PORTB is partially used for row addressing (you know what I mean) and I want to use its second half for extra memory addressing, I will have to make sure, that the existing code which "shares" PORTB will do the masking first. And that means some extra code.

    It turns out, that my little project brings other people's attention. I was explaining my idea to a fellow geek and he suggested, that I could (or even should) use a latch driven by PC2 or PC4 (right now I feel that I might need both of them...). This way I won't have to think about preserving PB's value between function calls. Great idea. Might give it a try, although I like to keep things (look ;-D) as simple as it gets. He also suggested that I could profit from constructing a simple device emulating my ROM chip, something like an SRAM chip with serial programming interface. Another good idea, definitely worth considering.

    I could really use a kind of versioning system since now I'm dealing with some proper source code:) All you wish me luck and keep the fingers crossed.

View all 17 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates