Микроконтроллер – универсальное устройство, содержащее в одном корпусе помимо процессора набор периферийных устройств, память программ и переменных. Своеобразный микрокомпьютер, позволяющий при минимальной внешней обвязке строить вычислительные системы. Но в виду требований к малой цене, низкому энергопотреблению производителям приходится ограничивать МК по частоте, разрядности (кроме 32 разрядных ARM и 16 MSP430), сложности ядра (входят ли в состав аппаратное умножение, конвейеры, модули работы с числами с плавающей запятой или ОЗУ). Поэтому при разработке программ на Си возникают определенные нюансы, которые и рассмотрим по отношению к распространенным 8-ми разрядным МК (AVR, PIC).
Разрядность данных
Типы данных, умещающиеся в разрядность, могут быть использованы без каких-либо проблем. Все 8-и битные типы приведены в таблице 1.
Таблица 1 – Стандартные 8-битные типы данных
Название типа | Знак | Диапазон значений |
unsigned char | без знака | 0..255 |
char | со знаком | -128..127 |
Если в вычислениях превышается диапазон, то результат автоматически отсекается до 8-ми разрядов, например от суммы 250 + 15 = 265, при отсечении остается только 265 – 256 = 9.
С увеличением разрядности операндов у МК возникает необходимость проводить дополнительные вычисления над всеми байтами переменных. Из чего следует рекомендация не использовать лишнюю разрядность (например в циклах до 255 повторений нет смысла применять более однобайтовой переменной для счетчика циклов). Стандартные для всех компиляторов Си типы переменных повышенной разрядности приведены в таблице 2 (повышенной по отношению к рассматриваемым МК).
Таблица 2 – Стандартные многоразрядные типы данных
Название типа | Разрядность | Знак | Диапазон значений |
unsigned int | 16 | без знака | 0..65535 |
int | 16 | со знаком | -32768..32767 |
unsigned long | 32 | без знака | 0..4294967295 |
long | 32 | со знаком | -2147483648..214783647 |
unsigned long long | 64 | без знака | 0.. 18446744073709551615 |
long long | 64 | со знаком | -9 223 372 036 854 775 808.. 9 223 372 036 854 775 807 |
float | 32 | со знаком | ±1,175*10-38.. ±3,402*1038 |
double | 64 | со знаком | ±2,2*10-308.. ±1,8*10308 |
Сложение и вычитание
Сложение происходит побайтно, начиная с простого сложения младших и с учетом флага переноса для последующих. Вычитание происходит аналогично. Эти операции затрачивают немного процессорного времени, особенно если в МК есть команды сложения и вычитания с учетом переноса.
Для примера рассмотрим реализацию сложения и вычитания в компиляторе Си для AVR – WinAVR. Компиляция производится с отключением оптимизации, что позволяет получить дизассемблированный код в Proteus в читаемом виде.
Сложение двух 16-битных чисел:
1 2 3 4 5 6 7 8 9 10 11 12 | Си код: a = b + c; Диззассемблированный код: LD R18, Y+17 // загрузить мл. байт первого операнда из ОЗУ LD R19, Y+18 // загрузить ст. байт первого операнда из ОЗУ LD R24, Y+15 // загрузить мл. байт второго операнда из ОЗУ LD R25, Y+16 // загрузить ст. байт второго операнда из ОЗУ ADD R24, R18 // сложить младшие байты ADC R25, R19 // сложить старшие с учетом переноса ST Y+14, R25 // сохранить результат в ОЗУ ST Y+13, R24 |
Вычитание 16-битных чисел:
1 2 3 4 5 6 7 8 9 10 11 12 13 | Си код: a = b - c; Диззассемблированный код: LD R18, Y+17 // загрузить мл. байт первого операнда из ОЗУ LD R19, Y+18 // загрузить ст. байт первого операнда из ОЗУ LD R24, Y+15 // загрузить мл. байт второго операнда из ОЗУ LD R25, Y+16 // загрузить ст. байт второго операнда из ОЗУ MOVW R21:R20, R19:R18 // копирование регистров во временные регистры SUB R20, R24 // вычесть младшие байты SBC R21, R25 // вычесть старшие с учетом переноса MOVW R25:R24, R21:R20 // копирование результата из временных регистров ST Y+14, R25 // сохранить результат в ОЗУ ST Y+13, R24 |
В случае вычитания, видно двойное копирование регистров, которое вызывает затрачивание 2 лишних тактов на операцию. Обусловлено это отсутствием оптимизации, а также возможно специальной заточкой этих регистров для вычислений (подобное копирование будет также появляться при умножении).
Умножение и деление
Произведение зависит от аппаратного умножителя. При его наличии, операция получается путем перекрестных умножений и сложений байтов операндов – для 16 бит это 3 умножения и 3 сложения, результат 16-ти разрядный (зависит от компилятора, полный же диапазон такой операции 32 бита). Аналогично при умножении 32-х разрядных чисел, получается 32 разряда (вместо 64). Результат отсекается по разрядности операндов, что удобно для оптимизации и повышения быстродействия, но заводит в тупик новичков. Если необходим весь диапазон значений, то проблема решается приведением операндов к типу результата. Аппаратная операция занимает относительно небольшое время.
Если умножителя нет, то действие реализуется путем многократного двоичного сдвига со сложением. Число сдвигов и сложений пропорционально разрядности множителя. Время выполнения увеличивается заметно и становится непостоянным. Например, для 16-ти разрядных операндов, получается 16 циклов состоящих из: 2 сдвига вправо множителя, 1 проверка переноса, 4 сложения побайтно с результатом, если был перенос, и 5 сдвигами вправо результата побайтно на один цикл. В зависимости от числа единиц в множителе, цикл короче или длиннее на 5 сдвигов, что изменяет время выполнения.
Деление в простом случае реализуется простым циклом вычитания делителя из делимого с подсчетом числа вычитаний. Время выполнения операции в этом случае очень сильно зависит от операндов, особенно велико при малом делителе и большом делимом (самое большое при делении 255 на 1 при 8 битах). В компиляторах Си деление реализуется с помощью более сложных алгоритмов, которые выполняются быстрее решения в лоб, но сильно отстают от умножения в скорости.
В некоторых применениях можно ускорить деление. Часто это происходит при необходимости усреднять данные. В таком случае удобно собирать и суммировать данные в количестве кратном степени двойки, а затем выполнять операцию сдвига вправо над суммой.
Рассмотрим на примере WinAVR реализацию умножения при наличии аппаратной операции – умножим два 16-ти битных числа, результат в 16-ти битной переменной.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Си код: a = b * c; Диззассемблированный код: LD R18, Y+25 // загрузить мл. байт первого операнда из ОЗУ LD R19, Y+26 // загрузить ст. байт первого операнда из ОЗУ LD R24, Y+23 // загрузить мл. байт второго операнда из ОЗУ LD R25, Y+24 // загрузить ст. байт второго операнда из ОЗУ MOVW R21:R20, R25:R24 // копирование во временные регистры MUL R18, R20 // перемножение мл. байтов, результат в R1:R0 MOVW R25:R24, R1:R0 // сохранение результата MUL R18, R21 // перемножение мл. 1-го и ст. 2-го байтов операндов ADD R25, R0 // промежуточное сложение MUL R19, R20 // перемножение ст. 1-го и мл. 2-го байтов операндов ADD R25, R0 // промежуточное сложение CLR R1 // очистка регистра ST Y+22, R25 // сохранить результат в ОЗУ ST Y+21, R24 |
Вещественные числа
Особняком стоит использование вещественных типов данных в МК. Из-за малой разрядности и отсутствия блоков обработки чисел с плавающей запятой, производительность вычислений сильно падает. Медленной скорости обработки способствует также упакованный формат хранения вещественного числа. Для их обработки компилятор подключает дополнительные библиотеки, и размер программ заметно увеличивается. Например, умножение и деление добавляют примерно по 400-500 байт кода подпрограмм в WinAVR для Atmega 128.
Это не означает, что такие переменные категорически нельзя использовать, просто следует учитывать их влияние на размер и скорость работы программы. Бывает что необходимы в особо точные расчеты, или быстродействие не так важно. В простых случаях рекомендуется использовать искусственную запятую – фиксированную (что часто применяется в отображении информации в промышленности на панелях операторов). Для этого число условно делят на целую и дробную часть, вычисления производят с учетом коррекций на введенную запятую. Так, для 8-битного числа можно взять диапазоны 0.0..25.5 или -12.8..+12.7, которые подойдут для хранения значений напряжений. Все вычисления над такими числами будут целочисленные и быстрые.
Математическая библиотека
Для вычисления логарифмических, тригонометрических, степенных функций, в языке Си предусмотрена математическая библиотека math. Алгоритмы расчетов циклические, итеративные, выполняются в вещественных числах, что замедляет программу. Для ориентира можно привести документацию к WinAVR. Там приводится среднее время выполнения функций в циклах, для ядер с поддержкой и без поддержки аппаратного умножения. Часть данных представлена в таблице 3.
Таблица 3 – Время выполнения некоторых функций в циклах МК.
Функция | Действие | Нет умножителя | Умножитель есть |
sin(1.2345) | синус | 3353 | 1653 |
tan(1.2345) | тангенс | 4381 | 2426 |
log10(1.2345) | десятичный логарифм | 4498 | 2134 |
sqrt(1.2345) | корень квадратный | 494 | 492 |
pow(1.234, 5.678) | возведение в степень | 9293 | 5047 |
Из таблицы видно, что аппаратное умножение повышает быстродействие почти в два раза. Также заметно, как много времени необходимо затратить на одно вычисление.
Выводы по библиотеке совпадают с мнением по вещественным числам – если без этих функций не обойтись, то придется смириться с потерей быстродействия.
Выводы
Микроконтроллеры довольно сильно ограничены в вычислительных возможностях низкими значениями разрядности арифметико-логических устройств и тактовой частоты. Компенсируется это хорошей оптимизацией алгоритмов вычислений, реализованных в компиляторах. Также ускорить работу программы могут сами программисты – минимизацией количества вещественных переменных и сложных математических функций, использованием целочисленных типов без избыточной разрядности, чисел с фиксированной запятой.
LrStein
25 Фев 2014Ковыряя дизассемблированный код, с оптимизацией, нашел интересную замену операции сдвига влево и вправо регистра при сдвиге на 4 и более шагов. Необходимо применить команду смены тетрад регистра, и очистить незначащие биты командой И с константой. Это можно реализовать на PIC и AVR:
например сдвиг направо на 5 шагов AVR:
SWAP R16 // меняем тетрады местами HL -> LH
ANDI R16, 0b00000111 // обнуляем незначащие 5 бит
сдвиг влево на 4 шага PIC:
SWAPF REG, 0 // меняем тетрады местами HL -> LH
ANDLW B’11110000′ // обнуляем незначащие 4 бита
MOVWF REG
как видно операция сокращается по времени, из-за того что не нужно писать подряд 4-7 команд свдига.
Magomedoff
24 Окт 2014Тоже, весьма интересное применение XOR:
Обмен значений переменных без использования дополнительной переменной
С использованием операции XOR можно реализовать обмен значений однотипных пременных без использования дополнительной переменной:
int x = 5, y = 7;
x = x^y; // x == 2
y = x^y; // y == 5
x = x^y; // x == 7
или в более короткой записи:
y ^= (x ^= y);
x ^= y;
ричард дансо
4 Окт 2018Найти количество чисел, попадающих в заданный диапазон в массиве 16-разрядных
чисел со знаком