Sunday, 5 March 2023

MiniMorseMac

Introduction

After porting Mini Morse from the ZX81 to the ZX80, it occurred to me that Mini Morse would be a good candidate for converting to a classic Macintosh. I used my original introduction to classic Macintosh programming: Inside The Toolbox with Think Pascal.

I have that book, but used it to develop programs using Think C 5.0.4, because I had a proper copy of that; it works in a similar way and I'm more comfortable with C instead of Pascal. It turned out that it was fairly straightforward and satisfying to code, with the final application coming in at 3020 bytes.

Digital Archeology

Unfortunately, although Think C is readily available, I'm not sure of its legal status with regard to copying, even though it's abandonware. However, I am aware that Think Pascal, certainly up to version 4.5.4 at the end of its development is available for downloads for personal use.

I had had a version of Think Pascal 4.5.4 downloaded well over a decade ago, and had used it for some minor projects. Again, unfortunately, it's not easy to download. The web page still exists, but the Symantec link it uses at the time is now broken.

So, initially I ported my Mini Morse program to my Think Pascal 4.5.4, which I copied indirectly from my PowerBook 1400c (via a SCSI Zip drive and USB Zip drive). Then I found that the application it created, was about 16kB long. It seemed very unlikely to me that a straightforward Toolbox program, converted from Think C 5 for a Mac 7 Toolbox geared for Pascal would be that much bigger. And this lead me to the conclusion that Think Pascal 4.5.4 is not such a great IDE for early era Mac application development.

Then began the quest to see if it was any better under an earlier version. It turns out that Think Pascal 4.0.2 is available from a number of sources including Macintosh Garden, but the source I would trust the most is the link from MacGui.com. This downloads a Stuffit .sit file. You'll also need to download ResEdit, which you can from the same site here as a disk image.

Now, Stuffit still exists as a format for compressing Mac and Windows files, but the .sit file in question can't be expanded by the version of Stuffit Lite I had on my miniVMac emulator (as this Stuffit Lite was too early), and it couldn't be expanded by a later version of Stuffit Lite (because the compression format is no longer supported). After several approaches, I found that my iMac G3 running Mac OS 9 had a version of Stuffit that could expand that file, but I couldn't then use it to compress it so that it could work with Stuffit Lite on miniVMac.

In the end, I took a different approach. I used Disk Copy on the iMac G3 to create a .img disk image from the Think Pascal folder; which I could then open as a disk on miniVMac. From here I could create a new project in Think Pascal 4.0.2, which, when compiled as an application, took up only 2803 bytes.

This illustrates an increasing issue with the maintenance of retro software on modern platforms, via emulation or other means: the number of bridging technologies is becoming more convoluted as modern systems leave behind archaic technology. It's why I think I have to, or ought to maintain a number of Macs between my oldest (a 68000 Mac Plus from the 1986 era) to my MacBook Pro from 2018 via a Performa 400 (68030 Colour Mac) PowerBook 1400C (an early PowerPC Mac which runs System 7.5.3 and Mac OS 8.1 reasonable); iMac G3 (which runs Mac OS 9.2 and Mac OS X 10.3 OK); Mac Book Core Duo (which runs up to Mac OS X 10.6.8). Using these machines gives me a continuity of technology.

Running MiniMorse For 68K Mac

The purpose of this blog is to show how compact a 1kB ZX81 program can be on an early GUI computer. The ZX81 was a simple text-based 8-bit home computer which could officially support up to 16kB of RAM, but came with 1kB of RAM as default, along with its programming language, in an 8kB ROM.

The early Macintosh was a major conceptual leap in computing, supporting a sophisticated (for the time) Graphical User Interface with Windows, Icons, Menus and directly controllable gadgets. The earliest versions used a 32-bit, 68000 CPU and provided 128x the memory of a ZX81, with 8x the ROM. I never had the opportunity to use one of these machines directly, but at the University Of East Anglia, where I did my Computer Science BSc, they had the next model, with 512kB of RAM. These Macs could run simple development environments such as the interpreted Mac Pascal; or ETH Modula-2.

So, it's a natural question to ask how much more bloated equivalent applications were, on computers with that level of sophistication and about 8x to 32x the resources. And the answer, as far as Macs go, is that they are proportionally more sophisticated (a 3kB program / 400 bytes is about 7.5x larger).

What is really different is the length of the program itself. The ZX81 program is tokenised, so the source code for the version that takes up just under 400 bytes, is also 400 bytes, because the source is the final program. On the other hand, the Mac Pascal version takes up nearly 7kB of source code, around 17x larger, even though it compiles into a program only 7.5x larger.

The MiniMorse application can be found here. Download it onto your computer. You'll need a means of converting from .hqx to the application itself, but in https://system7.app that's easy. First you need to copy the file, by opening "The Outside World" server then dragging the MiniMorse app onto the Downloads folder. Drag the .hqx file to The Infinite Hard Disk. Then you open Infinite HD:Utilities:Compact Pro; Open Compact Pro and select Misc:Convert FROM Bin Hex. Navigate to the .hqx file in The Infinite Hard Disk and then save the Application in the same location. After a few seconds, the application appears in The Infinite Hard Disk.

Double-click the application to run it. You'll see a simple Mac Application:


As with the ZX81 version, typing a letter or digit generates the Morse code and typing a morse code generates the letter or digit. In addition to the ZX81 program, like a good Macintosh application MiniMorse sets the cursor to an arrow (as you can see), supports an about dialog:

And a Quit option with the command Q short-cut.

You'll find that MiniMorseMac handles window updates properly, can move to the back or front and can launch a desk accessory. It should work with Multifinder under System 6 or earlier or simply the Finder. You can shrink the memory allocation down to about 16kB and it will work fine.

Typing In The Program

MiniMorse Mac is short enough to type in. So, here's how to create the application from scratch. Firstly, create a folder called DevProjects and within it, create a folder called MiniMorse. This is where the Pascal project will go.

Next, run Think Pascal. It might complain that it needs 4MB to run, but it doesn't. Change the memory allocation to just 1MB and it'll be fine (From the Finder, choose File:Get Info on the application and change the box on the bottom right).

When you run Think Pascal, it will want to start with a project. Navigate to the MiniMorse folder and click on [New]. It will ask for a project name, type MiniMorse.π ('π' is option+p). Don't create an instant project, click on Done and the project window will be created. Save and then Quit.

The next step is to create the resources. Find ResEdit 2.1 and open the application. Create a new file and call it MiniMorse.π.rsrc. It's fairly tricky to describe how create resources interactively, but the following table should condense the information. You may find that the WIND, DITL and ALRT resources have fields for Width and Height instead of Right and Bottom, in which case the e.g. WIND menu will give you an option to switch the fields. Finally, [¹] means choose Resource:Create New Resource... (Command)K; [²] means choose Resource:Get Resource Info... (Command)I and in menus, [ENTER] means you literally need to press the [ENTER] key.:


Re-source¹ Fields.. [Close Window] Info².. [Close, Close]
WIND [OK] TOP=40, Bottom=240, Left=40, Right=280, Initially Visible, Close Box, Default Color ID=400, Name="MiniMorse", Purgeable (only)
MENU [OK] [X] Enabled, Title=• Apple Menu[ENTER], [X] Enabled, Title="About MiniMorse..."[ENTER] [ ]Enabled, • Separator line ID=128, No attributes.
MENU [OK] [X] Enabled, Title="File"[ENTER], [X] Enabled, Title="Quit", Cmd-Key:Q[ENTER] ID=129, No attributes.
DITL [OK] Drag [Static Text] to DITL and double-click it. [X] Enabled. Text="MiniMorse ©2023 Julian Skidmore[ENTER]Press a letter or digit to convert to Morse code, or press a sequence of '-' and '.' to convert a letter/digit", Top:4, Bottom:73, Left:64, Right:311. Close, Drag Button below Text. Double-click it. [X] Enabled. Text="OK" Top:90, Bottom:110, Left:159, Right:217. Close. ID=400, Only attribute=Purgeable.
ALRT [OK] Color:Default, DITL ID=400, Top:40, Bottom:240, Left:40, Right:280 ID=400, Only attribute=Purgeable.

When you've finished, close the .rsrc file. ResEdit will ask you to save it - save it. Then open up the MiniMorse.π project.  Choose File:New and create a stub of a pascal program:

program MiniMorse;
begin
end.

Choose File:Save to save it as MiniMorse.p. Choose Project:Add "MiniMorse.p" to add the file to the project. Next, add the resource file by choosing Run:Run Options... Tick the [ ] Use Resource File field and then choose the MiniMorse.π.rsrc file you created. Finally, Click [OK]

Now you want to replace the dummy program with the rest of file. When you've finished...

program MiniMorse;
  const
    kWindIdBase = 400;
    kMenuBarId = kWindIdBase;
    kMenuIdApple = 128;
    kMenuAbout = 1;
    kMenuIdFile = 129;
    kMenuQuit = 1;
    kAboutAlertId = 400;
    kWneTrapNum = $60;
    kUnimplTrapNum = $9f;
    { We want to support a 1s event timeout. }
    kSleep = $60;
    kOriginX = 10;
    kOriginY = 10;
    kMaxMorseLen = 5;
    kTapMax = 15;
    kMorseLetterCodes = 26;
    { 0..9=0..9, 10..16 :;<=>?@}
    {17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30}
    {A B C D E F G H I J K L M N}
    gLettersToMorse = '6AE92D;@4N=B75?FK:83<H&gt>IMC';
    kMorseDigitCodes = 10;
    gDigitsToMorse = '_^\XP@ACGO';
    kMorseCodes = 64;
    gMorseToAlpha = '0.ETIANMSURWDKGOHVF.L.PJBXCYZQ3.54.3...24....:.16.5....:7...8.90';
    cMorseSyms = '.-';
  var
    gMorseWin: WindowPtr;
    gDone, gWNEImplemented: boolean;
    gTheEvent: EventRecord;
    gHelloStr, gNumStr: Str255;
    gMorseStr: string[6];
    gKeyCode, gMorseSyms: string[2];
    gUpdates, gMorseTap: integer;
    gTapTimeout: longint;
    { 0.5s timeout }

  function Later (aTimeout: LONGINT): LONGINT;
  begin
    Later := TickCount + aTimeout;
  end;

  function After (aTimeout: LONGINT): boolean;
    var
      tick: longint;
      timedOut: boolean;
  begin
    tick := TickCount;
    timedOut := aTimeout - tick < 0;
    After := timedOut;
  end;

  function DoMenuBarInit: boolean;
    var
      menu: MenuHandle;
  begin
    menu := nil;
    menu := GetMenu(128);
    AddResMenu(menu, 'DRVR');
    InsertMenu(menu, 0); { add after all.}
    menu := NewMenu(129, 'File');
    AppendMenu(menu, 'Quit/Q');
    InsertMenu(menu, 0);
    DrawMenuBar;
  end;

  function Init: boolean;
    var
      fontNum: integer;
      ok: boolean;
  begin
    gDone := false;
    gUpdates := 0;
    gKeyCode := ' ';
    gMorseStr := ' ';
    gMorseTap := 1;
    gMorseSyms := '.-';
    gTapTimeout := Later(kTapMax);
    gWNEImplemented := NGetTrapAddress(kWindIdBase, ToolTrap) <> NGetTrapAddress(kUnimplTrapNum, ToolTrap);
    gMorseWin := GetNewWindow(kWindIdBase, nil, WindowPtr(-1));
    ok := gMorseWin <> nil;
    if ok then
      begin
        SetWTitle(gMorseWin, 'MiniMorse');
        SetPort(gMorseWin);
        GetFNum('monaco', fontNum);
        TextFont(fontNum);
        TextSize(9); { just standard size.}
        ShowWindow(gMorseWin);
        ok := DoMenuBarInit;
        if ok then
          InitCursor;
      end;
    Init := ok;
  end;

  procedure DoMenuApple (aItem: integer);
    var
      accName: Str255;
      accNumber, itemNumber, dummy: integer;
      appleMenu: MenuHandle;
  begin
    case aItem of
      kMenuAbout:
        dummy := NoteAlert(kAboutAlertId, nil);
      otherwise
        begin
          appleMenu := GetMHandle(kMenuIdApple);
          GetItem(appleMenu, aItem, accName);
          accNumber := OpenDeskAcc(accName);
        end;
    end;
  end;

  procedure DoMenuFile (aItem: integer);
  begin
    case aItem of
      kMenuQuit:
        gDone := true;

    end;
  end;

  procedure DoMenuChoice (aMenuChoice: LONGINT);
    var
      menu, item: integer;
  begin
    if aMenuChoice <> 0 then
      begin
        menu := HiWord(aMenuChoice);
        item := LoWord(aMenuChoice);
        case menu of
          kMenuIdApple:
            DoMenuApple(item);

          kMenuIdFile:
            DoMenuFile(item);

        end;
        HiliteMenu(0);
      end;
  end;


  procedure DoMouseDown;
    var
      whichWindow: WindowPtr;
      thePart: integer;
      windSize, menuChoice: LONGINT;
      oldPort: GrafPtr;
  begin
    thePart := FindWindow(gTheEvent.where, whichWindow);
    case thePart of
      inMenuBar:
        begin
          menuChoice := MenuSelect(gTheEvent.where);
          DoMenuChoice(menuChoice);
        end;
      inSysWindow:
        SystemClick(gTheEvent, whichWindow);
      inDrag:
        DragWindow(whichWindow, gTheEvent.where, screenBits.bounds);
      inContent:
        if whichWindow <> FrontWindow then
          begin
            SelectWindow(whichWindow);
          end;

      inGrow:
        ; { don't support.}
      inGoAway:
        gDone := true;
      inZoomIn:
        ;
      inZoomOut:
        ; { don't support.}
    end;
  end;

  procedure Repaint (aWindow: WindowPtr);
    var
      oldPort: GrafPtr;
  begin
    GetPort(oldPort);
    SetPort(aWindow);
  { tempRgn=NewRgn;}
  { GetClip(tempRgn);}
    EraseRect(aWindow^.portRect);
    MoveTo(kOriginX, kOriginY);
  { DrawString('Updates=');}
  { NumToString(++gUpdates, gNumStr);}
  { DrawString(gNumStr);}
    DrawString('Key=');
    DrawString(gKeyCode);
  { MoveTo(kOriginX, kOriginY+10);}
    DrawString(' Morse=');
    DrawString(gMorseStr);
    SetPort(oldPort);
  end;

  procedure DoUpdate (aWindow: WindowPtr);
    var
      myRect, drawingClipRect: Rect;
      oldPort: GrafPtr;
      tempRgn: RgnHandle;
  begin
    BeginUpdate(aWindow);
    if aWindow = gMorseWin then
      begin
    { DrawMyStuff}
        Repaint(aWindow);
      end;
    EndUpdate(aWindow);
  end;

 { @ brief , handle Key Event , assume Later bottom byte .}
 {Keys aren 't sent to it now.}
  procedure DoKey;
    var
      ch: char;
      len, morseCode: integer;
  begin
    ch := char(BitAnd(gTheEvent.message, 255));
    if BitAnd(gTheEvent.modifiers, cmdKey) = cmdKey then
      begin
        DoMenuChoice(MenuKey(ch));
      end
    else
      begin
        morseCode := 1;
        len := 0;
        if (ch >= '0') and (ch <= '9') then
          begin
            morseCode := ord(gDigitsToMorse[ord(ch) - ord('0') + 1]) - 32;
          end
        else if (ch >= 'a') and (ch <> 'z') or (ch >= 'A') and (ch <= 'Z') then
          begin
            morseCode := ord(gLettersToMorse[BitAnd(ord(ch), $1f)]) - 48;
          end;
        if (ch = '.') or (ch = '-') then
          begin
            gMorseTap := (gMorseTap * 2);
            if ch = '-' then
              gMorseTap := gMorseTap + 1;
            gTapTimeout := Later(kTapMax);
          end
        else
          begin
            gMorseStr := ' ';
            while morseCode > 1 do
              begin
                len := len + 1;
                gMorseStr[len] := gMorseSyms[1 + BitAnd(morseCode, 1)];
                morseCode := morseCode div 2;
              end;
            Repaint(gMorseWin);
          end;
      end;
  end;

  procedure DoNull;
    var
      front: WindowPtr;
  begin
    if (gMorseTap > 1) and After(gTapTimeout) then
      begin
        front := FrontWindow;
        if front = gMorseWin then
          begin
      { We have a valid Morse pattern!}
            gKeyCode[1] := gMorseToAlpha[BitAnd(gMorseTap, 63) + 1];
            Repaint(gMorseWin);
            gMorseTap := 1;
          end;
      end;
  end;

  procedure DoEvent;
    var
      gotOne: boolean;
  begin
    gotOne := false;
    if gWNEImplemented then
      begin
        gotOne := WaitNextEvent(everyEvent, gTheEvent, kTapMax, nil);
      end
    else
      begin
        SystemTask;
        gotOne := GetNextEvent(everyEvent, gTheEvent);
      end;
    if gotOne or not (gotOne) and (gTheEvent.what = nullEvent) then
      begin
        case gTheEvent.what of
          nullEvent: { Handle end; Morse tapping.}
            DoNull;
          mouseDown: { handle mousedown.}
            DoMouseDown;
          mouseUp:
            ; { handle mouse.}
          keyDown:
            DoKey;
          updateEvt:
            DoUpdate(WindowPtr(gTheEvent.message));
          activateEvt:
            ; { Handle DoActivate;}
          diskEvt:
            ; { don't handle.}
          keyUp:
            ; { do nothing.}
          autoKey:
            ;
        end;
      end;
  end;

begin
  if Init then
    begin
      while not (gDone) do
        begin
          DoEvent;
        end;
    end;
end.
Finally, to create the application, choose Project:Build Application... Type a name for the application (e.g. MiniMorse), click [OK] and it should complete. Look for the compiled application and open it!

Conclusion

It's really simple to write a version of MiniMorse for a ZX81, but a lot more involved trying to create a version of the application for an early GUI environment on a Mac. It would almost certainly be much more complex still to do the same thing for Windows 3.1 or GEM (Atari ST). Nevertheless, it's possible to create a small pascal program at just under 3kB, because the Classic Mac's operating system interface is straightforward and compact.

Sunday, 29 January 2023

Mini-Morse

 Mini-Morse is a type-in 1K ZX81 program, but it's also a little treatise on encoding and Morse tutorials. Firstly, the listing which you can type in using an on-line ZX81 Emulator. Type POKE 16389,68 <Newline> New <Newline> to reduce the ZX81's memory to a proper 1K and then continue with the listing.


The Tutor

Then you can type RUN <Newline> to start it. It's really simple to use, just type a letter or digit and it'll convert the character to a morse code made of dots and dashes. Alternatively, type a sequence of '.'s (to the right of the 'M') and '-'s (Shift+J), fairly quickly and it'll translate your Morse code. In that case it's best to select the sequence you want and remember it, then type it out rather than trying to read and copy it. You'll find you'll pick up the technique fairly quickly. It only shows one letter at a time.

This is always the kind of Morse Tutor I would have wanted to use, even though it doesn't care about the relative lengths of the dots and dashes. That's because I want a low-barrier to entry and I don't want it to be too guided, with me having to progress from Level 1 to whatever they've prescribed. Also, the basics of Morse code is simple, so a program that handles it should be simple.

Encoding

So, here's the interesting bit. What's the easiest way to encode Morse? Here's the basic international set from the Wikipedia Morse Code entry:

The Puzzle with Morse code is that it's obvious that it's a kind of binary encoding, but at the same time it can't quite be, because many of the patterns have equivalent binary values. For example E= '.' = 0 = I = '..' = S = '...' = H = '....' = 5 = '.....'. Every pattern <=5 symbols will have at least one other letter or number with an equivalent binary value.

When people type in Morse they solve the problem by generating a third symbol - a timing gap between one letter and the next. And in tables of Morse code the same thing is done, at least one extra symbol is added - usually an extended space at the end of the symbol.

This implies that Morse can be encoded the same way on a computer, by encoding it as a set of variable-length strings (which involves the use of a terminating symbol or a length character), or encoding it in base 3 (so that a third symbol can be represented).

However, we should feel uneasy about this as an ideal, because everything about Morse code itself, still looks like it's in binary. Shouldn't it be possible to encode it as such?

The Trick

The answer is yes! And here's the trick. The key thing to observe when trying to convert Morse into pure binary is that every symbol is equivalent to another with an indefinite number of 0s prepended. As in my examples above, both E and H would be 0 in an 8-bit encoding: 00000000 and 00000000. So, all we have to do to force them to be different is to prevent the most significant digit from being part of an indefinite number of 0s, by prepending a 1. This splits the indefinite preceding 0s from the morse encoding. Then E and H become: 00000010 and 00010000. Of course, when it comes to displaying the symbols we'd begin after the first 1 digit. Another way of looking at it is to observe that the length is encoded by the number of 0s preceding the first '1' or the number of digits following the first '1', but the true insight is to grasp that this works for a variable-length morse-type encoding up to any number of dots and dashes.

You can persuade yourself that this works by trying it out on a number of Morse symbols above. An implication of this technique is that it means we know we can encode Morse in 1+the maximum sequence length bits. Here we only use basic letters and numbers so we only need 6 bits at most.

In the program above, Morse code uses that trick to encode using a pair of strings. K$ converts digits and letters into dots and dashes while M$ converts dots and dashes into digits and letters. I could have used just one string and searched through it to find the other mapping, but this is faster.

One thing else to note. M$ encodes dots and dashes as you might expect (e.g. 'E' is at M$(2), because E is now '10'. However, K$ encodes characters into Morse in reverse bit order, because it's easier to test and strip the bottom bit from a value in ZX BASIC, which lacks bitwise operators. The same trick works regardless of the bit order: appending a '1' (or '-') at the end of all the patterns and then '.'s to a fixed length encodes unique patterns for all characters.

Conclusion

Learning Morse code is tedious. It was great for communications in the 19th century when the world had nothing better than the ability to create a momentary, electrical spark from making or breaking a contact on a single wire, but the symbols are all fairly random and hard to learn. This is not to understate the amazing tech breakthroughs they needed (e.g. amplifiers and water-proofing cables!).

I've wanted to write a simple Morse tutor for a while and a 1K ZX81 seems a natural platform for such a simple exercise. Plus, the Morse to character translation is a bit real-time and I really wanted to pass on the encoding trick. MiniMorse takes me full circle to a hack of mine in 2010 which created a morse-code type encoding for a POV display based on the layout of a phone keypad. Check out Hackaday's entry for PhorseCode or my Google Site web page for it. PhorseCode could be converted to a proper Morse Code system using a different translation table.



Postscript

It is, of course, possible to reduce the memory size of MiniMorse. Here's a version that's only a mere 405 bytes long, with just 32b of variables. I could reduce it a bit further by combining M and A as they're never used at the same time. Ironically, many of the space-saving techniques on the ZX81 make the program appear bigger. This is due to the fact that literal numbers incur a 6 byte overhead as the internal representation + prefix of CHR$ 114 gets inserted into the code. By employing tricks such as NOT PI for 0, SGN PI for 1, INT PI for 3; CODE "char" for any number in the range 2, 4 to 63 or 128 to 191; VAL "number" we can save bytes by preventing the internal representation from being used. Caching very frequently used values as variables can sometimes also save memory. Finally, the biggest difference was made by evaluating the K$ and M$ strings directly in the code, which saved over 128b because they're no longer duplicated in the variables region.


And there are yet more improvements. It's possible to replace a GOTO CODE [CHR$ 13] with GOTO PI; and string$<>"" with LEN string$; string$="" with NOT LEN string$; CODE [CHR$ 10] with PI*PI and finally we only need 4 spaces and no ';' on the print statement at the end. This takes it down to 393 bytes!


Mini-Morse ZX80

It's possible to write a variant of MiniMorse for the 1K ZX80. We need to do this in two parts. Strings can't be indexed on a ZX80; we can't type all the printable characters (can't type inverse characters); and some characters are remapped if you type them (e.g. PRINT CHR$(18), CODE("-") displays '-' then 220. You can find the character set near the end of the user manual at this URL.

So, instead we put the conversions in a REM statement and PEEK them. Programs start at 16424 and the first character of a REM statement is at 16427.  So, the first stage is to enter all the Morse codes we can't easily enter (i.e. the letters).


RUN the program and type the values on the second row:

A.B. C. D E F. G. H. I J. K. L. M.N O. P. Q. R. S T U. V. W  X. Y. Z

6 17 21 9 2 20 11 16 4 30 13 18 7 5 15 22 27 10 8 3 12 24 14 25 29 19


After it has run, check the REM statement matches the first line of the final program. When it does, delete lines 30 to 70 and complete the rest of the program as shown below:


MiniMorse for the ZX80 works slightly differently, because you have to press <Newline> after typing in the letter or Morse pattern you want to convert: the ZX80 doesn't support INKEY$. The easiest way to escape the program is by typing in 0 and then pressing <Space> immediately after pressing <Newline>.

Monday, 28 November 2022

My New Mac is an old Mac

 I've picked up an old Powerbook 1400 from eBay, for £65. It's missing a floppy drive / CD-ROM drive, so it's going to be a bit of a challenge getting data in and out of it. I think perhaps at my Dad's house there's a HDI-30 to SCSI converter, but failing that, I can use AppleTalk between my Performa 400 and the Powerbook 1400 or maybe simple serial transfer.

The PowerBook 1400 in question is the PowerPC 603e, 117MHz model without a level 2 cache. So, it's slow. But how slow, is the big question. I tried to find out from LowEndMac. It gave the performance as 

"Performance: 114/137/152 (117/133/166 MHz), MacBench 4 (also 42,076 (117 MHz) Whetstones)"

But when I tried to compare it with other Macs of the era on LowEndMac, particularly the Performa 5200, they all provided benchmarks for different test suites. The 6100/60 was given relative to MacBench 5, MacBench 2 and Speedometer 4. Performa 5200 was just xxx relative to a Mac SE. And so forth.

However, a little while later I came across this reddit page:

https://www.reddit.com/r/VintageApple/comments/q4rg86/performa_620075_benchmarks_compared_to_other_macs/

And it gave a screenshot of a bunch of early PowerPC Macs (and a Quadra 630) benchmarks for MacBench 4!!! Here's the data as a simple table:

Model CPU FPU Disk Mean*
Quadra 630 35 7 73 26
Performa 6200/75(SCSI) 93 95 94 94
Performa 6200/75(IDE) 93 95 121 102
PowerMac 6100/60 100 100 100 100
PowerMac 7200/75 102 117 107 108
PowerBook 1400/117 124 143 92 118
PowerMac 8100/80 142 138 132 137
Performa 6320/120 134 148 161 147
PowerMac 7500/100 162 164 164 163
PowerMac 7200/120 174 202 191 189
Performa 6400/180 184 207 123 167
Performa 6400/200 258 262 163 223
PowerMac 7600/132 251 243 212 235

So, I've used a Performa 5200 before - in fact this was the first PowerPC computer I used and at the time seemed amazingly fast compared with the Performa 400 I'd had previously! The Powerbook 1400 ought to be about 21% faster. I also had a Powerbook 5300 running at 100MHz for a few years when I consolidated my PowerMac 4400 + Powerbook Duo/Full dock setup and apart from the fact that the hard disk failed on the Powerbook 5300, I had found that the Powerbook 5300 was good enough. So, I think the 1400 will be fine too: faster than a first generation 6100 or perhaps even a 6100/66; a 7100/66; 6200 (5200)/75 and even the slowest PCI PowerMac.

Future blog posts will cover the progress I've been making!
[* The mean is a geometric mean where the 3 values are multiplied together, then the cube root is applied]


Wednesday, 26 October 2022

ZX81, 1K Hanoi

 In my previous blog-post I described a coding convention for the Intel 8048 and used an embedded implementation of the Towers of Hanoi program as an example. It had a rudimentary user interface; outputting merely the column numbers between each step, which advanced on a button press.

Towers of Hanoi is an interesting toy program, and here we explore a different implementation, targeted at a 1K ZX81.

A standard ZX81 has 8K of built-in ROM + 1K of RAM which is shared between the user program (+variables); the 8K ROM's system variables; the 32x24 screen (which expands from 25 bytes to 793 bytes depending on how much is displayed on it); the 33b printer buffer and the machine stack used by the ROM. All this is described in Chapter 27 of the user manual.

So, in some respects it has less usable memory than an Intel 8048 (because 1K + 64b > 1K-system variables-screen...), but in others it has more (because the ROM is useful library code that would have to be included in the Intel 8048 equivalent). In addition, BASIC is generally less compact than assembler (and ZX81 BASIC is even worse).

The Listing


It doesn't look like there's much here, but a surprising amount thought went into it. You can run a javascript version of a zx81 from here, but many other emulators are available. To prove it will work in 1K, you will need to type POKE 16389,68 [Newline] NEW [Newline] to set the RAMTOP top of BASIC to the end of memory on a 1kB ZX81.

It's not obvious how to type the graphics, but the top line is 2 spaces, then ▗ █ ▙ 6 spaces Graphic Shift 5, 6 spaces Graphic Shift 8. The rest can be deduced fairly easily then.

The application will use 634 bytes. An earlier version used a mere 41 bytes more and this was enough to cause an out of memory error.

Using The Screen

The most significant change to the ZX81 version is to use the screen to provide a graphical representation of the puzzle. Because we still need to save space, it's essential to use the ZX81 block graphics (rather than whole characters) and I chose to move a ring by unplotting a ring's old position while plotting its new position rather than, e.g. animating the movement (which would have been very slow on the ZX81 anyway).

In the first version I used loops and plot commands to generate the puzzle, but it uses less memory to print the puzzle out directly. I could save another 4 bytes by assigning the graphics for the spaces and the poles to R$ and substituting R$ at the end of lines 10 to 30.

I also save a bit of space by calculating the minimum pole spacing. It looks like there isn't enough room between poles, but this isn't correct,  because at maximum we only ever need space for a 7 pixel wide ring and a 6 pixel wide ring. Therefore 7+1+6+1=15 pixels is enough between the poles.

This means the graphics take up: (8+15+15+7)/2=23 chars for row 4, 22 chars for row 3; 21 chars for row 2 and 20 chars for row 1(because the end of the higher rows are only ever filled with shorter rings). That's 86 bytes in total. The Column->Column moves are also displayed and this takes 4 bytes.

Moving The Rings

This is relatively simple: we have a string, R$, which is used as a byte array (to save space) to hold the number of occupied rings on each column. The width of each ring to move is determined by the current level: 1 for level 0 up to 7 for level 6. S and D determine the start and end coordinate. We plot to the ring we move to, while unplotting the ring we move from except for when x=0 in order to leave the pole. At the end we adjust the number of occupied rings, -1 for the source ring and +1 for the destination ring.

Memory-Saving Techniques

This program uses the normal ZX81 BASIC memory saving techniques. '0', '1' and '3' are replaced by NOT PI, SGN PI and INT PI to save 5 bytes each time. Values in the printable character range are replaced by CODE "x", saving 3 or 4 bytes each time; while other values use VAL "nnn" to save 3 bytes each time. This also applies to line numbers, so that placing the Hanoi function itself below line 1000 saves several bytes.

Using R$ as a byte array involves a number of clumsy VAL, CODE and CHR$ conversions, but replacing R$ with a DIM R(D) array would end up costing another 9 bytes, so it's a net saving to use R$.

Hanoi Algorithm Change

It turns out we can make the Hanoi algorithm itself less recursive than in the Intel 8048 version. In that version we pushed the source and destination columns on each call, but in fact that was done to demonstrate how to manage the data structure.

It's not necessary to do that. The original source and destination values can be restored after each recursive call, because 6-S-D is a reversible operation. Similarly, because L is decremented at the beginning of each call (instead of passing L-1 as a parameter to each call), then by incrementing it at the end of the function, it too, doesn't need to be saved.

Conclusion

The constraints and capabilities of running Hanoi on a different platform and language present challenges and opportunities which this ZX81 implementation amply demonstrates, not in the least by the 4:30 minutes of patience needed to fully run it for 7 rings (vs <1s for the Intel 8048 version). Finally, this implementation circumvents the ZX81 BASIC's lack of support for stack data structures to reduce the amount of recursive memory needed, which begs the question: how much recursion is really needed to implement the algorithm?

Saturday, 22 October 2022

Intel 8048 Coding Guide

This is a short guide on programming the largely obsolete Intel 8048 series of Microcontrollers from the viewpoint of a conventional coding paradigm. It describes how to essentially hand compile programs notionally written in a high-level language into assembler using a consistent methodology.

We shall chose a relatively simple toy program: a towers of Hanoi application. It uses 4 LEDs for output at each step (the source column and the destination column); a single button; some simple expressions; recursion (only up to 6 levels) and a simple interrupt routine to read the button.

Register Usage

The datasheets for the 8048 series shows it is an accumulator architecture, with direct access to 8 registers [r0 to r7] with which the accumulator can perform arithmetic / logic operations and indirect access to the rest of RAM via r0 and r1, with which it can also perform a similar set of ALU operations.

So, the convention here is to use r2 to r7 for parameters, locals and temporaries; while r0 and r1 point to globals. Thus, accessing a RAM location (e.g. x) takes 2 instructions:

    mov r0,#x
    mov a,@r0

Because r0 and r1 can both be used as pointers, we can cache up to 2 globals at any one time, e.g. b^=c =>
    mov r0,#b
    mov a,@r0
    mov r1,#c
    xrl a,@r1
    mov @r0,a ;7b
Becomes a straight-forward translation. If b and c had been in r2 and r3 it would have become just
    mov a,r2
    xrl a,r3
    mov r2,a ;3 bytes instead of 7b.

The 8048 has a second bank of registers, these will only be used within interrupts. That way, we never have to save context and we can be sure that nothing we do with r0' to r7' will affect the main program.

This means we can now write the interrupt-handling code, which debounces a button press. If we assume the 8048 runs at 8MHz, then the timer will overflow every 8MHz/15/32/256 = 65Hz, which is a reasonable period for debounce. The algorithm is fairly simple, on every overflow interrupt we simply shift in the button press value to a holding register and if the bottom 4 bits are 1100, then this means that the button was seen as being debounced and off ('11') then debounced and on ('00') so a button press has occurred and the button press bit is set. The key input routine waits for that bit to be set, then clears it.

    org 7 ;start tmr interrupt at its address.
TmrInt:
    sel rb1
    in A,p1 ;button in bit 0.
    roc A ;now in carry.
    mov A,r2 ;button press history
    rlc A ;now bottom
    mov r2,A
    anl #12 ;
    xrl #12 ;zero => button!
    jnz Tmr10
    clr f0
    cpl f0 ;F0= button result.
Tmr10:
    sel rb0 ;back to main reg set
    retr ;return from int. 16b.

Key: ;wait for key.
    jf0 Key10
    jump Key ;still clear
Key10:
    clr f0 ;ready for next keypress.
    ret ;Return. 6b.

TmrInit:
    mov a,#1
    orl a,p1
    out p1,a
    sel rb1
    mov r2,#0 ;button had been 'pressed'
    sel rb0
    en tcnti
    strt cnt ;65Hz.
    ret ;11b.

;16+6+11 = 33b.

Data Structures And Stack Frames

The Tower of Hanoi recursive program is fairly simple. If we want to move all the rings from column a to column c; we first move all the rings above from column a to column b; then move a ring from column a to column c, then move all the rings above from column b to column c.

Normally, because a 8048 program is poor at handling indexed data structures, it will be best to map stack frames to static stack frames. However, for this application We'll use r2 to r7 as locals and r0 as a stack pointer. The stack pointer will push down from the top of RAM and we'll assume it's an 8048 with 64b of RAM, so r0 starts at 64. The function is equivalent to the following 'C' function:

void Hanoi(uint8_t aLevel, uint8_t aFrom, uint8_t aToo)
{
    if(aLevel>0) {
        Hanoi(aLevel-1, aFrom, aToo^aFrom);
    }
    Out(p1,(((aFrom<<2)|aToo)<<1)|1);
    Key();
    if(aLevel>0) {
        Hanoi(aLevel-1, aFrom^aToo, aToo);
    }
}

I tend to choose caller-saved conventions, so the first thing Hanoi will do is save from and too; and restore them at the end. We'll assume a 6 level Hanoi, with columns numbered as 0, to 2. We can always compute the via as 3^from^to. e.g. 0 to 2 => 3^0^2 => 1. 0 =>1 is 3^1^0 => 2. 1 to 2 => 3^1^2 => 0.

Hanoi ;r2=aLevel, r3=aFrom, r4=aToo.
    mov a,r3
    dec r0
    mov @r0,a ;push aFrom
    mov a,r4
    dec r0
    mov @r0,a ;push aToo.
    dec r2 ;if the result is 0
    mov a,r2 ;
    JZ Hanoi10
    mov a, #3
    xrl a,r3
    xrl a,r4
    mov r4,a ;from source to via.
    call Hanoi ;recurse.
Hanoi10:
    mov a,r3
    rl a
    rl a
    orl a,r4
    clr c
    cpl c ;set c.
    rlc a ;because bit 0=button input.
    out p1,a ;output the move
    call Key ;wait for button
    mov a,r2 ;level==0?
    jz Hanoi20
Hanoi20:
    mov a,#3
    xrl a,r3
    xrl a,r4
    mov r3,a ;move via to dest.
    call Hanoi
    inc r2 ;restore level above.
    mov a,@r0 ;restore from and too at the end.
    inc r0
    mov r4,a
    mov a,@r0
    inc r0
    mov r3,a
    ret ;45b

Thus it can be seen that the implementation is very simple. Initialisation involves setting up the timer interrupt and the initial Hanoi call:

    org 0;reset
    jmp Main
Main:
    call TmrInit
    mov r0,#64 ;sp
    mov r2,#6 ;6 levels
    mov r3,#0
    mov r4,#2
    call Hanoi
Main10:
    jmp Main10;14b

Thus we now have a complete implementation with user interaction, display, interrupts, expressions, data structures, locals, and recursion. It takes only 7+33+45+14 bytes = 103b in total. Note, at the time of writing, the Hanoi application hasn't been tested.

Static Stack Frame Algorithm.

The Static stack frame algorithm needs to be computed by hand. First we work out the call tree for the application. Secondly, for each function we allocate a call level to it based on the deepest call to that function. Thirdly, for each call level we allocate the number of bytes = the maximum number of stack bytes needed over the set of functions at that call level. Fourthly, we set the start address for the higher call level to the start of spare RAM and each lower call level to the start address of the previous call level + the stack bytes calculated in step 3.

Conclusion

Although the instruction set for the 8048 family is fairly comprehensive for an early 8-bit MCU and its multiple source interrupt feature makes it far better than the contemporary PIC 1655, the limited and clumsy access to memory outside of a local set of 8 registers makes coding challenging. Many of these problems were fixed by its successor, the Intel 8051.

Nevertheless, some fairly simple coding techniques provide for a fairly straight-forward and efficient coding convention.