Skip to content

Worked examples

Short, self-contained programs suitable for adding to a project, building with dccmake, and running under an emulator such as ntvcm. For example, from the DCC checkout:

dccmake tests/texsort.c dcc-output=TEXSORT
ntvcm build/TEXSORT.COM

See Building and linking for build options and the manual pipeline.

Sorting and searching an int array

qsort orders the array, then bsearch locates a key with the same comparator. The comparator returns negative / zero / positive — here the branchless (x > y) - (x < y) idiom.

static int cmp_int(const void *a, const void *b)
{
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);
}

int main(void)
{
    int v[8];
    int key = 13;
    const int *hit;

    v[0] = 2; v[1] = 8; v[2] = 5; v[3] = 13;
    v[4] = 1; v[5] = 21; v[6] = 3; v[7] = 34;

    qsort(v, 8U, sizeof(int), cmp_int);     /* 1 2 3 5 8 13 21 34 */
    hit = (const int *)bsearch(&key, v, 8U, sizeof(int), cmp_int);
    if (hit)
        printf("found %d at index %d\n", *hit, (int)(hit - v));
    else
        puts("not found");
    return 0;
}

Output: found 13 at index 5.

Sorting an array of structs by a key field

Any element width works because qsort swaps whole elements byte-by-byte. The comparator reads the field it sorts on — here a string member via strcmp — and bsearch reuses it to look a record up by name.

struct item {
    char name[8];
    int  qty;
};

static int by_name(const void *a, const void *b)
{
    return strcmp(((const struct item *)a)->name,
                  ((const struct item *)b)->name);
}

int main(void)
{
    struct item items[3];
    struct item key;
    const struct item *hit;
    int i;

    strcpy(items[0].name, "pears");  items[0].qty = 4;
    strcpy(items[1].name, "apples"); items[1].qty = 9;
    strcpy(items[2].name, "kiwis");  items[2].qty = 2;

    qsort(items, 3U, sizeof(struct item), by_name);
    for (i = 0; i < 3; i++)
        printf("%-8s %d\n", items[i].name, items[i].qty);

    strcpy(key.name, "kiwis");
    hit = (const struct item *)bsearch(&key, items, 3U,
                                       sizeof(struct item), by_name);
    if (hit)
        printf("%s: %d in stock\n", hit->name, hit->qty);
    return 0;
}

Output:

apples   9
kiwis    2
pears    4
kiwis: 2 in stock

A printf-style logging wrapper

Forwarding a va_list to vfprintf supports custom diagnostic wrappers without re-parsing the arguments.

static void logmsg(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
}

int main(void)
{
    logmsg("ready: %d items, %lx flags\n", 3, 0xBEEFL);
    return 0;
}

Reading a text file line by line

int main(void)
{
    FILE *fp = fopen("DATA.TXT", "r");
    char  line[128];

    if (!fp) {
        perror("DATA.TXT");
        return 1;
    }
    while (fgets(line, sizeof line, fp))
        fputs(line, stdout);
    fclose(fp);
    return 0;
}

Parsing input with sscanf

sscanf reads from a string using the same conversion subset as scanf and fscanf (integers and strings; no floating input). Each conversion stores through a pointer argument.

int main(void)
{
    int  value;
    char word[16];
    int  hexval;
    long big;

    sscanf("-12 hello 0x2a", "%d %s %i", &value, word, &hexval);
    printf("value=%d word=%s hexval=%d\n", value, word, hexval);

    sscanf("123456", "%ld", &big);
    printf("big=%ld\n", big);
    return 0;
}

Output:

value=-12 word=hello hexval=42
big=123456

Buffered console output with a user-declared buffer

setvbuf allows supplying a user-allocated buffer for console output, so output accumulates instead of going to CP/M one character at a time. A larger buffer means fewer BDOS calls. Drain it with fflush, and detach it (setvbuf(stdout, NULL, _IOLBF, 0)) before the buffer's storage is reused — see Console output buffering.

int main(void)
{
    static char obuf[1024];   /* user-declared console buffer */
    int i;
    long total;

    /* Adopt obuf and fully buffer: output accumulates instead of going to the
     * BDOS one character at a time. */
    if (setvbuf(stdout, obuf, _IOFBF, sizeof obuf) != 0) {
        puts("setvbuf failed");
        return 1;
    }

    total = 0;
    for (i = 1; i <= 20; i = i + 1) {
        printf("row %2d: %ld\n", i, total);
        total = total + (long) i * i;
    }

    /* Nothing has reached the console yet (fully buffered, under 1 KB).
     * Drain it explicitly. */
    fflush(stdout);

    printf("sum of squares 1..20 = %ld\n", total);

    /* Flush before detaching: switching buffers while output is still
     * pending in the old one is unspecified behavior (some libc's drop it
     * instead of auto-flushing). */
    fflush(stdout);

    /* Detach the buffer before it goes out of scope / is reused, so the
     * automatic flush at exit uses the internal buffer. */
    setvbuf(stdout, (char *) 0, _IOLBF, 0);

    return 0;
}

Output ends with sum of squares 1..20 = 2870. The static buffer keeps it off the small CP/M stack; a malloc'd buffer works too, but free it only after detaching it from the stream. This snippet is pulled verbatim from the tests/tbufex.c regression test, so the documented code is exactly what is built and run by the suite.

Waiting for an I/O-adapter timer

The example debugger I/O adapter provides a 16-bit millisecond timer on ports 24 and 25. Write the delay's high byte to port 24, then its low byte to port 25 to start the timer. Reading either port returns 1 while it is running and 0 after it expires.

#include <stdio.h>
#include <stdlib.h>

#define TIMER_0_HIGH 24
#define TIMER_0_LOW  25

static void start_timer(unsigned delay_ms)
{
    outp(TIMER_0_HIGH, delay_ms >> 8);
    outp(TIMER_0_LOW, delay_ms & 0xff);
}

int main(void)
{
    puts("Waiting for one second...");
    start_timer(1000U);
    while (inp(TIMER_0_LOW) != 0)
        ;
    puts("Timer expired.");
    return 0;
}

This program requires dcc-debug-host with the example I/O adapter loaded; ordinary emulators need an equivalent device on those ports. The source lives beside the adapter and builds with:

./dccmake src/dcc_debug_host/examples/io_adapter/timer.c dcc-output=TIMER

See Direct port I/O for the inp and outp runtime contract.

Handling periodic I/O-adapter interrupts

The example adapter also provides a periodic maskable-interrupt source on port 52. Writing a value from 1 through 255 selects that many interrupts per second; writing zero disables the source and clears pending requests. Reading the port returns the configured rate.

This example installs a Z80 interrupt mode 1 vector at 0038H, waits with HALT, counts 50 interrupts at 10 Hz in a C function, then disables the source and restores CP/M's original vector:

#include <stdio.h>

#define TARGET_TICKS 50

volatile unsigned int irq_ticks;

extern void irq_install(volatile unsigned int *counter, void (*handler)(void));
extern void irq_wait(void);
extern unsigned int irq_count(void);
extern void irq_remove(void);

void irq_tick(void)
{
    irq_ticks++;
}

#asm
        public  _irq_install
        public  _irq_wait
        public  _irq_count
        public  _irq_remove

INTVEC  equ     0038h
INTPORT equ     52

; Save CP/M's vector and bind the C state passed through the normal dcc ABI:
; IX+4/5 = counter pointer, IX+6/7 = handler function pointer.
_irq_install:
        di
        push    ix
        ld      ix,0
        add     ix,sp
        ld      l,(ix+4)
        ld      h,(ix+5)
        ld      (irq_counter),hl
        ld      l,(ix+6)
        ld      h,(ix+7)
        ld      (irq_call+1),hl
        pop     ix

        ld      a,(INTVEC)
        ld      (irq_old),a
        ld      a,(INTVEC+1)
        ld      (irq_old+1),a
        ld      a,(INTVEC+2)
        ld      (irq_old+2),a

        ld      a,0c3h
        ld      (INTVEC),a
        ld      hl,irq_isr
        ld      (INTVEC+1),hl

        im      1
        ld      a,10
        out     (INTPORT),a
        ei
        ret

; Sleep until the next accepted interrupt. The ISR resumes after HALT.
_irq_wait:
        halt
        ret

; Return an atomic 16-bit snapshot in HL using the normal dcc return ABI.
_irq_count:
        di
        ld      hl,(irq_counter)
        ld      e,(hl)
        inc     hl
        ld      d,(hl)
        ex      de,hl
        ei
        ret

; Stop the source before restoring CP/M's original three vector bytes.
_irq_remove:
        di
        xor     a
        out     (INTPORT),a
        ld      a,(irq_old)
        ld      (INTVEC),a
        ld      a,(irq_old+1)
        ld      (INTVEC+1),a
        ld      a,(irq_old+2)
        ld      (INTVEC+2),a
        ret

; Preserve all visible Z80 register sets before entering ordinary C.
irq_isr:
        push    af
        push    bc
        push    de
        push    hl
        push    ix
        push    iy

        ex      af,af'
        push    af
        ex      af,af'
        exx
        push    bc
        push    de
        push    hl
        exx

irq_call:
        call    0000h

        exx
        pop     hl
        pop     de
        pop     bc
        exx
        ex      af,af'
        pop     af
        ex      af,af'

        pop     iy
        pop     ix
        pop     hl
        pop     de
        pop     bc
        pop     af
        ei
        reti

irq_counter:
        dw      0
irq_old:
        ds      3
#endasm

int main(void)
{
    unsigned int count;

    irq_ticks = 0;
    printf("DCC C + Z80 interrupt example\n");
    printf("Counting %u timer interrupts at 10 Hz...\n", TARGET_TICKS);

    irq_install(&irq_ticks, irq_tick);
    do {
        irq_wait();
        count = irq_count();
    } while (count < TARGET_TICKS);
    irq_remove();

    printf("C handler count: %u\n", count);
    return count == TARGET_TICKS ? 0 : 1;
}

The assembly wrapper preserves both Z80 register sets before calling C. The C handler only updates memory: interrupt code must not call BDOS, perform console I/O, allocate memory, or use other non-reentrant runtime services.

Build the source with debug metadata using:

./dccmake -g src/dcc_debug_host/examples/io_adapter/dccint.c \
  dcc-output=DCCINT

Run DCCINT.COM under dcc-debug-host with the example I/O adapter loaded. It counts for five seconds, prints C handler count: 50, restores the vector, and returns normally to CP/M.