Bỏ qua, đến nội dung chính
Bài viếtFirmwareMới

STM32 Beginner Guide P2: GPIO, Interrupt và Timer từ ví dụ đầu tiên

Học cách cấu hình GPIO, bắt sự kiện nút nhấn bằng EXTI interrupt và tạo nhịp định kỳ bằng timer trên STM32CubeIDE với board NUCLEO-G071RB.

Chia sẻ

LinkedInFacebookX
STM32 Nucleo board wired to an LED and push button on a practical electronics workbench

P1: Bắt đầu với STM32, mình đã chọn NUCLEO-G071RB làm board tham chiếu và phân biệt STM32 chip, module với development board. P2 là lúc mình nối phần cứng thật: một LED, một nút nhấn và một timer.

Đi trong series: P1: Bắt đầu với STM32 · P2 đang đọc · P3: UART, I2C và SPI

Mục tiêu không phải làm một demo thật lớn. Mình muốn bạn nhìn thấy ba cách cơ bản mà firmware tương tác với thế giới bên ngoài:

  • GPIO đọc hoặc xuất một mức logic.
  • Interrupt báo cho CPU biết một sự kiện vừa xảy ra.
  • Timer tạo ra một mốc thời gian đều đặn.

Các ví dụ dùng STM32CubeIDE, CubeMX và HAL. ST cũng tổ chức các example GPIO/EXTI và timer trong repository STM32CubeG0, nên bạn có thể mở source chính thức để đối chiếu khi muốn đi sâu hơn.

GPIO: chân digital không chỉ là HIGH và LOW

Board STM32 Nucleo nối với LED và nút nhấn trên breadboard để học GPIO.

GPIO chỉ là tín hiệu điều khiển; LED vẫn cần điện trở giới hạn dòng và mạch phải có GND chung.

GPIO là một chân đa dụng có thể được cấu hình làm input, output hoặc alternate function cho peripheral khác. Ở mức cơ bản, bạn sẽ gặp hai tình huống:

  • Output: STM32 điều khiển LED, relay driver hoặc một tín hiệu enable.
  • Input: STM32 đọc nút nhấn, công tắc hoặc tín hiệu digital từ cảm biến.

GPIO của NUCLEO-G071RB dùng logic 3,3 V. HIGH không phải là “nguồn cấp cho mọi thứ”; nó chỉ là mức logic. Một GPIO cũng không nên cấp trực tiếp cho motor, relay coil hay tải lớn. Với tải đó, hãy dùng transistor/MOSFET và nguồn riêng phù hợp.

Output push-pull

Với LED rời, mạch tối thiểu là:

GPIO ---- điện trở 330 Ω ---- anode LED
GND  ------------------------ cathode LED

Điện trở có thể nằm trước hoặc sau LED, miễn là nằm nối tiếp để giới hạn dòng. Không nối LED trực tiếp vào GPIO rồi tăng giảm delay để “thử xem có sao không”.

Trong CubeMX, chọn một chân GPIO còn trống, đặt GPIO_Output, output type là Push-Pull, pull là No pull và speed thấp hoặc trung bình. Tên symbol được sinh ra có thể là LED_PinLED_GPIO_Port; tên chính xác phụ thuộc cách bạn đặt trong project.

Code HAL tối thiểu thường trông như sau:

while (1)
{
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    HAL_Delay(500);
}

HAL_Delay(500) giúp bạn nhìn thấy LED đổi trạng thái mỗi 500 ms. Nó phù hợp cho demo đầu tiên, nhưng không nên trở thành cách điều phối toàn bộ ứng dụng. Trong phần timer, mình sẽ thay cách này bằng một sự kiện định kỳ không chặn.

Input và trạng thái floating

Một input để hở có thể đọc lúc HIGH, lúc LOW vì bị nhiễu. Trạng thái đó gọi là floating. Với nút nhấn đơn giản, bạn có thể dùng pull-up nội:

GPIO input ---- nút nhấn ---- GND

Khi cấu hình Pull-up, nút thả ra thường đọc GPIO_PIN_SET, còn khi nhấn sẽ kéo chân xuống GPIO_PIN_RESET. Logic bị đảo này khá dễ quên.

Nếu dùng pull-up hoặc pull-down ngoài, giá trị điện trở khoảng 4,7 kΩ–10 kΩ thường là điểm bắt đầu hợp lý cho nút nhấn. Giá trị cuối cùng còn phụ thuộc tốc độ, dòng rò và môi trường nhiễu của sản phẩm.

Interrupt: phản ứng khi sự kiện xảy ra

Polling nghĩa là vòng lặp liên tục hỏi: “Nút đã được nhấn chưa?”. Cách này đơn giản, nhưng lãng phí thời gian CPU và có thể bỏ lỡ những xung ngắn.

Với interrupt, bạn cấu hình một GPIO để tạo sự kiện khi tín hiệu có cạnh:

  • Rising edge: tín hiệu chuyển từ LOW lên HIGH.
  • Falling edge: tín hiệu chuyển từ HIGH xuống LOW.
  • Both edges: bắt cả hai hướng chuyển mức.

Cấu hình EXTI trong CubeMX

Chọn chân nút nhấn, đặt mode thành GPIO_EXTI với cạnh phù hợp. Nếu nút nối từ GPIO xuống GND và dùng pull-up, sự kiện nhấn thường là falling edge.

CubeMX sẽ tạo phần cấu hình GPIO, NVIC và interrupt handler. Ở tầng HAL, handler gọi vào callback. Với STM32G0, bạn có thể gặp hai callback riêng:

void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == USER_BUTTON_Pin)
    {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

Điểm này đáng chú ý: callback có thể khác giữa các dòng STM32 và phiên bản HAL. Example GPIO_EXTI chính thức của STM32CubeG0 dùng callback falling/rising tương ứng, vì vậy hãy mở file HAL của đúng series thay vì copy tên callback từ một tutorial F1 hoặc F4.

ISR nên ngắn

Callback interrupt nằm trong đường thực thi ưu tiên cao. Mình thường chỉ làm một trong ba việc ở đó:

  1. Xóa hoặc xử lý cờ cần thiết.
  2. Ghi nhận một event hoặc set một volatile flag.
  3. Đọc nhanh dữ liệu nhỏ nếu peripheral yêu cầu.

Không nên gọi HAL_Delay(), in log dài, thực hiện phép tính nặng hoặc chờ một peripheral hoàn thành bên trong ISR. Cách an toàn hơn là set cờ rồi xử lý ở while (1):

volatile uint8_t button_event = 0;

void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == USER_BUTTON_Pin)
    {
        button_event = 1;
    }
}

while (1)
{
    if (button_event)
    {
        button_event = 0;
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

volatile nhắc compiler rằng giá trị có thể thay đổi ngoài luồng thực thi chính. Với event phức tạp hơn, bạn có thể dùng queue hoặc bộ đếm thay vì một cờ đơn.

Nút nhấn bị dội

Một lần nhấn cơ học không tạo ra một cạnh sạch duy nhất. Tiếp điểm có thể dao động trong vài mili giây, khiến một lần nhấn bị hiểu thành nhiều interrupt.

Đây là button bounce, không phải lỗi của EXTI. Ba cách xử lý thường gặp:

  • Bỏ qua event trong một khoảng thời gian ngắn sau lần hợp lệ.
  • Dùng timer để xác nhận trạng thái ổn định.
  • Thêm mạch RC hoặc IC debounce khi yêu cầu phần cứng rõ ràng hơn.

Với demo, bạn có thể lưu timestamp bằng HAL_GetTick() và bỏ qua các event đến quá gần nhau. Trong sản phẩm, hãy chọn debounce theo loại nút, vỏ và trải nghiệm người dùng thực tế.

Timer: tạo nhịp mà không chặn CPU

Board STM32 Nucleo điều khiển LED trên breadboard trong một phép thử timer cạnh laptop và oscilloscope.

Timer tạo nhịp đều và phù hợp hơn HAL_Delay khi ứng dụng cần làm nhiều việc cùng lúc.

Timer là peripheral có bộ đếm chạy theo clock. Khi bộ đếm đạt giá trị định trước, nó có thể tạo update event, interrupt hoặc waveform PWM.

Công thức gần đúng cho tần số update là:

f_update = f_timer_clock / ((Prescaler + 1) × (Period + 1))

Ví dụ, với clock timer 64 MHz, muốn update mỗi 1 ms (1 kHz), bạn có thể chọn prescaler để đưa counter clock xuống 1 MHz, rồi đặt period là 999:

64 MHz / (64 × 1000) = 1 kHz

Đây là ví dụ tính toán, không phải một cấu hình cố định cho mọi project. Clock tree của bạn, bus timer và APB prescaler quyết định f_timer_clock thật sự. Hãy xem giá trị CubeMX sinh ra thay vì đoán từ tần số CPU.

Timer interrupt và cờ định kỳ

Trong CubeMX, bật một timer base, chọn prescaler/period, bật TIMx global interrupt trong NVIC rồi gọi:

HAL_TIM_Base_Start_IT(&htim6);

Callback update có thể set một cờ:

volatile uint8_t timer_event = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6)
    {
        timer_event = 1;
    }
}

while (1)
{
    if (timer_event)
    {
        timer_event = 0;
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

Khác với HAL_Delay(), CPU vẫn có thể chạy phần còn lại của while (1) trong thời gian chờ. Nếu timer chạy mỗi 500 ms, LED sẽ toggle theo nhịp đó mà không khóa vòng lặp trong nửa giây.

SysTick cũng là một timer time base

Ngay cả khi bạn chưa bật timer phần cứng, HAL thường đã dùng SysTick để tạo time base 1 ms cho HAL_Delay()HAL_GetTick().

Điều này dẫn đến hai lưu ý:

  • Không gọi HAL_Delay() trong ISR nếu chưa hiểu rõ priority của SysTick; ISR có thể chờ chính time base mà nó đang chặn.
  • Nếu thay SysTick bằng timer khác làm HAL time base, vẫn phải giữ cơ chế tick đúng theo đơn vị millisecond mà các timeout của HAL mong đợi.

Vì vậy, hãy xem SysTick là nền thời gian hệ thống, còn timer general-purpose là công cụ tạo event cho ứng dụng.

Ghép cả ba thành một bài test nhỏ

Một bài test dễ kiểm tra là:

  • LED đổi trạng thái mỗi 500 ms nhờ timer.
  • Nút nhấn tạo interrupt và bật/tắt một LED khác hoặc set một event.
  • Vòng lặp chính vẫn để trống chỗ cho UART và sensor ở các bài sau.

Khi test, đừng chỉ nhìn LED. Hãy đặt breakpoint trong callback, xem htim->Instance, kiểm tra GPIO input ở cả hai trạng thái và đo chân bằng multimeter hoặc oscilloscope nếu nhịp không đúng.

Lỗi dễ gặp

LED không sáng

Kiểm tra cực LED, điện trở, GND chung và tên LED_GPIO_Port/LED_Pin. Nếu bạn dùng LED onboard, hãy xem schematic: có board nối LED theo logic active-low.

Nút nhấn không tạo interrupt

Kiểm tra chân có đúng GPIO không, mode có phải GPIO_EXTI không, NVIC đã enable chưa và edge có khớp với mạch pull-up/pull-down không.

Một lần nhấn làm LED đổi nhiều lần

Đó gần như chắc chắn là bounce. Đừng vội tăng độ ưu tiên interrupt; hãy thêm debounce và chỉ xác nhận một event hợp lệ.

Timer chạy nhanh hoặc chậm hơn dự kiến

Kiểm tra clock tree, bus timer, prescaler, period và có gọi đúng HAL_TIM_Base_Start_IT() hay chưa. Một timer chưa được start sẽ không tự chạy chỉ vì bạn đã cấu hình nó trong CubeMX.

Code trong callback không được gọi

Kiểm tra interrupt handler do CubeMX sinh ra có gọi HAL_GPIO_EXTI_IRQHandler() hoặc HAL_TIM_IRQHandler() tương ứng không. Sau đó kiểm tra callback đúng tên cho dòng STM32 đang dùng.

Kết luận

GPIO giúp STM32 giao tiếp với tín hiệu digital. Interrupt giúp firmware phản ứng với sự kiện thay vì liên tục polling. Timer tạo nhịp đều để ứng dụng làm việc theo thời gian mà không phụ thuộc vào các đoạn delay chặn.

Nếu bạn chỉ cần nhớ ba điều sau bài này:

  • Cấu hình GPIO phải khớp với mạch thật, đặc biệt là pull-up/pull-down và logic active-low.
  • ISR cần ngắn; hãy ghi nhận event và xử lý phần nặng trong main loop.
  • Timer không chỉ để làm LED nhấp nháy; nó là nền tảng cho sampling, timeout, debounce và scheduler đơn giản.

P3: UART, I2C và SPI, mình sẽ dùng UART để debug firmware và đọc dữ liệu từ STM32. Khi nhìn thấy log trên máy tính, việc tìm lỗi GPIO, interrupt và timer sẽ bớt giống đoán mò hơn.

Tham khảo và mã nguồn mở

Chia sẻ

LinkedInFacebookX

Tiếp tục khám phá

Đọc tiếp

Bài viết liên quan

Xem thêm trong Bài viết

Nastrotek sử dụng cookie để phân tích truy cập và cá nhân hóa quảng cáo, giúp hiểu cách website được sử dụng. Bạn có thể chấp nhận hoặc từ chối các cookie không thiết yếu. Chính sách bảo mật