Ihads.ru

Все про недвижимость
19 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

STM32 — с нуля до RTOS. 2: Таймер и прерывания

Задержка пустым циклом — это кощунство, тем более на таком мощном кристалле как STM32, с кучей таймеров. Поэтому сделаем эту задержку с помощью таймера.

В STM32 есть разные таймеры, отличающиеся набором свойств. Самые простые — Basic timers, посложнее — General purpose timers, и самые сложные — Advanced timers. Простые таймеры ограничиваются просто отсчётом тактов. В более сложных таймерах появляется ШИМ. Самые сложные таймеры, к примеру, могут сгенерировать 3–фазный ШИМ с прямыми и инверсными выходами и дедтаймом. Нам хватит и простого таймера, под номером 6.

Немного теории

Всё, что нам требуется от таймера — досчитывать до определённого значения и генерировать прерывание (да, мы ещё и научимся использовать прерывания). Таймер TIM6 тактируется от системной шины, но не напрямую а через прескалер — простой программируемый счётчик–делитель (подумать только, в СССР выпускались специальные микросхемы–счётчики, причём программируемые были особым дефицитом — а теперь я говорю о таком счётчике просто между делом). Прескалер можно настраивать на любое значение от 1 (т.е. на счётчик попадёт полная частота шины, 24МГц) до 65536 (т.е. 366 Гц).

Тактовые сигналы в свою очередь, увеличивают внутренний счётчик таймера, начиная с нуля. Как только значение счётчика доходит до значения ARR — счётчик переполняется, и возникает соответствующее событие. По наступлению этого события таймер снова загружает 0 в счётчик, и начинает считать с нуля. Одновременно он может вызвать прерывание (если оно настроено).

STM lessons TIM overflow irq

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

Код будет очень похож на предыдущий, т.к. инициализация всей периферии происходит однотипно — за тем лишь исключением, что таймер TIM6 висит на шине APB1. Поэтому включение таймера: RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);

Теперь заводим структуру типа TIM_TimeBaseInitTypeDef, инициализируем её (TIM_TimeBaseStructInit), настраиваем, передаём её в функцию инициализации таймера (TIM_TimeBaseInit) и наконец включаем таймер (TIM_Cmd).

Что за магические числа? Как мы помним, на шине присутствует тактовая частота 24МГц (при наших настройках проекта). Настроив предделитель таймера на 24000, мы поделим эту частоту на 24 тысячи, и получим 1кГц. Именно такая частота попадёт на вход счётчика таймера.

Значение же в счётчике — 1000. Значит, счётчик переполнится за 1000 тактов, т.е. ровно за 1 секунду.

После этого у нас действительно появляется работающий таймер. Но это ещё не всё.

Embedder's life

Таймеры — еще один классический модуль, присутствующий практически во всех МК. Он позволяет решать множество задач, самая распространная из которых — задание стабильных временных интервалов. Второе по популярности применение — генерация ШИМ (о нем далее) на выводе МК. Несмотря на то, что, как уже было сказано, применение таймеров отнюдь не ограничивается этими задачами, здесь будут рассмотрены только эти две как наиболее распространенные.

Сам по себе таймер представляет собой двоичный счетчик, подключенный к системе тактирования микроконтроллера через дополнительный делитель. К нему, в свою очередь, подключены блоки сравнения (их может быть много), которые способны выполнять разные полезные функции и генерировать прерывания — в зависимости от настроек. Упрощенно устройство таймера можно представить следующим образом:

Настройка таймера, как и всей остальной периферии, производится через его регистры.

Генерация временных интервалов с помощью таймера.

Прерывания.

Из названия явствует, что главным назначением блоков сравнения является постоянное сравнение текущего значения таймера со значением, заданным в регистре OCRnX. Уже упоминалось, что имена регистров часто несут в себе глубокий сакральный смысл — и регистры сравнения не являются исключением. Так, n обозначает номер таймера, X — букву (тоже способ нумерации, блоков сравнения может быть много) регистра сравнения. Таким образом, OCR1A можно понять как Output Compare Register of 1 st timer, unit A. К слову, искушенному эмбеддеру это даcт возможность предположить, что, возможно, существует таймер 0 и регистр сравнения B…

Итак, блоки сравнения могут генерировать прерывания при каждом совпадении значения таймера (к слову, оно находится в регистре TCNTnTimer/CouNTer #n) с заданым числом. Читателю уже должно быть знакомо понятие прерывания, однако на всякий случай освежим его в памяти, а заодно и поговорим о том, как его описать на С. Так вот, вышесказанное значит, что, как только случится описанное событие, процессор сохранит номер текущей команды в стеке и перейдет к выполению специально определенного кода, а после вернется обратно. Все происходит почти так же, как и при вызове обычной функции, только вызывается она на аппаратном уровне. Объявляются такие функции с помощью макроса, объявленного в avr/interrupt.h (ISR — «Interrupt Service Routine», «обработчик прерывания»):

Каждому прерыванию (естесственно, их много) соответствует т.н. вектор прерывания — константа, также объявленная в avr/interrupt. Например, обработчик прерывания по совпадению значения таймера со значением регистра OCR1A будет иметь следующий вид:

Несомненно, проницательный читатель уже догадался, каким образом формируются имена векторов. Тем не менее, полный список этих констант можно посмотреть в документации на avr-libc (библиотека стандартных функций для AVR-GCC).

Итак, для того, чтобы получить функцию, вызываемую через точные промежутки времени, нам осталось только сконфигурировать блок сравнения. Нужные настройки находятся в регистрах TCCR1B, TIMSK1 и, конечно, OCR1A. Здесь нам придется обратиться к даташиту на ATmega48.

Даташит (от англ. datasheet) — файл технической документации, описание конкретного прибора (микросхемы, транзистора и т.д.). Содержит всю информацию о характеристиках и применении компонента. Почти всегда имеет формат PDF. Обычно гуглится как «<название компонента> pdf».

Последние три бита управляют предделителем, упомянутым в самом начале (остальные же нас пока не интересуют):

Сконфигурируем таймер так, чтобы прерывания происходили два раза в секунду. Выберем предделитель 64; для этого установим биты CS11 и CS10:

Тогда частота счета составит 8МГц/64=125КГц, т.е. каждые 8мкС к значению TCNT1 будет прибавляться единица. Мы хотим, чтобы прерывания происходили с периодом 500мС. Очевидно, что за это время таймер досчитает до значения 500мС/8мкС=62500, или 0xF424. Таймер 1 — шестнадцатибитный, так что все в порядке.

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

Читайте так же:
Счетчик это материальный запас

Осталось только разрешить прерывание по совпадению — за него отвечает бит в регистре TIMSK1:

Про него написано следующее:

Итак, устанавливаем нужное значение:

Кроме того, следует помнить, что перед использованием прерываний необходимо их глобально разрешить вызовом функции sei(). Для глобального запрета прерываний служит функция cli(). Эти функции устанавливают/очищают бит I в регистре SREG, управляя самой возможностью использования такого механизма, как прерывания. Регистры же вроде TIMSKn — не более чем локальные настройки конкретного модуля.

Как уже упоминалось, прерывание может возникнуть в любой момент, прервав программу в любом месте. Однако существуют случаи, когда это нежелательно. Механизм глобального запрета/разрешения прерываний позволяет решить эту проблему.

Итак, программу, мигающую светодиодом, с использованием прерываний можно переписать следующим образом:

Видно, что теперь в промежутках между переключениями светодиодов процессор абсолютно свободен для выполнения других задач, в то время как в первом примере он был занят бесполезным подсчетом тактов (функции _delay_xx() работают именно так). Таким образом, прерывания позволяют организовать примитивную многозадачность.

Генерация ШИМ с помощью таймера.

При определеных настройках блоки сравнения позволяют организовать аппаратную генерацию ШИМ-сигнала на ножках МК, обозначенных как OСnX:

ШИМ (PWM) — Широтно-Импульсная Модуляция (Pulse Width Modulation). ШИМ-сигнал представляет собой последовательность прямоугольных импульсов с изменяющейся длительностью:

Для ШИМ вводятся две родственные характеристики — коэффициент заполнения (duty cycle, D) и скважность — величина, обратная коэффицинту заполнения. Коэффициент заполнения представляет собой отношение времени импульса к длительности периода:

Коэффициент заполнения часто выражается в процентах, но так же распространена запись в десятичных дробях.

Значение ШИМ для народного хозяйства заключается в том, что действующее значение напряжения такого сигнала прямо пропоционально коэффициенту заполнения:

— с этим интегралом пособие смотрится солиднее; зависимость же выражается следующей формулой:

Uavg — среднее значение напряжения (тут — оно же действующее);
D — коэффициент заполнения;
Up-p — амплитуда импульса.

Таким образом, ШИМ является простым способом получить аналоговый сигнал с помощью микроконтроллера — для этого такую последовательность импульсов надо подать на фильтр низких частот (который, кстати, и является физическим воплощением интеграла, записанного выше).

Наиболее употребительным режимом ШИМ является т.н. Fast PWM (об остальных режимах можно прочесть непосредственно в документации), поэтому рассмотрим его. В этом случае блоки сравнения работают следующим образом: с обнулением таймера на выход OCnX подается высокий уровень; как только таймер досчитает до числа, записанного в OCRnX, OCnX переводится в состояние низкого уровня. Все это повторяется с периодом переполнения счетчика. Получается, что ширина выходного импульса зависит от значения OCRnX, а выходная частота равна тактовой частоте таймера, поделенной на его максимальное значение. Рисунок из даташита поясняет сказанное:

Возможен также инверсный режим, в котором изменение состояния OCnX производится в обратной последовательности, что бывает удобно на практике.

Настройка блока сравнения для генерации ШИМ.

Здесь нам опять поможет документация. Итак, сначала надо перевести блок сравнения в режим генерации ШИМ и выбрать интересующий выход из доступных. Эти настройки доступны в регистре TCCR0A:

Нас интересуют биты WGMxx и COMnXn. Про них сказано следующее:

Т.е., нас интересуют биты WGM00 и WGM01 — Fast PWM mode,

а также COM0A1 — non-inverting PWM на выводе OC0A. Настраиваем:

Естесственно, кроме этого выбранная ножка должна быть настроена на выход с помощью регистра DDR соответствующего порта.

Далее стоит инициализировать регистр OCR0A, например, значением 128 — 50% коэффициент заполнения:

И, наконец, включить таймер, выбрав делитель. Тут все так же:


Обычно для ШИМ выбирается максимально возможная частота (для того, чтобы получить максимальное качество выходного сигнала). Т.е., целесообразно установить минимальное значение делителя:

На этом этапе настройка ШИМ завершается, и на выбранной ножке можно увидеть сигнал.

Как упомянуто выше, ШИМ — простой способ получения аналогового сигнала с помощью МК. Например, можно организовать плавное мигание светодиода (в этом случае роль интегратора-ФНЧ выполняет глаз наблюдателя, так что светодиод можно подключить к ножке МК через обычный резистор).

Некоторые моменты в предлагаемом примере требуют пояснения.

В списке включаемых файлов присутствует загадочный stdint.h — в этом файле объявлены типы с явно указанной разрядностью, например

uint8_tunsigned 8-bit integer type
uint16_tunsigned 16-bit integer type
uint32_tunsigned 32-bit integer type
int8_t — signed 8-bit integer type

и так далее. Такие типы способствуют единообразию и удобочитаемости программы. Кроме того, гарантируется, что при портировании кода разрядность данных останется указанной. И, кстати, uint8_t писать гораздо быстрее, чем unsigned char.

Модификатор volatile означает, что компилятору запрещается оптимизировать данную переменную. Например, если скомпилировать следующий пример:

после чего изучить дизассемблированный код, можно обнаружить, что на самом деле никакой переменной создано не было, и программа представляет собой пустой цикл. Это произошло потому, что оптимизатор посчитал переменную неиспользуемой, и не включил ее в результирующий код. Если бы подобным образом объявленная переменная использовалась, например, в прерывании, такая вольность оптимизатора вызвала бы некорректную работу программы. Применение volatile исключает такое поведение.

STM32: Урок 6.2 — Таймеры общего назначения и продвинутые

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

  • До 4-х каналов для:
    • Захвата сигнала (input capture).
    • Сравнения вывода (output compare).
    • Генерации сигнала ШИМ (выровненного по границе или по центру).
    • Генерации одиночных импульсов.
    • Обновление: переполнение счётчика.
    • Событие-триггер: старт, остановка, инициализация счётчика или его обновление внутренним или внешним триггером.
    • Захват сигнала.
    • Сравнение (output compare).
    • Включение BRK.

    Вот как вы думаете, если у таймеров общего назначения так много функций, чем тогда продвинутые (advanced-control) таймеры отличаются от них? o_O
    Правильный ответ — почти ничем, это по факту просто таймеры общего назначения, которые не имеют никаких ограничений: в них напихано по 4 канала (с комплементарными) и есть все возможности сразу, без какого-то ни было разброса. Так что остальная часть статьи будет относиться ко всем таймерам выше базовых, а продвинутые таймеры я отдельно упоминать не буду.

    В даташите на STM32F100xx ( ещё ссылка ) есть сводная таблица возможностей таймеров, в которую тоже удобно поглядывать для справки:

    Кстати, обращайте внимание на сноски. Например, там написано, что у МК семейства Low density Value line нет таймера TIM4.

    Захват сигнала

    В этом режиме с выбранного канала захватываются импульсы, на каждый из них текущее значение счётчика таймера кладётся в регистр TIM_CCRx, где x — номер канала. Таким образом, период следования импульсов равен разнице между текущим значением TIM_CCRx и предыдущим. Ну а для того, чтобы получить период в каких-то внятных единицах измерения, нужно настроить предделитель через функции базового таймера.

    При этом можно настроить генерацию прерывания и запроса DMA на приход очередного импульса, и если в это время предыдущее значение TIM_CCRx не было считано, будет сгенерировано так называемое прерывание over-capture, т.е. сигнал о том, что предыдущее значение потерялось.

    Ловить можно фронты, спады или и то, и другое вместе. Есть настройка так называемого фильтра — числа выборок, после которого переход уровня будет считаться состоявшимся (полезно для устранения дребезга). Значение фильтра может принимать значения от 0 (фильтр выключен) до 15 (0xF). Также настраивается делитель входной частоты — 2, 4 или 8: будет ловиться каждый 2й, 4й или 8й импульс соответственно.

    Примера ради подёргаем вывод PB15 и замерим таймером TIM3 период, подключив PB15 к его каналу 1 (PA6):

    Также существует режим захвата ШИМ. На самом деле, это не отдельный режим, а просто особое сочетание настроек с таким эффектом. Таймер настраивается так, чтобы один канал ловил фронты и сбрасывал счётчик таймера, а второй ловил спады — тогда первый будет захватывать период ШИМ, а второй — заполнение. При этом оба канала подключаются к одному и тому же физическому входу. Суть работы этого «режима» показана в даташите следующим образом:

    Изменим предыдущий пример, используя захват ШИМ (прокомментированы только изменения):

    Режим чтения энкодера

    Работу с энкодером я уже как-то описывал , и тогда я считывал и декодировал данные с энкодера программно, здесь же таймер сделает работу за нас (не всю, конечно же). Боковые выводы энкодера надо подключить к двум каналам таймера, а средний вывод — к GND. Таймер в этом режиме сам обрабатывает поступающие с энкодера импульсы, а также увеличивает/уменьшает свой счётчик на 4 при каждом щелчке энкодера, и запоминает направление вращения.

    Так как мне захотелось ещё и прерывание заиметь, я сделал период равным 4 и разрешил счёт в обе стороны, так что теперь прерывание будет возникать при каждом щелчке энкодера. Использовал я каналы 1 и 2 таймера TIM3 (PA6 и PA7):

    Сравнение вывода (output compare)

    В этом режиме выбранный канал таймера будет подключен к соответствующему выводу и будет изменять его (вывода) состояние каждый раз, когда счётчик таймера досчитает до значения регистра TIM_CCRx. Состояние вывода, в зависимости от настройки, будет меняться на ноль, на единицу или на противоположное текущему. У многих таймеров у каналов есть комплементарные выводы, которые по умолчанию являются инверсными: на такой выход подаётся тот же сигнал, что и на обычный, но с противоположным уровнем.

    Смотрим в сводную таблицу по таймерам в даташите и видим, что комплементарных выводов у TIM3 нет, но вот у единственного канала таймера TIM16 есть такой вывод — этот таймер я и использую для примера. Вообще, комплементарные выводы есть и у нескольких других таймеров, но вот TIM15 — особенный: у него есть два канала, но комплементарный вывод имеет только 1й канал. Будьте бдительны!

    В таблице пинаутов находим выводы канала 1 таймера TIM16 — PB8 (основной) и PB6 (комплементарный). Для иллюстрации работы таймера подключим эти выводы к светодиодам на плате STM32VLDiscovery — PC8 и PC9, которые в коде мы отключим от греха подальше. Таким образом, выводы канала таймера будут напрямую мигать светодиодами:

    В это примере я выбрал режим переключения вывода в противоположное состояние (TIM_OCMode_Toggle), а остальные настройки оставил по умолчанию. Кстати, не забывайте вызывать функции типа TIM_OCStructInit() для инициализации соответствующих структур, даже если заполняете все поля структур вручную: copy&paste-ориентированное программирование никто не отменял, но при нём легко забыть заполнить какое-нибудь поле и ловить потом баги.

    Для обоих выводов канала можно настроить (поля TIM_OCPolarity и TIM_OCNPolarity структуры) так называемую «полярность» — состояние вывода в промежуток времени от начала отсчёта и до TIM_Pulse. По умолчанию для выводов выставляются значения TIM_OCPolarity_High и TIM_OCNPolarity_High, но комплементарный вывод является инверсным — поэтому, если на нём нужен обычный (не инверсный) сигнал, нужно ему выставить TIM_OCNPolarity_Low.

    В режиме сравнения генерируется то же самое прерывание, что и в режиме захвата сигнала — TIM_IT_CCx, но здесь оно было не нужно, поэтому я его не разрешал.

    Генерация ШИМ

    Вот это куда более интересная и практичная штука. Принципы ШИМ уже неоднократно были описаны — как на нашем сайте , так и у Di Halt’a (уж там разжёвано всё до мелочей), а я сосредоточусь на особенностях реализации в STM32.

    Настройка этого режима не слишком отличается от настройки output compare: вместо режима TIM_OCMode_Toggle нужно выбрать один из режимов ШИМ, тогда TIM_Period будет трактоваться как период ШИМ, а поле TIM_Pulse — как заполнение (duty cycle). Режимов ШИМ имеется два — выровненный по границе и по центру (edge-aligned и center-aligned). У микроконтроллеров AVR они называются Fast PWM и Phase Correct PWM, соответственно.

    Отличной иллюстрацией крутизны таймеров STM32 для генерации ШИМ будет типичная прикладная задача — управление сервомашинкой : Как известно, сервы управляются импульсами переменной ширины, которые шлются с частотой примерно 50 Гц (каждые 20 мс). У сервы, которая оказалась под рукой (Robbe 4.3 g), ширина управляющего импульса от 500 мкс (0°) до 2250 мкс (175°), судя по замерам — то есть, по 10 мкс на каждый градус поворота:

    ΔT = T₂ — T₁ = 2250 — 500 = 1750 мкс
    ∠A = 175°
    ΔT/A = 10 мкс/°

    1. Установить таймеру такой делитель частоты, чтобы отсчёт вёлся каждые 10 мкс.
    2. Задать период ШИМ в 20 мс, то есть 2000 отрезков времени по 10 мкс.
    3. Класть в регистр сравнения число, равное 50 (500 мкс / 10 мкс) + задаваемый угол.
    4. Регистр сравнения лучше обновлять строго в момент окончания периода во избежание дёргания сервы.

    Dead-time

    Если кто не знает, это задержка фронтов сигналов на основном и комплементарном выводах канала таймера. Эта функция есть у некоторых таймеров (смотрите руководство), и нужна она бывает для исключения сквозных токов при управлении силовыми ключами [полу]мостовых схем. Даже не спрашивайте меня, что это такое — это вне моей компетенции.

    Настраивается этот самый dead-time в поле TIM_DeadTime структуры TIM_BDTRInitTypeDef и имеет диапазон значений с 0 по 255 (0xFF). Но смысл этого числа не так уж прямолинеен:

    Ага, вот так оно и рассчитывается. Здесь Tdts — это длительность такта генератора dead-time (DTG), зависящая от Tdts — текущей частоты тактирования таймера. Обычно таймеры тактируются системной частотой, и TIM_Prescaler на это никак не влияет, а влияет поле TIM_ClockDivision структуры TIM_TimeBaseInitTypeDef — делитель частоты таймера.

    Для примера положим, что таймер затактирован без деления частоты (делитель равен 1, TIM_CKD_DIV1), системная частота F равна 24 МГц, а значение DTG = 150. Тогда:

    Tdts = 1/F = 1/24 мкс
    DTG = 150 = 100101102 ⇒ DTG[7:5] = 1002
    Tdtg = 2⋅Tdts = 1/12 мкс
    DT = (64 + DTG[5:0])Tdtg = (64 + 6)/12 = 5.8(3) мкс

    «Just like that» ☯ ChosunNinja

    Я тут для примера набросал код с dead-time попроще для расчёта: DTG=96 ⇒ DT=96.

    Для того, чтобы узреть этот самый dead-time на одноканальном осциллографе, нужно подключить PB8 и PB6 через резисторы 1 кОм к его щупу:

    Т.к. на эти выводы идут взаимно инверсные сигналы, на экране будут прекрасно видны места, где во время dead-time уровень на обоих входах одинаков из-за задержки фронтов:

    Ну, и напоследок — имейте ввиду, что если длительность dead-time превышает длительность импульса на выводе, то соответствующий импульс не будет сгенерирован вообще.

    Счётчик повторений

    Этот счётчик имеется у нескольких таймеров (TIM15, TIM16 и TIM17) и выполняет он очень простую функцию: генерировать событие (прерывание или запрос DMA) update не на каждое переполнение счётчика, а на каждые N переполнений. То есть, вы задаёте счётчик повторений, таймер его копирует в скрытый регистр и при каждом переполнении уменьшает значение копии на 1. Когда значение достигает нуля, генерируется событие update, таймер снова копирует счётчик повторений и т.д. На самом деле, перечисленные таймеры и так задействуют этот счётчик, просто по умолчанию его значение равно нулю, и событие генерируется на каждое переполнение.

    Счётчик может принимать значения от 0 до 255 (0xFF). Описывать тут особо нечего, потому что для использования этой функции достаточно при инициализации таймера написать что-то вроде:

    и всё. В этом случае событие update будет генерироваться каждые 8 переполнений (7 повторений).

    Вход BRK

    Если вам вдруг понадобится резко перевести выводы каналов таймера в заранее определённое состояние (например, выключить), то эта функция — то, что нужно. Включить её проще пареной репы — нужно сконфигурировать пин TIMx_BKIN на вход, и при инициализации BDTR включить вход BRK:

    По умолчанию для активации функции break нужно на вход BRK подать логический ноль, но это можно настроить в поле TIM_BreakPolarity. Как только break активирован, все выводы каналов переходят в состояние, которое задаётся при их инициализации полями TIM_OCIdleState и TIM_OCNIdleState в структуре TIM_OCInitTypeDef (по умолчанию на выводах будет низкий уровень). Dead-time при этом учитывается.

    Синхронизация таймеров

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

    Второй случай (цепочка таймеров) больше подойдёт для иллюстрации, ибо интереснее он. Сделаем-ка для примера 32-битный таймер из двух обычных 16-битных. Для примера я возьму таймеры TIM2 и TIM3. Задача состоит в том, таймер TIM3 тактировал таймер TIM2 по переполнению своего счётчика: то есть, счётчик таймера TIM2 будет увеличиваться при переполнении счётчика TIM3 — получаем 32-битный счётчик, «состоящий» из TIM2_CNT (старшие биты) и TIM3_CNT (младшие).

    Для этого нужно настроить выходной триггер таймера TIM3 на переполнение (update), а входной триггер таймера TIM2 — на вход с триггера TIM3. Смотрим в таблицу соединения триггеров для таймеров TIM2-TIM4 (таких таблиц несколько — для разных групп таймеров):

    Здесь мы видим, что TIM3 соединён с входом ITR2 таймера TIM2. И тут выясняется, что в Reference manual рассматриваемый случай описан в разделе «Using one timer as prescaler for another», но там допущена ошибка: вместо ITR2 там указан ITR1. Я джва года час искал ошибку в коде!

    Алгоритм написания кода для подключения динамической индикации

    Для большей конкретизации действий будем применять 4-х разрядный семисегментный индикатор с общим катодом. Первым делом следует создать массив цифр от 0 до 9. Этому мы уже научились ранее, вот здесь. Далее необходимо разбить 4-х значное число на четыре отдельных цифры. Например, число 1987 нужно разбить на 1, 9, 8 и 7. Затем единицу нужно отобразить в первом разряде индикатора, девятку – во втором, восьмерку – в третьем и семерку – в четвертом.

    Среди многих алгоритмов разбивки многозначного числа на отдельные числа мы воспользуемся операциями деления и остатком от деления. Рассмотрим пример:

    В языке С при использовании целочисленного типа данных int при выполнении деления все десятые, сотые и т. д., то есть все числа меньше единицы отбрасываются. Остаются только целые числа. Математическое округление здесь не работает, то есть 1,9 в данном случае будет 1, а не 2.

    Команда “остаток от деления” обозначается знаком процента «%». Данная команда отбрасывает все целые числа и оставляет остальную часть числа. Например, 1987%1000 → 987; 1987%100 → 87; 1987%10 → 7.

    Далее следует написать команду, которая сначала отобразит первый разряд и соответствующее ему число, потом, через некоторый промежуток времени, второй разряд и отвечающее ему число; и так далее. Ниже приведен код с комментариями.

    #define F_CPU 1000000L

    #include <avr/io.h>

    #include <util/delay.h>

    #define CHISLO PORTD

    #define RAZRIAD PORTB

    unsigned int razr1 = 0, razr2 = 0, razr3 = 0, razr4 = 0;

    unsigned int chisla [10] = <

    // числа от 0 до 10

    0x3f, 0x6, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x7, 0x7f, 0x6f

    void vse_chislo (unsigned int rabivka_chisla)

    razr1 = rabivka_chisla/1000; // тысячи

    razr2 = rabivka_chisla%1000/100; // сотни

    razr3 = rabivka_chisla%100/10; // десятки

    razr4 = rabivka_chisla%10; // единицы

    int main(void)

    DDRB = 0b00001111;

    DDRD = 0b11111111;

    RAZRIAD = 0b00000001; // изначально 1-й разряд

    CHISLO = 0x3f; // число 0

    while (1)

    vse_chislo(1987); // отображаемое число

    RAZRIAD = 0b00000001; // включаем 1-й разряд, остальные выключаем

    CHISLO = chisla [razr1]; // отображаем 1-ю цифру

    _delay_ms(3);

    RAZRIAD = 0b00000010; // включаем 2-й разряд, остальные выключаем

    CHISLO = chisla [razr2]; // отображаем 2-ю цифру

    _delay_ms(3);

    RAZRIAD = 0b00000100; // включаем 3-й разряд, остальные выключаем

    CHISLO = chisla [razr3]; // отображаем 3-ю цифру

    _delay_ms(3);

    RAZRIAD = 0b00001000; // включаем 4-й разряд, остальные выключаем

    CHISLO = chisla [razr4]; // отображаем 4-ю цифру

    _delay_ms(3);

    Таймеры Счетчики

    melkiy93

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

    Последние посетители 0 пользователей онлайн

    • Ни одного зарегистрированного пользователя не просматривает данную страницу

    Сообщения

    BAFI

    Alexandr Uvarov

    BAFI


    Адаптеры SOP SSOP TSSOP SOT23 и т.п. на DIP

    Похожий контент

    Константин Галкин

    Начинаю знакомство с STM32 после AVRок. Решил сделать дрыганог через прерывание по таймеру. Плата Nucleo на базе STM32F446re, частота работы до 180МГц, собираю проект в CubeIDE.
    Для опыта решил взять таймер6 (на самом деле мне всё равно какой) и вызывать прерывания по переполнению счётчика. По даташиту, его тактирование идёт от APB1, которая работает на 45МГц, с множителем Х2 (т.е получается 90МГЦ). Прескалер выставил в 89 (т.е счёт должен идти с частотой 1 МГц), каунтер на 1 (т.е по идее я должен получать прерывания с частотой 500КГц).
    static void MX_TIM6_Init(void) < TIM_MasterConfigTypeDef sMasterConfig = <0>; htim6.Instance = TIM6; htim6.Init.Prescaler = 89; htim6.Init.CounterMode = TIM_COUNTERMODE_UP; htim6.Init.Period = 1; htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_Base_Init(&htim6) != HAL_OK) < Error_Handler(); >sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK) < Error_Handler(); >> В обработчике прерывания вызываю смену состояния светодиода на PA5.
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) < if(htim->Instance == TIM6) //check if the interrupt comes from TIM1 < HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); >> Загрузив код и проверив логическим анализатором я получаю частоту 254,5Кгц. Меняя значения прескалера в меньшую сторону увеличения частоты добиться не могу (основной цикл пуст, в программе никаких вычислений кроме этого нет). Почему так происходит?

    Nosi Usi

    Добрый день. Подскажите пожалуйста, как бы мне реализовать схему автоотключения (забытого включенного света).
    Что есть: 12v аккумулятор, LED-лента, выключатель. Хочу собрать схему, при которой питание на LED-ленту будет подаваться не более чем N-минут.
    Всё что находил — это схемы через мосфет и конденсатор но с тач-кнопкой, это не совсем то, т.к. необходимо срабатывание «таймера» при замыкании цепи и отключение таймера и света при размыкании цепи. Желательно, что бы потребление схемы было минимальным, а в идеале — размыкание всей цепи по таймеру.

    Сергей Фомин

    IgnatiusF

    mr_smit

    100 мсек формировать на ножке МК, например,такую последовательность:

    Стартовую длительность формирует таймер, в первом же своем прерывании по совпадению активирует DMA и дальше уже DMA по запросу таймера загружает значение CCR из массива. Что то похожее на управление светодиодами WS2812B. То что я сочинил выдает на пин:

    Но только один раз при первом вызове. При последующих вызовах данные из массива выдаются без первоначальной длительности в 150 мкс.
    Не могу найти ошибку.

    Arduino и прерывания таймера

    Arduino и прерывания таймера

    2019-05-24 в 7:51, admin , рубрики: arduino, avr, timers, Разработка под Arduino

    Привет! Представляю вашему вниманию перевод статьи "Timer interrupts" автора

    Предисловие

    Плата Arduino позволяет быстро и минимальными средствами решить самые разные задачи. Но там где нужны произвольные интервалы времени (периодический опрос датчиков, высокоточные ШИМ сигналы, импульсы большой длительности) стандартные библиотечные функции задержки не удобны. На время их действия скетч приостанавливается и управлять им становится невозможно.

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

    Arduino и прерывания таймера - 1

    В этой статье обсуждаются таймеры AVR и Arduino и то, как их использовать в Arduino проектах и схемах пользователя.

    Что такое таймер?

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

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

    Итак, предположим, что имеется устройство, которое должно что-то делать, например мигать светодиодом каждые 5 секунд. Если не использовать таймеры, а писать обычный код, то надо установить переменную в момент зажигания светодиода и постоянно проверять не наступил ли момент ее переключения. С прерыванием по таймеру вам достаточно настроить прерывание, и затем запустить таймер. Светодиод будет мигать точно вовремя, независимо от действий основной программы.

    Как работает таймер?

    Он действует путем увеличения переменной, называемой счетным регистром. Счетный регистр может считать до определенной величины, зависящей от его размера. Таймер увеличивает свой счетчик раз за разом пока не достигнет максимальной величины, в этой точке счетчик переполнится и сбросится обратно в ноль. Таймер обычно устанавливает бит флага, чтобы дать вам знать, что переполнение произошло.

    Вы можете проверять этот флаг вручную или можете сделать таймерный переключатель — вызывать прерывание автоматически в момент установки флага. Подобно всяким другим прерываниям вы можете назначить служебную подпрограмму прерывания (Interrupt Service Routine или ISR), чтобы выполнить заданный код, когда таймер переполнится. ISR сама сбросит флаг переполнения, поэтому использование прерываний обычно лучший выбор из-за простоты и скорости.

    Чтобы увеличивать значения счетчика через точные интервалы времени, таймер надо подключить к тактовому источнику. Тактовый источник генерирует постоянно повторяющийся сигнал. Каждый раз, когда таймер обнаруживает этот сигнал, он увеличивает значение счетчика на единицу. Поскольку таймер работает от тактового источника, наименьшей измеряемой единицей времени является период такта. Если вы подключите тактовый сигнал частотой 1 МГц, то разрешение таймера (или период таймера) будет:

    T = 1 / f (f это тактовая частота)
    T = 1 / 1 МГц = 1 / 10^6 Гц
    T = (1 ∗ 10^-6) с

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

    Типы таймеров

    В стандартных платах Arduino на 8 битном AVR чипе имеется сразу несколько таймеров. У чипов Atmega168 и Atmega328 есть три таймера Timer0, Timer1 и Timer2. Они также имеют сторожевой таймер, который можно использовать для защиты от сбоев или как механизм программного сброса. Вот некоторые особенности каждого таймера.

    Timer0:
    Timer0 является 8 битным таймером, это означает, что его счетный регистр может хранить числа вплоть до 255 (т. е. байт без знака). Timer0 используется стандартными временными функциями Arduino такими как delay() и millis(), так что лучше не запутывать его если вас заботят последствия.

    Timer1:
    Timer1 это 16 битный таймер с максимальным значением счета 65535 (целое без знака). Этот таймер использует библиотека Arduino Servo, учитывайте это если применяете его в своих проектах.

    Timer2:
    Timer2 — 8 битный и очень похож на Timer0. Он используется в Arduino функции tone().

    Timer3, Timer4, Timer5:
    Чипы ATmega1280 и ATmega2560 (установлены в вариантах Arduino Mega) имеют три добавочных таймера. Все они 16 битные и работают аналогично Timer1.

    Конфигурация регистров

    Для того чтобы использовать эти таймеры в AVR есть регистры настроек. Таймеры содержат множество таких регистров. Два из них — регистры управления таймера/счетчика содержат установочные переменные и называются TCCRxA и TCCRxB, где x — номер таймера (TCCR1A и TCCR1B, и т. п.). Каждый регистр содержит 8 бит и каждый бит хранит конфигурационную переменную. Вот сведения из даташита Atmega328:

    TCCR1A
    Бит7654321
    0x80COM1A1COM1A0COM1B1COM1B0WGM11WGM10
    ReadWriteRWRWRWRWRRRWRW
    Начальное значение
    TCCR1B
    Бит7654321
    0x81ICNC1ICES1WGM13WGM12CS12CS11CS10
    ReadWriteRWRWRRWRWRWRWRW
    Начальное значение

    Наиболее важными являются три последние бита в TCCR1B: CS12, CS11 и CS10. Они определяют тактовую частоту таймера. Выбирая их в разных комбинациях вы можете приказать таймеру действовать на различных скоростях. Вот таблица из даташита, описывающая действие битов выбора:

    CS12CS11CS10Действие
    Нет тактового источника (Timer/Counter остановлен)
    1clk_io/1 (нет деления)
    1clk_io/8 (делитель частоты)
    11clk_io/64 (делитель частоты)
    1clk_io/256 (делитель частоты)
    11clk_io/1024 (делитель частоты)
    11Внешний тактовый источник на выводе T1. Тактирование по спаду
    111Внешний тактовый источник на выводе T1. Тактирование по фронту

    По умолчанию все эти биты установлены на ноль.

    Допустим вы хотите, чтобы Timer1 работал на тактовой частоте с одним отсчетом на период. Когда он переполнится, вы хотите вызвать подпрограмму прерывания, которая переключает светодиод, подсоединенный к ножке 13, в состояние включено или выключено. Для этого примера запишем Arduino код, но будем использовать процедуры и функции библиотеки avr-libc всегда, когда это не делает вещи слишком сложными. Сторонники чистого AVR могут адаптировать код по своему усмотрению.

    Сначала инициализируем таймер:

    Регистр TIMSK1 это регистр маски прерываний Таймера/Счетчика1. Он контролирует прерывания, которые таймер может вызвать. Установка бита TOIE1 приказывает таймеру вызвать прерывание когда таймер переполняется. Подробнее об этом позже.

    Когда вы устанавливаете бит CS10, таймер начинает считать и, как только возникает прерывание по переполнению, вызывается ISR(TIMER1_OVF_vect). Это происходит всегда когда таймер переполняется.

    Дальше определим функцию прерывания ISR:

    Сейчас мы можем определить цикл loop() и переключать светодиод независимо от того, что происходит в главной программе. Чтобы выключить таймер, установите TCCR1B=0 в любое время.

    Как часто будет мигать светодиод?

    Timer1 установлен на прерывание по переполнению и давайте предположим, что вы используете Atmega328 с тактовой частотой 16 МГц. Поскольку таймер 16-битный, он может считать до максимального значения (2^16 – 1), или 65535. При 16 МГц цикл выполняется 1/(16 ∗ 10^6) секунды или 6.25e-8 с. Это означает что 65535 отсчетов произойдут за (65535 ∗ 6.25e-8 с) и ISR будет вызываться примерно через 0,0041 с. И так раз за разом, каждую четырехтысячную секунды. Это слишком быстро, чтобы увидеть мерцание.

    Если мы подадим на светодиод очень быстрый ШИМ сигнал с 50% заполнением, то свечение будет казаться непрерывным, но менее ярким чем обычно. Подобный эксперимент показывает удивительную мощь микроконтроллеров — даже недорогой 8-битный чип может обрабатывать информацию намного быстрей чем мы способны обнаружить.

    Делитель таймера и режим CTC

    Чтобы управлять периодом, вы можете использовать делитель, который позволяет поделить тактовый сигнал на различные степени двойки и увеличить период таймера. Например, вы бы хотели мигания светодиода с интервалом одна секунда. В регистре TCCR1B есть три бита CS устанавливающие наиболее подходящее разрешение. Если установить биты CS10 и CS12 используя:

    то частота тактового источника поделится на 1024. Это дает разрешение таймера 1/(16 ∗ 10^6 / 1024) или 6.4e-5 с. Теперь таймер будет переполняться каждые (65535 ∗ 6.4e-5с) или за 4,194с. Это слишком долго.

    Но есть и другой режим AVR таймера. Он называется сброс таймера по совпадению или CTC. Вместо счета до переполнения, таймер сравнивает свой счетчик с переменой которая ранее сохранена в регистре. Когда счет совпадет с этой переменной, таймер может либо установить флаг, либо вызвать прерывание, точно так же как и в случае переполнения.

    Чтобы использовать режим CTC надо понять, сколько циклов вам нужно, чтобы получить интервал в одну секунду. Предположим, что коэффициент деления по-прежнему равен 1024.

    Расчет будет следующий:

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

    Функция настройки setup() будет такая:

    Также нужно заменить прерывание по переполнению на прерывание по совпадению:

    Сейчас светодиод будет зажигаться и гаснуть ровно на одну секунду. А вы можете делать все что угодно в цикле loop(). Пока вы не измените настройки таймера, программа никак не связана с прерываниями. У вас нет ограничений на использование таймера с разными режимами и настройками делителя.

    Вот полный стартовый пример который вы можете использовать как основу для собственных проектов:

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

    Поскольку переменная будет модифицироваться внутри ISR она должна быть декларирована как volatile. Поэтому, при описании переменных в начале программы вам надо написать:

    Послесловие переводчика

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

    голоса
    Рейтинг статьи
Ссылка на основную публикацию
Adblock
detector