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:

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…):

Tạo file main.c và add 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:

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.

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):

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):


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 độ):

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 = 11 và CNF13 = 00:

/* 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:

- 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 cao và tích cực thấp – ứng với hai giá trị bật/tắt khác nhau:

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.


