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=71 và ARR=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 ghiRCC_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 ghiGPIOx_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 bitTIM1EN(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
AFIOENlà 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.PSClà bộ chia tần số clock đầu vào của Timer, cònARRlà 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.
PSCgiú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. BitOC1M(Output Compare 1 Mode) cấu hình chế độ PWM (ví dụ: PWM Mode 1), và bitOC1PE(Output Compare 1 Preload Enable) kích hoạt chế độ preload cho thanh ghiCCR1. - 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 khiCNTlớn hơn hoặc bằngCCR1. 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ộ. KhiOC1PEđược bật, giá trị mới củaCCR1sẽ 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 đạtARRvà 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 ghiTIM1->CR1kích hoạt chế độ preload cho thanh ghiARR(Auto-Reload Register). - Why (Circuit-level): TẠI SAO? Tương tự như
OC1PEchoCCR1,ARPEđảm bảo rằng bất kỳ thay đổi nào đối với tần số PWM (thông quaARR) 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 ghiTIM1->CCERcho 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.
CC1Ekí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 ghiTIM1->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ếuMOEkhô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ị
CCR1và giá trịARR. Khi bộ đếmCNTnhỏ hơnCCR1, đầu ra PWM sẽ ở mức HIGH; khiCNTlớn hơn hoặc bằngCCR1, đầu ra sẽ ở mức LOW (đối với PWM Mode 1). Bằng cách thay đổi giá trịCCR1(từ 0 đếnARR), chúng ta có thể điều chỉnh duty cycle từ 0% đến 100%. DoOC1PEđã được bật, giá trịCCR1mớ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 ghiTIM1->CR1là 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”. KhiCENđược bật, bộ đếm của Timer bắt đầu hoạt động, đếm từ 0 lênARR, 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_APB2ENRtrướ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 ghiBDTR(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
MODE8vàCNF8để đả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).
- RCC->APB2ENR: Đảm bảo các bit
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ì?
PSCchia tần số clock cho timer,ARRxác định chu kỳ PWM, cònCCRquyết định duty cycle bằng cách so sánh với giá trị đếmCNT. (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. (MOEnằm trong thanh ghiBDTR(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.)
- TIM1 là Advanced Timer, nếu không bật
- 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.)
- 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
- 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=1trongCCMRx) đả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 đổiCCRcó thể gây nhiễu tức thì.)
- Có. Chỉ cần cập nhật giá trị
- 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 khiCNT ≥ 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.)
- PWM Mode 1 phổ biến hơn và dễ hiểu: HIGH khi
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!
