Альтернатива написанию масок для 32-битных микроконтроллеров

Я работаю над проектом, который включает в себя программирование микроконтроллеров ARM 32 бит. Как и во многих функциях кодирования встроенного программного обеспечения, настройка и очистка бит являются существенной и довольно повторяющейся задачей. Стратегия маскирования полезна при работе с микросотами вместо 32 бит для установки и очистки бит. Но при работе с 32-битными микроконсоллерами практически не нужно писать маски каждый раз, когда нам нужно установить/очистить один бит.

Функции записи для обработки этого могут быть решением; однако наличие функции занимает память, что не является идеальным в моем случае.

Есть ли лучшая альтернатива для обработки/очистки бит при работе с 32-битными микронами?

Ответ 1

В C или С++ вы обычно определяете макросы для бит-масок и объединяете их по желанию.

/* widget.h */
#define WIDGET_FOO 0x00000001u
#define WIDGET_BAR 0x00000002u

/* widget_driver.c */
static uint32_t *widget_control_register = (uint32_t*)0x12346578;

int widget_init (void) {
    *widget_control_register |= WIDGET_FOO;
    if (*widget_control_register & WIDGET_BAR) log(LOG_DEBUG, "widget: bar is set");
}

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

#define WIDGET_FOO (1u << 0)
#define WIDGET_BAR (1u << 1)

Вы можете определить макросы для установки бит:

/* widget.h */
#define WIDGET_CONTROL_REGISTER_ADDRESS ((uint32_t*)0x12346578)
#define SET_WIDGET_BITS(m) (*WIDGET_CONTROL_REGISTER_ADDRESS |= (m))
#define CLEAR_WIDGET_BITS(m) (*WIDGET_CONTROL_REGISTER_ADDRESS &= ~(uint32_t)(m))

Вы можете определять функции, а не макросы. Это имеет преимущество дополнительных проверок типов во время компиляций. Если вы объявляете функцию как static inline (или даже просто static) в заголовке, хороший компилятор будет встроить эту функцию всюду, поэтому использование функции в исходном коде не будет стоить никакой памяти кода (если предположить, что сгенерированная код для тела функции меньше, чем вызов функции, который должен иметь место для функции, которая просто устанавливает некоторые биты в регистр).

/* widget.h */
#define WIDGET_CONTROL_REGISTER_ADDRESS ((uint32_t*)0x12346578)
static inline void set_widget_bits(uint32_t m) {
    *WIDGET_CONTROL_REGISTER_ADDRESS |= m;
}
static inline void set_widget_bits(uint32_t m) {
    *WIDGET_CONTROL_REGISTER_ADDRESS &= ~m;
}

Ответ 2

Другая распространенная идиома для регистров, обеспечивающих доступ к отдельным битам или группам битов, заключается в определении бит struct, содержащих битовые поля для каждого регистра вашего устройства. Это может оказаться сложным, а зависит от реализации компилятора C. Но он также может быть более ясным, чем макросы.

Простое устройство с однобайтовым регистром данных, регистром управления и регистром состояния может выглядеть так:

typedef struct {
    unsigned char data;
    unsigned char txrdy:1;
    unsigned char rxrdy:1;
    unsigned char reserved:2;
    unsigned char mode:4;
} COMCHANNEL;
#define CHANNEL_A (*(COMCHANNEL *)0x10000100)
// ...
void sendbyte(unsigned char b) {
    while (!CHANNEL_A.txrdy) /*spin*/;
    CHANNEL_A.data = b;
}
unsigned char readbyte(void) {
    while (!CHANNEL_A.rxrdy) /*spin*/;
    return CHANNEL_A.data;
}

Доступ к полю mode равен CHANNEL_A.mode = 3;, что намного проще, чем писать что-то вроде *CHANNEL_A_MODE = (*CHANNEL_A_MODE &~ CHANNEL_A_MODE_MASK) | (3 << CHANNEL_A_MODE_SHIFT);. Конечно, последнее уродливое выражение обычно (в основном) покрывается макросами.

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

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

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

На самом деле вы можете использовать оба способа. Не так уж необычно обертывать декларации битового поля внутри union, которая описывает физические регистры, позволяя легко изменять их значения во всех битах сразу. (Я знаю, что я видел вариацию этого, где условная компиляция использовалась для предоставления двух версий битовых полей, по одному для каждого порядка бит, а в общем заголовочном файле использовались специальные определения для привязки к конкретным объектам, чтобы решить, какой из них выбрать.)

typedef struct {
    unsigned char data;
    union {
        struct {
            unsigned char txrdy:1;
            unsigned char rxrdy:1;
            unsigned char reserved:2;
            unsigned char mode:4;
        } bits;
        unsigned char status;
    };
} COMCHANNEL;
// ...
#define CHANNEL_A_MODE_TXRDY 0x01
#define CHANNEL_A_MODE_TXRDY 0x02
#define CHANNEL_A_MODE_MASK  0xf0
#define CHANNEL_A_MODE_SHIFT 4
// ...
#define CHANNEL_A (*(COMCHANNEL *)0x10000100)
// ...
void sendbyte(unsigned char b) {
    while (!CHANNEL_A.bits.txrdy) /*spin*/;
    CHANNEL_A.data = b;
}
unsigned char readbyte(void) {
    while (!CHANNEL_A.bits.rxrdy) /*spin*/;
    return CHANNEL_A.data;
}

Предполагая, что ваш компилятор понимает анонимный союз, вы можете просто обратиться к CHANNEL_A.status, чтобы получить весь байт, или CHANNEL_A.mode, чтобы ссылаться только на поле режима.

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

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

Эти проблемы зависят от поставщика компилятора очень. Даже без изменения поставщиков компиляторов параметры #pragma, параметры командной строки или более вероятные варианты уровня оптимизации могут влиять на шаблоны макета памяти, заполнения и доступа к памяти. В качестве побочного эффекта они, скорее всего, заблокируют ваш проект до единственной специальной компиляции компилятора, если только героические усилия не используются для создания файлов заголовков регистров, которые используют условную компиляцию для описания регистров по-разному для разных компиляторов. И даже тогда вы, вероятно, хорошо обслуживаете, чтобы включить хотя бы один регрессионный тест, который проверяет ваши предположения, чтобы любые обновления инструментальной цепочки (или благие намерения на уровне оптимизации) заставили бы все проблемы попасться, прежде чем они станут таинственными ошибками в коде, который "работал годами".

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

Ответ 3

Если вы используете Cortex M3, вы можете использовать бит-диапазон

Бит-banding отображает полное слово памяти на один бит в области бит-диапазона. Например, запись в одно из слов псевдонима устанавливает или очищает соответствующий бит в области битбота.

Это позволяет каждому отдельному биту в области бит-диапазона быть непосредственно доступным по адресу, выровненному по слову, с использованием одной инструкции LDR. Он также позволяет переключать отдельные биты с C, не выполняя последовательность команд read-modify-write.

Ответ 4

Если у вас есть С++, и есть достойный компилятор, то что-то вроде QFlags - хорошая идея. Он предоставляет вам тип безопасного интерфейса для битовых флагов.

Скорее всего, этот код будет лучше, чем использование битовых полей в структурах, поскольку битовые поля могут быть изменены только по одному и, скорее всего, будут переведены, по меньшей мере, на один файл load/modify/store для каждого измененного битового поля. С помощью метода QFlags -like вы можете получить один файл load/modify/store для каждого оператора or-assign или and-assign. Обратите внимание, что использование QFlags не требует включения всей рамки Qt. Это автономный файл заголовка (после небольших настроек).

Ответ 5

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

Непонятно, какие типы регистров вы устанавливаете и очищаете биты, но в целом есть два случая, о которых вам нужно беспокоиться во встроенных системах:

Установка и очистка бит в регистре чтения/записи Если вы хотите изменить один бит (или несколько бит) в регистре чтения и записи, вам сначала нужно будет прочитать регистр, установить или очистить соответствующий бит с помощью масок и все остальное, чтобы получить правильное поведение, а затем написать обратно в тот же регистр. Таким образом, вы не измените другие биты.

Запись для разделения регистров Set и Clear (общая для микроконтроллеров ARM) Иногда есть отдельные регистры Set и Clear. Вы можете написать только один бит в ясный регистр, и он очистит этот бит. Например, если есть регистр, который вы хотите очистить бит 9, просто напишите (1 < 9) в регистр очистки. Вам не нужно беспокоиться об изменении других бит. Аналогично для заданного регистра.

Ответ 6

Вы можете устанавливать и очищать биты с помощью функции, которая занимает столько же памяти, сколько делает ее с помощью маски:

#define SET_BIT(variableName, bitNumber)    variableName |= (0x00000001<<(bitNumber));
#define CLR_BIT(variableName, bitNumber)    variableName &= ~(0x00000001<<(bitNumber));

int myVariable = 12;

SET_BIT(myVariable, 0);    // myVariable now equals 13
CLR_BIT(myVariable, 1);    // myVariable now equals 11

Эти макросы будут выдавать точно такие же инструкции ассемблера, что и маска.

В качестве альтернативы вы можете сделать это:

#define BIT(n)        (0x00000001<<n)
#define NOT_BIT(n)   ~(0x00000001<<n)

int myVariable = 12;

myVariable |= BIT(4);        //myVariable now equals 28
myVariable &= NOT_BIT(3);    //myVariable now equals 20

myVariable |= BIT(5) |
              BIT(6) |
              BIT(7) |
              BIT(8);        //myVariable now equals 500