summaryrefslogtreecommitdiffstats
path: root/firmware/export/i2c-async.h
blob: f31a73452b8d9f2e9383674af02b4e6a930bac4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/***************************************************************************
 *             __________               __   ___.
 *   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 */
static 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. */
static 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 */
static 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__ */