Мой способ организации портов ввода вывода на c++. Часть 1
Приветствую, этой заметкой я начинаю серию постов о своем опыте изучения микроконтроллерных систем, возникающих передо мной задачах, способах их решения или просто мыслях на этот счет.
Никакая реальная микроконтроллерная система невозможна без взаимодействия с периферией. Любые периферийные устройства так или иначе управляются микроконтроллером через его порты ввода вывода общего назначения. Конечно, любой компилятор для микроконтроллера из коробки предоставляет возможность управления портами: чтение, запись, настройка на ввод либо вывод и т.п., однако мне оказалось этого недостаточно. Хочется:
- абстрагировать задачи, решаемые микроконтроллером от конкретных портов ввода вывода;
- изменять биты портов, используемых задачей в одном месте (обычно функция main)(корень композиции) без изменения кода самой задачи;
- было бы неплохо вообще отвязать задачи от конкретной серии микроконтроллера, в идеале, не теряя при этом производительности и читаемости исходного кода.
Стандартным способом чтения/записи в порты ввода вывода AVR микроконтроллера является использование конструкций вида
PORTB = 0x42;
auto x = PORTB;
Выражение PORTB является макросом препроцессора, разворачивающимся как
#define __SFR_OFFSET 0x20
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)
#define PORTB _SFR_IO8(0x18)
в разыменованный указатель вида:
#define PORTB (*(volatile uint8_t*)(0x38))
то есть запись или чтения порта ввода вывода микроконтроллера с точки зрения программирования представляется не более, чем запись или чтение оперативной памяти по фиксированному адресу. Это знание в дальнейшем позволит использовать адреса регистров ввода вывода как числовые значения вместо их мнемонических обозначений.
Как видно IO регистры имеют свою адресацию, начинающуюся с 0x00 по 0x3F, проецирующуюся на адресное пространство оперативной памяти микроконтроллера со смещением равным __SFR_OFFSET или 0x20. Таким образом регистр ввода вывода с адресом 0x18 в пространстве адресов регистров ввода вывода в адресном пространстве оперативной памяти будет иметь адрес 0x38.
С точки зрения ООП лучше всего организовать работу с портами ввода вывода в виде шаблонного класса
template<typename Size, int PinAddress, int DirectionAddress, int PortAddress, int... Bits>
class Port;
с шаблонными параметрами:
- Size - числовой тип, представляющий размер порта ввода вывода. Для AVR микроконтроллеров этот тип практически всегда является uint8_t, однако я все же вывел этот параметр как настраиваемый;
- PinAddress - адрес регистра для чтения непосредственного значения на ножках порта микроконтроллера (регистр PINX). Также этот регистр позволяет выполнять инверсию битов порта без выполнения процедуры чтения/модификации/записи;
- DirectionAddress - адрес регистра направления ввода или вывода порта микроконтроллера (DDRX). Этот регистр не участвует в изменении либо чтении значений порта, однако класс порта, помимо этого, позволяет настроить порт на ввод или на вывод;
- PortAddress - адрес регистра, непосредственно представляющего порт ввода вывода микроконтроллера (PORTX);
- Bits... - вариативный параметр шаблона, биты порта ввода вывода микроконтроллера, с которыми работает класс. В этой статье будет рассмотрены только варианты либо с одним битом порта, либо весь порт целиком, в таком случае этот параметр не должен быть указан.
Каким образом можно получить числовые адреса регистров PinAddress, DirectionAddress и PortAddress? Указывать непосредственные числовые значения - нецелесообразно, конкретные адреса портов меняются от одной модели микроконтроллера к другой (например для ATMega8 адрес PORTB, спроецированный на оперативную память равен 0x38, а для ATiny88 адрес того же порта 0x25). К счастью, так как выражение PORTB является разыменованным указателем на область памяти с фиксированным адресом, этот адрес можно получить, применив операцию взятия адреса к этому макросу, например &PORTB. Такая операция может быть вычислена на этапе компиляции и подставлена в виде шаблонного параметра, правда, дополнительно придется выполнить приведение этого выражения к типу int.
Вот как будет выглядеть описание класса для работы с портом B ввода вывода:
using PortB = Port<uint8_t, (int)&PINB, (int)&DDRB, (int)&PORTB>;
А вот так описание класса для работы с отдельным битом порта ввода вывода (в данном случае второй бит порта B):
using PortB2 = Port<uint8_t, (int)&PINB, (int)&DDRB, (int)&PORTB, 2>;
Естественно, можно сгенерировать описания для всех битов всех возможных портов и вынести их в общую библиотеку и не дублировать эти объявления в каждом проекте.
Заметьте, что список бит в определении шаблона является вариативным так, что если не указан ни один бит, класс работает со всем портом, как с единым целым; а если указан только один бит - только с указанным битом, не изменяя остальные биты порта. Случай с указанием нескольких битов в вариативном шаблоне будет разобран в следующей части статьи, посвященной созданию объединения нескольких битов нескольких портов как единого класса.
Класс, представляющий весь порт целиком и класс, представляющий отдельный бит порта ввода вывода - отдельные частичные специализации шаблона общего класса, это позволяет иметь общее имя Port для этих классов.
Реализации классов не представляют никакой сложности. Они содержат набор константных выражений, и таких статических методов:
- static void set() - все биты (или указанный бит) порта устанавливаются в 1;
- static void clear() - все биты (или указанный бит) порта устанавливаются в 0;
- static void toggle() - все биты (или указанный бит) порта инвертируются (для этого используется запись в регистр PINX);
- static void write(Size value) - в порт одномоментно заносится указанное значение. Если используется реализация с указанием конкретного бита порта, то этому биту присваивается нулевой бит указанного значения;
- static Size read() - чтение значения на ножках порта. Если используется реализация с указанием конкретного бита порта, то результатом является 1 в случае, если значение бита на ножке порта является логической единицей и 0 в противном случае;
- static void output() - этот и два следующих метода позволяют настроить порт на ввод или на вывод. Метод output перенастраивает порт на вывод. Его требуется единожды вызвать перед вызовом методов, изменяющих значение порта ввода вывода;
- static void pullUp() - настраивает порт на ввод с подтягивающим резистором;
- static void highImpedance() - настраивает порт в высокоимпедансное или Z-состояние.
Все реализации методов статические и не требуют создания экземпляра класса.
/*
Представляет порт ввода вывода
*/
template<typename Size, int PinAddress, int DirectionAddress, int PortAddress, int... Bits>
struct Port;
/*
Представляет выбранный бит порта ввода вывода
*/
template<typename Size, int PinAddress, int DirectionAddress, int PortAddress, int Bit>
struct Port<Size, PinAddress, DirectionAddress, PortAddress, Bit> final
{
public:
// Тип, определяющий размер текущего порта ввода вывода
using size = Size;
public:
// Адрес регистра порта ввода вывода
static constexpr auto address = PortAddress;
// Адрес регистра ножек порта ввода вывода
static constexpr auto pinAddress = PinAddress;
// Адрес регистра направления порта ввода вывода
static constexpr auto directionAddress = DirectionAddress;
// Адрес регистра порта ввода вывода
static constexpr auto portAddress = PortAddress;
// Выбранный бит порта ввода вывода
static constexpr auto bit = Bit;
private:
// Указатель на регистр ножек порта ввода вывода
static constexpr auto pinReg = (volatile Size*)PinAddress;
// Указатель на регистр направления порта ввода вывода
static constexpr auto directionReg = (volatile Size*)DirectionAddress;
// Указатель на регистр порта ввода вывода
static constexpr auto portReg = (volatile Size*)PortAddress;
// Маска, соответствующая выбранному биту порта ввода вывода
static constexpr auto mask = 1 << Bit;
public:
// Устанавливает выбранный бит порта ввода вывода равным 1
static void set() { *portReg |= mask; }
// Устанавливает выбранный бит порта ввода вывода равным 0
static void clear() { *portReg &= ~mask; }
// Инвертирует выбранный бит порта ввода вывода
static void toggle() { *portReg ^= mask; }
// Выполняет запись младшего бита значения в выбранный бит порта ввода вывода
static void write(Size value) { *portReg = (*portReg & ~mask) | ((value << Bit) & mask); }
// Выполняет чтение выбранного бита порта ввода вывода в младший бит возвращаемого значения
static Size read() { return (*portReg & mask) >> Bit; }
// Устанавливает режим ввода с подтягивающим резистором для выбранного бита порта ввода вывода
static void pullUp() { *directionReg &= ~mask, *portReg |= mask; }
// Устанавливает режим ввода с высокоимпедансным состоянием для выбранного бита порта ввода вывода
static void highImpedance() { *directionReg &= ~mask, *portReg &= ~mask; }
// Устанавливает режим вывода для выбранного бита порта ввода вывода
static void output() { *directionReg |= mask; }
};
/*
Представляет все биты порта ввода вывода
*/
template<typename Size, int PinAddress, int DirectionAddress, int PortAddress>
struct Port<Size, PinAddress, DirectionAddress, PortAddress> final
{
public:
// Тип, определяющий размер текущего порта ввода вывода
using size = Size;
public:
// Адрес регистра порта ввода вывода
static constexpr auto address = PortAddress;
// Адрес регистра ножек порта ввода вывода
static constexpr auto pinAddress = PinAddress;
// Адрес регистра направления порта ввода вывода
static constexpr auto directionAddress = DirectionAddress;
// Адрес регистра порта ввода вывода
static constexpr auto portAddress = PortAddress;
private:
// Указатель на регистр ножек порта ввода вывода
static constexpr auto pinReg = (volatile Size*)PinAddress;
// Указатель на регистр направления порта ввода вывода
static constexpr auto directionReg = (volatile Size*)DirectionAddress;
// Указатель на регистр порта ввода вывода
static constexpr auto portReg = (volatile Size*)PortAddress;
// Маска, соответствующая всем битам порта ввода вывода
static constexpr auto mask = ~(Size)0;
public:
// Устанавливает все биты порта ввода вывода равным 1
static void set() { *portReg = mask; }
// Устанавливает все биты порта ввода вывода равным 0
static void clear() { *portReg = ~mask; }
// Инвертирует все биты порта ввода вывода
static void toggle() { *portReg ^= mask; }
// Выполняет запись значения в порт ввода вывода
static void write(Size value) { *portReg = value; }
// Выполняет чтение порта ввода вывода в возвращаемое значение
static Size read() { return *portReg; }
// Устанавливает режим ввода с подтягивающим резистором для всех битов порта ввода вывода
static void pullUp() { *directionReg = ~mask, *portReg = mask; }
// Устанавливает режим ввода с высокоимпедансным состоянием для всех битов порта ввода вывода
static void highImpedance() { *directionReg = ~mask, *portReg = ~mask; }
// Устанавливает режим вывода для всех битов порта ввода вывода
static void output() { *directionReg = mask; }
};
Ну и напоследок, пример использования в классической мигалке светодиодом:
#include <avr/io.h>
#include <util/delay.h>
#include <sdk/Ports.h> // Файл с описанием портов ввода вывода
template<typename Port> // Порт передается как шаблонный параметр функции
void BlinkLed(void) // Выполняемая задача. Не имеет представления, с каким конкретно портом она работает
{ // Это даже может быть не сам порт, а заглушка для тестирования
Port::output(); // Настройка порта на вывод. Необходимо, так как задача меняет состояние порта
while (true)
{
Port::toggle(); // Переключение ножки в обратное логическое состояние
_delay_ms(1000);
}
}
int main(void)
{ // Решение о конкретных портах и битах принимается здесь, в корне приложения,
BlinkLed<PortB2>(); // абстрагируя от этого выполняемые задачи
}
Как видно из примера, задача мигания светодиодом не только абстрагирована от конкретной ножки микроконтроллера, но и практически от самой модели микроконтроллера. Причем, сгенерированный компилятором код не содержит никаких лишних инструкций: вызовы методов класса порта ввода вывода встроились в тело задачи. Единственное оставшееся знание - процедура задержки, однако это уже не является целью этой статьи.
В следующей части я расскажу о создании "виртуальных" портов, объединяющих отдельные биты разных портов в единое целое.