Lập trình STM32 với Keil C: Tạo project & Blink LED

Bài viết mở đầu này hướng dẫn lập trình STM32 với Keil C từ con số 0: cài đặt, tạo project cho STM32F103C8T6, và viết chương trình đầu tiên – nháy LED PC13 rồi đọc nút nhấn – bằng cách thao tác trực tiếp với thanh ghi GPIO. Cách code trên thanh ghi giúp bạn hiểu rõ bản chất từng ngoại vi trước khi chuyển sang dùng thư viện chuẩn ở các bài sau.

Keil C là gì?

Hiện nay có khá nhiều trình biên dịch ngôn ngữ C cho vi điều khiển như CubeIDE, Arduino IDE, Keil C… Những phần mềm này được gọi là môi trường phát triển tích hợp (IDE – Integrated Development Environment), với các vai trò chính:

  • Trình soạn thảo ngôn ngữ C, Assembly.
  • Trình biên dịch, hỗ trợ debug – phát hiện và sửa lỗi câu lệnh.
  • Biên dịch mã nguồn thành file hex để nạp vào vi xử lý.

Keil C là phần mềm chuyên dụng tạo môi trường lập trình cho nhiều dòng vi xử lý (ARM, AVR, 8051, PIC…) với hai ngôn ngữ chủ yếu là C và Assembly.

Tạo project đầu tiên với Keil C

Chuẩn bị

  • Cài bộ thư viện chuẩn của MCU cho Keil C (ở đây là dòng F1 của STM32 – STM32F103C8T6).
  • Cài ST-Link Driver để nạp và debug code.
  • Tải Datasheet, Reference Manual của MCU và KIT.

Các bước tạo project

Ở màn hình chính Keil C, vào Project → New uVision Project, chọn thư mục lưu và đặt tên. Sau đó Keil hiện cửa sổ chọn dòng MCU – ở đây chọn STM32F103C8:

Chọn MCU STM32F103C8 trong cửa sổ Select Device của Keil uVision
Hình 1. Chọn đúng dòng MCU STM32F103C8 khi tạo project trong Keil

Nhấn OK, Keil chuyển sang cửa sổ Manage Run-Time Environment để thêm các file và thư viện cần thiết (StdPeriph Drivers, Startup…):

Cửa sổ Manage Run-Time Environment trong Keil để thêm thư viện StdPeriph cho STM32F1
Hình 2. Thêm thư viện qua Manage Run-Time Environment

Tạo file main.c và add vào project:

Thêm file main.c vào Source Group của project Keil STM32
Hình 3. Tạo và thêm file main.c vào project

Như vậy project đã được tạo thành công – bạn có thể thử Build để kiểm tra lỗi khi link thư viện. Tóm lại, để lập trình MCU bằng Keil C: tải thư viện của MCU, tạo project mới, thêm các thư viện liên quan, cài driver nạp/debug và chuẩn bị tài liệu hướng dẫn.

Blink LED PC13

Kit Blue Pill có sẵn một user LED nối tới chân 13 của GPIOC (PC13), nên ta dùng nó để test hoạt động trong bài đầu tiên:

Sơ đồ nguyên lý LED nối chân PC13 trên kit Blue Pill STM32F103 (tích cực thấp)
Hình 4. LED trên kit nối PC13 – tích cực thấp (sáng khi PC13 = 0)

Cấu hình clock

Để cấu hình hoạt động cho ngoại vi, trước hết cần cấp xung clock cho chúng qua các bus. Các ngoại vi được cấp clock qua:

  • AHB (Advanced High Speed Bus): bus kết nối hệ thống.
  • APB1, APB2 (Advanced Peripheral Bus 1, 2): bus kết nối thiết bị ngoại vi, nối với hệ thống qua AHB.
Sơ đồ khối STM32F103 thể hiện các bus AHB, APB1, APB2 cấp clock cho ngoại vi
Hình 5. Sơ đồ khối STM32F103 – ngoại vi được cấp clock từ AHB/APB1/APB2

Các GPIO được cấp clock từ APB2. Bus APB2 được cấu hình bởi thanh ghi RCC_APB2ENR (APB2 peripheral clock enable register):

Thanh ghi RCC_APB2ENR với bit IOPCEN cấp clock cho GPIOC trên STM32F103
Hình 6. Thanh ghi RCC_APB2ENR – bit IOPCEN cấp clock cho GPIOC

GPIOC được cấp xung clock khi bit IOPCEN được set lên 1. Trong thư viện Keil C, thanh ghi này nằm trong struct RCC_TypeDef, truy cập bằng RCC->APB2ENR.

Cấu hình chế độ hoạt động của chân

Sau khi cấp clock, cần cấu hình chân trong thanh ghi Port configuration register, gồm hai thanh ghi CRL (chân 0–7) và CRH (chân 8–15):

Thanh ghi GPIOx_CRL cấu hình các chân 0 đến 7 của Port trên STM32
Hình 7. GPIOx_CRL – cấu hình chân 0–7
Thanh ghi GPIOx_CRH cấu hình các chân 8 đến 15 của Port trên STM32
Hình 8. GPIOx_CRH – cấu hình chân 8–15

Mỗi chân được xác định bởi một cặp bit CNFy (chế độ) và một cặp bit MODEy (hướng & tốc độ):

Ý nghĩa các bit CNFy và MODEy trong thanh ghi cấu hình GPIO của STM32F1
Hình 9. Ý nghĩa các bit CNFy và MODEy

Bảng tra nhanh chế độ chân theo CNF/MODE (tham khảo Reference Manual STM32F1, mục 9.2.1–9.2.2):

Chế độ chân MODE[1:0] CNF[1:0]
Input Analog 00 00
Input Floating 00 01
Input Pull-up/Pull-down 00 10
Output Push-Pull 01/10/11 00
Output Open-Drain 01/10/11 01

MODE: 01 = Output 10 MHz, 10 = Output 2 MHz, 11 = Output 50 MHz. Ví dụ cấu hình PC13 làm Output Push-Pull tốc độ 50 MHz thì ở GPIOC_CRH, cặp bit MODE13 = 11CNF13 = 00:

Bảng cấu hình bit Port (Table 20, 21) trong Reference Manual STM32F1
Hình 10. Bảng cấu hình bit Port (Table 20 & 21, Reference Manual STM32F1)
/* File main.c – cấu hình PC13 Output Push-Pull 50 MHz */
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;               // Cấp clock cho GPIOC

GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); // Reset bit CNF13 & MODE13

GPIOC->CRH |= GPIO_CRH_MODE13_0;   // MODE13[1:0] = 11: Output 50 MHz
GPIOC->CRH |= GPIO_CRH_MODE13_1;
GPIOC->CRH &= ~GPIO_CRH_CNF13_0;   // CNF13[1:0] = 00: Output Push-Pull
GPIOC->CRH &= ~GPIO_CRH_CNF13_1;

Ghi giá trị ra chân để nháy LED

Trạng thái các chân được xác định bởi thanh ghi Port output data register (GPIOx_ODR). Thay đổi bit ODR13 sẽ điều khiển LED ở PC13:

while(1){
    GPIOC->ODR |= 1<<13;      // PC13 = 1
    delay(10000000);
    GPIOC->ODR &= ~(1<<13);   // PC13 = 0
    delay(10000000);
}

Theo schematic, LED PC13 tích cực thấp: LED sáng khi PC13 = 0 (mức Logic 0) và tắt khi PC13 = 1. Hàm delay đơn giản tạo trễ bằng vòng lặp rỗng tiêu tốn chu kỳ máy:

void delay(__IO uint32_t timedelay){
    for(int i = 0; i < timedelay; i++){}
}

Đọc trạng thái nút nhấn để điều khiển LED

Tiếp theo ta đọc trạng thái một nút nhấn để điều khiển LED PC13. Nút nhấn khi nhấn có thể kéo chân lên 1 hoặc về 0 tùy cách lắp:

Sơ đồ lắp nút nhấn kiểu Pull-Up Resistor và Pull-Down Resistor cho GPIO STM32
Hình 11. Hai cách lắp nút nhấn: Pull-Up và Pull-Down Resistor
  • Pull-Up Resistor: chân ở mức 1 khi chưa nhấn, về 0 khi nhấn.
  • Pull-Down Resistor: chân ở mức 0 khi chưa nhấn, lên 1 khi nhấn.

Cấu hình chân nút nhấn (PA0 – Input Pull-Up)

Bài này chọn cách lắp Pull-Up, chân đọc là PA0. Ta cấu hình PA0 làm Input pull-up và đặt bit ODR0 = 1 (mức khi nút chưa nhấn):

/* File main.c – cấu hình PA0 Input Pull-Up */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;               // Cấp clock cho GPIOA

GPIOA->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0);   // Reset bit CNF0 & MODE0

GPIOA->CRL &= ~GPIO_CRL_MODE0_0;   // MODE0 = 00: Input mode
GPIOA->CRL &= ~GPIO_CRL_MODE0_1;
GPIOA->CRL |= GPIO_CRL_CNF0_1;     // CNF0 = 10: Input pull-up/pull-down
GPIOA->CRL &= ~GPIO_CRL_CNF0_0;
GPIOA->ODR |= GPIO_ODR_ODR0;       // ODR0 = 1: chọn pull-up

Đọc trạng thái qua thanh ghi IDR

Thanh ghi Input Data Register (GPIOx_IDR) phản ánh mức tín hiệu tại chân. Với cách lắp Pull-Up: IDR0 = 1 là nút chưa nhấn, IDR0 = 0 là nút được nhấn. Đọc nút bằng câu lệnh if:

if((GPIOA->IDR & (1<<0)) == 0){        // PA0 = 0 → nút được nhấn
    while((GPIOA->IDR & (1<<0)) == 0);  // chờ nhả phím (chống dội)
    // Do something...
}

Vòng lặp while giúp ổn định (chống dội) phím. Kết hợp với hàm đảo trạng thái LED:

void toggle(){
    if(GPIOC->ODR & (1<<13)){   // Đọc trạng thái LED
        GPIOC->ODR = 0<<13;      // Bật LED nếu đang tắt (tích cực thấp)
    } else {
        GPIOC->ODR = 1<<13;      // Tắt LED
    }
}

Chỉ cần gọi toggle() sau mỗi lần xác nhận nút nhấn là LED đảo trạng thái:

if((GPIOA->IDR & (1<<0)) == 0){
    while((GPIOA->IDR & (1<<0)) == 0);
    toggle();
}

Bạn có thể mắc thêm nhiều LED, nhiều nút nhấn để thử các ví dụ khác. Có hai kiểu mắc LED – tích cực caotích cực thấp – ứng với hai giá trị bật/tắt khác nhau:

Hai kiểu mắc LED tích cực cao và tích cực thấp với GPIO STM32
Hình 12. Mắc LED tích cực thấp (trái) và tích cực cao (phải)

Tổng kết và mở rộng

Qua bài mở đầu, bạn đã biết cách tạo project bằng Keil C và test hoạt động cơ bản với ngoại vi GPIO của STM32 bằng thanh ghi. Code trên thanh ghi giúp hiểu rõ cách hoạt động chi tiết của ngoại vi. Từ bài sau, ta sẽ chuyển sang dùng bộ thư viện chuẩn (SPL) với các API dễ tiếp cận hơn – xem bài GPIO trong STM32 (thư viện SPL).

Để thấy sự khác biệt giữa code thanh ghi và dùng API, ta có thể tự viết các hàm bọc lại thao tác thanh ghi. Ví dụ hàm ghi tín hiệu ra một chân:

void WritePin(GPIO_TypeDef *GPIO_Port, uint8_t Pin, uint8_t state){
    if(state == HIGH)
        GPIO_Port->ODR |= (1<<Pin);
    else
        GPIO_Port->ODR &= ~(1<<Pin);
}

Và hàm cấu hình một chân theo Port/Pin/MODE/CNF:

void GPIO_Config(GPIO_TypeDef *GPIO_Port, uint8_t Pin, uint8_t MODE, uint8_t CNF){
    uint8_t CMD;
    if(Pin > 7){
        CMD = (CNF<<2) | MODE;
        GPIO_Port->CRH |= (CMD<<(4*(Pin-8)));
    } else {
        CMD = (CNF<<2) | MODE;
        GPIO_Port->CRL |= (CMD<<(4*Pin));
    }
}

Về cơ bản, các hàm trong thư viện chuẩn cũng dùng thanh ghi để cấu hình như vậy, nhưng được đóng gói thành API trực quan, giúp người dùng dễ hiểu và code nhanh hơn.

Kết luận

Bạn vừa hoàn thành chương trình STM32 đầu tiên với Keil C: tạo project cho STM32F103, nháy LED PC13 và đọc nút nhấn PA0 – tất cả bằng thao tác thanh ghi GPIO (RCC_APB2ENR, CRL/CRH, ODR, IDR). Đây là nền tảng để bạn hiểu sâu cách vận hành phần cứng trước khi dùng thư viện chuẩn cho các ngoại vi phức tạp hơn trong những bài tiếp theo.

Leave a Reply