Си. Особенности программирования в условиях низкой разрядности.

Микроконтроллер – универсальное устройство, содержащее в одном корпусе помимо процессора набор периферийных устройств, память программ и переменных. Своеобразный микрокомпьютер, позволяющий при минимальной внешней обвязке строить вычислительные системы. Но в виду требований к малой цене, низкому энергопотреблению производителям приходится ограничивать МК по частоте, разрядности (кроме 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-битных чисел:

Вычитание 16-битных чисел:

 В случае вычитания, видно двойное копирование регистров, которое вызывает затрачивание 2 лишних тактов на операцию. Обусловлено это отсутствием оптимизации, а также возможно специальной заточкой этих регистров для вычислений (подобное копирование будет также появляться при умножении).

Умножение и деление

Произведение зависит от аппаратного умножителя. При его наличии, операция получается путем перекрестных умножений и сложений байтов операндов – для 16 бит это 3 умножения и 3 сложения, результат 16-ти разрядный (зависит от компилятора, полный же диапазон такой операции 32 бита). Аналогично при умножении 32-х разрядных чисел, получается 32 разряда (вместо 64). Результат отсекается по разрядности операндов, что удобно для оптимизации и повышения быстродействия, но заводит в тупик новичков. Если необходим весь диапазон значений, то проблема решается приведением операндов к типу результата. Аппаратная операция занимает относительно небольшое время.

Если умножителя нет, то действие реализуется путем многократного двоичного сдвига со сложением. Число сдвигов и сложений пропорционально разрядности множителя. Время выполнения увеличивается заметно и становится непостоянным. Например, для 16-ти разрядных операндов, получается 16 циклов состоящих из: 2 сдвига вправо множителя, 1 проверка переноса, 4 сложения побайтно с результатом, если был перенос, и 5 сдвигами вправо результата побайтно на один цикл. В зависимости от числа единиц в множителе, цикл короче или длиннее на 5 сдвигов, что изменяет время выполнения.

Деление в простом случае реализуется простым циклом вычитания делителя из делимого с подсчетом числа вычитаний. Время выполнения операции в этом случае очень сильно зависит от операндов, особенно велико при малом делителе и большом делимом (самое большое при делении 255 на 1 при 8 битах). В компиляторах Си деление реализуется с помощью более сложных алгоритмов, которые выполняются быстрее решения в лоб, но сильно отстают от умножения в скорости.

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

Рассмотрим на примере WinAVR реализацию умножения при наличии аппаратной операции – умножим два 16-ти битных числа,  результат в 16-ти битной переменной.

Вещественные числа

Особняком стоит использование вещественных типов данных в МК. Из-за малой разрядности и отсутствия блоков обработки чисел с плавающей запятой, производительность вычислений сильно падает. Медленной скорости обработки способствует также упакованный формат хранения вещественного числа. Для их обработки компилятор подключает дополнительные библиотеки, и размер программ заметно увеличивается. Например, умножение и деление добавляют примерно по 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

Из таблицы видно, что аппаратное умножение повышает быстродействие почти в два раза. Также заметно, как много времени необходимо затратить на одно вычисление.
Выводы по библиотеке совпадают с мнением по вещественным числам – если без этих функций не обойтись, то придется смириться с потерей быстродействия.

Выводы

Микроконтроллеры довольно сильно ограничены в вычислительных возможностях низкими значениями разрядности арифметико-логических устройств и тактовой частоты. Компенсируется это хорошей оптимизацией алгоритмов вычислений, реализованных в компиляторах. Также ускорить работу программы могут сами программисты – минимизацией количества вещественных переменных и сложных математических функций, использованием целочисленных типов без избыточной разрядности, чисел с фиксированной запятой.

У этой записи 3 комментариев

  1. Ковыряя дизассемблированный код, с оптимизацией, нашел интересную замену операции сдвига влево и вправо регистра при сдвиге на 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 команд свдига.

  2. Тоже, весьма интересное применение 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;

  3. Найти количество чисел, попадающих в заданный диапазон в массиве 16-разрядных
    чисел со знаком

Имя (обязательно)Email (обязательно)Веб-сайт

Добавить комментарий