13 ноября 2016
Кравченко Виктор

Arduino: ускоряем работу платы. Часть 2 — Аналого-цифровой преобразователь (АЦП) и analogRead()

Цифровые устройства Arduino Arduino Lang
01
Содержание:
02

Несмотря на кажущуюся схожесть с другой парой функций digitalRead() и digitalWrite(), которые работают по одному механизму, только в разных направлениях, функции analogRead() и analogWrite() имеют в корне разную природу, в первую очередь на аппаратном уровне. Поэтому и рассматривать их нужно в отрыве друг от друга — функционал analogRead() целиком и полностью реализован на «железном уровне» в аналого-цифровом преобразователе (АЦП), analogWrite() — в регистрах выходного сравнения OCR (ШИМ-генераторам), работающим на одном из встроенных таймеров.

03

АЦП и analogRead() — немного теории

В отличие от остальных функций, ускорение функции analogRead() достигается не модификацией её исходного кода, а настройкой режима работы АЦП. Но обо всем по порядку и начнем с описания работы АЦП микроконтроллера Atmega 328P.

Datasheet на микроконтроллер Atmega 328P можно скачать на официальном сайте производителя Atmel — Atmel-42735-8-bit-AVR-Microcontroller-ATmega328-328P_Datasheet.pdf (5,17 MB)
04

Микроконтроллер Atmega 328P, на котором построен Arduino Uno содержит встроенный 10-битный аналого-цифровой преобразователь (далее АЦП, англ. ADC — Analog to Digital Converter) последовательного приближения. Именно он, как видно из названия, отвечает за оцифровку входящего аналогового сигнала.

05

Для осуществления корректного преобразования, АЦП необходимо эталонное значение напряжения, с которым будет сравниваться входящий аналоговый сигнал. Это эталонное значение называется источником опорного напряжения (ИОН). Микроконтроллер Atmega 328P позволяет в качестве ИОН использовать:

  • напряжение питания микроконтроллера — 5 В,
  • внутренний опорный источник на 1,1 В (на Atmega8 — 2,56 В),
  • напряжение на выводе AREF (внешний ИОН, референсное напряжение).

06

По умолчанию, у Arduino, в качестве ИОН выступает напряжение пиитания МК — 5 В. Для использования в качестве ИОН других источников референсного внешнего напряжения у микроконтроллера есть дополнительный вход AREF. Перед преобразованием аналогового сигнала, при использовании внешнего ИОН необходимо вызвать функцию analogReference().

07

Сигнал, поданный на вход АЦП должен быть в границах заданного диапазона 0 (GND)...ИОН. 10-битный означает, что заданный диапазон будет разбит на $2^{10}=1024$ значения, и входной сигнал будет оцифрован в соответствующее цифровое значение из диапазона 0...1023:

08
09

Процесс оцифровки аналогового сигнала сводится к последовательному подбору наиболее близкого напряжения с известным (референсным) значением к входному напряжению. Принцип действия АЦП схематично показан на рисунке:

10
Схема работы 10-битного АЦП последовательного приближения
Схема работы 10-битного АЦП последовательного приближения
11

Механизм преобразования прост и состоит из 10 шагов (отсюда 10 бит). На первом шаге АЦП формирует напряжение $U_{АЦП}$ по величине равное половине опорного напряжения — $\frac{ 1}{ 2} U_{ИОН}$ и сравнивает его с входящим аналоговым сигналом $U_{Вх}$. Далее, если $U_{АЦП}$ меньше $U_{Вх}$, то соответствующему биту присваивается значение 1, и $U_{АЦП}$ увеличивается уже на $\frac{ 1}{ 4} U_{ИОН}$, если $U_{АЦП}$ больше $U_{Вх}$, то соответствующему биту присваивается значение 0, и $U_{АЦП}$ уменьшается на $\frac{ 1}{ 4} U_{ИОН}$. На следующем шаге коррекция составляет $\frac{ 1}{ 8} U_{ИОН}$, потом $\frac{ 1}{ 16} U_{ИОН}$ и т.д. Математически процесс можно представить следующей системой уравнений:

12
$$U_{АЦП_{n}}=\begin{cases} \frac{ 1}{ 2} U_{ИОН} & \text{, при } n=1 \\ U_{АЦП_{n-1}}+\frac{ 1}{ 2^n} U_{ИОН} & \text{, при }n>1, U_{АЦП_{n-1}}<U_{Вх} \\ U_{АЦП_{n-1}}-\frac{ 1}{ 2^n} U_{ИОН} & \text{, при }n>1, U_{АЦП_{n-1}}>U_{Вх} \end{cases}$$
13

Исходя из вышеизложенного и данных представленных на рисунке, нетрудно посчитать результат:
$512\times1+256\times0+128\times0+64\times1+32\times0+16\times1+8\times0+4\times0+2\times1+1\times0=594$

14

В один момент времени АЦП может оцифровывать сигнал только с одного аналогового входа. Это связано с тем, что за выбор сигнала отвечает мультиплексор — устройство, которое по команде АЦП из нескольких аналоговых входов на выход подает только один выбранный. У Atmega 328P аналоговых входов 6 (в корпусе DIP) — все они принадлежат порту C (PORTC — помните, в первой части?).

15

Запуск преобразования может осуществляться несколькими способами:

  • в ручном режиме — единичное преобразование
  • а автоматическом режиме — по сигналам из различных источников

16

Для управления АЦП существует 5 основных восьмибитных регистров:

17
Регистры Биты
7 6 5 4 3 2 1 0
ADCSRA ADEN ADSC ADATE(ADFR2) ADIF ADIE ADPS[2:0]
ADMUX REFS[1:0] ADLAR × MUX[3:0]
ADCSRB ADTS[2:0] × ×
×
×
×
ADCL Результат преобразования. Значение имеют только 10 бит.
Старшие или младшие - зависит от значения бита ADLAR.
ADCH
18

Регистр ADCSRA (ADC Control and Status Register A)

Назначение битов регистра ADCSRA (ADC Control and Status Register A, регистр управления и состояния):

  • ADEN (ADC Enable, включение АЦП) — флаг, разрешающий использование АЦП, при сбросе флага во время преобразования процесс останавливается;
  • ADATE (ADC Auto Trigger Enable) — выбор режима работы АЦП. 0 – разовое преобразование (Single Conversion Mode); 1 – включение автоматического преобразования по срабатыванию триггера (заданного сигнала). Источник автоматического запуска задается битами ADTS[2:0] регистра ADCSRB. Возможны следующие варианты:
  • ADTS[2:0] Источник запуска преобразования ADC
    000 Постоянное преобразование (Free Running mode)
    001 Аналоговый компаратор
    010 Внешний запрос на прерывание 0
    011 Timer/Counter0 Compare Match
    100 Timer/Counter0 Overflow
    101 Timer/Counter1 Compare Match B
    110 Timer/Counter1 Overflow
    111 Timer/Counter1 Capture Event
  • ADSC (ADC Start Conversion, запуск преобразования) — флаг установленный в 1 запускает процесс преобразования.
    В режиме Single Conversion (ADATE=0) для запуска каждого нового преобразования этот флаг должен быть установлен в единицу. После завершения преобразования, флаг ADSC сбрасывается в 0.
    В режиме Free Running Mode (ADATE=1, ADTS[2:0]=000) этот флаг должен быть установлен в единицу один раз — для запуска первого преобразования, следующие происходят автоматически.
    Если установка флага ADSC происходит во время или после разрешения АЦП (ADEN), то перед запрашиваемым преобразованием происходит дополнительное (extended) преобразование, во время которого происходит инициализация АЦП. Сброс флага во время преобразования не оказывает никакого влияния на процесс.
  • ADIF (ADC Interrupt Flag) — флаг прерывания от компаратора
  • ADIE (ADC Interrupt Enable) — разрешение прерывания от компаратора
  • ADPS[2:0] (ADC Prescaler Select) — комбинацией битов задается частота преобразования АЦП по отношению к частоте МК. Выбранная частота влияет на точность — чем выше частота, тем ниже точность. АЦП тактируется через выбранный делитель от частоты ядра микроконтроллера. Значения битов:
  • ADPS[2:0] Коэффициент деления
    0002
    0012
    0104
    0118
    10016
    10132
    11064
    111128

19

Здесь стоит подробнее рассказать о том, как значения битов ADPS[2:0] влияют на скорость и точность АЦП. Частота работы МК Atmega 328P 16МГц. При настройках по умолчанию используется предделитель 128 (ADPS[2:0]=[111]), а это значит, что АЦП работает на частоте 16МГц/128=125КГц, что укладывается в данные даташита – 50-200КГц. Отсюда очень низкая скорость выполнения преобразования, но и самая высокая точность. Для того, чтобы ускорить работу АЦП необходимо уменьшить предделитель, но необходимо помнить, что чем выше частота, тем ниже точность преобразования. Экспериментально можно получить значение предделителя – 16 (ADPS[2:0]=[100]), при котором возможен компромисс 10-кратного прироста скорости, при сохранении точности.

20

Напишем первый скетч, в котором используется родная функция analogRead(). Но отличие будет заключаться в том, что перед использованием, режим работы АЦП будет перенастроен:

21 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
int pinIn = A0; // Пин аналогового входа void setup() { pinMode(pinIn, INPUT);
ADCSRA |= (1 << ADPS2); //Биту ADPS2 присваиваем единицу - коэффициент деления 16
ADCSRA &= ~ ((1 << ADPS1) | (1 << ADPS0)); //Битам ADPS1 и ADPS0 присваиваем нули
} void loop() { Serial.println(analogRead(pinIn)); delay(1000); }
22

Замеры производительности показывают время выполнения функции — 16 мкс (вместо первоначальных 112 мкс):

1
2
analogRead(). Total time: 160052 microsec, AVG time: 16.00 microseconds. (iterations 10000) Ускорение в 7 раз.

23

А выбор предделителя с коэффициентом 8 (ADPS[2:0]=[011]) сократит время исполнения до 9 мкс... В общем здесь уже каждый в зависимости от целей и задач определяет необходимые параметры.

24

Используя директивы препроцессора (о них написана отдельная статья), код можно модифицировать в более удобный и понятный:

25 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
int pinIn = A0; // Пин аналогового входа void setup() { pinMode(pinIn, INPUT);
sbi(ADCSRA, ADPS2); //Биту ADPS2 присваиваем единицу
cbi(ADCSRA, ADPS1); //Битам ADPS1 и ADPS0 присваиваем нули
cbi(ADCSRA, ADPS0);
} void loop() { Serial.println(analogRead(pinIn)); delay(1000); }
26

Регистр ADMUX (ADC Multiplexer Selection Register)

Вспомним какие биты содержатся в регистре ADMUX:

Регистры Биты
7 6 5 4 3 2 1 0
ADMUX REFS[1:0] ADLAR MUX[3:0]

27

Назначение битов регистра ADMUX:

  • REFS[1:0] (Reference Selection Bit) — биты определяют источник опорного напряжения. Возможные значения:
  • REFS[1:0] Источник опорного напряжения
    00Внешний источник, подключенный к AREF, внутренний VREF отключен
    01AVCC с внешним конденсатором на выводе AREF
    10Зарезервировано
    11Внутренний источник опорного напряжения 2.56В с внешним конденсатором на выводе AREF
  • ADLAR (ADC Left Adjust Result) — бит отвечающий за порядок записи битов результата в регистры ADCL и ADCH. В зависимости от того значения, которое присвоено биту ADLAR возможны 2 варианта:
    Если ADLAR=0, то
  • Регистры Биты
    7 6 5 4 3 2 1 0
    ADCL ADC7 ADC6 ADC5 ADC4 ADC3 ADC2 ADC1 ADC0
    ADCH ADC9 ADC8

    Если ADLAR=1, то
  • Регистры Биты
    7 6 5 4 3 2 1 0
    ADCL ADC1 ADC0
    ADCH ADC9 ADC8 ADC7 ADC6 ADC5 ADC4 ADC3 ADC2
  • MUX[3:0] (Multiplexer Selection Input) — биты выбора аналогового входа. Значения:
  • MUX[3:0] Аналоговый вход
    0000ADC0
    0001ADC1
    0010ADC2
    0011ADC3
    0100ADC4
    0101ADC5
    0110ADC6
    0111ADC7
    1000Температурный датчик
    1001Зарезервировано
    1010Зарезервировано
    1011Зарезервировано
    1100Зарезервировано
    1101Зарезервировано
    11101.1V (VBG)
    11110V (GND)

28

Регистр ADCSRB (ADC Control and Status Register B)

У регистра ADCSRB всего одна группа битов — ADTS[2:0]. О ней упоминалось в контексте описания бита ADATE. Биты ADTS[2:0] отвечают за выбор источника автоматического запуска АЦП.

29

Здесь мы подошли ко второй важной части ускорения функции analogRead(). Если посмотреть на исходный код функции, можно увидеть строки вызова и ожидания результата:

30 Arduino (C++)
1
2
3
4
5
6
7
// ... // Запуск преобразования sbi(ADCSRA, ADSC); // Ожидание сброса бита ADSC, свидетельствующее об окончании преобразования
while (bit_is_set(ADCSRA, ADSC));
// ...
wiring_analog.c
31

Теперь получается, что после вызова функции микроконтроллер останавливается и просто ждет результата (строка 6). В приложении моргания светодиодом это может быть совсем не важно, а вот в системе постоянного опроса аналоговых датчиков, да и ещё с какой-либо логикой, такой поведение уже критично.

32

Для того, чтобы разгрузить микроконтроллер необходимо заставить АЦП самостоятельно заниматься оцифровкой, а по завершении преобразования АЦП сообщит об окончании прерыванием.

33

АЦП и прерывания (Interrupts)

Но полную мощь АЦП проявляет в связке с прерываниями — обособленный от ядра МК режим работы АЦП, при котором АЦП прерыванием информирует МК о завершенном преобразовании. Пока осуществляется преобразование, Arduino занимается своими делами — исполняет заданную программу.

34

Для реализации описанного режима необходимо выполнить следующие настройки (изменить биты регистров АЦП):

35 Arduino (C++)
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
uint8_t analog_ref = DEFAULT; // Эта переменная нужна будет для случаев перебора нескольких датчиков - //она будет хранить начальное состояние регистра ADMUX void setup() { //... ADCSRA = 0; // Сбрасываем регистр ADCSRA ADCSRB = 0; // Сбрасываем регистр ADCSRB ADMUX |= (1 << REFS0); // Задаем ИОН ADMUX |= (1 << ADLAR); // Меняем порядок записи бит, чтобы можно было читать только 8 бит регистра ADCH // Таким образом отсекаются 2 последних "шумных" бита, результат 8-битный (0...255) analog_ref = ADMUX; // Запоминаем состояние регистра - оно будет использоваться при смене пина входящего сигнала ADMUX |= (0 & 0x07); // Выбираем пин A0 для преобразования // Устанавливаем предделитель - 16 (ADPS[2:0]=100) ADCSRA |= (1 << ADPS2); //Биту ADPS2 присваиваем единицу ADCSRA &= ~ ((1 << ADPS1) | (1 << ADPS0)); //Битам ADPS1 и ADPS0 присваиваем нули ADCSRA |= (1 << ADATE); // Включаем автоматическое преобразование ADCSRA |= (1 << ADIE); // Разрешаем прерывания по завершении преобразования ADCSRA |= (1 << ADEN); // Включаем АЦП ADCSRA |= (1 << ADSC); // Запускаем преобразование }
36

Для обслуживания прерывания, генерируемого АЦП существует процедура ISR(ADC_vect) {...} (англ. Interrupt service routine — процедура обслуживания прерывания), которая запускается каждый раз в момент завершения преобразования. В этой процедуре можно считать результат преобразования и обработать его:

37 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
ISR(ADC_vect){ //... // Порядок записи результирующих битов был изменен из-за пренебрежения последними битами // ADLAR=1 int result = ADCH; // Считываем только значимые 8 бит - значение из диапазона 0...255 // Если нужны все 10 бит (полная 10-битная точность), то если ADLAR=0: int result = ADCL | (ADCH << 8); // Если ADLAR=1: int result = (ADCL>> 6) | (ADCH << 2); })
38

В данном примере переменная result объявлена как int, а значит будет сразу преобразована к числу диапазона 0...255 или 0...1023.

39 На заметку:
В случае использования в процедуре обработки прерывания переменной объявленной вне её, нельзя забывать использовать описатель volatile для корректной обработки переменной компилятором.
40 На заметку:
Если необходимо собирать данные с нескольких датчиков, то нужно после считывания преобразованного значения в процедуре ISR(ADC_vect) изменить входящий аналоговый пин на следующий:

1
2
3
4
5
6
7
8
9
volatile int analogPin = A0; ISR(ADC_vect) { int result = ADCL | (ADCH << 8); analogPin += 1; if (analogPin > A5) { analogPin = A0; };
ADMUX = analog_ref | (analogPin & 0x07);
})

Но запустив код, будет ждать разочарование — результаты каждый раз будут некорректными. Это связано с тем, в момент считывания преобразованного значения и переключения регистра ADMUX на другой аналоговый вход, АЦП уже начал новый процесс преобразования. Поэтому получается, что результат состоит из начала предыдущего преобразования и конца следующего. Чтобы устранить такое поведение, нужно пропускать первое после переключения входов преобразование — следующий результат будет достоверным:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
volatile int analogPin = A0; volatile bool trueValue = false; ISR(ADC_vect) { if (trueValue) { int result = ADCL | (ADCH << 8); analogPin += 1; if (analogPin > A5) { analogPin = A0; };
ADMUX = analog_ref | (analogPin & 0x07);
// Устанавливаем флаг смены входного пина trueValue = false; } else { // Первый раз пропускаем считывание и устанавливаем флаг на чтение в следующий раз trueValue = true; } })
41

Рассмотрим пример использования режима АЦП Free Runing Mode на следующем примере. Подключим к двум аналоговым входам потенциометр и датчик освещенности, и в зависимости от их значений будем регулировать яркость светодиодов. Схема:

42
43

Предделитель пришлось увеличить до 32 (ADPS[2:0]=101) — при значении предделителя 16 преобразования влияли друг на друга. Скетч:

44 Arduino (C++)
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
int pinLed1 = 6; // Потенциометр int pinLed2 = 11; // Фоторезистор uint8_t analog_ref = DEFAULT; void setup() { pinMode(pinLed1, OUTPUT); pinMode(pinLed2, OUTPUT); ADC_init(); // Все настройки АЦП вынесены в отдельную процедуру } void ADC_init() { ADCSRA = 0; // Сбрасываем регистр ADCSRA ADCSRB = 0; // Сбрасываем регистр ADCSRB ADMUX |= (1 << REFS0); // Задаем ИОН //ADMUX |= (1 << ADLAR); // Меняем порядок записи бит, чтобы можно было читать только 8 бит регистра ADCH // В этом случае считывание данных с АЦП будет выглядеть так: //int result = ADCH; analog_ref = ADMUX; // Запоминаем состояние регистра - из него мы будем формировать маску для смены входного пина ADMUX |= (0 & 0x07); // Выбираем пин A0 для преобразования // Таким образом отсекаются 2 последних "шумных" бита, результат 8-битный (0...255) // // Устанавливаем предделитель - 16 (ADPS[2:0]=100) // ADCSRA |= (1 << ADPS2) ; //Биту ADPS2 присваиваем единицу // ADCSRA &= ~ (1 << ADPS1) | (1 << ADPS0); //Битам ADPS1 и ADPS0 присваиваем нули // Устанавливаем предделитель - 32 (ADPS[2:0]=101) ADCSRA |= (1 << ADPS2) | (1 << ADPS0); //Биту ADPS2 присваиваем единицу ADCSRA &= ~ (1 << ADPS1); //Битам ADPS1 и ADPS0 присваиваем нули ADCSRA |= (1 << ADATE); // Включаем автоматическое преобразование ADCSRA |= (1 << ADIE); // Разрешаем прерывания по завершении преобразования ADCSRA |= (1 << ADEN); // Включаем АЦП ADCSRA |= (1 << ADSC); // Запускаем преобразование } volatile int currentSensorValue1 = 0; volatile int currentSensorValue2 = 0; long oldTime = 0; void loop() { if (millis() - 10 > oldTime) { analogWrite(pinLed1, map(currentSensorValue1, 0, 1023, 0 , 255)); analogWrite(pinLed2, map(currentSensorValue2, 0, 1023, 0 , 255)); oldTime = millis(); } } volatile int analogPin = A0; volatile bool trueValue = false; ISR(ADC_vect) { if (trueValue) { int result = ADCL | (ADCH << 8); // Получаем 10-битный результат if (analogPin == A0) { // Если актуальный входной пин A0, то присваиваем значение соответствующей переменной currentSensorValue1 = result; } else { currentSensorValue2 = result; } analogPin += 1; // Перебираем входные пины по кругу (А0...А1 - их может быть больше) if (analogPin > A1) { // Все нужные перебрали... analogPin = A0; // ...возвращаемся к первому }; int pin = analogPin; // Приводим пин вида A0 (14) к удобному для регистра ADMUX - 0. A0=14, A1=15, ... if (pin >= 14) pin -= 14; ADMUX = analog_ref | (pin & 0x07);// Устанавливаем новый вход для преобразования trueValue = false; // Устанавливаем флаг смены входного пина - следующее прерывание пропускаем } else { trueValue = true; // Первый раз пропускаем считывание и устанавливаем флаг на чтение в следующий раз } }
45

Для пренебрежения точностью, а с использованием в analogWrite() большая точность и не нужна, код можно модифицировать следующим образом:

46 Arduino (C++)
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
int pinLed1 = 6; // Потенциометр int pinLed2 = 11; // Фоторезистор uint8_t analog_ref = DEFAULT; void setup() { pinMode(pinLed1, OUTPUT); pinMode(pinLed2, OUTPUT); ADC_init(); // Все настройки АЦП вынесены в отдельную процедуру } void ADC_init() { ADCSRA = 0; // Сбрасываем регистр ADCSRA ADCSRB = 0; // Сбрасываем регистр ADCSRB ADMUX |= (1 << REFS0); // Задаем ИОН
ADMUX |= (1 << ADLAR); // Меняем порядок записи бит, чтобы можно было читать только 8 бит регистра ADCH
analog_ref = ADMUX; // Запоминаем состояние регистра - из него мы будем формировать маску для смены входного пина ADMUX |= (0 & 0x07); // Выбираем пин A0 для преобразования // Устанавливаем предделитель - 32 (ADPS[2:0]=101) ADCSRA |= (1 << ADPS2) | (1 << ADPS0); //Биту ADPS2 присваиваем единицу ADCSRA &= ~ (1 << ADPS1); //Битам ADPS1 и ADPS0 присваиваем нули ADCSRA |= (1 << ADATE); // Включаем автоматическое преобразование ADCSRA |= (1 << ADIE); // Разрешаем прерывания по завершении преобразования ADCSRA |= (1 << ADEN); // Включаем АЦП ADCSRA |= (1 << ADSC); // Запускаем преобразование } volatile int currentSensorValue1 = 0; volatile int currentSensorValue2 = 0; long oldTime = 0; void loop() { if (millis() - 10 > oldTime) {
analogWrite(pinLed1, currentSensorValue1);
analogWrite(pinLed2, currentSensorValue2);
oldTime = millis(); } } volatile int analogPin = A0; volatile bool trueValue = false; ISR(ADC_vect) { if (trueValue) {
int result = ADCH; // ADLAR=1, Получаем 8-битный результат, остальными битами пренебрегаем
if (analogPin == A0) { // Если актуальный входной пин A0, то присваиваем значение соответствующей переменной currentSensorValue1 = result; } else { currentSensorValue2 = result; } analogPin += 1; // Перебираем входные пины по кругу (А0...А1 - их может быть больше) if (analogPin > A1) { // Все нужные перебрали... analogPin = A0; // ...возвращаемся к первому }; int pin = analogPin; // Приводим пин вида A0 (14) к удобному для регистра ADMUX - 0. A0=14, A1=15, ... if (pin >= 14) pin -= 14; ADMUX = analog_ref | (pin & 0x07);// Устанавливаем новый вход для преобразования trueValue = false; // Устанавливаем флаг смены входного пина - следующее прерывание пропускаем } else { trueValue = true; // Первый раз пропускаем считывание и устанавливаем флаг на чтение в следующий раз } }
47

В описанном выше примере функция analogRead() фактически ни разу не вызывается, но в процедуре loop() можно делать все что захочется. При этом производительность от использования АЦП не проседает.

48

Бонус

В приведенном ниже коде показано, как можно сделать подсветку, плавно подстраивающуюся под изменение внешних условий:

49 Arduino (C++)
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
int pinLed1 = 6; // Потенциометр int pinLed2 = 11; // Фоторезистор uint8_t analog_ref = DEFAULT; void setup() { pinMode(pinLed1, OUTPUT); pinMode(pinLed2, OUTPUT); ADC_init(); // Все настройки АЦП вынесены в отдельную процедуру } void ADC_init() { ADCSRA = 0; // Сбрасываем регистр ADCSRA ADCSRB = 0; // Сбрасываем регистр ADCSRB ADMUX |= (1 << REFS0); // Задаем ИОН //ADMUX |= (1 << ADLAR); // Меняем порядок записи бит, чтобы можно было читать только 8 бит регистра ADCH analog_ref = ADMUX; // Запоминаем состояние регистра - из него мы будем формировать маску для смены входного пина ADMUX |= (0 & 0x07); // Выбираем пин A0 для преобразования // Устанавливаем предделитель - 32 (ADPS[2:0]=101) ADCSRA |= (1 << ADPS2) | (1 << ADPS0); //Битам ADPS2 и ADPS0 присваиваем единицу ADCSRA &= ~ (1 << ADPS1); //Биту ADPS1 присваиваем нули ADCSRA |= (1 << ADATE); // Включаем автоматическое преобразование ADCSRA |= (1 << ADIE); // Разрешаем прерывания по завершении преобразования ADCSRA |= (1 << ADEN); // Включаем АЦП ADCSRA |= (1 << ADSC); // Запускаем преобразование } int currentLEDValue1 = 0; int currentLEDValue2 = 0; volatile int currentSensorValue1 = 0; volatile int currentSensorValue2 = 0; long oldTime = 0; void loop() { if (millis() - 5 > oldTime) { if (currentLEDValue1 > currentSensorValue1) { currentLEDValue1--; analogWrite(pinLed1, map(currentLEDValue1, 0, 1023, 0 , 255)); } else if (currentLEDValue1 < currentSensorValue1) { currentLEDValue1++; analogWrite(pinLed1, map(currentLEDValue1, 0, 1023, 0 , 255)); } if (currentLEDValue2 > currentSensorValue2) { currentLEDValue2--; analogWrite(pinLed2, map(currentLEDValue2, 0, 1023, 0 , 255)); } else if (currentLEDValue2 < currentSensorValue2) { currentLEDValue2++; analogWrite(pinLed2, map(currentLEDValue2, 0, 1023, 0 , 255)); } oldTime = millis(); } } volatile int analogPin = A0; volatile bool trueValue = false; ISR(ADC_vect) { if (trueValue) { int result = ADCL | (ADCH << 8); // ADLAR=1, Получаем 8-битный результат, остальными битами пренебрегаем if (analogPin == A0) { // Если актуальный входной пин A0, то присваиваем значение соответствующей переменной currentSensorValue1 = result; } else { currentSensorValue2 = result; } analogPin += 1; // Перебираем входные пины по кругу (А0...А1 - их может быть больше) if (analogPin > A1) { // Все нужные перебрали... analogPin = A0; // ...возвращаемся к первому }; int pin = analogPin; // Приводим пин вида A0 (14) к удобному для регистра ADMUX - 0. A0=14, A1=15, ... if (pin >= 14) pin -= 14; ADMUX = analog_ref | (pin & 0x07);// Устанавливаем новый вход для преобразования trueValue = false; // Устанавливаем флаг смены входного пина - следующее прерывание пропускаем } else { trueValue = true; // Первый раз пропускаем считывание и устанавливаем флаг на чтение в следующий раз } }
50

Демонстрация на видео:

51
Подписывайтесь на канал , чтобы быть в курсе обновлений!
52

О том как ускорить работу функций digitalRead(), digitalWrite(), shiftOut() и т.д. написано в первой статье цикла, а об ускорении функции analogWrite() в третьей статье.

53
Содержание:
55

Похожие запросы:

  • Arduino: faster alternatives to digitalread() and digitalwrite()?
  • Arduino is Slow — and how to fix it!
  • Управление портами через регистры Atmega
  • Fast sampling from analog input
  • ANALOGREAD() IN LOOP{} 8X FASTER
  • Fast 8-bit ADC for the Arduino
comments powered by HyperComments