The last few month's free time has been spent knee deep in the Operating System for my 8 bit micro. The main area of attack has been on implementing a console (screen and keyboard) driver, but I've been working on a few other things as well.
Up until recently I have not tried linking the code that makes up the monitor, or MAXI09OS. Instead the assembler source code files are concatenated together via include statements. While this works, it means all labels and other symbols are global across the code. It's generally not nice structuring things this way, especially as the project grows to be quite large.
MAXI09OS is now assembled into object files and linked using aslink, a component of asxxxx. This means that only symbols that need to be global are marked as such, and other named symbols can be file-local. In all, it is a much tidier arrangement. There are still some further improvements I'd like to make to how MAXI09OS is built, especially around the idea of producing header (include) files which the "user" programs will need. I also want to structure the source code tree so that drivers have their own subdirectory etc.
I have made a fair amount of progress on the console driver. It's far from the VT102-emulated terminal I have in mind for the "finished" version, but it is useable.
The first thing I did was split out the low level hardware aspects of the UART driver into a new source file. The reason for doing this is because the console driver shares much of the UART code for interfacing with the keyboard controller. Once this was done the UART driver itself was modified to use the low level functions and re-tested. As a consequence of the keyboard controller using the slower 9600 baud rate, the low level UART code also contains a routine for setting the rate. This is also useable by the higher level UART driver's open routine through the b register.
The first thing to get working, within the console driver, was keyboard input. This operates in a similar manner to the UART driver's reading operation except that within the interrupt handler the scan codes, obtained from the keyboard MCU, are fed through a translation routine to turn them into ASCII. This routine also deals with shift key and control key handling. As before, no attempt is currently made to deal with key repeat. The resultant ASCII characters are fed into the user side circular buffer, to be picked up by the sysread subroutine when the task is woken up by a signal.
One additional complication revolves around "virtual" console support. Like with UART ports, each console is a "unit". But obviously the input side (keyboard) and output side (screen) are shared. Since they require very little MPU resources, and only consume the relatively bountiful video memory in the V9958 VDC, there are six virtual consoles. In the keyboard interrupt handler the function keys F1 to F6 are checked and if one is pushed the appropriate console is displayed by writing to the "pattern name" base register. Video memory is arranged as follows:
- 0x0000 - patterns AKA fonts
- 0x8000 - console unit 0
- 0x9000 - console unit 1
This continues up to console unit 5. Note a few details: I am only using the lowest 64KB of video RAM out of a possible 192KB, and also that although the consoles only consume 24 rows of 80 columns (1960) bytes, it is not possible to pack the consoles closer together because of limitations in the pattern name base register. Anyway, this all means that console switching is instantaneous, something I'm quite pleased with.
Actually getting the console to display something was accomplished by "porting" the previously written V9958 monitor VDC code into the console's write function. The code has been tidied up quite a lot but still only supports the very basic control codes: carriage return, new line, backspace, tab, and formfeed - which, per the norm, clears the console. I have also implemented vertical scrolling.
One quite serious restriction exists. Since the VDC manages its video memory via address pointer registers it is necessary to prevent any other task from manipulating the VDC's registers. The first, brute force, approach to this was to disable interrupts while the VDC is being used to stop other tasks from being scheduled. This certainly worked but it prevents all interrupts from being serviced, even if that interrupt had nothing to do with a task which might be using the VDC.
The solution I'm currently using is to leave interrupts enabled, but disable task switching. This is accomplished by having the scheduler return into the current task if task switching is disabled.
This solution remains suboptimal; there might be tasks waiting to run which are not using the VDC; they are needlessly blocked from running. The better way is to use a mutex to pause tasks that want access to the VDC once one task has obtained access to it. Other tasks would not be affected. I might try implementing some mutex routines, though I am concerned about them being efficient enough.
Putting this console driver with the work described previously, I now have a test setup for my little OS which includes the following test tasks, each running on their own virtual console:
- Firstly the timer-based task as previously described. It opens the console and a timer, starting a repeating second based timer, until the space bar is pressed. At which point a 5 second non repeating timer starts. It also shows the hex values for what is read from the console driver, ie. the ASCII values for key down events.
- Two copies of the "enter a string, print the string" looping task.
- A serial terminal. This ties together a virtual console and a UART port. Both are opened and then a loop entered. In the loop both devices are waited on. The device that caused the task to wake is read, and the opposite task is written to. UART port 1 is used here.
- I also have a task which performs the "enter a string, print the string" operation on UART port 0.
In general, things are looking pretty good. The timer task updates its console while another tasks's console is displayed, and the UART port presents its task simultaneously.
When it comes to the serial terminal task, there are a few issues, beyond the fact that the serial terminal is only honouring the very simplest control codes.
The big issue is with scrolling. The V9958 is a great video controller but it has no special support hardware for scrolling the screen in text mode. Instead the video RAM data has to be read into MPU RAM and then written out again, but shifted up a line. For most uses of the console this is ok; the task just has some extra work to do to scroll the display. But for the serial terminal task this extra work needs to be done before the serial ports FIFO fills up.
This problem is, perhaps, unfixable at least without hardware assistance. The problem lies in the time it takes the MPU to do the line scroll vs the time taken for the UART to receive a line of text. At 9600 baud a full line of text will take roughly 1 / (9600 / 10) by 80 characters which is 83ms. I have cycle counted the MPU scroll code and it takes - ignoring setup code - 43ms. This means everything is fine if the lines are long, but if a (say) 8 char line is received there is only 8.3ms to scroll the screen, which isn't long enough. Even with the UART FIFO and MPU circular buffer, data loss will eventually occur as "short" text lines are received.
The "solution" I have come up with is to scroll the screen in half-screen chunks instead of every line. This reduces both the frequency of scrolling that's required, and it also reduces the work required to do the scrolling since more time is spent filling the video memory with zeros instead of writing out previously read in data. The result of this is not very pretty - using the console feels a bit strange - but it mostly cures the data loss problem. Only mostly though; on very short lines - the yes command is a nice way to show the problem - occasionally characters are still lost.
There is, perhaps, a better solution involving the usage of the UARTs hardware flow control lines. Hardware flow control can be used to pause data transmission when the receiver is not ready to receive it. In theory this could be used to pause the transfer while the MPU is scrolling the display. Unfortunately despite playing with this for a few hours I cannot get it to work.
So far I'm very happy with my OS's progress. The scrolling problem is annoying but I'm going to move on - serial terminal support was always going to be used as a way to test the driver model instead of being anything actually "practical". The serial terminal is a nice way to improve, and learn about, terminal emulation though, when I get around to that.
There's a few different things I could work on next:
- The previously mentioned VT102 improvements.
- Some ability to measure how busy the system is would be nice. Currently I use the idle LED to get a rough idea for how idle the system is, but it would be nice to know which tasks are using the MPU the most.
- Some means for open device inheritance when a task creates another task. Currently each task is responsible for opening a UART port, or a console. It would be better if the parent task did it as this would mean the same task code could be used regardless of the IO device. It could then even be possible to have multiple instances of the same task's code running multiple times if a task either used only the stack or kept its variables in allocated memory, with the pointer to that memory stored in the stack (or perhaps the u register)
- Another massive chunk of the Operating System revolves around proving disk access to tasks. I have thus far given this problem very little thought.
I feel what I have now is at a nice point to go back and fill in some smallish missing parts before I start thinking about the disk routines, which will be a large chunk of work. Some smaller areas to tackle include:
- Tidying up the source tree into different subsystem directories.
- Improve the keyboard behaviour. Key repeat is something I've put off doing for months. And then there's support for the caps lock key.
- Reworking the debug console to make the output cleaner and easier to read.
- UART transmit interrupts.
- Improving interrupt processing speed. This also involves some work on DISCo's VHDL. I have a few ideas for how to simplify, and thus speed up, interrupt processing.
- More device drivers, for example a joystick driver would be interesting to do. It would need polled and event (changes) modes.
I've also made a short video showing the test tasks, including the serial terminal:
So many ideas, but so little time...