NetBSD Port
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.
I've still got to document how to build the root filesystem.
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/getctx_ready/putcclear_errorsset_intrintr_pendingprogram, which sets speed and frame format from termios flags. The example assumes a 16× divisor UART.
- The
compatiblestring 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:
- Add the device/attach/file lines to files.hb68k.
- Add a major number to majors.hb68k. 14 is free in your tree.
- Add a myuart* at fdt? pass 5 line to the kernel config.
- Add a device-tree node.
- 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);