Разобравшись с тем, как размещаются в памяти программы и данные и научившись работать с битами мы можем сделать следующий, весьма важный, шаг - научиться описывать в программах на С аппаратные регистры микроконтроллера.
Зачем это нужно, ведь всё уже описано в заголовочных файлах которые идут вместе с компилятором или доступны для скачивания с сайта производителя микроконтроллера?
Во первых, в комплекте с компилятором файлы описания регистров микроконтроллера идут не всегда. Во вторых, скачанные с сайта производителя файлы могут вызывать проблемы у используемого вами компилятора. И ярким тому примером может служить библиотека SPL для STM8, которая не только по условиям лицензии может использоваться не со всеми компиляторами, но и вызывает проблемы у SDCC.
В третьих, вам может попасться новый микроконтроллер, который похож на уже существующий (значит, компоновщик сможет собрать для него программу), но имеет дополнительные модули и несколько отличающиеся регистры. Ну и в четвертых, это просто полезно.
Не смотря на то, что в статье используется в качестве примера регистр из микроконтроллера STM8, я подробно рассмотрю все три семейства. И STM8, и PIC, и AVR. Наберитесь терпения и прочитайте статью до конца. Сравнение подходов для разных микроконтроллеров весьма интересно и показывает, что не надо зазубривать различные методы. Достаточно понять, и все станет легко и просто!
Рекомендую прочитать (или перечитать), если вы этого еще не сделали, статьи:
Наиболее важными являются части 28 и 33. Они понадобятся для понимания сегодняшней статьи.
Что бы избежать абстрактности давайте выберем "подопытный" регистр. В качестве такового выступит регистр ADC_CR1 из STM8S. Это управляющий регистр аналого-цифрового преобразователя располагающегося внутри микроконтроллера. Вот так выглядит его описание в документации
Внимание! Я не описываю в данной статье работу с АЦП. Регистр выбран только в качестве иллюстрации для описания в программе. Работа с АЦП будет описана в дальнейшем, в отдельных статьях.
Кроме описания отдельных бит и полей регистра здесь есть еще два важных момента. Во первых, это значение, которое регистр получает при сбросе микроконтроллера. Например, при включении питания. В данном случае, регистр обнуляется при сбросе. Во вторых, адрес регистра в адресном пространстве данных.
А вот с этого момента нам придется разделить различные семейства микроконтроллеров в отдельные разделы. В конце статьи мы снова сведем все воедино.
Самый простой случай - STM8
Если быть более точным, то здесь речь идет не об адресе, а о смещении адреса. Но смещение относительно чего? Для этого нам нужно определиться не только с семейством микроконтроллера, но и с конкретной моделью внутри семейства. Давайте выберем STM8S105 и откроем документацию на него.
Каждый модуль в STM8 имеет сгруппированный в единый блок набор регистров. И для каждого модуля в документации на конкретный микроконтроллер указывается базовый адрес этого блока. Да, во многих случаях базовый адрес блока регистров будет один и тот же для разных микроконтроллеров, но он имеет право быть и другим.
Для STM8S105 базовый адрес блока регистров АЦП (ADC) равен 0х53Е0. Если точнее, то это ADC1, поскольку в других микроконтроллерах АЦП может быть несколько. Наш регистр ADC_CR1 имеет смещение 0x21 от этого базового адреса блока регистров. А значит, реальный полный адрес регистра в адресном пространстве данных будет
0x53E0 + 0x21 = 0x5401
Если заглянуть документацию на микроконтроллер, в раздел "Memory and register map", подраздел " General hardware register map", и найти там ADC1, можно увидеть, что мы не ошиблись с определением адреса.
Пометки r/rw для бит и полей регистра показывают, доступен бит только для чтения, или для чтения и записи. Бывает еще вариант, что бит доступен только для записи. В нашем случае все биты, кроме зарезервированных (не реализованных и не используемых), доступны и для чтения и для записи.
Теперь мы готовы начать описывать регистр ADC_CR1 для использования в наших программах на С. Я буду использовать битовые поля для описания отдельных бит и групп бит регистра. Во первых, это нагляднее. Во вторых, компилятор SDCC отлично работает с битовыми полями. В третьих, нам не нужно выполнять для этого регистра операции сдвигов.
Здесь все очень просто, если вы знаете С и читали часть 33 цикла о работе с битами. Но одного такого описания нам недостаточно, так как иногда может потребоваться работа с регистром как с единым целым. Давайте добавим такую возможность.
Но у нас есть поле SPSEL которое определяет частоту работы АЦП. Вот какие значения это поле может принимать
Согласитесь, задавать значение поля в цифровом виде неудобно. Давайте определим символические константы для значений этого поля.
На первый взгляд можно определить символические константы примерно так
const unsigned F_MASTER_6=3;
Однако, SDCC при этом создает полноценную переменную F_MASTER_6, использование которой для присвоения значения полю SPSEL порождает излишний код, который занимает место и тратит время. Обратите внимание, что данное утверждение касается только SDCC, другой компилятор может повести себя и точно так же, и совершенно иначе. Например, оптимизатор может исключить выделение места под константу и использовать вместо нее литерал.
Поэтому, для SDCC, мы опишем перечисляемый тип для задания имен значениям поля
Если вы по каким либо причинам не любите перечисления, можете использовать директиву define препроцессора. Результат будет идентичный.
Теперь мы можем создавать переменные, по своей структуре соответствующие регистру ADC_CR1. Но реальный аппаратный регистр имеет четко определенный адрес, который будет явно отличаться от адреса размещения переменной компилятором.
И вот здесь я должен сделать небольшое отступление и сказать пару слов об SPL (Standard Peripherals Library) - стандартной библиотеке для STM8 предоставляемой фирмой STMicroelectronics. Я уже упоминал эту библиотеку.
Дело в том, что в данной библиотеке блок регистров каждого модуля описывается как структура, для которой и задается базовый адрес. То есть, в полном соответствии с собственной документацией. Но делается это в соответствии с базовым стандартом С. То есть, вот в таком стиле
#define ADC1 ((adc_cr1_t *) 0x53E0)
Как видно, ADC1 является указателем. Если посмотреть код, который SDCC генерирует для работы с регистрами (в данном случае, регистрами первого АЦП), то будет видно, что простая установка/сброс бита регистра производится довольно большим количеством команд, причем используются команды логических операций.
За такой код, вместо использования BSET/BRES компилятор SDCC часто ругают. Но компилятор тут не виноват! Точно такой же код сгенерирует и Cosmic, и другие компиляторы. Вспомните, что я говорил в предыдущей статье (часть 33, о работе с битами)
Все дело в том, что для команды BSET допустима только прямая адресация адреса байта в памяти и непосредственная для номера бита. Поэтому просто невозможно использовать BSET, так здесь требуется косвенная адресация.
Я честно предупреждал, что описанное в предыдущих статьях нам понадобится. Не смотря на кажущиеся скучность и теоретическую направленность в статьях нет лишней информации! Или почти нет.
В случае с SDCC (и с Cosmic!) не помогает даже явное разыменование указателя. Теоретически, оптимизатор мог бы заметить, что выполняется разыменование константного указателя на переменную и использовать сразу адрес переменной. Но это весьма частный случай стандартного использования указателей, поэтому оптимизатор это обрабатывать не обязан. Он и не обрабатывает.
Мы совершенно не обязательно должны следовать подходу SPL. Поэтому просто сразу опишем ADC_CR1 как переменную статически размещенную по заданному адресу. Для SDCC это можно сделать так
Раз мы определяем регистр первого АЦП, то и дадим ему соответствующее имя. Адрес размещения задается модификатором __at. Модификатор volatile нужен для того, что бы компилятор не строил никаких предположений о содержимом ADC1_CR1, а всегда считывал его реальное содержимое. И что бы сразу записывал в него все изменения. Регистр у нас аппаратный, а значит, может изменяться асинхронно и независимо от выполнения программы.
Все, теперь мы можем полноценно использовать результат наших трудов в реальной программе. Вот простейший пример
А вот код, который сформировал SDCC
Хорошо видно, что для установки бита CONT сгенерирована команда BSET, что является оптимальным вариантом. Присваивание регистру значения выполняется командой MOV, что так же является оптимальным вариантом. Присваивание значения полю SPSEL, как я и говорил в 33 статье цикла, требует дополнительных усилий. Для этого в регистр Х загружается адрес ADC1_CR1, а в аккумулятор маска для сброса битов поля SPSEL в ноль. Потом операция AND сбрасывает биты, результат оказывается в аккумуляторе. Наконец, операция OR устанавливает новое значение бит, после чего результат записывается обратно в ADC1_CR1.
Кстати, обратите внимание, что здесь я показал и задание сегмента (секции), в данном случае называемого областью (.AREA), для размещения кода.
Случай посложнее - PIC
Все выше описанное для STM8 полностью применимо к компилятору XC8. За тем исключением, что здесь адрес регистра не может быть таким большим. Поэтому просто примем, что он будет размещаться по адресу 0х91.
Наш пример прекрасно скомпилируется, а сгенерированный код будет таким
В целом, код такой же, как для STM8. Не смотря на то, что команды другие. Обратите внимание, что наш адрес 0х91 превратился в адрес 0х11, но в первом банке памяти данных. Это влияние банковой организации памяти данных. Не обращайте внимание на непоследовательную нумерацию строк. Я просто удалил пустые и незначащие строки, что бы не загромождать иллюстрацию.
А вот с компилятором СС5Х все гораздо печальнее. Он сразу же спотыкается на строке
unsigned : 2;
В чем же тут дело? Нет, проблема не в том, что поле структуры безымянное. Просто он не поддерживает битовые поля длиной более 1 бита.
Поэтому придется изменить подход и использовать расширения синтаксиса СС5Х. А заодно и препроцессор. Ничего особо страшного тут нет, на С получается примерно так
Обратите внимание, что адрес размещения переменной тут задается по иному, через символ @. Ну и биты регистра определяются с помощью расширенного синтаксиса компилятора. Кроме того, пришлось заменить ADON на _ADON, так как используемый для компиляции в примере микроконтроллер имел свой АЦП с точно таким же битом.
С полем SPSEL длиной 3 бита мы можем работать только с помощью логических операций и сдвигов. Что бы упростить эту работу я добавил пример двух макросов, для записи и чтения поля. Почему макросы, а не функции? Все очень просто - с функциями сгенерированный код получается больше. Причем не только из-за вызова/возврата, но и из-за обработки параметров и дополнительного переключения банков памяти по этой причине. Что бы показать чтение поля я добавил пример использования GetSPSEL.
Кстати, обратите внимание на константу 0х8F. Ее мы уже встречали в коде для STM8 и для PIC (XC8). Она используется для обнуления поля SPSEL.
Теперь CC5X прекрасно компилирует нашу программу, причем код получается вполне приличным. И точно таким же, какой генерировал XC8.
Размер кажется больше из-за добавленного чтения нашего регистра. Как видно, описание регистра получилось не столь простым и наглядным, как для STM8 или XC8, но и особо сложного ничего нет.
В чем то сложнее, в чем то нет - AVR
Дело в том, что наш регистр может оказаться и в адресном пространстве данных, и в адресном пространстве ввода-вывода. Сначала рассмотрим адресное пространство ввода-вывода.
Тут нам снова придется изменить адрес регистра, так как адресное пространство ввода-вывода имеет весьма ограниченный размер. Давайте примем, что наш регистр располагается по адресу 0х25. И мы получим вот такой пример на С
Здесь все так же, как для STM8 и XC8, но ощутимо изменился способ задания адреса размещения регистра. Теперь это делается так
volatile adc_cr1_t ADC1_CR1 __attribute__((io (0x25)));
-------- Корректировка от 30.09.2020 ------------------
Внимание! В данной статье я забыл сразу сказать, что avr-gcc при объявлении переменной в пространстве ввода-вывода вычитает из адреса 0х20. То есть, компилятор воспринимает адрес заданный параметром как адрес в пространстве данных и переносит его в пространство ввода-вывода с автоматической корректировкой учитывающей тот факт, что пространство ввода вывода располагается в пространства данных со смещением 0х20. Такое поведение компилятора надо учитывать так
volatile adc_cr1_t ADC1_CR1 __attribute__((io (0x25 + 0х20)));
Сгенерированный код от этого не изменится, но адреса в командах IN и OUT станут правильными.
Вспомните статьи, которые я рекомендовал прочитать ранее. В них я говорил о существовании модификатора __attribute__. В параметрах этого модификатора указывается адресное пространство, в данном случае io (ввод-вывод), и фактический адрес размещения.
А вот такой код будет сгенерирован компилятором avr-gcc
Я удалил строки пролога и эпилога функции main, поскольку речь сейчас о другом. Если не считать того, что установка бита CONT теперь выполняется командой ORI, а чтение и запись регистра выполняются командами IN и OUT, все работает так же, как и для других микроконтроллеров.
А если наш регистр расположился в пространстве данных? Нам придется изменить только одну строку. Ту, где задается адрес регистра. Теперь она будет такая
volatile adc_cr1_t ADC1_CR1 __attribute__((address (0x91)));
Здесь я снова использую адрес 0x91, как в примере для PIC. Я не буду приводить текст на С, так как в остальном он совсем не изменился. А вот и сгенерированный компилятором код
Вся разница с предыдущим случаем заключается в замене команд IN и OUT на команды LDS и STS.
Кстати, сам регистр определяется так
.globl ADC1_CR1
ADC1_CR1 = 145
145 десятичное соответствует 0х91, так что компилятор действительно все сделал так, как мы хотели. А в случае пространства ввода вывода адрес был равен 37, что что соответствует 0х25.
Заключение
Мы рассмотрели три существенно различающихся семейства микроконтроллеров. И что оказалось? Описание регистров оборудования практически идентичны, за исключением различия в задании адреса размещения. Но это зависит от компилятора, а не от микроконтроллера.
Исключением является компилятор СС5Х, где пришлось применять совсем иной подход. Но я уже говорил, что это довольно специфический компилятор, ориентированный именно на PIC. И он очень хорошо подходит для разработки встраиваемых систем. А генерируемый им код можно считать образцовым в большинстве случаев. Особенно, если пользоваться лицензированной версией, в которой оптимизатор прекрасно вычищает лишний код. Да, лицензированная версия не бесплатна, но гораздо дешевле, чем XC8.
Мы наглядно увидели, что описание аппаратных регистров не только почти идентично, но и не является сложным. Можно ли было сделать все по иному? Да, можно.
Из примера для CC5X видно, что вполне возможно, и легко, использовать логические операции обернув их макросами. А отсутствие таких удобных расширений синтаксиса можно компенсировать с помощью определений препроцессора и макросов типа bit_set и bit_clr, которые не сложно написать. И есть любители именно такого подхода.
Есть и еще один способ. Вообще скрыть структуру регистра и предоставив для работы с ним набор функций. Например, ADC_Start, ADC_GetResult, ADC_SelectChannel. Примерно такой подход использует SPL для STM8, только структура регистров там не скрывается.
В следующий раз мы отойдем от вопросов программирования и познакомимся с некоторыми электрическими параметрами микроконтроллеров. Их тоже нужно не только знать, но и понимать.
До новых встреч!