18 Đèn LED thở bằng PWM 
Quy trình cấu hình 
Chúng ta sẽ sử dụng một đèn LED ngoài để thực hiện hiệu ứng "thở" bằng kỹ thuật PWM (Điều chế độ rộng xung). Lưu ý cần chọn loại LED có thông số phù hợp để đảm bảo an toàn và hiệu suất.
Kết nối chân PB05 của bo mạch phát triển !!

Thông thường, khi sử dụng chức năng PWM của bộ định thời (Timer), chúng ta cần thực hiện các bước sau:
- Bật xung clock cho các ngoại vi cần dùng
- Cấu hình chân GPIO kết nối với LED
- Cấu hình bộ định thời (Timer) phù hợp
- Cấu hình chế độ PWM cho Timer
- Kích hoạt Timer để bắt đầu hoạt động
- Điều chỉnh độ rộng xung (duty cycle) của kênh PWM để tạo hiệu ứng thở
Bật xung clock 
Vi điều khiển STM32F407VET6 có tổng cộng 14 bộ định thời (Timer). Ngoại trừ các bộ định thời cơ bản (Basic Timer) không hỗ trợ PWM, các bộ định thời còn lại đều có 1, 2 hoặc 4 kênh PWM.
Trong bài này, chúng ta sẽ sử dụng chức năng PWM để tạo hiệu ứng đèn LED "thở". Đèn LED được kết nối vào chân PB5 (trước ghi nhầm là PA5).
Theo tài liệu datasheet (trang 43), chân PB5 có thể được cấu hình để sử dụng với nhiều kênh của các bộ định thời thông qua chức năng chân đa chức năng (alternate function), như hình minh họa dưới đây:

Ở đây, chúng ta chọn chức năng phụ trợ (Alternate Function) của chân PB5, tức là sử dụng kênh TIMER3_CH2 để xuất tín hiệu PWM.
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,  ENABLE);   // Bật clock cho Timer 3
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);   // Bật clock cho cổng GPIOBCấu hình GPIO 
Trước đó chúng ta đã nói rằng việc xuất PWM phụ thuộc vào bộ định thời (Timer), vì vậy cần phải cấu hình bộ định thời. Tuy nhiên, trong bài này không sử dụng ngắt của Timer, nên không cần cấu hình phần ngắt.
Do chúng ta sử dụng chân PB5, tương ứng với kênh 2 của Timer 3, nên cần cấu hình các thông số GPIO cho chân này.
Trước hết, ta cần khai báo một cấu trúc tham số cho GPIO như sau:
GPIO_InitTypeDef GPIO_InitStructure;  // Khai báo cấu trúc cấu hình GPIO
GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_TIM3);  
// Thiết lập chân PB5 sử dụng chức năng phụ là TIM3
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;              // Chọn chân PB5
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;           // Chế độ Alternate Function (chức năng phụ)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;     // Tốc độ cao (100MHz)
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;         // Kiểu output push-pull
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;           // Kéo lên (Pull-up)
GPIO_Init(GPIOB, &GPIO_InitStructure);                 // Áp dụng cấu hình cho GPIOB
GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_TIM3); // Cấu hình chân đa chức năng- Hàm - GPIO_PinAFConfigđược dùng để cấu hình chức năng đa chức năng (Alternate Function) cho các chân GPIO. Trên STM32, mỗi chân GPIO ngoài chức năng vào/ra cơ bản còn có thể được gán cho các chức năng ngoại vi như Timer, USART, I2C, SPI,... Chức năng này cho phép các ngoại vi của STM32 có thể giao tiếp với bên ngoài thông qua các chân GPIO.- Cú pháp hàm: c- void GPIO_PinAFConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_PinSource, uint8_t GPIO_AF);- Tham số: - GPIOx– Chỉ định cổng GPIO (ví dụ:- GPIOA,- GPIOB,...). Mỗi cổng điều khiển một nhóm các chân I/O.
- GPIO_PinSource– Chỉ định chân cụ thể trong cổng (ví dụ:- GPIO_PinSource0,- GPIO_PinSource1,... tương ứng với PIN0, PIN1...).
- GPIO_AF– Chỉ định chức năng phụ được gán cho chân đó (ví dụ:- GPIO_AF_TIM3nghĩa là gán chân đó cho chức năng của Timer 3).
 
Cấu hình Timer (Bộ định thời) 
Để cấu hình thông số cho Timer, ta cần sử dụng một cấu trúc đặc biệt như hình dưới đây:

Các thành phần chính trong cấu trúc cấu hình Timer bao gồm:
- TIM_Prescaler: Giá trị bộ chia trước (Prescaler) Dùng để chia tần số xung nhịp của Timer. Giá trị từ- 0x0000đến- 0xFFFF. → Mục đích: Làm chậm tốc độ đếm của Timer, giúp mở rộng khoảng thời gian điều khiển.
- TIM_CounterMode: Chế độ đếm của Timer Xác định cách Timer sẽ đếm:- TIM_CounterMode_Up: Đếm lên
- TIM_CounterMode_Down: Đếm xuống
- TIM_CounterMode_CenterAligned: Đếm lên rồi đếm xuống (đối xứng)
 
- TIM_Period: Giá trị chu kỳ (ARR - Auto-reload Register) Xác định điểm mà Timer sẽ “tràn” (reset về 0), từ đó tạo ra sự kiện cập nhật (update event). → Đây là yếu tố quyết định tần số PWM.
- TIM_ClockDivision: Chia xung nhịp nội bộ thêm một lần nữa sau Prescaler:- TIM_CKD_DIV1: Không chia
- TIM_CKD_DIV2: Chia 2
- TIM_CKD_DIV4: Chia 4
 
- TIM_RepetitionCounter: Bộ đếm lặp lại – chỉ áp dụng cho- TIM1và- TIM8Xác định số lần PWM lặp lại trước khi tạo ra một sự kiện cập nhật. Trong PWM:- (N + 1)là số chu kỳ PWM ở chế độ cạnh (edge-aligned)
- (N + 1)/2ở chế độ đối xứng (center-aligned)
 
Cấu trúc định nghĩa:
TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;Cấu hình tham số:
// Khởi tạo Timer 3
TIM_TimeBaseStructure.TIM_Period = arr;               // Giá trị auto-reload (chu kỳ PWM)
TIM_TimeBaseStructure.TIM_Prescaler = psc - 1;        // Giá trị prescaler (chia tần)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // Không chia thêm
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  // Đếm lênKhởi tạo Timer với các tham số đã cấu hình:
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// Khởi tạo Timer3 với thông số được lưu trong TIM_TimeBaseStructureCấu hình PWM 
Sau khi đã cấu hình các thông số cho Timer 3, chúng ta cần cấu hình một cấu trúc riêng để thiết lập chế độ PWM.
Khai báo cấu trúc cấu hình PWM:
TIM_OCInitTypeDef  TIM_OCInitStructure;Cấu hình các tham số PWM:
// Khởi tạo kênh 2 của TIM3 ở chế độ PWM
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;            // Chế độ PWM1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // Bật đầu ra so sánh
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;     // Cực tính đầu ra: mức cao khi kích hoạtÁp dụng cấu hình PWM cho kênh 2:
TIM_OC2Init(TIM3, &TIM_OCInitStructure); // Khởi tạo chế độ PWM cho kênh 2 của Timer 3Lưu ý: Mỗi kênh PWM sẽ có hàm cấu hình riêng biệt. Ví dụ:
- Kênh 2 sử dụng
TIM_OC2Init- Kênh 3 sử dụng
TIM_OC3Init- Kênh 4 sử dụng
TIM_OC4InitTương tự, các hàm cấu hình preload cũng theo từng kênh:
TIM_OC2PreloadConfig
TIM_OC3PreloadConfig- ...
Bật chức năng preload cho thanh ghi so sánh (CCR)
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);  // Bật chức năng preload cho thanh ghi CCR2 của TIM3Lưu ý: Mỗi kênh PWM sẽ có hàm cấu hình preload riêng. Ví dụ:
- Kênh 2 sử dụng
TIM_OC2PreloadConfig- Kênh 3 sử dụng
TIM_OC3PreloadConfig- Kênh 4 sử dụng
TIM_OC4PreloadConfigCác hàm này đảm bảo rằng giá trị mới trong thanh ghi so sánh (CCR) chỉ được cập nhật tại thời điểm thích hợp, tránh gây ra nhiễu xung trong quá trình phát PWM.
Kích hoạt Timer 
Sau khi hoàn tất cấu hình PWM và các thông số liên quan, chúng ta cần bật Timer để bắt đầu hoạt động. Sử dụng hàm sau:
TIM_Cmd(TIM3, ENABLE);  // Bật Timer 3Điều chỉnh độ rộng xung (Duty Cycle) của kênh Timer 
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint32_t Compare2)
Hàm TIM_SetCompare2 được dùng để thiết lập giá trị thanh ghi so sánh CCR2 (Capture Compare Register 2) của bộ định thời. Thông qua đó, ta có thể điều khiển chức năng so sánh đầu ra (Output Compare) hoặc bắt đầu vào (Input Capture) của Timer.
Hàm này rất hữu ích trong các ứng dụng như:
- Tạo độ trễ chính xác theo thời gian
- Đo độ rộng xung
- Sinh tín hiệu PWM có độ rộng xung thay đổi
Tham số: 
- TIMx: Xác định bộ định thời cần thao tác, ví dụ- TIM1,- TIM2,- TIM3,... (Số lượng Timer phụ thuộc vào dòng chip STM32 mà bạn đang sử dụng.)
- Compare2: Giá trị mới cho thanh ghi CCR2 Đây là giá trị được so sánh với bộ đếm Timer. Trong chế độ PWM, giá trị này xác định độ rộng xung (tức là thời gian ở mức cao trong một chu kỳ PWM).
Ghi chú: 
- Không có giá trị trả về (void)
- Trong ứng dụng PWM, việc tăng giá trị CCR2sẽ kéo dài thời gian mức cao → tăng duty cycle
- Ngược lại, giảm giá trị CCR2sẽ giảm duty cycle
Ví dụ: Nếu
ARR = 999vàCCR2 = 500→ duty cycle ≈ 50%
Hàm điều khiển đèn LED thở (fading led) 
Để tạo ra hiệu ứng đèn LED thở, trước tiên ta cần hiểu nguyên lý hoạt động của nó.
Hiệu ứng "thở" là quá trình đèn LED dần sáng lên, sau đó dần mờ đi, rồi lặp lại liên tục. Để điều khiển độ sáng của LED, ta sử dụng kỹ thuật PWM (Pulse Width Modulation) – tức là thay đổi độ rộng xung.
- Duty cycle (độ rộng xung) càng lớn → thời gian đèn bật trong mỗi chu kỳ nhiều hơn → LED sáng hơn
- Duty cycle càng nhỏ → thời gian bật ít → LED mờ hơn
Vì vậy, bằng cách tăng rồi giảm duty cycle, ta có thể tạo ra hiệu ứng "thở" cho LED.
Trước đó, chúng ta đã giới thiệu hàm
TIM_SetCompare2()dùng để thiết lập duty cycle.
void pwm_breathing_lamp(void)
{
    uint32_t brightness = 0;  // Độ sáng hiện tại
    int step = 10;            // Bước tăng/giảm độ sáng
    // Tăng dần độ sáng
    for (brightness = 0; brightness < 1000; brightness += step)
    {
        TIM_SetCompare2(TIM3, brightness); // Thiết lập duty cycle cho kênh 2 của TIM3
        delay_ms(10);
    }
    // Giảm dần độ sáng
    for (brightness = 1000; brightness > 10; brightness -= step)
    {
        TIM_SetCompare2(TIM3, brightness); // Tiếp tục điều chỉnh kênh 2
        delay_ms(10);
    }
}Giải thích chi tiết hoạt động của hàm PWM đèn thở:
- Khởi tạo độ sáng và bước thay đổi Biến brightnessbắt đầu từ 0, tức LED đang tắt hoặc ở mức sáng thấp nhất. Biếnstepđược đặt là 10, đại diện cho bước nhảy mỗi lần thay đổi độ sáng.
- Tăng dần độ sáng Dùng vòng lặp forđể tăngbrightnesstừ 0 đến 1000 (giả sử 1000 là mức sáng tối đa). Trong mỗi vòng lặp:- Gọi hàm TIM_SetCompare2(TIM3, brightness)để điều chỉnh duty cycle của PWM.
- Gọi delay_ms(10)để tạo hiệu ứng thay đổi sáng mượt mà (10 ms giữa mỗi lần thay đổi).
 
- Gọi hàm 
- Giảm dần độ sáng Sau khi đạt mức sáng tối đa (1000), vòng lặp thứ hai giảmbrightnessdần về 10 (gần tối hoàn toàn). Logic y hệt phần tăng sáng, nhưng lần này là giảm giá trị PWM để làm LED mờ dần.
Hiện tượng thực nghiệm 
Mã nguồn trong chương này có thể tìm thấy tại:
Đường dẫn trong tài liệu đi kèm bo mạch:立创·梁山派·天空星STM32F407VET6开发板资料/第03章软件资料/代码例程/009 PWM呼吸灯
Sau khi nạp chương trình vào bo mạch, bạn sẽ quan sát thấy đèn LED dần sáng lên, sau đó dần tối đi, tạo thành một chu kỳ sáng – mờ – sáng lặp đi lặp lại. Đây chính là hiệu ứng đèn thở được tạo ra bằng kỹ thuật PWM.