PIC Урок 5. Таймеры



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

Начнём мы с самого простого таймера. Да и по другому мы пока и не сможем, так как у контроллера PIC16F64A он единственный — это таймер 0.

Таймер 0 или TMR0, как он обозначен в технической документации, — это 8-разрядный таймер/счётчик, который умеет считать только от 0 до 255, и как только достигает данной величины, происходит прерывание, если оно конечно задействовано. Правда мы можем, конечно, в ходе программы в любой момент узнать значение счётчика, но, правда, не знаю, нужно ли нам это. Также мы можем устанавливать скорость счёта посредством использования предделителя, можем выбирать внутренний или внешний источник тактирования таймера, и ещё можем выбрать активный фронт — инкрементирование значения счётчика по положительному или по отрицательному фронту (спаду), но только при условии, что источник тактового сигнала внешний. Вот такие вот мы возможности имеем с данным таймером. Не густо, но и тем не менее пользу от этого таймера мы извлечём. И не только ту пользу, что мы ознакомимся с данным таймером и это нам поможет проще затем понимать структуру и работу более сложных таймеров у других контроллеров PIC, но и также по окончанию занятия мы наглядно увидим, что использование данного таймера, например для бегущих огней вместо бесконечного цикла никак не влияет на ход основной программы.

Вот блок-схема данного таймера

 

 

В данной схеме показаны все блоки нашего таймера, а также указаны биты, которые управляют данными блоками. Биты T0CS, T0SE, PSA, PS2:PS0 расположены в регистре OPTION_REG

 

 

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

TOCS (TMR0 Clock Source Select) — выбор сингала для таймера: 0 — внутренний тактовый сигнал, 1 — внешний.

T0SE (TMR0 Source Edge Select) — выбор фронта приращения при внешнем тактовом сигнале: 0 — по переднему фронту, 1 — по заднему.

PSA (Prescaler Assignment) — выбор способа включения предделителя: 0 — предделитель включен через TMR0, 1 — через WDT.

PS2:PS0 (Prescaler Rate Select) — коэффициент деления предделителя

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

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

Это регистр INTCON, предназначенный для управления прерываниями

 

 

В данном регистре нам потребуются сегодня только три бита:

GIE (Global Interrupt Enable) — разрешение глобальных прерываний: 0 — все прерывания запрещены, 1 — все немаскированные прерывания разрешены.

T0IE (TMR0 Overflow Interrupt Enable) — разрешение прерывания по переполнению таймера 0: 0 — прерывание запрещено, 1 — прерывание разрешено.

T0IF (TMR0 Overflow Interrupt Flag) — флаг прерывания по переполнению таймера 0: 0 — внешнего прерывания нет, 1 — произошло переполнение счётчика таймера 0 (сбрасывается программно).

Ну и соответственно регистр значения счёта нашего таймера

 

 

Теперь давайте потихоньку переходить к коду.

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

Откроем наш проект в MPLAB X и попробуем его собрать. Если всё нормально, то откроем ещё проект для претеуса и выберем файл прошивки, которая у нас только что сгенерировалась при сборке проекта в среде программирования.

В файле main.c (а другого-то у нас пока и нет) в функции main сначала включим нужные нам биты в регистре OPTION_REG

 

TRISA |= 0x04;

OPTION_REG=0x07;

 

Мы включили биты 0,1 и  2 — это биты предделителя, то есть мы используем деление частоты максимальное — на 256. То есть частота приращения счёта будет 1000000/256 или приблизительно 3,9 кГц. Но так как таймер будет считать от 0 до 255, то мы ещё раз делим на 256 и получаем приблизительно 15,3 Гц. Это потому, что прерывание происходит именно по окончанию счёта ну или по переполнению счётчика и получается, что обработка прерывания будет вызываться именно с такой частотой. То есть светодиоды будут у нас бежать с периодом около 65 милисекунд. Нормально.

Далее включим глобальные прерывания и прерывания от таймера, установив в 1 биты 7 и 5 регистра INTCON

 

OPTION_REG=0x07;

INTCON=0xA0;

 

И затем занесём 0 в регистр счёта таймера, тем самым запустим таймер

 

INTCON=0xA0;

TMR0=0;

 

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

 

#pragma config CP = OFF // Code Protection bit (Code protection disabled)

//--------------------------------------------

unsigned int TIM0_count=0;

//--------------------------------------------

 

Добавим специальную функцию-обработчик прерывания от таймера 0 выше функции main()

 

//--------------------------------------------

void interrupt timer0()

{

}

//--------------------------------------------

 

В данную функцию мы попадаем, когда происходит переполнение счёта, то есть когда счёт таймера достигнет числа 255.

 

 

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

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

 

void interrupt timer0()

{

  switch(TIM0_count%10)

  {

  case 0:

  PORTAbits.RA1 = 0;

  PORTBbits.RB0 = 1;

  break;

  case 1:

    PORTBbits.RB0 = 0;

    PORTBbits.RB1 = 1;

    break;

  case 2:

    PORTBbits.RB1 = 0;

    PORTBbits.RB2 = 1;

    break;

  case 3:

    PORTBbits.RB2 = 0;

    PORTBbits.RB3 = 1;

    break;

  case 4:

    PORTBbits.RB3 = 0;

    PORTBbits.RB4 = 1;

    break;

  case 5:

    PORTBbits.RB4 = 0;

    PORTBbits.RB5 = 1;

    break;

  case 6:

    PORTBbits.RB5 = 0;

    PORTBbits.RB6 = 1;

    break;

  case 7:

    PORTBbits.RB6 = 0;

    PORTBbits.RB7 = 1;

    break;

  case 8:

    PORTBbits.RB7 = 0;

    PORTAbits.RA0 = 1;

    break;

  case 9:

    PORTAbits.RA0 = 0;

    PORTAbits.RA1 = 1;

    break;

  }

}

 

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

 

 

Далее мы инкрементируем переменную счёта

 

    break;

  }

  TIM0_count++;

}

 

Так как у нас переменная для счёта 16-битная беззнаковая, то она будет считать до 65535 и получится что в самом конце она досчитает только до 5 в состоянии разделённом по модулю на 10. Поэтому мы обнулим её раньше, чтобы все циклы у нас доходили до 9 при разделении на 10 по модулю

 

TIM0_count++;

if(TIM0_count>3999)

{

  TIM0_count=0;

}

 

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

 

    TIM0_count=0;

  }

  T0IF=0;

}

 

Вот в принципе и весь код. Соберём его и посмотрим результат сначала в протеусе

 

 

Всё отлично работает.

Прошьём теперь настоящий контроллер и посмотрим всё, как говорится, наяву

 

 

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

А теперь самое интересное. Мы же не трогали код, который запускает бегущие огни по нажатию кнопки. Нажмём на кнопку и увидим вот такую картину

 

 

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

Но всё дело не в этом, а в том, что мы на практике увидели, что какие-то периодические действия, запущенные от таймера не влияют на ход основной программы. А это великое дело!

Всем спасибо за внимание!

Ждите следующих интересных уроков.

 

 

Предыдущий урок Программирование МК PIC Следующий урок

 

Исходный код

 

 

Купить программатор (неоригинальный) можно здесь: PICKit3

Купить программатор (оригинальный) можно здесь: PICKit3 original

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

PIC Таймеры

32 комментария на “PIC Урок 5. Таймеры
  1. Rudthaky:

    Тоже занялся изучением PIC-ов, правда в наличии только PIC18 по этому пробую на нем. В дополнение написанному Вами, хочу предложить не большой код по использованию WDT.
    //Игра в кости
    #include

    #pragma config OSC = RC // Oscillator Selection bits (External RC oscillator, CLKO function on RA6)
    #pragma config BOR = OFF // Brown-out Reset Enable bit (Brown-out Reset disabled)
    #pragma config WDT = ON // Watchdog Timer Enable bit (WDT enabled)
    #pragma config WDTPS = 256 // Watchdog Timer Postscale Select bits (1:256)
    #pragma config LVP = OFF // Low-Voltage ICSP Enable bit (Low-Voltage ICSP disabled)
    #pragma config MCLRE = ON // MCLR Pin Enable bit (MCLR pin enabled, RA5 input pin disabled)

    /*Data in programm mamory */
    /*sach table for cube*/
    //7. = 1, E = 2, 6 = 3
    //A = 4, 2 = 5, 8 = 6

    near char lookup[]=
    {
    0x77, 0x7E, 0x76, //1.1 1.2 1.3
    0x7A, 0x72, 0x78, //1.4 1.5 1.6
    0xE7, 0xEE, 0xE6, //2.1 2.2 2.3
    0xEA, 0xE2, 0xE8, //2.4 2.5 5.6
    0x67, 0x6E, 0x66, //3.1 3.2 3.3
    0x6A, 0x62, 0x68, //3.4 3.5 3.6
    0xA7, 0xAE, 0xA6, //4.1 4.2 4.3
    0xAA, 0xA2, 0xA8, //4.4 4.5 4.6
    0x27, 0x2E, 0x26, //5.1 5.2 5.3
    0x2A, 0x22, 0x28, //5.4 5.5 5.6
    0x87, 0x8E, 0x86, //6.1 6.2 6.3
    0x8A, 0x82, 0x88 //6.4 6.5 6.6
    };

    /*random number*/
    int count;

    void main (void)
    {
    ADCON1 = 0x0F; // all pin digital port
    TRISA = 0x01; //Port A, pin 0 is input
    TRISB = 0; //Port B all pin is output
    PORTB = 0xFF; //ALL led is off
    count = 0; //first point

    while(1)
    {
    ClrWdt(); //Reset Wotch Dog Timer
    if(PORTAbits.RA0 == 0) // if button is push
    {
    count++; //generic random number
    if(count == 36) //keep number in diapason of 0 to 36
    count = 0;

    PORTB = lookup[count]; //see number on led
    }
    }
    }

  2. Артем:

    Здравствуйте! Подскажите пожалуйста новичку, а как провернуть код с помощью таймера, чтобы светодиод плавно затухал, а затем плавно зажигался?

  3. ok_195:

    Добрый день, так как на форуме я не нашел обсуждения pic контролеров задам вопрос здесь, извините если туплю, только учусь писать программы. Написал программу измерения температуры с использованием PIC12f675+74HC595+DS18B20 с выводом температуры на семисегментный индикатор. Но так как измерения температуры занимает 750 мс, и не тратить это время на delay или на индикацию, которая может занимать от 750 мс до 1,5 с, для быстродействия решил в функцию main всунуть индикацию, а протокол обмена и датчиком через прерывания, проверяет глобальный flag_1, если флаг нулевой отправляет посылку измерения температуры, а если flag_1 поднят увеличивает flag_2 до 17, чтобы набрать 750 мс потом считывает показания и сбрасывает оба флага.

    • ok_195:

      Извините, случайно нажал отправить, и звените что долго объясняю проблему. Так вот я хочу чтобы после завершения кода из прерывания выполнение программы продолжалось с того же места, где была программа перед прерываниями ( то есть продолжала динамическую индикацию) вот код программы из прерывания:
      ///////////////////
      #int_TIMER0
      void TIMER0_isr(void)
      {
      disable_interrupts(GLOBAL);
      if(Flag_read_and_write)
      {
      if(Flag_timer16)
      {
      indled[0]=10;
      Temp_conv=(~Temp_conv)+1;
      }
      else
      indled[0]=11;
      indled[1]=Temp_conv/100;
      Temp_conv=Temp_conv%100;
      indled[2]=Temp_conv/10;
      indled[3]=Temp_conv%10;
      if(indled[1]==0)
      {
      indled[1]=11;
      if(indled[2]==0)
      indled[2]=11;
      }
      Flag_read_and_write=0;
      Flag_timer=0;
      set_timer0(0);
      enable_interrupts(INT_TIMER0);
      enable_interrupts(GLOBAL);

      }
      }
      else
      {
      init_1wire();
      transmitere_1wire(0xCC);
      transmitere_1wire(0x44);
      Flag_read_and_write=1;
      set_timer0(0);
      enable_interrupts(INT_TIMER0);
      enable_interrupts(GLOBAL);
      }
      #asm
      retfie
      #endasm
      }
      //////////////////

      Заранее благодарю за помощи, и извините за мою нудность.

  4. ok_195:

    Извините поправочка, вот правильный код
    #int_TIMER0
    void TIMER0_isr(void)
    {
    disable_interrupts(GLOBAL);
    if(Flag_read_and_write)
    {
    if(Flag_timer16)
    {
    indled[0]=10;
    Temp_conv=(~Temp_conv)+1;
    }
    else
    indled[0]=11;
    indled[1]=Temp_conv/100;
    Temp_conv=Temp_conv%100;
    indled[2]=Temp_conv/10;
    indled[3]=Temp_conv%10;
    if(indled[1]==0)
    {
    indled[1]=11;
    if(indled[2]==0)
    indled[2]=11;
    }
    Flag_read_and_write=0;
    Flag_timer=0;
    set_timer0(0);
    enable_interrupts(INT_TIMER0);
    enable_interrupts(GLOBAL);

    }
    }
    else
    {
    init_1wire();
    transmitere_1wire(0xCC);
    transmitere_1wire(0x44);
    Flag_read_and_write=1;
    set_timer0(0);
    enable_interrupts(INT_TIMER0);
    enable_interrupts(GLOBAL);
    }
    #asm
    retfie
    #endasm
    }

  5. ok_195:

    почему то он изменяет код
    вот начало
    {
    disable_interrupts(GLOBAL);
    if(Flag_read_and_write)
    {
    if(Flag_timer<11)
    {
    set_timer0(0);
    enable_interrupts(INT_TIMER0);
    enable_interrupts(GLOBAL);
    Flag_timer++;
    }

  6. ok_195:

    а вот продолжения до if(Flag_timer16)
    ////
    else
    {
    int8 TempH=0, TempL=0;
    int8 Temp_conv=0;
    init_1wire();
    transmitere_1wire(0xCC);
    transmitere_1wire(0xBE);
    delay_us(60);
    TempL=read_1_wire();
    TempH=read_1_wire();
    Temp_conv=TempL&0b11110000; //Se salveaza bitii de sus al primului byte 0bxxxxyyyy yyyyxxxx
    Temp_conv=Temp_conv|(TempH&0b00001111); // Se salveaza bitii de jos al al doilea byte, unde y este informatia utila
    swap(Temp_conv); // se inverseaza 4 cite 4 biti

    • ok_195:

      Спасибо за помощь))). Я уже сам разобрался, если кому поможет, из кода нужно выкинуть
      ///////////////
      disable_interrupts(GLOBAL);
      ////////////////
      ……….
      ////////////////
      enable_interrupts(INT_TIMER0);
      enable_interrupts(GLOBAL);
      ////////////////////////
      ……………..
      //////////////////
      #asm
      retfie
      #endasm
      ////////////////////
      Я пишу в CCS PICC, компилятор это всё сам выполняет, все пошло, всем удачи.

  7. Alex:

    Архив с исходным кодом не скачивается, т.к. в ссылке (https://narodstream.ru/pic/download/LESSON04/TIMER01.X.ZIP) ошибка.
    Правильная ссылка https://narodstream.ru/pic/download/LESSON05/TIMER01.X.ZIP

  8. Александр:

    Вопрос автору статьи.
    1.Урок конечно не для новичков. 2.Много ссылок на что-то, что мы должны знать, а как же новички?
    3. Если я не учил AVR, на который ссылается автор, что делать?4.Я использовал PIC18F4550, но как только попалась команда OPTION_REG=0x07; проект не собирается(не компилируется), и таймер не запускается. Предупреждение на то, что функция не назначена. А как ее назначить, я не знаю. Я же новичок. Язык программирования не учил, с памятью туго. Хочу по ходу разобраться, для меня так удобней. Подскажите как запустить таймер в PIC18F4550.

    • Скорее всего в даташите на данный таймер написано, как именно он запускается в нём. Причём, возможно, таким же самым образом. Могут различаться имена регистров.
      И, если Вы сразу переходите на такой сложный контроллер, то Вы не новичок. Поэтому Вы, наверно, перед тем, как перейти к данному контроллеру, внимательно изучили его техническую документацию, в которой написано, что таймер 0 здесь имеет отдельный регистр управления T0CON: TIMER0 CONTROL REGISTER, в котором и есть данные биты.

  9. Женя:

    Скажите пожалуйста, возможно ли одновременно использование TIMER0 и WDT? И возможно ли включать и выключать таймер из бесконечного цикла функции main. Хочу сделать, чтобы при высоком уровне на одном из портов включался таймер считал допустим до 5 секунд, затем отключался при низком уровне на том же порте, при снова высоком уровне все повторяется.
    Подскажите хотя-бы алгоритм.

  10. Михаил:

    при неоднократных попытках скомпелировать проект постоянно выдает две ошибки:
    1. variable has incomplete type 'void'
    2. expected ';' after top level declarator
    пробовал свой код и с архива
    компилятор XC8

    • Janis:

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

    • Елена:

      Аналогично.
      Компилятору не нравится слово interrupt в строке
      void interrupt timer0()
      Без него компилируется, но не работает (так как не обрабатывает прерывание), а с ним не компилируется.
      Выдаёт
      «/opt/microchip/xc8/v2.00/bin/xc8-cc» -mcpu=16F84A -c -fno-short-double -fno-short-float -fasmfile -maddrqual=ignore -xassembler-with-cpp -Wa,-a -DXPRJ_default=default -msummary=-psect,-class,+mem,-hex,-file -ginhx032 -Wl,—data-init -mno-keep-startup -mno-osccal -mno-resetbits -mno-save-resetbits -mno-download -mno-stackcall -std=c99 -gdwarf-3 -mstack=compiled:auto:auto -o build/default/production/main.p1 main.c
      ::: advisory: (2049) C99 compliant libraries are currently not available for baseline or mid-range devices, or for enhanced mid-range devices using a reentrant stack; using C90 libraries

      main.c:32:6: error: variable has incomplete type 'void'
      void interrupt timer0()
      ^
      main.c:32:15: error: expected ';' after top level declarator
      void interrupt timer0()
      ^
      ;
      2 errors generated.
      (908) exit status = 1

  11. Александр:

    Леночка, спасибо Вам Огромное! Нам новичкам эти тонкости недосягаемы (синтаксис меняется от версии к версии компилятора). Интересно: в связи с чем?

  12. Размик:

    здравствуйте посмотрел урок скопировал все с сайта но MPLAB пишет
    main.c:29:6: error: variable has incomplete type 'void'
    void interrupt timer0()
    ^
    main.c:29:15: error: expected ';' after top level declarator
    void interrupt timer0()
    ^
    ;
    не подскажете что делать заранее спасибо.

    • У Вас настроен компилятор на стандарт C99. Поэтому функции обзывайте тогда тоже согласно этого стандарта. А если не хотите то переключите стандарт на более старый, тот который был использован в уроке — C90. Делается это в свойствах проекта в разделе XC8 Global Options (прямо в корне) в пункте C Standard.

  13. АЛЕКСЕЙ:

    Здравствуйте. не могли бы вы подсказать как работает данный код программы, пройтись по строчкам кода. непонятно как значение переменной tim0_count%10 делится на десять по модулю и при этом переключаются case, инкрементация tim0_count ведь происходит на единицу. спрашиваю потому, что в си не силён, только учу. спасибо.

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

  14. Дмитрий:

    Здравствуйте!
    Спасибо за уроки!
    Вопрос Новичка. Программатор PICkit-3 это пока учимся или это инструмент на будущее и он может прошивать более мощные чипы? Целесообразность приобретения интересует

  15. Andy:

    Что означает запись «switch(TIM0_count%10)»?
    Почему не «switch(TIM0_count)»?

    И ещё, зачем эти танцы с портом А:
    TRISA &= ~0x03;
    PORTA &= ~0x03;
    TRISA |= 0x04;
    ?
    Почему бы не настроить его таким же образом, как и порт В?

    • Andy
      А это чтобы в 10 раз медленнее срабатывало. Танцы с портом стандартные — сброс битов 0 и 1 в первых двух регистрах, а затем в первом устанавливаем бит 3.

  16. Иван:

    спасибо за уроки. пишу на асме и хочу освоить С.
    если TIM0_count нужны значения от 1 до 10, то не надо его делать типом int, достаточно 8-ми битного unsigned char. посмотрите, как изменится при этом объем кода, особенно это заметно, если посмотреть Disassembly. операция вычисления остатка от деления выглядит красиво, но так же занимает время контроллера. проще:
    switch(TIM0_count)
    TIM0_count++
    if (TIM0_count == 10) TIM0_count = 0

  17. Илья:

    Здравствуйте!
    Не знаю насколько нужна тут моя мысль, но я бы предложил отказаться от TIM0_count. Зачем нам кол-во заходов по прерыванию? Нам нужно перебирать номера светодиодов.
    Создаем
    unsigned int ledNum=0;

    И в обработчике прерывания меняем:
    switch (ledNum) {

    }
    // и теперь инкремент ledNum
    ledNum++;
    if (ledNum > 9) { // Чтобы не вышел за кол-во светодиодов
    ledNum = 0;
    }

    Сэкономили деление и алгоритм стал логичнее.

    С уважением, Илья

  18. Николай:

    Елена, большое спасибо за подсказку. Если Вас не затруднит, сообщите, где Вы нашли решение данной проблемы.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*