Lập trình STM32F103 Thanh Ghi: Cấu hình PWM Mode STM32F103RCT6

Trong quá trình cấu hình PWM STM32F103, việc hiểu rõ và lập trình trực tiếp các thanh ghi đóng vai trò cốt lõi để làm chủ vi điều khiển ở mức bare-metal. Bài viết này sẽ hướng dẫn bạn chi tiết cách cấu hình PWM mode STM32F103 trên vi điều khiển STM32F103RCT6, tập trung vào việc sử dụng Timer 1 (TIM1) để tạo tín hiệu PWM ổn định với tần số 1kHz và chu kỳ nhiệm vụ (duty cycle) 50% trên kênh CH1 (chân PA8).

1. Giới thiệu ngoại vi & Tại sao cần cấu hình PWM STM32F103?

PWM (Pulse Width Modulation) là một kỹ thuật điều chế độ rộng xung, cho phép bạn tạo ra tín hiệu xung vuông với độ rộng xung có thể điều chỉnh. Kỹ thuật này được sử dụng rộng rãi để kiểm soát công suất cung cấp cho tải, như điều chỉnh độ sáng LED, tốc độ động cơ DC, định vị góc quay của servo hoặc tạo ra các loại âm thanh.

Trên STM32F103RCT6, việc cấu hình PWM mode STM32F103 sử dụng các bộ định thời (Timer) như TIM1 (một Advanced-control Timer) để xuất tín hiệu PWM qua các chân GPIO được cấu hình chức năng thay thế (Alternate Function). Ví dụ, với tần số 1kHz và duty cycle 50% trên kênh CH1 của TIM1, tín hiệu xuất ra chân PA8 sẽ ở mức HIGH trong 50% chu kỳ và mức LOW trong 50% còn lại. Điều này rất phù hợp cho việc điều chỉnh cường độ sáng của LED hoặc tốc độ quạt một cách mượt mà và hiệu quả.

Lập trình STM32F103 thanh ghi cho PWM Mode mang lại sự kiểm soát chính xác cao, không phụ thuộc vào các thư viện HAL hoặc SPL, giúp tối ưu hóa code và cung cấp cái nhìn sâu sắc về cách phần cứng hoạt động. Công thức tính tần số PWM cơ bản là:

fPWM = fCLK / ((PSC + 1) * (ARR + 1))

Với fCLK là tần số clock của Timer (ví dụ 72MHz cho APB2 timer clock trên STM32F103), PSC là giá trị bộ chia tần số (Prescaler), và ARR là giá trị tự động nạp lại (Auto-Reload Register). Ví dụ, với PSC=71ARR=999, chúng ta sẽ có tần số PWM là 1kHz (72MHz / (71+1) / (999+1) = 1kHz).

Ứng dụng của PWM Mode STM32F103

Cấu hình PWM mode STM32F103 được áp dụng rộng rãi trong các lĩnh vực:

  • Điều khiển độ sáng LED hoặc màn hình LCD để tạo hiệu ứng ánh sáng động.
  • Kiểm soát tốc độ motor DC hoặc stepper motor trong robot và máy móc tự động.
  • Tạo âm thanh cho buzzer hoặc loa, như báo hiệu trong thiết bị IoT.
  • Điều chỉnh công suất cho đèn sưởi ấm hoặc quạt gió trong hệ thống điều khiển nhiệt độ.
  • Trong servo motor, PWM dùng để định vị góc quay chính xác, hữu ích cho cánh tay robot.

Bằng cách sử dụng PWM mode STM32F103, bạn có thể tạo ra các hệ thống điều khiển mượt mà, tiết kiệm năng lượng và dễ mở rộng.

2. Hướng dẫn đọc Reference Manual (Self-study Guidance)

Để thực sự làm chủ ngoại vi này, bạn hãy mở tài liệu RM0008. Đây là cuốn cẩm nang chi tiết nhất về kiến trúc và cách hoạt động của STM32F103.

  • Sử dụng tính năng tìm kiếm (Ctrl + F) với từ khóa RCC để tìm chương về Reset and Clock Control. Tập trung vào thanh ghi RCC_APB2ENR (APB2 peripheral clock enable register) để cấp xung nhịp cho GPIOA và TIM1.
  • Tiếp theo, tìm từ khóa GPIO để đến chương General-purpose and alternate-function I/Os. Tại đây, hãy tập trung vào thanh ghi GPIOx_CRH (Port configuration register high) để cấu hình chân PA8.
  • Sau đó, tìm từ khóa TIM1 để đến chương Advanced-control timers (TIM1 and TIM8). Đây là chương quan trọng nhất, bạn cần nắm vững các thanh ghi như TIMx_PSC, TIMx_ARR, TIMx_CR1, TIMx_CCMR1, TIMx_CCER, và đặc biệt là TIMx_BDTR (cho các Advanced Timer).
  • Để hiểu rõ hơn về luồng tín hiệu và cách các khối chức năng kết nối với nhau, hãy tập trung tìm kiếm Block Diagram của TIM1 trong chương này. Nó sẽ giúp bạn hình dung toàn bộ cấu trúc và các đường đi của tín hiệu PWM.

Việc đọc và tham chiếu trực tiếp từ Reference Manual là kỹ năng không thể thiếu đối với một kỹ sư nhúng chuyên nghiệp.

3. Define Struct thanh ghi kiểu Bit-field

// Trích xuất từ tài liệu RM0008, Section 7.3.7, Trang 113
// Thanh ghi Kích hoạt Clock Ngoại vi APB2 (RCC_APB2ENR)
typedef union {
    struct {
        __IOM uint32_t AFIOEN   : 1;  // Bit 0: Alternate function I/O clock enable
        uint32_t reserved1     : 1;  // Bit 1: Reserved
        __IOM uint32_t IOPAEN   : 1;  // Bit 2: I/O port A clock enable
        __IOM uint32_t IOPBEN   : 1;  // Bit 3: I/O port B clock enable
        __IOM uint32_t IOPCEN   : 1;  // Bit 4: I/O port C clock enable
        __IOM uint32_t IOPDEN   : 1;  // Bit 5: I/O port D clock enable
        __IOM uint32_t IOPEEN   : 1;  // Bit 6: I/O port E clock enable
        __IOM uint32_t IOPFEN   : 1;  // Bit 7: I/O port F clock enable
        __IOM uint32_t IOPGEN   : 1;  // Bit 8: I/O port G clock enable
        __IOM uint32_t ADC1EN   : 1;  // Bit 9: ADC 1 interface clock enable
        __IOM uint32_t ADC2EN   : 1;  // Bit 10: ADC 2 interface clock enable
        __IOM uint32_t TIM1EN   : 1;  // Bit 11: TIM1 timer clock enable
        __IOM uint32_t SPI1EN   : 1;  // Bit 12: SPI 1 clock enable
        __IOM uint32_t TIM8EN   : 1;  // Bit 13: TIM8 timer clock enable
        __IOM uint32_t USART1EN : 1;  // Bit 14: USART1 clock enable
        __IOM uint32_t ADC3EN   : 1;  // Bit 15: ADC3 interface clock enable
        uint32_t reserved2     : 2;  // Bit 16-18: Reserved
        __IOM uint32_t TIM9EN   : 1;  // Bit 19: TIM9 timer clock enable
        __IOM uint32_t TIM10EN  : 1;  // Bit 20: TIM10 timer clock enable
        __IOM uint32_t TIM11EN  : 1;  // Bit 21: TIM11 timer clock enable
        uint32_t reserved3     : 10; // Bit 22-31: Reserved
    };
    __IOM uint32_t reg; // Truy cập toàn bộ thanh ghi
} RCC_APB2ENR_Type;

// Trích xuất từ tài liệu RM0008, Section 9.2.2, Trang 172
// Thanh ghi Cấu hình Cổng GPIO Cao (GPIOx_CRH)
typedef union {
    struct {
        __IOM uint32_t MODE8 : 2;  // Bit 0-1: Port x mode bits (pin 8)
        __IOM uint32_t CNF8  : 2;  // Bit 2-3: Port x configuration bits (pin 8)
        __IOM uint32_t MODE9 : 2;  // Bit 4-5: Port x mode bits (pin 9)
        __IOM uint32_t CNF9  : 2;  // Bit 6-7: Port x configuration bits (pin 9)
        __IOM uint32_t MODE10 : 2; // Bit 8-9: Port x mode bits (pin 10)
        __IOM uint32_t CNF10 : 2;  // Bit 10-11: Port x configuration bits (pin 10)
        __IOM uint32_t MODE11 : 2; // Bit 12-13: Port x mode bits (pin 11)
        __IOM uint32_t CNF11 : 2;  // Bit 14-15: Port x configuration bits (pin 11)
        __IOM uint32_t MODE12 : 2; // Bit 16-17: Port x mode bits (pin 12)
        __IOM uint32_t CNF12 : 2;  // Bit 18-19: Port x configuration bits (pin 12)
        __IOM uint32_t MODE13 : 2; // Bit 20-21: Port x mode bits (pin 13)
        __IOM uint32_t CNF13 : 2;  // Bit 22-23: Port x configuration bits (pin 13)
        __IOM uint32_t MODE14 : 2; // Bit 24-25: Port x mode bits (pin 14)
        __IOM uint32_t CNF14 : 2;  // Bit 26-27: Port x configuration bits (pin 14)
        __IOM uint32_t MODE15 : 2; // Bit 28-29: Port x mode bits (pin 15)
        __IOM uint32_t CNF15 : 2;  // Bit 30-31: Port x configuration bits (pin 15)
    };
    __IOM uint32_t reg; // Truy cập toàn bộ thanh ghi
} GPIO_CRH_Type;

// Trích xuất từ tài liệu RM0008, Section 14.4.1, Trang 338
// Thanh ghi Điều khiển Timer (TIMx_CR1)
typedef union {
    struct {
        __IOM uint32_t CEN     : 1;  // Bit 0: Counter enable
        __IOM uint32_t UDIS    : 1;  // Bit 1: Update disable
        __IOM uint32_t URS     : 1;  // Bit 2: Update request source
        __IOM uint32_t OPM     : 1;  // Bit 3: One pulse mode
        __IOM uint32_t DIR     : 1;  // Bit 4: Direction
        __IOM uint32_t CMS     : 2;  // Bit 5-6: Center-aligned mode selection
        __IOM uint32_t ARPE    : 1;  // Bit 7: Auto-reload preload enable
        __IOM uint32_t CKD     : 2;  // Bit 8-9: Clock division
        uint32_t reserved1     : 22; // Bit 10-31: Reserved
    };
    __IOM uint32_t reg; // Truy cập toàn bộ thanh ghi
} TIM_CR1_Type;

// Trích xuất từ tài liệu RM0008, Section 14.4.7, Trang 349
// Thanh ghi Chế độ So sánh/Chụp 1 (TIMx_CCMR1) - Chế độ đầu ra
typedef union {
    struct {
        __IOM uint32_t CC1S    : 2;  // Bit 0-1: Capture/Compare 1 selection
        __IOM uint32_t OC1FE   : 1;  // Bit 2: Output Compare 1 fast enable
        __IOM uint32_t OC1PE   : 1;  // Bit 3: Output Compare 1 preload enable
        __IOM uint32_t OC1M    : 3;  // Bit 4-6: Output Compare 1 mode
        __IOM uint32_t OC1CE   : 1;  // Bit 7: Output Compare 1 clear enable
        __IOM uint32_t CC2S    : 2;  // Bit 8-9: Capture/Compare 2 selection
        __IOM uint32_t OC2FE   : 1;  // Bit 10: Output Compare 2 fast enable
        __IOM uint32_t OC2PE   : 1;  // Bit 11: Output Compare 2 preload enable
        __IOM uint32_t OC2M    : 3;  // Bit 12-14: Output Compare 2 mode
        __IOM uint32_t OC2CE   : 1;  // Bit 15: Output Compare 2 clear enable
        uint32_t reserved1     : 16; // Bit 16-31: Reserved
    };
    __IOM uint32_t reg; // Truy cập toàn bộ thanh ghi
} TIM_CCMR1_Type;

// Trích xuất từ tài liệu RM0008, Section 14.4.9, Trang 353
// Thanh ghi Kích hoạt So sánh/Chụp (TIMx_CCER)
typedef union {
    struct {
        __IOM uint32_t CC1E    : 1;  // Bit 0: Capture/Compare 1 output enable
        __IOM uint32_t CC1P    : 1;  // Bit 1: Capture/Compare 1 output Polarity
        __IOM uint32_t CC1NE   : 1;  // Bit 2: Capture/Compare 1 complementary output enable
        __IOM uint32_t CC1NP   : 1;  // Bit 3: Capture/Compare 1 complementary output Polarity
        __IOM uint32_t CC2E    : 1;  // Bit 4: Capture/Compare 2 output enable
        __IOM uint32_t CC2P    : 1;  // Bit 5: Capture/Compare 2 output Polarity
        __IOM uint32_t CC2NE   : 1;  // Bit 6: Capture/Compare 2 complementary output enable
        __IOM uint32_t CC2NP   : 1;  // Bit 7: Capture/Compare 2 complementary output Polarity
        __IOM uint32_t CC3E    : 1;  // Bit 8: Capture/Compare 3 output enable
        __IOM uint32_t CC3P    : 1;  // Bit 9: Capture/Compare 3 output Polarity
        __IOM uint32_t CC3NE   : 1;  // Bit 10: Capture/Compare 3 complementary output enable
        __IOM uint32_t CC3NP   : 1;  // Bit 11: Capture/Compare 3 complementary output Polarity
        __IOM uint32_t CC4E    : 1;  // Bit 12: Capture/Compare 4 output enable
        __IOM uint32_t CC4P    : 1;  // Bit 13: Capture/Compare 4 output Polarity
        uint32_t reserved1     : 1; // Bit 14: Reserved
        __IOM uint32_t CC4NP   : 1;  // Bit 15: Capture/Compare 3 complementary output Polarity
        uint32_t reserved1     : 16; // Bit 16-31: Reserved
    };
    __IOM uint32_t reg; // Truy cập toàn bộ thanh ghi
} TIM_CCER_Type;

// Trích xuất từ tài liệu RM0008, Section 14.4.18, Trang 359
// Thanh ghi Ngắt và Thời gian Chết (TIMx_BDTR)
typedef union {
    struct {
        __IOM uint32_t DTG     : 8;  // Bit 0-7: Dead-time generator setup
        __IOM uint32_t LOCK    : 2;  // Bit 8-9: Lock configuration
        __IOM uint32_t OSSI    : 1;  // Bit 10: Off-state selection for Idle mode
        __IOM uint32_t OSSR    : 1;  // Bit 11: Off-state selection for Run mode
        __IOM uint32_t BKE     : 1;  // Bit 12: Break enable
        __IOM uint32_t BKP     : 1;  // Bit 13: Break polarity
        __IOM uint32_t AOE     : 1;  // Bit 14: Automatic output enable
        __IOM uint32_t MOE     : 1;  // Bit 15: Main output enable
        uint32_t reserved1     : 16; // Bit 16-31: Reserved
    };
    __IOM uint32_t reg; // Truy cập toàn bộ thanh ghi
} TIM_BDTR_Type;

// Bản đồ thanh ghi TIM (14.4.21 TIMx register map – Trang 363)
typedef struct {
    __IOM TIM_CR1_Type CR1; // 0x00: Control 1
    __IOM uint32_t CR2; // 0x04: Control 2
    __IOM uint32_t SMCR; // 0x08: Slave mode control
    __IOM uint32_t DIER; // 0x0C: DMA/interrupt enable
    __IOM uint32_t SR; // 0x10: Status
    __IOM uint32_t EGR; // 0x14: Event generation
    __IOM TIM_CCMR1_Type CCMR1; // 0x18: Capture/compare mode 1
    __IOM uint32_t CCMR2; // 0x1C: Capture/compare mode 2
    __IOM TIM_CCER_Type CCER; // 0x20: Capture/compare enable
    __IOM uint32_t CNT; // 0x24: Counter value
    __IOM uint32_t PSC; // 0x28: Prescaler
    __IOM uint32_t ARR; // 0x2C: Auto-reload
    __IOM uint32_t RCR; // 0x30: Repetition counter
    __IOM uint32_t CCR1; // 0x34: Capture/compare 1
    __IOM uint32_t CCR2; // 0x38: Capture/compare 2
    __IOM uint32_t CCR3; // 0x3C: Capture/compare 3
    __IOM uint32_t CCR4; // 0x40: Capture/compare 4
    __IOM TIM_BDTR_Type BDTR; // 0x44: Break and dead-time
    __IOM uint32_t DCR; // 0x48: DMA control
    __IOM uint32_t DMAR; // 0x4C: DMA address
} TIM_TypeDef;

// Cấu trúc tổng thể cho GPIOA
// Trích xuất từ tài liệu RM0008, Section 9.2, Trang 172
typedef struct {
    __IOM uint32_t              CRL;        // Offset 0x00
    __IOM GPIO_CRH_Type        CRH;        // Offset 0x04
    __IM uint32_t              IDR;        // Offset 0x08
    __IOM uint32_t              ODR;        // Offset 0x0C
    __IOM uint32_t              BSRR;       // Offset 0x10
    __IOM uint32_t              BRR;        // Offset 0x14
    __IOM uint32_t              LCKR;       // Offset 0x18
} GPIO_TypeDef;

// Cấu trúc tổng thể cho RCC
// Trích xuất từ tài liệu RM0008, Section 7.3, Trang 112
typedef struct {
    __IOM uint32_t              CR;         // Offset 0x00
    __IOM uint32_t              CFGR;       // Offset 0x04
    __IOM uint32_t              CIR;        // Offset 0x08
    __IOM uint32_t              APB2RSTR;   // Offset 0x0C
    __IOM uint32_t              APB1RSTR;   // Offset 0x10
    __IOM uint32_t              AHBENR;     // Offset 0x14
    __IOM RCC_APB2ENR_Type      APB2ENR;    // Offset 0x18
    __IOM uint32_t              APB1ENR;    // Offset 0x1C
    __IOM uint32_t              BDCR;       // Offset 0x20
    __IOM uint32_t              CSR;        // Offset 0x24
} RCC_TypeDef;

// Định nghĩa địa chỉ Base cho các ngoại vi
#define PERIPH_BASE           (0x40000000UL)
#define APB1PERIPH_BASE       (PERIPH_BASE)
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x20000UL)
#define AHBPERIPH_BASE        (PERIPH_BASE + 0x1F0000UL)

#define RCC_BASE              (AHBPERIPH_BASE + 0x1000UL)
#define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800UL)
#define TIM1_BASE             (APB2PERIPH_BASE + 0x2C00UL)

// Macro truy cập các ngoại vi
#define RCC                   ((RCC_TypeDef *) RCC_BASE)
#define GPIOA                 ((GPIO_TypeDef *) GPIOA_BASE)
#define TIM1                  ((TIM_TypeDef *) TIM1_BASE)

// Định nghĩa cho các thanh ghi AFIO
// Trích xuất từ tài liệu RM0008, Section 9.4.2, Trang 184
typedef union {
    struct {
        __IOM uint32_t SPI1_REMAP          : 1;  // Bit 0: SPI1 remapping
        __IOM uint32_t I2C1_REMAP          : 1;  // Bit 1: I2C1 remapping
        __IOM uint32_t USART1_REMAP        : 1;  // Bit 2: USART1 remapping
        __IOM uint32_t USART2_REMAP        : 1;  // Bit 3: USART2 remapping
        __IOM uint32_t USART3_REMAP        : 2;  // Bit 4-5: USART3 remapping
        __IOM uint32_t TIM1_REMAP          : 2;  // Bit 6-7: TIM1 remapping
        __IOM uint32_t TIM2_REMAP          : 2;  // Bit 8-9: TIM2 remapping
        __IOM uint32_t TIM3_REMAP          : 2;  // Bit 10-11: TIM3 remapping
        __IOM uint32_t TIM4_REMAP          : 1;  // Bit 12: TIM4 remapping
        __IOM uint32_t CAN1_REMAP          : 2;  // Bit 13-14: CAN1 remapping
        __IOM uint32_t PD01_REMAP          : 1;  // Bit 15: Port D0/Port D1 remapping on OSC_IN/OSC_OUT
        __IOM uint32_t TIM5CH4_IREMAP      : 1;  // Bit 16: TIM5 channel4 internal remap
        __IOM uint32_t ADC1_ETRGINJ_REMAP  : 1;  // Bit 17:  ADC 1 External trigger injected conversion
        __IOM uint32_t ADC1_ETRGREG_REMAP  : 1;  // Bit 18:  ADC 1 external trigger regular conversion
        __IOM uint32_t ADC2_ETRGINJ_REMAP  : 1;  // Bit 19: ADC 2 external trigger injected conversion
        __IOM uint32_t ADC2_ETRGREG_REMAP  : 1;  // Bit 20: ADC 2 external trigger regular conversion
        uint32_t reserved2                 : 3;  // Bit 21-23: Reserved
        __IOM uint32_t SWJ_CFG             : 3;  // Bit 24-26: Serial wire JTAG configuration
        uint32_t reserved3                 : 5;  // Bit 27-31: Reserved
    };
    __IOM uint32_t reg; // Truy cập toàn bộ thanh ghi
} AFIO_MAPR_Type;

#define AFIO_BASE             (APB2PERIPH_BASE + 0x0000UL)
#define AFIO                  ((AFIO_MAPR_Type *) AFIO_BASE)

4. Giải thích chuyên sâu (What – Why – How) chức năng từng thanh ghi liên qua

Thanh ghi RCC_APB2ENR: Kích hoạt Clock Ngoại vi APB2

  • What: Thanh ghi RCC_APB2ENR (APB2 Peripheral Clock Enable Register) dùng để bật/tắt xung nhịp (clock) cho các ngoại vi kết nối với bus APB2. Đối với PWM trên TIM1 và cấu hình chân GPIOA, chúng ta cần kích hoạt các bit TIM1EN (Bit 11), IOPAEN (Bit 2) và AFIOEN (Bit 0).
  • Why (Circuit-level): TẠI SAO? MCU được thiết kế theo kiến trúc tiết kiệm năng lượng. Các khối ngoại vi được cấp xung nhịp độc lập. Nếu không kích hoạt clock, khối logic của ngoại vi đó sẽ không nhận được tín hiệu đồng hồ và do đó sẽ không hoạt động, giống như việc một chiếc máy không có điện sẽ không thể chạy. Việc kích hoạt AFIOEN là quan trọng vì nó điều khiển ma trận chuyển mạch (multiplexer) để chọn chức năng thay thế (Alternate Function) cho chân GPIO, cho phép tín hiệu từ TIM1 xuất ra PA8.
  • How (Ví dụ):
    RCC->APB2ENR.TIM1EN = 1;  // Bật clock cho TIM1
    RCC->APB2ENR.IOPAEN = 1;  // Bật clock cho GPIOA
    RCC->APB2ENR.AFIOEN = 1;  // Bật clock cho Alternate Function I/O (quan trọng cho chức năng thay thế)

Thanh ghi GPIOA_CRH: Cấu hình Chân PA8 làm Alternate Function Push-Pull

  • What: Thanh ghi GPIOA_CRH (Port A Configuration Register High) điều khiển chế độ và cấu hình của các chân GPIO từ PA8 đến PA15. Đối với chân PA8 (TIM1_CH1), chúng ta cần cấu hình nó ở chế độ “Alternate Function Output Push-Pull” với tốc độ 50MHz.
  • Why (Circuit-level): TẠI SAO? Chân GPIO trên STM32 rất đa năng. Nó có thể là input, output thông thường, hoặc output chức năng thay thế. Để TIM1 có thể điều khiển chân PA8, chúng ta phải chuyển quyền điều khiển chân này từ khối GPIO thông thường sang khối chức năng thay thế của TIM1. Chế độ Push-Pull giúp tín hiệu ra/vào chân mạnh mẽ, đảm bảo biên độ và tốc độ cần thiết cho tín hiệu PWM. Tốc độ 50MHz đảm bảo khả năng đáp ứng nhanh của chân với các thay đổi xung PWM.
  • How (Ví dụ):
    GPIOA->CRH.MODE8 = 3; // Bit 0-1 (MODE8): 11 = Output mode, max speed 50 MHz
    GPIOA->CRH.CNF8  = 2; // Bit 2-3 (CNF8): 10 = Alternate function output Push-pull

Thanh ghi TIM1_PSC & TIM1_ARR: Đặt Tần số PWM (Prescaler và Auto-Reload Register)

  • What: Thanh ghi TIM1->PSC (Prescaler) và TIM1->ARR (Auto-Reload Register) cùng nhau xác định tần số hoạt động của Timer và từ đó là tần số của tín hiệu PWM. PSC là bộ chia tần số clock đầu vào của Timer, còn ARR là giá trị mà bộ đếm sẽ đếm tới trước khi reset và tạo một chu kỳ mới.
  • Why (Circuit-level): TẠI SAO? Clock hệ thống (72MHz) thường quá nhanh để dùng trực tiếp cho các ứng dụng định thời. PSC giúp giảm tần số clock này xuống một mức có thể quản lý được. ARR định nghĩa “độ dài” của một chu kỳ PWM. Ví dụ, nếu PSC giảm 72MHz xuống 1MHz, và ARR là 999, nghĩa là bộ đếm sẽ đếm 1000 xung (từ 0 đến 999) để hoàn thành 1ms (1MHz / 1000 = 1kHz).
  • How (Ví dụ):
    TIM1->PSC = 71;  // fCK_PSC = 72MHz / (71 + 1) = 1MHz
    TIM1->ARR = 999; // fPWM = 1MHz / (999 + 1) = 1kHz

Thanh ghi TIM1_CCMR1 (OC1M & OC1PE): Chọn Chế độ PWM Mode 1 và Bật Preload

  • What: Thanh ghi TIM1->CCMR1 (Capture/Compare Mode Register 1) điều khiển chế độ hoạt động của các kênh so sánh/chụp. Bit OC1M (Output Compare 1 Mode) cấu hình chế độ PWM (ví dụ: PWM Mode 1), và bit OC1PE (Output Compare 1 Preload Enable) kích hoạt chế độ preload cho thanh ghi CCR1.
  • Why (Circuit-level): TẠI SAO? PWM Mode 1 quy định cách tín hiệu PWM hoạt động: HIGH khi bộ đếm (CNT) nhỏ hơn CCR1, và LOW khi CNT lớn hơn hoặc bằng CCR1. Chế độ preload là một tính năng quan trọng để thay đổi duty cycle một cách mượt mà và đồng bộ. Khi OC1PE được bật, giá trị mới của CCR1 sẽ không được cập nhật ngay lập tức mà sẽ được lưu vào một thanh ghi bóng (shadow register) và chỉ nạp vào thanh ghi thực khi có sự kiện Update Event (thường là khi bộ đếm đạt ARR và reset). Điều này giúp tránh hiện tượng giật hoặc nhiễu tín hiệu PWM trong quá trình thay đổi.
  • How (Ví dụ):
    TIM1->CCMR1 = (6 << 4); // OC1M = 110 (PWM mode 1)
    TIM1->CCMR1 |= (1 << 3); // OC1PE = 1 (preload cho CCR1)

Thanh ghi TIM1_CR1 (ARPE): Bật Preload cho ARR

  • What: Bit ARPE (Auto-Reload Preload Enable) trong thanh ghi TIM1->CR1 kích hoạt chế độ preload cho thanh ghi ARR (Auto-Reload Register).
  • Why (Circuit-level): TẠI SAO? Tương tự như OC1PE cho CCR1, ARPE đảm bảo rằng bất kỳ thay đổi nào đối với tần số PWM (thông qua ARR) cũng sẽ được áp dụng một cách mượt mà và đồng bộ tại Update Event. Điều này đặc biệt hữu ích khi bạn cần thay đổi tần số PWM trong thời gian thực mà không gây ra lỗi hoặc giật tín hiệu.
  • How (Ví dụ):
    TIM1->CR1.ARPE = 1; // Bật Auto-reload preload cho ARR

Thanh ghi TIM1_CCER (CC1E): Bật Kênh Đầu ra CH1

  • What: Bit CC1E (Capture/Compare 1 Output Enable) trong thanh ghi TIM1->CCER cho phép tín hiệu đầu ra của kênh Capture/Compare 1 được xuất ra.
  • Why (Circuit-level): TẠI SAO? Sau khi cấu hình chế độ PWM và preload, chúng ta cần ‘mở cổng’ cho tín hiệu PWM nội bộ của Timer. CC1E kích hoạt đường dẫn tín hiệu từ khối so sánh của Timer ra khối điều khiển chân GPIO. Tuy nhiên, nó vẫn chưa đủ để tín hiệu ra chân vật lý, vì còn một ‘công tắc’ chính nữa cho các Advanced Timer.
  • How (Ví dụ):
    TIM1->CCER.CC1E = 1; // Kích hoạt đầu ra kênh CH1

Thanh ghi TIM1_BDTR (MOE): Bật Main Output Enable (Bắt Buộc cho Advanced Timers)

  • What: Bit MOE (Main Output Enable) trong thanh ghi TIM1->BDTR (Break and Dead-time Register) là bit điều khiển chính cho việc cho phép tín hiệu đầu ra từ Timer 1 và Timer 8 (các Advanced Timers) ra các chân GPIO vật lý.
  • Why (Circuit-level): TẠI SAO? Các Advanced Timers như TIM1/TIM8 thường được sử dụng trong các ứng dụng điều khiển động cơ công suất cao, nơi yêu cầu chức năng Break (ngắt khẩn cấp) và Dead-time (thời gian chết giữa các tín hiệu bổ sung). Để đảm bảo an toàn, nhà sản xuất thiết kế một “công tắc tổng” là MOE. Nếu MOE không được bật, dù các bước cấu hình khác có đúng đến đâu, tín hiệu PWM sẽ KHÔNG BAO GIỜ xuất hiện trên chân GPIO. Đây là một trong những nguyên nhân phổ biến nhất gây lỗi “không thấy PWM” khi lập trình các Advanced Timer.
  • How (Ví dụ):
    TIM1->BDTR |= (1 << 15); // Bật Main Output Enable

Thanh ghi TIM1_CCR1: Đặt Duty Cycle (Giá trị So sánh)

  • What: Thanh ghi TIM1->CCR1 (Capture/Compare Register 1) chứa giá trị so sánh quyết định độ rộng xung (duty cycle) của tín hiệu PWM trên kênh 1.
  • Why (Circuit-level): TẠI SAO? Duty cycle được tính bằng tỷ lệ giữa giá trị CCR1 và giá trị ARR. Khi bộ đếm CNT nhỏ hơn CCR1, đầu ra PWM sẽ ở mức HIGH; khi CNT lớn hơn hoặc bằng CCR1, đầu ra sẽ ở mức LOW (đối với PWM Mode 1). Bằng cách thay đổi giá trị CCR1 (từ 0 đến ARR), chúng ta có thể điều chỉnh duty cycle từ 0% đến 100%. Do OC1PE đã được bật, giá trị CCR1 mới sẽ được cập nhật mượt mà, tránh giật tín hiệu.
  • How (Ví dụ):
    TIM1->CCR1 = 499; // Đặt duty cycle 50% (ví dụ: 499/999 ~ 50%)

Thanh ghi TIM1_CR1 (CEN): Bật Bộ đếm (Counter Enable)

  • What: Bit CEN (Counter Enable) trong thanh ghi TIM1->CR1 là bit cuối cùng để kích hoạt bộ đếm của Timer.
  • Why (Circuit-level): TẠI SAO? Sau khi tất cả các tham số và chế độ đã được cấu hình, CEN đóng vai trò như nút “START”. Khi CEN được bật, bộ đếm của Timer bắt đầu hoạt động, đếm từ 0 lên ARR, sau đó reset và lặp lại, tạo ra các sự kiện so sánh và từ đó là tín hiệu PWM.
  • How (Ví dụ):
    TIM1->CR1.CEN = 1; // Kích hoạt bộ đếm của TIM1

5. Quy trình cấu hình PWM STM32F103 & Hardware Traps

Dưới đây là lưu đồ thuật toán chi tiết để cấu hình PWM STM32F103 bằng thanh ghi, cùng với những cảnh báo về các lỗi phần cứng thường gặp.

Lưu đồ thuật toán cấu hình

graph TD;
    A["Bắt đầu"] --> B["1. Bật Clock cho TIM1 và GPIOA (RCC_APB2ENR)"];
    B --> C["2. Bật Clock cho AFIO (RCC_APB2ENR)"];
    C --> D["3. Cấu hình chân PA8 làm Alternate Function Push-Pull (GPIOA_CRH)"];
    D --> E["4. Cấu hình Prescaler (TIM1->PSC)"];
    E --> F["5. Cấu hình Auto-Reload Register (TIM1->ARR)"];
    F --> G["6. Chọn PWM Mode 1 cho Kênh 1 (TIM1->CCMR1.OC1M = 6)"];
    G --> H["7. Bật Preload cho CCR1 (TIM1->CCMR1.OC1PE = 1)"];
    H --> I["8. Bật Preload cho ARR (TIM1->CR1.ARPE = 1)"];
    I --> J["9. Kích hoạt đầu ra kênh CH1 (TIM1->CCER.CC1E = 1)"];
    J --> K["10. BẬT MAIN OUTPUT ENABLE (MOE) (TIM1->BDTR.MOE = 1)"];
    K --> L["11. Đặt Duty Cycle (TIM1->CCR1)"];
    L --> M["12. Bật Bộ đếm (TIM1->CR1.CEN = 1)"];
    M --> N["Kết thúc"];

Cảnh báo Bẫy phần cứng (Hardware Traps)

⚠️ BẪY PHẦN CỨNG (HARDWARE TRAP):

  • Quên cấp Clock: Đây là lỗi kinh điển nhất. Rất nhiều bạn quên cấp xung nhịp Clock cho ngoại vi (TIM1, GPIOA, AFIO) ở thanh ghi RCC_APB2ENR trước khi cấu hình. Hậu quả là MCU có thể bị treo (HardFault) hoặc ngoại vi không hoạt động. Luôn nhớ kiểm tra và kích hoạt clock đầu tiên. (Tham khảo RM0008, Section 7.3.7, Trang 113)
  • Quên bật MOE cho Advanced Timers: Đối với các Advanced Timers như TIM1/TIM8, nếu không bật bit MOE (Main Output Enable) trong thanh ghi BDTR (Bit 15), tín hiệu PWM sẽ không bao giờ xuất ra chân GPIO vật lý dù bạn đã cấu hình mọi thứ khác đúng. Đây là một cơ chế an toàn đặc trưng. (Tham khảo RM0008, Section 14.4.9, Trang 386)
  • Cấu hình chân GPIO sai: Nếu chân GPIO (ví dụ PA8) không được cấu hình chính xác ở chế độ ‘Alternate Function Output Push-Pull’, tín hiệu PWM sẽ không thể được xuất ra. Chân sẽ hoạt động như một chân GPIO thông thường hoặc ở trạng thái không xác định. (Tham khảo RM0008, Section 9.2.2, Trang 172)
  • Sai thứ tự cấu hình: Mặc dù một số bước có thể linh hoạt, nhưng việc bật Timer Counter (CEN) quá sớm có thể dẫn đến các hành vi không mong muốn. Luôn bật bộ đếm sau khi tất cả các cấu hình khác đã được hoàn tất để đảm bảo khởi tạo đúng.

6. Code thực tế chạy trên Keil C

Dưới đây là chương trình mẫu đầy đủ để cấu hình PWM mode STM32F103 để xuất PWM ra PA8, thay đổi độ sáng LED bằng nút nhấn. Trạng thái ban đầu LED sáng 50% độ sáng, mỗi lần nhấn nút nhấn độ sáng giảm 10%. (Lưu ý: Code mẫu gốc có tăng duty, nhưng tôi điều chỉnh để khớp mô tả giảm 10%.)

/*==========================================================================
 *  TIM1 PWM Configuration (1 kHz, CH1 ? PA8)
 *  - Clock source: APB2 timer clock = 72 MHz
 *  - PSC = 71  ? 72 MHz / 72 = 1 MHz
 *  - ARR = 999 ? 1 MHz / 1000 = 1 kHz PWM frequency
 *  - CCR1 controls duty cycle (0–1000 ? 0%–100%)
 *==========================================================================*/
void TIM1_PWM_Config(void) {
    // Enable TIM1 and GPIOA clocks
    RCC->APB2ENR.TIM1EN = 1;
    RCC->APB2ENR.IOPAEN = 1;

    // Configure PA8 as TIM1_CH1
    GPIOA->CRH.MODE8 = 3;   // 50MHz output
    GPIOA->CRH.CNF8 = 2;    // Alternate function push-pull

    // Configure TIM1 for PWM: 1kHz, 50% duty
    TIM1->PSC = 71;         // 72MHz / 72 = 1MHz
    TIM1->ARR = 999;        // 1kHz
    TIM1->CCR1 = 499;       // 50% duty cycle
    TIM1->CCMR1 = (6 << 4); // OC1M = 110 (PWM mode 1) TIM1->CCMR1 |= (1 << 3); // OC1PE = 1 (preload for CCR1) TIM1->CCER = (1 << 0); // CC1E = 1 (enable CH1 output) TIM1->CR1.ARPE = 1;     // Auto-reload preload
    TIM1->BDTR |= (1 << 15); // Main Output Enable (MOE) TIM1->CR1.CEN = 1;      // Enable counter
}

Button btn;

int main(void)
{
    SystemClock_Config();
    GPIO_LED_Config();
    GPIO_Button_Config();
    SysTick_Config_1ms();
    TIM2_Counter_Config();

    TIM1_PWM_Config();  // Initialize TIM1 PWM output
   
    while (1) {
               
        /* Update button state (debounce + edge detection) */
        UpdateButton(GPIOC, 1, &btn, 0);  // PC1, active-low assumed

        /* If button is pressed (rising edge detected) */
        if (btn.keyEvent == BTN_PRESSED)
        {
            uint16_t duty = TIM1->CCR1;   // Current duty value

            /* Decrease duty by 10% (100 steps) */
            if (duty > 0)
            {
                duty -= 100;
                if (duty < 0) // Clamp to 0% duty = 0; } else { duty = 999; // Reset to 100% when min reached } /* Apply new duty cycle (preloaded, updates on next PWM cycle) */ TIM1->CCR1 = duty;
        }
    }
}

Mã này minh họa rõ ràng lập trình STM32F103 thanh ghi cho PWM, với các bước được thực hiện theo thứ tự logic để tránh lỗi.

Link Github: Download chương trình cấu hình STM32F103 PWM Mode

7. Cách kiểm tra cấu hình PWM STM32F103 (Debugging)

Sau khi nạp chương trình, việc kiểm tra xem tín hiệu PWM đã được xuất ra đúng như mong đợi hay chưa là rất quan trọng. Bạn có thể sử dụng các công cụ sau:

  • Logic Analyzer hoặc Oscilloscope: Đây là cách chính xác nhất để kiểm tra tín hiệu PWM. Kết nối Logic Analyzer hoặc Oscilloscope vào chân PA8 của STM32. Bạn sẽ có thể quan sát dạng sóng vuông, đo chính xác tần số và duty cycle để xác nhận nó khớp với cấu hình 1kHz và 50% duty cycle của bạn.
  • Keil uVision Debugger: Trong môi trường Keil C, bạn có thể kiểm tra trạng thái của các thanh ghi trong thời gian thực. Sau khi vào chế độ Debug, hãy mở cửa sổ Peripheral Viewer (Debug -> Peripherals -> System Viewer hoặc Memory View). Kiểm tra các thanh ghi sau:
    • RCC->APB2ENR: Đảm bảo các bit TIM1EN, IOPAEN, AFIOEN đã được set (giá trị 1).
    • GPIOA->CRH: Kiểm tra các bit MODE8CNF8 để đảm bảo chân PA8 được cấu hình đúng là Alternate Function Output Push-Pull (MODE8 = 3, CNF8 = 2).
    • TIM1->CR1, TIM1->PSC, TIM1->ARR, TIM1->CCR1, TIM1->CCMR1, TIM1->CCER, TIM1->BDTR: Kiểm tra từng thanh ghi này để xác nhận các giá trị đã được ghi đúng như trong code của bạn, đặc biệt là TIM1->PSC (71), TIM1->ARR (999), TIM1->CCR1 (499), TIM1->CR1.ARPE (1), TIM1->CCMR1.OC1M (6), TIM1->CCMR1.OC1PE (1), TIM1->CCER.CC1E (1), TIM1->BDTR.MOE (1), và TIM1->CR1.CEN (1).

Việc kết hợp cả phần cứng (Logic Analyzer/Oscilloscope) và phần mềm (Debug Keil) sẽ giúp bạn nhanh chóng phát hiện và khắc phục các vấn đề liên quan đến cấu hình PWM.

8. Câu Hỏi Thường Gặp (FAQ) Về PWM Mode Trên STM32F103

  • PWM Mode là gì trong STM32F103?
    • PWM Mode là chế độ của timer cho phép tạo xung vuông với độ rộng xung thay đổi (duty cycle), thường dùng để điều khiển độ sáng LED, tốc độ động cơ và công suất tải. (PWM thường được cấu hình qua Output Compare Mode trong timer.)
  • Sự khác nhau giữa PSC, ARR và CCR trong PWM là gì?
    • PSC chia tần số clock cho timer, ARR xác định chu kỳ PWM, còn CCR quyết định duty cycle bằng cách so sánh với giá trị đếm CNT. (Tần số PWM = f_CLK / ((PSC + 1) × (ARR + 1)), và duty cycle ≈ (CCR / (ARR + 1)) × 100%. Ví dụ: PSC=71, ARR=999, CCR=499 cho 1kHz với 50% duty.)
  • Tại sao phải bật MOE khi dùng TIM1 PWM?
    • TIM1 là Advanced Timer, nếu không bật MOE (Main Output Enable) thì tín hiệu PWM sẽ không xuất ra chân GPIO dù cấu hình đúng. (MOE nằm trong thanh ghi BDTR (bit 15), chỉ áp dụng cho TIM1/TIM8. Đây là lỗi phổ biến gây “không thấy PWM” trên chân như PA8.)
  • Duty cycle 50% nghĩa là gì?
    • Duty cycle 50% nghĩa là tín hiệu PWM ở mức HIGH trong nửa chu kỳ và LOW trong nửa chu kỳ còn lại. (Trong thực tế, với PWM Mode 1, điều này tương ứng CCR = ARR / 2 + 1, nhưng thường làm tròn để chính xác.)
  • Có thể thay đổi duty cycle khi chương trình đang chạy không?
    • Có. Chỉ cần cập nhật giá trị CCRx, PWM sẽ thay đổi mượt nếu đã bật preload (OCxPE). (Preload (OCxPE=1 trong CCMRx) đảm bảo giá trị mới chỉ cập nhật tại Update Event (overflow), tránh giật tín hiệu. Nếu không bật, thay đổi CCR có thể gây nhiễu tức thì.)
  • Nên dùng PWM Mode 1 hay PWM Mode 2?
    • PWM Mode 1 phổ biến hơn và dễ hiểu: HIGH khi CNT < CCR, LOW khi CNT ≥ CCR; PWM Mode 2 thì ngược lại. (PWM Mode 1 (OCxM=110) thường dùng cho điều khiển motor/LED vì bắt đầu HIGH; Mode 2 (OCxM=111) dùng cho một số ứng dụng đặc biệt như inverter. Lựa chọn tùy thuộc vào yêu cầu logic tín hiệu.)

9. Kết luận

Tóm lại, cấu hình PWM STM32F103 trực tiếp bằng thanh ghi là một kỹ năng nền tảng quan trọng trong lập trình nhúng, giúp bạn kiểm soát linh hoạt các thiết bị ngoại vi. Từ việc điều chỉnh độ sáng LED đến kiểm soát tốc độ motor, PWM mode STM32F103 mở ra vô vàn ứng dụng thực tế. Với hướng dẫn chi tiết về từng thanh ghi, mã nguồn mẫu và những cảnh báo về các bẫy phần cứng, bạn hoàn toàn có thể tự tin triển khai trên STM32F103RCT6 của mình. Hãy tham khảo mã nguồn đầy đủ tại đây và đừng ngần ngại chia sẻ câu hỏi hoặc kinh nghiệm của bạn trong phần bình luận!

Viết một bình luận

This site uses Akismet to reduce spam. Learn how your comment data is processed.