diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,16 @@
+arduino-copilot (1.4.0) unstable; urgency=medium
+
+  * New Copilot.Arduino.Library.EEPROMex module.
+  * Serial: Added FlashString, to display strings directly from flash
+    without copying into ram.
+  * Added millis and micros.
+  * MicroSeconds and MilliSeconds types changed to contain a Word32 not a
+    Word16.
+  * Renamed Voltage to ADC
+  * Added frequency.
+
+ -- Joey Hess <id@joeyh.name>  Sat, 01 Feb 2020 16:30:43 -0400
+
 arduino-copilot (1.3.0) unstable; urgency=medium
 
   * New polymorphic "input" action that can be used to read digital and
diff --git a/README b/README
--- a/README
+++ b/README
@@ -12,3 +12,15 @@
 
 This and other examples are included in the examples/ directory, each
 with their own README explaining how to build and use them.
+
+## Contributing
+
+Contributions are welcome, including adding support for more
+Arduino C libraries, provided that the C library is reasonably commonly
+used, and is licensed with a free software license.
+
+Contributors of such libraries will be expected to maintain them,
+otherwise they may get removed if they are blocking changes to
+arduino-copilot or are buggy.
+
+Any contribution should have well documented functions and types.
diff --git a/TODO b/TODO
--- a/TODO
+++ b/TODO
@@ -1,3 +1,4 @@
+
 * Currently all Inputs run each time through the loop, which could be a
   problem when an input is slow. It would be good if the @: operator could
   also rate limit when Inputs occur.
@@ -60,12 +61,23 @@
   three never runs, because both combinators must branch on the same
   Stream Bool.
 
+* The way serial output works is somewhat unsatisfying, because
+  it does not take a Stream FormatOutput. So the Copilot DSL can't be used
+  to adjust the output format as the arduino runs; it's fixed at compile
+  time. While Serial.byte does operate on a Stream, it would be hard to do
+  eg, an interactive program using it.
+
+  It might be possible, using Copilot's support for C structs, to allow
+  Stream FormatOutput to be accepted by Copilot. However, this issue
+  seems like it would probably get in the way of actually constructing such
+  a stream: https://github.com/Copilot-Language/copilot/issues/36
+
 * analogReference()
   Takes one of a set of defines, which vary depending on board,
   so how to pass that through Copilot? Could write a C
   switch statement, on an int, and have the boade modules define
   effectively enums.
-* Voltage is a raw ADC value; add a stream function to convert that
+* ADC is a raw ADC value; add a stream function to convert that
   to a floating point actual voltage, given also a stream that contains
   the reference voltage.
 * pulses
@@ -73,3 +85,8 @@
 * Wire library
 * test.hs is not wired into the cabal file because it depends on runghc
   having the library available
+* EEPROMex supports writing individial bits of a byte, but the haskell
+  library does not expose it. What would be a good haskell interface to
+  that, a Stream (Array 8 Bool)?
+* reading from EEPROMex ranges
+* Factor out Range from EEPROMex, and use it to also implement RAM ranges.
diff --git a/arduino-copilot.cabal b/arduino-copilot.cabal
--- a/arduino-copilot.cabal
+++ b/arduino-copilot.cabal
@@ -1,5 +1,5 @@
 Name: arduino-copilot
-Version: 1.3.0
+Version: 1.4.0
 Cabal-Version: >= 1.8
 License: BSD3
 Maintainer: Joey Hess <id@joeyh.name>
@@ -45,6 +45,14 @@
   examples/serialport/README
   examples/serialport/demo.hs
   examples/serialport/pre-build-hook.sh
+  examples/eeprom/Makefile
+  examples/eeprom/README
+  examples/eeprom/demo.hs
+  examples/eeprom/pre-build-hook.sh
+  examples/eepromrange/Makefile
+  examples/eepromrange/README
+  examples/eepromrange/demo.hs
+  examples/eepromrange/pre-build-hook.sh
   test.hs
 
 Library
@@ -58,6 +66,7 @@
     Copilot.Arduino.Library.Serial
     Copilot.Arduino.Library.Serial.Device
     Copilot.Arduino.Library.Serial.XBee
+    Copilot.Arduino.Library.EEPROMex
   Other-Modules:
     Copilot.Arduino.Main
   Build-Depends:
diff --git a/examples/button/demo.hs b/examples/button/demo.hs
--- a/examples/button/demo.hs
+++ b/examples/button/demo.hs
@@ -2,10 +2,10 @@
 
 import Copilot.Arduino.Uno
 
-longer_and_longer :: Stream Word16
+longer_and_longer :: Stream Word32
 longer_and_longer = counter true $ counter true false `mod` 64 == 0
 
-counter :: Stream Bool -> Stream Bool -> Stream Word16
+counter :: Stream Bool -> Stream Bool -> Stream Word32
 counter inc reset = cnt
    where
 	cnt = if reset
diff --git a/examples/eeprom/Makefile b/examples/eeprom/Makefile
new file mode 100644
--- /dev/null
+++ b/examples/eeprom/Makefile
@@ -0,0 +1,129 @@
+# Arduino Make file. Refer to https://github.com/sudar/Arduino-Makefile
+BOARD_TAG    = uno
+ARDUINO_LIBS = EEPROMEx
+CPPFLAGS     = -D_EEPROMEX_DEBUG
+include /usr/share/arduino/Arduino.mk
+
+# --- leonardo (or pro micro w/leo bootloader)
+#BOARD_TAG    = leonardo
+#MONITOR_PORT = /dev/ttyACM0
+#include /usr/share/arduino/Arduino.mk
+
+# --- mega2560 ide 1.0
+#BOARD_TAG    = mega2560
+#ARDUINO_PORT = /dev/ttyACM0
+#include /usr/share/arduino/Arduino.mk
+
+# --- mega2560 ide 1.6
+#BOARD_TAG    = mega
+#BOARD_SUB    = atmega2560
+#MONITOR_PORT = /dev/ttyACM0
+#ARDUINO_DIR  = /where/you/installed/arduino-1.6.5
+#include /usr/share/arduino/Arduino.mk
+
+# --- nano ide 1.0
+#BOARD_TAG    = nano328
+#MONITOR_PORT = /dev/ttyUSB0
+#include /usr/share/arduino/Arduino.mk
+
+# --- nano ide 1.6
+#BOARD_TAG   = nano
+#BOARD_SUB   = atmega328
+#ARDUINO_DIR = /where/you/installed/arduino-1.6.5
+#include /usr/share/arduino/Arduino.mk
+
+# --- pro mini
+#BOARD_TAG    = pro5v328
+#MONITOR_PORT = /dev/ttyUSB0
+#include /usr/share/arduino/Arduino.mk
+
+# --- sparkfun pro micro
+#BOARD_TAG         = promicro16
+#ALTERNATE_CORE    = promicro
+#BOARDS_TXT        = $(HOME)/arduino/hardware/promicro/boards.txt
+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/promicro/bootloaders
+#BOOTLOADER_PATH   = caterina
+#BOOTLOADER_FILE   = Caterina-promicro16.hex
+#ISP_PROG     	   = usbasp
+#AVRDUDE_OPTS 	   = -v
+#include /usr/share/arduino/Arduino.mk
+
+# --- chipkit
+#BOARD_TAG = mega_pic32
+#MPIDE_DIR = /where/you/installed/mpide-0023-linux64-20130817-test
+#include /usr/share/arduino/chipKIT.mk
+
+# --- pinoccio
+#BOARD_TAG         = pinoccio256
+#ALTERNATE_CORE    = pinoccio
+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/pinoccio/bootloaders
+#BOOTLOADER_PATH   = STK500RFR2/release_0.51
+#BOOTLOADER_FILE   = boot_pinoccio.hex
+#CFLAGS_STD        = -std=gnu99
+#CXXFLAGS_STD      = -std=gnu++11
+#include /usr/share/arduino/Arduino.mk
+
+# --- fio
+#BOARD_TAG = fio
+#include /usr/share/arduino/Arduino.mk
+
+# --- atmega-ng ide 1.6
+#BOARD_TAG    = atmegang
+#BOARD_SUB    = atmega168
+#MONITOR_PORT = /dev/ttyACM0
+#ARDUINO_DIR  = /where/you/installed/arduino-1.6.5
+#include /usr/share/arduino/Arduino.mk
+
+# --- arduino-tiny ide 1.0
+#ISP_PROG     	    = usbasp
+#BOARD_TAG          = attiny85at8
+#ALTERNATE_CORE     = tiny
+#ARDUINO_VAR_PATH   = $(HOME)/arduino/hardware/tiny/cores/tiny
+#ARDUINO_CORE_PATH  = $(HOME)/arduino/hardware/tiny/cores/tiny
+#AVRDUDE_OPTS 	    = -v
+#include /usr/share/arduino/Arduino.mk
+
+# --- arduino-tiny ide 1.6
+#ISP_PROG       = usbasp
+#BOARD_TAG      = attiny85at8
+#ALTERNATE_CORE = tiny
+#ARDUINO_DIR    = /where/you/installed/arduino-1.6.5
+#include /usr/share/arduino/Arduino.mk
+
+# --- damellis attiny ide 1.0
+#ISP_PROG       = usbasp
+#BOARD_TAG      = attiny85
+#ALTERNATE_CORE = attiny-master
+#AVRDUDE_OPTS   = -v
+#include /usr/share/arduino/Arduino.mk
+
+# --- damellis attiny ide 1.6
+#ISP_PROG       = usbasp
+#BOARD_TAG      = attiny
+#BOARD_SUB      = attiny85
+#ALTERNATE_CORE = attiny
+#F_CPU          = 16000000L
+#ARDUINO_DIR    = /where/you/installed/arduino-1.6.5
+#include /usr/share/arduino/Arduino.mk
+
+# --- teensy3
+#BOARD_TAG 	 = teensy31
+#ARDUINO_DIR = /where/you/installed/the/patched/teensy/arduino-1.0.6
+#include /usr/share/arduino/Teensy.mk
+
+# --- mighty 1284p
+#BOARD_TAG         = mighty_opt
+#BOARDS_TXT        = $(HOME)/arduino/hardware/mighty-1284p/boards.txt
+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/mighty-1284p/bootloaders
+#BOOTLOADER_PATH   = optiboot
+#BOOTLOADER_FILE   = optiboot_atmega1284p.hex
+#ISP_PROG     	   = usbasp
+#AVRDUDE_OPTS 	   = -v
+#include /usr/share/arduino/Arduino.mk
+
+# --- atmega328p on breadboard
+#BOARD_TAG    = atmega328bb
+#ISP_PROG     = usbasp
+#AVRDUDE_OPTS = -v
+#BOARDS_TXT   = $(HOME)/arduino/hardware/breadboard/boards.txt
+#include /usr/share/arduino/Arduino.mk
diff --git a/examples/eeprom/README b/examples/eeprom/README
new file mode 100644
--- /dev/null
+++ b/examples/eeprom/README
@@ -0,0 +1,25 @@
+This is a demo program using arduino-copilot, showing how to use EEPROMex.
+
+This needs the Arduino-Makefile to be installed. It relies on it
+to set CPPFLAGS=-D_EEPROMEX_DEBUG when building the C code.
+<https://github.com/sudar/Arduino-Makefile>
+
+The path in the `Makefile` to `Arduino.mk` may need to be adjusted (the
+default is the location where on a Debian system, `apt-get install
+arduino-mk` will install it).
+
+You will also need to install the EEPROMex library to someplace where
+the Arduino toolchain can find it.
+<https://github.com/thijse/Arduino-EEPROMEx/>
+
+First, run this to build and upload a sketch that zeros out some values of
+the EEPROM.
+
+	ZERO=y runghc demo.hs
+	ZERO=y make upload
+
+Then, you can build and upload the sketch that actually does something
+interesting.
+
+	runghc demo.hs
+	make upload
diff --git a/examples/eeprom/demo.hs b/examples/eeprom/demo.hs
new file mode 100644
--- /dev/null
+++ b/examples/eeprom/demo.hs
@@ -0,0 +1,78 @@
+{-# LANGUAGE RebindableSyntax #-}
+
+import Copilot.Arduino.Uno
+import qualified Copilot.Arduino.Library.EEPROMex as EEPROM
+import qualified Copilot.Arduino.Library.Serial as Serial
+import System.Posix.Env
+import qualified Prelude
+
+main :: IO ()
+main = do
+	z <- getEnvDefault "ZERO" ""
+	arduino $ case z of
+		"y" -> mainZero
+		_ -> mainReal
+
+-- Zero out the EEPROM, in preparation for mainReal.
+mainZero :: Sketch ()
+mainZero = do
+	EEPROM.maxAllowedWrites 10
+
+	(_, eepromloc) <- EEPROM.alloc' (20 :: ADC)
+	eepromloc =: constant (0 :: ADC) @: firstIteration
+
+	(_, uptimeloc) <- EEPROM.alloc' (0 :: Word32)
+	uptimeloc =: constant (0 :: Word32) @: firstIteration
+
+	Serial.baud 9600
+	Serial.device =: [ Serial.str "EEPROM zeroed" ] @: firstIteration
+
+mainReal :: Sketch ()
+mainReal = do
+	-- Store the maximum value read from a1 in the EEPROM.
+	-- There are only 1024 possible values, so this can only possibly
+	-- write new values that many times, so no delay is needed in this
+	-- program. Just in case though, limit the total number of writes:
+	EEPROM.maxAllowedWrites 2048
+	(bootval, eepromloc) <- EEPROM.alloc' (20 :: ADC)
+	currval <- input' a1 [10, 11, 25, 100, 99, 0, 0, 0, 0, 0]
+	let maxval = if currval > bootval then currval else bootval
+	-- Note that using maxBound here means it will never write
+	-- on the first iteration, even if a1 is then larger than bootval.
+	-- Any better way to write this that avoids the problem?
+	let newmax = currval > ([maxBound] ++ maxval)
+	eepromloc =: currval @: newmax
+
+	-- Store the maximum MilliSeconds this program has been left running
+	-- in the EEPROM too.
+	(bootuptime, uptimeloc) <- EEPROM.alloc' (0 :: Word32)
+	uptime <- input' millis (dummyclockinput [1, 1, 1, 2, 2, 2, 3, 3, 3])
+	-- micros overflows after ~70 minutes; detect that and use it to
+	-- trigger a periodic write of the uptime to EEPROM.
+	hourlyish <- microsOverFlowed
+	let maxuptime = if uptime > bootuptime then uptime else bootuptime
+	uptimeloc =: maxuptime @: hourlyish
+	
+	Serial.baud 9600
+	Serial.device =:
+		[ Serial.str "max a1 seen: "
+		-- This is not quite right, because maxval resets to
+		-- bootval when this is displayed at hourlyish.
+		-- Interpreting shows the problem:
+		--    --              (3)             (20,3)
+		-- How to fix this?
+		, Serial.show maxval
+		, Serial.str " max uptime: "
+		, Serial.show maxuptime
+		, Serial.char '\n'
+		]
+		@: (firstIteration || newmax || hourlyish)
+
+microsOverFlowed :: Sketch (Behavior Bool)
+microsOverFlowed = do
+	t <- input' micros (dummyclockinput [1, 2, 3])
+	let oldt = [0] ++ t
+	return (t < oldt)
+
+dummyclockinput :: [Word32] -> [Word32]
+dummyclockinput = Prelude.cycle
diff --git a/examples/eeprom/pre-build-hook.sh b/examples/eeprom/pre-build-hook.sh
new file mode 100644
--- /dev/null
+++ b/examples/eeprom/pre-build-hook.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec runghc demo.hs
diff --git a/examples/eepromrange/Makefile b/examples/eepromrange/Makefile
new file mode 100644
--- /dev/null
+++ b/examples/eepromrange/Makefile
@@ -0,0 +1,129 @@
+# Arduino Make file. Refer to https://github.com/sudar/Arduino-Makefile
+BOARD_TAG    = uno
+ARDUINO_LIBS = EEPROMEx
+CPPFLAGS     = -D_EEPROMEX_DEBUG
+include /usr/share/arduino/Arduino.mk
+
+# --- leonardo (or pro micro w/leo bootloader)
+#BOARD_TAG    = leonardo
+#MONITOR_PORT = /dev/ttyACM0
+#include /usr/share/arduino/Arduino.mk
+
+# --- mega2560 ide 1.0
+#BOARD_TAG    = mega2560
+#ARDUINO_PORT = /dev/ttyACM0
+#include /usr/share/arduino/Arduino.mk
+
+# --- mega2560 ide 1.6
+#BOARD_TAG    = mega
+#BOARD_SUB    = atmega2560
+#MONITOR_PORT = /dev/ttyACM0
+#ARDUINO_DIR  = /where/you/installed/arduino-1.6.5
+#include /usr/share/arduino/Arduino.mk
+
+# --- nano ide 1.0
+#BOARD_TAG    = nano328
+#MONITOR_PORT = /dev/ttyUSB0
+#include /usr/share/arduino/Arduino.mk
+
+# --- nano ide 1.6
+#BOARD_TAG   = nano
+#BOARD_SUB   = atmega328
+#ARDUINO_DIR = /where/you/installed/arduino-1.6.5
+#include /usr/share/arduino/Arduino.mk
+
+# --- pro mini
+#BOARD_TAG    = pro5v328
+#MONITOR_PORT = /dev/ttyUSB0
+#include /usr/share/arduino/Arduino.mk
+
+# --- sparkfun pro micro
+#BOARD_TAG         = promicro16
+#ALTERNATE_CORE    = promicro
+#BOARDS_TXT        = $(HOME)/arduino/hardware/promicro/boards.txt
+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/promicro/bootloaders
+#BOOTLOADER_PATH   = caterina
+#BOOTLOADER_FILE   = Caterina-promicro16.hex
+#ISP_PROG     	   = usbasp
+#AVRDUDE_OPTS 	   = -v
+#include /usr/share/arduino/Arduino.mk
+
+# --- chipkit
+#BOARD_TAG = mega_pic32
+#MPIDE_DIR = /where/you/installed/mpide-0023-linux64-20130817-test
+#include /usr/share/arduino/chipKIT.mk
+
+# --- pinoccio
+#BOARD_TAG         = pinoccio256
+#ALTERNATE_CORE    = pinoccio
+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/pinoccio/bootloaders
+#BOOTLOADER_PATH   = STK500RFR2/release_0.51
+#BOOTLOADER_FILE   = boot_pinoccio.hex
+#CFLAGS_STD        = -std=gnu99
+#CXXFLAGS_STD      = -std=gnu++11
+#include /usr/share/arduino/Arduino.mk
+
+# --- fio
+#BOARD_TAG = fio
+#include /usr/share/arduino/Arduino.mk
+
+# --- atmega-ng ide 1.6
+#BOARD_TAG    = atmegang
+#BOARD_SUB    = atmega168
+#MONITOR_PORT = /dev/ttyACM0
+#ARDUINO_DIR  = /where/you/installed/arduino-1.6.5
+#include /usr/share/arduino/Arduino.mk
+
+# --- arduino-tiny ide 1.0
+#ISP_PROG     	    = usbasp
+#BOARD_TAG          = attiny85at8
+#ALTERNATE_CORE     = tiny
+#ARDUINO_VAR_PATH   = $(HOME)/arduino/hardware/tiny/cores/tiny
+#ARDUINO_CORE_PATH  = $(HOME)/arduino/hardware/tiny/cores/tiny
+#AVRDUDE_OPTS 	    = -v
+#include /usr/share/arduino/Arduino.mk
+
+# --- arduino-tiny ide 1.6
+#ISP_PROG       = usbasp
+#BOARD_TAG      = attiny85at8
+#ALTERNATE_CORE = tiny
+#ARDUINO_DIR    = /where/you/installed/arduino-1.6.5
+#include /usr/share/arduino/Arduino.mk
+
+# --- damellis attiny ide 1.0
+#ISP_PROG       = usbasp
+#BOARD_TAG      = attiny85
+#ALTERNATE_CORE = attiny-master
+#AVRDUDE_OPTS   = -v
+#include /usr/share/arduino/Arduino.mk
+
+# --- damellis attiny ide 1.6
+#ISP_PROG       = usbasp
+#BOARD_TAG      = attiny
+#BOARD_SUB      = attiny85
+#ALTERNATE_CORE = attiny
+#F_CPU          = 16000000L
+#ARDUINO_DIR    = /where/you/installed/arduino-1.6.5
+#include /usr/share/arduino/Arduino.mk
+
+# --- teensy3
+#BOARD_TAG 	 = teensy31
+#ARDUINO_DIR = /where/you/installed/the/patched/teensy/arduino-1.0.6
+#include /usr/share/arduino/Teensy.mk
+
+# --- mighty 1284p
+#BOARD_TAG         = mighty_opt
+#BOARDS_TXT        = $(HOME)/arduino/hardware/mighty-1284p/boards.txt
+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/mighty-1284p/bootloaders
+#BOOTLOADER_PATH   = optiboot
+#BOOTLOADER_FILE   = optiboot_atmega1284p.hex
+#ISP_PROG     	   = usbasp
+#AVRDUDE_OPTS 	   = -v
+#include /usr/share/arduino/Arduino.mk
+
+# --- atmega328p on breadboard
+#BOARD_TAG    = atmega328bb
+#ISP_PROG     = usbasp
+#AVRDUDE_OPTS = -v
+#BOARDS_TXT   = $(HOME)/arduino/hardware/breadboard/boards.txt
+#include /usr/share/arduino/Arduino.mk
diff --git a/examples/eepromrange/README b/examples/eepromrange/README
new file mode 100644
--- /dev/null
+++ b/examples/eepromrange/README
@@ -0,0 +1,26 @@
+This is a demo program using arduino-copilot, showing how to use EEPROMex
+with ranges.
+
+This needs the Arduino-Makefile to be installed. It relies on it
+to set CPPFLAGS=-D_EEPROMEX_DEBUG when building the C code.
+<https://github.com/sudar/Arduino-Makefile>
+
+The path in the `Makefile` to `Arduino.mk` may need to be adjusted (the
+default is the location where on a Debian system, `apt-get install
+arduino-mk` will install it).
+
+You will also need to install the EEPROMex library to someplace where
+the Arduino toolchain can find it.
+<https://github.com/thijse/Arduino-EEPROMEx/>
+
+First, run this to build and upload a sketch that zeros out the entire
+EEPROM.
+
+	ZERO=y runghc demo.hs
+	ZERO=y make upload
+
+Then, you can build and upload the sketch that actually does something
+interesting.
+
+	runghc demo.hs
+	make upload
diff --git a/examples/eepromrange/demo.hs b/examples/eepromrange/demo.hs
new file mode 100644
--- /dev/null
+++ b/examples/eepromrange/demo.hs
@@ -0,0 +1,40 @@
+{-# LANGUAGE RebindableSyntax #-}
+
+import Copilot.Arduino.Uno
+import qualified Copilot.Arduino.Library.EEPROMex as EEPROM
+import qualified Copilot.Arduino.Library.Serial as Serial
+import System.Posix.Env
+
+-- Change this to use a smaller part of the EEPROM.
+sz :: Word16
+sz = sizeOfEEPROM
+
+main :: IO ()
+main = do
+	z <- getEnvDefault "ZERO" ""
+	arduino $ case z of
+		"y" -> mainZero
+		_ -> mainReal
+
+-- Zero out the EEPROM, in preparation for mainReal.
+mainZero :: Sketch ()
+mainZero = do
+	EEPROM.maxAllowedWrites sz
+
+	range <- EEPROM.allocRange sz :: Sketch (EEPROM.Range Word8)
+	range =: EEPROM.sweepRange 0 (constant (0 :: Word8))
+	
+	Serial.baud 9600
+	Serial.device =: [ Serial.str "EEPROM zeroing started" ]
+		@: firstIteration
+
+mainReal :: Sketch ()
+mainReal = do
+	EEPROM.maxAllowedWrites (sz * 10)
+	range <- EEPROM.allocRange sz :: Sketch (EEPROM.Range ADC)
+	v <- input' a1 ([10, 20..] :: [ADC])
+	range =: EEPROM.sweepRange 0 v @: frequency 3
+	led =: frequency 3
+	Serial.device =: [ Serial.show v, Serial.char '\n']
+	delay =: MilliSeconds (constant 10000)
+	Serial.baud 9600
diff --git a/examples/eepromrange/pre-build-hook.sh b/examples/eepromrange/pre-build-hook.sh
new file mode 100644
--- /dev/null
+++ b/examples/eepromrange/pre-build-hook.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec runghc demo.hs
diff --git a/examples/serialport/demo.hs b/examples/serialport/demo.hs
--- a/examples/serialport/demo.hs
+++ b/examples/serialport/demo.hs
@@ -16,11 +16,11 @@
 useXBee :: Bool
 useXBee = True
 
-userIsTyping :: Stream Int8 -> Stream Bool
+userIsTyping :: Behavior Int8 -> Behavior Bool
 userIsTyping userinput = userinput /= constant Serial.noInput
 
 getSerial :: Input o Int8 => Bool -> o -> Sketch (Behavior Int8)
-getSerial True i = input i
+getSerial True i = input' i (repeat Serial.noInput)
 getSerial False _ = return (constant Serial.noInput)
 
 main :: IO ()
@@ -28,14 +28,17 @@
 	when useSerial $ Serial.baud 9600
 	when useXBee $ XBee.configure pin2 pin3 (XBee.Baud 9600)
 
-	-- Measure the voltage of an analog input, and display it to the
+	-- Measure the an analog input, and display it to the
 	-- serial port. This is a handy demo because with nothing connected
 	-- to a0, just moving your hand close to the arduino will probably
 	-- cause some noise to be read.
-	n <- input a0 :: Sketch (Behavior Voltage)
+	n <- input' a0 [1, 50, 11, 99, 120] :: Sketch (Behavior ADC)
+	c <- input' millis [20, 60, 80, 100] :: Sketch (Behavior Word32)
 	let msg = 
 		[ Serial.str "analog input:"
 		, Serial.showFormatted n Serial.HEX
+		, Serial.str " millis:"
+		, Serial.show c
 		, Serial.char '\n'
 		]
 	when useSerial $ Serial.device =: msg
diff --git a/src/Copilot/Arduino.hs b/src/Copilot/Arduino.hs
--- a/src/Copilot/Arduino.hs
+++ b/src/Copilot/Arduino.hs
@@ -9,6 +9,8 @@
 {-# LANGUAGE RebindableSyntax #-}
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeApplications #-}
 
 module Copilot.Arduino (
 	-- * Arduino sketch generation
@@ -25,7 +27,8 @@
 	input,
 	input',
 	pullup,
-	Voltage,
+	millis,
+	micros,
 	-- * Outputs
 	--
 	-- | Only a few common outputs are included in this module.
@@ -36,11 +39,16 @@
 	(=:),
 	pwm,
 	delay,
+	-- * Other types
+	ADC,
 	MilliSeconds(..),
 	MicroSeconds(..),
+	ClockMillis,
+	ClockMicros,
 	-- * Utilities
 	blinking,
 	firstIteration,
+	frequency,
 	sketchSpec,
 	-- * Copilot DSL
 	--
@@ -62,6 +70,8 @@
 import Copilot.Arduino.Internals
 import Copilot.Arduino.Main
 import Control.Monad.Writer
+import Data.Proxy
+import Data.Maybe
 import qualified Data.Map as M
 import qualified Data.Set as S
 
@@ -82,11 +92,19 @@
 firstIteration :: Behavior Bool
 firstIteration = [True]++false
 
+-- | Use this to make an event occur 1 time out of n.
+--
+-- This is implemented using Copilot's `clk`:
+--
+-- > frequency = clk (period n) (phase 1)
+frequency :: Integer -> Behavior Bool
+frequency n = clk (period n) (phase 1)
+
 -- | A stream of milliseconds.
-data MilliSeconds = MilliSeconds (Stream Word16)
+data MilliSeconds = MilliSeconds (Stream Word32)
 
 -- | A stream of microseconds.
-data MicroSeconds = MicroSeconds (Stream Word16)
+data MicroSeconds = MicroSeconds (Stream Word32)
 
 -- | Use this to add a delay between each iteration of the `Sketch`.
 -- A `Sketch` with no delay will run as fast as the hardware can run it.
@@ -104,7 +122,46 @@
 instance Output Delay MicroSeconds where
 	Delay =: (MicroSeconds n) = tell
 		[(trigger "delayMicroseconds" true [arg n], mempty)]
-	
+
+-- | Number of MillisSeconds since the Arduino booted.
+--
+-- > n <- input millis
+-- 
+-- The value wraps back to zero after approximately 50 days.
+millis :: ClockMillis
+millis = ClockMillis
+
+-- | Number of MicroSeconds since the Arduino booted.
+--
+-- > n <- input micros
+-- 
+-- The value wraps back to zero after approximately 70 minutes.
+micros :: ClockMicros
+micros = ClockMicros
+
+data ClockMillis = ClockMillis
+data ClockMicros = ClockMicros
+
+instance Input ClockMillis Word32 where
+	input' ClockMillis = inputClock "millis"
+
+instance Input ClockMicros Word32 where
+	input' ClockMicros = inputClock "micros"
+
+inputClock :: [Char] -> [Word32] -> Sketch (Behavior Word32)
+inputClock src interpretvalues = mkInput $ InputSource
+	{ setupInput = []
+	, defineVar = [CLine $ showCType (Proxy @Word32) <> " " <> varname <>";"]
+	, inputPinmode = mempty
+	, readInput = [CLine $ varname <> " = " <> src <> "();"]
+	, inputStream = extern varname interpretvalues'
+	}
+  where
+	varname = "clock_" <> src
+	interpretvalues'
+		| null interpretvalues = Nothing
+		| otherwise = Just interpretvalues
+
 -- | Use this to read a value from a component of the Arduino.
 --
 -- For example, to read a digital value from pin12 and turn on the 
@@ -114,10 +171,11 @@
 -- > led =: buttonpressed
 --
 -- Some pins support multiple types of reads, for example pin a0
--- supports a digital read (`Bool`), and an analog read (`Voltage`).
--- In such cases you may need to specify the type of data to read:
+-- supports a digital read (`Bool`), and an analog to digital converter
+-- read (`ADC`). In such cases you may need to specify the type of
+-- data to read:
 --
--- > v <- input a0 :: Sketch (Behavior Voltage)
+-- > v <- input a0 :: Sketch (Behavior ADC)
 input :: Input o t => o -> Sketch (Behavior t)
 input o = input' o []
 
@@ -154,6 +212,4 @@
 -- This can be useful to intergrate with other libraries 
 -- such as copilot-theorem.
 sketchSpec :: Sketch a -> Spec
-sketchSpec (Sketch s) = sequence_ is
-  where
-	(is, _fs) = unzip (execWriter s)
+sketchSpec = fromMaybe (return ()) . fst . evalSketch
diff --git a/src/Copilot/Arduino/Internals.hs b/src/Copilot/Arduino/Internals.hs
--- a/src/Copilot/Arduino/Internals.hs
+++ b/src/Copilot/Arduino/Internals.hs
@@ -15,9 +15,12 @@
 
 import Language.Copilot
 import Control.Monad.Writer
+import Control.Monad.State.Strict
+import Data.Functor.Identity
 import qualified Data.Map as M
 import qualified Data.Set as S
 import Data.Type.Bool
+import Data.Proxy
 import GHC.TypeLits
 
 -- | A value that changes over time.
@@ -50,8 +53,14 @@
 --
 -- While it is a monad, a Sketch's outputs are not updated in any
 -- particular order, because Copilot does not guarantee any order.
-newtype Sketch t = Sketch (Writer [(Spec, Framework)] t)
-	deriving (Monad, Applicative, Functor, MonadWriter [(Spec, Framework)])
+newtype Sketch t = Sketch (WriterT [(Spec, Framework)] (State UniqueIds) t)
+	deriving 
+		( Monad
+		, Applicative
+		, Functor
+		, MonadWriter [(Spec, Framework)]
+		, MonadState UniqueIds
+		)
 
 instance Monoid (Sketch ()) where
 	mempty = Sketch (return ())
@@ -59,12 +68,36 @@
 instance Semigroup (Sketch t) where
 	(Sketch a) <> (Sketch b) = Sketch (a >> b)
 
+newtype UniqueIds = UniqueIds [Integer]
+
+evalSketch :: Sketch a -> (Maybe Spec, Framework)
+evalSketch (Sketch s) = (spec, mconcat fs)
+  where
+	(is, fs) = unzip $ 
+		runIdentity $ evalStateT (execWriterT s) (UniqueIds [1..])
+	-- Copilot will throw an ugly error if given a spec that does
+	-- nothing at all, so return Nothing to avoid that.
+	spec = if null is
+		then Nothing
+		else Just (sequence_ is)
+
+getUniqueId :: Sketch Integer
+getUniqueId = do
+	v <- get
+	case v of
+		UniqueIds (u:us) -> do
+			put (UniqueIds us)
+			return u
+		UniqueIds [] -> error "somehow UniqueIds ran out"
+
 -- | The framework of an Arduino sketch.
 data Framework = Framework
 	{ defines :: [CLine]
 	-- ^ Things that come before the C code generated by Copilot.
 	, setups :: [CLine]
 	-- ^ Things to do at setup, not including configuring pins.
+	, earlySetups :: [CLine]
+	-- ^ Things to do at setup, before the setups.
 	, pinmodes :: M.Map PinId (S.Set PinMode)
 	-- ^ How pins are used.
 	, loops :: [CLine]
@@ -78,12 +111,13 @@
 	a <> b = Framework
 		{ defines = defines a <> defines b
 		, setups = setups a <> setups b
+		, earlySetups = earlySetups a <> earlySetups b
 		, pinmodes = M.unionWith S.union (pinmodes a) (pinmodes b)
 		, loops = loops a  <> loops b
 		}
 
 instance Monoid Framework where
-	mempty = Framework mempty mempty mempty mempty
+	mempty = Framework mempty mempty mempty mempty mempty
 
 -- | Copilot only supports calling a trigger with a given name once
 -- per Spec; the generated C code will fail to build if the same name is
@@ -117,6 +151,7 @@
 	f = Framework
 		{ defines = defineVar i
 		, setups = setupInput i
+		, earlySetups = mempty
 		, pinmodes = M.map S.singleton (inputPinmode i)
 		, loops = readInput i
 		}
@@ -262,10 +297,10 @@
 			| null interpretvalues = Nothing
 			| otherwise = Just interpretvalues
 
--- | Voltage read from an Arduino's ADC. Ranges from 0-1023.
-type Voltage = Int16
+-- | Value read from an Arduino's ADC. Ranges from 0-1023.
+type ADC = Int16
 
-instance IsAnalogInputPin t => Input (Pin t) Voltage where
+instance IsAnalogInputPin t => Input (Pin t) ADC where
 	input' (Pin (PinId n)) interpretvalues = mkInput $ InputSource
 		{ defineVar = [CLine $ "int " <> varname <> ";"]
 		, setupInput = []
@@ -278,4 +313,19 @@
 		interpretvalues'
 			| null interpretvalues = Nothing
 			| otherwise = Just interpretvalues
+
+class ShowCType t where
+	showCType :: Proxy t -> String
+
+instance ShowCType Bool where showCType _ = "bool"
+instance ShowCType Int8 where showCType _ = "int8_t"
+instance ShowCType Int16 where showCType _ = "int16_t"
+instance ShowCType Int32 where showCType _ = "int32_t"
+instance ShowCType Int64 where showCType _ = "int64_t"
+instance ShowCType Word8 where showCType _ = "uint8_t"
+instance ShowCType Word16 where showCType _ = "uint16_t"
+instance ShowCType Word32 where showCType _ = "uint32_t"
+instance ShowCType Word64 where showCType _ = "uint64_t"
+instance ShowCType Float where showCType _ = "float"
+instance ShowCType Double where showCType _ = "double"
 
diff --git a/src/Copilot/Arduino/Library/EEPROMex.hs b/src/Copilot/Arduino/Library/EEPROMex.hs
new file mode 100644
--- /dev/null
+++ b/src/Copilot/Arduino/Library/EEPROMex.hs
@@ -0,0 +1,368 @@
+-- | EEPROMex library for arduino-copilot.
+--
+-- This module is designed to be imported qualified.
+--
+-- This is an interface to the C EEPROMex library, which will need to be
+-- installed in your Arduino development environment.
+-- https://playground.arduino.cc/Code/EEPROMex/
+
+{-# LANGUAGE RebindableSyntax #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Copilot.Arduino.Library.EEPROMex (
+	-- * Configuration
+	maxAllowedWrites,
+	memPool,
+	StartAddress(..),
+	EndAddress(..),
+	EEPROMable,
+	-- * Single values
+	alloc,
+	alloc',
+	Location,
+	-- * Ranges
+	Range,
+	RangeIndex,
+	allocRange,
+	sweepRange,
+	sweepRange',
+	RangeWrites(..),
+) where
+
+import Copilot.Arduino
+import Copilot.Arduino.Internals
+import Control.Monad.Writer
+import Data.Proxy
+
+-- | Set the maximum number of writes to EEPROM that can be made while
+-- the Arduino is running. When too many writes have been made,
+-- it will no longer write to the EEPROM, and will send warning
+-- messages to the serial port.
+--
+-- This is a strongly encouraged safety measure to use, because the
+-- Arduino's EEPROM can only endure around 100,000 writes, and
+-- a Sketch that's constantly writing to EEPROM, without a well chosen 
+-- `:@` rate limit or `delay`, could damage your hardware in just a few
+-- minutes.
+--
+-- Note that this module uses EEPROMex's update facility for all writes
+-- to EEPROM. That avoids writing a byte when its current value is
+-- the same as what needed to be written. That prevents excessive wear
+-- in some cases, but you should still use maxAllowedWrites too.
+--
+-- For this to work, CPPFLAGS needs to include "-D_EEPROMEX_DEBUG" when the
+-- EEPROMex C library gets built. When you use this, it generates C
+-- code that makes sure that is enabled.
+maxAllowedWrites :: Word16 -> Sketch ()
+maxAllowedWrites n = tell [(return (), f)]
+  where
+	f = mempty
+		{ earlySetups = 
+			[ CLine "#ifdef _EEPROMEX_DEBUG"
+			, CLine $ "EEPROM.setMaxAllowedWrites(" <> show n <> ");"
+			, CLine "#else"
+			, CLine "#error \"maxAllowedWrites cannot be checked because _EEPROMEX_DEBUG is not set.\""
+			, CLine "#endif"
+			]
+		, defines = [ includeCLine ]
+		}
+
+-- | The address of the first byte of the EEPROM that will be allocated
+-- by `alloc`. The default is to start allocation from 0. 
+--
+-- Picking a different StartAddress can avoid overwriting the 
+-- part of the EEPROM that is used by some other program.
+newtype StartAddress = StartAddress Word16
+	deriving (Num, Eq, Ord, Enum, Show, Read, Bounded, Integral, Real)
+
+-- | The address of the last byte of the EEPROM that can be allocated
+-- by `alloc`. The default is to allow allocating 512 bytes.
+--
+-- Modules for particular boards, such as Copilot.Arduino.Library.Uno,
+-- each define a sizeOfEEPROM value, so to use the entire EEPROM, use:
+--
+-- > EndAddress sizeOfEEPROM
+newtype EndAddress = EndAddress Word16
+	deriving (Num, Eq, Ord, Enum, Show, Read, Bounded, Integral, Real)
+
+-- | Configure the EEPROM memory pool that the program can use.
+--
+-- > EEPROM.memPool 0 (EndAddress sizeOfEEPROM)
+memPool :: StartAddress -> EndAddress -> Sketch ()
+memPool (StartAddress start) (EndAddress end) = tell [(return (), f)]
+  where
+	f = mempty
+		-- setMemPool() has to come before any
+		-- getAddress(), so do it in earlySetups.
+		{ earlySetups = 
+			[ CLine $ "EEPROM.setMemPool("
+				<> show start
+				<> ", "
+				<> show end
+				<> ");"
+			]
+		, defines = [ includeCLine ]
+		}
+
+-- | Allocates a location in the EEPROM. 
+--
+-- Two things are returned; first a `Behavior` which contains a value
+-- read from the EEPROM at boot; and secondly a `Location` that can be
+-- written to later on.
+--
+-- Here's an example of using it, to remember the maximum value read from
+-- a1, persistently across power cycles.
+--
+-- > EEPROM.maxAllowedWrites 100
+-- > (bootval, eepromloc) <- EEPROM.alloc
+-- > currval <- input a1 :: Sketch (Behavior ADC)
+-- > let maxval = [maxBound] ++ if currval > bootval then currval else bootval
+-- > eepromloc =: currval @: (currval > maxval)
+-- > delay =: MilliSeconds (constant 10000)
+--
+-- Of course, the EEPROM could already contain any value at the allocated
+-- location to start with (either some default value or something written
+-- by another program). So you'll need to first boot up a sketch that zeros
+-- it out before using the sketch above. Here's a simple sketch that zeros
+-- two values:
+--
+-- > EEPROM.maxAllowedWrites 100
+-- > (_, eepromloc1) <- EEPROM.alloc
+-- > (_, eepromloc2) <- EEPROM.alloc
+-- > eepromloc1 =: constant (0 :: ADC) :@ firstIteration
+-- > eepromloc2 =: constant (0 :: Word16) :@ firstIteration
+-- 
+-- `alloc` can be used as many times as you want, storing multiple values
+-- in the EEPROM, up to the limit of the size of the EEPROM.
+-- (Which is not statically checked.)
+--
+-- Do note that the EEPROM layout is controlled by the order of calls to
+-- `alloc`, so take care when reordering or deleting calls.
+alloc :: forall t. (EEPROMable t) => Sketch (Behavior t, Location t)
+alloc = alloc' (factoryValue (Proxy @t))
+
+-- | Same as `alloc'`, but with a value which will be used
+-- as the EEPROM's boot value when interpreting the Sketch,
+-- instead of the default of acting as if all bits of the EEPROM are
+-- set.
+alloc' :: forall t. (EEPROMable t) => t -> Sketch (Behavior t, Location t)
+alloc' interpretval = do
+	i <- getUniqueId
+	let addrvarname = "eeprom_address" <> show i
+	let bootvarname = "eeprom_boot_val" <> show i
+	let writername = "eeprom_write" <> show i
+	let proxy = Proxy @t
+	bootval <- mkInput $ InputSource
+		{ defineVar = 
+			[ includeCLine
+			, CLine $ "int " <> addrvarname <> ";"
+			, CLine $ showCType proxy <> " " <> bootvarname <> ";"
+			, CLine $ "void " <> writername
+				<> "(" <> showCType proxy <> " value) {"
+			, CLine $ "  EEPROM." <> writeValue proxy
+				<> "(" <> addrvarname <> ", value);"
+			, CLine "}"
+			]
+		, setupInput =
+			[ CLine $ addrvarname <> 
+				" = EEPROM.getAddress(sizeof(" 
+				<> showCType proxy <> "));"
+			, CLine $ bootvarname <>
+				" = EEPROM."
+				<> readValue proxy <> "(" <> addrvarname <> ");"
+			]
+		, readInput = []
+		, inputStream = extern bootvarname (Just (repeat interpretval))
+		, inputPinmode = mempty
+		}
+	return (bootval, Location writername)
+
+data Location t = Location String
+
+instance EEPROMable t => Output (Location t) (Event () (Stream t)) where
+	Location writername =: (Event v c) =
+		tell [(trigger writername c [arg v], mempty)]
+
+-- | A range of values in the EEPROM.
+data Range t = Range
+	{ rangeStart :: Location (Range t)
+	, rangeSize :: Word16 -- ^ number of `t` values in the Range
+	}
+
+-- | Allocates a Range in the EEPROM, which stores the specified number of
+-- items of some type.
+--
+-- This is an example of using a range of the EEPROM as a ring buffer,
+-- to store the last 100 values read from a1.
+--
+-- > EEPROM.maxAllowedWrites 1000
+-- > range <- EEPROM.allocRange 100 :: Sketch (EEPROM.Range ADC)
+-- > currval <- input a1 :: Sketch (Behavior ADC)
+-- > range =: EEPROM.sweepRange 0 currval
+-- > delay =: MilliSeconds (constant 10000)
+--
+-- `allocRange` can be used as many times as you want, and combined with
+-- uses of `alloc`, up to the size of the EEPROM. 
+-- (Which is not statically checked.)
+--
+-- Do note that the EEPROM layout is controlled by the order of calls to
+-- `allocRange` and `alloc`, so take care when reordering or deleting calls.
+allocRange :: forall t. (EEPROMable t) => Word16 -> Sketch (Range t)
+allocRange sz = do
+	i <- getUniqueId
+	let startaddrvarname = "eeprom_range_address" <> show i
+	let writername = "eeprom_range_write" <> show i
+	let proxy = Proxy @t
+	let f = Framework
+		{ defines = 
+			[ includeCLine
+			, CLine $ "int " <> startaddrvarname <> ";"
+			, CLine $ "void " <> writername
+				<> "(" <> showCType proxy <> " value"
+				<> ", " <> showCType (Proxy @Word16) <> " offset"
+				<> ") {"
+			, CLine $ "  EEPROM." <> writeValue proxy
+				<> "(" <> startaddrvarname 
+					<> " + offset*sizeof(" 
+						<> showCType proxy
+						<> ")"
+				<> ", value);"
+			, CLine "}"
+			]
+		, setups = 
+			[ CLine $ startaddrvarname <> " = EEPROM.getAddress"
+				<> "(sizeof(" <> showCType proxy <> ")" 
+				<> " * " <> show sz
+				<> ");"
+			]
+		, earlySetups = []
+		, pinmodes = mempty
+		, loops = mempty
+		}
+	tell [(return (), f)]
+	return (Range (Location writername) sz)
+
+-- | Description of writes made to a Range.
+--
+-- This can be turned into an `Event` by using `@:` with it, and that's
+-- what the Behavior Bool is needed for. Consider this example,
+-- that ignores the Behavior Bool, and just uses a counter for the
+-- `RangeIndex`:
+--
+-- > range =: EEPROM.RangeWrites (\_ -> counter) (constant 0) @: blinking
+--
+-- The use of `@:` `blinking` makes an `Event` that only occurs on every other
+-- iteration of the Sketch. But, the counter increases on every
+-- iteration of the Sketch. So that will only write to every other value
+-- in the `Range`.
+--
+-- The Behavior Bool is only True when an event occurs, and so
+-- it can be used to avoid incrementing the counter otherwise. See
+--`sweepRange'` for an example.
+data RangeWrites t = RangeWrites
+	(Behavior Bool -> Behavior RangeIndex) 
+	(Behavior t)
+
+-- | An index into a Range. 0 is the first value in the Range.
+--
+-- Indexes larger than the size of the Range will not overflow it,
+-- instead they loop back to the start of the Range.
+type RangeIndex = Word16
+
+instance EEPROMable t => Output (Range t) (RangeWrites t) where
+	(=:) = writeRange true
+
+instance EEPROMable t => Output (Range t) (Event () (RangeWrites t)) where
+	range =: Event ws c = 
+		writeRange c range ws
+
+instance EEPROMable t => IsBehavior (RangeWrites t) where
+	(@:) = Event
+
+type instance BehaviorToEvent (RangeWrites t) = Event () (RangeWrites t)
+
+writeRange :: EEPROMable t => Behavior Bool -> Range t -> RangeWrites t -> Sketch()
+writeRange c range (RangeWrites idx v) = 
+	tell [(trigger writername c [arg idx', arg v], mempty)]
+  where
+	Location writername = rangeStart range
+	idx' = idx c `mod` constant (rangeSize range)
+
+-- | Treat the range as a ring buffer, and starting with the specified
+-- `RangeIndex`, sweep over the range writing values from the
+-- `Behavior`.
+sweepRange :: RangeIndex -> Behavior t -> RangeWrites t
+sweepRange start = RangeWrites (sweepRange' start)
+
+-- | This is actually just a simple counter that increments on each write
+-- to the range. That's sufficient, because writes that overflow the
+-- end of the range wrap back to the start.
+sweepRange' :: RangeIndex -> Behavior Bool -> Behavior RangeIndex
+sweepRange' start c = cnt
+  where
+	cnt = [start] ++ rest
+	rest = if c then cnt + 1 else cnt
+
+class (ShowCType t, Typed t) => EEPROMable t where
+	readValue :: Proxy t -> String
+	writeValue :: Proxy t -> String
+	factoryValue :: Proxy t -> t
+	-- ^ The EEPROM comes from the factory with all bits set,
+	-- this follows suite. Eg, maxBound for unsigned ints,
+	-- minBound for signed ints since the sign bit being set
+	-- makes it negative, and NaN for floats.
+
+-- | This instance is not efficient; a whole byte is read/written
+-- rather than a single bit.
+instance EEPROMable Bool where
+	readValue _ = "readByte"
+	writeValue _ = "updateByte"
+	factoryValue _ = True
+
+instance EEPROMable Int8 where
+	readValue _ = "readByte"
+	writeValue _ = "updateByte"
+	factoryValue _ = minBound
+
+instance EEPROMable Int16 where
+	readValue _ = "readInt"
+	writeValue _ = "updateInt"
+	factoryValue _= minBound
+
+instance EEPROMable Int32 where
+	readValue _ = "readLong"
+	writeValue _ = "updateLong"
+	factoryValue _= minBound
+
+instance EEPROMable Word8 where
+	readValue _ = "readByte"
+	writeValue _ = "updateByte"
+	factoryValue _ = maxBound
+
+instance EEPROMable Word16 where
+	readValue _ = "readInt"
+	writeValue _ = "updateInt"
+	factoryValue _ = maxBound
+
+instance EEPROMable Word32 where
+	readValue _ = "readLong"
+	writeValue _ = "updateLong"
+	factoryValue _ = maxBound
+
+instance EEPROMable Float where
+	readValue _ = "readFloat"
+	writeValue _ = "updateFloat"
+	factoryValue _ = 0/0 -- NaN
+
+instance EEPROMable Double where
+	readValue _ = "readDouble"
+	writeValue _ = "updateDOuble"
+	factoryValue _ = 0/0 -- NaN
+
+includeCLine :: CLine
+includeCLine = CLine "#include <EEPROMex.h>"
diff --git a/src/Copilot/Arduino/Library/Serial.hs b/src/Copilot/Arduino/Library/Serial.hs
--- a/src/Copilot/Arduino/Library/Serial.hs
+++ b/src/Copilot/Arduino/Library/Serial.hs
@@ -10,13 +10,14 @@
 	device,
 	char,
 	str,
+	FlashString(..),
 	show,
 	showFormatted,
 	byte,
 	noInput,
 	SerialDevice,
 	FormatOutput,
-	ShowableType,
+	OutputString,
 	FormatableType,
 	Base(..),
 ) where
@@ -40,7 +41,7 @@
 -- that describes the serial output. Note that you can only do this once
 -- in a Sketch.
 --
--- > main = arduino $ doa
+-- > main = arduino $ do
 -- > 	Serial.baud 9600
 -- > 	b <- input pin4
 -- > 	Serial.device =:
diff --git a/src/Copilot/Arduino/Library/Serial/Device.hs b/src/Copilot/Arduino/Library/Serial/Device.hs
--- a/src/Copilot/Arduino/Library/Serial/Device.hs
+++ b/src/Copilot/Arduino/Library/Serial/Device.hs
@@ -75,7 +75,7 @@
 			, readInput = [CLine $ varname <> " = " <> devname <> ".read();"]
 			, inputStream = extern varname interpretvalues'
 			}
-		varname = "arduino_serial_" <> devname <> "_read"
+		varname = "input_" <> devname
 		interpretvalues'
 			| null interpretvalues = Nothing
 			| otherwise = Just interpretvalues
@@ -94,7 +94,7 @@
 		go = trigger triggername c (mapMaybe formatArg l)
 		f = mempty { defines = printer }
 
-		triggername = "arduino_serial_" <> devname <> "_output"
+		triggername = "output_" <> devname
 	
 		printer = concat
 			[ [CLine $ "void " <> triggername <> "("
@@ -120,7 +120,7 @@
 	, formatCLine :: SerialDeviceName -> String -> CLine
 	}
 
--- | Use this to output a Char
+-- | Use this to output a Char.
 char :: Char -> FormatOutput
 char c = FormatOutput Nothing Nothing
 	(\(SerialDeviceName devname) _ ->
@@ -131,21 +131,37 @@
 	esc '\n' = "\\n"
 	esc c' = [c']
 
--- | Use this to output a String
-str :: String -> FormatOutput
-str s = FormatOutput Nothing Nothing
-	(\(SerialDeviceName devname) _ ->
-		CLine $ devname <> ".print(\"" <> concatMap esc s <> "\");")
+quoteString :: String -> String
+quoteString s = '"' : concatMap esc s <> "\""
   where
 	esc '"' = "\""
 	esc '\\' = "\\"
 	esc '\n' = "\\\n"
 	esc c = [c]
 
+class OutputString t where
+	-- | Use this to output a `String` or `FlashString`
+	str :: t -> FormatOutput
+
+instance OutputString String where
+	str s = FormatOutput Nothing Nothing $ \(SerialDeviceName devname) _ ->
+		CLine $ devname <> ".print(" <> quoteString s <> ");"
+
+-- | Normally a String will be copied into ram before it is output.
+-- A FlashString will be output directly from flash memory.
+--
+-- Using this with `str` will reduce the amount of memory used by your
+-- program, but will likely slightly increase the size of the program.
+newtype FlashString = FlashString String
+
+instance OutputString FlashString where
+	str (FlashString s) = FormatOutput Nothing Nothing $ \(SerialDeviceName devname) _ ->
+		CLine $ devname <> ".print(F(" <> quoteString s <> "));"
+
 -- | Use this to show the current value of a Stream.
 --
 -- Numbers will be formatted in decimal. Bool is displayed as 0 and 1.
-show :: forall t. (ShowableType t, Typed t) => Stream t -> FormatOutput
+show :: forall t. (ShowCType t, Typed t) => Stream t -> FormatOutput
 show s = FormatOutput
 	(Just (arg s))
 	(Just (showCType (Proxy @t)))
@@ -171,7 +187,7 @@
 --
 -- > Serial.showFormatted (constant (78 :: Int8)) Serial.HEX -- "4E"
 showFormatted
-	:: forall t f. (ShowableType t, Typed t, FormatableType t f)
+	:: forall t f. (ShowCType t, Typed t, FormatableType t f)
 	=> Stream t
 	-> f
 	-> FormatOutput
@@ -182,21 +198,6 @@
 		CLine $ devname <> ".print(" <> v <> ", " <> formatter t f <> ");")
   where
 	t = Proxy @t
-
-class ShowableType t where
-	showCType :: Proxy t -> String
-
-instance ShowableType Bool where showCType _ = "bool"
-instance ShowableType Int8 where showCType _ = "int8_t"
-instance ShowableType Int16 where showCType _ = "int16_t"
-instance ShowableType Int32 where showCType _ = "int32_t"
-instance ShowableType Int64 where showCType _ = "int64_t"
-instance ShowableType Word8 where showCType _ = "uint8_t"
-instance ShowableType Word16 where showCType _ = "uint16_t"
-instance ShowableType Word32 where showCType _ = "uint32_t"
-instance ShowableType Word64 where showCType _ = "uint64_t"
-instance ShowableType Float where showCType _ = "float"
-instance ShowableType Double where showCType _ = "double"
 
 class FormatableType t f where
 	formatter :: Proxy t -> f -> String
diff --git a/src/Copilot/Arduino/Library/Serial/XBee.hs b/src/Copilot/Arduino/Library/Serial/XBee.hs
--- a/src/Copilot/Arduino/Library/Serial/XBee.hs
+++ b/src/Copilot/Arduino/Library/Serial/XBee.hs
@@ -11,13 +11,14 @@
 	device,
 	char,
 	str,
+	FlashString(..),
 	show,
 	showFormatted,
 	byte,
 	noInput,
 	SerialDevice,
 	FormatOutput,
-	ShowableType,
+	OutputString,
 	FormatableType,
 	Base(..),
 ) where
@@ -48,7 +49,7 @@
 -- that describes the serial output. Note that you can only do this once
 -- in a Sketch.
 --
--- > main = arduino $ doa
+-- > main = arduino $ do
 -- >    XBee.configure pin2 pin3 (XBee.Baud 9600)
 -- > 	b <- input pin4
 -- > 	XBee.device =:
diff --git a/src/Copilot/Arduino/Main.hs b/src/Copilot/Arduino/Main.hs
--- a/src/Copilot/Arduino/Main.hs
+++ b/src/Copilot/Arduino/Main.hs
@@ -5,10 +5,11 @@
 import Language.Copilot (Spec, interpret, reify)
 import Copilot.Compile.C99 (compile)
 import Copilot.Arduino.Internals
+import System.IO
 import System.Directory
 import System.Posix.Temp (mkdtemp)
 import System.FilePath
-import Control.Monad.Writer
+import System.Exit
 import Data.List (isInfixOf)
 import Data.Maybe
 import qualified Data.Map as M
@@ -41,23 +42,25 @@
 -- > (100)          (13,false)    
 -- > (100)          (13,true)     
 arduino :: Sketch () -> IO ()
-arduino (Sketch s) = go =<< execParser opts
+arduino s = go =<< execParser opts
   where
 	opts = info (parseCmdLine <**> helper)
 		( fullDesc
 		<> progDesc "Run this program with no options to generate an Arduino sketch."
 		)
 
-	go o = case interpretSteps o of
-		Just n -> interpret n spec
-		Nothing -> writeIno spec f
+	go o = case (mspec, interpretSteps o) of
+		(Nothing, _) -> do
+			hPutStrLn stderr "This Sketch does not do anything."
+			exitFailure
+		(Just spec, Just n) -> interpret n spec
+		(Just spec, Nothing) -> writeIno spec f'
 	
-	(is, fs) = unzip (execWriter s)
-	spec = sequence_ is
+	(mspec, f) = evalSketch s
 	
 	-- Strict evaluation because this may throw errors,
 	-- and we want to throw them before starting compilation.
-	!f = finalizeFramework (mconcat fs)
+	!f' = finalizeFramework f
 
 data CmdLine = CmdLine
 	{ interpretSteps :: Maybe Integer
@@ -111,7 +114,7 @@
 	,
 		[ "void setup()"
 		]
-	, codeblock $ map fromCLine (setups f)
+	, codeblock $ map fromCLine (earlySetups f <> setups f)
 	, [blank]
 	,
 		[ "void loop()"
diff --git a/src/Copilot/Arduino/Nano.hs b/src/Copilot/Arduino/Nano.hs
--- a/src/Copilot/Arduino/Nano.hs
+++ b/src/Copilot/Arduino/Nano.hs
@@ -25,6 +25,7 @@
 	, a5
 	, a6
 	, a7
+	, sizeOfEEPROM
 ) where
 
 import Copilot.Arduino
@@ -58,3 +59,8 @@
 a6, a7 :: Pin '[ 'AnalogInput ]
 a6 = Pin (PinId 20)
 a7 = Pin (PinId 21)
+
+-- | Different versions of Arduino Nano have different EEPROM sizes.
+-- This is a safe value that will work on all versions.
+sizeOfEEPROM :: Word16
+sizeOfEEPROM = 512
diff --git a/src/Copilot/Arduino/Uno.hs b/src/Copilot/Arduino/Uno.hs
--- a/src/Copilot/Arduino/Uno.hs
+++ b/src/Copilot/Arduino/Uno.hs
@@ -23,6 +23,7 @@
 	, a3
 	, a4
 	, a5
+	, sizeOfEEPROM
 ) where
 
 import Copilot.Arduino
@@ -51,3 +52,6 @@
 a3 = Pin (PinId 17)
 a4 = Pin (PinId 18)
 a5 = Pin (PinId 19)
+
+sizeOfEEPROM :: Word16
+sizeOfEEPROM = 512
