Работа с обычными переменными, включая структурные типы, довольно проста. А их низкоуровневая реализация уже немного затрагивалась мной в статьях посвященных режимам адресации. Там же я давал ссылку на статьи, где все это рассматривалось подробнее. А вот работа с отдельными битами тема более интересная.
Напрямую работать с отдельными битами приходится очень часто при разработке программ для микроконтроллеров. При этом обычные прикладные программисты на универсальных ЭВМ сталкиваются с этим довольно редко. А это иногда вызывает у них некоторые затруднения.Новичкам приходится особенно тяжело.
Сегодня мы будем рассматривать работу с битами на уровне языка С. При этом, как я говорил не раз, речь будет идти не об изучении языка С, а о практической реализации в компиляторах и влиянии на это архитектуры микроконтроллеров.
В статье будут использоваться "магические константы". Для простоты. Статья ни в коей мере не отражает и не продвигает какой либо стиль программирования. Если у вас лично, такие константы вызывают отторжение, то можете использовать, например, такой их эквивалент
0b00010000 <==> (1<<4) <==> (1 << bit4)
если ранее было определено bit4.
Специально обговариваю этот момент для пуристов С и блюстителей чистоты стиля. В статье приведены простейшие примеры фрагментов программ. Ни на что более не претендующие.
Есть две возможности работы битами на уровне стандарта С - использование логических побитовых операций и битовых полей. Однако, в компиляторах ориентированных на низкоуровневое и системное программирование зачастую предусматриваются и различные расширения языка существенно облегчающие работу с битами. И сегодня мы рассмотрим все три варианта.
Расширения языка
Я рассмотрю в качестве примера возможности только одного компилятора - CC5X для PIC. В этом компиляторе на базе подхода с битовыми полями реализовано расширение синтаксиса позволяющее легко адресовать любой бит любой переменной.
Просто после имени переменной, через точку (как при адресации поля структуры) указывается номер требуемого бита.
Кроме этого, есть возможность адресации отдельных байт и групп байт в переменных разрядностью более 8. Например, low8 выделяет младший байт переменной, а mid8 расположенный в середине (биты с 8 по 15) переменной. Полный список предопределенных диапазонов бит можно найти в документации на компилятор. При этом нет возможности задать свой диапазон. Например, нельзя получить доступ к битам с 3 по 7.
Кроме того, этот компилятор поддерживает битовые переменные (размером 1 бит) и удобную работу с ними.
А вот результат компиляции этого фрагмента программы.
Обратите внимание на использование команды BTFSC. Сначала бит 12 в var1 сбрасывается командой BCF. Затем, командой BTFSC, производится проверка bit_var и пропуск команды BSF, если bit_var равен нулю.
Небольшое лирическое отступление по мотивам обсуждения в комментариях к статье "Микроконтроллеры для начинающих. Часть 32. Мост к Си. Позиционная независимость". Кроме работы с битами данный пример показывает, что даже обычное присваивание может генерировать команды перехода. Причем далеко не всегда ситуация складывается так удачно. А значит, потенциальное влияние на позиционную независимость могут оказать самые обычные операции, которые, на первый взгляд, никакого отношения к передаче управления не имеют.
У СС5Х есть еще одно полезное расширение синтаксиса, возможность разделять точками группы бит в двоичных константах.
0b0001101011001110 => 0b000.1101.011.0011.10
Это позволяет зрительно выделить группировку бит.
Расширения синтаксиса в компиляторах полезны и удобны, но не всегда доступны. Кроме того, они бывают очень разными, поэтому рассмотреть их все не возможно. А раз так, давайте перейдем к предусмотренным стандартом С вариантам.
Побитовые логические операции и сдвиги
В языке С есть побитовые операции
& - побитовое И
| - побитовое ИЛИ
~ - инверсия (дополнение)
^ - исключающее ИЛИ
<< - сдвиг влево
>> - сдвиг вправо
Для тех, кто только начал изучать С сразу отмечу, что не нужно путать эти операции с чисто логическими &&, ||, !.
Давайте посмотрим на простой пример использования побитовых логических операций для управления отдельными битами в переменных.
Здесь показано использование всех побитовых операций. Следует учитывать, что в С нет отдельных операций для арифметических и логических сдвигов. Компилятор выберет правильный вариант исходя из того, знаковая переменная сдвигается, или без знаковая.
Но давайте посмотрим, какой код формирует компилятор для разных микроконтроллеров.
Здесь все просто и логично. Для установки бита использована команда BSF, а для сброс BCF. Обратите внимание, компилятор "понял", что побитовое И с инвертированным значением второго аргумента это сброс бита. Однако, если у нас в двоичной константе будет более одного единичного бита, то компилятор сформирует полноценные последовательности из команды загрузки константы и команды логической операции. Точно так же, как это сделано для операции инверсии бита в данном случае.
Команд сдвига в выбранном для примера микроконтроллере нет. И такой выбор сделан специально, что бы показать, как в такой ситуации поступает компилятор. Сдвиги он заменил на вращения, но они выполняются только через перенос. Поэтому были добавлены и команды сброса флага переноса.
Для STM8 тоже все просто и логично. Однако, обратите внимание на странности в работе генератора кода и оптимизатора. Для операции установки бита компилятор использовал целых три команды, хотя можно было обойтись одной BSET. Зато для сброса бита он выбрал правильную команду - BRES. Точно так же, для инверсии бита можно было использовать одну команду BCPL, а не три команды. В данном случае использовался компилятор sdcc, который умеет работать с несколькими архитектурами. Возможно именно с этим связаны такие причуды в генерации кода.
Зато в STM8 есть команды сдвигов, которые и использовал компилятор.
Для AVR компилятор avr-gcc использовал обычные команды логических операций. Почему так произошло я рассказывал в статье "Микроконтроллеры для начинающих. Часть 21. Команды манипуляции битами". В остальном, тут нет ничего особенного. Для сдвига использовались соответствующие команды, которые имеются в AVR.
Разумеется, этот же подход можно использовать и для установки не одного, а нескольких бит. При этом, как я уже говорил, компилятор будет формировать команды логических операций, а не команды манипуляции битами.
Однако, для работы с логически связанными группами бит все выглядит, пусть и совсем чуть чуть, но сложнее. Давайте рассмотрим некоторый управляющий регистр абстрактного микроконтроллера
Здесь есть логически связанная группа бит (2, 3, 4) которые делитель какого то устройства. Предположим, нам нужно занести в эти биты требуемое значение. Да, можно использовать манипуляцию отдельными битами, но лучше все таки семантику задачи.
Мы не можем просто взять и переписать часть переменной (часть байта, в данном случае). Мы сначала должны очистить ее, а потом установить в нужное значение. Пусть наш регистр называется some_reg. Тогда это будет делаться так
some_reg &= ~0b00011100;
some_reg |= new_value;
Есть возможность переложить это усложнение на плечи компилятора. И мы скоро увидим, как это можно сделать.
Однако, есть одна операция, которую нельзя легко задать на уровне С. Это операции вращения. Например, давайте посмотрим, как можно описать на С операцию вращения влево на один разряд, причем без участия переноса.
var = ((var >> 7) | (var << 1));
Все ли здесь верно? Первый сдвиг (вправо на 7 разрядов) дает нам в младшем разряде значение самого старшего бита. Второй сдвиг (влево на один разряд) дает нам верное значение результата вращения, но без самого младшего бита, который должен принять значение старшего бита. Наконец, операция побитового ИЛИ собирает все вместе и дает итоговый результат вращения без участия переноса.
Некоторые компиляторы могут распознать в подобной записи вращение и сгенерировать команду вращения вместо нескольких команд сдвига и логических операций. Однако, тут может возникнуть сложность, если команды вращения работают только с флагом переноса.
Почему авторы языка С, которые работали на машинах PDP, не сделали вращения частью языка, я не знаю. На машинах PDP команды вращений были. Да, вращения требуются реже, чем сдвиги. Но писать программы шифрования или вычисления контрольных сумм без этой возможности сложнее.
Битовые поля
Битовые поля это структура, только размер ее элементов задается в битах. Вот простейший пример фрагмента программы
А теперь посмотрим, какой код будет сгенерирован компиляторами для различных микроконтроллеров.
Как видно, для PIC результат не отличается от примера с использованием побитовых операций.
А вот здесь отличия уже есть. Теперь компилятору не нужно было задумываться на тем, что же имел ввиду программист - логическую операцию и битовую. К этому примеру для STM8 мы еще вернемся чуть позже.
Для AVR ничего не изменилось. Да и не могло измениться.
Но я ничего не сказал про инверсию бита при использовании битовых полей. Если вспомнить использование побитовых операций, то для инверсии нужно использовать исключающее ИЛИ. Другими словами, что то вроде такого
bit_var.bit5 ^= 1;
И это действительно работает так, как и ожидалось при использовании компилятора CC5X для PIC. А вот sdcc и avr-gcc генерируют совсем не такой код, как ранее
Позвольте оставить этот код без комментариев... Но это является еще одним примером того, что используемый компилятор нужно хорошо знать, а сформированный им код весьма полезно изучать.
Для STM8 чудес не меньше, чем для AVR. Я тоже оставлю это без комментариев...
Но на самом деле объяснение такому поведению компиляторов есть. Просто они не воспринимают такую операцию как битовую, а значит, они сначала превращают наше битовое поле (элемент структуры) в целое число, с которым и выполняется операция. И это не смотря на то, что слева стоит константа, причем с единственным не нулевым битом. Причем обратите внимание, и avr-gcc, и sdcc для работы использовали команды работы с регистром состояния. Для AVR это bld, а для STM8 это bccm.
Но ведь и другой вариант
bit_var.bit5 = ~bit_var.bit5;
или
bit_var.bit5 = !bit_var.bit5;
Увы, это тоже не поможет. Для CC5X это является ограничением компилятора. А для avr-gcc и sdcc по той же самой причине - компилятор, а точнее оптимизатор, не воспринимает это как битовый оператор.
Зато работа с логически сгруппированными битами (вспомните пример с неким абстрактным регистром) будет выглядеть проще.
Здесь, как и говорил ранее, мы перекладываем дополнительную работу на плечи компилятора. И сгенерированным им код в точности соответствует тому, что мы писали с использованием С
К сожалению, с СС5Х этот фокус не пройдет, так как мы упираемся в ограничение компилятора.
Ни сдвиги, ни вращения для битовых полей невозможны.
Влияние системы команд процессора
Собственно говоря, мы уже видели, как система команд влияет на генерацию кода. Нет, речь не идет о том, что команды имеют разные имена. Речь идет о том, что некоторые команды могут быть недоступны. Например, отсутствуют команды сдвигов. Но может быть и более тонкая ситуация. Команда может существовать, но с ограничениями по режимам адресации.
Я рассмотрю этот момент только для STM8. Так как в интернете не редко встречаются не совсем верные выводы о работе компиляторов для него при использовании SPL.
Вот небольшой пример, который я и буду рассматривать
Примерно так, как здесь описывается PA_ODR и описаны регистры STM8 в SPL. Да, SPL предполагает, что вместо низкоуровневого доступа к регистрам программист использует специальные функции. Но кого это останавливает?
PAD_ODR здесь описан как указатель на переменную типа bits8_t расположенную по адресу 0x5000. PB_ODR как разыменованный указатель, то есть, как уже собственно переменная. Разница между этими описаниями безусловно есть, и большая. Но нам интересно, как это отразится на сгенерированном коде.
PC_ODR описана как обычная переменная, с модификатором volatile, поскольку соответствует регистру оборудования. Адрес регистра задается с помощью специального модификатора __at.
А теперь давайте посмотрим на результат компиляции sdcc
Код для PA_ODR и PB_ODR идентичный. Если честно, оптимизатор тут мог бы сработать лучше. Но винить sddc не стоит, это не очередная его проблема. Точно такой же год генерирует и Cosmic. А теперь посмотрите на то, насколько короче получился код для PC_ODR, всего одна команда! Давайте попробуем разобраться, почему так получилось.
А все дело в том, что для команды BSET допустима только прямая адресация адреса байта в памяти и непосредственная для номера бита. Поэтому для PA_ODR просто невозможно использовать BSET, так здесь требуется косвенная адресация. Поэтому и используется регистр Х для задания адреса для получения и сохранения переменной и команда OR для установки бита.
В случае с PB_ODR мы используем уже разыменованный указатель, поэтому оптимизатор мог бы и заметить это и заменить косвенную адресацию на прямую. Это бы позволило использовать команду BSET и значительно сократить код. Видимо разработчики sdcc и Cosmic посчитали такой вариант использования указателя на фиксированный адрес не самым распространенным, что бы уделять ему внимание в оптимизаторе.
А вот PC_ODR это обычная переменная со статическим размещение по заданному адресу. Поэтому компилятор легко использует команду BSET.
Кстати, этот пример позволяет сделать еще один важный вывод. В случае с STM8 работа с битами в локальных переменных будет не самой быстрой и короткой, так как они не имеют постоянного адреса и доступ к ним осуществляется с помощью косвенной адресации. А это, как мы уже видели, исключает использование команд BSET и BRES.
Аналогичное влияние специфики системы команд процессора можно найти и для других микроконтроллеров.
Заключение
Так какой же способ работы с битами в программах на С предпочтительнее? Однозначного ответа не существует. Все зависит от компилятора и требуемых операций с переменной. В особо критичных ситуациях возможно и применение встроенного ассемблера. Но и тут надо учитывать много нюансов, например, использование регистров процессора компилятором. Кроме того, это может повлиять на работу оптимизатора.
Как видно, работа с отдельными битами на С вполне возможна и не столь сложна, как некоторым кажется. Просто нужно быть внимательным и знать особенности своего компилятора и процессора. Но вы же знали, на что идете начиная изучать программирование для микроконтроллеров или системное программирование?
В статье я ни разу не упомянул компилятор ХС8. Дело в том, что в бесплатных вариантах он формирует далеко не самый удачный код. А официальная лицензия стоит не дешево и вряд ли новичкам имеет смысл ее приобретать. Пр этом этот компилятор является пожалуй самым удачным в плане универсальности и заложенных в нем возможностей. А это делает его легким для освоения новичками. Но ровно до того момента, пока программы являются лишь простыми примерами, которые легко (и с запасом) помещаются в памяти микроконтроллера, а к скорости их работы нет почти никаких требований.
До новых встреч!