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

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

TCCR1A = 0;
TCCR1B = 0;

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

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

, 👍11

Обсуждение

Не знаю достаточно, чтобы ответьте, но: http://electronics.stackexchange.com/questions/92350/what-is-the-difference-between-tccr1a-and-tccr1b, http://forum.arduino.cc/index.php?topic=134602.0 и http://stackoverflow.com/questions/9475482/pulse-width-modulation-pwm-on-avr-studio. Не знаю, видели ли вы их уже., @Anonymous Penguin

Загрузите полное техническое описание для своего устройства с [веб-сайта Atmel](http://atmel.com/) и прочитайте главы о таймерах. На мой взгляд, даташит на удивление хорош для чтения., @jippie


3 ответа


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

15

Это не выглядит странно. Так на самом деле выглядит обычный код MCU.

Здесь представлен пример концепции периферийных устройств с отображением памяти. По сути, аппаратное обеспечение MCU имеет специальные места в адресном пространстве SRAM назначенного ему MCU. Если вы записываете по этим адресам, биты байта, записанные по адресу n, управляют поведением периферийного устройства m.

По сути, некоторые банки памяти буквально имеют небольшие провода, идущие от ячейки SRAM к оборудованию. Если вы записываете «1» в этот бит в этом байте, это устанавливает эту ячейку SRAM на логический высокий уровень, который затем включает некоторую часть оборудования.

Если вы посмотрите на заголовки для MCU, там есть большие таблицы соответствия ключевых слов<->адреса. Вот как такие вещи, как TCCR1B и т. д., разрешаются во время компиляции.

Этот механизм отображения памяти чрезвычайно широко используется в микроконтроллерах. Его используют микроконтроллеры ATmega в Arduino, а также серии микроконтроллеров PIC, ARM, MSP430, STM32 и STM8, а также множество микроконтроллеров, с которыми я не сразу знаком.


Код

Arduino странный, с функциями, которые косвенно обращаются к управляющим регистрам MCU. Хотя это несколько "красивее" выглядит, это также намного медленнее и использует намного больше программного пространства.

Все загадочные константы подробно описаны в Техническое описание ATmega328P, которое вам действительно следует прочитать, если вы заинтересованы в чем-то большем, чем время от времени переключать контакты на Arduino.

Выберите выдержки из приведенной выше таблицы данных:

Например, TIMSK1 |= (1 << TOIE1); устанавливает бит TOIE1 в TIMSK1. Это достигается сдвигом двоичной единицы (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), выполнения над ней побитовой операции НЕ (~), а затем объединения И TIMSK1 на это значение NOTed (которое равно 0b11111110).

Обратите внимание, что во всех этих случаях значения таких элементов, как (1 << TOIE1) или _BV(TOIE1), полностью разрешаются при компилировании. time, поэтому они функционально сводятся к простой константе и, следовательно, не требуют времени выполнения для вычислений во время выполнения.


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

uint8_t transactByteADC(uint8_t outByte)
{
    // Передает один байт в АЦП и одновременно получает один байт
    // ничего не делает с выборкой чипа
    // Сначала старший бит, данные синхронизируются по переднему фронту

    uint8_t loopCnt;
    uint8_t retDat = 0;

    for (loopCnt = 0; loopCnt < 8; loopCnt++)
    {
        if (outByte & 0x80)         // если текущий бит высокий
            PORTC |= _BV(ADC_MOSI);     // устанавливаем строку данных
        else
            PORTC &= ~(_BV(ADC_MOSI));  // иначе сбросить его

        outByte <<= 1;              // и сдвигаем выходные данные для следующей итерации
        retDat <<= 1;               // сдвиг по считанным данным обратно

        PORTC |= _BV(ADC_SCK);          // Установить часы на высокий уровень

        if (PINC & _BV(ADC_MISO))       // выборка входной строки
            retDat |= 0x01;         // и устанавливаем бит в ретвале, если на входе высокий уровень

        PORTC &= ~(_BV(ADC_SCK));       // устанавливаем часы на низкий уровень
    }
    return retDat;
}

PORTC — это регистр, который управляет значением выходных контактов в пределах PORTC ATmega328P. PINC — это регистр, в котором доступны входные значения PORTC. По сути, подобные вещи происходят внутри, когда вы используете функции digitalWrite или digitalRead. Тем не менее, существует операция поиска, которая преобразует «номера контактов» Arduino в фактические номера аппаратных контактов, что занимает где-то около 50 тактовых циклов. Как вы, наверное, догадались, если вы пытаетесь работать быстро, тратить 50 тактов на операцию, которая должна требовать только 1, немного нелепо.

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

,

Обратите внимание, что этот код также должен работать с Atmega32u4 (используется в Leonardo), так как он содержит больше таймеров, чем ATmega328P., @jfpoilpret

+1 Отличный ответ!! Не могли бы вы немного пояснить, что вы подразумеваете под «странным кодом Arduino»? Что именно уникального в коде Arduino, чего обычно нет на других платформах?, @Ricardo

@Ricardo - Вероятно, более 90% встроенного кода малых микроконтроллеров использует прямую манипуляцию регистрами. Делать что-то с помощью косвенных служебных функций — это совсем не обычный способ манипулирования вводом-выводом/периферийными устройствами. Есть несколько наборов инструментов для абстрагирования аппаратного управления (например, Atmel ASF), но они, как правило, написаны для максимально возможной компиляции, чтобы уменьшить накладные расходы во время выполнения, и почти всегда требуют фактического понимания периферийных устройств путем чтения технических описаний., @Connor Wolf

По сути, ардуино, говоря «вот функции, которые делают X», не утруждая себя ссылками на реальную документацию или на то, как аппаратное обеспечение *делает* то, что оно делает, очень ненормально. Я понимаю, что это полезно как начальный инструмент, но, за исключением быстрого прототипирования, он никогда не используется в реальной профессиональной среде., @Connor Wolf

Чтобы было ясно, то, что делает код Arduino необычным для встроенной прошивки MCU, не *уникально* для кода Arduino, это функция общего подхода. По сути, как только вы хорошо разбираетесь в *фактическом* микроконтроллере, правильное выполнение действий (например, использование аппаратных регистров напрямую) практически не требует дополнительного времени. Таким образом, если вы хотите изучить настоящий разработчик MCU, гораздо лучше просто сесть и понять, что ваш MCU *на самом деле* делает, а не полагаться на чью-то *чужую* абстракцию, которая имеет тенденцию быть негерметичной., @Connor Wolf

По сути, вся система Arduino более или менее спроектирована таким образом, чтобы обеспечить низкий порог входа для *не*программистов. Он делает это хорошо, но в то же время он не помогает конечным пользователям действительно *понимать* аппаратное обеспечение просто потому, что это несовместимо с низким барьером входа. Я понимаю, что это может (и является) ценной ступенькой для людей на пути к изучению программного обеспечения / разработки MCU, но любой, кто его использует, не должен питать никаких иллюзий, что путь arduino обязательно лучший, или даже иногда хороший способ делать то, что он делает., @Connor Wolf

Обратите внимание, что я могу быть немного циничным, но многие действия, которые я вижу в сообществе arduino, представляют собой программирование анти-паттернов. Я вижу много программирования "копирование-вставка", обращение с библиотеками как с черными ящиками и просто плохие методы проектирования в сообществе в целом. Конечно, я довольно активен на EE.stackexchange, поэтому у меня может быть несколько предвзятое мнение, поскольку у меня есть некоторые инструменты модератора, и поэтому я вижу много закрытых вопросов. Определенно существует предвзятость в вопросах Arduino, которые я видел там, в сторону «скажите мне, что нужно исправить C&P», а не «почему это не работает»., @Connor Wolf

Вероятно, также стоит отметить, что само использование C++ *несколько* необычно. Большая часть работы с небольшими микроконтроллерами выполняется только на чистом C., @Connor Wolf

Понятно! Мне было непонятно, что вы называете непрямым доступом к регистрам в Arduino, но теперь я знаю, что это абстракция типа digitalWrite(). Это снижает барьер для входа, но позже становится барьером для движения вперед. Но ваш ответ — большой шаг к устранению этого препятствия. По крайней мере для меня., @Ricardo

@ Рикардо - Ага. Одна из вещей, которые мне действительно *нравятся* в Arduino как учебном инструменте, заключается в том, что она дает вам рабочую среду для непосредственного использования оборудования, без необходимости иметь дело с часто самой раздражающей частью: начальным обучением. Я работал на платформах, где у меня был только интернет-провайдер и не было рабочих примеров. Приходится пытаться устранить неполадки, почему последовательный интерфейс отладки, который я пытался заставить работать без какого-либо реального интерфейса отладки,... задействован. Очень приятно иметь платформу с простыми в использовании последовательными библиотеками., @Connor Wolf


3

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

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

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 не затрагиваются.

,