NetBSD Port

Warning

This page is very much a work in progress!

Building

I've forked a copy of the NetBSD sources.

Kernel

NetBSD contains a port called hb68k which is designed for any 680x0 sbc type of board and I used that to create a variant for my SBC, which I've called SBC3.

git clone https://github.com/znac049/netbsd-src.git
cd netbsd-src
./build.sh -U -u -j4 -O ~/obj -m hb68k tools
./build.sh -U -u -j4 -O ~/obj -m hb68k kernel=SBC3
./build.sh -U -u -j4 -O ~/obj -m hb68k distribution
  • The first step builds the tools (compiler etc) that will be used to build the kernel and userspace.
  • The next step builds the kernel.
  • The final step takes a Long Time(tm) and builds the userspace, which will need to be added to the root filesystem.

Root File System

./mksbc3img.sh -k ~/obj/sys/arch/hb68k/compile/SBC3/netbsd -s 4096  sbc3.img

Device Driver Templates

I asked Claude about templates for coding device drivers. Here's what it said:

Uart

The template is myuart.c. It’s a complete single-channel UART driver for hb68k: it attaches through the device tree, can be the system console, and gives you a normal tty with interrupt-driven I/O.

What you change: the two CHIP-SPECIFIC sections near the top

  • Register map: placeholder offsets and bit definitions for data, status, control, baud divisor and interrupt enable. The real byte offset is reg << reg-shift, so register spacing comes from the device tree rather than the code.
  • Eight small hardware functions, the only code that touches the chip:
    • rx_ready / getc
    • tx_ready / putc
    • clear_errors
    • set_intr
    • intr_pending
    • program, which sets speed and frame format from termios flags. The example assumes a 16× divisor UART.
  • The compatible string your device-tree node will use.

Everything below those sections is generic, and you shouldn’t need to touch it for a first version: console, autoconfiguration, interrupt handling, soft interrupt, and the tty and cdevsw entry points. Each part has a comment explaining its role and the locking.

How data moves:

  • Receive: the hard interrupt copies bytes into a ring buffer. A soft interrupt later passes them to the tty layer, because the hard interrupt mustn’t call into it.
  • Transmit: the tty layer hands over a chunk of output. The transmit interrupt feeds it to the chip, then switches itself off when the chunk is done.
  • Console: the console uses polling and works from very early in boot. BREAK on the console drops into DDB.

Setup steps, all listed at the top of the file:

  1. Add the device/attach/file lines to files.hb68k.
  2. Add a major number to majors.hb68k. 14 is free in your tree.
  3. Add a myuart* at fdt? pass 5 line to the kernel config.
  4. Add a device-tree node.
  5. On the target, make the device node by hand (mknod /dev/ttyM0 c 14 0), since MAKEDEV won’t know about the driver.

Deliberately left out:

  • modem control (carrier is treated as always present)
  • hardware flow control
  • split input/output speeds
  • sending BREAK

The ioctl function marks where the last of these goes. Your mc68681_duart.c is a worked example of the same structure extended to a two-channel chip, with an IVR, if you want to compare.

One constraint carries over from your board: the driver’s lock is a spin mutex at IPL_VM, so the chip’s interrupt level in the device tree must be 5 or lower.

/*	$NetBSD$	*/

/*-
 * Copyright (c) 2026 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * myuart -- template driver for a simple, single-channel UART attached
 * via Flattened Device Tree (FDT), with console and tty support.
 *
 * ======================================================================
 * HOW TO USE THIS TEMPLATE
 * ======================================================================
 *
 * 1. Copy this file to e.g. sys/arch/hb68k/dev/<chip>.c and replace
 *    "myuart" with your driver name throughout (keep it short; it
 *    becomes the device name, e.g. "myuart0").
 *
 * 2. Fill in the section marked "CHIP-SPECIFIC" below.  Everything
 *    else is generic NetBSD plumbing that works for most UARTs and
 *    shouldn't need touching for a first version.
 *
 * 3. Hook it into the build:
 *
 *    sys/arch/hb68k/conf/files.hb68k:
 *	device	myuart: tty
 *	attach	myuart at fdt
 *	file	arch/hb68k/dev/myuart.c		myuart
 *
 *    sys/arch/hb68k/conf/majors.hb68k (pick an unused number < 144):
 *	device-major	myuart		char 14			myuart
 *
 *    Kernel config (e.g. conf/SBC3):
 *	myuart*		at fdt? pass 5
 *
 * 4. Describe the chip in the device tree:
 *
 *	serial@XXXXXX {
 *		compatible = "vendor,mychip";	(must match compat_data[])
 *		reg = <0xXXXXXX 0x10>;		(base, size of register block)
 *		reg-shift = <1>;		(log2 of register spacing)
 *		clock-frequency = <1843200>;	(UART input clock, Hz)
 *		interrupts = <3 0>;		(IPL, vector; 0 = auto-vector)
 *	};
 *
 *    To make it the console, point /chosen/stdout-path at it, e.g.
 *	stdout-path = "serial1:9600n8";
 *    with an alias "serial1" for the node.  The platform code must
 *    provide a static mapping for its registers (as sbc3_machdep.c
 *    does for the I/O window), because the console is set up before
 *    the VM system is running.
 *
 * 5. Device nodes: MAKEDEV doesn't know about a new driver, so create
 *    them by hand on the target (char major from step 3, minor = unit):
 *	mknod /dev/ttyM0 c 14 0
 *    and add a line to /etc/ttys if you want a login prompt on it.
 *
 * ======================================================================
 * HOW THE PIECES FIT TOGETHER
 * ======================================================================
 *
 *  Autoconfiguration	myuart_match() / myuart_attach() are called when
 *			the FDT bus finds a node whose "compatible"
 *			matches compat_data[].  attach maps the registers,
 *			allocates a tty and hooks up the interrupt.
 *
 *  Console		myuart_cnattach() runs very early (from
 *			consinit(), before autoconfiguration) if the
 *			node is /chosen/stdout-path.  The console uses
 *			polled I/O only: myuart_cngetc()/myuart_cnputc().
 *
 *  Hard interrupt	myuart_intr() runs at the device's hardware IPL.
 *			It must be quick: it moves received bytes into a
 *			ring buffer, feeds the transmitter, and schedules
 *			the soft interrupt.  It must not call into the
 *			tty layer.
 *
 *  Soft interrupt	myuart_soft() runs later at IPL_SOFTSERIAL.  It
 *			hands received bytes to the line discipline and,
 *			when a transmit burst finishes, asks the tty
 *			layer for more output.
 *
 *  tty entry points	open/close/read/write/ioctl/poll come from the
 *			cdevsw table; myuartstart() and myuartparam()
 *			are called by the tty layer to start output and
 *			to change speed/framing.
 *
 *  Locking		sc_lock is a spin mutex at the device's IPL.  It
 *			protects the ring buffer, the transmit pointers,
 *			the interrupt-enable shadow and register access
 *			shared between the interrupt handler and the
 *			rest of the driver.
 *
 * This template deliberately leaves out modem control (carrier is
 * assumed always present), hardware flow control, split speeds, and
 * power management.  Add them later if your hardware needs them.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD$");

#include <sys/param.h>
#include <sys/bus.h>
#include <sys/conf.h>
#include <sys/device.h>
#include <sys/fcntl.h>
#include <sys/intr.h>
#include <sys/kauth.h>
#include <sys/poll.h>
#include <sys/proc.h>
#include <sys/syslog.h>
#include <sys/systm.h>
#include <sys/tty.h>

#include <dev/cons.h>

#include <dev/fdt/fdtvar.h>
#include <dev/fdt/fdt_console.h>

/*
 * ######################################################################
 * CHIP-SPECIFIC: registers
 * ######################################################################
 *
 * Replace these with your chip's register map.  Offsets are register
 * *numbers*; the real byte offset is (reg << reg-shift), so the same
 * code works whether the chip sits on every byte, every other byte,
 * or every fourth byte.
 *
 * The placeholder layout below is a typical simple UART:
 *	DATA	read: receive holding register, write: transmit holding
 *	STATUS	read-only status bits
 *	CTRL	frame format and enables
 *	BAUD	baud rate divisor (16 bits, split over two registers)
 *	IER	interrupt enable bits
 */
#define	MYUART_REG_DATA		0
#define	MYUART_REG_STATUS	1
#define	MYUART_REG_CTRL		2
#define	MYUART_REG_BAUDLO	3
#define	MYUART_REG_BAUDHI	4
#define	MYUART_REG_IER		5

/* STATUS bits */
#define	MYUART_SR_RXRDY		0x01	/* a received byte is waiting */
#define	MYUART_SR_TXRDY		0x02	/* transmit register is empty */
#define	MYUART_SR_OVERRUN	0x10	/* receiver overrun */
#define	MYUART_SR_PARITY	0x20	/* parity error on this byte */
#define	MYUART_SR_FRAMING	0x40	/* framing error on this byte */
#define	MYUART_SR_BREAK		0x80	/* break received */

/* CTRL bits */
#define	MYUART_CTRL_ENABLE	0x01	/* enable receiver + transmitter */
#define	MYUART_CTRL_8BIT	0x02	/* 8 data bits (else 7) */
#define	MYUART_CTRL_PARENB	0x04	/* parity enable */
#define	MYUART_CTRL_PARODD	0x08	/* odd parity (else even) */
#define	MYUART_CTRL_2STOP	0x10	/* 2 stop bits (else 1) */

/* IER bits */
#define	MYUART_IER_RX		0x01	/* interrupt when RXRDY */
#define	MYUART_IER_TX		0x02	/* interrupt when TXRDY */

#define	MYUART_DEFAULT_FREQ	1843200	/* if the DT has no clock-frequency */
#define	MYUART_DEFAULT_SPEED	9600	/* if stdout-path has no speed */

/*
 * Everything needed to talk to the registers.  Shared by the console
 * (which runs before the softc exists) and the attached device.
 */
struct myuart_regs {
	bus_space_tag_t		ur_bst;
	bus_space_handle_t	ur_bsh;
	u_int			ur_shift;	/* reg-shift */
	u_int			ur_freq;	/* input clock, Hz */
};

static inline uint8_t
myuart_read(const struct myuart_regs *ur, u_int reg)
{
	return bus_space_read_1(ur->ur_bst, ur->ur_bsh, reg << ur->ur_shift);
}

static inline void
myuart_write(const struct myuart_regs *ur, u_int reg, uint8_t val)
{
	bus_space_write_1(ur->ur_bst, ur->ur_bsh, reg << ur->ur_shift, val);
}

/*
 * ######################################################################
 * CHIP-SPECIFIC: low-level operations
 * ######################################################################
 *
 * These eight small functions are the whole hardware interface.  The
 * rest of the driver only ever touches the chip through them.
 */

/* Is a received byte waiting?  Returns the status bits for that byte. */
static inline bool
myuart_hw_rx_ready(const struct myuart_regs *ur, uint8_t *srp)
{
	const uint8_t sr = myuart_read(ur, MYUART_REG_STATUS);

	*srp = sr;
	return (sr & MYUART_SR_RXRDY) != 0;
}

/* Read the received byte.  Call only after rx_ready returned true. */
static inline uint8_t
myuart_hw_getc(const struct myuart_regs *ur)
{
	return myuart_read(ur, MYUART_REG_DATA);
}

/* Can another byte be written to the transmitter? */
static inline bool
myuart_hw_tx_ready(const struct myuart_regs *ur)
{
	return (myuart_read(ur, MYUART_REG_STATUS) & MYUART_SR_TXRDY) != 0;
}

/* Write one byte to the transmitter. */
static inline void
myuart_hw_putc(const struct myuart_regs *ur, uint8_t c)
{
	myuart_write(ur, MYUART_REG_DATA, c);
}

/*
 * Clear any error / break condition after it has been read.  Many
 * chips clear errors automatically when the data register is read;
 * if yours does, leave this empty.
 */
static inline void
myuart_hw_clear_errors(const struct myuart_regs *ur)
{
	/* e.g. myuart_write(ur, MYUART_REG_CTRL, ... | RESET_ERRORS); */
}

/*
 * Set the interrupt enables.  `rx' and `tx' say which sources may
 * interrupt.  The transmit interrupt must only be enabled while there
 * is something to send: most UARTs assert "transmitter empty"
 * continuously, so leaving it enabled would interrupt forever.
 */
static inline void
myuart_hw_set_intr(const struct myuart_regs *ur, bool rx, bool tx)
{
	myuart_write(ur, MYUART_REG_IER,
	    (rx ? MYUART_IER_RX : 0) | (tx ? MYUART_IER_TX : 0));
}

/*
 * Did this chip cause the current interrupt?  Used to share an
 * interrupt level with other devices.  If your chip has an "interrupt
 * pending" bit, test that instead.
 */
static inline bool
myuart_hw_intr_pending(const struct myuart_regs *ur, bool tx_enabled)
{
	const uint8_t sr = myuart_read(ur, MYUART_REG_STATUS);

	return (sr & (MYUART_SR_RXRDY | MYUART_SR_BREAK)) != 0 ||
	    (tx_enabled && (sr & MYUART_SR_TXRDY) != 0);
}

/*
 * Program speed and frame format, and enable the receiver and
 * transmitter.  Returns 0, or EINVAL if the speed or format can't be
 * done.  Called with interrupts for this device blocked.
 *
 * `cflag' uses the termios(4) bits: CSIZE (CS5..CS8), CSTOPB, PARENB,
 * PARODD.
 */
static int
myuart_hw_program(const struct myuart_regs *ur, int speed, tcflag_t cflag)
{
	uint8_t ctrl = MYUART_CTRL_ENABLE;
	u_int div;

	if (speed <= 0) {
		return EINVAL;
	}

	/*
	 * Example for a 16x-oversampling divisor UART:
	 *	divisor = clock / (16 * speed), rounded to nearest.
	 * Reject rates more than 3% off.
	 */
	div = (ur->ur_freq + 8 * speed) / (16 * speed);
	if (div == 0 || div > 0xffff) {
		return EINVAL;
	}
	const u_int actual = ur->ur_freq / (16 * div);
	const u_int err = actual > (u_int)speed ?
	    actual - speed : speed - actual;
	if (err * 100 > (u_int)speed * 3) {
		return EINVAL;
	}

	switch (ISSET(cflag, CSIZE)) {
	case CS8:
		ctrl |= MYUART_CTRL_8BIT;
		break;
	case CS7:
		break;
	default:
		return EINVAL;		/* this example chip only does 7/8 */
	}
	if (ISSET(cflag, PARENB)) {
		ctrl |= MYUART_CTRL_PARENB;
		if (ISSET(cflag, PARODD)) {
			ctrl |= MYUART_CTRL_PARODD;
		}
	}
	if (ISSET(cflag, CSTOPB)) {
		ctrl |= MYUART_CTRL_2STOP;
	}

	myuart_write(ur, MYUART_REG_CTRL, 0);		/* quiesce */
	myuart_write(ur, MYUART_REG_BAUDLO, div & 0xff);
	myuart_write(ur, MYUART_REG_BAUDHI, (div >> 8) & 0xff);
	myuart_write(ur, MYUART_REG_CTRL, ctrl);

	return 0;
}

/*
 * ######################################################################
 * Device tree matching
 * ######################################################################
 *
 * List every "compatible" string this driver handles.
 */
static const struct device_compatible_entry compat_data[] = {
	{ .compat = "vendor,mychip" },
	DEVICE_COMPAT_EOL
};

/*
 * ######################################################################
 * Generic driver below this line
 * ######################################################################
 */

#define	MYUART_RBUF_SIZE	256	/* receive ring size, power of 2 */

struct myuart_softc {
	device_t		sc_dev;
	struct myuart_regs	sc_regs;
	kmutex_t		sc_lock;	/* see "Locking" above */
	void			*sc_ih;		/* hardware interrupt */
	void			*sc_si;		/* soft interrupt */
	struct tty		*sc_tty;

	bool			sc_console;	/* we are the console */
	bool			sc_rx_on;	/* shadow of RX intr enable */
	bool			sc_tx_on;	/* shadow of TX intr enable */

	/* Receive ring: (status << 8) | data, filled by myuart_intr(). */
	uint16_t		sc_rbuf[MYUART_RBUF_SIZE];
	u_int			sc_rbput;
	u_int			sc_rbget;
	u_int			sc_rbovf;	/* bytes dropped, ring full */

	/* Transmit burst, set up by myuartstart(). */
	u_char			*sc_tba;	/* next byte to send */
	int			sc_tbc;		/* bytes left */
	bool			sc_tx_done;	/* burst finished */
};

static int	myuart_match(device_t, cfdata_t, void *);
static void	myuart_attach(device_t, device_t, void *);

CFATTACH_DECL_NEW(myuart, sizeof(struct myuart_softc),
    myuart_match, myuart_attach, NULL, NULL);

extern struct cfdriver myuart_cd;

static dev_type_open(myuartopen);
static dev_type_close(myuartclose);
static dev_type_read(myuartread);
static dev_type_write(myuartwrite);
static dev_type_ioctl(myuartioctl);
static dev_type_stop(myuartstop);
static dev_type_tty(myuarttty);
static dev_type_poll(myuartpoll);

/* The character device switch; its major number comes from majors.*. */
const struct cdevsw myuart_cdevsw = {
	.d_open = myuartopen,
	.d_close = myuartclose,
	.d_read = myuartread,
	.d_write = myuartwrite,
	.d_ioctl = myuartioctl,
	.d_stop = myuartstop,
	.d_tty = myuarttty,
	.d_poll = myuartpoll,
	.d_mmap = nommap,
	.d_kqfilter = ttykqfilter,
	.d_discard = nodiscard,
	.d_flag = D_TTY
};

static void	myuartstart(struct tty *);
static int	myuartparam(struct tty *, struct termios *);
static int	myuart_intr(void *);
static void	myuart_soft(void *);

/* Call with sc_lock held: update the hardware interrupt enables. */
static void
myuart_update_intr(struct myuart_softc *sc)
{
	KASSERT(mutex_owned(&sc->sc_lock));
	myuart_hw_set_intr(&sc->sc_regs, sc->sc_rx_on, sc->sc_tx_on);
}

/*
 * ----------------------------------------------------------------------
 * Console
 * ----------------------------------------------------------------------
 *
 * The console is set up long before autoconfiguration, so it can't use
 * the softc.  Its state lives in this static structure instead.
 */
static struct {
	struct myuart_regs	regs;
	bus_addr_t		addr;	/* to recognise ourselves in attach */
	int			speed;
	tcflag_t		cflag;
	bool			active;
} myuart_cons;

static cnm_state_t myuart_cnm_state;	/* console "magic" (break -> DDB) */

static int
myuart_cngetc(dev_t dev)
{
	uint8_t sr;
	int s, c;

	s = splhigh();
	while (!myuart_hw_rx_ready(&myuart_cons.regs, &sr)) {
		continue;
	}
	c = myuart_hw_getc(&myuart_cons.regs);
	splx(s);

	return c;
}

static void
myuart_cnputc(dev_t dev, int c)
{
	int s;

	s = splhigh();
	while (!myuart_hw_tx_ready(&myuart_cons.regs)) {
		continue;
	}
	myuart_hw_putc(&myuart_cons.regs, c);
	splx(s);
}

static void
myuart_cnpollc(dev_t dev, int on)
{
	/* Nothing to do: the console routines always poll. */
}

static struct consdev myuart_consdev = {
	.cn_getc = myuart_cngetc,
	.cn_putc = myuart_cnputc,
	.cn_pollc = myuart_cnpollc,
	.cn_dev = NODEV,		/* filled in by myuart_attach() */
	.cn_pri = CN_NORMAL,
};

static int
myuart_console_match(int phandle)
{
	return of_compatible_match(phandle, compat_data);
}

/*
 * Called from consinit() when /chosen/stdout-path points at one of our
 * nodes.  `uart_freq' is the node's clock-frequency (or the platform's
 * fallback).
 */
static void
myuart_cnattach(struct fdt_attach_args *faa, u_int uart_freq)
{
	const int phandle = faa->faa_phandle;
	struct myuart_regs * const ur = &myuart_cons.regs;
	bus_addr_t addr;
	bus_size_t size;

	if (fdtbus_get_reg(phandle, 0, &addr, &size) != 0) {
		return;
	}

	ur->ur_bst = faa->faa_bst;
	ur->ur_shift = fdtbus_get_reg_shift(phandle, 0);
	ur->ur_freq = uart_freq ? uart_freq : MYUART_DEFAULT_FREQ;

	/* This must hit a static mapping: UVM isn't running yet. */
	if (bus_space_map(ur->ur_bst, addr, size, 0, &ur->ur_bsh) != 0) {
		return;
	}

	/* Speed and format from stdout-path, e.g. "serial0:9600n8". */
	myuart_cons.speed = fdtbus_get_stdout_speed();
	if (myuart_cons.speed < 0) {
		myuart_cons.speed = MYUART_DEFAULT_SPEED;
	}
	myuart_cons.cflag = fdtbus_get_stdout_flags();

	myuart_hw_set_intr(ur, false, false);
	if (myuart_hw_program(ur, myuart_cons.speed,
			      myuart_cons.cflag) != 0) {
		/* Fall back to something sane rather than staying silent. */
		myuart_cons.speed = MYUART_DEFAULT_SPEED;
		myuart_cons.cflag = CS8 | CREAD | CLOCAL;
		if (myuart_hw_program(ur, myuart_cons.speed,
				      myuart_cons.cflag) != 0) {
			return;
		}
	}

	myuart_cons.addr = addr;
	myuart_cons.active = true;
	cn_tab = &myuart_consdev;
}

static const struct fdt_console myuart_fdt_console = {
	.match = myuart_console_match,
	.consinit = myuart_cnattach,
};

FDT_CONSOLE(myuart, &myuart_fdt_console);

/*
 * ----------------------------------------------------------------------
 * Autoconfiguration
 * ----------------------------------------------------------------------
 */

static int
myuart_match(device_t parent, cfdata_t cf, void *aux)
{
	struct fdt_attach_args * const faa = aux;

	return of_compatible_match(faa->faa_phandle, compat_data);
}

static void
myuart_attach(device_t parent, device_t self, void *aux)
{
	struct myuart_softc * const sc = device_private(self);
	struct fdt_attach_args * const faa = aux;
	const int phandle = faa->faa_phandle;
	struct myuart_regs * const ur = &sc->sc_regs;
	char intrstr[128];
	bus_addr_t addr;
	bus_size_t size;
	struct tty *tp;

	sc->sc_dev = self;

	/* --- Registers ------------------------------------------------ */
	if (fdtbus_get_reg(phandle, 0, &addr, &size) != 0) {
		aprint_error(": couldn't get registers\n");
		return;
	}
	ur->ur_bst = faa->faa_bst;
	ur->ur_shift = fdtbus_get_reg_shift(phandle, 0);
	if (of_getprop_uint32(phandle, "clock-frequency", &ur->ur_freq) != 0) {
		ur->ur_freq = MYUART_DEFAULT_FREQ;
	}
	if (bus_space_map(ur->ur_bst, addr, size, 0, &ur->ur_bsh) != 0) {
		aprint_error(": couldn't map registers\n");
		return;
	}

	sc->sc_console = myuart_cons.active && myuart_cons.addr == addr;

	/*
	 * The lock is a spin mutex at IPL_VM, which on hb68k blocks
	 * hardware levels 1-5.  So the device's interrupt level (first
	 * cell of "interrupts") must be 5 or lower.
	 */
	mutex_init(&sc->sc_lock, MUTEX_DEFAULT, IPL_VM);

	aprint_naive("\n");
	aprint_normal(": UART, %u Hz clock%s\n", ur->ur_freq,
	    sc->sc_console ? ", console" : "");

	/* --- Quiet the chip (but leave the console's settings alone) - */
	mutex_spin_enter(&sc->sc_lock);
	sc->sc_rx_on = sc->sc_tx_on = false;
	myuart_update_intr(sc);
	mutex_spin_exit(&sc->sc_lock);

	/* --- tty ------------------------------------------------------ */
	tp = tty_alloc();
	tp->t_oproc = myuartstart;	/* tty layer: "start output" */
	tp->t_param = myuartparam;	/* tty layer: "change settings" */
	tty_attach(tp);
	sc->sc_tty = tp;

	if (sc->sc_console) {
		/* Tell the console layer which /dev entry is the console. */
		myuart_consdev.cn_dev = makedev(
		    cdevsw_lookup_major(&myuart_cdevsw), device_unit(self));
		aprint_normal_dev(self, "console\n");
	}

	/* --- Interrupts ----------------------------------------------- */
	sc->sc_si = softint_establish(SOFTINT_SERIAL, myuart_soft, sc);
	if (sc->sc_si == NULL) {
		aprint_error_dev(self, "couldn't establish soft interrupt\n");
		return;
	}

	if (!fdtbus_intr_str(phandle, 0, intrstr, sizeof(intrstr))) {
		aprint_error_dev(self, "couldn't decode interrupt\n");
		return;
	}
	sc->sc_ih = fdtbus_intr_establish_xname(phandle, 0, IPL_VM,
	    FDT_INTR_MPSAFE, myuart_intr, sc, device_xname(self));
	if (sc->sc_ih == NULL) {
		aprint_error_dev(self,
		    "couldn't establish interrupt at %s\n", intrstr);
		return;
	}
	aprint_normal_dev(self, "interrupting at %s\n", intrstr);

	/*
	 * The console listens all the time, so that a BREAK (or the
	 * configured magic sequence) can drop into DDB even when no
	 * process has the tty open.
	 */
	if (sc->sc_console) {
		cn_init_magic(&myuart_cnm_state);
		cn_set_magic("\047\001");	/* BREAK */

		mutex_spin_enter(&sc->sc_lock);
		sc->sc_rx_on = true;
		myuart_update_intr(sc);
		mutex_spin_exit(&sc->sc_lock);
	}
}

/*
 * ----------------------------------------------------------------------
 * Hardware interrupt
 * ----------------------------------------------------------------------
 *
 * Runs at the device's IPL with sc_lock held.  Keep it short.
 */

static void
myuart_rxintr(struct myuart_softc *sc)
{
	struct myuart_regs * const ur = &sc->sc_regs;
	const bool open = ISSET(sc->sc_tty->t_state, TS_ISOPEN);
	uint8_t sr, c;

	while (myuart_hw_rx_ready(ur, &sr)) {
		c = myuart_hw_getc(ur);

		if (sr & (MYUART_SR_OVERRUN | MYUART_SR_PARITY |
			  MYUART_SR_FRAMING | MYUART_SR_BREAK)) {
			myuart_hw_clear_errors(ur);
		}

		/* Console magic: BREAK (or a byte sequence) -> DDB. */
		if (sc->sc_console) {
			if (sr & MYUART_SR_BREAK) {
				cn_check_magic(myuart_consdev.cn_dev,
				    CNC_BREAK, myuart_cnm_state);
			} else {
				cn_check_magic(myuart_consdev.cn_dev,
				    c, myuart_cnm_state);
			}
		}

		if (!open || (sr & MYUART_SR_BREAK)) {
			continue;	/* nobody listening, or a BREAK */
		}

		if (sc->sc_rbput - sc->sc_rbget >= MYUART_RBUF_SIZE) {
			sc->sc_rbovf++;	/* ring full: drop it */
			continue;
		}
		sc->sc_rbuf[sc->sc_rbput++ & (MYUART_RBUF_SIZE - 1)] =
		    ((uint16_t)sr << 8) | c;
	}
}

static void
myuart_txintr(struct myuart_softc *sc)
{
	struct myuart_regs * const ur = &sc->sc_regs;

	/* Fill the transmitter (or its FIFO) as far as it will go. */
	while (sc->sc_tbc > 0 && myuart_hw_tx_ready(ur)) {
		myuart_hw_putc(ur, *sc->sc_tba++);
		sc->sc_tbc--;
	}

	if (sc->sc_tbc == 0) {
		/* Burst done: stop TX interrupts, let the softint refill. */
		sc->sc_tx_on = false;
		myuart_update_intr(sc);
		sc->sc_tx_done = true;
	}
}

static int
myuart_intr(void *arg)
{
	struct myuart_softc * const sc = arg;

	mutex_spin_enter(&sc->sc_lock);
	if (!myuart_hw_intr_pending(&sc->sc_regs, sc->sc_tx_on)) {
		mutex_spin_exit(&sc->sc_lock);
		return 0;	/* not ours (shared interrupt level) */
	}

	myuart_rxintr(sc);
	if (sc->sc_tx_on) {
		myuart_txintr(sc);
	}
	mutex_spin_exit(&sc->sc_lock);

	softint_schedule(sc->sc_si);
	return 1;
}

/*
 * ----------------------------------------------------------------------
 * Soft interrupt
 * ----------------------------------------------------------------------
 *
 * Runs at IPL_SOFTSERIAL, where it's safe to call the tty layer.
 */
static void
myuart_soft(void *arg)
{
	struct myuart_softc * const sc = arg;
	struct tty * const tp = sc->sc_tty;
	uint16_t ent;
	u_int ovf;
	bool tx_done;
	int c;

	/* Report ring-buffer overflows (not too often: use log(9)). */
	mutex_spin_enter(&sc->sc_lock);
	ovf = sc->sc_rbovf;
	sc->sc_rbovf = 0;
	mutex_spin_exit(&sc->sc_lock);
	if (ovf != 0) {
		log(LOG_WARNING, "%s: %u receive overflow(s)\n",
		    device_xname(sc->sc_dev), ovf);
	}

	/* Hand received bytes to the line discipline. */
	for (;;) {
		mutex_spin_enter(&sc->sc_lock);
		if (sc->sc_rbget == sc->sc_rbput) {
			mutex_spin_exit(&sc->sc_lock);
			break;
		}
		ent = sc->sc_rbuf[sc->sc_rbget++ & (MYUART_RBUF_SIZE - 1)];
		mutex_spin_exit(&sc->sc_lock);

		c = ent & 0xff;
		if (ent & (MYUART_SR_FRAMING << 8)) {
			SET(c, TTY_FE);
		}
		if (ent & (MYUART_SR_PARITY << 8)) {
			SET(c, TTY_PE);
		}
		if (ISSET(tp->t_state, TS_ISOPEN)) {
			(*tp->t_linesw->l_rint)(c, tp);
		}
	}

	/* If a transmit burst finished, release it and ask for more. */
	mutex_spin_enter(&sc->sc_lock);
	tx_done = sc->sc_tx_done;
	sc->sc_tx_done = false;
	mutex_spin_exit(&sc->sc_lock);

	if (tx_done) {
		CLR(tp->t_state, TS_BUSY);
		if (ISSET(tp->t_state, TS_FLUSH)) {
			CLR(tp->t_state, TS_FLUSH);
		} else {
			ndflush(&tp->t_outq,
			    (int)(sc->sc_tba - tp->t_outq.c_cf));
		}
		(*tp->t_linesw->l_start)(tp);
	}
}

/*
 * ----------------------------------------------------------------------
 * tty layer callbacks
 * ----------------------------------------------------------------------
 */

static struct myuart_softc *
myuart_lookup(dev_t dev)
{
	struct myuart_softc *sc;

	sc = device_lookup_private(&myuart_cd, TTUNIT(dev));
	if (sc == NULL || sc->sc_si == NULL || sc->sc_ih == NULL) {
		return NULL;	/* no such unit, or attach failed */
	}
	return sc;
}

/*
 * Start output: take a contiguous chunk of the tty's output queue and
 * let the transmit interrupt send it.
 */
static void
myuartstart(struct tty *tp)
{
	struct myuart_softc * const sc = myuart_lookup(tp->t_dev);
	int s;

	if (sc == NULL) {
		return;
	}

	s = spltty();
	if (ISSET(tp->t_state, TS_BUSY | TS_TIMEOUT | TS_TTSTOP)) {
		goto out;	/* already sending, or stopped (^S) */
	}
	if (!ttypull(tp)) {
		goto out;	/* nothing to send */
	}

	mutex_spin_enter(&sc->sc_lock);
	sc->sc_tba = tp->t_outq.c_cf;
	sc->sc_tbc = ndqb(&tp->t_outq, 0);
	SET(tp->t_state, TS_BUSY);
	sc->sc_tx_on = true;		/* interrupts as soon as TX is empty */
	myuart_update_intr(sc);
	mutex_spin_exit(&sc->sc_lock);
 out:
	splx(s);
}

/* Stop output (e.g. on ^S or a flush). */
static void
myuartstop(struct tty *tp, int flag)
{
	struct myuart_softc * const sc = myuart_lookup(tp->t_dev);

	if (sc == NULL) {
		return;
	}

	mutex_spin_enter(&sc->sc_lock);
	if (ISSET(tp->t_state, TS_BUSY)) {
		sc->sc_tbc = 0;		/* finish after the current byte */
		if (!ISSET(tp->t_state, TS_TTSTOP)) {
			SET(tp->t_state, TS_FLUSH);
		}
	}
	mutex_spin_exit(&sc->sc_lock);
}

/* Change speed / framing (stty, tcsetattr). */
static int
myuartparam(struct tty *tp, struct termios *t)
{
	struct myuart_softc * const sc = myuart_lookup(tp->t_dev);
	int error;

	if (sc == NULL) {
		return ENXIO;
	}

	/* Unsupported in this template: split speeds, flow control. */
	if (t->c_ispeed != 0 && t->c_ispeed != t->c_ospeed) {
		return EINVAL;
	}
	if (ISSET(t->c_cflag, CRTSCTS | MDMBUF)) {
		return EINVAL;
	}

	mutex_spin_enter(&sc->sc_lock);
	error = myuart_hw_program(&sc->sc_regs, t->c_ospeed, t->c_cflag);
	mutex_spin_exit(&sc->sc_lock);
	if (error) {
		return error;
	}

	tp->t_ispeed = 0;
	tp->t_ospeed = t->c_ospeed;
	tp->t_cflag = t->c_cflag;

	/* No modem control: carrier is always present. */
	(void)(*tp->t_linesw->l_modem)(tp, 1);

	return 0;
}

/*
 * ----------------------------------------------------------------------
 * cdevsw entry points
 * ----------------------------------------------------------------------
 */

static int
myuartopen(dev_t dev, int flag, int mode, struct lwp *l)
{
	struct myuart_softc * const sc = myuart_lookup(dev);
	struct tty *tp;
	int error;

	if (sc == NULL) {
		return ENXIO;
	}
	tp = sc->sc_tty;

	if (kauth_authorize_device_tty(l->l_cred, KAUTH_DEVICE_TTY_OPEN, tp)) {
		return EBUSY;
	}

	/* First open: set defaults and turn on receive interrupts. */
	if (!ISSET(tp->t_state, TS_ISOPEN) && tp->t_wopen == 0) {
		struct termios t;

		tp->t_dev = dev;

		t.c_ispeed = 0;
		if (sc->sc_console) {
			t.c_ospeed = myuart_cons.speed;
			t.c_cflag = myuart_cons.cflag;
		} else {
			t.c_ospeed = TTYDEF_SPEED;
			t.c_cflag = TTYDEF_CFLAG;
		}
		SET(t.c_cflag, CLOCAL);
		CLR(t.c_cflag, CRTSCTS | MDMBUF);

		tp->t_ospeed = 0;	/* force myuartparam() to program */
		(void)myuartparam(tp, &t);
		tp->t_iflag = TTYDEF_IFLAG;
		tp->t_oflag = TTYDEF_OFLAG;
		tp->t_lflag = TTYDEF_LFLAG;
		ttychars(tp);
		ttsetwater(tp);

		mutex_spin_enter(&sc->sc_lock);
		sc->sc_rbget = sc->sc_rbput = 0;
		sc->sc_rx_on = true;
		myuart_update_intr(sc);
		mutex_spin_exit(&sc->sc_lock);
	}

	error = ttyopen(tp, TTDIALOUT(dev), ISSET(flag, O_NONBLOCK));
	if (error == 0) {
		error = (*tp->t_linesw->l_open)(dev, tp);
	}
	return error;
}

static int
myuartclose(dev_t dev, int flag, int mode, struct lwp *l)
{
	struct myuart_softc * const sc = myuart_lookup(dev);
	struct tty *tp;

	if (sc == NULL) {
		return 0;
	}
	tp = sc->sc_tty;

	if (!ISSET(tp->t_state, TS_ISOPEN)) {
		return 0;
	}
	(*tp->t_linesw->l_close)(tp, flag);
	ttyclose(tp);

	/* Last close: go quiet (the console keeps listening for BREAK). */
	if (!ISSET(tp->t_state, TS_ISOPEN) && tp->t_wopen == 0 &&
	    !sc->sc_console) {
		mutex_spin_enter(&sc->sc_lock);
		sc->sc_rx_on = sc->sc_tx_on = false;
		myuart_update_intr(sc);
		mutex_spin_exit(&sc->sc_lock);
	}
	return 0;
}

static int
myuartread(dev_t dev, struct uio *uio, int flag)
{
	struct myuart_softc * const sc = myuart_lookup(dev);

	if (sc == NULL) {
		return ENXIO;
	}
	return (*sc->sc_tty->t_linesw->l_read)(sc->sc_tty, uio, flag);
}

static int
myuartwrite(dev_t dev, struct uio *uio, int flag)
{
	struct myuart_softc * const sc = myuart_lookup(dev);

	if (sc == NULL) {
		return ENXIO;
	}
	return (*sc->sc_tty->t_linesw->l_write)(sc->sc_tty, uio, flag);
}

static int
myuartpoll(dev_t dev, int events, struct lwp *l)
{
	struct myuart_softc * const sc = myuart_lookup(dev);

	if (sc == NULL) {
		return POLLHUP;
	}
	return (*sc->sc_tty->t_linesw->l_poll)(sc->sc_tty, events, l);
}

static struct tty *
myuarttty(dev_t dev)
{
	struct myuart_softc * const sc = myuart_lookup(dev);

	return sc != NULL ? sc->sc_tty : NULL;
}

static int
myuartioctl(dev_t dev, u_long cmd, void *data, int flag, struct lwp *l)
{
	struct myuart_softc * const sc = myuart_lookup(dev);
	struct tty *tp;
	int error;

	if (sc == NULL) {
		return ENXIO;
	}
	tp = sc->sc_tty;

	/* Line discipline first, then the generic tty ioctls. */
	error = (*tp->t_linesw->l_ioctl)(tp, cmd, data, flag, l);
	if (error != EPASSTHROUGH) {
		return error;
	}
	error = ttioctl(tp, cmd, data, flag, l);
	if (error != EPASSTHROUGH) {
		return error;
	}

	/*
	 * Device-specific ioctls go here, e.g. TIOCSBRK / TIOCCBRK to
	 * start and stop sending a break, or TIOCMGET / TIOCMSET for
	 * modem control lines.
	 */
	return EPASSTHROUGH;
}

Timer

Confirmed: a NULL argument makes the dispatcher pass the interrupt frame. Now a compile test:

The timer template is mytimer.c, built the same way as the UART one. I compiled it into an SBC3 kernel twice, once each way for the auto-reload setting, and both built cleanly. Then I took it back out of the tree.

What you change: the CHIP-SPECIFIC sections

  • Registers: a placeholder register map for a simple down-counter (control, status, 24-bit reload).
  • Limits and reload behaviour:
    • MYTIMER_COUNT_MAX is the largest reload value.
    • MYTIMER_PRESCALE is any fixed divider in front of the counter.
    • MYTIMER_AUTO_RELOAD says whether the chip restarts itself at zero. If it doesn’t, the driver restarts it by hand on every tick. That loses a little time per tick, so the clock runs slightly slow.
  • Three hardware functions:
    • hw_stop() stops the counter and clears pending interrupts.
    • hw_start(count) loads the count and starts periodic interrupts.
    • hw_ack() clears the interrupt. Getting this wrong makes the machine appear to hang, because the CPU re-enters the handler immediately.

How it fits into hb68k: a timer driver provides only mechanism. The clock code in hb68k/clock.c owns the policy and calls the driver through four functions in a small table (struct clock_if):

Function When it’s called What it does
clk_init once, late in boot establishes the interrupt at IPL_SCHED
clk_arm(interval_us) once, right after init starts periodic interrupts, e.g. every 10 000 µs for 100 Hz
clk_intr at the start of every tick acknowledges the interrupt, and reloads if needed
clk_disarm shutdown or error stops the timer

The file header explains:

  • How netbsd,hardclock / netbsd,statclock under /chosen pick each timer’s role, and what happens without them.
  • Why the timer belongs at IPL 6.
  • Which HZ values are allowed (they must divide 1 000 000).
  • Why the interrupt handler’s argument must be NULL. I checked this in the dispatcher: with NULL, the clock code receives the interrupt frame, which it needs.

Setup is lighter than the UART’s:

  • Add device/attach/file to files.hb68k.
  • Add mytimer* at fdt? pass 3 to the config.
  • Add a device-tree node with clocks or clock-frequency, plus an interrupts entry.

There’s no major number or /dev node, since nothing opens a timer.

Left out deliberately:

  • A high-resolution timecounter. That needs a free-running counter, not a counter that resets every tick, so it depends on what your chip offers.
  • Randomising the statclock interval. hb68k’s clock code doesn’t do this yet either.
/*	$NetBSD$	*/

/*-
 * Copyright (c) 2026 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * mytimer -- template driver for a periodic hardware timer used as a
 * NetBSD/hb68k system clock, attached via Flattened Device Tree (FDT).
 *
 * ======================================================================
 * HOW TO USE THIS TEMPLATE
 * ======================================================================
 *
 * 1. Copy this file to e.g. sys/arch/hb68k/dev/<chip>_timer.c and
 *    replace "mytimer" with your driver name throughout (it becomes
 *    the device name, e.g. "mytimer0").
 *
 * 2. Fill in the sections marked "CHIP-SPECIFIC" below.  Everything
 *    else is generic hb68k plumbing.
 *
 * 3. Hook it into the build:
 *
 *    sys/arch/hb68k/conf/files.hb68k:
 *	device	mytimer
 *	attach	mytimer at fdt
 *	file	arch/hb68k/dev/mytimer.c	mytimer
 *
 *    Kernel config (e.g. conf/SBC3).  Timers attach after the
 *    interrupt controller (pass 2), hence pass 3:
 *	mytimer*	at fdt? pass 3
 *
 *    A timer has no /dev entry, so it needs no major number.
 *
 * 4. Describe the chip in the device tree:
 *
 *	timer@XXXXXX {
 *		compatible = "vendor,mytimer";	(must match compat_data[])
 *		reg = <0xXXXXXX 0x10>;		(base, size of register block)
 *		reg-shift = <1>;		(log2 of register spacing)
 *		clocks = <&osc>;		(a fixed-clock node), OR
 *		clock-frequency = <1000000>;	(input clock in Hz)
 *		interrupts = <6 0>;		(IPL, vector; 0 = auto-vector)
 *	};
 *
 *    and say which job it does under /chosen, using an alias:
 *
 *	chosen {
 *		netbsd,hardclock = "timer0";	(the main system tick)
 *		netbsd,statclock = "timer1";	(optional, see below)
 *	};
 *	aliases {
 *		timer0 = "/obio/timer@XXXXXX";
 *	};
 *
 *    Without those properties, the first timer to attach becomes the
 *    hardclock and the second (if any) the statclock.
 *
 * ======================================================================
 * HOW hb68k USES A TIMER
 * ======================================================================
 *
 * The machine-independent clock code in sys/arch/hb68k/hb68k/clock.c
 * owns the policy; a timer driver only provides mechanism, through the
 * five-member `struct clock_if' (machine/clockvar.h):
 *
 *  clk_dev	The timer's device_t.
 *
 *  clk_init	Called once, from cpu_initclocks(), late in boot.  Must
 *		establish the hardware interrupt with the handler it is
 *		given (at IPL_SCHED) and return 0, or an error.
 *
 *  clk_arm	Start the timer interrupting periodically, every
 *		`interval_us' microseconds (10000 for the usual
 *		hz = 100).  Called once, right after clk_init.
 *
 *  clk_intr	Called by the clock code at the start of every timer
 *		interrupt.  Acknowledge the interrupt here and, if the
 *		chip doesn't reload by itself, restart the count.
 *
 *  clk_disarm	Stop the timer (used at shutdown / error paths).
 *
 * The driver registers itself with clock_attach() at the end of
 * attach.  The clock code then decides which role the timer plays:
 *
 *  hardclock	Drives the system tick: timekeeping, scheduling,
 *		callouts.  Every system needs one.
 *
 *  statclock	Optional second timer for CPU-usage statistics and
 *		profiling.  With only one timer, the hardclock does both
 *		jobs, and boot prints "No statclock; using hardclock."
 *
 * Interrupt level: the hardclock runs at IPL_SCHED, which on hb68k is
 * hardware level 6, so wire the timer to IPL 6 and use 6 as the first
 * cell of "interrupts".  A lower level works, but the clock interrupt
 * can then be delayed or blocked by other devices and by the soft
 * interrupt level (1), so the system time will drift.
 *
 * hz must divide 1000000 exactly (the clock code checks this): use
 * 50, 100, 200, 250, 400, 500 or 1000.  Set it in the kernel config
 * with "options HZ=..." if the default of 100 doesn't suit the
 * hardware.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD$");

#include <sys/param.h>
#include <sys/bus.h>
#include <sys/device.h>
#include <sys/intr.h>
#include <sys/systm.h>

#include <machine/clockvar.h>

#include <dev/fdt/fdtvar.h>

/*
 * ######################################################################
 * CHIP-SPECIFIC: registers
 * ######################################################################
 *
 * Replace these with your chip's register map.  Offsets are register
 * *numbers*; the real byte offset is (reg << reg-shift).
 *
 * The placeholder layout below is a typical simple down-counter:
 *	CTRL		enable, interrupt enable, auto-reload
 *	STATUS		"counter reached zero" flag, write 1 to clear
 *	RELOAD0..2	24-bit reload (preload) value, low byte first
 */
#define	MYTIMER_REG_CTRL	0
#define	MYTIMER_REG_STATUS	1
#define	MYTIMER_REG_RELOAD0	2	/* bits 7..0 */
#define	MYTIMER_REG_RELOAD1	3	/* bits 15..8 */
#define	MYTIMER_REG_RELOAD2	4	/* bits 23..16 */

/* CTRL bits */
#define	MYTIMER_CTRL_RUN	0x01	/* counter running */
#define	MYTIMER_CTRL_IRQEN	0x02	/* interrupt on reaching zero */
#define	MYTIMER_CTRL_RELOAD	0x04	/* reload from RELOAD on zero */

/* STATUS bits */
#define	MYTIMER_SR_ZERO		0x01	/* counter hit zero (write 1 = clear) */

/*
 * Counter limits.  MAX is the largest value the reload register can
 * hold; PRESCALE is any fixed divider between the input clock and the
 * counter (1 if none).  Together with the input clock these decide the
 * longest interval the timer can produce: MAX * PRESCALE / freq.
 */
#define	MYTIMER_COUNT_MAX	0x00ffffffU
#define	MYTIMER_PRESCALE	1

/*
 * Set to true if the counter reloads itself when it reaches zero
 * (most timers can).  If false, clk_intr() restarts it by hand; the
 * time spent getting to the interrupt handler is then lost on every
 * tick, and the clock runs slightly slow.
 */
#define	MYTIMER_AUTO_RELOAD	true

/*
 * ######################################################################
 * Device tree matching
 * ######################################################################
 */
static const struct device_compatible_entry compat_data[] = {
	{ .compat = "vendor,mytimer" },
	DEVICE_COMPAT_EOL
};

/*
 * ######################################################################
 * Driver state
 * ######################################################################
 */
struct mytimer_softc {
	device_t		sc_dev;
	bus_space_tag_t		sc_bst;
	bus_space_handle_t	sc_bsh;
	u_int			sc_shift;	/* reg-shift */
	uint32_t		sc_freq;	/* counter decrement rate, Hz */
	uint32_t		sc_reload;	/* count for one interval */
	void			*sc_ih;		/* interrupt handle */
	struct clock_if		sc_clkif;	/* our interface to clock.c */
};

static inline uint8_t
mytimer_read(struct mytimer_softc *sc, u_int reg)
{
	return bus_space_read_1(sc->sc_bst, sc->sc_bsh, reg << sc->sc_shift);
}

static inline void
mytimer_write(struct mytimer_softc *sc, u_int reg, uint8_t val)
{
	bus_space_write_1(sc->sc_bst, sc->sc_bsh, reg << sc->sc_shift, val);
}

/*
 * ######################################################################
 * CHIP-SPECIFIC: low-level operations
 * ######################################################################
 *
 * The only code that touches the hardware.
 */

/* Stop the counter and clear any pending interrupt. */
static void
mytimer_hw_stop(struct mytimer_softc *sc)
{
	mytimer_write(sc, MYTIMER_REG_CTRL, 0);
	mytimer_write(sc, MYTIMER_REG_STATUS, MYTIMER_SR_ZERO);
}

/*
 * Load `count' and start counting down, interrupting at zero.  With
 * MYTIMER_AUTO_RELOAD the chip then repeats on its own.
 */
static void
mytimer_hw_start(struct mytimer_softc *sc, uint32_t count)
{
	mytimer_write(sc, MYTIMER_REG_RELOAD0, count & 0xff);
	mytimer_write(sc, MYTIMER_REG_RELOAD1, (count >> 8) & 0xff);
	mytimer_write(sc, MYTIMER_REG_RELOAD2, (count >> 16) & 0xff);
	mytimer_write(sc, MYTIMER_REG_CTRL,
	    MYTIMER_CTRL_RUN | MYTIMER_CTRL_IRQEN |
	    (MYTIMER_AUTO_RELOAD ? MYTIMER_CTRL_RELOAD : 0));
}

/*
 * Acknowledge the interrupt so the chip drops its request line.  If
 * this is forgotten, the CPU takes the interrupt again the moment the
 * handler returns, and the machine appears to hang.
 */
static void
mytimer_hw_ack(struct mytimer_softc *sc)
{
	mytimer_write(sc, MYTIMER_REG_STATUS, MYTIMER_SR_ZERO);
}

/*
 * ######################################################################
 * Generic driver below this line
 * ######################################################################
 */

/* Convert an interval in microseconds to counter ticks. */
static uint32_t
mytimer_us_to_ticks(struct mytimer_softc *sc, unsigned int interval_us)
{
	const uint64_t ticks =
	    ((uint64_t)sc->sc_freq * interval_us + 500000) / 1000000;

	if (ticks == 0 || ticks > MYTIMER_COUNT_MAX) {
		/*
		 * The input clock is too slow (ticks == 0) or too fast
		 * (ticks too big) for this interval.  Change the clock,
		 * the prescaler, or HZ.
		 */
		panic("%s: can't make a %u us interval from %u Hz "
		    "(ticks=%llu)", device_xname(sc->sc_dev), interval_us,
		    sc->sc_freq, (unsigned long long)ticks);
	}

	return (uint32_t)ticks;
}

/* clock_if: establish our interrupt.  `intrfunc' is clock.c's handler. */
static int
mytimer_init(struct clock_if *clk, int (*intrfunc)(void *))
{
	struct mytimer_softc * const sc =
	    container_of(clk, struct mytimer_softc, sc_clkif);
	const int phandle = devhandle_to_of(device_handle(sc->sc_dev));
	char intrstr[128];

	if (!fdtbus_intr_str(phandle, 0, intrstr, sizeof(intrstr))) {
		aprint_error_dev(sc->sc_dev, "couldn't decode interrupt\n");
		return ENXIO;
	}

	/*
	 * The handler's argument must be NULL: clock.c's handler then
	 * receives the interrupt frame, which hardclock() needs to tell
	 * whether the interrupted code was in user or kernel mode.
	 */
	sc->sc_ih = fdtbus_intr_establish_xname(phandle, 0, IPL_SCHED,
	    FDT_INTR_MPSAFE, intrfunc, NULL, device_xname(sc->sc_dev));
	if (sc->sc_ih == NULL) {
		aprint_error_dev(sc->sc_dev,
		    "couldn't establish interrupt at %s\n", intrstr);
		return ENXIO;
	}
	aprint_normal_dev(sc->sc_dev, "interrupting at %s\n", intrstr);

	return 0;
}

/* clock_if: start periodic interrupts every `interval_us'. */
static int
mytimer_arm(struct clock_if *clk, unsigned int interval_us)
{
	struct mytimer_softc * const sc =
	    container_of(clk, struct mytimer_softc, sc_clkif);

	sc->sc_reload = mytimer_us_to_ticks(sc, interval_us);
	mytimer_hw_stop(sc);
	mytimer_hw_start(sc, sc->sc_reload);

	return 0;
}

/* clock_if: called at the start of every timer interrupt. */
static void
mytimer_intr(struct clock_if *clk)
{
	struct mytimer_softc * const sc =
	    container_of(clk, struct mytimer_softc, sc_clkif);

	mytimer_hw_ack(sc);
	if (!MYTIMER_AUTO_RELOAD) {
		mytimer_hw_start(sc, sc->sc_reload);
	}
}

/* clock_if: stop the timer. */
static void
mytimer_disarm(struct clock_if *clk)
{
	struct mytimer_softc * const sc =
	    container_of(clk, struct mytimer_softc, sc_clkif);

	mytimer_hw_stop(sc);
}

/*
 * ----------------------------------------------------------------------
 * Autoconfiguration
 * ----------------------------------------------------------------------
 */

static int
mytimer_match(device_t parent, cfdata_t cf, void *aux)
{
	struct fdt_attach_args * const faa = aux;

	return of_compatible_match(faa->faa_phandle, compat_data);
}

static void
mytimer_attach(device_t parent, device_t self, void *aux)
{
	struct mytimer_softc * const sc = device_private(self);
	struct fdt_attach_args * const faa = aux;
	const int phandle = faa->faa_phandle;
	bus_addr_t addr;
	bus_size_t size;
	struct clk *clk;
	uint32_t freq;

	sc->sc_dev = self;
	sc->sc_bst = faa->faa_bst;

	/* --- Registers ------------------------------------------------ */
	if (fdtbus_get_reg(phandle, 0, &addr, &size) != 0) {
		aprint_error(": couldn't get registers\n");
		return;
	}
	sc->sc_shift = fdtbus_get_reg_shift(phandle, 0);
	if (bus_space_map(sc->sc_bst, addr, size, 0, &sc->sc_bsh) != 0) {
		aprint_error(": couldn't map registers\n");
		return;
	}

	/*
	 * --- Input clock ----------------------------------------------
	 * Prefer a "clocks" reference to a clock node; fall back to a
	 * plain "clock-frequency" property.
	 */
	clk = fdtbus_clock_get_index(phandle, 0);
	if (clk != NULL) {
		freq = clk_get_rate(clk);
	} else if (of_getprop_uint32(phandle, "clock-frequency", &freq) != 0) {
		aprint_error(": no \"clocks\" or \"clock-frequency\"\n");
		return;
	}
	sc->sc_freq = freq / MYTIMER_PRESCALE;
	if (sc->sc_freq == 0) {
		aprint_error(": invalid clock rate %u Hz\n", freq);
		return;
	}

	/* Make sure the timer is quiet until the clock code arms it. */
	mytimer_hw_stop(sc);

	aprint_naive("\n");
	aprint_normal(": timer, %u Hz count rate\n", sc->sc_freq);

	/* --- Register with the clock code ------------------------------ */
	sc->sc_clkif.clk_dev = self;
	sc->sc_clkif.clk_init = mytimer_init;
	sc->sc_clkif.clk_arm = mytimer_arm;
	sc->sc_clkif.clk_intr = mytimer_intr;
	sc->sc_clkif.clk_disarm = mytimer_disarm;
	clock_attach(&sc->sc_clkif);
}

CFATTACH_DECL_NEW(mytimer, sizeof(struct mytimer_softc),
    mytimer_match, mytimer_attach, NULL, NULL);
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9