I. Chuẩn bị thiết yếu trước khi phát triển: Tài liệu và môi trường
1. Danh sách tài liệu bắt buộc (chìa khóa tránh lỗi)
Khi tiếp cận bất kỳ MCU nào, cần xây dựng kiến thức nền tảng thông qua ba loại tài liệu chính:- Sổ tay dữ liệu chip (Datasheet): Xác định lõi CPU, dung lượng bộ nhớ, thông số nguồn (ví dụ: điện áp hoạt động 3.3V cho STM32F103).
- Sổ tay tham chiếu (Reference Manual): Cung cấp bản đồ thanh ghi ngoại vi (ví dụ: địa chỉ cơ sở GPIOA_BASE = 0x40020000U) và mô tả chức năng chi tiết.
- Sơ đồ mạch in (Schematic): Làm rõ kết nối chân (ví dụ: UART_TX nối với RX của module chuyển USB sang serial).
Mẹo đọc tài liệu: Bắt đầu bằng phần "Tổng quan" để có cái nhìn tổng thể, sau đó tập trung vào chương "Cấu hình thanh ghi" tương ứng với ngoại vi mục tiêu. Đánh dấu các bit quan trọng (ví dụ: bit chọn chế độ ở GPIO_MODER).
2. Thiết lập môi trường phát triển đa nền tảng
a) Lựa chọn công cụ
| Loại công cụ | STM32 đề xuất | ESP32 đề xuất |
|---|---|---|
| IDE | Keil MDK, STM32CubeIDE | VS Code + PlatformIO, ESP-IDF |
| Công cụ cấu hình | STM32CubeMX (cấu hình trực quan) | ESP-IDF Menuconfig |
| Bộ gỡ lỗi | ST-Link, J-Link | J-Link, USB CDC |
b) Kiểm tra môi trường – 3 bước cơ bản
- Tạo dự án tối giản: Sử dụng CubeMX để cấu hình cây xung nhịp (ví dụ: dùng HSE + PLL tạo xung hệ thống 72MHz cho STM32).
- Biên dịch và chạy chớp LED: Dùng hàm HAL_GPIO_TogglePin để đảo mức logic, kiểm tra chức năng GPIO.
- Kết nối bộ gỡ lỗi: Chạy chế độ Debug trong Keil, theo dõi sự thay đổi giá trị thanh ghi (ví dụ: bit0 của GPIO_ODR).
II. Phát triển 4 ngoại vi cốt lõi: Từ thanh ghi đến thư viện
1. GPIO – Điều khiển cơ bản nhất
Nguyên lý底层: GPIO hoạt động thông qua 4 nhóm thanh ghi: MODER (chế độ), OTYPER (loại ngõ ra), OSPEEDR (tốc độ), ODR (dữ liệu). Ví dụ điều khiển LED qua PA0 trên STM32:
#define GPIOA_BASE_ADDR 0x40020000U
#define MODER_REG (*(volatile uint32_t*)(GPIOA_BASE_ADDR + 0x00))
#define OUTPUT_DATA_REG (*(volatile uint32_t*)(GPIOA_BASE_ADDR + 0x14))
void led_gpio_init(void) {
// Bật clock cho cổng A
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// Đặt PA0 thành ngõ ra đẩy kéo
MODER_REG &= ~(3U << 0);
MODER_REG |= (1U << 0);
}
void toggle_led_pin(void) {
OUTPUT_DATA_REG ^= (1U << 0); // Đảo bit để chớp sáng
}
Dùng thư viện HAL (STM32):
void init_led_hal(void) {
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef config = {0};
config.Pin = GPIO_PIN_0;
config.Mode = GPIO_MODE_OUTPUT_PP;
config.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &config);
}
// Trong vòng lặp chính
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
HAL_Delay(500);
2. Hệ thống xung nhịp – "nhịp tim" của hệ thống
Khái niệm cốt lõi: Nguồn xung gồm nội bộ (HSI/LSI) và bên ngoài (HSE/LSE), được nhân tần bằng PLL để tạo xung hệ thống. Ví dụ cấu hình xung 168MHz cho STM32F407 từ HSE 8MHz:
void configure_system_clock(void) {
RCC_OscInitTypeDef osc_config = {0};
RCC_ClkInitTypeDef clk_config = {0};
osc_config.OscillatorType = RCC_OSCILLATORTYPE_HSE;
osc_config.HSEState = RCC_HSE_ON;
osc_config.PLL.PLLState = RCC_PLL_ON;
osc_config.PLL.PLLSource = RCC_PLLSOURCE_HSE;
osc_config.PLL.PLLM = 8; // Chia 8MHz → 1MHz
osc_config.PLL.PLLN = 168; // Nhân lên 168MHz
osc_config.PLL.PLLP = 2; // Chia APB1 xuống 84MHz
HAL_RCC_OscConfig(&osc_config);
clk_config.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1;
clk_config.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk_config.APB1CLKDivider = RCC_HCLK_DIV2;
HAL_RCC_ClockConfig(&clk_config, FLASH_LATENCY_5);
}
Lưu ý quan trọng:
- Luôn bật clock cho ngoại vi trước khi sử dụng (ví dụ: thiết lập RCC->APB2ENR cho UART1).
- Với tần số cao, cần cấu hình thời gian chờ bộ nhớ (FLASH_LATENCY_5 cho 168MHz).
3. UART – Giao tiếp không đồng bộ
Kết nối phần cứng: USART1_TX (PA9) → RX của bộ chuyển USB, USART1_RX (PA10) → TX, nối mass chung.
Nhận dữ liệu theo ngắt (HAL):
UART_HandleTypeDef uart_handle;
uint8_t rx_buffer[1];
void setup_uart(void) {
uart_handle.Instance = USART1;
uart_handle.Init.BaudRate = 115200;
uart_handle.Init.WordLength = UART_WORDLENGTH_8B;
uart_handle.Init.StopBits = UART_STOPBITS_1;
uart_handle.Init.Parity = UART_PARITY_NONE;
uart_handle.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(&uart_handle);
HAL_UART_Receive_IT(&uart_handle, rx_buffer, 1);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart == &uart_handle) {
HAL_UART_Transmit(huart, rx_buffer, 1, 100);
HAL_UART_Receive_IT(huart, rx_buffer, 1);
}
}
So sánh với ESP32:
#include "driver/uart.h"
void esp32_uart_setup(void) {
const uart_config_t config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
uart_param_config(UART_NUM_0, &config);
uart_set_pin(UART_NUM_0, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, -1, -1);
uart_driver_install(UART_NUM_0, 1024, 0, 0, NULL, 0);
}
4. I2C – Giao tiếp cảm biến phổ biến
Chuẩn bị phần cứng: Kết nối SCL (PB6), SDA (PB7) tới BMP280, thêm điện trở kéo lên 4.7kΩ.
Đọc dữ liệu nhiệt độ từ BMP280:
I2C_HandleTypeDef i2c_handle;
uint8_t sensor_addr = 0x48 << 1;
uint8_t raw_data[2];
void init_i2c(void) {
i2c_handle.Instance = I2C1;
i2c_handle.Init.ClockSpeed = 100000;
i2c_handle.Init.DutyCycle = I2C_DUTYCYCLE_2;
i2c_handle.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
HAL_I2C_Init(&i2c_handle);
}
float read_temperature_bmp280(void) {
// Gửi địa chỉ thanh ghi nhiệt độ
HAL_I2C_Master_Transmit(&i2c_handle, sensor_addr, 0xFA, 1, 100);
// Đọc 2 byte dữ liệu
HAL_I2C_Master_Receive(&i2c_handle, sensor_addr, raw_data, 2, 100);
// Tính toán nhiệt độ
int16_t temp_raw = (raw_data[0] << 8) | raw_data[1];
return (temp_raw / 16.0f) - 40.0f;
}
III. Gỡ lỗi và tối ưu: Giải quyết vấn đề thực tế
1. Ba bước xử lý lỗi ngoại vi
- Cấp phần cứng: Dùng đồng hồ vạn năng đo điện áp, máy phân tích logic kiểm tra dạng sóng I2C/SPI.
- Cấp thanh ghi: Dùng chế độ Debug để kiểm tra trạng thái thanh ghi (ví dụ: bit TC trong UART_SR).
- Cấp mã nguồn: Kiểm tra bật clock, cấu hình chân đa năng (AF mode).
2. Tối ưu tiêu thụ năng lượng
- STM32: Tắt clock ngoại vi không dùng, đặt GPIO ở chế độ kéo xuống.
- ESP32: Dùng ULP coprocessor cho tác vụ nhẹ, tắt Wi-Fi/Bluetooth để giảm dòng ngủ xuống 5μA.
3. Xử lý treo hệ thống
Kiểm tra các nguyên nhân:- Gọi HAL_Delay() bên trong ngắt (gây deadlock do phụ thuộc SysTick).
- Ghi sai vào thanh ghi chỉ đọc (ví dụ: sửa trực tiếp SPI_SR).
- Xung đột bus: Địa chỉ I2C trùng, tín hiệu CS SPI không được kích hoạt đúng.
IV. Hướng phát triển nâng cao
- Nắm vững lớp dưới cùng: Viết driver PWM bằng thanh ghi thuần, đọc ADC đa kênh.
- Tích hợp hệ thống: Kết nối ngoại vi với FreeRTOS (dùng queue truyền dữ liệu UART).
- Hỗ trợ đa nền tảng: So sánh khác biệt giữa STM32 và ESP32 (ví dụ: ESP32 không có CAN tích hợp).