Ранее на нашем сайте мы рассмотрели основы работы с микроконтроллерами PIC с помощью MPLABX IDE, отладку программы мигания светодиодом для них в симуляторе и на "реальном" железе, а также мигание с их помощью последовательностью светодиодов. В этой же статье на основе предыдущего проекта мигания последовательностью светодиодов мы рассмотрим основы работы с таймерами в микроконтроллерах PIC. По сравнению с предыдущим проектом мигания последовательностью светодиодов в данном проекте мы добавим одну дополнительную кнопку.
Ранее на нашем сайте мы достаточно подробно рассматривали работу с таймерами в платах Arduino.
Зачем нужны таймеры
Таймеры играют важную роль в большинстве проектов встраиваемой электроники. Они позволяют отмерять заданные интервалы времени. Но вы можете спросить зачем нам нужны таймеры если мы можем делать то же самое с помощью функции задержки (__delay_ms())?
Здесь дело в том, что во время задержки, организованной с помощью функции __delay_ms()), микроконтроллер не может больше делать ничего: ни считывать значения со входов АЦП, ни проверять состояние контактов, ни записывать какие либо значения в свои регистры и т.д.
Также у задержек, организуемых с помощью функции __delay_ms()), есть и другие недостатки:
- Значение задержки задается константой и его нельзя изменить во время исполнения программы.
- Значение задержки не такое точное как в случае использования для этой цели таймеров.
- Невозможно организовать очень длительные задержки, к примеру, полчаса и более. Максимальная величина задержки зависит от используемой частоты кварцевого генератора.
Таймеры в микроконтроллерах PIC
На физическом уровне таймер представляет собой регистр, значение в котором непрерывно увеличивается до 255, и затем счет в нем начинается сначала: 0, 1, 2, 3, 4...255....0, 1, 2, 3......и т. д.
Микроконтроллер PIC16F877A содержит три таймера, с именами Timer0, Timer1 и Timer2. Timer0 и Timer2 являются 8-битными, а Timer1 – 16-битным. В нашем проекте мы рассмотрим работу с Timer0. Если вы поймете принцип его работы, то по аналогии с ним вы сможете работать и с таймерами Timer1 и Timer2
Таймер/счетчик Timer0 имеет следующие особенности:
- 8-битный;
- можно записывать и считывать информацию;
- 8-битный программируемый предварительный делитель частоты (предделитель, prescaler);
- выбор внешней или внутренней частоты синхронизации;
- прерывание при переполнении от FFh до 00h;
- выбор спада или нарастания импульса для сигнала внешней синхронизации.
Для начала разберемся с рядом терминов, оказывающих влияние на функционирование таймера.
Предварительный делитель частоты (предделитель, prescaler) – это компонент микроконтроллера, который делит частоту задающего генератора перед тем как она достигает логического уровня, увеличивающего состояние таймера. Коэффициент деления предделителя для микроконтроллеров PIC лежит в диапазоне от 1 до 256, он устанавливается с помощью регистра OPTION (этот же регистр используется и для управления подтягивающими резисторами). К примеру, если значение (его коэффициент деления) предделителя равно 64, то с каждым 64-м импульсом частоты генератора значение таймера будет увеличиваться на 1.
Когда при инкрементировании таймера его значение достигает 255 (максимальное его значение) он инициализирует прерывание и сбрасывает свое значение снова до 0. Этот прерывание называется прерыванием от таймера. Оно информирует микроконтроллер что истек определенный промежуток времени.
Обозначим за Fosc частоту тактового генератора (Frequency of the Oscillator), то есть частоту используемого кристалла. Интервал времени, отсчитываемый таймером, зависит от значения предделителя (Prescaler) и значения частоты Fosc.
Объяснение программы для микроконтроллера PIC
Полный код программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты.
В нашем проекте мы будем использовать 2 кнопки и 8 светодиодов, которые подключены к 8 контактам микроконтроллера PIC. С помощью 1-й кнопки мы будем устанавливать временную задержку (500ms на каждое нажатие), а 2-й кнопкой будем запускать процесс мигания последовательности светодиодов. К примеру, если первую кнопку мы нажмем трижды (500*3 = 1500ms), то задержка составит 1,5 секунды и при нажатии второй кнопки светодиоды будут включаться и выключаться с этим значением задержки. Более подробно работу проекта вы можете посмотреть на видео, приведенном в конце статьи.
Первым делом в программе мы зададим значение битов конфигурации, но этот фрагмент программы мы здесь рассматривать не будем поскольку мы его подробно рассмотрели в соответствующей статье. Поэтому перейдем сразу к функции main, в которой мы будем задавать параметры работы таймера Timer0.
1 2 3 4 5 6 7 8 9 |
void main() { /*****Port Configuration for Timer ******/ OPTION_REG = 0b00000101; // Timer0 with external freq and 64 as prescalar // Also Enables PULL UPs TMR0=100; // Load the time value for 0.0019968s; delayValue can be between 0-256 only TMR0IE=1; //Enable timer interrupt bit in PIE1 register GIE=1; //Enable Global Interrupt PEIE=1; //Enable the Peripheral Interrupt /***********______***********/ |
Вначале посмотрим на структуру регистра OPTION в даташите нашего микроконтроллера PIC.
Как мы уже рассмотрели в предыдущей статье, бит 7 используется для управления внутренними подтягивающими резисторами порта PORTB. Как видно из представленного рисунка, если бит 3 установить в 0, то предделитель будет устанавливаться для нашего таймера, а не для сторожевого таймера (WatchDogTimer, WDT). Режим работы таймера устанавливается при помощи установки бита 5 (T0CS) в 0 (OPTION_REG<5>).
Биты 2-0 используются для установки значения предделителя. Как видно из представленной таблицы, чтобы установить значение предделителя равным 64, в эти биты нужно записать 101.
Теперь посмотрим на регистры, ассоциированные с Timer0.
Таймер при своей установке начинает счет и переполняется при достижении значения 256, чтобы разрешить срабатывание прерывания от таймера в этой точке регистр TMR0IE необходимо установить в high. Поскольку Timer 0 по своей сути является периферийным устройством, мы также должны разрешить прерывания от периферийных устройств установив PEIE=1. И, наконец, мы должны установить глобальное разрешение прерываний для микроконтроллера, это делается при помощи установки GIE=1.
Задержку, отсчитываемую таймером, можно определить по формуле:
Delay = ((256-REG_val)*(Prescal*4))/Fosc
Если
REG_val = 100,
Prescal = 64,
Fosc = 20000000,
То по данной формуле получим значение задержки равное Delay = 0.0019968s
Далее в программе сконфигурируем работу портов ввода/вывода.
1 2 3 4 5 6 |
/*****Port Configuration for I/O ******/ TRISB0=1; //Instruct the MCU that the PORTB pin 0 is used as input for button 1. TRISB1=1; //Instruct the MCU that the PORTB pin 1 is used as input for button 1. TRISD = 0x00; //Instruct the MCU that all pins on PORT D are output PORTD=0x00; //Initialize all pins to 0 /***********______***********/ |
Конфигурация работы портов такая же, как и в нашей предыдущей статье поскольку мы используем одно и то же "железо", за исключением того, что мы добавили в схему проекта еще одну дополнительную кнопку – она конфигурируется с помощью строки TRISB1=1.
Далее, внутри нашего бесконечного цикла мы имеем два блока кода. Один из них используется для установки значения таймера пользователем, а второй управляет включением/выключением последовательности светодиодов.
1 2 3 4 5 6 7 8 9 10 11 12 |
while(1) { count =0; //Do not run timer while in main loop //*******Get the number delay from user****////// if (RB0==0 && flag==0) //When input given { get_scnds+=1; //get_scnds=get_scnds+1//Increment variable flag=1; } if (RB0==1) //To prevent continuous incrementation flag=0; /***********______***********/ |
Переменная get_scnds инкрементируется с каждым нажатием первой кнопки. Переменная flag (определяемая программно) используется для удержания процесса инкрементирования пока пользователь не удалит свой палец с кнопки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//*******Execute sequence with delay****////// while (RB1==0) { PORTD = 0b00000001<<i; //Left shit LED by i if(hscnd==get_scnds) //If the required time is reached { i+=1; //Move to next LED after the defined Delay hscnd=0; } flag=2; } if (flag==2 && RB1==1) //Reset timer if button is high again { get_scnds=0;hscnd=0;i=0; PORTD=0; //Turn off all LEDs } /***********______***********/ |
Следующий блок программы начинает работать при нажатии второй кнопки. Значение заданной пользователем задержки в результате работы первого блока сохранилось у нас в переменной get_scnds. В этом же блоке программы мы будем использовать переменную hscnd, которая будет управлять процедурой обработки прерывания (Interrupt service routine, ISR).
Данная процедура будет вызываться каждый раз при переполнении Timer0. Как мы уже указывали ранее, значение задержки должно увеличиваться на полсекунды при каждом нажатии первой кнопки, поэтому мы должны инкрементировать переменную hscnd для каждой полсекунды. Поскольку ранее мы запрограммировали задержку для нашего таймера равную 0.0019968s (~ 2ms), то чтобы отсчитать полсекунды значение переменной должно быть равно 250 поскольку 250*2ms = 0.5 секунды. Поэтому когда значение переменной count достигает 250 (250*2ms = 0.5 секунды) это будет означать что прошло полсекунды, поэтому увеличиваем значение переменной hscnd на 1 и сбрасываем значение переменной count в 0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void interrupt timer_isr() { if(TMR0IF==1) // Timer flag has been triggered due to timer overflow { TMR0 = 100; //Load the timer Value TMR0IF=0; // Clear timer interrupt flag count++; } if (count == 250) { hscnd+=1; // hscnd will get incremented for every half second count=0; } } |
В дальнейшем мы переключаем состояние светодиодов, основываясь на заданной задержке.
Схема проекта
Схема проекта для демонстрации использования таймеров в микроконтроллерах PIC представлена на следующем рисунке.
Внешний вид собранной конструкции проекта показан на следующих рисунках.
Исходный код программы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
// CONFIG #pragma config FOSC = HS // Oscillator Selection bits (HS oscillator) #pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled) #pragma config PWRTE = ON // Power-up Timer Enable bit (PWRT enabled) #pragma config BOREN = ON // Brown-out Reset Enable bit (BOR enabled) #pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming) #pragma config CPD = OFF // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off) #pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control) #pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off) // #pragma config statements should precede project file includes. // Use project enums instead of #define for ON and OFF. #include <xc.h> #define _XTAL_FREQ 20000000 //TIMER0 8-bit $$RegValue = 256-((Delay * Fosc)/(Prescalar*4)) delay in sec and Fosc in hz //формула для расчета значения задержки //Delay = ((256-REG_val)*(Prescal*4))/Fosc char hscnd = 0; int count = 0; char get_scnds =0; char flag =0; char i=0; void interrupt timer_isr() { if(TMR0IF==1) // Timer flag has been triggered due to timer overflow { TMR0 = 100; //загружаем значение в таймер TMR0IF=0; // очищаем флаг обработки прерываний от таймера count++; } if (count == 250) { hscnd+=1; // hscnd будет инкрементироваться каждые полсекунды count=0; } } void main() { /*****Port Configuration for Timer ******/ OPTION_REG = 0b00000101; // Timer0 с внешней частотой и значение 64 для предделителя // Also Enables PULL UPs TMR0=100; // загружаем значение в таймер для задержки 0.0019968s; delayValue can be between 0-256 only TMR0IE=1; //устанавливаем бит разрешения прерываний от таймера в регистре PIE1 GIE=1; //глобальное разрешение прерываний PEIE=1; //разрешение прерываний от периферийных устройств /***********______***********/ /*****Port Configuration for I/O ******/ TRISB0=1; //Instruct the MCU that the PORTB pin 0 is used as input for button 1 (к данному контакту подключена 1-я кнопка) TRISB1=1; //Instruct the MCU that the PORTB pin 1 is used as input for button 2 (к данному контакту подключена 2-я кнопка) TRISD = 0x00; //режим работы контактов порта PORT D – на вывод данных PORTD=0x00; //устанавливаем значение всех контактов порта в 0 /***********______***********/ while(1) { count =0; //Do not run timer while in main loop //*******получаем значение задержки от пользователя****////// if (RB0==0 && flag==0) //When input given { get_scnds+=1; //get_scnds=get_scnds+1//Increment variable flag=1; } if (RB0==1) //для предотвращения непрерывного инкрементирования flag=0; /***********______***********/ //*******управляем последовательностью включения светодиодов с заданной задержкой****////// while (RB1==0) { PORTD = 0b00000001<<i; //Left shit LED by i if(hscnd==get_scnds) //если достигнуто заданное время { i+=1; //переходим к следующему светодиоду после заданной задержки hscnd=0; } flag=2; } if (flag==2 && RB1==1) //Reset timer if button is high again { get_scnds=0;hscnd=0;i=0; PORTD=0; //выключаем все светодиоды } /***********______***********/ } } |
int Crank=0;
void main()
{
OPTION_REG=0b00000111;
INTCON=0b11100000;
TRISD=0x00;
PORTD=0x00;
TMR0=0x00;
while(TMR0IF==0) // Флаг переполнения таймераб если флаг Труе, выполняется цикл, если флаг фолс, цикл не выполняется.
if (Crank==50) // Если переменная кранк Фолс, следущая команда пропускается, если труе следущая команда выполняется.
{
RD1=1; // Включить выход порта D
Crank=0; // Обнулить переменну кранк
break; // Выйти из цикла
}
else
{
Crank++; // Инкрементировать переменной кранк +1
}
TMR0IF=0; // Обнулить флаг таймера
GIE=1; // Включить глобальные прерывания
while(TMR0IF==0) // Флаг переполнения таймераб если флаг Труе, выполняется цикл, если флаг фолс, цикл не выполняется.
if (Crank==50) // Если переменная кранк Фолс, следущая команда пропускается, если труе следущая команда выполняется.
{
RD1=0; // Вылючить выход порта D
Crank=0; // Обнулить перемнную кранк
break; // Выйти из цикла
}
else
{
Crank++; // Инкрементировать переменной кранк +1
}
TMR0IF=0; // Обнулить флаг таймера
GIE=1; // Включить глобальные прерывания
return;
}
Можно сказать моя первая программа на Си)
Было не плохо если подскажите как сделать что бы каждые 24 импульса генерировался еще 1 импульс в этом же цикле.
Завести еще одну переменную в этом цикле, которая будет инкрементироваться только тогда, когда (Crank==24) или (Crank==48)
Я разобрался как в Си указывать конкретный бит, просто по названию бита. Я только учусь и начинал изучение с ассемблера, а сейчас в си кажется как то сложнее, так как в ассемблере ты конкретно указываешь что нужно сделать, а тут какие то команды, не совсем с понятной логикой работы. Хоть в ассемблере и больше кода писать, но там как то более понятно что конкретно происходит в памяти. А тут тот же цикл вайл, хрен поймешь когда и как какое условие он выполняет. Если взять ассемблер, там указал, что проверить вот это там то там, если так то перейти туда все просто. В си чет я так и не додумался как мне вызвать например функцию. Например функция ИМЯ, выполняется код, я хочу что бы программа перешла на выполнение этого кода, и вернулась обратно. Так и не додумался как это реализовать в си. На ассемблере если не ошибаюсь это команда GOTO, и указываешь адрес куда перейти. Выполнилось, вернулось обратно, выполняет дальше инструкции. Короче Си какой то мутный)
И я разобрался с генерацией импульсов. 3 дня голову ломал как это реализовать...
Теперь не понятно как реализовать что бы в этом же цикле генерировался второй импульс. Я так понимаю если я из цикла выйдут, в котором реализована генерация импульсов, то и генерация прекратится. Выходит что надо это как то сделать в этом же цикле.
Ну так сделайте этот цикл внутри бесконечного цикла while, и тогда после выполнения ваш цикл будет выполняться снова и снова
Не компилируется файл, пишет ошибку...
из за вот это строчки void interrupt timer_isr()
слово interrupt убираешь, комплируется, в протеусе не работает, пишет оверфлоу интеррупт.
всю голову сломал, так и не понял в чем проблема...
2 errors generated.
(908) exit status = 1
nbproject/Makefile-default.mk:107: recipe for target 'build/default/production/main.p1' failed
make[2]: Leaving directory 'C:/Users/Dream/MPLABXProjects/Timer Crank.X'
nbproject/Makefile-default.mk:91: recipe for target '.build-conf' failed
make[1]: Leaving directory 'C:/Users/Dream/MPLABXProjects/Timer Crank.X'
nbproject/Makefile-impl.mk:39: recipe for target '.build-impl' failed
BUILD FAILED (exit value 2, total time: 205ms)
К сожалению, с протеусом уже давно не работал, не могу подсказать здесь что то конкретное. А оверфлоу это означает переполнение в общем случае. У вас другие проекты (без использования прерываний) нормально компилируются?
Я не пробовал. Я только учусь.
Может подскажете как решить такую задучу.
Мне нужно что бы с выхода контроллера выходили импульсы, обычные прямоугольные импульсы.
Надо зайдествовать 2 порта. На одном генерируется 24 импульса, на другом 1. И так до бесконечности.
Не могу сообразить как написать код. сижу изучаю таймеры уже весь день.
Вам в этом случае проще использовать обычную функцию задержки. Делаете цикл от 0 до 23 (он должен стоять внутри бесконечного цикла while в основной функции main). В нем с нужной вам задержкой (зависит от того какая длительность импульса вам нужна) переключаете состояние контакта, на котором вам нужно формировать 24 импульса. А внутри этого цикла ставите условие (например, когда переменная цикла равна 0) чтобы состояние второго контакта, на котором нужно формировать 1 импульс, переключалось всего один раз за время этого цикла
Я так пробовал. Тогда импульсы кривые и разные. Мне нужно что бы четко выходило 24 импульса одной длины. Тоесть например счетчик досчитал до 255, это равно длине времени 50мс, порт включился на 50мс и через 50мс выключился. Когда на одном порту прошло 23 импульса, то на 24 импульс включился второй порт на такой же промежуток времени.
Тоесть с порта D7 выходят 24 имульса 50мс. 50мс включено, 50мс выключено. (Время 50мс не обязательно), длина не особо важна, главное что бы эти импульсы были одинаковые. И на 23 импульс порта D7 выходит импульс 50мс с порта D6.
Контролллер PIC16F877A.
И еще вопрос немогу сообразить, Как проверить отдельный бит регистра специального назначения,
Например регистр INTCON bit2.
Например я хочу в цикле проверять регистр INTCON bit2 На флаг переполения таймера. Когда флаг поднялся INTCON BIT2 (1), Мне нужно записать 1 в другой регистр.
например я пишу цикл while (INTCON) А как мне тут проверить имено второй бит?
Или я что то не так понимаю.
Спасибо)
Его можно проверить сделав операцию логического И с маской, в которой все биты, кроме второго, равны 0