Porting OpenBSD to the Solbourne S4000

[ Index ] [ Prev: Taming The MMU, continued ] [ Next: Introducing The CPU ]


A Few Words on Serial

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 3 
So 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 !sun386
Ok, 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
	 restore
And 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 ]


miod@online.fr