Бит манипуляции хорошие практики - PullRequest
0 голосов
/ 02 ноября 2018

Интересно, как начинающему программисту на Си, что было бы лучшим, легким для понимания и понятным решением для установки управляющих битов в устройстве. Есть ли стандарты ? Любой пример кода для имитации? 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;

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

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

Ответы [ 7 ]

0 голосов
/ 02 ноября 2018

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

Конечно, вы можете использовать константы, а не функции для деталей реализации, но 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-входе, перечисляющем детали регистров.

0 голосов
/ 02 ноября 2018

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

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

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

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

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

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

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

0 голосов
/ 02 ноября 2018

Другие ответы уже охватили большую часть материала, но, возможно, стоит упомянуть, что даже если вы не можете использовать нестандартный синтаксис 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
0 голосов
/ 02 ноября 2018

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

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

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

DMA_base_ptr[DMA_CONTROL_OFFS] |= HW;

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

DMA_base_ptr[DMA_CONTROL_OFFS] &= ~HW;
0 голосов
/ 02 ноября 2018

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

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

0 голосов
/ 02 ноября 2018

Старый способ 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 в списке часто задаваемых вопросов C .

0 голосов
/ 02 ноября 2018

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

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

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

В качестве примера рассмотрим определение 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;
...