Найти тему
Разумный мир

Микроконтроллеры для начинающих. Часть 56. Задержки на таймерах

Оглавление

Итак, мы уже немного (именно немного!) познакомились с таймерами

Микроконтроллеры для начинающих. Часть 52. Таймеры

И некоторыми особенностями в их практической реализации в различных семействах микроконтроллеров

Микроконтроллеры для начинающих. Часть 53. Таймеры PIC

Микроконтроллеры для начинающих. Часть 54. Таймеры AVR

Микроконтроллеры для начинающих. Часть 55. Таймеры STM8

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

Микроконтроллеры для начинающих. Часть 51. Бегущие огни. Запускаем!

в котором мы использовали программные циклы для получения задержки на требуемое время. Подробное описание программного способа формирования задержки я описывал здесь

Микроконтроллеры для начинающих. Часть 50, внеплановая. Delay(), или программная задержка

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

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

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

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

Все примеры программ в статье являются полными (за исключением одного фрагмента для PIC). Они корректно обрабатываются указанными компиляторами и корректно загружаются в микроконтроллеры. И полностью работоспособны. Для AVR биты fuse соответствуют указанным в 51 части цикла. Для PIC биты конфигурации определены в программе. Для STM8 используются значения по умолчанию.

Основные принципы использования таймера. Без прерываний.

Для реализации программной задержки нам достаточно было установить начальное значение счетчика цикла, который мы потом декрементировали в цикле до достижения нулевого значения. Поэтому о какой то инициализации говорить смысла почти не имело.

А вот с таймером ситуация иная. Для простоты предположим, что у нас есть свободный таймер, который мы можем свободно использовать в своих целях. Но этого недостаточно. Так как нам нужно этот таймер настроить.

Начальная инициализация

  1. Выбрать источник счетных импульсов. В большинстве случаев это будет системный тактовый генератор. Однако, таймер может иметь и свой собственный генератор. А иногда есть возможность выбора частоты генератора тактовых импульсов. Речь идет об использовании таймера для счета времени, а не событий.
  2. Выбрать коэффициенты деления предварительного и последующего (если есть) делителей. Выбор коэффициентов деления может быть ограниченным. Кроме того, иногда требуется настроить и назначение делителей.
  3. Выбрать режим работы таймера и направление счета (если возможно).

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

По поводу пункта 2 требуется некоторое уточнение. Далеко не всегда выбор коэффициента деления нужно включать в начальную инициализацию. Если требуется формировать задержки в большом диапазоне длительностей, то может оказаться полезным изменять коэффициенты деления в зависимости от требуемой длительности задержки. Но вот остальную часть настройки делителей нужно делать именно в рамках начальной инициализации.

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

При этом не стоит гнаться за максимальной гибкостью и возможностью задать все параметры через параметры вызова. Для микроконтроллеров зачастую достаточно довольно простой настройки. А оформление в виде процедуры просто упрощает и делает стандартизованным способ вызова. В любой программе будет достаточно вызвать, например, функцию TimerInit(). А вот подробности инициализации придется писать для каждого микроконтроллера в отдельности. Можно сказать, что TimerInit будет частью HAL.

Формирование задержки

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

Кроме того, нам возможно придется включить (запустить) таймер. Так как он не обязательно будет работать непрерывно. А возможно, и остановить таймер перед записью в его регистр-счетчик.

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

Когда счетчик переполнится (станет равным нулю), мы остановим таймер. А формирование задержки будет закончено. Можно остановить таймер, если это предусмотрено.

Особенности. Плюсы и минусы

Как видно, использование таймера для формирования задержки приводит к большим накладным (сторонним) издержкам. Начальную инициализацию можно не учитывать, она занимает место, но выполняется один раз.

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

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

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

Но длительность программной задержки зависела еще и кода процедуры задержки. А значит, даже простое обновление версии компилятора, не говоря уже о сменен компилятора, могло привести к неверной работе программы (неверному времени задержки). По этой причине я и указывал точную версию компилятора, и используемые ключи, в текстах программ бегущих огней.

А для некоторых микроконтроллеров, STM8, например, длительность зависела и от физического расположения программы в памяти.

Использование таймеров устраняет именно вот эту зависимость, от времени выполнения программного цикла. И это большой плюс.

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

Начальная инициализация

Давайте начнем с базовой подготовки и настройки таймера. Прямо по тем пунктам, что я привел в самом начале.

Выбор источника счетных импульсов

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

Однако....

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

Кроме того, в некоторых микроконтроллерах выбор источников может быть довольно большим. Например, для PIC16F18855 и его TIMER0 (мы это рассматривали ранее) можно выбрать один из 4 доступных источников, не считая входа внешних импульсов. А в популярном микроконтроллере ATmega328 TIMER2 может использовать собственный генератор.

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

Но это не помешает показать собственно выбор источника.

Настройка делителей

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

Для повышения гибкости в задании времени задержки мы не будем устанавливать коэффициент деления заранее. Это несколько увеличивает минимально время задержки, так как потребуется каждый раз тратить время на установку коэффициента деления. Но заметно увеличивает гибкость. Что для вас важнее, решать только вам.

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

Выбор режима работы таймера

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

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

Формирование задержки

Здесь все немного проще. Но свои нюансы есть. Вот их и рассмотрим.

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

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

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

Практическая реализация задержек

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

Универсальный способ работы с таймерами я рассмотрю только на примере PIC. Так именно линейка BaseLine не имеет ни прерываний, ни флагов таймеров. Это линейка самых простых микроконтроллеров.

Еще раз отмечу, что сегодня мы не используем прерывания, так как их еще не изучали.

PIC, 8 битный базовый таймер

В нашем первом учебном проекте (Бегущие огни) мы использовали PIC16F630. Будем использовать его и сегодня. При этом реализация таймеров в нем довольно типична, поэтому с использование других микроконтроллеров PIC не вызовет особых проблем.

TIMER0 работает всегда. Счетные импульсы могут поступать от системного тактового генератора (Fosc/4) или внешнего источника импульсов. Мы используем только первый вариант, как и договаривались.

А вот предварительный делитель может использоваться либо TIMER0, либо сторожевым таймером (WDT). Сторожевые таймеры мы еще не изучали. Поэтому я ограничусь лишь переключением предварительного делителя на работу с TIMER0.

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

Я не буду описывать биты конфигурации. Они не относятся к сегодняшней теме. Используется компилятор CC5X, как и для большинства программ для PIC.

Процедура начальной инициализации таймера называется Timer0_Init. Она начинается со строки 20. Особых комментариев здесь не требуется, мы уже все рассмотрели. Только напомню, что биты T0CS и PSA располагаются в регистре OPTION_REG.

Сама процедура задержки называется Timer0_Delay и располагается со строки 29. Процедура имеет два параметра. Первый, код соответствующий коэффициенту деления. Второй, начальное значение счетчика таймера.

Здесь тоже не требуется особых комментариев. Бит T0IF это бит запроса прерывания таймера. Поскольку прерывания мы не используем, установка бита означает переполнение таймера. И именно установки этого флага мы ожидаем в строке 43. Именно в этой строке и формируется задержка.

Функция main показывает, как используются две процедуры задержки. Мы настраиваем бит 2 порта C на выход. Осциллографом на этом выводе мы сможем посмотреть, как работает наша программа.

Инициализация таймера выполняется один раз, как и планировалось. Наш пример показывает формирование задержки 10 мс. Для этого мы используем коэффициент деления 1:256. Внутренний генератор микроконтроллера работает на частоте 4 МГц, что дает системную тактовую частоту 1 МГц (период 1 мкс). После делителя период импульсов составляет 256 мкс. То есть, для 10 мс нам требуется 40 импульсов.

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

Не смотрите, что у нас параметр беззнаковый, а мы используем отрицательное значение. Компилятор все обработает верно.

А на осциллографе мы увидим прямоугольные импульсы с периодом 20 мс и скважностью 2. Правда, так как внутренний генератор не очень точный, вполне допустимы отклонения.

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

Может возникнуть вопрос, почему я не предусмотрел возможности задать для Timer0_Delay непосредственно длительность задержки? Это можно сделать. Но это потребует дополнительных вычислений внутри Timer0_Delay, что увеличит ее размер. И увеличит минимальное время задержки.

Микроконтроллер это не универсальная ЭВМ. И в программах обычно требуется некоторое количество заранее определенных времен задержек. Поэтому имеет смысл еще при разработке программы задать для них соответствующие константы, предварительно их рассчитав. Но вы можете сделать как вам больше нравится.

PIC, 8 битный базовый таймер, универсальный способ

Если у нас не аппаратного способа отслеживать переполнение таймера, то мы вынуждены делать это программно. Это чрезвычайно просто, очевидно, и абсолютно универсально.

Здесь не нужны никакие комментарии. Кроме одного, для новичков. В строке 11 мы ожидаем ненулевого значения счетчика, что бы избежать неверного формирования задержки в случае, когда начальное значение счетчика равно 0. Если эту строку убрать, наша задержка может закончиться не начавшись.

PIC, 16 битный универсальный таймер

TIMER1 является универсальным 16 битным таймером. Его разрядность превышает разрядность процессора микроконтроллера. А значит, нам необходимо вспомнить об атомарности доступа.

К счастью, таймер полностью принадлежит нам. А значит, мы можем просто остановить его и снова запустить. И проблема атомарности будет решена.

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

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

При этом сама программа, main, практически не изменилась. Количество импульсов при вызове Timer1_Delay стало 16-битным.

Мы формируем ту же самую задержку 10 мс. Но теперь период импульсов счета 8 мс (коэффициент деления 1:8). И для 10 мс нам нужно 1250 импульсов.

Давайте попробуем оценить выгоду от использования 16-битного таймера. Для 8-битного базового таймера максимальное время задержки составляло 65 мс. Для 16 битного - 524 мс. Так что для длинных задержек использовать TIMER1 действительно имеет смысл.

AVR, 8 битный универсальный таймер

У ATmega328P, который мы использовали в первом учебном проекте, 8 битный таймер является не базовым, а универсальным. Но это не повлияет на его использования для нашей задачи.

Теперь я буду использовать компилятор avr-gcc, но, как и всегда, без включенных в его состав файлов описания микроконтроллеров. Все регистры, только нужные нам биты, мы опишем сами. Поэтому и фрагмент программы будет касаться более сложным. На самом деле это не так.

В TIMER0 ATmega328P выбор источника счетных импульсов и коэффициента деления предварительного делителя осуществляются одновременно. Поэтому наша процедура начальной настройки таймера сводится к единственно строчке, останавливающей таймер.

Процедура Timer0_Delay функционально аналогична ранее рассмотренным для PIC. Но, поскольку таймер у нас уже остановлен, мы можем сразу установить начальное значение счетчика и сбросить флаг переполнения (да, именно так, записью 1, вспоминайте статью с описанием таймера).

После этого мы устанавливаем источник и коэффициент деления, за одну операцию. Это и запускает таймер. Остается просто подождать установки флага переполнения таймера.

Внимание! Мы не можем сбросить счетчики (триггеры) предварительного делителя, как это делается автоматически в таймерах PIC. Так как предварительный делитель один на два таймера. И такой сброс повлияет и на работу Timer1.

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

При использовании внутреннего тактового генератора на вход предварительного делителя таймера поступает частота 1 МГц, по умолчанию. А значит, у нас даже коэффициент деления (просто так повезло) и начальное значение счетчика остались такими же, как в случае PIC, для формирования задержки 10 мс.

AVR, 16 битный универсальный таймер

TIMER1 в AVR является универсальным 16-битым. Но, как и всегда, все эти особенности никак не повлияют на основную программу, функцию main. Все изменения коснутся лишь описания регистров и внутреннего мира наших двух функций, Timer1_Init и Timer1_Delay. Причем эти изменения будут небольшими.

Обратите внимание, что адреса некоторых регистров я определил через address, а не через io. Дело в том, эти адреса выходят за границы адресного пространства ввода-вывода, они лежат в адресном пространстве данных. Если вы немного подзабыли подробности организации адресных пространств AVR, прочтите еще раз

Микроконтроллеры для начинающих. Часть 12. Память и процессор в AVR

Процедура начальной инициализации таймера у нас не изменилась. Изменилось лишь имя регистра.

А вот в процедуре Timer1_Delay изменений больше. Совершенно естественным является то, что теперь параметр count стал 16-битным. Но обратите внимание на то, в каком порядке мы записываем в регистр-счетчик таймера начальное значение. Этот порядок очень важен! Почему порядок важен я писал в статье с описанием работы таймеров AVR.

Других изменений, в общем то, и нет. Не считая изменения имен регистров.

Для примера мы формируем задержку не 10, а 100 мс. Что и потребовало изменения начального значения с 40 на 400, так как коэффициент деления у нас остался прежним. Все просто и логично.

Давайте теперь оценим целесообразность использовать 16-битный таймер, вместо 8-битного. Для Timer0 максимальное время задержки равняется 260 мс. А для Timer1 - 67 секунд.

Весьма впечатляющее различие. Но слишком сильно радоваться не стоит. Дело в том, что кроме максимальных величин немаловажной является и дискретность установки этих величин. А для коэффициента деления 1:1024 минимальный шаг изменения длительности равняется 1 мс. Просто имейте это ввиду.

STM8, 8-битный TIMER4

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

Как обычно, для STM8 я использую компилятор sdcc.

Самый простой TIMER4 не имеет возможности выбора источника счетных импульсов. Это всегда Fmaster. Микроконтроллер стартует с внутренним тактовым генератором частотой 16 МГц и коэффициентом деления 8. То есть, сразу после старта тактовая частота и процессора, и периферии равна 2 Мгц. Мы не изменяем настройки системного генератора синхросигналов.

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

Однако, я все таки оставил процедуру начальной инициализации, для совместимости. А следуя принципу "доверяй, но проверяй" останавливаю в этой процедуре таймер. Это не требуется в нашем случае, но в общем случае может быть нужным.

Мы опять описали все нужные биты всех регистров сами. И это занимает большую часть текста программы.

При этом процедура Timer4_Delay у нас опять оказалась чрезвычайно похожей на другие семейства микроконтроллеров. Мы устанавливаем коэффициент деления и начальное значение счетчика таймера. После чего просто ждем установки флага переполнения.

Обратите внимание, мы совершенно не вмешивались в процессы формирования UEV в таймере. Все настройки оставлены по умолчанию. И мы не разрешали формирование запроса прерывания (запрещено по умолчанию). По UEV у нас устанавливается UIF, чем мы и пользуемся. Но запрос прерывания при установке UIF у нас запрещен. И нас это совершенно устраивает, в данном случае.

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

Функция main у нас опять осталась без изменений! Не считая установки режима работы вывода порта, к которому подключается осциллограф. Это можно было спрятать в функцию MCU_Init, как я делал раньше, но из-за пары строк имеет не очень большой смысл.

А вот параметры вызова стали другие... Теперь у нас нет коэффициента деления 1:256. А тактовая частота в два раза выше предыдущих случаев. Поэтому и начальное значение счетчика стало в 4 раза больше. А длительность задержки по прежнему 10 мс.

STM8, 16-битный TIMER2

TIMER2 в STM8 является не только 16-битным, но и весьма гибким и функциональным. И в режимах его работы действительно не всегда так просто разораться. Но для нашей сегодняшней задачи отличий от 8-битного простого TIMER4 почти нет.

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

Мы опять формируем задержку 10 мс, но теперь мы используем привычное начальное значение счетчика, так как выбор коэффициентов деления больше и мы можем скомпенсировать этим вдвое большую тактовую частоту.

Давайте снова оценим максимальные времена задержек. Для 8 битного TIMER4 максимальная длительность задержки равна 16 мс. А для 16-битного TIMER2 более 1000 секунд (более 16 минут). Довольно впечатляющее увеличение.

Заключение

На сегодня все. Статья получилась очень большой. Но большую ее часть составляют тексты программ-примеров.

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

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

До новых встреч!