[ Index ] [ Prev: Taming The MMU, continued ] [ Next: Introducing The CPU ]
At the same time I was sorting MMU details out, I continued reading the OS/MP header files and doing simple experiments at the PROM or with simple bits of code.
But getting my results through the front LED shortly proved itself to be limited: at this point, I needed to know values, and I was not keen writing a morse code routine to go further! What I needed was a simple serial console output routine.
In the same way I had been blinking the LED, I could play with the serial port registers. Of the two ZSCC in the machine, one drives the keyboard and mouse port, the other drives the to RS-232 ports. The OS/MP boot messages told me that
zs1 at obio 0x80004000 pri 3 zs0 at obio 0x80008000 pri 3So the serial port I was interested in was zs0, at address 80008000 onwards.
Of course, it would be too naive to expect the serial port to be wired in the same way it is on Sun hardware. kapdev/zsreg.h tells us that, even on a chip with two registers, things can become horribly convoluted and confusing:
#ifndef sun386 struct zscc_device { #ifdef solbourne #ifdef kbus unsigned char zscc_control; unsigned char fill1[15]; /* Filler */ unsigned char zscc_data; unsigned char fill2[15]; /* Filler */ #endif kbus #ifdef S4000 unsigned char zscc_control; unsigned char fill1[7]; unsigned char zscc_data; unsigned char fill2[7]; #endif S4000 #else solbourne unsigned char zscc_control; unsigned char :8; /* Filler */ unsigned char zscc_data; unsigned char :8; /* Filler */ #endif solbourne }; #else sun386 #ifdef SUN386 struct zscc_device { unsigned int zscc_control; unsigned int zscc_data; }; #endif SUN386 #ifdef AT386 struct zscc_device { unsigned char zscc_control; unsigned char zscc_data; unsigned char :8; /* Filler */ unsigned char :8; /* Filler */ }; #endif AT386 #endif !sun386Ok, I was naughty, I pasted the whole definition, which covers Sun SPARC-based hardware, the Sun 386i hardware, and SunOS on regular PC clone hardware, and adds the idt (S4000) and kbus Solbourne machines (which can't even use the same wiring!) While you are looking at this, note the old-fashioned, but effective, way to declare anonymous padding fields with anonymous bit fields:
unsigned char :8; /* Filler */
Back to our address hunt. The ZSCC has two ports, each port being covered by a struct zscc_device above, port B being at the lowest address. For the S4000, we can see that struct zscc_device is 16 bytes long, with the control register at offset 0, and the data register at offset 8. Our map is then:
80008000 port B control 80008008 port B data 80008010 port A control 80008018 port A data
Now all I needed was to implement a delay after transmitting characters. Not wanting to tackle the clock yet, I opted for a simple countdown loop. I ended up with the following code:
#define MINIPUTC(c) \ call _C_LABEL(miniputc); \ mov c, %o0 ENTRY(miniputc) save %sp, -64, %sp sethi %hi(ZS0_BASE), %l1 1: ldub [%l1 + %lo(ZS0_BASE) + 0x10], %l2 btst 4, %l2 ! ZS_RR0_TX_READY be 1b nop stb %i0, [%l1 + %lo(ZS0_BASE) + 0x18] set 0x1000, %l1 ! wait for transmitter to settle 1: cmp %l1, 0 bne 1b dec %l1 ret restoreAnd I started to sprinkle calls to MINIPUTC in my kernel, to monitor how far it would go before crashing.
For the record, the first word the BSD kernel wrote on the serial port was ``ABCDEFGHIJ'', letter by letter.
While I could play with MINIPUTC a longer time, my life would become much easier if I could turn this routine into a real console output, in order to be able to use printf from the kernel.
On the sparc port, this is done by forcing the console structure to point to a simple PROM console, which invokes the PROM input and output routines. On the Solbourne, things are not that easy - the PROM interface does not have dedicated console routines. It is, however, possible to print a string by composing a command buffer which prints what we want, and have it evaluate it.
Sure, I could use this interface, but it felt gross. Kinda like hammering a fly. I decided to stick to the simple serial port routines, and turn them into kernel console entry points. All I needed was a few lines in (for now) machdep.c:
#include <dev/cons.h> cons_decl(early); struct consdev consdev_early = { earlycnprobe, earlycninit, earlycngetc, earlycnputc, nullcnpollc }; struct consdev *cn_tab = &consdev_early; void earlycnprobe(struct consdev *cn) { cn->cn_dev = makedev(0, 0); cn->cn_pri = CN_INTERNAL; } void earlycninit(struct consdev *cn) { } /* getc, putc in locore.s */By initializing cn_tab to my routines at compilation time, I could use the output functions early in the kernel (before consinit() actually gets invoked), without risking the output to be lost. The routines in locore.s did not change much:
ENTRY(earlycnputc) save %sp, -64, %sp sethi %hi(ZS0_BASE), %l1 1: ldub [%l1 + %lo(ZS0_BASE) + 0x10], %l2 btst 4, %l2 ! ZS_RR0_TX_READY be 1b nop stb %i1, [%l1 + %lo(ZS0_BASE) + 0x18] sethi %hi(0x1000), %l1 add %l1, %lo(0x1000), %l1 1: cmp %l1, 0 bne 1b dec %l1 ret restore ENTRY(earlycngetc) save %sp, -64, %sp sethi %hi(ZS0_BASE), %l1 1: ldub [%l1 + %lo(ZS0_BASE) + 0x10], %l2 btst 1, %l2 ! ZS_RR0_RX_READY be 1b nop ldub [%l1 + %lo(ZS0_BASE) + 0x18], %i0 sethi %hi(0x1000), %l1 add %l1, %lo(0x1000), %l1 1: cmp %l1, 0 bne 1b dec %l1 ret restore
One reboot later, the kernel could greet me with an early panic...
ok boot aoutbsd rarp: using IP address 10.0.1.164 = A0001A4 rarp: server at IP address 10.0.1.101 = A000165 Boot: tftp.ei()/aoutbsd Entry: 0xfd080000 Size: 0xc8000+0x1f6f0+0x336f8 panic: Unknown CPU type: cpu: impl 5, vers 0; mmu: impl -1, vers -1...and I could start working in the C code, for a change!
[ Index ] [ Prev: Taming The MMU, continued ] [ Next: Introducing The CPU ]