Addition of digitalIOPerformance to libs

This commit is contained in:
Josh Stewart 2013-09-16 17:38:22 +10:00
parent 804d471e91
commit 48a94abb7d
6 changed files with 8839 additions and 0 deletions

View File

@ -0,0 +1,193 @@
# digitalIOPerformance.h
digitalIOPerformance.h is a single-file Arduino library (header file)
that adds higher-performance digital I/O functions to Arduino
programs.
# Quick Start
* Copy the "digitalIOPerformance" directory to your [Arduino libraries folder](http://arduino.cc/en/Guide/Libraries).
* Add _#include "digitalIOPerformance.h"_ near the top of your sketch.
* Done! When you recompile, performance of digitalRead/digitalWrite &
pinMode should be substantially faster in most cases. However,
functionality should be otherwise identical to the original Arduino
functions.
* Your sketch's compiled size may also go down (depending on how much
digital I/O you do.)
## What Becomes Faster?
Any digital I/O when the pin number is known at compile time:
const int led_pin = 13;
digitalWrite(led_pin, HIGH); // <-- gets ~30x faster
#define RELAY_PIN 11
digitalWrite(RELAY_PIN, x>2); // <-- also ~30x faster
Not the case where the pin number isn't known at compile time:
int my_pin = 4; // <-- note this is a variable, no 'const' marker!
void loop() {
digitalWrite(my_pin, LOW); // <-- same speed as normal Arduino
}
## Option: More performance, smaller compiled size, on PWM Enabled pins
The Arduino's digital I/O functions deal with the
possibility that a pin is used for PWM output with
[analogWrite()](http://arduino.cc/en/Tutorial/PWM), and then the same
pin gets used again later for normal digital output with
digitalWrite(). Something like this:
analogWrite(MY_PIN, 120);
// ... do some stuff
digitalWrite(MY_PIN, LOW);
If you never mix analogWrite and digitalRead/Write on the same pin, you can
boost performance on PWM-enabled pins (making them equal with
non-PWM-enabled pins) by adding a second line above the first:
#define DIGITALIO_NO_MIX_ANALOGWRITE
#include "digitalIOPerformance.h
Digital read & write performance will be higher, and the compiled
sketch size will be smaller.
... if you still want to mix analogWrite() and
digitalRead/digitalWrite() on the same pin, but also use
DIGITALIO_NO_MIX_ANALOGWRITE, then you can use the noAnalogWrite()
function in between to turn off the PWM output:
analogWrite(MY_PIN, 120);
// ... do some stuff
noAnalogWrite(MY_PIN);
digitalWrite(MY_PIN, LOW);
## Option: Last shreds of performance
Under some circumstances the Arduino digital functions have to protect
against [interrupts](http://www.uchobby.com/index.php/2007/11/24/arduino-interrupts/) occuring while an I/O read or write is in progress. This can cause things to get out of sync if an I/O operation takes more than a single instruction to process ("interrupt unsafe".)
When using digitalIOPerformance, most writes only take one instruction anyway, so they are naturally "interrupt safe". However some cases have to be made "interrupt safe":
* Writing to certain pins on Arduino Mega models.
* Setting pinMode() to INPUT_PULLUP.
By default, "digitalIOPerformance" makes these cases interrupt safe,
in order to keep them as safe as the built-in Arduino
versions. However if your Arduino sketch doesn't use interrupts, or
you don't care about interrupt safety for digital I/O, then you can
add another line to get faster digital I/O on them:
#define DIGITALIO_NO_INTERRUPT_SAFETY
#include "digitalIOPerformance.h
Only do this one if you're very sure you don't need interrupt safe
digital reads & writes. If you're already using
DIGITALIO_NO_MIX_ANALOGWRITE then the only real improvement is on an
Arduino Mega, and even then only on some of the pins.
## Option: Disable automatic performance boost
If you don't want the library to automatically replace your
digitalRead/digitalWrite/pinMode function calls, you can do that as
well:
#define DIGITALIO_MANUAL
#include "digitalIOPerformance.h
You can still use the functions in the library if you call them by
their original names (given below.)
# Functions Defined
These functions are defined by the library:
## digitalWriteSafe / digitalReadSafe / pinModeSafe
These versions of digitalWrite/digitalRead & pinMode run faster
than the built-in Arduino versions, if the pin number is known at
compile time.
They are also just as safe as the built-in Arduino versions - they're
interrupt safe, and they disable any previous analogWrite() calls.
When you include the library, these functions automatically replace
the built-in digitalWrite & pinMode functions. If you don't want this
to happen, define DIGITALIO_MANUAL before including (as shown above.)
## digitalWriteFast / digitalReadFast / pinModeFast
These versions of digitalWrite/digitalRead & pinMode will usually
compile down to a single port register instruction (as fast as is
possible to be) if the pin number is known at compile time. If the pin
number is a variable then they fall through to the slower Arduino
version if the pin number is a variable.
You can have these functions automatically replace all Arduino
digitalWrite/digitalRead & pinMode functions if you include the
library thus:
#define DIGITALIO_NO_INTERRUPT_SAFETY
#define DIGITALIO_NO_MIX_ANALOGWRITE
#include "digitalIOPerformance.h
## noAnalogWrite
Using digitalWriteFast() will not automatically turn off a
previous analogWrite() to that port, unlike Arduino's digitalWrite().
If you are mixing analogWrite() and digitalWriteFast() on a port, call
this function after immediately before calling digitalWriteFast(), if
you had previously called analogWrite().
The "safe" methods already call noAnalogWrite() any time you access a
PWM-capable pin, unless you've defined DIGITALIO_NO_MIX_ANALOGWRITE.
# Status
New, untested, hacky, work in progress. :)
Please raise an issue if this doesn't work with your Arduino install,
or doesn't seem to inline properly (ie massively bloated code size
instead of shrinking code size!)
Minimal testing done with Windows Arduino 1.0.3 (gcc 4.3) and my
Ubuntu Arduino 1.0.3 (gcc 4.7.) Should work with any Arduino version
above 1.0 (I think.)
# Known Shortcomings
* No ARM support, does nothing at all on the Arduino Due.
# Internal Workings
digitalIOPerformance.h is code generated automatically from an
existing Arduino installation by the script generateDigitalIOHeader.py.
You shouldn't need to run the code generation script unless you have a
newer/different Arduino version than the one it was last run against.
However, having code generation means it should be simple to update
against future new boards like the Leonardo (assuming the file
formats don't change much.)
# Thanks
Big thanks to the authors of digitalWriteFast - Paul Stoffregen, Bill
Westfield, an John Raines. I wrote this instead of updating
digitalWriteFast.h to support the Leonardo (code generation was more
appealing than handwritten bit fiddles!)
Also thanks to Alastair D'Silva who told me a while ago about the
trick of preprocessing pins_arduino.h to extract Arduino pin
information, he uses this in the performance-oriented AVR library
[MHVLib](http://www.makehackvoid.com/project/MHVLib).

View File

@ -0,0 +1,70 @@
/* Arduino board:
* %(id)s
* %(name)s
* MCU: %(build.mcu)s
*/
#if %(ifdef_clause)s
#ifdef _DIGITALIO_MATCHED_BOARD
#error "This header's Arduino configuration heuristics have matched multiple boards. The header may be out of date."
#endif
#define _DIGITALIO_MATCHED_BOARD
__attribute__((always_inline))
static inline void pinModeFast(uint8_t pin, uint8_t mode) {
if(!__builtin_constant_p(pin)) {
pinMode(pin, mode);
}
%(pinmode_clause)s
}
__attribute__((always_inline))
static inline void digitalWriteFast(uint8_t pin, uint8_t value) {
if(!__builtin_constant_p(pin)) {
digitalWrite(pin, value);
}
%(digitalwrite_clause)s
}
__attribute__((always_inline))
static inline int digitalReadFast(uint8_t pin) {
if(!__builtin_constant_p(pin)) {
return digitalRead(pin);
}
%(digitalread_clause)s
return LOW;
}
__attribute__((always_inline))
static inline void noAnalogWrite(uint8_t pin) {
if(!__builtin_constant_p(pin)) {
return; // noAnalogWrite is taken care of by digitalWrite() for variables
}
%(timer_clause)s
}
__attribute__((always_inline))
static inline bool _isPWMPin(uint8_t pin) {
%(ispwm_clause)s
return false;
}
__attribute__((always_inline))
static inline bool _directionIsAtomic(uint8_t pin) {
%(direction_clause)s
return false;
}
__attribute__((always_inline))
static inline bool _outputIsAtomic(uint8_t pin) {
%(output_clause)s
return false;
}
__attribute__((always_inline))
static inline bool _inputIsAtomic(uint8_t pin) {
%(input_clause)s
return false;
}
#endif

View File

@ -0,0 +1,13 @@
#ifndef _DIGITALIO_MATCHED_BOARD
#error "This header's Arduino configuration heuristics couldn't match this board configuration. No fast I/O is available. The header may be out of date."
#endif
#undef _DIGITALIO_MATCHED_BOARD
#ifndef DIGITALIO_MANUAL
#define digitalWrite digitalWriteSafe
#define digitalRead digitalReadSafe
#define pinMode pinModeSafe
#endif
#endif
#endif

View File

@ -0,0 +1,116 @@
/*
*
* Header for high performance Arduino Digital I/O
*
* Automatically generated from the Arduino library setup (boards.txt & pins_arduino.h)
*
* See the accompanying file README.md for documentation.
*
* ****
*
* This header is a derived work of the Arduino microcontroller libraries, which are
* licensed under LGPL. Although as a header file it is not bound by the same usage
* clauses as the library itself (see "3. Object Code Incorporating Material from
* Library Header Files.)"
*
* Note that although the code generated functions below here look horrific,
* they're written to inline only very small subsets of themselves at compile
* time (they generate single port-register instructions when the parameters
* are constant.)
*
*
*/
#ifdef __AVR__
#ifndef _DIGITALIO_PERFORMANCE
#define _DIGITALIO_PERFORMANCE
#include "Arduino.h"
#include <util/atomic.h>
// Forward declarations for per-Arduino-board functions:
inline static void pinModeFast(uint8_t pin, uint8_t mode);
inline static void digitalWriteFast(uint8_t pin, uint8_t value);
inline static int digitalReadFast(uint8_t pin);
inline static void noAnalogWrite(uint8_t pin);
// These few per-board functions are designed for internal use, but
// you can call them yourself if you want.
inline static bool _isPWMPin(uint8_t pin);
inline static bool _directionIsAtomic(uint8_t pin);
inline static bool _outputIsAtomic(uint8_t pin);
inline static bool _inputIsAtomic(uint8_t pin);
#ifdef DIGITALIO_NO_INTERRUPT_SAFETY
#define DIGITALIO_NO_INTERRUPT_SAFETY 1
#else
#define DIGITALIO_NO_INTERRUPT_SAFETY 0
#endif
#ifdef DIGITALIO_NO_MIX_ANALOGWRITE
#define DIGITALIO_NO_MIX_ANALOGWRITE 1
#else
#define DIGITALIO_NO_MIX_ANALOGWRITE 0
#endif
// All the variables & conditionals in these functions should evaluate at
// compile time not run time...
__attribute__((always_inline))
static inline void pinModeSafe(uint8_t pin, uint8_t mode) {
if(!__builtin_constant_p(pin)) {
pinMode(pin, mode);
}
else {
if((mode == INPUT || mode == INPUT_PULLUP) && !DIGITALIO_NO_MIX_ANALOGWRITE)
noAnalogWrite(pin);
const bool write_is_atomic = DIGITALIO_NO_INTERRUPT_SAFETY
|| (__builtin_constant_p(mode)
&& mode == OUTPUT
&& _directionIsAtomic(pin));
if(write_is_atomic) {
pinModeFast(pin, mode);
}
else {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
pinModeFast(pin, mode);
}
}
}
}
__attribute__((always_inline))
static inline void digitalWriteSafe(uint8_t pin, uint8_t value) {
if(!__builtin_constant_p(pin)) {
digitalWrite(pin, value);
}
else {
if(!DIGITALIO_NO_MIX_ANALOGWRITE)
noAnalogWrite(pin);
if(DIGITALIO_NO_INTERRUPT_SAFETY || _outputIsAtomic(pin)) {
digitalWriteFast(pin, value);
}
else {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
digitalWriteFast(pin, value);
}
}
}
}
__attribute__((always_inline))
static inline int digitalReadSafe(uint8_t pin) {
if(!__builtin_constant_p(pin)) {
return digitalRead(pin);
}
else {
if(!DIGITALIO_NO_MIX_ANALOGWRITE)
noAnalogWrite(pin);
return digitalReadFast(pin);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,309 @@
#!/usr/bin/env python
"""
Code generation script to build digitalIOPerformance.h from the Arduino
boards.txt & pins_arduino.h files.
You shouldn't need to rerun this unless you have a new version of
Arduino installed, or you're using different boards to the default
installation.
See the README file for more details.
Copyright (c) 2013 Angus Gratton. Script licensed under the GNU Lesser
General Public License, version 2 or above.
"""
# TODO: command line options
ARDUINO_PATH="/Applications/Arduino.app/Contents/Resources/Java/"
import os, re, subprocess
def main():
boards = extract_boards()
for board in boards:
add_variant_macros(board)
find_unambiguous_macros(boards)
identifying_keys = find_unique_macro_keys(boards)
boards = merge_matching_boards(boards, identifying_keys)
for board in boards:
extract_portnames_pins(board)
with open("digitalIOPerformance.h", "w") as output:
generate_header_file(boards, identifying_keys, output)
def extract_boards():
""" Parse the Arduino boards file and return a list of all the boards,
with each board as a dictionary containing all their board-specific
key-value pairs
"""
with open(os.path.join(ARDUINO_PATH, "hardware/arduino/boards.txt")) as board_contents:
boards = dict()
RE_BOARDENTRY = re.compile(r"^([A-Za-z][^=]+)=(.+)$", re.MULTILINE)
for full_key,value in re.findall(RE_BOARDENTRY, board_contents.read()):
board = full_key.split(".")[0]
key = ".".join(full_key.split(".")[1:])
if not board in boards:
boards[board] = { "id" : board }
boards[board][key] = value
return list(boards.values())
def run_preprocessor(board, additional_args=[]):
"""
Run the C preprocessor over a particular board, and return
the contents of the processed file as a string
"""
source_path = board["pin_path"]
args = ["/Applications/Arduino.app/Contents/Resources/Java/hardware/tools/avr/bin/avr-gcc", "-DARDUINO_MAIN", "-E",
"-mmcu=%s" % board["build.mcu"],
"-DF_CPU=%s" % board["build.f_cpu"] ]
if "build.vid" in board:
args += [ "-DUSB_VID=%s" % board["build.vid" ] ]
if "build.pid" in board:
args += [ "-DUSB_PID=%s" % board["build.pid" ] ]
print args + additional_args + [ source_path ]
proc = subprocess.Popen(args + additional_args + [ source_path ],stdout=subprocess.PIPE)
proc.wait()
if proc.returncode != 0:
raise Error("Failed to run preprocessor")
return proc.stdout.read()
def add_variant_macros(board):
"""
Run the pin_arduinos.h header for this board through the preprocessor,
extract all defined macros and attach them to the board dict under key
'macros'
"""
print os.path.join(ARDUINO_PATH, "hardware/arduino/variants/%s/pins_arduino.h" % (board["build.variant"]))
board["pin_path"] = os.path.join(ARDUINO_PATH, "hardware/arduino/variants/%s/pins_arduino.h" % (board["build.variant"]))
macros = run_preprocessor(board, ["-dM"])
macros = [ re.sub(r"^#define ", "", macro) for macro in macros.split("\n") ]
macros = [ tuple(macro.split(" ", 1)) for macro in macros if " " in macro ]
board["macros"] = dict(macros)
def find_unambiguous_macros(boards):
"""
Trim the macros defined against any of the boards, to leave only those with
only numeric values (anything else is too tricky, especially with
token representation ambiguities.)
Modifies the board dict in-place.
"""
ambiguous_macros = set()
# build list of ambiguous macros
for board in boards:
for key,value in board["macros"].items():
if not re.match(r"^-?0?x?[\dA-Fa-f]+L?$", value.strip()):
ambiguous_macros.add(key)
# trim the ambiguous macros from any of the boards
for board in boards:
for ambiguous in ambiguous_macros:
if ambiguous in board["macros"]:
del board["macros"][ambiguous]
def find_unique_macro_keys(boards):
"""
Go through each board and find a small subset of unique macro
values that distinguish each board from the others.
This allows us to generate a header file that can automatically
determine which board profile it's being compiled against, at compile
time.
Returns a set of macro names.
"""
identifying_keys = set()
for board in boards:
duplicates = list( dup for dup in boards if dup != board )
for dup in duplicates:
# skip anything that's already unique as per existing keys
identified = False
for key in identifying_keys:
identified = identified or board["macros"].get(key,"") != dup["macros"].get(key, "")
if identified:
continue
# find something unique about this duplicate
uniques = set(board["macros"].items()) ^ set(dup["macros"].items())
uniques = [ key for key,value in uniques ]
# find the least selective key in the uniques
def selectiveness(key):
return len([d for d in duplicates if ( d["macros"].get(key, "") == board["macros"].get(key, ""))])
uniques = sorted(uniques, key=selectiveness)
if len(uniques) > 0:
identifying_keys.add(uniques[0])
return identifying_keys
def merge_matching_boards(boards, identifying_keys):
"""
Go through each board and merge together any that we can't unambiguously
identify by using the macros defined in identifying_keys.
We assume any matching boards will have matching 'variants' defined, otherwise
we throw an error.
Returns a new list of boards, with an ambiguously defined ones merged together.
"""
def key(board):
return str([ board["macros"].get(key,"") for key in identifying_keys ])
# Merge together any boards with identical keys (making composite names & ids)
unique_boards = []
for board in boards:
found = False
for unique in unique_boards:
if key(unique) == key(board):
if board["build.variant"] != unique["build.variant"]:
raise RuntimeError("Ambiguous boards %s / %s have matching variant! Can't distinguish reliably between them." % (board["id"], unique["id"]))
unique["id"] += " | " + board["id"]
unique["name"] += " | " + board["name"]
found = True
if not found:
unique_boards += [ board ]
return unique_boards
def extract_portnames_pins(board):
"""
Run the preprocessor over this boards' pin header file to pull
out port names and bitmasks.
"""
def extract_array(array_name):
""" Match a multiline C array with the given name (pretty clumsy.) """
content = re.search(array_name + ".+?\{(.+?)}", output, re.MULTILINE|re.DOTALL).group(1)
return [ e.strip() for e in content.split(",") if len(e.strip()) ]
output = run_preprocessor(board)
board["ports"] = extract_array("digital_pin_to_port_PGM")
# strip P prefix, ie PD becomes D
board["ports"] = [ e[1] if e[0] == "P" else None for e in board["ports"] ]
board["bitmasks"] = extract_array("digital_pin_to_bit_mask_PGM")
pin_to_timer = extract_array("digital_pin_to_timer_PGM")
board["timers"] = [ p if p != "NOT_ON_TIMER" else None for p in pin_to_timer ]
# do some sanity checks on the data we extracted
if len(board["ports"]) != len(board["bitmasks"]):
raise RuntimeError("Number of ports in %s doesn't match bitmask count - %d vs %d" % (board["id"], len(board["ports"]), len(board["bitmasks"])))
if len(board["ports"]) != int(board["macros"]["NUM_DIGITAL_PINS"]):
raise RuntimeError("Number of ports in %s doesn't match number of digital pins reported in header" % (board["id"], len(board["ports"]), board["macros"]["NUM_DIGITAL_PINS"]))
DIGITALWRITE_TEMPLATE = """
else if(pin == %(number)s && value) PORT%(port)s |= %(bitmask)s;
else if(pin == %(number)s && !value) PORT%(port)s &= ~%(bitmask)s;
""".lstrip("\n")
PINMODE_TEMPLATE = """
else if(pin == %(number)s && mode == INPUT) {
DDR%(port)s &= ~%(bitmask)s;
PORT%(port)s &= ~%(bitmask)s;
} else if(pin == %(number)s && mode == INPUT_PULLUP) {
DDR%(port)s &= ~%(bitmask)s;
PORT%(port)s |= %(bitmask)s;
} else if(pin == %(number)s) DDR%(port)s |= %(bitmask)s;
""".lstrip("\n")
DIGITALREAD_TEMPLATE = """
else if(pin == %(number)s) return PIN%(port)s & %(bitmask)s ? HIGH : LOW;
""".lstrip("\n")
TIMER_TEMPLATE = """
else if(pin == %(number)s) %(timer_reg)s &= ~_BV(%(timer_bit)s);
""".lstrip("\n")
ISPWM_TEMPLATE = """
if(pin == %(number)s)
return true;
""".lstrip("\n")
PORT_TEST_TEMPLATE = """
if(pin == %(number)s)
return _SFR_IO_REG_P(%(port)s);
""".lstrip("\n")
# Lookup table from the timer specifications given in pins_arduino.h
# to the actual timer register bits
#
# This is equivalent of the case statement in wiring_digital.c, so if that
# ever changes this needs to change too. :(
TIMER_LOOKUP = {
"TIMER1A": ("TCCR1A", "COM1A1"),
"TIMER1B": ("TCCR1A", "COM1B1"),
"TIMER2": ("TCCR2", "COM21"),
"TIMER0A": ("TCCR0A", "COM0A1"),
"TIMER0B": ("TCCR0A", "COM0B1"),
"TIMER2A": ("TCCR2A", "COM2A1"),
"TIMER2B": ("TCCR2A", "COM2B1"),
"TIMER3A": ("TCCR3A", "COM3A1"),
"TIMER3B": ("TCCR3A", "COM3B1"),
"TIMER3C": ("TCCR3A", "COM3C1"),
"TIMER4A": ("TCCR4A", "COM4A1"),
"TIMER4B": ("TCCR4A", "COM4B1"),
"TIMER4C": ("TCCR4A", "COM4C1"),
"TIMER4D": ("TCCR4C", "COM4D1"),
"TIMER5A": ("TCCR5A", "COM5A1"),
"TIMER5B": ("TCCR5A", "COM5B1"),
"TIMER5C": ("TCCR5A", "COM5C1"),
}
def generate_header_file(boards, identifying_keys, output):
"""
Write a header file with fast inlined methods for all the board types
"""
with open("components/board_template.cpp") as template:
BOARD_TEMPLATE = template.read()
with open("components/header.cpp") as header:
output.write(header.read())
for board in boards:
# Work out the macro conditional
ifdef_clause = []
for key in identifying_keys:
# all the +0 nonsense here is to detect defined-but-empty macros
# (Arduino-mk inserts them)
if key in board["macros"]:
ifdef_clause.append("defined(%(key)s) && (%(key)s+0) == %(value)s" %
{"key": key, "value": board["macros"][key]})
else:
ifdef_clause.append("(!defined(%s) || !(%s+0))" % (key,key))
ifdef_clause = " && ".join(ifdef_clause)
# Build up the macro conditionals
digitalwrite_clause = ""
digitalread_clause = ""
pinmode_clause = ""
timer_clause = ""
ispwm_clause = ""
direction_clause = ""
output_clause = ""
input_clause = ""
for number,port,bitmask,timer in zip(range(len(board["ports"])),
board["ports"],
board["bitmasks"],
board["timers"]):
if port is not None:
digitalwrite_clause += DIGITALWRITE_TEMPLATE % locals()
digitalread_clause += DIGITALREAD_TEMPLATE % locals()
pinmode_clause += PINMODE_TEMPLATE % locals()
if timer is not None:
timer_reg,timer_bit = TIMER_LOOKUP[timer]
timer_clause += TIMER_TEMPLATE % locals()
ispwm_clause += ISPWM_TEMPLATE % locals()
direction_clause += PORT_TEST_TEMPLATE % {"number":number,
"port":"DDR"+port}
output_clause += PORT_TEST_TEMPLATE % {"number":number,
"port":"PORT"+port}
input_clause += PORT_TEST_TEMPLATE % {"number":number,
"port":"PIN"+port}
output.write(BOARD_TEMPLATE % dict(locals(), **board))
with open("components/footer.cpp") as footer:
output.write(footer.read())
if __name__ == "__main__":
main()