Timer STM32 + Rust: Interrupt untuk Mengukur Lebar Pulsa Lebih Lama
Pada artikel sebelumnya kita sudah membahas mengenai cara mengukur lebar pulsa dengan menggunkan fitur input capture. Seperti yang dijelaskan sebelumnya, bahwa cara sebelumnya masih memiliki kekurangan yaitu ada batas maksimal durasi waktu (lebar pulsa) yang dapat diukur sesuai dengan persamaan berikut:
Keterangan:
- : Lebar pulsa (durasi)
- : Selisih nilai tick (register capture/compare) akhir dengan awal.
- : Frekuensi timer yang digunakan.
Pada artikel sebelumnya kita mengatur frekuensi timer ke 10 KHz (10000 Hz) dengan nilai register Auto-Reload ke nilai maksimal yaitu 65535. Sehingga lebar pulsa maksimal yang dapat diukur adalah 6553.5 ms (6.5535 detik). Jika kita menekan push button lebih dari 6553.5 ms, maka lebar pulsa yang terdeteksi menjadi tidak akurat karena nilai register Counter (TIMx_CNT) akan berulang dari 0 setiap kali melewati nilai register Auto-Reload (TIMx_ARR). Sehingga jumlah total tick push button ditekan menjadi tidak sesuai.
Untuk mengatasi hal tersebut, kita harus mendapatkan jumlah total tick yang benar selama push button ditekan. Disini kita akan menghitung jumlah berapa kali timer mengalami overflow (nilai register Counter melebihi nilai register Auto-Reload). Kemudian nilai tersebut kita kalikan dengan 65536 ( ) setelah itu ditambahkan dengan selisih nilai tick akhir dengan tick awal. Kita bisa menghitung timer overflow dengan memanfaatkan fitur interrupt pada timer STM32F103C8. Berikut merupakan persamaan untuk mencari total tick:
Keterangan:
- : Jumlah total tick dari awal hingga akhir.
- : Banyaknya overflow yang terjadi.
- : Selisih nilai register Capture/Compare akhir dan awal.
Selanjutnya dengan menggunakan kita dapat menghitung lebar pulsa dengan persamaan sebagai berikut:
Pengenalan Interrupt pada Timer STM32F103C8 (Blue Pill)
Interrupt merupakan fitur dari mikrokontroler STM32, yang memungkinkan CPU untuk mengeksekusi perintah yang ditentukan (Interrupt Service Routine) diselah-selah ketika mengeksekusi program utama. Berikut merupakan Event pada Timer STM32F103C8 yang dapat memicu terjadinya interrupt:
- Update event: Ketika nilai register counter mengalami overflow (melewati nilai register Auto-Reload yang ditentukan) atau ketika nilai register counter mencapai 0.
- Capture/Compare: pada channel yang dikonfigurasi sebagai input event Capture/Compare akan aktif ketika ada perubahan tegangan pada pin yang dihubungkan ke channel yang dipilih. Sedangkan jika channel dikonfigurasi sebagai output, maka event Capture/Compare akan aktif ketika nilai register counter sama dengan nilai register Capture/Compare (
TIMx_CCR) yang ditentukan. Ada 4 event Capture/Compare untuk masing-masing channel.
Persiapan Hardware
Komponen-komponen yang kita digunakan pada artikel ini sama dengan komponen-komponen pada artikel sebelumnya: Mikrokontroler STM32F103C8, ST-Link USB Downloader/Debuger, Breadboard, Push Button, kabel jumper (male to male dan female to female). Penjelasan lebih detail mengenai fungsi komponen-komponen yang digunakan dapat dilihat pada halaman Setup STM32F103C8 dengan Rust dan halaman Menggunakan GPIO STM32F103C8 dengan Rust.
Menggunakan Interrupt pada Timer STM32F103C8 (Blue Pill) dengan Rust
Pada artikel kali ini kita akan menggunakan interrupt pada Timer STM32F103C8 untuk menghitung banyaknya kejadian overflow pada Timer. Sesuai dengan penjelasan diatas kita akan menggunakan event update untuk melakukan hal tersebut. Setiap terjadi interrupt akibat dari event update kita akan menghitung kejadian tersebut. Pada tutorial ini kita masih akan menggunakan Timer 2 dengan channel 1 sebagai input capture.
Sebelum lanjut ke bagian program, kita terlebih dahulu membuat rangkaian pada breadboard sesuai dengan gambar skematik berikut:

Skematik tersebut sama seperti yang digunakan pada artikel sebelumnya, silakan baca artikel tersebut untuk penjelasan skematik yang lebih detail.
Pemrograman Interrupt Timer STM32F103C8 sebagai Input Capture dengan Rust
Pertama mari buat project Rust baru sesuai dengan yang dijelaskan pada artikel ini. Buka file Cargo.toml kemudian definisikan binary executable baru dengan nama timer-interrupt :
1[[bin]]
2name = "timer-interrupt"
3path = "src/main.rs"
4test = false
5bench = falseSelanjutnya buka file src/main.rs, lalu isi dengan kode berikut:
Rust
1#![no_std]
2#![no_main]
3
4use core::sync::atomic::{AtomicU32, Ordering};
5
6use defmt_rtt as _;
7use panic_probe as _;
8
9use cortex_m_rt::entry;
10use stm32f1xx_hal::{
11 flash::FlashExt,
12 pac::{self, interrupt},
13 prelude::*,
14 rcc::{Config, RccExt},
15 time::Hertz,
16 timer::{Event, Timer},
17};Seperti biasa kita memberitahu kompiler Rust untuk tidak menggunakan standard library dan tidak menjalankan program diatas sistem operasi.
Kita juga mendefinisikan library yang akan digunakan dalam project:
- library
atomicdigunakan untuk mendefinisikan variabel global agar dapat diakses dari bloklooputama dan dapat diakses dari fungsiinterrupt. - Library/crate
cortex_m_rtuntuk menentukan fungsi entry program akan mulai berjalan dan menangani proses startup program. defmt_rttberfungsi untuk mengirimkan data ke PC/laptop untuk logging menggunakan protokol real time transfer.- Library
stm32f1xx_halberfungsi agar kita dapat mengakses periferal mikrokontoler STM32F103C8 secara aman. panic_probedigunakan untuk menangani jika terjadi runtime error, dan akan otomatis mengirimkan log eror yang terjadi ke host PC/laptop.
Rust
1static NUMB_OVERFLOW: AtomicU32 = AtomicU32::new(0);Membuat variabel global dengan nama NUMB_OVERFLOW untuk menyimpan jumlah timer mengalami overflow. Disini kami menggunakan tipe variabel AtomicU32 agar variabel dapat diakses baik dari blok loop utama maupun dari blok fungsi interrupt (Interrupt Service Routine/ISR).
Rust
1defmt::println!("STM32F103C8 Timer Interrupt with Input Capture");Mengirim pesan ke PC/laptop untuk menandai sebagai program timer dengan intterupt.
Rust
1let dp = pac::Peripherals::take().unwrap();
2
3let mut flash = dp.FLASH.constrain();
4
5let rcc = dp.RCC.constrain();
6
7let clock_config = Config::default()
8 .use_hse(Hertz::MHz(8))
9 .sysclk(Hertz::MHz(72))
10 .hclk(Hertz::MHz(72))
11 .pclk1(Hertz::MHz(36));
12
13let mut clocks = rcc.freeze(clock_config, &mut flash.acr);Sama seperti artikel sebelumnya kita mengonfigurasi clock untuk menggunakan clock external sebesar 8 MHz, kemudian menggunakan clock System sebesar 72 MHz, clock Advanced-High Bus Periferal sebesar 72 MHz, dan clock Advanced-Periferal Bus 1 sebesar 36 MHz. Sehingga frekuensi clock Timer 2 akan menjadi 72 MHz.
Rust
1let mut gpioa = dp.GPIOA.split(&mut clocks);
2
3let channel_1 = gpioa.pa0.into_pull_down_input(&mut gpioa.crl);Mengakses periferal GPIO Port A (GPIOA) dan mengonfigurasi pin PA0 sebagai input pull-down. Tahap selanjutnya pin PA0 akan kita hubungkan ke channel 1 Timer 2.
Rust
1let timer2 = Timer::new(dp.TIM2, &mut clocks);
2unsafe {
3 pac::NVIC::unmask(interrupt::TIM2);
4}Mengakses periferal Timer 2 dan mengaktifkan interrupt untuk Timer 2 pada NVIC (Nested Vectored Interrupt Controller).
Rust
1let mut counter = timer2.counter_hz();Mengaktifkan counter pada Timer 2.
Rust
1counter.listen(Event::Update);Mengaktifkan interrupt untuk event update pada Timer 2 STM32F103C8.
Rust
1let timer_register = unsafe { &*pac::TIM2::ptr() };Mengakses pointer register Timer 2 STM32F103C8 secara langsung.
Rust
1timer_register.ccmr1_input().modify(|_, w| w.cc1s().ti1());Mengonfigurasi channel 1 pada Timer 2 sebagai input dan menghubungkan channel 1 Timer 2 ke pin PA0.
Rust
1timer_register
2 .ccmr1_input()
3 .modify(|_, w| w.ic1f().fck_int_n8());Mengaktifkan filter digital untuk channel 1 pada Timer 2 untuk mengurangi debounce.
Rust
1timer_register.ccer().modify(|_, w| w.cc1e().set_bit());Mengaktifkan input capture channel 1 pada Timer 2.
Rust
1 timer_register.ccer().modify(|_, w| w.cc1p().clear_bit());Mengonfigurasi agar channel 1 pada Timer 2 mendeteksi Rising edge.
Rust
1 counter.start_raw(7199, 65535);Menjalankan Timer 2 dengan prescaler 7199 dan nilai register Auto-Reload sebesar 65535.
Rust
1let mut start_press_tick = 0u16;
2
3let mut is_pressed = false;Membuat variabel untuk menyimpan nilai tick saat push button mulai ditekan dan menyimpan status kondisi push button.
Rust
1loop {
2 if counter.get_interrupt().contains(Event::C1) {
3 // Capture channel 1 interupt
4 }
5}Di dalam blok loop kita mengecek event capture/compare pada channel 1. Jika ada event capture/compare pada channel 1 maka CPU akan mengeksekusi kode dibawah. Berbeda dengan artikel sebelumnya, dimana kita membaca langsung register TIM2_SR bit CC1IF, pada tutorial ini kita memanfaatkan method get_interrupt yang disediakan oleh crate stm32f1xx_hal. Keduanya memiliki fungsi yang sama.
Jika terjadi event capture/compare pada channel 1 maka program berikut akan dieksekusi:
Rust
1let captured_tick = timer_register.ccr1().read().ccr().bits() as u16;
2
3let pin_is_high = channel_1.is_high();
4
5if pin_is_high && !is_pressed {
6 start_press_tick = captured_tick;
7 NUMB_OVERFLOW.store(0, Ordering::SeqCst);
8 timer_register.ccer().modify(|_, w| w.cc1p().set_bit());
9
10 is_pressed = true;
11 defmt::println!("--- Button Pressed ---");
12 defmt::println!("start: {}", start_press_tick);
13} else if !pin_is_high && is_pressed {
14 // ELSE
15}
16
17counter.clear_interrupt(Event::C1);Jika terjadi event capture/compare maka ambil nilai pada register Capture/Compare Timer 2. Selanjutnya lihat status dari pin PA0. Jika pin PA0 bernilai HIGH dan nilai variabel is_pressed false (terjadi Rising edge), maka update nilai variable start_press_tick dengan nilai tick yang sudah diambil, kemudian reset nilai variabel global NUMB_OVERFLOW ke 0, selanjutnya konfigurasi channel 1 untuk mengecek Falling edge, dan mengubah nilai variabe is_pressed ke true. Jangan lupa untuk membersihkan capture/compare interrupt flag channel 1 meskipun biasanya secara otomatis dibersihkan.
Ketika capture/compare channel 1 mendeteksi Falling edge, maka program berikut akan dieksekusi:
Rust
1timer_register.ccer().modify(|_, w| w.cc1p().clear_bit());
2
3let total_overflow = NUMB_OVERFLOW.load(Ordering::SeqCst);
4
5let total_tick =
6 (total_overflow * 65536) + captured_tick as u32 - start_press_tick as u32;
7
8defmt::println!(
9 "end: {} | total_overflow: {}",
10 captured_tick,
11 total_overflow
12);
13defmt::println!("press long: {} ms", total_tick as f32 / 10f32);
14defmt::println!("----------------------");
15is_pressed = false;Jika pin PA0 bernilai LOW dan nilai variabel is_pressed true (terjadi Falling edge), maka kita akan menghitung total tick selama push buttong ditekan. Selanjutnya menggunakan total tick untuk menghitung durasi push button ditekan, dan mengubah nilai is_pressed ke false. Disini juga kita mengubah kembali mode input capture channel 1 menjadi rising edge.
Selanjutnya jangan lupa untuk membuat fungsi (Interrupt Service Routine/ISR) yang akan dieksekusi CPU ketika terjadi interrupt pada Timer 2, berikut merupakan fungsi interrupt yang akan kita gunakan.
Rust
1
2#[interrupt]
3fn TIM2() {
4 let timer_register = unsafe { &*pac::TIM2::ptr() };
5
6 // check update interrupt flag
7 if timer_register.sr().read().uif().bit_is_set() {
8 // Increment overflow count
9 NUMB_OVERFLOW.fetch_add(1, Ordering::SeqCst);
10 // Clear update interrupt flag
11 timer_register.sr().modify(|_, w| w.uif().clear_bit());
12 }
13}Membuat fungsi interrupt (ISR) untuk Timer 2. Di dalam fungsi tersebut kita mengecek update interrupt flag. Jika interrupt disebabkan oleh event update maka nilai global variabel NUMB_OVERFLOW akan kita tambah 1, dan selanjutnya membersihkan bit update interrupt flag.
1#![no_std]
2#![no_main]
3
4use core::sync::atomic::{AtomicU32, Ordering};
5
6use defmt_rtt as _;
7use panic_probe as _;
8
9use cortex_m_rt::entry;
10use stm32f1xx_hal::{
11 flash::FlashExt,
12 pac::{self, interrupt},
13 prelude::*,
14 rcc::{Config, RccExt},
15 time::Hertz,
16 timer::{Event, Timer},
17};
18
19static NUMB_OVERFLOW: AtomicU32 = AtomicU32::new(0);
20
21#[entry]
22fn main() -> ! {
23 defmt::println!("STM32F103C8 Timer Interrupt with Input Capture");
24
25 let dp = pac::Peripherals::take().unwrap();
26
27 let mut flash = dp.FLASH.constrain();
28
29 let rcc = dp.RCC.constrain();
30
31 let clock_config = Config::default()
32 .use_hse(Hertz::MHz(8))
33 .sysclk(Hertz::MHz(72))
34 .hclk(Hertz::MHz(72))
35 .pclk1(Hertz::MHz(36));
36
37 let mut clocks = rcc.freeze(clock_config, &mut flash.acr);
38
39 let mut gpioa = dp.GPIOA.split(&mut clocks);
40
41 let channel_1 = gpioa.pa0.into_pull_down_input(&mut gpioa.crl);
42
43 let timer2 = Timer::new(dp.TIM2, &mut clocks);
44
45 // mengaktifkan listen update overflow
46 // timer_register.dier.modify(|_, w| w.uie().set_bit());
47
48 unsafe {
49 pac::NVIC::unmask(interrupt::TIM2);
50 }
51
52 let mut counter = timer2.counter_hz();
53
54 counter.listen(Event::Update);
55
56 let timer_register = unsafe { &*pac::TIM2::ptr() };
57
58 // 00 output
59 // 01 input
60 // 10 Input, (Cross-mapping channel 2).
61 timer_register.ccmr1_input().modify(|_, w| w.cc1s().ti1());
62
63 timer_register
64 .ccmr1_input()
65 .modify(|_, w| w.ic1f().fck_int_n8());
66
67 timer_register.ccer().modify(|_, w| w.cc1e().set_bit());
68
69 timer_register.ccer().modify(|_, w| w.cc1p().clear_bit());
70
71 counter.start_raw(7199, 65535);
72
73 let mut start_press_tick = 0u16;
74
75 let mut is_pressed = false;
76
77 loop {
78 if counter.get_interrupt().contains(Event::C1) {
79 let captured_tick = timer_register.ccr1().read().ccr().bits() as u16;
80
81 let pin_is_high = channel_1.is_high();
82
83 if pin_is_high && !is_pressed {
84 start_press_tick = captured_tick;
85 NUMB_OVERFLOW.store(0, Ordering::SeqCst);
86 timer_register.ccer().modify(|_, w| w.cc1p().set_bit());
87
88 is_pressed = true;
89 defmt::println!("--- Button Pressed ---");
90 defmt::println!("start: {}", start_press_tick);
91 } else if !pin_is_high && is_pressed {
92 timer_register.ccer().modify(|_, w| w.cc1p().clear_bit());
93
94 let total_overflow = NUMB_OVERFLOW.load(Ordering::SeqCst);
95
96 let total_tick =
97 (total_overflow * 65536) + captured_tick as u32 - start_press_tick as u32;
98
99 defmt::println!(
100 "end: {} | total_overflow: {}",
101 captured_tick,
102 total_overflow
103 );
104 defmt::println!("press long: {} ms", total_tick as f32 / 10f32);
105 defmt::println!("----------------------");
106 is_pressed = false;
107 }
108
109 counter.clear_interrupt(Event::C1);
110 }
111 }
112}
113
114#[interrupt]
115fn TIM2() {
116 let timer_register = unsafe { &*pac::TIM2::ptr() };
117
118 // mengecek apakah interrupt karena update interrupt flag (overflow)
119 if timer_register.sr().read().uif().bit_is_set() {
120 // Increment overflow count setiap kali timer melewati 65535
121 NUMB_OVERFLOW.fetch_add(1, Ordering::SeqCst);
122 // Clear update interrupt flag
123 timer_register.sr().modify(|_, w| w.uif().clear_bit());
124 }
125}Dengan menggunakan ST-Link USB Downloader dan Debuger hubungkan Board Blue Pill (mikrokontroler STM32F103C8) dengan PC/laptop seperti yang dijelaskan pada artikel ini. Setelah itu jalan kan program dengan perintah cargo run --bin timer-interrupt pada terminal VSCode. Berikut merupakan hasil ketika program dijalankan.
Pada hasil tersebut dapat kita lihat bahwa meskipun push button ditekan lebih dari 6553.5 ms, durasi yang diukur tetap akurat.
Source Code
Source code yang digunakan pada artikel ini dapat diakses di repositori Github
Jika anda mengalami kendala dalam mengikuti tutorial ini jangan ragu untuk menghubungi kami melalui halaman kontak