68000 ASM generating a tone on single FM channel YM2612

For anything related to sound (YM2612, PSG, Z80, PCM...)

Moderator: BigEvilCorporation

Count SymphoniC
Very interested
Posts: 149
Joined: Sat Nov 17, 2012 3:58 am

68000 ASM generating a tone on single FM channel YM2612

Post by Count SymphoniC » Fri Sep 19, 2014 12:46 am

I came to this site well over a year ago interested in information regarding programming a music tracker (like MilkyTracker, LSDJ, Sunvox, VGM Music Maker) that runs on actual MD hardware. Long story short, I moved to a new state, lost internet, and lost my desktop pc. But I don't want to give up on this project just yet. I got a lot of people's hopes up with this idea. I want to see it through to completion.

So starting where we left off... It was suggested that I use 68000 instead of z80. I do not know 68000 ASM. But in the interest to learn it and get my feet wet (and start on a sound driver), I thought of a great idea - Generating a tone on the 1st FM channel. I figured if I can do that in assembly, I can do anything. Small steps at a time.

So... how do I do it? :D
More specifically, I'm looking for a general overview of the process and resources to help me move along. I need orientation. I don't need psuedo/source code. I'm learning a language from scratch here. Yes assembly freaks me out but that's no longer an excuse. If it takes years to accomplish the goal, then that's what it takes.

EDIT: I found this viewtopic.php?t=1840
For generating a sine wave. Is this enough to get me started? I did some digging around this forum and found many docs and bigevilcorporation's blog. This all should be very useful. Any tips especially for dealing with the sound driver would be superb.

powerofrecall
Very interested
Posts: 237
Joined: Fri Apr 17, 2009 7:35 pm
Location: USA

Post by powerofrecall » Fri Sep 19, 2014 4:21 am

First you'll want some good 68000 info, so here's a couple of book pdf's I have:
https://app.box.com/s/i1203fhhhowrrpnsiayl

If you only grab one, I'd get the 68000 Primer. Don't be freaked out by assembly, especially 68k assembly--it's really nice and quite logical compared to some CPUs. Concentrate on learning the move instructions, the arithmetic instructions, the addressing modes, flow control and branch instructions, the stuff you'll need to start writing a real program. You have the right attitude for learning assembly; don't expect to be able to rule the world with it out of the gate. It's all little steps, one step at a time to build a big program. It's kind of Zen-like, I like it.

As an aside, I learned a ton by learning how to use IDA to disassemble 68k code. Other good practical sources are looking at the Sonic 1 disassembly, it's worth downloading a copy of.
https://github.com/sonicretro/s1disasm

The text file known as "sega2.doc" I think is probably the gold standard for getting started with how the Genesis works.
http://emu-docs.org/Genesis/sega2f.htm

You'll also want the "Genesis Sound Software Manual" and "Genesis Software Development Manual."
Here's those:
https://app.box.com/s/1faz3f28ncgjjf3mmaed

I know you said you didn't want source code, but I recommend you find a known-good (i.e. official, or the older Paul Lee one that goes around) startup code before going off on this adventure. It can save you a lot of trouble. Lots of weird bugs will crop up from not having the Genesis in a known-good state and it's better to avoid them for the sake of learning. You can probably find all sorts of examples of good startup code by searching this board.

As far as assemblers, I like the vasm assembler with standard Motorola syntax but truthfully it's not really better or worse than any other 68k assembler. We aren't in the old days where stuff like how fast an assembler is of real importance, so it's mostly a matter of personal preference. The GNU binutils are also a fine assembler and you get the benefit of mature preprocessor facilities but the syntax is a little different and might be a bit more of an obstacle while learning assembly. For what it's worth, vasm can be made to assemble just about any variety of 68k syntax. If you're on windows, try looking for Amiga or Atari Jaguar dev sites to get some binaries. If you're on Linux, you'll have to build it yourself, but it will build on any system with GNU make and a C compiler. Or you could just use ASM68K, it's popular enough.
http://info.sonicretro.org/index.php?ti ... edirect=no

edit: You'll also want a good emulator. If you have a PC that can run it I'd get Exodus, if not, MESS has a good debugger.

Hopefully this is enough in the way of resources to get you started on the journey ahead. I wish you the best of luck! I like your tracker idea, it'd be great to see it one day.

Count SymphoniC
Very interested
Posts: 149
Joined: Sat Nov 17, 2012 3:58 am

Post by Count SymphoniC » Fri Sep 19, 2014 5:51 am

Thanks for the encouragement and the useful information. "Zen-like" is a comforting way to put it.

I tried messing around with moving bytes to registers (succesfully, everything looked good in the debugger). Then I tried moving those bytes to actual addresses... now the emulator (Regen) crashes when I open up the debugger.

Code: Select all

Loop:
	move.b #$AF, d0   ; Move byte AF intro register d0
	move.b d0, d1      ; Move byte from register d0 into register d1
	movea.l #$00000022, a0 ; Setting up address in a0
	move.b d1, (a0) ; AF should now be in 00000022 memory.
	jmp Loop              ; Jump back up to 'Loop'
So I'm thinking, "Ok, what the hell." Is my problem related to not having the genesis properly initiated?

powerofrecall
Very interested
Posts: 237
Joined: Fri Apr 17, 2009 7:35 pm
Location: USA

Post by powerofrecall » Fri Sep 19, 2014 6:22 am

Regen might not like that because based on the genesis memory map, you'd be writing to ROM there. Genesis RAM starts at $FFFF0000 and lasts till $FFFFFFFF (64kbytes). Try that out and you can peep in Regen's RAM viewer to see it work.

Count SymphoniC
Very interested
Posts: 149
Joined: Sat Nov 17, 2012 3:58 am

Post by Count SymphoniC » Fri Sep 19, 2014 7:02 am

powerofrecall wrote:Regen might not like that because based on the genesis memory map, you'd be writing to ROM there. Genesis RAM starts at $FFFF0000 and lasts till $FFFFFFFF (64kbytes). Try that out and you can peep in Regen's RAM viewer to see it work.
Yeah I probably should've taken a closer look at the sega doc. But what's worse is that the problem persists after changing 00000022 to FFFF0022. Debugger crashes the emulator, the ram map shows all 0's. Is there something wrong with the code? Regen? Do I need to use Mess?

The code at present is...

Code: Select all

Loop:
	move.b #$AF, d0   ; Move byte AF intro register d0
	move.b d0, d1      ; Move byte from register d0 intro register d1
	movea.l #$FFFF0022, a0 ; Setting up address in a0
	move.b d1, (a0) ; AF should now be in FFFF0022 memory.
	jmp Loop              ; Jump back up to 'Loop'
If it is the code, tell me that it is, but don't tell me what it is, I want to see if I can figure this one out on my own.

twosixonetwo
Very interested
Posts: 58
Joined: Tue Feb 25, 2014 3:38 pm

Post by twosixonetwo » Fri Sep 19, 2014 7:40 am

The code works for me in regen (only changed the syntax to AT&T asm). Did you make sure your code is word-aligned? If not, this might be the reason for the crash.

powerofrecall
Very interested
Posts: 237
Joined: Fri Apr 17, 2009 7:35 pm
Location: USA

Post by powerofrecall » Fri Sep 19, 2014 2:44 pm

Are you using a startup code, or is that code all you have total? You'll need at absolute minimum to have a vector table before your code begins or else nothing will work.

Regen is sort of crashy especially when it encounters code it doesn't like, and the debugger isn't the most stable. Get MESS and start it on the commandline with

Code: Select all

mess genesis -cart whatever.bin -debug
It'll start with a debug window. F5 to run, F11 to single step, F10 to step over. Ctrl+M brings up a memory window, but it starts at 0. You can type in the box where you want to look in memory (ffff0022 would take you right to where you want to look).

Oh, just remembered--make sure your assembler is outputting a flat binary and not another format like srecord or a linker object.

Count SymphoniC
Very interested
Posts: 149
Joined: Sat Nov 17, 2012 3:58 am

Post by Count SymphoniC » Fri Sep 19, 2014 4:41 pm

I'm using no startup code. The code above is all I have. So basically I need a header and code to initialize the genesis before this code can even be used right? I was hoping I could just jump in and start learning and toying with 68k without having to do all that. I'm using asm68k.exe.

Although I tested the code in EASy68k and it works there. I need startup code to do this on the Genesis don't I?
Last edited by Count SymphoniC on Fri Sep 19, 2014 5:47 pm, edited 1 time in total.

powerofrecall
Very interested
Posts: 237
Joined: Fri Apr 17, 2009 7:35 pm
Location: USA

Post by powerofrecall » Fri Sep 19, 2014 5:45 pm

Unfortunately, you have to, but on the upside once you have a good startup code you don't have to modify it. At the barest minimum you need the vector table or the CPU just won't "run." Just include this at the top of the code you're writing, declare a label 'main:' and put your code under it.

Code: Select all

*;------------------------------------------------------- 
*; 
*;       Sega startup code for the Sozobon C compiler 
*;       Written by Paul W. Lee 
*;       Modified from Charles Coty's code 
*; 
*;------------------------------------------------------- 

    org    $0 
    dc.l    $0,$200 
    dc.l    INT,INT,INT,INT,INT,INT,INT 
    dc.l    INT,INT,INT,INT,INT,INT,INT,INT 
    dc.l    INT,INT,INT,INT,INT,INT,INT,INT 
    dc.l    INT,INT,INT,HBL,INT,VBL,INT,INT 
    dc.l    INT,INT,INT,INT,INT,INT,INT,INT 
    dc.l    INT,INT,INT,INT,INT,INT,INT,INT 
    dc.l    INT,INT,INT,INT,INT,INT,INT,INT 
    dc.l    INT,INT,INT,INT,INT,INT,INT 

    dc.b    'SEGA MEGA DRIVE '                        ; Console name. 16 bytes long 
    dc.b    '(C)SEGA 2005.APR'                        ; Copyright notice. 16 bytes long 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00    ; Domestic game name. 48 bytes long 
    dc.b    'ob',$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00    ; Overseas game name. 48 bytes long 
    dc.b    'GM'                                ; Type of product. 2 bytes long 
    dc.b    ' 00000000-00'                            ; Product code, version number. 12 bytes long 
    dc.w    $FDDB                                ; Checksum. 2 bytes long 
    dc.b    'J               '                        ; I/O support. 16 bytes 
    dc.l    $00000000,$00020000                        ; ROM capacity (start, end). 4 bytes long each. 
    dc.l    $00FF0000,$FFFFFFFF                        ; RAM (start, end). 4 bytes long each 
    dc.b    '            '                            ; Padder. 12 bytes long 
    dc.b    '            '                            ; Modem. 12 bytes long 
    dc.b    '                                        '            ; Memo. 40 bytes long 
    dc.b    'E               '                        ; Country game. 16 bytes long 

    tst.l    $a10008 
    bne    SkipJoyDetect                              
    tst.w    $a1000c 
SkipJoyDetect: 
    bne    SkipSetup 
    lea    Table,a5 
    movem.w    (a5)+,d5-d7 
    movem.l    (a5)+,a0-a4 
    move.b    -$10ff(a1),d0          ; Check Version Number                      
    andi.b    #$0f,d0 
    beq    WrongVersion 
    move.l    #$53454741,$2f00(a1)   ; Sega Security Code (SEGA)    
WrongVersion: 
    move.w    (a4),d0 
    moveq    #$00,d0 
    movea.l    d0,a6 
    move    a6,usp 
    moveq    #$17,d1                ; Set VDP registers 
FillLoop: 
    move.b    (a5)+,d5 
    move.w    d5,(a4) 
    add.w    d7,d5 
    dbra    d1,FillLoop 
    move.l    (a5)+,(a4) 
    move.w    d0,(a3) 
    move.w    d7,(a1) 
    move.w    d7,(a2) 
L0250: 
    btst    d0,(a1) 
    bne    L0250 
    moveq    #$25,d2                ; Put initial vaules into a00000                
Filla: 
    move.b    (a5)+,(a0)+ 
    dbra    d2,Filla 
    move.w    d0,(a2) 
    move.w    d0,(a1) 
    move.w    d7,(a2) 
L0262: 
    move.l    d0,-(a6) 
    dbra    d6,L0262 
    move.l    (a5)+,(a4) 
    move.l    (a5)+,(a4) 
    moveq    #$1f,d3                ; Put initial values into c00000                  
Filc0: 
    move.l    d0,(a3) 
    dbra    d3,Filc0 
    move.l    (a5)+,(a4) 
    moveq    #$13,d4                ; Put initial values into c00000                  
Fillc1: 
    move.l    d0,(a3) 
    dbra    d4,Fillc1 
    moveq    #$03,d5                ; Put initial values into c00011                  
Fillc2: 
    move.b    (a5)+,$0011(a3) 
    dbra    d5,Fillc2 
    move.w    d0,(a2) 
    movem.l    (a6),d0-d7/a0-a6 
    move    #$2700,sr 
SkipSetup: 
    bra    Continue 
Table: 
    dc.w    $8000,$3fff,$0100,$00a0,$0000,$00a1,$1100,$00a1 
    dc.w    $1200,$00c0,$0000,$00c0,$0004,$0414,$302c,$0754 
    dc.w    $0000,$0000,$0000,$812b,$0001,$0100,$00ff,$ff00 
    dc.w    $0080,$4000,$0080,$af01,$d91f,$1127,$0021,$2600 
    dc.w    $f977,$edb0,$dde1,$fde1,$ed47,$ed4f,$d1e1,$f108 
    dc.w    $d9c1,$d1e1,$f1f9,$f3ed,$5636,$e9e9,$8104,$8f01 
    dc.w    $c000,$0000,$4000,$0010,$9fbf,$dfff 

Continue: 
    tst.w    $00C00004 

    move.l   #$0,a7              ; set stack pointer 

    move.w  #$2300,sr       ; user mode 

    lea     $ff0000,a0      ; clear Genesis RAM 
    moveq   #0,d0 
clrram: move.w  #0,(a0)+ 
    subq.w  #2,d0 
    bne     clrram 

*;----------------------------------------------------------        
*; 
*;       Load driver into the Z80 memory 
*; 
*;----------------------------------------------------------        

    move.w  #$100,$a11100     ; halt the Z80 
    move.w  #$100,$a11200     ; reset it 

    lea     Z80Driver,a0 
    lea     $a00000,a1 
    move.l  #Z80DriverEnd,d0 
    move.l  #Z80Driver,d1 
    sub.l   d1,d0 
Z80loop: 
    move.b  (a0)+,(a1)+ 
    subq.w  #1,d0 
    bne     Z80loop 

    move.w  #$0,$a11100       ; enable the Z80 

*;----------------------------------------------------------        

    jmp    main 

INT:    
    rte 

*; --- Do nothing for this demo --- 
HBL: 
    rte 

VBL: 
    addq.l    #1,vtimer 
    rte 
*;---------------------------------------------------------- 
*; 
*;       Z80 Sound Driver 
*; 
*;---------------------------------------------------------- 
Z80Driver: 
    dc.b    $c3,$46,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$00,$00 
    dc.b    $00,$00,$00,$00,$00,$00,$f3,$ed 
    dc.b    $56,$31,$00,$20,$3a,$39,$00,$b7 
    dc.b    $ca,$4c,$00,$21,$3a,$00,$11,$40 
    dc.b    $00,$01,$06,$00,$ed,$b0,$3e,$00 
    dc.b    $32,$39,$00,$3e,$b4,$32,$02,$40 
    dc.b    $3e,$c0,$32,$03,$40,$3e,$2b,$32 
    dc.b    $00,$40,$3e,$80,$32,$01,$40,$3a 
    dc.b    $43,$00,$4f,$3a,$44,$00,$47,$3e 
    dc.b    $06,$3d,$c2,$81,$00,$21,$00,$60 
    dc.b    $3a,$41,$00,$07,$77,$3a,$42,$00 
    dc.b    $77,$0f,$77,$0f,$77,$0f,$77,$0f 
    dc.b    $77,$0f,$77,$0f,$77,$0f,$77,$3a 
    dc.b    $40,$00,$6f,$3a,$41,$00,$f6,$80 
    dc.b    $67,$3e,$2a,$32,$00,$40,$7e,$32 
    dc.b    $01,$40,$21,$40,$00,$7e,$c6,$01 
    dc.b    $77,$23,$7e,$ce,$00,$77,$23,$7e 
    dc.b    $ce,$00,$77,$3a,$39,$00,$b7,$c2 
    dc.b    $4c,$00,$0b,$78,$b1,$c2,$7f,$00 
    dc.b    $3a,$45,$00,$b7,$ca,$4c,$00,$3d 
    dc.b    $3a,$45,$00,$06,$ff,$0e,$ff,$c3 
    dc.b    $7f,$00 
Z80DriverEnd: 

powerofrecall
Very interested
Posts: 237
Joined: Fri Apr 17, 2009 7:35 pm
Location: USA

Post by powerofrecall » Fri Sep 19, 2014 5:48 pm

By the way if you just want to learn 68k without having to worry about getting the machine in the right state, I think EASy68k is worth checking out. It is a simulator of a basic 68000 machine designed to teach 68k assembly, so you won't have to worry about the nuts and bolts while you're learning the basic instructions. All the stuff you learn on it will carry over to the Genesis.

Count SymphoniC
Very interested
Posts: 149
Joined: Sat Nov 17, 2012 3:58 am

Post by Count SymphoniC » Fri Sep 19, 2014 6:45 pm

Okay.
I got a couple of errors with that init code.

Line 55 move a6,usp
asm68k complained about data size, so I changed the move opcode to move.l

Line 154? addq.l #1,vtimer
asm68k complained about vtimer being undefined. I looked around to see if I could figure this one out, but couldn't and just commented this line out.

After that there were no errors and the code finally runs in Regen. My AF byte has been successfully moved to FF000022 and I'm happy to see that in the RAM viewer. I'm not too happy about having to tinker with the init code to get it working, who knows what problems I may have created (insight on this)? But I can now start learning 68k on Sega without too much worry I think.

Thanks for helping me out, I couldn't find the exact startup code you gave me, although I found others which appeared to have problems.

powerofrecall
Very interested
Posts: 237
Joined: Fri Apr 17, 2009 7:35 pm
Location: USA

Post by powerofrecall » Fri Sep 19, 2014 8:49 pm

Some assemblers have slight differences in what is allowed with how instructions are written. You didn't break anything. With many assemblers, leaving off the .l/.w/.b suffix will have the assembler automatically pick the right instruction. This is usually helpful for branch opcodes where you might not know what size of branch to pick. Anyway, move.l is correct in this case.

The vtimer variable isn't important and is just a frame counter. If you declare a variable for vtimer at the start of your code like

vtimer: equ $FFFF0000

then every elapsed frame the longword from $FFFF0000-FFFF0003 will increment by 1. This can be useful for timing things (hence the name of the variable).

Count SymphoniC
Very interested
Posts: 149
Joined: Sat Nov 17, 2012 3:58 am

Post by Count SymphoniC » Fri Sep 19, 2014 11:36 pm

Ahhh so basically that code was basically a bit sloppily done (no offense to the original coder of course) by a coder who was relying on the assembler to do optimizations.

As for vtimer, I figured I'd have to declare that variable, I just wasn't sure how to go about it properly in asm (I attempted doing something very similar to what you posted just without the colon and I had put it in the VBLANK: routine, got an invalid opcode error). I'll save vblank timers for a future date, hopefully soon, when I get better with this stuff anyways. Right now is the time for the basics first. Maybe I can worry about the timer when doing a hello world. So far so good though, I like how easy it is to understand 68k. Maybe I've discovered a new obsession.

powerofrecall
Very interested
Posts: 237
Joined: Fri Apr 17, 2009 7:35 pm
Location: USA

Post by powerofrecall » Sat Sep 20, 2014 1:12 am

Yeah, 68k isn't as daunting as it looks.

In (most?) 68k assemblers, the position of labels and instructions is important-- the label should be the first thing on a line. If you tab it in, it might think you're trying to use an opcode or a macro.

There are no real "variables" in assembly like in other higher level languages, it's sort of a manual process. Also the colon is optional in most assemblers, I prefer it because it's a little easier to read for me.

name: equ $location

is just declaring a name for a pointer in memory, it's completely up to you to manage it and pay attention to how you use it. There are no types, it's just memory. You could write either the label name or $location in your source and it would assemble and work the same way either way.

So variables in assembly programs end up looking like

var_1: equ $FFFF0000
var_2: equ $FFFF0004
var_3: equ $FFFF0005
var_4: equ $FFFF0006
var_5: equ $FFFF0008

and it ends up being that you can store a longword in var_1, a byte in var_2 and var_3, a word in var_4, and anything in var_5 (as long as nothing else encroaches upon it). Also, since variables are labels, they're all global--you can't insert them within a function, but there's no need to do so.

Also, automatic branch size selection is a nice feature for an assembler to have and it's easy to get into the habit of leaving the branch size off of branch instructions since it's cumbersome to calculate it manually (and branch size also affects speed, so there's another reason to leave it to the assembler). Some assemblers also don't require the size extension on particular instructions where that size extension would be implied (that move to USP instruction). So it's partially code style, too. I don't use ASM68K but I would bet that it has commandline options to select/deselect some of these quirks.

Count SymphoniC
Very interested
Posts: 149
Joined: Sat Nov 17, 2012 3:58 am

Post by Count SymphoniC » Sat Sep 20, 2014 4:07 am

Well I think you've given quite a comprehensive explanation as to why my attempt didn't quite work. That primer book is very useful and easy to understand. Thanks to all your help I can move on to the next step and continue to learn this fascinating language.

Now while I'm playing around with 68k and learning it, I also want to gather information that will help me when I get to actually writing a sound driver for YMDJ. Must be prepared. What branches of math am I likely going to need to know to do the sound driver? I may need to brush up on some archaic skills haha... obviously the common +,-,x,/ algebra probably and boolean, but surely there's more. I have a feeling I'll be messing with some cryptic formulas...

Post Reply