diff --git a/core/esp_gpio_interrupts.c b/core/esp_gpio_interrupts.c
index 99c7640..ef0a47d 100644
--- a/core/esp_gpio_interrupts.c
+++ b/core/esp_gpio_interrupts.c
@@ -25,7 +25,7 @@
    OR
 
    - Implement a single function named gpio_interrupt_handler(). This
-     will need to manually check GPIO_STATUS_REG and clear any status
+     will need to manually check GPIO.STATUS and clear any status
      bits after handling interrupts. This gives you full control, but
      you can't combine it with the first approach.
 
@@ -67,14 +67,14 @@ const gpio_interrupt_handler_t gpio_interrupt_handlers[16] = {
 
 void __attribute__((weak)) IRAM gpio_interrupt_handler(void)
 {
-    uint32_t status_reg = GPIO_STATUS_REG;
-    GPIO_STATUS_CLEAR = status_reg;
+    uint32_t status_reg = GPIO.STATUS;
+    GPIO.STATUS_CLEAR = status_reg;
     uint8_t gpio_idx;
     while((gpio_idx = __builtin_ffs(status_reg)))
     {
         gpio_idx--;
         status_reg &= ~BIT(gpio_idx);
-        if(GPIO_CTRL_REG(gpio_idx) & GPIO_INT_MASK)
+        if(FIELD2VAL(GPIO_CONF_INTTYPE, GPIO.CONF[gpio_idx]))
             gpio_interrupt_handlers[gpio_idx]();
     }
 }
diff --git a/core/include/common_macros.h b/core/include/common_macros.h
index 1ed2e22..669203a 100644
--- a/core/include/common_macros.h
+++ b/core/include/common_macros.h
@@ -23,7 +23,8 @@
 #define VAL2FIELD(fieldname, value) (((value) & fieldname##_M) << fieldname##_S)
 #define FIELD2VAL(fieldname, regbits) (((regbits) >> fieldname##_S) & fieldname##_M)
 
-#define SETFIELD(regbits, fieldname, value) (((regbits) & ~(fieldname##_M << fieldname##_S)) | VAL2FIELD(fieldname, value))
+#define FIELD_MASK(fieldname) (fieldname##_M << fieldname##_S)
+#define SET_FIELD(regbits, fieldname, value) (((regbits) & ~FIELD_MASK(fieldname)) | VAL2FIELD(fieldname, value))
 
 /* Use this macro to store constant values in IROM flash instead
    of having them loaded into rodata (which resides in DRAM)
diff --git a/core/include/esp/gpio.h b/core/include/esp/gpio.h
index c4bca56..ab4b241 100644
--- a/core/include/esp/gpio.h
+++ b/core/include/esp/gpio.h
@@ -9,7 +9,7 @@
 #ifndef _ESP_GPIO_H
 #define _ESP_GPIO_H
 #include <stdbool.h>
-#include "esp/registers.h"
+#include "esp/gpio_regs.h"
 #include "esp/iomux.h"
 #include "esp/cpu.h"
 #include "xtensa_interrupts.h"
@@ -32,26 +32,27 @@ INLINED void gpio_enable(const uint8_t gpio_num, const gpio_direction_t directio
     switch(direction) {
     case GPIO_INPUT:
         iomux_flags = 0;
-        ctrl_val = GPIO_SOURCE_GPIO;
+        ctrl_val = 0;
         break;
     case GPIO_OUTPUT:
         iomux_flags = IOMUX_PIN_OUTPUT_ENABLE;
-        ctrl_val = GPIO_DRIVE_PUSH_PULL|GPIO_SOURCE_GPIO;
+        ctrl_val = GPIO_CONF_DRIVER_ENABLE;
         break;
     case GPIO_OUT_OPEN_DRAIN:
         iomux_flags = IOMUX_PIN_OUTPUT_ENABLE;
-        ctrl_val = GPIO_DRIVE_OPEN_DRAIN|GPIO_SOURCE_GPIO;
+        ctrl_val = 0;
         break;
     case GPIO_INPUT_PULLUP:
         iomux_flags = IOMUX_PIN_PULLUP;
-        ctrl_val = GPIO_SOURCE_GPIO;
+        ctrl_val = 0;
+        break;
     }
     iomux_set_gpio_function(gpio_num, iomux_flags);
-    GPIO_CTRL_REG(gpio_num) = (GPIO_CTRL_REG(gpio_num)&GPIO_INT_MASK) | ctrl_val;
-    if(direction == GPIO_OUTPUT)
-        GPIO_DIR_SET = BIT(gpio_num);
+    GPIO.CONF[gpio_num] = (GPIO.CONF[gpio_num] & FIELD_MASK(GPIO_CONF_INTTYPE)) | ctrl_val;
+    if (iomux_flags & IOMUX_PIN_OUTPUT_ENABLE)
+        GPIO.ENABLE_OUT_SET = BIT(gpio_num);
     else
-        GPIO_DIR_CLEAR = BIT(gpio_num);
+        GPIO.ENABLE_OUT_CLEAR = BIT(gpio_num);
 }
 
 /* Disable GPIO on the specified pin, and set it Hi-Z.
@@ -61,7 +62,7 @@ INLINED void gpio_enable(const uint8_t gpio_num, const gpio_direction_t directio
  */
 INLINED void gpio_disable(const uint8_t gpio_num)
 {
-    GPIO_DIR_CLEAR = BIT(gpio_num);
+    GPIO.ENABLE_OUT_CLEAR = BIT(gpio_num);
     *gpio_iomux_reg(gpio_num) &= ~IOMUX_PIN_OUTPUT_ENABLE;
 }
 
@@ -72,9 +73,9 @@ INLINED void gpio_disable(const uint8_t gpio_num)
 INLINED void gpio_write(const uint8_t gpio_num, const bool set)
 {
     if(set)
-        GPIO_OUT_SET = BIT(gpio_num);
+        GPIO.OUT_SET = BIT(gpio_num);
     else
-        GPIO_OUT_CLEAR = BIT(gpio_num);
+        GPIO.OUT_CLEAR = BIT(gpio_num);
 }
 
 /* Toggle output of a pin
@@ -89,10 +90,10 @@ INLINED void gpio_toggle(const uint8_t gpio_num)
        get an invalid value. Prevents one task from clobbering another
        task's pins, without needing to disable/enable interrupts.
     */
-    if(GPIO_OUT_REG & BIT(gpio_num))
-        GPIO_OUT_CLEAR = BIT(gpio_num);
+    if(GPIO.OUT & BIT(gpio_num))
+        GPIO.OUT_CLEAR = BIT(gpio_num);
     else
-        GPIO_OUT_SET = BIT(gpio_num);
+        GPIO.OUT_SET = BIT(gpio_num);
 }
 
 /* Read input value of a GPIO pin.
@@ -102,38 +103,28 @@ INLINED void gpio_toggle(const uint8_t gpio_num)
  */
 INLINED bool gpio_read(const uint8_t gpio_num)
 {
-    return GPIO_IN_REG & BIT(gpio_num);
+    return GPIO.IN & BIT(gpio_num);
 }
 
-typedef enum {
-    INT_NONE = 0,
-    INT_RISING = GPIO_INT_RISING,
-    INT_FALLING = GPIO_INT_FALLING,
-    INT_CHANGE = GPIO_INT_CHANGE,
-    INT_LOW = GPIO_INT_LOW,
-    INT_HIGH = GPIO_INT_HIGH,
-} gpio_interrupt_t;
-
 extern void gpio_interrupt_handler(void);
 
 /* Set the interrupt type for a given pin
  *
- * If int_type is not INT_NONE, the gpio_interrupt_handler will be attached and unmasked.
+ * If int_type is not GPIO_INTTYPE_NONE, the gpio_interrupt_handler will be attached and unmasked.
  */
-INLINED void gpio_set_interrupt(const uint8_t gpio_num, const gpio_interrupt_t int_type)
+INLINED void gpio_set_interrupt(const uint8_t gpio_num, const gpio_inttype_t int_type)
 {
-    GPIO_CTRL_REG(gpio_num) = (GPIO_CTRL_REG(gpio_num)&~GPIO_INT_MASK)
-        | (int_type & GPIO_INT_MASK);
-    if(int_type != INT_NONE) {
+    GPIO.CONF[gpio_num] = SET_FIELD(GPIO.CONF[gpio_num], GPIO_CONF_INTTYPE, int_type);
+    if(int_type != GPIO_INTTYPE_NONE) {
         _xt_isr_attach(INUM_GPIO, gpio_interrupt_handler);
         _xt_isr_unmask(1<<INUM_GPIO);
     }
 }
 
 /* Return the interrupt type set for a pin */
-INLINED gpio_interrupt_t gpio_get_interrupt(const uint8_t gpio_num)
+INLINED gpio_inttype_t gpio_get_interrupt(const uint8_t gpio_num)
 {
-    return (gpio_interrupt_t)(GPIO_CTRL_REG(gpio_num) & GPIO_INT_MASK);
+    return FIELD2VAL(GPIO_CONF_INTTYPE, GPIO.CONF[gpio_num]);
 }
 
 #endif
diff --git a/core/include/esp/gpio_regs.h b/core/include/esp/gpio_regs.h
new file mode 100644
index 0000000..cbda14b
--- /dev/null
+++ b/core/include/esp/gpio_regs.h
@@ -0,0 +1,133 @@
+/* esp/iomux_regs.h
+ *
+ * ESP8266 GPIO register definitions
+ *
+ * Not compatible with ESP SDK register access code.
+ */
+
+#ifndef _ESP_GPIO_REGS_H
+#define _ESP_GPIO_REGS_H
+
+#include <stdint.h>
+#include "common_macros.h"
+
+#define GPIO_BASE 0x60000300
+#define GPIO (*(struct GPIO_REGS *)(GPIO_BASE))
+
+/** GPIO output registers GPIO.OUT, GPIO.OUT_SET, GPIO.OUT_CLEAR:
+ *
+ * _SET and _CLEAR write-only registers set and clear bits in the main register,
+ * respectively.
+ *
+ * i.e.
+ *   GPIO.OUT_SET = BIT(3);
+ * and
+ *   GPIO.OUT |= BIT(3);
+ *
+ * ... are equivalent, but the former uses fewer CPU cycles.
+ *
+ * ENABLE_OUT / ENABLE_OUT_SET / ENABLE_OUT_CLEAR:
+ *
+ * Determine whether the corresponding GPIO has its output enabled or not.
+ * When clear, GPIO can function as an input.  When set, GPIO will drive its
+ * output (and IN register will simply reflect the output state).
+ *
+ * (_SET/_CLEAR function similarly to OUT registers)
+ *
+ * STATUS / STATUS_SET / STATUS_CLEAR:
+ *
+ * Indicates which GPIOs have triggered an interrupt.  Interrupt status should
+ * be reset by writing to STATUS or STATUS_CLEAR.
+ *
+ * (_SET/_CLEAR function similarly to OUT registers)
+ */
+
+struct GPIO_REGS {
+    uint32_t volatile OUT;              // 0x00
+    uint32_t volatile OUT_SET;          // 0x04
+    uint32_t volatile OUT_CLEAR;        // 0x08
+    uint32_t volatile ENABLE_OUT;       // 0x0c
+    uint32_t volatile ENABLE_OUT_SET;   // 0x10
+    uint32_t volatile ENABLE_OUT_CLEAR; // 0x14
+    uint32_t volatile IN;               // 0x18
+    uint32_t volatile STATUS;           // 0x1c
+    uint32_t volatile STATUS_SET;       // 0x20
+    uint32_t volatile STATUS_CLEAR;     // 0x24
+    uint32_t volatile CONF[16];         // 0x28 - 0x64
+    uint32_t volatile PWM;              // 0x68
+    uint32_t volatile RTC_CALIB;        // 0x6c
+    uint32_t volatile RTC_CALIB_RESULT; // 0x70
+} __attribute__ (( packed ));
+
+/* Double-check the structure size to make sure the compiler hasn't done
+ * something strange (or somebody typoed in the struct definition, etc)
+ */
+_Static_assert(sizeof(struct GPIO_REGS) == 0x74, "GPIO_REGS is the wrong size");
+
+/* Bit mapping for CONF[i] registers */
+
+/* GPIO.CONF[i] control the pin behavior for the corresponding GPIO in/output.
+ *
+ * GPIO_CONF_CONFIG (multi-value)
+ *     FIXME: Unclear what these do.  Need to find a better name.
+ *
+ * GPIO_CONF_WAKEUP_ENABLE (boolean)
+ *     Can an interrupt contion on this pin wake the processor from a sleep
+ *     state?
+ *
+ * GPIO_CONF_INTTYPE (multi-value)
+ *     Under what conditions this GPIO input should generate an interrupt.
+ *     (see gpio_inttype_t enum below for values)
+ *
+ * GPIO_CONF_DRIVER_ENABLE (boolean)
+ *     When set, a high output state will pull the pin up to +Vcc (3.3V).  When
+ *     cleared, output functions in "open drain" mode (low state will pull down
+ *     to ground, but high state allows output to "float").
+ *
+ * GPIO_CONF_SOURCE_PWM (boolean)
+ *     When set, GPIO pin output will be connected to the sigma-delta PWM
+ *     generator (controlled by the GPIO.PWM register).  When cleared, pin
+ *     output will function as a normal GPIO output (controlled by the
+ *     GPIO.OUT* registers).
+ */
+
+#define GPIO_CONF_CONFIG_M       0x00000003
+#define GPIO_CONF_CONFIG_S       11
+#define GPIO_CONF_WAKEUP_ENABLE  BIT(10)
+#define GPIO_CONF_INTTYPE_M     0x00000007
+#define GPIO_CONF_INTTYPE_S     7
+#define GPIO_CONF_DRIVER_ENABLE  BIT(2)
+#define GPIO_CONF_SOURCE_PWM     BIT(0)
+
+/* Valid values for the GPIO_CONF_INTTYPE field */
+typedef enum {
+    GPIO_INTTYPE_NONE       = 0,
+    GPIO_INTTYPE_EDGE_POS   = 1,
+    GPIO_INTTYPE_EDGE_NEG   = 2,
+    GPIO_INTTYPE_EDGE_ANY   = 3,
+    GPIO_INTTYPE_LEVEL_LOW  = 4,
+    GPIO_INTTYPE_LEVEL_HIGH = 5,
+} gpio_inttype_t;
+
+/* Bit mapping for PWM register */
+
+#define GPIO_PWM_ENABLE          BIT(16)
+#define GPIO_PWM_PRESCALER_M     0x000000ff
+#define GPIO_PWM_PRESCALER_S     8
+#define GPIO_PWM_TARGET_M        0x000000ff
+#define GPIO_PWM_TARGET_S        0
+
+/* Bit mapping for RTC_CALIB register */
+
+#define GPIO_RTC_CALIB_START     BIT(31)
+#define GPIO_RTC_CALIB_PERIOD_M  0x000003ff
+#define GPIO_RTC_CALIB_PERIOD_S  0
+
+/* Bit mapping for RTC_CALIB_RESULT register */
+
+#define GPIO_RTC_CALIB_RESULT_READY       BIT(31)
+#define GPIO_RTC_CALIB_RESULT_READY_REAL  BIT(30)
+#define GPIO_RTC_CALIB_RESULT_VALUE_M     0x000fffff
+#define GPIO_RTC_CALIB_RESULT_VALUE_S     0
+
+#endif /* _ESP_GPIO_REGS_H */
diff --git a/core/include/esp/registers.h b/core/include/esp/registers.h
index ae8f169..5b8a785 100644
--- a/core/include/esp/registers.h
+++ b/core/include/esp/registers.h
@@ -17,6 +17,7 @@
 #include "esp/types.h"
 
 #include "esp/iomux_regs.h"
+#include "esp/gpio_regs.h"
 
 /* Internal macro, only defined in header body */
 #define _REG(BASE, OFFSET) (*(esp_reg_t)((BASE)+(OFFSET)))
@@ -31,7 +32,7 @@
 #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 GPIO0_BASE (MMIO_BASE + 0x0300)
 #define TIMER_BASE (MMIO_BASE + 0x0600)
 #define RTC_BASE   (MMIO_BASE + 0x0700)
 //#define IOMUX_BASE (MMIO_BASE + 0x0800)
@@ -42,79 +43,6 @@
 #define RTCS_BASE  (MMIO_BASE + 0x1100)
 #define RTCU_BASE  (MMIO_BASE + 0x1200)
 
-
-/*
- * Based on descriptions by mamalala at https://github.com/esp8266/esp8266-wiki/wiki/gpio-registers
- */
-
-/** GPIO OUTPUT registers GPIO_OUT_REG, GPIO_OUT_SET, GPIO_OUT_CLEAR
- *
- * Registers for pin outputs.
- *
- * _SET and _CLEAR write-only registers set and clear bits in _REG,
- * respectively.
- *
- * ie
- * GPIO_OUT_REG |= BIT(3);
- * and
- * GPIO_OUT_SET = BIT(3);
- *
- * ... are equivalent, but latter uses less CPU cycles.
- */
-#define GPIO_OUT_REG   _REG(GPIO0_BASE, 0x00)
-#define GPIO_OUT_SET   _REG(GPIO0_BASE, 0x04)
-#define GPIO_OUT_CLEAR _REG(GPIO0_BASE, 0x08)
-
-/* GPIO DIR registers GPIO_DIR_REG, GPIO_DIR_SET, GPIO_DIR_CLEAR
- *
- * Set bit in DIR register for output pins. Writing to _SET and _CLEAR
- * registers set and clear bits in _REG, respectively.
-*/
-#define GPIO_DIR_REG   _REG(GPIO0_BASE, 0x0C)
-#define GPIO_DIR_SET   _REG(GPIO0_BASE, 0x10)
-#define GPIO_DIR_CLEAR _REG(GPIO0_BASE, 0x14)
-
-
-/* GPIO IN register GPIO_IN_REG
- *
- * Reads current input values.
- */
-#define GPIO_IN_REG    _REG(GPIO0_BASE, 0x18)
-
-/* GPIO interrupt 'status' flag
-
-   Bit set if interrupt has fired (see below for interrupt config
-   registers.
-
-   Lower 16 bits only are used.
-*/
-#define GPIO_STATUS_REG   _REG(GPIO0_BASE,0x1c)
-#define GPIO_STATUS_SET   _REG(GPIO0_BASE,0x20)
-#define GPIO_STATUS_CLEAR _REG(GPIO0_BASE,0x24)
-
-#define GPIO_STATUS_MASK  0x0000FFFFL
-
-/* GPIO pin control registers for GPIOs 0-15
- *
- */
-#define GPIO_CTRL_REG(GPNUM) _REG(GPIO0_BASE, 0x28+(GPNUM*4))
-
-#define GPIO_SOURCE_GPIO 0
-#define GPIO_SOURCE_DAC BIT(0) /* "Sigma-Delta" */
-#define GPIO_SOURCE_MASK BIT(0
-
-#define GPIO_DRIVE_PUSH_PULL 0
-#define GPIO_DRIVE_OPEN_DRAIN BIT(2)
-#define GPIO_DRIVE_MASK BIT(2)
-
-#define GPIO_INT_NONE 0
-#define GPIO_INT_RISING BIT(7)
-#define GPIO_INT_FALLING BIT(8)
-#define GPIO_INT_CHANGE (BIT(7)|BIT(8))
-#define GPIO_INT_LOW BIT(9)
-#define GPIO_INT_HIGH (BIT(7)|BIT(9))
-#define GPIO_INT_MASK (BIT(7)|BIT(8)|BIT(9))
-
 /* TIMER registers
  *
  * ESP8266 has two hardware(?) timer counters, FRC1 and FRC2.
diff --git a/examples/blink/blink.c b/examples/blink/blink.c
index 8f1b4bf..52d69a5 100644
--- a/examples/blink/blink.c
+++ b/examples/blink/blink.c
@@ -41,12 +41,12 @@ void blinkenTask(void *pvParameters)
 */
 void blinkenRegisterTask(void *pvParameters)
 {
-    GPIO_DIR_SET = BIT(gpio);
+    GPIO.ENABLE_OUT_SET = BIT(gpio);
     IOMUX_GPIO14 = IOMUX_GPIO14_FUNC_GPIO | IOMUX_PIN_OUTPUT_ENABLE; /* change this line if you change 'gpio' */
     while(1) {
-        GPIO_OUT_SET = BIT(gpio);
+        GPIO.OUT_SET = BIT(gpio);
         vTaskDelay(1000 / portTICK_RATE_MS);
-        GPIO_OUT_CLEAR = BIT(gpio);
+        GPIO.OUT_CLEAR = BIT(gpio);
         vTaskDelay(1000 / portTICK_RATE_MS);
     }
 }