💾 Archived View for dioskouroi.xyz › thread › 29408046 captured on 2021-12-03 at 14:04:38. Gemini links have been rewritten to link to archived content
➡️ Next capture (2021-12-04)
-=-=-=-=-=-=-
________________________________________________________________________________
Nice work, congrats!
I think the most surprising thing to me is the lack of interrupts. If you read this, jstanley: did you struggle much with that decision? I think of interrupts as nearly universally available on 8-bit micros, much less 16, but I have also wondered before how important they really were for what those machines ended up being used for. The (S)NES polled their controllers' shift registers in a really inefficient way, but it turned out fine. On the other hand, though, the vertical blank interrupt was pretty critical to their software and, of course, it became extremely common for NES cartridges to add extra chips to raise interrupts on particular horizontal scanlines. But, aside from neat graphical tricks, maybe interrupts just weren't all that important?
The reason I didn't implement interrupts is that I couldn't understand how to do it.
It would be better with interrupts, but only by a little bit. The times I really feel the lack of interrupts are:
- the text editor sometimes takes a long time to redraw the screen after reading input, and can drop characters if you type too fast because it isn't ready to poll for another character yet
- if a program is stuck in an infinite loop and not doing any system calls, you can't kill it with ^C because the kernel never gets to find out that you typed ^C
Those problems would go away if the arrival of a character on the serial port triggered an interrupt which caused the kernel to grab the character from the UART and stick it in a buffer, but I'm not sure the increase in hardware complexity is worth it.
It's worth it. I played with Propeller chips a while back. They have multiple cores and a shared memory but no interrupts. They can work, but you dedicate an entire CPU (expensive!) to a task, usually. The scant times you don't need to is because you poured time into a more difficult cooperative multitasking solution that isn't flexible.
You don't need to implement a nested vectored interrupt controller. Even just a timer/counter with overflow interrupt would solve your current issues with lockup by allowing you to implement preemptive multitasking. Once you do that it would be easy-ish to extend to the serial controller.
Basically, on interrupt flag = 1,
1. Disable further interrupts (no nesting). This makes things easier to implement.
2. Save register state. For speed a special set of shadow registers can be used, or you can put register state on the stack.
3. Jump to interrupt routine code start.
4. Let interrupt handler run, clear interrupt flag, then return.
In the simplest case an input flag simply causes a CALL (push pc, jump to address). In that case the interrupt handler needs to know to save state. You can test flags between instructions to see if you need to inject the push/jump.
There is a neat story told by Bil Herd about the C128's VDC video chip:
https://youtu.be/wzMsgnnDIRE?t=597
> It turns out it didn't have an interrupt when you're done. And when I > figured out, well, the 6845 has got an interrupt, why don't you? I asked > the designer, he said "Well, you can simply look at a register" [...] From > then on, we had this running joke where nobody would wait for the phone to > ring, we just all kept checking our phones whenever this guy was around [...]
> nearly universally available on 8-bit micros, much less 16, but I have also wondered before how important they really were for what those machines ended up being used for
Usually available, yes. But not all that widely used by those systems. The stock Apple II for example, has nothing in it that can even generate an interrupt. You'd have to install an expansion card. The standard serial cards can generate an interrupt upon character receipt for example, but they were usually programmed with polled IO anyway. Later with network cards and hard disks, you do see some use of interrupts.
> On the other hand, though, the vertical blank interrupt was pretty critical to their software
The Atari 2600 had no hardware interrupt support at all. You could tell the graphics IC to sleep the CPU until the next scanline. You'd turn vblank on, generate 3 lines of vblank, then turn vblank off. You'd be lined up with vblank from that point if you counted carefully.
On an in-order-execution processor, most things can in principle be translated into periodic polling and cycle counting. But it involves some extraordinarily painful coding. A timer and an interrupt are quite a relief when you're doing something time-sensitive.
>"The stock Apple II for example, has nothing in it that can even generate an interrupt."
Sorry if this is a naive question but how did the keyboard on those work without interrupts?
There's a good comment from an older discussion here on that:
https://news.ycombinator.com/item?id=20404825
Oh wow, whose excellent idea was it to add a key that hard-reset the CPU right above the return key?
Ah this is fascinating. Thanks for the link. Cheers!
That surprised me too. The very awkward implementation of interrupts was also one of the things that held back the popularity of the Intel 8008.
The Atari 2600 doesn't use IRQs or NMIs. You have to sync with the frame by setting a timer, making sure your code doesn't run over it, and spinlocking on it until it expires.
However, the CPU can tell the video chip to make the CPU halt until the beginning of the next scanline though.
That's the only platform I know of that's anything like interruptless.
Are there many resources available for someone who has an interest in putting together a basic OS on top of older hardware? Lets say 6502 for an example - where would someone who is experienced with much higher languages (C#/Java, etc) begin to even learn something like that?
(Hi, this CPU is my project).
I got into this stuff by buying an RC2014[0] kit and putting it together, and learning a bit about how CP/M works on it.
If you're interested in that sort of thing, you could do much worse than buying an RC2014 and just writing low-level programs for it for fun.
CP/M is really incredibly simple. You have some "kernel" type stuff towards the top of memory, with routines for reading/writing files on disk, and interacting with the console and a paper tape "reader + punch". When you type a command, the named program is loaded from disk into memory starting at 0x100 and then execution starts at 0x100. To interact with hardware, the program calls the kernel's routines (analogous to system calls). And that's basically all there is to it.
The OS for my CPU is very similar in operation to CP/M, but more pleasant to use for someone coming from a Unix background.
[0]
Wait a minute, is that why DOS .com files are loaded at 0x100? CP/M compatibility?
Yes. The Program Segment Prefix is designed for CP/M compatibility:
https://faydoc.tripod.com/structures/13/1378.htm
yep, lots of other things, too. MS-DOS was CP/M-compatible.
Windows 95 was DOS-compatible.
and so on. and even though the base OS changed starting with Windows 2000, lots of compatibility things were kept because they were supported in previous versions of Windows or MS-DOS.
All the way down to files with special names such as "AUX" or "CON", and the backslash as directory path separator are all from CP/M, though the path thing happened to maintain CP/M compatibility, and not because CP/M had filesystem directories, because early on (maybe always?) it didn't have directories.
The backslash as path separator didn't come from CP/M (it also didn't have directories at the time). The forward slash couldn't be used because it was already heavily used for command-line switches in DOS 1.0 applications. That choice was likely copied from DEC TOPS-10 which many Microsoft employees would have been using.
Microsoft apparently wanted to break compatibility and use the forward slash, and even coded for the possibility by making the switch character configurable in the CONFIG.SYS of DOS 2.0, but IBM was against it.
DOS 1.0 used forward slashes for arguments because CP/M did the same. DOS 1.0 was almost an exact copy of CP/M, changing only a few small things, which is why it didn't support directories, either.
DOS 2 needed to be compatible with DOS 1, and DOS 1 needed to be able to run CP/M programs in order to capture the business it was gaining at that point, so DOS 2 kept what DOS 1 had, and so on. all the way up to Windows 11 today.
The backslash didn't come from CP/M, as I said. it came because they needed a directory separator for DOS 2, and being visually similar to the UNIX directory separator character (a simple diagonal line), the backslash was chosen. so, as I said earlier (very poorly, looking back), the backslash was chosen because of CP/M, but was _not_ copied from it.
CP/M didn't use the slash for arguments. Slashes as arguments in PC operating systems seemed to start with PC-DOS 1.0. And even then, the number of included programs with arguments could be counted on one hand.
True, but none of these things began with CP/M or DOS. They were present in systems from the 60's. "pip" for example was in RSX-11, and probably didn't originate there either.
In fact, DOS 1.x didn't have directories either. Those were added in DOS 2.0
yep, because DOS 1 was a virtual clone of CP/M and kept the same feature set.
Not sure! I doubt they would actually be compatible anyway since the CPU is too different. It seems likely that prior experience with CP/M COM files being loaded at 0x100 influenced the design though.
Yeah, QDOS was a CP/M clone for 8086. It wasn't source compatible, but was 'major ideas and structure compatible'.
The 6502 is a CPU, not a full machine - and the OS really is about interacting with the whole machine. You need to learn about a specific 6502-based system - the C64, the Apple II, the NES...
Moreover, usually when you’re writing a 6502 program, you’re writing a program targeted towards an individual machine, with no OS in the way - the program you’re writing is written against a particular machine; it effectively _is_ the OS.
So the answer is: learn how to write programs for a 6502-based machine first. Then, make your program an OS. Your ability to make it anything like a modern OS is going to be limited, though.
Do I have the website for you:
I've build the computer and simple OS/Monitor.
Learn C and dive in.
The various "osdev" communities are good places to hang out, though I personally find IRC to be somewhat better than the forums and such.
If you can reason about the code you write in a memory-byte-accurate and handwavingly "what it actually uses in assembly" manner, you can start in. Most hobby OS projects don't ever get past toy stage, a few do.
Unix, however, was designed to be portable, so getting Unix-like OSes running on all sorts of random stuff is easier than writing from scratch.
C on the 6502 is pretty painful.
Extremely painful.
* There are only three data registers (A, X, Y), and they aren't interchangeable. Values get spilled to the stack or memory constantly.
* Memory addresses are 16 bits, so they cannot be stored in registers at all, and performing even simple operations on them (like incrementing an address) requires carry handling.
* The stack is hard-limited to 256 bytes, so pushing too many temporary values onto the stack risks stack overflow.
* Addressing modes are limited. There is no "address + immediate offset" addressing mode, making pointers to dynamically allocated structures awkward to work with, and some indexed addressing modes will roll over if they're used to index past the end of a 256-byte page.
* There's also no stack-relative addressing mode (unless you move the stack pointer to the X register).
* No instructions to push or pop anything besides A or status flags.
The combination of all of this basically means there's no good way to do local variables on the 6502. IIRC the C compilers typically reserve a bunch of memory for a separate "C stack" to avoid these limitations, but this is rather inefficient in terms of both performance and memory use.
Idiomatic 6502 assembly tends to have a mindset of "all variables are global", with only a few bytes for temporary/intermediate values that you have to juggle around carefully and deliberately.
see
https://news.ycombinator.com/item?id=27324653
A project to port LLVM to the MOS 6502
and
http://calc6502.com/RobotGame/summary.html
comparing ASM, C and Forth of the same program
UNIX wasn't designed to be portable, it became portable around V5 edition.
6502 is a very simple CPU and you can learn all of its instructions in several hours. But as it uses 1-address instructions, the code gets pretty verbose. For example, to add 10 to a variable you will have to write:
LDA var CLC ADC #10 STA var
That's 4 lines and many keypresses for a single addition! You might want to invent slightly more high-level language that would compile into assembly, like this:
var, var2 = variables(2) var += 10 var2 << 5 # Assign 5 to var2
By the way, you can use Python and operator overloading so that the lines above written in Python will generate assembly code when executed. I have used << instead of = in assignment because Python doesn't allow to overload it.
Are there no advanced macro assemblers targeting 6502? For x86, that's what you'd normally use to automate cases like that.
Yes, there are plenty. Some are pretty advanced.
The early OS's were pretty simple. The best way to learn about these things is to study the other early ones. Some were little more than a bunch of routines shoved in ROM with a BASIC on top. Others were a little more sophisticated. All of them were hacked and abused in unspeakable ways.
They really only had to do a few things: boot from the disk, map files somehow to the disk, read/write/append to said files on the disk, and load and execute a program. Look for how each of the different systems tried to accomplish those tasks.
CP/M is the classic to look at. But consider things such as the Apple ROM, and the Apple DOS. The C64 ROM and how rather than having a DOS per se, it relied on a very "smart" disk drive. Atari has CIO, "Central I/O", which is an extensible device system. There's also FLEX, which is akin to CP/M in that it was a portable OS for 6800 based machines. TRS-DOS for the TRS-80 is a fascinating study, it has some really interesting features you find nowhere else.
Don't forget to look at the old Forth implementations, "language, editor, assembler and OS in 8K of memory". It's very (very) simple, but many folks in the day used it. It needs little more than block disk I/O. But, at the time, thats easier said than done if you take in to account things like sector interleave and other exotic disk layouts. The computers were so slow that you didn't put sectors back to back on the floppy disk. This is because while trying to read two consecutive sectors, by the time the system got done handling the read of the first sector, the second sector (assuming it was adjacent) would have passed the head already and you'd have to wait until it all came around again, which slowed things down dramatically. So, you'd alternate sectors (1 every 2 or 3 sectors), "interleaving" the sectors along the track. Obviously not something you'd need to worry about with modern solid state storage, but back in the day mapping a simple "load sector 123" in to the appropriate track and sector on the disk drive was an entire little subsystem of the OS.
The DOS part is the hardest part. Organizing files on the disk, keep track of the used sectors, the whole kit. There's a lot to do. The rest are little more than monitor routines. Read a serial port, write a serial port, handle an interrupt.
All of the old OS's pretty much have source code published somewhere today that you can audit. They're all in assembly (Z80, 8080, 6502, 6800). But they're a good study.
Doing a survey of them, you see how they try to all solve the same problems, but how they approach them in different ways.
Get a nice book on assembly and try to implement something that is simple enough to totally overview (say, making a traffic sensitive traffic light controller or something like that) starting at the reset vector. You can make your life a lot simpler by using an emulator.
Congratulations on getting this amazing machine working IRL. I think the best takeaway for me was the resources section of your README. It's veritable goldmine for anyone who wants to do something even remotely similar!
Is this in any way related to Udamonic Scamp (
https://udamonic.com/what-is-a-scamp.html
)?
They’re both 16-bit processors, but other than that appear unrelated.
I like my Udamonic Scamp 3, but I was a little disappointed when I finally realized I could accomplish basically the same thing by putting FlashFORTH on a $3 Arduino Nano clone. The Udamonic board has an additional 16 LEDs built-in, which is nice.
Nope, I only found out later that "SCAMP" appears to be the most widely used CPU name in history!
Very impressive!
The technical skills alone to do this are amazing, but what I'm absolutely in love with is the full on blinkenlights/rackmount/mini computer style of the hardware.
Named after this Scottish car? :)
https://en.wikipedia.org/wiki/Scottish_Aviation_Scamp
IBM also has a computer called the SCAMP.
https://en.wikipedia.org/wiki/IBM_5100
Checkout this awesome mock for a C64 OS:
http://move.rupy.se/file/C64-NaxOS.jpg
or the real thing:
https://www.c64-wiki.com/wiki/GEOS
and
http://toastytech.com/guis/c64g.html
(previous discussion of GEOS:
https://news.ycombinator.com/item?id=10188367
)
I wanted to note that abbreviations for signals like DI or AI are not very good for understanding, long names like DATA_IN or ADR_OUT would be better in my opinion.