As a C embedded beginner, this project might be a good starter.
You can try build one following my log.
I also noted some background story of mine!
- Countdown, alarm when time is up;
- Start counting up if is set to
00:00; - Pause and continue freely;
- Remembers last time set.
- Says
Hion boot; - Debounce buttons with software only;
- Beep on any button press;
- Add 1 minute (1 second) on press;
- Reset time to
00:00and clear up flags and states when combo of start/stop button and one of the time-setting buttons pressed at the same time; - Loop back to
00:00when countup overflow; - Blink the colon symbol on the display when counting;
- Longpress to add time quickly.
- An
STM32F103C8T6 - An
ST-LINK/V2ICD (in-circuit debugger) - A
TM16374-digits 7-seg number display with a colon - A
TMB12A05buzzer - Some un-debounced buttons
- Some breadboards and wires
| Component | Signal | STM32 Pin | Notes |
|---|---|---|---|
| TM1637 | CLK | PB6 | Output (bit-bang) |
| TM1637 | DIO | PB7 | Output (open-drain) |
| BTN_MULTI | OUT | PA1 | GPIO_EXTI Rising-edge (GPIO_Input when test),pull-down, active LOW |
| BTN_MIN | OUT | PA2 | GPIO_EXTI Rising-edge (GPIO_Input when test),pull-down, active LOW |
| BTN_SEC | OUT | PA3 | GPIO_EXTI Rising-edge (GPIO_Input when test),pull-down, active LOW |
| TMB12A05 | S | PA8 | Output (active HIGH) |
.iocGUI >Pinout & Config>System Core>SYS> Debug: SelectSerial Wire
| Component | STM32 Pin | Notes |
|---|---|---|
SYS auto |
PA13 |
for debugging |
SYS auto |
PA14 |
for debugging |
| BTN_S2 | PA0 |
Input, pull-up, active LOW |
| LED_D2 | PC13 |
Output, no pull |
| Device | 3V3 | GND |
|---|---|---|
| TM1637 | VCC |
GND |
| BTNs | VCC |
GND |
| TMB12A05 | (mid pin) | - |
-
Declaration
In
/* USER CODE BEGIN PFP */:void alarm_sound(void);
-
Implementation
In
/* USER CODE BEGIN 4 */:void alarm_sound(void) { for(int i = 0; i < 4; i++) { HAL_GPIO_WritePin(GPIOA, BUZ_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(LED_D2_GPIO_Port, LED_D2_Pin, 0); HAL_Delay(70); HAL_GPIO_WritePin(GPIOA, BUZ_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(LED_D2_GPIO_Port, LED_D2_Pin, 1); HAL_Delay(70); } HAL_Delay(700); }
-
Call in main while loop
alarm_sound();
I got only 2 buttons but I have a 3-pin switch (that have 2 states: A-B & B-C).
I had to make BTN_MIN and BTN_SEC Pull-down,
so that when floating they won't output any signals.
ChatGPT provided me the
seg_mapas follow:static const uint8_t seg_map[11] = { 0x3F,//0 0x06,//1 0x5B,//2 0x4F,//3 0x66,//4 0x6D,//5 0x7D,//6 0x07,//7 0x7F,//8 0x6F,//9 };so that I can control the board seg-precisely, with
tm1637_raw()from nimaltd's driver.
I added a flag.c to share the current min and sec state across files.
Also prepared a place for flags in future coding of the core logic.
I want a pure interrupt-driven firmware.
To do this,
add __WFI(); inside the main while loop.
Now the HAL_Delay() will ruin the timer's normal functionality,
so get rid of them all with their related logics from now on.
Since there is a 8 MHz crystal on board, let's use it.
- Setup HSE
In .ioc > Pinout Config > System Core > RCC, find High Speed Clock and select Crystal/Ceramic Resonator.
Then in Clock Config:
| Setting | Value |
|---|---|
| HSE Input | 8 MHz |
| PLL Source | HSE |
| PLLMul | x9 |
| System Clock Mux | PLLCLK |
| SYSCLK | 72 MHz |
| HCLK | 72 MHz |
| APB1 | / 2 (resulting 36 MHz) |
- Setup
TIM2
In Pinout Config > Timers > TIM2 > Mode
find Clock Source and select Internal Clock.
Configuration contents will appear.
Under NVIC Settings, enable TIM2 global interrupt.
Then in Parameter Settings:
| Param | Value |
|---|---|
| Prescaler (PSC) | 7200-1 |
| Counter Period (ARR) | 5000-1 |
| Counter Mode | Up |
Explain of the value:
The goal is to interrupt every 0.5 seconds.
The formula:f_update = TIM_CLK / ((PSC + 1) * (ARR + 1))For TIM2,
TIM_CLKis 72 MHz;
- Prescaler =
7200 - 1- Period =
5000 - 1Result:- 72 MHz / 7200 = 10 kHz
- 10 kHz / 5000 = 2 Hz
Save .ioc and generate code.
- Test TIM ISR (Interrupt Service Routine)
Now test the timer interrupt.
In /* USER CODE BEGIN 4 */:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if(htim->Instance==TIM2){
// Callback when interrupt triggered every 0.5 sec
}
}ChatGPT warns that we should keep ISR logic short.
So we better only set a flag in the ISR callback, and move heavy work into the main while loop.
while(1){ if(flag){ flag = 0; // handling logic... } __WFI(); }
EXTI (External Interrupt) is an ISR from a GPIO input.
-
Re-configure Pins
In
.ioc>Pinout & Config>Pinout view, set the 3 button's pins asGPIO_EXTIn.Then go to
Pinout & Config>System Core>GPIOand set:Setting Value GPIO mode External Interrupt Mode with Rising edge trigger GPIO Pull Pull-up User Label BTN_MULTI_Pin/BTN_MIN_Pin/BTN_SEC_Pin -
Enable NVIC (Nested Vectored Interrupt Controller)
In
Pinout & Config>System Core>NVIC, check the interrupt of EXTI 1, 2, and 3.Save
.ioc. -
ISR callback function
In
/* USER CODE BEGIN 4 */:void HAL_GPIO_EXTI_Callback(uint16_t Pin){ if(pin==BTN_MULTI_Pin){ B_MULTI.exti = 1u; } // ... }
Then in main while loop, make a test that toggles led on button press:
// test if(B_MULTI.exti){ handle(); } void handle(){ // debounce if(HAL_GetTick() - BS->useTick < DEBOUNCE_MS) return; BS->exti = 0; GF.led ^= 1; led(GF.led); }
Buttons, physically, are often 2 pieces of thin metal touching. The pressing of the button usually creates one or several bounces of those metals under few milliseconds, which may trigger the EXTI Rising-edge event more than twice with only one button press.
We can fix this just with a few lines of code.
-
Begin a 39 ms timer (
DEBOUNCE_MS) at the first Rising-edge.Inside
HAL_GPIO_EXTI_Callback:// exti (External Interrupt) Service Routine static void eisr(ButtonState *BS){ if(!BS->exti){ BS->useTick = HAL_GetTick(); BS->exti = 1u; } }
The
BS->extiis a "pending" flag, leaving heavy mission to the main while loop. -
In the main loop's
handle_btn():- Take a break during debouncing.
if((HAL_GetTick() - BS->useTick) < DEBOUNCE_MS) return; BS->exti = 0;
- When button still pressed after 39 ms, proceed working the button ought to.
bool level = (read_btn(BS) == GPIO_PIN_SET); if(level){ GF.led ^= 1; led(GF.led); countup(); settime(1u); }
- Take a break during debouncing.
Later version took core logic codes out from the
ifblock, also the pending flag resets afterwards.if(!level) return; BS->exti = 0; // main logic...
Pure software, the timer core logics, were implemented afterwards.
Made beep sound on any button press.
See onpress_beep_update().
Also made the classic time up alarm tone using HAL software timestamp.
See the new version of alarm_sound() in the current codebase instead of the one showing in README.
Final tests and minor fixes occurred before repo archiving.
I totally forgot to code longpress before v1.0 release ㅠ ㅠ
Existing ButtonState->useTick was used to trigger.
See longpress().
Also optimized BS->exti resetting in debouncing logic.
Oh hi! This is Thisoe based in Korea.
As of 2026, I experienced a year (in 2021) coding ESP8266 using MicroPython when I was in college, and another 3+ years of JavaScript (1+ year of TypeScript) web developing as hobby.
I gained enough coding experience. I also curious and eager to learn more about lower-level languages and hardware basics. So I started this new journey of C embedded with teacher OJ Tube aka 오지완 선생님.
Mr. OJ's teaching style was basic enough that middle school students could understand lol and I'm a bit tired following with his tutorial, so I turned to start a project right away.
Holding the freshly purchased STM32 board, I saw my cute little timer sticking on the fridge, and thereupon pulled out my old TM1637 (the time displayer) and TMB12A05 (the buzzer) out of the ESP8266 box.
I'm attending full-time job, so I coded 1 ~ 3 hrs per day, from the 5th to 19th of April 2026.
During those days, I also learned minimal hardware knowledges and basics on how to wire things up from Ben Eater's early vods.
Random playing Mix Playlist of instrumentals of Suisei's songs. They buff my productivity incredibly. Btw the song «BEEP BEEP» released when I was coding buzzer functions. Looped the album while coding.
Didn't notice my markdown syntax has got THIS natural and as good as fellow AIs lmao ㄷㄷㄷ エグい
Since I probably won't come back and touch this repo ever again, I'm just gonna archive it... or maybe after adding Korean, Mandarin, and Japanese README later.
Contact me at thisoecode@gmail.com, or DM me on 𝕏 for a chit chat :D