Information wants to be free...

Dual Web Hosting

In order to get a little more exposure and act as a backup of sorts, I will dual host my web pages for a while. In addition to my "regular" location on http://kobolt.website/infocenter/ I will be utilizing https://kobolt.github.io/. This is easy since my pages are basically static content anyways. On the regular location it will still be served by PHP and SQL as before, while on the new location the HTML is already generated into separate pages.

I started on these web pages almost 15 years ago in the summer of 2007. Some of the articles are quite dated, but I will keep them around anyway. One notable example is the "Perl YouTube Fetcher" from 2008 which I am pretty sure stopped working many years ago :-) and we have modern alternatives for. Another one is the "OpenTTD music files in other formats" where OpenTTD has had a lot of improvements and changes during the last decade and more!

Topic: General, by Kjetil @ 15/01-2022, Article Link

XT-IDE Loaded from Fake VBR

Here is a trick I have used to get my Commodore PC 50-II to boot DOS off a unsupported Compact Flash (CF) card. The BIOS on this machine is dated between 1987-1989 and only has a set of hard-coded hard disk types with specific C/H/S values. A common way to get around such limitations is to use the excellent XT-IDE BIOS, which is usually put in a separate option ROM. Instead of using a EPROM I wanted to find a way to load it using software instead.

My software trick uses a fake volume boot record (VBR) to load XT-IDE into RAM. This approach is similar to optromloader which loads ROMs from floppies.

Since the BIOS on this machine has support for hard disks, it will always be able to load the master boot record (MBR) located at the first sector 0/0/1, regardless of C/H/S settings. The VBR (for DOS) is usually stored at the first sector of the second head 0/1/1. Most of the hard disk types available in this BIOS has the "Sector per Track" set to 17, and this is what I will use. The CF card I use has real C/H/S values 1003/16/32. This means that when the MBR initially loads the VBR through the original BIOS int 13h routines it will actually get sector 18 from the CF card, and this is where to store the fake VBR that loads XT-IDE. When the boot sequence is re-initiated after this the MBR will get reloaded, but this time it will use the int 13h routines provided by XT-IDE and get the real DOS VBR from sector 33.

Here is a diagram that shows the layout of the start of the CF card:

 Content   Sector (Byte Offset)
+--------+ 1 (0)
|  MBR   |
+--------+ 2 (512)
|        |
| XT-IDE |
|  ROM   |
|        |
+--------+ 18 (8704)
|  Fake  |
|  VBR   |
+--------+ 19 (9216)
|        |
| Unused |
| Space  |
|        |
+--------+ 33 (16384)
|  Real  |
|  VBR   |
+--------+ 34 (16896)
|        |
|        |
          


Here is the assembly source code for the fake VBR that loads the XT-IDE ROM:

org 0x7C00
bits 16
cpu 8086

section .text
start:
  ; Setup stack area for boot loader code.
  cli
  xor ax, ax
  mov ss, ax
  mov sp, 0x7C00
  push ax
  pop ds
  sti

  ; Initial values for BIOS disk operations.
  mov bx, 0x9E00 ; Destination segment.
  mov es, bx
  xor bx, bx     ; Destination offset.
  mov cx, 0x0002 ; Sector 2, Cylinder 0.
  mov dx, 0x0080 ; First harddisk, Head 0.

read_again:
  ; Call BIOS to reset disk system.
  xor ax, ax
  int 0x13

  ; Push registers to stack before doing BIOS video service.
  push bx
  push cx
  push dx

  ; Display last BIOS disk operation status.
  mov dl, cl ; Column = Sector number.
  sub dl, 2
  mov dh, 1 ; Row 1
  mov bx, 0x000F
  mov ah, 0x02 ; Set cursor position.
  int 0x10
  mov cx, 1
  mov ah, 0x09
  mov al, [int13_status]
  add al, 0x30 ; Convert to ASCII numerical or higher.
  and al, 0x7F ; Make sure it is a valid ASCII value.
  int 0x10

  ; Pop registers from stack after doing BIOS video service.
  pop dx
  pop cx
  pop bx

  ; Call BIOS to read 1 sector.
  mov ax, 0x0201
  int 0x13
  mov [int13_status], ah
  jb read_again

  ; Increment destination memory and sector number.
  add bh, 2
  inc cl
  cmp cl, 18 ; Stop at 16 sectors (8KB).
  jne read_again

  ; Call BIOS to get memory size.
  int 0x12
  sub ax, 8       ; Subtract 8K blocks...
  mov [0x413], ax ; ...and store new memory size in BDA at 0x40:0x0013

  call 0x9E00:0003 ; Call the option ROM.
  int 0x19 ; Call BIOS to initiate boot process (again).

int13_status:
  db 0
          


To put this at the correct location on the CF card a utility coded in C is provided:

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

int main(int argc, char *argv[])
{
  FILE *fh_vbr    = NULL;
  FILE *fh_optrom = NULL;
  FILE *fh_disk   = NULL;
  int c, n, vbr_sector;

  if (argc != 5) {
    printf("Usage: %s <vbr> <optrom> <disk> <vbr-sector>\n", argv[0]);
    return 1;
  }

  fh_vbr = fopen(argv[1], "rb");
  if (NULL == fh_vbr) {
    printf("Error: Unable to open '%s'\n", argv[1]);
    goto end;
  }

  fh_optrom = fopen(argv[2], "rb");
  if (NULL == fh_optrom) {
    printf("Error: Unable to open '%s'\n", argv[2]);
    goto end;
  }

  fh_disk = fopen(argv[3], "r+b");
  if (NULL == fh_disk) {
    printf("Error: Unable to open '%s'\n", argv[3]);
    goto end;
  }

  vbr_sector = atoi(argv[4]);
  if (0 == vbr_sector) {
    printf("Error: Invalid VBR sector: %s\n", argv[4]);
    goto end;
  }

  /* Write option ROM, after MBR on sector 2 (1-indexed) onwards. */
  fseek(fh_disk, 512, SEEK_SET);
  while ((c = fgetc(fh_optrom)) != EOF) {
    fputc(c, fh_disk);
  }

  /* Write fake VBR on selected sector (1-indexed). */
  n = 0;
  fseek(fh_disk, 512 * (vbr_sector - 1), SEEK_SET);
  while ((c = fgetc(fh_vbr)) != EOF) {
    fputc(c, fh_disk);
    n++;
  }

  /* Pad rest of the fake VBR area with zeroes... */
  for (; n < 0x1FE; n++) {
    fputc('\x00', fh_disk);
  }
  /* ...and finish with the correct signature. */
  fputc('\x55', fh_disk);
  fputc('\xAA', fh_disk);

end:
  if (NULL != fh_vbr)    fclose(fh_vbr);
  if (NULL != fh_optrom) fclose(fh_optrom);
  if (NULL != fh_disk)   fclose(fh_disk);
  return 0;
}
          


Both can be assembled/compiled with this Makefile:

all: vbr.bin modify-disk

vbr.bin: vbr.asm
	nasm vbr.asm -fbin -o vbr.bin

modify-disk: modify-disk.c
	gcc -o modify-disk -Wall -Wextra modify-disk.c

.PHONY: clean
clean:
	rm -f vbr.bin modify-disk
          


These tools are meant to be used with a XT-IDE ROM of 8KB in size. I should also mention that I did not get this machine to boot with the 386 version, this always resulted in "Boot sector not found" errors. Instead I have used the "ide_at.bin" version.

Once assembled and compiled the tools are typically used as such, where /dev/sdX is the CF card:

./modify-disk vbr.bin ide_at.bin /dev/sdX 18
          


A "screenshot" of what it should look like when booting:

XT-IDE loaded on Commodore PC 50-II

Take note of the 0s near the top, which represents status codes, in this case OK, from sector reads.

Topic: Scripts and Code, by Kjetil @ 08/01-2022, Article Link

Commodore PC 20-III CGA Composite

The Commodore PC 20-III is a XT-class 8088 clone PC running at 4.77 to 9.54MHz with 640KB of RAM. It also features an on-board Paradise PCV4 chip that provides CGA graphics on both D-Sub/RGBI and RCA/Composite connectors. While the RGBI worked fine, I could not get any picture with the composite connection.

I measured with both an oscilloskope and a multimeter and the composite signal was around 5 volts, which is not correct at all. Fortunately, circuit diagrams for this PC can be found on Bo Zimmerman's web pages, where I found something interesting:

Commodore PC 20-III Circuit Diagram Snippet


There was a high chance the 5 volts came from the main VCC voltage rail through this transistor, which could be shorted. I confirmed a short circuit between collector and emitter using a multimeter and proceeded to remove the transistor. As expected the short was now gone. I did not have any 2N3904 in my stash, but used a BC549B as a replacement, which should be equivalent. Here is the replaced transistor in the middle:

Commodore PC 20-III Circuit with Transistor


I powered on the PC and measured with an oscilloscope again, which now shows a proper composite video signal:

Commodore PC 20-III Composite Video Signal


I also ran the 8088 MPH demo to test for more colors on CGA:

Commodore PC 20-III Composite Video Signal


Topic: Repair, by Kjetil @ 11/12-2021, Article Link

Arduino Z80 CPU Tester

Here is a way to test Z80 CPUs that I created. I got the idea after seeing something about an Arduino being used as some sort of logic analyzer. For this I have used the Arduino Mega 2560 since this has a lot of digital I/O pins available.

It is a simple Arduino sketch, that uses the built-in USB CDC serial interface to communicate. It will respond to certain commands and on every cycle dump the status of the pins, which will look something like this:

  |------\_/------|
0 | A11       A10 | 0
0 | A12        A9 | 0
0 | A13        A8 | 0
0 | A14        A7 | 0
0 | A15        A6 | 0
1 | CLK        A5 | 0
0 | D4         A4 | 0
1 | D3         A3 | 0
0 | D5         A2 | 1
0 | D6         A1 | 0
- | +5V        A0 | 1
1 | D2        GND | -
0 | D7      ~RFSH | -
1 | D0        ~M1 | -
1 | D1     ~RESET | 1
- | ~INT   ~BUSRQ | -
- | ~NMI    ~WAIT | -
- | ~HALT  ~BUSAK | -
- | ~MREQ     ~WR | 1
- | ~IORQ     ~RD | 1
  |---------------|
Address: 0x5 Data: 0xF
          


The commands available in this version is 'c' to toogle the CLK clock pin, 'r' to toggle the RESET pin and 0 through 7 to toggle the data bit pins. It is possible to easily expand on this, but these are the minimum pins required to make the sketch program useful. A simple test that can be done with the Z80 is to put 0xC3 on the data bit pins, and then just toggle the clock over and over. This will cause it to read the opcodes 0xC3 0xC3 0xC3, meaning an absolute jump to address 0xC3C3 which will get reflected back on the address pins.

There is no circuit diagram, instead use the #define in the Arduino sketch to determine the connections. In addition to the signals defined there, +5V and GND is also needed of course, gotten from the Arduino as well.

Here is the sketch:

#define PIN_A0     11
#define PIN_A1     12
#define PIN_A2     10
#define PIN_A3     61
#define PIN_A4     60
#define PIN_A5     59
#define PIN_A6     58
#define PIN_A7     57
#define PIN_A8     56
#define PIN_A9     55
#define PIN_A10    54
#define PIN_A11    62
#define PIN_A12    63
#define PIN_A13    64
#define PIN_A14    65
#define PIN_A15    66
#define PIN_D0     17
#define PIN_D1     16
#define PIN_D2     19
#define PIN_D3     69
#define PIN_D4     68
#define PIN_D5     21
#define PIN_D6     20
#define PIN_D7     18
#define PIN_CLK    67
#define PIN_RESET  8 
#define PIN_WR     4 
#define PIN_RD     3 

void setup() {
  Serial.begin(115200);
  
  pinMode(LED_BUILTIN, OUTPUT);

  pinMode(PIN_A0, INPUT);
  pinMode(PIN_A1, INPUT);
  pinMode(PIN_A2, INPUT);
  pinMode(PIN_A3, INPUT);
  pinMode(PIN_A4, INPUT);
  pinMode(PIN_A5, INPUT);
  pinMode(PIN_A6, INPUT);
  pinMode(PIN_A7, INPUT);
  pinMode(PIN_A8, INPUT);
  pinMode(PIN_A9, INPUT);
  pinMode(PIN_A10, INPUT);
  pinMode(PIN_A11, INPUT);
  pinMode(PIN_A12, INPUT);
  pinMode(PIN_A13, INPUT);
  pinMode(PIN_A14, INPUT);
  pinMode(PIN_A15, INPUT);
  pinMode(PIN_D0, OUTPUT);
  pinMode(PIN_D1, OUTPUT);
  pinMode(PIN_D2, OUTPUT);
  pinMode(PIN_D3, OUTPUT);
  pinMode(PIN_D4, OUTPUT);
  pinMode(PIN_D5, OUTPUT);
  pinMode(PIN_D6, OUTPUT);
  pinMode(PIN_D7, OUTPUT);
  pinMode(PIN_CLK, OUTPUT);
  pinMode(PIN_RESET, OUTPUT);
  pinMode(PIN_WR, INPUT);
  pinMode(PIN_RD, INPUT);
}

void dump() {
  uint16_t bus;

  Serial.println("  |------\\_/------|  ");

  Serial.print(digitalRead(PIN_A11), DEC);
  Serial.print(" | A11       A10 | ");
  Serial.println(digitalRead(PIN_A10), DEC);

  Serial.print(digitalRead(PIN_A12), DEC);
  Serial.print(" | A12        A9 | ");
  Serial.println(digitalRead(PIN_A9), DEC);

  Serial.print(digitalRead(PIN_A13), DEC);
  Serial.print(" | A13        A8 | ");
  Serial.println(digitalRead(PIN_A8), DEC);

  Serial.print(digitalRead(PIN_A14), DEC);
  Serial.print(" | A14        A7 | ");
  Serial.println(digitalRead(PIN_A7), DEC);

  Serial.print(digitalRead(PIN_A15), DEC);
  Serial.print(" | A15        A6 | ");
  Serial.println(digitalRead(PIN_A6), DEC);

  Serial.print(digitalRead(PIN_CLK), DEC);
  Serial.print(" | CLK        A5 | ");
  Serial.println(digitalRead(PIN_A5), DEC);

  Serial.print(digitalRead(PIN_D4), DEC);
  Serial.print(" | D4         A4 | ");
  Serial.println(digitalRead(PIN_A4), DEC);

  Serial.print(digitalRead(PIN_D3), DEC);
  Serial.print(" | D3         A3 | ");
  Serial.println(digitalRead(PIN_A3), DEC);

  Serial.print(digitalRead(PIN_D5), DEC);
  Serial.print(" | D5         A2 | ");
  Serial.println(digitalRead(PIN_A2), DEC);

  Serial.print(digitalRead(PIN_D6), DEC);
  Serial.print(" | D6         A1 | ");
  Serial.println(digitalRead(PIN_A1), DEC);

  Serial.print("- | +5V        A0 | ");
  Serial.println(digitalRead(PIN_A0), DEC);

  Serial.print(digitalRead(PIN_D2), DEC);
  Serial.println(" | D2        GND | -");

  Serial.print(digitalRead(PIN_D7), DEC);
  Serial.println(" | D7      ~RFSH | -");

  Serial.print(digitalRead(PIN_D0), DEC);
  Serial.println(" | D0        ~M1 | -");

  Serial.print(digitalRead(PIN_D1), DEC);
  Serial.print(" | D1     ~RESET | ");
  Serial.println(digitalRead(PIN_RESET), DEC);

  Serial.println("- | ~INT   ~BUSRQ | -");
  Serial.println("- | ~NMI    ~WAIT | -");
  Serial.println("- | ~HALT  ~BUSAK | -");

  Serial.print("- | ~MREQ     ~WR | ");
  Serial.println(digitalRead(PIN_WR), DEC);

  Serial.print("- | ~IORQ     ~RD | ");
  Serial.println(digitalRead(PIN_RD), DEC);

  Serial.println("  |---------------|  ");

  bus = digitalRead(PIN_A15);
  bus <<= 1;
  bus += digitalRead(PIN_A14);
  bus <<= 1;
  bus += digitalRead(PIN_A13);
  bus <<= 1;
  bus += digitalRead(PIN_A12);
  bus <<= 1;
  bus += digitalRead(PIN_A11);
  bus <<= 1;
  bus += digitalRead(PIN_A10);
  bus <<= 1;
  bus += digitalRead(PIN_A9);
  bus <<= 1;
  bus += digitalRead(PIN_A8);
  bus <<= 1;
  bus += digitalRead(PIN_A7);
  bus <<= 1;
  bus += digitalRead(PIN_A6);
  bus <<= 1;
  bus += digitalRead(PIN_A5);
  bus <<= 1;
  bus += digitalRead(PIN_A4);
  bus <<= 1;
  bus += digitalRead(PIN_A3);
  bus <<= 1;
  bus += digitalRead(PIN_A2);
  bus <<= 1;
  bus += digitalRead(PIN_A1);
  bus <<= 1;
  bus += digitalRead(PIN_A0);
  Serial.print("Address: 0x");
  Serial.print(bus, HEX);
  Serial.print(" ");

  bus = digitalRead(PIN_D7);
  bus <<= 1;
  bus += digitalRead(PIN_D6);
  bus <<= 1;
  bus += digitalRead(PIN_D5);
  bus <<= 1;
  bus += digitalRead(PIN_D4);
  bus <<= 1;
  bus += digitalRead(PIN_D3);
  bus <<= 1;
  bus += digitalRead(PIN_D2);
  bus <<= 1;
  bus += digitalRead(PIN_D1);
  bus <<= 1;
  bus += digitalRead(PIN_D0);
  Serial.print("Data: 0x");
  Serial.println(bus, HEX);
}

void loop() {
  if (Serial.available() > 0) {
    switch (Serial.read()) {
    case 'r':
      digitalWrite(PIN_RESET, digitalRead(PIN_RESET) ? 0 : 1);
      break;

    case 'c':
      digitalWrite(PIN_CLK, digitalRead(PIN_CLK) ? 0 : 1);
      break;

    case '0':
      digitalWrite(PIN_D0, digitalRead(PIN_D0) ? 0 : 1);
      break;

    case '1':
      digitalWrite(PIN_D1, digitalRead(PIN_D1) ? 0 : 1);
      break;

    case '2':
      digitalWrite(PIN_D2, digitalRead(PIN_D2) ? 0 : 1);
      break;

    case '3':
      digitalWrite(PIN_D3, digitalRead(PIN_D3) ? 0 : 1);
      break;

    case '4':
      digitalWrite(PIN_D4, digitalRead(PIN_D4) ? 0 : 1);
      break;

    case '5':
      digitalWrite(PIN_D5, digitalRead(PIN_D5) ? 0 : 1);
      break;

    case '6':
      digitalWrite(PIN_D6, digitalRead(PIN_D6) ? 0 : 1);
      break;

    case '7':
      digitalWrite(PIN_D7, digitalRead(PIN_D7) ? 0 : 1);
      break;
      
    default:
      break;
    }
    dump();
  }

  digitalWrite(LED_BUILTIN, digitalRead(LED_BUILTIN) ? 0 : 1);
  delay(100);
}
          

The LED blinking is used as a indicator to see that the sketch program is actually running.

Here is a photo of the setup:

Arduino Z80 Tester


Topic: Scripts and Code, by Kjetil @ 27/11-2021, Article Link

FAT16 File Extraction

This is a program I made as part of another bigger project, where I needed to read files from a FAT16 file system. It implements the bare-minimum to be able to extract a file from the root directory, and is only setup to handle the specifications of FAT16. (FAT12 or FAT32 will not work.) It can be used either on a disk image, or actually on a disk block device directly which does not need to be mounted. Calling the program with only the disk image argument will print the root directory, then add the target filename to dump that file.

Here is the code:

#include <stdio.h>
#include <stdint.h>
#include <string.h>

static void read_sector(FILE *fh, int no, uint8_t sector[])
{
  fseek(fh, no * 512, SEEK_SET);
  fread(sector, sizeof(uint8_t), 512, fh);
}

int main(int argc, char *argv[])
{
  FILE *disk;
  uint8_t sector[512];

  uint32_t vbr_offset;
  uint32_t fat_offset;
  uint32_t root_dir_offset;
  uint32_t data_offset;

  uint8_t partition_type;
  uint8_t sectors_per_cluster;
  uint16_t reserved_sectors;
  uint8_t no_of_fats;
  uint16_t root_entries;
  uint16_t sectors_per_fat;

  char file_name[13]; /* 8.3 format at NULL byte. */
  uint32_t file_size;
  uint16_t file_cluster;

  uint16_t cluster = 0; /* Target cluster for dump. */
  uint32_t cluster_file_size;
  uint16_t cluster_end_indicator;
  uint16_t cluster_fat;
  uint16_t cluster_offset;

  if (argc <= 1) {
    printf("Usage: %s <disk image> [dump file]\n", argv[0]);
    return 1;
  }

  disk = fopen(argv[1], "rb");
  if (disk == NULL) {
    printf("Error: Unable to open: %s\n", argv[1]);
    return 1;
  }

  read_sector(disk, 0, sector);

  partition_type = sector[450];
  vbr_offset = sector[454];

  printf("First partiton type: 0x%02x\n", partition_type);
  printf("First VBR @ 0x%x\n", vbr_offset * 512);

  if (partition_type != 0x04 && partition_type != 0x06) {
    printf("Error: First partition on disk is not FAT16!");
    fclose(disk);
    return 1;
  }

  read_sector(disk, vbr_offset, sector);

  sectors_per_cluster = sector[13];
  reserved_sectors    = sector[14] + (sector[15] << 8);
  no_of_fats          = sector[16];
  root_entries        = sector[17] + (sector[18] << 8);
  sectors_per_fat     = sector[22] + (sector[23] << 8);

  printf("Sectors per cluster: %d\n", sectors_per_cluster);
  printf("Reserved sectors: %d\n", reserved_sectors);
  printf("No of FATs: %d\n", no_of_fats);
  printf("Root entries: %d\n", root_entries);
  printf("Sectors per FAT: %d\n", sectors_per_fat);

  fat_offset = vbr_offset + reserved_sectors;
  root_dir_offset = fat_offset + (sectors_per_fat * no_of_fats);
  data_offset = root_dir_offset + ((root_entries * 32) / 512);

  printf("FAT Region @ 0x%x\n", fat_offset * 512);
  printf("Root Directory Region @ 0x%x\n", root_dir_offset * 512);
  printf("Data Region @ 0x%x\n", data_offset * 512);

  printf("\nRoot Directory:\n");

  for (uint32_t offset = root_dir_offset; offset < data_offset; offset++) {
    read_sector(disk, offset, sector);

    for (int i = 0; i < 512; i += 32) {
      if (sector[i] == '\0') {
        break; /* End of files. */
      }

      if (sector[i+11] & 0x10 || sector[i+11] & 0x08) {
        continue; /* Subdirectory or volume label. */
      }

      int n = 0;
      for (int j = 0; j < 8; j++) {
        if (sector[i+j] == ' ') {
          break;
        }
        file_name[n] = sector[i+j];
        n++;
      }

      file_name[n] = '.';
      n++;

      for (int j = 0; j < 3; j++) {
        if (sector[i+8+j] == ' ') {
          break;
        }
        file_name[n] = sector[i+8+j];
        n++;
      }

      file_name[n] = '\0';

      file_cluster = sector[i+26] + (sector[i+27] << 8);
      file_size = sector[i+28] +
                 (sector[i+29] << 8) +
                 (sector[i+30] << 16) +
                 (sector[i+31] << 24);

      if (argc >= 3) {
        if (strcmp(argv[2], file_name) == 0) {
          cluster = file_cluster;
          cluster_file_size = file_size;
        }
      }
      printf("%12s (%d bytes) (cluster 0x%04x)\n",
        file_name,
        file_size,
        file_cluster);
    }
  }
  printf("\n");

  if (cluster > 0) { /* Dump a file? */
    FILE *out;

    out = fopen(argv[2], "wbx");
    if (out == NULL) {
      printf("Failed to open file for writing: %s\n", argv[2]);
      fclose(disk);
      return 1;
    }

    read_sector(disk, fat_offset, sector);

    cluster_end_indicator = sector[2] + (sector[3] << 8);
    
    printf("Cluster end indicator: 0x%04x\n", cluster_end_indicator);

    unsigned int bytes_written = 0;
    while ((cluster >> 3) != (cluster_end_indicator >> 3)) {
      printf("Read cluster: 0x%04x\n", cluster);

      for (int s = 0; s < sectors_per_cluster; s++) {
        read_sector(disk,
                    data_offset + 
                    ((cluster - 2) * sectors_per_cluster) +
                    s, sector);

        bytes_written += fwrite(sector, sizeof(uint8_t), 
          (bytes_written + 512 > cluster_file_size)
            ? cluster_file_size - bytes_written
            : 512, out);
      }

      /* Calculate next cluster. */
      cluster_fat = cluster / 256;
      cluster_offset = (cluster % 256) * 2;

      read_sector(disk, fat_offset + cluster_fat, sector);
      cluster = sector[cluster_offset] + (sector[cluster_offset + 1] << 8);
    }

    fclose(out);
  }

  fclose(disk);
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 06/11-2021, Article Link

XP MCE Keyboard on Raspberry Pi Zero W

I have an old Remote Keyboard for Windows XP Media Center Edition which I have managed to "connect" to a Raspberry Pi Zero W through IR.

To get a IR functionality on the Pi, I followed these instructions and bought a Vishay TSOP38238 IR Receiver. This can be connected (or in my case soldered) directly to the GPIO header of the Pi.

|----------------|---------|---------------|
|                | TSOP    | Raspberry Pi  | 
| Name           | 38238   | Zero W Header |
|----------------|---|-----|----|----------|
| Signal Data    | 1 | OUT | 8  | GPIO14   |
| Ground         | 2 | GND | 6  | Ground   |
| Supply Voltage | 3 | VS  | 1  | 3V3      |
|----------------|---|-----|----|----------|
          


IR Receiver on Raspberry Pi Zero W


To enable the GPIO pin 14 as IR the following must be set in /boot/config.txt on the Pi:

dtoverlay=gpio-ir,gpio_pin=14
          


To be able to configure the IR related mappings in the Linux kernel, the "ir-keytable" program must be installed:

sudo apt-get install ir-keytable
          


This XP MCE keyboard uses both the RC-6 protocol for multimedia buttons and a custom protocol of the regular keys, to enable both add this to /etc/rc.local on the Pi:

ir-keytable -p rc-6 -p mce_kbd
          


Now, this should have been enough to get it working, but in my case it didn't. I suspect there might be a bug/mismatch in either the Linux kernel itself or with the ir-keytable program. At the time of writing, the Pi is running kernel version 5.10.17+ and ir-keytable version 1.16.3. The problem I observed is that most of the keys on the keyboard does not send a EV_KEY event when monitoring with the "evtest" program, which in turn causes the key to not really work at all. After some debugging and troubleshooting I discovered that the affected keys are missing from the keybit[] array in the input_dev structure for the driver.

My solution to this is to patch the Linux kernel with a custom "ir-mce_kbd-decoder.ko" kernel module. To build this you will of course need the relevant Linux kernel sources for the Pi. Using this script and instructions seems to be the easiest way. The specific kernel version downloaded in my case was commit 3a33f11c48572b9dd0fecac164b3990fc9234da8.

Here is one way to build that single kernel module, assuming you have the kernel sources and the build tools installed:

mkdir ir-mce_kbd-decoder
cd ir-mce_kbd-decoder/
cp /home/pi/linux-3a33f11c48572b9dd0fecac164b3990fc9234da8/drivers/media/rc/ir-mce_kbd-decoder.c .
cp /home/pi/linux-3a33f11c48572b9dd0fecac164b3990fc9234da8/drivers/media/rc/rc-core-priv.h .
echo "obj-m := ir-mce_kbd-decoder.o" > Makefile
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
          


The ir-mce_kbd-decoder.c file needs to be patched with the following to set those missing bits in the keybit[] array:

--- ir-mce_kbd-decoder.orig     2021-10-17 12:28:27.991142273 +0200
+++ ir-mce_kbd-decoder.c        2021-10-17 13:18:46.908921902 +0200
@@ -360,11 +360,20 @@

 static int ir_mce_kbd_register(struct rc_dev *dev)
 {
+       int i;
        struct mce_kbd_dec *mce_kbd = &dev->raw->mce_kbd;

        timer_setup(&mce_kbd->rx_timeout, mce_kbd_rx_timeout, 0);
        spin_lock_init(&mce_kbd->keylock);

+       for (i = 0; i < 256; i++) {
+               if (kbd_keycodes[i] != KEY_RESERVED) {
+                       __set_bit(kbd_keycodes[i], dev->input_dev->keybit);
+               }
+       }
+       __set_bit(BTN_LEFT, dev->input_dev->keybit);
+       __set_bit(BTN_RIGHT, dev->input_dev->keybit);
+
        return 0;
 }
          


To load the new module temporarily for test, use the following commands:

sudo modprobe -r ir-mce_kbd-decoder
sudo insmod ir-mce_kbd-decoder.ko
sudo ir-keytable -p rc-6 -p mce_kbd
          

If it works fine, the module may be copied (and overwritten) to /usr/lib/modules/5.10.17+/kernel/drivers/media/rc/ir-mce_kbd-decoder.ko

Topic: Configuration, by Kjetil @ 23/10-2021, Article Link

CP/M on the GR-SAKURA

I have ported my "kaytil" Z80 CP/M 2.2 emulator to the Gadget Renesas GR-SAKURA reference board. This means it's possible to run all kinds of off-the shelf CP/M games and software on this board.

Even though it's based on the "kaytil" codebase, some heavy modifications have been done. All the trace and debug functions have been removed to reduce the binary size and also to speed up emulation. The Z80 emulation core remains the same but the console and disk emulations have been adapted to the different hardware. The CPM 2.2 and CBIOS binaries are embedded directly into the code to allow for easier standalone operation.

For the floppy disk emulation, the floppy disk images needs to be placed at the root directory on a FAT16 formatted MicroSD card (first partition) and be named "A.IMG", "B.IMG", "C.IMG" and "D.IMG" for each of the four drives. The MicroSD card on the GR-SAKURA is read through SPI and is kind of slow, so a simple caching mechanism has been implemented where the lowest sectors are cached in memory. To get optimal performance the FAT16 partition should be placed as close as possible to the MBR, possibly on sector 3, as this should get the actual FAT table cached into memory. Note that there is only read support, any writes to floppy disk by the CP/M OS are simply ignored. The four on-board LEDs on the GR-SAKURA are used as disk activity indicators.

For the console emulation I was first considering to use Renesas USB-CDC library to provide this over the USB connection, but I could not get it to work properly, as it seemed to get congested by too much traffic and just freeze. Instead, the first UART is used, marked by pins 0 (RX) and pin 1 (TX) on the GR-SAKURA, and it runs on 115200 baud for best performance. Same as the "kaytil" emulator on Linux; conversion between ADM-3A codes to VT100/ANSI codes are done in the emulator already. I wonder if it would have been technically possible to remove this conversion and connect a real Lear Siegler ADM-3A terminal (with a voltage level shifter) directory to the GR-SAKURA...

The RX CPU on the GR-SAKURA runs at 96MHz I believe, and compiling the emulator with -O2 optimization makes the speed nearly similar to a real Kaypro II, which is nice.

You can download the first version here and there is also a branch on the "kaytil" git repo.

Topic: Open Source, by Kjetil @ 04/10-2021, Article Link

Fujitsu FKB8530 Keyboard Repair

I got hold of a strange looking modular keyboard, the Fujitsu FKB8530, which I wanted to refurbish due to its unique construction. Unfortunately I discovered that it had sustained some kind of liquid damage, which in turn had caused corrosion on the membrane. The result was that some of the keys did not work. Measurements showed that the resistance had gotten too high between some of the points on one of the traces on the membrane.

I was able to repair this by gently scraping of the top layer on the membrane trace with a knife, then applying conductive silver paint (Loctite 3863 Circuit +) on top. I used regular Scotch tape to mask off the relevant area:

Silver paint applied


The keyboard after re-assembly:

Re-assembled keyboard


Topic: Repair, by Kjetil @ 22/09-2021, Article Link

Linux System on a Floppy

Referring to my previous project to build a Linux distribution for LOADLIN. It is actually possible to also boot this directly from a 1.44M floppy disk, which can be quite useful. The output from that build should be the Linux kernel "bzImage" and the root filesystem "rootfs.cramfs". I renamed "rootfs.cramfs" to simply "rootfs" to avoid any conflicts with the 8.3 filename format.

SYSLINUX will be used for booting, and it needs "syslinux.cfg" configuration file with the followng contents:

DEFAULT linux
LABEL linux
  KERNEL bzImage
  APPEND initrd=rootfs
          


These are the steps to make the floppy disk image:

dd if=/dev/zero of=floppy.img bs=512 count=2880
mkdosfs floppy.img
syslinux --install floppy.img
mkdir /tmp/floppy
mount -o loop floppy.img /tmp/floppy
cp bzImage /tmp/floppy/
cp rootfs /tmp/floppy/
cp syslinux.cfg /tmp/floppy/
umount /tmp/floppy
          


The floppy disk image can be tested in QEMU with:

qemu-system-i386 -fda floppy.img -boot a
          


Writing the floppy disk image to an actual floppy disk is typically done like so:

dd if=floppy.img of=/dev/fd0 bs=512
          


For convenience, download the floppy disk image here.

Topic: Configuration, by Kjetil @ 05/09-2021, Article Link

Tandon TM9362 Data Recovery

I have a Tandon TM9362 hardcard. Such hardcards are notorious for failing, a common problem is that the heads and other mechanical parts get stuck over time. Despite this I managed to fully recover the data from it. The actual hard drive on the card is a Tandon TM362, which is a 21MB MFM drive.

Tandon TM9362 and Bike Oil


The shaft of the motor controlling the heads on these drives are actually accessible, which makes things easer. I managed to turn the shaft manually with my fingers to loosen it. Afterwards I applied 2 drops of "Bike Oil", which is similar to bearing oil, into the motor through the shaft and wiggled it some more. The card was then connected to a 486-class PC. Still, even with this oil applied it would not always power up on startup, but some more wiggling and power cycling made it spin up in the end.

This card contains it's own BIOS routines for accessing the disk, so I removed the existing hard drive on the PC to prevent any interference. It booted up with MS-DOS 3.20 and timestamps from 1990, so it could have been over 30 years since it was last accessed. I think MS-DOS 3.20 has issues with newer 1.44M floppy disks, so I booted DOS 5.0 from a floppy instead, still with the hardcard connected of course.

This PC has a 3Com 3C509 Ethernet card, so I loaded the Crynwr Packet Drivers for this and configured mTCP for TCP/IP networking. I setup the mTCP "ftpsrv" FTP server to host ALL files from the C: drive and used "ncftpget" from a Linux box to recursively download everything.

An additional cool but not really necessary step is to get those files onto a virtual disk image that can be loaded in QEMU. This particular hardcard has a CHS configuration of 615 cylinders, 4 heads and 17 sectors (per cylinder). Multiplying this with the standard sector size of 512 gives 41820, so a blank disk image can be made by:

dd if=/dev/zero of=tandon.img bs=512 count=41820
          


Importantly, in order for QEMU to properly forward the same CHS settings into the emulation, it needs to be specified on startup like so:

qemu-system-i386 -drive file=tandon.img,format=raw,if=none,id=dr0 -device drive=dr0,driver=ide-hd,cyls=615,heads=4,secs=17 -boot c
          


So actually get the files over I booted QEMU with the virtual disk image and another copy of a MS-DOS 3.20 floppy boot disk image that I found online. From the emulated DOS i ran the DOS "fdisk" and "format c: /s" to format the virtual disk in a compatible manner. Then I ran the Linux "fdisk" on the image and noticed the FAT partition was placed on sector 17 and onwards. Multiplying this by 512 gives 8704, so the partition on the virtual disk image can be loopback mounted like so:

sudo mount -o loop,offset=8704 tandon.img /tmp/tandon
          


Afterwards the recovered files can just be copied into the mounted loopback drive. The result is a bootable (in QEMU) virtual disk image with the correct MBR and VBR.

Topic: Repair, by Kjetil @ 07/08-2021, Article Link

Older articles