diff --git a/core/esp_timer.c b/core/esp_timer.c
index b00e086..bb23f51 100644
--- a/core/esp_timer.c
+++ b/core/esp_timer.c
@@ -14,12 +14,12 @@
  * the arguments aren't known at compile time (values are evaluated at
  * compile time otherwise.)
  */
-uint32_t _timer_freq_to_count_runtime(const timer_frc_t frc, const uint32_t freq, const timer_div_t div)
+uint32_t _timer_freq_to_count_runtime(const timer_frc_t frc, const uint32_t freq, const timer_clkdiv_t div)
 {
     return _timer_freq_to_count_impl(frc, freq, div);
 }
 
-uint32_t _timer_time_to_count_runtime(const timer_frc_t frc, uint32_t us, const timer_div_t div)
+uint32_t _timer_time_to_count_runtime(const timer_frc_t frc, uint32_t us, const timer_clkdiv_t div)
 {
     return _timer_time_to_count_runtime(frc, us, div);
 }
diff --git a/core/include/esp/dport_regs.h b/core/include/esp/dport_regs.h
new file mode 100644
index 0000000..f80a01e
--- /dev/null
+++ b/core/include/esp/dport_regs.h
@@ -0,0 +1,53 @@
+/* esp/dport_regs.h
+ *
+ * ESP8266 DPORT0 register definitions
+ *
+ * Not compatible with ESP SDK register access code.
+ */
+
+#ifndef _ESP_DPORT_REGS_H
+#define _ESP_DPORT_REGS_H
+
+#include "esp/types.h"
+#include "common_macros.h"
+
+#define DPORT_BASE 0x3ff00000
+#define DPORT (*(struct DPORT_REGS *)(DPORT_BASE))
+
+/* DPORT registers
+
+   Control various aspects of core/peripheral interaction... Not well
+   documented or understood.
+*/
+
+struct DPORT_REGS {
+    uint32_t volatile _unknown0;    // 0x00
+    uint32_t volatile INT_ENABLE;   // 0x04
+} __attribute__ (( packed ));
+
+_Static_assert(sizeof(struct DPORT_REGS) == 0x08, "DPORT_REGS is the wrong size");
+
+/* Details for INT_ENABLE register */
+
+/* Set flags to enable CPU interrupts from some peripherals. Read/write.
+
+   bit 0 - Is set by RTOS SDK startup code but function is unknown.
+   bit 1 - INT_ENABLE_TIMER0 allows TIMER 0 (FRC1) to trigger interrupt INUM_TIMER_FRC1.
+   bit 2 - INT_ENABLE_TIMER1 allows TIMER 1 (FRC2) to trigger interrupt INUM_TIMER_FRC2.
+
+   Espressif calls this register "EDGE_INT_ENABLE_REG". The "edge" in
+   question is (I think) the interrupt line from the peripheral, as
+   the interrupt status bit is set. There may be a similar register
+   for enabling "level" interrupts instead of edge triggering
+   - this is unknown.
+*/
+
+#define DPORT_INT_ENABLE_TIMER0  BIT(1)
+#define DPORT_INT_ENABLE_TIMER1  BIT(2)
+
+/* Aliases for the Espressif way of referring to TIMER0 (FRC1) and TIMER1
+ * (FRC2).. */
+#define DPORT_INT_ENABLE_FRC1  DPORT_INT_ENABLE_TIMER0
+#define DPORT_INT_ENABLE_FRC2  DPORT_INT_ENABLE_TIMER1
+
+#endif /* _ESP_DPORT_REGS_H */
diff --git a/core/include/esp/registers.h b/core/include/esp/registers.h
index 5b8a785..98c73c0 100644
--- a/core/include/esp/registers.h
+++ b/core/include/esp/registers.h
@@ -18,6 +18,8 @@
 
 #include "esp/iomux_regs.h"
 #include "esp/gpio_regs.h"
+#include "esp/timer_regs.h"
+#include "esp/dport_regs.h"
 
 /* Internal macro, only defined in header body */
 #define _REG(BASE, OFFSET) (*(esp_reg_t)((BASE)+(OFFSET)))
@@ -27,13 +29,13 @@
    You shouldn't need to use these directly.
  */
 #define MMIO_BASE   0x60000000
-#define DPORT_BASE  0x3ff00000
+//#define DPORT_BASE  0x3ff00000
 
 #define UART0_BASE (MMIO_BASE + 0)
 #define SPI1_BASE  (MMIO_BASE + 0x0100)
 #define SPI_BASE   (MMIO_BASE + 0x0200)
 //#define GPIO0_BASE (MMIO_BASE + 0x0300)
-#define TIMER_BASE (MMIO_BASE + 0x0600)
+//#define TIMER_BASE (MMIO_BASE + 0x0600)
 #define RTC_BASE   (MMIO_BASE + 0x0700)
 //#define IOMUX_BASE (MMIO_BASE + 0x0800)
 #define WDT_BASE   (MMIO_BASE + 0x0900)
@@ -43,135 +45,6 @@
 #define RTCS_BASE  (MMIO_BASE + 0x1100)
 #define RTCU_BASE  (MMIO_BASE + 0x1200)
 
-/* TIMER registers
- *
- * ESP8266 has two hardware(?) timer counters, FRC1 and FRC2.
- *
- * FRC1 is a 24-bit countdown timer, triggers interrupt when reaches zero.
- * FRC2 is a 32-bit countup timer, can set a variable match value to trigger an interrupt.
- *
- * FreeRTOS tick timer appears to come from XTensa core tick timer0,
- * not either of these.  FRC2 is used in the FreeRTOS SDK however. It
- * is set to free-run, interrupting periodically via updates to the
- * MATCH register. sdk_ets_timer_init configures FRC2 and assigns FRC2
- * interrupt handler at sdk_vApplicationTickHook+0x68
- */
-
-/* Load value for FRC1, read/write.
-
-   When TIMER_CTRL_RELOAD is cleared in TIMER_FRC1_CTRL_REG, FRC1 will
-   reload to TIMER_FRC1_MAX_LOAD once overflowed (unless the load
-   value is rewritten in the interrupt handler.)
-
-   When TIMER_CTRL_RELOAD is set in TIMER_FRC1_CTRL_REG, FRC1 will reload
-   from the load register value once overflowed.
-*/
-#define TIMER_FRC1_LOAD_REG   _REG(TIMER_BASE, 0x00)
-
-#define TIMER_FRC1_MAX_LOAD 0x7fffff
-
-/* Current count value for FRC1, read only? */
-#define TIMER_FRC1_COUNT_REG  _REG(TIMER_BASE, 0x04)
-
-/* Control register for FRC1, read/write.
-
-   See the bit definitions TIMER_CTRL_xxx lower down.
- */
-#define TIMER_FRC1_CTRL_REG  _REG(TIMER_BASE, 0x08)
-
-/* Reading this register always returns the value in
- * TIMER_FRC1_LOAD_REG.
- *
- * Writing zero to this register clears the FRC1
- * interrupt status.
- */
-#define TIMER_FRC1_CLEAR_INT_REG  _REG(TIMER_BASE, 0x0c)
-
-/* FRC2 load register.
- *
- * If TIMER_CTRL_RELOAD is cleared in TIMER_FRC2_CTRL_REG, writing to
- * this register will update the FRC2 COUNT value.
- *
- * If TIMER_CTRL_RELOAD is set in TIMER_FRC2_CTRL_REG, the behaviour
- * appears to be the same except that writing 0 to the load register
- * both sets the COUNT register to 0 and disables the timer, even if
- * the TIMER_CTRL_RUN bit is set.
- *
- * Offsets 0x34, 0x38, 0x3c all seem to read back the LOAD_REG value
- * also (but have no known function.)
- */
-#define TIMER_FRC2_LOAD_REG   _REG(TIMER_BASE, 0x20)
-
-/* FRC2 current count value. Read only? */
-#define TIMER_FRC2_COUNT_REG _REG(TIMER_BASE, 0x24)
-
-/* Control register for FRC2. Read/write.
-
-   See the bit definitions TIMER_CTRL_xxx lower down.
-*/
-#define TIMER_FRC2_CTRL_REG  _REG(TIMER_BASE, 0x28)
-
-/* Reading this value returns the current value of
- * TIMER_FRC2_LOAD_REG.
- *
- * Writing zero to this value clears the FRC2 interrupt status.
- */
-#define TIMER_FRC2_CLEAR_INT_REG  _REG(TIMER_BASE, 0x2c)
-
-/* Interrupt match value for FRC2. When COUNT == MATCH,
-   the interrupt fires.
-*/
-#define TIMER_FRC2_MATCH_REG _REG(TIMER_BASE, 0x30)
-
-/* Timer control bits to set clock divisor values.
-
-   Divider from master 80MHz APB_CLK (unconfirmed, see esp/clocks.h).
-*/
-#define TIMER_CTRL_DIV_1  0
-#define TIMER_CTRL_DIV_16 BIT(2)
-#define TIMER_CTRL_DIV_256 BIT(3)
-#define TIMER_CTRL_DIV_MASK (BIT(2)|BIT(3))
-
-/* Set timer control bits to trigger interrupt on "edge" or "level"
- *
- * Observed behaviour is like this:
- *
- *  * When TIMER_CTRL_INT_LEVEL is set, the interrupt status bit
- *    TIMER_CTRL_INT_STATUS remains set when the timer interrupt
- *    triggers, unless manually cleared by writing 0 to
- *    TIMER_FRCx_CLEAR_INT.  While the interrupt status bit stays set
- *    the timer will continue to run normally, but the interrupt
- *    (INUM_TIMER_FRC1 or INUM_TIMER_FRC2) won't trigger again.
- *
- *  * When TIMER_CTRL_INT_EDGE (default) is set, there's no need to
- *    manually write to TIMER_FRCx_CLEAR_INT. The interrupt status bit
- *    TIMER_CTRL_INT_STATUS automatically clears after the interrupt
- *    triggers, and the interrupt handler will run again
- *    automatically.
- *
- */
-#define TIMER_CTRL_INT_EDGE 0
-#define TIMER_CTRL_INT_LEVEL BIT(0)
-#define TIMER_CTRL_INT_MASK BIT(0)
-
-/* Timer auto-reload bit
-
-   This bit interacts with TIMER_FRC1_LOAD_REG & TIMER_FRC2_LOAD_REG
-   differently, see those registers for details.
-*/
-#define TIMER_CTRL_RELOAD BIT(6)
-
-/* Timer run bit */
-#define TIMER_CTRL_RUN BIT(7)
-
-/* Read-only timer interrupt status.
-
-   This bit gets set on FRC1 when interrupt fires, and cleared on a
-   write to TIMER_FRC1_CLEAR_INT (cleared automatically if
-   TIMER_CTRL_INT_LEVEL is not set).
-*/
-#define TIMER_CTRL_INT_STATUS BIT(8)
-
 /* WDT register(s)
 
    Not fully understood yet. Writing 0 here disables wdt.
@@ -180,29 +53,4 @@
  */
 #define WDT_CTRL       _REG(WDT_BASE, 0x00)
 
-/* DPORT registers
-
-   Control various aspects of core/peripheral interaction... Not well
-   documented or understood.
-*/
-
-/* Set flags to enable CPU interrupts from some peripherals. Read/write.
-
-   bit 0 - Is set by RTOS SDK startup code but function is unknown.
-   bit 1 - INT_ENABLE_FRC1 allows TIMER FRC1 to trigger interrupt INUM_TIMER_FRC1.
-   bit 2 - INT_ENABLE_FRC2 allows TIMER FRC2 to trigger interrupt INUM_TIMER_FRC2.
-
-   Espressif calls this register "EDGE_INT_ENABLE_REG". The "edge" in
-   question is (I think) the interrupt line from the peripheral, as
-   the interrupt status bit is set. There may be a similar register
-   for enabling "level" interrupts instead of edge triggering
-   - this is unknown.
-*/
-#define DP_INT_ENABLE_REG _REG(DPORT_BASE, 0x04)
-
-/* Set to enable interrupts from TIMER FRC1 */
-#define INT_ENABLE_FRC1 BIT(1)
-/* Set to enable interrupts interrupts from TIMER FRC2 */
-#define INT_ENABLE_FRC2 BIT(2)
-
 #endif
diff --git a/core/include/esp/timer.h b/core/include/esp/timer.h
index d1eea45..b83a0ee 100644
--- a/core/include/esp/timer.h
+++ b/core/include/esp/timer.h
@@ -11,12 +11,12 @@
 
 #include <stdbool.h>
 #include <xtensa_interrupts.h>
-#include "esp/registers.h"
+#include "esp/timer_regs.h"
 #include "esp/cpu.h"
 
 typedef enum {
-    TIMER_FRC1,
-    TIMER_FRC2,
+    FRC1 = 0,
+    FRC2 = 1,
 } timer_frc_t;
 
 /* Return current count value for timer. */
@@ -31,14 +31,8 @@ INLINED void timer_set_load(const timer_frc_t frc, const uint32_t load);
 /* Returns maximum load value for timer. */
 INLINED uint32_t timer_max_load(const timer_frc_t frc);
 
-typedef enum {
-    TIMER_DIV1,
-    TIMER_DIV16,
-    TIMER_DIV256,
-} timer_div_t;
-
 /* Set the timer divider value */
-INLINED void timer_set_divider(const timer_frc_t frc, const timer_div_t div);
+INLINED void timer_set_divider(const timer_frc_t frc, const timer_clkdiv_t div);
 
 /* Enable or disable timer interrupts
 
@@ -62,7 +56,7 @@ INLINED bool timer_get_reload(const timer_frc_t frc);
 /* Return a suitable timer divider for the specified frequency,
    or -1 if none is found.
  */
-INLINED timer_div_t timer_freq_to_div(uint32_t freq);
+INLINED timer_clkdiv_t timer_freq_to_div(uint32_t freq);
 
 /* Return the number of timer counts to achieve the specified
  * frequency with the specified divisor.
@@ -73,12 +67,12 @@ INLINED timer_div_t timer_freq_to_div(uint32_t freq);
  *
  * Compile-time evaluates if all arguments are available at compile time.
  */
-INLINED uint32_t timer_freq_to_count(const timer_frc_t frc, uint32_t freq, const timer_div_t div);
+INLINED uint32_t timer_freq_to_count(const timer_frc_t frc, uint32_t freq, const timer_clkdiv_t div);
 
 /* Return a suitable timer divider for the specified duration in
    microseconds or -1 if none is found.
  */
-INLINED timer_div_t timer_time_to_div(uint32_t us);
+INLINED timer_clkdiv_t timer_time_to_div(uint32_t us);
 
 /* Return the number of timer counts for the specified timer duration
  * in microseconds, when using the specified divisor.
@@ -89,7 +83,7 @@ INLINED timer_div_t timer_time_to_div(uint32_t us);
  *
  * Compile-time evaluates if all arguments are available at compile time.
  */
-INLINED uint32_t timer_time_to_count(const timer_frc_t frc, uint32_t us, const timer_div_t div);
+INLINED uint32_t timer_time_to_count(const timer_frc_t frc, uint32_t us, const timer_clkdiv_t div);
 
 /* Set a target timer interrupt frequency in Hz.
 
diff --git a/core/include/esp/timer_private.h b/core/include/esp/timer_private.h
index fad3283..530a396 100644
--- a/core/include/esp/timer_private.h
+++ b/core/include/esp/timer_private.h
@@ -10,6 +10,7 @@
 #include <limits.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include "esp/dport_regs.h"
 
 /* Timer divisor index to max frequency */
 #define _FREQ_DIV1  (80*1000*1000)
@@ -20,104 +21,90 @@ const static uint32_t IROM _TIMER_FREQS[] = { _FREQ_DIV1, _FREQ_DIV16, _FREQ_DIV
 /* Timer divisor index to divisor value */
 const static uint32_t IROM _TIMER_DIV_VAL[] = { 1, 16, 256 };
 
-/* Timer divisor to mask value */
-const static uint32_t IROM _TIMER_DIV_REG[] = { TIMER_CTRL_DIV_1, TIMER_CTRL_DIV_16, TIMER_CTRL_DIV_256 };
-
-INLINED esp_reg_t _timer_ctrl_reg(const timer_frc_t frc)
-{
-    return (frc == TIMER_FRC1) ? &TIMER_FRC1_CTRL_REG : &TIMER_FRC2_CTRL_REG;
-}
-
 INLINED uint32_t timer_get_count(const timer_frc_t frc)
 {
-    return (frc == TIMER_FRC1) ? TIMER_FRC1_COUNT_REG : TIMER_FRC2_COUNT_REG;
+    return TIMER(frc).COUNT;
 }
 
 INLINED uint32_t timer_get_load(const timer_frc_t frc)
 {
-    return (frc == TIMER_FRC1) ? TIMER_FRC1_LOAD_REG : TIMER_FRC2_LOAD_REG;
+    return TIMER(frc).LOAD;
 }
 
 INLINED void timer_set_load(const timer_frc_t frc, const uint32_t load)
 {
-    if(frc == TIMER_FRC1)
-        TIMER_FRC1_LOAD_REG = load;
-    else
-        TIMER_FRC2_LOAD_REG = load;
+    TIMER(frc).LOAD = load;
 }
 
 INLINED uint32_t timer_max_load(const timer_frc_t frc)
 {
-    return (frc == TIMER_FRC1) ? TIMER_FRC1_MAX_LOAD : UINT32_MAX;
+    return (frc == FRC1) ? TIMER_FRC1_MAX_LOAD : UINT32_MAX;
 }
 
-INLINED void timer_set_divider(const timer_frc_t frc, const timer_div_t div)
+INLINED void timer_set_divider(const timer_frc_t frc, const timer_clkdiv_t div)
 {
-    if(div < TIMER_DIV1 || div > TIMER_DIV256)
+    if(div < TIMER_CLKDIV_1 || div > TIMER_CLKDIV_256)
         return;
-    esp_reg_t ctrl = _timer_ctrl_reg(frc);
-    *ctrl = (*ctrl & ~TIMER_CTRL_DIV_MASK) | (_TIMER_DIV_REG[div] & TIMER_CTRL_DIV_MASK);
+    TIMER(frc).CTRL = SET_FIELD(TIMER(frc).CTRL, TIMER_CTRL_CLKDIV, div);
 }
 
 INLINED void timer_set_interrupts(const timer_frc_t frc, bool enable)
 {
-    const uint32_t dp_bit = (frc == TIMER_FRC1) ? INT_ENABLE_FRC1 : INT_ENABLE_FRC2;
-    const uint32_t int_mask = BIT((frc == TIMER_FRC1) ? INUM_TIMER_FRC1 : INUM_TIMER_FRC2);
+    const uint32_t dp_bit = (frc == FRC1) ? DPORT_INT_ENABLE_FRC1 : DPORT_INT_ENABLE_FRC2;
+    const uint32_t int_mask = BIT((frc == FRC1) ? INUM_TIMER_FRC1 : INUM_TIMER_FRC2);
     if(enable) {
-        DP_INT_ENABLE_REG |= dp_bit;
+        DPORT.INT_ENABLE |= dp_bit;
         _xt_isr_unmask(int_mask);
     } else {
-        DP_INT_ENABLE_REG &= ~dp_bit;
+        DPORT.INT_ENABLE &= ~dp_bit;
         _xt_isr_mask(int_mask);
     }
 }
 
 INLINED void timer_set_run(const timer_frc_t frc, const bool run)
 {
-    esp_reg_t ctrl = _timer_ctrl_reg(frc);
     if (run)
-        *ctrl |= TIMER_CTRL_RUN;
+        TIMER(frc).CTRL |= TIMER_CTRL_RUN;
     else
-        *ctrl &= ~TIMER_CTRL_RUN;
+        TIMER(frc).CTRL &= ~TIMER_CTRL_RUN;
 }
 
 INLINED bool timer_get_run(const timer_frc_t frc)
 {
-    return *_timer_ctrl_reg(frc) & TIMER_CTRL_RUN;
+    return TIMER(frc).CTRL & TIMER_CTRL_RUN;
 }
 
 INLINED void timer_set_reload(const timer_frc_t frc, const bool reload)
 {
-    esp_reg_t ctrl = _timer_ctrl_reg(frc);
     if (reload)
-        *ctrl |= TIMER_CTRL_RELOAD;
+        TIMER(frc).CTRL |= TIMER_CTRL_RELOAD;
     else
-        *ctrl &= ~TIMER_CTRL_RELOAD;
+        TIMER(frc).CTRL &= ~TIMER_CTRL_RELOAD;
 }
 
 INLINED bool timer_get_reload(const timer_frc_t frc)
 {
-    return *_timer_ctrl_reg(frc) & TIMER_CTRL_RELOAD;
+    return TIMER(frc).CTRL & TIMER_CTRL_RELOAD;
 }
 
-INLINED timer_div_t timer_freq_to_div(uint32_t freq)
+INLINED timer_clkdiv_t timer_freq_to_div(uint32_t freq)
 {
     /*
       try to maintain resolution without risking overflows.
       these values are a bit arbitrary at the moment! */
     if(freq > 100*1000)
-        return TIMER_DIV1;
+        return TIMER_CLKDIV_1;
     else if(freq > 100)
-        return TIMER_DIV16;
+        return TIMER_CLKDIV_16;
     else
-        return TIMER_DIV256;
+        return TIMER_CLKDIV_256;
 }
 
 /* timer_timer_to_count implementation - inline if all args are constant, call normally otherwise */
 
-INLINED uint32_t _timer_freq_to_count_impl(const timer_frc_t frc, const uint32_t freq, const timer_div_t div)
+INLINED uint32_t _timer_freq_to_count_impl(const timer_frc_t frc, const uint32_t freq, const timer_clkdiv_t div)
 {
-    if(div < TIMER_DIV1 || div > TIMER_DIV256)
+    if(div < TIMER_CLKDIV_1 || div > TIMER_CLKDIV_256)
         return 0; /* invalid divider */
 
     if(freq > _TIMER_FREQS[div])
@@ -127,9 +114,9 @@ INLINED uint32_t _timer_freq_to_count_impl(const timer_frc_t frc, const uint32_t
     return counts;
 }
 
-uint32_t _timer_freq_to_count_runtime(const timer_frc_t frc, const uint32_t freq, const timer_div_t div);
+uint32_t _timer_freq_to_count_runtime(const timer_frc_t frc, const uint32_t freq, const timer_clkdiv_t div);
 
-INLINED uint32_t timer_freq_to_count(const timer_frc_t frc, const uint32_t freq, const timer_div_t div)
+INLINED uint32_t timer_freq_to_count(const timer_frc_t frc, const uint32_t freq, const timer_clkdiv_t div)
 {
     if(__builtin_constant_p(frc) && __builtin_constant_p(freq) && __builtin_constant_p(div))
         return _timer_freq_to_count_impl(frc, freq, div);
@@ -137,33 +124,33 @@ INLINED uint32_t timer_freq_to_count(const timer_frc_t frc, const uint32_t freq,
         return _timer_freq_to_count_runtime(frc, freq, div);
 }
 
-INLINED timer_div_t timer_time_to_div(uint32_t us)
+INLINED timer_clkdiv_t timer_time_to_div(uint32_t us)
 {
     /*
       try to maintain resolution without risking overflows. Similar to
       timer_freq_to_div, these values are a bit arbitrary at the
       moment! */
     if(us < 1000)
-        return TIMER_DIV1;
+        return TIMER_CLKDIV_1;
     else if(us < 10*1000)
-        return TIMER_DIV16;
+        return TIMER_CLKDIV_16;
     else
-        return TIMER_DIV256;
+        return TIMER_CLKDIV_256;
 }
 
 /* timer_timer_to_count implementation - inline if all args are constant, call normally otherwise */
 
-INLINED uint32_t _timer_time_to_count_impl(const timer_frc_t frc, uint32_t us, const timer_div_t div)
+INLINED uint32_t _timer_time_to_count_impl(const timer_frc_t frc, uint32_t us, const timer_clkdiv_t div)
 {
-    if(div < TIMER_DIV1 || div > TIMER_DIV256)
+    if(div < TIMER_CLKDIV_1 || div > TIMER_CLKDIV_256)
         return 0; /* invalid divider */
 
     const uint32_t TIMER_MAX = timer_max_load(frc);
 
-    if(div != TIMER_DIV256) /* timer tick in MHz */
+    if(div != TIMER_CLKDIV_256) /* timer tick in MHz */
     {
         /* timer is either 80MHz or 5MHz, so either 80 or 5 MHz counts per us */
-        const uint32_t counts_per_us = ((div == TIMER_DIV1) ? _FREQ_DIV1 : _FREQ_DIV16)/1000/1000;
+        const uint32_t counts_per_us = ((div == TIMER_CLKDIV_1) ? _FREQ_DIV1 : _FREQ_DIV16)/1000/1000;
         if(us > TIMER_MAX/counts_per_us)
             return 0; /* Multiplying us by mhz_per_count will overflow TIMER_MAX */
         return us*counts_per_us;
@@ -186,9 +173,9 @@ INLINED uint32_t _timer_time_to_count_impl(const timer_frc_t frc, uint32_t us, c
     }
 }
 
-uint32_t _timer_time_to_count_runtime(const timer_frc_t frc, uint32_t us, const timer_div_t div);
+uint32_t _timer_time_to_count_runtime(const timer_frc_t frc, uint32_t us, const timer_clkdiv_t div);
 
-INLINED uint32_t timer_time_to_count(const timer_frc_t frc, uint32_t us, const timer_div_t div)
+INLINED uint32_t timer_time_to_count(const timer_frc_t frc, uint32_t us, const timer_clkdiv_t div)
 {
     if(__builtin_constant_p(frc) && __builtin_constant_p(us) && __builtin_constant_p(div))
         return _timer_time_to_count_impl(frc, us, div);
@@ -201,7 +188,7 @@ INLINED uint32_t timer_time_to_count(const timer_frc_t frc, uint32_t us, const t
 INLINED bool _timer_set_frequency_impl(const timer_frc_t frc, uint32_t freq)
 {
     uint32_t counts = 0;
-    timer_div_t div = timer_freq_to_div(freq);
+    timer_clkdiv_t div = timer_freq_to_div(freq);
 
     counts = timer_freq_to_count(frc, freq, div);
     if(counts == 0)
@@ -211,7 +198,7 @@ INLINED bool _timer_set_frequency_impl(const timer_frc_t frc, uint32_t freq)
     }
 
     timer_set_divider(frc, div);
-    if(frc == TIMER_FRC1)
+    if(frc == FRC1)
     {
         timer_set_load(frc, counts);
         timer_set_reload(frc, true);
@@ -219,7 +206,7 @@ INLINED bool _timer_set_frequency_impl(const timer_frc_t frc, uint32_t freq)
     else /* FRC2 */
     {
         /* assume that if this overflows it'll wrap, so we'll get desired behaviour */
-        TIMER_FRC2_MATCH_REG = counts + TIMER_FRC2_COUNT_REG;
+        TIMER(1).ALARM = counts + TIMER(1).COUNT;
     }
     return true;
 }
@@ -239,20 +226,20 @@ INLINED bool timer_set_frequency(const timer_frc_t frc, uint32_t freq)
 INLINED bool _timer_set_timeout_impl(const timer_frc_t frc, uint32_t us)
 {
     uint32_t counts = 0;
-    timer_div_t div = timer_time_to_div(us);
+    timer_clkdiv_t div = timer_time_to_div(us);
 
     counts = timer_time_to_count(frc, us, div);
     if(counts == 0)
         return false; /* can't set frequency */
 
     timer_set_divider(frc, div);
-    if(frc == TIMER_FRC1)
+    if(frc == FRC1)
     {
         timer_set_load(frc, counts);
     }
     else /* FRC2 */
     {
-        TIMER_FRC2_MATCH_REG = counts + TIMER_FRC2_COUNT_REG;
+        TIMER(1).ALARM = counts + TIMER(1).COUNT;
     }
 
     return true;
diff --git a/core/include/esp/timer_regs.h b/core/include/esp/timer_regs.h
new file mode 100644
index 0000000..f2aacfc
--- /dev/null
+++ b/core/include/esp/timer_regs.h
@@ -0,0 +1,125 @@
+/* esp/timer_regs.h
+ *
+ * ESP8266 Timer register definitions
+ *
+ * Not compatible with ESP SDK register access code.
+ */
+
+#ifndef _ESP_TIMER_REGS_H
+#define _ESP_TIMER_REGS_H
+
+#include "esp/types.h"
+#include "common_macros.h"
+
+#define TIMER_BASE 0x60000600
+#define TIMER(i) (*(struct TIMER_REGS *)(TIMER_BASE + (i)*0x20))
+#define TIMER_FRC1 TIMER(0)
+#define TIMER_FRC2 TIMER(1)
+
+/* TIMER registers
+ *
+ * ESP8266 has two hardware timer counters, FRC1 and FRC2.
+ *
+ * FRC1 is a 24-bit countdown timer, triggers interrupt when reaches zero.
+ * FRC2 is a 32-bit countup timer, can set a variable match value to trigger an interrupt.
+ *
+ * FreeRTOS tick timer appears to come from XTensa core tick timer0,
+ * not either of these.  FRC2 is used in the FreeRTOS SDK however. It
+ * is set to free-run, interrupting periodically via updates to the
+ * ALARM register. sdk_ets_timer_init configures FRC2 and assigns FRC2
+ * interrupt handler at sdk_vApplicationTickHook+0x68
+ */
+
+struct TIMER_REGS {            // FRC1  FRC2
+    uint32_t volatile LOAD;    // 0x00  0x20
+    uint32_t volatile COUNT;   // 0x04  0x24
+    uint32_t volatile CTRL;    // 0x08  0x28
+    uint32_t volatile STATUS;  // 0x0c  0x2c
+    uint32_t volatile ALARM;   //       0x30
+} __attribute__ (( packed ));
+
+_Static_assert(sizeof(struct TIMER_REGS) == 0x14, "TIMER_REGS is the wrong size");
+
+#define TIMER_FRC1_MAX_LOAD 0x7fffff
+
+/* Details for LOAD registers */
+
+/* Behavior for FRC1:
+ *
+ * When TIMER_CTRL_RELOAD is cleared in TIMER(0).CTRL, FRC1 will
+ * reload to its max value once underflowed (unless the load
+ * value is rewritten in the interrupt handler.)
+ *
+ * When TIMER_CTRL_RELOAD is set in TIMER(0).CTRL, FRC1 will reload
+ * from the load register value once underflowed.
+ *
+ * Behavior for FRC2:
+ *
+ * If TIMER_CTRL_RELOAD is cleared in TIMER(1).CTRL, writing to
+ * this register will update the FRC2 COUNT value.
+ *
+ * If TIMER_CTRL_RELOAD is set in TIMER(1).CTRL, the behaviour
+ * appears to be the same except that writing 0 to the load register
+ * both sets the COUNT register to 0 and disables the timer, even if
+ * the TIMER_CTRL_RUN bit is set.
+ *
+ * Offsets 0x34, 0x38, 0x3c all seem to read back the LOAD_REG value
+ * also (but have no known function.)
+ */
+
+/* Details for CTRL registers */
+
+/* Observed behaviour is like this:
+ *
+ *  * When TIMER_CTRL_INT_HOLD is set, the interrupt status bit
+ *    TIMER_CTRL_INT_STATUS remains set when the timer interrupt
+ *    triggers, unless manually cleared by writing 0 to
+ *    TIMER(x).STATUS.  While the interrupt status bit stays set
+ *    the timer will continue to run normally, but the interrupt
+ *    (INUM_TIMER_FRC1 or INUM_TIMER_FRC2) won't trigger again.
+ *
+ *  * When TIMER_CTRL_INT_HOLD is cleared (default), there's no need to
+ *    manually write to TIMER(x).STATUS. The interrupt status bit
+ *    TIMER_CTRL_INT_STATUS automatically clears after the interrupt
+ *    triggers, and the interrupt handler will run again
+ *    automatically.
+ */
+
+/* The values for TIMER_CTRL_CLKDIV control how many CPU clock cycles amount to
+ * one timer clock cycle.  For valid values, see the timer_clkdiv_t enum below.
+ */
+
+/* TIMER_CTRL_INT_STATUS gets set when interrupt fires, and cleared on a write
+ * to TIMER(x).STATUS (or cleared automatically if TIMER_CTRL_INT_HOLD is not
+ * set).
+ */
+
+#define TIMER_CTRL_INT_HOLD    BIT(0)
+#define TIMER_CTRL_CLKDIV_M    0x00000003
+#define TIMER_CTRL_CLKDIV_S    2
+#define TIMER_CTRL_RELOAD      BIT(6)
+#define TIMER_CTRL_RUN         BIT(7)
+#define TIMER_CTRL_INT_STATUS  BIT(8)
+
+typedef enum {
+    TIMER_CLKDIV_1 = 0,
+    TIMER_CLKDIV_16 = 1,
+    TIMER_CLKDIV_256 = 2,
+} timer_clkdiv_t;
+
+/* Details for STATUS registers */
+
+/* Reading this register always returns the value in
+ * TIMER(x).LOAD
+ *
+ * Writing zero to this register clears the FRC1
+ * interrupt status.
+ */
+
+/* Details for FRC2.ALARM register */
+
+/* Interrupt match value for FRC2. When COUNT == ALARM,
+   the interrupt fires.
+*/
+
+#endif /* _ESP_TIMER_REGS_H */