Information wants to be free...

VT100 Terminal Emulator on Raspberry Pi Pico

Here is another Raspberry Pi Pico project, this time a DEC VT100 Terminal emulator. Host communication goes through the "standard" UART, composite video is used for the output and a PS/2 keyboard is used for the input. I have named this program "Terminominal".

Since I'm Norwegian I have made some design choices that reflect this. The first is that the composite video uses the PAL standard at 50 Hz. The keyboard scancode handling expects the Norwegian layout, but this can be changed to US layout with a compile time define. Finally the built-in font/character set is ISO-8859-1 (latin-1) compatible.

The composite video (CVBS) is generated by one of the Pico's PIO cores running at 40 MHz, which in turn creates time slices of 0.05 microseconds that affects a lot of the architecture in this program. After taking care of overscan, there is 880 dots on 240 scanlines remaining for the "visible" picture. First I tried using a standard CGA font at 8x8 which would give 110 columns by 30 rows, but the picture was almost unreadable. Instead I designed my own 11x10 font which fits perfectly for a 80 column by 24 rows picture and is quite readable on a CRT display.

The first ARM core reads the UART for incoming bytes and processes these to emulate the VT100 behaviour. Escape codes are processed and then a "screen" array is updated which represents all the characters currently on the screen. The second ARM core fetches characters from this "screen" array, if they have changed, and converts these to scanline and dot information using the font data and puts this into a "frame" array. DMA is used by the PIO core to fetch the "frame" array and push this out on the GPIO pins.

A 2-bit DAC for the video is created by using two resistors. Since the GPIO voltage is 3.3V and the composite video input impedance (on a TV set) is 75 ohms, good values are 680 ohms and 220 ohms for these resistors. It is also possible to use 1000 ohms and 330 ohms, which gives a slightly dimmer picture. The 2 bits gives four signal levels, where one is sync and the other three shades of: black, grey and white.

The PS/2 keyboard protocol is handled by the second PIO core which generates an interrupt once the required 11 bit frame has been read. The interrupt handler converts the scancode to the appropriate byte and sends it out on the UART. Note that since the PS/2 protocol uses 5V and the Raspberry Pi Pico GPIO pins uses 3.3V a level shifter is needed in between!

The VT100 emulation has been tested with vttest and the most important features are supported. VT52 and VT102 modes are not really supported. It should work fine against a Linux host at least. The UART runs at 115200 baud, but can be changed through re-compilation to other speeds.

Here is a block diagram which shows the architecture:

Terminominal Diagram


Here is a connection/function table for the GPIO pins:

|--------|-----------|------------|-------------------------|
| Pin No | Pin Name  | Function   | Connected To            |
|--------|-----------|------------|-------------------------|
| 1      | GP0       | UART TX    |                         |
| 2      | GP1       | UART RX    |                         |
| 6      | GP4       | PS/2 Data  | 3.3V<->5V Level Shifter |
| 7      | GP5       | PS/2 Clock | 3.3V<->5V Level Shifter |
| 21     | GP16      | CVBS DAC   | 680 Ohm Resistor        |
| 22     | GP17      | CVBS DAC   | 220 Ohm Resistor        |
| 36     | 3V3 (OUT) | +3.3V      | 3.3V<->5V Level Shifter |
| 40     | VBUS      | +5V        | 3.3V<->5V Level Shifter |
|--------|-----------|------------|-------------------------|
          


Here is a breadboard layout created with Fritzing:

Terminominal Breadboard


Notice that the GPIO pins for the composite video is on the other end of the Pico. This was needed because it generates a lot of noise, so much in fact that it affects the PS/2 signalling. This would result in lost bits and parity errors.

A snapshot of version 0.1 can be downloaded here, or check the Git repository here. Check the included README.md for build instructions. It is also possible to compile an SDL version which is used for testing directly on Linux.

I appreciate the good instructions from Van Hunter Adams on how to use DMA on the Raspberry Pi Pico and Javier Valcarce on how to generate PAL video signals.

Here is a photo of Terminominal running on one Pico and connected to another Pico running the kaytil CP/M emulator:

Terminominal and Kaytil


Topic: Open Source, by Kjetil @ 03/06-2022, Article Link

Telephone Handset Connector Replacements

I have some old telephone handsets, where at least one of them seems to date back to the 1930's based on some patents ("British Patents 319837.328926.433234") that I found. All of them had strange connectors incompatible with modern equipment:

Old connection on handset #1

Old connection on handset #2

Old connection on handset #3


But since the speaker and microphone technology has remained mostly the same for over a century, I decided to simply replace the connectors with modern mini jack plugs:

Mini jack plug replacements

This makes it possible to connect them easily to a modern PC and even use them for voice chat.

On the first one the original cable disintegrated when I tried to re-use it, so I replaced the entire cable. This one also has a switch that I wired to pin 8 and 7 on a DE9 connector, so that I can use the CTS/RTS trick to read it through a standard PC serial port.

Handset #1 finished


On the second one I was able to re-use the original cable. This one also has a switch, but I did not wire that up to anything.

Handset #2 finished


On the third one I had issues with a broken wire in the original cable, so that had to be replaced as well. Here I ended up simply re-using the entire cable with connectors from a broken PC headset.

Handset #3 finished


Topic: Repair, by Kjetil @ 13/05-2022, Article Link

CP/M on the Raspberry Pi Pico

A new port is available of my "kaytil" Z80 CP/M 2.2 emulator, this time targeting the new Raspberry Pi Pico.

A snapshot of the "Version 0.2" source can be downloaded here, or check out the Git repository here. The GR-SAKURA version has now also been incorporated into the same tarball and Git repo, instead of residing on a branch.

In the same fashion as the GR-SAKURA version, the CP/M 2.2 and CBIOS binaries are embedded directly into the final binary. For the Pico version this is taken even a step further since the disk images are ALSO embedded into the final binary! This means the disk images are in fact hard-coded and needs to be provided/updated before building. The disk images provided with the source code are just dummies and should be replaced with something more useful.

I figured that embedding the disk images was the easiest way to get things up and running considering the constraints of the Raspberry Pi Pico platform. This works nicely since the Pico has 2MB of Flash and can easily hold four IBM 3740 floppy disk images, typically 256KB in size each. Embedding the disk images is done using objcopy and forcefully placed in the ".rodata" section which causes them to reside in the Pico Flash instead of the SRAM.

The console is available on the "standard" UART at pins 1 and 2 and running at 115200 baud. Conversion between ADM-3A codes to VT100/ANSI codes are done in the emulator in the same way as the other versions.

Building requires the ARM GCC toolchain, CMake and the Pico SDK; just follow the official guide on how to set this up. In typical CMake fashion, create a build folder and call cmake pointing to the "pico/" subdirectory containing the CMakeLists.txt file:

mkdir build
cd build
PICO_SDK_PATH=/path/to/pico-sdk cmake /path/to/kaytil/pico/
make
          

The resulting "kaytil.elf" file can be flashed with SWD, or the "kaytil.uf2" file can be copied through USB in the BOOTSEL mode.

Here is a photo showing it in action, using a Commodre 64 as a terminal:

Kaytil on the Pico with a C64 Terminal

For this to work I used a level shifter between the C64 user port and the Raspberry Pi Pico to convert between +5V and +3.3V. In addition I changed the baudrate to 2400 baud by calling "uart_set_baudrate(uart0, 2400)" in "console_init()".

Topic: Open Source, by Kjetil @ 22/04-2022, Article Link

Terminal Mode Commodore 64 Emulator Update

I have performed a massive overhaul of the proof-of-concept C64 emulator I published about last year. This new version,which I have dubbed "0.2" is much more usable.

A major change in the architecture is that the old version was blocking on input from stdin, which is an interesting concept, but not feasible for more advanced usage. It is now based around curses with non-blocking stdin instead. Moving to curses means there is now a lot better support for graphics, including colors if a 256-color terminal is used. Conversion is done from PETSCII to ASCII and will work best as long as the default character sets are used. There is no sprite emulation.

Pressing Ctrl-C at any time will switch to a (non-curses) debugger. In this mode it is possible to dump memory locations and get a CPU trace. In addition there is support for setting read and write breakpoints on memory addresses which breaks back into this same debugger. In order to properly exit the emulator one must also typically enter the debugger and issue a 'q' from there to quit.

The emulator can be run at close to C64 speed (for games) or in "warp mode" where 100% CPU is used (for raw BASIC programs). I also needed to add support for the timers in the CIA since some games rely on these for random number generation. One unique feature is that getting the TOD (Time of Day) registers from the CIA will return the actual clock from the host system.

As before the emulator needs the ROMs from VICE to run, but the location of these can now be specified on the command line in case they are not at the default location.

There is no Datasette (tape) or 1541 (floppy disk) support, but C64 PRG-style programs may be loaded directly from the command line (or debugger) instead. These are injected directly into memory where they are supposed to be for the BASIC "RUN" command to work. Most BASIC programs should probably work, but machine code programs are hit-and-miss.

The new version can be downloaded here and the GitHub repository has also been updated.

Finally, some color screenshots:

BASIC Prompt

Rolling Demo #1

Rolling Demo #2

Lord of the Balrogs

Oslo B0rs

Regneark

Stock Market


Topic: Open Source, by Kjetil @ 08/04-2022, Article Link

lazyboNES Emulator

Here is a special NES emulator made for the specific purpose of playing Super Mario Bros with text-based graphics, that is, using curses.

The NES graphics is based around a background of 32 by 30 tiles, each 8x8 pixels. This emulator maps each tile to a character in the terminal and you need to resize it to at least 32x30 for everything to fit. In addition to the text based interface, an SDL2 based graphical output can also be run in parallel. Sound and joystick input will always be provided by SDL2, even when running only in text mode.

Although it is technically possible to play with the keyboard directly into the terminal, it is highly recommended to play with a joystick/gamepad instead. Using the keyboard into the terminal means that all input is fed into a non-blocking "standard in" stream, which makes key combinations impossible. E.g. when walking and then jumping, the walk key will get "unpressed" causing Mario to stop.

The emulator is very specific to Super Mario Bros since the characters are directly mapped to its pattern tables. Other games, even if they start, will probably look really bad. Internally NTSC timings are used, so maybe only the USA version will work.

Several shortcuts have been made with regards to the emulation. The PPU (graphics) emulation only supports horizontal scrolling and no vertical scrolling. PPU background/sprite priority is also not implemented, so sprites are always drawn on top of the background. Rendering on the edges is also not hidden away. APU (sound) emulation supports pulse/triangle/noise channels but not the DMC channel, and due to lack of filters and such the sound quality is far from perfect.

The joystick/gamepad is detected on startup if connected, but the button mapping is hardcoded, so edit the source and re-build to change it. Save and load of the state is supported but only in one slot and only in memory, so it's not saved when quitting the emulator.

6502 CPU emulation is a modified version from my Commodore 64 emulator so it contains BCD mode which is not actually present on the Ricoh 2A03 used in the NES. Ctrl+C in the terminal will break into a debugger where various data can be dumped, including a CPU trace if compiled.

Here is picture of both the curses and SDL2 graphic outputs running in parallel:

lazyboNES screenshot


The first version of the source code is available here, but it has also been uploaded to GitHub for possible future developments.

Topic: Open Source, by Kjetil @ 25/03-2022, Article Link

DTMF Sound Generator

This is a simple program to generate DTMF sounds using SDL as used by telephones. Simply pass the "phone number" as the command line argument.

The code:

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_audio.h>
#include <math.h> /* sin() */

#define AUDIO_SAMPLE_RATE 44100

typedef struct audio_data_s {
  uint32_t sample_no;
  double freq_1;
  double freq_2;
  bool stop;
  bool stopped;
} audio_data_t;

static void dtmf(char c, double *freq_h, double *freq_v)
{
  switch (c) {
  default:
  case '1':
  case '4':
  case '7':
  case '*':
    *freq_h = 1209;
    break;
  case '2':
  case '5':
  case '8':
  case '0':
    *freq_h = 1336;
    break;
  case '3':
  case '6':
  case '9':
  case '#':
    *freq_h = 1477;
    break;
  case 'A':
  case 'B':
  case 'C':
  case 'D':
    *freq_h = 1633;
    break;
  }

  switch (c) {
  default:
  case '1':
  case '2':
  case '3':
  case 'A':
    *freq_v = 697;
    break;
  case '4':
  case '5':
  case '6':
  case 'B':
    *freq_v = 770;
    break;
  case '7':
  case '8':
  case '9':
  case 'C':
    *freq_v = 852;
    break;
  case '*':
  case '0':
  case '#':
  case 'D':
    *freq_v = 941;
    break;
  }
}

static void audio_callback(void *userdata, Uint8 *stream, int len)
{
  int i;
  audio_data_t *audio_data;
  double sample;
  double time;
  
  audio_data = (audio_data_t *)userdata;

  if (audio_data->stop == false) {
    audio_data->stopped = false;
  }

  for (i = 0; i < len; i++) {
    time = audio_data->sample_no / (double)AUDIO_SAMPLE_RATE;
    sample = sin(2.0 * M_PI * audio_data->freq_1 * time);
    sample += sin(2.0 * M_PI * audio_data->freq_2 * time);
    sample /= 2;
    if (audio_data->stopped) {
      stream[i] = 128;
    } else {
      stream[i] = (Uint8)(127 + (sample * 127));
      if (audio_data->stop && stream[i] >= 126 && stream[i] <= 130) {
        audio_data->stopped = true;
      }
    }
    audio_data->sample_no++;
  }
}

int main(int argc, char *argv[])
{
  SDL_AudioSpec desired, obtained;
  audio_data_t audio_data;
  char *number;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <number>\n", argv[0]);
    return 0;
  }

  if (SDL_Init(SDL_INIT_AUDIO) != 0) {
    fprintf(stderr, "SDL_Init() failed: %s\n", SDL_GetError());
    return 1;
  }
  atexit(SDL_Quit);

  desired.freq     = AUDIO_SAMPLE_RATE;
  desired.format   = AUDIO_U8;
  desired.channels = 1;
  desired.samples  = 1024;
  desired.userdata = &audio_data;
  desired.callback = audio_callback;

  if (SDL_OpenAudio(&desired, &obtained) != 0) {
    fprintf(stderr, "SDL_OpenAudio() failed: %s\n", SDL_GetError());
    return 1;
  }

  if (obtained.format != AUDIO_U8) {
    fprintf(stderr, "Did not get unsigned 8-bit audio format!\n");
    SDL_CloseAudio();
    return 1;
  }

  for (number = &argv[1][0]; *number != '\0'; number++) {
    audio_data.sample_no = 0;
    audio_data.stop = false;
    dtmf(*number, &audio_data.freq_1, &audio_data.freq_2);
    SDL_PauseAudio(0);
    SDL_Delay(200);
    audio_data.stop = true;
    SDL_Delay(20);
    SDL_PauseAudio(1);
    SDL_Delay(30);
  }

  SDL_CloseAudio();

  return 0;
}

          


Topic: Scripts and Code, by Kjetil @ 11/03-2022, Article Link

DOS Keyboard Fix TSR

I have an old AST Premium Exec 386SX/20 laptop running DOS. After the mechanical harddisk failed I replaced it with a flashdisk, but after re-assembly some of the keys on the keyboard would no longer work. I traced this to the '.', '', '', '+' and '\' keys, all of which happens to be on one row/column on the keyboard matrix. After further troubleshooting I could find no fault with the keyboard or associated cables, so unfortunately the problem seems to be within the keyboard controller which is embedded into a custom chipset.

Custom AST Actel Chipset


The lack of '.' and '\' makes it hard to navigate around in DOS, so I started looking into a solution. Since it is nearly impossible to find spare parts like that custom chipset I ended up with a software solution, specifically in the form of a TSR.

This TSR hooks into and intercepts the int 16h calls that are used for keyboard services. By pressing Alt+F1 a scancode representing '.' is returned instead. Also Alt+F2 returns '\' and Alt+F3 returns ':'. This will only work for DOS programs like COMMAND.COM or EDIT that actually use the BIOS services. For most games it probably won't work, but luckily the affected keys are seldom used in games.

The code:

org 0x100
bits 16
cpu 8086

section .text
start:
  jmp main

int16_interrupt:
  sti ; Allow other interrupts!
  mov word [int16_incoming_ax], ax ; Store incoming parameter for later use.

  ; Call original interrupt handler:
  pushf
  cli
original_int16:
  call original_int16:original_int16 ; Will be overwritten runtime!
  sti

  ; Check if result should be intercepted:
  pushf
  push bx
  push ax
  mov word bx, [int16_incoming_ax]
  cmp bh, 0x00 ; Check if AH was "Wait for Keypress".
  je int16_check_f1
  cmp bh, 0x10 ; Check if AH was "Extended Wait for Keypress".
  je int16_check_f1
  jmp int16_end_pop_ax

int16_check_f1:
  cmp ax, 0x6800 ; Alt + F1
  jne int16_check_f2
  pop ax
  mov ax, 0x342E ; '.'
  jmp int16_end

int16_check_f2:
  cmp ax, 0x6900 ; Alt + F2
  jne int16_check_f3
  pop ax
  mov ax, 0x2B5C ; '\'
  jmp int16_end

int16_check_f3:
  cmp ax, 0x6A00 ; Alt + F3
  jne int16_end_pop_ax
  pop ax
  mov ax, 0x273A ; ':'
  jmp int16_end

int16_end_pop_ax:
  pop ax
int16_end:
  pop bx
  popf
  retf 2

int16_incoming_ax:
  dw 0

tsr_end: ; TSR end marker.

main:
  ; NOTE: No protection to prevent TSR from being loaded twice or more!

  ; Call DOS to get original interrupt handler:
  mov al, 0x16
  mov ah, 0x35
  int 0x21
  mov word [original_int16 + 3], es
  mov word [original_int16 + 1], bx

  ; Call DOS to set new interrupt handler:
  mov al, 0x16
  mov ah, 0x25
  ; DS is already same as CS, no need to change.
  mov dx, int16_interrupt
  int 0x21

  ; Terminate and Stay Resident:
  mov dx, tsr_end
  shr dx, 1
  shr dx, 1
  shr dx, 1
  shr dx, 1
  add dx, 0x11 ; Add 0x1 for remainder and 0x10 for PSP.
  mov ax, 0x3100
  int 0x21
          


Assemble it with NASM: nasm fixkeys.asm -fbin -o fixkeys.com

Topic: Scripts and Code, by Kjetil @ 25/02-2022, Article Link

Airline Management Simulator

I made a simple airline management simulator game as part of studying how the GNU Readline library works with regards to command completion. This means the game is based around a CLI which offers tab completion.

The goal of the game is to reach a certain amount of cash after buying different airplanes and setting up routes between cities at different ticket prices. Most of the data, like the cities, are randomly generated on startup. The challenge lies in the hidden information that can only be found through trial and error, like finding which cities have citizens that are willing to pay for profitable ticket prices. I realized that it is very hard to balance simulation games, so this game as well might be too hard or too easy, I am not sure.

What it looks like:

Screenshot airline management simulator.


Anyway, here is the source code, try to compile it with: gcc -lreadline -lncurses -lm -Wall

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <math.h>
#include <inttypes.h>

#include <readline/readline.h>
#include <readline/history.h>



#define CITY_MAX 24
#define AIRPLANE_MAX 96

#define CITY_X_MAX 32768 /* Kilometers */
#define CITY_Y_MAX 32768 /* Kilometers */
#define CITY_DISTANCE_MINIMUM 500

#define COMMAND_NAME_MAX 10
#define COMMAND_ARG_MAX 4
#define COMMAND_ARG_NAME_MAX 16

#define GAME_CASH_START 200000
#define GAME_CASH_GOAL 1000000000
#define GAME_RED_DAYS_MAX 10



typedef enum {
  COMMAND_ARG_TYPE_NONE = -1,
  COMMAND_ARG_TYPE_PRICE,
  COMMAND_ARG_TYPE_CITY_NAME,
  COMMAND_ARG_TYPE_CITY_SRC,
  COMMAND_ARG_TYPE_CITY_DST,
  COMMAND_ARG_TYPE_AIRPLANE_ID,
  COMMAND_ARG_TYPE_AIRPLANE_TYPE,
  COMMAND_ARG_TYPE_MAX,
} command_arg_type_t;

typedef char *(*command_arg_completion_function_t)(const char *, int);

typedef struct command_arg_data_s {
  char name[6];
  bool optional;
  command_arg_completion_function_t function;
} command_arg_data_t;

typedef void (*command_function_t)(int, const char *[]);

typedef struct command_s {
  char name[COMMAND_NAME_MAX];
  command_function_t function;
  command_arg_type_t arg[COMMAND_ARG_MAX];
} command_t;

typedef struct city_s {
  char name[4]; /* Airport Code! */
  uint16_t x;
  uint16_t y;
  uint32_t demand[CITY_MAX];
  uint8_t size;
  uint8_t stinginess;
} city_t;

typedef enum {
  AIRPLANE_TYPE_INVALID = -1,
  AIRPLANE_TYPE_SMALL,
  AIRPLANE_TYPE_MEDIUM,
  AIRPLANE_TYPE_LARGE,
  AIRPLANE_TYPE_MAX,
} airplane_type_t;

typedef struct airplane_s {
  bool exists;
  bool parked;
  int src;
  int dst;
  uint32_t ticket_price;
  airplane_type_t type;
} airplane_t;

typedef struct airplane_data_s {
  char name[8];
  uint32_t capacity;
  uint32_t price;
  double fuel_consumption_factor;
  uint32_t upkeep_cost;
} airplane_data_t;

typedef struct game_state_s {
  bool loop_running;
  uint32_t day;
  int64_t cash;
  uint32_t current_fuel_price;
  uint32_t nominal_fuel_price;
  uint8_t days_in_the_red;
} game_state_t;



static void game_cycle(void);

static void function_airplane(int argc, const char *argv[]);
static void function_buy(int argc, const char *argv[]);
static void function_city(int argc, const char *argv[]);
static void function_help(int argc, const char *argv[]);
static void function_park(int argc, const char *argv[]);
static void function_quit(int argc, const char *argv[]);
static void function_route(int argc, const char *argv[]);
static void function_sell(int argc, const char *argv[]);
static void function_status(int argc, const char *argv[]);
static void function_wait(int argc, const char *argv[]);
#ifdef DEBUG_COMMAND
static void function_debug(int argc, const char *argv[]);
#endif /* DEBUG_COMMAND */

static char *command_arg_completion_city(const char *text, int state);
static char *command_arg_completion_airplane_id(const char *text, int state);
static char *command_arg_completion_airplane_type(const char *text, int state);



static command_t commands[] = {
  {"airplane", function_airplane, {-1,-1,-1,-1}},
  {"buy",      function_buy,      {COMMAND_ARG_TYPE_AIRPLANE_TYPE,-1,-1,-1}},
  {"city",     function_city,     {COMMAND_ARG_TYPE_CITY_NAME,-1,-1,-1}},
  {"help",     function_help,     {-1,-1,-1,-1}},
  {"park",     function_park,     {COMMAND_ARG_TYPE_AIRPLANE_ID,-1,-1,-1}},
  {"quit",     function_quit,     {-1,-1,-1,-1}},
  {"route",    function_route,    {COMMAND_ARG_TYPE_AIRPLANE_ID,
                                   COMMAND_ARG_TYPE_CITY_SRC,
                                   COMMAND_ARG_TYPE_CITY_DST,
                                   COMMAND_ARG_TYPE_PRICE}},
  {"sell",     function_sell,     {COMMAND_ARG_TYPE_AIRPLANE_ID,-1,-1,-1}},
  {"status",   function_status,   {-1,-1,-1,-1}},
  {"wait",     function_wait,     {-1,-1,-1,-1}},
#ifdef DEBUG_COMMAND
  {"debug",    function_debug,    {-1,-1,-1,-1}},
#endif /* DEBUG_COMMAND */
};

#define COMMANDS_MAX (sizeof(commands) / sizeof(commands[0]))

static command_arg_data_t command_arg_data[COMMAND_ARG_TYPE_MAX] = {
  {"price", false, NULL},
  {"city",  true,  command_arg_completion_city},
  {"src",   false, command_arg_completion_city},
  {"dst",   false, command_arg_completion_city},
  {"id",    false, command_arg_completion_airplane_id},
  {"type",  false, command_arg_completion_airplane_type},
};

static airplane_data_t airplane_data[AIRPLANE_TYPE_MAX] = {
  {"small",   50,  50000, 0.5,  500},
  {"medium", 150, 150000, 1.0, 1000},
  {"large",  400, 400000, 2.0, 5000},
};



static game_state_t game;
static city_t city[CITY_MAX];
static airplane_t airplane[AIRPLANE_MAX];



static int city_distance(int city1_index, int city2_index)
{
  /* Pythagorean theorem */
  double a = abs(city[city1_index].x - city[city2_index].x);
  double b = abs(city[city1_index].y - city[city2_index].y);
  double c = sqrt((a * a) + (b * b));
  return (int)c;
}



static void city_generate(void)
{
  for (int i = 0; i < CITY_MAX; i++) {

    /* Generate uniqe name. */
    bool name_unique = false;
    while (! name_unique) {
      name_unique = true;
      city[i].name[0] = 0x41 + random() % 26;
      city[i].name[1] = 0x41 + random() % 26;
      city[i].name[2] = 0x41 + random() % 26;
      city[i].name[3] = '\0';
      for (int j = 0; j < i; j++) {
        if (0 == strncmp(city[i].name, city[j].name, strlen(city[i].name))) {
          name_unique = false;
        }
      }
    }

    /* Generate location, but at a minimum distance from others. */
    bool distance_ok = false;
    while (! distance_ok) {
      distance_ok = true;
      city[i].x = random() % CITY_X_MAX;
      city[i].y = random() % CITY_Y_MAX;
      for (int j = 0; j < i; j++) {
        if (city_distance(i, j) < CITY_DISTANCE_MINIMUM) {
          distance_ok = false;
        }
      }
    }

    city[i].size       = 1 + (random() % 255);
    city[i].stinginess = 1 + (random() % 255);
  }

  /* Calculate base demand when all cities are setup. */
  for (int i = 0; i < CITY_MAX; i++) {
    for (int j = 0; j < CITY_MAX; j++) {
      city[i].demand[j] = city[i].size + city[j].size;
      city[i].demand[j] += random() % 64;
    }
  }
}



static int city_index_by_name(const char *city_name)
{
  for (int i = 0; i < CITY_MAX; i++) {
    if (0 == strncmp(city[i].name, city_name, strlen(city[i].name))) {
      return i;
    }
  }
  return -1;
}



static char *city_size_description(int city_index)
{
  uint8_t size = city[city_index].size;

  if (size > 224) {
    return "huge";
  } else if (size > 192) {
    return "large";
  } else if (size > 128) {
    return "medium";
  } else if (size > 64) {
    return "small";
  } else {
    return "tiny";
  }
}



static airplane_type_t airplane_type_by_name(const char *type_name)
{
  for (int i = 0; i < AIRPLANE_TYPE_MAX; i++) {
    if (0 == strncmp(airplane_data[i].name, type_name,
      strlen(airplane_data[i].name))) {
      return i;
    }
  }
  return -1;
}



static void function_quit(int argc, const char *argv[])
{
  game.loop_running = false;
}



static void function_help(int argc, const char *argv[])
{
  for (int i = 0; i < COMMANDS_MAX; i++) {
    printf("%10s ", commands[i].name);
    for (int j = 0; j < COMMAND_ARG_MAX; j++) {
      command_arg_type_t type = commands[i].arg[j];
      if (type == COMMAND_ARG_TYPE_NONE) {
        break;
      }
      if (command_arg_data[type].optional) {
        printf("[");
      } else {
        printf("<");
      }
      printf("%s", command_arg_data[type].name);
      if (command_arg_data[type].optional) {
        printf("] ");
      } else {
        printf("> ");
      }
    }
    printf("\n");
  }
}



static void function_city(int argc, const char *argv[])
{
  if (argc == 1) {
    for (int i = 0; i < CITY_MAX; i++) {
      /* Convert X,Y to Latitude/Longitude. */
      char latitude_direction, longitude_direction;
      double latitude_degrees, longitude_degrees;
      latitude_degrees =  ((double)city[i].x / (double)CITY_X_MAX) * 180;
      longitude_degrees = ((double)city[i].y / (double)CITY_Y_MAX) * 180;
      if (latitude_degrees > 90.0) {
        latitude_direction = 'E';
        latitude_degrees = latitude_degrees - 90.0;
      } else {
        latitude_direction = 'W';
        latitude_degrees = 90.0 - latitude_degrees;
      }
      if (longitude_degrees > 90.0) {
        longitude_direction = 'N';
        longitude_degrees = longitude_degrees - 90.0;
      } else {
        longitude_direction = 'S';
        longitude_degrees = 90.0 - longitude_degrees;
      }
      printf("%s @ %4.1f %c, %4.1f %c (%s city)\n", city[i].name,
        longitude_degrees, longitude_direction,
        latitude_degrees, latitude_direction,
        city_size_description(i));
    }

  } else {

    int index = city_index_by_name(argv[1]);
    if (index < 0) {
      printf("City does not exist!\n");
      return;
    }

    for (int i = 0; i < CITY_MAX; i++) {
      if (index == i) {
        continue;
      }
      printf("%s -> %s   Demand: %d\n",
        city[index].name,
        city[i].name,
        city[index].demand[i]);
    }
  }
}



static void function_airplane(int argc, const char *argv[])
{
  int airplanes = 0;

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (airplane[i].exists) {
      printf("#%d   ", i + 1);
      printf("Type: %-6s   ", airplane_data[airplane[i].type].name);
      if (airplane[i].parked) {
        printf("Parked\n");
      } else {
        printf("At: %s   Going To: %s   Charging: $%d\n",
          city[airplane[i].src].name,
          city[airplane[i].dst].name,
          airplane[i].ticket_price);
      }
      airplanes++;
    }
  }

  if (0 == airplanes) {
    printf("No airplanes owned, buy one...\n");
  }
}



static void function_buy(int argc, const char *argv[])
{
  if (argc != 2) {
    printf("Please specify airplane type!\n");
    printf("Valid types are: ");
    for (int i = 0; i < AIRPLANE_TYPE_MAX; i++) {
      printf("%s ", airplane_data[i].name);
    }
    printf("\n");
    return;
  }

  airplane_type_t type = airplane_type_by_name(argv[1]);
  if (AIRPLANE_TYPE_INVALID == type) {
    printf("Invalid airplane type specified!\n");
    return;
  }

  if (game.cash < airplane_data[type].price) {
    printf("Not enough cash!\n");
    return;
  }

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (! airplane[i].exists) {
      airplane[i].type = type;
      airplane[i].exists = true;
      airplane[i].parked = true;
      airplane[i].ticket_price = 0;
      game.cash -= airplane_data[type].price;

      printf("Airplane #%d with capacity %d bought for $%d.\n", i + 1,
        airplane_data[type].capacity, airplane_data[type].price);
      game_cycle();
      return;
    }
  }

  printf("Cannot buy more airplanes!\n");
}



static void function_route(int argc, const char *argv[])
{
  if (argc != 5) {
    printf("Missing arguments!\n");
    return;
  }

  int airplane_index = atoi(argv[1]);
  airplane_index--; /* ID to index */
  if (airplane_index < 0 || airplane_index >= AIRPLANE_MAX) {
    printf("Invalid airplane ID!\n");
    return;
  }

  if (false == airplane[airplane_index].exists) {
    printf("Airplane #%d does not exist!\n", airplane_index + 1);
    return;
  }

  if (false == airplane[airplane_index].parked) {
    printf("Airplane #%d is in operation (not parked)!\n", airplane_index + 1);
    return;
  }

  int src_index = city_index_by_name(argv[2]);
  if (src_index < 0) {
    printf("Source city does not exist!\n");
    return;
  }

  int dst_index = city_index_by_name(argv[3]);
  if (dst_index < 0) {
    printf("Destination city does not exist!\n");
    return;
  }

  int price = atoi(argv[4]);
  if (price <= 0) {
    printf("Price is invalid!\n");
    return;
  }

  airplane[airplane_index].parked = false;
  airplane[airplane_index].src = src_index;
  airplane[airplane_index].dst = dst_index;
  airplane[airplane_index].ticket_price = price;

  printf("Airplane #%d now flying from %s to %s for $%d.\n",
    airplane_index + 1, city[src_index].name, city[dst_index].name, price);
  game_cycle();
}



static void function_park(int argc, const char *argv[])
{
  if (argc != 2) {
    printf("Please specify airplane ID!\n");
    return;
  }

  int airplane_index = atoi(argv[1]);
  airplane_index--; /* ID to index */
  if (airplane_index < 0 || airplane_index >= AIRPLANE_MAX) {
    printf("Invalid airplane ID!\n");
    return;
  }

  if (false == airplane[airplane_index].exists) {
    printf("Airplane #%d does not exist!\n", airplane_index + 1);
    return;
  }

  if (true == airplane[airplane_index].parked) {
    printf("Airplane #%d already parked!\n", airplane_index + 1);
    return;
  }

  airplane[airplane_index].parked = true;

  printf("Airplane #%d parked.\n", airplane_index + 1);
  game_cycle();
}



static void function_status(int argc, const char *argv[])
{
  int airplanes = 0;

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (airplane[i].exists) {
      airplanes++;
    }
  }

  printf("Day        : %d\n", game.day);
  printf("Cash       : $%"PRIi64"\n", game.cash);
  printf("Airplanes  : %d\n", airplanes);
  printf("Fuel price : $%d per kilometer\n", game.current_fuel_price);

  for (int i = 0; i < 3; i++) {
    printf("\n");
    printf("Data for '%s' airplane:\n", airplane_data[i].name);
    printf("  Capacity         : %d passengers\n", airplane_data[i].capacity);
    printf("  Purchase price   : $%d\n", airplane_data[i].price);
    printf("  Upkeep cost      : $%d\n", airplane_data[i].upkeep_cost);
    printf("  Fuel consumption : %d%%\n", 
      (int)(airplane_data[i].fuel_consumption_factor * 100));
  }
}



static void function_sell(int argc, const char *argv[])
{
  if (argc != 2) {
    printf("Please specify airplane ID!\n");
    return;
  }

  int airplane_index = atoi(argv[1]);
  airplane_index--; /* ID to index */
  if (airplane_index < 0 || airplane_index >= AIRPLANE_MAX) {
    printf("Invalid airplane ID!\n");
    return;
  }

  if (false == airplane[airplane_index].exists) {
    printf("Airplane #%d does not exist!\n", airplane_index + 1);
    return;
  }

  if (false == airplane[airplane_index].parked) {
    printf("Airplane #%d is in operation (not parked)!\n", airplane_index + 1);
    return;
  }

  airplane[airplane_index].exists = false;
  game.cash += airplane_data[airplane[airplane_index].type].price;

  printf("Airplane #%d sold for $%d.\n", airplane_index + 1,
    airplane_data[airplane[airplane_index].type].price);
  game_cycle();
}



static void function_wait(int argc, const char *argv[])
{
  game_cycle();
}



#ifdef DEBUG_COMMAND
static void function_debug(int argc, const char *argv[])
{
  printf("Raw City Data:\n");
  for (int i = 0; i < CITY_MAX; i++) {
    printf("%s @ %05hu,%05hu, Size: %d, Stinginess: %d\n",
      city[i].name, city[i].x, city[i].y, city[i].size, city[i].stinginess);
  }

  printf("\n");
  printf("Distances:\n");
  printf("     |");
  for (int i = 0; i < CITY_MAX; i++) {
    printf(" %s |", city[i].name);
  }
  printf("\n");

  for (int i = 0; i < CITY_MAX; i++) {
    printf(" %s |", city[i].name);
    for (int j = 0; j < CITY_MAX; j++) {
      printf("%5d|", city_distance(i, j));
    }
    printf("\n");
  }

  printf("\n");
  printf("Nominal Fuel Price: %d\n", game.nominal_fuel_price);
}
#endif /* DEBUG_COMMAND */



static void game_fluctuate(uint32_t *value,
  int32_t variance_start, int32_t variance_end,
  uint32_t absolute_minimum, uint32_t absolute_maximum)
{
  /* Conversion to signed integers to handle negative values. */
  int32_t variance = (random() % ((variance_end - variance_start) + 1));
  variance += variance_start;
  int32_t new_value = *value;
  new_value += variance;
  if (new_value < (int32_t)absolute_minimum) {
    new_value = absolute_minimum;
  } else if (new_value > (int32_t)absolute_maximum) {
    new_value = absolute_maximum;
  }
  *value = new_value;
}



static void game_cycle(void)
{
  /* Fly airplanes according to routes. */
  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (airplane[i].exists) {
      if (! airplane[i].parked) {

        /* Calculate passengers willing to fly! */
        int distance = city_distance(airplane[i].src, airplane[i].dst);
        int demand = city[airplane[i].src].demand[airplane[i].dst];

        /* Nominal ticket price is based on having a full flight with a
           medium size airplane with the nominal fuel price
           giving exactly zero profits. */
        double nominal_price = ((distance * game.nominal_fuel_price) /
          airplane_data[AIRPLANE_TYPE_MEDIUM].capacity);

        /* The stinginess of the city gives an additional factor. */
        double stinginess = city[airplane[i].src].stinginess / 32.0;

        /* Passengers are divided into five groups of different size. */
        int group[5];
        group[0] = demand / 20; /*  5% */
        group[1] = demand / 5;  /* 20% */
        group[2] = demand / 2;  /* 50% */
        group[3] = demand / 5;  /* 20% */
        group[4] = demand / 20; /*  5% */

        /* The five groups accepts different prices. */
        double accepts_price[5];
        accepts_price[0] = nominal_price * 0.70 * stinginess;
        accepts_price[1] = nominal_price * 0.95 * stinginess;
        accepts_price[2] = nominal_price * 1.05 * stinginess;
        accepts_price[3] = nominal_price * 1.15 * stinginess;
        accepts_price[4] = nominal_price * 1.30 * stinginess;

#ifdef DEBUG_DEMAND
        printf("%s -> %s, Nominal: $%.2f, Stinginess: %.2f\n",
          city[airplane[i].src].name, city[airplane[i].dst].name,
          nominal_price, stinginess);
        for (int j = 0; j < 5; j++) {
          printf("  Group #%d of %d people accepts anything below $%.0f\n",
            j, group[j], accepts_price[j]);
        }
#endif /* DEBUG_DEMAND */

        int passengers = 0;
        for (int j = 0; j < 5; j++) {
          if (airplane[i].ticket_price < accepts_price[j]) {
            passengers += group[j];
          }
        }

        if (passengers > airplane_data[airplane[i].type].capacity) {
          passengers = airplane_data[airplane[i].type].capacity;
        }

        printf("Airplane #%d flew %d passenger%s from %s to %s\n", i + 1,
          passengers, (passengers == 1) ? "" : "s",
          city[airplane[i].src].name, city[airplane[i].dst].name);

        /* Swap source and destination. */
        int swap = airplane[i].src;
        airplane[i].src = airplane[i].dst;
        airplane[i].dst = swap;

        /* Generate cash from ticket prices. */
        game.cash += passengers * airplane[i].ticket_price;

        /* Subtract fuel price. */
        game.cash -= distance * ((double)game.current_fuel_price *
          airplane_data[airplane[i].type].fuel_consumption_factor);
      }
      /* Subtract upkeep price for airplane, even when parked. */
      game.cash -= airplane_data[airplane[i].type].upkeep_cost;
    }
  }

  /* Fluctuate traveling demand. */
  for (int i = 0; i < CITY_MAX; i++) {
    for (int j = 0; j < CITY_MAX; j++) {
      if (i == j) {
        continue;
      }

      uint32_t variance = city[i].size + city[j].size;
      variance += random() % 64;
      variance /= 10;
      game_fluctuate(&city[i].demand[j], -variance, variance, 0, 1000);
    }
  }

  /* Price/cost fluctuations. */
  game_fluctuate(&game.current_fuel_price, -1, 1, 3, 10);
  game_fluctuate(&airplane_data[0].upkeep_cost, -20, 20, 200, 1000);
  game_fluctuate(&airplane_data[1].upkeep_cost, -50, 50, 500, 2000);
  game_fluctuate(&airplane_data[2].upkeep_cost, -250, 250, 2000, 10000);
  game_fluctuate(&airplane_data[0].price, -500, 500, 20000, 100000);
  game_fluctuate(&airplane_data[1].price, -1000, 1000, 50000, 300000);
  game_fluctuate(&airplane_data[2].price, -5000, 5000, 100000, 800000);

  /* Check and possibly warn about balance. */
  if (game.cash < 0) {
    if (game.days_in_the_red >= GAME_RED_DAYS_MAX) {
      printf("Game over! You went bankrupt...\n");
      game.loop_running = false;
      return;
    }
    printf("WARNING! Cash in the red, you have %d day%s left resolve this.\n",
      GAME_RED_DAYS_MAX - game.days_in_the_red,
      (GAME_RED_DAYS_MAX - game.days_in_the_red == 1) ? "" : "s");
    game.days_in_the_red++;
  } else {
    game.days_in_the_red = 0;
  }

  if (game.cash >= GAME_CASH_GOAL) {
    printf("Congratulations! You reached the goal of %d$ in %d days.\n",
        GAME_CASH_GOAL, game.day);
    game.loop_running = false;
    return;
  }

  game.day++;
}



static void game_init(void)
{
  srandom(time(NULL));
  city_generate();

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    airplane[i].exists = false;
  }

  game.day = 1;
  game.days_in_the_red = 0;
  game.cash = GAME_CASH_START;
  game.current_fuel_price = 4 + (random() % 4);
  game.nominal_fuel_price = game.current_fuel_price;
}



static char *command_arg_completion_city(const char *text, int state)
{
  static int index;

  if (state == 0) {
    index = 0;
  }

  while (index < CITY_MAX) {
    if (0 == strncmp(city[index].name, text, strlen(text))) {
      return strdup(city[index++].name);
    }
    index++;
  }

  return NULL;
}



static char *command_arg_completion_airplane_id(const char *text, int state)
{
  static int index;
  static char airplane_id_string[AIRPLANE_MAX][4];
  static int airplanes;

  if (state == 0) {
    index = 0;
    airplanes = 0;
    /* Generate the list of possible numbers as strings. */
    for (int i = 0; i < AIRPLANE_MAX; i++) {
      if (airplane[i].exists) {
        snprintf(airplane_id_string[i], sizeof(airplane_id_string[i]),
          "%d", i + 1);
        airplanes++;
      }
    }
  }

  if (0 == airplanes) {
    return NULL;
  }

  while (index < AIRPLANE_TYPE_MAX) {
    if (0 == strncmp(airplane_id_string[index], text, strlen(text))) {
      return strdup(airplane_id_string[index++]);
    }
    index++;
  }

  return NULL;
}



static char *command_arg_completion_airplane_type(const char *text, int state)
{
  static int index;

  if (state == 0) {
    index = 0;
  }

  while (index < AIRPLANE_TYPE_MAX) {
    if (0 == strncmp(airplane_data[index].name, text, strlen(text))) {
      return strdup(airplane_data[index++].name);
    }
    index++;
  }

  return NULL;
}



static char *command_completion(const char *text, int state)
{
  static int index;

  if (state == 0) {
    index = 0;
  }

  while (index < COMMANDS_MAX) {
    if (0 == strncmp(commands[index].name, text, strlen(text))) {
      return strdup(commands[index++].name);
    }
    index++;
  }

  return NULL;
}



static char **command_completion_entry(const char *text, int start, int end)
{
  rl_attempted_completion_over = 1;
  if (start == 0) {
    /* Command part. */
    return rl_completion_matches(text, command_completion);

  } else {
    /* Arguments part. */
    for (int i = 0; i < COMMANDS_MAX; i++) {
      if (0 == strncmp(commands[i].name, rl_line_buffer,
                strlen(commands[i].name))) {
        /* Found the command entered... */
        int arg_no = -1;
        bool saw_space = false;
        for (char *p = rl_line_buffer; *p != '\0'; p++) {
          if (*p == ' ') {
            if (! saw_space) {
              arg_no++;
              saw_space = true;
            }
          } else {
            saw_space = false;
          }
        }
        /* ...Found the argument number... */
        if (arg_no < COMMAND_ARG_MAX) {
          command_arg_type_t type = commands[i].arg[arg_no];
          command_arg_completion_function_t function = 
            command_arg_data[type].function;
          if (function != NULL) {
            /* ...Call the specific function. */
            return rl_completion_matches(text, function);
          }
        }
        break;
      }
    }

    return NULL;
  }
}



static bool command_function_call(char *command)
{
  const char *argv[COMMAND_ARG_MAX + 1]; /* Includes the command name. */

  /* Split command into argc and argv. */
  int argc = 0;
  char *token = strtok(command, " ");
  do {
    if (token != NULL) {
      argv[argc] = token;
      argc++;
      if (argc == COMMAND_ARG_MAX + 1) {
        break;
      }
    }
    token = strtok(NULL, " ");
  } while (token != NULL);

  /* Attempt to call matching command. */
  if (argc == 0) {
    return false;
  }
  for (int i = 0; i < COMMANDS_MAX; i++) {
    if (0 == strncmp(commands[i].name, argv[0],
              strlen(commands[i].name))) {
      (commands[i].function(argc, argv));
      return true;
    }
  }

  printf("Unknown command: %s\n", command);
  return false;
}



int main(int argc, const char *argv[])
{
  char *command;
  char prompt[32];

  printf("*** AIRLINE MANAGEMENT SIMULATOR ***\n");
  printf("Reach the goal of %d$\n", GAME_CASH_GOAL);
  printf("Type 'help' or use tab completion.\n");

  game_init();

  rl_attempted_completion_function = command_completion_entry;

  game.loop_running = true;
  while (game.loop_running) {
    snprintf(prompt, sizeof(prompt), "[%u] $%"PRIi64" > ", game.day, game.cash);
    command = readline(prompt);
    if (command == NULL) { /* EOF */
      return 0;
    }
    if (0 == strlen(command)) {
      continue;
    }

    add_history(command);
    command_function_call(command);
    free(command);
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 05/02-2022, Article Link

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

Older articles