Information wants to be free...

Amiga 500 with the Framemeister

Here is how I connected the Amiga 500 to the Micomsoft XRGB-mini Framemeister to be able to get color graphics on a modern display through HDMI.

The tricky part is getting a "DB23" connector, but I ended up using a regular DB25 connector (made from plastic) and shaving off part of it to make it fit into the Amiga 500. The mini-DIN connector on the Framemeister only accepts composite sync (not separate horizontal and vertical which is more common) but this is available on one of the pins on the Amiga. However, it is a 5V TTL level signal which is a little bit too hot so it's recommended to reduce this with a resistor. I ended up using two 360 Ohm resistors in series at 720 Ohm since that is what I found in stock at the moment. Some diagrams I found online claim that ground should be taken from pin 16 from the Amiga, but this did NOT work for me. Pin 19 which is composite video ground worked fine.

Here is the cable pin out:

|----------------|-------|---------|--------------|
|                | Amiga |         | Framemeister |
| Signal         | DB23  |         | Mini-DIN     |
|----------------|-------|---------|--------------|
| Red            | 3     |         | 8            |
| Green          | 4     |         | 7            |
| Blue           | 5     |         | 6            |
| Composite Sync | 10    | 720 Ohm | 3            |
| Ground         | 19    |         | 4            |
|----------------|-------|---------|--------------|
          


When testing the connections with clip leads I got some "jail bars" (faint traces of vertical lines) on the display, but these more or less disappeared when I soldered everything together properly. Not sure if that was caused by general noise or by poor clip lead cables.

The finished cable connected:

Amiga to Framemeister connection.


Topic: Configuration, by Kjetil @ 24/07-2021, Article Link

Z80 CP/M 2.2 Emulator

After finishing a prototype of a MOS 6502 emulator I almost immediately started working on another one. This time a Zilog Z80 based one that runs CP/M 2.2 with inspiration taken from the Kaypro II computer, so I named the emulator "Kaytil".

Despite taking inspiration from the Kaypro II, it does not actually emulate any of the Kaypro II specific hardware, with the exception of the Lear Siegler ADM-3A compatible terminal emulation. But I think most Kaypro II software should work as long as it is not hardware specific and only expects the CP/M OS functionality. ADM-3A escape codes are converted directly to ANSI counterparts without going through (n)curses, which should be good enough.

To be able to run CP/M I adapted the skeletal CBIOS from Digital Research's manual to the emulator code, which is using IN/OUT instructions to communicate with each other. This, together with the reverse engineered CP/M 2.2 source from Clark A. Calkins is assembled using Digital Research's MAC macro assembler. Since the assembler is itself a CP/M program, I have run this in YAZE, which is another popular Z80 CP/M emulator.

Note that the Z80 emulation is not complete, but simply good enough, so some functionality is missing. I have made sure that it passes all the ZEXDOC tests for documented instruction flags, but I have made no effort so support the undocumented flags required to pass the ZEXALL tests. I also think the emulator is quite slow compared to others, probably due to ineffective O(n) instruction lookup and passing of data through functions instead of using global variables. In order for some games to run correctly, the speed is also artificially slowed down by using an interval timer, so it kind of matches that of a real Kaypro II. Some of these things can be adjusted by compile time defines though.

For storage, the emulator operates around on IBM 3740 8-inch floppy disk images. These can easily be managed with the Cpmtools package, which can create new images (mkfs.cpm) or copy files to and from them (cpmcp) from the host system. A total of 4 floppy drives (A, B, C & D) are emulated and can each contain its own image. By default the images are "read only" so any changes are not written back to the disk image file, but this can be overridden with the command line arguments.

Here are some games I have been able to run:
* Ladder (Yahoo Software, Kaypro II Version)
* Aliens (Yahoo Software, Kaypro II Version)
* CatChum (Yahoo Software, Kaypro II Version)
* Colossal Cave Adventure (Michael Goetz, Kaypro II Version)
* Sargon Chess (Hayden Books, CP/M Version)
* Deadline (Infocom, CP/M Version)
* Zork I (Infocom, CP/M Version)
* Zork II (Infocom, CP/M Version)
* Zork III (Infocom, CP/M Version)
* Nemesis (SuperSoft, CP/M Version)

The initial version can be gotten here but I have also put the source code on here for possible further development.

Finally, some screenshots of different games:

Aliens

CatChum

Deadline

Ladder

Sargon Chess


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

Retrobright Experiment

I finally got around to trying the Retrobright technique to remove yellowing. You need UV light, so doing this in Norway is a bit tricky because the weather is quite unpredictable and we do not have a lot of sun. Also, the Hydrogen peroxide (H2O2) needed is expensive and only comes in maximum 6% solution. I have an old computer mouse which was very yellowed and a perfect candidate for this experiment due to its small size.

There are several different methods and recipes to use for Retrobrighting, but I chose the plastic wrap method and using only H2O2 and corn starch. I put the 6% H2O2 into a cup and gradually added corn starch until I thought the mixture was thick enough. Afterwards I poured the mixture on the plastic wrap, put the yellow part of the mouse on the mixture and wrapped it together.

It was supposed to be a sunny day so I put the wrapped mouse outside at 08:30 in the morning, and then retrieved it at 18:30 in the evening, so it got around 10 hours of sunlight in total. All in all I was quite satisfied with the result.

Here is a picture showing the before and after and the different stages, you may notice the small bubbles appearing which is a sign the process is working.

Retrobright steps of Brother Mouse


Topic: Repair, by Kjetil @ 19/06-2021, Article Link

Terminal Mode Commodore 64 Emulator

Just for fun I have made a primitive Commodore 64 emulator, or more correctly mostly a 6502 CPU emulator that happens to run the Commodore 64 ROMs with just a few tweaks. It was mostly to learn about the 6502 CPU itself, so if you actually need a proper emulator, then use VICE instead! The goal was to be able to load and run the Commodore BASIC ROM, which it does, in a Linux terminal.

Anyway, I am releasing the source code under the MIT license and it can be downloaded here.

The 6502 CPU is fully emulated and even supports decimal mode, although only with NMOS (not CMOS) compatibility, so the N, V and Z flags are not set correctly. Nevertheless it passes the excellent test suite made by Klaus Dormann, and even some of the tests made by Wolfgang Lorenz.

To support the C64 architecture, some simple memory bank switching support has been implemented, and hooks are placed when reading and writing to certain memory locations to capture the user input and output. The emulator blocks on input when waiting for a keyboard press, which is hugely beneficial to CPU load. I thought about using curses, but this makes things more complicated so the emulator sets the terminal into canonical mode (with no echo) for the BASIC editor to work instead.

To find the ROMs the emulator assumes that you have VICE installed from before, which puts these at "/usr/lib64/vice/C64/". The location can be changed by editing a #define if they happen to be elsewhere.

Here is what it looks like running in XTerm, and you can also see the CPU disassembly/trace and memory dump functionality embedded into the emulator:

Terminal Mode Commodore 64 Emulator


Topic: Open Source, by Kjetil @ 01/06-2021, Article Link

FreeRTOS support for GR-SAKURA

I have managed to get FreeRTOS running on the GR-SAKURA board. Currently it's just a demo that blinks the LEDs, but maybe I will improve on that later. Anyway, this makes it possible to do multitasking. It is based code from the other FreeRTOS demos and some of the stuff from my earlier GR-SAKURA projects.

I started on this a while ago so the code is built around version 10.0.0 of FreeRTOS, but maybe it will work on newer versions as well.

Download the code from here and unpack the "GR-SAKURA" directory alongside the others into the "Demos" directory of FreeRTOS. Compile it by running "make" from that directory, but you will need the GCC toolchain for the Renesas RX CPU installed first.

I also finally got hold of the excellent Segger J-Link debug probe which makes it possible to use GDB ("rx-elf-gdb" from the RX GCC toolchain) to connect to the GR-SAKURA JTAG port, which I have soldered on:


Topic: Open Source, by Kjetil @ 08/05-2021, Article Link

Multiplayer Tank Game

Here is another game project I have had lying around for many years but never finished, until now that is. The idea was to make a graphical multiplayer game and the result is this game which lets up to 6 players drive a tank and shoot at each other. No emphasis on any advanced functionality, I just wanted to get this done and released. Note that a lot of trust is placed on the client here, which makes things simpler, but also makes it pretty easy to cheat by modifying the code.

The game is written in C and uses version 2 of the SDL library for graphics and sound. To make the networked multiplayer portion work, it uses the ENet reliable UDP networking library. It was created to be used on Linux, and I have not tried compiling and running it on other platforms. It probably works best on a local LAN, but I did test it through the Internet as well.

A screenshot:

Tank game screenshot


The map data is stored as a text file that looks like this, easily edited:

################################################################
#      #                                                       #
#      #                                                       #
#      #                                                       #
#   T  #                                                    T  #
#      #                                                       #
#      #                             T                         #
#      #      #########                                 ########
#                                                              #
#                                                              #
#                                                              #
#                                                              #
#             #                                                #
#             #                                                #
#             #                T                               #
#             #                              ###########       #
#             #                              #                 #
#             #                              #                 #
#             #                              #                 #
#             #                              #                 #
#             #            #########                           #
#             #            #########                           #
#             #            #########                           #
#     T       #            #########                           #
#             #            #########              T            #
#             #                                                #
#             #                                                #
#             #                                                #
#             #                                                #
#             #                                                #
#             #                                                #
#             #                T     #                         #
#             #                      #                         #
#             #                      #                         #
#             #                      ###################       #
#             #                                                #
#             #        #######                                 #
#                                                              #
#                                                              #
#                                                              #
########                                                #      #
#                                        T              #      #
#                                                       #      #
#                                                       #      #
#   T                                                   #   T  #
#                                                       #      #
#                                                       #      #
################################################################
          


The source code is released under the MIT license and can be downloaded here.

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

DVB-T USB Stick Playback

I got one of those DVB-T tuners a while ago, in the form of a USB stick, specifically this one:

15a4:1001 Afatech Technologies, Inc. AF9015/AF9035 DVB-T stick
          


However, it took me many years to finally get this working on Linux after a lot of trial and error. There are two important things I discovered during this time. The first is that MPlayer is quite bad at handling MPEG TS files (or streams). The second is that, at least in my case with this particular stick, the /dev/dvb/adapter0/dvr0 device would not work as advertised.

The solution I ended up with is w_scan for scanning, v4l-utils for tuning and ffmpeg for playback.

Use these steps to scan and generate the channels list:

w_scan --output-initial > channels.conf
dvbv5-scan --input-format=CHANNEL --output=dvb_channel.conf channels.conf
          


Then use these steps for playback:

mkfifo /tmp/dvb.ts
dvbv5-zap -c dvb_channel.conf "name-of-the-channel" -o /tmp/dvb.ts &
ffplay /tmp/dvb.ts
          


I found that TV (video and audio) playback works OK, but radio (audio only) will buffer for a very long time before playback starts, so not very convenient.

Topic: Configuration, by Kjetil @ 27/03-2021, Article Link

Space Ship Simulator in OpenGL

I finally finished a project I had suspended many years ago when I was looking into OpenGL. At that time my solution had problems that I now later have discovered was related to the dreaded Gimbal lock caused by the use of Euler angles. The new solution uses rotation matrices directly. It uses the GLUT library which is kind of deprecated, but still very widely available and convenient for simpler OpenGL programs and demos like this one.

This space ship simulator is quite simple, but allows movement in all directions. When the program is started, the "universe" is populated with some randomly generated (really small) planets which can be navigated around. Use W/S to pitch, A/S to yaw, Z/C to roll and R/F to increase/decrease thrusters.

Here is the code, compile it like so: gcc -o space space.c -lGL -lGLU -lglut -lm -Wall

#include <GL/glut.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>

#define WORLD_SIZE 25 /* Distance to fake walls from origin. */

#define GRID_SPACING 5
#define GRID_START ((0 - WORLD_SIZE) + GRID_SPACING)
#define GRID_END (WORLD_SIZE - GRID_SPACING)
#define GRID_SIZE ((GRID_END - GRID_START) / GRID_SPACING)

#define PLANETS_MAX 10
#define PLANETS_CHANCE (GRID_SIZE * GRID_SIZE * GRID_SIZE) / PLANETS_MAX



typedef struct vector_s {
  float x;
  float y;
  float z;
} vector_t;

typedef struct planet_s {
  vector_t pos;
  float size;
  float color_r;
  float color_g;
  float color_b;
} planet_t;

typedef struct planet_distance_s {
  int planet_index;
  float distance;
} planet_distance_t;



static int camera_mode = 0;
static float ship_speed = 0.0;
static int old_mouse_x;
static int old_mouse_y;

static planet_t planets[PLANETS_MAX];
static int planets_count = 0;

static vector_t ship_forward_vector = {1,0,0};
static vector_t ship_up_vector      = {0,1,0};
static vector_t ship_right_vector   = {0,0,1};
static vector_t ship_position       = {0,0,0};



static vector_t vector_normalize(vector_t v)
{
  float magnitude;
  vector_t normalized;

  magnitude = sqrt((v.x * v.x) + (v.y * v.y) + (v.z * v.z));
  normalized.x = v.x / magnitude;
  normalized.y = v.y / magnitude;
  normalized.z = v.z / magnitude;

  return normalized;
}

static vector_t vector_cross_product(vector_t a, vector_t b)
{
  vector_t cross;

  cross.x = (a.y * b.z) - (a.z * b.y);
  cross.y = (a.z * b.x) - (a.x * b.z);
  cross.z = (a.x * b.y) - (a.y * b.x);

  return cross;
}

static vector_t vector_multiply(vector_t v, float f)
{
  vector_t product;

  product.x = v.x * f;
  product.y = v.y * f;
  product.z = v.z * f;

  return product;
}

static vector_t vector_sum(vector_t a, vector_t b)
{
  vector_t sum;

  sum.x = a.x + b.x;
  sum.y = a.y + b.y;
  sum.z = a.z + b.z;

  return sum;
}

static vector_t vector_difference(vector_t a, vector_t b)
{
  vector_t difference;

  difference.x = a.x - b.x;
  difference.y = a.y - b.y;
  difference.z = a.z - b.z;

  return difference;
}

static float vector_distance(vector_t a, vector_t b)
{
  return sqrt(((a.x - b.x) * (a.x - b.x)) +
              ((a.y - b.y) * (a.y - b.y)) +
              ((a.z - b.z) * (a.z - b.z)));
}



static void ship_pitch(float angle)
{
  ship_forward_vector = vector_normalize(
    vector_sum(
      vector_multiply(ship_forward_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_up_vector, sin(angle * (M_PI / 180)))));

  ship_up_vector = vector_cross_product(
    ship_forward_vector, ship_right_vector);

  ship_up_vector = vector_multiply(ship_up_vector, -1.0);
}

static void ship_yaw(float angle)
{
  ship_forward_vector = vector_normalize(
    vector_difference(
      vector_multiply(ship_forward_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_right_vector, sin(angle * (M_PI / 180)))));

  ship_right_vector = vector_cross_product(
    ship_forward_vector, ship_up_vector);
}

static void ship_roll(float angle)
{
  ship_right_vector = vector_normalize(
    vector_sum(
      vector_multiply(ship_right_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_up_vector, sin(angle * (M_PI / 180)))));

  ship_up_vector = vector_cross_product(
    ship_forward_vector, ship_right_vector);

  ship_up_vector = vector_multiply(ship_up_vector, -1.0);
}

static void ship_advance(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_forward_vector, distance));
}

#ifdef DEBUG_MODE
static void ship_ascend(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_up_vector, distance));
}

static void ship_strafe(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_right_vector, distance));
}
#endif /* DEBUG_MODE */



static void idle()
{
  if (ship_speed != 0) {
    ship_advance(ship_speed);
  }

  glutPostRedisplay();

#ifdef DEBUG_MODE
  fprintf(stderr, "Speed: %.2f, X: %.2f, Y: %.2f, Z: %.2f\n",
    ship_speed, ship_position.x, ship_position.y, ship_position.z);
#endif /* DEBUG_MODE */
}

static void draw_ship(void)
{
  float rotation[16];

  glTranslatef(ship_position.x, ship_position.y, ship_position.z);

  rotation[0]  = ship_right_vector.x;
  rotation[1]  = ship_right_vector.y;
  rotation[2]  = ship_right_vector.z;
  rotation[3]  = 0;
  rotation[4]  = ship_up_vector.x;
  rotation[5]  = ship_up_vector.y;
  rotation[6]  = ship_up_vector.z;
  rotation[7]  = 0;
  rotation[8]  = ship_forward_vector.x;
  rotation[9]  = ship_forward_vector.y;
  rotation[10] = ship_forward_vector.z;
  rotation[11] = 0;
  rotation[12] = 0;
  rotation[13] = 0;
  rotation[14] = 0;
  rotation[15] = 1;
  glMultMatrixf(rotation);

  glScalef(0.5, 0.5, 0.5);
  glColor3f(0.9, 0.9, 0.9);

  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(-1.0, 1.0, 0.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(-1.0, 1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, 1.0, 0.0);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
}

#ifdef DEBUG_MODE
static void draw_axis_indicator(void)
{
  /* Draw it at origin. */
  GLfloat old_line_width;
  glGetFloatv(GL_LINE_WIDTH, &old_line_width);
  glLineWidth(3.0);

  glBegin(GL_LINES);
    /* Red X-Axis */
    glColor3f(1, 0, 0);
    glVertex3f(0, 0, 0);
    glVertex3f(1, 0, 0); 
    /* Green Y-Axis */
    glColor3f(0, 1, 0);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 1, 0);
    /* Blue Z-Axis */
    glColor3f(0, 0, 1);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 0, 1); 
  glEnd();

  glLineWidth(old_line_width);
}
#endif /* DEBUG_MODE */

static int planet_distance_compare(const void *p1, const void *p2)
{
  return ((planet_distance_t *)p1)->distance <
         ((planet_distance_t *)p2)->distance;
}

static void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glLoadIdentity();

  if (camera_mode == 0) {
    /* Inside ship. */
    vector_t view_point;
    view_point = vector_sum(ship_position, ship_forward_vector);

    gluLookAt(ship_position.x, ship_position.y, ship_position.z,
              view_point.x, view_point.y, view_point.z,
              ship_up_vector.x, ship_up_vector.y, ship_up_vector.z);
  } else {
    /* Outside ship, a static look towards origin. */
    glRotatef(45.0, 1.0, 0.0, 0.0);
    glRotatef(120.0, 0.0, 1.0, 0.0);
    glTranslatef(WORLD_SIZE / 2, 0 - (WORLD_SIZE / 2), WORLD_SIZE / 2);
  }

  /* Draw floor/roof/walls, which are actually big cubes! */
  glColor3f(0.0, 0.0, 0.0);

  glPushMatrix();
    glTranslatef(0.0, -WORLD_SIZE, 0.0);
    glScalef(WORLD_SIZE * 2, 0.1, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.5, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, WORLD_SIZE, 0.0);
    glScalef(WORLD_SIZE * 2, 0.1, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.0, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(WORLD_SIZE, 0.0, 0.0);
    glScalef(0.1, WORLD_SIZE * 2, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.0, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(-WORLD_SIZE, 0.0, 0.0);
    glScalef(0.1, WORLD_SIZE * 2, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.5, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, 0.0, WORLD_SIZE);
    glScalef(WORLD_SIZE * 2, WORLD_SIZE * 2, 0.1);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.0, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, 0.0, -WORLD_SIZE);
    glScalef(WORLD_SIZE * 2, WORLD_SIZE * 2, 0.1);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.5, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  /* Calculate distance from ship to planets, because they need to be
     drawn/rendered from the furthest away first. */
  planet_distance_t distance[PLANETS_MAX];
  for (int i = 0; i < planets_count; i++) {
    distance[i].planet_index = i;
    distance[i].distance = vector_distance(ship_position, planets[i].pos);
  }
  qsort(distance, planets_count, sizeof(planet_distance_t),
    planet_distance_compare);

  /* Draw planets. */
  for (int i = 0; i < planets_count; i++) {
    int n = distance[i].planet_index;
    glPushMatrix();
      GLUquadric *quadric = gluNewQuadric();
      if (quadric != 0) {
        glTranslatef(planets[n].pos.x, planets[n].pos.y, planets[n].pos.z);
        glColor3f(planets[n].color_r, planets[n].color_g, planets[n].color_b);
        gluSphere(quadric, planets[n].size, 20, 20);
        gluDeleteQuadric(quadric);
      }
    glPopMatrix();
  }
    
  if (camera_mode != 0) {
    glPushMatrix();
      draw_ship();
    glPopMatrix();
  }

#ifdef DEBUG_MODE
  glPushMatrix();
    draw_axis_indicator();
  glPopMatrix();
#endif /* DEBUG_MODE */

  glFlush();
  glutSwapBuffers();
}

static void reshape(int w, int h)
{
  glViewport(0, 0, (GLsizei)w, (GLsizei)h);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glFrustum(-1.0, 1.0, -1.0, 1.0, 1.5, 150.0);
  glMatrixMode(GL_MODELVIEW);
}

static void keyboard(unsigned char key, int x, int y)
{
  switch (key) {
  case 's':
    ship_pitch(-1.0);
    break;
  case 'w':
    ship_pitch(1.0);
    break;
  case 'd':
    ship_yaw(-1.0);
    break;
  case 'a':
    ship_yaw(1.0);
    break;
  case 'z':
    ship_roll(-1.0);
    break;
  case 'c':
    ship_roll(1.0);
    break;
  case 'r':
    ship_speed += 0.01;
    break;
  case 'f':
    ship_speed -= 0.01;
    break;
#ifdef DEBUG_MODE
  case 't':
    ship_advance(0.1);
    break;
  case 'g':
    ship_advance(-0.1);
    break;
  case 'y':
    ship_ascend(0.1);
    break;
  case 'h':
    ship_ascend(-0.1);
    break;
  case 'u':
    ship_strafe(0.1);
    break;
  case 'j':
    ship_strafe(-0.1);
    break;
  case 'x':
    camera_mode = (camera_mode) ? 0 : 1;
    break;
#endif /* DEBUG_MODE */
  case 'q':
    exit(0);
    break;
  }
}

static void motion(int x, int y)
{
  if (x > old_mouse_x) {
    ship_yaw(1.0);
  } else if (x < old_mouse_x) {
    ship_yaw(-1.0);
  }

  if (y > old_mouse_y) {
    ship_pitch(-1.0);
  } else if (y < old_mouse_y) {
    ship_pitch(1.0);
  }

  old_mouse_x = x;
  old_mouse_y = y;
}

void generate_planets(void)
{
  for (int x = GRID_START; x < GRID_END; x += GRID_SPACING) {
    for (int y = GRID_START; y < GRID_END; y += GRID_SPACING) {
      for (int z = GRID_START; z < GRID_END; z += GRID_SPACING) {
        if (x == 0 && y == 0 && z == 0) {
          /* Ignore origin. */
          continue;
        }

        if ((rand() % PLANETS_CHANCE) == 0) {
          planets[planets_count].pos.x = (x - 2) + (rand() % 4);
          planets[planets_count].pos.y = (y - 2) + (rand() % 4);
          planets[planets_count].pos.z = (z - 2) + (rand() % 4);
          planets[planets_count].size = 
            ((rand() % ((GRID_SPACING * 10) - 1)) / 20.0) + 0.1;
          planets[planets_count].color_r = (rand() % 11) / 10.0;
          planets[planets_count].color_g = (rand() % 11) / 10.0;
          planets[planets_count].color_b = (rand() % 11) / 10.0;

#ifdef DEBUG_MODE
          printf("X=%.1f, Y=%.1f, Z=%.1f, Size=%.1f, Col=%.1f;%.1f;%.1f\n", 
            planets[planets_count].pos.x,
            planets[planets_count].pos.y,
            planets[planets_count].pos.z,
            planets[planets_count].size,
            planets[planets_count].color_r,
            planets[planets_count].color_g,
            planets[planets_count].color_b);
#endif /* DEBUG_MODE */

          if ((planets_count + 1) >= PLANETS_MAX) {
            return;
          } else {
            planets_count++;
          }
        }
      }
    }
  }
}

int main(int argc, char *argv[])
{
  srand(time(NULL));
  generate_planets();
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
  glutInitWindowSize(800, 800);
  glutInitWindowPosition(100, 100);
  glutCreateWindow(argv[0]);
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glShadeModel(GL_FLAT);
  glutDisplayFunc(display);
  glutReshapeFunc(reshape);
  glutIdleFunc(idle);
  glutKeyboardFunc(keyboard);
  glutMotionFunc(motion);
  glutMainLoop();
  return 0;
}
          


Here is a screenshot of what it looks like, just some "planets" that looks like spheres:

Space Ship Simulator Screenshot


Topic: Scripts and Code, by Kjetil @ 05/03-2021, Article Link

Turning Frog VL6180 Upgrade

Many years ago I got a soldering kit, the "ELENCO 21-882" which is a "frog robot" with two motors that reacts to sound. I had been thinking about ways to upgrade it, and I finally got around to doing that by controlling it with a Arduino Pro Mini and a VL6180 sensor to measure distance.

The original sound based control works by clocking a "4017 Decade Counter" IC with the input from a electret microphone. The easiest way to hijack the control is to remove this IC from the socket and wire the Arduino into it.

I used the following connections:
Arduino RAW <-> Socket pin 16 (VDD, 9V supply from battery.)
Arduino GND <-> Socket pin 8 (VSS)
Arduino D10 <-> Socket pin 1 (Output, diode for left motor.)
Arduino D11 <-> Socket pin 7 (Output, diode for right motor.)
Arduino D12 <-> Socket pin 2 (Output, diodes for both motors.)

Cobbled together it looks like this:

Frog Upgrade

The FTDI is only used for uploading the program and can be removed of course.

The Arduino Sketch code is very simple:

#include "Adafruit_VL6180X.h"

static Adafruit_VL6180X vl = Adafruit_VL6180X();
static int keep_going = 0;

void setup() {
  pinMode(10, OUTPUT); // Left motor
  pinMode(11, OUTPUT); // Right motor
  pinMode(12, OUTPUT); // Both motors
  pinMode(13, OUTPUT); // On-board Arduino LED
  vl.begin();
}

void loop() {
  uint8_t range = vl.readRange();
  uint8_t status = vl.readRangeStatus();

  if ((status == VL6180X_ERROR_NONE) && (range < 200)) {
    // Go left
    digitalWrite(12, LOW);
    digitalWrite(10, HIGH);
    keep_going = 10; // Keep going to finish the turn...
  } else {
    if (keep_going > 0) {
      keep_going--;
    } else {
      // Go forward
      digitalWrite(10, LOW);
      digitalWrite(12, HIGH);
    }
  }
  
  delay(50);
}
          


Topic: Scripts and Code, by Kjetil @ 13/02-2021, Article Link

USB IR REMOCON Firmware Replacement

I bought this Bit Trade One USB Infrared Remote Control Kit at an electronics store in Japan, which lets you use a standard TV remote to control a PC. While the device works on Linux, since it presents itself as a standard USB HID device, it can only be configured in Windows. But a bigger problem is that the original firmware contained some kind of bug where part of the TV remote mappings would get corrupted after power cycling.

To get around these limitations and problems I have created a new firmware replacement for the onboard PIC18F14K50 microcontroller. For maximum flexibility I decided to make something that emulates the protocol used by the Dangerous Prototypes USB IR Toy. This means the device can be used together with LIRC on Linux, which offers a lot of advanced functionality. This also moves the configuration aspect to software on Linux instead of needing to re-flash the PIC microcontroller EEPROM. Note that this device also has an IR sender, but I only implemented support for the IR receive part.

You can get the firmware here or the MPLAB project source code here. I have used Microchip's own USB stack for this project, which is licensed under the Apache license. The parts I wrote myself I consider to be released unlicensed directly into the public domain. I'm including the main part of the code here for an easy reference:

#include "system.h"

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

#include "usb.h"

#include "app_irtoy_emu.h"
#include "usb_config.h"

static uint8_t readBuffer[CDC_DATA_OUT_EP_SIZE];
static uint8_t writeBuffer[CDC_DATA_IN_EP_SIZE];

static bool inSamplingMode = false;
static uint16_t pulseHighLen = 0;
static uint16_t pulseLowLen = 0;
static uint16_t pendingPulse = 0;
static bool endOfTransmission = true;
static bool waitForEOT = false;

void samplingModeSetup(void)
{
    /* Set variables to known state. */
    pulseHighLen = 0;
    pulseLowLen = 0;
    pendingPulse = 0;
    endOfTransmission = true;
    waitForEOT = false;

    T0CON = 0b11000000; /* Enable Timer0 as 8-Bit with no scaling. */

    RCONbits.IPEN = 1; /* Enable priority levels on interrupts. */
    INTCON2bits.TMR0IP = 1; /* Make Timer0 a high priority interrupt. */
    INTCONbits.TMR0IF = 0; /* Clear Timer0 interrupt flag. */
    INTCONbits.TMR0IE = 1; /* Enable Timer0 interrupts. */
    INTCONbits.GIEH = 1; /* Make sure high priority interrupts are enabled. */
    INTCONbits.GIEL = 1; /* Make sure low priority interrupts are enabled. */
}

void samplingModeService(void)
{
    uint8_t numBytesRead;

    numBytesRead = getsUSBUSART(readBuffer, 1);
    if (numBytesRead == 1 && readBuffer[0] == 0x00) /* Reset */
    {
        /* Disable Timer0 interrupts. */
        INTCONbits.TMR0IE = 0;
        inSamplingMode = false;
        return;
    }

    INTCONbits.TMR0IE = 0;
    if (pendingPulse > 5)
    {
        writeBuffer[0] = (uint8_t)(pendingPulse / 256);
        writeBuffer[1] = (uint8_t)(pendingPulse % 256);
        putUSBUSART(writeBuffer, 2);
        pendingPulse = 0;
        endOfTransmission = false;
        waitForEOT = true;
    }

    if ((waitForEOT == true) && (endOfTransmission == true))
    {
        writeBuffer[0] = 0xFF;
        writeBuffer[1] = 0xFF;
        putUSBUSART(writeBuffer, 2);
        waitForEOT = false;
    }
    INTCONbits.TMR0IE = 1;
}

void APP_IRToyEmulationInterrupt(void)
{
    if (INTCONbits.TMR0IF == 1)
    {
        /* Check if IR sensor (inverted input) is active. */
        if (PORTCbits.RC7 == 0)
        {
            if (pulseLowLen > 5)
            {
                pendingPulse = pulseLowLen;
            }
            pulseLowLen = 0;

            pulseHighLen += 2;
        }
        else
        {
            if (pulseHighLen > 5)
            {
                pendingPulse = pulseHighLen;
                endOfTransmission = false;
            }
            pulseHighLen = 0;

            if (endOfTransmission == false)
            {
                pulseLowLen += 2;
                if (pulseLowLen >= 0xFFFE)
                {
                    pulseLowLen = 0;
                    endOfTransmission = true;
                }
            }
        }
        INTCONbits.TMR0IF = 0;
    }
}

void APP_IRToyEmulationInitialize()
{   
    line_coding.bCharFormat = 0;
    line_coding.bDataBits = 8;
    line_coding.bParityType = 0;
    line_coding.dwDTERate = 115200;

    /* I/O settings as used in orignal Bit Trade One firmware. */
    TRISB  = 0x00;
    TRISC  = 0x80;
    LATC   = 0xFF;
    ANSEL  = 0x00;
    ANSELH = 0x00;

    /* But turn off IR sender diode output, no support implemented. */
    PORTCbits.RC5 = 0;
}

void APP_IRToyEmulationTasks()
{
    /* If the USB device isn't configured yet, we can't really do anything
     * else since we don't have a host to talk to.  So jump back to the
     * top of the while loop. */
    if (USBGetDeviceState() < CONFIGURED_STATE)
    {
        return;
    }

    /* If we are currently suspended, then we need to see if we need to
     * issue a remote wakeup.  In either case, we shouldn't process any
     * keyboard commands since we aren't currently communicating to the host
     * thus just continue back to the start of the while loop. */
    if (USBIsDeviceSuspended() == true)
    {
        return;
    }

    if (inSamplingMode == true)
    {
        samplingModeService();
    }
    else
    {
        if (USBUSARTIsTxTrfReady() == true)
        {
            uint8_t numBytesRead;
            numBytesRead = getsUSBUSART(readBuffer, 1);

            if (numBytesRead == 1)
            {
                switch (readBuffer[0])
                {
                case 'S': /* IRIO Sampling Mode */
                case 's':
                    writeBuffer[0] = 'S'; /* Answer OK */
                    writeBuffer[1] = '0';
                    writeBuffer[2] = '1';
                    putUSBUSART(writeBuffer, 3);
                    samplingModeSetup();
                    inSamplingMode = true;
                    break;

                case 'V': /* Acquire Version */
                case 'v':
                    writeBuffer[0] = 'V'; /* Answer OK */
                    writeBuffer[1] = '9'; /* Hardware Version */
                    writeBuffer[2] = '9'; /* Firmware Version High */
                    writeBuffer[3] = '9'; /* Firmware Version Low */
                    putUSBUSART(writeBuffer, 4);
                    break;

                default: /* All the rest is unsupported! */
                    writeBuffer[0] = '?';
                    putUSBUSART(writeBuffer, 1);
                    break;
                }
            }
        }
    }

#if defined(USB_POLLING)
    USBDeviceTasks();
#endif
    CDCTxService();
}
          


To use it in LIRC, setup lirc_options.conf similar to the IR Toy using:

driver = irtoy
device = /dev/ttyACM0
          


Topic: Scripts and Code, by Kjetil @ 23/01-2021, Article Link

Older articles