Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 23 Aug 2018 18:46:05 +0000 (UTC)
From:      Emmanuel Vadot <manu@FreeBSD.org>
To:        src-committers@freebsd.org, svn-src-all@freebsd.org, svn-src-head@freebsd.org
Subject:   svn commit: r338272 - in head/sys: arm/allwinner arm64/conf conf
Message-ID:  <201808231846.w7NIk5Jm087329@repo.freebsd.org>

next in thread | raw e-mail | index | archive | help
Author: manu
Date: Thu Aug 23 18:46:05 2018
New Revision: 338272
URL: https://svnweb.freebsd.org/changeset/base/338272

Log:
  a10_timer: Update the driver so we can use it on other SoC
  
  a10_timer is currently use in UP allwinner SoC (A10 and A13).
  Those don't have the generic arm timer.
  The arm generic timecounter is broken in the A64 SoC, some attempts have
  been made to fix the glitch but users still reported some minor ones.
  Since the A64 (and all Allwinner SoC) still have this timer controller, rework
  the driver so we can use it in any SoC.
  Since it doesn't have the 64 bits counter on all SoC, use one of the
  generic 32 bits counter as the timecounter source.
  
  PR:	229644

Modified:
  head/sys/arm/allwinner/a10_timer.c
  head/sys/arm64/conf/GENERIC
  head/sys/conf/files.arm64

Modified: head/sys/arm/allwinner/a10_timer.c
==============================================================================
--- head/sys/arm/allwinner/a10_timer.c	Thu Aug 23 18:33:42 2018	(r338271)
+++ head/sys/arm/allwinner/a10_timer.c	Thu Aug 23 18:46:05 2018	(r338272)
@@ -38,7 +38,6 @@ __FBSDID("$FreeBSD$");
 #include <sys/timetc.h>
 #include <sys/watchdog.h>
 #include <machine/bus.h>
-#include <machine/cpu.h>
 #include <machine/intr.h>
 #include <machine/machdep.h>
 
@@ -46,65 +45,86 @@ __FBSDID("$FreeBSD$");
 #include <dev/ofw/ofw_bus.h>
 #include <dev/ofw/ofw_bus_subr.h>
 
-#include <machine/bus.h>
+#include <dev/extres/clk/clk.h>
 
-#include <sys/kdb.h>
-
+#if defined(__aarch64__)
+#include "opt_soc.h"
+#else
 #include <arm/allwinner/aw_machdep.h>
+#endif
 
 /**
  * Timer registers addr
  *
  */
-#define SW_TIMER_IRQ_EN_REG 	0x00
-#define SW_TIMER_IRQ_STA_REG 	0x04
-#define SW_TIMER0_CTRL_REG 	0x10
-#define SW_TIMER0_INT_VALUE_REG	0x14
-#define SW_TIMER0_CUR_VALUE_REG	0x18
+#define	TIMER_IRQ_EN_REG 	0x00
+#define	 TIMER_IRQ_ENABLE(x)	(1 << x)
 
-#define SW_COUNTER64LO_REG	0xa4
-#define SW_COUNTER64HI_REG	0xa8
-#define CNT64_CTRL_REG		0xa0
+#define	TIMER_IRQ_STA_REG 	0x04
+#define	 TIMER_IRQ_PENDING(x)	(1 << x)
 
-#define CNT64_RL_EN		0x02 /* read latch enable */
+/*
+ * On A10, A13, A20 and A31/A31s 6 timers are available
+ */
+#define	TIMER_CTRL_REG(x)		(0x10 + 0x10 * x)
+#define	 TIMER_CTRL_START		(1 << 0)
+#define	 TIMER_CTRL_AUTORELOAD		(1 << 1)
+#define	 TIMER_CTRL_CLKSRC_MASK		(3 << 2)
+#define	 TIMER_CTRL_OSC24M		(1 << 2)
+#define	 TIMER_CTRL_PRESCALAR_MASK	(0x7 << 4)
+#define	 TIMER_CTRL_PRESCALAR(x)	((x - 1) << 4)
+#define	 TIMER_CTRL_MODE_MASK		(1 << 7)
+#define	 TIMER_CTRL_MODE_SINGLE		(1 << 7)
+#define	 TIMER_CTRL_MODE_CONTINUOUS	(0 << 7)
+#define	TIMER_INTV_REG(x)		(0x14 + 0x10 * x)
+#define	TIMER_CURV_REG(x)		(0x18 + 0x10 * x)
 
-#define TIMER_ENABLE		(1<<0)
-#define TIMER_AUTORELOAD	(1<<1)
-#define TIMER_OSC24M		(1<<2) /* oscillator = 24mhz */
-#define TIMER_PRESCALAR		(0<<4) /* prescalar = 1 */
+/* 64 bit counter, available in A10 and A13 */
+#define	CNT64_CTRL_REG		0xa0
+#define	 CNT64_CTRL_RL_EN	0x02 /* read latch enable */
+#define	CNT64_LO_REG	0xa4
+#define	CNT64_HI_REG	0xa8
 
-#define SYS_TIMER_CLKSRC	24000000 /* clock source */
+#define	SYS_TIMER_CLKSRC	24000000 /* clock source */
 
+enum a10_timer_type {
+	A10_TIMER = 1,
+	A23_TIMER,
+};
+
 struct a10_timer_softc {
 	device_t 	sc_dev;
 	struct resource *res[2];
-	bus_space_tag_t sc_bst;
-	bus_space_handle_t sc_bsh;
 	void 		*sc_ih;		/* interrupt handler */
 	uint32_t 	sc_period;
-	uint32_t 	timer0_freq;
-	struct eventtimer et;
+	uint64_t 	timer0_freq;
+	struct eventtimer	et;
+	enum a10_timer_type	type;
 };
 
-int a10_timer_get_timerfreq(struct a10_timer_softc *);
-
 #define timer_read_4(sc, reg)	\
-	bus_space_read_4(sc->sc_bst, sc->sc_bsh, reg)
+	bus_read_4(sc->res[A10_TIMER_MEMRES], reg)
 #define timer_write_4(sc, reg, val)	\
-	bus_space_write_4(sc->sc_bst, sc->sc_bsh, reg, val)
+	bus_write_4(sc->res[A10_TIMER_MEMRES], reg, val)
 
 static u_int	a10_timer_get_timecount(struct timecounter *);
 static int	a10_timer_timer_start(struct eventtimer *,
     sbintime_t first, sbintime_t period);
 static int	a10_timer_timer_stop(struct eventtimer *);
 
-static uint64_t timer_read_counter64(void);
+static uint64_t timer_read_counter64(struct a10_timer_softc *sc);
+static void a10_timer_eventtimer_setup(struct a10_timer_softc *sc);
 
-static int a10_timer_hardclock(void *);
+static void a23_timer_timecounter_setup(struct a10_timer_softc *sc);
+static u_int a23_timer_get_timecount(struct timecounter *tc);
+
+static int a10_timer_irq(void *);
 static int a10_timer_probe(device_t);
 static int a10_timer_attach(device_t);
 
+#if defined(__arm__)
 static delay_func a10_timer_delay;
+#endif
 
 static struct timecounter a10_timer_timecounter = {
 	.tc_name           = "a10_timer timer0",
@@ -114,47 +134,52 @@ static struct timecounter a10_timer_timecounter = {
 	.tc_quality        = 1000,
 };
 
-struct a10_timer_softc *a10_timer_sc = NULL;
+static struct timecounter a23_timer_timecounter = {
+	.tc_name           = "a10_timer timer0",
+	.tc_get_timecount  = a23_timer_get_timecount,
+	.tc_counter_mask   = ~0u,
+	.tc_frequency      = 0,
+	/* We want it to be selected over the arm generic timecounter */
+	.tc_quality        = 2000,
+};
 
+#define	A10_TIMER_MEMRES		0
+#define	A10_TIMER_IRQRES		1
+
 static struct resource_spec a10_timer_spec[] = {
 	{ SYS_RES_MEMORY,	0,	RF_ACTIVE },
 	{ SYS_RES_IRQ,		0,	RF_ACTIVE },
 	{ -1, 0 }
 };
 
-static uint64_t
-timer_read_counter64(void)
-{
-	uint32_t lo, hi;
+static struct ofw_compat_data compat_data[] = {
+	{"allwinner,sun4i-a10-timer", A10_TIMER},
+	{"allwinner,sun8i-a23-timer", A23_TIMER},
+	{NULL, 0},
+};
 
-	/* Latch counter, wait for it to be ready to read. */
-	timer_write_4(a10_timer_sc, CNT64_CTRL_REG, CNT64_RL_EN);
-	while (timer_read_4(a10_timer_sc, CNT64_CTRL_REG) & CNT64_RL_EN)
-		continue;
-
-	hi = timer_read_4(a10_timer_sc, SW_COUNTER64HI_REG);
-	lo = timer_read_4(a10_timer_sc, SW_COUNTER64LO_REG);
-
-	return (((uint64_t)hi << 32) | lo);
-}
-
 static int
 a10_timer_probe(device_t dev)
 {
 	struct a10_timer_softc *sc;
+#if defined(__arm__)
 	u_int soc_family;
+#endif
 
 	sc = device_get_softc(dev);
 
-	if (!ofw_bus_is_compatible(dev, "allwinner,sun4i-a10-timer"))
+	if (ofw_bus_search_compatible(dev, compat_data)->ocd_data == 0)
 		return (ENXIO);
 
+#if defined(__arm__)
+	/* For SoC >= A10 we have the ARM Timecounter/Eventtimer */
 	soc_family = allwinner_soc_family();
 	if (soc_family != ALLWINNERSOC_SUN4I &&
 	    soc_family != ALLWINNERSOC_SUN5I)
 		return (ENXIO);
+#endif
 
-	device_set_desc(dev, "Allwinner A10/A20 timer");
+	device_set_desc(dev, "Allwinner timer");
 	return (BUS_PROBE_DEFAULT);
 }
 
@@ -162,10 +187,11 @@ static int
 a10_timer_attach(device_t dev)
 {
 	struct a10_timer_softc *sc;
+	clk_t clk;
 	int err;
-	uint32_t val;
 
 	sc = device_get_softc(dev);
+	sc->type = ofw_bus_search_compatible(dev, compat_data)->ocd_data;
 
 	if (bus_alloc_resources(dev, a10_timer_spec, sc->res)) {
 		device_printf(dev, "could not allocate resources\n");
@@ -173,12 +199,10 @@ a10_timer_attach(device_t dev)
 	}
 
 	sc->sc_dev = dev;
-	sc->sc_bst = rman_get_bustag(sc->res[0]);
-	sc->sc_bsh = rman_get_bushandle(sc->res[0]);
 
 	/* Setup and enable the timer interrupt */
-	err = bus_setup_intr(dev, sc->res[1], INTR_TYPE_CLK, a10_timer_hardclock,
-	    NULL, sc, &sc->sc_ih);
+	err = bus_setup_intr(dev, sc->res[A10_TIMER_IRQRES], INTR_TYPE_CLK,
+	    a10_timer_irq, NULL, sc, &sc->sc_ih);
 	if (err != 0) {
 		bus_release_resources(dev, a10_timer_spec, sc->res);
 		device_printf(dev, "Unable to setup the clock irq handler, "
@@ -186,18 +210,91 @@ a10_timer_attach(device_t dev)
 		return (ENXIO);
 	}
 
-	/* Set clock source to OSC24M, 16 pre-division */
-	val = timer_read_4(sc, SW_TIMER0_CTRL_REG);
-	val |= TIMER_PRESCALAR | TIMER_OSC24M;
-	timer_write_4(sc, SW_TIMER0_CTRL_REG, val);
+	if (clk_get_by_ofw_index(dev, 0, 0, &clk) != 0)
+		sc->timer0_freq = SYS_TIMER_CLKSRC;
+	else {
+		if (clk_get_freq(clk, &sc->timer0_freq) != 0) {
+			device_printf(dev, "Cannot get clock source frequency\n");
+			return (ENXIO);
+		}
+	}
 
-	/* Enable timer0 */
-	val = timer_read_4(sc, SW_TIMER_IRQ_EN_REG);
-	val |= TIMER_ENABLE;
-	timer_write_4(sc, SW_TIMER_IRQ_EN_REG, val);
+#if defined(__arm__)
+	a10_timer_eventtimer_setup(sc);
+	arm_set_delay(a10_timer_delay, sc);
+	a10_timer_timecounter.tc_priv = sc;
+	a10_timer_timecounter.tc_frequency = sc->timer0_freq;
+	tc_init(&a10_timer_timecounter);
+#elif defined(__aarch64__)
+	a23_timer_timecounter_setup(sc);
+#endif
 
-	sc->timer0_freq = SYS_TIMER_CLKSRC;
+	if (bootverbose) {
+		device_printf(sc->sc_dev, "clock: hz=%d stathz = %d\n", hz, stathz);
 
+		device_printf(sc->sc_dev, "event timer clock frequency %ju\n", 
+		    sc->timer0_freq);
+		device_printf(sc->sc_dev, "timecounter clock frequency %jd\n", 
+		    a10_timer_timecounter.tc_frequency);
+	}
+
+	return (0);
+}
+
+static int
+a10_timer_irq(void *arg)
+{
+	struct a10_timer_softc *sc;
+	uint32_t val;
+
+	sc = (struct a10_timer_softc *)arg;
+
+	/* Clear interrupt pending bit. */
+	timer_write_4(sc, TIMER_IRQ_STA_REG, TIMER_IRQ_PENDING(0));
+
+	val = timer_read_4(sc, TIMER_CTRL_REG(0));
+
+	/*
+	 * Disabled autoreload and sc_period > 0 means 
+	 * timer_start was called with non NULL first value.
+	 * Now we will set periodic timer with the given period 
+	 * value.
+	 */
+	if ((val & (1<<1)) == 0 && sc->sc_period > 0) {
+		/* Update timer */
+		timer_write_4(sc, TIMER_CURV_REG(0), sc->sc_period);
+
+		/* Make periodic and enable */
+		val |= TIMER_CTRL_AUTORELOAD | TIMER_CTRL_START;
+		timer_write_4(sc, TIMER_CTRL_REG(0), val);
+	}
+
+	if (sc->et.et_active)
+		sc->et.et_event_cb(&sc->et, sc->et.et_arg);
+
+	return (FILTER_HANDLED);
+}
+
+/*
+ * Event timer function for A10 and A13
+ */
+
+static void
+a10_timer_eventtimer_setup(struct a10_timer_softc *sc)
+{
+	uint32_t val;
+
+	/* Set clock source to OSC24M, 1 pre-division, continuous mode */
+	val = timer_read_4(sc, TIMER_CTRL_REG(0));
+	val &= ~TIMER_CTRL_PRESCALAR_MASK | ~TIMER_CTRL_MODE_MASK | ~TIMER_CTRL_CLKSRC_MASK;
+	val |= TIMER_CTRL_PRESCALAR(1) | TIMER_CTRL_OSC24M;
+	timer_write_4(sc, TIMER_CTRL_REG(0), val);
+
+	/* Enable timer0 */
+	val = timer_read_4(sc, TIMER_IRQ_EN_REG);
+	val |= TIMER_IRQ_ENABLE(0);
+	timer_write_4(sc, TIMER_IRQ_EN_REG, val);
+
 	/* Set desired frequency in event timer and timecounter */
 	sc->et.et_frequency = sc->timer0_freq;
 	sc->et.et_name = "a10_timer Eventtimer";
@@ -209,25 +306,6 @@ a10_timer_attach(device_t dev)
 	sc->et.et_stop = a10_timer_timer_stop;
 	sc->et.et_priv = sc;
 	et_register(&sc->et);
-
-	if (device_get_unit(dev) == 0) {
-		arm_set_delay(a10_timer_delay, sc);
-		a10_timer_sc = sc;
-	}
-
-	a10_timer_timecounter.tc_frequency = sc->timer0_freq;
-	tc_init(&a10_timer_timecounter);
-
-	if (bootverbose) {
-		device_printf(sc->sc_dev, "clock: hz=%d stathz = %d\n", hz, stathz);
-
-		device_printf(sc->sc_dev, "event timer clock frequency %u\n", 
-		    sc->timer0_freq);
-		device_printf(sc->sc_dev, "timecounter clock frequency %lld\n", 
-		    a10_timer_timecounter.tc_frequency);
-	}
-
-	return (0);
 }
 
 static int
@@ -250,20 +328,20 @@ a10_timer_timer_start(struct eventtimer *et, sbintime_
 		count = sc->sc_period;
 
 	/* Update timer values */
-	timer_write_4(sc, SW_TIMER0_INT_VALUE_REG, sc->sc_period);
-	timer_write_4(sc, SW_TIMER0_CUR_VALUE_REG, count);
+	timer_write_4(sc, TIMER_INTV_REG(0), sc->sc_period);
+	timer_write_4(sc, TIMER_CURV_REG(0), count);
 
-	val = timer_read_4(sc, SW_TIMER0_CTRL_REG);
+	val = timer_read_4(sc, TIMER_CTRL_REG(0));
 	if (period != 0) {
 		/* periodic */
-		val |= TIMER_AUTORELOAD;
+		val |= TIMER_CTRL_AUTORELOAD;
 	} else {
 		/* oneshot */
-		val &= ~TIMER_AUTORELOAD;
+		val &= ~TIMER_CTRL_AUTORELOAD;
 	}
 	/* Enable timer0 */
-	val |= TIMER_ENABLE;
-	timer_write_4(sc, SW_TIMER0_CTRL_REG, val);
+	val |= TIMER_IRQ_ENABLE(0);
+	timer_write_4(sc, TIMER_CTRL_REG(0), val);
 
 	return (0);
 }
@@ -277,62 +355,104 @@ a10_timer_timer_stop(struct eventtimer *et)
 	sc = (struct a10_timer_softc *)et->et_priv;
 
 	/* Disable timer0 */
-	val = timer_read_4(sc, SW_TIMER0_CTRL_REG);
-	val &= ~TIMER_ENABLE;
-	timer_write_4(sc, SW_TIMER0_CTRL_REG, val);
+	val = timer_read_4(sc, TIMER_CTRL_REG(0));
+	val &= ~TIMER_CTRL_START;
+	timer_write_4(sc, TIMER_CTRL_REG(0), val);
 
 	sc->sc_period = 0;
 
 	return (0);
 }
 
-int
-a10_timer_get_timerfreq(struct a10_timer_softc *sc)
+/*
+ * Timecounter functions for A23 and above
+ */
+
+static void
+a23_timer_timecounter_setup(struct a10_timer_softc *sc)
 {
-	return (sc->timer0_freq);
+	uint32_t val;
+
+	/* Set clock source to OSC24M, 1 pre-division, continuous mode */
+	val = timer_read_4(sc, TIMER_CTRL_REG(0));
+	val &= ~TIMER_CTRL_PRESCALAR_MASK | ~TIMER_CTRL_MODE_MASK | ~TIMER_CTRL_CLKSRC_MASK;
+	val |= TIMER_CTRL_PRESCALAR(1) | TIMER_CTRL_OSC24M;
+	timer_write_4(sc, TIMER_CTRL_REG(0), val);
+
+	/* Set reload value */
+	timer_write_4(sc, TIMER_INTV_REG(0), ~0);
+	val = timer_read_4(sc, TIMER_INTV_REG(0));
+
+	/* Enable timer0 */
+	val = timer_read_4(sc, TIMER_CTRL_REG(0));
+	val |= TIMER_CTRL_AUTORELOAD | TIMER_CTRL_START;
+	timer_write_4(sc, TIMER_CTRL_REG(0), val);
+
+	val = timer_read_4(sc, TIMER_CURV_REG(0));
+
+	a23_timer_timecounter.tc_priv = sc;
+	a23_timer_timecounter.tc_frequency = sc->timer0_freq;
+	tc_init(&a23_timer_timecounter);
 }
 
-static int
-a10_timer_hardclock(void *arg)
+static u_int
+a23_timer_get_timecount(struct timecounter *tc)
 {
 	struct a10_timer_softc *sc;
 	uint32_t val;
 
-	sc = (struct a10_timer_softc *)arg;
+	sc = (struct a10_timer_softc *)tc->tc_priv;
+	if (sc == NULL)
+		return (0);
 
-	/* Clear interrupt pending bit. */
-	timer_write_4(sc, SW_TIMER_IRQ_STA_REG, 0x1);
+	val = timer_read_4(sc, TIMER_CURV_REG(0));
+	/* Counter count backwards */
+	return (~0u - val);
+}
 
-	val = timer_read_4(sc, SW_TIMER0_CTRL_REG);
-	/*
-	 * Disabled autoreload and sc_period > 0 means 
-	 * timer_start was called with non NULL first value.
-	 * Now we will set periodic timer with the given period 
-	 * value.
-	 */
-	if ((val & (1<<1)) == 0 && sc->sc_period > 0) {
-		/* Update timer */
-		timer_write_4(sc, SW_TIMER0_CUR_VALUE_REG, sc->sc_period);
+/*
+ * Timecounter functions for A10 and A13, using the 64 bits counter
+ */
 
-		/* Make periodic and enable */
-		val |= TIMER_AUTORELOAD | TIMER_ENABLE;
-		timer_write_4(sc, SW_TIMER0_CTRL_REG, val);
-	}
+static uint64_t
+timer_read_counter64(struct a10_timer_softc *sc)
+{
+	uint32_t lo, hi;
 
-	if (sc->et.et_active)
-		sc->et.et_event_cb(&sc->et, sc->et.et_arg);
+	/* Latch counter, wait for it to be ready to read. */
+	timer_write_4(sc, CNT64_CTRL_REG, CNT64_CTRL_RL_EN);
+	while (timer_read_4(sc, CNT64_CTRL_REG) & CNT64_CTRL_RL_EN)
+		continue;
 
-	return (FILTER_HANDLED);
+	hi = timer_read_4(sc, CNT64_HI_REG);
+	lo = timer_read_4(sc, CNT64_LO_REG);
+
+	return (((uint64_t)hi << 32) | lo);
 }
 
-u_int
+#if defined(__arm__)
+static void
+a10_timer_delay(int usec, void *arg)
+{
+	struct a10_timer_softc *sc = arg;
+	uint64_t end, now;
+
+	now = timer_read_counter64(sc);
+	end = now + (sc->timer0_freq / 1000000) * (usec + 1);
+
+	while (now < end)
+		now = timer_read_counter64(sc);
+}
+#endif
+
+static u_int
 a10_timer_get_timecount(struct timecounter *tc)
 {
 
-	if (a10_timer_sc == NULL)
+	if (tc->tc_priv == NULL)
 		return (0);
 
-	return ((u_int)timer_read_counter64());
+	return ((u_int)timer_read_counter64(tc->tc_priv));
 }
 
 static device_method_t a10_timer_methods[] = {
@@ -352,16 +472,3 @@ static devclass_t a10_timer_devclass;
 
 EARLY_DRIVER_MODULE(a10_timer, simplebus, a10_timer_driver, a10_timer_devclass, 0, 0,
     BUS_PASS_TIMER + BUS_PASS_ORDER_MIDDLE);
-
-static void
-a10_timer_delay(int usec, void *arg)
-{
-	struct a10_timer_softc *sc = arg;
-	uint64_t end, now;
-
-	now = timer_read_counter64();
-	end = now + (sc->timer0_freq / 1000000) * (usec + 1);
-
-	while (now < end)
-		now = timer_read_counter64();
-}

Modified: head/sys/arm64/conf/GENERIC
==============================================================================
--- head/sys/arm64/conf/GENERIC	Thu Aug 23 18:33:42 2018	(r338271)
+++ head/sys/arm64/conf/GENERIC	Thu Aug 23 18:46:05 2018	(r338272)
@@ -108,6 +108,9 @@ options 	SOC_BRCM_BCM2837
 options 	SOC_ROCKCHIP_RK3328
 options 	SOC_XILINX_ZYNQ
 
+# Timer drivers
+device		a10_timer
+
 # Annapurna Alpine drivers
 device		al_ccu			# Alpine Cache Coherency Unit
 device		al_nb_service		# Alpine North Bridge Service

Modified: head/sys/conf/files.arm64
==============================================================================
--- head/sys/conf/files.arm64	Thu Aug 23 18:33:42 2018	(r338271)
+++ head/sys/conf/files.arm64	Thu Aug 23 18:46:05 2018	(r338272)
@@ -26,6 +26,7 @@ cloudabi64_vdso_blob.o		optional	compat_cloudabi64	\
 
 # Allwinner common files
 arm/allwinner/a10_ehci.c	optional	ehci aw_ehci fdt
+arm/allwinner/a10_timer.c	optional	a10_timer fdt
 arm/allwinner/aw_gpio.c		optional	gpio aw_gpio fdt
 arm/allwinner/aw_mmc.c		optional	mmc aw_mmc fdt | mmccam aw_mmc fdt
 arm/allwinner/aw_nmi.c		optional	aw_nmi fdt \



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201808231846.w7NIk5Jm087329>