From 94b40ed314e980a7ecc3c3cada8d6f002cf85f58 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 23 Mar 2021 15:42:54 +0000 Subject: 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 --- firmware/SOURCES | 4 + firmware/drivers/i2c-async.c | 398 +++++++++++++++++++++++++++++++++++++++++++ firmware/export/i2c-async.h | 302 ++++++++++++++++++++++++++++++++ 3 files changed, 704 insertions(+) create mode 100644 firmware/drivers/i2c-async.c create mode 100644 firmware/export/i2c-async.h (limited to 'firmware') diff --git a/firmware/SOURCES b/firmware/SOURCES index 430eb4119b..236933bdb7 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -1965,6 +1965,10 @@ target/hosted/sdl/filesystem-sdl.c drivers/touchpad.c #endif +#ifdef HAVE_I2C_ASYNC +drivers/i2c-async.c +#endif + /* firmware/kernel section */ #ifdef HAVE_CORELOCK_OBJECT kernel/corelock.c diff --git a/firmware/drivers/i2c-async.c b/firmware/drivers/i2c-async.c new file mode 100644 index 0000000000..f18bea565f --- /dev/null +++ b/firmware/drivers/i2c-async.c @@ -0,0 +1,398 @@ +/*************************************************************************** + * __________ __ ___. + * 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. + * + ****************************************************************************/ + +#include "i2c-async.h" +#include "config.h" +#include "system.h" +#include "kernel.h" + +/* To decide on the queue size, typically you should use the formula: + * + * queue size = max { D_1, ..., D_n } * max { T_1, ..., T_m } + * + * where + * + * n = number of busses + * m = number of devices across all busses + * D_i = number of devices on bus i + * T_j = number of queued transactions needed for device j + * + * The idea is to ensure nobody waits for a queue slot, but keep the queue + * size to a minimum so that queue management features don't take too long. + * (They have to iterate over the entire queue with IRQs disabled, so...) + * + * If you don't use the asychronous features then waiting for a queue slot + * is a moot point since you'll be blocking until completion anyway; in that + * case feel free to use single-entry queues. + * + * Note that each bus gets the same sized queue. If this isn't good for + * your target for whatever reason, it might be worth implementing per-bus + * queue sizes here. + */ +#if !defined(I2C_ASYNC_BUS_COUNT) || !defined(I2C_ASYNC_QUEUE_SIZE) +# error "Need to add #defines for i2c-async frontend" +#endif + +#if I2C_ASYNC_QUEUE_SIZE < 1 +# error "i2c-async queue size must be >= 1" +#endif + +/* Singly-linked list for queuing up transactions */ +typedef struct i2c_async_txn { + struct i2c_async_txn* next; + i2c_descriptor* desc; + int cookie; +} i2c_async_txn; + +typedef struct i2c_async_bus { + /* Head/tail of transaction queue. The head always points to the + * currently running transaction, or NULL if the bus is idle. + */ + i2c_async_txn* head; + i2c_async_txn* tail; + + /* Head of a free list used to allocate nodes. */ + i2c_async_txn* free; + + /* Next unallocated cookie value */ + int nextcookie; + + /* Semaphore for reserving a node in the free list. Nodes can only be + * allocated after a successful semaphore_wait(), and after being freed + * semaphore_release() must be called. + */ + struct semaphore sema; + +#ifdef HAVE_CORELOCK_OBJECT + /* Corelock is required for multi-core CPUs */ + struct corelock cl; +#endif +} i2c_async_bus; + +static i2c_async_txn i2c_async_txns[I2C_ASYNC_BUS_COUNT][I2C_ASYNC_QUEUE_SIZE]; +static i2c_async_bus i2c_async_busses[I2C_ASYNC_BUS_COUNT]; + +static i2c_async_txn* i2c_async_popfree(i2c_async_txn** head) +{ + i2c_async_txn* node = *head; + *head = node->next; + return node; +} + +static void i2c_async_pushfree(i2c_async_txn** head, i2c_async_txn* node) +{ + node->next = *head; + *head = node; +} + +static i2c_async_txn* i2c_async_pool_init(i2c_async_txn* array, int count) +{ + /* Populate forward pointers */ + for(int i = 0; i < count - 1; ++i) + array[i].next = &array[i+1]; + + /* Last pointer is NULL */ + array[count - 1].next = NULL; + + /* Return head of pool */ + return &array[0]; +} + +static void i2c_async_bus_init(int busnr) +{ + const int q_size = I2C_ASYNC_QUEUE_SIZE; + + i2c_async_bus* bus = &i2c_async_busses[busnr]; + bus->head = bus->tail = NULL; + bus->free = i2c_async_pool_init(i2c_async_txns[busnr], q_size); + bus->nextcookie = 1; + semaphore_init(&bus->sema, q_size, q_size); + corelock_init(&bus->cl); +} + +/* Add a node to the end of the transaction queue */ +static void i2c_async_bus_enqueue(i2c_async_bus* bus, i2c_async_txn* node) +{ + node->next = NULL; + if(bus->head == NULL) + bus->head = bus->tail = node; + else { + bus->tail->next = node; + bus->tail = node; + } +} + +/* Helper function called to run descriptor completion tasks */ +static void i2c_async_bus_complete_desc(i2c_async_bus* bus, int status) +{ + i2c_descriptor* d = bus->head->desc; + if(d->callback) + d->callback(status, d); + + bus->head->desc = d->next; +} + +void __i2c_async_complete_callback(int busnr, int status) +{ + i2c_async_bus* bus = &i2c_async_busses[busnr]; + corelock_lock(&bus->cl); + + i2c_async_bus_complete_desc(bus, status); + if(status != I2C_STATUS_OK) { + /* Skip remainder of transaction after an error */ + while(bus->head->desc) + i2c_async_bus_complete_desc(bus, I2C_STATUS_SKIPPED); + } + + /* Dequeue next transaction if we finished the current one */ + if(!bus->head->desc) { + i2c_async_txn* node = bus->head; + bus->head = node->next; + i2c_async_pushfree(&bus->free, node); + semaphore_release(&bus->sema); + } + + if(bus->head) { + /* Submit the next descriptor */ + __i2c_async_submit(busnr, bus->head->desc); + } else { + /* Fixup tail after last transaction */ + bus->tail = NULL; + } + + corelock_unlock(&bus->cl); +} + +void __i2c_async_init(void) +{ + for(int i = 0; i < I2C_ASYNC_BUS_COUNT; ++i) + i2c_async_bus_init(i); +} + +int i2c_async_queue(int busnr, int timeout, int q_mode, + int cookie, i2c_descriptor* desc) +{ + i2c_async_txn* node; + i2c_async_bus* bus = &i2c_async_busses[busnr]; + int rc = I2C_RC_OK; + + int irq = disable_irq_save(); + corelock_lock(&bus->cl); + + /* Scan the queue unless q_mode is a simple ADD */ + if(q_mode != I2C_Q_ADD) { + for(node = bus->head; node != NULL; node = node->next) { + if(node->cookie == cookie) { + if(q_mode == I2C_Q_REPLACE && node != bus->head) + node->desc = desc; + else + rc = I2C_RC_NOTADDED; + goto _exit; + } + } + } + + /* Try to claim a queue node without blocking if we can */ + if(semaphore_wait(&bus->sema, TIMEOUT_NOBLOCK) != OBJ_WAIT_SUCCEEDED) { + /* Bail out now if caller doesn't want to wait */ + if(timeout == TIMEOUT_NOBLOCK) { + rc = I2C_RC_BUSY; + goto _exit; + } + + /* Wait on the semaphore */ + corelock_unlock(&bus->cl); + restore_irq(irq); + if(semaphore_wait(&bus->sema, timeout) == OBJ_WAIT_TIMEDOUT) + return I2C_RC_BUSY; + + /* Got a node; re-lock */ + irq = disable_irq_save(); + corelock_lock(&bus->cl); + } + + /* Alloc the node and push it to queue */ + node = i2c_async_popfree(&bus->free); + node->desc = desc; + node->cookie = cookie; + i2c_async_bus_enqueue(bus, node); + + /* Start the first descriptor if the bus is idle */ + if(node == bus->head) + __i2c_async_submit(busnr, desc); + + _exit: + corelock_unlock(&bus->cl); + restore_irq(irq); + return rc; +} + +int i2c_async_cancel(int busnr, int cookie) +{ + i2c_async_bus* bus = &i2c_async_busses[busnr]; + int rc = I2C_RC_NOTFOUND; + + int irq = disable_irq_save(); + corelock_lock(&bus->cl); + + /* Bail if queue is empty */ + if(!bus->head) + goto _exit; + + /* Check the running transaction for a match */ + if(bus->head->cookie == cookie) { + rc = I2C_RC_BUSY; + goto _exit; + } + + /* Walk the queue, starting after the head */ + i2c_async_txn* prev = bus->head; + i2c_async_txn* node = prev->next; + while(node) { + if(node->cookie == cookie) { + prev->next = node->next; + rc = I2C_RC_OK; + goto _exit; + } + + prev = node; + node = node->next; + } + + _exit: + corelock_unlock(&bus->cl); + restore_irq(irq); + return rc; +} + +int i2c_async_reserve_cookies(int busnr, int count) +{ + i2c_async_bus* bus = &i2c_async_busses[busnr]; + int ret = bus->nextcookie; + bus->nextcookie += count; + return ret; +} + +static void i2c_sync_callback(int status, i2c_descriptor* d) +{ + struct semaphore* sem = (struct semaphore*)d->arg; + d->arg = status; + semaphore_release(sem); +} + +static void i2c_reg_modify1_callback(int status, i2c_descriptor* d) +{ + if(status == I2C_STATUS_OK) { + uint8_t* buf = (uint8_t*)d->buffer[1]; + uint8_t val = *buf; + *buf = (val & ~(d->arg >> 8)) | (d->arg & 0xff); + d->arg = val; + } +} + +int i2c_reg_write(int bus, uint8_t addr, uint8_t reg, + int count, const uint8_t* buf) +{ + struct semaphore sem; + semaphore_init(&sem, 1, 0); + + i2c_descriptor desc; + desc.slave_addr = addr; + desc.bus_cond = I2C_START | I2C_STOP; + desc.tran_mode = I2C_WRITE; + desc.buffer[0] = ® + desc.count[0] = 1; + desc.buffer[1] = (uint8_t*)buf; + desc.count[1] = count; + desc.callback = &i2c_sync_callback; + desc.arg = (intptr_t)&sem; + desc.next = NULL; + + i2c_async_queue(bus, TIMEOUT_BLOCK, I2C_Q_ADD, 0, &desc); + semaphore_wait(&sem, TIMEOUT_BLOCK); + + return desc.arg; +} + +int i2c_reg_read(int bus, uint8_t addr, uint8_t reg, + int count, uint8_t* buf) +{ + struct semaphore sem; + semaphore_init(&sem, 1, 0); + + i2c_descriptor desc; + desc.slave_addr = addr; + desc.bus_cond = I2C_START | I2C_STOP; + desc.tran_mode = I2C_READ; + desc.buffer[0] = ® + desc.count[0] = 1; + desc.buffer[1] = buf; + desc.count[1] = count; + desc.callback = &i2c_sync_callback; + desc.arg = (intptr_t)&sem; + desc.next = NULL; + + i2c_async_queue(bus, TIMEOUT_BLOCK, I2C_Q_ADD, 0, &desc); + semaphore_wait(&sem, TIMEOUT_BLOCK); + + return desc.arg; +} + +int i2c_reg_modify1(int bus, uint8_t addr, uint8_t reg, + uint8_t clr, uint8_t set, uint8_t* val) +{ + struct semaphore sem; + semaphore_init(&sem, 1, 0); + + uint8_t buf[2]; + buf[0] = reg; + + i2c_descriptor desc[2]; + desc[0].slave_addr = addr; + desc[0].bus_cond = I2C_START | I2C_STOP; + desc[0].tran_mode = I2C_READ; + desc[0].buffer[0] = &buf[0]; + desc[0].count[0] = 1; + desc[0].buffer[1] = &buf[1]; + desc[0].count[1] = 1; + desc[0].callback = &i2c_reg_modify1_callback; + desc[0].arg = (clr << 8) | set; + desc[0].next = &desc[1]; + + desc[1].slave_addr = addr; + desc[1].bus_cond = I2C_START | I2C_STOP; + desc[1].tran_mode = I2C_WRITE; + desc[1].buffer[0] = &buf[0]; + desc[1].count[0] = 2; + desc[1].buffer[1] = NULL; + desc[1].count[1] = 0; + desc[1].callback = &i2c_sync_callback; + desc[1].arg = (intptr_t)&sem; + desc[1].next = NULL; + + i2c_async_queue(bus, TIMEOUT_BLOCK, I2C_Q_ADD, 0, &desc[0]); + semaphore_wait(&sem, TIMEOUT_BLOCK); + + if(val && desc[1].arg == I2C_STATUS_OK) + *val = desc[0].arg; + + return desc[1].arg; +} 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 +#include +#include + +/* 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__ */ -- cgit v1.2.3