Nano Hash - криптовалюты, майнинг, программирование

Передовой опыт битовых манипуляций

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

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

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

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;

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

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

02.11.2018

  • Кстати, использование 0b для констант с основанием два нестандартно. 02.11.2018
  • @SteveSummit Подожди; какая?! 03.11.2018
  • @Alexander Что касается стандарта C, у вас есть ведущий 0x для шестнадцатеричного или ведущий 0 для восьмеричного, иначе десятичный. Довольно часто хочется, чтобы был способ вводить константы с основанием два, а ведущее 0b является очевидным прозвищем (которое, очевидно, реализовано некоторыми компиляторами), но, как я уже сказал, это не стандарт. 03.11.2018
  • Кроме того, показано девять битов, поэтому этот регистр должен быть больше, чем обычный байт. Было бы неплохо указать длину регистра (или что-то еще). Вы можете указывать биты их обычными значениями шестнадцатеричной маски (0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40 и т. д.). Может быть, включить полную длину, например 0x0001, 0x0002 и т. д.? 03.11.2018
  • Вы не упомянули, предназначен ли код, который вы пишете, для обычного компьютера (например, в качестве драйвера устройства) или для встроенной системы. Условные обозначения существенно различаются между платформами (стандарты драйверов Linux не совсем такие же, как для Windows, хотя они больше похожи, чем встроенные AVR). 03.11.2018

Ответы:


1

Проблема с битовыми полями заключается в том, что стандарт 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;
02.11.2018
  • Это приемлемо для этого файла, потому что он является частью системы. Это также приемлемо, потому что Linux в значительной степени де-факто требует GCC для компиляции. Другой компилятор может изменить способ назначения битовых полей, даже если порядок следования байтов остается прежним. 02.11.2018
  • Компиляторы C в Unix-подобных системах должны соответствовать не только стандарту C, но и ABI платформы, чтобы они могли взаимодействовать с библиотеками платформы. 02.11.2018
  • Почему бы не использовать enum вместо определения переменных-констант, потенциально вызывающих проблемы ODR? 02.11.2018
  • @Ruslan Предположительно, потому что с перечислениями вы не можете контролировать, в каком целочисленном типе они реализованы. 03.11.2018
  • Вы можете написать различные тесты для ваших битовых полей и структур и т. д. Либо как обычные тесты времени выполнения, либо как макросы static_assert. Затем, если биты не соответствуют ожидаемым, сообщите об ошибке и остановитесь. 03.11.2018

  • 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
    
    02.11.2018
  • Для начинающих: скобки в макросах, таких как #define DMA_BYTE (1U << 0), чрезвычайно важны - см. этот вопрос. 02.11.2018
  • @mgarey Я бы сказал, что они важны для всех разработчиков C, а не только для начинающих. Недостаточное использование круглых скобок в макросе, я бы сказал, является ошибкой в ​​​​макросе, независимо от того, кто вы собираетесь использовать макрос. 03.11.2018
  • @kasperd Я думаю, дело было в том, что не новички уже были укушены этим и поэтому научились ставить скобки в свои макросы. знак равно 03.11.2018

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

    02.11.2018
  • Разве вы не можете просто определить порядок битов, обратившись к заголовочным файлам stdlib.h и stddef.h для конкретной системы? 02.11.2018
  • Я не вижу фундаментальной проблемы с использованием битовых полей для сопоставления аппаратных регистров на конкретной встроенной платформе, например, поскольку код в любом случае имеет тенденцию быть непереносимым (привязанным к этому конкретному устройству и часто либо к одному компилятору). Повышение удобочитаемости и удобства для многобитных полей, в частности, может стоить того. (Конечно, могут быть и другие проблемы, такие как размер кода или производительность, которые необходимо проверить, но я считаю, что я бы не стал автоматически игнорировать битовые поля для такого использования.) 02.11.2018
  • Просто будьте осторожны при использовании ~, потому что он содержит неявное целочисленное продвижение. Хорошей практикой является возврат к предполагаемому типу: DMA_base_ptr[DMA_CONTROL_OFFS] &= (uint8_t)~WRITEACCESS; 02.11.2018
  • Спасибо за ответ, я нахожу детали использования чрезвычайно полезными и обязательно воспользуюсь кое-чем или двумя 02.11.2018
  • @Arkku, ClayRaynor: В конце концов, это вопрос стиля. На мой взгляд, попытка привести структуру данных в памяти в соответствие с навязанной извне структурой хранения просто больше проблем, чем того стоит. Однако это может быть мнение меньшинства, потому что огромное количество программистов на C тратят огромное количество времени, пытаясь организовать такие соответствия. (И иногда, конечно, им это удается, в том числе когда они сопоставляют отдельные биты с битовыми полями.) 02.11.2018
  • Что ж, это правда, что использование битовых полей для соответствия оборудованию делает код непереносимым (теоретически, возможно, даже для разных компиляторов для одного и того же оборудования), поэтому я согласен, что по умолчанию их не следует использовать для этого. Но в то же время я думаю, что предпосылка сопоставления битов в аппаратном регистре является признаком того, что этот код может быть настолько непереносимым, что добавление битовых полей в микс не будет таким серьезным. Для 1-битных полей я бы все равно не стал этого делать, но для 2+-битных полей в одноразовом непереносимом проекте я мог бы, по крайней мере, рассмотреть это только из-за красивого синтаксиса. знак равно 02.11.2018
  • @Arkku, Стив Саммуит, я должен согласиться с обоими вашими чувствами. Я полностью за попытку максимизировать мобильность. Но я не думаю, что переносимость должна быть главной задачей, поскольку вы работаете с кодом, зависящим от аппаратного обеспечения. Я также понимаю и согласен с проблемами сопоставления ограничений внешнего хранилища. 02.11.2018
  • @Arkku: проблема с битовыми полями заключается в том, что аппаратное обеспечение может иметь ограничения на выполнение операций чтения-модификации-записи, но компилятор, как правило, не может знать о них или соблюдать их. Например, если битовое поле занимает биты 8-15 слова, компилятор может попытаться заменить хранилище простым байтом, даже при доступе к аппаратному регистру, который не поддерживает 8-битный доступ (и который, таким образом, потребует 16-битная последовательность чтения-изменения-записи). С другой стороны, если 16-битный регистр ввода-вывода поддерживает запись байтов, и если основной код и код прерывания должны записывать противоположные половины... 03.11.2018
  • ... использование доступа к байтам может избежать необходимости отключать прерывания основной линии при записи своей половины регистра. Учитывая такую ​​последовательность, как FOO->CR1 = (FOO->CR1 & ~0xFF) | someValue;, необходимость следить за одновременными действиями в ISR может быть несколько очевидной, но если бы она была записана как FOO->someMember = 123;, то тот факт, что такая операция может конфликтовать с записью в FOO->someOtherMember, вряд ли был бы очевиден. 03.11.2018
  • @supercat Конечно, правильные замечания, особенно о дырявой абстракции, запутывающей что-то важное. В любом случае, я не выступаю за использование битовых полей по умолчанию (более того, я советую выше, что по умолчанию должно быть не использовать их), я просто в основном выступаю против категорического определенно не используйте битовые поля в этом ответе. 03.11.2018

  • 4

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

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

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

    DMA_base_ptr[DMA_CONTROL_OFFS] |= HW;
    

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

    DMA_base_ptr[DMA_CONTROL_OFFS] &= ~HW;
    
    02.11.2018

    5

    Современные компиляторы 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, в котором перечислены детали регистров.

    02.11.2018
  • Может быть, я странный, но если я имею дело с низкоуровневыми вещами, такими как управление DMA, я предпочитаю сам видеть биты, чем заворачивать их в bool, и действительно те, которые я не могу прочитать или установить более одного за раз. время. (И если идея состоит в том, чтобы предложить API действительно более высокого уровня, то (экспортированные) функции также должны быть более высокого уровня, чем set_BYTE. По крайней мере, по названию.) 02.11.2018
  • @Arkku Конечно, может быть API более высокого уровня, и там будет решаться установка нескольких битов за раз. Предположительно, полезны только некоторые комбинации битов, хотя они, конечно, различаются. Обеспечение безопасности типов, т.е. не использовать битовые шаблоны dma на uart, это небольшая проблема в C... 02.11.2018

  • 6

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

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

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

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

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

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

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

    02.11.2018
  • ре. используя enum вместо макросов, одно из преимуществ макросов заключается в том, что они могут включать приведение к определенному типу (например, соответствие ширине аппаратного регистра), тогда как фактический тип enum определяется реализацией. (И да, вы можете привести здесь тот же аргумент, что и с битовыми полями, что это не проблема, если вы знаете, как определяется реализация, и это правильный аргумент. =) 03.11.2018
  • Что ж, значение enum всегда задается как int, а тип, в котором оно представляется при использовании, должен быть совместим с int, так что для этих целей на самом деле это все еще просто int. Также я бы решительно возражал против включения приведения типов в определения макросов. Вы можете добавить приведение в то время, когда вы используете enum, или в то время, когда вы используете константу (независимо от того, из микропрограммы это или нет), если это необходимо, хотя обычно такие приведения являются просто лишним шумом для нас, людей. приходится читать и выяснять, действительно ли они чем-то отличаются от того, если бы их там не было. 03.11.2018
  • Я не знаю, почему за это проголосовали. Я думаю, что этот ответ хорош. 05.06.2020

  • 7

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

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

    02.11.2018
  • Битовые поля крайне не переносимы. Любой компилятор может делать что хочет. Согласно 6.7.2.1 Спецификаторы структуры и объединения, параграф 11 стандарта C: ... независимо от того, помещается ли битовое поле, которое не подходит, в следующий блок или перекрывает соседние блоки, определяется реализацией. Порядок размещения битовых полей в блоке (от старшего к младшему или от младшего к старшему) определяется реализацией. Выравнивание адресного блока памяти не указано. 02.11.2018
  • В любом случае вам следует проверять определения в ваших заголовочных файлах stddef.h и limits.h, поскольку размер ваших целочисленных примитивов зависит от платформы, и ваши операции сдвига битов могут выполняться системой Endianness. Кроме того, в руководстве по компилятору должно быть указано поведение битовых полей. Кроме того, это зависит от аппаратного обеспечения, поэтому переносимость уже за окном. 02.11.2018
  • Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

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

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

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..