Это не странно выглядит. Вот как на самом деле выглядит нормальный код MCU.
То, что вы имеете здесь, является примером концепции периферийных устройств с отображением в памяти . По сути, аппаратное обеспечение MCU имеет специальные местоположения в адресном пространстве SRAM назначенного ему MCU. Если вы пишете по этим адресам, биты байта, записанного для адреса n, управляют поведением периферийного устройства m .
По сути, некоторые банки памяти буквально имеют небольшие провода, идущие от ячейки SRAM к оборудованию. Если вы записываете «1» в этот бит в этом байте, он устанавливает для этой ячейки SRAM логический высокий уровень, который затем включает некоторую часть аппаратного обеспечения.
Если вы загляните в заголовки для MCU, то вы увидите, что есть большие таблицы сопоставлений адресов с ключевым словом <->. Вот как такие вещи, как и TCCR1B
т.д ... решаются во время компиляции.
Этот механизм отображения памяти чрезвычайно широко используется в MCU. Он используется в микроконтроллере ATmega в Arduino, как и в микроконтроллерах серий PIC, ARM, MSP430, STM32 и STM8, а также во многих микроконтроллерах, с которыми я не сразу знаком.
Код Arduino - странная штука с функциями, которые косвенно обращаются к регистрам управления MCU. Хотя это выглядит несколько «лучше», оно также намного медленнее и использует гораздо больше места для программ.
Все загадочные константы очень подробно описаны в техническом описании ATmega328P , которое вам действительно следует прочитать, если вы хотите сделать что-то большее, чем случайное переключение контактов на Arduino.
Выберите выдержки из таблицы данных, связанной выше:
Так, например, TIMSK1 |= (1 << TOIE1);
устанавливает бит TOIE1
в TIMSK1
. Это достигается путем сдвига двоичного 1 ( 0b00000001
) влево на TOIE1
биты, TOIE1
причем в заголовочном файле он определен как 0. Затем он побитовым ИЛИ устанавливается в текущее значение TIMSK1
, что эффективно устанавливает этот бит на единицу.
Глядя на документацию для бита 0 TIMSK1
, мы видим, что она описана как
Когда этот бит записывается в единицу и установлен I-флаг в регистре состояния (глобальные прерывания включены), прерывание переполнения таймера / счетчика1 активируется. Соответствующий вектор прерывания (см. «Прерывания» на стр. 57) выполняется, когда установлен флаг TOV1, расположенный в TIFR1.
Все остальные строки должны интерпретироваться одинаково.
Некоторые заметки:
Вы также можете увидеть такие вещи, как TIMSK1 |= _BV(TOIE1);
. _BV()
является широко используемым макросом, изначально взятым из реализации AVR libc . _BV(TOIE1)
функционально идентична (1 << TOIE1)
, с преимуществом лучшей читаемости.
Кроме того, вы также можете увидеть такие строки, как: TIMSK1 &= ~(1 << TOIE1);
или TIMSK1 &= ~_BV(TOIE1);
. Это имеет противоположную функцию в том TIMSK1 |= _BV(TOIE1);
, что оно сбрасывает бит TOIE1
в TIMSK1
. Это достигается путем взятия битовой маски, созданной путем _BV(TOIE1)
выполнения побитовой операции NOT над ней ( ~
), а затем с TIMSK1
помощью AND с этим значением NOTed (которое равно 0b11111110).
Обратите внимание, что во всех этих случаях значение таких вещей, как (1 << TOIE1)
или _BV(TOIE1)
полностью разрешается во время компиляции , поэтому они функционально сводятся к простой константе и, следовательно, не требуют времени выполнения для вычисления во время выполнения.
Правильно написанный код, как правило, содержит комментарии, встроенные в код, подробно описывающий, что делать назначаемым регистрам. Вот довольно простая подпрограмма софт-SPI, которую я недавно написал:
uint8_t transactByteADC(uint8_t outByte)
{
// Transfers one byte to the ADC, and receives one byte at the same time
// does nothing with the chip-select
// MSB first, data clocked on the rising edge
uint8_t loopCnt;
uint8_t retDat = 0;
for (loopCnt = 0; loopCnt < 8; loopCnt++)
{
if (outByte & 0x80) // if current bit is high
PORTC |= _BV(ADC_MOSI); // set data line
else
PORTC &= ~(_BV(ADC_MOSI)); // else unset it
outByte <<= 1; // and shift the output data over for the next iteration
retDat <<= 1; // shift over the data read back
PORTC |= _BV(ADC_SCK); // Set the clock high
if (PINC & _BV(ADC_MISO)) // sample the input line
retDat |= 0x01; // and set the bit in the retval if the input is high
PORTC &= ~(_BV(ADC_SCK)); // set clock low
}
return retDat;
}
PORTC
это регистр, который контролирует значение выходных контактов в PORTC
ATmega328P. PINC
регистр, в котором доступны входные значения PORTC
. По сути, такие вещи происходят внутри вас, когда вы используете функции digitalWrite
или digitalRead
. Тем не менее, существует операция поиска, которая преобразует «номера выводов» arduino в реальные номера выводов аппаратного обеспечения, что занимает где-то в области 50 тактов. Как вы, вероятно, можете догадаться, если вы пытаетесь идти быстро, тратить 50 тактов на операцию, которая должна требовать только 1, немного нелепо.
Вышеупомянутая функция, вероятно, занимает где-то 100-200 тактовых циклов для передачи 8 битов. Это влечет за собой 24 пин-записи и 8 операций чтения. Это во много раз быстрее, чем при использовании digital{stuff}
функций.