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

Тюнинг Arduino или ускоряем работу платы

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

Если вы попали на эту страницу, значит вас перестало устраивать быстродействие вашей платы Arduino, вы понимаете, что что-то не так, но не можете понять что. Именно для вас эта статья.

03

Может я сделаю открытие, но стандартные для Arduino-версии языка C++ самые распространенные функции pinMode, digitalWrite, digitalRead, analogWrite, analogRead, Serial.print, Serial.println (и другие) — это всего лишь удобные обертки для тех, кто не желает лезть в дебри программирования. Эти функции-обертки содержат определенные механизмы проверок корректности исполнения, которые увеличивают время получения результата в разы!

04 На заметку:
Исходные коды функций можно найти в папке {Program Files}\Arduino\hardware\arduino\avr\cores\arduino в соответствующих файлах:
  • pinMode, digitalWrite, digitalRead — файл wiring_digital.c
  • analogWrite, analogRead — файл wiring_analog.c
  • shiftOut — файл wiring_shift.c
и т.д.
05 На заметку:
Частота платы Arduino Uno 16 МГц. Это означает, что плата за 1 секунду совершает 16 000 000 тактов. На один такт уходит — 0,0625 мкс (микросекунд, в 1 секунде 1 000 000 микросекунд) — 62,5 наносекунд!
06

Проведем небольшой замер производительности, напишем скетч, который измерит среднее время исполнения каждой из указанных функций. Этот скетч выполняется на плате Arduino Uno, к которой не подключено ни одного устройства, только питание:

07 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
int pinIn = 13; // Пин проверки int pinOut = 12; // Пин проверки int clockPin = 11; //Пин shiftOut unsigned long _time; String strToOut = ""; int count=10000; void setup() { Serial.begin(9600); // будем выводить информацию о тестировании pinMode(pinOut, OUTPUT); pinMode(pinIn, INPUT); pinMode(clockPin, INPUT); } void loop() { testDWrite(pinOut); testDRead(pinIn); testARead(pinIn); testAWrite(pinOut); testShiftOut(pinOut); delay(1000); } //========================================================= void testDWrite(int pin) { _time = micros(); for (int i = 0; i < count; i++) { digitalWrite(pin, HIGH); digitalWrite(pin, LOW); } showResult ("digitalWrite()", _time, count*2 ); } //========================================================= void testDRead(int pin) { _time = micros(); int k = 0; for (int i = 0; i < count; i++) { k = digitalRead(pin); } showResult ("digitalRead()", _time, count ); } //========================================================= void testARead(int pin) { _time = micros(); int k = 0; for (int i = 0; i < count; i++) { k = analogRead(pin); } showResult ("analogRead()", _time, count ); } //========================================================= void testAWrite(int pin) { _time = micros(); for (int i = 0; i < count; i++) { analogWrite(pin, 100); } showResult ("analogWrite()", _time, count ); } //========================================================= void testShiftOut(int pin) { _time = micros(); for (int i = 0; i < count; i++) { shiftOut(pin,clockPin, LSBFIRST, B11110000); } showResult ("shiftOut()", _time, count ); } //========================================================= void showResult (String func, long timeStart, int iter) { unsigned long timeTotal = micros() - _time; unsigned long time1 = timeTotal / iter; strToOut = func + ". Total time: " + String(timeTotal) + " microsec, AVG time: " + String(time1) + " microseconds. (iterations " + String(iter) + ")"; Serial.println(strToOut); }
08

Результаты тестирования выглядят следующим образом:

09
1
2
3
4
5
digitalWrite() Total time: 109456 microsec, AVG time: 5.47 microseconds. (iterations 20000) digitalRead() Total time: 46284 microsec, AVG time: 4.63 microseconds. (iterations 10000) analogRead() Total time: 1120048 microsec, AVG time: 112.00 microseconds. (iterations 10000) analogWrite() Total time: 133840 microsec, AVG time: 13.38 microseconds. (iterations 10000) shiftOut() Total time: 1692008 microsec, AVG time: 169.20 microseconds. (iterations 10000)
10

На фоне длительности такта 0,0625 мкс, длительность самой быстрой функции digitalRead() — 4,63 мкс не кажется такой уж впечатляющей.

11

Манипуляции с портами Arduino — низкоуровневый доступ

Начнем с теории...

12 На заметку:
В этом разделе будет приведена вольная трактовка статьи о манипуляциях с портами платы Arduino — Port Maniulation
13

На самом деле у языка Arduino есть функции по низкоуровневому взаимодействию с пинами микроконтроллера Atmega 328P (в т.ч. Arduino Uno) в обход стандартных функций, но перед тем как к ним перейти нужно рассказать о том как они работают. Чип Atmega имеет 3 порта: B, C, D — каждый отвечает за свою группу пинов:


Распиновка контроллера Atmega168 (совпадает с Atmega328 — Arduino Uno)
14
B цифровые пины 8...13 (два старших бита 6 и 7 — не используются)
C аналоговые пины A0...A5
D цифровые пины 0...7 (нужно помнить, что пины 0 (RX) и 1 (TX) используются для программирования микроконтроллера)
15

Каждый порт управляется 3 регистрами:

16
DDR регистр управляет режимами пина INPUT/OUTPUT — аналог функции pinMode()
PORT регистр управляет значениями пина — считывает/записывает — HIGH/LOW — аналог функции digitalWrite()
PIN считывает значения с пинов, режим которых установлен как INPUT — аналог функции digitalRead()
17 На заметку:
Для лучшего понимания того, что будет происходить дальше, лучше ознакомиться со статьей об операциях над битами.
18

Для того, чтобы использовать какой-либо регистр необходимо к регистру (DDR, PORT, PIN) добавить букву порта (B, C, D), и полученной переменной присвоить битовое значение (байт), в котором каждый бит будет отвечать за отдельный пин-выход:

1
2
DDRD = B11111110; // Регистр DDR, порт D PORTB &= B00000011; // Регистр PORT, порт B

19
Предостережение! Перед тем как начать играться с регистрами необходимо предостеречь по некоторым моментам:
  • код становится нечитаемым и сложным в отладке, не говоря уже об изучении вашего кода сторонними программистами
  • код становится непереносимым, так как распиновка и расположение регистров и портов может сильно различаться между разными микроконтроллерами — код придется переписывать при смене платформы
  • очень опасно (в данном случае) использовать порт D, ведь если случайно назначить 0 пину режим OUTPUT, то вероятно вы уже не сможете заливать скетчи в вашу плату.
20
Преимущества подхода! Имеет смысл рассмотреть вариант использования этого метода в ряде случаев:
  • конечно, первая причина указана в заголовке статьи — это скорость;
  • если нужно переключать одновременно пины, принадлежащие одному порту — использование функций digitalWrite() неминуемо вызовет задержку между переключениями пинов, а операции над портом позволят это реализовать;
  • если нужно сократить объем программного кода.
21

Покажем на примере, как обращаться с регистрами и портами.

22 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void setup() { // назначем пины 1 (serial transmit) и 2..7 как Output, // но оставляем пин 0 (serial receive) как Input // (в противном случае serial port перестанет работать!) ... // Регистр DDR - дописываем букву порта - DDRD, DDRB DDRD = B11111110; // цифровые пины 7,6,5,4,3,2,1,0 // назначаем 8..13 как Output... DDRB = B00111111; // цифровые пины -,-,13,12,11,10,9,8 // выключаем пины 2..7 (аналог DigitalWrite(pin, LOW)) PORTD &= B00000011; // выключаем 2..7, а пины 0 и 1 не трогаем (логическое И) // Включаем/отключаем напряжение на пинах 8..13... PORTB = B00111000; // включаем 13,12,11; выключаем 10,9,8 }
23

Если очень хочется...

Для того, чтобы все-таки максимально обезопасить себя от неприятностей при использовании этого метода, можно использовать:

  • для включения (HIGH) бита регистра порта — операцию побитового ИЛИPORTB |= B00100000; (при побитовом ИЛИ ноль не меняет значения бита, а 1 — присваивает биту значение единицы),
  • для выключения (LOW) бита регистра порта — операцию побитового ИPORTB &= ~B00100000; (при побитовом И единица не меняет значения бита, а 0 — присваивает биту нулевое значение).

24

В данном случае операция контроллируется битовой маской (B00100000) соответствующей выбранному пину. Во втором случае: PORTB &= ~B00100000; для использования единой битовой маски используется побитовое НЕ, код эквивалентен: PORTB &= B11011111;

25 На заметку:
Маску в большинстве случаев удобнее задавать битовым сдвигом вида byte mask = (1 << номер_бита). Таким образом приведенные выше примеры приобретут вид:
PORTB |= (1 << 5);
PORTB &= ~ (1 << 5);
26 На заметку:
Для присвоения значения сразу нескольким битам с использованием операции битового сдвига можно воспользоваться логическим OR (|):
PORTB |= (1 << 5) | (1 << 2) | (1 << 0); // Присвоить значения 5, 2 и 0 битам
PORTB &= ~ ((1 << 5) | (1 << 2) | (1 << 0)); // Присвоить значения 5, 2 и 0 битам
27

Таким образом появляется возможность влиять «точечно» только на один конкретный бит, не затрагивая прочие биты регистра.

28
29

Также есть возможность обращаться к каждому из битов регистра по его буквенному наименованию. Для этого используется функция-макрос _BV, а её использование выглядит следующим образом:

1
2
PORT{letter} |= _BV(P{letter}{number}); // HIGH PORT{letter} &= ~ _BV(P{letter}{number}); // LOW

30 На заметку:
Макрос _BV следующего содержания:
1
#define _BV(bit) (1 << (bit))
конвертирует номер бита в байтовую маску. Информация на сайте Atmel — http://www.atmel.com/webdoc/...
31

Для нашего примера, воздействие на пин 13 со светодиодом, имя которого PB5 будет выглядеть следующим образом:

32 Arduino (C++)
1
2
PORTB |= _BV(PB5); // HIGH PORTB &= ~ _BV(PB5); // LOW
33

Как упростить?

Есть простой способ для того, чтобы устранить возможную путаницу — конструкция #define. Особенность конструкции заключается в том, что именованная строка заданная директивой #define будет подставляться компилятором вместо всех упоминаний имени этой строки в коде:

34 На заметку:
Почитать о директиве #define можно почитать на официальном сайте Arduino — #Define и на сайте Майкрософт, в документации к языку С++ — Директива #define (C/C++).
35 Arduino (C++)
1
2
3
4
5
6
7
8
9
// Синтаксис: // #define name string #define LED 15 void setup() { Serial.begin(9600); Serial.println(LED); // Компилятор видит как Serial.println(15); }
36

Но главная особенность этой директивы заключается в том, что параметром string этой директивы можно устанавливать выражения, которые будут исполняться при каждом упоминании имени этого выражения:

37 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
long i=0; #define LED i+=2 void setup() { Serial.begin(9600); Serial.println(LED); // Результат 2: LED это i+2 Serial.println(LED); // Результат 4: LED это i+2 Serial.println(LED); // Результат 6: LED это i+2 } void loop() { }
38

Ускоряем digitalWrite()

Перед тем как ускорять функцию, стоит проанализировать, что происходит в штатной функции digitalWrite(), и почему на её исполнение расходуется столько времени:

39 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
void digitalWrite(uint8_t pin, uint8_t val) { uint8_t timer = digitalPinToTimer(pin); // Вычисляется таймер пина - это необходимо для некоторых пинов, поддерживающих ШИМ uint8_t bit = digitalPinToBitMask(pin); // Вычисляется битовая маска для соответствующего пина uint8_t port = digitalPinToPort(pin); // Вычисляется порт, которому принадлежит пин volatile uint8_t *out; if (port == NOT_A_PIN) return; // Если задан некорректный пин - выходим // If the pin that support PWM output, we need to turn it off // before doing a digital write. if (timer != NOT_ON_TIMER) turnOffPWM(timer); // Если пин поддерживает ШИМ, то перед установкой значения нужно отключить таймер, генерирующий ШИМ out = portOutputRegister(port); // Конвертируем полученный порт в адрес uint8_t oldSREG = SREG; // SREG - регистр хранящий флаг прерываний, запоминаем его cli(); // Запрещаем прерывания if (val == LOW) { // Устанавливаем значения *out &= ~bit; } else { *out |= bit; } SREG = oldSREG; // Возвращаем регистру прерываний состояние до запрета прерываний }
wiring_digital.c
40

Как видите в коде происходит масса проверок и преобразований. Все это сделано для удобства конечного пользователя — но все это занимает ресурсы процессора и в итоге выливается в серьезное снижение производительности.

41

Вернемся к ускорению функции digitalWrite(). Теперь, по аналогии с приведенным выше кодом, мы вполне себе можем состряпать собственную библиотеку по ускорению работы платы Arduino. И приведем, конечно же, пример по миганию светодиода на пине D13:

42 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
byte pin13mask = (1 << 5); #define D13_SET_OUTPUT DDRB |= pin13mask // Устанавливаем пин D13 в режим OUTPUT #define D13_SET_INPUT DDRB &= ~pin13mask // Устанавливаем пин D13 в режим INPUT #define D13_WRITE_HIGH PORTB |= pin13mask // Устанавливаем бит отвечающий за пин D13 в HIGH, остальные пины не трогаем #define D13_WRITE_LOW PORTB &= ~pin13mask // Устанавливаем бит отвечающий за пин D13 в LOW, остальные пины не трогаем void setup() { Serial.begin(9600); D13_SET_OUTPUT; // Пин 13 в режим OUTPUT } void loop() { D13_WRITE_HIGH; // Зажигаем светодиод delay(300); D13_WRITE_LOW; // Выключаем светодиод delay(300); }
43

Теперь уже будет тяжело что-либо напутать.

44

Можно также использовать функцию bitWrite():

1
bitWrite(PORTB, 5, HIGH)
но поскольку это такая же обертка, как и digitalWrite(), результаты будут немного хуже.

45

Результат тестирования:

1
2
3
D13_WRITE...(). Total time: 9472 microsec, AVG time: 0,47 microseconds. (iterations 20000) 473.6 nanoseconds 9472 мкс вместо 109456 мкс (одна операция - 0,47 мкс вместо 5,47 мкс) - ускорение в 11.5 раз, на 91.35%

46

Ускоряем digitalRead()

C более быстрым эквивалентом функции digitalWrite() разобрались, теперь приступим к digitalRead(). Как было показано на примере выше значения состояний содержатся в байтах каждого регистра-порта. Остается только считать значение конкретного бита в этом байте. Сделать это можно при помощи функции bitRead():

47 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
byte pin13mask = B00100000; #define D13_SET_OUTPUT DDRB |= pin13mask // Устанавливаем пин D13 в режим OUTPUT #define D13_SET_INPUT DDRB &= ~pin13mask // Устанавливаем пин D13 в режим INPUT #define D13_WRITE_HIGH PORTB |= pin13mask // Устанавливаем бит отвечающий за пин D13 в HIGH, остальные пины не трогаем #define D13_WRITE_LOW PORTB &= ~pin13mask // Устанавливаем бит отвечающий за пин D13 в LOW, остальные пины не трогаем
#define D13_READ bitRead(PINB, 5) // Команда чтения 5-го бита - соответствует пину D13
void setup() { Serial.begin(9600); D13_SET_OUTPUT; // Пин 13 в режим OUTPUT } void loop() { D13_WRITE_HIGH; // Зажигаем светодиод Serial.println(D13_READ); // 1 delay(300); D13_WRITE_LOW; // Выключаем светодиод Serial.println(D13_READ); // 0 delay(300); }
48 На заметку:
Функции bitRead() и bitWrite() — именно тот случай, когда их низкоуровневая реализация не приведет к существенному увеличению производительности, так что в большинстве случаев проще использовать их именно в таком виде.
49

Ускоряем shiftOut()

Вопреки порядку, установленному в начале статьи — для улучшенного понимания и по возрастанию сложности — перескочим на функцию shiftOut():

50 На заметку:
Для некоторых функций (например shiftOut()) ускоренную версию можно написать, ознакомившись с её исходным кодом в папке C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino (для функции shiftOut() — файл wiring_shift.c)
51

Исходный код функции shiftOut() в файле wiring_shift.c:

52 Arduino (C++)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val) { uint8_t i; for (i = 0; i < 8; i++) { if (bitOrder == LSBFIRST)
digitalWrite(dataPin, !!(val & (1 << i)));
else
digitalWrite(dataPin, !!(val & (1 << (7 - i))));
digitalWrite(clockPin, HIGH);
digitalWrite(clockPin, LOW);
} }
wiring_shift.c
53

Теперь обратите внимание на строки 7, 9, 11, 12 — здесь мы видим, что функция digitalWrite() вызывается 24 раза (3 вызова в каждой из 8 итераций цикла for) — думаю, теперь понятно, откуда такая медлительность — 169 микросекунд.

54

Остается заменить каждое вхождение функции digitalWrite() на уже написанные директивы D13_WRITE_HIGH и D13_WRITE_LOW и готово. Для агрессивных ускорителей можно ещё посоветовать убрать из кода блок if () {...}.

55

Отдельно о процедуре loop()

В данной статье обязательно стоит упомянуть в контексте раскрываемой темы о специфике процедуры loop(). Дело в том, что исполнение функции loop() также не обходится без «пожирания» ресурсов. Сравним два очень похожих фрагмента кода:

56 Arduino (C++)
1
2
3
4
5
6
void setup() { } void loop() { digitalWrite(13, HIGH); digitalWrite(13, LOW); }
57 Arduino (C++)
1
2
3
4
5
6
7
8
void setup() { } void loop() { while(1) { digitalWrite(13, HIGH); digitalWrite(13, LOW); } }
58

По функционалу эти 2 фрагмента эквивалентны — в обоих крутится один и тот же бесконечный цикл. Сравним время затрачиваемое на выполнение этих фрагментов. для чистоты эксперимента заменим digitalWrite() на ускоренный аналог. Первый скетч — цикл loop():

59 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
byte pin13mask = (1 << 5); #define D13_WRITE_HIGH PORTB |= pin13mask // Устанавливаем бит отвечающий за пин D13 в HIGH, остальные пины не трогаем #define D13_WRITE_LOW PORTB &= ~pin13mask // Устанавливаем бит отвечающий за пин D13 в LOW, остальные пины не трогаем int pinOut = 12; // Пин проверки unsigned long _time; // Переменные для отслеживания времени исполнения unsigned long _timeOld; unsigned long _global; long ii = 0; long count = 100000; void setup() { pinMode(pinOut, OUTPUT); Serial.begin(9600); } void loop() { D13_WRITE_HIGH; D13_WRITE_LOW; _time = micros(); //Запоминаем время прошедщее с момента выполнения побочных операций _global += _time - _timeOld; ii++; if (ii == count) { showResult("WithLoop", _global, count); ii = 0; _global = 0; } _timeOld = micros(); //Начинаем следить за временем } void showResult (String func, long timeTotal, long iter) { unsigned long time1 = timeTotal / iter; String strToOut = func + ". Total time: " + String(timeTotal) + " microsec, AVG time: " + String(time1) + " microseconds. (iterations " + String(iter) + ")"; Serial.println(strToOut); }
60

Результат тестирования:

1
2
WithLoop. Total time: 457300 microsec, AVG time: 4,57 microseconds. (iterations 100000) В среднем на выполнение одной итерации уходит 4,6 микросекунды

61

Скетч с циклом while() (добавлено только 2 строчки):

62 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
byte pin13mask = (1 << 5); #define D13_WRITE_HIGH PORTB |= pin13mask // Устанавливаем бит отвечающий за пин D13 в HIGH, остальные пины не трогаем #define D13_WRITE_LOW PORTB &= ~pin13mask // Устанавливаем бит отвечающий за пин D13 в LOW, остальные пины не трогаем int pinOut = 12; // Пин проверки unsigned long _time; // Переменные для отслеживания времени исполнения unsigned long _timeOld; unsigned long _global; long ii = 0; long count = 100000; void setup() { pinMode(pinOut, OUTPUT); Serial.begin(9600); } void loop() {
while (1) {
D13_WRITE_HIGH; D13_WRITE_LOW; _time = micros(); //Запоминаем время прошедщее с момента выполнения побочных операций _global += _time - _timeOld; ii++; if (ii == count) { showResult("WithoutLoop", _global, count); ii = 0; _global = 0; } _timeOld = micros(); //Начинаем следить за временем
}
} void showResult (String func, long timeTotal, long iter) { unsigned long time1 = timeTotal / iter; String strToOut = func + ". Total time: " + String(timeTotal) + " microsec, AVG time: " + String(time1) + " microseconds. (iterations " + String(iter) + ")"; Serial.println(strToOut); }
63

Результат тестирования:

1
2
WithoutLoop. Total time: 383548 microsec, AVG time: 3,83 microseconds. (iterations 100000) В среднем на выполнение одной итерации уходит 3,8 микросекунды

64

Разница почти в 1 микросекунду!!! Мало? А если перевести в проценты — выигрыш почти на 20%. Так что, казалось бы, в одинаковом функционале тоже можно высвободить дополнительные ресурсы.

65

Ускоряем analogRead() и analogWrite()

C функциями analogRead() и analogWrite() все немного сложнее, чем в предыдущих примерах, поэтому об их ускорении написаны статьи — Arduino: ускоряем работу платы. Часть 2 - Аналого-цифровой преобразователь (АЦП) и analogRead() и Arduino: ускоряем работу платы. Часть 3 - analogWrite().

67

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

  • Arduino: faster alternatives to digitalread() and digitalwrite()?
  • Arduino is Slow — and how to fix it!
  • Управление портами через регистры Atmega
comments powered by HyperComments