# This is both documentation and a test file for the Sarcasm assembler. # # http://www.ecstaticlyrics.com/electronics/z80/sarcasm/ # # Sarcasm is unlike any other assemblers in several ways, and so documentation # is absolutely necessary. You know, unlike if it were similar to other # assemblers in which case you'd just know how to use it, wouldn't you? # # So here's a combination documentation and example file. If nothing else, # this ensures that all of the examples will assemble. Whether or not they # work is another matter, but we'll not worry about that. After all, I'm not # teaching you how to debug your code. # # I kind of named the sections, but this is all kind of random as well. # You'll find the 'output' directive documented under the 'GOTO' header. # It doesn't matter, you'll have to read this entire thing anyway. # ...plus I'm sure you have text search capabilities. # # # Instructions # ============ # # I renamed many of the instructions. For example, 'ld' is now 'mov' instead. # I also changed the 'ix' and 'iy' registers to 'st' and 'uv' so that their # high and low parts can be called 's', 't', 'u' and 'v' similar to how 'hl' # is split into 'h' and 'l'. It only made sense. When instructions have # operands that are required, I left out the required operands. Why type # "add a, c" when the only thing you can add 'c' to is 'a'? So it's # "add a, 1" or "add bc, 2" but it's "add c" when numbers aren't involved. # # To make sense of this mess, the general rule is that when you can't figure # out how to get Sarcasm to recognize an instruction, first look it up on # the internet and figure out it's encoding. Say you're trying to use # the "load and increment" instruction. Once you've found it's encoding # is "EDA0" you just look it up in the opcodes file: # # ED70 in # ED71 out # ED72 sbc hl sp # ED73 mov [xxxx] sp # ED78 in a # ED79 out a # ED7A adc hl sp # ED7B mov sp [xxxx] # EDA0 movsi <--- There it is! # EDA1 cmpsi # EDA2 insi # EDA3 outsi # # ...and now you know. It's 'movsi' in Sarcasm, with no operands. # # I have to admit this is a pain in the ass and confusing as bloody fuck, # but I just had to do it anyway. Standard Z80 assembly syntax is shit as # far as I'm concerned, and when I wrote Sarcasm, I had never used any other # Z80 assembler, so I saw no point in learning a syntax I didn't like when # it would be just as easy to learn one that I do like. For someone who # already knows Z80 assembly, this is likely a total pain in the ass, but # for someone like myself, it's simply a matter of translating small bits of # code I find on the web, which is an issue you usually have to deal with no # matter what assembler you're using. Just try compiling MASM code in NASM. # # # Text Parsing # ============ # # Comments begin with a '#' symbol. This is pratically a standard anymore. # You're used to using '#' as a comment character, I hope. Just look at how # normal this file appears compared to standard assembly source code. # # The semicolon, used by most assemblers to mark comments, is used by Sarcasm # to support multiple instructions on a line, like this: mov a, [bc]; mov [de], a # To help with debugging, Sarcasm tells you which part of a line is responsible # for assembly errors: #mov a, [bc]; add a, c; mov [de], a # If uncommented, that line will generate this error message: # # example_test.asm: 27.2 -- add a c # -- "add a c" not found # # The line number, 27.2, means line 27, part number 2. Additionally, that part # of that line is displayed just to make sure everyone is on the same page. # # The error message -- "add a c" not found -- means that Sarcasm doesn't know # how to assemble 'add' with the operands 'a' and 'c'. This is because the # opcodes file doesn't 'feature' required operands. The add instruction # always uses register 'a' as a destination and so you do not specify the # register 'a' but instead only the register 'c'. add c # Continuing the theme of not requiring unnecessary things, commas are not # required either because Sarcasm can always seperate operands without them. # In fact, it will flat out ignore your commas as if they were spaces. # Exactly how it manages to seperate operands without commas is best # explained as magic, but if you really want to know, read the source code. # # Anyhoo, Sarcasm should interpret things the way a human would, e.g.: bytes 0 +1 +3 - 1 -2 + 5 +6 + -2 9-3 3+ 4 # Granted, that's a little hard to look at, but any intelligent human is # going to guess that what the programmer meant was this: bytes 0, 1, 3 - 1, -2 + 5, 6 + -2, 9 - 3, 3 + 4 # ...and that's exactly what Sarcasm will make of it. ...but since it is so # hard to look at, Sarcasm does allow commas, it just doesn't require them. # Also worth mentioning is that Sarcasm uses square brackets instead of # parenthesis like most other Z80 assembers: mov [bc], a # Parenthesis are better for math. If only Sarcasm knew what they meant... # # # The 'section' Directive # ======================= section start, $0000, $0038 section interrupt, $0038, $002E section nmi, $66, $1A section common, $0080, $0080 # The 'section' directive allows you to name sections of memory. The first # operand is the name of the section, the second is its offset in Z80 memory, # and the third is its size. # # You do not have to use the section directive. If left out, everything will # be assembled to a section named 'default' with a size of 64k. # # One nice thing about sections is that Sarcasm will tell you if your code # exceeds the size of the section it is in. For example, the start section # only has $38 bytes of space because interrupt calls go to address $0038, # so you might put this in your start section: section start jmp the_start data "Sarcasm Example Code", $00 data "Copyright (C) 2007 Your Mom", $00 #data "This text won't fit!", $00 # The code as written will fit into the section, but if you uncomment the last # 'data' statement, it will no longer fit, and Sarcasm will generate an error. # # example_test.asm: 140 -- data "This text won't fit!" $00 # -- start section's limit exceeded. # Perhaps more usefully, the sections allow you to define what memory is # RAM and what memory is ROM. For example, a typical Z80 system might have # 32k of ROM starting at address $0000 and 32k of RAM starting at address # $8000. This we can divide into 'code', 'data', and 'ram' sections, like # this: section code, $0100, $3F00 section data, $4000, $4000 section ram, $8000, $8000 # The code section begins at $0100 because the other sections defined above # (start, interrupt and nmi) already use addresses below $0100. Sarcasm # will allow you to have overlapping sections, but if you do so, it will # happily assemble code on top of other code. This is useful if you know # what you are doing, but if you don't know what you are doing, then you # don't want your sections to overlap. # # By far, the most useful thing about sections is that you can switch back # and forth between them: section code the_start mov bc, [some_data] mov [some_ram], bc some_label section data some_data words some_label section ram some_ram words 0 # In too many assemblers, you'd be at the end of the source code at this # point. Having typed code that appears at address $8000, you wouldn't # be able to go back to wherever you were in the code section. That sucks, # and that's why Sarcasm isn't like that. If you want to go back to the # code section, just do this... section code # ...and whatever instructions you type next will appear after whatever you # last put into the code section. This means that variables used by a # function can be declared right next to that function. That is wonderful # for readability. Much better than scrolling up and down and up and down. # # # The 'namespace' Directive # ========================= # # The namespace directive provides for global and local variable names. # However, so does the local variable system, and when the two combine, # it's all a bit weird. Here's how it goes: namespace zoo turtle # Creates zoo.turtle .shell # Creates zoo.turtle.shell .eyes # Creates zoo.turtle.eyes monkey # Creates zoo.monkey .tail # Creates zoo.monkey.tail .eyes # Creates zoo.monkey.eyes snake # Creates zoo.snake .eyes # Creates zoo.snake.eyes namespace basement mouse # Creates basement.mouse .eyes # Creates basement.mouse.eyes .tail # Creates basement.mouse.tail snake # Creates basement.snake .eyes # Creates basement.snake.eyes namespace snake eyes # Creates snake.eyes .pupils # Creates snake.eyes.pupils # Under each namespace, there are ordinary variable names. Under these # ordinary variable names there are local variables. # # In all places, variables may be referenced by their full names, as in the # comments above to the right of each variable. Depending on where you are, # some variables may be accessed with shorter names. namespace basement jmp snake # Jumps to basement.snake namespace zoo jmp snake # Jumps to zoo.snake owl .feathers .wings .eyes jmp .feathers # Jumps to zoo.owl.feathers jmp monkey.tail # Jumps to zoo.monkey.tail # However, one small bit of ambiguity arises: jmp snake.eyes # ??? zoo.snake.eyes -or- snake.eyes ??? # This could, in theory, reference either 'zoo.snake.eyes' or the 'eyes' # under namespace 'snake'. Sarcasm assumes that you're more interested # in labels in your current namespace than you are in labels in some # other namespace. This can be useful if you have one namespace for a # library of code, but you want to replace one of those functions with # a local function. If you don't like the 'sqrt' in your math library # under the name 'math.sqrt' then you just create your own 'math.sqrt' # in your local namespace. Then code in that namespace references your # replacement while all other code continues to reference the original. # # You can switch namespaces at any time. Unlike switching sections, # switching namespaces has no effect on where in memory instructions are # assembled to. It only changes what labels are referenced by shorter # labels and what namespace new variables appear in. # # BTW, you can do this: what.ever # ...to simultaneously define a normal label and a local label, but you # cannot do this: #no.way.jose # You have to use the namespace word to switch namespaces. # # Anyway, let's get the rest of this file out of that awful namespace. namespace awesomecross # # # The 'goto' Directive # ==================== # # This just moves Sarcasm's pointer to where it is writing code to in Z80 # memory. You can use this to avoid having to use sections, but it is much # more useful (I assume, I've not used it for this) to use it for what it # was intended for: writing code for more than 64k of memory. # # The idea is like this: # # We've already have our 'start', 'interrupt', 'nmi' and 'common' sections # defined above, and they cover the first 256 bytes of ROM. ...but say we # want to write like 64k of ROM and still have 32k left over for RAM. # We need to have some external hardware to switch between different address # mappings, then we need an assembler capable of putting together such a # piece of software. # # So what we do is decide what code is going on which ROM chip. Say we'll # have two chips for simplicity. In the source file, first we put the # code for the first three sections at the top of the file, then we start # on the 'common' section which will also be common to both chips. section common chipswitch # First we figure out which ROM chip is active, then decide if a switch # is necessary or not. in $00; cmp [st+0]; jnz .switch # If no switch is necessary, just jump to the function. # When it returns, it will return to the original code. mov l [st+0]; mov h [st+1]; jmp hl # If a switch is necessary, then we switch, then call the function, # then switch back. .switch; push af # Save the current ROM number. mov a [st+0]; out $00 # Switch ROMs... # Call the function. (we can't call hl, so we call a jmp to the address) mov h [st+1]; mov l [st+2]; call .nonsense; .nonsense; jmp hl pop af; out $00 # Switch back to the previous ROM. ret function_table .on_chip_one; bytes 1; words function_on_chip_one .on_chip_two; bytes 2; words function_on_chip_two # This function is called with code like this: section code # Call the function on chip one: mov st [function_table.on_chip_one]; call chipswitch # Call the function on chip two: mov st [function_table.on_chip_two]; call chipswitch # The chipswitch function figures out which ROM the code is in so that the # main code doesn't have to worry about it. This certainly isn't the best # solution as it slows down function calls and causes the stack to be a # little different depending on whether the function is on the same ROM or # not, but it shows what is possible. # # Anyway, to do this, you write the code section for the first ROM... section code; goto $0100 mov st [function_table.on_chip_one]; call chipswitch mov st [function_table.on_chip_two]; call chipswitch data 'This text is in the first ROM chip!' # Then you write that data to a file like this: output "chip_one.rom" $0000 $7FFF # Then to start over for the second ROM you do this: section code; goto $0100 mov st [function_table.on_chip_one]; call chipswitch mov st [function_table.on_chip_two]; call chipswitch data 'This text is in the second ROM chip!' # Then you write out the data for the second ROM chip... output "chip_two.rom" $0000 $7FFF # ...and after assembling this file, you can look at the two files with a # hex editor. You'll see one says "first ROM chip" and the other says # "second ROM chip" but all of the other data is the same. That's what we # want. The code to handle interrupts and the code to handle ROM switching # needs to be in each ROM chip, and since we didn't overwrite it, it is. # # Hmm... Seems I need to define "function_on_chip_***" now... function_on_chip_one; function_on_chip_two # There we go! # # # The 'output' Directive # ====================== # # Just in case you haven't figured it out already... # # output "filename.rom", [base address], [limit address] # # It writes a portion of Sarcasm's current image of Z80 memory to the specified # file. It starts at 'base address' and stops after writing 'limit address'. # This is the only way to write files. If you've used some other assemblers # and accidentally specified the source and desitnation files backwards, # you'll agree this is much more fun than having your source files erased # because of a minor mistake. # # # The 'bytes', 'words' and 'data' Directives # ========================================== # # The 'bytes' directive works like 'db' in other assemblers, except that # it doesn't accept strings. It only does bytes. bytes 1, 2, $14, -33, $9 # The 'words' directive works like 'dw' in other assemblers. words 1234, 5678, $9ABC, -666 # The 'data' directive is Sarcasm's supercool data declaration directive. # # It does all kinds of data: # # Text strings: data "This is a text string!" 'This is a text string too!' # Both bytes and words, provided they're hexidecimal and contain exactly # two or four digits, so that it knows which is which: data $1234 "This is a null-terminated string!" $00 # ...and to top it off, it does something I call a 'byte string'... data !0123456789ABCDEF # ...and that's the same as... data $01 $23 $45 $67 $89 $AB $CD $EF # ...except it's a whole lot easier to type when you have a lot of such data. # # Finally, the string quoting is somewhat intelligent: data 'This ain't a valid string in other assemblers!' # The difference between single quotes and apostrophes is that single quotes # always have a space or a comma next to them, whereas apostrophes have a # letter on each side of them. # # # # # # # To finish things off, here's some actual code I wrote with the previous # version of Sarcasm for a Z80 system I built. I think it ran a four digit # seven-segment display while playing some kind of music and scanning a # keyboard matrix. Whatever it did, the idea is to let you see some code so # that you can ask yourself the all-important question: # # "Do I want to write code that looks like this, or do I want to write code # that looks like that?" # # ...with 'that' referring to code written for someone else's assembler. # # I think you'll agree the choice is simple. Sarcasm's syntax kicks ass! # # # # # # # Seems I tossed so much code in the start section that I need to reset it: section start; goto $0000 # There we go, that's all fixed now. # # # ###################################### section code nmi_handler push af; push hl mov hl [time_word]; inc hl; mov [time_word] hl # Deactivate current display digit... xor a; out $E0; dec a; out $F0 # Activate next display digit... mov a l; and $03 cmp 2; jp .above_1 cmp 1; jz .is_1 mov a, $11; out $E0; mov a, [display_data+0]; out $F0; in $E0; mov [key_data+0], a; jmp .digit_done .is_1 mov a, $22; out $E0; mov a, [display_data+1]; out $F0; in $E0; mov [key_data+1], a; jmp .digit_done .above_1; jz .is_2 mov a, $88; out $E0; mov a, [display_data+3]; out $F0; in $E0; mov [key_data+3], a; jmp .key_thing .is_2 mov a, $44; out $E0; mov a, [display_data+2]; out $F0; in $E0; mov [key_data+2], a; #jmp .digit_done .digit_done pop hl; pop af retn .key_thing push bc mov a $FF mov hl key_data .key_loop mov c [hl] mov b 0; .xx rcr c; jnc .not_pressed bit7 a; jz .bad_code mov a l; rol a; rol a; or b; and $0F .not_pressed inc b; bit2 b; jz .xx inc hl; bit2 l; jz .key_loop mov [key_code] a .bad_code pop bc jmp .digit_done ######################################## section code code_start call checksum_check rol a; rol a; rol a; rol a mov [display_number] a mov [display_number+1] a call calc_display mov a $FF; mov [display_data+0] a; mov [display_data+3] a; # .checksum_delay # mov a [time_word+1] # cmp 2; jnz .checksum_delay mov hl 0; mov [position] hl mov hl [time_word]; inc hl mov [nexttime] hl mov de 10 .1 # Wait until time for next note... mov bc [nexttime] .2 halt; mov hl [time_word]; stc; cmc; sbc hl bc jnz .2 # Calculate next note time... mov hl [nexttime]; add hl de; mov [nexttime] hl # And.... mov st, [position]; mov bc voice1; add st bc mov b, 0; mov c, [st]; shl c; mov uv, notes; add uv, bc mov a, [uv]; out $00; mov a, [uv+1]; out $00 mov st, [position]; mov bc voice2; add st bc mov b, 0; mov c, [st]; shl c; mov uv, notes; add uv, bc mov a, [uv]; out $01; mov a, [uv+1]; out $01 mov hl, [position]; inc hl; mov [position], hl mov bc, [length]; stc; cmc; sbc hl, bc; jnz .1 mov c $FF .3; mov a [key_code] cmp c; jz .3 mov c a cmp $FF; jz .4 mov hl display_number; rold; inc hl; rold call calc_display mov h $7E; mov a [key_code]; shl a; and $06; mov l a mov a [hl]; out $00; inc hl; mov a [hl]; out $00 mov h $7E; mov a [key_code]; shr a; and $06; or $08; mov l a mov a [hl]; out $01; inc hl; mov a [hl]; out $01 jmp .3 .4 mov a 4; out $00; out $01; mov a 0; out $00; out $01 jmp .3 ######################################## section code calc_display mov h, $7F mov a, [display_number+1]; shr a; shr a; shr a; shr a; mov l, a mov a, [hl]; mov [display_data+0], a mov a, [display_number+1]; and $0F; mov l, a mov a, [hl]; mov [display_data+1], a mov a, [display_number+0]; shr a; shr a; shr a; shr a; mov l, a mov a, [hl]; mov [display_data+2], a mov a, [display_number+0]; and $0F; mov l, a mov a, [hl]; mov [display_data+3], a ret ######################################## section code checksum_check mov hl, 0; mov a, 0 .1 add [hl]; inc hl bit7 h; jrz .1 ret ######################################################################## section data voice1 bytes 27 27 27 27 bytes 27 27 27 00 30 30 30 00 35 35 35 35 35 35 35 00 35 35 35 00 30 30 30 00 27 27 27 27 27 27 27 00 bytes 30 30 30 30 30 30 30 30 30 30 30 00 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 00 27 27 27 27 bytes 27 27 27 00 30 30 30 00 35 35 35 35 35 35 35 00 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 bytes 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 00 voice2 bytes 35 35 35 35 bytes 35 35 35 00 37 37 37 00 39 39 39 39 39 39 39 00 39 39 39 00 37 37 37 00 35 35 35 35 35 35 35 00 bytes 37 37 37 37 37 37 37 37 37 37 37 00 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 00 35 35 35 35 bytes 35 35 35 00 37 37 37 00 39 39 39 39 39 39 39 00 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 bytes 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 00 length; words 124 # -2- # 1 3 # -0- # 7 5 # -6- 4 goto $7E00 words 4963 4491 4062 3674 6376 7042 7792 8608 # bytes to write for each hex digit - bits are inverted goto $7F00; hexbits data $11 $D7 $32 $92 $D4 $98 $18 $D3 $10 $90 $50 $1C $39 $16 $38 $78 goto $7F80; notes words 4 51484 48594 45867 43293 40863 38569 36405 34361 32433 30613 28894 27273 25742 24297 22934 21646 20431 19285 18202 17181 16216 15306 14447 13636 12871 12149 11467 10823 10216 9642 9101 8590 8108 7653 7224 6818 6436 6074 5733 5412 5108 4821 4551 4295 4054 3827 3612 3409 3218 3037 2867 2706 2554 2411 2275 2148 2027 1913 1806 1705 1609 1519 ######################################################################## section ram time_word; data $0000 display_number; data $0000 display_data; data $00 $00 $00 $00 key_data; data $00 $00 $00 $00 key_code; data $00 position; words $0000 nexttime; words $0100 ######################################## section start mov sp $0000 mov hl $0000 mov [time_word] hl # Set up interval timer to 256 Hz, for interrupts... mov a $B4; out $03 mov a $09; out $02 mov a $3D; out $02 # Turn off sound... mov a $36; out $03 mov a $76; out $03 mov a $04; out $00 mov a $00; out $00 mov a $04; out $01 mov a $00; out $01 jmp code_start ######################################## section interrupt halt ######################################## section nmi jmp nmi_handler