Unverified Commit 11814823 authored by Earle F. Philhower, III's avatar Earle F. Philhower, III Committed by GitHub

Add asynchronous I2C read and write operations (#2167)

Fixes #1730

Uses DMA operations to avoid the need to bit-bang or busy wait for I2C operations
that might be very slow.  Optional, adds new API calls to enable.  Simple example
included.
parent e6c7ee78
......@@ -24,3 +24,41 @@ Master transmissions are buffered (up to 256 bytes) and only performed
on ``endTransmission``, as is standard with modern Arduino Wire implementations.
For more detailed information, check the `Arduino Wire documentation <https://www.arduino.cc/en/reference/wire>`_ .
Asynchronous Operation
----------------------
Applications can use asynchronous I2C calls to allow for processing while long-running I2C operations are
being performed. For example, a game could send a full screen update out over I2C and immediately start
processing the next frame without waiting for the first one to be sent over I2C. DMA is used to handle
the transfer to/from the I2C hardware freeing the CPU from bit-banging or busy waiting.
Note that asynchronous operations can not be intersped with normal, synchronous ones. Fully complete an
asynchronous operation before attempting to do a normal ``Wire.beginTransaction()`` or ``Wire.requestFrom``.
Also, all buffers need to be valid throughout the entire operation. Read data cannot be accessed until
the transaction is completed and can't be "peeked" at while the operation is ongoing.
bool writeAsync(uint8_t address, const void \*buffer, size_t bytes, bool sendStop)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Begins an I2C asynchronous write transaction. Writes to ``address`` of ``bytes`` from ``buffer`` and
at the end will send an I2C stop if ``sendStop`` is ``true``.
Check ``finishedAsync()`` to determine when the operation completes and conclude the transaction.
This operation needs to allocate a buffer from heap equal to 2x ``bytes`` in size.
bool readAsync(uint8_t address, void \*buffer, size_t bytes, bool sendStop)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Begins an I2C asynchronous read transaction. Reads from ``address`` for ``bytes`` into ``buffer`` and
at the end will send an I2C stop if ``sendStop`` is ``true``.
Check ``finishedAsync()`` to determine when the operation completes and conclude the transaction.
This operation needs to allocate a buffer from heap equal to 4x ``bytes`` in size.
bool finishedAsync()
~~~~~~~~~~~~~~~~~~~~
Call to check if the asynchronous operations is completed and the buffer passed in can be either read or
reused. Frees the allocated memory and completes the asynchronous transaction.
void abortAsync()
~~~~~~~~~~~~~~~~~
Cancels the outstanding asynchronous transaction and frees any allocated memory.
// Simple asynchronous I2C master and slave demo - Earle F. Philhower, III
// Released into the public domain.
//
// Using both onboard I2C interfaces, have one master and one slave
// and send data both ways between them.
//
// Uses the async Wire calls to allow applications to do other work while
// I2C transactions are ongoing.
//
// To run, connect GPIO0 to GPIO2, GPIO1 to GPIO3 on a single Pico
#include <Wire.h>
void setup() {
Serial.begin(115200);
delay(5000);
Wire.setSDA(0);
Wire.setSCL(1);
Wire.begin();
Wire1.setSDA(2);
Wire1.setSCL(3);
Wire1.begin(0x30);
Wire1.onReceive(recv);
Wire1.onRequest(req);
}
static char buff[100];
void loop() {
static int p;
char b[90];
int loops;
// Write a value over I2C to the slave
Serial.println("\n\nSending...");
sprintf(b, "This buffer is larger than the I2C FIFO by a whole lot, Pass #%d", p++);
Wire.writeAsync(0x30, b, strlen(b), true);
// A real application would go do some useful work here and check the
// finishedAsync value when it needs the I2S operation to be completed
// Here' we'll just increment a counter to show how much work could be done...
loops = 0;
while (!Wire.finishedAsync()) {
loops++;
}
Serial.printf("Write idle loops: %d\n", loops);
// Ensure the slave processing is done and print it out
delay(1000);
Serial.printf("buff: '%s'\n", buff);
Serial.printf("Receiving...\n");
// Read from the slave and print out
bzero(b, sizeof(b));
Wire.readAsync(0x30, b, 73, true);
loops = 0;
while (!Wire.finishedAsync()) {
loops++;
}
Serial.printf("Read idle loops: %d\n", loops);
Serial.print("recv: '");
for (int i = 0; i < 73; i++) {
Serial.print(b[i]);
}
Serial.println("'");
delay(1000);
}
// These are called in an **INTERRUPT CONTEXT** which means NO serial port
// access (i.e. Serial.print is illegal) and no memory allocations, etc.
// Called when the I2C slave gets written to
void recv(int len) {
int i;
// Just stuff the sent bytes into a global the main routine can pick up and use
for (i = 0; i < len; i++) {
buff[i] = Wire1.read();
}
buff[i] = 0;
}
// Called when the I2C slave is read from
void req() {
static int ctr = 765;
char buff[100];
// Return a simple incrementing hex value
sprintf(buff, "Slave responds with a message that's longer than FIFO as well, id #%06X", (ctr++) % 65535);
Wire1.write(buff, 73);
}
......@@ -21,6 +21,10 @@ onReceive KEYWORD2
onRequest KEYWORD2
setSDA KEYWORD2
setSCL KEYWORD2
writeAsync KEYWORD2
readAsync KEYWORD2
finishedAsync KEYWORD2
abortAsync KEYWORD2
#######################################
# Instances (KEYWORD2)
......
......@@ -22,6 +22,7 @@
*/
#include <Arduino.h>
#include <hardware/dma.h>
#include <hardware/gpio.h>
#include <hardware/i2c.h>
#include <hardware/irq.h>
......@@ -441,6 +442,176 @@ void TwoWire::flush(void) {
}
// DMA/asynchronous transfers. Do not combime with synchronous runs or bad stuff will happen
// All buffers must be valid for entire DMA and not touched until `finished()` returns true.
bool TwoWire::writeAsync(uint8_t address, const void *buffer, size_t bytes, bool sendStop) {
if (!_running || _txBegun || _rxBegun) {
return false;
}
// We need to expand the data to include side-channel start/stop bits for the I2C FIFO
_dmaBuffer = (uint16_t*)malloc(bytes * 2);
if (!_dmaBuffer) {
return false;
}
const uint8_t *srcBuff = (const uint8_t *)buffer;
for (size_t i = 0; i < bytes; i++) {
bool first = i == 0;
bool last = i == bytes - 1;
_dmaBuffer[i] = bool_to_bit(first && _i2c->restart_on_next) << I2C_IC_DATA_CMD_RESTART_LSB | bool_to_bit(last && sendStop) << I2C_IC_DATA_CMD_STOP_LSB | srcBuff[i];
}
_channelDMA = dma_claim_unused_channel(false);
if (_channelDMA == -1) {
free(_dmaBuffer);
_dmaBuffer = nullptr;
return false;
}
dma_channel_config c = dma_channel_get_default_config(_channelDMA);
channel_config_set_transfer_data_size(&c, DMA_SIZE_16); // 16b transfers into I2C FIFO
channel_config_set_read_increment(&c, true); // Reading incrementing addresses
channel_config_set_write_increment(&c, false); // Writing to the same FIFO address
channel_config_set_dreq(&c, i2c_get_dreq(_i2c, true)); // Wait for the TX FIFO specified
channel_config_set_chain_to(&c, _channelDMA); // No chaining
channel_config_set_irq_quiet(&c, true); // No need for IRQ
dma_channel_configure(_channelDMA, &c, &_i2c->hw->data_cmd, _dmaBuffer, bytes, false);
_i2c->hw->enable = 0;
_i2c->hw->tar = address;
_i2c->hw->dma_cr = 1 << 1; // TDMAE
_i2c->hw->enable = 1;
_i2c->restart_on_next = !sendStop;
dma_channel_start(_channelDMA);
_txBegun = true;
return true;
}
bool TwoWire::readAsync(uint8_t address, void *buffer, size_t bytes, bool sendStop) {
if (!_running || _txBegun || _rxBegun) {
return false;
}
_channelDMA = dma_claim_unused_channel(false);
if (_channelDMA == -1) {
return false;
}
_channelSendDMA = dma_claim_unused_channel(false);
if (_channelSendDMA == -1) {
dma_channel_unclaim(_channelDMA);
return false;
}
// We need to expand the data to include side-channel start/stop bits for the I2C FIFO
_dmaBuffer = (uint16_t*)malloc(bytes * 2);
if (!_dmaBuffer) {
return false;
}
// We need to write one entry for every byte we want to read for the sideband info
_dmaSendBuffer = (uint16_t *)malloc(bytes * 2);
if (!_dmaSendBuffer) {
free(_dmaBuffer);
_dmaBuffer = nullptr;
return false;
}
for (size_t i = 0; i < bytes; i++) {
bool first = i == 0;
bool last = i == bytes - 1;
_dmaSendBuffer[i] =
bool_to_bit(first && _i2c->restart_on_next) << I2C_IC_DATA_CMD_RESTART_LSB |
bool_to_bit(last && sendStop) << I2C_IC_DATA_CMD_STOP_LSB |
I2C_IC_DATA_CMD_CMD_BITS; // -> 1 for read
}
_dmaBytes = bytes;
_rxFinalBuffer = (uint8_t *)buffer;
dma_channel_config c = dma_channel_get_default_config(_channelSendDMA);
channel_config_set_transfer_data_size(&c, DMA_SIZE_16); // 16b transfers into I2C FIFO
channel_config_set_read_increment(&c, true); // Reading incrementing addresses
channel_config_set_write_increment(&c, false); // Writing to the same FIFO address
channel_config_set_dreq(&c, i2c_get_dreq(_i2c, true)); // Wait for the TX FIFO specified
channel_config_set_chain_to(&c, _channelSendDMA); // No chaining
channel_config_set_irq_quiet(&c, true); // No need for IRQ
dma_channel_configure(_channelSendDMA, &c, &_i2c->hw->data_cmd, _dmaSendBuffer, bytes, false);
c = dma_channel_get_default_config(_channelDMA);
channel_config_set_transfer_data_size(&c, DMA_SIZE_16); // 16b transfers into I2C FIFO
channel_config_set_read_increment(&c, false); // Reading same FIFO address
channel_config_set_write_increment(&c, true); // Writing to the buffer
channel_config_set_dreq(&c, i2c_get_dreq(_i2c, false)); // Wait for the RX FIFO specified
channel_config_set_chain_to(&c, _channelDMA); // No chaining
channel_config_set_irq_quiet(&c, true); // No need for IRQ
dma_channel_configure(_channelDMA, &c, _dmaBuffer, &_i2c->hw->data_cmd, bytes, false);
_i2c->hw->enable = 0;
_i2c->hw->tar = address;
_i2c->hw->dma_cr = 1 | (1 << 1); // TDMAE | RDMAE
_i2c->hw->enable = 1;
_i2c->restart_on_next = !sendStop;
dma_channel_start(_channelDMA);
dma_channel_start(_channelSendDMA);
_rxBegun = true;
return true;
}
bool TwoWire::finishedAsync() {
if (!_running || !_dmaBuffer) {
return true;
}
if (dma_channel_is_busy(_channelDMA)) {
return false;
}
if (_txBegun) {
if (_i2c->hw->txflr) {
return false;
}
dma_channel_cleanup(_channelDMA);
dma_channel_unclaim(_channelDMA);
_i2c->hw->dma_cr = 0;
free(_dmaBuffer);
_dmaBuffer = nullptr;
_txBegun = false;
return true;
} else if (_rxBegun) {
// For RX, don't care if more data in FIFO. The DMA read is done for the size requested
dma_channel_cleanup(_channelDMA);
dma_channel_unclaim(_channelDMA);
dma_channel_cleanup(_channelSendDMA);
dma_channel_unclaim(_channelSendDMA);
_i2c->hw->dma_cr = 0;
// Need to convert from x16 to x8, strip off the status bits
for (auto i = 0; i < _dmaBytes; i++) {
_rxFinalBuffer[i] = _dmaBuffer[i] & 0xff;
}
free(_dmaBuffer);
_dmaBuffer = nullptr;
free(_dmaSendBuffer);
_dmaSendBuffer = nullptr;
_rxBegun = false;
return true;
}
return true;
}
void TwoWire::abortAsync() {
if (!_running || !_dmaBuffer) {
return;
}
dma_channel_cleanup(_channelDMA);
dma_channel_unclaim(_channelDMA);
if (_rxBegun) {
dma_channel_cleanup(_channelSendDMA);
dma_channel_unclaim(_channelSendDMA);
}
_i2c->hw->dma_cr = 0;
free(_dmaBuffer);
_dmaBuffer = nullptr;
free(_dmaSendBuffer);
_dmaSendBuffer = nullptr;
_rxBegun = false;
_txBegun = false;
}
void TwoWire::onReceive(void(*function)(int)) {
_onReceiveCallback = function;
}
......
......@@ -82,6 +82,13 @@ public:
}
using Print::write;
// DMA/asynchronous transfers. Do not combime with synchronous runs or bad stuff will happen
// All buffers must be valid for entire DMA and not touched until `finished()` returns true.
bool writeAsync(uint8_t address, const void *buffer, size_t bytes, bool sendStop);
bool readAsync(uint8_t address, void *buffer, size_t bytes, bool sendStop);
bool finishedAsync(); // Call to check if the async operations is completed and the buffer can be reused/read
void abortAsync(); // Cancel an outstanding async I2C operation
void setTimeout(uint32_t timeout = 25, bool reset_with_timeout = false); // sets the maximum number of milliseconds to wait
bool getTimeoutFlag(void);
void clearTimeoutFlag(void);
......@@ -116,6 +123,15 @@ private:
// TWI clock frequency
static const uint32_t TWI_CLOCK = 100000;
// DMA
bool _rxBegun = false;
int _channelDMA;
int _channelSendDMA;
uint16_t *_dmaBuffer = nullptr;
uint16_t *_dmaSendBuffer = nullptr;
int _dmaBytes;
uint8_t *_rxFinalBuffer;
};
extern TwoWire Wire;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment