Лого однойменної групи. Не маю ні найменшого уявлення, що у них за музика. Здавалося б, невинне словосполучення: "Two timers". Дивимося словники: TheFreeDictionary та UrbanDictionary |
- TIM1 генеруватиме імпульс заданої довжини ~ 10-15 мкс, в ролі TRIG
- TIM3 вимірюватиме тривалість імпульсу на лінії ECHO
- TIM2 -- не важливий для власне роботи, тому не врахований в заголовку, візуально показуватиме віддаль до перешкоди, за допомогою світлодіоду та PWM (ШІМ)
Апаратна конфігурація відрізняється від попередніх прикладів із використанням GPIO, так як канали таймерів прив'язані до конкретних піні (навіть із ремапінгом варіантів не так багато). Для цього прикладу вона наступна:
- TRIG -- PA10 (TIM1_CH3)
- ECHO -- PA6 (TIM3_CH1, CH2 indirect)
- Світлодіод, яскравість якого показує віддаль до перешкоди -- PA0 (TIM2_CH1)
- Світлодіоди плати, як завжди, синій на піні PB8 і зелений на PB9
Логіка роботи програми
Логіка роботи програми наступна. Після всіх необхідних ініціалізацій:
- Генерується імпульс, активуючи TIM1. Щоб зберегти контроль над моментом наступного імпульсу, таймер автоматично зупиняється -- використано режим OPM.
- Програма чекає, поки TIM1 зупиниться -- слідкуючи за бітом CEN із CR1.
- Коли TIM1 зупинився -- пора починати слухати ECHO. Тим займається TIM3, працюючи в режимі захоплення ШІМ.
- Обробники переривання TIM3 займаються наступним:
- якщо відбулося оновлення, поки чекали на початок імпульсу -- імпульс не почався, повідомляємо і пробуємо ще раз;
- якщо відбулося захоплення початку імпульсу (канал CH1) -- програма починає чекати на кінець імпульсу;
- якщо, поки чекали на кінець імпульсу, відбулося переповнення -- не дочекалися кінця, ECHO не перейшов у нуль, програма повідомляє і пробує ще раз;
- якщо, поки чекали кінець імпульсу, відбулося захоплення на каналі кінця імпульсу (CH2, який ловить спади) -- маємо повноцінний імпульс.
- Тривалість імпульсу перераховується у віддаль, виводиться результат. Пропорційно йому змінюється яскравість світлодіода, котрим керує ШІМ-канал таймера TIM2.
Для того, щоб знати, в якому стані програма зараз програма, використовується змінна state типу перерахування state_t, а для збереження виміряного інтервалу -- measured_time, обидві, звичайно, volatile:
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;
За таким підходом стоять ідеї скінчених автоматів, методика яких при програмуванні щодо контролерів гарно описана в статті DiHalt-a "AVR. Учебный курс. Конечный автомат", серії статей Владимира Татарчевского, на яку він посилається, (див. також більш складний приклад: "AVR. Учебный Курс. Использование AVR TWI для работы с шиной IIC (i2c)". Звичайно, у цій програмі даний підхід використовується дуже обмежено, але настійливо рекомендую ознайомитися -- надзвичайно корисна та потужна річ!
Програма використовує вже відому нам delay_some_us() із всією інфраструктурою та кількома простими функціями:
inline void TIM3_start() { TIM3->CR1 |= TIM_CR1_CEN; } inline void TIM3_stop() { TIM3->CR1 &= ~TIM_CR1_CEN; } inline void TIM1_start() { TIM1->CR1 |= TIM_CR1_CEN; } inline void TIM1_stop() { TIM1->CR1 &= ~TIM_CR1_CEN; } inline void set_LED_lum(uint16_t arg) { // TIM2->CCR1 = TIM2->ARR - arg; int lm = -6*arg + TIM2->ARR; TIM2->CCR1 = lm>=0 ? lm : 0; }
Щодо set_LED_lum() -- вона зроблена так, щоб розтягнути діапазон яскравості світлодіода на віддалях від 0 до одного метра -- так зручніше тестувати. Хоча, все рівно, результат не дуже наочний -- треба підібрати хитрішу функцію...
Функція on_error() служить обробником помилкових ситуацій:
А константа:
задає тривалість імпульсу, після якої вважається, що далекомір відповів "безмежна віддаль або відсутня луна".
На початку main() відбувається ініціалізація, подробиці якої ми розглянемо пізніше:
Потім запускаємо таймер -- TIM3 працюватиме постійно:
Взагалі, запуск -- це "TIM3_start();", попередніх два рядки -- спроба позбутися переривання переповнення, яке прилітає зразу після запуску таймера. До свого сорому, так і не зрозумів, звідки воно береться.
Далі починається звичний нескінчений цикл:
І починаємо чекати на луну:
Щоб в процесі маніпуляцій із state не вклинилося переривання, забороняємо їх перед (викликом NVIC_DisableIRQ()) та дозволяємо після зміни state.
Перш ніж вона може з'явитися, TIM1 має зупинитися. Тому, (замість більш загального, але і незрівнянно громіздкішого підходу -- в обробнику переривань встановлювати прапорець), чекаємо, поки його біт CEN не занулиться:
Перевіряємо, чи лінія TRIG перейшла в нуль (це дозволяє виловити ряд помилок, див. подробиці вибору режиму PWM2 для TIM1 нижче, в розмові про ініціалізацію), якщо так -- чекаємо, поки переривання від TIM3 прийде до того чи іншого висновку:
Після чого залишається вивести результат, погасити зелений світлодіод, почекати трішки (від вимірів, які йдуть один за одним, мене значно швидше починають нити ясна :-), і повернутися на початок циклу:
Решту цікавого відбувається в обробнику переривань:
Як і обіцяли:
Функція on_error() служить обробником помилкових ситуацій:
void on_error(const char* text, bool hang) { GPIOC->BSRR = GPIO_BSRR_BS8; // Синім позначатимемо помилку puts(text); if(hang) { while(1); } else { state = IDLE_S; } }
А константа:
const int TOO_FAR_TIMEOUT = 38000;
задає тривалість імпульсу, після якої вважається, що далекомір відповів "безмежна віддаль або відсутня луна".
На початку main() відбувається ініціалізація, подробиці якої ми розглянемо пізніше:
int main(void) { enableClock(); initGPIO(); initTimers(); // Перевіримо, чи лінія 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); }
Потім запускаємо таймер -- TIM3 працюватиме постійно:
puts("Starting"); TIM3->SR = 0; NVIC_ClearPendingIRQ(TIM3_IRQn); TIM3_start();
Взагалі, запуск -- це "TIM3_start();", попередніх два рядки -- спроба позбутися переривання переповнення, яке прилітає зразу після запуску таймера. До свого сорому, так і не зрозумів, звідки воно береться.
Далі починається звичний нескінчений цикл:
while(1) { GPIOC->BSRR = GPIO_BSRR_BS9; // Вмикаємо зелений світлодіод на початку виміру TIM3->CNT = 0; TIM1_start();
- Вмикаємо зелений світлодіод, який показує, що відбуваються виміри,
- зануляємо лічильник CNT таймера TIM3 -- так як останній неперервно рахує, а ми покладаємося на подію переповнення, щоб розпізнати таймаут, без цього можуть бути сюрпризи,
- запускаємо TIM1, котрий згенерує на TRIG потрібний імпульс і вимкнеться.
І починаємо чекати на луну:
NVIC_DisableIRQ(TIM3_IRQn); state = WAITING_FOR_ECHO_START_S; NVIC_EnableIRQ(TIM3_IRQn);
Щоб в процесі маніпуляцій із state не вклинилося переривання, забороняємо їх перед (викликом NVIC_DisableIRQ()) та дозволяємо після зміни state.
Перш ніж вона може з'явитися, TIM1 має зупинитися. Тому, (замість більш загального, але і незрівнянно громіздкішого підходу -- в обробнику переривань встановлювати прапорець), чекаємо, поки його біт CEN не занулиться:
while(TIM1->CR1 & TIM_CR1_CEN );
Перевіряємо, чи лінія TRIG перейшла в нуль (це дозволяє виловити ряд помилок, див. подробиці вибору режиму PWM2 для TIM1 нижче, в розмові про ініціалізацію), якщо так -- чекаємо, поки переривання від TIM3 прийде до того чи іншого висновку:
if( GPIOA->IDR & GPIO_IDR_IDR10 ) { state = TRIG_NOT_WENT_LOW_S; puts("Trigger does not went low!"); NVIC_DisableIRQ(TIM3_IRQn); state = IDLE_S; NVIC_EnableIRQ(TIM3_IRQn); continue; } else { while(state != READING_DATA_S && state != ECHO_TIMEOUT_S && state != ECHO_NOT_WENT_LOW_S );
Після чого залишається вивести результат, погасити зелений світлодіод, почекати трішки (від вимірів, які йдуть один за одним, мене значно швидше починають нити ясна :-), і повернутися на початок циклу:
NVIC_DisableIRQ(TIM3_IRQn); state_t state_copy = state; state = IDLE_S; NVIC_EnableIRQ(TIM3_IRQn); if( GPIOA->IDR & GPIO_IDR_IDR6 ) { 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"); set_LED_lum(TOO_FAR_TIMEOUT); } else { set_LED_lum(measured_time); 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); } } } GPIOC->BSRR = GPIO_BSRR_BR9; // Гасимо зелений, завершивши вимір delay_some_us(500000/10); }
Решту цікавого відбувається в обробнику переривань:
void TIM3_IRQHandler(void) { if( TIM3->SR & TIM_SR_CC1IF ) { TIM3->SR &= ~TIM_SR_CC1IF; TIM3->SR &= ~TIM_SR_UIF; if(state == WAITING_FOR_ECHO_START_S) state = WAITING_FOR_ECHO_STOP_S; } if( TIM3->SR & TIM_SR_CC2IF ) { TIM3->SR &= ~TIM_SR_CC2IF; // Очищається при читанні CCR2, але ми його // не кожен раз читаємо. Простіше зробити вручну, ніж // else всілякі дописувати if( state == WAITING_FOR_ECHO_STOP_S ) { measured_time = TIM3->CCR2; state = READING_DATA_S; } } if( TIM3->SR & TIM_SR_UIF ) { TIM3->SR &= ~TIM_SR_UIF; if(state == WAITING_FOR_ECHO_START_S) { state = ECHO_TIMEOUT_S; } if(state == WAITING_FOR_ECHO_STOP_S) { state = ECHO_NOT_WENT_LOW_S; } } }
Як і обіцяли:
- Якщо є захоплення на каналі CH1, і ми чекаємо на імпульс (state == WAITING_FOR_ECHO_START_S), починаємо чекати на його завершення (state = WAITING_FOR_ECHO_STOP_S). Так як в цьому місці прилітає і подія оновлення, очищаємо відповідний прапорець -- ця подія нам зараз не потрібна.
- Якщо є захоплення на другому каналі -- по спаду сигналу, і в той час ми на нього чекали (state == WAITING_FOR_ECHO_STOP_S), зберігаємо результат, вміст CCR2 і переходимо в стан виведення даних (state = READING_DATA_S).
- Якщо прийшла подія оновлення, поки ми чекали на початок імпульсу -- повідомляємо решту програми, що імпульс не почався, а якщо коли чекали на кінець -- що не закінчився. Інші оновлення ігноруємо -- таймер ж крутиться неперервно.
Ініціалізація
Першою в коді йде ініціалізація:
- Ввімкнути тактування використаних портів, (A та C в нашому випадку), та таймерів -- TIM1/2/3.
- Налаштувати піни належним чином -- кого на ввід, кого на вивід, керований таймером -- "AF" (альтернативні функції).
- Налаштувати таймери -- задати подільники, напрямок відліку, тощо.
- Налаштувати канали таймерів -- PWM/Capture compare для того, що керуватиме TRIG, пару каналів TIM3 -- на захоплення PWM, PWM для таймера візуалізації, TIM2.
enableClock(); initGPIO(); initTimers(); // Перевіримо, чи лінія 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");
Вмикаємо тактування (enableClock()), ініціалізуємо піни та порти (initGPIO()), ініціалізуємо таймери (initTimers()), перевіряємо, чи немає на ECHO чогось зайвого (перестраховка, безперечно, але кілька раз допомагала виявити неправильно зібрану схему чи невірний код), запускаємо SysTick -- він використовується для пауз між циклами вимірів, повідомляємо, що готові до роботи.
void enableClock() { // Світлодіоди плати STM32VLDiscovery: PC8 -- blue, PC9 -- green RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Дозволяємо тактування порту C RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // Дозволяємо тактування порту A RCC->APB2ENR |= RCC_APB2Periph_TIM1; // Дозволяємо тактування TIM1 RCC->APB1ENR |= RCC_APB1Periph_TIM2; // Дозволяємо тактування TIM2 RCC->APB1ENR |= RCC_APB1Periph_TIM3; // Дозволяємо тактування TIM3 }
Налаштування пінів теж тривіальне (звичайно, код нижче можна зробити ефективнішим та елегантнішим -- об'єднавши встановлення різних бітів тих же регістрів, але з педагогічних міркувань залишено саме так):
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 //! 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; }
Ініціалізація таймерів
Нагадаємо, задача TIM1 -- згенерувати імпульс, тривалістю ~12 мкс, задача TIM3 -- виміряти тривалість імпульсу, яка знаходитиметься між десятками мікросекунд та 38 мілісекунд.Для простоти, щоб не возитися із переведенням часу із "папуг" в секунди, знаючи тактову частоту мікроконтролера -- 24МГц, беремо подільник рівний 24-1, при тому відлік відбуватиметься кожну мікросекунду. TIM1, завершивши імпульс, зупиняється -- користуємося бітом OPM -- час наступного імпульсу нам би хотілося визначати більш гнучко, програмними засобами, а не просто механічно гнати вимір за виміром.
TIM2 генеруватиме ШІМ для візуального представлення віддалі, то подільник беремо рівним 0 -- відлік відбуватиметься на частоті ядра.
Щодо TIM1 -- якщо скористатися очевидним підходом, PWM1 + OPM, із малою різницею між CCR3 i ARR -- тривалістю імпульсу, і, в даному випадку -- віддаллю до наступного, буде сюрприз:
Один імпульс в режимі PWM1 |
Таймер зупиниться, дорахувавши до ARR -- у відповідь на подію оновлення. Однак, канал, на завершення, перейде у активний стан! Якщо різниця між ARR i CCR3 буде достатньо великою, далекомір таки спрацює, (3 мікросекунди, як показали експерименти, вистачає гарантовано, 2 - майже вистачає, іноді навіть для однієї спрацьовує), і він цілком терпимий до того, що поки випромінюється ультразвук та слухається його луна, на TRIG буде логічна одиничка. Однак, це "не діло" -- не варто так нехтувати вимогами документації на далекомір. Тому беремо режим PWM2:
Один імпульс в режимі PWM2 |
Вся ця загальна ініціалізація таймерів зібрана в функції baseTimersInit():
void baseTimersInit() { TIM3->PSC = SystemCoreClock/1000000 - 1; // Подільник частоти -- 1 тік на мікросекунду TIM3->ARR = UINT16_MAX; // Максимальна тривалість імпульса від таймера -- 38 000 мкс // Тому, як відбулося переповнення -- ми точно не дочекалися, // бо пройшло ~65 000 мкс TIM3->CR1 &= ~TIM_CR1_DIR; // Відлік -- вверх. (Default) const int delay = 12; TIM1->PSC = SystemCoreClock/1000000 - 1; TIM1->ARR = delay+1; // TRIG -- 12 мкс TIM1->CCR3 = 1; TIM1->CR1 &= ~TIM_CR1_DIR// Відлік -- вверх. (Default) TIM1->CR1 |= TIM_CR1_OPM;
TIM1->BDTR |= TIM_BDTR_MOE; //====================================== //! Візуальне відображення виміряної віддалі TIM2->PSC = 0; TIM2->ARR = TOO_FAR_TIMEOUT; TIM2->CCR1 = 0; TIM2->CR1 &= ~TIM_CR1_DIR; // Відлік -- вверх. (Default) TIM2->CR1 |= TIM_CR1_CEN; }
Важливе зауваження. TIM1 є просунутим таймером, і підтримує функцію break, "аварійну зупинку". Тому поки не встановлено біт MOE -- Master Output Enabled, із регістра BDTR -- Break and Dead-Time Register, нічого подавати на свої виводи він не буде. Лічильник рахуватиме, канали перехоплюватимуть (CC --- Capture Compare), переривання прилітатимуть. І тільки на виводах буде 0. Іншими словами, якщо взяти той самий код для TIM2 чи TIM3, все працюватиме, а для TIM1 -- ні, якщо не додати рядка "TIM1->BDTR |= TIM_BDTR_MOE".
Зауваження: Можливо, зручнішим було б тут просто використати відцентрований ШІМ: Center-aligned PWM, у якому вивід виглядає так:
Зауваження: Можливо, зручнішим було б тут просто використати відцентрований ШІМ: Center-aligned PWM, у якому вивід виглядає так:
Ініціалізація TIM1_CH3, який відповідає за TRIG
Тут все знайоме:
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_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 }
Варто звернути увагу хіба на те, що канал -- третій, тому використовується CCMR2, а не CCMR1, як в інших прикладах.
Ініціалізація каналів TIM3_CH1/2, які ловлять ECHO
Тут теж все знайоме по посту, присвяченому захопленню ШІМ:
void echoChannelsInit() { // Канали ECHO -- PWM capture, PA6 TIM3->CCMR1 &= ~ TIM_CCMR1_IC1PSC; // Prescaler disabled TIM3->CCMR1 &= ~ TIM_CCMR1_IC2PSC; // Prescaler disabled TIM3->CCMR1 |= TIM_CCMR1_IC1F; // Filter -- по максимуму TIM3->CCMR1 |= TIM_CCMR1_IC2F; // Filter -- по максимуму TIM3->CCMR1 &= ~ TIM_CCMR1_CC1S_1; // CCR1 <- TI1 (01) TIM3->CCMR1 |= TIM_CCMR1_CC1S_0; TIM3->CCER &= ~ TIM_CCER_CC1P; // Rising edge TIM3->CCMR1 |= TIM_CCMR1_CC2S_1; // CCR2 <- TI1 (10) TIM3->CCMR1 &= ~ TIM_CCMR1_CC2S_0; TIM3->CCER |= TIM_CCER_CC2P; // Falling edge // Trigger input -- TI1FP1, TS=101b TIM3->SMCR &= ~ TIM_SMCR_TS_1; TIM3->SMCR |= ( TIM_SMCR_TS_2 | TIM_SMCR_TS_0 ); // SMS 100: reset mode TIM3->SMCR &= ~ ( TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0 ); TIM3->SMCR |= TIM_SMCR_SMS_2; TIM3->CCER |= ( TIM_CCER_CC1E | TIM_CCER_CC2E ); // Enable capture TIM3->DIER |= ( TIM_DIER_CC1IE | TIM_DIER_CC2IE ); // Enable interrupt on capture }
Дозволяємо переривання по захопленню -- вони нам знадобляться.
Ініціалізація "декоративного" TIM2_CH1
Тут -- банальний ШІМ:
void initLedPWMChannel() { TIM2->CCER |= (TIM_CCER_CC1E); // Capture/Compare 1 output enable -- PA0 TIM2->CCER &= ~TIM_CCER_CC1P; // Полярність -- active high (Default) TIM2->CCMR1|=(TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2); // PWM1 -- OCxM=0b110 TIM2->CCMR1&=~TIM_CCMR1_OC1M_0; TIM2->CCMR1&= ~TIM_CCMR1_CC1S; // Зануляємо -- на вивід. (Default) TIM2->CR1 |= TIM_CR1_ARPE; // Preload для ARR, вимагається Datasheet для PWM }
Функція повної ініціалізації таймерів
Попередніх три функції викликаються initTimers(), яка, завершивши ініціалізацію, дозволяє переривання від TIM3:
void initTimers() { baseTimersInit(); trigChannelInit(); echoChannelsInit(); initLedPWMChannel(); TIM3->DIER |= TIM_DIER_UIE; // При запуску любить зразу переривання прилетіти... TIM3->SR = 0; NVIC_ClearPendingIRQ(TIM3_IRQn); NVIC_EnableIRQ(TIM3_IRQn); }
З ініціалізацією теж розібралися.
Завершуючи
Скачати проект можна тут.Повний текст main.c, не наводжу його тут через громіздкість. Щодо нього є ще одне зауваження -- у ньому є ряд макросів виду PUT_TO_TRACE_NO_IRQ(), які по замовчуванню (це контролюється макросом USE_TRACE) не роблять нічого. Вони використовуються для відстежування роботи програми, коли інтерактивно це робити не можливо -- далекомір не чекатиме, поки ми в дебаггері зупинилися. Зараз просто ігноруйте ці виклики, про них буде окрема розмова.
Зауваження. Програма стала значно громіздкішою, ніж коли ми користувалися GPIO. Це -- плата за більшу гнучкість. Для такої задачі, як вона вирішує, немає потреби робити так складно. Однак, якщо контролер, крім опитування далекоміра, матиме ще якісь задачі -- використання підходу GPIO різко ускладниться, а розглянутий тут підхід (особливо якщо пам'ятати про скінчені автомати), масштабується дуже добре.
На цьому все, наступного разу спробуємо зробити те ж, засобами HAL/STM32CubeMX.
Немає коментарів:
Дописати коментар