15 января 2017
Кравченко Виктор

Arduino: ускоряем работу платы. Часть 3 — analogWrite()

Цифровые устройства Arduino Arduino Lang
02

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

03

Перед тем, как переходить к модификации кода функции analogWrite() в целях ускорения работы платы, автор рекомендует ознакомиться со статьей о ШИМ, таймерах Arduino, управляющих ими регистрах и работе таймеров с прерываниями.

04

Ускоряем analogWrite()

Проведем эксперимент — посмотрим на замеры времени исполнения функции analogWrite() на разных пинах (как известно у Arduino Uno их 6):

05 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
unsigned long _timeStart, _timeEnd; int pins[] = {6, 5, 9, 11, 3, 10}; int count = 10000; void setup() { Serial.begin(9600); // будем выводить информацию о тестировании for (int i = 0; i < 6; i++) { pinMode(pins[i], OUTPUT); } } void loop() { for (int i = 0; i < 6; i++) { testAWrite(pins[i]); } delay(1000); } //========================================================= void testAWrite(int pin) { _timeStart = micros(); for (int i = 0; i < count; i++) { analogWrite(pin, 100); } _timeEnd = micros(); String funcname = "analogWrite(" + (String)pin + ")"; showResult (funcname, count ); } void showResult (String func, int iter) { unsigned long timeTotal = _timeEnd - _timeStart; unsigned long time1 = timeTotal / iter; String strToOut = func + ". Total time: " + String(timeTotal) + " microsec, AVG time: " + String(time1) + " microseconds. (iterations " + String(iter) + ")"; Serial.println(strToOut); }
06

Результаты (порядок пинов подобран по убыванию скорости исполнения функции):

07
1
2
3
4
5
6
analogWrite(6). Total time: 47164 microsec, AVG time: 4,72 microseconds. (iterations 10000) analogWrite(5). Total time: 48152 microsec, AVG time: 4,81 microseconds. (iterations 10000) analogWrite(9). Total time: 50056 microsec, AVG time: 5,00 microseconds. (iterations 10000) analogWrite(11). Total time: 50056 microsec, AVG time: 5,00 microseconds. (iterations 10000) analogWrite(3). Total time: 51320 microsec, AVG time: 5,13 microseconds. (iterations 10000) analogWrite(10). Total time: 54488 microsec, AVG time: 5,45 microseconds. (iterations 10000)
08

Разница исполнения функции analogWrite() на разных пинах достигает 0,7 мкс.

09

Немного о таймерах Arduino

ШИМ на Arduino целиком и полностью (не берем в расчет программную эмуляцию) реализован благодаря встроенным таймерам — и у Arduino Uno их 3 — два 8-битных (Timer0 и Timer2) и один 16-битный (Timer1). Каждый таймер управляется несколькими регистрами. Основные регистры управления — TCCRnA и TCCRnB. Ниже представлены биты, принадлежащие этим регистрам, которые нам понадобятся в рамках данной статьи:

  • CSn[2:0] (Clock Select) — биты регистра TCCRnB отвечающие за выбор источника синхронизации, предделителя и режима работы таймера.
  • WGMn[2:0] (Waveform Generation Mode) — выбор режима генерации волны. Биты расположены в регистрах TCCRnA и TCCRnB
  • COMnA[1:0] и COMnB[1:0] — режим совпадения вывода A/B — включен/выключен/инвертирован (биты расположены в регистре TCCRnA).



Подробнее о таймерах Arduino, регистрах управления, ШИМ и прерываниях таймеров можно почитать в статье — Таймеры Arduino и прерывания
10
Регистры Биты
7 6 5 4 3 2 1 0
TCCRnA COMnA1 COMnA0 COMnB1 COMnB0 × ×
WGMn1 WGMn0
TCCRnB FOCnA FOCnB × × WGMn2 CSn[2:0]
11

Посмотрим на код функции analogWrite():

12 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
void analogWrite(uint8_t pin, int val) { pinMode(pin, OUTPUT); if (val == 0) { digitalWrite(pin, LOW); } else if (val == 255) { digitalWrite(pin, HIGH); } else { switch(digitalPinToTimer(pin)) { case TIMER0A: // connect pwm to pin on timer 0, channel A sbi(TCCR0A, COM0A1); OCR0A = val; // устанавливаем уровень ШИМ break; // И так далее... //case ... Много однотипных условий //case ... //case ... //case ... // ... case NOT_ON_TIMER: default: if (val < 128) { digitalWrite(pin, LOW); } else { digitalWrite(pin, HIGH); } } } }
13

Именно все эти проверки switch-case и преобразования вносят свою, далеко не лучшую, лепту в скорость исполнения функции. Если присмотреться внимательнее, то дольше функция исполняется на тех пинах, которые имеют перед собой большее количество проверок switch-case — на их исполнение тоже уходит драгоценное время.

14

Теперь, если знать на какой пин воздействовать, какой таймер за какой пин отвечает...

15
Таймер Выход Пин Arduino
Timer0 OCR0B 5
OCR0A 6
Timer1 OCR1B 10
OCR1A 9
Timer2 OCR2B 3
OCR2A 11
16

...не составит труда сократить код функции до 2 строчек:

17 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) void setup() { // ... pinMode(3, OUTPUT); // Не забываем предварительно выставить pinMode } void analogWritePin3(int val) { // Предполагается, что при val==0 или val==255 задается не ШИМ, а LOW или HIGH sbi(TCCR2A, COM2B1); OCR2B = val; // устанавливаем уровень ШИМ }
18

Посмотрим теперь на время исполнения новой функции:

19 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
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) unsigned long _timeStart, _timeEnd; int count = 10000; void setup() { Serial.begin(9600); // будем выводить информацию о тестировании pinMode(3, OUTPUT); } void loop() { testAWrite(); delay(1000); } //========================================================= void testAWrite() { _timeStart = micros(); for (int i = 0; i < count; i++) { analogWritePin3(100); } _timeEnd = micros(); showResult ("analogWritePin3()", count ); } void showResult (String func, int iter) { unsigned long timeTotal = _timeEnd - _timeStart; unsigned long time1 = timeTotal / iter; String strToOut = func + ". Total time: " + String(timeTotal) + " microsec, AVG time: " + String(time1) + " microseconds. (iterations " + String(iter) + ")"; Serial.println(strToOut); } void analogWritePin3(int val) { // Предполагается, что при val==0 или val==255 задается не ШИМ, а LOW или HIGH sbi(TCCR2A, COM2B1); OCR2B = val; // устанавливаем уровень ШИМ }
20

Результат:

21
1
analogWritePin3(). Total time: 6920 microsec, AVG time: 0,69 microseconds. (iterations 10000)
22

Впечатляет? Нам удалось сократить время исполнения примерно в 7 раз — до 0,69 мкс. Но как видно из примера такое достижение, как и в случае с digitalRead()/digitalWrite(), далось в ущерб гибкости, понятности и универсальности. Там где скорость исполнения не в приоритете, имеет смысл оставить штатную функцию analogWrite().

24

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

  • Arduino: faster alternatives to digitalread() and digitalwrite()?
  • Arduino is Slow — and how to fix it!
  • Таймеры Atmega 328P
comments powered by HyperComments