неділю, 27 листопада 2016 р.

Далекомір HC-SR04 -- зовнішні переривання EXTI/CMSIS

Взято тут: "STM32 External Interrupt"
Перекласти всю роботу із далекоміром на таймери (чи таймер) -- зручно, але, іноді, надміру складно -- якісь Capture-compare, захоплення ШІМ, інший "страх и ужас". Мікроконтролери мають ще один, простіший спосіб фіксувати тривалість імпульсу -- за допомогою "банального" зовнішнього переривання -- EXTI: фіксується момент зміни стану лінії ECHO з 0 в 1, потім -- назад, тривалість -- різниця часу між ними. Таймер, звичайно, використовується -- для вимірювання часу, але лише як лічильник. 

Спрощений варіант роботи за допомогою переривань було розглянуто в "методичному" пості: "Зовсім просто про далекомір HC-SR04 із GPIO/HAL", тут дотримуватимуся більш систематичного, але дещо заскладного для конкретної простої задачі підходу. 


Для відліку часу -- як під час подання сигналу на TRIG, так і для заміру тривалості ECHO, використано TIM1:
  • Trig знаходиться на його каналі 3 -- пін PA10
  • Echo під'єднано до PA6.
Логіка роботи програми наступна: 
  • За допомогою таймера на Trig генерується імпульс потрібної довжини.
  • Лічильник цього таймера використовується для заміру часу, а його переривання переповнення -- для фіксації таймауту, факту, що далекомір не реагує і слід спробувати наново.
  • Коли Trig перейшов з 1 в 0 -- починаємо чекати на Echo (а якщо не перейшов -- повідомляємо про помилку).
  • Для цього дозволяємо переривання по зростанню (по фронту) на PA6.
  • Якщо переривання сталося -- фіксуємо момент та чекаємо на переривання про спаду.
  • Якщо воно сталося, забороняємо переривання від Echo (PA6) та виводимо результат.
  • Якщо таймаут стався раніше, ніж на лінії Echo почався та закінчився імпульс, повідомляємо про помилку.
Стан програми задається, як і раніше, значеннями із відповідного enum:

typedef enum state_t{IDLE_S, TRIGGERING_S, WAITING_FOR_ECHO_START_S, 
                     WAITING_FOR_ECHO_STOP_S, TRIG_NOT_WENT_LOW_S, 
                     ECHO_TIMEOUT_S, ECHO_NOT_WENT_LOW_S, READING_DATA_S, 
                     ERROR_S} state_t;

volatile state_t state;
volatile uint32_t measured_time;

Переривання 

Про підсистему переривань ARM Cortex M сподіваюся, напишу окремо, а тут зосередимося на зовнішніх перериваннях STM32. Детальніше див., скажімо:
STM32F100 має 18 ліній зовнішніх переривань. 16 -- по одній на кожен номер піна (див. нижче), з номерами 0-15, і дві спеціалізованих, 16-та  під'єднана до Programmable voltage detector (PVD), а 17-та до RTC Alarm.

STM32F303 має 36 ліній! З них, на жаль, до пінів під'єднано тих же 16, решта -- до внутрішньої периферії:
(с) STMicroelectronics
Під'єднання цих ліній переривань до пінів зроблено хитро:

Мультиплексори ліній переривань.
Тобто, лінії EXTI0 відповідає пін 0 будь-якого, але одного, із портів. Не можна одночасно реагувати на зміну стану, скажімо, PA0 i PB0. Аналогічно, EXTI1 відповідає пін 1 котрогось із портів і т.д. Котрий порт працює із даною лінією, визначається одним із чотирьох регістрів AFIO_EXTICRx. 

Перш ніж змінювати його вміст, слід ввімкнути тактування блоку альтернативних функцій пінів -- AFIO:

    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;

У нас Echo далекоміра приєднане до PA6, тому кажемо, що шоста лінія отримує сигнал із PortA (див. функцію echoExtiInit()):

  AFIO->EXTICR[1] |= AFIO_EXTICR2_EXTI6_PA;

У відповідь на виникнення сигналу на лінії переривання, мікроконтролер може:
  • Згенерувати, власне, переривання -- викликати код обробника.
  • Створити подію -- встановити той чи інший прапорець (на який, у свою чергу, може прореагувати інша периферія, наприклад, АЦП здійснити перетворення). 
Первинною є подія -- кожне переривання генерується у відповідь на подію, але не кожна подія генерує переривання. Крім того, є можливість згенерувати переривання програмно, див. далі.

Біти регістру  EXTI_IMR -- Interrupt mask register, дозволяють чи забороняють переривання від відповідних ліній. (В STM32F3 таких регістрів два -- EXTI_IMR1 і EXTI_IMR2, бо ліній переривань більше ніж 32):

Регістр EXTI_IMR для STM32F100. (c) STMicroelectronics

У нашому випадку дозволяємо переривання на лінії 6:

  EXTI->IMR |= EXTI_IMR_MR6; 

Аналогічний регістр (регістри) --- EXTI_EMR, є і для генерації подій.


Регістри EXTI_RTSR (Rising trigger selection register)  і EXTI_FTSR (Falling trigger selection register) керують спрацюванням по фронту та по спаду сигналу. Кожній лінії відповідає по біту в цих регістрах. Якщо встановити 1 в обидва, переривання буде і по фронту сигналу і по спаду. В STM32F3 їх теж по два: EXTI_RTSR1, EXTI_RTSR2, EXTI_FTSR1, EXTI_FTSR2.

В коді використано 4 наступних очевидних допоміжних функції:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void enable_capture_falling() {
 EXTI->FTSR |= (EXTI_FTSR_TR6); // Falling trigger selection register
}

void enable_capture_rising() {
 EXTI->RTSR |= (EXTI_RTSR_TR6); // Rising trigger selection register
}

void disable_capture_falling() {
 EXTI->FTSR &= ~(EXTI_FTSR_TR6); // Falling trigger selection register
}

void disable_capture_rising() {
 EXTI->RTSR &= ~(EXTI_RTSR_TR6); // Rising trigger selection register
}

Дуже важливим є регістр EXTI_PR -- після виникнення зовнішнього переривання, мікроконтролер встановлює відповідний біт цього регістра в 1. Очищати його слід програмно, наприклад, в обробнику переривання, інакше після його завершення, обробник буде знову викликано. УВАГА! Для очищення бітів цього регістра слід в них записувати 1!


Згадана вище можливість реалізувати переривання програмно, реалізується регістром EXTI_SWIER (Software interrupt event register), запис у відповідний біт якого дає той же ефект, що виникнення цього переривання.

Отож, послідовність дій, щоб дозволити зовнішнє переривання:

  1.  Дозволити тактування AFIO та порту, на пін якого реагуватимемо.
  2. В регістрі AFIO->EXTICR вказати, а котрий порт нас цікавить.
  3. Дозволити відповідне переривання в EXTI->IMR.
  4. Встановити реакцію на фронт чи/і на спад сигналу в регістрах EXTI->RTSR і EXTI->FTSR.
  5. Чекати на виникнення переривання. Пам'ятати в обробнику очистити відповідний біт  у EXTI->PR.
Звичайно, переривання повинні бути глобально дозволені, наприклад, викликом __enable_irq().

Обробники EXTI

На кожну із перших 5-ти ліній, EXTI0-EXTI5, приходиться по обробнику -- "слабкій" (weak) функції із іменем виду EXTIx_IRQHandler(), наприклад EXTI2_IRQHandler(). Якщо написати таку функцію (котра нічого не приймає і не повертає), вона викликатиметься у відповідь на переривання.
Нагадаю, переривання ARM Cortex M викликаються так, що їх обробником може бути звичайна функція С. Говорячи більш технічно, при виклику процесор автоматично зберігає всі scratch-регістри, визначені ARM C ABI а при поверненні із обробника -- відновлює їх. Порівняйте із написанням IRQ для x86, хто ще пам'ятає. :-)
Ситуація для ліній із більшими номерами трішки складніша -- є по одному обробнику для всіх ліній від 5 до 9 -- EXTI9_5 та, аналогічно, єдиний EXTI15_10, для ліній 10..15 (зверніть увагу на зворотній порядок номерів у назві). Наша лінія -- 6-та, тому обробник виглядатиме так:


1
2
3
4
5
6
7
void EXTI9_5_IRQHandler(void) {
 if (EXTI->PR & EXTI_PR_PR6) {
  EXTI->PR |= EXTI_PR_PR6; // This bit is cleared by 
                           // writing a 1 into the bit!
.................................
        }
}



Налаштування таймера 


Детально налаштування відповідних можливостей таймерів розглядалися в "Таймери STM32 -- ШІМ/CMSIS" та "Таймери STM32 -- відлік часу/CMSIS". Тут лише коротко:

  • Функція baseTimersInit() встановлює відлік кожної мікросекунди та Capture-compare подію на каналі CH3 через 12 мкс, задаючи цим тривалість імпульсу. Період таймера -- максимальний, відлік вверх, біт автоматичної зупинки, OPM, встановлено -- щоб таймер не подав ще один імпульс, поки наша програма чимось іншим зайнята, даремно ганяючи далекомір.  
  • trigChannelInit() налаштовує 3-й канал (який керую Trig), але поки не дозволяє вивід на ньому! Полярність -- active high, PWM1. Вона ж пам'ятає встановити для таймера TIM1 біт MOE -- Master Output Enable, без нього нічого не буде! (Про цей біт див. також тут "Далекомір HC-SR04 -- два таймери/CMSIS", після слів "TIM1 є просунутим таймером").
  • Функція initTimers() викликає дві попередні, а також дозволяє переривання від події CC3 та переповнення. 
  • Функції TIM1_CC3_output_enable() і TIM1_CC3_output_disable() використовуються в коді, щоб дозволяти чи забороняти відповідний канал.
Обробник переривання захоплення зразу і забороняє свій канал; переривання по переповненню встановлює, який же таймаут відбувся, в залежності від поточного стану програми. Їх повний текст -- див. нижче.

Програма


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

Скачати демонстраційний проект можна тут.

Наступного разу зробимо це ж, за допомогою Cube/HAL, а поки --

Дякую за увагу!



main.c -- повний текст




  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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>

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

volatile uint32_t curTicks;

#ifdef __cplusplus
extern "C" {
#endif

void SysTick_Handler(void) {
 curTicks++; /* Зроблено ще один тік */
}

#define MICROSECONDS_GRANULARITY 100
// Потрібна частота спрацювань таймера SysTick в герцах
#define FREQ ((1000000)/(MICROSECONDS_GRANULARITY))
#define TICKS ((SystemCoreClock)/(FREQ))

/*! Затримка в мікросекундах, грануляція -- microseconds_granularity мікросекунд,
 * одну не витягує сам контролер. 24МГц -- один такт це 42нс=0.042мкс */
inline static void delay_some_ms(uint32_t mks) {
 uint32_t ticks = mks / MICROSECONDS_GRANULARITY;
 int stop_ticks = ticks + curTicks;
 while (curTicks < stop_ticks) {
 };
}

//===============================================================

void enable_capture_falling() {
 EXTI->FTSR |= (EXTI_FTSR_TR6); // Falling trigger selection register
}

void enable_capture_rising() {
 EXTI->RTSR |= (EXTI_RTSR_TR6); // Rising trigger selection register
}

void disable_capture_falling() {
 EXTI->FTSR &= ~(EXTI_FTSR_TR6); // Falling trigger selection register
}

void disable_capture_rising() {
 EXTI->RTSR &= ~(EXTI_RTSR_TR6); // Rising trigger selection register
}

inline void TIM1_start() {
 // TIM1->CNT = 0; // In OPM mode update event clears it
 TIM1->CR1 |= TIM_CR1_CEN;
}

inline void TIM1_stop() {
 TIM1->CR1 &= ~TIM_CR1_CEN;
}

inline void TIM1_CC3_output_enable() {
 TIM1->CCER |= (TIM_CCER_CC3E); // Capture/Compare 3 output enable -- PA10
}

inline void TIM1_CC3_output_disable() {
 TIM1->CCER &= ~(TIM_CCER_CC3E); // Capture/Compare 3 output disable -- PA10
}
//===============================================================

const int TOO_FAR_TIMEOUT = 38000;

typedef enum state_t {
 IDLE_S,
 TRIGGERING_S,
 WAITING_FOR_ECHO_START_S,
 WAITING_FOR_ECHO_STOP_S,
 TRIG_NOT_WENT_LOW_S,
 ECHO_TIMEOUT_S,
 ECHO_NOT_WENT_LOW_S,
 READING_DATA_S,
 ERROR_S
} state_t;



volatile state_t state = IDLE_S;
volatile uint32_t measured_time;

//====================================================================
//=ISR=and=tools======================================================
//====================================================================

volatile uint16_t echo_start_ticks;
volatile uint16_t echo_finish_ticks;
void EXTI9_5_IRQHandler(void) {
 if (EXTI->PR & EXTI_PR_PR6) {
  EXTI->PR |= EXTI_PR_PR6; // This bit is cleared by writing a 1 into the bit!
  switch (state) {
  case WAITING_FOR_ECHO_START_S: {
   echo_start_ticks = TIM1->CNT;
   state = WAITING_FOR_ECHO_STOP_S;
   disable_capture_rising();
   enable_capture_falling();
   break;
  }
  case WAITING_FOR_ECHO_STOP_S: {
   echo_finish_ticks = TIM1->CNT;
   measured_time = echo_finish_ticks - echo_start_ticks;
   state = READING_DATA_S;
   disable_capture_falling();
   break;
  }
  default:
   puts("Unexpected signal on EXTI"); // Задовга операція, щоб тут залишати, якщо часто виконуватиметься
  }

 }
}

void TIM1_CC_IRQHandler(void) {
 if (TIM1->SR & TIM_SR_CC3IF) {
  TIM1->SR &= ~TIM_SR_CC3IF;
  TIM1_CC3_output_disable();
 }

}

void TIM1_UP_TIM16_IRQHandler(void) {
 if (TIM1->SR & TIM_SR_UIF) // Перевіряємо джерело
 {
  TIM1->SR &= ~TIM_SR_UIF; //Очищаємо прапорець переривання
  switch (state) {
  case WAITING_FOR_ECHO_START_S:
   state = ECHO_TIMEOUT_S;
   break;
  case WAITING_FOR_ECHO_STOP_S:
   state = ECHO_NOT_WENT_LOW_S;
   break;
  case IDLE_S:
  default:
   ;
   //puts("Unexpected status");
  }
 }
}

#ifdef __cplusplus
}
#endif

void enableClock();
void initGPIO();
void baseTimersInit();
void trigChannelInit();
void echoChannelsInit();
void initTimers();

void on_error(const char* text, bool hang) {
 GPIOC->BSRR = GPIO_BSRR_BS8; // Синім позначатимемо помилку
 puts(text);
 if (hang) {
  while (1)
   ;
 } else {
  // resetTimer();
  state = IDLE_S;
 }
}

int main(void) {

 enableClock();
 initGPIO();

 initTimers();
 echoExtiInit();

 // Перевіримо, чи лінія Echo в нулі поки ми ще не подали імпульс
 if (GPIOA->IDR & GPIO_IDR_IDR6 /* (1 << 2) */) {
  on_error("Error -- Echo line is high, though no impulse was given.",
    true);
 }

 // Ініціалізуємо таймер SysTick
 if (SysTick_Config(TICKS)) {
  on_error("Error -- SysTick failed to start.", true);
 }

 puts("Starting");

 NVIC_ClearPendingIRQ(EXTI9_5_IRQn);
 NVIC_EnableIRQ(EXTI9_5_IRQn);
 while (1) {
  TIM1_CC3_output_enable();
  TIM1_start();

  NVIC_DisableIRQ(EXTI9_5_IRQn);
  state = WAITING_FOR_ECHO_START_S;
  NVIC_EnableIRQ(EXTI9_5_IRQn);

  enable_capture_rising();
  while (TIM1->CCER & TIM_CCER_CC3E)
   // Поки вивід дозволено -- чекаємо
   ;

  if (GPIOA->IDR & GPIO_IDR_IDR10) {
   state = TRIG_NOT_WENT_LOW_S;
   puts("Trigger does not went low!");
   NVIC_DisableIRQ(EXTI9_5_IRQn);
   state = IDLE_S;
   NVIC_EnableIRQ(EXTI9_5_IRQn);
   continue;
  } else {
   while (state != READING_DATA_S && state != ECHO_TIMEOUT_S
     && state != ECHO_NOT_WENT_LOW_S);
   NVIC_DisableIRQ(EXTI9_5_IRQn);
   state_t state_copy = state;
   state = IDLE_S;
   NVIC_EnableIRQ(EXTI9_5_IRQn);
   if (GPIOA->IDR & GPIO_IDR_IDR6 || state == ECHO_NOT_WENT_LOW_S) {
    puts("Echo line does not went low!");
    if (state_copy == ECHO_NOT_WENT_LOW_S)
     puts("\tConfirmed from interrupt");
   }

   if (state_copy == ECHO_TIMEOUT_S) {
    printf("Echo timeout!\n");
   } else {
    if (measured_time > TOO_FAR_TIMEOUT) {
     printf("Tooo far -- no echo received, wating for %"PRIu32" mks\n", measured_time);
    } else
    {
     uint32_t distance_mm = (measured_time*10)/58;
     printf("Distance: %"PRIu32" mm, measured time: %"PRIu32" us\n",distance_mm, measured_time);
    }
   }
  }

  delay_some_ms(500000 / 10);
 }

}

void enableClock() {
 // Дозволяємо тактування порту C, на ньому -- світлодіоди плати STM32VLDiscovery
 // PC8 -- blue, PC9 -- green
 RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

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

 RCC->APB2ENR |= RCC_APB2Periph_TIM1; // Дозволяємо тактування TIM1

 RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; // Вмикаємо мультиплексор переривань
}

void initGPIO() {
 //! PA10 -- TIM1_CH3, TRIG
 GPIOA->CRH &= ~GPIO_CRH_CNF10_0; // 10 -- AF PP (Alternative function -- push-pull)
 GPIOA->CRH |= GPIO_CRH_CNF10_1;
 GPIOA->CRH |= GPIO_CRH_MODE10; //Встановити обидва біти MODE для піна 1 -- швидкість 50MHz

 GPIOA->CRH &= ~GPIO_CRH_CNF8_0; // 10 -- AF PP (Alternative function -- push-pull)
 GPIOA->CRH |= GPIO_CRH_CNF8_1;
 GPIOA->CRH |= GPIO_CRH_MODE8; //Встановити обидва біти MODE для піна 1 -- швидкість 50MHz

 //! PA6 -- TIM3_CH1, ECHO
 GPIOA->CRL |= GPIO_CRL_CNF6_0; // 01 -- Input floating
 GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
 GPIOA->CRL &= ~GPIO_CRL_MODE6; // Має бути 00 -- Input

 //! PA0 -- TIM2_CH1, візуальна віддаль, яскравістю LED
 GPIOA->CRL &= ~GPIO_CRL_CNF0_0; // 10 -- AF PP (Alternative function -- push-pull)
 GPIOA->CRL |= GPIO_CRL_CNF0_1;
 GPIOA->CRL |= GPIO_CRL_MODE0; //Встановити обидва біти MODE для піна 1 -- швидкість 50MHz

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

}

void baseTimersInit() {

 const int delay = 12;
 TIM1->PSC = SystemCoreClock / 1000000 - 1;
 //! По завершенню циклу таймер зупиняється, але перед тим переводить у
 //! високий стан! Тому слід використовувати PWM2
 //! І тому, якщо робити паузу хоча б +3, після імпульсу,
 //! далекомір встигав спрацювати, а якщо +1 -- частіше ні, +2 -- частіше так.
 TIM1->ARR = UINT16_MAX;
 TIM1->CCR3 = delay; // TRIG -- 12 мкс
 TIM1->CR1 &= ~TIM_CR1_DIR; // Відлік -- вверх. (Default)
 TIM1->CR1 |= TIM_CR1_OPM;
}

void trigChannelInit() {
 // Канал TRIG -- CH3
 //TIM1->CCER |= (TIM_CCER_CC3E); // Capture/Compare 3 output enable -- PA10
 TIM1->CCER &= ~TIM_CCER_CC3P; // Полярність -- active high (Default)

 TIM1->CCMR2 |= (TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3M_2); // PWM1 -- OCxM=0b110
 TIM1->CCMR2 &= ~TIM_CCMR2_OC3M_0;
 //TIM1->CCMR2 |= (TIM_CCMR2_OC3M_0 | TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3M_2); // PWM2 -- OCxM=0b111

 TIM1->CCMR2 &= ~TIM_CCMR2_CC3S; // Зануляємо -- на вивід. (Default)
 TIM1->CCMR2 |= TIM_CCMR2_OC3PE; // Preload для CCR3, вимагається Datasheet для PWM

 TIM1->CR1 |= TIM_CR1_ARPE; // Preload для ARR,  вимагається Datasheet для PWM

 TIM1->BDTR |= TIM_BDTR_MOE;
}

void echoExtiInit() {
 AFIO->EXTICR[1] |= AFIO_EXTICR2_EXTI6_PA;
 EXTI->IMR |= EXTI_IMR_MR6;
}

void initTimers() {
 baseTimersInit();
 trigChannelInit();

 TIM1->DIER |= (TIM_DIER_CC3IE | TIM_DIER_UIE);
 NVIC_EnableIRQ(TIM1_UP_TIM16_IRQn);
 NVIC_EnableIRQ(TIM1_CC_IRQn);

}


Немає коментарів:

Дописати коментар