Locomotive BASIC
This article needs additional citations for verification. (December 2009) |
First appeared | 1984 |
---|---|
OS | AMSDOS |
License | Proprietary |
Influenced by | |
Locomotive Basic is a proprietary dialect of the BASIC programming language written by Locomotive Software.
It was modified (many custom features to support the platform) and used on the Amstrad CPC as "Amstrad BASIC" (where it was built-in on ROM).
Later Locomotive BASIC-2 was produced for the IBM PC compatibles platform as a GEM application on the Amstrad PC1512 and 1640 and was a descendant of Mallard BASIC,[1] the interpreter for CP/M supplied with the Amstrad PCW.
There are two versions of Amstrad; BASIC 1.0 which only came with the CPC464 (and had a buggy DEC$
function), and BASIC 1.1 which corrected this and shipped with all other CPCs. BASIC 1.1 was also included in the Amstrad CPC Plus series machines, as part of the included game cartridge.
Development
[edit]Development was based on existing work recently undertaken writing Mallard BASIC for Acorn Computers Z80 addon for the BBC Micro. It is reported to have taken around 12 weeks to enhance the existing code, and was "very influenced" by BBC BASIC, though adding additional functions to do things that would have required assembly language on the BBC.[1]
Locomotive created the "ARNOLD" platform (the CPC464 development codename) as solid enough to attract the "serious" users Amstrad wanted and this is demonstrated in the approach to the firmware and hardware, which methods were almost unheard of in any one contemporary platform - the tidy approach to firmware access with fixed jump blocks, the hypothecation of the firmware into modules, the clever hardware allowing expandability, with multiple pages of RAM and ROM all occupying the same address-space, simultaneously accessible, re-locatable display memory etc. many of which appeared only partially, or not at all on rival platforms.
Features of the CPC platform
[edit]If a programmer is concerned only with writing in a rich BASIC dialect they have an excellent machine on which to ply their skills, but to fully comprehend the ethos behind Amstrad BASIC, it is arguably good to understand, at least superficially, the design approach to the machine as a whole.
Fundamentally, the CPC platform consists of clever hardware with a rich firmware to exploit it; compartmentalised into sub-sytems with standardised jump tables to access functionality. Amstrad BASIC was written to expose as much of this platform and firmware as possible to the programmer.
Firmware jump blocks
[edit]At a machine level on the CPC platform, application software need not be concerned with code moving about in the actual firmware (e.g. for different versions and re-assembly) so long as it uses the documented jump block addresses. For example, the firmware call TXT_PRINT_CHAR is ubiquitous - but note from below the firmware vectors are different in BASIC 1.0 and BASIC 1.1 ($9400 vs $93FE). In many machines, re-assemblies of the firmware and subsequent moving of routines can cause major headaches, but with the CPC platform so long as software uses the published jump block calls (e.g. $BB5A), software will run on all CPC platforms with no issues.
The firmware addresses have changed between versions but the address for TXT_PRINT_CHAR remains $BB5A.
The firmware modules
[edit]Kernel Key Manager Text VDU Graphics VDU Screen Pack Cassette / AMSDOS (AMStrad Disc Operationg System) Sound Manager Machine Pack Maths Pack
Amstrad BASIC leveraged and exposed the platform being written to take full advantage of the machine, bundling up BASIC code into the requisite calls into the firmware[2], providing specific commands for functionality rather than reliance on generic *FX or PEEK & POKE statements required to access features on competitor platforms.
BASIC and version differences
[edit]Internal version numbers of the BASIC ROM differ from the agreed, published versions shown below.
Although not unique, the platform was unusual among contemporaries in that it used the ASCII character set rather than its own idiosyncratic form - an arguable strength as it instantly made the CPC platform a more serious offering, somewhat underpinning Amstrad's marketing claims of it being capable of professional business use.
In common with many other dialects, multiple program statements were separated on a single line using the colon :
.
WHILE/WEND
is the only loop construct other than FOR/NEXT
.
Reserved words (functions, commands, statements etc.) are capitalized in a program by virtue of being tokenized. This reduces program size and accelerates execution. Listing a program then decodes the token to its reserved word which are all in upper case in the token table (it was this method which revealed the presence of DEC$ in BASIC 1.0 - see below). When saving a basic program, the fully tokenized form is saved in a binary format. Alternatively, saving with option "A" creates a fully text compatible ascii file e.g. SAVE "MYPROG",A
BASIC 1.0
[edit]Supplied with the Amstrad CPC464, it was a good release with only a single notable bug, being the DEC$()
function (it provides PRINT USING
style output but as a string rather than screen output). No time was available to fix this before the software had to go for ROM chip production. Consequently, the function was simply left un-documented and pre-fixed to force a syntax error when encountered. The curious programmer found the word dec$
capitalised in their code indicating it was a recognised reserved word - inexplicable at the time, until the history of the bug and disassemblies of the BASIC ROM were available.
Exploration of the basic code can find the remnants of the function and it's crippled token table entry.
BASIC 1.1
[edit]Supplied with the CPC664/6128, besides fixing DEC$, it introduced (among others) the following aditional keywords:
COPYCHR$, CURSOR, FILL, FRAME, GRAPHICS PAPER, GRAPHICS PEN and MASK
It also introduced a clumsy but adequate method of trapping disc errors with the DERR
function. Errors still had to be trapped in the usual way with ON ERROR GOTO ...
but the single "Broken In" error 32 could now be expanded to provide more detail. Examining DERR gives up to ten further indicators. Values returned are the AMSDOS error number plus 128, e.g. 14 is reported as a DERR value of 142 (14+128). Possible errors included the usual file not found, disk full etc. as well as the more specific "142 - The stream is not in a suitable state" and "149 - Disc changed while files were open". DERR is a function of BASIC 1.1 and so was not available on the FD1 expanded CPC464, with its combination of AMSDOS and BASIC 1.0. Disc errors on the CPC464 were obtainable but relied on PEEK
ing into the AMSDOS system area.
Variables and functions
[edit]Variable names must begin with a letter but can have up to 40 character long names made up of A-Z
, the digits 0-9
and the .
symbol. Names are case-insensitive, but case-as-typed is maintained in program listings.
Three data-types are supported signified by an identifier suffix on the variable name and can be selectively presumed by use of the DEFINT, DEFREAL
and DEFSTR
directives:
- Strings with type marker $, e.g.
MyString$
, up to 255 bytes in length - 2-byte signed integers with type marker %, e.g.
My.Integer%
, in the range -32768 to +32767 - 5-byte real (floating point) with type marker !, e.g.
My99Real!
, in the approximate range +/-10E+38 to +/-4E-39 with nine significant digits. Type real is the default for un-typed variable names. All Floating point mathematics is the result of Amstrad BASIC exposing the MATHS_PACK of the firmware (very complete and entirely usable from machine code).
Names are only unique within type, e.g. S% and S! are two different variables.
Dimensioned variables (arrays) have 0 as their lowest element number(s), and can use square brackets around their elements which helps distinguish them from other parentheses in complex expressions in program code. e.g A(3)
can be written as A[3]
.
Named single-line functions are supported and follow the type and name convention of variables but pre-fixed with FN
. e.g. FNMy.Func!
The address of a variable in memory can be determined with the @ directive, e.g. PRINT @A$
. This replaces the VARPTR() function found in other dialects of BASIC and is mostly used to pass a variable pointer to machine code routines allowing bi-directional data transfer.
Integer and Real variable values are stored in the variable table (see memory map below). String variable contents are stored on the heap. This allows the aggregation of fragmented memory via Garbage collection (computer science). Consequently, for strings, the variable table held a three byte descriptor made up of the length of the variable and the address in the heap where the actual contents are to be found.
Two missed opportunities?
[edit]BBC Basic uses a clever method for handling some integer variables, which despite claims of it heavily influencing Locomotive BASIC (and by extension Amstrad BASIC), was not copied in the latter. BBC BASIC has a 54-byte table reserved for the values of the 27 single letter integers @%, A%, B% ... Z%
so they did not occupy nor require lookup in the variable table - a simple left-shift and addition was all required to determine the value of a given variable. This makes access to the subject variables very fast and considering their wide range of use it is, perhaps, surprising this mechanism was not used in Amstrad BASIC.
For strings, the descriptor is three bytes in the variable table, made up of a length descriptor and a further (little-endian) address of the actual payload. Had the value returned by @ been the two byte address in the heap (and the length prefix moved to the heap also) this would have substantially reduced access time by replacing the two address lookups with just one.
Variable table structure for strings.
Graphics and colour
[edit]Handling of graphics and colour was straightforward with commands such as DRAW, PLOT, INK, PAPER, BORDER
etc.
With Amstrad BASIC V1.1 (CPC664 onwards), the FILL
command permitted painting complex-shaped, bounded areas of the screen and an eight bit mask could be applied DRAW enabling dotted lines etc.
A table giving the numeric codes for the 27 system colors was printed on the casing of the built-in 3" disk drive on the 664 and later machines.
Text handling
[edit]Text moved through the system using a concept of "streams" numbered 0-9, appropriate commands used the hash #
symbol to pre-fix the stream number.
Streams 0-7 are text windows. The TEXT_VDU firmware pack allows for the creation of multiple text windows each with their own cursor, dimensions, content, colours etc. An individual window is addressed using its stream number, e.g. PRINT#3,"Hello World!"
. Omitting this defaults to stream 0 - PRINT"Hello World!"
being equivalent to PRINT#0,"Hello World!"
. Text windows are simple screen areas and can overlap but there is no built in provision to preserve the contents of windows "under" others, thus careful attention is required to avoid corruption. Later, machine code routines became available to intercept the firmware calls (by patching the jump block) and preserve the content of a window just before another was drawn over it, restoring it when the new window closed. This gave the much more useful "Windows" look to which we have grown accustomed - being mindful of the times; "pop-ups" were a novel concept in 1984. When using the text windows, screen scrolling relies on software (as only part of the screen content is moved) rather than using the hardware to scroll the entire screen by adjusting the screen-base offset of the CRT controller in RAM. Two SCREEN_PACK firmware calls are provided for the two forms; $BC4D SCR_HW_ROLL (Scrolls the entire screen up or down by eight pixel rows i.e one character line) and $BC50 SCR_SW_ROLL (Scrolls part of the screen up or down by eight pixel lines). The latter can result in a pronounced "ripple" when scrolling large areas.
Stream 8 is the centronics parallel port. To send a line of text to the printer one could use the command PRINT#8,"Hello World!"
. Creating a printed listing was simply a matter of LISTing the program to stream 8, e.g. LIST#8
. This flexible method dispenses with the legacy BASIC commands LPRINT
and LLIST
. When later, serial interfaces became available and opened up access to a greater range of printers and other devices, #8 could be diverted by patching $BD2B MC_PRINT_CHAR in the MACHINE_PACK jumpblock.
Stream 9 is the currently open file (either casette or disc, input or output) and relevant commands are context sensitive; e.g. it is possible to have both input and output files open simultaneously with PRINT#9
and INPUT#9
routing text accordingly.
Advanced features
[edit]A stand-out feature among almost every other BASIC of the time was a timer-based software interrupt mechanism at 50 ticks per second using the EVERY
, AFTER
and REMAIN
commands (leveraging the KERNEL for its software interrupts). Four timers were available (0-3, with associated reducing priority (a BASIC imposition not a platform limitation) and allowed the programmer to run sections of their BASIC program, asynchronously, after a given delay, repeating if required, e.g. EVERY 50,0 GOSUB <line>
produces a repeating one-second call with no further work. When answering an EVERY or AFTER ... GOSUB, there is an implicit DI (see below) to all lower-priority interrupts which is automatically cleared by the corresponding RETURN
. As with all Interrupt Service Routines (ISR), such GOSUBs should be kept as short as possible, rather setting a flag for further processing of large chunks of program than trying to complete it inside the ISR.
The interrupt ability requires some method to limit its impact when unwanted. Two commands; DI
and EI
(which obviously draw on the Z80 heritage of the platform) Disable and Enable BASIC interrupts respectively. DI should be used sparingly as it will interfere with all BASIC interrupt driven functionality: timers, colour flashing, keyboard scanning, sound output etc., thus periods of DI should be confined to short essential sections of code.
Amstrad BASIC granted a relatively high level of control over the sound chip, an AY-3-8912 with 3 melodic channels and 1 noise channel and interrupt driven sound generation with comprehensive co-ordination of the three audio channels and associated BASIC commands: SQ()
(Sound Queue), ON SQ <channel> GOSUB <line>
etc. The same chip was also used on late-model ZX Spectrums, as well as the Atari ST and MSX computers, but none of those had such a complete built-in SOUND
command. Many things, from selecting a particular channel or a combination of channels, setting envelopes, volume, pitch, noise, and so on could be done with a single SOUND
command, with up to 7 parameters. Granted, especially complex and/or low-level techniques could not be done with BASIC due to their requiring more precise or direct access to the hardware, e.g. especially complex music from trackers (including simulated chords using arpeggios, etc.), the playback of digitally sampled sounds as in the game RoboCop for example, and so on.
All disc, tape and file management managed by BASIC is through the firmware and were adequate for simple file management with commands such as SAVE, LOAD, MERGE, RUN", CAT
etc. During this time, it was common for desktop computers to be "boot to BASIC" and that supplied with most low-cost home computers also acted as a simple operating system, managing files and moving program code and data between the machine and storage medium.
Also available was a parametric LOAD
command, allowing, for example, to load a file containing "raw" picture data into video memory, causing it to be displayed, with a couple of BASIC instructions. Adding a memory address as parameter to the commands LOAD
or SAVE
would allow easy loading of raw uncompressed 16 KB screen pictures.
Machine code support and RSXs
[edit]Machine code was well supported (but not to the impressive heights of the BBC Micro's, in-line assembler). There was an easy method of allocating "safe" memory (see below) and once loaded either from disc or cassette or POKE
d in, Z80 machine code could be executed with CALL <addr>
. CALL supported a method to pass parameters to the machine code using BASIC variables (including strings) using the form CALL <addr>,Q,@X$
. Integer variables and immediates were passed directly as their value (ByVAL). String and "real" variables (floating point) were passed using the @
directive, which passed the address of a variable (ByREF). Thus, machine code could pass values directly back to a variable or manipulate its contents making transfer of data between a BASIC program and machine code trivial.
Another method, leveraging the firmware, allowed for named sections of machine code using a feature known as Resident System eXtensions (RSX). Although not a purely BASIC feature, Amstrad BASIC embraces this mechanism of RSXs and the names can be used in the program as if they were BASIC reserved keywords - each being introduced by the bar |
symbol, the remaining syntax being identical to CALL. e.g. |MYRSX,Q,@X$
(note a comma is required between the RSX name and any arguments). The code for the RSXs had a very specific header structure - the RSX names, their entry points and 4 bytes of scratchpad RAM for the firmware to link in and out of the list. A call was made using the KERNEL routine $BCD1 KL_LOG_EXT to initialise the names and they were then available to use throughout the system (by finding each named section using $BCD4 KL_FIND_COMMAND - BASIC does this for | commands). A major advantage of RSXs was it allowed calling to sections of code no matter where they reside in the entire memory map in all ROM/RAM banks (see "bank switching" below). Code could simply call sideways by searching for the RSX name and from the link structure, the firmware immediately knows which ROM/RAM bank the code occupies. KL_SIDE_CALL
, KL_SIDE_PCHL
etc. ran the program code in banked memory. Thus all memory became usable for machine code program, the only restriction being that it must be broken into self-contained 16KB chunks. Use of RSXs ensured that machine code in sideways ROMs was fully integrated with BASIC and the disc commands |DIR, |A, |TAPEIN, |ERA
etc. all resided in ROM and were used throughout BASIC program code. On the CPC464, disc commands were only added when the FDI1 (floppy disc interface, with it's sideways ROM) was plugged in the rear expansion port.
In UK CPC circles at that time, is was popular to pun words that began with "bar" and name RSXs accordingly, e.g. |BQ, |BARASTREISAND, |STARD etc.
Memory allocation and management
[edit]Generally speaking, BASIC programmers do not have to concern themselves too much with the inner workings of their chosen platform to get decent results. Should a programmer wish to ignore all this and stick only to BASIC, they are well catered-for with a speedy, rich dialect. For those who want to go beyond and want more control, they are likewise accommodated.
In many contemporary systems, allocating a block of memory seems an after-thought with no "proper" method of achieving it. Innovative methods were adopted by programmers which resulted in some, often, highly cryptic and fiddly methods, such as creating REM
statements with the required length and then POKE
ing the data into known addresses in the BASIC program line - the command LET L=USR 16514
was famous in ZX81 circles. 16514 being the address of the first byte after a REM statement, on the first line of a BASIC program - the only piece of memory guaranteed not to move around or be corrupted with added program lines, garbage collect etc.
Amstrad BASIC program space occupies RAM from $170 extending to around $A6F0, just under the BASIC system area, AMSDOS area and the high jump blocks - approximately 41KB. Numeric variables and functions etc. are located immediately above the program with the remaining, free memory in between is termed "the heap". The contents of string variables are stored near the top of the heap, with the string descriptors being stored in similar fashion to numerics - just above the program (see Variables and functions above). BASIC program grew up and strings grew down towards each other in RAM, gradually consuming the heap in between. Amstrad BASIC has a garbage collector which de-fragments the heap to maximise usable memory - as with most computer languages, as strings are created then sliced/deleted, chunks of un-allocated memory between used parts begin to appear and the upper heap can become very "porous". Notably the BBC Micro did not have garbage collect and manipulation of strings could result in "out of memory" errors simply because a requested block did not exist in one chunk. In common with other dialects, Amstrad BASIC provides the FRE() function to return the amount of free heap. The function uses two forms: FRE(numeric)
, e.g. PRINT FRE(0), returns the size, in bytes, of the largest single block in the heap. The second form, FRE("")
, forces a garbage collect and returns a true count of free heap. Used areas (mainly strings and temporary structures) are aggregated at the top of the heap with no gaps - data is moved and pointers updated, leaving available heap as a single chunk of memory extending down to the program and variables. This latter form can take some time to run on badly fragmented systems, thus in large programs with much string manipulation it should be run often in "quiet times" or only as attempted mitigation in an error trap.
Amstrad BASIC provided the function, HIMEM
, to return the last address used by BASIC. The MEMORY
command can be used to adjust this last address and through a combination of the two, space can be reserved easily and controllably - a primitive form of malloc()
. For example, suppose a block of 1KB was required, space could be provided (with full awareness of BASIC) using MEMORY HIMEM-1024
which adjusted down the maximum extent of BASIC memory use. Machine code, say, could then be safely loaded into and called at HIMEM+1
, safe from other processes and accidental relocation/corruption.
The CPC range made provision, through the SCREEN_PACK, for User Defined Graphics (UDG) whereby the 8x8 pixel matrix for a character could be redefined allowing for the creation of special characters. By default, the top 16 characters (128 bytes) were "soft" and if that was sufficient, no further adjustment was required. Following on the ethos of exposing the platform capabilities in BASIC, it passed forward this functionality and the memory for UDG was allocated from program space (i.e. the more characters, the lower HIMEM
). More characters (and thus more RAM) could be allocated with the SYMBOL AFTER <char>
command - where char represents the character code from which UDG is available. e.g. SYMBOL AFTER 32
would allow the entire printable ASCII character set to be redefined, with the corresponding reduction in available program space (1784 bytes). The firmware copied the default character matrices from ROM into the newly defined space, thus defining the character matrix was not essential for clarity. If UDG were not required, memory could be returned to BASIC (resulting in fixed character matrices) with the command SYMBOL AFTER 256
.
The simplified default CPC memory map as it appears to BASIC.
A notable omission - memory bank switching
[edit]Although as a BASIC implementation, Amstrad BASIC is fairly complete and goes to great pains to support the CPC platform, one notable omission in all versions is native access to the memory bank switching mechanism used to expand the Z80 memory space; re-using address ranges by overlaying sections of physical ROM or RAM.
Through an arrangement of the custom circuitry, switching pages into one of the four 16KB pages in the Z80 memory map ($0xxx, $4xxx, $8xxx, $Cxxx) was possible. For instance, the top page ($C000) is occupied by both the video RAM, the BASIC ROM and (on disc equipped machines) the AMSDOS ROM with the hardware and firmware providing the necessary control of access. The firmware ROM was bank-switched to the lower 16KB and sideways ROMs (containing user machine code or 3rd party software) are mapped to the top page ($C000). All CPC models supported this fairly advanced (for the time) bank-switching to increase the potential memory storage - even an un-expanded CPC464 fully used 96KB of ROM and RAM mapped into the Z80 64KB address space. Memory writes are always applied only to RAM regardless of switched state, e.g. if the lower ROM ($0-$3FFF) is switched into the memory map, POKE &800,0
will write into RAM regardless. Various configurations of where the switched banks occurred in the memory map were supported by the KERNEL.
The CPC464/664 hardware can address upto 256KB including up to eight 16KB sideways ROMs. One position is used by the BASIC ROM and both the CPC664 and CPC464 expanded with the FDI1 use a further slot for the AMSDOS ROM, leaving six slots. The CPC6128 hardware permitted bank switching up to 4MB with increased space for 16 (32 with a PCB hack) 16KB sideways ROMs. Again, two positions were occupied with BASIC and AMSDOS leaving 14 available for user or 3rd parties. The Kernel provides several methods of accessing banked memory; some involve providing a pseudo 24bit address via a vector, extending the HL register pair using the C register and for ROMS, encoding the high or low ROM switch in the top few bits of a 16-bit address. Still others would retrieve data from a bank as if it were in the mainstream Z80 address map or directly from RAM regardless of the state of the high and low ROMs. Kernel routines to access banked memory include KL_FAR_CALL, KL_FAR_JMP, KL_RAM_LAM
among others. When "calling sideways", i.e. switching in a bank and jumping to execute machine code at an address there, the kernel sets up structures on the Z80 stack to ensure the safe capture of inbound RET
urns from banked memory, restoring the state of the banks and ensuring code execution continues without issue. It was quite normal to have calls between banked ROMs, indeed this happens seemlessly every time BASIC uses AMSDOS (disc commands) as both occupy the same physical address space.
Despite BASIC not providing support for this kernel functionality, use of banked memory in a BASIC program was certainly possible but required the use of machine code routines to do the switch and take advantage of the result. Banked memory could not be used for BASIC program code.
On the bundled CP/M 2.2 disc was a two-part program to add primitive-but-adequate bank-switching commands to BASIC. When run, BANKMAN.BAS
reserved 1317 bytes of RAM into which; it loaded BANKMAN.BIN
, made a call to the binary and then deleted itself. The latter file contained machine code to initialise a set of RSXs to enable RAM bank switching from BASIC programs. Functionality included saving and loading screens from banks, copying between banks etc. It was not generally considered practical as you had to include both BANKMAN files with your own software products and run them at the command prompt before your own program could take advantage of bank switching RAM. To add further hinderance, although not copyrighted, BANKMAN.BAS was saved in protected mode and so was not listable - meaning that you could not easily determine the method to load the RSX binary for use in your own programs and eliminate the two-stage start up.
Besides using banked memory to store screen images and rapidly switch between them, some applications implemented "RAM Discs" but these were not supported under the Cassette/AMSDOS firmware pack, being quite proprietary.
Contemporary rivals
[edit]This section possibly contains original research. (July 2019) |
Unlike the Commodore 64's built in BASIC (Commodore BASIC), which had no dedicated commands for graphics or sound, Amstrad BASIC allowed doing pretty much anything that was within the standard capabilities of the machine. This was not unimportant, as some other machines of the era required programmers to use assembler in order to access the full sound and graphics capabilities of their system. MSX, Sinclair Spectrum and some others offered a similar, more or less complete command set for their sound and graphics capabilities. The only things going clearly beyond BASIC capabilities were the overscan modes used in games and demos, 27-color graphics modes, digital sound playback, and smooth scrolling.
Unlike Sinclair BASIC or Commodore 64 BASIC, which had various keyboard command shortcuts or specialized keys for choosing symbols or colors, Amstrad BASIC keywords were typed in full and the interpreter parsed, recognized and tokenised them. However, there were abbreviations like "?
" for "PRINT
" and a few shortcuts. Programs could be saved onto Compact Cassette or floppy disk and retrieved as binary or ASCII files.
References
[edit]- ^ Smith, Tony (12 February 2014). "You're NOT fired: The story of Amstrad's amazing CPC 464". The Register. Retrieved 17 February 2014.
- ^ https://www.theregister.com/Print/2014/02/12/archaeologic_amstrad_cpc_464/