Skip to content

The BBC Native Build Process

Julie Montoya edited this page Dec 28, 2020 · 5 revisions

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.

PASS ONE

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 ENDs.

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.

PASS TWO

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 *SAVEd, 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.


HOW IT WORKS: DBM75SC ANNOTATED

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.

FINALE ANNOTATED

   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"*"

Generalising the Build Process

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% and p% to pass through O% and P% 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.


BCP Beeb Build 3

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).

  1. Each section -- WKS02SC, MTH11SC, DBM57SC, GFX50SC and DES17SC -- 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 variables A%-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.
  2. 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 and DES17SC 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.
  3. 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 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.

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.

The Photoplotter Font Generation Program

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.