Learn Multi platform Z80 Assembly
Programming...With Vampires!
Absolute beginners series
The Beginners series is
designed as an introduction to the concepts of Assembly and retro
programming, it does not aim to teach a particular assembly language
(I have other tutorials for that!).
This series is designed to
give you a good 'foundation' of background knowledge before you
start learning Assembly, or as a supplement if you're looking at
assembly programming and are finding it tough (Which is completely
understandable)
Don't like to read? you can learn while you
watch and listen instead!
Every Lesson in this series has a matching YOUTUBE video... with
commentary and practical examples
Visit the authors Youtube
channel, or Click the icons to the right when you see them
to watch the Lessons video!
Lesson
1 - Basics... Introduction to Assembly! Interested in Retro programming, Want to learn the concepts
of Assembly and retro computers... This is for you!
This episode will go over the concepts of computing, and what
assembly assembly language is, and what it can do for you!
What is Assembly
Language?
Assembly Language is an early very Low level language. Its
commands are more readable than the 'Machine Code' the CPU actually
runs, but is still reasonably human readable to help the programmer.
Rather than a 'High Level Language' like C++ which does a lot to
help us, Assembly converts
straight to the machine code that the CPU runs.
This makes it faster than High level languages, but it means the
programmer has do do more of the work of writing the program.
That sounds
difficult! Why should I use Assembly Language then?
Well, if you're writing a program for your job � you almost
certainly should use something else! But if you're looking to learn
programming for a hobby, or you're trying to make an impressive game
on an old system with limited hardware, then assembly is worth taking
a look at.
I would say It's like taking a train compared to running a
marathon. Do you want to get somewhere quickly, or do you want a
challenge?
Assembly programming will teach you new skills, and let you take
a different look at the old computers you've used for a long time.
Assembly language is harder to get started than C++, and you'll
almost certainly need to research the hardware you're programming, and
plan your project well. But the extra effort will be worth it, as the
result will be a program that you made 100% by yourself!
Unlike with High level languages, there will be no Unreal engine doing
all the game engine, no SDL handling the sound and no OpenGL doing the
graphics. You'll have done it all yourself and know how it all works!
What can Assembly Language Do?
Anything you want! Because it's so low level, other languages,
like C++, Forth and Basic, all end up as Assembly. So in theory
anything those languages can do, you can do in Assembly, if you have
the persistence!
If you're just starting out, you should probably be aiming to
make simple games like Pong, Space Invaders or something of that
style. While it's possible to write your own Window based Operating
System in Assembly, you'd probably be better off aiming for something
more realistic to start unless you're very confident.
How do I learn
Assembly Language?
The only way to learn Assembly is through a combination of study
and practice. You'll need to understand the instruction set of the CPU
you're interested in, the hardware of the platform you're wanting to
develop for and probably the OS of the system as well.
This can only be achieved by research and study and all the
documentation you'll need is out there free on the web. But you'll
probably find it impossible to understand without trying it for
yourself, so writing little test programs to put what you read into
practice will help.
Enough talk! Let's start learning Assembly!
Overview of the retro computer system
Every computer
is different, but the general structure of the classic 8 and
16 bits tends to follow some common patterns.
Of course the part our
commands will run on is the 'CPU', but the CPU can't do
anything on its own.
To call the CPU the
'Brain' of the computer would be an overstatement, as it's a
'brain' that can't remember anything for very long, so it
needs the Memory to store things for it. Computer systems
will have a combination of RAM (Random Access Memory - for
storing data) and ROM (Read Only Memory - for the operating
system).
Some systems will have a
Keyboard, but every system will have at least some kind of
input like a gamepad. These don't connect directly to the
CPU. There will be some kind of 'Input/Output chip' we'll
have to 'ask' for the data.
Most systems will have
some kind of sound. This will usually be some kind of chip
we can tell what sound to make, but some have a 'beeper',
where we basically have to make the shape of the sound with
the CPU.
Depending on the system,
there may be a tape or disk device we can read data from.
The reading or writing procedure is too complex to do
directly, so we probably want to ask the operating system to
do the work for us.
Finally we have the
graphics, because we'll want to 'see' some results!
How the
graphics work
will vary from system to system. Some systems have separate graphics
memory and others do not. All will have some kind of 'graphics
chips' which we can tell to do things, like change the
colors on screen, or change the screen resolution.
Let's take a
look at an 'imaginary system' its parts and how they connect
-> The layout of an imaginary 'typical' system.
CPU Terminology
Let's start by looking at some common terms you'll come across in
Assembly. We'll discuss what they mean to you as a programmer.
CPU
The CPU is the 'Calculator' of our machine,
it reads in numbers and commands, and calculates the results.
Commands and parameters will be usually read in from
memory, the results will be stored back to memory. Sometimes
the source or destination may be a 'device' like a joystick or
speaker. How connection to such devices works depends on the
CPU and the hardware.
The CPU calculates the current calculation.
The memory stores the results and future calculations.
Memory
Memory is our main storage for our program
code, graphics data and values such as our score, the XY
position of our player and everything else!
Each byte of memory has a numeric 'address'. Think of it
like a huge set of lockers, each has a number and each
'locker' contains a byte of data which we can read or write.
The 'Address bus' is a set of wires that connects the CPU
to the Memory.
Though its calculations are 8 bit, a Z80 system has a 16
bit 'address bus' so it can address 64K of memory (0-65535),
though this can be extended with 'Bank Switching'.
Bank switching is where part of the memory map will
'switch' between different areas of memory. On the 128K CPC,
the range &4000-&7FFF can switch between one of the 8
available 16k banks, giving our program access to more memory
within the 64K address space.
Memory is like a bank of numbered 'lockers'
that can each store 1 byte messages.
RAM and ROM!
There are two kinds of memory:
RAM is 'Random Access Memory', this just means it can be
read or written.
ROM is 'Read only memory', like a CD ROM we can't change its
contents.
Systems like the CPC and Spectrum have lots of RAM, and a
bit of ROM for things like screen and tape routines.
On Systems like the Sega Master System and NeoGeo, the
Cartridge which stores our program is read only ROM, and the
game system has a small amount of writeable RAM for our
variables and other stuff.
RAM can be read or written, ROM can only be
read.
Registers
The CPU has a very small amount of built in
memory to remember parameters for the calculations, usually just
a few bytes (depending on the system).
This is 'short term storage'. It's much faster than RAM
because it's inside the CPU but we have very few registers, so
we have to use the slower normal memory a lot.
Systems like the 6502 have just 3 main registers, and
systems like the Z80 have over a dozen. That's not to say the
6502 is 'worse', it just works differently. Though the 6502
has fewer registers it can access memory faster instead.
Registers are 'short term memory', just a
few bytes are available. RAM is 'Long term memory' many
Kilobytes or even Megabytes will be available.
There's lots
more technical stuff to learn about the CPU, but we'll look at
that next time!
If you're feeling super confident, you can jump over to the
Assembly tutorials of whatever CPU interests you... otherwise
stick around, and next time we'll look at more of the CPU
technical details.
Lesson
2 - Basics... The Mysteries of the CPU! When we program in assembly, we need to know more than 'The
CPU does processing'!... we'll need to understand what it can
do, and how it does it!
Fortunately, there are a lot of things that are common to many
CPUs! In this lesson we'll learn some common concepts that will
help you with your ASM programming!
Accumulator Register (A)
The Accumulator is the main register for calculations. It can
only store one calculation result at a time, you can think of it like
the screen on your calculator. The register is referenced by the
letter 'A'.
Old CPU's, like the 6502 and Z80, had a single Accumulator.
More modern ones, like the ARM/68000, don't work like that. On the
68000 or ARM any register of the half dozen or so can do the
calculations, so there's no single 'Accumulator'.
Program Counter Register (PC) / Instruction Pointer (IP)
The code of our program will be somewhere in
memory. The Program Counter points to the address of the line of
code the CPU is going to process next.
It's like following a 'to do list' and pointing your finger at
each line as you move through the list. Without the program
counter the CPU wouldn't know what bytes to read in next to work
on.
The Program Counter points to the current
'job' being worked on.
Flags Register / Condition Codes
Whenever we do a calculation, the flag
register will store the 'answers' to some questions we may ask.
For Example:
Z Flag Was the result Zero?
V Flag Did the result go too high to store correctly
(oVerflow)?
C flag Did a calculation transfer data out of a register
(Carry)?
There are also some special 'Flags' which change the way
the CPU works, turning things on like 'Binary Coded Decimal'
(a special calculation mode) or disabling 'Maskable
Interrupts' (stopping the Operating System taking over the
CPU).
Flags can be set (1) or Clear (0).
Flags are called Condition Codes on some CPUs and are held in the
'Condition Code Register' (CCR).
Not all commands set the flags, you will need to check the
instruction documentation to know if they do. Also some commands may
leave a flag in an 'undetermined
state', meaning it's changed but not predictable or useful.
We can take advantage of
unchanged flags. On the Z80, "DEC A" changes the z
flag, but "LD" does not. We
can take advantage of this in our program code.
For Example: Consider the code "DEC A LD A,10 jr z,AccZero". This takes advantage of the fact "LD
A,#" does not change the z flag.
If DEC A sets the zero flag, the jump
will occur, and the Accumulator will equal 10 after the jump.
In the instruction references of this book, the 'Flags Affected'
section will show a minus '-' when an instruction leaves a flag
unchanged, an uppercase letter when a flag is correctly updated (for
example 'C'), and a lowercase letter when a flag is
changed to an undetermined state (for example 'c').
The Carry Flag
The Carry flag is quite important on 8 bit
systems, as it allows us to combine registers together to store
larger values than the resister can contain. It works in a
similar way to 'carrying' if you do long addition or
multiplication on paper.
For Example:
On the 8 bit 6502, suppose
there is a 16 bit pair in $01,$02
and we want to add the
pair at $03,$04. We do this by adding the two low
bytes together ($01 and $03) then adding with the carry the
high bytes ($02,$04).
Because the 6502 is a Little Endian CPU, $01 and $03 are
the Low byte and $02 and $04 are the High byte.
If the first addition goes
over 255, the carry will be set to 1, and this will be added
to the high byte.
The 6502 uses
8 bit registers, but we can use the 'Carry flag' to
transfer the 'carry' during addition between two or more
bytes to allow us to support 16 or more bit values.
I/O Ports � Input/Output Ports
There will be times we need to transfer data
to a device other than RAM memory.
For Example: We may want to read the status of the Joysticks, or
make a sound, and we use I/O ports to do this.
On the Z80 and 8086 system we have special commands to do
this.
On systems like the 6502 and 68000 these are 'memory mapped'
and appear as an address in our normal memory range.
Things can be very different depending on the computer.
On the Z80 based CPC the VRAM (Video Ram) is part of the
normal memory, but on the Z80 based MSX it is separate and we
have to use I/O ports to access it.
The RAM addresses and I/O ports are
separate.
Stack
The stack is a temporary store for data. We
don't have very many registers, and there will be times (like in
a subroutine) where we need to put all the current data to one
side and do another job for a while, then bring all the old data
back. This is what the Stack is for.
When we have some data we need later, but need to do
something else first, we put it on the stack. Later we'll take
it off and use it again.
You can think of the stack like your office in-tray. We
can put jobs in our 'in tray' when we want to do them later,
and pull them out when we want to work on them again. We can
put as many items into our in-tray as we want, but
we have to take them off the top of the in-tray, not from the
middle.
The same is true of the stack. We can put many items onto
the stack, but they always go on top of the last one, and we
have to remove them in the reverse order, taking out first the
last item put in. This is called a Last In First Out stack.
This means we put items 1,2,3 onto the stack and we'll take
them back in order 3,2,1.
The current position in the stack (the top of our
in-tray) is marked by the Stack Pointer (SP).
Technically speaking, the stack actually goes DOWN in
memory. If the Stack Pointer starts at $C000 and we put (PUSH) two bytes onto the
stack, the Stack Pointer will point to $BFFE. When we take the
bytes off (PULL/POP) the Stack Pointer will point to $C000.
The Stack Pointer always points to the first empty byte of the
stack.
Items we'll look at later go on the top of the stack.
Interrupts
Interrupts are exactly what they sound like!
Sometimes a device, Like a disk or mouse, will want to send some
data to be processed RIGHT NOW!
The CPU will have to stop whatever it was doing and deal
with the data. The current running program will be
interrupted, and a 'sub program' will run (usually in system
ROM) to deal with the interrupt. When the interrupt is done,
the original running program will resume as if nothing
happened.
On 8 bit systems we can turn interrupts off in many
cases. On most systems we can create our own interrupt
handlers to do clever things, like make the screen wobble by
moving things as the screen is drawing.
There are two kinds of interrupt:
Maskable interrupts are interrupts that can be disabled
(sometimes called IRQ � Interrupt ReQuest). For Example: IM1
calls to Address $0038 on the Z80 can be disabled by DI.
Non-Maskable Interrupts (NMI) cannot be disabled. On the Z80
NMI interrupts result in a CALL to address $0066.
NMI can't be stopped by Disabled Interrupts
(DI).
Endian, Big Endian and Little Endian
Even on an 8 bit system there will be times
when we need to store a 16 bit or more number, and whether our
CPU is 8,16 or 32 bits, each memory address in RAM will only
store 1 byte, so we'll need to split larger numbers up into
individual bytes for storage.
Let's imagine we have 32 bits to store, this will take
four bytes of RAM.
But in what order should these four bytes be stored? It may be
surprising to hear that there are two options.
'Big Endian' stores the highest byte first and the lowest
byte last.
'Little Endian' stores the smallest byte first, and the
largest byte last.
It's not really up to us what 'Endian' to use, as our CPU
will have its 'Endian' built into its memory addressing.
Little Endian is used by the Z80, 6502, 8086, PDP-11 and
ARM*.
Big Endian is used by the 68000, 6809 and TMS9900.
Big and Little Endian compared.
The ISO9660 CD format uses a
format known as �Both Endian�, where a value is stored as Little
Endian, followed by Big Endian. This means a 16 bit value takes 32
bits of storage space, and is intended to allow easy portability
with systems using either type of CPU.
Fun Fact: The
terms 'Big Endian' and 'Little Endian' actually come from the
fictional nation of "Lilliput" in "Gulliver's Travels" which
split in two factions over from which 'end' an egg should be
eaten! Take a look at this link for more details:
Lesson
3 - Basics... More Detailed Mysteries of the CPU!
We learned quite a lot about the CPU last time, but there's some
more specialist stuff we'll need to know for certain CPUs
Lets look at some more technical CPU wonders!
RST / Traps
RST (ReSeT) commands on the Z80, Traps on the 68000, and Interrupts on the
8086 and ARM are special 'commands' which cause a special subroutine to
run, What these do depends on the machine.
These can sometimes be caused by hardware events, but Traps are also used
for debugging, error handling and operating system calls.
On many Z80 systems RST7 occurs when the screen starts drawing.
On the 68000 Traps are also used for errors (like Divide by zero) and even
help with debugging! There is a special "Step" trap which will occur after
each command so we can check what's happening.
Interrupts on systems like DOS using the 'INT' command on the 8086, and
Software Interrupts on RiscOS using the 'SWI' command on the ARM are used
to perform system actions, like changing screen mode.
Privilege Modes
In the 8 bit days, CPUs were pretty basic, and user applications
and the operating system both had the same power and abilities, but
the later CPUs like the 68000 had 'Privilege' modes.
On 16 and 32 bit CPUs Privilege would often be split into 'User'
and 'Supervisor'
User mode would be for the running program.
Supervisor would be for the Interrupts and other system tasks.
There are often 'alternate' registers and commands available only to
supervisor mode. It's quite probable there will be many more levels of
privilege on more modern CPUs.
Zero Page / Direct Page
On 8 bit CPUs with few registers, like the
6502 or 6809, the lack of registers is compensated for by the
'Zero page'.
This uses a block of 256 bytes for quick storage of
values. Reading and writing to this range is faster than
regular memory. The Zero Page uses the memory range
$0000-$00FF. Rather than specifying a full address we just
specify a single byte, so a command like "LDA $66" will load A
from address $0066. The top byte is always Zero, hence the
name!
On later systems like, the 65816, the top byte of the
resulting address was configured
by a one byte 'Direct Page Register', so the 'Zero Page' is
referred to as the 'Direct Page' on these systems, though its
function is basically the same.
The Zero Page uses the first 256 bytes of RAM. Zero page
addresses are specified with a single byte.
Segment Registers / Bank Registers
These only exist in CPUs with a 20/24 bit address bus,
like the eZ80, 65816 or 8086. These have a 24 bit address bus,
but keep their original 16 bit registers. This leaves a
'shortfall' of 8 bits for address calculation.
The solution to
this is often an 8 bit register defining the top byte. This
register is known as a 'Segment Register' on the 8086, or
'Bank register' on the 65816 and 'Mbase' on the eZ80.
The Segment register is actually 16 bit on the 8086, we'll
cover it later in the 8086 chapter.
Segment Registers allow extra memory to be used, while
retaining compatibility with the old programming methods.
However they are more limited than a true 24 bit addressing
system like the 68000 CPU.
An 8 bit segment register is added to the top of a
specified 16 bit address to make a 24 bit one.
Segments also relate to Logical and
Physical addresses. On a CPU like the 65816, Our 64K program may
only be able to see a 16 bit, 64K of memory with memory addresses
$0000-$FFFF. In this case this would be referred to as the programs
'Logical Address'.
However, that range will be part
of a much larger range of the actual resources the system offers.
For Example: The true 24 bit address of that memory could be
$7F0000-$7FFFFF. This 'True Address' is the 'Physical Address'.
Base Pointer +
Offset
There are many times
where the source or destination address for a command will
be calculated from two separate
values.
The terminology may vary depending on CPU, but for this
example we'll call them
a Base
and an Offset.
This is where the
destination address will be calculated from two parts. The
first may be a register (the base), and the second may be a
fixed immediate number (the offset). The actual address used
by the command will be the base plus the offset.
The advantage of this is
improved efficiency. If we have 20 commands that use
addresses in the range $200000-$200010, we can set a
register to the 24 bit base address $200000, and specify the
8 bit offsets in the 20 commands, saving memory and time.
Also, if we later need to
use the same offsets in the range $300000-$300010, we can
just change the base register and reuse the code.
The above example is just
an example. The concept of Base+Offset could be done by the
processor with Index Registers on the Z80, the 6502 Zero
page, or in software by our code.
Using a Base plus an Offset, the resulting address used in the
operation will be the sum of the two.
Index Registers on 8 bit systems
8 bit systems frequently offer this 'Base Pointer + Offset'
addressing via special 'Index Registers', these allow us more
flexibility for specifying the source or destination of our
parameters.
For Example: The Z80 has IX as an Index register. Let's look at
the Z80 command "LD A,(IX+7)" which sets the Accumulator. If IX=10
then the Accumulator will be read from address 17.
The advantage of this is that code using index registers can be
'reused' easily. If we have a 'ShowSprite' function that uses IX, we
can point IX to each of our objects we want to show, and the code can
be designed to find the relevant settings for that object as relative
offsets.
The 6502 also has registers for this purpose. X and Y are used
for similar purposes.
Absolute Addresses and Relative Addresses
In our code there will be many times we will need to specify
addresses, either for reading or writing data, calling subroutines, or
branching on conditions. In 8 bit code, these will tend to be Absolute
Addresses - addresses at a fixed location.
Relative addresses are specified as an offset to the current
position in code (the PC register). These are only really used for
jumps and branches in 8 bit code (not subroutines). 16 bit systems use
relative code more often, as their programs are often 'relocatable' in
memory.
Examples of Absolute addressing are:
Function
Z80 Version
6502 Version
Load a value from address $1000
LD A,($1000)
LDA $1000
Save it to $2000
LD ($2000),A
STA $2000
Jump to address $3000
JMP $3000
JMP $3000
Examples of Relative addressing are:
Function
Z80 Version
6502 Version
Jump to the address 16 bytes away if Z flag is set
JR Z,16
BEQ 16
RISC and CISC CPUs
RISC stands for "Reduced Instruction Set Computer", CISC stands
for "Complex Instruction Set Computer".
RISC processors tend to need more
commands to do a single job than their CISC counterparts, but those
commands will tend to occur faster, and the processor will be
relatively simpler, meaning it's probably more power efficient.
Examples of RISC are the ARM,
RISC-V, MIPS and POWER PC.
Examples of CISC are the Z80, 68000 and 8086.
These terms aren't particularly
helpful to the programmer, especially as the "RISC ARM" instruction
set is more advanced than the "CISC Z80", they are just included in
here to explain the term.
If you find yourself looking at
ARM or RISC-V asking yourself "why isn't it as easy to do X as on
the 68000?" then the answer is probably "Because it's a RISC CPU!"
Load and Store Architecture
'Load and Store' architecture is a
common feature of RISC processors like MIPS and RISC-V.
This refers to the fact that many of the data processing commands
only work between registers, so we need to load the values into
registers in one command, do our operation in a second, and if
needed store the result back to memory in the third.
This can feel a bit frustrating if we're used to 'Register and
Memory' systems like the 68000 or 8086, which allow memory
operations to be combined with calculations, as it is likely to make
our programs longer. However RISC systems are designed to gain
efficiency from this simplified range of addressing modes.
The Instruction Pipeline
While not relevant to 8 bit and most 16 bit processors, Later 16
and 32 bit processors, like the ARM, 386 and 68040 introduced a
concept of Instruction Pipelining.
This is where the processor loads future instructions while the
current ones are still being processed, Generally this increases
the speed, but it does cause some issues.
An imaginary processor with three stages:
Fetch, Decode and Execute
Lets suppose we have an an imaginary processor that loads 3
instructions in advance before decoding. If the first instruction
upon decoding is found to be a 'Jump' then the next two commands
in the pipeline will not be correct. The processor will need to
'Flush' the pipeline of the not needed commands and refill the
pipeline with commands from the address jumped to, slowing down
execution. A technique known as 'Branch Prediction' can be used to
reduce this).
If the executed command causes a branch to somewhere else, the
other commands which are in the pipeline should not be
executed, as they are after the jump. The pipeline must be
'flushed' clearing out these commands, but wasting time in the
process!
The ARM allows individual commands to be conditionally
executed, reducing the number of branches needed, and therefore
reducing the number of pipeline flushes.
On the MIPS processor this causes a different problem, the concept
of Delay Slots! Commands like Jump command should flush the
pipeline, but it does not, and instead the command AFTER the jump
is actually executed before the jump occurs. The jump is �Delayed�
by one command to reduce the commands flushed from the pipeline.
This is sometimes referred to as a Control Hazard. If we don�t
want this to happen we can put a 'No Operation' (NOP) command
after the jump.
Another MIPS side effect is Delayed Loads, where the command after
a load cannot use the register that was loaded, as the load may
not have actually occurred yet! This is sometimes referred to as a
Data Hazard.
Processors like the SH2 and MIPS use a 'Delay
Slot' after a jump. When the code branches somewhere else, the
command immediately after the command causing the jump is
executed anyway! While this makes the code a little difficult
for us to read, it increases the efficiency of the processor,
as less loaded commands are wasted by the flush.
This is all rather confusing, but fortunately delay slots only affect a
few CPUs such as MIPS and SuperH (SH2/SH3), you don�t really need to
worry about the Instruction Pipeline on other systems.
Lesson
4 - Data Representation
Knowing different representations of numbers tends to be more
important in Assembly than languages like Basic and C++ that tend
to do everything for us.
The representation we'll want to use in our code will vary
depending on how we want to use it. We'll also need them
to read documentation and manuals relating to the hardware and
peripherals we want to use from our programs. There
are various forms of representation for numbers we'll want to
know about.
Decimal
Decimal is what we're used to - the 0-9 numbers on our clock, our
receipts, and our normal calculator.
It's known as 'Base 10', as each 'digit' has a value of 0-9.
Binary
Binary is Base 2. Each digit can only be 1 or 0.
"01" in binary is 1 in decimal, "10" in binary is 2 in decimal, "100"
is 4, "101" is 5 and so on.
This works better for computers, which tend to work in only 'Off' or
'On'.
In Assembly, Binary is often shown starting % . You may see 2 in
binary shown as "%00000010", though other assemblers used different
terminology, so you may see the same value as "00000010b" or
"0b00000010".
Hexadecimal
As computers work in Binary all our registers and memory values
will contain values made up of a number of bits. 8 bits would have a
decimal range of 0-255, and 16 bits would have a decimal range of
0-65536.
These may not always be the most 'clear' way to represent these
numbers, which is where Hexadecimal comes in!.
Hexadecimal is 'Base 16', and this is the way
computers combine binary bits into digits we can easily use in our
source code, it effectively represents 4 bits as a single 'digit'.
It uses the normal digits from 0-9, then uses letters 'ABCDEF'
as 'digits' for 10-15. '&10' in hexadecimal is "16" in decimal!
Depending on the Assembler syntax, the way we identify a
Hexadecimal number will vary. Hexadecimal is often shown starting with
a '$' or '&', or sometimes '0x', or ending in 'h'. You may see the
Decimal value 31 shown in hex as '$1F', '&1F', '0x1F', or even
'01Fh'.
Octal
Octal is Base 8. It's not really used any more, but it's worth
remembering the name as you'll hear it from time to time.
If you�re learning PDP-11, however, then it becomes pretty essential,
as the hardware and instruction set is entirely oriented around Octal,
so you�ll pretty much have to use it.
'255' in decimal is '377' in Octal. 65535 in decimal is '177777'
in octal.
On the PDP-11 the instruction set is structured around 3 bit
'clusters' which makes using Octal very logical. Apparently it was
also convenient as it allowed bit representation of values on old LED
displays that could only display digits 0-9, and therefore couldn't
show Hexadecimal.
ASCII
ASCII stands for "American Standard Code for Information
Interchange".This is what you're reading right now! It's
just regular letters, numbers and symbols.
ASCII defines the first 128 characters in the character set, but
many computers allow 256. The second 128 are different on each system.
Note: some systems, like the C64, do not use ASCII, they have a
different character set.
As our assembler will probably convert letters to ASCII bytes,
this may mean strings of text we put in our ASM code do not appear
correctly on screen. We will either need to use an assembler that can
work with this, or to write our code to convert ASCII to the matching
letters on the system.
Byte (Bytes)
A byte is 8 bits. An unsigned byte can represent a value from 0
to 255 in Decimal, which is $00 to $FF in Hexadecimal, or %00000000 to
%11111111 in Binary.
A signed byte will have a value from -128 to +127 in Decimal.
A byte is the smallest unit of memory in a system. Registers on
an 8 bit system work in bytes, and each CPU memory address refers to a
byte of data. Address $0000 is the first byte of memory, and $0001 is
the second, and so on.
Bit
A bit is a single binary number 1 or 0, there are 8 per
byte.
Bits in a byte or word are numbered backwards from right to
left.
In a byte, Bit 0 is the least significant (with a value of one),
Bit 7 is the most significant (with a value of 128).
Numbered Bits in a Word and their respective value.
You'll need to know the position numbers for bit testing
commands, and reading hardware documentation.
Nibble (Nybble)
A Nibble is half a byte, that's 4 bits. A nibble can
therefore have a value of 0-15, or $0-$F in hexadecimal.
Note: There are always two in a byte, there is no such thing as
a system that has a 1 nibble register, or 1 nibble per memory
address.
The unit 'Nibble' is of course a pun on Bite and Bit,
disappointingly there is no such unit as a 'Munch'!
Nibbles in a byte. Nibbles are always 4
bits, so there are two per byte.
Kilobyte
A Kilobyte
is 1024 bytes.
You may ask
why isn't it 1000 bits? Well, because numbers in computing are made up
binary, decimal 1000 wouldn't be very convenient for representing the
numbers we'll frequently need to use. 1000 in hexadecimal is $3E8, but
1024 is a much more tidy $400!
Our 8 bit
machine with its 16 bit address bus has a memory limit of65536, 64
Kilobytes (64K).
The
abbreviation for Kilobyte is KB.
Kb (lowercase
'b') is kilobit (1024 bits - 128 bytes). This is sometimes used in
documentation relating to data transfer speeds or memory capacity and
can make things rather misleading!
Even more annoyingly, sometimes 1 Kb is referred to as 1000 bits,
not1024. When referring to speeds 1 Kb is usually 1000 bits, when
referring to memory 1 Kb is usually 1024 bits.
Strictly
speaking 1024 bits is referred to a 'Kibibit' (1 Kibit) � though
personally I've never seen this term actually used!
Unit
Bytes
Unit
Bytes
1 KB (kilobyte)
1,024
1 Kb (kilobit)
128
1 MB (megabyte)
1,048,576
1 Mb (megabit)
131,072
1 GB (gigabyte)
107,374,1824
1 Gb (gigabit)
134,217,728
Word
The size of a word depends on the system. On 8 and 16 bit systems
(Z80/6502/68000) it's 2 bytes, but on 32 bit systems, like ARM, it's 4
bytes.
A word on an 8 or 16 bit system goes from 0 to 65535, or $0000 to
$FFFF in Hexadecimal.
A signed word on an 8 or 16 bit system goes from -32768 to +32767.
A word on a 32 bit system goes from 0 to 4,294,967,290, or
$00000000 to $FFFFFFFF in Hexadecimal.
Long / Double Word
A Long is two words. On 8 and 16 bit systems this is 4 bytes. On
systems like ARM it's 8 bytes.
A Long on an 8 bit system goes from 0 to 4,294,967,295, or $00000000
to $FFFFFFFF in Hexadecimal.
A signed Long goes from -2147483648 to +2147483647 .
Signed Number
A Signed number is a number that can be positive or negative.
An Unsigned 8 bit number can go from 0 to 255 but a signed number can go
from -128 to +127.
The way signed numbers work in assembly is odd, and exploits a 'quirk' of
the way the processor registers work.
A byte value in an 8 bit register can only go from 0 to 255. If we add a
large number to a byte register (for example adding 255 to 128), the
register will 'Overflow'.
This 'Overflow' does not cause any kind of 'error' as the value in the
register simply goes back to zero, then 'carries on' counting up.
This means when we add a large value to a register, we will actually end
up with a smaller number, and in this way adding 255 to an 8 bit register
is actually the same as subtracting 1!
So 255 is effectively -1, 254 is effectively -2 and so on.
The processor doesn't 'know' whether the 255 in a byte register is -1 or
+255, and it doesn't need to, as we use different condition statements for
signed and unsigned numbers, and we code according to whether our byte is
signed or not.
Don't worry if that sounds odd, it will make more sense later!
Converting a positive to a negative is easy. In code we flip all the bits
via an XOR / EOR or other special command (like CPL), and add one to the
result with an INC or ADDQ (or equivalent). This is known as Two's
Complement.
For Example: Let's look at converting 1 to -1. Our register starts with a
decimal value 1. After the bits are flipped and one is added this will
result in a decimal value of 255 ($01 converted to $FF in hexadecimal).
If we want to calculate the byte or word value a for a negative number on
our calculator we take the maximum value plus 1 (256 for a byte, 65536 for
a word) and subtract the number we want to negate.
For example to get the value of -10 we would calculate 256-10 = 246 ($F6
in hexadecimal).
Binary Coded Decimal
Working in hexadecimal is great for mathematics and calculating memory
addresses, but it's difficult for showing decimal numbers on screen.
Suppose we use a 16 bit number for our 4 digit game score, how can we
easily split the score 65535 into 4 decimal digits quickly for display?
Unfortunately we can't! It would be slow and take lots of divide commands,
something very difficult on 8 bit processors.
This is where Binary Coded Decimal (BCD) comes in!
With normal register values, if a byte has the value $73 in hexadecimal,
it's decimal value will be 115.
In Binary Coded Decimal each 4 bit nibble is a single decimal digit, so
the value $73 in a byte is actually the value 73 in decimal! This makes it
very easy to convert numbers for display.
We've effectively ignored the A-F part of the hexadecimal numbers. As a
result Binary Coded Decimal 'wastes' some memory, as now a byte can only
store 0-99, but it's a much easier way to store data which we need to
convert to decimal digits to ASCII for display.
There are two Binary Coded Decimal formats:
Packed format stores two digits per byte. In this format the decimal value
'1234' would would take 2 bytes, and would be stored '$12 $34'. This is
the most common BCD format.
Unpacked format stores just one digit per byte. In this format the
decimal value '1234' would take 4 bytes, and would be stored '$01 $02 $03
$04'.
Lesson
5 - Assembler Terminology.
The Assembler is the program we will use to turn our text source
file into an assembled program we can run.
Lets learn the terminology we're likely to come across in our
quest to go from source to something we can run!
Assembly Source file
The source file is a text file which contains
the commands that make up our program, usually with an ASM file
extension. We can't run an ASM file on an emulator or computer,
we'll need to convert it to a binary file with an Assembler.
We can edit the file with whatever we prefer, Notepad,
Notepad++ or Visual Studio Code, it's the Assembler that does
the 'real work' of making a runnable program.
An ASM source file being edited with
Notepad++
Binary file
A binary file is pure data, the content could
be any kind of data. Binary files could be an image, some sound
or even a program our retro computer can run. We'll need to know
what to do with it to make use of it, a sound file copied to video RAM
won't help much!
This hex editor shows bytes as Hexadecimal
and their ASCII equivalent.
Assembler
The Assembler will take a source file
(usually with an ASM file extension) and convert it to binary
data.
On a cartridge based system we may end up with a usable
game once it's assembled, but on home computers we may need to
do more work, like add it to a disk or tape image for our
emulator to run.
VASM and most other assemblers are command
line tools.
Compiler
Compilers often make up part of a
�high level language� toolchain (Like Basic or C++).
They will take the high level
language source code, and convert it to Assembly source code (a low
level language), which an assembler will then covert to Assembly.
Linker
Depending on your destination file format and Assembler, assembly
may not be enough to produce a runnable program.
A linker can take multiple files, combine them together and
produce a runnable file.
Debugger / Monitor
A Debugger is a tool which helps us figure
out what is happening in our program, typically when there's a
'Bug' (something going wrong).
They will often show the contents of the CPU registers,
the running code (via disassembly) and allow us to see parts
of the memory.
They may also allow us to 'step' through the code,
running just one line at a time to see what's really
happening.
Debuggers are also sometimes called 'Monitors', in the
sense they monitor the running state of the code.
The very comprehensive debugger of ep128emu.
Not all emulators have such good functionality!
Listing File
A listing file is a summary of how the ASM
source converted to output bytes, it also typically includes the
symbols like a symbol file.
Assemblers don't usually output these by default, but
they can be essential to figure out what's going wrong when
things don't happen how you expect.
A Listing file, the source code and the
resulting bytes are shown.
Symbol file
A symbol file contains the names of Labels and Symbols,
with the text name and resulting byte value.
A symbol file can be used with a Disassembler or debugger
to keep the label names in disassembled code.
You don't need symbol files to build your program,
however if things don't work as expected, they can help you
identify what's going wrong, and also help you learn more
about how the assembler compiles the source to resulting
bytes.
The optional Symbol file. This tracks all
the values that the symbols and labels ended up with.
Disassembler
The Assembler converts a source file to a binary so we can
run it, Binary files don't make much sense to humans though and
we may want to see how a program works that we don't have the
source for. That's what a Disassembler does!
The Disassembler takes a binary file and converts it to a
Source File.
Unfortunately it doesn't do a perfect
job. Label names, Symbols and Comments are not included in the
binary, so these are all lost. Also it can be hard for the
Disassembler to work out what parts of the binary are program
code and what parts are images, sound or other data, and it
may mix the two up.
If you have a "Symbol File" from the assembly stage, the
Disassembler will be able to recover these label and symbol
names, which can help for real time disassembly of a program
while it's running.
A disassembled file - but the labels have been lost for
the addresses
Disk Images and Tape Images
Our home computers probably can't run a binary 'as is',
we'll need to make it into something the computer can use.
We'll need to put our binary onto a 'Tape' or 'Disk'. We
won't use a real one though, we'll use a fake 'Disk' or 'Tape'
image.
We'll need special software to make this Disk or Tape
image. Unfortunately, Disk, Tape and Cartridge formats are
platform dependent, there's not 'one solution', so you'll need
to check the documentation relating to the system you're
interested in.
Amstrad CPC Disk Image Editor: CpcDiskXP.
Unfortunately you typically need a different disk image editor
for each system.
Lesson
6 - More Assembler Terminology!
The Assembler is the program we will use to turn our text
sourcefile into an assembled program we can run.
Lets learn the terminology we're likely to come across!
Immediate Value
An 'Immediate' is a fixed number parameter specified in the
assembler source after the command.
For Example: Let's look at the commands "LD A,3" (Z80) or "LDA #3"
(6502).
In these examples the value 3 is an 'Immediate'.
Other commands like the commands, "LD A,(3)" or "LDA 3", are
loading from the address '3', so these are not Immediate values.
Don't worry if you can't tell Immediate values from values loaded
from addresses at this stage, as the syntax varies depending on the
assembly language you're using.
You'll notice on the 6502 and 68000, Immediate values start with
a "#", and if you forget that symbol the value will be be treated as
an address. This is a common mistake you're likely to make, so it's
worth double checking when things go wrong!
Label
A label marks part of the code which we will refer to
later, it's like a line number in Basic. Usually it's a
destination for a jump (a command like GOTO in Basic) or some
data we'll read or write elsewhere in our code (like sprites or
variables).
Typically (unlike command opcodes) labels have to be at
the far left with no tab indent, and have a colon after the
label.
A label called "PrintString" - we'll CALL this later in our
program. Note the label is at the far left, but the code is
indented by at least one tab.
Operator
An operator is a 'command' like ADD or SUB. The
abbreviated commands used in Assembly are called 'OP Codes'.
Commands in Assembly typically need to be 'indented' with a tab
to identify them as not being labels, though this can vary
depending on your assembler.
Note: You may see
�Opcodes� referred to as the �Byte data� that the command
assembles to, and the human readable command referred to as
an �mnemonic�
For example, the Z80
mnemonic �NOP� assembles to the byte opcode �&00�
LD, INC and OUT are operators.
Operand
An operand is a parameter, often a Register or an
Immediate value.
A, HL and TI_LCD_DATA are operands - the
parameters of the operator.
Assembler Directives
Assembler directives are commands which instruct the
Assembler to do something. These are not converted directly to
command bytes for the CPU, instead they change the function of
the Assembler and tell it how to assemble
the code.
There are a wide range of assembler
directives, and their format will vary depending on the
Assembler, but some examples are: "EQU" (Symbols) "IFDEF"
(Conditional Compilation) "ORG" (tells the Assembler the
address the code will run from) ".186" (tells the Assembler
what version CPU we're compiling for) and "DB" (Defined Bytes
of data).
You'll need to check your
Assembler manual to know the commands available to you,
and you'll probably only need a few of the wide range
available. We'll discuss some of the general ones you're
likely to need throughout this chapter.
There's a huge range of Assembler directives, depending on
your CPU, Assembler and syntax choice. The only thing you can
do is check your Assembler's manual.
Symbol
A symbol is a fixed value (it doesn't change during our
code). "PI EQU 3.1415926" sets the symbol "PI" to the number
3.1415926. We can now use PI in our code, rather than typing all
those numbers again.
It's the assembler that converts the symbol, the binary
file will not change whether we use the number or the symbol
in our code.
EQU stands for 'EQUate' or 'EQUivalence', it tells the
assembler the symbol has the same value as the number which
follows. The exact syntax of the command varies depending on
your assembler.
In VASM, EQU statements have to be at the far left like
labels.
Two Symbols defined for the TI graphics
ports.
Code Comments
There will be times we will want to put notes within our
code so we can remember how it works. These are known as
'Comments' or 'Remarks' (REM statements).
While the syntax varies depending on your assembler,
comments in assembly usually start with a semicolon (;). They
can be on a line on their own, or at the end of a line of
code. After the semicolon, the assembler will ignore the rest
of the line, so adding a semicolon to a line of code will
quickly disable it.
These are totally optional, they make no difference to
the resulting program, but you will probably find them
essential to help make the function of the code you're writing
clearer, as when you're debugging it in a few weeks, or trying
to reuse part of your old code many months later, you'll
benefit from having spent the extra time to add at least a few
comments to your code.
Code comments can be a whole line, or after a command. On most
assemblers a comment starts with a semicolon (;).
Conditional Assembly
There may be times when we want to build multiple
versions of our program from the same source.
For Example: Maybe we want two versions of our game, a 'Trial
Version' and a 'Paid version' with more features.
We don't want to keep two separate copies of the source
files, as this would increase our developing and maintenance
time, instead what we do is use 'Conditional Compilation'.
By using assembler directives, such as "IFDEF mysymbol"
(IFD on 68000) and ENDIF, we can define blocks of code
which will only assemble if a symbol is defined.
By defining only certain
symbols, we can enable and disable these blocks, changing
the code that is assembled, and the resulting program.
Enabling and disabling symbols can be done by simply putting
a semicolon (;) at the start of the line, turning them into
comments, or symbols can often be defined on the assembler
command line, meaning we can run different scripts to build
different versions of our code.
Conditional assembly allows us to make it
possible to have one source file that can have different build
versions.
Macro
A Macro is a bit like a symbol. It defines a set of
commands that are given a 'name'.
We can then use that name in our code and the assembler
will replace it with all the commands we defined.
For Example: We could create a macro that automatically
prints a letter and call it PRINTCHAR. We can then use
PRINTCHAR 'A' or PRINTCHAR 'B' in our code.
The Assembler will use our definition to produce the
resulting program with the contents of the macro replacing
this.
It's a bit like making our own commands. It saves us
copying the same code over and over and it saves a bit of
time, rather than writing a subroutine.
The syntax of a macro definition depends on the
assembler. You'll need to check the documentation of your
Assembler.
We've defined a macro "z_ld_iyh_l". When we
use this name the assembler will 'put in' all the commands we
specified here.
Defined Data
There will be times when we want to put sections with byte data
in our code. These could be values for lookup tables, addresses for
indirect jumps, or bitmap data.
The assembler directives
we use to do this will vary depending on our assembler, but 8 bit
assemblers often use DB and DW for Define Byte and Define Word.
DS (Define Storage) will define a block of data (usually
initialized to zero). For example to define a block of 512 bytes DS
512 could be used.
On 16 bit systems the syntax is often DC.B, DC.W and DC.L for
Define Constant Byte, Word or Long.
DS.B, DS.W and DS.L can be used to define a block of 8, 16 or 32
bit data.
For example: "DS.W 100" will define 100*16 bit words, 200 bytes
total (the same as "DC.B 200" would).
A similar concept on 68000 systems is the BSS Section. This stands for
'Block Started by Symbol', though that doesn't really make its purpose
clear. The BSS section is an area of memory which is allocated to our
program but starts with zero values, so we can use it to store our
sprites or level data, but we could use it for a screen buffer.
Lesson
7 - Even More Assembler Terminology!!!!111
The Assembler is the program we will use to turn our text
sourcefile into an assembled program we can run.
Lets learn the terminology we're likely to come across!
Indirection / Pointers / Vector Tables
Indirection is a common way of using
registers for 'lookups'. This is where a register contains an
address to look at for the source or destination value for a
command.
It's kind of like going to the cupboard, and finding a
note saying 'The pickles are in the fridge'!
Pointers (like those used in C and C++) are a form of
indirection, and this kind of functionality is frequently used
in assembly as well.
'Vector Tables' are used by interrupt handlers on some
CPUs like the 6502, they are effectively a list of address
pointers.
Indirection on the 65c02. The data is read
from the address at an address.
Jumps and Branches
Normally after each command the CPU will run
the next command, but there will be times we need to jump
elsewhere.
Unconditional jumps will ALWAYS jump to a different place, this
is like a 'GOTO' command in Basic.
Conditional jumps will SOMETIMES jump, depending usually on one
of the flags registers. This is like the 'IF THEN GOTO' commands
in Basic.
Depending on the processor and command, There are two kinds of
Address that you may come across, 'Absolute' and 'Relative'.
On an 8 bit system, 'Absolute jumps' use a full 16 bit address,
meaning the destination can be anywhere in memory, but
'Relative jumps' use a signed 8 bit offset, and are relative to
the current line of code. This result in smaller files and code
is relocatable, but can't jump very 'far' forward or backward in
code (-128 bytes to +127 bytes).
On processors like the 6502, 'Jumps' use an absolute address,
meaning a full 16 bit address is used, but 'Branches' are
relative jumps, and use a signed 8 bit offset, limiting the
possible range of the branch.
Absolute and Relative jumps in Z80 code. The
Assembler converts the labels to numeric Addresses and
relative offsets.
Subroutines
Subroutines are a bit like a JUMP � however
these will run part of the code, and then come back when return
occurs. This is like GOSUB / RETURN in Basic.
Subroutines are completely essential! When we write our
program, we'll need to break up the problem.
Suppose we want to show the message 'Hello'. We'll
probably run a 'PrintString' subroutine, that will probably
make multiple calls to a 'PrintChar' subroutine which may call
a 'DrawPixel' subroutine!
We write and test each subroutine separately. Once we get
'PrintChar' working right, it's no harder to use than the
PRINT command in Basic.
This example calls a subroutine called Newline.
Self Modifying Code
Self Modifying Code is code which changes
itself. This is usually done to improve speed, but also saves
memory.
For Example: Suppose during our game we have to check
either the keys or joystick, depending on the option selected.
We could read the 'controller' byte, and call the Key
routine, or the Joystick routine, but it would be quicker to
call the Key routine and rewrite the call if the player
enables joystick. This will save a few bytes, and a little CPU power, but
it makes the code harder to read.
Here we've got a routine that draws 8 bytes
of pixels to the right. The "INC HL" is changed to a "DEC HL"
via self modifying code, and 8 bytes are drawn to the left
Aligned Code
There may be times when we want to specify
our code to be aligned on a certain byte boundary.
For Example: We may want the bottom byte of our data to be a
$00. So $1100 or $1200 is OK for our purposes, but $1180 is no
good. We can do this with a command like 'ALIGN
8' which will align the data to an 8 bit boundary, meaning the
bottom 8 bits of the new address will be zero.
This is useful for times we're reading data from lookup
tables, and when we want to only increase the low byte of a 16
bit address ("INC L" is faster than "INC HL").
The 68000 can only read
words (including commands) on even boundaries (where the
bottom bit is 0). This has a special EVEN command to do this
(equivalent of ALIGN 2).
Note: Other assemblers may use other syntax. For Example:
VASM uses ALIGN 8, but WinApe uses ALIGN 256.
An example Align statement and resulting assembled
binary. The "Align 4" has caused the 'Hello World' to
start at $90.
Bitwise Operations / Logical Operations
Bitwise Operations (also known as Logical Operations) are commands
performed on all the bits of a parameter. Each logical Operation will
take two parameters and the bits in the result will be the logical
result of the two having the operation applied to it.
Different CPU's have a different operations available (Many don't have
BIC), and sometimes the names may differ (XOR is often called EOR).
Here is how the result of common operations are calculated based on
different source values
AND
OR
Exclusive Or
Bit Clear
AND
OR
XOR / EOR
BIC
Lesson
8 - Programming Techniques
Assembly programming has some common programming techniques and
methods it's worth pointing out, as they may help you design your
programs.
Lookup Table
A Lookup table is a table of pre-calculated values for some
purpose. They are used to save time where the calculation job
would be too slow.
Common uses for a lookup table would include Sine values,
Multiplication calculations or transparency masking colors of
sprite data, though there are any number of possible practical
uses for lookup tables.
For Example: Suppose we want to divide numbers between 0
and 279 by 7, and get a whole number (quotient) and remainder
result. On an 8 bit CPU this will be very slow.
If we can spare 280*2 bytes then we can pre-calculate the
whole number and remainder for each value 0-279, and store them
in pairs in the lookup table.
When we need the answer, we just look at the correct
offset, and the two values are there.
16 Byte Look up tables for generated
movements.
Jump Block
A Jump block is a set of Jump commands at a specific
location. These jumps will have a defined 'purpose'.
For example: If we had a jump block at address $3000, we
could have a jump to a ClearScreen function at $3000, and a Jump
to DrawCharacter at $3003. Our code would call these addresses
to perform the task required.
The advantage of this is a later revision of the program
could have completely different internal structure, but,
provided the address and defined
purpose of the jumps was unchanged, anything
that uses the jump block would still work the same.
For this reason Jump blocks are often used in system ROM,
but we may want to use them in our own programs as part of our
'Game Engine', especially if we're making a 'Multi Load' game
that loads each level separately, as it means we can make
improvements later without changing (and recompiling) all our
levels.
The ChibiAkumas Jump block with functions
provided by the game core.
Relocatable Code
When we assemble
our program, often it will have 'Absolute addresses'. This means it must
be loaded and executed at the address the code was assembled
for (defined by an ORG statement or similar). If the code is loaded to a
different address, then the Jumps will go to memory addresses which do
not contain the commands they should.
For Example: Let's suppose our program starts at $1000, and there
is a jump to absolute address $1010 ("JMP $1010" in 6502).
If we load this program to address $2000 then the "JMP $1010"
command will still go to address $1010, and the code will not be there.
(it would be at address $2020).
Sometimes we'll want our code to be able to move, and be run at any
address in memory. This is known as 'Relocatable code'. To achieve this
we need to ensure we only use 'Relative addresses' in our code *.
If our example program starts at address $1000, with a relative
jump to +$10 ("JR $10 in 6502), but we load the program to $2000 the JR
command will now go to the correct $2010. The program is 'Relocatable'
and will work from any address.
The assembler can't help us with this, we need to only use relative
commands in our code. If we always use commands like JR instead of JMP
on the Z80, and BRA instead of JMP on the 65c02, then we can create a
relocatable program.
* Note: Technically speaking some 68000 based systems CAN relocate
absolute addresses, they have a 'Relocation' table which contains
pointers to the bytes of code which are absolute addresses, and these
are modified by the operating system before the program is executed.
Lesson
9 - Graphics Terminology
Lets look at some of the terminology we'll come across when it comes
to graphics and video hardware.
Sprite
A sprite is an small moving image used in our
game. There are two types:
Hardware sprites are shown by the hardware. They are very
fast, and removing them is very easy, we just turn it 'off',
but there's a limit to how many are on the screen. Most
consoles 8 and 16 bit consoles use hardware sprites.
'Software sprites' are drawn to the bitmap video memory
by 'us'. We have to basically plot each pixel to the screen to
draw the image to the screen. When we want to remove them we
have to redraw the background where the sprite was. This is
slower, but there are no limits. Many 8 bit home computers
like the Amstrad CPC and ZX spectrum were not capable of
hardware sprites, so could only use software sprites.
An imaginary platform game screen with tiles
and sprites.
Right: The final image is made up of 2 layers. The top layer
is hardware sprites, The second is the tile map.
Tiles, Patterns and Tile maps
Hardware Tile maps are very common on console
and arcade hardware.
The Tile map is a grid type 'layer' of square 'tiles',
usually 8x8 pixel squares.
Each entry in the tile map is not a picture, it's a
number which refers to a Pattern which is the tile bitmap
itself. This saves a lot of memory, as a 32x32 tile map will
take 1024 or 2048 bytes, but means there's often a limit to
how many 'unique' tiles the screen can have.
For Example: The Sega Master System would require 768
tile patterns to have a completely unique 256x192 pixel
screen, but the master system only supports 512 tiles. This
means any 'full screen image' will have to have duplicated
squares (probably blank).
8 bit systems usually have just one tile layer, but 16
bit systems often have 2 or more. This allows for 'Parallax'
where there's a foreground and a background that move at
different speeds. You may think you've seen this on 8 bit
systems, as many games with just one layer do clever tricks to
simulate this effect!
Just like with sprites, bitmap based computers can also
use 'tile maps' but these will be software based, and
therefore much slower than the hardware based ones.
An imaginary platformer game screen.
The tile map uses repeating
objects, a star, a grass pattern, and a block - these are
repeats of the same pattern. A 1 byte tile number can define
this large area.
Bitmap screens
A bitmap screen is an image where the visible contents of the
screen are defined by a block of byte data in memory, and each pixel
can be independently altered. This gives more flexibility than tilemap
based systems allowing for vector 3d graphics and other complex and
varied screens, but uses more memory than a tilemap.
Depending on the pixel depth, the number of pixels per byte will
differ.
A 256 color system will have 1 pixel per byte.
A 16 color system will have 2 pixels per byte.
A 4 color system will have 4 pixels per byte.
A 2 color system will have 8 pixels per byte.
This is only a general rule, some systems like the spectrum have
'color attributes' and there's a few bytes of 'wasted space' in the
CPC screen memory area.
Let's compare different system memory usage:
The 320x200 4 color screen of the CPC takes about
16k.
The 256x192 16 color screen of the MSX2 and SAM Coupe takes 24k.
The Spectrum's 256x192 screen takes 6k.
This is part of the reason MSX2 games tend to be slow, and Spectrum
games are much faster than the CPC!
Color Attributes
Color attributes are a way of 'saving' memory but giving
a more colorful screen.
The Spectrum's black and white screen uses 6k of bitmap
data and 768 bytes of color attributes. The extra 768 bytes
turn a black and white screen into a color screen with 2
colors per 8x8 square.
Color attributes on ChibiAkumas on the ZX
Spectrum, each 8x8 square only has 2 colors.
Palettes
A Palette is a set of colors.
Typically our screen will be limited to 4 or 16 colors, but we can choose those colors
from a wider range (27 on the CPC).
Palettes are also relevant to consoles. A console like the SNES
uses 16 color sprites, but each sprite can choose its 16 color palette
from a wider range, giving 256 on screen colors! Doing so saves
memory, as a 16 color per pixel image takes half the memory of a 256
color one.
Raster Beam
Cathode ray tubes (old non flat-screen TVs)
use a 'Raster beam' that scans the screen in a zigzag from top
left, to top right and down the screen.
The raster beam scans the screen from left
to right, top to bottom - it happens so fast the eye cannot
see the redraw.
Many old games took advantage of this to
perform 'clever tricks', changing things as the screen was
redrawing.
For Example: ChibiAkumas used this to get 16 colors on a
4 color screen. Other games use it to make the screen 'wavy'
and systems like the C64, which has an 8 sprite limit, move
those sprites the line after they've drawn to a new position,
overcoming the sprite limit (ChibiAkumas also did this on the
CPC+).
ChibiAkumas Ep2 Title screen. The screen
uses 4 color mode, but 2 colors are changed 4 times during the
raster redraw to make 12 colors appear.
VBlank
Vertical blank is the time after the screen
has finished drawing, but the next screen hasn't started yet.
On many systems this is the only time we can alter Video
RAM, but 'waiting for VBlank' is also an easy way to limit the
maximum speed of our game and stop it running too fast.
There is also a 'HBlank' (Horizontal blank) between
lines, however it is very short so not as useful.
VBlank and HBlank.
CRTC
Cathode Ray Tube Controller. This is
the chip used on some systems to turn bytes of memory into a picture.
Changing the settings of this will change the shape of the screen, the
address used for the screen, and other effects.
VDP
Video Display Processor. This term is used by
some systems to refer to the 'graphics card' of the machine.
The VDP will often have separate memory from the main
CPU, which cannot be accessed in the same way.
The CPU talks to the VDP which has its own
memory, and generates the image for display.
Bitplanes and Linear data.
These are two different ways of storing image
data.
Let's suppose we have a system with 4
color sprites, 4 colors requires 2 bits per pixel (2bpp).
If we want to store 8 pixels (two bytes), we have two choices:
1. Store the two pixels for each byte together. This
could be called 'Linear' data.
2. Store all the bit 0s for each of the 8 pixels in one
byte, store all the bit 1s in a second byte. This is known as
storing in
'Bitplanes'.
Example of 2bpp as bitplanes or 'linear'
data.
Note: Some non-bitplane bitmap screens use other orders for
pixel bits (Like Mode 0 of the CPC).
Screen Buffer
A Screen buffer is the memory that makes up the bitmap screen. If
we write data into this, the visible screen will change.
Double Buffering is where we use a pair of bitmap screens. One is
the 'Visible screen' we show to the player, the other is the 'Drawing
screen' which we update. Once the Drawing screen is completed, we flip
the two buffers, showing the newly drawn screen, and using the
previously visible screen as the new drawing screen.
Lesson
10 - Sound Terminology.
If you're like me, you know quite a bit about programming but
next to nothing about sound! Unfortunately, this can make getting to
grips with controlling the sound hardware of retro computers a bit
of a challenge, as it often means reading over technical
documentation with lots of technical lingo and concepts I've no
prior experience of.
In this lesson, We'll look at some of the concepts of making sounds
for the 'less musical' programmer!
Sound and Waveforms
Sounds are waveforms created by our speakers, and these are
created by a Digital to Analog converter (DAC).
If we have an oscilloscope we can 'see' these waveforms. The
vertical axis of an oscilloscope view will be the 'Volume Level'
and the horizontal axis is 'Time'.
Figure 56: The 'Height' of a wave is known as the Amplitude
(Volume to you and I!). The 'Distance between peaks' is the
'Wavelength', and the number of waves in a given period of time is
the 'Frequency'. High frequency/Short wavelength is high pitch
(treble) Low frequency/Long wave length is low frequency (Bass)
Figure 57: If we look at a fraction of a second, we'll see the
shape of the tones. The 'Taller' the wave (Amplitude), the louder
the volume. The 'Shorter' the wave, the higher the pitch of the
wave
Retro Sound Waves!
Retro computers and their sound processors often create a
variety of simple 'instruments' which have very distinctive
waveforms. Most of these waves are 'named after' the shape they
appear!
The first of these is a 'Sine Wave'. This is the simplest wave
used by FM synthesis, it gives a nice 'smooth' sounding note.
'Square Wave' is more artificial sounding and can sound a little
harsh, it's the staple of sound chips like the AY-3-8910. It
just repeatedly switches between 'Off' and 'On' at even times,
so is easily created on systems with no dedicated sound chip
like the ZX Spectrum.
'Triangle Wave' is used by sound chips like the C64 SID, it
sounds a little less harsh than the Square wave.
The 'Saw Tooth Wave' has a more 'buzzing' sound, these are also
used by the SID.
A 'Pulse Wave' is also similar to the Square wave, though the
length of the peak of the wave, and time between waves differs,
giving it a different sound.
An honourable mention goes to 'Noise'! A noise wave will have
apparently random amplitudes. Many sound chips will have some kind
of noise generator which will create noise.
Figure 58: Various waves generated by the AY, ADLIB and SID,
viewed using a software oscilloscope
Beeper sound
The term 'Beeper' is often used to refer to computers which do not have
a dedicated sound chip, and are only capable of simple sound, which is
dependant on using the main CPU to build a wave form.
On the spectrum, we can only turn the sound 'on' or 'off' via a single
bit. by switching the wave at timed intervals we can build a square wave
in this way. The shorter the time between the switches, the higher
pitched the sound, but as sound can only be on or off we can't really
control the volume. Also unfortunately, as building the wave form
requires constant CPU control, it's not really possible to play music
during gameplay on these systems.
Some systems (like the Dragon 32/ Tandy Coco) have slightly more
advanced sound� these have a Digital to Analog converter (DAC) which can
take an output level (6 bits on the Dragon 0-63), these can be used to
form more complex sounds and clear volume levels, however it still
requires CPU power to play sounds.
Programmable Sound Generators (PSG)
Programmable Sound Generators refer to the more simple sound generators
which produce sound with simple wave patterns like Square waves.
Examples include the AY-3-8910 sound of the CPC & Vectrex, the SMS
& BBC SN76489 and the C64 SID chip� these sound chips were also used
on many other systems, and many later sound chips are backwards
compatible with them...The NeoGeo is backwards compatible with the AY,
and the Genesis is backwards comaptible with the SN76489.
As a dedicated sound chip is keeping the wave playing, the CPU can do
other things meaning the sound can play during gameplay!
Frequency Modulation (FM)
Frequency Modulation is the generation of sound by using oscillators to
modulate a waveform making complex sounds far more 'realistic' than
simple sine or square waves.
FM Synthesis was popular in home and arcade systems in the early 90's,
before Wavetable synthesis became available in home computers.
MOD tracks and Wavetable synthesis
With the introduction of the Amiga and it's digital sound capabilities
MOD trackers (Module Trackers) were born!� rather than using FM
synthesis, these used a wave sample of an instrument (often sneakily
ripped-off a music CD or LP!), To keep the tone when a key is held down
part of the sample can be repeated, To make the pitch higher or lower,
the speed the sample was played back was increased or decreased. However
these relatively crude techniques often gave the notes an 'unnatural'
sound.
Wavetable synthesis is a more advanced, but similar concept which uses a
large bank of different sound samples, When playing a tone various
samples are combined along with other techniques (usually in hardware)
to produce more natural instruments. A simple example would be storing a
different sample of an instrument at each octave, and pitch bending the
sample within an octave, this would mitigate the artificial sound that
would be caused when this is done in a simple MOD file.
Total level (Volume by another name!)
Volume is pretty fundamental to making sounds, but this is often
referred to as 'Total Level' in sound documentation. This may also be
called Amplitude.
Channels
Channels can refer to 'Left Speaker' and 'Right Speaker', but they also
often refer to the number of 'Playing sounds' the hardware can manage at
the same time. On some systems we can assign different Left and Right
volumes to set the Left-Right Panning position, but on others like the
CPC AY chip it's fixed with Channel A being 'Left', B being 'Center' and
C being 'Right'.
Envelope
In this case Envelopes have nothing to do with letters! In
music an Envelope affects how the sound changes over time. In many
cases, the Envelope will define how the volume fades, or ripples
after the tone starts, but it's also possible for envelopes to
relate to the pitch.
What options and capabilities for envelopes that are available
will vary depending on your sound processor � If they are
available at all!
Figure 59: The AY-3-8910 Envelopes that can be applied to the
channel volume, these can make a note fade in, fade out or 'wave'
over time.
Attack, Delay, Sustain, Release and K-On
More advanced sound processors, such as FM synthesis, will
attempt to simulate the realistic sound an instrument will make by
adjusting the sound effect over time.
For Example, Lets consider a real piano keyboard.
The sound will start when you press the key. In programmable sound
synthesis this is often referred to as K-ON.
When you press a key, a hammer will hit a string in the piano
causing a sound to occur quickly. This is known as the 'Attack'
phase.
Immediately after the key was pressed, the sound will quickly
change for a short period. This is known as the 'Decay' phase.
After a while, if the key is held down, the sound will 'level
off', but the string will continue to vibrate. This is the
'sustain' phase.
The point the key on the piano is released is known as K-OFF in
sound programming.
On our Piano when the key is released, the damper will stop the
string vibrating, causing the sound to quickly silence. This is
known as the 'Release' phase.
To allow sound processors to create realistic sounds, it's common
to see these stages represented, and configurable, on our sound
processor.
Figure 60: Typical stages of a sound across time as a key is
pressed, and after it is released.
Frequency Generators, Noise Generators, Envelope
Generators
Many Sound
processors will 'Build' a sound out of a combination of 'Generators'.
These can be configured, and enabled and disabled as required based on
the sound channel. Sometimes different channels on the sound chip will
have different capabilities, if you look at the documentation of the
sound processor you may see a block chart showing how the generators and
channels link and combine to produce the sound.
LFO
A Low Frequency Oscillator is a low frequency signal generator which can
be used to add a relatively 'slow' oscillating effect to sound. This can
be affect the Pitch, Volume or even panning position
Tone and sweep
On some PSG based sound processors certain channels may offer a 'Sweep'
function.
This allows the channel to affect the pitch of the tone over time,
making it bend up or down.
Examples of systems that offer this function are the NES and Gameboy.
Octaves, Notes and key fractions
Many sound chips allow use to produce sounds just with a numeric
'frequency' � we often just specify a number of 12-16 bits to
create a range of sounds from low to high pitch. However some
processors require us to work in notes and Octaves, and for the
musically challenged (like me) this can be a bit of a shock!
The Octave refers to the fact there are 8 notes between two
notes of the same letter for example two 'C notes'.
It's
easiest to think of the Octave when looking at a piano keyboard.
Many of the notes have a black key 'Sharp' (♯) or 'Flat' (♭) key
which is an 'In-between' note, which is a half 'step' up or down
to the next note. C♯ and D♭
Of course, we may not want to make a sound that matches a clearly
defined note, so it's likely that our hardware will offer some
kind of 'Key fraction' to allow finely granular tones between two
notes. You'll need to check the hardware documentation to see what
options are available.
Figure 60: The Octave as shown in sheet music and a piano keyboard
Operators and connections
Fundamental to FM Sound generation is the concept of Operators
and Connectors.
An Operator is typically the combination of an Oscillator, an
Envelope and an Amplifier. We can configure an operator's
frequency and volume to change the sound generated. Multiple
operators are 'Connected' to form more complex � and hopefully
more 'realistic' sounds. FM Processors will offer a variety of
options for how the Operators are Connected to form the final
sound
Figure 61: Two Connection options on the ADLIB Soundcard showing
how the Operators are combined.
Digital Samples � PCM Waves, Signed and unsigned
On modern machines Playing digital sound of speech and other
digital sound is commonplace, but it's actually surprisingly easy
to get almost all these old retro machines to achieve the same
thing.
All we need to do is set the volume level of one of our sound
channels sample by sample to 'Build' the wave file. The only
downside of this is that it uses all our CPU power, apart from
the memory used by the wave file of course!
Actually, the biggest challenge is converting our source file
(Which is likely to be a 16 bit signed wave) to the range
required by the volume level of our system - often unsigned 4
bit is best as it gives us two samples per source byte, but on a
system like the ZX spectrum who's sound can be only 'on' or
'off' we need a 1 bit per sample sound file!
For most systems we just need to set the volume level of the
channel, but on a small number this alone will make no sound, and
we'll need to set the frequency of the channel to some high pitch
to form the wave.
Figure 62: We will need to convert the range of values in our wave
file into a range of unsigned values suitable for our classic
computers sound system.
16 bit Sound samples can get pretty big pretty quickly, so some
kind of compression may be desired! Advanced codecs like MP3 would
be great, but these would be too complex for early hardware,
fortunately ADPCM comes to the rescue, offering a 75% reduction in
file size, minimal sound quality loss, and a simple codec!
Suppose we have a 16 bit wave form we want to compress, the wave
will go up and down over time. Rather than storing each sample,
we could store the 'Difference' between the two samples. In
theory we coud reduce this to just 4 bits per per sample, and
have a 'look up table' of 16 difference values, using the
nearest to attempt to recreate the waveform.
The trouble is, using just 16 possible differences between
samples wouldn't allow for much subtlety, and this is where
'Adaptive Differential PCM' comes in!
ADPCM uses a larger lookup table of more than 16 values, and 4
bits per sample representing a -8 to +7 shift in the used
position in the lookup table, this allows the codec to represent
large sweeping changes at one time, but small subtle ones at
another � all while only using 4 bits per sample!
The 'lookup table' used, and how exactly the algorithm works will
vary depending on the hardware or software implementation, the
above description is just a simple explanation of how ADPCM works
in theory.
Figure 63: An imaginary ADPCM implementation. Although numerically
the stored values look very different, overall the shape of the
waveform is not that different.
I/O Port and Joysticks
It may seem odd to mention Joysticks on the topic of sound, however in
some cases the Joysticks are connected to the sound chip, so do not be
surprised if you find yourself referring to sound hardware documentation
when it comes to reading the Joystick or controlling other devices.
The AY-3-8910 compatible sound chips had a built in I/O port, which was
used for the Joysticks on many systems (Like the CPC,MSX and FM7), and
on the Tandy COCO the DAC that handles sound is also used to read the
analog Joystick.
Lesson
11 - Other Hardware Terminology
There's a few other weird and (somewhat) wonderful terminology we
may come across, that it's worth discussing at some point.... and
that point is right here!
Analog Joystick
An Analog Joystick is one that is not just
'On or Off', it will give a range of values depending on the
precise position of the joystick.
If the range was 8 bit, the centre would be a value like
128, full left would be 0 and 'a bit left' would be 100.
They give smooth movement, but unfortunately they're
often harder to read than a digital joystick, which is only On
or Off. This is partially because reading the numeric position
is often more complex, and because our imaginary example is
unrealistic.
An Analog joystick will be slightly 'unreliable', so on a
joystick with a centre of 128, you will find it won't actually
return to this value every time. The 'centre' would probably
be a range of values, and we may consider any value in the
range 112-144 to be treated as the 'center',
this is known as a Joystick "Dead Zone".
Analog and Digital Joysticks.
Strobe
A sequence sent to a data port (or single bit of a
port) to signal to a device. This is often done to initialize a device
and prepare it to start sending data.
For Example: We may strobe a joypad port to tell
it to start sending the data for the first of four joypads, subsequent
reads will get joypads 2 3 and so on.
DAC / ADC
A Digital to Analog Converter converts a digital signal to an
Analog one, for example taking bytes and converting them to sound. ADC
is the reverse, taking Analog and converting it to Digital.
Converting between Analog and Digital is needed for sound, non
digital joysticks and other such tasks.
Multiplexer
A device which allows for multiple devices to
share a single line.
For Example: On the Tandy COCO, the multiplexer shares the sound
DAC between the joystick, speaker and cassette.
A multiplexer allows multiple devices to
connect to one line.