The BBC Native Build Process
Building BCP natively on a real or emulated BBC Micro takes two passes (of which one is actually a double pass, so maybe it takes three passes). The 6502 Assembler Source Code is split among several BASIC programs; each of which assembles a section of code, then chainloads the next program. In the first pass, we are simply collecting label definitions, ignoring ones that we have indicated as not to be exported; by the second pass, we already have correct values for all labels referenced, and can build the code in earnest. We use the offset facility built into BBC BASIC to assemble the code at a different location in memory from where it would be run, and the fourth, "reload address" parameter of the *SAVE
command to ensure it runs in its proper intended place. Once the last code section is assembled and saved, one final program stitches the code together into a single file.
During the first pass, each program invokes a utility to export a dump of variable names and contents, straight from BASIC's own storage; this takes the form of a *SPOOL
file of a series of immediate-mode BASIC variable definition commands, e.g.
scaleM=&A58
scaleD=&A5A
plotmode=&A7F
parse_cmd=&57F7
sel_draw_rt=&57FA
oswrch=&FFEE
Each program begins by reading this file; the use of immediate mode means a variable takes up no additional space in memory beyond its name, its contents and some sundry housekeeping data. (Otherwise, there would need to be a program line containing a copy of the name and the human-readable form of the value. These copies would be redundant once the variable was stored in BASIC's internal database.)
WKS02SC
is just a series of BASIC variable initialisations and assembler label definitions. It offers the option to enable paged mode, and to display assembled code on the last pass or not. Its "code" (really, just some initial defaults in workspace; but crucially, with their labels) is assembled in one pass and saved. It then exports the variables it has just created and chainloads MTH11SC
.
MTH11SC
defines function key f8
to *EXEC the variable dump file, then resume execution using GOTO 100
. This function key is injected into the keyboard buffer and the program END
s.
Once the variables are read back, execution resumes at line 100, preserving the variables we just defined. The code is then assembled above HIMEM
with OPT 4
, which just gathers up all labels defined in the program. Then we export the variables again, including the ones we just defined, as well as the ones we read in, and chainload DBM57SC
. We will not be saving anything else to disc until the file has grown as large as it is going to get, so the file should have freedom to grow.
DBM57SC
imports the variables defined in MTH11SC
, adds some more of its own and chainloads GFX50SC
.
GFX50SC
does likewise and chainloads DES17SC
.
DES17SC
does likewise, alters some variables and chainloads MTH11SC
for the second pass. The variables file now contains the correct values for all the variables that we want to export.
This probably could all be done with cassettes, if you had three separate tapes with the BASIC programs, the variables and the assembled output and you were very careful about rewinding so as to go exactly to the beginning of the dump file you just saved and no further.
MTH11SC
begins in earnest now all labels defined in all sections have values. We still need to do a first pass with OPT 4
to pick up values for the labels we did not export; then we do the final pass with OPT 6
or OPT 7
actually to assemble the code above HIMEM, but using addresses appropriate to its intended location in memory.
After the second pass, the assembled code is *SAVE
d, with the appropriate reload address and an execution address pointing to some convenient RTS instruction, then DBM57SC
is chain loaded.
DBM57SC
, GFX50SC
and DES17SC
all proceed likewise; but at the end, DES17SC
chainloads FINALE
.
FINALE
reads the final list of exported variables, and dumps out a minimal selection of them to enable the test program DT42G
to be run. It then concatenates together the individual machine code sections as ALLCODE
.
You can edit FINALE
to *DELETE
the individual machine code files before saving ALLCODE, and this will be saved in the space where they were.
An abridged version of DBM57SC
is shown below, with annotations. The same general principles apply to each stage, with the exceptions noted above.
10MODE7:HIMEM=&7300
20*K.8*EXEC L.VARS|MG.100|M
Set up f8
to read in the variable definitions and jump into the program.
30*FX138,0,136
Inject f8
into the keyboard buffer.
40END
100S%=&4B70:V%=57
The starting address and version number of the code. It doesn't increase as crazily fast anymore as it used to :) The workspace and maths codes were abstracted out late and did not change much, which is why they have such low version numbers.
110O$="DBC"+STR$V%:B$="D"+STR$V%+"VARS"
120*KEY9L.|M*SP.|M
130IFK%PRINT"Press SHIFT to scroll screen.";CHR$14
140FORJ%=4TO7STEPH%
H% will be 4 the first time around, so we will make a single pass with OPT 4. The second time we run the program, H% will be 2 or 3, and we will make first a label-gathering pass (which might still leave some duff addresses in the assembled code) with OPT 4, followed by an actual assembly pass with OPT 6 or OPT 7 defending if or not we wish to display the assembled code.
150O%=HIMEM
160P%=S%
..... Or maybe we could set O%=o%
and P%=p%
if we just wanted to carry on where the previous program left off.
170[OPT J%:._begin
180.database_begin
190.find_fp1
200LDA fpbase
210STA fpb
220LDA fpbase+1
Some code :) We can miss most of it out for now; it's essential for BCP, but not for the more general build process. That's the same regardless of what we are actually building. BeebAsm's syntax is close enough to that of the BASIC assembler for the code just to drop right in.
10050.steps
10060EQUB5
10070EQUB25
10080EQUB50
10090EQUB100
10100.database_end
15000._end
15010]
There the assembly code ends and we return to BASIC.
15020NEXTJ%
15025IFH%<4GOTO15050
Skip the space test (which would fail anyway) and variable export (which is not needed anymore, since we have picked up all labels by this stage).
15030IF_end>B%PRINT'"***** _end (=&";~_end;") > &";~B%;"! *****"':STOP
15040PROCev
HOUSEKEEPING: Abort the process if we overflow the space available. (If you resume the process partway through after correcting an error, a value of B% meant for a different stage can trigger a false positive on this. Just G.ERL+10
to go to the line 10 after the line on which the last error occurred.)
15050PRINT'"&";~B%-_end;" bytes slack space at end."
15060C$="SAVE M."+O$+" "+STR$~HIMEM+" +"+STR$~(_end-_begin)+" "+STR$~database_rts+" "+STR$~_begin
15070IFH%<4OSCLIC$
H% will be equal to 4 on the first pass, and either 2 or 3 on the second pass; in which case, we *SAVE the code.
15080CHAIN"GFX50SC"
15090END
We probably won't ever actually hit the END
statement, since execution has usually begun elsewhere if a CHAIN
command succeeded.
15100REM
15110DEFFNhex(V%,L%)
15120=RIGHT$(STRING$(L%,"0")+STR$~V%,L%)
This is a rare case of a vestigial function ;) It must have served an important purpose to an evolutionary ancestor long ago.
15130DEFPROCev
15140L%=0
15150o%=O%:p%=P%
15160OSCLI"SPOOL L.VARS"
15170*VDUMP
15180PRINT"B%=&";~_begin
15190*SPOOL
15200ENDPROC
This PROCedure copies the terminal output to a disc file called L.VARS
. L%
is used by the VDUMP
utility; if non-zero, it would generate numbered program lines. o%
and p%
enable the values of O% and P% to be exported, allowing assembly to resume at a particular point. We also set B% so the next program can error out if we try to assemble over something else.
10MODE7:HIMEM=&7300
20*K.8*EXEC L.VARS|MG.100|M
30*FX138,0,136
40END
100ONERROROSCLI"SPOOL":REPORT:PRINT" at line ";ERL'"*SPOOL stopped":END
In the event of an error while *SPOOL
is active, we first issue *SPOOL
to close the file, and also print a slightly fancy version of the error message including an explicit mention that the *SPOOL is stopped. Otherwise the file will grow and potentially cause another error.
110*SPOOL L.MINVARS
120RESTORE
130REPEAT
140READV$
150IFV$>="@"PRINTV$;"=&";~EVALV$
160UNTILV$="*"
170*SPOOL
180ONERROROFF
We no longer need to stop a *SPOOL
command, so we can disable our fancy error handling code.
190$&900="SAVE ALLCODE "+STR$~des_ext_begin+" "+STR$~maths_end+" "+STR$~jump_table
200CLEAR
On a Model B, the variables would have hit the code area. So we save just what we need (in the form of an OS command, ready to go with embedded hex values and terminating CR) and ditch the rest.
210*L.M.MMC11
220*L.M.DBC57
230*L.M.GMC50
240*L.M.APC17
250X%=0:Y%=9:CALL&FFF7
We don't even need the OSCLI command for once :)
260PRINT'"*************** ALL DONE ***************"
270END
Now the rest of it is just DATA. This is just like how the old PROCev
from the old build system worked; except probably a bit safer, since things aren't going to change so often as those days :) It's mostly just workspaces and addresses in a JMP
table anyway.
280DATAfpb,ssb,cbb,pcb,plb,wlb,rtb,wpb
290DATAcardbuf,cenX,curX,dstX,pinX,absX,relX
300DATAscaleM,scaleD,vptL,vptB,vptR,vptT,scrX,lmX
310DATAfprt,pins,pside,pangle,part,lgdmode,bdyL,bdyB,bdyR,bdyT,lgdX
320DATAstep,plotmode,padL,padW,padR,padS,nfp,refindex,desP,layers,palette
330DATApadmode,pv_width,pv_layer,rt_width,rt_layer,nparts
340DATAfpbase,pnbase,ssbase,plbase,wlbase,route,wpbase,nextwp
350DATAnnodes,nroutes,moveX,moveY,plotbuf
360DATApartinfo,curinfo,M_cmd,V_cmd,draw_vis,R_cmd,W_cmd,next_pin_D
370DATAnext_pin,np_1,show_route,sel_draw_rt,search_node,sn_resume,sn_repeat
380DATAparse_pap,use_pap,rat_nest,prepare_pad,select_pad,draw_pad_anyway
390DATAdraw_via,reset_org,draw_track,test_pivp,test_pt
400DATAunpack_part,unpack_waypt,disp_dec_Y,unpack_desr,disp_decnum
410DATAparse_card,pack_desr,find_part,disp_desr,select_fp,select_part
420DATAdraw_footprint,parse_cmd,oswrch
430DATA"*"
Nothing in the build process is specific to BCP, so it should be adaptable to any large project. As general guidelines:
- You can squeeze enough readable source to produce 2Kbytes of binary into a model B with PAGE=&1900, and still have room for variables.
- Use _ and £ for variables that are not to be exported; for example, conditional branch targets often have only one reference to themselves.
- Keep sections self-contained wherever possible. Obviously a subroutine library will necessarily have many entry points, but the code that makes use of it probably will have fewer entry points.
- Use
o%
andp%
to pass throughO%
andP%
for contiguous code (or just don't change the values of O% and P% at the beginning, and rely on their persistence across loading a new program); but make sure never to try to run a section out of order, and that includes after it has already run once! - Watch where your variable dump runs from! It must be either below PAGE or above HIMEM so as not to stomp on the BASIC program or its variables. It is safe to overwrite the assembled code. The final program section will contribute the last of the labels in the first pass, and we can just use this version thenceforth; there is no need to export the variables at all during the second pass.
BCP breaks some of these rules for historical reasons. I had already started doing it a certain way before thinking of a better way.
This version is designed to be built entirely in the target environment.
It builds successfully on an emulated Master 128, but the build process runs out of memory on a 32K Model B. This might be as simple to solve as exporting
fewer variables, or could involve splitting chunks further. Set o%=O%:p%=P%
in PROCev
and begin next chunk with O%=o%:P%=p%
to continue assembling
after code already assembled.
TO BUILD IT
S%=TRUE
CHAIN"VDUMP25"
(or just use the VDUMP
out of the .ssd file.) Note, if S%=FALSE
then the code must be saved by hand!
CHAIN "WKS02SC"
Answer N to "paged mode", Y to "show code" and watch the build process.
The build takes two passes (of which one is actually a double pass, so maybe it takes three passes).
- Each section --
WKS02SC
,MTH11SC
,DBM57SC
,GFX50SC
andDES17SC
-- uses a*EXEC
command to import any variables from the previous program as a series of immediate-mode BASIC commands, so they take up no additional space in memory beyond the variable name, its contents and some sundry housekeeping data; then assembles its code above HIMEM with OPT 4. At the end of this pass, all labels defined in the code are properly assigned to BASIC variables. The program then invokes a utility to dump out as a*SPOOL
file, the definitions of all variables except the static integer variablesA%
-Z%
and@%
, and any variables whose names begin with an_
underscore or£
pound sign; and finally, chain-loads the next program. The effect of this is for variables to persist from one BASIC program to another. - By the end of pass one, we have gathered up all labels in the code not starting with an underscore or pound sign (these can be used to indicate
labels not referenced outside this section of code, such as destinations of conditional branches which only matter within the loop to which they belong,
so only important entry points need get exported). We now work through
MTH11SC
,DBM57SC
,GFX50SC
andDES17SC
again (WKS02SC
is just a series of BASIC variable initialisations and assembler label definitions, and already saved its output the first time); running one assembler pass with OPT 4 to gather up the labels we did not export, and a second with OPT 6 or OPT 7 actually to assemble the code above HIMEM, but using addresses appropriate to its intended location in memory. Lastly, each section saves its assembled code, with the appropriate reload address and an execution address pointing to some convenient RTS instruction, and chain-loads the next one. - After all the code has been assembled,
FINALE
is run. This reads the final list of exported variables, and dumps out a minimal selection of them to enable the test programDT42G
to be run. It then concatenates together the individual machine code sections asALLCODE
.
You can edit FINALE
to *DELETE
the individual machine code files before saving ALLCODE and this will be saved in the space where they were.
TO RUN IT
You will need a disc in drive 1 with D.JSI0
and any other design file you wish to load.
*DRIVE 1
*LOAD :0.M.PAGEA
CHAIN ":0.DT42G"
(You should only need to load PAGEA once per session unless something goes very badly wrong and/or you reset the machine.)
You can draw saved routes by crashing out of the program, setting A%=0
and pressing f3
. Press f2
to increase A%
, then f3
again .....
Focus will now shift fully to development under BeebAsm, since the concept of a native build has been proven.
This source file is a little bit special, as it does not contain any assembly language, but rather generates its output directly from embedded DATA
statements; however, it can optionally generate the BeebAsm source for the photoplotter font from the same DATA. It is actually a triple-purpose program since if it is run in a graphics mode, it can draw the characters on screen; this is obviously deliberately kept from testing in case it is useful in future.
If the program is modified to change the font while retaining some way of previewing the changes, the assembly language output can be saved and then extracted from the disc image; you will need to do this in order to build the new version in BeebAsm.