-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathadc1_cal.py
455 lines (395 loc) · 14.8 KB
/
adc1_cal.py
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
"""MicroPython ESP32 ADC1 conversion using V_ref calibration value"""
###############################################################################
# adc1_cal.py
#
# This module provides the ADC1Cal class
#
# MicroPython ESP32 ADC1 conversion using V_ref calibration value
#
# The need for calibration is described in [1] and [4].
#
# Limitations of the current implementation ("works for me"):
# - only ADC1 is supported (as the name says)
# - only "V_ref"-calibration
#
# For a full discussion of the three different calibration options see [1]
#
# The V_ref calibration value can be read with the tool espefuse.py [1]
# Example:
# $ espefuse.py --port <port> adc_info
# Detecting chip type... ESP32
# espefuse.py v3.0
# ADC VRef calibration: 1065mV
#
# This is now done in the constructor if its argument <vref> is None.
#
# The ESP32 documentation is very fuzzy concerning the ADC input range,
# full scale value or LSB voltage, respectively.
# The MicroPython quick reference [3] is also (IMHO) quite misleading.
# A good glimpse is provided in [4].
#
# - "Per design the ADC reference voltage is 1100 mV, however the true
# reference voltage can range from 1000 mV to 1200 mV amongst different
# ESP32s." [1]
#
# - Attenuation and "suggested input ranges" [1]
# +----------+-------------+-----------------+
# | | attenuation | suggested range |
# | SoC | (dB) | (mV) |
# +==========+=============+=================+
# | | 0 | 100 ~ 950 |
# | +-------------+-----------------+
# | | 2.5 | 100 ~ 1250 |
# | ESP32 +-------------+-----------------+
# | | 6 | 150 ~ 1750 |
# | +-------------+-----------------+
# | | 11 | 150 ~ 2450 |
# +----------+-------------+-----------------+
# | | 0 | 0 ~ 750 |
# | +-------------+-----------------+
# | | 2.5 | 0 ~ 1050 |
# | ESP32-S2 +-------------+-----------------+
# | | 6 | 0 ~ 1300 |
# | +-------------+-----------------+
# | | 11 | 0 ~ 2500 |
# +----------+-------------+-----------------+
#
#
# Please refer to the section "Minimizing Noise" in [1].
#
# The calibration algorithm and constants are based on [2].
#
# [1] https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html#adc-calibration
# [2] https://github.com/espressif/esp-idf/blob/master/components/esp_adc_cal/esp_adc_cal_esp32.c
# [3] https://docs.micropython.org/en/latest/esp32/quickref.html#adc-analog-to-digital-conversion
# [4] https://esp32.com/viewtopic.php?t=1045 ([Answered] What are the ADC input ranges?)
#
# created: 04/2021
#
# This program is Copyright (C) 04/2021 Matthias Prinke
# <m.prinke@arcor.de> and covered by GNU's GPL.
# In particular, this program is free software and comes WITHOUT
# ANY WARRANTY.
#
# History:
#
# 20210418 Created
# 20210510 Ported calibration from esp_adc_cal_esp32.c [2]
# 20210511 Added internal reading of efuse calibration value
# Fixed usage example
# 20210512 Modified class ADC1Cal to inherit from machine.ADC class
# All bit widths are supported now
# Removed rounding of the result in voltage()
# Added support of 0/2.5/6 dB attenuation
# 20211206 Merged pull request by codemee: added support for ATTN_11DB
# 20230204 Improved coding style
#
# ToDo:
# - add support of "Two Point Calibration"
#
###############################################################################
import machine
from machine import ADC
# fmt: off
# Constant from
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/soc.h
_DR_REG_EFUSE_BASE = const(0x3FF5A000)
# Constants from
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/efuse_reg.h
_EFUSE_ADC_VREF = const(0x0000001F)
_EFUSE_BLK0_RDATA4_REG = _DR_REG_EFUSE_BASE + 0x010
# Constants from
# esp_adc_cal_esp32.c
_ADC_12_BIT_RES = const(4096)
_LIN_COEFF_A_SCALE = const(65536)
# LIN_COEFF_A_SCALE/2
_LIN_COEFF_A_ROUND = const(32768)
_ADC1_VREF_ATTEN_SCALE = [57431, 76236, 105481, 196602]
_ADC1_VREF_ATTEN_OFFSET = [75, 78, 107, 142]
_VREF_REG = _EFUSE_BLK0_RDATA4_REG
_VREF_OFFSET = const(1100)
_VREF_STEP_SIZE = const(7)
_VREF_FORMAT = const(0)
_VREF_MASK = const(0x1F)
_LUT_VREF_LOW = const(1000)
_LUT_VREF_HIGH = const(1200)
_LUT_ADC_STEP_SIZE = const(64)
_LUT_POINTS = const(20)
_LUT_LOW_THRESH = const(2880)
_LUT_HIGH_THRESH = _LUT_LOW_THRESH + _LUT_ADC_STEP_SIZE
# fmt: on
# 20 Point lookup tables, covering ADC readings from 2880 to 4096, step size of 64
# LUT for VREF 1000mV
_lut_adc1_low = [
2240,
2297,
2352,
2405,
2457,
2512,
2564,
2616,
2664,
2709,
2754,
2795,
2832,
2868,
2903,
2937,
2969,
3000,
3030,
3060,
]
# LUT for VREF 1200mV
_lut_adc1_high = [
2667,
2706,
2745,
2780,
2813,
2844,
2873,
2901,
2928,
2956,
2982,
3006,
3032,
3059,
3084,
3110,
3135,
3160,
3184,
3209,
]
#################################################################################
# ADC1Cal class - ADC voltage output using V_ref calibration value and averaging
#################################################################################
class ADC1Cal(machine.ADC):
"""
Extension of ADC class for using V_ref calibration value and averaging
Attributes:
name (string): instance name (for debugging)
_pin (int): ADC input pin no.
_div (float): voltage divider (V_in = V_meas * div)
_width (int): encoded width of ADC result (0...3)
_samples (int): number of ADC samples for averaging
vref (int): ADC reference voltage in mV (from efuse calibration data or supplied by programmer)
_coeff_a (float): conversion function coefficient 'a'
_coeff_b (float): conversion function coefficient 'b'
"""
def __init__(self, pin, div, vref=None, samples=10, name=""):
"""
The constructor for Battery class.
Parameters:
pin (machine.Pin): ADC input pin
div (float): voltage divider (V_in = V_meas * div)
vref (int): reference voltage (optionally supplied by programmer)
samples (int): number of ADC samples for averaging
name (string): instance name
"""
super().__init__(pin)
# fmt: off
self.name = name
self._div = div
self._width = 3
self._samples = samples
self.vref = self.read_efuse_vref() if (vref is None) else vref
self._atten = None
self.atten(ADC.ATTN_6DB)
# fmt: on
def atten(self, attenuation):
"""
Select attenuation of input signal
Parameter identical to ADC.atten()
Currently ADC.ATTN_11DB is not supported!
Parameters:
attenuation (int): ADC.ATTN_0DB / ADC.ATTN_2_5DB / ADC.ATTN_6DB / ADC.ATTN_11DB
"""
super().atten(attenuation)
# fmt: off
self._coeff_a = self.vref * _ADC1_VREF_ATTEN_SCALE[attenuation] / _ADC_12_BIT_RES
self._coeff_b = _ADC1_VREF_ATTEN_OFFSET[attenuation]
self._atten = attenuation
# fmt: on
def width(self, adc_width):
"""
Select bit width of conversion result
Parameter identical to ADC.width()
Parameters:
adc_width (int): ADC.WIDTH_9BIT / ADC.WIDTH_10BIT / BITADC.WIDTH_11BIT / ADC.WIDTH_12BIT
"""
assert (
adc_width >= 0 and adc_width < 4
), "Expecting ADC_WIDTH9 (0), ADC_WIDTH10 (1), ADC_WIDTH11 (2), or ADC_WIDTH12 (3)"
super().width(adc_width)
self._width = adc_width
def read_efuse_vref(self):
"""
Read V_ref calibration value from efuse (i.e. read SOC hardware register)
Returns:
int: calibrated ADC reference voltage (V_ref) in mV
"""
# eFuse stores deviation from ideal reference voltage
# Ideal vref
ret = _VREF_OFFSET
# GET_REG_FIELD():
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/soc.h
# Bit positions:
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/efuse_reg.h
# EFUSE_RD_ADC_VREF : R/W ;bitpos:[12:8] ;default: 5'b0
bits = (machine.mem32[_VREF_REG] >> 8) & _VREF_MASK
ret += self.decode_bits(bits, _VREF_MASK, _VREF_FORMAT) * _VREF_STEP_SIZE
# ADC Vref in mV
return ret
def decode_bits(self, bits, mask, is_twos_compl):
"""
Decode bit value from two's complement or sign-magnitude to integer
Parameters:
bits (int): bit-field value
mask (int): bit mask
is_twos_complement (bool): True - two's complement / False: sign-magnitude
Returns:
int: decoded value
"""
# Check sign bit (MSB of mask)
if bits & ~(mask >> 1) & mask:
# Negative
if is_twos_compl:
# 2's complement
ret = -(((~bits) + 1) & (mask >> 1))
else:
# Sign-magnitude
ret = -(bits & (mask >> 1))
else:
# Positive
ret = bits & (mask >> 1)
return ret
# Only call when ADC reading is above threshold
def calculate_voltage_lut(self, adc):
# Get index of lower bound points of LUT
i = int((adc - _LUT_LOW_THRESH) / _LUT_ADC_STEP_SIZE)
# Let the X Axis be self.vref, Y axis be ADC reading, and Z be voltage
# (x2 - x)
x2dist = _LUT_VREF_HIGH - self.vref
# (x - x1)
x1dist = self.vref - _LUT_VREF_LOW
# (y2 - y)
y2dist = ((i + 1) * _LUT_ADC_STEP_SIZE) + _LUT_LOW_THRESH - adc
# (y - y1)
y1dist = adc - ((i * _LUT_ADC_STEP_SIZE) + _LUT_LOW_THRESH)
# For points for bilinear interpolation
# Lower bound point of _lut_adc1_low
q11 = _lut_adc1_low[i]
# Upper bound point of _lut_adc1_low
q12 = _lut_adc1_low[i + 1]
# Lower bound point of _lut_adc1_high
q21 = _lut_adc1_high[i]
# Upper bound point of _lut_adc1_high
q22 = _lut_adc1_high[i + 1]
# Bilinear interpolation
# Where z = 1/((x2-x1)*(y2-y1)) * ((q11*x2dist*y2dist) + (q21*x1dist*y2dist) + (q12*x2dist*y1dist) + (q22*x1dist*y1dist))
voltage = (
(q11 * x2dist * y2dist)
+ (q21 * x1dist * y2dist)
+ (q12 * x2dist * y1dist)
+ (q22 * x1dist * y1dist)
)
# Integer division rounding
# voltage += ((_LUT_VREF_HIGH - _LUT_VREF_LOW) * _LUT_ADC_STEP_SIZE) / 2
# Divide by ((x2-x1)*(y2-y1))
voltage /= (_LUT_VREF_HIGH - _LUT_VREF_LOW) * _LUT_ADC_STEP_SIZE
return voltage
def interpolate_two_points(self, y1, y2, x_step, x):
# Interpolate between two points (x1,y1) (x2,y2) between 'lower' and 'upper' separated by 'step'
return ((y1 * x_step) + (y2 * x) - (y1 * x) + (x_step / 2)) / x_step
def calculate_voltage_linear(self, raw_val):
# Apply linear correction coefficients
voltage = (
((self._coeff_a * raw_val) + _LIN_COEFF_A_ROUND) / _LIN_COEFF_A_SCALE
) + self._coeff_b
return voltage
@property
def voltage(self):
"""
Get voltage measurement [mV].
Returns:
float: voltage [mV]
"""
assert self._atten is not None, "Currently ADC.ATTN_11DB is not supported!"
raw_val = 0
# Read and accumulate ADC samples
for _ in range(self._samples):
raw_val += self.read()
# Calculate average
raw_val = int(round(raw_val / self._samples))
# Extend result to 12 bits (required by calibration function)
raw_val <<= 3 - self._width
# Check if in non-linear region
if self._atten == ADC.ATTN_11DB and raw_val >= _LUT_LOW_THRESH:
# Use lookup table to get voltage in non linear portion of ADC_ATTEN_DB_11
lut_voltage = self.calculate_voltage_lut(raw_val)
# If ADC is transitioning from linear region to non-linear region
if raw_val <= _LUT_HIGH_THRESH:
# Linearly interpolate between linear voltage and lut voltage
linear_voltage = self.calculate_voltage_linear(raw_val)
voltage = self.interpolate_two_points(
linear_voltage, lut_voltage, _LUT_ADC_STEP_SIZE, (raw_val - _LUT_LOW_THRESH)
)
else:
voltage = lut_voltage
else:
# Apply calibration function
voltage = self.calculate_voltage_linear(raw_val)
# Apply external input voltage divider
voltage = voltage / self._div
return voltage
def __str__(self):
_atten = ["0dB", "2.5dB", "6dB", "11dB"]
if self.name != "":
name_str = "Name: {} ".format(self.name)
else:
name_str = ""
raw_val = self.read()
return "{} width: {:2}, attenuation: {:>5}, raw value: {:4}, value: {}".format(
name_str, 9 + self._width, _atten[self._atten], raw_val, self.voltage
)
from time import sleep
from machine import Pin
if __name__ == "__main__":
# ADC input pin no.
ADC_PIN = 35
# V_ref in mV (device specific value -> espefuse.py --port <port> adc_info)
VREF = 1065
# DIV = 100 / (100 + 200) # (R1 / R1 + R2) -> V_meas = V(R1 + R2); V_adc = V(R1)
DIV = 1
# no. of samples for averaging
AVERAGING = 10
adc_widths = [ADC.WIDTH_9BIT, ADC.WIDTH_10BIT, ADC.WIDTH_11BIT, ADC.WIDTH_12BIT]
adc_atten = [ADC.ATTN_0DB, ADC.ATTN_2_5DB, ADC.ATTN_6DB, ADC.ATTN_11DB]
# Using programmer-supplied calibration value
# ubatt = ADC1Cal(Pin(ADC_PIN, Pin.IN), DIV, VREF, AVERAGING, "ADC1 User Calibrated")
# Using efuse calibration value
ubatt = ADC1Cal(Pin(ADC_PIN, Pin.IN), DIV, None, AVERAGING, "ADC1 eFuse Calibrated")
# Test all supported attenuation/width permutations
for att in adc_atten:
# set attenuation
ubatt.atten(att)
for width in adc_widths:
# set ADC result width
ubatt.width(width)
# Print object info
print(ubatt)
# set ADC result width
ubatt.width(ADC.WIDTH_10BIT)
# set attenuation
ubatt.atten(ADC.ATTN_6DB)
print()
print("ADC Vref: {:4}mV".format(ubatt.vref))
print()
while 1:
print("Voltage: {:4.1f}mV".format(ubatt.voltage))
sleep(5)