реализация sbi() и cli()

Я часто видел cli() и sbi() в коде Arduino. Обычно я не обращаю на них внимания, поскольку знаю, что они делают (очищают или устанавливают бит, указанный как второй аргумент в регистре микроконтроллера, указанном как первый). Я всегда думал, что эти функции — всего лишь понятный способ выполнения простых манипуляций с битами, которые могут быть весьма подвержены ошибкам. Что-то вроде этого: #define cli(reg,bit) (*reg &= ~(1 << bit) и #define sbi(reg,bit) (*reg |= (1 << bit)).

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

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

Это как раз то, о чём я раньше думал... но с двумя дополнительными макросами. Я искал их в библиотеках Arduino:

#define _BV(bit) (1 << (bit))

и

#define _SFR_BYTE(sfr) _MMIO_BYTE(_SFR_ADDR(sfr))

который содержит

#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))

и

#if _SFR_ASM_COMPAT
#if (__SFR_OFFSET == 0x20)
#define _SFR_ADDR(sfr) _SFR_MEM_ADDR(sfr)
#elif !defined(__ASSEMBLER__)
#define _SFR_ADDR(sfr) (_SFR_IO_REG_P(sfr) ? (_SFR_IO_ADDR(sfr) + 0x20) : _SFR_MEM_ADDR(sfr))
#endif
#else  /* !_SFR_ASM_COMPAT */    
#define _SFR_ADDR(sfr) _SFR_MEM_ADDR(sfr)

Дальнейшее исследование привело меня к определениям, которые я не мог понять (например, потому что они объясняются в комментариях с использованием множества неизвестных мне аббревиатур). Итак, у меня есть следующие вопросы:

  • Почему sfr приводится к *(volatile uint8_t *)?
  • Что означают аббревиатуры, используемые в этом коде? (ASM, SFR, MMIO)
  • При каких обстоятельствах определяется __ASSEMBLER__ и где (в каком файле)?
  • Почему мы просто не можем написать #define cli(reg,bit) (*reg &= ~(1 << bit) и #define sbi(reg,bit) (*reg |= (1 << bit))?

, 👍3


2 ответа


Лучший ответ:

4

Почему sfr приводится к *(volatile uint8_t *)?

Потому что вам нужно содержимое (первый *) указанного вами адреса памяти, и вам нужно, чтобы оно было изменчивым, чтобы не кэшировалось. О, и это 8-битное значение.

Что означают аббревиатуры, используемые в этом коде? (ASM, SFR, MMIO)

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

  • ASM означает язык ассемблера.
  • Регистр специального назначения — это что-то вроде DDRB.
  • Ввод-вывод с отображением в памяти — это способ взаимодействия с периферийными устройствами путем чтения и записи в области обычного адресного пространства памяти.

При каких обстоятельствах определяется __ASSEMBLER__ и где (в каком файле)?

Он определяется компилятором при компиляции файла сборки.

Почему мы не можем просто написать #define cli(reg,bit) (reg &= ~(1 << bit) и #define sbi(reg,bit) (reg |= (1 << bit))?

Потому что reg — это просто адрес. Вам нужно преобразовать его в переменную в памяти, к которой вы можете надежно получить доступ (отсюда и volatile). Кроме того, не все чипы используют MMIO для всех периферийных устройств — к некоторым можно получить доступ с помощью более эффективных инструкций ввода-вывода. Тем не менее, во многих случаях да, вы можете просто использовать более простой код. В конце концов, такие вещи, как DDRB |= 0x04, работают нормально. Все зависит от того, как изначально определено значение, передаваемое как reg. Принудительно применяя определенное приведение volatile, вы можете гарантировать, что каким бы способом оно ни было определено (с volatile или без него и т. д.), оно будет работать.

,

4

В микроконтроллерах Atmel (теперь Microchip) AVR инструкции языка ассемблера sbi, cbi, sbis и sbic могут адресовать только 32 порта ввода-вывода по адресам от 0x20 до 0x3f. Atmega168 и 328 (использовавшиеся в Arduino до Leonardo) имеют НАМНОГО больше 32 регистров ввода-вывода, поэтому только часть из них может быть адресована sbi и cbi (набор инструкций AVR был закреплен ГОДЫ назад, когда 32 регистра ввода-вывода казались более чем достаточными).

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

Насколько я помню, в AVRlibC было несколько макросов, которые делали почти одно и то же, но имели разные варианты использования:

  • Можно было бы скомпилировать самые быстрые и эффективные инструкции, доступные для этого конкретного адреса ввода-вывода.

  • Один всегда компилируется в sbi/cbi и выдает ошибку компилятора, если вы пытаетесь получить доступ к регистру за пределами 0x20-0x3f

  • ВСЕГДА можно было бы скомпилировать в последовательность load-act-store, независимо от того, можно ли было бы использовать sbi/cbi вместо этого.

  • Один всегда компилируется в последовательность load-act-store, но выдает ошибку компилятора, если вы пытаетесь использовать его по адресу между 0x20 и 0x3f.

По сути, выбор позволял вам выбирать между оптимизированной (но переменной) производительностью, равномерно медленным (но детерминированным) временем выполнения или максимально быстрым (но детерминированным во время компиляции) вариантом [который требовал, чтобы программист знал, находится ли рассматриваемый регистр в пределах или выше 0x20-0x3f].

,