| Vi điều khiển | STM32F103C8 (LQFP48, 64KB Flash, 20KB RAM) |
| Công cụ build | Keil MDK-ARM, ARM Compiler V5.06 update 6 (ARMCC5), HAL do STM32CubeMX sinh |
| Driver công suất | SLM2009 (3-phase gate driver, 6 MOSFET) |
| Phản hồi vị trí | 3 cảm biến Hall (đọc bằng polling) |
| Giao tiếp | UART3 115200bps - debug log + khung lệnh điều khiển |
| Trạng thái dự án | Đang bring-up phần cứng, chạy chế độ "test quay tay" (production flow đã viết sẵn, chờ gắn cảm biến dòng thật) |
| Ngày tạo tài liệu | 25/08/2026 (bản cập nhật) |
Dự án DieuKhienBLDC điều khiển một động cơ BLDC 3 pha để vận hành một cánh cửa tự động
(mở/đóng theo 2 chiều THUẬN - NGHỊCH). Bộ điều khiển dùng vi điều khiển STM32F103C8,
mã nguồn sinh bởi STM32CubeMX (lớp HAL) và được bổ sung thêm một lớp thư viện riêng (BSP / Motor /
Debug) trong thư mục MyLibrary để xử lý các chức năng đặc thù của BLDC.
Hệ thống gồm 4 khối chức năng chính:
Run_Task() nhưng
hiện BỊ BYPASS bởi một chế độ tạm thời gọi là "test quay tay": nhấn nút FORWARD/REVERSE
sẽ quay một số vòng cố định rồi tự dừng, hoàn toàn open-loop theo Hall, không đọc ADC để bảo vệ. Xem
chi tiết ở mục 13.
| Chân MCU | Tín hiệu | Ngoại vi | Ghi chú |
|---|---|---|---|
| PA8 | WH (High-side pha W) | TIM1_CH1 | PWM, điều chế duty |
| PA9 | VH (High-side pha V) | TIM1_CH2 | PWM, điều chế duty |
| PA10 | UH (High-side pha U) | TIM1_CH3 | PWM, điều chế duty |
| PB15 | UL (Low-side pha U) | GPIO | Cực tính LIN ĐẢO PHA - xem mục 7 |
| PB14 | VL (Low-side pha V) | GPIO | Cực tính LIN ĐẢO PHA - xem mục 7 |
| PB13 | WL (Low-side pha W) | GPIO | Cực tính LIN ĐẢO PHA - xem mục 7 |
| Chân MCU | Ký hiệu | Vai trò |
|---|---|---|
| PB4 | Hall_A | Bit 0 (LSB) của mã Hall 3-bit |
| PB5 | Hall_B | Bit 2 (MSB) của mã Hall 3-bit |
| PB0 | Hall_C | Bit 1 (giữa) của mã Hall 3-bit |
Cả 3 chân đọc trong CÙNG một lần đọc thanh ghi GPIOB->IDR để tránh "xé" (tearing)
giữa các bit khi Hall đổi rất nhanh - xem mục 5.
| Index buffer | Kênh ADC | Chân | Ý nghĩa |
|---|---|---|---|
| [0] | ADC_CHANNEL_0 | PA0 (sscurent) | Dòng pha A - chưa đấu nối vật lý, trị số trôi nổi |
| [1] | ADC_CHANNEL_1 | PA1 (sscurenB) | Dòng pha B - kênh thật, đang dùng cho stall-detect |
| [2] | ADC_CHANNEL_2 | PA2 (sscurenC) | Dòng pha C - kênh thật, đang dùng cho stall-detect |
| [3] | ADC_CHANNEL_3 | PA3 | VR5 - tốc độ chạy chậm (creep) |
| [4] | ADC_CHANNEL_4 | PA4 | VR4 - % quãng đường chậm cuối đoạn ĐÓNG |
| [5] | ADC_CHANNEL_5 | PA5 | VR6 - thời gian chờ trước khi tự động đóng |
| [6] | ADC_CHANNEL_6 | PA6 | VR3 - tốc độ đóng cửa |
| [7] | ADC_CHANNEL_7 | PA7 | VR2 - % quãng đường chậm cuối đoạn MỞ |
| [8] | ADC_CHANNEL_9 | PB1 | VR1 - tốc độ mở cửa |
| Tín hiệu | Vai trò | Xử lý |
|---|---|---|
| forward | Nút bấm mở cửa thủ công | Debounce 5 lần đọc, phát hiện sườn lên |
| reverce | Nút bấm đóng cửa thủ công | Debounce 5 lần đọc, phát hiện sườn lên |
| ssopendoor | Cảm biến người/vật (PIR...) kích hoạt mở cửa tự động | Debounce 2 lần đọc (phản hồi nhanh hơn nút bấm) |
| auto80 | Công tắc chọn chế độ chỉ mở 80% hành trình | Debounce 5 lần đọc |
| PB12 (SSCR_U) | Tín hiệu báo quá dòng từ phần cứng (driver/cảm biến dòng riêng) | Đọc trực tiếp, KHÔNG debounce (cần bắt được cả xung ngắn), có chốt (latch) qua phần mềm |
PA15 và PB3 mặc định thuộc chức năng JTAG - BSP_GPIO_Init() chủ động gỡ bỏ (remap SWJ-NOJTAG)
để giải phóng 2 chân này cho mục đích khác.
| Ngoại vi | Tốc độ | Vai trò |
|---|---|---|
| USART3 (TX/RX qua DMA) | 115200 bps | Xuất log debug (Hall/dòng điện) VÀ nhận khung lệnh điều khiển mở/đóng cửa - xem mục 15 |
Mã nguồn được chia thành 3 lớp trên nền HAL do CubeMX sinh (Core/Src):
| Lớp | Thư mục | File chính | Trách nhiệm |
|---|---|---|---|
| BSP (Board Support Package) |
MyLibrary/BSP |
bsp_hall.c | Đọc 3 bit Hall từ GPIOB->IDR |
bsp_pwm.c | Điều khiển 6 MOSFET (3 PWM High-side + 3 GPIO Low-side), xử lý đảo cực LIN của SLM2009 | ||
bsp_adc.c | Khởi động ADC DMA vòng, cung cấp giá trị tức thời + peak-hold cho từng kênh | ||
bsp_gpio.c | Debounce nút bấm/cảm biến, đọc/chốt tín hiệu báo quá dòng SSCR_U | ||
bsp_uart.c | Gọi TX DMA, vòng đệm RX DMA (đọc con trỏ DMA để lấy byte mới) | ||
bsp_led.c | Điều khiển LED báo trạng thái (phụ trợ) | ||
| Motor | MyLibrary/Motor |
motor_commutation.c | Thuật toán 6-step commutation, hãm, đảo hướng (xem mục 6) |
motor_position.c | Đếm step Hall tuyệt đối, quy đổi ra số vòng cơ | ||
motor_speed.c | Đo chu kỳ Hall qua TIM3 (1 tick = 1us) để tính RPM điện/cơ và xác định chiều quay | ||
motor_pid.c, motor_profile.c, motor.c | Dự bị cho vòng điều khiển tốc độ kín (PID) - hiện chưa được kích hoạt trong luồng chính | ||
| Debug / Command | MyLibrary/Debug |
debug_uart.c | Định dạng và gửi các dòng log qua UART3 (Hall, đỉnh dòng, chuỗi Hall) |
command.c | Bộ phân tích khung lệnh nhị phân từ UART3 RX (máy trạng thái + CRC8) | ||
| Ứng dụng | Core/Src |
main.c |
Khởi tạo hệ thống, vòng lặp chính 7 bước, máy trạng thái vận hành Run_Task() |
Core/Src/*.c (main.c, usart.c, adc.c, stm32f1xx_it.c...) được CubeMX SINH LẠI mỗi khi
regenerate từ file .ioc. Mọi nội dung nằm NGOÀI cặp /* USER CODE BEGIN X */ ... /* USER CODE END X */
sẽ bị GHI ĐÈ / XÓA. Trong quá trình bring-up, dự án này đã từng mất một fix quan trọng (USART3 IRQ handler)
vì fix được đặt sai vị trí - đã được chuyển vào đúng cặp USER CODE để tồn tại qua các lần regenerate tiếp theo
(xem mục 16, mục 1).
Mỗi vòng lặp while(1) trong main() thực hiện tuần tự 7 bước sau:
Hall_Read() lấy mã Hall thô, chỉ chấp nhận
giá trị mới vào hall_confirmed sau HALL_DEBOUNCE_COUNT (3) lần đọc liên tiếp
giống nhau và hợp lệ.hall_confirmed vừa đổi
so với lần commutate trước, gọi Motor_Speed_Update() và Motor_Position_Update(),
đồng thời ghi lại mốc thời gian hall_last_change_tick (dùng cho watchdog stall ở mục 11).BSP_GPIO_Update() debounce
forward/reverce/ssopendoor/auto80; riêng forward/reverce chỉ quan tâm sườn lên (bấm 1 lần là chạy hết chặng).
Bước 3b song song poll khung lệnh UART3 (Command_Poll()). Bước 3c cập nhật đỉnh dòng PA1/PA2 và
chốt báo quá dòng SSCR_U mỗi vòng lặp để không bỏ sót xung ngắn.RUN_STATE_TEST_IDLE)
và có sườn lên forward/reverce (hoặc lệnh UART tương ứng), chuyển sang trạng thái quay tương ứng, đặt
test_spin_start_position/test_spin_start_tick, reset đỉnh dòng, và áp duty
"kick" khởi động (mục 10).Run_Task(hall_confirmed), nơi
chứa toàn bộ máy trạng thái vận hành (mục 13): tính duty theo ramp, gọi commutation, kiểm tra điều kiện
chuyển trạng thái (tới đích, stall, hết thời gian brake...).hall_confirmed đổi, gửi ngay dòng
"Hall=abc" (nếu UART rảnh) và gom vào bộ đệm 6 phần tử để gửi thêm 1 dòng tổng hợp 1 chu kỳ
điện khi đủ 6 bước.DEBUG_CURRENT_PRINT_INTERVAL_MS (100ms),
nếu UART rảnh, gửi dòng "ADC=xxxx" với giá trị MAX(PA1, PA2) - dùng để quan sát/hiệu chỉnh
ngưỡng stall thực tế.Motor_Commutation_Direction(): tắt hết MOSFET + dead time +
bật lại) CHỈ được gọi khi Hall hoặc hướng quay THỰC SỰ thay đổi - không gọi vô điều kiện mỗi vòng lặp. Nếu
chỉ cần đổi độ rộng xung (duty) mà bước Hall không đổi, dùng Motor_Commutation_UpdateDuty() -
hàm này chỉ ghi lại giá trị CCR, không đụng lại GPIO Low-side, tránh hiện tượng "chopping" làm dòng không
liên tục.
ĐIỂM BẮT ĐẦU → chân GPIO nút bấm forward
(mục 2.4) được đọc và debounce 5 lần đọc liên tiếp trong
BSP_GPIO_Update() (bsp_gpio.c) → hệ thống phát hiện
sườn lên (rising edge) của tín hiệu đã debounce này (bước 3, mục 4)
→ nếu đang ở trạng thái rảnh RUN_STATE_TEST_IDLE, vòng lặp chính
(bước 4, mục 4) chuyển trạng thái sang RUN_STATE_TEST_SPIN_FWD, lưu
test_spin_start_position/test_spin_start_tick, áp duty "kick" khởi động ban
đầu MOTOR_KICK_DUTY_PERCENT (mục 10) → mỗi vòng lặp kế tiếp, trong hàm
Run_Task(hall_confirmed) (bước 5, mục 4), nhánh
case RUN_STATE_TEST_SPIN_FWD gọi Motor_RampDuty_Generic()
(mục 9) tính duty hiện tại theo progress (accel → run → decel) → gọi
Motor_Commutation_Direction() (Hall/hướng vừa đổi) hoặc
Motor_Commutation_UpdateDuty() (chỉ đổi duty) (mục 6.2), tra bảng chuyển mạch CHIỀU THUẬN (mục 6.1, dùng THẲNG mã Hall đọc được - KHÔNG còn
dịch bước, xem mục 6.4) để chọn đúng cặp High-PWM / Low-ON → giá trị
duty được ghi vào thanh ghi Compare (CCR) của TIM1 cho đúng kênh High-side
(PA8/PA9/PA10 = WH/VH/UH, mục 8), đồng thời BSP_PWM_xx_On()/Off() điều
khiển đúng chân Low-side GPIO (PB13/14/15 = WL/VL/UL) theo cực tính đảo của SLM2009 (mục 7) →
ĐIỂM KẾT THÚC: xung PWM thực tế xuất hiện trên chân High-side tương ứng,
điều chế bởi TIM1 - đây là tín hiệu điều khiển trực tiếp đưa vào driver công suất SLM2009 để quay động cơ.
Sơ đồ thể hiện nhánh RUN_STATE_TEST_SPIN_FWD của Run_Task() - nhánh
TEST_SPIN_REV giống hệt về cấu trúc, chỉ đổi dấu tính progress, đổi tham số
direction = -1, và tra bảng chuyển mạch NGHỊCH thay vì THUẬN.
Hall_Read() (trong bsp_hall.c) đọc CẢ 3 bit Hall trong CÙNG một lần truy cập
thanh ghi GPIOB->IDR để tránh hiện tượng "xé" (tearing) - tình huống 2 lần đọc riêng lẻ
rơi vào đúng lúc một bit vừa đổi, cho ra tổ hợp Hall không từng tồn tại thực sự.
uint8_t Hall_Read(void)
{
uint32_t idr = GPIOB->IDR;
uint8_t u = (idr >> 4) & 1U; /* PB4 = Hall_A -> bit0 */
uint8_t v = idr & 1U; /* PB0 = Hall_C -> bit1 */
uint8_t w = (idr >> 5) & 1U; /* PB5 = Hall_B -> bit2 */
return (w << 2) | (v << 1) | u;
}
Giá trị thô này được đưa qua bộ lọc debounce trong vòng lặp chính (bước 1, mục 4): một giá trị Hall chỉ
được chấp nhận vào hall_confirmed - biến DUY NHẤT mà toàn bộ logic điều khiển (commutation,
vị trí, tốc độ, máy trạng thái, log UART) sử dụng - sau khi đọc ổn định HALL_DEBOUNCE_COUNT = 3
lần liên tiếp VÀ là một trong 6 mã hợp lệ (Hall_IsValid(): 001, 101, 100, 110, 010, 011 -
loại bỏ 000 và 111 là lỗi cảm biến/dây đứt).
Đây là thuật toán trung tâm của toàn bộ hệ thống, cài đặt trong motor_commutation.c. BLDC
3 pha điều khiển theo phương pháp "trapezoidal 6-step": tại mỗi thời điểm chỉ 2 trong 3 pha dẫn dòng
(1 pha High-side PWM, 1 pha Low-side ON cố định), pha còn lại thả nổi (dùng để cảm nhận back-EMF trong
các thiết kế cảm biến-less, nhưng dự án này dùng cảm biến Hall thật nên không cần).
| Thứ tự Hall (chu kỳ) | |
|---|---|
| Chiều THUẬN | 001 -> 101 -> 100 -> 110 -> 010 -> 011 -> (001) |
| Chiều NGHỊCH | 001 -> 011 -> 010 -> 110 -> 100 -> 101 -> (001) |
Luồng CHIỀU THUẬN (chú thích): xuất phát từ Hall = 001 (điểm BẮT ĐẦU của chu kỳ) → 101 → 100 → 110 → 010 → 011 → quay VÒNG LẶP LẠI về 001 (không có điểm "kết thúc" - động cơ còn quay thuận thì chuỗi này lặp lại vô hạn). Trạng thái hiện tại (sau khi HOÀN TÁC lead-commutation, xem mục 6.4): vector PWM/Low áp dụng ĐÚNG BẰNG mã Hall vừa đọc được - KHÔNG còn dịch trước 1 bước như bản thử nghiệm trước đó.
| Hall | High PWM | Low ON |
|---|---|---|
| 001 | U | V |
| 101 | U | W |
| 100 | V | W |
| 110 | V | U |
| 010 | W | U |
| 011 | W | V |
| Hall | High PWM | Low ON |
|---|---|---|
| 001 | V | U |
| 101 | W | U |
| 100 | W | V |
| 110 | U | V |
| 010 | U | W |
| 011 | V | W |
Bảng NGHỊCH chính là bảng THUẬN với High và Low HOÁN ĐỔI cho nhau trên cùng một cặp pha - đã được đối chiếu từng dòng và xác nhận khớp 100% với comment đầu file trong lần rà soát gần nhất.
Motor_Commutation_Direction(hall, direction) - gọi khi Hall HOẶC hướng
quay THỰC SỰ thay đổi so với bước trước (active_hall/active_direction).
Nếu không đổi gì, hàm thoát ngay (không làm gì). Nếu có đổi: BSP_PWM_AllOff() tắt toàn bộ
6 MOSFET -> chờ dead time -> bật lại đúng cặp MOSFET High/Low theo bảng chuyển
mạch của hướng tương ứng.
Motor_Commutation_UpdateDuty() - gọi khi CHỈ cần đổi độ rộng xung (duty),
Hall/hướng không đổi. Chỉ ghi lại giá trị Compare (CCR) của kênh PWM đang active, KHÔNG tắt/đụng lại
GPIO Low-side, KHÔNG dead time - đảm bảo dòng điện liên tục, không bị "bấm/nháp" (chopping) ở tần số
vòng lặp.
/* Dead time hiện tại: vòng lặp NOP - KHÔNG chính xác tuyệt đối về thời gian,
có thể bị ảnh hưởng bởi ngắt/trình biên dịch. Chỉ phù hợp cho mục đích
test ở duty thấp; cần thay bằng DWT cycle counter hoặc timer riêng
nếu triển khai công suất thật. */
static void Motor_DeadTime(void)
{
volatile uint32_t i;
for (i = 0; i < 50; i++) { __NOP(); }
}
Motor_Commutation_Brake(): tắt toàn bộ PWM High-side (duty=0) và BẬT CẢ 3 Low-side cùng lúc,
ngắn mạch 3 cuộn dây qua Low-side. Dòng cảm ứng sinh ra từ sức điện động ngược (rotor còn quay do quán
tính) bị ngắn mạch tạo mô-men hãm ngược chiều quay, giúp dừng nhanh hơn đáng kể so với cắt điện hoàn toàn
(coast). Không cần dead time ở bước này vì không có High-side nào PWM cùng lúc với Low-side đang bật.
Giải pháp đã THỬ NGHIỆM là kỹ thuật "lead commutation" chuẩn cho trường hợp Hall bị lệch
pha: dịch bảng THUẬN đi 1 bước qua hàm Hall_ThuanNext() (dùng vector của bước Hall KẾ TIẾP
trong chuỗi thuận thay vì bước hiện tại), áp dụng trong cả Motor_Commutation_Direction() và
Motor_Commutation_UpdateDuty(), chỉ cho nhánh THUẬN. Kết quả đã xác nhận qua máy phân tích
logic (Saleae Logic 2): chiều THUẬN tự khởi động và chạy đúng qua nhiều chu kỳ liên tục sau khi áp dụng.
motor_commutation.c, cả 2 nhánh
Motor_Commutation_Direction() và Motor_Commutation_UpdateDuty() đã trả về
switch (hall) / switch (active_hall) THẲNG (không qua hàm dịch bước nào cả).
Build ARMCC5 xác nhận sạch: 0 Error(s), 0 Warning(s),
Program Size: Code=14884 RO-data=312 RW-data=172 ZI-data=2332. Hệ quả đã biết:
đây là trạng thái TRƯỚC KHI có fix, từng thể hiện hiện tượng chiều THUẬN không tự khởi động được (cần mồi
tay) ngay cả ở duty cao (55%) - nếu gặp lại hiện tượng này, đó là hành vi ĐÃ BIẾT của cấu hình hiện tại,
không phải lỗi mới phát sinh.
| Hall | High PWM | Low ON |
|---|---|---|
| 001 | U | V |
| 101 | U | W |
| 100 | V | W |
| 110 | V | U |
| 010 | W | U |
| 011 | W | V |
| Hall đọc được | High PWM | Low ON |
|---|---|---|
| 001 | U | W |
| 101 | V | W |
| 100 | V | U |
| 110 | W | U |
| 010 | W | V |
| 011 | U | V |
| Hall | High PWM | Low ON |
|---|---|---|
| 001 | V | U |
| 101 | W | U |
| 100 | W | V |
| 110 | U | V |
| 010 | U | W |
| 011 | V | W |
| Hall đọc được | High PWM | Low ON |
|---|---|---|
| 001 | V | W |
| 011 | U | W |
| 010 | U | V |
| 110 | W | V |
| 100 | W | U |
| 101 | V | U |
2 bảng bên phải chỉ mang tính THAM KHẢO cho tương lai (nếu cần thử nghiệm lại lead-commutation) - hiện KHÔNG có dòng code nào áp dụng chúng. Cả 2 chiều THUẬN và NGHỊCH đang dùng đúng 1 bảng duy nhất (cột trái) tra thẳng theo mã Hall đọc được, giống hệt cách NGHỊCH vẫn luôn hoạt động.
Hall_ThuanNext() như
bảng tham khảo ở trên và gọi nó trong 2 hàm commutation chỉ cho nhánh THUẬN; hoặc (2) ưu tiên gắn tải cơ
khí thật trước (cánh cửa thật) rồi đánh giá lại - vì lần xác nhận gần nhất (Saleae Logic 2, file
reversekoquay.csv, mục 16 #8) cho thấy đây có thể là hiện tượng thiếu mô-men khởi
động ở điều kiện KHÔNG TẢI chứ không hoàn toàn là lỗi lệch góc điện.
Driver công suất SLM2009 điều khiển các chân Low-side (LIN -> LO) theo cực tính NGƯỢC so với High-side (HIN -> HO):
| Tín hiệu điều khiển | Quan hệ |
|---|---|
| HIN (High-side) | IN PHASE với HO: HIN=1 -> HO=1 (ON) |
| LIN (Low-side) | OUT OF PHASE với LO: LIN=0 -> LO=1 (ON), LIN=1 -> LO=0 (OFF) |
XX_On() phải ghi RESET (0V) để LO thực sự lên mức 1 (ON); hàm
XX_Off() phải ghi SET để LO về 0 (OFF). Điều này đã được xác nhận bằng
cả datasheet gốc lẫn đo thực tế trên board (đo được LIN=0V trong khi LO vẫn dao động theo HIN, vì
LIN=0 đang "xin" LO=ON). Nếu code vô tình đảo lại logic này sẽ gây Low-side ON/OFF sai hoàn toàn so
với ý đồ, nguy cơ shoot-through.
void BSP_PWM_UL_On(void)
{
HAL_GPIO_WritePin(UL_GPIO_Port, UL_Pin, GPIO_PIN_RESET); /* LIN=0 -> LO=1 (ON) */
}
void BSP_PWM_UL_Off(void)
{
HAL_GPIO_WritePin(UL_GPIO_Port, UL_Pin, GPIO_PIN_SET); /* LIN=1 -> LO=0 (OFF) */
}
Cùng logic áp dụng cho VL/WL. Đây là một trong những điểm dễ gây nhầm lẫn nhất khi đọc raw log tín hiệu (logic analyzer) - cần luôn quy đổi qua bảng này trước khi kết luận một pha có đang "ON" hay không (xem thêm mục 16, hạng mục kiểm tra shoot-through).
3 kênh PWM High-side dùng chung TIM1 với ARR = 3199 (Period). Duty phần trăm được quy đổi sang giá trị Compare theo công thức:
compare = ((ARR + 1) * percent) / 100; /* percent được giới hạn 0..100 trước đó */ /* ví dụ: 0% -> 0 | 50% -> ~1600 | 100% -> ~3200 (giới hạn lại tối đa 3199) */
BSP_PWM_SetDutyPercent() áp dụng CÙNG một giá trị duty cho cả 3 kênh (dùng khi cần đặt đồng
loạt, ví dụ lúc hãm điện). Trong vận hành bình thường, module motor_commutation.c tự chọn
ĐÚNG kênh cần PWM (U/V/W) theo bảng chuyển mạch ở mục 6, các kênh còn lại được đặt về 0 bởi
BSP_PWM_AllOff() trước khi chuyển bước.
Duty không được áp đặt ngay mức "chạy" (run) mà tăng/giảm dần theo dạng hình thang, thực hiện bởi
Motor_RampDuty_Params() - hàm dùng chung cho cả chế độ test-spin lẫn chế độ production (VR).
Hàm nhận vào progress (số step đã đi kể từ lúc bắt đầu) và tính duty theo 3 vùng:
progress < accel_steps, duty tăng tuyến tính từ duty_accel_start lên duty_run.progress nằm giữa vùng tăng tốc và điểm bắt đầu giảm tốc, giữ nguyên duty_run.progress >= target_steps - decel_steps, duty giảm tuyến tính từ duty_run xuống duty_decel_min khi tiến tới target_steps.
Hàm này được tham số hóa đầy đủ (duty_accel_start, duty_run, duty_decel_min,
accel_steps, decel_steps) để dùng chung cho cả:
Motor_RampDuty_Generic()): dùng hằng số cố định
MOTOR_TEST_DUTY_ACCEL_START / MOTOR_TEST_DUTY_RUN / MOTOR_TEST_DUTY_DECEL_MIN
và MOTOR_ACCEL_STEPS/MOTOR_DECEL_STEPS (4 vòng điện mỗi bên, 24 step/vòng).prod_fwd_run_duty, prod_fwd_creep_duty, prod_fwd_decel_steps... -
xem mục 14).
Mục đích: thắng "điểm kết" cơ khí/từ lúc rotor đứng yên bằng cách ép duty CỐ ĐỊNH cao hơn
duty_accel_start trong MOTOR_KICK_DURATION_MS (300ms) đầu tiên ngay sau khi
nhận lệnh quay, trước khi chuyển về ramp bình thường - tương tự thao tác "mồi tay" nhưng bằng điện.
#define MOTOR_KICK_DUTY_PERCENT MOTOR_TEST_DUTY_ACCEL_START /* đang = accel_start -> VÔ HIỆU HÓA */
#define MOTOR_KICK_DURATION_MS 300U
MOTOR_TEST_DUTY_ACCEL_START, tức vô hiệu hóa kick (vì
accel_start tự nó đã đảm nhận vai trò này ở mức độ thấp hơn, an toàn hơn khi đang
test lúc CHƯA gắn tải cơ khí thật).
Dùng trong luồng production (calibration tìm giới hạn cơ khí, phát hiện vật cản khi đóng cửa). Đọc
MAX(PA1, PA2) - dòng pha B và pha C, hai kênh đã xác nhận có đấu nối thật - so với
ngưỡng MOTOR_STALL_ADC_THRESHOLD. Lý do lấy MAX cả 2 kênh: tại một thời điểm chuyển mạch,
BLDC chỉ dẫn dòng qua 2/3 pha, nên phải đảm bảo không bỏ sót trường hợp kẹt đứng vào đúng bước mà pha B
hoặc pha C đang là pha dẫn dòng.
#define MOTOR_STALL_ADC_THRESHOLD 2048U /* placeholder - CẦN HIỆU CHỈNH BẰNG THỰC NGHIỆM */ #define MOTOR_STALL_DEBOUNCE_COUNT 5U /* số lần đọc VƯỢT ngưỡng liên tiếp để xác nhận */
sscurent, dự định là kênh dòng pha A) hiện chưa được đấu nối vật lý -
đọc giá trị trôi nổi ngẫu nhiên (khoảng 1780-2076) không phản ánh dòng điện thật, nên KHÔNG còn được
dùng trong Motor_StallDetected() nữa. Ngưỡng 2048 chỉ là điểm giữa thang đo ADC 12-bit
(0..4095), chưa được đối chiếu với mạch cảm biến dòng thật - PHẢI hiệu chỉnh lại trước khi đưa vào
sản xuất, bằng cách quan sát log ADC=xxxx (mục 15) lúc chạy bình thường và lúc bị chặn/vật cản.
Vì RUN_STATE_TEST_SPIN_FWD/REV không đọc ADC để bảo vệ (chế độ test tạm thời), cần một lớp
bảo vệ riêng: nếu hall_confirmed không đổi trong MOTOR_STALL_TIMEOUT_MS kể từ
lần đổi Hall gần nhất, PWM được cắt NGAY (không chờ brake) để tránh dòng cao liên tục gây sụt áp nguồn
hoặc hỏng MOSFET/driver.
| Giá trị | Giai đoạn | Lý do |
|---|---|---|
| 1000ms | Ban đầu | Giá trị an toàn mặc định |
| 400ms | Hạ xuống để debug | Giảm thời gian chịu dòng cao nếu kẹt thật; nhưng sau khi hạ, chiều NGHỊCH (trước đó tự khởi động tốt) lại cần mồi tay - dấu hiệu quá trình thắng ma sát tĩnh cần nhỉnh hơn 400ms |
| 700ms (hiện tại) | Nâng lại | Trung gian giữa an toàn (ngắn hơn nhiều so với 1000ms cũ) và đủ thời gian chờ khởi động tự nhiên |
PB12 là tín hiệu báo quá dòng từ mạch ngoài (đọc trực tiếp, không debounce). BSP_GPIO_WasCurrentTripUHighSinceReset()
chốt (latch) trạng thái này từ lần lên mức cao gần nhất, chỉ được reset chủ động qua BSP_GPIO_ResetCurrentTripULatch()
(gọi mỗi lần bắt đầu một lần test-spin mới). Kết hợp với đỉnh dòng PA1/PA2 (peak-hold), giá trị này được
gửi qua UART khi watchdog stall kích hoạt (dòng "PA1max=.. PA2max=.. SSCR_U=..") để phục vụ
phân tích sau sự cố.
Vị trí được biểu diễn bằng bộ đếm step Hall tuyệt đối (int32_t motor_position):
mỗi lần Hall THỰC SỰ đổi và hợp lệ, bộ đếm tăng (chiều THUẬN) hoặc giảm (chiều NGHỊCH) 1 đơn vị.
Quy ước: 6 Hall step = 1 chu kỳ điện, 4 chu kỳ điện = 1 vòng cơ -> 24 Hall step = 1 vòng cơ.
Đây là cơ sở để tính progress dùng cho ramp duty (mục 9) và target đích (mục 13).
Chu kỳ Hall được đo bằng TIM3 chạy ở 1MHz (1 tick = 1us): mỗi lần Hall đổi, tính khoảng
cách tick so với lần trước (hall_period_us), từ đó suy ra RPM điện và RPM cơ:
RPM_dien = 60,000,000 / (period_us * 6) // 6 step Hall / vòng điện RPM_co = 60,000,000 / (period_us * 24) // 24 step Hall / vòng cơ
Chiều quay được suy ra độc lập từ CẶP Hall (trước, hiện tại) qua Motor_Speed_HallDirection(),
đối chiếu với 2 bảng thứ tự THUẬN/NGHỊCH ở mục 6.1 - nếu cặp Hall không khớp bảng nào cả (nhảy 2 bước
hoặc Hall lỗi), hàm trả về 0 và bỏ qua lần cập nhật đó.
Module này hiện chủ yếu cung cấp giá trị RPM/vị trí cho mục đích giám sát/debug; vòng điều khiển tốc độ
kín qua Motor_Speed_Task() (tăng/giảm duty dần để bám target_rpm) đã được viết
sẵn nhưng chưa được gọi trong luồng chính (target_rpm mặc định = 0).
Toàn bộ logic điều khiển cấp cao nằm trong hàm Run_Task(), điều phối bởi biến run_state.
Bảng dưới đây liệt kê đầy đủ 10 trạng thái có trong enum, phân biệt trạng thái ĐANG được sử dụng (chế độ
test tay hiện tại) và trạng thái ĐÃ VIẾT SẴN cho production (đang bị bỏ qua vì chưa có cảm biến dòng thật):
| Trạng thái | Mô tả | Tình trạng |
|---|---|---|
| TEST_IDLE | Đứng yên, chờ sườn lên forward/reverce (hoặc lệnh UART) để kích hoạt một lần quay | Đang dùng |
| TEST_SPIN_FWD | Quay THUẬN một số step cố định (MOTOR_TEST_SPIN_STEPS, mặc định 80 vòng), ramp + kick, có watchdog stall | Đang dùng |
| TEST_SPIN_REV | Quay NGHỊCH tương tự FWD nhưng theo hướng ngược | Đang dùng |
| TEST_BRAKE | Hãm điện (Motor_Commutation_Brake()) trong MOTOR_TEST_BRAKE_MS (300ms) trước khi cắt điện hẳn, chống trôi quá đà | Đang dùng |
| CAL_FIND_TOP | Chạy THUẬN chậm tới khi chạm giới hạn cơ khí TRÊN (phát hiện bằng stall thật) | Chờ cảm biến dòng thật |
| CAL_FIND_BOTTOM | Chạy NGHỊCH chậm tới khi chạm giới hạn DƯỚI - đây là mốc "home" (vị trí 0), gọi Motor_Position_Reset() ngay khi tìm thấy, đồng thời tính door_travel_steps (tổng hành trình) | Chờ cảm biến dòng thật |
| IDLE_HOME | Đứng yên tại DƯỚI (cửa đóng), chờ lệnh hoặc chờ cảm biến ssopendoor kích hoạt - khi kích hoạt, lấy mẫu VR1/VR2/VR5 và chuyển sang FWD_MOVING | Chờ cảm biến dòng thật |
| FWD_MOVING | Đang mở cửa (chạy lên) tới run_target_steps (100% hoặc 80% nếu auto80), dùng ramp với tham số từ VR | Chờ cảm biến dòng thật |
| IDLE_DEST | Đứng yên tại đích (mở). Nếu là chặng AUTO (do cảm biến kích hoạt): chờ cảm biến hết kích hoạt + delay (VR6) rồi tự động đóng. Nếu thủ công: chờ REVERSE. | Chờ cảm biến dòng thật |
| REV_MOVING | Đang đóng cửa (chạy xuống), CÓ giám sát vật cản qua Motor_StallDetected() - nếu stall xảy ra TRƯỚC khi về tới DƯỚI, coi là vật cản và đảo chiều mở lại hoàn toàn ngay lập tức | Chờ cảm biến dòng thật |
run_state khởi
tạo (cuối hàm main()) trở lại RUN_STATE_CAL_FIND_TOP, và đổi điều kiện nút bấm
(bước 4 trong while(1)) về kiểm tra RUN_STATE_IDLE_HOME/RUN_STATE_IDLE_DEST
thay vì RUN_STATE_TEST_IDLE, để kích hoạt lại toàn bộ luồng calibration + phát hiện vật cản
như thiết kế ban đầu. Hiện tại run_state được khởi tạo thẳng vào RUN_STATE_TEST_IDLE.
6 biến trở vật lý trên panel điều khiển được đọc qua ADC (giá trị thô 0..4095) và quy đổi sang đơn vị kỹ
thuật bởi 3 hàm tiện ích (VR_ScaleDutyPercent, VR_ScaleStepsPercent,
VR_ScaleTimeMs - nội suy tuyến tính trong khoảng min/max cấu hình sẵn):
| Biến trở | Chức năng | Khoảng quy đổi |
|---|---|---|
| VR1 | Tốc độ mở cửa (cruise, chiều THUẬN) | 20% .. 90% duty |
| VR2 | % quãng đường cuối đoạn MỞ chạy chậm lại trước khi tới đích | 5% .. 40% hành trình |
| VR3 | Tốc độ đóng cửa (cruise, chiều NGHỊCH) | 20% .. 90% duty |
| VR4 | % quãng đường cuối đoạn ĐÓNG chạy chậm lại trước khi tới đích | 5% .. 40% hành trình |
| VR5 | Tốc độ chạy chậm (creep) - dùng chung cho cả tăng tốc đầu đoạn và giảm tốc gần cuối đoạn, cả 2 chiều | 5% .. 40% duty |
| VR6 | Thời gian chờ tại đích trước khi tự động đóng lại | 1000ms .. 30000ms |
USART3 (115200bps, DMA cả TX lẫn RX) đảm nhận 2 vai trò độc lập:
| Định dạng dòng log | Nguồn gọi | Ý nghĩa |
|---|---|---|
| Hall=abc\r\n | Debug_UART_Hall() | Mã Hall 3-bit hiện tại, gửi mỗi lần hall_confirmed đổi |
| abc abc abc ...\r\n | Debug_UART_HallSequence() | Gom 6 giá trị Hall liên tiếp (1 chu kỳ điện) thành 1 dòng duy nhất, để xem cả thứ tự chuyển mạch trên 1 dòng khi quay tay kiểm tra |
| ADC=xxxx\r\n | Debug_PrintCurrent() | Giá trị MAX(PA1,PA2) tức thời, gửi định kỳ 100ms - dùng để hiệu chỉnh MOTOR_STALL_ADC_THRESHOLD |
| PA1max=.. PA2max=.. SSCR_U=..\r\n | Debug_UART_Peaks() | Đỉnh dòng (peak-hold) từng kênh + cờ báo quá dòng phần cứng, gửi khi watchdog stall (test-spin) kích hoạt |
Tất cả đều gửi qua BSP_UART_SendDMA() (bắt đầu truyền bất đồng bộ bằng HAL_UART_Transmit_DMA),
chỉ gọi khi BSP_UART_TxReady() (huart3.gState == HAL_UART_STATE_READY) trả về
đúng - tránh chồng lệnh gửi trong khi DMA còn đang truyền dòng trước.
command.c cài đặt một bộ phân tích khung (frame parser) dạng máy trạng thái, đọc byte từ
vòng đệm DMA RX (BSP_UART_RxRead() - tính toán vị trí ghi hiện tại qua con trỏ DMA
__HAL_DMA_GET_COUNTER() để suy ra các byte mới đến, không cần ngắt RX). Định dạng khung:
[STX = 0xAA] [CMD] [LEN] [PAYLOAD x LEN byte] [CRC8]
CRC8 tính trên (CMD, LEN, PAYLOAD) theo đa thức 0x07. Các lệnh hiện hỗ trợ:
| Mã lệnh | Tác dụng |
|---|---|
| CMD_ID_DOOR_OPEN | Tương đương sườn lên nút FORWARD (mở cửa) |
| CMD_ID_DOOR_CLOSE | Tương đương sườn lên nút REVERSE (đóng cửa) |
| CMD_ID_DOOR_OPEN_80 | Mở cửa nhưng chỉ tới 80% hành trình (giống công tắc auto80 vật lý) |
Gói tin sai CRC hoặc vượt độ dài payload tối đa (16 byte) sẽ bị âm thầm hủy (không dispatch), quay về chờ
STX mới. Lệnh hợp lệ được lưu vào pending_command và chỉ được Command_GetPending()
trả về MỘT LẦN DUY NHẤT (tự động reset về CMD_NONE sau khi đọc) - hoạt động như một cạnh lên
ảo, tương tự forward_rising/reverse_rising của nút bấm vật lý.
huart3.gState
không bao giờ trở lại HAL_UART_STATE_READY). Nguyên nhân: thiếu hàm
USART3_IRQHandler() và thiếu HAL_NVIC_EnableIRQ(USART3_IRQn) - vì trong file
.ioc không tick chọn "USART3 global interrupt", CubeMX không tự sinh 2 phần này. Đã bổ sung
thủ công CẢ HAI, đặt trong cặp USER CODE của stm32f1xx_it.c và usart.c
để không bị CubeMX xóa mất khi regenerate lại (xem mục 16, hạng mục #1).
Danh sách các vấn đề thực tế đã gặp và cách xử lý trong quá trình đưa hệ thống lên phần cứng thật, theo thứ tự thời gian - giúp hiểu vì sao một số đoạn code hiện có dạng như vậy.
Triệu chứng: chỉ gửi được 1 dòng log, sau đó BSP_UART_TxReady() luôn trả về sai.
Nguyên nhân: thiếu USART3_IRQHandler() và HAL_NVIC_EnableIRQ(USART3_IRQn) -
không có ngắt để HAL báo hoàn tất DMA TX, gState kẹt vĩnh viễn ở trạng thái BUSY.
Xử lý: bổ sung cả 2 phần, đặt trong cặp USER CODE để sống sót qua các lần CubeMX regenerate.
Triệu chứng: lỗi #1 tái xuất hiện sau khi CubeMX đổi cấu hình chân (PA1/PA2 sang dòng pha B/C).
Nguyên nhân: fix ban đầu nằm NGOÀI cặp USER CODE BEGIN/END, bị CubeMX ghi đè khi regenerate.
Xử lý: chuyển toàn bộ fix vào đúng cặp USER CODE tương ứng.
Triệu chứng: giá trị ADC thỉnh thoảng đọc được 0 bất thường.
Nguyên nhân: NbrOfConversion trong adc.c (8) lệch với độ dài DMA transfer
trong bsp_adc.c (BSP_ADC_CHANNEL_COUNT = 9), gây trượt chỉ số kênh qua từng vòng quét.
Xử lý: bổ sung cấu hình kênh thứ 9 còn thiếu, đồng bộ NbrOfConversion = 9.
Triệu chứng: Motor_StallDetected() và log dòng điện đọc giá trị vô nghĩa.
Nguyên nhân: cảm biến dòng pha A (PA0) chưa được lắp đặt trên phần cứng, chân đọc giá trị
trôi nổi ngẫu nhiên.
Xử lý: chuyển cả 2 nơi đọc dòng sang MAX(PA1, PA2) - 2 kênh đã xác nhận có đấu thật.
Triệu chứng: chỉ quay được nếu mồi tay theo, dù bảng chuyển mạch đối xứng đúng về logic với NGHỊCH.
Nguyên nhân (xác định qua thực nghiệm): lệch góc điện do 3 cảm biến Hall không đạt hoàn
toàn đối xứng cơ khí.
Xử lý: áp dụng kỹ thuật lead-commutation (Hall_ThuanNext()) chỉ cho nhánh
THUẬN - xác nhận tự khởi động và chạy đúng nhiều chu kỳ qua logic analyzer (xem mục 6.4).
Triệu chứng: chiều NGHỊCH bị kẹt với dòng cao (đo được PA2max~3428, SSCR_U=1) sau khi
thêm kick 55%/300ms.
Nguyên nhân: lúc test CHƯA gắn tải cơ khí thật, rotor không tải tăng tốc quá nhanh ngay
từ đầu, vòng lặp debounce Hall không theo kịp tốc độ thực, khiến commutation áp vector cho vị trí đã cũ -
vô tình chống lại chiều quay, gây hãm đột ngột và dòng vọt cao.
Xử lý: hạ kick về bằng MOTOR_TEST_DUTY_ACCEL_START (vô hiệu hóa), giữ lại code
để dùng khi gắn tải thật (xem mục 10).
Triệu chứng: với timeout 400ms, chiều NGHỊCH (trước đó tự khởi động tốt ở 25%) lại cần mồi tay.
Nguyên nhân: quá trình thắng ma sát tĩnh tự nhiên cần nhỉnh hơn 400ms nhưng dưới 1000ms -
watchdog cắt PWM quá sớm, TRƯỚC KHI động cơ kịp tự khởi động.
Xử lý: nâng MOTOR_STALL_TIMEOUT_MS lên 700ms (xem mục 11.2).
Xuất CSV từ cả 2 lần test THUẬN và NGHỊCH, đối chiếu với bảng chuyển mạch (có tính đến đảo cực LIN ở mục 7)
để kiểm tra 2 điều: (1) không có trường hợp High và Low CÙNG một pha cùng dẫn (shoot-through), (2) cặp
pha đang dẫn tại mọi thời điểm khớp đúng với bảng lý thuyết theo Hall/hướng đang áp dụng. Kết quả: 0
trường hợp shoot-through thực sự ở cả 2 chiều (một vài blip 10us đơn lẻ được xác định là nhiễu/rung
điện khi chuyển mạch, không phải lỗi thật, vì hằng số thời gian L/R của cuộn dây lớn hơn nhiều so với 10us).
Riêng file reversekoquay.csv (một lần test NGHỊCH không quay được) cho thấy watchdog 700ms hoạt
động đúng thiết kế - áp dụng vector Hall hợp lệ liên tục suốt ~697ms rồi cắt sạch, không đoản mạch - nhưng
rotor vẫn không nhúc nhích, xác nhận đây là trường hợp thiếu mô-men khởi động thật ở đúng vị trí
nghỉ đó (không tải), không phải lỗi phần mềm/thời gian.
Bối cảnh: sau khi xác nhận fix "lead commutation" (mục 6.4) giúp chiều THUẬN tự khởi động
ổn định, người dùng yêu cầu gỡ bỏ hoàn toàn cơ chế này khỏi code, chấp nhận quay lại hành vi cũ.
Thay đổi: xóa hàm Hall_ThuanNext() và comment giải thích trong
motor_commutation.c; 2 vị trí gọi switch (Hall_ThuanNext(hall)) /
switch (Hall_ThuanNext(active_hall)) trả về switch (hall) /
switch (active_hall) thẳng.
Kiểm chứng: build lại qua Keil ARMCC5 - 0 Error(s), 0 Warning(s),
Program Size: Code=14884 RO-data=312 RW-data=172 ZI-data=2332 (giảm 68 byte Code
so với bản có Hall_ThuanNext, đúng như dự kiến khi bỏ 1 hàm nhỏ + 2 lời gọi hàm).
Hệ quả chấp nhận: chiều THUẬN có thể quay lại hiện tượng không tự khởi động được ở duty
cao (đã ghi nhận #5) - đây là đánh đổi có chủ đích, không phải lỗi phát sinh. Bảng tra cứu "nếu dịch bước"
được giữ lại ở mục 6.4.1 để tham khảo nếu cần khôi phục sau này.
Bối cảnh: người dùng yêu cầu giảm duty để quan sát log Hall ổn định hơn, giảm rủi ro dòng
cao trong lúc kiểm tra.
Thay đổi: MOTOR_TEST_DUTY_ACCEL_START hiện là 8%,
MOTOR_TEST_DUTY_RUN 10%, MOTOR_TEST_DUTY_DECEL_MIN
8% (xem mục 17) - MOTOR_KICK_DUTY_PERCENT tự động theo
ACCEL_START nên cũng là 8%.
Lưu ý: ở mức duty thấp này, động cơ có thể KHÔNG ĐỦ mô-men tự khởi động (cần mồi tay) -
đây là đánh đổi chủ động để ưu tiên quan sát Hall an toàn, không phải lỗi.
Các hằng số quan trọng nhất, trích xuất trực tiếp từ main.c tại thời điểm xuất tài liệu:
| Hằng số | Giá trị | Ý nghĩa |
|---|---|---|
| MOTOR_TEST_DUTY_ACCEL_START | 8% | Duty bắt đầu ramp (giai đoạn tăng tốc) - cũng là duty kick (mục 10) |
| MOTOR_TEST_DUTY_RUN | 10% | Duty mức chạy ổn định (chế độ test-spin) |
| MOTOR_TEST_DUTY_DECEL_MIN | 8% | Duty tối thiểu lúc giảm tốc gần đích |
| MOTOR_KICK_DUTY_PERCENT | = ACCEL_START (vô hiệu hóa) | Duty cố định trong khoảng kick khởi động |
| MOTOR_KICK_DURATION_MS | 300ms | Thời gian áp duty kick sau khi bấm nút |
| MOTOR_STALL_TIMEOUT_MS | 700ms | Watchdog: cắt PWM nếu Hall đứng yên quá lâu (test-spin) |
| MOTOR_STALL_ADC_THRESHOLD | 2048 (placeholder) | Ngưỡng ADC coi là stall/vật cản - CẦN hiệu chỉnh thực tế |
| MOTOR_STALL_DEBOUNCE_COUNT | 5 lần | Số lần đọc liên tiếp vượt ngưỡng để xác nhận stall |
| HALL_DEBOUNCE_COUNT | 3 lần | Số lần đọc Hall giống nhau liên tiếp để chấp nhận ổn định |
| MOTOR_HALL_STEPS_PER_REV | 24 step | Số Hall step / 1 vòng cơ (6 step/chu kỳ điện x 4 chu kỳ điện/vòng) |
| MOTOR_ACCEL_REVOLUTIONS / DECEL_REVOLUTIONS | 4 vòng / 4 vòng | Quãng đường dành cho tăng tốc / giảm tốc (chế độ test-spin) |
| MOTOR_TEST_SPIN_REVOLUTIONS | 80 vòng | Tổng số vòng quay mỗi lần kích hoạt test-spin |
| MOTOR_TEST_BRAKE_MS | 300ms | Thời gian hãm điện trước khi cắt điện hẳn |
| MOTOR_CAL_DUTY | 8% | Duty cố định dùng riêng cho calibration (chưa kích hoạt) |
| MOTOR_AUTO_CLOSE_DELAY_MS | 5000ms | Giá trị mặc định cho thời gian tự động đóng (sẽ bị VR6 thay thế khi đã đọc được) |
| DEBUG_CURRENT_PRINT_INTERVAL_MS | 100ms | Chu kỳ gửi log dòng điện qua UART |
| TIM1 ARR (PWM period) | 3199 | Độ phân giải duty PWM High-side |
run_state khởi tạo về
RUN_STATE_CAL_FIND_TOP (cũng sửa lại điều kiện nút bấm ở bước 4) để kích hoạt toàn bộ
luồng production đã viết sẵn: calibration hành trình, tự động đóng sau khi mở, phát hiện vật cản
khi đóng (mục 13).
MOTOR_STALL_ADC_THRESHOLD bằng dữ liệu thực tế (log
ADC=xxxx qua UART) - giá trị 2048 hiện chỉ là placeholder giữa thang đo, chưa đối chiếu
với mạch cảm biến dòng thật (mục 11.1, mục 17).
.ioc, kiểm tra lại các
fix thủ công quan trọng (đặc biệt USART3 IRQ, xem mục 16 #1-#2) vẫn còn nguyên trong cặp USER CODE,
tránh lặp lại lỗi đã gặp.
Tài liệu này phản ánh trạng thái mã nguồn và kết quả kiểm chứng phần cứng TẠI THỜI ĐIỂM cập nhật (25/08/2026). Các giá trị hằng số, kết luận chẩn đoán và hướng khuyến nghị cần được đối chiếu lại với mã nguồn/dữ liệu đo đạc mới nhất trước khi sử dụng cho quyết định kỹ thuật tiếp theo.