понеділок, 21 березня 2016 р.

Таймери STM32 -- захоплення вводу/CMSIS

Для роботи із далекоміром нам треба не тільки подавати імпульс заданої довжини, але і заміряти тривалість імпульсу на Echo, яка визначає час польоту звуку. Хоча це можна зробити і вручну, опитуванням, як раніше, або скориставшись зовнішнім перериванням EXTI, як буде розглянуто в подальшому, цікавіше зробити все автоматично -- за допомогою таймерів, які мають спеціальний режим -- Input capture mode.

Кожен канал має регістр TIMx_CCRx, в який, у режимі захоплення, зберігається значення лічильника в момент приходу сигналу. При тому встановлюється відповідний біт CCXIF у регістрі TIMx_SR та може бути згенеровано переривання чи запит DMA. Якщо під час захоплення біт CCXIF, який автоматично очищається при читанні з TIMx_CCRx чи може бути очищений програмно, все ще рівний 1 -- значить попереднє значення не було прочитано, встановлюється прапорець CCxOF в тому ж регістрі статусу, та може генеруватися відповідне переривання чи запит -- over-capture.
Захоплення може відбуватися по зростанню, (фронту), спаданню сигналу чи і по тому і по тому. Можна встановити фільтр -- кількість вибірок, після якої можна вважати, що сигнал встановився (такий собі захист від дрижання контактів). Можна ділити вхідну частоту на 2, 4 або 8 --- реєструвати кожну другу-четверту-восьму подію.
Почнемо із найпростішого -- запускаємо таймер та зберігаємо значення лічильника у момент натискання кнопки.


Для збереження використовуватиметься простенький кільцевий буфер. Детальніше він описаний в додатку.
Тут можна б було застосувати також DMA, але, думаю, поговоримо про нього на якомусь іншому прикладі -- кому-як, а мені таймери вже починають набридати.
Так як таймер 16-бітний, із 16-бітним подільником, він може відраховувати максимум 65536*65536 = 4294967296 = 2^32 тактів ядра. На частоті мікроконтролера плати  STM32VLDiscovery -- 24МГц, це дозволяє вимірювати інтервали до 178 секунд (ну добре, до майже 179, а ще можна тактову частоту контролера знизити, однак це все несерйозно). Крім того, працювати із подільником 24000 (48000) -- коли відлік кожну мілісекунду чи дві, багато зручніше,  але й зменшує доступний проміжок часу. Щоб мати можливість заміряти більші інтервали, можна залучити інший таймер -- як вже описувалося, але тут зробимо по іншому -- коли відбуватиметься переповнення, вручну збільшуватимемо лічильник переповнень. Тоді можна взяти подільник 24000 (нагадаємо вкотре, що в регістр PSC слід покласти 24000 - 1), і безпосередньо отримувати результат в мілісекундах.

Виводитимемо результат за допомогою Semihosting та Newlib-printf.

Ініціалізуємо все що треба. Кнопка на платі під'єднана до піна PA0, його -- на вхід, решта -- як завжди:

 RCC->APB2ENR  |= RCC_APB2ENR_IOPAEN; // Дозволяємо тактування порту A

 RCC->APB1ENR  |= RCC_APB1Periph_TIM2; // Дозволяємо тактування TIM2


 GPIOA->CRL |=  GPIO_CRL_CNF0_0;// 01 -- Input floating
 GPIOA->CRL &= ~GPIO_CRL_CNF0_1;
 GPIOA->CRL  &= ~GPIO_CRL_MODE0; // Має бути 00 -- Input

Задаємо базові властивості таймера:

 TIM2->PSC = 24000 - 1;
 TIM2->CNT = 0;
 TIM2->ARR = reload_val;

де reload_val -- константа, рівна 10000, яка використовується і в обробці переривань -- на неї множиться лічильник переповнень, див. далі.

PA0 -- відповідає каналу 1 таймера TIM2, тому його й використано. Тепер слід таймеру повідомити, що канал 1 служитиме як TI1:

  // CC1S: Capture/Compare 1 selection: CC1 channel is configured as input, 
  // IC1 is mapped on TI1
 TIM2->CCMR1 &= ~ TIM_CCMR1_CC1S_1; // CCR1 <- TI1
 TIM2->CCMR1 |=   TIM_CCMR1_CC1S_0;

Захоплення по фронту:

TIM2->CCER  &= ~ TIM_CCER_CC1P; //Rising edge

Дозволяємо захоплення:

TIM2->CCER |= TIM_CCER_CC1E;    // Enable capture

Якщо, випробовуючи програму, бачите повторні спрацювання, між якими може не бути і мілісекунди, знайте, це -- дрижання контактів кнопки. Щоб мінімізувати їх кількість (див. також попередній пост), фільтр вмикаємо "по максимуму":

TIM2->CCMR1 |=   TIM_CCMR1_IC1F; // Filter -- по максимуму

Так як хочемо ловити кожне натискання, подільник для каналу вимикаємо:

TIM2->CCMR1 &= ~ TIM_CCMR1_IC1PSC; // Prescaler disabled

Дозволяємо переривання по захопленню -- щоб зберігати момент натискання та по переповненню, щоб відраховувати великі інтервали часу:

 TIM2->DIER |= TIM_DIER_CC1IE; // Enable interrupt on capture
 TIM2->DIER |= TIM_DIER_UIE; // Enable interrupt on update 
                               //-- for long counter;

Тепер можна запустити таймер:

TIM2->CR1  |= TIM_CR1_CEN;

Та дозволити йому генерувати переривання:

NVIC_EnableIRQ(TIM2_IRQn);


Перш ніж перейти до головного циклу програми, розглянемо процедуру обробки переривання (від таймера-2). Для збереження даних використовується два глобальних кільцевих буфери, raw_data -- куди зберігаються безпосередньо значення регістра CCR1, (приклад, все ж, навчальний) та abs_data, куди потрапляє час, у мілісекундах, що минув від ввімкнення таймера до натискання кнопки. Реалізація кільцевого буфера знаходиться у файлах rb.h та rb.c, коротко про неї буде далі. Для того, щоб покласти дані в кінець буфера, використовується функція rb_append(), котра повертає false, якщо буфер заповнений, щоб забрати дані з початку буфера -- rb_take().

Глобальна змінна time_from_poweron містить кількість переповнень з початку роботи таймера.
Так як при початку роботи таймера чогось (певне, в документації описано, але я якось не розібрався...) зразу прилітає подія оновлення, на початку в цю змінну кладеться -1.

void TIM2_IRQHandler(void)
{
 // Джерело перевіряти не треба. А от походження переривання -- треба
 if( TIM2->SR & TIM_SR_CC1IF )
 {
  // Очищати цю подію не потрібно -- очищається автоматично 
  // при читанні CCR1!
  // TIM2->SR &= ~TIM_SR_CC1IF;

  int res = TIM2->CCR1;
  buffer_is_full = !rb_append(&raw_buffer, res);
  rb_append(&abs_buffer, reload_val*time_from_poweron + res);
 }
 // Рекомендовано робити після читання даних -- щоб не впустити overcapture,
 // яке виникне зразу після читання прапорця, перед читанням даних
 if(TIM2->SR & TIM_SR_CC1OF)
 {
  TIM2->SR &= ~TIM_SR_CC1OF;
  overcapture = true;
 }

 if( TIM2->SR & TIM_SR_UIF )
 {
  TIM2->SR &= ~TIM_SR_UIF;
  ++time_from_poweron;
 }
}

Обробник простий -- перевіряє, що послужило причиною події.
  • Якщо захоплення -- зберігаємо вміст CCR1 в буфер, у інший  буфер -- абсолютне значення. Глобальна змінна  buffer_is_full служить прапорцем -- чи не переповнився буфер. 
  • Якщо переповнення -- попередній результат не було прочитано, перш ніж в CCR1 з'явився новий -- встановлюється відповідний прапорець.
  • Якщо оновлення -- збільшується лічильник.
Обробники, за потреби, програмно очищають біти події (див. також коментарі).

Повертаємося до головного циклу програми -- там, де вона повинна забрати результати. З цим є нюанс. Говорячи термінами "великого" програмування та "великих" операційних систем, задача багатопоточна. Переривання виникає в будь-який, не передбачуваний, момент часу. Зокрема, це може статися в процесі виконання коду main(), що маніпулює буфером. На щастя, тут ситуація простіша, ніж в загальному випадку багатопоточної програми. Обробник переривання у нас перервати ніхто не може. (Взагалі, більш пріоритетні переривання можуть "перебивати" менш пріоритетні, але у нас воно одне). Значить, всередині обробника можна працювати як завжди. У свою чергу, main() на таке покладатися не може. Але вона може заборони переривання, перш ніж почати маніпуляції з буфером.

while(1)
    {
     NVIC_DisableIRQ(TIM2_IRQn); // <==========
     if( !rb_is_empty(&raw_buffer) )
     {
      rb_take(&raw_buffer, &raw_data_copy );
      rb_take(&abs_buffer, &abs_data_copy );
      have_data = true;
      local_buffer_is_full = buffer_is_full;
      buffer_is_full = false;
     }
     NVIC_EnableIRQ(TIM2_IRQn); // <==========
     if(overcapture)
     {
      printf("Overcaptured!\n");
      overcapture = false;
     }
     if(local_buffer_is_full)
     {
      printf("Buffer is full!\n");
      local_buffer_is_full = false;
     }

     if(have_data)
     {
      //ue cnt --- update events counter, якщо що
      printf("At %"PRId64", raw CCR1: %"PRId64", ue cnt: %"PRId64"\n",
         abs_data_copy, raw_data_copy, time_from_poweron);
      have_data = false;
     }

    }


Забороняти надовго їх не варто! А Semihosting повільний. Тому копіюємо дані собі і зразу дозволяємо їх знову.
Зауваження: не те, щоб це було аж так необхідно, але для лічильника переповнень використовую 64-бітний тип, uint64_t -- щоб точно вистачило. Однак, printf() безпосередньо його виводити не вміє -- через надмірну свободу С із вибором розміру вбудованих типів. Тому використано специфікатор %"PRId64" із  <inttypes.h>. На щастя, newlib його підтримує.
В результаті роботи програми отримуємо щось таке:

At 3543, raw CCR1: 3543, ue cnt: 0
At 5111, raw CCR1: 5111, ue cnt: 0
At 7314, raw CCR1: 7314, ue cnt: 0
At 7402, raw CCR1: 7402, ue cnt: 0
At 7685, raw CCR1: 7685, ue cnt: 0
At 10319, raw CCR1: 319, ue cnt: 1
At 11894, raw CCR1: 1894, ue cnt: 1
At 13872, raw CCR1: 3872, ue cnt: 1
At 28115, raw CCR1: 8115, ue cnt: 2
At 29920, raw CCR1: 9920, ue cnt: 2


Додаток -- кільцевий буфер

Зовсім простенька реалізація знаходиться в файлах rb.h i rb.c. Він описується структурою ring_buffer_t:

typedef struct {
    volatile uint64_t * buffer_ptr;
    size_t length;
    size_t start;
    size_t end;
} ring_buffer_t;

Структура ця зберігає вказівник на масив для даних, який слід створити окремо. В цій реалізації буфер зберігає 64-бітні цілі числа.

Функція rb_append():

bool rb_append( volatile ring_buffer_t* rb, uint64_t data);

отримує  вказівник на структуру, що описує буфер та елемент даних, який слід до нього додати. Елемент додається в кінець буферу. Якщо місця в буфері немає, повертається false (природно, тоді нічого нікуди не додаватиметься), якщо додано успішно -- повертає true.

Функція rb_take():

bool rb_take(volatile ring_buffer_t * rb, volatile uint64_t* pdata);

Забирає елемент з початку та кладе його за переданим вказівником. Якщо буфер порожній -- повертає false.

Крім того, є ряд допоміжних функцій із очевидними назвами:

void rb_init(volatile ring_buffer_t* rb, volatile uint64_t* pdata, size_t len);
size_t rb_data_size(
volatile const ring_buffer_t* rb);
bool rb_is_empty(
volatile const ring_buffer_t* rb);
bool rb_is_full(
volatile const ring_buffer_t* rb);

Правда, у цьому прикладі, так як буфер має бути глобальною змінною, для простоти ініціалізую його безпосередньо, без використання rb_init():

#define  rb_buf_size 16 // Чистий С const не сприйме тут
volatile uint64_t raw_data[rb_buf_size];
volatile uint64_t abs_data[rb_buf_size];

volatile ring_buffer_t raw_buffer = {raw_data, rb_buf_size, 0, 0};
volatile ring_buffer_t abs_buffer = {abs_data, rb_buf_size, 0, 0};

Ремарки щодо використання volatile:
  • Масив, оголошений volatile, буде цілий таким -- всі його елементи вважатимуться volatile. (Див. тут). Те ж стосується і структур.
  • Однак, якщо в оголошенні функції не вказано, що аргумент вказує на volatile пам'ять, функція може бути оптимізована, щоб не читати дані повторно. Це, за великих рівнів оптимізації, приводить до того, що, наприклад,  rb_is_empty(), використана в циклі, завжди повертатиме true, беручи дані із регістра, без звертання до пам'яті. Тому всюди слід писати щось виду: "volatile ring_buffer_t* rb".



main.c



#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>

#include <stm32f10x.h>
#include <semihosting.h>
#include <inttypes.h>

#include  "rb.h"


#ifdef __cplusplus
extern "C"{
#endif

#define reload_val 10000

#define  rb_buf_size 16 // Чистий С const не сприйме тут
volatile uint64_t raw_data[rb_buf_size];
volatile uint64_t abs_data[rb_buf_size];

volatile bool buffer_is_full = false;
volatile bool overcapture = false;

volatile ring_buffer_t raw_buffer = {raw_data, rb_buf_size, 0, 0};
volatile ring_buffer_t abs_buffer = {abs_data, rb_buf_size, 0, 0};

volatile uint64_t time_from_poweron = 0;


void TIM2_IRQHandler(void)
{
 // Джерело перевіряти не треба. А от походження переривання -- треба
 if( TIM2->SR & TIM_SR_CC1IF )
 {
  // Очищати цю подію не потрібно -- очищається автоматично при читанні CCR1!
  // TIM2->SR &= ~TIM_SR_CC1IF;

  int res = TIM2->CCR1;
  buffer_is_full = !rb_append(&raw_buffer, res);
  rb_append(&abs_buffer, reload_val*time_from_poweron + res);
  /* //Try overcapture
  int x = 0;
   while(x<10000000)
    ++x;
  */
 }
 // Рекомендовано робити після читання даних -- щоб не впустити overcapture,
 // яке виникне зразу після читання прапорця, перед читанням даних
 if(TIM2->SR & TIM_SR_CC1OF)
 {
  TIM2->SR &= ~TIM_SR_CC1OF;
  overcapture = true;
 }

 if( TIM2->SR & TIM_SR_UIF )
 {
  TIM2->SR &= ~TIM_SR_UIF;
  ++time_from_poweron;
 }
}

#ifdef __cplusplus
}
#endif

int main(void)
{
 // Дозволяємо тактування порту C, на ньому -- світлодіоди плати STM32VLDiscovery
 // PC8 -- blue, PC9 -- green

 RCC->APB2ENR  |= RCC_APB2ENR_IOPCEN; // Дозволяємо тактування порту C

 RCC->APB2ENR  |= RCC_APB2ENR_IOPAEN; // Дозволяємо тактування порту A

 RCC->APB1ENR  |= RCC_APB1Periph_TIM2; // Дозволяємо тактування TIM2


 GPIOA->CRL |=  GPIO_CRL_CNF0_0;// 01 -- Input floating
 GPIOA->CRL &= ~GPIO_CRL_CNF0_1;
 GPIOA->CRL  &= ~GPIO_CRL_MODE0; // Має бути 00 -- Input

 //! Порти світлодіодів - теж на вивід. Звертаємося до старших пінів, тому CRH -- High
 GPIOC->CRH &= ~GPIO_CRH_CNF8;
 GPIOC->CRH  |= GPIO_CRH_MODE8;
 GPIOC->CRH &= ~GPIO_CRH_CNF9;
 GPIOC->CRH  |= GPIO_CRH_MODE9;



 TIM2->PSC = 24000;
 TIM2->CNT = 0;
 TIM2->ARR = reload_val;

 // CC1S: Capture/Compare 1 selection: CC1 channel is configured as input, IC1 is mapped on TI1
 TIM2->CCMR1 &= ~ TIM_CCMR1_CC1S_1; // CCR1 <- TI1
 TIM2->CCMR1 |=   TIM_CCMR1_CC1S_0;

 TIM2->CCER  &= ~ TIM_CCER_CC1P; //Rising edge
 TIM2->CCER |= TIM_CCER_CC1E;    // Enable capture

 TIM2->CCMR1 |=   TIM_CCMR1_IC1F; // Filter -- по максимуму
 TIM2->CCMR1 &= ~ TIM_CCMR1_IC1PSC; // Prescaler disabled

 TIM2->DIER |= TIM_DIER_CC1IE; // Enable interrupt on capture
 TIM2->DIER |= TIM_DIER_UIE; // Enable interrupt on update -- for long counter;

 --time_from_poweron; // Чомусь зразу прилітає одна подія оновлення...
 // Тому віднімаємо 1, щоб після неї почати з нуля.
 // Код нижче не допоміг
 //TIM2->SR &= ~TIM_SR_UIF;
 //TIM2->SR &= ~TIM_SR_CC1IF
 TIM2->CR1  |= TIM_CR1_CEN;


 NVIC_EnableIRQ(TIM2_IRQn);


 uint64_t raw_data_copy;
 uint64_t abs_data_copy;
 bool have_data=false;
 bool local_buffer_is_full;
    while(1)
    {
     NVIC_DisableIRQ(TIM2_IRQn);
     if( !rb_is_empty(&raw_buffer) )
     {
      rb_take(&raw_buffer, &raw_data_copy );
      rb_take(&abs_buffer, &abs_data_copy );
      have_data = true;
      local_buffer_is_full = buffer_is_full;
      buffer_is_full = false;
     }
     NVIC_EnableIRQ(TIM2_IRQn);
     if(overcapture)
     {
      printf("Overcaptured!\n");
      overcapture = false;
     }
     if(local_buffer_is_full)
     {
      printf("Buffer is full!\n");
      local_buffer_is_full = false;
     }

     if(have_data)
     {
      //ue cnt --- update events counter, якщо що
      printf("At %"PRId64", raw CCR1: %"PRId64", ue cnt: %"PRId64"\n",
         abs_data_copy, raw_data_copy, time_from_poweron);
      have_data = false;
     }

    }
} 



Проект, із повним текстом програми, можна скачати тут.

8 коментарів:

  1. Спасибо Вам, очень интересный блог. Есть просьба - могли бы Вы подробнее рассказать об одновременной работе с разными каналами одного таймера в режиме захвата?
    Поясню вопрос - есть вращающаяся турбинка с магнитом, сигнал с которой снимается датчиком холла. Таких турбин - три. Нужно снимать показания скорости вращения всех трех, с выводом показаний на дисплей. Скорость вращения - от 0 до 225 оборотов (импульсов) в минуту.
    Вопроса, собственно, два
    1.Прошу Вас подсказать, верно ли я понял, что для измерения частоты, способ измерения периода с помощью захвата - наиболее точный на такой низкой частоте?
    2. Поясните пожалуйста, можно ли решить мой вопрос с помощью одного таймера и его трех каналов, или нужно все же три таймера?

    ВідповістиВидалити
    Відповіді
    1. И Вам спасибо за отзыв. :-)

      Увы, не могу пока ответить более подробно, но:
      1. Собственно, захват особенно полезен на больших частотах, но использовать можно и на низких -- почему нет?

      2. Да, одного таймера (у которого хотя бы три канала есть, конечно!) должно хватить -- ведь каждый канал имеет свой регистр захвата, биты разрешения и прерывания и т.д: TIMx_CCR1, TIMx_CCR2, TIMx_CCR3, TIMx_CCR4, биты CC1E, CC1P, и т.д. в TIMx_CCER; IC1PSC, IC1F, CC1S и т.д. в TIMx_CCMR1/TIMx_CCMR2; CC1G и другие из TIMx_EGR; биты статуса CC1IF, CC1OF и т.д. из TIMx_SR.

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

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

      В принципе, захват ШИМ как раз одновременно два канала и использует: http://indrekis2.blogspot.com/2016/03/stm32-cmsis_26.html -- при чем, по разному настроенные. Единственно, что в этом режиме оба канала пользуются тем же входом.

      Видалити
  2. Дякую за відповідь. Не треба переходити із ввічливості для мене одного на російску. Я українську добре розумію, тільки розмовляю та пишу не дуже добре - роблю багато помилок. Мені зручніше писати російскою, бо нею я звик мислити. Так виріс. Що до справи - я початківець у контролерах, тому так багато питань. Як що в Вас буде час та бажаня, буду дуже радий та вдячний побачити у Вашому виконанні ще один туторіал з приводу читання та обробки сигналів з трьох датчиків, подибніх до ціх - http://www.banggood.com/ru/G1-2-Water-Flow-Sensor-Flowmeter-Hall-Flow-Meter-4-Wire-p-943922.html?rmmds=search

    ВідповістиВидалити
    Відповіді
    1. Та я не стільки з ввічливості, скільки не знаючи, звідки Ви. Видно в статистиці, що блог перекладали на російську, англійську, іспанську, навіть японську і т.д. Але українською мені писати, безперечно, приємніше! :-)

      Щодо сигналів із трьох датчиків -- мені теж цікаво, при нагоді спробую написати, але, на жаль, не знаю, коли дійдуть руки -- велика черга статей для моїх студентів висить. :-(

      А щодо питань - пишіть! Негайної відповіді не гарантую, через критичну недостачу сил і часу, але раптом мені буде просто відповісти, або хтось інший відповість.

      Видалити
  3. Благодарю за ответ. Жду с нетерпением продолжения. Вопрос есть, только я не знаю, куда его правильнее было бы отправить. Пытаясь научится программировать свой 32F0308DISCOVERY, узнал о возможности выводить сообщения и значения с помощью стандартно функции printf() в режиме semihosting. (поддержка SWO в F0 - отсутствует).
    Я установил несколько Keil, IAR, SW. SW - сложная настройка. В IAR все казалось бы настроено по умолчанию. Но вывода в Terminal I/O - нет. Библиотеку stdio.h - подключил Все компилируется без ошибок, но вывода в окно терминала - нет. Даже простой printf("Hello!"); - проходит совсем без следов. Не могли бы вы рассказать о semihosting и SWO ?

    ВідповістиВидалити
  4. semihosting в IAR - вопрос снялся. Оказывается, не выводятся строки без символа перевода строки \n
    Но, все равно, тема очень интересная. Буду рад, если Вы напишете статью и на эту тему.

    ВідповістиВидалити
    Відповіді
    1. Насправді, про це все я вже писав:
      http://indrekis2.blogspot.com/2015/11/c-semihosting-stm32-coide.html -- особливості використання стандартної бібліотеки С, зокрема і про SemiHosting. Правда, для середовища, яке саме по собі, не надає майже нічого, але дозволяє підключати компоненту із реалізацією взаємодії цим інтерфейсом. Ставив ціллю показати, як воно працює зсередини -- щоб не залежати від примх конкретних інструментів.
      Здається навіть про те, що поки немає \n, буфери не спорожнюються до заповнення, і виводу може не бути.

      Отут: http://indrekis2.blogspot.com/2016/10/hc-sr04-gpiohal.html -- "Додаток: SemiHosting i libc для лінивих" опис процедури, як цей код прикрутити до System Workbench for STM32.

      Планую ще написати, як зробити, щоб були різні "файли" для виводу -- SemiHosting, UART, файли на SD-картах (за допомогою FatFS), причесати трохи той код.

      Видалити
  5. Доброго дня, блог просто шикарний, дуже багато корисного.
    Але хотів би запитати може Ви з таким зустрічались!
    В мене є прямокутний сигнал, мені треба захвачувати перший імпульс, по зростанню, і наступний також по зростанню, і знаходити період в залежності від ширини імпульсу. Тобто по зміні положення від 0 до 1, мені треба робити перший захват і при наступній зміні положення знову захватувати значення, звідси знайти ширину імпульту тобто період, і вирахувати частоту.
    Головна проблема полягає в коді. Якщо б я міг з вами зв'язатись, і показати вам свій код.
    Буду дуже вдячний за відповіть.

    ВідповістиВидалити