Skip to main content

On Sale: GamesAssetsToolsTabletopComics
Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines
(+1)

I've completed what is, I think, my most complicated Senbir project yet tonight.  A multitask-capable kernel of sorts.  It's pretty jury-rigged, and suffers from some major technical limitations (not to mention performance issues, due to the 180Hz max speed), but serves as a decent proof of concept for a functional OS core.  It also relies on a new update that I just released, containing a new op-code and some minor bugfixes here & there.  It's also meant to run on the Compy6c preset I provided in the OP of this thread - it needs 1024 addresses of RAM to run, though it should be possible to port to larger systems.

So, the numbers.

  • Supports 8 simultaneous processes, though speed cannot be guaranteed, especially beyond one or two.
  • Each process gets 96 addresses (384 bytes) of RAM to do with as it pleases, the first address of which will always be executed for the program's clock cycle.
  • The first 32 addresses of RAM are JMPs that go to various system functions.  Currently, only 0 does anything (tells the kernel it can let the next program start a clock cycle), but I plan to include some other ones down the line (e.g, #1 will wipe a program's chunk of RAM and mark it as no longer executing, leaving the slot open for another process)
  • Addresses 800-1015 are the kernel itself, only going up to address 865 at the moment.
  • Addresses 1016-1023 are used to store the starting address/state of each of the 8 process slots.  If it's not zero, then it's a pointer to where a given process wants execution started (at the moment, this is just where it starts getting written into RAM, and is equal to 32+(programID*96))
  • Register 0 is the active program ID.
  • Registers 1, 2, and 3 are all used by various TC-06 functions, and thusly are going to be used by both the kernel and programs it runs.
  • Registers 4 & 5 are currently unassigned, and generally safe for programs to use - just make sure to put their values in RAM if they're not serving as temporary variables.
  • Registers 6, 7, 8, and 9 are use for temporary math operations & self-modifying code.
  • Register 10 is the integer 1024 - the length of RAM.
  • Register 11 is the integer 96 - the number of addresses a program will get.
  • Register 12 is the integer 32 - the offset from the start of RAM where programs get put.
  • Register 13 is the integer 8 - the number of process slots there are.
  • Register 14 is the integer 1 - used for basic addition & whatnot in loops.
  • Register 15 is the integer 0 - used in some cases for IFJMP.

Multitasked Blinkenlights

Debugger register view visible on the side.
It also requires a custom bootloader program to be put in ROM, which loads in the function calls for addresses 0-31, and then the actual OS at the end of RAM.

NILLIST 32 //Addr 0-31.  Ignore so the program won't get overwritten.
DATAC 00000000000000000000010000000000 //Addr 32.  Integer 1024.
MOVI 10 32 //Addr 33.  Move 1024 into register 10, initializing it for the OS.
SET 11 3 96 //Addr 34.  Initialize register 11.
SET 12 3 32 //Addr 35.  Initialize register 12.
SET 13 3 8 //Addr 36.  Initialize register 13.
SET 14 3 1 //Addr 37.  Initialize register 14.  Register 15 is already zero - no worries there.
SET 3 3 31 //Addr 38.  Set register 3 for loading in the function calls that live at the start of RAM.
GETDATA 1 3 2 //Addr 39.  Beginning of load loop 1.
MOVI 9 43 //Addr 40.  Move the MOVO to modify into register 9.
PMOV 2 9 23 31 0 1 //Addr 41.  Splice the updated address into the MOVO op.
MOVO 9 43 //Addr 42.  Splice it in!
MOVO 1 0 //Addr 43.  Move the loaded data into RAM.
MATH 14 2 0 //Addr 44.  Increment the counter.
IFJMP 0 2 0 //Addr 45.  If we're done loading, jump ahead.
JMP 2 7 //Addr 46.  Otherwise, continue the loop.
DATAC 00000000000000000000001100100000 //Addr 47.  Int 818.
MOVI 2 47 //Addr 48.  Put 818 into register 2.
MATH 10 3 5 //Addr 49.  Set register 3 to the number 1024.
MATH 14 3 1 //Addr 50.  Subtract 1 from it, so we get 1023.
GETDATA 1 3 2 //Addr 51.  Beginning of load loop 2.
MOVI 9 55 //Addr 52.  Move the MOVO to modify into register 9.
PMOV 2 9 15 31 0 1 //Addr 53.  Splice the updated address into the MOVO op.
MOVO 9 55 //Addr 54.  Splice it in!
MOVO 1 0 //Addr 55.  Move the loaded data into RAM.
MATH 14 2 0 //Addr 56.  Increment the counter.
IFJMP 1 800 0 //Addr 57.  If we're done loading, jump to the OS.
JMP 2 7 //Addr 58.  Otherwise, continue the loop.

This version only supports loading 2 programs, two different Blinkenlights (one in light orange, color ID 27, in the upper-left, and one in light lime, color ID 30, in the upper-middle), using the keys 1 and 2.  There's no way to terminate the processes, and if you load in more than one copy of either, odd side-effects may arise.

This is a WIP and likely to have bugs.  I plan to update it with support for loading up to 8 programs (as well as having it automatically pick an empty slot to load in, instead of the haphazard way it works now), but here - the kernel itself, along with the two blinkenlights.  Compile & install to the harddrive, rather than ROM.

//-=BEGINNING OF SYSTEM FUNCTION CALLS=-
JMP 1 800 //Addr 0: Function ID 0 - Jump to the END PROCESSING function, which indicates a given program is finished with its cycle.
JMP 1 869 //Addr 1: Function ID 1 - Jump to the HALT PROGRAM function in the operating system.
NILLIST 30 //Addrs 2-31: Blank space.  Other system operations are not yet implemented.
//-=END OF SYSTEM FUNCTION CALLS=-
//-=BEGINNING OF PROGRAM SPACE=-
NILLIST 768 //Addrs 32-800: Blank space.
//-=END OF PROGRAM SPACE=-
//-=BEGINNING OF PROCESS LOOP=-
MATH 14 0 0 //Addr 800.  Begins process of, well, ticking a process.
OFST 0 //Addr 801.  Reset offset.
MATH 0 2 5 //Addr 802.  Part one of checking if we've gone out of bounds.
MATH 13 3 5 //Addr 803.  Part two of checking if we've gone out of bounds.
IFJMP 0 2 1 //Addr 804.  If we're NOT out of bounds, jump ahead to the next part.
MATH 0 0 1 //Addr 805.  We ARE out of bounds, so let's jump back to program 0.  Subtract the process counter from itself.
MATH 10 8 5 //Addr 806.  Move the max RAM length into register 8.
MATH 13 8 1 //Addr 807.  Subtract the max number of processes.
MATH 0 8 0 //Addr 808.  Add the current process.
MOVI 9 812 //Addr 809.  Load in the MOVI function to modify.
PMOV 8 9 15 31 0 1 //Addr 810.  Splice the new address in.
MOVO 9 812 //Addr 811.  Load the modified MOVI into RAM.
MOVI 2 1015 //Addr 812.  Copy the value of the appropriate process status into register 2.
MATH 3 3 1 //Addr 813.  Set register 3 to zero.
IFJMP 0 9 0 //Addr 814.  If this process slot is NOT ACTIVE, jump to the loader test.
MOVI 9 822 //Addr 815.  Load the JMP to modify into register 9.
PMOV 2 9 15 31 2 0 //Addr 816.  Splice the new address in.
MOVO 9 822 //Addr 817.  Load the modified JMP back into RAM.
MOVI 9 821 //Addr 818.  Move the OFST operation to modify into register 9.
PMOV 2 9 15 31 0 1 //Addr 819.  Splice the new address in.
MOVO 9 821 //Addr 820.  Load the modified OFST operation back into RAM.
OFST 32 //Addr 821.  Set the offset.
JMP 1 32 //Addr 822.  Jump to the program.
//-=END OF PROCESS LOOP=-
//-=BEGINNING OF BASIC LOAD SUBROUTINE=-
GETDATA 2 3 15 //Addr 823.  Get whatever the last key was.
MATH 3 3 1 //Addr 824.  Set register 3 to zero.
MATH 3 2 5 //Addr 825.  Set register 2 to zero.
MATH 1 2 5 //Addr 826.  Set register 2 to the last keypress.
MATH 8 8 1 //Addr 827.  Set math temp register 8 to zero.
SET 3 3 00110001 //Addr 828.  Set register 3 to the key id for the number 1.
IFJMP 1 833 0 //Addr 829.  If lastKey==numeralOne, then jump to the code to load things.
SET 3 3 00110010 //Addr 830.  Set register 3 to the key id for the number 2.
IFJMP 1 836 0 //Addr 831.  If lastKey==numberalTwo, then jump to the code to load things.
JMP 1 800 //Addr 832.  Otherwise, jump back to the start of the process function.
//-=END OF THE BASIC LOAD SUBROUTINE=-
//-=BEGINNING OF THE KEY SET=-
SET 8 2 00000100 //Addr 833.  Set the drive position.  1024.  ----KEY: 1----
SET 8 3 00000000 //Addr 834.  See above.  1024.
JMP 0 3 //Addr 835.  Jump to the loading portion.
SET 8 2 00000100 //Addr 836.  Set the drive position.  1025.  ----KEY: 1----
SET 8 3 00000001 //Addr 837.  See above.  1025.
JMP 0 1 //Addr 838.  Jump to the loading portion.
//-=END OF THE KEY SET=-
//-=BEGINNING OF THE ACTUAL LOADER=-
JMP 0 1 //Addr 839.  SKIP IT YO
GETDATA 1 3 8 //Addr 840.  Load the thing.
MATH 1 2 5 //Addr 841.  Transfer the starting position into register 2.
GETDATA 1 3 2 //Addr 842.  Load the ending address.
MATH 1 3 5 //Addr 843.  Put the requested ending address in register 3.
MATH 11 9 5 //Addr 844.  Put the integer 96 in reg9.
MATH 0 9 2 //Addr 845.  Multiply 96 by the current program ID.
MATH 12 9 0 //Addr 846.  Add 32 to register 9.
MATH 9 6 5 //Addr 847.  Clone reg 9's contents to reg 6.
MATH 14 2 0 //Addr 848.  --BEGIN LOAD LOOP--  Add 1 to the thing.
IFJMP 0 10 1 //Addr 849.  Check to see if we're done loading.  If we aren't, continue the load process.
MATH 10 8 5 //Addr 850.  Move 1024 into register 8.
MATH 13 8 1 //Addr 851.  Subtract 8 from register 8.
MATH 14 8 1 //Addr 852.  Subtract 1 from register 8.  (==1015)
MATH 0 8 0 //Addr 853.  Add the current program ID to it.
MOVI 7 857 //Addr 854.  Move the MOVO op into register 7.
PMOV 8 7 15 31 0 1 //Addr 855.  Splice the address in.
MOVO 7 857 //Addr 856.  Move it back into RAM.
MOVO 9 1015 //Addr 857.  Move the load address into RAM.
JMP 1 806 //Addr 858.  Jump to the loaded program.  --PROGRAM LOAD COMPLETE--
GETDATA 1 3 2 //Addr 859.  Load things.  --IFJMP DESTINATION--
MOVI 7 863 //Addr 860.  Move the MOVO operation to modify into register 7.
PMOV 6 7 15 31 0 1 //Addr 861.  Splice the new address into the MOVO op.
MOVO 7 863 //Addr 862.  Move the modified MOVO operation back into RAM.
MOVO 1 32 //Addr 863.  Move the loaded data into RAM.
MATH 14 6 0 //Addr 864.  Add 1 to reg 6, incrementing the write location.
JMP 1 848 //Addr 865.  Jump back to the beginning of the loop.
//-=END OF THE ACTUAL LOADER=-
//-=BEGINNING OF FUNCTION 0: HALT PROGRAM=-
JMP 1 0 //Addr 866.  HALT IS UNFINISHED.
NILLIST 157 //Addr 867-1023.  Fill in the blank space for the unused portions of the operating system.
DATAC 00000000000000000000010000000010 //Addr 1024.  Position of BLINKENLIGHT 1. (1026)
DATAC 00000000000000000000010000011001 //Addr 1025.  Position of BLINKENLIGHT 2. (1049)
//-=BEGINNING OF BLINKENLIGHT 1=-
DATAC 00000000000000000000010000011000 //Addr 1026.  LAddr -1.  End address of BLINKENLIGHT 1 (1048)
MOVI 2 1 //Addr 1027.  LAddr 0.  Move the current counter state into register 2 for comparison.
NIL //Addr 1028.  LAddr 1.  RAM position for the counter variable.
MATH 15 3 5 //Addr 1029.  LAddr 2.  Move 0 into register 3 for comparison.
IFJMP 0 4 0 //Addr 1030.  LAddr 3.  If the counter is 0, jump to the "toggle pixel" function.  Otherwise, increment the counter.
MATH 14 2 1 //Addr 1031.  LAddr 4.  Count down.
MOVO 2 1 //Addr 1032.  LAddr 5.  Move the counter back into RAM so it won't be lost when the next process starts.
JMP 1 0 //Addr 1033.  LAddr 6.  Tell the OS that we're done here and it can go ahead and start the next process now.
MOVI 2 8 //Addr 1034.  LAddr 7.  IFJMP TARGET.  Move the current pixel state into register 2.
NIL //Addr 1035.  LAddr 8.  Current pixel state in RAM.  0 or 1.
IFJMP 0 4 1 //Addr 1036.  LAddr 9.  If the pixel is currently ON...
SETDATA 0 1 10 //Addr 1037.  LAddr 10.  Turn the pixel ON to color 27 - light orange.
MATH 14 2 5 //Addr 1038.  LAddr 11.  Set pixel state to ON in register.
JMP 0 3 //Addr 1039.  LAddr 12.  Jump to the end of the toggle function.
SETDATA 0 1 8 //Addr 1040.  LAddr 13.  IFJMP TARGET.  Turn the pixel OFF.
MATH 15 2 5 //Addr 1041.  LAddr 14.  Set pixel state to OFF in register.
MOVO 2 8 //Addr 1042.  LAddr 15.  Put the pixel state back into RAM.
MOVI 2 1 //Addr 1043.  LAddr 16.  Put the counter state back into register 2.
MATH 14 2 5 //Addr 1044.  LAddr 17.  Set the counter state to 1 tick.
MOVO 2 1 //Addr 1045.  LAddr 18.  Move the counter back into RAM so it won't be lost when the next process starts.
JMP 1 0 //Addr 1046.  LAddr 19.  Tell the OS that we're done here and it can go ahead and start the next process now.
DATAC 01101100000000000000000000000000 //Addr 1047.  LAddr 20.  COL: 27, X: 0, Y: 0
DATAC 00000000000000000000000000000000 //Addr 1048.  LAddr 21.  COL: 00, X: 0, Y: 0
//-=END OF BLINKENLIGHT 1=-
//-=BEGINNING OF BLINKENLIGHT 2=-
DATAC 00000000000000000000010000110000 //Addr 1049.  LAddr -1.  End address of BLINKENLIGHT 2 (1071)
MOVI 2 1 //Addr 1050.  LAddr 0.  Move the current counter state into register 2 for comparison.
NIL //Addr 1051.  LAddr 1.  RAM position for the counter variable.
MATH 15 3 5 //Addr 1052.  LAddr 2.  Move 0 into register 3 for comparison.
IFJMP 0 4 0 //Addr 1053.  LAddr 3.  If the counter is 0, jump to the "toggle pixel" function.  Otherwise, increment the counter.
MATH 14 2 1 //Addr 1054.  LAddr 4.  Count down.
MOVO 2 1 //Addr 1055.  LAddr 5.  Move the counter back into RAM so it won't be lost when the next process starts.
JMP 1 0 //Addr 1056.  LAddr 6.  Tell the OS that we're done here and it can go ahead and start the next process now.
MOVI 2 8 //Addr 1057.  LAddr 7.  IFJMP TARGET.  Move the current pixel state into register 2.
NIL //Addr 1058.  LAddr 8.  Current pixel state in RAM.  0 or 1.
IFJMP 0 4 1 //Addr 1059.  LAddr 9.  If the pixel is currently ON...
SETDATA 0 1 10 //Addr 1060.  LAddr 10.  Turn the pixel ON to color 30 - light lime.
MATH 14 2 5 //Addr 1061.  LAddr 11.  Set pixel state to ON in register.
JMP 0 3 //Addr 1062.  LAddr 12.  Jump to the end of the toggle function.
SETDATA 0 1 8 //Addr 1063.  LAddr 13.  IFJMP TARGET.  Turn the pixel OFF.
MATH 15 2 5 //Addr 1064.  LAddr 14.  Set pixel state to OFF in register.
MOVO 2 8 //Addr 1065.  LAddr 15.  Put the pixel state back into RAM.
MOVI 2 1 //Addr 1066.  LAddr 16.  Put the counter state back into register 2.
MATH 14 2 5 //Addr 1067.  LAddr 17.  Set the counter state to 1 tick.
MOVO 2 1 //Addr 1068.  LAddr 18.  Move the counter back into RAM so it won't be lost when the next process starts.
JMP 1 0 //Addr 1069.  LAddr 19.  Tell the OS that we're done here and it can go ahead and start the next process now.
DATAC 01111010000000000000000000000000 //Addr 1070.  LAddr 20.  COL: 27, X: ???, Y: 0
DATAC 00000010000000000000000000000000 //Addr 1071.  LAddr 21.  COL: 00, X: ???, Y: 0
//-=END OF BLINKENLIGHT 2=-

This has taken several days to put together, and no small amount of "AAAAAAARRRRGH" directed towards my computer (both virtual or otherwise) when it inevitably failed.  The comments aren't great.  I think.  Either that, or I spend so long working on it my brain just goes numb.  Dunno.

Goals

I think I can work with this kernel to start building my actual OS idea.  Making, say, the first two processes basically be OS daemons, and then leaving the remaining six to be things a user can launch?  I think it'd work well.  Overlays would be mandatory, of course, but that is in-and-of-itself a neat challenge; writing an OS designed to run a chunk at a time, without disrupting other programs.

I also don't think it'd be that hard to expand into a WINDOWED operating system, especially if I can eventually manage an implementation of the TC-06 in C++ to get a performance boost (and enable a processor speed like, say, 180 kilohertz instead of the 180 hertz speed I'm stuck with right now) - that's definitely an end-game goal, though.  For now, terminals are probably easier to work with.

(+1)

Nice! This is pretty impressive. I'm not surprised it's caused some frustration.

Cooperative multitasking like this is pretty neat - IIRC MS Windows 3 used that. (I also think it's the best kind that is possible when we don't have any timer interrupts (without resorting to emulation anyway).)

I also agree that a windowed OS based on this would probably not be too hard - in fact, I suspect that might be easier than a terminal based one, since the screen is inherently graphical with no built-in text mode. Though I suppose that could be handled with some library/OS functions. Neither would be trivial, though.