01 |
Содержание:
|
|
02 |
Если вы попали на эту страницу, значит вас перестало устраивать быстродействие вашей платы Arduino, вы понимаете, что что-то не так, но не можете понять что. Именно для вас эта статья. |
|
03 |
Может я сделаю открытие, но стандартные для Arduino-версии языка C++ самые распространенные функции pinMode, digitalWrite, digitalRead, analogWrite, analogRead, Serial.print, Serial.println (и другие) — это всего лишь удобные обертки для тех, кто не желает лезть в дебри программирования. Эти функции-обертки содержат определенные механизмы проверок корректности исполнения, которые увеличивают время получения результата в разы! |
|
04 | На заметку: |
Исходные коды функций можно найти в папке {Program Files}\Arduino\hardware\arduino\avr\cores\arduino в соответствующих файлах:
|
|
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 |
|
|
15 |
Каждый порт управляется 3 регистрами: |
|
16 |
|
|
17 | На заметку: |
Для лучшего понимания того, что будет происходить дальше, лучше ознакомиться со статьей об операциях над битами.
|
|
18 |
Для того, чтобы использовать какой-либо регистр необходимо к регистру (DDR, PORT, PIN) добавить букву порта (B, C, D), и полученной переменной присвоить битовое значение (байт), в котором каждый бит будет отвечать за отдельный пин-выход: 1 2 DDRD = B11111110; // Регистр DDR, порт D
PORTB &= B00000011; // Регистр PORT, порт B |
|
19 |
Предостережение! Перед тем как начать играться с регистрами необходимо предостеречь по некоторым моментам:
|
|
20 |
Преимущества подхода! Имеет смысл рассмотреть вариант использования этого метода в ряде случаев:
|
|
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 |
Если очень хочется... Для того, чтобы все-таки максимально обезопасить себя от неприятностей при использовании этого метода, можно использовать:
|
|
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 |
Таким образом появляется возможность влиять «точечно» только на один конкретный бит, не затрагивая прочие биты регистра. |
|
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)) |
|
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) |
|
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(). |
|
66 |
Что почитать: |
|
67 |
Похожие запросы:
|
|