Hướng dẫn nạp code cho XNUCLEO STM32 F411RE (STM32CubeMX & Keil C)

Tài liệu này hướng dẫn chi tiết các bước để cấu hình, viết mã, biên dịch và nạp chương trình cho board Nucleo-F411RE sử dụng phần mềm STM32CubeMX và Keil C.

Giới thiệu Board Phát Triển

Bài hướng dẫn này được thực hiện trên board phát triển XNUCLEO-F411RE. Đây là một board có layout tương tự các board Nucleo của ST, với đầy đủ các chân cắm mở rộng theo chuẩn Arduino. Sơ đồ chân (pinout) của board được minh họa trong hình dưới đây, giúp bạn dễ dàng xác định vị trí các đèn LED và nút nhấn mà chúng ta sẽ sử dụng.

Sơ đồ chân board XNUCLEO

Trong hướng dẫn này, chúng ta sẽ tập trung vào:

  • USER_BUTTON (BTN): Nối với chân PC13.
  • USER_LED1: Nối với chân PA5 (Thường là LED có sẵn trên các board Nucleo).
  • USER_LED2, LED3, LED4: Nối với các chân PC9, PC8, và PC5.

Lưu ý: Mặc dù hình ảnh có thể ghi model khác (F103RB), sơ đồ vị trí các LED và nút nhấn này tương thích với cấu hình cho F411RE mà chúng ta đang sử dụng.

Kết nối mạch nạp

Kết nối mạch nạp với xnucleo STM32F411RE

Phần 1: Cấu hình chi tiết trong STM32CubeMX

Bước 1: Pinout & Configuration

Đây là bước cấu hình các chân và các ngoại vi cơ bản.

  1. Cấu hình Debug: Để có thể nạp code và gỡ lỗi, bạn cần kích hoạt giao thức Serial Wire Debug (SWD).
    • Trong mục Categories bên trái, chọn System Core > SYS.
    • Trong phần Debug, chọn Serial Wire. Thao tác này sẽ dành riêng chân PA13 và PA14 cho việc debug.
  2. Cấu hình Debug
  3. Cấu hình chân GPIO:
    • Nút nhấn (BTN): Tìm chân PC13, click vào và chọn GPIO_Input. Sau đó, click chuột phải vào chân PC13, chọn Enter User Label và đặt tên là BTN. Trong bảng cấu hình bên dưới (GPIO Mode and Configuration), đảm bảo chế độ là Internal Pull-up.
    • Các đèn LED: Lần lượt tìm các chân PA5, PC9, PC8, PC5. Click vào từng chân và chọn GPIO_Output. Đặt User Label tương ứng là LED1, LED2, LED3, và LED4.
  4. Cấu hình chân GPIO

Sau khi hoàn tất, giao diện của bạn sẽ trông giống như hình dưới đây:

Hình ảnh cấu hình chân trong STM32CubeMX

Bước 2: Clock Configuration

Đây là nơi bạn thiết lập tốc độ hoạt động cho vi điều khiển.

  1. Chuyển sang tab Clock Configuration.
  2. Board Nucleo-F411RE sử dụng thạch anh tần số cao bên ngoài (HSE) được cung cấp bởi mạch ST-LINK. Chúng ta sẽ cấu hình hệ thống để chạy ở tốc độ tối đa là 100 MHz.
  3. Trong sơ đồ cây xung nhịp, tìm đến ô HCLK (MHz), nhập giá trị 100 và nhấn Enter. STM32CubeMX sẽ tự động tính toán và đề xuất một cấu hình hợp lệ. Bạn chỉ cần nhấn OK để chấp nhận.
Hình ảnh cấu hình Clock trong STM32CubeMX

Bước 3: Project Manager & Code Generation

Đây là bước cuối cùng để thiết lập thông tin project và tạo mã nguồn.

  1. Chuyển sang tab Project Manager.
  2. Trong mục Project:
    • Project Name: Đặt tên cho project (ví dụ: XNUCLEO_STM32F411RE).
    • Project Location: Nhấn Browse và chọn thư mục bạn muốn lưu project.
    • Toolchain / IDE: Chọn MDK-ARM từ danh sách thả xuống.
    Project Manager
  3. Trong mục Code Generator:
    • Đảm bảo bạn đã chọn "Copy all used libraries into the project folder".
    • Nên chọn "Generate peripheral initialization as a pair of '.c/.h' files per peripheral" để cấu trúc code gọn gàng hơn.
    Code Generator:

Cuối cùng, nhấn vào nút GENERATE CODE ở góc trên bên phải. STM32CubeMX sẽ tạo ra toàn bộ cấu trúc project cho Keil C. Sau khi tạo xong, một hộp thoại sẽ hiện ra, nhấn vào Open Project để mở project trong Keil C.

Phần 2: Viết mã, Biên dịch và Nạp chương trình

Bước 1: Viết mã điều khiển trong Keil C

Sau khi project được mở trong Keil C, bạn cần tìm đến file main.c. File này nằm trong cây thư mục bên trái, thường là trong Application/User/Core. Chúng ta sẽ thêm code vào 2 vị trí: khai báo biến trước vòng lặp while(1) và logic xử lý bên trong vòng lặp đó.

Logic chương trình mới sẽ như sau: mỗi lần nhấn nút, chương trình sẽ chuyển trạng thái để bật lần lượt từng đèn LED, theo chu kỳ: Tắt hết -> LED1 sáng -> LED2 sáng -> LED3 sáng -> LED4 sáng -> Tắt hết.

Đầu tiên, thêm đoạn code sau vào giữa /* USER CODE BEGIN 2 *//* USER CODE END 2 */ để khai báo các biến cần thiết.


/* USER CODE BEGIN 2 */
uint8_t ledState = 0; // 0:Tat, 1:LED1, 2:LED2, 3:LED3, 4:LED4
/* USER CODE END 2 */
            

Tiếp theo, thay thế toàn bộ code trong vòng lặp while(1) (giữa /* USER CODE BEGIN 3 *//* USER CODE END 3 */) bằng đoạn code logic mới dưới đây.


while (1)
{
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
  // 1. Kiểm tra xem nút BTN có được nhấn không (dùng Label)
  if (HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET)
  {
    // 2. Chống dội phím đơn giản bằng delay
    HAL_Delay(20); 

    // 3. Tăng biến trạng thái để chuyển LED
    ledState++;

    // 4. Nếu trạng thái vượt quá 4, quay về 0 để lặp lại chu trình
    if (ledState > 4)
    {
      ledState = 0;
    }

    // 5. Tắt tất cả các LED trước khi bật LED mới (dùng Label)
    HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(LED4_GPIO_Port, LED4_Pin, GPIO_PIN_RESET);

    // 6. Dùng switch-case để bật LED tương ứng với trạng thái (dùng Label)
    switch (ledState)
    {
      case 1: // Sáng LED1
        HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
        break;
      case 2: // Sáng LED2
        HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET);
        break;
      case 3: // Sáng LED3
        HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin, GPIO_PIN_SET);
        break;
      case 4: // Sáng LED4
        HAL_GPIO_WritePin(LED4_GPIO_Port, LED4_Pin, GPIO_PIN_SET);
        break;
      case 0: // Tắt tất cả
        // Không cần làm gì vì đã tắt hết ở trên
        break;
    }

    // 7. Chờ cho đến khi nút được nhả ra hoàn toàn
    // Điều này ngăn chương trình chạy lặp lại khi bạn nhấn giữ nút
    while (HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET);
    HAL_Delay(50); // Thêm một khoảng delay nhỏ sau khi nhả nút
  }
  /* USER CODE END 3 */
}
Lưu ý: Các tên như BTN_GPIO_Port, BTN_Pin, LED1_GPIO_Port, v.v. được STM32CubeMX tự động định nghĩa trong file main.h dựa trên User Label bạn đã đặt.

Bước 2: Biên dịch (Build) và Nạp code (Download)

Sau khi đã thêm code, bạn cần thực hiện các bước sau để nạp chương trình xuống board.

  1. Cấu hình Debugger:
    • Nhấn vào nút "Options for Target" (biểu tượng cây đũa thần màu đỏ).
    • Trong hộp thoại hiện ra, chuyển sang tab "Debug".
    • Ở menu thả xuống bên phải, chọn ST-Link Debugger.
    • Nhấn vào nút "Settings" bên cạnh. Chuyển sang tab "Flash Download". Đảm bảo rằng tùy chọn Reset and Run đã được tích. Điều này sẽ giúp board tự động chạy chương trình ngay sau khi nạp xong. Nhấn OK để đóng các hộp thoại.
    Hình ảnh cấu hình ST-Link Debugger trong Keil C
  2. Biên dịch (Build):
    • Nhấn vào nút "Build" (phím tắt F7) hoặc "Rebuild all target files".
    • Quan sát cửa sổ "Build Output" ở phía dưới. Nếu không có lỗi (0 Errors), bạn đã sẵn sàng để nạp code.
    Biên dịch (Build)
  3. Nạp Code (Download):
    • Kết nối board Nucleo với máy tính qua cổng USB.
    • Nhấn vào nút "Download" (phím tắt F8). Keil C sẽ nạp file đã biên dịch (.hex) vào bộ nhớ Flash của vi điều khiển.
    • Cửa sổ "Build Output" sẽ hiển thị tiến trình nạp và thông báo "Flash download finished" khi hoàn tất.
    Nạp Code (Download)

Phần 3: Kết quả

Sau khi nạp code thành công, chương trình sẽ bắt đầu chạy.

  • Khi bạn chưa nhấn nút BTN (nút màu xanh trên board), tất cả các đèn LED (LED1, LED2, LED3, LED4) sẽ tắt.
  • Khi bạn nhấn nút BTN, thì LED sẽ được sáng lần lượt từ LED1 -> LED4. Mỗi lần nhấn sẽ chuyển sang LED kế tiếp.

Chúc mừng! Bạn đã nạp thành công chương trình đầu tiên của mình lên board STM32 Nucleo.

Mô phỏng

LED1
LED2
LED3
LED4
// Khi nhấn nút...
ledState++;
if (ledState > 4) { ledState = 0; }

// Tắt hết LED
HAL_GPIO_WritePin(..., GPIO_PIN_RESET);

// Bật LED tương ứng
switch (ledState)
{
  case 1: HAL_GPIO_WritePin(LED1, GPIO_PIN_SET); break;
  case 2: HAL_GPIO_WritePin(LED2, GPIO_PIN_SET); break;
  case 3: HAL_GPIO_WritePin(LED3, GPIO_PIN_SET); break;
  case 4: HAL_GPIO_WritePin(LED4, GPIO_PIN_SET); break;
}

Nhận xét

Mới hơn Cũ hơn