Ok, the first version of the I2C_DMAC library is complete. Please find a copy of the library as a zip file below. I'll also post it on Github in due course.
The I2C_DMAC library allows non-blocking and/or blocking communication over the I2C bus. It's currently only designed for the Arduino Zero as the bus master.
It uses the I2C in Smart Mode in conjuction with the SAMD21's Direct Memory Access Controller (DMAC). Smart mode configures the I2C bus for completely autonomous operation, while the DMAC moves data from memory to the I2C bus and vice-versa, but without direct intervention from the CPU.
Tests show that even during blocking transfers, the I2C_DMAC library marginally out performs the standard Wire library. This is probably due to the fact that it's completely clocked using hardware.
Non-blocking operation allows the I2C data transfer to occur in parallel to the CPU. The CPU's sketch checks at a later point in the program to see if the transfer is complete. It's the parallel nature of the operation that can provide a significant performance enhancement over blocking code. On my Arduino Zero based Falcon 1 flight controller, using the I2C_DMAC library with non-blocking code provides at least a 25% increase in loop() time frequency performance over the standard Wire library.
Installation
Simply un-zip the file below and place the DMAC and I2C_DMAC directories in your ...Arduino/libraries folder. The Arduino folder is the one where your sketches are usually located.
Usage
Simply include the I2C_DMAC.h file the beginning of the sketch:
#include <I2C_DMAC.h>
The I2C_DMAC object is created (instantiated) automatically and the oject can be called using the I2C prefix, for example:
I2C.begin();
The I2C_DMAC library's functions operate in the following way:
The "init" functions simply set up the DMAC prior to transfer, while the "read" and "write" functions do the actual transmission.
All the other read and write functions are just a combination of the these three base level operations.
The write functions allow for the transmission of the device address, plus the following options:
Device Address->Data->Data Count (bytes)
Device Address->8-bit Register Address
Device Address->16-bit Register Address
Device Address->8-bit Register Address->1 Byte Data
Device Address->8-bit Register Address->Data->Data Count (bytes)
Device Address->16-bit Register Address->1 Byte Data
Device Address->16-bit Register Address->Data->Data Count (bytes)
The 8-bit register address is used to access most small I2C devices, such as sensors, while the 16-bit resgister address can be used to access I2C EEPROM devices.
The read functions allow for the transmission of the device address, plus the reception of the following options:
Device Address->1 Byte Data
Device Address->Data->Data Count (bytes)
Single bytes of data are handled by the library, meaning that you can simply enter constants as a single byte of data without having to allocate any memory. This is useful for configuring an I2C device.
A block of data can be a simple array and needs to be declared and "in scope" for the duration of the transfer. The block data size is limited to 255 bytes of data, (including the register address length). This limitation in imposed by the hardware.
Note that the I2C_DMAC doesn't use a ring buffer like the standard Wire library, it simply allows you to send and receive data from memory already allocated in your program. This also makes it more efficient as it isn't necessary to pull data off the ring buffer, the data is instead transfer directly to where you specify.
By default the DMAC uses channel 0 to write and 1 to read, but it's possible to select your DMAC channels of choice (0-11). It's also possible to set the priority level (0 lowest-3 highest). This is only necessary if you're using the DMAC channels for other purposes as well.
It's possible to initialise the DMAC only one time and then continuouly call the read() and write() functions in the loop() to initiate multiple transfers. In other words it isn't necessary to set-up the DMAC each time if you're doing a repeated operation.
To allow the sketch to check if the DMAC read or write operation is complete it's necessary to poll the respective busy flags:
while(I2C.busyWrite);
It's also possible to allocate callback functions that are executed when a read or write has completed, or when an error occurs.
The DMAC_Handler() and SERCOM3_Handler are provided as weak linker symbols allowing them to be overriden in your sketch for inclusion of your own handler functions, should that be necessary.
Here's a list of the available functions, it looks quite daunting, but trust me it's pretty easy to use and the differing (overloaded) functions are just there to handle various situations you may encounter, (see the examples included in the zip file):
//
// Configuration functions
//
void begin(); // Begin with 100kHz I2C bus clock speed and 8-bit register address mode
void begin(uint32_t baudrate); // Begin with specified baud rate and 8-bit register address mode
void begin(uint32_t baudrate, uint8_t regAddrMode); // Begin with specified baud rate and register address mode
void end(); // Tear down and tidy up resources
void setClock(uint32_t baudrate); // Set the I2C bus clock speed to the specified baud rate
void setWriteChannel(uint8_t channel); // Set the DMAC write channel number (0 - 11), default 0
void setReadChannel(uint8_t channel); // Set the DMAC read channel number (0 - 11), default 1
void setPriority(uint8_t priority); // Set the priority level for both read and write DMAC channels (0 - 3), default 0
void setRegAddrMode(uint8_t regAddrMode); // Set the register address mode REG_ADDR_8BIT or REG_ADDR_16BIT
//
// DMAC Configuration functions
//
void initWriteBytes(uint8_t devAddress, uint8_t* data, uint8_t count); // Initialise DMAC to send data, (no register address)
void initWriteBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count); // Initialise DMAC to send register address + data
void initWriteByte(uint8_t devAddress, uint16_t regAddress, uint8_t data); // Initialise DMAC to send register address + 1 data byte
void initWriteRegAddr(uint8_t devAddress, uint16_t regAddress); // Initialise DMAC to send register address, (no data)
void initReadBytes(uint8_t devAddress, uint8_t* data, uint8_t count); // Initialise DMAC to receive data
void initReadByte(uint8_t devAddress); // Initialise DMAC to receive 1 data byte
//
// Data transmission functions
//
uint8_t getData(); // Retrieve the received data byte
void write(); // Transmit on I2C bus
void read(); // Receive on I2C bus
void writeBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count); // Transmit data to the I2C device's register address
void writeByte(uint8_t devAddress, uint16_t regAddress, uint8_t data); // Transmit 1 data byte to the I2C device's register address
void writeRegAddr(uint8_t devAddress, uint16_t regAddress); // Write the register address to the I2C device
void readBytes(uint8_t devAddress, uint8_t* data, uint8_t count); // Receive data from the I2C device
void readByte(uint8_t devAddress); // Receive 1 data byte from the I2C device
void readBytes(uint8_t devAddress, uint16_t regAddress, uint8_t* data, uint8_t count); // Receive data from the I2C device's register address
void readByte(uint8_t devAddress, uint16_t regAddress); // Receive a byte from the I2C device's register address
//
// Callback functions
//
void attachWriteCallback(voidFuncPtr callback); // Attach a DMAC write callback function
void attachReadCallback(voidFuncPtr callback); // Attach a DMAC read callback function
void attachDmacErrorCallback(voidFuncPtr callback); // Attach a DMAC error callback function
void attachSercom3ErrorCallback(voidFuncPtr callback); // Attach a SERCOM3 error callback function
void detachWriteCallback(); // Detach the DMAC write callback function
void detachReadCallback(); // Detach the DMAC read callback function
void detachDmacErrorCallback(); // Detach the DAMC error callback function
void detachSercom3ErrorCallback(); // Detach the SERCOM3 error callback function
//
// Interrupt handler functions
//
static void DMAC_IrqHandler(); // DMAC interrupt handler function
static void SERCOM3_IrqHandler(); // SERCOM3 interrupt handler function
//
// Read and write busy flags
//
volatile boolean writeBusy; // Write busy flag - indicates write transfer in progress
volatile boolean readBusy; // Read busy flag - indicates read transfer in progress
I2C_DMAC Library.zip (13.2 KB)