STM32 Timer + Rust: Reading 1 GPIO Pin Using 2 Channels

In the previous articles (input capture and timer interrupt), we discussed how to measure pulse width using the input capture feature on Timer 2 Channel 1. Since the STM32F103C8 Timer 2 does not support both-edge input capture mode, in those articles, we continuously toggled the input capture mode on channel 1 from rising edge to falling edge and vice versa.

If a high-frequency signal is applied to the GPIO input pin (causing rapid capture/compare events), the pulse width measurement will become inaccurate because the CPU requires time to change the Capture/Compare Polarity (CC1P) register value for channel 1 (resulting in blocking). To overcome this issue, we will utilize the channel cross-mapping (channel input multiplexing) feature of the STM32F103C8 timer.

Introducing Channel Cross-Mapping on the STM32F103C8 (Blue Pill) Timer

Channel cross-mapping or Channel Input Multiplexing is a feature of the STM32 Timer peripheral that allows two or more timer channels to monitor the same GPIO pin. In the STM32F103C8 microcontroller, channel cross-mapping can only be performed on paired channels, such as Channel 1 with Channel 2 and Channel 3 with Channel 4. Therefore, cross-mapping between Channel 1 or 2 and Channel 3 or 4 is not possible.

Taking Timer 2 of the STM32F103C8 microcontroller as an example, we can perform cross-sampling as shown in the following table:

Pin (Default)Channel (Direct)Channel Cross-Mapping
PA0Channel 1Channel 2
PA1Channel 2Channel 1
PA2Channel 3Channel 4
PA3Channel 4Channel 3

As shown in the table, a single GPIO pin can be monitored by two channels simultaneously (the direct/normal channel and the cross-mapping channel). This also applies to pins that have been remapped, as explained in this article. Consequently, if we configure pin PA0 as an input, we can monitor it using both Channel 1 and Channel 2 as input capture units. We can configure Channel 1 to detect the rising edge and Channel 2 to detect the falling edge. This eliminates the need to continuously toggle the input capture mode between rising and falling edges, as we did in the previous article.

Channel cross-mapping is particularly useful for monitoring high-frequency input signals where Capture/Compare events occur rapidly.

Hardware Preparation

The hardware components used in this article are the same as those in the previous article: STM32F103C8 microcontroller, ST-Link USB Downloader/Debugger, breadboard, push button, and jumper wires (male-to-male and female-to-female). A more detailed explanation regarding the functions of these components can be found on the Setup STM32F103C8 with Rust and Using STM32F103C8 GPIO with Rust pages.

STM32F103C8 (Blue Pill) Timer with Channel Cross-Mapping

Similar to the previous article, in this tutorial, we will measure the duration (pulse width) of a push button press. The difference is that this time we will utilize two channels with cross-mapping (Channel 1 and Channel 2) on the STM32F103C8 Timer 2.

In this tutorial, we will also use pin PA0 to receive the input signal from the push button. Pin PA0 is configured as a pull-down input. Thus, when the push button is pressed, the voltage on pin PA0 will rise from 0V to 3.3V; conversely, when released, the voltage on pin PA0 will drop from 3.3V to 0V. First, let’s build the circuit on the breadboard according to the following schematic:

Push button circuit with STM32F103C8 for input capture
Push button circuit with STM32F103C8 as input capture on Timer 2 Channel 1

We are still using the same circuit as in the previous article. For a more detailed explanation regarding the schematic used, please refer to the input capture article.

Programming STM32F103C8 (Blue Pill) Timer with Channel Cross-Mapping Using Rust

Please create a new Rust project as explained in this article. Next, open the Cargo.toml file and define a new binary executable named ’ timer-cross-mapping ’ by adding the following code:

1[[bin]]
2name = "timer-cross-mapping"
3path = "src/main.rs"
4test = false
5bench = false

Next, open the src/main.rs file and fill it with the following code:

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};

Inform the Rust compiler not to use the standard library and to run the program without an operating system (bare-metal).

Then, define the libraries that will be used in the project:

  • The atomic library is used to define global variables so they can be accessed from both the main loop block and the interrupt functions.
  • The cortex_m_rt library/crate is used to define the program’s entry point and handle the startup process.
  • defmt_rtt is used to send data to a PC/laptop for logging using the Real-Time Transfer (RTT) protocol.
  • The stm32f1xx_hal library allows us to access the STM32F103C8 microcontroller peripherals safely.
  • panic_probe is used to handle runtime errors, automatically sending error logs to the host PC/laptop."

Rust

1static NUMB_OVERFLOW: AtomicU32 = AtomicU32::new(0);

Create a global variable named NUMB_OVERFLOW to store the number of times the timer has overflowed. Here, we use the AtomicU32 variable type so that the variable can be accessed safely from both the main loop block and the interrupt function block (Interrupt Service Routine/ISR).

In the main function, add the following code:

Rust

1defmt::println!("STM32F103C8 Timer Channel Cross Mapping");    

Send a message to the PC/laptop to label it as a timer program with 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    

Configure the clock to use an 8 MHz external clock (HSE), then set the System clock to 72 MHz, the Advanced High-performance Bus (AHB) peripheral clock to 72 MHz, and the Advanced Peripheral Bus 1 (APB1) clock to 36 MHz. Consequently, the Timer 2 clock frequency will be 72 MHz.

Rust

1let mut gpioa = dp.GPIOA.split(&mut clocks);
2
3let channel_1 = gpioa.pa0.into_pull_down_input(&mut gpioa.crl);    

Access the GPIO Port A (GPIOA) peripheral and configure the PA0 pin as a pull-down input. In the next step, we will connect the PA0 pin to both Channel 1 and Channel 2 of Timer 2.

Rust

1let timer2 = Timer::new(dp.TIM2, &mut clocks);
2
3unsafe {
4    pac::NVIC::unmask(interrupt::TIM2);
5}

Access the Timer 2 peripheral and enable the interrupt for Timer 2 in the NVIC (Nested Vectored Interrupt Controller).

Rust

1let mut counter = timer2.counter_hz();

“Set up Timer 2 as a counter.

Rust

1counter.listen(Event::Update);

Enable the update event interrupt for Timer 2 on the STM32F103C8.

Rust

1let timer_register = unsafe { &*pac::TIM2::ptr() };
2    

Access the Timer 2 register pointer of the STM32F103C8 directly.

Rust

1timer_register.ccmr1_input().modify(|_, w| w.cc1s().ti1());
2timer_register.ccmr1_input().modify(|_, w| w.cc2s().ti1());

Configure Channel 1 and Channel 2 as input capture. Then, both Channel 1 and Channel 2 will be connected to the same pin, which is 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());

Enable the digital filter for Channel 1 and Channel 2 on Timer 2 to reduce debouncing.

Rust

1timer_register.ccer().modify(|_, w| w.cc1e().set_bit());
2timer_register.ccer().modify(|_, w| w.cc2e().set_bit());

Enable input capture for Channel 1 and Channel 2 on Timer 2.

Rust

1timer_register.ccer().modify(|_, w| w.cc1p().clear_bit());
2timer_register.ccer().modify(|_, w| w.cc2p().set_bit());

Configure Channel 1 on Timer 2 to detect the Rising edge, while Channel 2 on Timer 2 is configured to detect the Falling edge

Rust

1counter.start_raw(7199, 65535);

Start Timer 2 with a prescaler of 7199 and an Auto-Reload Register (ARR) value of 65535. Timer frequency become 10 KHz.

Rust

1let mut start_press_tick = 0u16;
2
3let mut is_pressed = false;

Create variables to store the tick value when the push button is first pressed and to store the current status of the push button.

Rust

1loop {
2        
3}

Create a loop block to run the main program continuously

Inside the loop block, we populate it with the following code:

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}

Inside the loop block, we check for the capture/compare event on Channel 1. If a capture/compare event occurs, indicating a rising edge on pin PA0, we retrieve the value from the Timer 2 Capture/Compare Register 1 (CCR1). Next, check the status of pin PA0. If pin PA0 is HIGH and the is_pressed variable is false, then update the start_press_tick variable with the captured tick value, reset the NUMB_OVERFLOW global variable to 0, and set the is_pressed variable to true. Finally, ensure the Channel 1 capture/compare interrupt flag is cleared, even though it is typically cleared automatically."

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}

Check for the capture/compare event on Channel 2. If a capture/compare event occurs, indicating a falling edge on pin PA0, retrieve the value from the Timer 2 Capture/Compare Register 2 (CCR2).

Next, check the status of pin PA0. If pin PA0 is LOW and the is_pressed variable is true, calculate the total ticks elapsed while the push button was pressed. Then, use the total ticks to calculate the press duration and set the is_pressed variable to false. Finally, ensure the Channel 2 capture/compare interrupt flag is cleared, even though it is typically cleared automatically.

Next, create the Interrupt Service Routine (ISR) that the CPU will execute whenever a Timer 2 interrupt occurs. The following is the interrupt function we will be using.

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}

Create the interrupt function (ISR) for Timer 2. Inside the function, we check the update interrupt flag. If the interrupt is triggered by an update event, we increment the NUMB_OVERFLOW global variable by 1, and subsequently clear the update interrupt flag bit.

Show full code: STM32F103C8 Timer with Channel Cross-Mapping
  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}

Using the ST-Link USB Downloader and Debugger, connect the Blue Pill board (STM32F103C8 microcontroller) to your PC or laptop as described in this article. After that, execute the program by running the command ‘cargo run --bin timer-cross-mapping’ in the VSCode terminal. The following is the output when the program is executed.

Source Code

The source code used in this article can be accessed at the GitHub repository.

If you encounter any difficulties following this tutorial, please do not hesitate to reach out to us through our contact page.