-
-
Notifications
You must be signed in to change notification settings - Fork 107
/
I2CManager.cpp
366 lines (329 loc) · 13.4 KB
/
I2CManager.cpp
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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
/*
* © 2023, Neil McKechnie
* © 2022 Paul M Antoine
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This 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 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdarg.h>
#include "I2CManager.h"
#include "DIAG.h"
// Include target-specific portions of I2CManager class
#if defined(I2C_USE_WIRE)
#include "I2CManager_Wire.h"
#elif defined(ARDUINO_ARCH_AVR)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_AVR.h" // Uno/Nano/Mega2560
#elif defined(ARDUINO_ARCH_MEGAAVR)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_Mega4809.h" // NanoEvery/UnoWifi
#elif defined(ARDUINO_ARCH_SAMD)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_SAMD.h" // SAMD21 for now... SAMD51 as well later
#elif defined(ARDUINO_ARCH_STM32)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_STM32.h" // STM32F411RE for now... more later
#else
#define I2C_USE_WIRE
#include "I2CManager_Wire.h" // Other platforms
#endif
// Helper function for listing device types
static const FSH * guessI2CDeviceType(uint8_t address) {
if (address >= 0x20 && address <= 0x26)
return F("GPIO Expander");
else if (address == 0x27)
return F("GPIO Expander or LCD Display");
else if (address == 0x29)
return F("Time-of-flight sensor");
else if (address >= 0x3c && address <= 0x3d)
return F("OLED Display");
else if (address >= 0x48 && address <= 0x4f)
return F("Analogue Inputs or PWM");
else if (address >= 0x40 && address <= 0x4f)
return F("PWM");
else if (address >= 0x50 && address <= 0x5f)
return F("EEPROM");
else if (address == 0x68)
return F("Real-time clock");
else if (address >= 0x70 && address <= 0x77)
return F("I2C Mux");
else
return F("?");
}
// If not already initialised, initialise I2C
void I2CManagerClass::begin(void) {
if (!_beginCompleted) {
_beginCompleted = true;
// Check for short-circuit or floating lines (no pull-up) on I2C before enabling I2C
const FSH *message = F("WARNING: Check I2C %S line for short/pullup");
pinMode(SDA, INPUT);
if (!digitalRead(SDA))
DIAG(message, F("SDA"));
pinMode(SCL, INPUT);
if (!digitalRead(SCL))
DIAG(message, F("SCL"));
// Now initialise I2C
_initialise();
#if defined(I2C_USE_WIRE)
DIAG(F("I2CManager: Using Wire library"));
#endif
// Probe and list devices. Use standard mode
// (clock speed 100kHz) for best device compatibility.
_setClock(100000);
unsigned long originalTimeout = _timeout;
setTimeout(1000); // use 1ms timeout for probes
#if defined(I2C_EXTENDED_ADDRESS)
// First count the multiplexers and switch off all subbuses
_muxCount = 0;
for (uint8_t muxNo=I2CMux_0; muxNo <= I2CMux_7; muxNo++) {
if (I2CManager.muxSelectSubBus({(I2CMux)muxNo, SubBus_None})==I2C_STATUS_OK)
_muxCount++;
}
#endif
// Enumerate devices that are visible
bool found = false;
for (uint8_t addr=0x08; addr<0x78; addr++) {
if (exists(addr)) {
found = true;
DIAG(F("I2C Device found at 0x%x, %S?"), addr, guessI2CDeviceType(addr));
}
}
#if defined(I2C_EXTENDED_ADDRESS)
// Enumerate all I2C devices that are connected via multiplexer,
// i.e. that respond when only one multiplexer has one subBus enabled
// and the device doesn't respond when the mux subBus is disabled.
// If any probes time out, then assume that the subbus is dead and
// don't do any more on that subbus.
for (uint8_t muxNo=I2CMux_0; muxNo <= I2CMux_7; muxNo++) {
uint8_t muxAddr = I2C_MUX_BASE_ADDRESS + muxNo;
if (exists(muxAddr)) {
// Select Mux Subbus
for (uint8_t subBus=0; subBus<=SubBus_No; subBus++) {
muxSelectSubBus({(I2CMux)muxNo, (I2CSubBus)subBus});
for (uint8_t addr=0x08; addr<0x78; addr++) {
uint8_t status = checkAddress(addr);
if (status == I2C_STATUS_OK) {
// De-select subbus
muxSelectSubBus({(I2CMux)muxNo, SubBus_None});
if (!exists(addr)) {
// Device responds when subbus selected but not when
// subbus disabled - ergo it must be on subbus!
found = true;
DIAG(F("I2C Device found at {I2CMux_%d,SubBus_%d,0x%x}, %S?"),
muxNo, subBus, addr, guessI2CDeviceType(addr));
}
// Re-select subbus
muxSelectSubBus({(I2CMux)muxNo, (I2CSubBus)subBus});
} else if (status == I2C_STATUS_TIMEOUT) {
// Bus stuck, skip to next one.
break;
}
}
}
// Deselect all subBuses for this mux. Otherwise its devices will continue to
// respond when other muxes are being probed.
I2CManager.muxSelectSubBus({(I2CMux)muxNo, SubBus_None}); // Deselect Mux
}
}
#endif
if (!found) DIAG(F("No I2C Devices found"));
_setClock(_clockSpeed);
setTimeout(originalTimeout); // set timeout back to original
}
}
// Set clock speed to the lowest requested one. If none requested,
// the Wire default is 100kHz.
void I2CManagerClass::setClock(uint32_t speed) {
if (speed < _clockSpeed && !_clockSpeedFixed) {
_clockSpeed = speed;
DIAG(F("I2C clock speed set to %l Hz"), _clockSpeed);
}
_setClock(_clockSpeed);
}
// Force clock speed to that specified.
void I2CManagerClass::forceClock(uint32_t speed) {
_clockSpeed = speed;
_clockSpeedFixed = true;
_setClock(_clockSpeed);
DIAG(F("I2C clock speed forced to %l Hz"), _clockSpeed);
}
// Check if specified I2C address is responding (blocking operation)
// Returns I2C_STATUS_OK (0) if OK, or error code.
// Suppress retries. If it doesn't respond first time it's out of the running.
uint8_t I2CManagerClass::checkAddress(I2CAddress address) {
I2CRB rb;
rb.setWriteParams(address, NULL, 0);
rb.suppressRetries(true);
queueRequest(&rb);
return rb.wait();
}
/***************************************************************************
* Write a transmission to I2C using a list of data (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(I2CAddress address, uint8_t nBytes, ...) {
uint8_t buffer[nBytes];
va_list args;
va_start(args, nBytes);
for (uint8_t i=0; i<nBytes; i++)
buffer[i] = va_arg(args, int);
va_end(args);
return write(address, buffer, nBytes);
}
/***************************************************************************
* Initiate a write to an I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(I2CAddress i2cAddress, const uint8_t writeBuffer[], uint8_t writeLen) {
I2CRB req;
uint8_t status = write(i2cAddress, writeBuffer, writeLen, &req);
return finishRB(&req, status);
}
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write_P(I2CAddress i2cAddress, const uint8_t * data, uint8_t dataLen) {
I2CRB req;
uint8_t status = write_P(i2cAddress, data, dataLen, &req);
return finishRB(&req, status);
}
/***************************************************************************
* Initiate a write (optional) followed by a read from the I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen)
{
I2CRB req;
uint8_t status = read(i2cAddress, readBuffer, readLen, writeBuffer, writeLen, &req);
return finishRB(&req, status);
}
/***************************************************************************
* Overload of read() to allow command to be specified as a series of bytes (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeSize, ...) {
va_list args;
// Copy the series of bytes into an array.
va_start(args, writeSize);
uint8_t writeBuffer[writeSize];
for (uint8_t i=0; i<writeSize; i++)
writeBuffer[i] = va_arg(args, int);
va_end(args);
return read(address, readBuffer, readSize, writeBuffer, writeSize);
}
/***************************************************************************
* Finish off request block by posting status, etc. (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::finishRB(I2CRB *rb, uint8_t status) {
if ((status == I2C_STATUS_OK) && rb)
status = rb->wait();
return status;
}
/***************************************************************************
* Get a message corresponding to the error status
***************************************************************************/
const FSH *I2CManagerClass::getErrorMessage(uint8_t status) {
switch (status) {
case I2C_STATUS_OK: return F("OK");
case I2C_STATUS_TRUNCATED: return F("Transmission truncated");
case I2C_STATUS_NEGATIVE_ACKNOWLEDGE: return F("No response from device (address NAK)");
case I2C_STATUS_TRANSMIT_ERROR: return F("Transmit error (data NAK)");
case I2C_STATUS_OTHER_TWI_ERROR: return F("Other Wire/TWI error");
case I2C_STATUS_TIMEOUT: return F("I2C bus timeout");
case I2C_STATUS_ARBITRATION_LOST: return F("Arbitration lost");
case I2C_STATUS_BUS_ERROR: return F("I2C bus error");
case I2C_STATUS_UNEXPECTED_ERROR: return F("Unexpected error");
case I2C_STATUS_PENDING: return F("Request pending");
default: return F("Error code not recognised");
}
}
/***************************************************************************
* Declare singleton class instance.
***************************************************************************/
I2CManagerClass I2CManager = I2CManagerClass();
// Buffer for conversion of I2CAddress to char*.
/* static */ char I2CAddress::addressBuffer[30];
/////////////////////////////////////////////////////////////////////////////
// Helper functions associated with I2C Request Block
/////////////////////////////////////////////////////////////////////////////
/***************************************************************************
* Block waiting for request to complete, and return completion status.
* Timeout monitoring is performed in the I2CManager.loop() function.
***************************************************************************/
uint8_t I2CRB::wait() {
while (status==I2C_STATUS_PENDING) {
I2CManager.loop();
};
return status;
}
/***************************************************************************
* Check whether request is still in progress.
* Timeout monitoring is performed in the I2CManager.loop() function.
***************************************************************************/
bool I2CRB::isBusy() {
if (status==I2C_STATUS_PENDING) {
I2CManager.loop();
return true;
} else
return false;
}
/***************************************************************************
* Helper functions to fill the I2CRequest structure with parameters.
***************************************************************************/
void I2CRB::setReadParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen) {
this->i2cAddress = i2cAddress;
this->writeLen = 0;
this->readBuffer = readBuffer;
this->readLen = readLen;
this->operation = OPERATION_READ;
this->status = I2C_STATUS_OK;
}
void I2CRB::setRequestParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen) {
this->i2cAddress = i2cAddress;
this->writeBuffer = writeBuffer;
this->writeLen = writeLen;
this->readBuffer = readBuffer;
this->readLen = readLen;
this->operation = OPERATION_REQUEST;
this->status = I2C_STATUS_OK;
}
void I2CRB::setWriteParams(I2CAddress i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen) {
this->i2cAddress = i2cAddress;
this->writeBuffer = writeBuffer;
this->writeLen = writeLen;
this->readLen = 0;
this->operation = OPERATION_SEND;
this->status = I2C_STATUS_OK;
}
void I2CRB::suppressRetries(bool suppress) {
if (suppress)
this->operation |= OPERATION_NORETRY;
else
this->operation &= ~OPERATION_NORETRY;
}
// Helper function for converting a uint8_t to four characters (e.g. 0x23).
void I2CAddress::toHex(const uint8_t value, char *buffer) {
char *ptr = buffer;
// Just display hex value, two digits.
*ptr++ = '0';
*ptr++ = 'x';
uint8_t bits = (value >> 4) & 0xf;
*ptr++ = bits > 9 ? bits-10+'a' : bits+'0';
bits = value & 0xf;
*ptr++ = bits > 9 ? bits-10+'a' : bits+'0';
}
#if !defined(I2C_EXTENDED_ADDRESS)
/* static */ bool I2CAddress::_addressWarningDone = false;
#endif