Бит манипуляции хорошие практики

Как начинающему программисту на Си, мне интересно, что было бы лучшим, легким для понимания и понятным решением для установки управляющих битов в устройстве. Есть ли стандарты? Любой пример кода для имитации? Google не дал достоверного ответа.

Например, у меня есть карта блока управления: map

Первый способ, который я вижу, состоит в том, чтобы просто установить необходимые биты. Это требует кучу объяснений в комментариях и, кажется, не все так профессионально.

DMA_base_ptr[DMA_CONTROL_OFFS] = 0b10001100;

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

struct DMA_control_block_struct
{ 
    unsigned int BYTE:1; 
    unsigned int HW:1; 
    // etc
} DMA_control_block_struct;

Один из вариантов лучше другого? Есть ли варианты, которых я просто не вижу?

Любой совет будет высоко ценится

Ответ 1

Проблема с битовыми полями состоит в том, что стандарт C не предписывает, что порядок, в котором они определены, совпадает с порядком их реализации. Таким образом, вы не можете устанавливать биты, которые вы думаете, что вы.

Раздел 6.7.2.1p11 стандарта C гласит:

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

В качестве примера рассмотрим определение struct iphdr, представляющей заголовок IP, из файла /usr/include/netinet/ip.h в Linux:

struct iphdr
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ihl:4;
    unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    unsigned int version:4;
    unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
    u_int8_t tos;
    ...

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

Так что не используйте битовое поле.

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

const uint8_t BIT_BYTE =     0x1;
const uint8_t BIT_HW   =     0x2;
const uint8_t BIT_WORD =     0x4;
const uint8_t BIT_GO   =     0x8;
const uint8_t BIT_I_EN =     0x10;
const uint8_t BIT_REEN =     0x20;
const uint8_t BIT_WEEN =     0x40;
const uint8_t BIT_LEEN =     0x80;

DMA_base_ptr[DMA_CONTROL_OFFS] = BIT_LEEN | BIT_GO | BIT_WORD;

Ответ 2

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

#define DMA_BYTE  (1U << 0)
#define DMA_HW    (1U << 1)
#define DMA_WORD  (1U << 2)
#define DMA_GO    (1U << 3)
// …

Обратите внимание, как последнее число соответствует столбцу "число бит" в документации.

Использование для установки и очистки битов не меняется:

#define DMA_CONTROL_REG DMA_base_ptr[DMA_CONTROL_OFFS]

DMA_CONTROL_REG |= DMA_HW | DMA_WORD;    // set HW and WORD
DMA_CONTROL_REG &= ~(DMA_BYTE | DMA_GO); // clear BYTE and GO

Ответ 3

Старый способ C состоит в том, чтобы определить кучу битов:

#define WORD  0x04
#define GO    0x08
#define I_EN  0x10
#define LEEN  0x80

Тогда ваша инициализация становится

DMA_base_ptr[DMA_CONTROL_OFFS] = WORD | GO | LEEN;

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

DMA_base_ptr[DMA_CONTROL_OFFS] |= I_EN;

Вы можете очистить отдельные биты, используя & и ~:

DMA_base_ptr[DMA_CONTROL_OFFS] &= ~GO;

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

if(DMA_base_ptr[DMA_CONTROL_OFFS] & WORD) ...

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

Смотрите также вопросы 20.7 и 2.26 в списке часто задаваемых вопросов.

Ответ 4

Там нет стандарта для битовых полей. В этом случае отображение и битовая операция зависят от компилятора. Двоичные значения, такие как 0b0000, также не стандартизированы. Обычный способ сделать это - определить шестнадцатеричные значения для каждого бита. Например:

#define BYTE (0x01)
#define HW   (0x02)
/*etc*/

Когда вы хотите установить биты, вы можете использовать:

DMA_base_ptr[DMA_CONTROL_OFFS] |= HW;

Или вы можете очистить биты с помощью:

DMA_base_ptr[DMA_CONTROL_OFFS] &= ~HW;

Ответ 5

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

Конечно, вы можете использовать константы, а не функции для деталей реализации, но API должен быть функциями. Это также позволяет использовать макросы вместо функций, если вы используете древний компилятор.

Например:

#include <stdbool.h>
#include <stdint.h>

typedef union DmaBase {
  volatile uint8_t u8[32];
} DmaBase;
static inline DmaBase *const dma1__base(void) { return (void*)0x12340000; }

// instead of DMA_CONTROL_OFFS
static inline volatile uint8_t *dma_CONTROL(DmaBase *base) { return &(base->u8[12]); }
// instead of constants etc
static inline uint8_t dma__BYTE(void) { return 0x01; }

inline bool dma_BYTE(DmaBase *base) { return *dma_CONTROL(base) & dma__BYTE(); }
inline void dma_set_BYTE(DmaBase *base, bool val) {
  if (val) *dma_CONTROL(base) |= dma__BYTE();
  else *dma_CONTROL(base) &= ~dma__BYTE();
}
inline bool dma1_BYTE(void) { return dma_BYTE(dma1__base()); }
inline void dma1_set_BYTE(bool val) { dma_set_BYTE(dma1__base(), val); }

Такой код должен генерироваться машиной: я использую gsl (с известностью 0mq) для генерации тех, которые основаны на шаблоне, и некоторые входные данные XML со списком деталей регистров.

Ответ 6

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

Однако то, что до сих пор, похоже, упускали другие, - это различные аспекты того, как аппаратные устройства с отображением памяти могут вести себя, которые могут быть нелогичными при работе с языками более высокого уровня, такими как C, и функциями оптимизации, которые предлагают такие языки. Например, каждое чтение или запись аппаратного регистра может иногда иметь побочные эффекты, даже если биты не изменяются при записи. Между тем оптимизатор может затруднить определение того, когда сгенерированный код фактически читает или записывает по адресу регистра, и даже когда объект C, описывающий регистр, тщательно квалифицирован как volatile, требуется большая осторожность для контроля, когда ввод/вывод происходит.

Возможно, вам потребуется использовать определенный метод, определенный вашим компилятором и системой, чтобы правильно манипулировать отображаемыми в памяти аппаратными устройствами. Это касается многих встраиваемых систем. В некоторых случаях поставщики компиляторов и систем действительно будут использовать битовые поля, как в некоторых случаях использует Linux. Я бы посоветовал сначала прочитать руководство к вашему компилятору.

Представленная вами таблица описания битов, по-видимому, предназначена для управляющего регистра ядра контроллера Intel Avalon DMA. Столбец "чтение/запись/очистка" дает подсказку о том, как ведет себя конкретный бит, когда он читается или записывается. Регистр состояния для этого устройства имеет пример бита, в котором запись нуля приведет к сбросу значения бита, но он может не считывать то же значение, которое было записано - т.е. запись в регистр может иметь побочный эффект в устройстве, в зависимости от значения бита DONE. Интересно, что они документируют бит SOFTWARERESET как "RW", но затем описывают процедуру как запись 1 в него дважды, чтобы инициировать сброс, а затем они также предупреждают: Выполнение сброса программного обеспечения DMA, когда активна передача DMA, может привести к постоянной блокировке шины. (до следующего сброса системы). Поэтому бит SOFTWARERESET не должен записываться, кроме как в крайнем случае. Управление сбросом в C потребует некоторого тщательного программирования независимо от того, как вы описываете регистр.

Что касается стандартов, ИСО/МЭК подготовили "технический отчет", известный как "ИСО/МЭК ТР 18037", с подзаголовком "Расширения для поддержки встроенных процессоров". В нем обсуждается ряд вопросов, связанных с использованием C для управления аппаратной адресацией и вводом-выводом устройства, и, в частности, для типов битовых регистров, которые вы упоминаете в своем вопросе, он документирует ряд макросов и методов, доступных через включаемый файл, который они звоните <iohw.h>. Если ваш компилятор предоставляет такой заголовочный файл, вы можете использовать эти макросы.

Доступны черновые копии TR 18037, последний из которых - TR 18037 (2007), хотя он обеспечивает довольно сухое чтение. Однако он содержит пример реализации <iohw.h>.

Возможно, хорошим примером реальной реализации <iohw.h> является QNX. Документация по QNX предлагает хороший обзор (и пример, хотя я настоятельно рекомендовал бы использовать enum для целочисленных значений, а не макросов): QNX <iohw.h>

Ответ 7

Обязательно инициализируйте биты известным значением по умолчанию при объявлении переменной для хранения их значений. В C, когда вы объявляете переменную, вы просто резервируете блок памяти по адресу, а размер блока зависит от его типа. Если вы не инициализируете переменную, вы можете столкнуться с неопределенным/неожиданным поведением, поскольку значение переменной будет зависеть от того, какое значение/состояние памяти в этом блоке было до того, как вы ее объявили. Инициализируя переменную значением по умолчанию, вы очищаете этот блок памяти от его существующего состояния и переводите его в известное состояние.

Что касается читабельности, вы должны использовать битовое поле для хранения значений бита. Битовое поле позволяет вам хранить значения битов в структуре. Это упрощает организацию, так как вы можете использовать точечную запись. Кроме того, вы должны обязательно прокомментировать объявление битового поля, чтобы объяснить, для чего разные поля используются в качестве передового опыта. Надеюсь, это ответит на ваш вопрос. Удачи вам с программированием на C !