summaryrefslogtreecommitdiff
path: root/firmware/export/i2c-async.h
diff options
context:
space:
mode:
authorAidan MacDonald <amachronic@protonmail.com>2021-03-23 15:42:54 +0000
committerAidan MacDonald <amachronic@protonmail.com>2021-03-23 15:42:54 +0000
commit94b40ed314e980a7ecc3c3cada8d6f002cf85f58 (patch)
tree644ddc9801a3ba0c63b57cc76d71e05381997acf /firmware/export/i2c-async.h
parenteedc8934a978bbe05be748c7bc93a2aab5b47a5d (diff)
Add asynchronous I2C bus API
The driver core is based off of the i.MX233 I2C implementation and should work on any platform. Change-Id: I3b9c15e12a689ef02a51c285be08d29d35e323dc
Diffstat (limited to 'firmware/export/i2c-async.h')
-rw-r--r--firmware/export/i2c-async.h302
1 files changed, 302 insertions, 0 deletions
diff --git a/firmware/export/i2c-async.h b/firmware/export/i2c-async.h
new file mode 100644
index 0000000000..2877d1875c
--- /dev/null
+++ b/firmware/export/i2c-async.h
@@ -0,0 +1,302 @@
+/***************************************************************************
+ * __________ __ ___.
+ * Open \______ \ ____ ____ | | _\_ |__ _______ ___
+ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
+ * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
+ * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
+ * \/ \/ \/ \/ \/
+ * $Id$
+ *
+ * Copyright (C) 2021 Aidan MacDonald
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+
+#ifndef __I2C_ASYNC_H__
+#define __I2C_ASYNC_H__
+
+#include "config.h"
+
+/* i2c-async provides an API for asynchronous communication over an I2C bus.
+ * It's not linked to a specific target, so device drivers using this API can
+ * be shared more easily among multiple targets.
+ *
+ * Transactions are built using descriptors, and callbacks can be used to
+ * perform work directly from interrupt context. Callbacks can even change
+ * the descriptor chain on the fly, so the transaction can be altered based
+ * on data recieved over the I2C bus.
+ *
+ * There's an API for synchronous operations on devices using 8-bit register
+ * addresses. This API demonstrates how you can build more specialized routines
+ * on top of the asynchronous API, and is useful in its own right for dealing
+ * with simple devices.
+ */
+
+#ifdef HAVE_I2C_ASYNC
+
+#include "i2c-target.h"
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+/* Queueing codes */
+#define I2C_RC_OK 0
+#define I2C_RC_BUSY 1
+#define I2C_RC_NOTADDED 2
+#define I2C_RC_NOTFOUND 3
+
+/* Descriptor status codes */
+#define I2C_STATUS_OK 0
+#define I2C_STATUS_ERROR 1
+#define I2C_STATUS_TIMEOUT 2
+#define I2C_STATUS_SKIPPED 3
+
+/* I2C bus end conditions */
+#define I2C_START (1 << 0)
+#define I2C_RESTART (1 << 1)
+#define I2C_CONTINUE (1 << 2)
+#define I2C_STOP (1 << 3)
+#define I2C_HOLD (1 << 4)
+
+/* Transfer modes */
+#define I2C_READ 0
+#define I2C_WRITE 1
+
+/* Queue modes */
+#define I2C_Q_ADD 0
+#define I2C_Q_ONCE 1
+#define I2C_Q_REPLACE 2
+
+/* Flag for using 10-bit addresses */
+#define I2C_10BIT_ADDR 0x8000
+
+/* Descriptors are used to set up an I2C transfer. The transfer mode is
+ * specified by 'tran_mode', and is normally a READ or WRITE operation.
+ * The transfer mode dictates how the buffer/count fields are interpreted.
+ *
+ * - I2C_WRITE
+ * buffer[0] must be non-NULL and count[0] must be at least 1.
+ * The transfer sends count[0] bytes from buffer[0] on the bus.
+ *
+ * buffer[1] and count[1] can be used to specify a second buffer
+ * whose contents are written after the buffer[0]'s last byte.
+ * No start/stop conditions are issued between the buffers; it is
+ * as if you had appended the contents of buffer[1] to buffer[0].
+ *
+ * If not used, buffer[1] must be NULL and count[1] must be 0.
+ * If used, buffer[1] must be non-NULL and count[1] must be >= 1.
+ *
+ * - I2C_READ
+ * buffer[1] must be non-NULL and count[1] must be at least 1.
+ * The transfer will request count[1] bytes and writes the data
+ * into buffer[1].
+ *
+ * This type of transfer can send some data bytes before the
+ * read operation, eg. to send a register address for the read.
+ * If this feature is not used, set buffer[0] to NULL and set
+ * count[0] to zero.
+ *
+ * If used, buffer[0] must be non-NULL and count[0] must be >= 1.
+ * Between the last byte written and the first byte read, the bus
+ * will automatically issue a RESTART condition.
+ *
+ * Bus conditions are divided into two classes: start and end conditions.
+ * You MUST supply both a start and end condition in the 'bus_cond' field,
+ * by OR'ing together one start condition and one end condition:
+ *
+ * - I2C_START
+ * Issue a START condition before the first data byte. This must be
+ * specified on the first descriptor of a transaction.
+ *
+ * - I2C_RESTART
+ * Issue a RESTART condition before the first data byte. On the bus this
+ * is physically identical to a START condition, but drivers might need
+ * this to distinguish between these two cases.
+ *
+ * - I2C_CONTINUE
+ * Do not issue any condition before the first data byte. This is only
+ * valid if the descriptor continues a previous descriptor which ended
+ * with the HOLD condition; otherwise the results are undefined.
+ *
+ * - I2C_STOP
+ * Issue a STOP condition after the last data byte. This must be set on
+ * the final descriptor in a transaction, so the bus is left in a usable
+ * state when the descriptor finishes.
+ *
+ * - I2C_HOLD
+ * Do not issue any condition after the last data byte. This is only
+ * valid if the next descriptor starts with an I2C_CONTINUE condition.
+ */
+typedef struct i2c_descriptor {
+ /* Address of the target device. To use 10-bit addresses, simply
+ * OR the address with the I2C_10BIT_ADDR. */
+ uint16_t slave_addr;
+
+ /* What to do at the ends of the data transfer */
+ uint8_t bus_cond;
+
+ /* Transfer mode */
+ uint8_t tran_mode;
+
+ /* Buffer/length fields. Their use depends on the transfer mode. */
+ void* buffer[2];
+ int count[2];
+
+ /* Callback which is invoked when the descriptor completes.
+ *
+ * The first argument is a status code and the second argument is a
+ * pointer to the completed descriptor. The status code is the only
+ * way of checking whether the descriptor completed successfully,
+ * and it is NOT saved, so ensure you save it yourself if needed.
+ */
+ void(*callback)(int, struct i2c_descriptor*);
+
+ /* Argument field reserved for the user; not touched by the driver. */
+ intptr_t arg;
+
+ /* Pointer to the next descriptor. */
+ struct i2c_descriptor* next;
+} i2c_descriptor;
+
+/* Public API */
+
+/* Aysnchronously enqueue a descriptor, optionally waiting on a timeout
+ * if the queue is full. The exact behavior depends on 'q_mode':
+ *
+ * - I2C_Q_ADD
+ * Always try to enqueue the descriptor.
+ *
+ * - I2C_Q_ONCE
+ * Only attempt to enqueue the descriptor if no descriptor with the same
+ * cookie is already running or queued. If this is not the case, then
+ * returns I2C_RC_NOTADDED.
+ *
+ * - I2C_Q_REPLACE
+ * If a descriptor with the same cookie is queued, replace it with this
+ * descriptor and do not run the old descriptor's callbacks. If the
+ * matching descriptor is running, returns I2C_RC_NOTADDED and does not
+ * queue the new descriptor. If no match was found, then simply add the
+ * new descriptor to the queue.
+ *
+ * The 'cookie' is only useful if you want to use the ONCE or REPLACE queue
+ * modes, or if you want to use i2c_async_cancel(). Cookies used for queue
+ * management must be reserved with i2c_async_reserve_cookies(), to prevent
+ * different drivers from stepping on each other's toes.
+ *
+ * When you do not need queue management, you can use a 'cookie' of 0, which
+ * is reserved for unmanaged transactions. Only use I2C_Q_ADD if you do this.
+ *
+ * Queuing is only successful if I2C_RC_OK is returned. All other codes
+ * indicate that the descriptor was not queued, and therefore will not be
+ * executed.
+ *
+ * Be careful about how/when you modify and queue descriptors. It's unsafe to
+ * modify a queued descriptor: it could start running at any time, and the bus
+ * might see a half-rewritten version of the descriptor. You _can_ queue the
+ * same descriptor twice, since the i2c-async driver is not allowed to modify
+ * any fields, but your callbacks need to be written with this case in mind.
+ *
+ * You can use queue management to help efficiently re-use descriptors.
+ * Typically you can alternate between multiple descriptors, always keeping one
+ * free to modify, and using your completion callbacks to cycle the free slot.
+ * You can also probe with i2c_async_cancel() to ensure a specific descriptor
+ * is not running before modifying it.
+ */
+extern int i2c_async_queue(int bus, int timeout, int q_mode,
+ int cookie, i2c_descriptor* desc);
+
+/* Cancel a queued descriptor. Searches the queue, starting with the running
+ * descriptor, for a descriptor with a matching cookie, and attempts to remove
+ * it from the queue.
+ *
+ * - Returns I2C_RC_NOTFOUND if no match was found.
+ * - Returns I2C_RC_BUSY if the match is the currently running transaction.
+ * - Returns I2C_RC_OK if the match was found in the pending queue and was
+ * successfully removed from the queue.
+ */
+extern int i2c_async_cancel(int bus, int cookie);
+
+/* Reserve a range of cookie values for use by a driver. This should only be
+ * done once at startup. The driver doesn't care what cookies are used, so you
+ * can manage them any way you like.
+ *
+ * A range [r, r+count) will be allocated, disjoint from all other allocated
+ * ranges and with r >= 1. Returns 'r'.
+ */
+extern int i2c_async_reserve_cookies(int bus, int count);
+
+/* Synchronous API to read, write, and modify registers. The register address
+ * width is limited to 8 bits, although you can read and write multiple bytes.
+ *
+ * The modify operation can do a clear-and-set on a register with 8-bit values.
+ * It also returns the original value of the register before modification, if
+ * val != NULL.
+ */
+extern int i2c_reg_write(int bus, uint8_t addr, uint8_t reg,
+ int count, const uint8_t* buf);
+extern int i2c_reg_read(int bus, uint8_t addr, uint8_t reg,
+ int count, uint8_t* buf);
+extern int i2c_reg_modify1(int bus, uint8_t addr, uint8_t reg,
+ uint8_t clr, uint8_t set, uint8_t* val);
+
+/* Variant to write a single 8-bit value to a register */
+inline int i2c_reg_write1(int bus, uint8_t addr, uint8_t reg, uint8_t val)
+{
+ return i2c_reg_write(bus, addr, reg, 1, &val);
+}
+
+/* Variant to read an 8-bit value from a register; returns the value
+ * directly, or returns -1 on any error. */
+inline int i2c_reg_read1(int bus, uint8_t addr, uint8_t reg)
+{
+ uint8_t v;
+ int i = i2c_reg_read(bus, addr, reg, 1, &v);
+ if(i == I2C_STATUS_OK)
+ return v;
+ else
+ return -1;
+}
+
+/* Variant to set or clear one bit in an 8-bit register */
+inline int i2c_reg_setbit1(int bus, uint8_t addr, uint8_t reg,
+ int bit, int value, uint8_t* val)
+{
+ uint8_t clr = 0, set = 0;
+ if(value)
+ set = 1 << bit;
+ else
+ clr = 1 << bit;
+
+ return i2c_reg_modify1(bus, addr, reg, clr, set, val);
+}
+
+/* Internal API */
+
+/* Must be called by the target's i2c_init() before anyone uses I2C. */
+extern void __i2c_async_init(void);
+
+/* Called by the target's interrupt handlers to signal completion of the
+ * currently running descriptor. You must ensure IRQs are disabled before
+ * calling this function.
+ *
+ * If another descriptor is queued for submission, either as part of the
+ * same transaction or another one, then this may call __i2c_async_submit()
+ * to start the next descriptor.
+ */
+extern void __i2c_async_complete_callback(int bus, int status);
+
+/* Called by the i2c-async core to submit a descriptor to the hardware bus.
+ * This function is implemented by the target. Just start the transfer and
+ * unmask needed interrupts here, and try to return as quickly as possible.
+ */
+extern void __i2c_async_submit(int bus, i2c_descriptor* desc);
+
+#endif /* HAVE_I2C_ASYNC */
+#endif /* __I2C_ASYNC_H__ */