It has been a while since I've been at my keyboard and written anything useful, so I thought it might be a good time to write some notes on a recent discussion at
Z88DK for the 8085, and also to discuss using a simple example how to use the
Z88DK to write mixed C and assembly programs for other CPUs in the extended 8080 family (eg z80, z180, ez80, z80n, gbz80, etc).
The exercise is to write a quick proof that the 8085 undocumented instruction LD DE,HL+n doesn't affect the carry or any other flag bits.
There would be many ways to do this. I've just dome a simple demo here.
Rather than writing raw assembly to output and format a result, the easiest method is to use CP/M as a host platform and just use the C stdio functions to produce a nice result with one line of code.
But to actually test the instruction we need to write an
assembly function to exercise all the opcode options. The assembly function needs to take and return parameters to the main routine, and then can be written to the screen.
Here's the whole program in one file, I'll break down each line as we go through...
/*
check the 8085 ld de,hl+n instruction doesn't influence carry
clear flags, add 1 to input hl, and look at flag bits
*/
// zcc +cpm -clib=8085 -SO2 --list bug-2631.c -o bug-2631.com
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#pragma printf = "%s %c %04X" // enables %s %c %X only
uint32_t testBug( uint16_t input) __z88dk_fastcall;
uint32_t testBug( uint16_t input)
{
__asm__( \
"xor a \n" \
"ld de,hl+1 \n" \
"push af \n" \
"pop hl \n" \
); \
}
void main (void)
{
for (uint16_t test = 0x0001; test != 0x0000; ++test) {
fprintf(stdout, " Input: HL %04X Output: LD DE,HL+1 %04X AF %04X\n", test, (uint16_t)(testBug(test)>>16), (uint16_t)testBug(test));
}
}
First the comment line showing the
zcc command to be used to initiate the compiler / assembler / linker / packager system to produce a
.COM binary for CP/M.
We're picking the
cpm target and specifying that this is for a 8085 CPU. For z80 the library specification can be omitted.
SO2 is for strong inline optimisation, but doesn't push for the size minimisation that SO3 would do. SO3 would use helper subroutines for all variable fetches which saves space, but slows things down considerably.
--list provides us with listings of the resulting assembled program, and the CRT0 code from which _main will be called.
// zcc +cpm -clib=8085 -SO2 --list bug-2631.c -o bug-2631.com
The next interest point is the
#pragma line, which defines which printf format converters need to be available, and whether the formatting versions (larger) or simple versions can be selected. Here we're just asking to be able to print strings and characters, and to be able to format unsigned hexadecimal integers.
#pragma printf = "%s %c %04X" // enables %s %c %X only
Now we declare our test function inventively titled
testBug, and the desired
calling convention.
It is to be a fastcall function, which means that it is passed only one parameter, and that parameter is found in the DEHL registers, right justified.
It also returns one value, also in the DEHL registers, right justified. That means that a 16-bit parameter will be found in the HL registers, and an 8-bit parameter is in the L register.
The fastcall declaration avoids the compiler pushing parameters onto the stack, and then popping them off on return, saving substantial cycles in stack management.
If there are multiple parameters to pass then the _z88dk_callee decoration causes the compiler to assume that the callee function has removed its own parameters from the calling stack, which can also save some cycles.
The testBug function is going to take a 16-bit input and return two 16-bit results, cheated into one uint32_t C variable.
We'll see later how this is done.
uint32_t testBug( uint16_t input) __z88dk_fastcall;
Now, writing the testBug function means putting some assembly code within a C function. This is not the cleanest way to write assembly with z88dk, but for the purpose of our one line program it is the most straightforward way to get the result needed.
Using the
__asm__() MACRO the assembly code is just written into the C function.
This doesn't prevent the compiler from doing its normal entry and function return handling, like for instance adding a ret opcode at the end of the function.
The testBug function clears the A register, and resets the Carry and Sign flags. Which flags are set by XOR A is left for further reading.
Then the opcode we're investigating is issued with the result in DE, and the resulting flag byte moved into the L register to be returned to the C program.
uint32_t testBug( uint16_t input)
{
__asm__( \
"xor a \n" \
"ld de,hl+1 \n" \
"push af \n" \
"pop hl \n" \
); \
}
Finally the
main function simply iterates over the values that need to be tested, and prints the results.
Lazily the
testBug function is called twice each iteration, but with a little more complexity an intermediate variable could hold the result and return it. To get access to the
DE result the return value is shifted 16 bits to the right. The compiler interprets this as an
EX DE,HL instruction and it is very efficient.
To format the output so that it looks nice the %04X converter is used. This puts the resulting hexadecimal (with capital numerals) into 4 columns.
void main (void)
{
for (uint16_t test = 0x0001; test != 0x0000; ++test) {
fprintf(stdout, " Input: HL %04X Output: LD DE,HL+1 %04X AF %04X\n", test, (uint16_t)(testBug(test)>>16), (uint16_t)testBug(test));
}
}
So that's it. The resulting
bug-2631.com is then sent to CP/M using XMODEM or XM or similar and, when run, the results appear on the screen.
bug-2631
Input: HL 0001 Output: LD DE,HL+1 0002 AF 0044
Input: HL 0002 Output: LD DE,HL+1 0003 AF 0044
Input: HL 0003 Output: LD DE,HL+1 0004 AF 0044
Input: HL 0004 Output: LD DE,HL+1 0005 AF 0044
Input: HL 0005 Output: LD DE,HL+1 0006 AF 0044
Input: HL 0006 Output: LD DE,HL+1 0007 AF 0044
Input: HL 0007 Output: LD DE,HL+1 0008 AF 0044
Input: HL 0008 Output: LD DE,HL+1 0009 AF 0044
Input: HL 0009 Output: LD DE,HL+1 000A AF 0044
Input: HL 000A Output: LD DE,HL+1 000B AF 0044
Input: HL 000B Output: LD DE,HL+1 000C AF 0044
Input: HL 000C Output: LD DE,HL+1 000D AF 0044
Input: HL 000D Output: LD DE,HL+1 000E AF 0044
Input: HL 000E Output: LD DE,HL+1 000F AF 0044
Input: HL 000F Output: LD DE,HL+1 0010 AF 0044
Input: HL 0010 Output: LD DE,HL+1 0011 AF 0044
Input: HL 0011 Output: LD DE,HL+1 0012 AF 0044
Input: HL 0012 Output: LD DE,HL+1 0013 AF 0044
And we see, as expected, that the LD DE,HL+n opcode doesn't affect the flags at all.
For further reading
Ken Shirriff explains that the undocumented 8085 instructions are implemented in the
ACT register of the
ALU (bypassing the Accumulator), and therefore don't have access to the flag bit control lines.
Anyway, I hope that was useful.
Phillip