Может ли кто-нибудь объяснить этот странно выглядящий код, используемый для настройки таймеров?

10

Глядя на эскизы, написанные другими людьми, я иногда сталкиваюсь с кодом, который выглядит примерно так:

TCCR1A = 0;
TCCR1B = 0;

TCNT1 = 34286;
TCCR1B |= (1 << CS12);
TIMSK1 |= (1 << TOIE1);

Все, что я знаю, - это то, что связано с таймерами / таймерами (я думаю). Как я могу расшифровать и создать такой код? Что TCCR1A, TCCR1B, TCNT1, CS12, TIMSK1, и TOIE1?

Парень в шляпе
источник
Не знаю достаточно, чтобы ответить, но: electronics.stackexchange.com/questions/92350/… , forum.arduino.cc/index.php?topic=134602.0 и stackoverflow.com/questions/9475482/… . Не знаю, видели ли вы это уже.
Анонимный Пингвин
1
Загрузите «Полная» таблица данных для вашего устройства с веб-сайта Atmel и прочитайте главы о таймерах. На мой взгляд, данные на удивление хороши для чтения.
Джиппи

Ответы:

15

Это не странно выглядит. Вот как на самом деле выглядит нормальный код 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это регистр, который контролирует значение выходных контактов в PORTCATmega328P. PINCрегистр, в котором доступны входные значения PORTC. По сути, такие вещи происходят внутри вас, когда вы используете функции digitalWriteили digitalRead. Тем не менее, существует операция поиска, которая преобразует «номера выводов» arduino в реальные номера выводов аппаратного обеспечения, что занимает где-то в области 50 тактов. Как вы, вероятно, можете догадаться, если вы пытаетесь идти быстро, тратить 50 тактов на операцию, которая должна требовать только 1, немного нелепо.

Вышеупомянутая функция, вероятно, занимает где-то 100-200 тактовых циклов для передачи 8 битов. Это влечет за собой 24 пин-записи и 8 операций чтения. Это во много раз быстрее, чем при использовании digital{stuff}функций.

Коннор Вольф
источник
Обратите внимание, что этот код также должен работать с Atmega32u4 (используется в Leonardo), так как он содержит больше таймеров, чем ATmega328P.
jfpoilpret
1
@Ricardo - Вероятно, более 90% встроенного кода с малым MCU использует прямую манипуляцию с регистром. Работа с непрямыми служебными функциями не очень распространенный способ манипулирования вводом / выводом. Существуют некоторые наборы инструментов для абстрагирования от аппаратного управления (например, Atmel ASF), но он, как правило, написан так, чтобы максимально компилировать, чтобы уменьшить накладные расходы времени выполнения, и почти всегда требует реального понимания периферийных устройств путем чтения таблиц данных.
Коннор Вольф
1
По сути, Arduino, говоря «здесь есть функции, которые делают X», не заботясь о том, чтобы ссылаться на фактическую документацию или на то, как аппаратное обеспечение выполняет то, что делает, очень ненормально. Я понимаю, что это ценность как вводный инструмент, но, за исключением быстрого прототипирования, в реальной профессиональной среде его никогда не бывает.
Коннор Вольф
1
Ясно, что то, что делает код arduino необычным для встроенного микропрограммного обеспечения MCU, не уникально для кода arduino, это функция общего подхода. По сути, если вы хорошо разбираетесь в реальном MCU, то для правильной работы (например, непосредственно с использованием аппаратных регистров) требуется совсем немного времени. Таким образом, если вы хотите изучить настоящего разработчика MCU, гораздо лучше просто сесть и понять, что на самом деле делает ваш MCU , а не полагаться на чужую абстракцию, которая имеет тенденцию быть дырявой.
Коннор Вольф
1
Обратите внимание, что я могу быть здесь немного циничным, но поведение, которое я вижу в сообществе arduino, - это программирование анти-паттернов. Я вижу много программ «копирование-вставка», отношение к библиотекам как к черным ящикам и просто общие плохие методы проектирования в сообществе в целом. Конечно, я довольно активен в EE.stackexchange, поэтому у меня может быть несколько наклонное представление, так как у меня есть некоторые инструменты модератора, и поэтому я вижу много закрытых вопросов. В вопросах arduino, которые я там видел, определенно есть предвзятость: «скажите мне, что исправить в C & P», а не «почему это не работает».
Коннор Вольф
3

TCCR1A регистр управления таймером / счетчиком 1

TCCR1B регистр управления таймером / счетчиком 1

TCNT1 значение счетчика таймера / счетчика 1

CS12 3-й бит выбора часов для таймера / счетчика 1

TIMSK1 регистр маски прерываний таймера / счетчика 1

TOIE1 активируется ли прерывание по переполнению таймера / счетчика 1

Таким образом, код активирует таймер / счетчик 1 на 62,5 кГц и устанавливает значение 34286. Затем он разрешает прерывание по переполнению, поэтому, когда оно достигает 65535, оно запускает функцию прерывания, скорее всего, помеченную как ISR(timer0_overflow_vect)

Доктор
источник
1

CS12 имеет значение 2, так как он представляет бит 2 регистра TCCR1B.

(1 << CS12) принимает значение 1 (0b00000001) и сдвигает его влево 2 раза, чтобы получить (0b00000100). Порядок операций диктует, что сначала в () происходят вещи, так что это делается до того, как "| =" будет вычислено.

(1 << CS10) принимает значение 1 (0b00000001) и сдвигает его влево 0 раз, чтобы получить (0b00000001). Порядок операций диктует, что сначала в () происходят вещи, так что это делается до того, как "| =" будет вычислено.

Итак, теперь мы получаем TCCR1B | = 0b00000101, который совпадает с TCCR1B = TCCR1B | 0b00000101.

С "|" равно "ИЛИ", все биты, кроме CS12 в TCCR1B, не затрагиваются.

Венкатесан
источник