Sorted by Squirrels.

Sunday, 27 April 2008

Like a record baby!

I've been inspired by someone or other's homebuilt spinner. I probably saw it on Make or Hack-a-day, I forget. If you aren't familiar with the aforementioned input device it's like, well you know, like an arcade game control that could be found on such classics as Tempest, Arkanoid, Cameltry and many many more. You spin it. Hence the name.

I had to have a go. I have a project stack 100 deep at the moment and no time to do anything, so I needed a quick fix, a micro-project if you will. And this seemed like the perfect candidate.

So the obligatory VCR head was pulled from a dead Matsui. This particular model had a wonderful head motor mechanism that came apart very easily and cleanly, leaving no random sprue like some I've been near. It also furnished me with convenient screw holes. The motor shaft has a metal disk at the end which was just the right size for some idle tweaking. There's something very relaxing and .. zen I suppose .. about VCR head motor mechanisms. I suggest everyone try hefting one and idly spinning away a few minutes :)

An old USB mouse was inspected and found to be of the correct vintage - ball era. The optical encoder disks were removed and the pcb ritually prepared. The left/right encoder was severed and the traces replaced with kynar wire. Did I already say that this stuff has changed my life? I did have to modify my wire strippers to nip away that micro-fine insulation and I must admit the very act made me feel quite hardcore!

So with the emitter/receiver pair free to do their thing I started to try and mount the encoder disk on the base of the motor. What a nightmare. I tried different adhesives and when the little black brolly wasn't teetering drunkenly on the shaft or resolutely embracing my fingers it was wholly and utterly eccentric. In the end I liberated one of the tiny rare earth magnets from a sneakily swiped Magnetix stick. To this I super-glued the encoder on its truncated shaft. This could then be pinged onto the end of the shaft and repositioned easily whilst still holding position. When it was rotating as centrally as I thought it could, out came the hot glue. One quick spin draged a glistening thread of glue from the gun like silk from a spider's arse. And there was the disk in place, semi-permanently fixed. Result!

To mount the spinner I chose a CD 'cake box'. I'll leave the details of mounting the motor to your imagination as I am wont to do. All I will say is knife and screws were involved, but not my friend the rocket scientist.

So with the motor mounted all that needed to be achieved was the solid mounting of the transceiver pair. Tip #2: Don't use stiff wire. I thought that this would be simple, quick and give me plenty lattitide to position the payload precisely in space with little play. Wrong. It bounced around like Katie Price on a trampoline. So I did what I always do at moments like this - reach for the Lego. A small 1x2 hinge, a 1x8 plate and some more hot glue later I had the PCB shard mounted in the right position. More hot glue saw it fixed and locked. Game on!

[Click for a much, much bigger view! Erk!]


Cuts on your fingertips are painful. Rough-edged spinning steel disks attached to a momentum powerhouse cause cuts. Thinks needed :) The rubber 'tyre' that encased the middle mouse wheel was promptly repurposed. An almost perfect fit! I was understandably quite chuffed. While in the heightened state of chuff I figured some buttons might be useful too, in order to save splitting control between the keyboard and spinner when necessary. So off to the bits box and one feisty rummage later we have control! And the old BBC Master computer has 2 less keys.

One leisurely wiring of these buttons to the mouse PCB switch pads and there we have it. One spinner. It may not be pretty, but it was a project! And it works great with Tempest :D

[Click for a much, much bigger view! Zoinks!]

Sunday, 13 April 2008

u-Fat filesystem

Not a truncated attempt at a verbal slight, but a solution to PC compatible file access for micro-controllers!

Many FAT file system implementations are, by necessity, too large to take a place in micro-controller applications. Long file names, fragmented files, the FAT tables - no wonder a 'proper' solution weighs in at many K of code and buffers. One solution is to carry out raw access to the card, but this then involves compromises when getting the data off the card onto the desktop.

My solution is to reduce weight of code by applying stringent use restrictions:

Only 16 files (potentially), named in 8.3 format, pre-allocated and contiguous.

This enables us to locate the start sector of the file and read/write to it as we wish. The files can be freely swapped between PC and micro controller application using standard operating system commands. To reach this state of nirvana all that needs to be done is format the card before copying the pre-allocated files to it.

The only external requirement for servicing the code is a function that reads sectors, using logical block addressing, from your chosen FAT device. If you look in the ACE post previously the code zip there includes a complete MMC solution.

I found a schematic on the arduino site that is almost identical to my own and comes very highly recommended. If you haven't got a card reader that you can cannibalise then edge connector works well too. It's a common way of doing it and there are many good examples around - you can find one here.

Here's the header which shows just how micro micro can be!

 /*
/ Microfat.
/
/ Developed by Charlie Robson in 2008.
/
/ This is the bare minimum* functionality required to read and write data
/ to a FAT formatted device. In this case, an MMC card.
/
/ Rather than build a fully-featured FAT implimentation this will allow
/ the user to locate sectors associated with a file on the device, and
/ read and write to them in a brute force manner. No error checking, no
/ long filenames, not even any support for fragmented files :) This is
/ raw access, we only deal with existing files.
/
/ What you do get is a very lightweight module for extremely limited
/ environments. Arduino has 2k of SRAM. Holding the sector of FAT data
/ needed to traverse a fragged file would instantly gobble up 1/4
/ of that. Enough said. Another benefit to not using the FAT is that
/ there's no need to worry about whether a device is FAT12 or 16.
/
/ To use:
/ Format a card.
/ Make a file big enough to hold all the data you expect to write.
/ Copy to the card.
/
/ With the MMC module started up, call initialise. This will cache some
/ relevant information about the card such as the sector location of the
/ root directory and data area.
/
/ Call locateFileStart, passing the name of the file which is providing
/ the backing store for your transfers, and assuming the file is found
/ in the directory you'll receive its start sector and file size.
/
/ Now feel free to read and write data to and from the sectors allocated
/ to the file. Code carefully though - stray writes can twat your card!
/
/ Enjoy :)
/
/ *Massive understatement!
*/

#ifndef __MICROFAT_H__
#define __MICROFAT_H__

// some data structures we'll be encountering on our travels.

typedef struct
{
byte bootable;
byte chsAddrOfFirstSector[3];
byte partitionType;
byte chsAddrOfLastSector[3];
uint32_t lbaAddrOfFirstSector;
uint32_t partitionLengthSectors;
}
partition_record;

typedef struct
{
byte jump[3];
char oemName[8];
uint16_t bytesPerSector;
byte sectorsPerCluster;
uint16_t reservedSectors;
byte fatCopies;
uint16_t rootDirectoryEntries;
uint16_t totalFilesystemSectors;
byte mediaDescriptor;
uint16_t sectorsPerFAT;
uint16_t sectorsPerTrack;
uint16_t headCount;

uint32_t hiddenSectors;
uint32_t totalFilesystemSectors2;
byte logicalDriveNum;
byte reserved;
byte extendedSignature;
uint32_t partitionSerialNum;
char volumeLabel[11];
char fsType[8];
byte bootstrapCode[447];
byte signature[2];
}
boot_sector;

typedef struct
{
char filespec[11];
byte attributes;
byte reserved[10];
uint16_t time;
uint16_t date;
uint16_t startCluster;
uint32_t fileSize;
}
directory_entry;


namespace microfat
{
// Cache some relevant info about the card.
//
bool initialise(byte* buffer);

// Get start sector, file size for given filename. Returns false if the file
// is not found in the directory.
//
bool locateFileStart(const char* filename, unsigned long& sector, unsigned long& size);
};


#endif // __MICROFAT_H__



Now for the meat:

#include <wprogram.h>
#include "microfat.h"
#include "mmc.h"

// Data which remains constant over one session. Re-initialise if
// the card is changed.
//
static struct
{
uint16_t sectorsPerCluster;

uint32_t rootDirSect, cluster2;

byte* buffer;
}
vars;


bool microfat::initialise(byte* buffer)
{
vars.buffer = buffer;

if (RES_OK != mmc::readSectors(vars.buffer, 0, 1))
{
return false;
}

partition_record* p = (partition_record*)&vars.buffer[0x1be];
long bootSector = p->lbaAddrOfFirstSector;

if (RES_OK != mmc::readSectors(vars.buffer, bootSector, 1))
{
return false;
}

boot_sector* b = (boot_sector*)vars.buffer;

if (BYTESPERSECTOR != b->bytesPerSector)
{
return false;
}

vars.sectorsPerCluster = b->sectorsPerCluster;
vars.rootDirSect = bootSector + b->reservedSectors + (b->fatCopies * b->sectorsPerFAT);

long dirBytes = b->rootDirectoryEntries * 32;
long dirSects = dirBytes / BYTESPERSECTOR;

if (dirBytes % BYTESPERSECTOR != 0)
{
++dirSects;
}

vars.cluster2 = vars.rootDirSect + dirSects;

return true;
}



// Get start sector for given filename.
//
// Returns false if the specified file wasn't found in the directory.
// Short filenames only. Be aware that the directory fills up with 'deleted'
// filenames. Rather than deleting too many files try a quick reformat.
//
bool microfat::locateFileStart(const char* filename, uint32_t& sector, uint32_t& size)
{
if (RES_OK == mmc::readSectors(vars.buffer, vars.rootDirSect, 1))
{
// The filenames are stored in [8][3] format. No dot, all upper case.
// Names shorter than 8 chars are padded with spaces.
//
// Cook the supplied name. fred.txt -> FRED----TXT
//
char cookedName[11];
for(int i = 0; i < 12; ++i)
{
cookedName[i] = 0x20;
}

for (int i = 0, j = 0; i < 12 && filename[i]; ++i)
{
if (filename[i] != '.')
{
// Force char to uppercase. Cheesy I know :)
//
cookedName[j] = filename[i] >= 96 ? filename[i] - 32 : filename[i];
++j;
}
else
{
// Continue cooking chars at the extension position
//
j = 8;
}
}

for (int i = 0; i < BYTESPERSECTOR; i += 32)
{
directory_entry* de = (directory_entry*)&vars.buffer[i];

// Don't match with deleted or system/volname/subdir/hidden files
//
if (de->filespec[0] != 0xe5 && (de->attributes & 0x1e) == 0 && memcmp(cookedName, de->filespec, 11) == 0)
{
sector = vars.cluster2 + ((de->startCluster-2) * vars.sectorsPerCluster);
size = de->fileSize;
return true;
}
}
}

return false;
}




Which as you can see is smaller than the header - due in no small measure to the size of the FAT structure definitions.

As it stands this code could be improved by using non-static file info structures - only one is supported right now but the change would be trivial. Also allowing the filename search to traverse the entire root directory space would allow access to more files. However I don't really see the utility in ths -wherever I've used this code I've never needed more than one file :)

As usual any Qs are welcome.

Sunday, 6 April 2008

More ACEness



Here's a more portable command-line driven utility to generate wav files ready for ACE's ears. There's a tiny enhancement that ramps up the volume during the lead-in which should prevent the kind of pants-wetting shock that I keep delivering to myself :)



/*
* Support code for ACE - the arduino cassette engine
*
* Example code to write a wav file which can be played to
* a microcontroller running the ACE software.
*
* Should be fairly portable unlike the original example.
*
* Enjoy.
*
*/

#include <iostream>
#include <fstream>

typedef unsigned char uint8_t;

// adjust these to control endiannesses.
//
static const int ENDIAN0 = 0;
static const int ENDIAN1 = 1;
static const int ENDIAN2 = 2;
static const int ENDIAN3 = 3;

static const int ENDIAN0_S = 0;
static const int ENDIAN1_S = 1;

// desired wave structure - AHEM THESE ARE READ-ONLY
// - THEY CAN'T BE CHANGED WITHOUT FUTZING THE CODE. SORRY.
//
static const int CHANNELS = 2;
static const int SAMPLERATE = 44100;
static const int BITSPERSAMPLE = 16;

// this is as loud as we can get. not quite 11,
// but approaching it.
//
static const double maxLevel = (double)SHRT_MAX * 1.0;
static const short maxLevelS = (short)maxLevel;
static const double minLevel = (double)SHRT_MIN * 1.0;
static const short minLevelS = (short)minLevel;


// number of samples per cycle. 60 samples = 1 low or 2 high cycles.
//
static const int f1 = 60;
static const int f1cyc = 1;
static const int f2 = f1 / 2;
static const int f2cyc = f1cyc * 2;

// useful values, maybe
//
static const int usPerCycle = (int)(((double)f1 / 44100) * 1000000.0);
static const int usPerHalfCycle = (int)(((double)f1 / 44100) * 500000.0);
static const int usPerQuarterCycle = (int)(((double)f1 / 44100) * 250000.0);
static const int usPerEighthCycle = (int)(((double)f1 / 44100) * 125000.0);

static const int baudrate = (1000000 / usPerCycle);

// 8 + stop + start;
//
static const int bitsPerFrame = 10;

static const int samplesPerBit = f1 * f1cyc;
static const int samplesPerFrame = bitsPerFrame * samplesPerBit;


static const int LOW_BIT = 0;
static const int HIGH_BIT = !LOW_BIT;

static const int LEADIN_BIT = LOW_BIT;
static const int START_BIT = !LEADIN_BIT;
static const int STOP_BIT = !START_BIT;


// NASTY HACK FOR WRITING WAVS. PLEASE LOOK AWAY NOW.

void CreateWaveFile(std::ostream& output)
{
char chars[4];

output << "RIFF";
*(int*)chars = 16;
output << chars[ENDIAN0] << chars[ENDIAN1] << chars[ENDIAN2] << chars[ENDIAN3];

output << "WAVE";
output << "fmt ";

*(int*)chars = 16;
output << chars[ENDIAN0] << chars[ENDIAN1] << chars[ENDIAN2] << chars[ENDIAN3];

*(short*)chars = 1;
output << chars[ENDIAN0_S] << chars[ENDIAN1_S];

*(short*)chars = CHANNELS;
output << chars[ENDIAN0_S] << chars[ENDIAN1_S];

*(int*)chars = SAMPLERATE;
output << chars[ENDIAN0] << chars[ENDIAN1] << chars[ENDIAN2] << chars[ENDIAN3];

*(int*)chars = (int)(SAMPLERATE*(BITSPERSAMPLE/8*CHANNELS));
output << chars[ENDIAN0] << chars[ENDIAN1] << chars[ENDIAN2] << chars[ENDIAN3];

*(short*)chars = (short)(BITSPERSAMPLE/8*CHANNELS);
output << chars[ENDIAN0_S] << chars[ENDIAN1_S];

*(short*)chars = (short)BITSPERSAMPLE;
output << chars[ENDIAN0_S] << chars[ENDIAN1_S];

output << "data";
*(int*)chars = 0;
output << chars[ENDIAN0] << chars[ENDIAN1] << chars[ENDIAN2] << chars[ENDIAN3];
}


void FinaliseWaveFile(std::ostream& output, size_t bytesWrit)
{
char chars[4];

output.seekp(40);

*(int*)chars = (int)bytesWrit;
output << chars[ENDIAN0] << chars[ENDIAN1] << chars[ENDIAN2] << chars[ENDIAN3];

output.seekp(4);

*(int*)chars = (int)(bytesWrit + 36);
output << chars[ENDIAN0] << chars[ENDIAN1] << chars[ENDIAN2] << chars[ENDIAN3];
}

// OK IT'S SAFE TO LOOK AGAIN



// here we represent:
//
// 0 bit as 1 cycle of low frequency,
// 1 bit as 2 cycles of high frequency
//
// where the high frequency is double that of the low.
//
void OutputBit(std::ostream& output, int bit, size_t& written,
short maxval = maxLevelS, short minval = minLevelS)
{
// default to low
//
int cycles = f1cyc;
int samplesPerCycle = f1;

if (bit != 0)
{
cycles = f2cyc;
samplesPerCycle = f2;
}

// generate square pulse, start with rising edge
//
for (int i = 0; i < cycles; ++i)
{
for (int j = 0; j < samplesPerCycle / 2; ++j)
{
// high [1] period
//
output << uint8_t(maxval & 0xff);
output << uint8_t(maxval >> 8);
output << uint8_t(maxval & 0xff);
output << uint8_t(maxval >> 8);
written += 4;
}
for (int j = 0; j < samplesPerCycle / 2; ++j)
{
// low [0] period
//
output << uint8_t(minval & 0xff);
output << uint8_t(minval >> 8);
output << uint8_t(minval & 0xff);
output << uint8_t(minval >> 8);
written += 4;
}
}
}


// outputs a start bit, 8 data bits and a stop bit
//
void OutputByte(std::ostream& output, uint8_t value, size_t& written)
{
OutputBit(output, START_BIT, written);

for (int i = 0; i < 8; ++i)
{
OutputBit(output, (value & 0x80) ? HIGH_BIT : LOW_BIT, written);
value <<= 1;
}

OutputBit(output, STOP_BIT, written);
}


// outputs a lead train of bits, terminated with a start bit.
//
void OutputLeader(std::ostream& output, int milliseconds, size_t& written)
{
int microseconds = (milliseconds * 1000) - usPerCycle;;

// this is a bit of a decoration -
// ramp up the volume during the 1st half of the leader. this might
// prevent some unexpected eardrum-rupture events.. and yes, i know
// that volume should be ramped non-linearly :) this will do for
// the time being.
//
double vol = 0, ramp = 1.0 / (microseconds/(usPerCycle*2));

while (microseconds >= 0)
{
OutputBit(output, LEADIN_BIT, written, (short)(vol * maxLevel), (short)(vol * minLevel));
vol += ramp;
vol = __min(vol, 1.0);

microseconds -= usPerCycle;
}

OutputBit(output, START_BIT, written);
}


void main (int argc, char **argv)
{
if (argc < 3)
{
std::cout << "Usage: makewav [input file] [output file]" << std::endl;
exit(1);
}

std::ifstream inner(argv[1], std::ios_base::binary);
if (!inner.is_open())
{
std::cout << "Could not open input file." << std::endl;
exit(1);
}

std::ofstream outer(argv[2], std::ios_base::binary);
if (!outer.is_open())
{
std::cout << "Could not open output file." << std::endl;
exit(1);
}

// write wav header
//
CreateWaveFile(outer);

// determine input file size
//
inner.seekg(0, std::ios_base::end);
size_t size = (size_t)inner.tellg();
inner.seekg(0);

size_t pc = 0;
size_t written = 0;

// file is structured:
// some seconds of leader
// size msb
// size lsb
// {
// data[512]
// some milliseconds of leader
// }

OutputLeader(outer, 2000, written);

OutputByte(outer, uint8_t(size >> 8), written);
OutputByte(outer, uint8_t(size & 0xff), written);

while (pc != size)
{
// dummy data - replace this with either read from buffer indexed by [pc]
// or a byte get from the file to be encoded
//
OutputByte(outer, inner.get(), written);

++pc;
if (pc % 512 == 0 && pc != size)
{
OutputLeader(outer, 50, written);
}
}

FinaliseWaveFile(outer, written);

outer.close();
}

Thursday, 3 April 2008

No, really!


Mrs. Edna Welthorpe of Hatheringon under Nicely wrote in to let me know that she'd seen through 'my little April fool joke'.

Well, Mrs. Wellthorpe, the joke is indeed on you. The ACE posting is 100% genuine :)

Enjoy!

Tuesday, 1 April 2008

Awesome mix #6

I've been laid up with a succession of minor bugs of the viral kind and subsequently there's been no movement on the hardware front. I think it's time to post something though and with that in mind please be upstanding for the one - the only - ACE!

That's the Arduino Cassette Engine to me and you.

Hankering after some old-skool interfacing? Got the urge for PCM? Here's some Arduino code to read a data stream encoded in a frequency shift keyed manner. As usual I'm presenting this in its completely raw form. There are plenty of comments in the code and enough information to allow any intermediate level programmer to get started.

Also included in the archive is the c# app that creates demo data to be read. It was adapted from another application I wrote to get data from binary images into my zx81 which explains the cockamamy structure. And the icon! If you have problems downloading let me know.

Beware of the output from SquareWave - it's as loud as it can possibly be. Make sure that volumes are turned right down before running it. You have been warned!

Enough blabber - here's the skinny ripped right from the code:

 /*
 /  'Arduino Cassette Engine'.
 /
 /  Developed by Charlie Robson in 2008.
 /
 /  This is half of a solution for getting data into an arduino. The other
 /  half being code to generate frequency shift keyed (FSK) data in the form
 /  of WAV files which should be widely playable. These can be recorded or
 /  otherwise electronically distributed. Why do it this way? 1. Because it's
 /  funny. 2. Having given an updatable arduino-powered 'toy' to someone, I
 /  can't expect them to re-program the device by traditional means in order
 /  to update it. Everyone has a means of playing a sound!
 /
 /  On with the show.
 /
 /  The data is encoded like so:
 /
 /    Lead-in
 /    High byte of data length
 /    Low byte of data length
 /    {
 /     Data[0 < size <= 512]
 /     (Lead-in)
 /    }
 /
 /  Lead-in is a train of bits terminated with a start bit. This means the lead-in
 /  bit needs to be the logical complement of the start bit.
 /
 /  Every byte is represented by 10 bits - a start bit, 8 data bits, and a stop
 /  bit. The start and stop bits are complementary.
 /
 /  Data arrives in 512 byte blocks. Each block except the last is followed by a
 /  short period, about 25 milliseconds, of lead-in. This is to allow processing time
 /  on each block as it arrives.
 /
 /  Decoding a data bit is done something like so:
 /
 /    Await a rising edge then take 2 samples at a period which will allow us
 /    to distinguish a 0 or 1 bit. Sampling 2 identical values yields a 0 bit.
 /    Delay so the next bit read will occur when the signal is in a low period.
 /
 /  Following is a picture of a '0' bit, or low frequency cycle, followed by
 /  a '1' bit or high frequency cycle, with some important times noted.
 /
 /    The X shows a rough expected time at which we start the bit reading.
 /    The 0 shows relative time zero, when we detect the rising edge.
 /    The + signs show the times at which we sample.
 /    The * shows the earliest time at which we can await a new bit. IOW, X.
 /
 /        __`_____`__             __
 /       |  `     `  |           |
 /       |  `     `  |           |
 /       |  `     `  |     `     |
 /     ` |  `     `  |     `     |
 /   __`_|  `     `  | ____`_____|
 /     ` `  `     `        ` 
 /     X 0  +     +        *
 /     ` `__`__   `   _____`
 /     ` |  `  |  `  |     |
 /     ` |  `  |  `  |     |
 /       |     |     |     |
 /       |     |     |     |     |
 /   ____|     |_____|     |_____|
 /       `
 /       `
 /       |-----------------------| = usPerCycle        = 1360uS
 /       |-----------|             = usPerHalfCycle    = 680uS
 /       |-----|                   = usPerQuarterCycle = 340uS
 /       |--|                      = usPerEighthCycle  = 170uS
 /
 /
 /  It's assumed that the audio signal is connected to PORTB-0 which is
 /  digital pin 8. If you'd like to change that, then replace occurrences
 /  of...
 /
 /      PINB & 1
 /
 /  ... with the direct bit-access for your implementation. I do it this
 /  way because it's slightly faster than digitalRead()ing. If you do change
 /  the port you'll also need to change the logic in readBit as this relies
 /  on the fact that we work with bit 0. 
 /
 */



Enjoy!

This is how we do it

MMC (9) acorn atom (7) zx81 (7) arduino (5) Atari 800 (3) c128 (3) sd card (3) sd2iec (3) sio2sd (3) tatung einstein (3) 6502 (2) Chuckie egg (2) M5 (2) Max6956 (2) QL (2) RCM (2) Sord (2) assembler (2) avr (2) c64 (2) cadsoft eagle (2) eeprom (2) einSDein (2) mmbeeb (2) multi-cart (2) spi (2) system 80 (2) ufat2 (2) vic20 (2) video genie (2) 6502 second processor (1) 6522 (1) 8255 (1) Acorn BBC Micro (1) Apple 2e (1) Apple ][ 2 two (1) BBC 6502 second processor (1) BBC micro (1) DevicePrint (1) Double Choc Chip Muffins (1) FAT (1) IO (1) Jupiter Ace (1) LED (1) Master 128 (1) PCB (1) PIC (1) POV (1) PROGMEM (1) ST (1) Spectrum 128 (1) antex (1) arcade spinner (1) arduino shield (1) atari (1) atmel (1) bakewell tart (1) beer (1) bird's nest (1) bitbucket (1) brokenated XC special (1) cake (1) cassette (1) cassette interface (1) compact flash (1) convert (1) dac (1) de-yellowing (1) eaca (1) efficient (1) einsdein. z80 (1) eye strain (1) failosophy (1) filesystem (1) finally (1) fram (1) french polishing (1) fuse (1) fuses (1) gaming (1) github (1) glue (1) google chrome (1) heroic failure (1) high voltage programming (1) hot irons (1) if (1) jiffydos (1) joey beltram (1) lego robot (1) library (1) lying (1) machine code (1) matron (1) microcode (1) mmc interface (1) mmc2iec (1) mmm (1) mouse guts (1) multicart (1) oscilloscopes (1) pcm (1) pic32mx (1) porn (1) proto shield (1) purple (1) repo (1) retro computer museum (1) retro hard-on (1) rom box (1) sd (1) sd2mmc (1) seadragon (1) silliness (1) small (1) software master (1) soldering (1) sord m5 (1) spi software master (1) stray capacitance (1) string (1) techadventure (1) test equipment porn (1) ts1000 (1) turtle cheesecake (1) tweaking (1) vc20 (1) video head (1) video ram replacement (1) weewee (1) wingasm (1) wire library (1) wodges of IO (1) xilinx cpld (1) yellowing (1) zx spectrum (1) zxpander (1)

About Sir Morris

My photo

Loves: Old computers, Old Techno, Old ladies. Cake.
Hates: New computers.

Unless otherwise stated all of the original work presented here is:

Creative Commons License
Licensed under a Creative Commons Attribution-Noncommercial 2.5 Generic License.

The work of others where referenced will be attributed appropriately. If I've failed to do this please let me know.