blob: dbbfc56700903d8fd9f87b6c6da1706a5c13eeb9 [file] [log] [blame]
Brian Silvermanb5b46ca2016-03-13 01:14:17 -05001#include "frc971/wpilib/dma.h"
Brian Silverman2aa83d72015-01-24 18:03:11 -05002
Austin Schuh58d8cdf2015-02-15 21:04:42 -08003#include <string.h>
4
Brian Silverman2aa83d72015-01-24 18:03:11 -05005#include <algorithm>
Brian Silvermand49fd782015-01-30 16:43:17 -05006#include <type_traits>
7
8#include "DigitalSource.h"
9#include "AnalogInput.h"
10#include "Encoder.h"
Brian Silvermane48dbc12017-02-04 20:06:29 -080011#ifdef WPILIB2017
12#include "HAL/HAL.h"
13#endif
Brian Silvermand49fd782015-01-30 16:43:17 -050014
15// Interface to the roboRIO FPGA's DMA features.
Brian Silverman2aa83d72015-01-24 18:03:11 -050016
17// Like tEncoder::tOutput with the bitfields reversed.
18typedef union {
19 struct {
20 unsigned Direction: 1;
21 signed Value: 31;
22 };
23 struct {
24 unsigned value: 32;
25 };
26} t1Output;
27
Brian Silvermane48dbc12017-02-04 20:06:29 -080028static const int32_t kNumHeaders = 10;
Brian Silverman2aa83d72015-01-24 18:03:11 -050029
Austin Schuh91c75562015-12-20 22:23:10 -080030static constexpr ssize_t kChannelSize[20] = {2, 2, 4, 4, 2, 2, 4, 4, 3, 3,
31 2, 1, 4, 4, 4, 4, 4, 4, 4, 4};
Brian Silvermane48dbc12017-02-04 20:06:29 -080032
33#ifndef WPILIB2017
34#define HAL_GetErrorMessage getHALErrorMessage
Austin Schuh91c75562015-12-20 22:23:10 -080035#endif
Brian Silverman2aa83d72015-01-24 18:03:11 -050036
37enum DMAOffsetConstants {
38 kEnable_AI0_Low = 0,
39 kEnable_AI0_High = 1,
40 kEnable_AIAveraged0_Low = 2,
41 kEnable_AIAveraged0_High = 3,
42 kEnable_AI1_Low = 4,
43 kEnable_AI1_High = 5,
44 kEnable_AIAveraged1_Low = 6,
45 kEnable_AIAveraged1_High = 7,
46 kEnable_Accumulator0 = 8,
47 kEnable_Accumulator1 = 9,
48 kEnable_DI = 10,
49 kEnable_AnalogTriggers = 11,
50 kEnable_Counters_Low = 12,
51 kEnable_Counters_High = 13,
52 kEnable_CounterTimers_Low = 14,
53 kEnable_CounterTimers_High = 15,
Austin Schuh91c75562015-12-20 22:23:10 -080054 kEnable_Encoders_Low = 16,
55 kEnable_Encoders_High = 17,
56 kEnable_EncoderTimers_Low = 18,
57 kEnable_EncoderTimers_High = 19,
Brian Silverman2aa83d72015-01-24 18:03:11 -050058};
59
60DMA::DMA() {
61 tRioStatusCode status = 0;
62 tdma_config_ = tDMA::create(&status);
Austin Schuh58d8cdf2015-02-15 21:04:42 -080063 tdma_config_->writeConfig_ExternalClock(false, &status);
Brian Silvermane48dbc12017-02-04 20:06:29 -080064 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -050065 if (status != 0) {
66 return;
67 }
68 SetRate(1);
69 SetPause(false);
70}
71
72DMA::~DMA() {
73 tRioStatusCode status = 0;
74
75 manager_->stop(&status);
76 delete tdma_config_;
77}
78
79void DMA::SetPause(bool pause) {
80 tRioStatusCode status = 0;
81 tdma_config_->writeConfig_Pause(pause, &status);
Brian Silvermane48dbc12017-02-04 20:06:29 -080082 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -050083}
84
85void DMA::SetRate(uint32_t cycles) {
86 if (cycles < 1) {
87 cycles = 1;
88 }
89 tRioStatusCode status = 0;
90 tdma_config_->writeRate(cycles, &status);
Brian Silvermane48dbc12017-02-04 20:06:29 -080091 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -050092}
93
Austin Schuh58d8cdf2015-02-15 21:04:42 -080094void DMA::Add(Encoder *encoder) {
Brian Silverman2aa83d72015-01-24 18:03:11 -050095 tRioStatusCode status = 0;
96
97 if (manager_) {
98 wpi_setErrorWithContext(NiFpga_Status_InvalidParameter,
99 "DMA::Add() only works before DMA::Start()");
100 return;
101 }
Austin Schuh91c75562015-12-20 22:23:10 -0800102 const int index = encoder->GetFPGAIndex();
103
Austin Schuh91c75562015-12-20 22:23:10 -0800104 if (index < 4) {
105 // TODO(austin): Encoder uses a Counter for 1x or 2x; quad for 4x...
106 tdma_config_->writeConfig_Enable_Encoders_Low(true, &status);
107 } else if (index < 8) {
108 // TODO(austin): Encoder uses a Counter for 1x or 2x; quad for 4x...
109 tdma_config_->writeConfig_Enable_Encoders_High(true, &status);
110 } else {
111 wpi_setErrorWithContext(
112 NiFpga_Status_InvalidParameter,
113 "FPGA encoder index is not in the 4 that get logged.");
114 return;
115 }
Brian Silverman2aa83d72015-01-24 18:03:11 -0500116
Brian Silvermane48dbc12017-02-04 20:06:29 -0800117 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -0500118}
119
120void DMA::Add(DigitalSource * /*input*/) {
121 tRioStatusCode status = 0;
122
123 if (manager_) {
124 wpi_setErrorWithContext(NiFpga_Status_InvalidParameter,
125 "DMA::Add() only works before DMA::Start()");
126 return;
127 }
128
129 tdma_config_->writeConfig_Enable_DI(true, &status);
Brian Silvermane48dbc12017-02-04 20:06:29 -0800130 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -0500131}
132
Brian Silvermand49fd782015-01-30 16:43:17 -0500133void DMA::Add(AnalogInput *input) {
134 tRioStatusCode status = 0;
135
136 if (manager_) {
137 wpi_setErrorWithContext(NiFpga_Status_InvalidParameter,
138 "DMA::Add() only works before DMA::Start()");
139 return;
140 }
141
Brian Silvermand49fd782015-01-30 16:43:17 -0500142 if (input->GetChannel() <= 3) {
143 tdma_config_->writeConfig_Enable_AI0_Low(true, &status);
144 } else {
145 tdma_config_->writeConfig_Enable_AI0_High(true, &status);
146 }
Brian Silvermane48dbc12017-02-04 20:06:29 -0800147 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silvermand49fd782015-01-30 16:43:17 -0500148}
149
Brian Silverman2aa83d72015-01-24 18:03:11 -0500150void DMA::SetExternalTrigger(DigitalSource *input, bool rising, bool falling) {
151 tRioStatusCode status = 0;
152
153 if (manager_) {
154 wpi_setErrorWithContext(NiFpga_Status_InvalidParameter,
155 "DMA::SetExternalTrigger() only works before DMA::Start()");
156 return;
157 }
158
159 auto index =
160 ::std::find(trigger_channels_.begin(), trigger_channels_.end(), false);
161 if (index == trigger_channels_.end()) {
162 wpi_setErrorWithContext(NiFpga_Status_InvalidParameter,
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800163 "DMA: No channels left");
Brian Silverman2aa83d72015-01-24 18:03:11 -0500164 return;
165 }
166 *index = true;
167
Austin Schuhb19fddb2015-11-22 22:25:29 -0800168 const int channel_index = ::std::distance(trigger_channels_.begin(), index);
Brian Silverman2aa83d72015-01-24 18:03:11 -0500169
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800170 const bool is_external_clock =
171 tdma_config_->readConfig_ExternalClock(&status);
172 if (status == 0) {
173 if (!is_external_clock) {
174 tdma_config_->writeConfig_ExternalClock(true, &status);
Brian Silvermane48dbc12017-02-04 20:06:29 -0800175 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800176 if (status != 0) {
177 return;
178 }
179 }
180 } else {
Brian Silvermane48dbc12017-02-04 20:06:29 -0800181 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -0500182 return;
183 }
184
Austin Schuh3b5b69b2015-10-31 18:55:47 -0700185 nFPGA::nRoboRIO_FPGANamespace::tDMA::tExternalTriggers new_trigger;
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800186
187 new_trigger.FallingEdge = falling;
188 new_trigger.RisingEdge = rising;
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800189 new_trigger.ExternalClockSource_AnalogTrigger = false;
Austin Schuh91c75562015-12-20 22:23:10 -0800190 unsigned char module = 0;
Brian Silvermane48dbc12017-02-04 20:06:29 -0800191 uint32_t channel =
192#ifdef WPILIB2017
193 input->GetChannel();
194#else
195 input->GetChannelForRouting();
196#endif
Austin Schuh91c75562015-12-20 22:23:10 -0800197 if (channel >= kNumHeaders) {
198 module = 1;
199 channel -= kNumHeaders;
200 } else {
201 module = 0;
202 }
203
204 new_trigger.ExternalClockSource_Module = module;
205 new_trigger.ExternalClockSource_Channel = channel;
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800206
Austin Schuhb19fddb2015-11-22 22:25:29 -0800207// Configures the trigger to be external, not off the FPGA clock.
Austin Schuh91c75562015-12-20 22:23:10 -0800208 tdma_config_->writeExternalTriggers(channel_index / 4, channel_index % 4,
209 new_trigger, &status);
210 if (status != 0) {
Brian Silvermane48dbc12017-02-04 20:06:29 -0800211 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Austin Schuh91c75562015-12-20 22:23:10 -0800212 return;
213 }
Brian Silverman2aa83d72015-01-24 18:03:11 -0500214}
215
216DMA::ReadStatus DMA::Read(DMASample *sample, uint32_t timeout_ms,
Brian Silvermand49fd782015-01-30 16:43:17 -0500217 size_t *remaining_out) {
Brian Silverman2aa83d72015-01-24 18:03:11 -0500218 tRioStatusCode status = 0;
219 size_t remainingBytes = 0;
Brian Silvermand49fd782015-01-30 16:43:17 -0500220 *remaining_out = 0;
Brian Silverman2aa83d72015-01-24 18:03:11 -0500221
222 if (!manager_.get()) {
223 wpi_setErrorWithContext(NiFpga_Status_InvalidParameter,
224 "DMA::Read() only works after DMA::Start()");
225 return STATUS_ERROR;
226 }
227
Brian Silvermand49fd782015-01-30 16:43:17 -0500228 sample->dma_ = this;
Brian Silverman2aa83d72015-01-24 18:03:11 -0500229 manager_->read(sample->read_buffer_, capture_size_, timeout_ms,
230 &remainingBytes, &status);
231
Brian Silverman2aa83d72015-01-24 18:03:11 -0500232 // TODO(jerry): Do this only if status == 0?
Brian Silvermand49fd782015-01-30 16:43:17 -0500233 *remaining_out = remainingBytes / capture_size_;
Brian Silverman2aa83d72015-01-24 18:03:11 -0500234
Brian Silverman2aa83d72015-01-24 18:03:11 -0500235 // TODO(austin): Check that *remainingBytes % capture_size_ == 0 and deal
236 // with it if it isn't. Probably meant that we overflowed?
237 if (status == 0) {
238 return STATUS_OK;
239 } else if (status == NiFpga_Status_FifoTimeout) {
240 return STATUS_TIMEOUT;
241 } else {
Brian Silvermane48dbc12017-02-04 20:06:29 -0800242 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -0500243 return STATUS_ERROR;
244 }
245}
246
Brian Silvermand49fd782015-01-30 16:43:17 -0500247const char *DMA::NameOfReadStatus(ReadStatus s) {
248 switch (s) {
249 case STATUS_OK: return "OK";
250 case STATUS_TIMEOUT: return "TIMEOUT";
251 case STATUS_ERROR: return "ERROR";
252 default: return "(bad ReadStatus code)";
253 }
254}
255
Brian Silverman2aa83d72015-01-24 18:03:11 -0500256void DMA::Start(size_t queue_depth) {
257 tRioStatusCode status = 0;
258 tconfig_ = tdma_config_->readConfig(&status);
Brian Silvermane48dbc12017-02-04 20:06:29 -0800259 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -0500260 if (status != 0) {
261 return;
262 }
263
264 {
265 size_t accum_size = 0;
266#define SET_SIZE(bit) \
267 if (tconfig_.bit) { \
268 channel_offsets_[k##bit] = accum_size; \
269 accum_size += kChannelSize[k##bit]; \
270 } else { \
271 channel_offsets_[k##bit] = -1; \
272 }
273
274 SET_SIZE(Enable_AI0_Low);
275 SET_SIZE(Enable_AI0_High);
276 SET_SIZE(Enable_AIAveraged0_Low);
277 SET_SIZE(Enable_AIAveraged0_High);
278 SET_SIZE(Enable_AI1_Low);
279 SET_SIZE(Enable_AI1_High);
280 SET_SIZE(Enable_AIAveraged1_Low);
281 SET_SIZE(Enable_AIAveraged1_High);
282 SET_SIZE(Enable_Accumulator0);
283 SET_SIZE(Enable_Accumulator1);
284 SET_SIZE(Enable_DI);
285 SET_SIZE(Enable_AnalogTriggers);
286 SET_SIZE(Enable_Counters_Low);
287 SET_SIZE(Enable_Counters_High);
288 SET_SIZE(Enable_CounterTimers_Low);
289 SET_SIZE(Enable_CounterTimers_High);
Austin Schuh91c75562015-12-20 22:23:10 -0800290 SET_SIZE(Enable_Encoders_Low);
291 SET_SIZE(Enable_Encoders_High);
292 SET_SIZE(Enable_EncoderTimers_Low);
293 SET_SIZE(Enable_EncoderTimers_High);
Brian Silverman2aa83d72015-01-24 18:03:11 -0500294#undef SET_SIZE
295 capture_size_ = accum_size + 1;
296 }
297
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800298 manager_.reset(
299 new nFPGA::tDMAManager(0, queue_depth * capture_size_, &status));
300
Brian Silvermane48dbc12017-02-04 20:06:29 -0800301 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -0500302 if (status != 0) {
303 return;
304 }
305 // Start, stop, start to clear the buffer.
306 manager_->start(&status);
Brian Silvermane48dbc12017-02-04 20:06:29 -0800307 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -0500308 if (status != 0) {
309 return;
310 }
311 manager_->stop(&status);
Brian Silvermane48dbc12017-02-04 20:06:29 -0800312 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -0500313 if (status != 0) {
314 return;
315 }
316 manager_->start(&status);
Brian Silvermane48dbc12017-02-04 20:06:29 -0800317 wpi_setErrorWithContext(status, HAL_GetErrorMessage(status));
Brian Silverman2aa83d72015-01-24 18:03:11 -0500318 if (status != 0) {
319 return;
320 }
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800321
Brian Silverman2aa83d72015-01-24 18:03:11 -0500322}
323
Brian Silvermand49fd782015-01-30 16:43:17 -0500324static_assert(::std::is_pod<DMASample>::value, "DMASample needs to be POD");
325
Brian Silverman2aa83d72015-01-24 18:03:11 -0500326ssize_t DMASample::offset(int index) const { return dma_->channel_offsets_[index]; }
327
Austin Schuh8f314a92015-11-22 21:35:40 -0800328uint32_t DMASample::GetTime() const {
329 return read_buffer_[dma_->capture_size_ - 1];
330}
331
Brian Silverman2aa83d72015-01-24 18:03:11 -0500332double DMASample::GetTimestamp() const {
Austin Schuh8f314a92015-11-22 21:35:40 -0800333 return static_cast<double>(GetTime()) * 0.000001;
Brian Silverman2aa83d72015-01-24 18:03:11 -0500334}
335
336bool DMASample::Get(DigitalSource *input) const {
337 if (offset(kEnable_DI) == -1) {
Austin Schuh91c75562015-12-20 22:23:10 -0800338 wpi_setStaticErrorWithContext(
339 dma_, NiFpga_Status_ResourceNotFound,
Brian Silvermane48dbc12017-02-04 20:06:29 -0800340 HAL_GetErrorMessage(NiFpga_Status_ResourceNotFound));
Brian Silverman2aa83d72015-01-24 18:03:11 -0500341 return false;
342 }
Brian Silvermane48dbc12017-02-04 20:06:29 -0800343 const uint32_t channel =
344#ifdef WPILIB2017
345 input->GetChannel();
346#else
347 input->GetChannelForRouting();
348#endif
349 if (channel < kNumHeaders) {
350 return (read_buffer_[offset(kEnable_DI)] >> channel) & 0x1;
Brian Silverman2aa83d72015-01-24 18:03:11 -0500351 } else {
Brian Silvermane48dbc12017-02-04 20:06:29 -0800352 return (read_buffer_[offset(kEnable_DI)] >> (channel + 6)) & 0x1;
Brian Silverman2aa83d72015-01-24 18:03:11 -0500353 }
354}
355
356int32_t DMASample::GetRaw(Encoder *input) const {
Austin Schuh91c75562015-12-20 22:23:10 -0800357 int index = input->GetFPGAIndex();
358 uint32_t dmaWord = 0;
Austin Schuh91c75562015-12-20 22:23:10 -0800359 if (index < 4) {
360 if (offset(kEnable_Encoders_Low) == -1) {
361 wpi_setStaticErrorWithContext(
362 dma_, NiFpga_Status_ResourceNotFound,
Brian Silvermane48dbc12017-02-04 20:06:29 -0800363 HAL_GetErrorMessage(NiFpga_Status_ResourceNotFound));
Austin Schuh91c75562015-12-20 22:23:10 -0800364 return -1;
365 }
366 dmaWord = read_buffer_[offset(kEnable_Encoders_Low) + index];
367 } else if (index < 8) {
368 if (offset(kEnable_Encoders_High) == -1) {
369 wpi_setStaticErrorWithContext(
370 dma_, NiFpga_Status_ResourceNotFound,
Brian Silvermane48dbc12017-02-04 20:06:29 -0800371 HAL_GetErrorMessage(NiFpga_Status_ResourceNotFound));
Austin Schuh91c75562015-12-20 22:23:10 -0800372 return -1;
373 }
374 dmaWord = read_buffer_[offset(kEnable_Encoders_High) + (index - 4)];
375 } else {
376 wpi_setStaticErrorWithContext(
377 dma_, NiFpga_Status_ResourceNotFound,
Brian Silvermane48dbc12017-02-04 20:06:29 -0800378 HAL_GetErrorMessage(NiFpga_Status_ResourceNotFound));
Austin Schuh91c75562015-12-20 22:23:10 -0800379 return 0;
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800380 }
381
Brian Silverman2aa83d72015-01-24 18:03:11 -0500382 int32_t result = 0;
383
Austin Schuhb19fddb2015-11-22 22:25:29 -0800384 // Extract the 31-bit signed tEncoder::tOutput Value using a struct with the
385 // reverse packed field order of tOutput. This gets Value from the high
386 // order 31 bits of output on little-endian ARM using gcc. This works
387 // even though C/C++ doesn't guarantee bitfield order.
388 t1Output output;
Brian Silverman2aa83d72015-01-24 18:03:11 -0500389
Austin Schuhb19fddb2015-11-22 22:25:29 -0800390 output.value = dmaWord;
391 result = output.Value;
Brian Silverman2aa83d72015-01-24 18:03:11 -0500392
393 return result;
394}
395
396int32_t DMASample::Get(Encoder *input) const {
397 int32_t raw = GetRaw(input);
398
Brian Silvermand49fd782015-01-30 16:43:17 -0500399 return raw / input->GetEncodingScale();
400}
401
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800402uint16_t DMASample::GetValue(AnalogInput *input) const {
Austin Schuh91c75562015-12-20 22:23:10 -0800403 uint32_t channel = input->GetChannel();
404 uint32_t dmaWord;
405 if (channel < 4) {
406 if (offset(kEnable_AI0_Low) == -1) {
407 wpi_setStaticErrorWithContext(
408 dma_, NiFpga_Status_ResourceNotFound,
Brian Silvermane48dbc12017-02-04 20:06:29 -0800409 HAL_GetErrorMessage(NiFpga_Status_ResourceNotFound));
Austin Schuh91c75562015-12-20 22:23:10 -0800410 return 0xffff;
411 }
412 dmaWord = read_buffer_[offset(kEnable_AI0_Low) + channel / 2];
413 } else if (channel < 8) {
414 if (offset(kEnable_AI0_High) == -1) {
415 wpi_setStaticErrorWithContext(
416 dma_, NiFpga_Status_ResourceNotFound,
Brian Silvermane48dbc12017-02-04 20:06:29 -0800417 HAL_GetErrorMessage(NiFpga_Status_ResourceNotFound));
Austin Schuh91c75562015-12-20 22:23:10 -0800418 return 0xffff;
419 }
420 dmaWord = read_buffer_[offset(kEnable_AI0_High) + (channel - 4) / 2];
421 } else {
422 wpi_setStaticErrorWithContext(
423 dma_, NiFpga_Status_ResourceNotFound,
Brian Silvermane48dbc12017-02-04 20:06:29 -0800424 HAL_GetErrorMessage(NiFpga_Status_ResourceNotFound));
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800425 return 0xffff;
Brian Silvermand49fd782015-01-30 16:43:17 -0500426 }
Austin Schuhc6cc4102015-02-15 23:19:53 -0800427 if (channel % 2) {
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800428 return (dmaWord >> 16) & 0xffff;
429 } else {
Austin Schuh91c75562015-12-20 22:23:10 -0800430 return dmaWord & 0xffff;
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800431 }
Brian Silvermand49fd782015-01-30 16:43:17 -0500432 return static_cast<int16_t>(dmaWord);
433}
434
435float DMASample::GetVoltage(AnalogInput *input) const {
Austin Schuh58d8cdf2015-02-15 21:04:42 -0800436 uint16_t value = GetValue(input);
437 if (value == 0xffff) return 0.0;
Brian Silvermand49fd782015-01-30 16:43:17 -0500438 uint32_t lsb_weight = input->GetLSBWeight();
439 int32_t offset = input->GetOffset();
440 float voltage = lsb_weight * 1.0e-9 * value - offset * 1.0e-9;
441 return voltage;
Brian Silverman2aa83d72015-01-24 18:03:11 -0500442}