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
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


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


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



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

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


  • 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:)


    .TBL03:	TABLE
    	DB    29H
            DB    14H


    .TBL03:	TABLE
    	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...

    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:

    	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);
    	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.

  • - march 2016 -

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

      I'm more and more convinced, that I can't do much without an assembler. I have even started to read some theory behind the inner mechanisms of assemblers because I might end up writing my own one. (timelapse) Oh, no - that won't be necessary (well, maybe someday I will try to write my own one, just because of fun and things you can learn), there is one called "The macro assembler AS" written by Alfred Arnold (huge thanks about that!) which fortunately supports NEC 78(C)10 microcontroller family! Now that's what I call luck. Anyway, it won't be that straightforward, because the unidasm syntax is a little bit different than the AS' one. So, the story begins...

      First of all I started to write down all the things that must be done in order to assemble the file:

      1. find a way to do the jumps and calls properly
      2. decode all the jump tables and data
      3. fix malformed opcodes (resulting from the disassembly of data)
      4. find a way to put some code (like interrupt vectors) where it belongs

      The first task was rather easy - using some regular expressions I have converted the addresses in front of each line into labels - shortened them to 16 bits, added a leading dot and trailing "H" (H for hexadecimal, more syntax conversions later) and a colon (00000000 -> .0000H:). I have also moved the hexadecimal macine code values to the very end of each line turning them into the comments. I will need them to decode jump tables and other data later. After such conversion, each line looked more or less like this:

      "00000008: 54 FF 00 JMP $00FF" => ".0008H: JMP $00FF ;54 FF 00"

      The next thing to do was to convert jump addresses into labels aswell. The one reason is, that AS requires labels for jumps and calls (I'm not sure about that, but let's leave it this way), but what's more important, this way we can assure, that we don't have to deal with hardcoded addresses anymore. Neat. Another regex, another success:

      ".0008H: JMP $00FF ;54 FF 00" => ".0008H: JMP .00FFH ;54 FF 00"

      AS can adapt to multiple numbering formats like $0000, 0x0000 or 0000H but I have chosen to convert all the numbers from Motorola ($0000) into Intel (0000H) syntax (just because, according to AS manual, NEC used this format). This didn't go as well as I thought, because I had to convert some numbers manually.

      Ok, so far so good. Let's do the next point on my list. Decoding jump tables (fortunately not that many) either involves programming some heuristics or just using the CPU between your ears. The human brain can spot some patterns and regularities much easier than a piece of software. Basically there are 2 possible uses of the TABLE opcode:

      1. to prepare the jump address which will be executed using the following JB opcode - in this case you have to validate to which point the data makes sense - this is a little bit tricky, especially in case of very long jump tables

      2. to store a value in the C register used for example as an offset for something - that's easy, because in most cases, the next opcode after TABLE is a JR to where the data ends, apart from that this data comes mostly in sequences, so it's rather easy to spot.

      After I have figured out where the data ends, I have converted it into the series of "DB" directives. This time it was all done manually and, what I realised later, I have done only one small mistake (I can't remember if I typed the same data twice or just skipped a part of it).

      Sometimes it was necessary to adjust some opcodes - unidasm can't distinguish between data and code so in some rare cases the first byte after table data was merged with the last byte of data and decoded incorrectly.

      Ok, now it gets really interesting - there's a bunch of "stuff" at the very end of file, you can't miss it, because it comes after a series of FF's (which indicate an empty space). I have no idea what kind of data it...

    Read more »

  • - someday in 2016 -

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

    After a longer break I got back to upd7810 stuff. This time I changed my approach a little bit. First of all I have found a way to debug the code using (slightly modified) mame debugger - as I stated earlier, NEC 7810 was used in a rather unpopular handheld console called Hartung GameMaster (it was planned to be a GameBoy killer, but things went a little bit different I guess). All I needed to do was to modify the ROM size and declare remaining 56kB as read-write space to enable memory mapped IO. Apart from that I have enabled PORTA-PORTD to be visible in emulator and I was ready to go. Working with MAME debugger is somewhat similar to working with gdb - once you learn the syntax, it becomes a really powerful tool. I was able to trace some code visually, set some break- and watchpoints on memory locations of my interest and this way I figured out some more memory locations which hide data I was looking for.

    Note: I cheated here just a little bit - as you can see, I'm watching addresses from $27E9 to $27EC where probably key presses are saved. That's one of my most recent findings, but back then I didn't have a clue about that:) Just trying to make it look nice.

  • Day 5

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

    More assembly reading today. And some findings too. There are 64 patches, each of them is 26 bytes long (I figured it out the hard way - by reading the save/load routines, but this number and the internal patch structure is described in the service manual, d'oh!), 0x2000h is the patch #0 address as well as the beginning of ext-RAM, next free address (0x2680h) is the "working patch" address, i.e. where the currently edited patch parameters are stored. BTW, if you look at the patch structure, you will see that there's no single parameter or parameter pair which occupy 4 bits at most:

    So if I'd like to add the new bank parameter, I either have to do it using two addresses (to store two 'high' bits and two 'low' ones) or to reduce the number of banks to 8. But the bright side is, that I'd be able to store the bank numbers along with the patches. Neat! I started to draw a RAM memory map, but I still have too many gaps.

View all 14 project logs

Enjoy this project?



Similar Projects

Does this project spark your interest?

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