-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathAdafruit_GP8403.cpp
More file actions
456 lines (405 loc) · 12.3 KB
/
Copy pathAdafruit_GP8403.cpp
File metadata and controls
456 lines (405 loc) · 12.3 KB
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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
// SPDX-FileCopyrightText: 2026 Limor Fried for Adafruit Industries
//
// SPDX-License-Identifier: MIT
/**
* @file Adafruit_GP8403.cpp
*
* @mainpage Adafruit GP8403 Arduino Library
*
* @section intro_sec Introduction
*
* Arduino driver for the GP8403 dual-channel 12-bit voltage-output DAC.
*
* Written by Limor "ladyada" Fried for Adafruit Industries.
*/
#include "Adafruit_GP8403.h"
#include <Adafruit_BusIO_Register.h>
/**
* @brief Construct a new GP8403 driver.
*/
Adafruit_GP8403::Adafruit_GP8403() {
_i2c_dev = nullptr;
_range = GP8403_RANGE_5V;
_raw[0] = 0;
_raw[1] = 0;
}
/**
* @brief Destroy the GP8403 driver and release its BusIO device.
*/
Adafruit_GP8403::~Adafruit_GP8403() {
delete _i2c_dev;
_i2c_dev = nullptr;
}
/**
* @brief Initialize the GP8403 in a known, safe state.
*
* The device is probed, placed in the 0 V to 5 V range, and both channels are
* set to zero. The GP8403 has no readable identification register.
*
* @param address 7-bit I2C address.
* @param wire I2C interface to use.
* @return true if the probe and all initialization writes succeeded.
*/
bool Adafruit_GP8403::begin(uint8_t address, TwoWire* wire) {
delete _i2c_dev;
_i2c_dev = new Adafruit_I2CDevice(address, wire);
if (_i2c_dev == nullptr || !_i2c_dev->begin()) {
return false;
}
return setOutputRange(GP8403_RANGE_5V);
}
/**
* @brief Select the output range after first setting both outputs to zero.
*
* @param range Desired output range.
* @return true if both the zero write and range write succeeded.
*/
bool Adafruit_GP8403::setOutputRange(gp8403_output_range_t range) {
if (!setRawValues(0, 0)) {
return false;
}
return writeOutputRange(range);
}
/**
* @brief Get the cached output range.
*
* @return Cached range; this is not hardware readback.
*/
gp8403_output_range_t Adafruit_GP8403::getOutputRange() {
return _range;
}
/**
* @brief Set one channel using a 12-bit raw DAC value.
*
* @param channel Output channel to update.
* @param value Raw value from 0 through 4095.
* @return true if the value was valid and the I2C write succeeded.
*/
bool Adafruit_GP8403::setRaw(uint8_t channel, uint16_t value) {
if (channel > 1 || value > GP8403_MAX_RAW_VALUE) {
return false;
}
return writeRaw(channel, value);
}
/**
* @brief Set both channels in one I2C transaction.
*
* @param channel0 Raw channel 0 value from 0 through 4095.
* @param channel1 Raw channel 1 value from 0 through 4095.
* @return true if both values were valid and the I2C write succeeded.
*/
bool Adafruit_GP8403::setRawValues(uint16_t channel0, uint16_t channel1) {
if (channel0 > GP8403_MAX_RAW_VALUE || channel1 > GP8403_MAX_RAW_VALUE) {
return false;
}
return writeRawValues(channel0, channel1);
}
/**
* @brief Get a channel's cached 12-bit DAC value.
*
* @param channel Output channel to query.
* @return Cached raw value, or zero for an invalid channel.
*/
uint16_t Adafruit_GP8403::getRaw(uint8_t channel) {
if (channel > 1) {
return 0;
}
return _raw[channel];
}
/**
* @brief Set one channel to a voltage in volts.
*
* @param channel Output channel to update.
* @param volts Requested voltage in volts.
* @return true if the request was valid and the I2C write succeeded.
*/
bool Adafruit_GP8403::setVoltage(uint8_t channel, float volts) {
uint16_t value;
if (channel > 1 || !voltageToRaw(volts, value)) {
return false;
}
return writeRaw(channel, value);
}
/**
* @brief Set both channels to voltages in one I2C transaction.
*
* @param channel0Volts Requested channel 0 voltage in volts.
* @param channel1Volts Requested channel 1 voltage in volts.
* @return true if both requests were valid and the I2C write succeeded.
*/
bool Adafruit_GP8403::setVoltages(float channel0Volts, float channel1Volts) {
uint16_t channel0;
uint16_t channel1;
if (!voltageToRaw(channel0Volts, channel0) ||
!voltageToRaw(channel1Volts, channel1)) {
return false;
}
return writeRawValues(channel0, channel1);
}
/**
* @brief Get a channel's cached, quantized voltage in volts.
*
* @param channel Output channel to query.
* @return Cached quantized voltage, or NAN for an invalid channel.
*/
float Adafruit_GP8403::getVoltage(uint8_t channel) {
if (channel > 1) {
return NAN;
}
return (float)_raw[channel] * fullScaleVoltage() /
(float)GP8403_MAX_RAW_VALUE;
}
/**
* @brief Write the output-range command and update the cached range.
*
* @param range Output range to write.
* @return true if the I2C write succeeded.
*/
bool Adafruit_GP8403::writeOutputRange(gp8403_output_range_t range) {
if (_i2c_dev == nullptr) {
return false;
}
uint8_t rangeData = GP8403_RANGE_DATA_5V;
if (range == GP8403_RANGE_10V) {
rangeData = GP8403_RANGE_DATA_10V;
}
Adafruit_BusIO_Register rangeRegister(_i2c_dev, GP8403_COMMAND_RANGE);
if (!rangeRegister.write(rangeData)) {
return false;
}
_range = range;
return true;
}
/**
* @brief Write one raw channel value and update its cached value.
*
* @param channel Output channel to write.
* @param value Valid 12-bit raw value.
* @return true if the I2C write succeeded.
*/
bool Adafruit_GP8403::writeRaw(uint8_t channel, uint16_t value) {
if (_i2c_dev == nullptr) {
return false;
}
uint16_t wireValue = value << 4;
uint8_t command = GP8403_COMMAND_CHANNEL_0;
if (channel == 1) {
command = GP8403_COMMAND_CHANNEL_1;
}
Adafruit_BusIO_Register channelRegister(_i2c_dev, command, 2, LSBFIRST);
if (!channelRegister.write(wireValue)) {
return false;
}
_raw[channel] = value;
return true;
}
/**
* @brief Write both raw channel values and update their cached values.
*
* @param channel0 Valid channel 0 raw value.
* @param channel1 Valid channel 1 raw value.
* @return true if the I2C write succeeded.
*/
bool Adafruit_GP8403::writeRawValues(uint16_t channel0, uint16_t channel1) {
if (_i2c_dev == nullptr) {
return false;
}
uint16_t wireChannel0 = channel0 << 4;
uint16_t wireChannel1 = channel1 << 4;
uint32_t wireValues = ((uint32_t)wireChannel1 << 16) | wireChannel0;
Adafruit_BusIO_Register channelRegister(_i2c_dev, GP8403_COMMAND_CHANNEL_0, 4,
LSBFIRST);
if (!channelRegister.write(wireValues)) {
return false;
}
_raw[0] = channel0;
_raw[1] = channel1;
return true;
}
/**
* @brief Convert a requested voltage to the nearest 12-bit DAC value.
*
* @param volts Requested voltage in volts.
* @param value Destination for the converted value.
* @return true if the voltage is finite and within the selected range.
*/
bool Adafruit_GP8403::voltageToRaw(float volts, uint16_t& value) {
float fullScale = fullScaleVoltage();
if (!isfinite(volts) || volts < 0.0 || volts > fullScale) {
return false;
}
value = (uint16_t)(volts * (float)GP8403_MAX_RAW_VALUE / fullScale + 0.5);
return true;
}
/**
* @brief Get the selected full-scale voltage.
*
* @return 5.0 or 10.0 volts according to the cached range.
*/
float Adafruit_GP8403::fullScaleVoltage() {
if (_range == GP8403_RANGE_10V) {
return 10.0;
}
return 5.0;
}
namespace {
constexpr uint8_t GP8403_NVM_HALF_PERIOD_US = 5;
class GP8403NVMWriter {
public:
GP8403NVMWriter(uint16_t sdaPin, uint16_t sclPin) {
_sdaPin = sdaPin;
_sclPin = sclPin;
}
void releaseBus() {
release(_sdaPin);
release(_sclPin);
}
bool start() {
releaseBus();
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
if (digitalRead(_sdaPin) != HIGH || digitalRead(_sclPin) != HIGH) {
return false;
}
driveLow(_sdaPin);
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
driveLow(_sclPin);
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
return true;
}
bool stop() {
driveLow(_sdaPin);
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
release(_sclPin);
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
bool clockHigh = digitalRead(_sclPin) == HIGH;
release(_sdaPin);
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
return clockHigh && digitalRead(_sdaPin) == HIGH;
}
bool writeBit(bool value) {
driveLow(_sclPin);
if (value) {
release(_sdaPin);
} else {
driveLow(_sdaPin);
}
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
release(_sclPin);
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
bool clockHigh = digitalRead(_sclPin) == HIGH;
bool dataHigh = !value || digitalRead(_sdaPin) == HIGH;
driveLow(_sclPin);
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
return clockHigh && dataHigh;
}
bool writeByte(uint8_t value) {
bool clocksHigh = true;
for (int8_t bit = 7; bit >= 0; bit--) {
clocksHigh = writeBit(value & (1U << bit)) && clocksHigh;
}
return clocksHigh;
}
bool writeAcknowledgedByte(uint8_t value) {
bool clocksHigh = writeByte(value);
driveLow(_sclPin);
release(_sdaPin);
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
release(_sclPin);
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
bool acknowledged =
digitalRead(_sclPin) == HIGH && digitalRead(_sdaPin) == LOW;
driveLow(_sclPin);
delayMicroseconds(GP8403_NVM_HALF_PERIOD_US);
return clocksHigh && acknowledged;
}
bool writePreamble() {
if (!start()) {
return false;
}
bool waveformOK = writeBit(false);
waveformOK = writeBit(true) && waveformOK;
waveformOK = writeBit(false) && waveformOK;
return stop() && waveformOK;
}
private:
static void driveLow(uint16_t pin) {
digitalWrite(pin, LOW);
pinMode(pin, OUTPUT);
}
static void release(uint16_t pin) {
pinMode(pin, INPUT);
}
uint16_t _sdaPin;
uint16_t _sclPin;
};
} // namespace
/**
* @brief Persist the current GP8403 output voltage data to NVM.
*
* The datasheet specifies a nonstandard bus waveform rather than an ordinary
* I2C register write. This implementation uses portable open-drain GPIO
* signaling on the supplied SDA and SCL pins. The caller must release the I2C
* peripheral before this call and restore its configuration afterward.
*
* A true return value confirms that the waveform was emitted and the documented
* entry and exit acknowledgments were observed. It cannot verify persistence
* without a power cycle.
*
* @param sdaPin SDA pin used by the configured Wire interface.
* @param sclPin SCL pin used by the configured Wire interface.
* @return true if the complete waveform and acknowledged bytes succeeded;
* false if uninitialized, unsupported, or not acknowledged.
*/
bool Adafruit_GP8403::saveToNVM(uint16_t sdaPin, uint16_t sclPin) {
if (_i2c_dev == nullptr || sdaPin == sclPin) {
return false;
}
GP8403NVMWriter writer(sdaPin, sclPin);
bool entryPreambleOK = writer.writePreamble();
bool entryStartOK = entryPreambleOK && writer.start();
bool entryCommandACK = false;
bool entryDataACK = false;
if (entryStartOK) {
entryCommandACK = writer.writeAcknowledgedByte(0x10);
entryDataACK = writer.writeAcknowledgedByte(0x03);
}
bool entryStopOK = entryStartOK && writer.stop();
bool entered = entryPreambleOK && entryStartOK && entryCommandACK &&
entryDataACK && entryStopOK;
bool dataWaveformOK = false;
if (entered) {
dataWaveformOK = writer.start();
if (dataWaveformOK) {
dataWaveformOK = writer.writeByte(0xB0) && dataWaveformOK;
dataWaveformOK = writer.writeBit(true) && dataWaveformOK;
for (uint8_t byte = 0; byte < 8; byte++) {
dataWaveformOK = writer.writeByte(0x00) && dataWaveformOK;
dataWaveformOK = writer.writeBit(true) && dataWaveformOK;
}
dataWaveformOK = writer.stop() && dataWaveformOK;
}
}
if (entered) {
delay(8);
}
// Attempt the complete exit pair even if entry acknowledgment failed, so a
// partially accepted entry command receives its documented cleanup sequence.
// Do not drive further if another device is holding the bus busy.
bool exitPreambleOK = entryStartOK && writer.writePreamble();
bool exitStartOK = false;
bool exitCommandACK = false;
bool exitDataACK = false;
bool exitStopOK = false;
if (exitPreambleOK) {
exitStartOK = writer.start();
if (exitStartOK) {
exitCommandACK = writer.writeAcknowledgedByte(0x10);
exitDataACK = writer.writeAcknowledgedByte(0x00);
exitStopOK = writer.stop();
}
}
bool exited = exitPreambleOK && exitStartOK && exitCommandACK &&
exitDataACK && exitStopOK;
writer.releaseBus();
return entered && dataWaveformOK && exited;
}