18–20. NVIC Interrupts and EXTI · USART Serial Communication · ADC Analog Input
Polling a button ties up the CPU and can miss short inputs. With interrupts, main does its work and jumps immediately to a handler (ISR) on an event, then returns. The NVIC governs interrupts on Cortex-M, and EXTI turns external pin changes into interrupts. We take a PA0 button as an EXTI0 interrupt to toggle an LED, and learn to always clear the pending flag (PR) in the ISR. Now the chip talks to people·PCs in text. UART (universal async serial) is everywhere — debug messages, sensor modules, bootloaders. We set USART1 to 9600bps, send a greeting, and echo received characters back. μVision's Serial window shows the traffic directly, making UART the easiest peripheral to verify in the simulator. Every pin so far has been digital (0/1). Most real-world signals — temperature·brightness·sound — are continuous analog. An ADC (Analog-to-Digital Converter) turns analog voltage into numbers the MCU can use. With STM32F103's 12-bit ADC we read PA0's voltage as 0–4095 and convert to millivolts. As the last of the STM32 peripheral series (13–20), it uses clock·pin·polling together.
What you'll learn
- 1Explain polling vs interrupts and their trade-offs
- 2Understand NVIC's role and NVIC_EnableIRQ()
- 3Turn a pin edge (rising/falling) into an interrupt via EXTI
- 4Map a pin to an EXTI line via AFIO->EXTICR
- 5Write an ISR and clear the pending flag (PR) correctly
- 6Understand UART async communication and the 8N1 frame
- 7Configure USART1's pins (PA9/PA10) and clock
- 8Compute and set baud via BRR
- 9Implement polling send/receive with TXE/RXNE flags
- 10Verify traffic in μVision's Serial window
- 11Understand 12-bit resolution (0–4095) and reference voltage
- 12Set the ADC clock limit (≤14MHz) and prescaler
- 13Configure channel·sample time·conversion sequence
- 14Implement F1 ADC calibration (RSTCAL/CAL) and EOC polling
- 15Convert the ADC count to millivolts
18. NVIC Interrupts and EXTI
Polling a button ties up the CPU and can miss short inputs. With interrupts, main does its work and jumps immediately to a handler (ISR) on an event, then returns. The NVIC governs interrupts on Cortex-M, and EXTI turns external pin changes into interrupts. We take a PA0 button as an EXTI0 interrupt to toggle an LED, and learn to always clear the pending flag (PR) in the ISR.
Introduction
The NVIC (Nested Vectored Interrupt Controller) manages each interrupt's enable·priority·pending, and EXTI detects rising/falling edges on 16 lines (EXTI0–15) to raise interrupts.
Key concepts
1) NVIC and EXTI registers
| Register | Role |
|---|---|
| EXTI->IMR | interrupt mask (line enable) |
| EXTI->RTSR/FTSR | rising/falling edge trigger |
| EXTI->PR | pending flag — write 1 to clear |
NVIC_EnableIRQ(EXTI0_IRQn); /* CMSIS: enable in NVIC */2) AFIO->EXTICR — pin→line mapping
/* EXTI line n is at EXTICR[n/4], (n%4)*4 bits. 0=PA, 1=PB, 2=PC... */
AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0; /* EXTI0 ← PA0 */
/* mapping needs the AFIO clock (RCC_APB2ENR_AFIOEN) */3) ISR and clearing pending
void EXTI0_IRQHandler(void) {
if (EXTI->PR & EXTI_PR_PR0) {
EXTI->PR = EXTI_PR_PR0; /* write 1 to clear! else infinite re-entry */
GPIOC->ODR ^= (1u << 13);
}
}The handler name must match the startup vector table (EXTI0_IRQHandler). Without clearing PR, the same interrupt fires again the moment you leave the handler.
Core example
#include "stm32f10x.h"
void EXTI0_IRQHandler(void){
if (EXTI->PR & EXTI_PR_PR0) { EXTI->PR = EXTI_PR_PR0; GPIOC->ODR ^= (1u<<13); }
}
int main(void){
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN;
GPIOA->CRL &= ~0xFu; GPIOA->CRL |= 0x8u; GPIOA->ODR &= ~1u; /* PA0 input pull-down */
GPIOC->CRH &= ~(0xFu<<20); GPIOC->CRH |= (0x2u<<20); /* PC13 output */
AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0; /* EXTI0 ← PA0 */
EXTI->RTSR |= EXTI_RTSR_TR0; /* rising edge */
EXTI->IMR |= EXTI_IMR_MR0; /* unmask */
NVIC_EnableIRQ(EXTI0_IRQn);
while (1) { /* main is free */ }
}In the simulator's Command window, `PORTA |= 0x0001` (PA0 rising edge) enters EXTI0_IRQHandler and toggles PC13.
Common mistakes
Q. The interrupt fires once and never leaves the handler.
A. You didn't clear EXTI->PR. PR is cleared by writing 1 (EXTI->PR = EXTI_PR_PR0;). Skip it and it re-enters immediately.
Q. The handler is never called.
A. Check ① the AFIO clock (AFIOEN), ② unmask via EXTI->IMR, ③ NVIC_EnableIRQ, and the handler name EXTI0_IRQHandler.
Q. Can I do long work inside the ISR?
A. No. Keep ISRs short; set only a flag and handle heavy work in main (used in lesson 22 ring buffer).
Summary
- Interrupts auto-call on events, using the CPU efficiently
- The NVIC governs interrupts; enable with NVIC_EnableIRQ
- EXTI turns pin edges into interrupts via IMR/RTSR/FTSR/PR
- Map pin→line via AFIO->EXTICR (needs AFIO clock)
- Always clear pending (PR) in the ISR and keep handlers short
Exercises
- Switch to falling-edge trigger via FTSR and observe the difference
- Configure EXTI0 for a PB0 button (map EXTICR to PB)
- Refactor so the ISR only sets a volatile flag and main handles the LED
19. USART Serial Communication
Now the chip talks to people·PCs in text. UART (universal async serial) is everywhere — debug messages, sensor modules, bootloaders. We set USART1 to 9600bps, send a greeting, and echo received characters back. μVision's Serial window shows the traffic directly, making UART the easiest peripheral to verify in the simulator.
Introduction
UART exchanges data at an agreed baud with no clock line. The sender's TX crosses to the receiver's RX, sharing GND. The common setup is 8N1 (data8·no parity·stop1).
Key concepts
1) USART1 pins·clock and baud
| Signal | Pin | Mode |
|---|---|---|
| TX | PA9 | AF push-pull (0xB) |
| RX | PA10 | floating input (0x4) |
/* USART1 is APB2 (RCC_APB2ENR_USART1EN). fCK = PCLK2 = 72MHz */
BRR = fCK / baud; /* 9600 → 72000000/9600 = 7500 */2) Send/receive (polling)
while (!(USART1->SR & USART_SR_TXE)) { } /* until TX buffer empty */
USART1->DR = c; /* send one char */
if (USART1->SR & USART_SR_RXNE) c = USART1->DR; /* reading clears RXNE */Enable TE (transmit)·RE (receive)·UE (USART enable) in CR1: `CR1 = TE | RE | UE`.
Core example
#include "stm32f10x.h"
static void usart1_init(void){
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_USART1EN;
GPIOA->CRH &= ~((0xFu<<4) | (0xFu<<8));
GPIOA->CRH |= (0xBu<<4); /* PA9 TX = AF push-pull */
GPIOA->CRH |= (0x4u<<8); /* PA10 RX = floating input */
USART1->BRR = 7500u; /* 9600 @ 72MHz */
USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
}
static void tx(char c){ while(!(USART1->SR & USART_SR_TXE)){} USART1->DR=(uint16_t)c; }
int main(void){
usart1_init();
const char *s = "Hello, STM32 UART!\r\n"; while(*s) tx(*s++);
while (1) { if (USART1->SR & USART_SR_RXNE) tx((char)USART1->DR); } /* echo */
}BRR check: 9600→7500, 19200→3750, 115200→625. The greeting appears in View → Serial Windows → UART #1 and typed characters echo back.
Common mistakes
Q. Garbled characters.
A. Baud mismatch. Confirm the fCK used for BRR (USART1=PCLK2=72MHz) matches the actual clock and the terminal baud.
Q. Only the first char appears, then it stops.
A. Writing DR consecutively without waiting for TXE overwrites the previous char. Wait with while(!(SR & TXE)) before each char.
Q. I left PA9/PA10 as plain I/O.
A. TX(PA9) must be AF push-pull (0xB) and RX(PA10) an input for USART signals to reach the pins.
Summary
- UART is async over two wires (TX/RX) at a matched baud (8N1 common)
- USART1 is PA9(TX)/PA10(RX), APB2 bus, BRR=fCK/baud
- Send after TXE, receive after RXNE
- View traffic directly in the simulator's UART #1 window
- TX must be AF push-pull
Exercises
- Write print_num() that sends an integer as a decimal string
- Echo lowercase letters converted to uppercase
- Compute and apply the BRR for 115200bps
20. ADC Analog Input
Every pin so far has been digital (0/1). Most real-world signals — temperature·brightness·sound — are continuous analog. An ADC (Analog-to-Digital Converter) turns analog voltage into numbers the MCU can use. With STM32F103's 12-bit ADC we read PA0's voltage as 0–4095 and convert to millivolts. As the last of the STM32 peripheral series (13–20), it uses clock·pin·polling together.
Introduction
A 12-bit ADC splits the input range (0–Vref, usually 3.3V) into 4096 steps. One step (LSB) is ~0.806mV. mV = adc × 3300 / 4095. Potentiometers·light·temperature sensors all come in via the ADC.
Key concepts
1) Clock limit and channel
RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6; /* ADC clock ≤14MHz: 72/6 = 12MHz */
ADC1->SQR3 = 0u; /* 1st conversion channel 0 (PA0) */
ADC1->SMPR2 |= (7u << 0); /* channel0 max sample time (239.5 cycle) */
/* ADC pin must be analog input (0x0) — no digital buffer/pull-up */2) F1 calibration and conversion (EOC)
ADC1->CR2 |= ADC_CR2_ADON; /* power on (wait tSTAB) */
ADC1->CR2 |= ADC_CR2_RSTCAL; while(ADC1->CR2 & ADC_CR2_RSTCAL){}
ADC1->CR2 |= ADC_CR2_CAL; while(ADC1->CR2 & ADC_CR2_CAL){} /* calibrate */
ADC1->CR2 |= ADC_CR2_ADON; /* re-set ADON = start conversion */
while (!(ADC1->SR & ADC_SR_EOC)) { } /* wait for conversion done */
uint16_t v = ADC1->DR & 0x0FFFu; /* 12-bit result (reading clears EOC) */F1 needs calibration (RSTCAL→CAL) once after power-on for accuracy. For millivolt conversion always multiply then divide, in 32-bit: (uint32_t)adc * 3300 / 4095.
Core example
#include "stm32f10x.h"
static void adc1_init(void){
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN;
RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6; /* 12MHz */
GPIOA->CRL &= ~(0xFu << 0); /* PA0 analog input (0x0) */
ADC1->SQR3 = 0u; ADC1->SMPR2 |= (7u << 0);
ADC1->CR2 |= ADC_CR2_ADON; for(volatile int i=0;i<10000;i++){}
ADC1->CR2 |= ADC_CR2_RSTCAL; while(ADC1->CR2 & ADC_CR2_RSTCAL){}
ADC1->CR2 |= ADC_CR2_CAL; while(ADC1->CR2 & ADC_CR2_CAL){}
}
static uint16_t adc_read(void){
ADC1->CR2 |= ADC_CR2_ADON; while(!(ADC1->SR & ADC_SR_EOC)){}
return (uint16_t)(ADC1->DR & 0x0FFFu);
}
int main(void){
adc1_init();
while (1) {
uint16_t adc = adc_read();
volatile uint32_t mv = (uint32_t)adc * 3300u / 4095u;
(void)mv;
}
}Conversion check: ADC=0→0mV, 2048→1650mV, 4095→3300mV.
Common mistakes
Q. It stalls waiting for EOC.
A. Confirm ADC power (ADON) is on, the conversion is started (re-set ADON/SWSTART), and the ADC clock (ADC1EN) is active.
Q. Values are noisy and inaccurate.
A. ① skipped calibration (RSTCAL/CAL), ② sample time too short (high-impedance source), ③ ADC clock over 14MHz — check ADCPRE.
Q. The mV calculation gives 0.
A. adc/4095*3300 truncates to 0 in integers. Always multiply then divide: (uint32_t)adc * 3300 / 4095.
Summary
- The ADC turns analog voltage into a 12-bit integer (0–4095)
- mV = adc × 3300 / 4095 (multiply then divide, in 32-bit)
- ADC clock ≤14MHz — divide via ADCPRE
- F1 calibrates (RSTCAL/CAL), converts, polls EOC, then reads DR
- Set the ADC pin to analog input (0x0)
Exercises
- Send the read ADC value via lesson-19 USART as "adc=NNNN mv=MMMM"
- Average several reads to reduce noise (a simple filter)
- Add channel1 (PA1) by changing SQR3/SMPR
All lecture materials and example code (with homework and answers) are openly available on GitHub.
View on GitHub ↗Comments 0
No comments yet. Be the first.
Only members can comment.
Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy