Porting BBC Basic to a standalone emulator
Nick Downing (9849) 3 posts |
hi guys, I checked out https://gitlab.riscosopen.org/RiscOS/Sources/Programmer/BASIC.git as I was looking for examples of earlier ARM source code, and it looks comprehensible. I would like to assemble this and run it under an emulator such as the one at https://sourceforge.net/projects/softgun/ which supports up to ARM9. I’m technically oriented and have ported various dialects of BASIC to standalone emulators in this manner (I am making a collection) … however I know next to nothing about RISC OS. I was wondering if there is an easy way to obtain a binary of the compiled BASIC code? I can see SD card images for Beaglebone and Raspberry PI so I suppose one approach might be to install one of these and then try to copy the BASIC executable to the Internet from within RISC OS. But is there an easier way? Such as a tool for working with RISC OS disc images? How are the images built normally? I assume the build is self-hosted in RISC OS? Any tips appreciated, |
Julie Stamp (8365) 474 posts |
Hi Nick, You can find a compiled BASIC within this archive in the directory !System/310/Modules. To get there click on Downloads at the top, choose Miscellaneous and then it’s System Resources. |
Charles Ferguson (8243) 427 posts |
I don’t know about softgun, but using Unicorn Engine as the emulation system, with Python, I had BASIC working in about 15 days (June 9th to June 24th), with the first recording of it running 6 days later – https://asciinema.org/a/254330. However, I wasn’t trying to get BASIC working, but the RISC OS interfaces, and BASIC was just just the mechanism for stepping up through the parts of the system. If you’re not planning to implement RISC OS, then you’re going to have to replace the environment handling and system calls for I/O. I’m pleased that you think that the BBC BASIC source is comprehensible – many people would be put off by the lack of structured calls, comments or many of the other niceties that come with modern assembler code. However this was common with code of the time, particularly coming from the 8-bit world. |
Nick Downing (9849) 3 posts |
Thankyou Julie and Charles for the helpful responses. I didn’t think of taking it from the RISC OS < 5 upgrade package (had seen the package but didn’t have any idea what it would contain), good thought. I found your (Charles’s) video very impressive and I simply looove old BASICs. Brings back the memories :) About the Unicorn Engine, I have looked at this emulator but it seems to be not what I’m after, I am going for simplicity and need it in C99. I hope it does not take me 15 days to implement the BASIC, although to clarify I’m not trying to implement advanced interfaces or load/save functionality — just the console. At present I edit the code locally using “vi” and then I simulate typing it into the console on startup to run it. I have found it necessary to insert a lot of NULs to make this work since the BASICs often poll for ^C. As an example, here is the code I did for emulating MSBASIC.COM (8086 version that wants to run under DOS):
I did similar code for MBASIC.COM (the CP/M version). For other BASICs like MS 6502, 6800 and 6809 BASIC I simulate a simple console hardware that always returns “character ready” and then blocks when reading one. I also have DEC paper-tape BASIC on PDP-11 emulator and it is similar to MS since MS is derived from that. (Unlike Apple’s original Integer BASIC which is derived from HP BASIC). I have two different 68000 BASICs but they are not MS-derived. I do not have a MIPS or VAX BASIC (I believe there was a VAX BASIC under VMS but it was more of an IDE and used bytecode compilation so I consider it too difficult to port that one). To get a compatible BASIC under 68000, MIPS and VAX, I intend to compile bas-2.5 (a C Unix BASIC) on a bare-metal 68000, MIPS or VAX compiler, I may also do an ARM version. But I like having the original BBC BASIC for ARM and I am told that it is a good emulator test. This whole project started as emulator test :) |
Theo Markettos (89) 919 posts |
Charles has way more experience of this than me, but I think there are roughly two things that need to be done: 1. BASIC uses system calls (SWIs) for I/O. You’d need to implement the appropriate RISC OS SWIs to make that work. Simple character I/O probably wouldn’t be too complex (OS_WriteC etc) and file handling likely not too difficult (OS_File, OS_GBPB and similar). Without a VDU subsystem you won’t get graphics (BASIC might emit the VDU characters but they won’t do anything) but maybe that’s OK. VDU is a little bit like RISC OS’ ‘terminal emulation mode’ where particular control codes mean things like ‘draw a circle’ – a bit like the old Tektronix terminals. 2. BASIC exists as a RISC OS module (ie partially running in ARM’s supervisor mode) that creates a region of application memory for the program. The interpreter runs out of the module (but in user mode) and uses the application memory as its workspace (containing the BASIC program being run, its data, etc). You’d either need to mimic that, or change BASIC to do it another way. I’d expect the I/O to be straightforward but the module+memory handling to be mildly annoying to either implement or strip out and replace. |
Charles Ferguson (8243) 427 posts |
To be clear, my 15 days to get BASIC running did include setting up memory management for the OS, module management and a bunch of interfaces to handle SWI calls and debug them… I was aiming for bigger things than just BASIC, so I full expect you should be able to get things working more quickly if you only target BASIC. As Theo suggests, almost certainly the most difficult part of what you’re attempting – if you’re ignoring implementing RISC OS – is trying to bypass the module invocation and system setup. All the OS_ChangeEnvironment calls which set up the different ways that the system can be called will need to be provided in some form. The escape key isn’t strictly polled for in BBC BASIC. The OS_ChangeEnvironment handler for Escape is called out of band when the escape key is pressed (or when the escape condition is cleared). In BBC BASIC this sets a flag which is then checked on each of the bytecode executions (IIRC). At the command prompt, the SWI OS_ReadLine returns with CS if the escape key was pressed, and when waiting for a key on a `GET` (OS_ReadC) the same is also true. The point at which you probably want to start emulating is MODULEMAIN, which you can see here: https://gitlab.riscosopen.org/RiscOS/Sources/Programmer/BASIC/-/blob/master/s/Basic#L23 – you’ll have to find that manually in whatever binary you’ve got but as it is an MSR followed by a SWI OS_GetEnv call, I think you can probably locate that very easily. I think, looking at the code, the first thing I’d be looking to do was to make the OS_ChangeEnvironment calls do nothing but print out their registers (R0-R3; R0 is the handler number and R1-R3 are the values), and ensure that your OS_WriteI+X, OS_WriteC, OS_Write0, OS_WriteS and OS_Newline implementations work. That will at least get you something working. If you’ve not seen the memory map for RISC OS before, I’d suggest placing the BASIC module at 0×3800000 and mapping RAM at 0×8000 to whatever size you like. BASIC needs at least 4K to run, but you probably just want to give it 128K or something because memory doesn’t really matter. OS_GetEnv will return you the command line that was supplied (which could include a filename – just make it return an empty string for now), and the memory limit. Documentation you will find useful if you don’t already have it:
If you haven’t already seen, the SWI interface in RISC OS uses the system call number in the instruction, which differs from how modern systems use the instruction – they tend to pass the system call number in a register. System calls may modify the PSR on return, but usually only deal with the C flag. The V flag is always used to indicate that an error is present with an ‘X’-SWI is called. ‘X’-SWIs are so called because their names are prefixed with an ‘X’, but you may also see them referred to as error returning SWI calls. These ‘X’ SWI numbers are the same as the non-X ones, but with 0×40000 added to them – instead of raising an error, they will return the error pointer in R0, and return with V set. More details on the error operations from SWIs can be found here: http://www.riscos.com/support/developers/prm/errors.html Hope some of that helps. Have fun. |
Mr Rooster (1610) 21 posts |
Many (many) years ago I wrote a fairly terrible ARM interpreter for my final year project at uni. This ran the (ARM2) version of BASIC from RISC OS 3 (IIRC) in a windows command window well enough that I could do: 10 FOR x%=1 TO 10 20 PRINT"Hello world" 30 NEXT RUN and it would work as expected. I never did anything further (so no I/O or VDU support), I’m not even sure if INPUT would work, probably not… :) However, I can tell you that the bare minimum SWIs you’d need to implement are: #define SWI_OS_GetEnv 0x10 #define SWI_OS_ChangeEnvironment 0x40 #define SWI_OS_WriteS 0x01 #define SWI_OS_NewLine 0x03 #define SWI_OS_WriteC 0 #define SWI_OS_ReadLine 0xe #define SWI_OS_Byte 0x6 #define SWI_OS_ConvertHex8 0xd4 #define SWI_OS_GenerateError 0x2b #define SWI_OS_WriteN 0x46 #define SWI_OS_Exit 0x11 FWIW my implementations of GetEnv and ChangeEnvironment that were sufficient to get the ARM2 BASIC running are: case SWI_OS_GetEnv : dprintf(" OS_GetEnv"); strcpy(buff,"BASIC"); mmu_memcpyin(proc->memory,buff,proc->osptr,strlen(buff)); mmu_memcpyin(proc->memory,buff+5,proc->osptr+8,1); *(proc->r0_15[0])=(unsigned long)proc->osptr; *(proc->r0_15[1])=(unsigned long)0x000a0000; *(proc->r0_15[2])=(unsigned long)proc->osptr+8; break; case SWI_OS_ChangeEnvironment : dprintf(" OS_ChangeEnvironment"); /* Badly handled. */ *(proc->r0_15[1])=0; *(proc->r0_15[2])=0; *(proc->r0_15[3])=0; break; No idea what they’re doing really, it’s been 20 years and I’ve not programerised on RISC OS for a while. (I suspect that comment is pertenent too. ;) ) It looks like osptr points to 0×800 in the ARM memory map, but I can’t remember if that’s a significant address, or just a chunk of memory I decided to use? HTH, Ian |
Simon Willcocks (1499) 519 posts |
You could try blowing the dust of this old thing, too: https://sourceforge.net/p/ro-lf/code/HEAD/tree/ROLF/rolf/Libs/Compatibility/ |
Theo Markettos (89) 919 posts |
I suppose the minimalist OS_ChangeEnvironment doesn’t actually need to allocate any memory, you can just start with the memory already set up at 0×8000. Then OS_ChangeEnvironment doesn’t need to do anything in the first iteration, and you just pass in the size of the memory allocation in R1 returned from OS_GetEnv. Other functions of OS_ChangeEnvironment can perhaps be skipped in a first iteration (no Escape handling, no exception handling, etc).
I think there were various low memory addresses that were ‘special’ and used for holding state that was shared between different components unofficially behind the scenes, but I think (many/all) of those have been eliminated. It’s possible they were still necessary for the ARM2 version. That said, if you’re going to allocate some buffers for the environment string and the time to return from OS_GetEnv, and you aren’t running other RISC OS pieces, that’s as good a place as any to put them. |
Nick Downing (9849) 3 posts |
Thanks guys, I am overwhelmed and I mean that in a good way — it generally takes me a fair bit of experimentation with the memory map and other aspects to get a new BASIC up and running, so this really helps. One of my first steps will be to port the original source to an ARM cross-assembler supporting the relevant architecture level and make sure that it can recreate the original binary. This tends to help me a bit in determining where the executable lies in memory, but it can be confusing if the executable contains relocation information which I expect this one might. Thanks for the documentation tips, I’ll peruse them. |
nemo (145) 2552 posts |
For completeness, the SWIs (and in some cases reason codes) you’ll need are (subject to interpreter version):
Those reason code lists that end in an asterisk require all reason codes if CALL emulation is included (again, subject to interpreter version). Custom builds can include many others. And note that Basic, unusually, does not use the X-form of SWIs (in almost every case), so errors including missing SWI implementations will call the |
Stuart Swales (8827) 1357 posts |
;-) Error Handler |
Rick Murray (539) 13850 posts |
An enterprising person might use this as a way of knowing what SWIs still need to be emulated. ;) |
nemo (145) 2552 posts |
FFS (Fat Finger Syndrome)
It’s interesting (to me) that Basic does use the X form in a very few cases. Such as OS_Plot. No idea why.
|
Michael Grunditz (8594) 259 posts |
I have a BBC BASIC with unicorn emulation , worked after a very short amount of work. Do it in steps , you don’t need to support all SWI’s to start BASIC. |
tymaja (278) 174 posts |
In case anyone else is thinking of doing something similar (and I suspect some people will be working on something like this at any given time!); some insights I had: (sorry about thread resurrection): 1a. The module has, at position 0×00, a number, usually 0×000001FC, which points to an MSR instruction (IOMD), or CPSIE (modern hardware). This can be used as a direct entry point to start the module, without needing to run anything else in RISC OS. 1. It is very useful to have a machine running RISC OS 5. This could be a Raspberry Pi, or RPCEmu on a supported platform. 2. I would recommend starting with ‘RISC OS 5 IOMD’ (Risc PC version) initially. This uses a smaller set of ARM instructions (ARMv3, essentially). These ARM instructions make up the vast majority of instructions actually used in ARM code, so will be useful for developing an emulator Regarding system SWIs, setting it up is quite easy; you essentially need to implement: Other gotchas : the environment string in OS_GetEnv needs to exist, or BASIC will just attempt to exit before the command prompt; Once you have BASIC running, you could try a recompiled version set to standalone to remove dependence on MessageTrans. I will say that, even before having a full ARM emulation with MMU emulation etc … you can map your RAM at 0×0000 upwards, and map your ROM starting at &FC000000, and you can use some OS routines ‘directly’, by catching the SWI, then manually jumping to a place in the RISC OS ROM (which includes BASIC). OS_PrettyPrint is the easiest one to do first (after implementing a mode variable block). If you find the right place, you can jump into the ROM, continue emulation, and it will call the OS_WriteC character print commands, and return once it is complete (it branches to an address like FFED5713 or something – if you map a block of MOV PC,R14 instructions in there, it will return seamlessly to the BASIC module! For other stuff, you need to emulate modes (SVC mode) for register banking. At this point, it will become tempting to develop a fully fledged RISC OS platform emulator! |
Michael Grunditz (8594) 259 posts |
I am using Unicorn. BASIC was the first thing I did , and it didn’t take long to have it running. It still is my cli of choice, since I can test SWIs in a controllable way. After BASIC I started to implement a lot of SWIs and also support for ARM modules. For example I can used the shared clib module. It works , but floating point doesn’t since it relies on trapping undefined instructions. I have a simple framebuffer for drawing with OS_Plot. Right now my only supported host platform is Genode (?!?!).It is single tasking and I have a fixed app slot that the arm app lives in. So when starting a new app it will overwrite the last one. I don’t use MMU so it is far from complete. I have also brought up unicorn on bare metal. See here for more info: https://genodians.org/mickenx/2024-08-15-genode-and-riscos-demo I would like to release the emulator part, but it needs to compile in a controllable way before releasing. |