-
Notifications
You must be signed in to change notification settings - Fork 1
/
capmeter.ino
327 lines (278 loc) · 10.2 KB
/
capmeter.ino
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
// See https://github.com/reinderien/capmeter
// This is the original version - half-second update, fairly cautious about power usage.
#ifndef __AVR_ATmega2560__
#error Arduino Mega 2560 required. For others, contact the author or take care during porting.
#endif
#define VERBOSE 1
struct {
float R; // Resistor driven for this range
uint8_t pin_mask; // PORTF mask for driving resistor
uint16_t prescale; // Timer 1 prescale factor
uint8_t CS; // CS1 bits to select this prescaler
// min = floor(2^16 * pres[n+1]/pres[n] * R[n]/R[n+1])
uint16_t min; // ICR threshold below which range should grow
} static const ranges[] = {
// R pin pres CS min
{ 270, 1, 1024, B101, 16384},
{ 270, 1, 256, B100, 16384},
{ 270, 1, 64, B011, 8192},
{ 270, 1, 8, B010, 8192},
{ 270, 1, 1, B001, 9437},
{ 15e3, 2, 8, B010, 8192},
{ 15e3, 2, 1, B001, 7864},
{ 1e6, 4, 8, B010, 8192},
{ 1e6, 4, 1, B001, 0}
};
static const uint8_t n_ranges = sizeof(ranges)/sizeof(*ranges);
static uint8_t r_index = 4;
static uint16_t captured;
static volatile bool refresh_ready = false, measured = false;
static bool zeroed = false;
static float zerocap;
static void setup_power() {
// Power reduction - see ch11.10.2
// Initially, turn everything off. Selectively re-enable later.
PRR0 = 0xFF;
PRR1 = 0xFF;
// Set sleep mode to idle - CPU will stop but timers will run
// and interrupts will wake us up. See ch11.2
SMCR = (B000 << SM0) | // sleep will idle
(1 << SE); // enable sleep support
}
static void setup_ports() {
/* Ports - see ch13, ch11.9.6
per 13.2.6, unused ports should be set to input with weak pullup
If DDRx is configured for input, PORTx must be 1 for pullup, 0 for no pullup
*/
MCUCR &= ~(1 << PUD); // Enable weak pullup support
DDRA = 0; PORTA = 0xFF; // Completely unused ports:
DDRC = 0; PORTC = 0xFF; // all input, all weak pullup
DDRD = 0; PORTD = 0xFF;
DDRG = 0; PORTG = 0xFF;
DDRH = 0; PORTH = 0xFF;
DDRJ = 0; PORTJ = 0xFF;
DDRK = 0; PORTK = 0xFF;
DDRL = 0; PORTL = 0xFF;
DDRB = B10000000; // PB - only one output, for LED
PORTB = B01111111; // PB - LED off, others WPU
DDRE = 0; // PE all input
PORTE = B11110100; // PE all WPU except PE3 (AIN1) and UART0
DIDR1 = (1 << AIN1D) | // Turn off digital input buffer for PE3 (AIN1)
(0 << AIN0D);
DDRF = B00000111; // PF0-2 set to discharge initially; others unused
PORTF = 0xFF; // All pullups or sourcing
DIDR0 = B00000111; // Turn off digital input buffer for ADC0-2 (PF0-2)
}
static void setup_refresh() {
/*
Todo - This needs to change for big caps, but for now leave it
Use timer 3 for output refresh (timers 0, 2 are 8-bit,
timer 1 is used for charge capture). This is 16-bit.
Use Clear Timer on Compare Match (Auto Reload) - see ch17.9.2
Use a /256 prescaler.
*/
PRR1 &= ~(1 << PRTIM3); // Power on timer 3
TIMSK3 = (1 << OCIE3A); // Only enable compare A interrupt
TCCR3A = (B00 << COM3A0) | // OC pins unused
(B00 << COM3B0) |
(B00 << COM3C0) |
(B00 << WGM30); // CTC
TCCR3B = (0 << ICNC3) | // Disable noise canceller
(0 << ICES3) | // Capture edge doesn't apply here
(B01 << WGM32) | // CTC, OCR3A top
(B100 << CS30); // Start counting, 1/256 prescaler
// Do NOT do this before initializing TCCR3A
OCR3A = 31250; // 500ms * 16e6 / 256
}
static void setup_serial() {
PRR0 &= ~(1 << PRUSART0); // Power up USART0 for output to USB over pins 0+1
Serial.begin(115200); // UART at 115200 baud
#if VERBOSE
Serial.println("\nInitialized");
#endif
}
static void setup_comptor() {
/* Analog comparator: ch25, p265
+ connected to bandgap ref via ACSR.ACBG=1
- connected to AIN1 (PE3 "pin 5") via ADCSRB.ACME=0
ACO output connected via ACIC=1 to input capture
Since the ACIS edge selector appears after ACO, and ACO itself is sent to
capture, and we don't use the AC interrupt itself, ACIS should not matter.
Internal bandgap ref:
stability described in ch12.3, p60
shown as 1.1V in ch31.5, p360
*/
ACSR = (0 << ACD) | // comparator enabled
(1 << ACBG) | // select 1.1V bandgap ref for +
(0 << ACO) | // output - no effect
(1 << ACI) | // "clear" interrupt flag
(0 << ACIE) | // comptor interrupt disabled
(1 << ACIC) | // enable timer capture
(B11 << ACIS0); // AC interrupt on rising edge
ADCSRA = (0 << ADEN) | // Disable ADC
(0 << ADSC) | // don't start conversion
(0 << ADATE) | // no auto-trigger
(1 << ADIF) | // This "clears" the ADC interrupt flag
(0 << ADIE) | // disable ADC interrupts
(B000 << ADPS0); // ADC prescaler doesn't matter
ADCSRB = (0 << ACME) | // comptor- connected to AIN1
(0 << MUX5) | // unused
(B000 << ADTS0); // auto-trigger source unused
}
static void setup_capture() {
/* Input capture, ch17.6, p140
Uses timer/counter1 value in TCNT1 copied to ICR1
The docs claim that TICIE1 must be set to enable interrupt, but that's
a typo and ICIE1 (per p162) should be used instead.
ICF1 and TOV1 flags will be set and autocleared on their respective interrupt.
Timer 1, ch17, p133
16-bit counter
fclkI/O is described in ch10.2.2 p39
The Arduino source (wiring.c) configures this for "8-bit phase-correct PWM mode"
but let's go ahead and ignore that
*/
PRR0 &= ~(1 << PRTIM1); // Turn on power for T1
TIMSK1 = (1 << ICIE1) | // enable capture interrupt
(0 << OCIE1C) | // disable output compare interrupts
(0 << OCIE1B) |
(0 << OCIE1A) |
(1 << TOIE1); // enable overflow interrupt
TCCR1A = (B00 << COM1A0) | // OC pins unused
(B00 << COM1B0) |
(B00 << COM1C0) |
(B00 << WGM10); // Normal count up, no clear (p145)
// Leave TCCR1B until we start capture and choose a prescaler
}
static void start_capture() {
// Todo - this needs some work
// ACSR &= ~(1 << ACD); // Enable comparator
PRR0 &= ~(1 << PRTIM1); // Turn on power for T1
TCNT1 = 0; // Clear timer value
// CS1 prescaler is based on the selected range
TCCR1B = (0 << ICNC1) | // Disable noise cancellation
(1 << ICES1) | // ICP rising edge
(B00 << WGM12) | // Normal count up, no clear (p145)
(ranges[r_index].CS << CS10); // Start counting, internal clock source
}
static void stop_capture() {
// Todo - this needs some work
// ACSR |= 1 << ACD; // Disable comparator
TCCR1B = 0; // Stop clock by setting CS1=000
PRR0 |= 1 << PRTIM1; // Turn off power for T1
}
static void print_si(float x) {
static const char pre[] = "\0pnum kMG";
const char *p = pre+5;
for (; x < 1 && p[-1]; p--)
x *= 1e3;
for (; x >= 1e3 && p[1]; p++)
x /= 1e3;
uint8_t digs = 3;
for (float xsig = x; xsig > 1; xsig /= 10)
digs--;
Serial.print(x, digs);
Serial.print(*p);
}
static void print_cap(uint16_t timer) {
const float taus = 1.514128, // ln(5/1.1)
f = F_CPU/ranges[r_index].prescale,
t = ((float)timer)/f,
R = ranges[r_index].R;
float C = t/taus/R;
if (!zeroed) {
if (r_index == n_ranges-1 && C < 100e-12) {
zerocap = C;
#if VERBOSE
{
Serial.print("\nZeroing to ");
print_si(zerocap);
Serial.println('F');
}
#endif
zeroed = true;
}
}
if (zeroed) {
C -= zerocap;
if (C < 0) C = 0;
}
#if VERBOSE
{
Serial.print("r_index="); Serial.print(r_index, DEC); Serial.print(' ');
Serial.print("f="); print_si(f); Serial.print("Hz ");
Serial.print("t="); print_si(t); Serial.print("s ");
Serial.print("timer="); Serial.print(timer, DEC); Serial.print(' ');
Serial.print("R="); print_si(R); Serial.print("ohm ");
}
#endif
Serial.print('C');
if (timer == 0xFFFF) {
Serial.print('>');
PORTB &= B01111111; // Clear LED if we overflowed
}
else {
Serial.print('=');
PORTB |= B10000000; // Set LED if we've measured a capacitance
}
print_si(C); Serial.print("F \r");
}
static void charge() {
DDRF = ranges[r_index].pin_mask; // All inputs except current R
// reset the timer value
start_capture();
// Start charging the cap
// 1: unused pins that stay as pullups
// 0: either input-no-pullup, or sinking for current R to charge
PORTF = B11111000;
}
static void discharge() {
DDRF = B00000111; // PF0-2 set to output discharge; others unused
PORTF = 0xFF; // All pullups or sourcing
stop_capture();
}
static void rerange(uint16_t timer) {
if (timer == 0xFFFF) { // overflow
if (r_index > 0)
r_index--;
}
else { // increase for better resolution
if (timer < ranges[r_index].min)
r_index++;
}
}
void setup() {
cli(); // disable interrupts until we're done setting up
setup_power();
setup_ports();
setup_comptor();
setup_capture();
setup_refresh();
setup_serial();
sei(); // re-enable interrupts
}
void loop() {
for (;;) { // do not allow serialEvent
charge();
while (!measured)
__asm__("sleep");
measured = false;
uint16_t timer = captured;
discharge();
print_cap(timer);
rerange(timer);
while (!refresh_ready)
__asm__("sleep");
refresh_ready = false;
}
}
ISR(TIMER3_COMPA_vect) { // refresh every 0.5s
refresh_ready = true;
}
ISR(TIMER1_CAPT_vect) { // comparator capture (ok charge time)
captured = ICR1;
measured = true;
}
ISR(TIMER1_OVF_vect) { // timer overflow (took too long to charge)
captured = 0xFFFF;
measured = true;
}