Skip to content

Công nghệ 9 giây

“Website hướng dẫn, thử nghiệm và giải thích công nghệ theo cách dễ hiểu cho mọi người.”

Menu
  • Home
  • Công nghệ
  • STM32
  • ESP32
  • Altium Desinger
  • Kiến thức điện máy
Menu

BÀI 37 – TIMER NÂNG CAO TRÊN STM32F103: NHIỀU TASK CHẠY ĐỒNG THỜI KHÔNG DÙNG delay()

Posted on August 23, 2026August 23, 2026 by congnghe9s

BÀI 37 – TIMER NÂNG CAO TRÊN STM32F103: NHIỀU TASK CHẠY ĐỒNG THỜI KHÔNG DÙNG delay()

Ở Bài 36, chúng ta đã tự tạo được:

volatile uint32_t millis = 0;

với:

TIM2
  ↓
Interrupt mỗi 1 ms
  ↓
millis++

Bây giờ chúng ta sẽ dùng millis() để xây dựng một hệ thống nhiều tác vụ chạy đồng thời.

Đây là bước cực kỳ quan trọng trước khi học:

  • Timer nâng cao
  • PWM
  • Input Capture
  • Output Compare
  • UART không blocking
  • State Machine
  • RTOS

1. Vấn đề của chương trình kiểu Arduino đơn giản

Giả sử bạn muốn Blue Pill làm 4 việc:

LED       → mỗi 500 ms
Button    → kiểm tra mỗi 10 ms
ADC       → đọc mỗi 100 ms
LCD       → cập nhật mỗi 1000 ms

Người mới thường viết:

while (1)
{
    LED();
    delay_ms(500);

    Button();
    delay_ms(10);

    ADC();
    delay_ms(100);

    LCD();
    delay_ms(1000);
}

Đây là sai về mặt kiến trúc.

Các tác vụ sẽ phải chờ nhau.


2. Cách chúng ta muốn

Ta muốn:

                 millis()
                    │
        ┌───────────┼───────────┐
        ↓           ↓           ↓
      LED          ADC         LCD
     500 ms       100 ms      1000 ms
        │           │           │
        └───────────┴───────────┘
                    ↓
                  main

Mỗi task tự kiểm tra:

“Đã đến lúc tôi phải chạy chưa?”


3. Đây gọi là Software Timer

Phần cứng chỉ có:

TIM2

nhưng chúng ta tạo ra nhiều timer bằng phần mềm:

Software Timer 1 → LED
Software Timer 2 → ADC
Software Timer 3 → LCD
Software Timer 4 → Button
Software Timer 5 → Motor

Tất cả dựa trên:

millis

4. Một ví dụ cực kỳ quan trọng

uint32_t last_led = 0;

if ((uint32_t)(millis - last_led) >= 500)
{
    last_led = millis;

    LED_Toggle();
}

Trong khi đó:

uint32_t last_adc = 0;

if ((uint32_t)(millis - last_adc) >= 100)
{
    last_adc = millis;

    ADC_Read();
}

Và:

uint32_t last_lcd = 0;

if ((uint32_t)(millis - last_lcd) >= 1000)
{
    last_lcd = millis;

    LCD_Update();
}

Ba task hoạt động độc lập.


5. Hãy hình dung dòng thời gian

Giả sử:

millis = 0

Các task bắt đầu đếm.

100 ms

ADC chạy

200 ms

ADC chạy

300 ms

ADC chạy

400 ms

ADC chạy

500 ms

LED chạy
ADC chạy

600 ms

ADC chạy

1000 ms

LED chạy
ADC chạy
LCD chạy

Không task nào cần delay().


6. Tạo một System Tick

Chúng ta giữ nguyên phần Bài 36:

volatile uint32_t millis = 0;

Timer:

TIM2->PSC = 7199;
TIM2->ARR = 9;

ISR:

void TIM2_IRQHandler(void)
{
    if (TIM2->SR & TIM_SR_UIF)
    {
        TIM2->SR &= ~TIM_SR_UIF;

        millis++;
    }
}

Đây là:

System Tick 1 ms


7. Tại sao không xử lý task trong ISR?

Ví dụ không nên làm:

void TIM2_IRQHandler(void)
{
    millis++;

    LED_Toggle();

    ADC_Read();

    LCD_Update();

    Motor_Control();
}

Tại sao?

Vì ISR chạy mỗi:

1 ms

Nếu trong ISR làm quá nhiều việc:

TIM2 interrupt
      ↓
LED
      ↓
ADC
      ↓
LCD
      ↓
Motor
      ↓
UART
      ↓
...

CPU có thể mất quá nhiều thời gian trong interrupt.


8. Kiến trúc đúng

TIM2_IRQHandler()
       │
       └── millis++

main()
 │
 ├── Task_LED()
 ├── Task_Button()
 ├── Task_ADC()
 ├── Task_LCD()
 └── Task_Motor()

Đây là kiến trúc chúng ta sẽ sử dụng.


9. Tách từng task thành function

Ví dụ:

void Task_LED(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 500)
    {
        last = millis;

        GPIOC->ODR ^= (1 << 13);
    }
}

Button:

void Task_Button(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 10)
    {
        last = millis;

        // Đọc button
    }
}

ADC:

void Task_ADC(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 100)
    {
        last = millis;

        // Đọc ADC
    }
}

LCD:

void Task_LCD(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 1000)
    {
        last = millis;

        // Update LCD
    }
}

10. static ở đây rất quan trọng

Ví dụ:

void Task_LED(void)
{
    static uint32_t last = 0;
}

Tại sao dùng:

static

Nếu viết:

void Task_LED(void)
{
    uint32_t last = 0;
}

thì mỗi lần gọi function:

last = 0

lại từ đầu.

Như vậy Timer sẽ không hoạt động đúng.

Còn:

static uint32_t last = 0;

thì biến vẫn giữ giá trị sau khi function kết thúc.


11. Ví dụ

Lần đầu:

Task_LED()
last = 0

Sau 500 ms:

last = 500

Function kết thúc.

Lần sau gọi:

last vẫn = 500

Sau đó:

millis - last

tiếp tục được tính.


12. Đây là một kỹ thuật C rất quan trọng

Bạn đã học:

Bài 7–13
Function
Struct
Typedef
Enum

Bây giờ những kiến thức đó bắt đầu được sử dụng trong firmware thực tế.


13. Tạo task scheduler đơn giản

Ta có thể viết:

void Scheduler_Run(void)
{
    Task_LED();
    Task_Button();
    Task_ADC();
    Task_LCD();
}

Trong main:

while (1)
{
    Scheduler_Run();
}

Đây chính là một cooperative scheduler rất đơn giản.


14. Vì sao gọi là cooperative?

Bởi vì các task tự nguyện chạy xong rồi trả quyền điều khiển về main.

Ví dụ:

main
 ↓
LED task
 ↓
return
 ↓
Button task
 ↓
return
 ↓
ADC task
 ↓
return
 ↓
LCD task
 ↓
return
 ↓
lặp lại

Không có RTOS.

Không có thread.

Không có context switching.

Nhưng vẫn có thể chạy nhiều tác vụ.


15. Điều kiện cực kỳ quan trọng

Mỗi task phải:

Chạy nhanh và không blocking.

Ví dụ tốt:

void Task_LED(void)
{
    if (...)
    {
        LED_Toggle();
    }
}

Ví dụ xấu:

void Task_LED(void)
{
    LED_ON();

    delay_ms(500);

    LED_OFF();

    delay_ms(500);
}

Task xấu sẽ khóa toàn bộ scheduler.


16. Ví dụ thực tế

Giả sử:

Task LED      → 500 ms
Task Button   → 10 ms
Task ADC      → 20 ms
Task UART     → liên tục
Task LCD      → 1000 ms

Main:

while (1)
{
    Task_LED();
    Task_Button();
    Task_ADC();
    Task_UART();
    Task_LCD();
}

Nếu mỗi task mất:

< 100 µs

thì vòng while có thể chạy rất nhanh.


17. Code hoàn chỉnh

Đây là phiên bản bạn nên chạy thử trên Blue Pill.

#include "stm32f10x.h"

volatile uint32_t millis = 0;


/* =====================================================
   GPIO
   ===================================================== */

void GPIO_Init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    /* PC13 = Output Push-Pull 2 MHz */

    GPIOC->CRH &= ~(0xF << 20);
    GPIOC->CRH |=  (0x2 << 20);
}


/* =====================================================
   TIM2 - 1 ms System Tick
   ===================================================== */

void TIM2_Init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

    /*
        TIM2 clock = 72 MHz

        72 MHz / 7200 = 10 kHz

        10 kHz / 10 = 1 kHz

        1 kHz = 1 ms
    */

    TIM2->PSC = 7199;
    TIM2->ARR = 9;

    TIM2->CNT = 0;

    /* Enable Update Interrupt */
    TIM2->DIER |= TIM_DIER_UIE;

    /* NVIC */
    NVIC_SetPriority(TIM2_IRQn, 2);
    NVIC_EnableIRQ(TIM2_IRQn);

    /* Start Timer */
    TIM2->CR1 |= TIM_CR1_CEN;
}


/* =====================================================
   TIM2 ISR
   ===================================================== */

void TIM2_IRQHandler(void)
{
    if (TIM2->SR & TIM_SR_UIF)
    {
        TIM2->SR &= ~TIM_SR_UIF;

        millis++;
    }
}


/* =====================================================
   TASK LED
   ===================================================== */

void Task_LED(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 500)
    {
        last = millis;

        GPIOC->ODR ^= (1 << 13);
    }
}


/* =====================================================
   TASK 2
   ===================================================== */

void Task_2(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 100)
    {
        last = millis;

        /*
            Công việc Task 2
        */
    }
}


/* =====================================================
   TASK 3
   ===================================================== */

void Task_3(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 1000)
    {
        last = millis;

        /*
            Công việc Task 3
        */
    }
}


/* =====================================================
   SCHEDULER
   ===================================================== */

void Scheduler_Run(void)
{
    Task_LED();

    Task_2();

    Task_3();
}


/* =====================================================
   MAIN
   ===================================================== */

int main(void)
{
    GPIO_Init();

    TIM2_Init();

    while (1)
    {
        Scheduler_Run();
    }
}

18. Điều gì đang thực sự xảy ra?

CPU liên tục chạy:

Scheduler
   ↓
Task_LED
   ↓
Task_2
   ↓
Task_3
   ↓
Scheduler
   ↓
Task_LED
   ↓
...

Trong khi đó cứ mỗi 1 ms:

TIM2
 ↓
Interrupt
 ↓
millis++
 ↓
return

19. Hãy nhìn vào timeline

Giả sử:

millis = 0

millis = 100

Task_2()

millis = 200

Task_2()

millis = 500

Task_LED()
Task_2()

millis = 1000

Task_LED()
Task_2()
Task_3()

millis = 1500

Task_LED()
Task_2()

millis = 2000

Task_LED()
Task_2()
Task_3()

Đây chính là nhiều task chạy theo chu kỳ riêng.


20. Một vấn đề tinh tế: last = millis hay last += interval?

Chúng ta đang viết:

last = millis;

Ví dụ task:

interval = 100 ms

Nếu task chạy đúng lúc:

100
200
300
400

thì không vấn đề.

Nhưng nếu scheduler bị bận và task chạy muộn:

100
205
310
415

thì chu kỳ thực tế bị trôi.


21. Cách thứ hai

Ta có thể viết:

last += interval;

Ví dụ:

if ((uint32_t)(millis - last) >= 100)
{
    last += 100;

    Task();
}

Nếu:

last = 100
millis = 205

thì:

last = 200

Thời gian mục tiêu được giữ ổn định hơn.


22. Hai kiểu có ý nghĩa khác nhau

Kiểu 1

last = millis;

Có nghĩa:

“Tính khoảng thời gian tiếp theo kể từ lúc task thực sự chạy.”

Phù hợp cho:

  • button polling;
  • timeout;
  • các tác vụ không cần chu kỳ chính xác.

Kiểu 2

last += interval;

Có nghĩa:

“Giữ nhịp theo lịch định trước.”

Phù hợp cho:

  • sampling;
  • control loop;
  • periodic task.

23. Ví dụ

void Task_ADC(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 10)
    {
        last += 10;

        ADC_Read();
    }
}

Nếu ADC cần:

10 ms

thì cách này giữ nhịp tốt hơn.


24. Nhưng có một vấn đề nữa

Nếu CPU bị bận quá lâu:

last = 100
millis = 1000

và:

last += 10;

chỉ xử lý một lần.

Bạn có thể cần:

while ((uint32_t)(millis - last) >= 10)
{
    last += 10;

    ADC_Read();
}

Nhưng không nên lạm dụng vì có thể làm task chạy nhiều lần liên tiếp và làm scheduler càng bận hơn.


25. Scheduler của chúng ta chưa phải RTOS

Điều này rất quan trọng.

Chúng ta đang có:

Timer Interrupt
+
Software Timer
+
Cooperative Scheduler

Nhưng chưa có:

Thread
Task Priority
Context Switching
Stack riêng
Mutex
Semaphore

Đó là những thứ chúng ta sẽ gặp khi học RTOS.


26. Vậy tại sao phải học cách này trước RTOS?

Bởi vì khi bạn hiểu:

Timer
Interrupt
millis
Task
Scheduler

thì khi chuyển sang FreeRTOS bạn sẽ hiểu:

RTOS đang giải quyết những vấn đề gì.

Nếu chưa hiểu những thứ này mà nhảy ngay vào RTOS, rất dễ trở thành:

copy code
↓
chạy được
↓
không hiểu tại sao

27. Đây cũng là nền tảng cho robot của bạn

Ví dụ sau này Blue Pill điều khiển:

HC-SR04
28BYJ-48
Servo
LCD
Button
UART

Bạn có thể thiết kế:

Task_Ultrasonic() → 50 ms

Task_Motor()      → 10 ms

Task_Servo()      → 20 ms

Task_LCD()        → 200 ms

Task_Button()     → 10 ms

Task_UART()       → liên tục

Tất cả dựa trên:

TIM2
 ↓
millis

28. Một ví dụ gần với dự án thực tế của bạn

Giả sử:

28BYJ-48

cần điều khiển theo từng bước.

Không nên:

StepMotor();
delay_ms(5);
StepMotor();
delay_ms(5);

Thay vào đó:

void Task_Stepper(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 5)
    {
        last += 5;

        StepMotor_NextStep();
    }
}

Khi đó CPU có thể làm việc khác trong thời gian giữa hai bước.


29. Một ví dụ với cảm biến khoảng cách

Sau này:

void Task_Ultrasonic(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 50)
    {
        last += 50;

        Ultrasonic_StartMeasurement();
    }
}

Motor:

void Task_Motor(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 10)
    {
        last += 10;

        Motor_Control();
    }
}

LCD:

void Task_LCD(void)
{
    static uint32_t last = 0;

    if ((uint32_t)(millis - last) >= 200)
    {
        last += 200;

        LCD_Update();
    }
}

Đây chính là kiểu kiến trúc bạn sẽ gặp rất nhiều trong embedded.


30. Một khái niệm mới: Task Period

Mỗi task có:

Period

Ví dụ:

Task Motor      Period = 10 ms
Task Button     Period = 10 ms
Task Sensor     Period = 50 ms
Task LCD        Period = 200 ms
Task LED        Period = 500 ms

Ta có thể lập bảng:

Task Period
Motor 10 ms
Button 10 ms
Sensor 50 ms
LCD 200 ms
LED 500 ms

31. Một nguyên tắc rất quan trọng

Period của task không được nhỏ hơn khả năng xử lý thực tế của task.

Ví dụ:

Task LCD mất 5 ms

thì không hợp lý khi đặt:

LCD period = 1 ms

Vì:

Task cần 5 ms
nhưng cứ 1 ms lại muốn chạy

Scheduler sẽ bị quá tải.


32. CPU utilization

Đây là khái niệm bạn sẽ gặp nhiều hơn về sau.

Ví dụ:

Motor task:
period = 10 ms
execution = 0.2 ms

Tỷ lệ sử dụng:

[
0.2/10 = 2%
]

Nếu có:

Motor = 2%
Sensor = 1%
LCD = 5%
UART = 3%

tổng khoảng:

11%

Hệ thống còn khá nhiều thời gian rảnh.


33. Nếu tổng tải quá cao?

Ví dụ:

Motor = 30%
Sensor = 25%
LCD = 20%
UART = 20%
Other = 15%

Tổng:

110%

Không thể đáp ứng tất cả deadline.

Đây là lúc cần:

tối ưu code
↓
giảm tần suất task
↓
DMA
↓
Interrupt
↓
Timer khác
↓
hoặc RTOS

34. Bài 37 đã đưa chúng ta đến một khái niệm mới

Bạn hãy ghi nhớ:

Hardware Timer
      ↓
System Tick
      ↓
Software Timer
      ↓
Periodic Task
      ↓
Cooperative Scheduler

Đây là cả một chuỗi kiến thức.


35. Bài tập thực hành trên Blue Pill

Hãy tạo 4 task:

LED        → 500 ms
BUTTON     → 10 ms
ADC        → 100 ms
LCD        → 1000 ms

Trong đó hiện tại chưa cần phần cứng ADC/LCD thật.

Bạn có thể dùng biến đếm:

volatile uint32_t led_count = 0;
volatile uint32_t adc_count = 0;
volatile uint32_t lcd_count = 0;

hoặc đơn giản hơn dùng static.


36. Bài tập nâng cao

Hãy thử tạo:

Task 1 → 10 ms
Task 2 → 20 ms
Task 3 → 50 ms
Task 4 → 100 ms
Task 5 → 1000 ms

Không được dùng:

delay()

và không được tạo thêm Timer phần cứng.

Chỉ sử dụng:

TIM2
+
millis

37. Bài tập đặc biệt

Hãy thử trả lời câu hỏi này:

Nếu millis = 4294967290 và sau đó Timer tiếp tục chạy khiến millis quay về 0, đoạn code sau có còn hoạt động không?

if ((uint32_t)(millis - last_time) >= 100)
{
    last_time = millis;
}

Có.

Đây chính là lý do chúng ta dùng phép trừ với kiểu uint32_t.


38. Những điều cần nhớ sau Bài 37

1.

millis++

được thực hiện trong Timer ISR.

2.

Task không nên chạy trong ISR.

3.

Mỗi task có thể có một static last.

4.

Không dùng delay() trong scheduler.

5.

Công thức:

(uint32_t)(millis - last) >= period

là cực kỳ quan trọng.

6.

Một Timer phần cứng có thể tạo ra rất nhiều Software Timer.

7.

Các task phải ngắn và non-blocking.

8.

Đây là một cooperative scheduler đơn giản, chưa phải RTOS.


39. Sơ đồ kiến trúc bạn nên ghi vào vở

                    STM32F103
                        │
                        ▼
                      TIM2
                        │
                   1 ms interrupt
                        │
                        ▼
                     millis++
                        │
                        ▼
                  ┌───────────┐
                  │   main    │
                  │ scheduler │
                  └─────┬─────┘
                        │
       ┌────────────────┼────────────────┐
       │                │                │
       ▼                ▼                ▼
   Task Motor       Task Sensor       Task LCD
    10 ms              50 ms          200 ms
       │                │                │
       └────────────────┼────────────────┘
                        │
                        ▼
                    Task LED
                    500 ms

🎯 Bài 37 bạn cần làm được 3 việc

Mức 1 – bắt buộc

Tạo TIM2 interrupt mỗi 1 ms và tăng:

volatile uint32_t millis;

Mức 2

Điều khiển LED mỗi 500 ms mà không dùng delay().

Mức 3 – quan trọng nhất

Chạy đồng thời:

Task A → 10 ms
Task B → 100 ms
Task C → 500 ms
Task D → 1000 ms

chỉ bằng một TIM2.

Nếu làm được 3 mức này, bạn đã bắt đầu có tư duy firmware embedded thực thụ, chứ không còn chỉ là viết từng đoạn code điều khiển GPIO nữa.

Bài 38 hợp lý tiếp theo sẽ là TIMER INPUT/OUTPUT COMPARE – hiểu sâu CNT, PSC, ARR, CCR và cách Timer có thể tự tạo sự kiện chính xác mà CPU không cần liên tục kiểm tra.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recent Posts

  • Bạn có thể hướng dẫn tôi quy trình sửa chữa một mainboard laptop cũng như mainboard của máy tính để bàn không?
  • Có những mức nguồn DC nào trên một bo mạch mainboard laptop?Kỹ thuật đốt dòng để kiểm tra nguồn laptop được thực hiện như thế nào?
  • bài tiếp theo hợp lý là: BÀI 63 – SENSOR FUSION THỰC CHIẾN: ENCODER + IMU MPU6050 + EKF + ODOMETRY X,Y,θ TRÊN STM32F103
  • BÀI 62 – EKF (EXTENDED KALMAN FILTER) CHO ROBOT 2 BÁNH: FUSION ENCODER + IMU + ƯỚC LƯỢNG X, Y, θ, V, ω
  • BÀI 61 – KALMAN FILTER CHO ENCODER + IMU: ƯỚC LƯỢNG TỐC ĐỘ, GÓC θ VÀ SENSOR FUSION

Recent Comments

No comments to show.

Archives

  • September 2026
  • August 2026
  • July 2026
  • June 2026
  • May 2026
  • February 2026

Categories

  • Altium Desinger
  • Công nghệ
  • ESP32
  • Khác
  • Kiến thức điện máy
  • STM32
Live 3D Globes Visitor
©2026 Công nghệ 9 giây | Design: Newspaperly WordPress Theme