Showing posts with label Motorola 68000. Show all posts
Showing posts with label Motorola 68000. Show all posts

Tuesday, 19 November 2024

The Mythical Mac 256K

Introduction

This MacGUI blog post covers the boot process for the early System Software on a Mac 512K (or Mac 128K). In part of it, he talks about the Mythical Mac 256K.

At the beginning of the boot blocks are several stored parameters. The version number is two bytes. Another two bytes hold flags for the secondary sound and video pages. Then come a series of seven, 16-byte long file names. These are the names of the System resource file, Finder (or shell), debugger, disassembler, startup screen, initial application to run, and clipboard.

Following the names is a value for how many open files there may be at once, and the event queue size. Then come three, 4-byte values which are system heap size for a Mac with 128K of RAM, 256K, and 512K of RAM, respectively. If you could find me a Mac with 256K of RAM, I'd sure be impressed.  Wink

<snip>

If you landed from planet Krypton with a Mac 256K, your system heap size would be $8000 (decimal 32,768).

Disappointingly, as he also covers in this blog post, the 64K ROM only checks for the 128K or 512K Mac models and the 128kB model memory size is defined explicitly by the line:

4002E4: LEA $0001FFFC,A1 ;128K RAM

The tests work by exploiting incomplete memory decoding on early Macs. A Mac 128K's memory appears to be duplicated 4 times (actually it's duplicated 32 times all the way up to 4MB); whereas a Mac 256K is duplicated twice and a Mac 512K's memory doesn't get duplicated at all (up to 512K).

First (Failed) Attempt

I took a 64K Mac ROM and changed it to $0003FFFC, then added 2 to the 3rd byte of the ROM checksum to adjust it, which is in the first long word of the ROM. Then I added a miniVMac option for a Mac 256K, hacking about with a few scripts and some of the source code. I thought I'd correctly achieved a 256kB Mac, but I hadn't - for my 19kB MacWrite test document I had misread the MacWrite splash screen, which says how much memory is free, and used. I'd mistaken 'free' and 'used' for each other, because the values were fairly plausible for a Mac 256K.



PICO-Mac

I left the problem for quite a while after that, not knowing how to test it, but also because I lost the HDMI cable adapter for the Raspberry Pi 5 I was developing it on. Then Matt Evans referenced my post on his Axio.ms blog when creating a Raspberry Pi Pico based Mac emulator. I myself have another design for a PICO-based Mac, which would be able to fit the entire 68K emulator in its 16kB of SPI flash cache and still run at full-speed, allowing for a full 256kB of RAM on a standard PICO. This is for the future, if ever.

PCE-Mac Emulator

The key to being able to solve the problem is to use the PCE-Mac emulator, which has a step-time debugger, allowing me to figure out what really went wrong. 

What it looks like is this: Apple originally wanted to be able to support a 128KB, 512KB Mac, or a 256KB Mac at least for development purposes. Perhaps some of the engineers wanted to be able to bolt on another 128KB just for a bit more space. That made sense in 1983, because 256kBit RAM chips didn't yet exist and a 512kB Mac would need 64 RAM chips; whereas a 256kB Mac would only need 32. 

However, either they made a mistake in the ROM, which they tried, but failed to correct in the disk Boot block, which prevented them from doing that. The ROM itself is at fault, because a 256kB Mac will look like a 512kB Mac to the ROM. Here's the ROM code that works it out:

FindMemSize:;
4002DE:LEA $7FFFC,A0; 512K RAM
4002E4:LEA $1FFFC,A1; 128K RAM
4002EA:CLR.L (A0)+
4002EC:TST.L (A1)+
4002EE:BNE.S 4002F2; branch if 512K
4002F0:MOVE.L A1,A0; this is a 128K machine
4002F2:MOVE.L A0,$0108; set MemTop

A0 is set to point to the last word of a Mac 512K and A1, the last word of a Mac 128K. Initially all the RAM is set to $ffffffff. At $4002EA, the last word of a 512K machine is cleared and then the last word of the Mac 128K is tested. On a Mac 512K it won't be clear, so it won't be 0, so it'll branch and A0 will be used as is to define MemTop. On a Mac 128K it will be clear so it will be 0. It won't branch and so A1 (=128K) will be copied to A0 and then used for MemTop.

However, on a Mac 256K, the long word at $1FFFC also won't be cleared, so the ROM will think it's a Mac 512K. Nevertheless, it won't work to hack the ROM to make line 4002E4 LEA $3FFFC,A1 for a 256kB Mac, because:
  • It would mess up the ROM test for a 128KB Mac.
  • It turns out the Boot block on the Floppy Disk does its own test, explicitly for a Mac 256K and does the wrong thing if MemTop is set to 256K.
This is the critical part of the boot block:

0001008A:2238 0108MOVE.L $0108, D1
0001008E:4841SWAP D1
00010090:0C41 0004CMPI.W #$0004, D1
00010094:6E2CBGT.S $000100C2
00010096:7200MOVEQ #$00000000, D1
00010098:50F9 0001 FFF0ST $0001FFF0
0001009E:42B9 0003 FFF0CLR.L $0003FFF0
000100A4:4AB9 0001 FFF0TST.L $0001FFF0
000100AA:6716BEQ.S $000100C2
000100AC:7002MOVEQ #$00000002, D0
000100AE:4840SWAP D0
000100B0:D1B8 0108ADD.L D0, $0108
000100B4:D1B8 0824ADD.L D0, $0824
000100B8:D1B8 0266ADD.L D0, $0266
000100BC:D1B8 010CADD.L D0, $010C
000100C0:7204MOVEQ #$00000004, D1

Initially it checks for a 512kB Mac at $10090 and if it isn't falls through to the clever, but weird section at $10098 to $100A4. This code sets the top of RAM for a 128KB Mac to -1, then explicitly clears the top of RAM for a 256KB Mac and if the address at the top of a 128kB Mac has become 0, then it knows that the address range from $20000 to $3FFFF mirrored $00000 to $1FFFF and therefore it's a 128KB Mac. It will then execute the BEQ skipping the underlined code which adds $20000 to all the key system variables, starting with MemTop.

The upshot is that the boot block expects a Mac 256 to be set up as a Mac 128 in the ROM; and then the system variables are adjusted. And of course the ROM won't do that: a Mac 256 is set up as a Mac 512 in the ROM so the BGT instruction at $10094 skips the whole routine. They therefore could have fixed the boot block to check MemTop for a 512kB Mac and if it's really a Mac 256, subtract 256K from all the system variables.

Because they didn't do that, the ROM has to be changed.

A Correct ROM Fix

The answer is to save bytes on the LEA instructions. It's possible in some cases to generate large values in data registers by using the sequence MOVEQ, then SWAP. It takes just 2 words instead of 3 and because 128kB, 256kB and 512kB all have 0s in the bottom 16-bits, we can use this technique (the boot block uses this technique to test for 256kB too!). However, we need a data register to do this and I can't be sure it won't corrupt a useful data register value unless the execution path later overwrites a data register.

Fortunately, it does. At $322, there's a MOVEQ #2,d1. So, now we know we can use D1. The trick then is to set up both A1 and A0 to point to 256kB and 512kB respectively. My sequence is 1 word shorter than the original. Then, instead of clearing and testing the last words and post-incrementing to get the sizes; we pre-decrement from the sizes to get to the last words of RAM and test them. This will return the same result for both 128kB and 256kB Macs, but a different result for the 512kB Mac.

We don't need A0 and A1 now, because the actual size (for a 512kB Mac) is already in D1 so we can use our word of ROM space saved to move D1 to A0. And for the 256kB or 128kB cases, we can take the 512kB value; divide by 4 (by shifting right twice) to get 128kB, which is also a 1 word instruction substituting the MOVEA A1,A0 in the original code. This means that ROM routine now looks like this:

000002DE:7204MOVEQ #$00000004, D1
000002E0:4841SWAP D1
000002E2:2241MOVEA.L D1, A1
000002E4:D281ADD.L D1, D1
000002E6:2041MOVEA.L D1, A0
000002E8:42A0CLR.L -(A0)
000002EA:4AA1TST.L -(A1)
000002EC:6602BNE.S $000002F0
000002EE:E489LSR.L #$2, D1
000002F0:2041MOVEA.L D1, A0

We also need to make a change to the checksum again, which is a sum of the 16-bit words in the ROM. It now needs to be $28BA8FB5 instead of $28BA61CE.

Results

My 19kB MacWrite document now reports a reasonable value for a 256KB Mac:
On a Mac 512K it'd be 95%:5%. For testing purposes I wrote another program called FreeMem, which allocates 1kB blocks until it runs out of memory. The app itself uses about 260 bytes. There is a proper API for this, but I found that it wasn't reporting the value I was expecting. On a Mac 128 Freemen goes up to 75kB and on a Mac 512 it's about 430kB or more. On this Mac 256 it's 188kB:

This is only possible for a Mac 256K

In Matt Evans' blog post he said that it wasn't possible to run MacPaint on a Mac 256 disk, because it writes data to the boot blocks. I have tested this out and found that in fact it does run OK. Here's a screenshot, even though it's not actual proof since MacPaint can't tell you how much RAM is free AFAIK.

Conclusion

The Mythical Mac 256K has been talked about for a number of years and perhaps a few even existed during the early Macintosh development period. I tried, but failed to create one under the miniVmac emulator in December 2023, but after it was mentioned in Matt Evans' Pico-Mac blog and a couple more times on the 68KMLA when discussing a Pico-Mac based on the new Raspberry Pi PICO 2, I decided to explore it again. It turns out it really is possible and as far as I know, this is the first Mac 256 that actually runs, albeit in emulation!

But what's the point? Well, there's a real digital archeological purpose behind the Mac 256K, simply because the floppy disk Boot block code for System 1 up to (I believe) System 4.1 contains code for it. It seems unlikely, that someone would make the effort to create such an arcane hack if no-one considered the possibility of a Mac 256K. Nevertheless, the ROM won't allow a Mac 256K to boot properly. It is possible that a Mac 256K could have been built though, all an engineer would have to do is build a toggle-switch logically OR'd to address line A17; start up a Mac with the toggle switch connected to 5V (the Mac would then look like a Mac 128K); then flip the switch (which would enable A17, which would enable the second bank of 128K RAM) and insert a boot disk: The Boot disk would then see 256K and adjust the system variables.

The second reason is that in the 1983 to 1985 period, microcomputers with 256K of RAM were at the upper end of the market and the Mac was intended to be released in 1983 (and even before if it had been possible). The IBM PC/AT, for example was released in 1984 with 256kB as standard. A few early Atari ST's were sold with 256kB of RAM in 1985.

The third reason is that Apple already knew that the Mac 128K's RAM was limited to the point of near impracticality. MacWrite could only store a document with a few pages on it. However, a Mac 256K is far more usable. The extra 128kB of RAM can store between 16,000 to 18,000 more words in a document; enough for about 32 to 36 pages, and easily enough to store an undergraduate dissertation or a whole chapter of a book. 256K is big enough for a proper, small Pascal compiler (note: MacPascal was an interpreter); making development much more feasible.

On the downside, a Mac 256K would have needed 32 of the 64kBit chips the Mac was supplied with and this would have made the motherboard bigger, more costly and perhaps affected the shape and design of the Mac itself. The Mac 512K on the other hand, still only needed 16 RAM chips - in fact the motherboard was designed to accommodate them by simply fitting the new chips and cutting a track.

Next Steps

The 64K ROM for the Mac 256K can be downloaded from the 68KMLA thread. The next two major steps really are to use it with PICO-Mac running on a Raspberry Pi PICO 2 (which gets it closer to the hardware); and perhaps at the same time, get someone to burn the ROM and install it on a genuine Mac 128K, fitted with 256K of RAM (this is a big ask).

In the meantime I'll see if can get it to work with miniVMac and continue to work on M0̸Bius, my super-fast 68K emulator in M0+ assembler.

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.

Tuesday, 3 November 2015

Parallel CRC16 Collection 2


Hello folks,

I recently posted a collection of byte-parallel CRC16 implementations and thought I'd add a few more.

Firstly, a 6800 version. Since I don't have the original BSI volume, I don't know exactly how it was coded then, but I vaguely remember it being about 26 instructions long. The 6800 version is actually shorter and faster than my 6809 version despite the 6809 being a far better CPU with better CPI and more registers. That's because I end up having to use 0 page for speed and that's still faster than stack indexing:


Crc16_6800: ;a:b=CRC, X^data.
;uses page 0 temp0, temp1, Len.
Crc16_6800Loop:
eora 0,x
staa temp1 ;hi now swapped
lsra
lsra
lsra
lsra
eora temp1
staa temp1
stab temp0
lsla
lsla
lsla
lsla
eora temp0
staa temp0
rorb
rorb
rorb
tba
anda #31
eora temp0
rorb
andb #0xe0
eorb temp1
inx
dec len
bne Crc16_6800Loop
rts

Next, the Hitachi H8 version. In this version, R0=CRC, R1^data, R2=Len. R3 is a temp. The H8 is interesting, because it was part of a 1990s generation of Microcontrollers designed for high-level languages like 'C'. Hitachi claimed it was RISC, though it isn't due to the excessive number of instruction formats and addressing modes. It's more like a 16-bit micro-controller version of the 68000 or pdp-11. The performance looks decent at 57 ø-clock cycles, but it isn't really that great because a ø-clock cycle is half the XTAL clock frequency, making it 114 XTAL oscillations per CRC byte.

Crc16_H8:
push r3
Crc16_H8Loop: ;start by pretending to swap.
mov.b @r1+,r3h ;get the next byte.
xor.b r3h,r0h ;
mov.b r0h,r3h ;need a copy.
shlr r3h
shlr r3h
shlr r3h
shlr r3h ; >>4
xor.b r3h,r0h ;
mov.b r0h,r3h ;copy again.
shll r3h
shll r3h
shll r3h
shll r3h ; <<4
xor.b r3h,r0l ;Into what was the low byte.
mov.b r0l,r3h
rotr r3h
rotr r3h
rotr r3h
mov.b r3h,r3l
and.b #31,r3l
xor.b r0h,r3l
and.b #0xe0,r3h
xor.b r0l,r3h
mov.w r3,r0
subs #1,r2
bne Crc16_H8Loop
pop r3
rts

So, let's compare that with the PIC I originally coded it for, a PIC16C55. This isn't the actual code, I don't have rights to that, so I've just rewritten the equivalent algorithm, and anyway, the version I used would probably have calculated the CRC on the fly, as each byte was received. Surprisingly, the humble PIC cpu (which predates Commerical RISC CPUs by a decade), manages to implement the algorithm in 25 instructions and 25 instruction cycles per byte, which works out as 100 clocks per byte.

Crc16_Pic16C55: ;pretend we've swapped.
movf ind,w ;got the next byte.
incf fsr
xorwf gCrc16+1,f ;xor with lo byte.
swap gCrc16,w ;copy and swap.
andlw 15 ;this is the >>4!
xorwf gCrc16+1,w ;'lo' byte again.
movwf gTemp2
swap gCrc16,w ;copy and swap
andlw 0xf0
xorwf gCrc16,f ;this time into 'hi' byte.
rrf gTemp2,w ;get 'lo' mostly calc'd Crc.
movwf gTemp1 ;save in temp1.
rrf gTemp1,f ;Have to rotate into f, because
rrf gTemp1,f ;can't rotate w by itself.
movf gTemp1,w
andlw 31
xorwf gCrc16,w
movwf gCrc16+1 ;OK to overwrite, since new 'lo' byte in gTemp2
rrf gTemp1,w ;and again, because of the carry.
andlw 0xe0
xorwf gTemp2,w
movwf gCrc16
decfsz gCrcLen
goto Crc16_Pic16C55
retlw 0 ;done.

We've mentioned the 68000 CPU a couple of times, so let's compare it. Here, we can see how the 68000 is well suited to high-level languages, the straight-forward implementation closely matches the algorithm and is  only 19words, 38 bytes long, almost as short as a Z80! The timing on the other hand - yike, 134 cycles per byte, only slightly more efficient than a Z80. This is mostly due to the fact that a 68000 had a 4 clock bus cycle, but also because the 68000 isn't very good at byte manipulation. On the other hand, the 68000 has a decent decrement and branch instruction.

Crc16_68K:
move.l d2.w,-(sp)
bra.s Crc16_68KWhile
Crc16_68KLoop:
rol.w #8,d0 ;this time we have to rotate to begin with.
eor.b (a0)+,d0 ;
move.b d0,d2 ;crc&0xff just need byte ops this time
lsr.b #4,d2 ;..>>4
eor.b d2,d0 ;crc^= above calculation.
move.b d0,d2 ;crc&0xff (we'll shift out upper 8 bits).
lsl.w #8,d2 ;..<<8
lsl.w #4,d2 ;..<<4
eor.w d2,d0 ;crc^= above calculation.
moveq #0,d2 ;(Faster than a move.b and and.w #0xff)
move.b d0,d2 ;crc&0xff
lsl.w #5,d2 ;..<<5
eor.w d2,d0 ;done.
Crc16_68KWhile:
dbra d1,Crc16_68KLoop
move.l (sp)+,d2
rts

Finally, let's also compare it with the 68000's arch-rival, the 8086. Here we can see that the 8086 isn't so well suited to a high-level language, mostly due to the optimisations needed for the byte operations and judging single bit shifts vs shifting with cl. The length is the second worst at 54 bytes and the standard instruction timings for the 8086 would give a total cycle time of 80 cycles (as efficient as a 6809) but this fails to take into account how the prefetch queue empties when executing linear code whose instructions are < 4 cycles. This bumps it up to 112 cycles per byte, which means a standard 8MHz 68000 would be about 33% faster than a 5MHz 8086. Both of them, however, are much faster than a pdp-11/34, (2.8x and 2.13x respectively).

Crc16_8086:
push dx
Crc16_8086Loop:
mov dh,al ;2(4)
mov dl,ah ;swap to begin with. 2(2)
lodsb ;al=[si]+ 12(6)
xor dl,al ;3(5)
mov al,dl ;2(3)
shr al,1 ;2(1)
shr al,1 ;4(0)
shr al,1 ;4(0)
shr al,1 ;4(0), still faster than mov cl,4: shr al,cl
xor dl,al ;4(0)
mov al,dl ;4(0) upper byte.
shl al,1 ;4(0)
shl al,1 ;4(0)
shl al,1 ;4(0)
shl al,1 ;4(0)
xor dh,al ;4(0) the <<12
xor ah,ah ;4(0)
mov al,dl ;4(0)
shl ax,1 ;4(0)
shl ax,1 ;4(0)
shl ax,1 ;4(0)
shl ax,1 ;4(0)
shl ax,1 ;4(0) Perform a straight-forward <<5
xor ax,dx ;4(0)
loop Crc16_8086Loop ;17c. 2 bytes fetched = 4c, so 13c can refill BIU
pop dx
ret