Timer STM32 + Rust: Membaca 1 Pin GPIO Menggunakan 2 channel
Pada artikel sebelumnya (input capture dan timer interrupt) kita sudah membahas mengenai cara mengukur lebar pulsa dengan menggunakan fitur input capture menggunakan channel 1 pada Timer 2. Karena Timer 2 STM32F103C8 tidak mendukung input capture dengan mode both edge, maka pada artikel tersebut kita mengubah mode input capture pada channel 1 dari rising edge ke falling edge dan sebaliknya secara terus-menerus.
Jika input pada pin GPIO diberikan sinyal dengan frekuensi tinggi (event capture/compare terjadi dengan cepat), maka pembacaan lebar pulsa akan menjadi tidak akurat karena CPU membutuhkan waktu untuk merubah nilai register Capture/Compare Polarity (CC1P) channel 1 (terjadi blocking). Untuk mengatasi hal tersebut, kita akan menggunakan fitur channel cross-mapping (channel input multiplexing) pada timer STM32F103C8.
Pengenalan Channel Cross-Mapping pada Timer STM32F103C8 (Blue Pill)
Channel cross-mapping atau Channel Input Multiplexing merupakan fitur pada periferal Timer STM32 yang memungkinkan 2 atau lebih channel Timer memantau 1 pin GPIO yang sama. Pada mikrokontroler STM32F103C8 channel cross-mapping hanya dapat dilakukan pada channel yang berpasangan seperti channel 1 dengan channel 2 dan channel 3 dengan channel 4. Sehingga channel cross-mapping untuk channel 1 atau channel 2 dengan channel 3 atau channel 4 tidak dapat dilakukan.
Jika kita menggunakan Timer 2 mikrokontroler STM32F103C8 sebagai contoh, maka kita dapat melakukan cross sampling seperti tabel berikut:
| Pin (Default) | Channel Pembaca direct | Channel Pembaca Cross-Mapping |
|---|---|---|
| PA0 | Channel 1 | Channel 2 |
| PA1 | Channel 2 | Channel 1 |
| PA2 | Channel 3 | Channel 4 |
| PA3 | Channel 4 | Channel 3 |
Seperti yang kita lihat pada tabel, 1 pin GPIO dapat dibaca oleh 2 channel sekaligus (channel direct/normal dan channel cross-mapping). Hal yang sama juga berlaku untuk pin yang di remap sesuai penjelasan pada artikel ini. Sehingga jika kita mengonfigurasi pin PA0 sebagai input kita dapat memantaunya dengan menggunakan channel 1 dan channel 2 sebagai input capture. Kita dapat mengonfigurasi channel 1 untuk mendeteksi rising edge dan channel 2 untuk mendeteksi falling edge, sehingga kita tidak perlu mengubah mode input capture dari rising edge ke falling edge dan sebaliknya secara terus-menerus seperti yang kita lakukan pada artikel sebelumnya.
Channel cross-mapping akan sangat berguna jika kita ingin memantau sinyal input dengan frekuensi yang tinggi yang dimana event Capture/Compare terjadi dengan cepat.
Persiapan Hardware
Komponen-komponen Hardware 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.
Timer STM32F103C8 (Blue Pill) dengan Channel Cross-Mapping
Sama seperti artikel sebelumnya, pada tutorial kali ini kita akan membaca durasi (lebar pulsa) push button ditekan. Bedanya kali ini kita akan menggunakan 2 channel dengan cross-mapping (channel 1 dan channel 2) pada Timer 2 STM32F103C8.
Pada tutorial kali ini kita juga akan memanfaatkan pin PA0 untuk menerima input sinyal dari push button. Pin PA0 kita konfigurasi menjadi input pull-down. Sehingga ketika push button ditekan tegangan pada pin PA0 akan naik dari 0V ke 3V3, sebaliknya ketika dilepas maka tegangan pada pin PA0 akan turun dari 3.3V ke 0V. Pertama mari buat rangkaian pada breadboard sesuai dengan gambar skematik berikut:

Kita masih menggunakan rangkaian yang sama dengan artikel sebelumnya. Untuk penjelasan lebih mendetail mengenai skematik rangkaian yang digunakan, silakan baca pada artikel input capture.
Pemrograman Timer STM32F103C8 (Blue Pill) dengan Channel Cross-Mapping Menggunakan Rust
Silakan buat project Rust baru sesuai dengan yang dijelaskan pada artikel ini. Selanjutnya Buka file Cargo.toml, kemudian definisikan binary executable baru dengan nama ’ timer-cross-mapping ’ dengan menambahkan kode brikut:
1[[bin]]
2name = "timer-cross-mapping"
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};Memberitahu kompiler Rust untuk tidak menggunakan standard library dan tidak menjalankan program diatas sistem operasi.
Kemudian 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 mikrokontroler 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) secara aman.
pada fungsi main tambahkan kode berikut:
Rust
1defmt::println!("STM32F103C8 Timer Channel Cross Mapping");
Mengirim pesan ke PC/laptop untuk menandai sebagai program timer dengan channel cross-mapping.
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);
14
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. Pada tahap selanjutnya pin PA0 akan kita hubungkan ke channel 1 dan ke channel 2 pada Timer 2.
Rust
1let timer2 = Timer::new(dp.TIM2, &mut clocks);
2
3unsafe {
4 pac::NVIC::unmask(interrupt::TIM2);
5}Mengakses periferal Timer 2 dan mengaktifkan interrupt untuk Timer 2 pada NVIC (Nested Vectored Interrupt Controller).
Rust
1let mut counter = timer2.counter_hz();Melakukan setup Timer 2 sebagai counter
Rust
1counter.listen(Event::Update);Mengaktifkan interrupt untuk event update pada Timer 2 STM32F103C8.
Rust
1let timer_register = unsafe { &*pac::TIM2::ptr() };
2
Mengakses pointer register Timer 2 STM32F103C8 secara langsung.
Rust
1timer_register.ccmr1_input().modify(|_, w| w.cc1s().ti1());
2timer_register.ccmr1_input().modify(|_, w| w.cc2s().ti1());Mengonfigurasi channel 1 dan channel 2 sebagai input capture. Kemudian channel 1 dan channel 2 akan dihubungkan ke pin yang sama yaitu pin PA0 (TI1).
Rust
1timer_register
2 .ccmr1_input()
3 .modify(|_, w| w.ic1f().fck_int_n8());
4timer_register
5 .ccmr1_input()
6 .modify(|_, w| w.ic2f().fck_int_n8());Mengaktifkan filter digital untuk channel 1 dan channel 2 pada Timer 2 untuk mengurangi debounce.
Rust
1timer_register.ccer().modify(|_, w| w.cc1e().set_bit());
2timer_register.ccer().modify(|_, w| w.cc2e().set_bit());Mengaktifkan input capture channel 1 dan channel 2 pada Timer 2.
Rust
1timer_register.ccer().modify(|_, w| w.cc1p().clear_bit());
2timer_register.ccer().modify(|_, w| w.cc2p().set_bit());Mengonfigurasi agar channel 1 pada Timer 2 mendeteksi Rising edge. Sedangkan channel 2 pada Timer 2 dikonfigurasi untuk mendeteksi Falling edge.
Rust
1counter.start_raw(7199, 65535);Menjalankan Timer 2 dengan prescaler 7199 dan nilai register Auto-Reload sebesar 65535. Sehingga frekuensi timer menjadi 10 KHz
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
3}Membuat blok loop untuk menjalankan program utama secara terus-menerus.
Di dalam blok loop kita isi dengan kode berikut:
Rust
1if counter.get_interrupt().contains(Event::C1) {
2 let captured_tick = timer_register.ccr1().read().ccr().bits() as u16;
3
4 let pin_is_high = channel_1.is_high();
5
6 if pin_is_high && !is_pressed {
7 start_press_tick = captured_tick;
8 NUMB_OVERFLOW.store(0, Ordering::SeqCst);
9
10 is_pressed = true;
11 defmt::println!("--- Button Pressed ---");
12 defmt::println!("start: {}", start_press_tick);
13 }
14 counter.clear_interrupt(Event::C1);
15}Di dalam blok loop kita mengecek event capture/compare pada channel 1. Jika ada event capture/compare artinya terjadi rising edge pada pin PA0, maka ambil nilai pada register Capture/Compare channel 1 Timer 2. Selanjutnya lihat status dari pin PA0. Jika pin PA0 bernilai HIGH dan nilai variabel is_pressed false, maka update nilai variable start_press_tick dengan nilai tick yang sudah diambil, kemudian reset nilai variabel global NUMB_OVERFLOW ke 0, dan mengubah nilai variabe is_pressed ke true. Jangan lupa untuk membersihkan capture/compare interrupt flag channel 1 meskipun biasanya secara otomatis dibersihkan.
Rust
1if counter.get_interrupt().contains(Event::C2) {
2 let captured_tick = timer_register.ccr2().read().ccr().bits() as u16;
3
4 let pin_is_high = channel_1.is_high();
5 if !pin_is_high && is_pressed {
6 let total_overflow = NUMB_OVERFLOW.load(Ordering::SeqCst);
7
8 let total_tick =
9 (total_overflow * 65536) + captured_tick as u32 - start_press_tick as u32;
10
11 defmt::println!(
12 "end: {} | total_overflow: {}",
13 captured_tick,
14 total_overflow
15 );
16 defmt::println!("press long: {} ms", total_tick as f32 / 10f32);
17 defmt::println!("----------------------");
18 is_pressed = false;
19 }
20
21counter.clear_interrupt(Event::C2);
22}mengecek event capture/compare pada channel 2. Jika ada event capture/compare artinya terjadi falling edge pada pin PA0, maka ambil nilai pada register Capture/Compare channel 2 Timer 2. Selanjutnya lihat status dari pin PA0. Jika pin PA0 bernilai LOW dan nilai variabel is_pressed true, maka hitung total tick selama push buttong ditekan. Selanjutnya menggunakan total tick untuk menghitung durasi push button ditekan, dan mengubah nilai is_pressed ke false. Jangan lupa untuk membersihkan capture/compare interrupt flag channel 2 meskipun biasanya secara otomatis dibersihkan.
Selanjutnya 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#[interrupt]
2fn TIM2() {
3 let timer_register = unsafe { &*pac::TIM2::ptr() };
4
5 // check update interrupt flag
6 if timer_register.sr().read().uif().bit_is_set() {
7 // Increment overflow count
8 NUMB_OVERFLOW.fetch_add(1, Ordering::SeqCst);
9 // Clear update interrupt flag
10 timer_register.sr().modify(|_, w| w.uif().clear_bit());
11 }
12}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 Channel Cross Mapping");
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 unsafe {
46 pac::NVIC::unmask(interrupt::TIM2);
47 }
48
49 let mut counter = timer2.counter_hz();
50
51 counter.listen(Event::Update);
52
53 let timer_register = unsafe { &*pac::TIM2::ptr() };
54
55 timer_register.ccmr1_input().modify(|_, w| w.cc1s().ti1());
56 timer_register.ccmr1_input().modify(|_, w| w.cc2s().ti1());
57
58 timer_register
59 .ccmr1_input()
60 .modify(|_, w| w.ic1f().fck_int_n8());
61 timer_register
62 .ccmr1_input()
63 .modify(|_, w| w.ic2f().fck_int_n8());
64
65 timer_register.ccer().modify(|_, w| w.cc1e().set_bit());
66 timer_register.ccer().modify(|_, w| w.cc2e().set_bit());
67
68 timer_register.ccer().modify(|_, w| w.cc1p().clear_bit());
69 timer_register.ccer().modify(|_, w| w.cc2p().set_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
87 is_pressed = true;
88 defmt::println!("--- Button Pressed ---");
89 defmt::println!("start: {}", start_press_tick);
90 }
91 counter.clear_interrupt(Event::C1);
92 }
93
94 if counter.get_interrupt().contains(Event::C2) {
95 let captured_tick = timer_register.ccr2().read().ccr().bits() as u16;
96
97 let pin_is_high = channel_1.is_high();
98 if !pin_is_high && is_pressed {
99 let total_overflow = NUMB_OVERFLOW.load(Ordering::SeqCst);
100
101 let total_tick =
102 (total_overflow * 65536) + captured_tick as u32 - start_press_tick as u32;
103
104 defmt::println!(
105 "end: {} | total_overflow: {}",
106 captured_tick,
107 total_overflow
108 );
109 defmt::println!("press long: {} ms", total_tick as f32 / 10f32);
110 defmt::println!("----------------------");
111 is_pressed = false;
112 }
113
114 counter.clear_interrupt(Event::C2);
115 }
116 }
117}
118
119#[interrupt]
120fn TIM2() {
121 let timer_register = unsafe { &*pac::TIM2::ptr() };
122
123 // check update interrupt flag
124 if timer_register.sr().read().uif().bit_is_set() {
125 // Increment overflow count
126 NUMB_OVERFLOW.fetch_add(1, Ordering::SeqCst);
127 // Clear update interrupt flag
128 timer_register.sr().modify(|_, w| w.uif().clear_bit());
129 }
130}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-cross-mapping’ pada terminal VSCode. Berikut merupakan hasil ketika program dijalankan.
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