Z80 Assembly 39: Software Sound Synthesis (Beyond the Square Wave)

Limitations of the Beeper (Square Wave) The simple beeper (Part 28) can only produce a square wave (a harsh, buzzy tone) because it only switches the speaker ON or OFF. To create richer sounds, like music that sounds closer to a sine wave, or complex speech effects, we need to simulate varying amplitude (volume). Pulse-Width Modulation (PWM) Synthesis Since the Z80 cannot vary the voltage to the speaker, we simulate varying amplitude using Pulse-Width Modulation (PWM). ...

September 27, 2025

Z80 Assembly 30: Emulating Floating-Point Arithmetic (High-Level Math)

The Challenge of Floating-Point The Z80 is an integer processor—it can only handle whole numbers. Floating-point numbers (like 3.14159 or 1.2e-5) are necessary for high-precision math used in physics simulations, graphics transformations, or advanced financial calculations. To use them, we must emulate the standard IEEE 754 format using multiple bytes of memory. Floating-Point Structure (The Four-Byte Standard): A typical 32-bit (4-byte) single-precision float is stored across four consecutive memory locations: ...

September 27, 2025

Z80 Assembly 24: Color and Attributes (Manipulating Screen Appearance)

The Need for Attribute Memory On many Z80 retro systems (such as the ZX Spectrum), the screen display is split into two distinct areas of memory: Pixel Data: Where the bits defining the shape of the image (the ‘black’ or ‘white’ dots) are stored. Attribute Data: Where the bits defining the appearance (color, brightness, flash) of the image are stored. This separation is necessary to save memory, as one attribute byte typically controls the appearance of an entire $8\times 8$ block of pixels. ...

September 27, 2025

Z80 Assembly 23: Graphics and Pixel Plotting (Bitwise Drawing)

The Screen Memory Layout On a bitmap Z80 system (like the ZX Spectrum), the screen is a large array of memory where each byte controls a group of 8 horizontal pixels. To plot a single pixel at coordinate (X, Y), you need two things: Memory Address: The 16-bit address of the byte that contains the pixel. Bit Mask: A byte with a single bit set (10000000B, 01000000B, etc.) to isolate the target pixel within that byte. Step 1: Calculating the Byte Address The calculation for the byte address is highly complex and specific to each system’s memory layout. It typically involves combining the Y-coordinate (row) and the X-coordinate (column). ...

September 27, 2025

Z80 Assembly 20: Writing a Simple Macro (Code Generation)

What is a Macro? A macro is a sequence of instructions or data definitions that you define once and can then reuse multiple times throughout your code by simply typing the macro’s name. When the assembler encounters the macro name, it substitutes the entire predefined block of code in its place. Macro vs. Subroutine: Subroutine (CALL): Saves memory by running one copy of the code, but is slower because it incurs the overhead of the CALL and RET instructions (17 cycles). Macro: Faster because the code is pasted directly (inline) every time it’s used, but increases the size of the assembled program. Defining a Simple Macro The syntax for defining a macro varies slightly between assemblers (e.g., TASM, Z80Asm, sjasmplus), but they typically use MACRO and ENDM or DEFM. ...

September 27, 2025

Z80 Assembly 19: Relocatable Code and Position-Independent Code (PIC)

The Problem with Absolute Addressing Normally, Z80 instructions like JP 8000H or LD HL, DATA_ADDR use absolute addressing. If you compile a program to run at address 8000H but later load it at 9000H, every one of those absolute addresses will be wrong, and the program will crash. Relocatable Code and Position-Independent Code (PIC) solve this problem. Relocatable Code (The Assembler’s Job) Relocatable code is written with absolute addresses, but the assembler generates a file that includes a relocation table. ...

September 27, 2025

Z80 Assembly 16: Efficient 16-bit Multiplication Algorithm

The Challenge: Multiplying 16-bit Numbers The Z80 performs 8-bit addition natively (ADD), but has no instruction for 16-bit multiplication (RR×RR). When you multiply two 16-bit numbers (like HL and DE), the result is a 32-bit number. We must build this algorithm using basic operations. The Algorithm: We use the same technique taught in grade school: break the numbers into parts, multiply each part, and sum the results. However, in assembly, it’s faster to use repeated shifting and conditional addition. ...

September 27, 2025

Z80 Assembly 15: Timers, Delays, and the Refresh Register (R)

Creating Software Delay Loops Since the Z80 runs at a fixed clock speed, you can create a precise time delay by executing a loop a fixed number of times, knowing the exact number of clock cycles each instruction takes. The Delay Routine: A delay routine typically uses nested loops and relies on the DJNZ instruction, as it’s efficient for looping. Example: Basic 16-bit Delay Loop This example uses the BC register pair for a longer delay (B for the outer loop, C for the inner loop). ...

September 27, 2025

Z80 Assembly 13: Instruction Set Encoding (Reading Machine Code)

Assembly vs. Machine Code Assembly language uses mnemonics (like LD or ADD) that are easy for humans to read. Machine code is the raw binary sequence of bytes (the opcodes) that the CPU directly executes. Understanding the encoding helps with optimization and debugging. Opcode Structure: The Z80’s Chessboard Most Z80 instructions fit into a flexible, 8-bit format that can be visualized as a grid. An 8-bit opcode is often broken into three 2- or 3-bit fields: ...

September 27, 2025

Z80 Assembly 12: Block I/O (INIR, OTDR) for Hardware Speed

Block I/O: The Fastest Way to Talk to Hardware The Z80 features a unique set of block I/O instructions designed to quickly move data between a memory block and a single, fixed I/O port. These are crucial for fast operations like dumping screen memory to a display controller or quickly reading keyboard buffers. The Register Setup: Block I/O relies on the following registers for definition: Register Pair Purpose in Block I/O HL Memory Address (Source or Destination) B Counter (Number of transfers to perform) C I/O Port Address (The fixed hardware port) Input Block Commands (Reading from Port) The INIR (Input, Increment, Repeat) and INDR (Input, Decrement, Repeat) commands read data from the port specified by C and store it into memory pointed to by HL. ...

September 27, 2025