We need to:
We use:
Why External Clock Mode?
Because it allows the timer’s counter to increment on each incoming pulse directly from the pin’s input capture hardware, bypassing CPU limitations.
Component | Function |
TIMx (32-bit or overflow logic) | Counts pulses at hardware speed up to 8 MHz |
Push Button | Trigger data read and reset count |
UART | Sends pulse count to serial terminal |
EXTI | Detects push button press |
Software ISR | Reads count, sends via UART, clears counter |
Step 1 — Configure Timer in External Clock Mode
Effect: Each incoming pulse increments TIM->CNT in hardware at up to 8 MHz.
Step 2 — Configure Push Button Input
Step 3 — Configure UART
Step 4 — Main Code Flow
So, by considering the above points, we can implement the task.
Below are the solutions to the given task using different microcontrollers
We’re using an STM32 NUCLEO-F103RB board.
Step-by-Step Pin Mapping:
Circuit Diagram
Project Setup in STM32CubeIDE
HAL_Init()
– Initializes the HAL library.SystemClock_Config()
– Configures the system clock.MX_USART2_UART_Init()
→ Sets up USART2.MX_GPIO_Init()
– Configures GPIOs.MX_TIM2_Init()
– Configures TIM2.Timer 2 initialization
void MX_TIM2_Init(void) {
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 65535;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
HAL_TIM_Base_Init(&htim2);
TIM_SlaveConfigTypeDef sSlaveConfig;
sSlaveConfig.SlaveMode = TIM_SLAVEMODE_EXTERNAL1;
sSlaveConfig.InputTrigger = TIM_TS_ETRF;
HAL_TIM_SlaveConfigSynchro(&htim2, &sSlaveConfig);
}
UART Setup
void MX_USART2_UART_Init(void) {
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(&huart2);
}
GPIO Setup (Button with Pull-Up)
void MX_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
User Code and Main Loop Logic
// Track timer overflow for 32-bit pulse count
volatile uint32_t g_overflowCounter = 0;
// TIM2 overflow callback handler
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
g_overflowCounter++; // Extend counter beyond 65535
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
MX_TIM2_Init();
// Start TIM2 base in interrupt mode
HAL_TIM_Base_Start_IT(&htim2);
g_overflowCounter = 0;
while (1) {
// Check if button (PA8) is pressed (active low)
if (HAL_GPIO_ReadPin(SWITCH_PORT, SWITCH_PIN) == GPIO_PIN_RESET) {
uint32_t count = __HAL_TIM_GET_COUNTER(&htim2) + (65536 * g_overflowCounter);
char buff;
sprintf(buff, "Count = %lu \r\n", (unsigned long int)count);
HAL_UART_Transmit(&huart2, (uint8_t *)buff, strlen(buff), 100);
//Resets the counter
g_overflowCounter = 0;
// Set TIM2 counter to 0
__HAL_TIM_SET_COUNTER(&htim2, 0);
// Button debounce, delay
HAL_Delay(1000);
}
}
}
Explanation:
g_overflowCounter
is used to extend the 16-bit TIM2 counter (max 65,535) into a virtual 32-bit counter by incrementing on every timer overflow.65536 × g_overflowCounter
→ giving the total pulse count since start.Key STM32/HAL-Specific Notes
When the push button (PA8) is pressed:
Typical UART Console Output:
Count = 83625
The complete STM32CubeIDE project (including .ioc configuration, main.c, and HAL files) is available here:
📥 Download Project
Note: Although the task mentions using a timer, ESP32’s general-purpose timers don't have the ability to count external pulses. So use the PCNT (Pulse Counter) peripheral, which is dedicated hardware for reliable external pulse counting.
#include <Arduino.h>
#include "driver/pcnt.h"
#define SWITCH_PIN 14 // Button input (active LOW with INPUT_PULLUP)
#define PULSE_PIN 4 // Pulse input pin (3.3V logic)
#define DEBOUNCE_DELAY 50 // Button debounce time (ms)
// Button debounce state
bool last_button_state = HIGH;
bool current_button_state = HIGH;
unsigned long last_debounce_time = 0;
// PCNT (Pulse Counter) setup
#define PCNT_UNIT PCNT_UNIT_0 // Use PCNT unit 0
#define PCNT_CHANNEL PCNT_CHANNEL_0 // Use channel 0 of the unit
volatile int32_t overflowCount = 0; // Software overflow counter (extended width)
// PCNT interrupt handler: called when the hardware counter hits the high limit.
void IRAM_ATTR pcnt_intr_handler(void *arg) {
uint32_t status = 0;
pcnt_get_event_status(PCNT_UNIT, &status); // Read and clear event status bits
if (status & PCNT_EVT_H_LIM) { // High-limit reached (0..32767)
overflowCount++; // Extend count by tracking overflows
pcnt_counter_clear(PCNT_UNIT); // reset HW count to 0 so next block can accumulate
}
// Underflow not used in this project
}
// Configure and start PCNT to count rising edges on PULSE_PIN.
void pcnt_init() {
pcnt_config_t pcnt_config = {};
pcnt_config.pulse_gpio_num = PULSE_PIN; // Count pulses arriving on this GPIO
pcnt_config.ctrl_gpio_num = PCNT_PIN_NOT_USED;// No control pin
pcnt_config.unit = PCNT_UNIT;
pcnt_config.channel = PCNT_CHANNEL;
pcnt_config.counter_h_lim = 32767; // High limit → overflow event
pcnt_config.counter_l_lim = 0; // Low limit (not used)
pcnt_config.pos_mode = PCNT_COUNT_INC; // +1 on rising edges
pcnt_config.neg_mode = PCNT_COUNT_DIS; // Ignore falling edges
pcnt_config.lctrl_mode = PCNT_MODE_KEEP; // No control action
pcnt_config.hctrl_mode = PCNT_MODE_KEEP;
pcnt_unit_config(&pcnt_config); // Apply configuration
pcnt_event_enable(PCNT_UNIT, PCNT_EVT_H_LIM); // Enable high-limit event interrupt
pcnt_isr_service_install(0); // Install common ISR service (default flags)
pcnt_isr_handler_add(PCNT_UNIT, pcnt_intr_handler, NULL); // Attach our handler
pcnt_counter_pause(PCNT_UNIT); // Ensure stopped before clearing
pcnt_counter_clear(PCNT_UNIT); // Reset hardware counter to 0
pcnt_counter_resume(PCNT_UNIT); // Start counting
}
void setup() {
Serial.begin(115200);
pinMode(SWITCH_PIN, INPUT_PULLUP); // Button uses internal pull-up
pcnt_init(); // Start PCNT counting
}
void loop() {
// On a debounced button press: read, print, and reset counts
if (is_debounced_press(SWITCH_PIN)) {
int16_t count = 0;
pcnt_get_counter_value(PCNT_UNIT, &count); // Read current 0..32767 hardware count
// Each overflow represents 32768 counts (0..32767 inclusive)
uint32_t totalPulses = (overflowCount * 32768u) + (uint16_t)count;
Serial.print("Number of Pulses: ");
Serial.println(totalPulses);
pcnt_counter_clear(PCNT_UNIT); // Clear hardware counter for next window
overflowCount = 0; // Clear software overflow extension
}
}
// Debounce helper: returns true exactly once per valid button press.
bool is_debounced_press(int button_pin) {
int reading = digitalRead(button_pin);
if (reading != last_button_state) { // Edge detected → restart debounce timer
last_debounce_time = millis();
}
last_button_state = reading;
if ((millis() - last_debounce_time) > DEBOUNCE_DELAY) {
if (reading != current_button_state) { // Stable new state
current_button_state = reading;
if (current_button_state == LOW) { // Active-LOW press
return true;
}
}
}
return false;
}
Let’s understand the important parts
pcnt_config_t pcnt_config + pcnt_unit_config(&pcnt_config)
PCNT
peripheral (which GPIO, how to count, and limits).pulse_gpio_num = PULSE_PIN
→ count edges arriving on this pin.counter_h_lim = 32767
, counter_l_lim = 0 → 16-bit-ish window; when 32767 is reached, fire the H_LIM event.pos_mode = PCNT_COUNT_INC
→ increment on rising edges.neg_mode = PCNT_COUNT_DIS
→ ignore falling edges.ctrl_gpio_num = PCNT_PIN_NOT_USED
and both hctrl_mode/lctrl_mode = KEEP → no control pin, mode stays unchanged.pcnt_config_t pcnt_config = {};
pcnt_unit_config(&pcnt_config);
pcnt_event_enable(PCNT_UNIT, PCNT_EVT_H_LIM)
pcnt_event_enable(PCNT_UNIT, PCNT_EVT_H_LIM);
pcnt_isr_service_install(0)
and pcnt_isr_handler_add(PCNT_UNIT, pcnt_intr_handler, NULL)
pcnt_isr_service_install(0);
pcnt_isr_handler_add(PCNT_UNIT, pcnt_intr_handler, NULL);
void IRAM_ATTR pcnt_intr_handler(void *arg)
pcnt_get_event_status(...)
.If PCNT_EVT_H_LIM is set, increments overflowCount.In code:
|
IRAM_ATTR
? Ensures the ISR is placed in internal RAM for reliability/latency.pcnt_counter_pause(PCNT_UNIT);
pcnt_counter_clear(PCNT_UNIT);
pcnt_counter_resume(PCNT_UNIT);
pcnt_get_counter_value(PCNT_UNIT, &count)
int16_t count = 0;
pcnt_get_counter_value(PCNT_UNIT, &count);
is_debounced_press(...)
We are using the Arduino UNO development board and programming it using the Arduino IDE.
We are using a timer to count pulses with External Clock Mode.
Pulse Counter ( Arduino UNO )
The pulse generator produces pulses on the Output Pin, which is connected to digital pin 5 of the pulse counter (Arduino UNO). Digital pin 5 serves as the input for the external clock source to Timer 1.
#define SWITCH_PIN 12
#define DEBOUNCE_DELAY 50 // debounce delay
bool last_button_state = 1; // Previous button state (1: not pressed, 0: pressed)
bool current_button_state = 1; // Current button state
unsigned long last_debounce_time = 0; // Timestamp of the last button state change
uint8_t overflowFlag = 0;
void setup() {
Serial.begin(115200);
pinMode(SWITCH_PIN, INPUT_PULLUP);
// Configure Timer1 as a counter
TCCR1A = 0x00; // Normal mode
TCCR1B = 0x07; // External clock source on T1 pin, rising edge
TIMSK1 |= B00000001; // Enable Timer Overflow Interrupt
TCNT1 = 0; // Initialize counter to 0
}
void loop() {
if (is_debounced_press(SWITCH_PIN)) {
uint32_t pulseCount = (overflowFlag*65536) + TCNT1;
Serial.println("Number of Pulses: ");
Serial.println(pulseCount);
TCNT1 = 0;
overflowFlag = 0;
}
}
bool is_debounced_press(int button_pin) {
int reading = digitalRead(button_pin);
// If the button state has changed, reset the debounce timer
if (reading != last_button_state) {
last_debounce_time = millis();
}
last_button_state = reading;
// If the button state is stable for more than 50 msec the debounce delay, update the state.
if ((millis() - last_debounce_time) > DEBOUNCE_DELAY) {
if (reading != current_button_state) {
current_button_state = reading;
if (current_button_state == 0) {
return true; // valid press detected
}
}
}
return false; // No valid press detected
}
ISR(TIMER1_OVF_vect)
{
overflowFlag++;
}
is_debounced_press()
The function checks for a debounced button press.(overflowFlag * 65536) + TCNT1
.TCNT1
) and overflow flag (overflowFlag
) are reset to 0.ISR(TIMER1_OVF_vect)
function increments overflowFlag each time Timer1 overflows.Timer Configuration and Calculations
TCCR1A = 0x00
: Timer1 is set to normal mode (no PWM or waveform generation).TCCR1B = 0x07
: Timer1 is configured to use an external clock source (T1 pin) and increment on the rising edge of the input signal.TIMSK1 |= B00000001
: The Timer1 overflow interrupt is enabled. This interrupt triggers when the counter overflows (i.e., exceeds 65,536).TCNT1 = 0
: The Timer1 counter is initialized to 0.ISR(TIMER1_OVF_vect)
interrupt service routine is triggered, and the overflow flag is incremented.Total Pulses = (overflowFlag * 65536) + TCNT1.
= 131072 + 1234.
= 132306.
Button Debouncing Logic
SWITCH_PIN
).last_debounce_time
).DEBOUNCE_DELAY
).Hardware Setup of Pulse counter(one Arduino UNO) and Pulse Generator( another Arduino UNO)
Serial Monitor Output