Kjetil's Information Center: A Blog About My Projects

Epson HX-20 Image View

Here is a set of programs to easily display an image on the LCD screen of the Epson HX-20. The image needs to be 120x32, the resolution of the HX-20 LCD screen, and monochrome. It should be converted to the PBM format so it can be easily converted to pixel data which is embedded into the resulting HX-20 binary program.

Here is the assembly program part, to be assembled with dasm:

        processor hd6303

; Hardware registers:
LCDSEL  equ $26
LCDDAT  equ $2A
GATEB   equ $28

; Kernel routines:
DSPLCN  equ $FF49
KEYIN   equ $FF9A
MENU    equ $FF25

        org $1000 ; Run from HX-20 RAM.
; Program:
start:
        ; Initialize variables:
        ldab #0
        stab lcd_col
        stab lcd_row
        stab px_byte
        stab px_byte+1
        ldab #%00001000
        stab lcd_no
        ldab #$ff
        stab px_bit

        ; Clear the whole LCD screen:
        ldab #0
        jsr DSPLCN

next_lcd:
        ; Check if reached the last LCD:
        ldaa lcd_no
        cmpa #%00001110
        bne continue
        jmp end

continue:
        inc lcd_no
        ldaa #$80
        staa lcd_col

next_col:
        ldaa #$40
        staa lcd_row

next_row:
        jsr px_inc
        ldd px_byte
        addd #pixels
        xgdx
        ldaa 0,x
        ldab px_bit

px_shift:
        beq px_shift_done
        asra
        decb
        jmp px_shift
px_shift_done:
        anda #1
        beq px_skip_draw ; Change to 'bne' to invert image.

        ; Draw one pixel:
        ldaa lcd_no
        staa LCDSEL
        ldaa lcd_col
        staa LCDDAT
        sei
lcd_busy_1:
        tst GATEB
        bpl lcd_busy_1
        ldx LCDDAT ; Clock LCD SCK signal.
        ldx LCDDAT
        ldx LCDDAT
        ldx LCDDAT
        cli
        ldaa lcd_row
        staa LCDDAT
        sei
lcd_busy_2:
        tst GATEB
        bpl lcd_busy_2
        ldx LCDDAT ; Clock LCD SCK signal.
        ldx LCDDAT
        ldx LCDDAT
        ldx LCDDAT
        cli

px_skip_draw:
        ldaa lcd_row
        adda #4
        staa lcd_row
        suba #$60 ; Subtract to compare.
        bne next_row
        ldaa lcd_col
        inca
        staa lcd_col
        cmpa #$E8
        beq next_lcd
        cmpa #$A8
        bne next_col
        adda #$18 ; Will be 0xA8 here.
        staa lcd_col ; Store as 0xC0.
        jmp next_col

end:
        ; Wait for keypress and go back to main menu.
        jsr KEYIN
        jmp MENU

; Pixel increment subroutine:
px_inc: ldaa px_bit
        inca
        staa px_bit
        cmpa #8
        bne px_rts
        ldd px_byte
        addd #1
        std px_byte
        ldaa #0
        staa px_bit
px_rts: rts

; Variables:
lcd_no:  dc.b
lcd_col: dc.b
lcd_row: dc.b
px_byte: dc.w
px_bit:  dc.b

; Imported pixel data:
pixels:
        incbin "img.bin"
          


Here is the C program to convert PBM images to binary pixel data:

#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static bool image[32][120];

static int pbm_read(const char *path)
{
  FILE *fh;
  char line[128];
  int c;
  int x;
  int y;

  fh = fopen(path, "rb");
  if (fh == NULL) {
    fprintf(stderr, "fopen() failed with errno: %d\n", errno);
    return -1;
  }

  fgets(line, sizeof(line), fh); /* Header */
  if (line[0] != 'P' && line[1] != '4') {
    fprintf(stderr, "Not a binary PBM image!\n");
    fclose(fh);
    return -1;
  }

reread:
  fgets(line, sizeof(line), fh); /* Additional Headers */
  if (line[0] == '#') {
    goto reread;
  }

  if (strncmp(line, "120 32", 6) != 0) {
    fprintf(stderr, "Dimensions must be 120x32!\n");
    fclose(fh);
    return -1;
  }

  x = 0;
  y = 0;
  while ((c = fgetc(fh)) != EOF) {
    image[y][x]   = (c >> 7) & 1;
    image[y][x+1] = (c >> 6) & 1;
    image[y][x+2] = (c >> 5) & 1;
    image[y][x+3] = (c >> 4) & 1;
    image[y][x+4] = (c >> 3) & 1;
    image[y][x+5] = (c >> 2) & 1;
    image[y][x+6] = (c >> 1) & 1;
    image[y][x+7] =  c       & 1;
    x += 8;
    if (x == 120) {
      x = 0;
      y++;
      if (y == 32) {
        break;
      }
    }
  }

  fclose(fh);
  return 0;
}

static int pixels_write(const char *path)
{
  FILE *fh;
  int c;
  int x;
  int y;
  int base_x;
  int base_y;
  int lcd_no;

  fh = fopen(path, "wb");
  if (fh == NULL) {
    fprintf(stderr, "fopen() failed with errno: %d\n", errno);
    return -1;
  }

  for (lcd_no = 0; lcd_no < 6; lcd_no++) {
    base_y = lcd_no / 3 * 16;
    base_x = (lcd_no * 40) % 120;
    for (y = 0; y < 16; y += 8) {
      for (x = 0; x < 40; x++) {
        c  = (image[base_y+y]  [base_x+x]       );
        c |= (image[base_y+y+1][base_x+x] << 1);
        c |= (image[base_y+y+2][base_x+x] << 2);
        c |= (image[base_y+y+3][base_x+x] << 3);
        c |= (image[base_y+y+4][base_x+x] << 4);
        c |= (image[base_y+y+5][base_x+x] << 5);
        c |= (image[base_y+y+6][base_x+x] << 6);
        c |= (image[base_y+y+7][base_x+x] << 7);
        fputc(c, fh);
      }
    }
  }

  fclose(fh);
  return 0;
}

int main(int argc, char *argv[])
{
  if (argc != 3) {
    fprintf(stderr, "Usage: %s <in> <out>\n", argv[0]);
    return EXIT_FAILURE;
  }

  if (pbm_read(argv[1]) != 0) {
    return EXIT_FAILURE;
  }

  if (pixels_write(argv[2]) != 0) {
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}
          


And here is a Makefile to tie it all together:

all: hx20img.srec hx20img.wav

hx20img.srec: hx20img.bin
	srec_cat hx20img.bin -binary -offset 0x1000 -o hx20img.srec -motorola

hx20img.wav: hx20img.bin
	bin2hxwav hx20img.bin hx20img.wav 1000 hx20img

hx20img.bin: hx20img.asm img.bin
	dasm hx20img.asm -ohx20img.bin -f3 -lhx20img.lst

img.pbm: img.png
	convert img.png img.pbm

pbm2bin: pbm2bin.c
	gcc -o pbm2bin pbm2bin.c -Wall -Wextra

img.bin: pbm2bin img.pbm
	./pbm2bin img.pbm img.bin

.PHONY: clean
clean:
	rm -f pbm2bin img.pbm *.wav *.srec *.bin *.lst
          


Running make will produce both an S-record and a WAV file as long as the proper tools are installed and an "img.png" image file is present. Check here for the special "bin2hxwav" program. The S-record can be loaded into the hex20 emulator.

The WAV file can be played to a real Epson HX-20 using the external cassette interface. Enter the MONITOR and type these commands:

A
1000
FFFF
/
R C,*.*
          


Play the WAV file, and when it's done, type:

G1000
          


Sample image:

Sample image.


Displayed on a real HX-20:

Sample image displayed.


Topic: Scripts and Code, by Kjetil @ 18/09-2026, Article Link