Tài liệu kỹ thuật nội bộ

Hệ thống điều khiển động cơ BLDC
cho cửa tự động

Quy trình, phương pháp và thuật giải đang triển khai trên dự án DieuKhienBLDC
Vi điều khiểnSTM32F103C8 (LQFP48, 64KB Flash, 20KB RAM)
Công cụ buildKeil MDK-ARM, ARM Compiler V5.06 update 6 (ARMCC5), HAL do STM32CubeMX sinh
Driver công suấtSLM2009 (3-phase gate driver, 6 MOSFET)
Phản hồi vị trí3 cảm biến Hall (đọc bằng polling)
Giao tiếpUART3 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ệu25/08/2026 (bản cập nhật)

-Mục lục

1. Tổng quan hệ thống
2. Kiến trúc phần cứng - ánh xạ chân
3. Kiến trúc phần mềm (các lớp module)
4. Vòng lặp chính (7 bước trong while(1)) + sơ đồ giải thuật forward
5. Đọc Hall và debounce
6. Thuật toán commutation 6 bước (6-step BLDC)
7. Driver SLM2009 và vấn đề đảo cực LIN
8. Điều chế PWM và duty
9. Ramp tốc độ hình thang (accel / run / decel)
10. Cơ chế "kick" khởi động
11. Bảo vệ / an toàn (stall, quá dòng)
12. Đo vị trí và tốc độ động cơ
13. Máy trạng thái vận hành (RunState_t)
14. Biến trở panel điều khiển (VR1..VR6)
15. Giao tiếp UART3 (debug log + khung lệnh)
16. Nhật ký bring-up: các lỗi đã xử lý
17. Bảng tham số hiện hành
18. Tồn đọng và khuyến nghị tiếp theo

1.Tổng quan hệ thống

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:

Trạng thái triển khai hiện tại Dự án đang trong giai đoạn bring-up phần cứng: cảm biến dòng cho pha A (PA0) chưa được đấu nối vật lý, nên toàn bộ luồng "production" (tự hiệu chuẩn hành trình bằng stall-detect, tự động đóng sau khi mở, giám sát vật cản khi đóng...) đã được lập trình đầy đủ trong 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.

2.Kiến trúc phần cứng - ánh xạ chân

2.1. Khối công suất (High-side PWM qua TIM1)

Chân MCUTín hiệuNgoại viGhi chú
PA8WH (High-side pha W)TIM1_CH1PWM, điều chế duty
PA9VH (High-side pha V)TIM1_CH2PWM, điều chế duty
PA10UH (High-side pha U)TIM1_CH3PWM, điều chế duty
PB15UL (Low-side pha U)GPIOCực tính LIN ĐẢO PHA - xem mục 7
PB14VL (Low-side pha V)GPIOCực tính LIN ĐẢO PHA - xem mục 7
PB13WL (Low-side pha W)GPIOCực tính LIN ĐẢO PHA - xem mục 7

2.2. Cảm biến Hall

Chân MCUKý hiệuVai trò
PB4Hall_ABit 0 (LSB) của mã Hall 3-bit
PB5Hall_BBit 2 (MSB) của mã Hall 3-bit
PB0Hall_CBit 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.

2.3. Kênh ADC (ADC1, quét vòng qua DMA, 9 kênh)

Thứ tự rãnh trong adc_buffer[] dùng khớp với thứ tự Rank cấu hình trong adc.c
Index bufferKênh ADCChânÝ nghĩa
[0]ADC_CHANNEL_0PA0 (sscurent)Dòng pha A - chưa đấu nối vật lý, trị số trôi nổi
[1]ADC_CHANNEL_1PA1 (sscurenB)Dòng pha B - kênh thật, đang dùng cho stall-detect
[2]ADC_CHANNEL_2PA2 (sscurenC)Dòng pha C - kênh thật, đang dùng cho stall-detect
[3]ADC_CHANNEL_3PA3VR5 - tốc độ chạy chậm (creep)
[4]ADC_CHANNEL_4PA4VR4 - % quãng đường chậm cuối đoạn ĐÓNG
[5]ADC_CHANNEL_5PA5VR6 - thời gian chờ trước khi tự động đóng
[6]ADC_CHANNEL_6PA6VR3 - tốc độ đóng cửa
[7]ADC_CHANNEL_7PA7VR2 - % quãng đường chậm cuối đoạn MỞ
[8]ADC_CHANNEL_9PB1VR1 - tốc độ mở cửa

2.4. Nút bấm / cảm biến / báo lỗi

Tín hiệuVai tròXử lý
forwardNút bấm mở cửa thủ côngDebounce 5 lần đọc, phát hiện sườn lên
reverceNút bấm đóng cửa thủ côngDebounce 5 lần đọc, phát hiện sườn lên
ssopendoorCảm biến người/vật (PIR...) kích hoạt mở cửa tự độngDebounce 2 lần đọc (phản hồi nhanh hơn nút bấm)
auto80Công tắc chọn chế độ chỉ mở 80% hành trìnhDebounce 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.

2.5. Giao tiếp

Ngoại viTốc độVai trò
USART3 (TX/RX qua DMA)115200 bpsXuấ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

3.Kiến trúc phần mềm (các lớp module)

Mã nguồn được chia thành 3 lớp trên nền HAL do CubeMX sinh (Core/Src):

LớpThư mụcFile chínhTrá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.cKhởi động ADC DMA vòng, cung cấp giá trị tức thời + peak-hold cho từng kênh
bsp_gpio.cDebounce nút bấm/cảm biến, đọc/chốt tín hiệu báo quá dòng SSCR_U
bsp_uart.cGọ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.cThuậ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.cDự 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.cBộ 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()
Lưu ý về STM32CubeMX Các file 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).

4.Vòng lặp chính (7 bước trong while(1))

Mỗi vòng lặp while(1) trong main() thực hiện tuần tự 7 bước sau:

1
Đọc Hall + debounce - 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ệ.
2
Hall đổi -> cập nhật tốc độ + vị trí - nếu hall_confirmed vừa đổi so với lần commutate trước, gọi Motor_Speed_Update()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).
3
Đọc công tắc/cảm biến + phát hiện sườn lên - 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.
4
Xử lý nút bấm - kích hoạt quay - nếu đang ở trạng thái rảnh (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).
5
Xử lý theo trạng thái hiện tại - gọi 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...).
6
Gửi log Hall qua UART - mỗi lần 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.
7
Gửi log dòng điện định kỳ - mỗi 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ế.
Nguyên tắc quan trọng Các hàm gây gián đoạn dòng điện (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.

4.1. Chú thích: luồng tín hiệu FORWARD - từ nút nhấn đến PWM

Đ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ơ.

4.2. Sơ đồ giải thuật tổng quan (vòng lặp chính + máy trạng thái)

Nhấn nút forward (bước 3-4) Debounce 5 lần + phát hiện sườn lên (rising edge) run_state: TEST_IDLE -> TEST_SPIN_FWD (kick duty) Run_Task(): Motor_RampDuty_Generic() tính duty theo progress (accel/run/decel) Motor_Commutation_Direction()/UpdateDuty() tra bảng chuyển mạch THUẬN (mục 6.1) Ghi CCR (TIM1) High-side + GPIO Low-side (đảo cực SLM2009) Xung PWM ra MOSFET progress >= target_steps ? CHƯA (không) lặp lại mỗi vòng while(1) ĐỦ rồi Motor_Commutation_Brake() -> TEST_BRAKE (300ms) Cắt điện hẳn -> TEST_IDLE Watchdog song song Nếu Hall đứng yên >= MOTOR_STALL_TIMEOUT_MS (700ms) -> cắt PWM ngay

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.

5.Đọc Hall và debounce

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

6.Thuật toán commutation 6 bước (6-step BLDC)

Đâ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).

6.1. Thứ tự Hall và bảng chuyển mạch

Thứ tự Hall (chu kỳ)
Chiều THUẬN001 -> 101 -> 100 -> 110 -> 010 -> 011 -> (001)
Chiều NGHỊCH001 -> 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ỳ) → 101100110010011 → 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 đó.

Chu kỳ 6 bước Hall - THUẬN (xanh, cùng chiều kim đồng hồ) và NGHỊCH (đỏ, ngược chiều kim đồng hồ) 001 101 100 110 010 011 THUẬN: 001→101→100→110→010→011→(001), dùng bảng gốc (không dịch bước) NGHỊCH: 001→011→010→110→100→101→(001), không đổi từ trước đến nay
Bảng chuyển mạch - CHIỀU THUẬN
HallHigh PWMLow ON
001UV
101UW
100VW
110VU
010WU
011WV
Bảng chuyển mạch - CHIỀU NGHỊCH (đảo High/Low từng trạng thái)
HallHigh PWMLow ON
001VU
101WU
100WV
110UV
010UW
011VW

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.

6.2. Hai hàm commutation: Direction() và UpdateDuty()

/* 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(); }
}

6.3. Hãm điện (dynamic brake)

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.

6.4. Thử nghiệm "lead commutation" cho chiều THUẬN - ĐÃ ÁP DỤNG RỒI HOÀN TÁC

Vấn đề phát hiện (giai đoạn bring-up trước) Chiều NGHỊCH tự khởi động tốt ở duty 25% nhưng chiều THUẬN - dù bảng chuyển mạch đối xứng ĐÚNG về logic với NGHỊCH (đã rà soát khớp 100% với bảng gốc) - vẫn "bó" (không tự quay) ngay cả ở duty 55% giữ 300ms. Nếu chỉ thiếu mô-men thì 55% phải dễ khởi động hơn 25%, nên đây là dấu hiệu lệch góc điện: nếu 3 cảm biến Hall không đạt hoàn toàn đối xứng cơ khí với rotor, bảng "đúng logic" vẫn có thể rơi vào vùng mô-men yếu/âm cho MỘT chiều, trong khi chiều kia (tính cờ) lại rơi đúng vùng mô-men dương.

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()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.

Quyết định HOÀN TÁC (25/08/2026) Sau khi xác nhận fix hoạt động, người dùng yêu cầu GỠ BỎ Hall_ThuanNext() khỏi code - hàm này và toàn bộ comment giải thích đã được XÓA khỏi motor_commutation.c, cả 2 nhánh Motor_Commutation_Direction()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.

6.4.1. Bảng so sánh: đang ÁP DỤNG (raw Hall) vs THAM KHẢO (đã dịch bước, không còn dùng)

ĐANG ÁP DỤNG THUẬN - tra thẳng theo Hall đọc được
HallHigh PWMLow ON
001UV
101UW
100VW
110VU
010WU
011WV
THAM KHẢO - không dùng THUẬN - đã dịch 1 bước (Hall_ThuanNext, đã gỡ bỏ)
Hall đọc đượcHigh PWMLow ON
001UW
101VW
100VU
110WU
010WV
011UV
ĐANG ÁP DỤNG NGHỊCH - không đổi từ trước đến nay
HallHigh PWMLow ON
001VU
101WU
100WV
110UV
010UW
011VW
THAM KHẢO - chưa từng dùng NGHỊCH - nếu dịch 1 bước (giả định, chưa áp dụng trong code)
Hall đọc đượcHigh PWMLow ON
001VW
011UW
010UV
110WV
100WU
101VU

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.

Nếu THUẬN lại "bó" trong lần test tới Đây là hướng xử lý đã biết (chưa áp dụng lại): (1) khôi phục lại hàm 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.

7.Driver SLM2009 và vấn đề đảo cực LIN

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ểnQuan 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)
Hệ quả bắt buộc trong code Chân GPIO nối vào LIN của SLM2009 PHẢI ghi NGƯỢC lại so với trực giác "1 = ON": hàm 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).

8.Điều chế PWM và duty

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.

9.Ramp tốc độ hình thang (accel / run / decel)

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

0 accel_steps vùng chạy ổn định (run) decel_start .. target duty_run duty_start Duty theo progress (số bước Hall đã đi)

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:

  1. Vùng tăng tốc (accel): khi progress < accel_steps, duty tăng tuyến tính từ duty_accel_start lên duty_run.
  2. Vùng chạy ổn định (run): khi 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.
  3. Vùng giảm tốc (decel): khi 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ả:

10.Cơ chế "kick" khởi động

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
Lịch sử hiệu chỉnh

11.Bảo vệ / an toàn (stall, quá dòng)

11.1. Phát hiện stall qua cảm biến dòng điện (Motor_StallDetected)

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 */
Quan trọng PA0 (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.

11.2. Watchdog thời gian cho chế độ test-spin (không đọc ADC)

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ạnLý do
1000msBan đầuGiá trị an toàn mặc định
400msHạ xuống để debugGiả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ạiTrung 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

11.3. Tín hiệu báo quá dòng phần cứng (SSCR_U)

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ố.

12.Đo vị trí và tốc độ động cơ

12.1. Vị trí (motor_position.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).

12.2. Tốc độ (motor_speed.c)

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

13.Máy trạng thái vận hành (RunState_t)

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áiMô 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_FWDQuay 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_REVQuay NGHỊCH tương tự FWD nhưng theo hướng ngượcĐang dùng
TEST_BRAKEHã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_TOPChạ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_BOTTOMChạ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_MOVINGChờ 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ừ VRChờ 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ứcChờ cảm biến dòng thật
Vì sao chế độ production chưa được kích hoạt Comment trong mã nguồn ghi rõ: sau khi gắn cảm biến dòng PA0 thật, cần đổi 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.

14.Biến trở panel điều khiển (VR1..VR6)

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ăngKhoảng quy đổi
VR1Tố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 đích5% .. 40% hành trình
VR3Tố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 đích5% .. 40% hành trình
VR5Tố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ều5% .. 40% duty
VR6Thời gian chờ tại đích trước khi tự động đóng lại1000ms .. 30000ms
Ghi chú Các mốc min/max trên là lựa chọn sản phẩm ban đầu, CHƯA có spec chính thức từ nhà sản xuất panel gốc - cần điều chỉnh lại theo yêu cầu thực tế khi triển khai. VR được lấy mẫu 1 LẦN mỗi khi bắt đầu một chặng chạy (không đọc lại liên tục trong lúc đang chạy), tránh đổi tốc độ đột ngột nếu người dùng vô tình xoay VR trong lúc cửa đang di chuyển.

15.Giao tiếp UART3 (debug log + khung lệnh)

USART3 (115200bps, DMA cả TX lẫn RX) đảm nhận 2 vai trò độc lập:

15.1. Xuất log debug (hướng MCU -> PC)

Định dạng dòng logNguồn gọiÝ nghĩa
Hall=abc\r\nDebug_UART_Hall()Mã Hall 3-bit hiện tại, gửi mỗi lần hall_confirmed đổi
abc abc abc ...\r\nDebug_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\nDebug_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\nDebug_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.

15.2. Nhận lệnh điều khiển (hướng PC -> MCU)

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ệnhTác dụng
CMD_ID_DOOR_OPENTương đương sườn lên nút FORWARD (mở cửa)
CMD_ID_DOOR_CLOSETương đương sườn lên nút REVERSE (đóng cửa)
CMD_ID_DOOR_OPEN_80Mở 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ý.

Điểm hay gây lỗi - điều kiện tiên quyết cho UART DMA TX Trong quá trình bring-up, UART3 từng bị "treo" hoàn toàn sau lần gửi đầu tiên (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.cusart.c để không bị CubeMX xóa mất khi regenerate lại (xem mục 16, hạng mục #1).

16.Nhật ký bring-up: các lỗi đã xử lý

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.

#1 - UART3 không gửi dữ liệu sau lần đầu

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()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.

#2 - CubeMX regenerate xóa mất fix thủ công

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.

#3 - ADC đọc gián đoạn về 0

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.

#4 - PA0 không đấu nối vật lý

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.

#5 - Chiều THUẬN không tự khởi động

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

#6 - Kick 55% gây quá dòng nguy hiểm khi không tải

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

#7 - Hiệu chỉnh watchdog stall timeout

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

#8 - Xác minh bảng chuyển mạch qua máy phân tích logic (Saleae Logic 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.

#9 - Gỡ bỏ Hall_ThuanNext() theo yêu cầu người dùng (25/08/2026)

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.

#10 - Hạ duty test-spin để kiểm tra lại trạng thái Hall

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.

17.Bảng tham số hiện hành

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_START8%Duty bắt đầu ramp (giai đoạn tăng tốc) - cũng là duty kick (mục 10)
MOTOR_TEST_DUTY_RUN10%Duty mức chạy ổn định (chế độ test-spin)
MOTOR_TEST_DUTY_DECEL_MIN8%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_MS300msThời gian áp duty kick sau khi bấm nút
MOTOR_STALL_TIMEOUT_MS700msWatchdog: cắt PWM nếu Hall đứng yên quá lâu (test-spin)
MOTOR_STALL_ADC_THRESHOLD2048 (placeholder)Ngưỡng ADC coi là stall/vật cản - CẦN hiệu chỉnh thực tế
MOTOR_STALL_DEBOUNCE_COUNT5 lầnSố lần đọc liên tiếp vượt ngưỡng để xác nhận stall
HALL_DEBOUNCE_COUNT3 lầnSố lần đọc Hall giống nhau liên tiếp để chấp nhận ổn định
MOTOR_HALL_STEPS_PER_REV24 stepSố Hall step / 1 vòng cơ (6 step/chu kỳ điện x 4 chu kỳ điện/vòng)
MOTOR_ACCEL_REVOLUTIONS / DECEL_REVOLUTIONS4 vòng / 4 vòngQuãng đường dành cho tăng tốc / giảm tốc (chế độ test-spin)
MOTOR_TEST_SPIN_REVOLUTIONS80 vòngTổng số vòng quay mỗi lần kích hoạt test-spin
MOTOR_TEST_BRAKE_MS300msThời gian hãm điện trước khi cắt điện hẳn
MOTOR_CAL_DUTY8%Duty cố định dùng riêng cho calibration (chưa kích hoạt)
MOTOR_AUTO_CLOSE_DELAY_MS5000msGiá trị mặc định cho thời gian tự động đóng (sẽ bị VR6 thay thế khi đã đọc được)
DEBUG_CURRENT_PRINT_INTERVAL_MS100msChu kỳ gửi log dòng điện qua UART
TIM1 ARR (PWM period)3199Độ phân giải duty PWM High-side

18.Tồn đọng và khuyến nghị tiếp theo

  1. Đấu nối cảm biến dòng pha A (PA0) và chuyển 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).
  2. Hiệu chỉnh 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).
  3. Thử nghiệm lại cơ chế kick khởi động với tải cơ khí thật (cánh cửa thật thay vì trục quay tự do) - kick 25% (hiện tại) có thể không đủ mô-men, nhưng 55% đã gây mất đồng bộ debounce Hall lúc không tải; cần tìm lại mức phù hợp khi đã có tải và có thể cần điều chỉnh thêm tốc độ vòng lặp debounce để theo kịp tốc độ rotor cao hơn.
  4. Xử lý trường hợp NGHỊCH cần mồi tay ở một số vị trí nghỉ rotor - đã xác nhận qua logic analyzer là thiếu mô-men khởi động thật (không phải lỗi phần mềm), chỉ xảy ra khi không tải. Cần đánh giá lại sau khi gắn tải cơ khí thật; nếu vẫn còn, các hướng xử lý đã đề xuất gồm: chấp nhận cần mồi tay trên bàn test không tải / thử kick mức trung gian (~40%) / ưu tiên gắn tải thật để kiểm tra lại trước.
  5. Nâng cấp dead time từ vòng lặp NOP (không chính xác tuyệt đối, phụ thuộc ngắt/trình biên dịch) sang DWT cycle counter hoặc timer riêng, trước khi triển khai ở mức công suất/duty cao hơn mức test hiện tại (mục 6.2).
  6. Lưu ý quy trình CubeMX: mỗi lần regenerate từ file .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.