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.

Sunday, 8 May 2022

A Tale Of Two Banners: VIC-20

 I previously posted about a Banner program I wrote for the 40th anniversary of the ZX Spectrum. One of my friend's followers tweeted that he always thought my friend was a Commodore C64 owner, who could PEEK and POKE with the best of them.

This set me thinking - what would a Commodore C64 version be like? And then, because a C64 version would be too easy, what would an unexpanded VIC-20 version be like? For sure, it's more challenging than a ZX Spectrum version.

Here's a bunch of reasons why:

  • The VIC-20 has a smaller screen area, just 22 x 23 characters; so an 8x5 character banner can't be done with 8x8 pixel characters.
  • The VIC-20 has no graphics commands. It can redefine the character set and that's about it. It can't easily PRINT AT any location on the screen.
  • The VIC-20 supports an ink colour per character, but only a global paper colour. That's because it only has 4-bits per colour byte instead of 8-bits (which gives room for an ink + paper per character). Therefore, I can't use the same colour trick as the ZX Spectrum.
  • The VIC-20's INKEY$ function (GET stringVar$) doesn't return proper ASCII upper and lower case characters, but PETSCII codes (weird).
  • The VIC-20 fouls up the character set pointer when you press Shift+[C=].
Nevertheless, I was able to do it, and here I'll describe how:














Smaller Screen Area

The VIC-20 has a smaller screen area, and if I understand it correctly, the screen can't be more than 512 characters (though they can be double-height!). Normally the screen is 22x23 characters, which isn't enough to fit 8 characters across made up from Battenberg, 4x4 pixels each. You'd need 32 characters across for that. However, it's almost enough to support 8 characters across made up from 6x6 block graphic fonts from 4x4 pixel Battenberg graphics.

And... the VIC-20 screen size can be redefined. By making it 24x21 there's room for 8 characters across x 7 characters down, even more than the ZX Spectrum!

Of course, on a VIC-20 it has to be done using POKEs:

1000 A=7504:POKE 36864,10: POKE 36867,42: POKE 36866,152


So, 36867 is the number of rows, *2 in bits 1..6, Address 36866 is the number of columns in bits 0..6. The default values were 46 and 150 respectively, so I changed them to 21*2 for 21 rows and 152 for 24 columns.

Where does all the information about the POKEs come from? Well the most concise information I've found is from here, an extensive resource on the VIC-20 memory map.

The values can be directly poked in, though I'd start with changing the dimension that gets smaller, so that the screen area is always <512b.

Finally, we need to adjust the left-hand side of the screen so that it's better centred. Address 36864 does that and changing it to 10 was found by experimentation.

There Are No Graphics Commands

However, the VIC-20 can display graphics characters, and there are Battenberg block graphics characters inherited from the Commodore PET. Strangely, and unlike the ZX81 or ZX Spectrum, they don't have a very logical order. Instead, in the sequence I'd use, the codes are:

9000 DATA 32, 124, 126, 226, 108, 225, 127, 251

9010 DATA 123, 255, 97, 236, 98, 254, 252, 160


Given that we have all the Block character graphics selected, all we need to do now is define the character set in terms of them. Unfortunately, that's not trivial either.  The first thing I did was to take a 6x6 bitmapped character set I'd used for a FIGnition example program:


 

















I have a java program which reads opens the image as a .png and then copies the pixels to an array where they can be subsequently transformed into a different image format.

I needed to be able to transform the character bitmaps so they could be represented in VIC-20 BASIC. I couldn't encode them as proper full bytes, because all 256 symbols can't be typed. I could have encoded them as 6-bit text, but again, the odd non-ascii use of VIC-20 characters made that more complex. So, I simply encoded them as 4-bit text using the characters A..O and then indexing each character (-65) in an array of Battenberg graphic characters. This meant the 96 printable characters would take up 864 bytes in themselves+ some overhead for the individual lines and BASIC commands, a good chunk the unexpanded VIC-20's 3.5kB memory space! Encoding as 6-bits could would have saved 33%, about 288 bytes.

Unfortunately, it wasn't likely to be feasible to just store the whole font in strings, so I figured that I could store them in DATA statements and then do RESTORE line to point to the right data statement where the character I wanted was defined.

Unfortunately, the VIC-20 only supports RESTORE to the beginning of the program. So, instead - yet again (and this is a common theme) I had to use memory PEEKing. I placed the data statements at the end, and when I'd read all the other data in the setup, I stored the system variable for where the DATA statement pointer was, and then literally PEEKed the right memory location to get the bytes.

It's possible to do a PRINT AT on a VIC-20 by printing the home and cursor control characters. Home is an inverse S, which you can display by literally typing PRINT " and then the home key, because the VIC-20 re-interprets keystrokes within quotes and similarly, you can move the print position to different locations by typing PRINT " and then the cursor keys themselves, for the same reason. This means that the VIC-20's screen editor, which is usually easy to use turns into a pain within quotes, because moving the cursor starts overwriting the rest of the text, so you have to wrestle with it to get it back into proper cursor mode (typing " usually works).

And colours work the same way, you type PRINT " and then a colour key and it will change the INK colour.

So, you can assign these to strings and then print "[HomeKey]";LEFT$(CD$,Y);LEFT$(CR$,X); to get the the right location, but it's fairly slow compared with poking directly into screen memory at 7680+22*row+column and of course, the cursor key technique doesn't work when the screen dimensions have been changed!

So, POKEing the screen is the best solution and you have to poke the colour attribute byte too, because the VIC-20 for some reason doesn't fill it in when it displays spaces. Clear screen, for example (PRINT "[Shift+Home]"; ) doesn't fill the attribute bytes with the current ink colour; it just clears the text bytes.

This is why in the real code I have to clear them explicitly:

FOR F=7680 TO 8183:POKE F,42:NEXT F

And the reason why it's code 42 and not 32 will be explained next:

Producing The Diagonal Stripes

I was pleased with how I generated the diagonal stripes on the ZX Spectrum, as it's a challenge when only 2 colours are allowed per character, and, helpfully enough, the VIC-20 does have a diagonal character!

Yet, doing the same thing on a VIC-20 is several times harder, because only 1 unique foreground colour can be defined per character and clearly we need two. Yet, it is just about possible, but only just!

The solution is that the VIC-20 supports 2-bits per pixel colours on a character-by-character basis, by setting bit 3 of every colour attribute byte. Each bit pair then selects one of four possible colours:

00: Which is the paper colour, bits 7-4 of location 36879.
01: Which is the border colour, bits 3-0 of location 36879.
10: Which is the auxiliary colour, bits 7-4 of location 36878 (the bottom 4 bits are the sound volume level).
11: Which is the ink colour of the character.

This means that one diagonal half can have a choice of 3 possible colours, while the other diagonal half (ink) can have a choice of 7 possible colours. We need to handle 5 colours: the black background (paper), Red, Yellow, Green and Cyan.





Using pairs of pixels also forces us to pair up the rows in the UDGs giving us an effective resolution of 4x4 for each character. You can see that the stripes are more blocky than an ideal 8x8 diagonal would be.

It also means we can't use the standard VIC-20 diagonal graphics character, because we actually need 5 different types of diagonal characters with bit pair combinations of xx/11 and 11/xx. This means we have to allocate space for a character set and in turn that means we can't use the built-in block graphics characters, we have to defines copies of those too. In total we need 16+5 characters (though in fact I used 16+6). In essence, then we need to first allocate space for the graphics characters:

5 POKE 52,29:POKE 51,80:POKE 56,29:POKE 55,80:PRINT CHR$(8);:CLR

Allocate the character set pointer to give us 64 graphics (thus the first character will be at code 64-6-16 = 42) and assign the Auxiliary and background colours.

1100 POKE 36878,112:POKE 36869,255:POKE 646,1:POKE 36879,11:P=7680

Copy over the block graphics from ROM (we could calculate them, but this is easier).

1010 READ P:P=P*8+32768

1015 FOR F=0 TO 7:POKE F+A,PEEK(P+F):NEXT F

1020 A=A+8:IF A<7632 THEN 1010

...

9000 DATA 32, 124, 126, 226, 108, 225, 127, 251

9010 DATA 123, 255, 97, 236, 98, 254, 252, 160


Generate the stripes characters:

1030 READ N,M:FOR F=0 TO 6 STEP 2:POKE A+F,N:POKE A+F+1,N:N=(N*4+M)AND 255:NEXT F

1040 A=A+8:IF A<7680 THEN 1030

...

9020 DATA 2,2,168,0,86,2,169,1,254,2,171,3


Clear the screen the hard way:

1105 FOR F=7680 TO 8183:POKE F,42:NEXT F:PRINT “[Home]”;


Then read the character codes for the stripes and place them at the right locations.

1120 FOR X=0 TO 4:READ N,M:P=8176+X

1130 FOR F=0 TO 7-X:POKE P+30720,M:POKE P,N:P=P-23:NEXT F

1140 NEXT X

...

9030 DATA 58,10,63,10,62,13,61,13,60,8


Ascii Code Conversions & Stopping Case Swapping

You can swap between Capitals + Graphics and Capitals and Lower Case (+ a few graphics) on the VIC-20 using Shift+[C=]. However, this doesn't affect what character codes are read by GET x$. Normal lower-case characters return upper-case ASCII characters and holding down shift gives the same codes + 128.

Fortunately, that's just a simple case of mapping the characters:

111 K$=CHR$((ASC(K$)+(32 AND (K$>=“A” AND K$<=“Z”)))AND 127)

Also, fixing the case swapping issue is fairly easy, it's done by printing a control character: PRINT CHR$(8) in line 5.

Conclusion

Early 80s computers had to be creative with graphics hardware, because the relatively high memory costs limited graphics detail, and lower memory bandwidth limited the range of colours. The ZX Spectrum and VIC-20, at first sight provided a very similar style of graphics, using 1 bit per pixel + an attribute byte for colour per character, but short-cuts in the colour memory (only 4-bits per character instead of 8) added even more limitations.

Consequently, porting a program from one architecture to another often involved a lot of additional work to map or work around the respective limitations. In the case of the VIC-20, a critical aspect of the Banner program (the diagonal red, yellow, green and cyan stripes against a black background) were only made possible by the VIC chip's ability to support 2 bit per pixel multi-colour graphics, plus the ability of one of those colours to be the ink colour at the character. An ordinary 2 bit per pixel graphics mode, such as that offered by the 6847 graphics chip could not have reproduced the stripes, even though, at a minimum, 96x84 pixels graphics would need 2kB of RAM vs the 932 bytes of RAM actually used.

Finally, even the differences in the implementation of what was accepted as the standard microcomputer language: BASIC could have serious ramifications; and often hacking directly into the OS or memory map was the only solution.

The Banner program is a great, and simple way of exploring the architectural differences, and at the end of it, it's fun to type out colourful chunky characters across the whole screen!

The Listing

Finally, here's the listing! There's about 1kB free on the unexpanded VIC-20 once it's been typed in. In VICE it's possible to copy and paste a line at a time, but you need to convert the characters to lower-case first!

5 POKE 52,29:POKE 51,80:POKE 56,29:POKE 55,80:PRINT CHR$(8);:CLR

10 POKE 36869,240:GOSUB 1000

100 FOR X=0 TO 2:BG(X)=PEEK(P+48+X):POKE P+48+X,54:NEXT X

110 GET K$:IF K$=“” THEN 110

111 K$=CHR$((ASC(K$)+(32 AND (K$>=“A” AND K$<=“Z”)))AND 127)

112 F=ASC(K$):IF F<32 AND F<>13 THEN 110

113 FOR X=0 TO 2:POKE P+48+X,BG(X)::NEXT X

116 IF ASC(K$)=13 THEN P=INT((P-7680)/24)*24+7752:GOTO 160

120 C=ASC(K$)-32:C=C0+(C AND 3)*9+INT(C/4)*44

130 I=INT(RND(0)*7)+1

140 FOR Y=0 TO 2: FOR X=0 TO 2:POKE P+X,PEEK(C+X)-23:POKE P+X+30720,I:NEXT X

150 P=P+24:C=C+3:NEXT Y:P=P-69

155 P=P-7680:P=(P-INT(P/24)*24)+INT((P+48)/72)*72+7680

160 IF P>=8184 THEN P=7680

170 GOTO 100

999 POKE 36869,240:POKE 36864,12:POKE 36866,150:POKE 36867,174:STOP

1000 A=7504:POKE 36864,10: POKE 36867,42: POKE 36866,152

1005 DIM BG(3)

1010 READ P:P=P*8+32768

1015 FOR F=0 TO 7:POKE F+A,PEEK(P+F):NEXT F

1020 A=A+8:IF A<7632 THEN 1010

1030 READ N,M:FOR F=0 TO 6 STEP 2:POKE A+F,N:POKE A+F+1,N:N=(N*4+M)AND 255:NEXT F

1040 A=A+8:IF A<7680 THEN 1030

1100 POKE 36878,112:POKE 36869,255:POKE 646,1:POKE 36879,11:P=7680

1105 FOR F=7680 TO 8183:POKE F,42:NEXT F:PRINT “[Home]”;

1120 FOR X=0 TO 4:READ N,M:P=8176+X

1130 FOR F=0 TO 7-X:POKE P+30720,M:POKE P,N:P=P-23:NEXT F

1140 NEXT X

1150 C0=PEEK(65)+256*PEEK(66)+7:P=7680

1999 RETURN

9000 DATA 32, 124, 126, 226, 108, 225, 127, 251

9010 DATA 123, 255, 97, 236, 98, 254, 252, 160

9020 DATA 2,2,168,0,86,2,169,1,254,2,171,3

9030 DATA 58,10,63,10,62,13,61,13,60,8

9100 DATA“AAAAAAAAAAKAACAACAFFAAAAAAANNINNIBBA"

9110 DATA"AOIBOADKAPECEGICBCJIAJMCBCCAKAAAAAAA"

9120 DATA"AJAAKAABAAGAAFAACAIIIFPACCCAKADLCACA"

9130 DATA"AAAAEAACAAAADDCAAAAAAAAAACAAECECACAA"

9140 DATA"JHIOCKBDAEKAAKABDADDIJDADDCDDIBDIDDA"

9150 DATA"EHAONIABALDCDDIDDAEDCLDIBDADDKAJAACA"

9160 DATA"JDIJDIBDAJDIBHCBCAAIAAIAAAAAAAACAECA"

9170 DATA"AJABIAABAMMIMMIAAABIAAJABAAJDIADAACA"

9180 DATA"JLIKDCBDCJDILDKCACLDILDIDDAJDCKAABDC"

9190 DATA"LGAKECDCALDCLDADDCLDCLDACAAJDCKDKBDC"

9200 DATA"KAKLDKCACBLAAKABDAAHCAFABCAFECFGABAC"

9210 DATA"FAAFAABDCOEKKCKCACOAKKGKCACJDIKAKBDA"

9220 DATA"LDILDACAAJDIKGKBDCJDILLACBCJDABDIBDA"

9230 DATA"DLCAKAACAKAKKAKBDAKAKGECACAKAKKKKBBA"

9240 DATA"GECEGACACGECAKAACADHCECADDCALAAKAADA"

9250 DATA"GAAAGAAACAHAAFAADAEGAAAAAAAAAAAAAMMM"

9260 DATA"EDAHCADDCEMAKFABDAOIAKFADCAEIAKAABCA"

9270 DATA"ENAKFABDAEIALDABDAEDAFDABAAEMAGNAEJA"

9280 DATA"OIAKFACBAAIAAIAACAACAAKAECAKAALLACBA"

9290 DATA"KAAKAABCAEIAPFACBAMIAKFACBAEIAKFABCA"

9300 DATA"JGAOJACAAJGAGNAABCAMAFAABAAAMABIADAA"

9310 DATA"FIAFAAADAIEAKFABCAIEAOCACAAIEAPPABCA"

9320 DATA"IEAFKACBAIEAGNAEJAMMAECADDAAJABKAABA"

9330 DATA"AKAAKAACABIAALABAAJJAAAAAAAJHIKHKBDA”