🌱 STM32 - 3. Cấu hình thanh ghi module Clock RCC vi điều khiển STM32
Trước khi học cách cấu hình các thanh ghi trong vi điều khiển STM32, chúng ta cần phải nắm được địa chỉ của chúng trong Vi điều khiển, có thể tiến hành lập trình trực tiếp truy xuất vào thanh ghi sử dụng pointer. Tuy nhiên, nên sử dụng các define macro với từng địa chỉ để dễ sử dụng và lưu trữ hơn. Đây cũng chính là ý tưởng của các thư viện.
Ở bài viết này, mình sẽ cùng các bạn tìm hiểu về cách lập trình cấu hình Clock module RCC với vi điều khiển STM32, truy xuất trực tiếp vào thanh ghi của RCC và kết hợp sử dụng các Define macro để dễ dàng cho việc đọc hiểu.
Để cấu hình thanh ghi module RCC (Reset and Clock Control) trên vi điều khiển STM32 (bao gồm STM32F401 như bạn đang quan tâm), bạn cần hiểu cách thiết lập nguồn clock, cấu hình PLL (Phase-Locked Loop), và phân chia tần số cho các bus (AHB, APB1, APB2). Hãy đọc bài viết:
👉 Để nắm được địa chỉ các thanh ghi, chúng ta cần tham khảo 2 nguồn tài liệu. Một là bản đồ bộ nhớ - Memory Map trong Vi điều khiển STM32 và hai là Reference Manual của Vi điều khiển.
Theo Memory Map thì vùng nhớ cho Peripheral sẽ bắt đầu từ địa chỉ 0x4000.000, và đó cũng chính là địa chỉ của bus APB1. Bus AHB1 có offset là 0x0002.0000 so với APB1 bus.
Vì vậy, có thể define các địa chỉ của các bus như hình trên. Khối RCC và GPIOA được cấp clock từ bus AHB1, vì vậy, địa chỉ offset sẽ theo base của bus AHB1, tương ứng offset là 3800 và 0000.
👉 Bây giờ, để define địa chỉ của các thanh ghi RCC cấu hình Clock, các bạn tham khảo tài liệu RM. Ví dụ, thanh ghi quan trọng nhất để cấu hình chọn Clock nội hay ngoại là thanh ghi RCC_CR
Thanh ghi này có offset là 0x00 so với địa chỉ Base của RCC mà chúng ta define ở trên. Vì vậy, có thể định nghĩa địa chỉ của thanh ghi này là:
#define RCC_CR RCC_BASE_ADDR
Tương tự với những thanh ghi khác, khi đã có địa chỉ Base và Address Offset thì chỉ cần cộng 2 giá trị địa chỉ này lại với nhau ta sẽ có địa chỉ thanh ghi.
👉 Sau khi có địa chỉ của thanh ghi, bây giờ chúng ta có thể cấu hình chúng theo mong muốn.
Ví dụ 1: Cấu hình HSE làm System Clock
Mục tiêu: Sử dụng HSE (High-Speed External, ví dụ thạch anh 8 MHz) làm nguồn clock hệ thống (SYSCLK).
➤ Bước 1: Định nghĩa địa chỉ các thanh ghi
Theo bài viết, chúng ta cần định nghĩa địa chỉ cơ sở của RCC và các offset của thanh ghi liên quan:
- Địa chỉ cơ sở của RCC (RCC_BASE_ADDR) dựa trên bus AHB1: 0x40023800 (theo Memory Map của STM32F401 trong RM0368).
- Offset của RCC_CR: 0x00.
- Offset của RCC_CFGR: 0x08.
#define AHB1_BASE_ADDR 0x40020000 // Base Address of bus AHB1
#define RCC_BASE_ADDR (AHB1_BASE_ADDR + 0x3800) // RCC offset 0x3800 from AHB1
#define RCC_CR (RCC_BASE_ADDR + 0x00) // RCC_CR
#define RCC_CFGR (RCC_BASE_ADDR + 0x08) // RCC_CFGR
➤ Bước 2: Tạo pointer cho các thanh ghi
Sử dụng con trỏ kiểu volatile uint32_t* để truy cập trực tiếp vào thanh ghi:
volatile uint32_t* pRCC_CR = (volatile uint32_t*)RCC_CR; // Pointer to RCC_CR
volatile uint32_t* pRCC_CFGR = (volatile uint32_t*)RCC_CFGR; // Pointer to RCC_CFGR
❓Tại sao cần dùng từ khóa Volatile khi định nghĩa thanh ghi tại đây
➤ Bước 3: Enable HSE và đợi clock ổn định
Trong RCC_CR:
- Bit 16 (HSEON): Đặt thành 1 để bật HSE.
- Bit 17 (HSERDY): Kiểm tra để đợi HSE ổn định.
*pRCC_CR |= (1 << 16); // Set HSE (HSEON = 1)
while (!(*pRCC_CR & (1 << 17))); // Wait HSERDY = 1 for HSE clock is stable
➤ Bước 4: Chọn HSE làm System Clock
Trong RCC_CFGR:
- Bit 0-1 (SW): Đặt thành 01 để chọn HSE làm SYSCLK.
*pRCC_CFGR &= ~(3 << 0); // Clear bit SW[1:0]
*pRCC_CFGR |= (1 << 0); // SW = 01: Choose HSE as SYSCLK
while ((*pRCC_CFGR & (3 << 2)) != (1 << 2)); // Wait SWS = 01 (SYSCLK switch to HSE)
➤ Code hoàn chỉnh cho Ví dụ 1
#define AHB1_BASE_ADDR 0x40020000
#define RCC_BASE_ADDR (AHB1_BASE_ADDR + 0x3800)
#define RCC_CR (RCC_BASE_ADDR + 0x00)
#define RCC_CFGR (RCC_BASE_ADDR + 0x08)
void HSE_SystemClock_Config(void)
{
volatile uint32_t* pRCC_CR = (volatile uint32_t*)RCC_CR;
volatile uint32_t* pRCC_CFGR = (volatile uint32_t*)RCC_CFGR;
// Set HSE is ON
*pRCC_CR |= (1 << 16);
while (!(*pRCC_CR & (1 << 17)));
// Setup HSE as SYSCLK
*pRCC_CFGR &= ~(3 << 0);
*pRCC_CR |= (1 << 0);
while ((*pRCC_CFGR & (3 << 2)) != (1 << 2));
}
Kết quả: SYSCLK sẽ chạy ở tần số của HSE (ví dụ 8 MHz nếu thạch anh là 8 MHz). Nếu muốn tăng tần số, cần cấu hình thêm PLL (Ví dụ 3)
Ví dụ 2: Cấu hình cấp Clock cho ngoại vi GPIOA
Mục tiêu: Cấu hình enable clock cho GPIOA (nằm trên bus AHB1). Các ngoại vi luôn cần có clock để hoạt động (Clock cho phép Core có thể ghi vào thanh ghi ngoại vi - Bus Peripheral Clock, và clock cho sự hoạt động nhanh/chậm của các ngoại vi - như Timer hay UART).
➤ Bước 1: Định nghĩa địa chỉ thanh ghi
- Địa chỉ cơ sở của RCC: 0x40023800.
- Offset của RCC_AHB1ENR: 0x30 (theo Reference Manual).
#define AHB1_BASE_ADDR 0x40020000
#define RCC_BASE_ADDR (AHB1_BASE_ADDR + 0x3800)
#define RCC_AHB1ENR (RCC_BASE_ADDR + 0x30) // RCC_AHB1ENR address
➤ Bước 2: Tạo pointer cho thanh ghi
volatile uint32_t* pRCC_AHB1ENR = (volatile uint32_t*)RCC_AHB1ENR; // Pointer to RCC_AHB1ENR
➤ Bước 3: Cấu hình cấp clock cho GPIOA
-
Trong RCC_AHB1ENR:
- Bit 0 (GPIOAEN): Đặt thành 1 để bật clock cho GPIOA.
*pRCC_AHB1ENR |= (1 << 0); // Enable clock for GPIOA (GPIOAEN = 1)
➤ Code hoàn chỉnh cho Ví dụ 2
#define AHB1_BASE_ADDR 0x40020000
#define RCC_BASE_ADDR (AHB1_BASE_ADDR + 0x3800)
#define RCC_AHB1ENR (RCC_BASE_ADDR + 0x30)
void GPIOA_Clock_Config(void)
{
volatile uint32_t* pRCC_AHB1ENR = (volatile uint32_t*)RCC_AHB1ENR;
// Enable clock for GPIOA
*pRCC_AHB1ENR |= (1 << 0);
}
Kết quả: GPIOA được cấp clock từ bus AHB1 và sẵn sàng để cấu hình (ví dụ trong bài về GPIO sau).
Ví dụ 3: Cấu hình clock cho core (SYSCLK) = 84 MHz
Mục tiêu: Setup tần số SYSCLK (clock cho core) đạt 84 MHz, sử dụng HSE (8 MHz) qua PLL. Có nhiều ứng dụng yêu cầu tần số cao - khi hoạt động với tần số càng cao thì CPU sẽ xử lý các câu lệnh càng nhanh, tuy nhiên năng lượng tiêu tốn cũng sẽ nhiều hơn.
➤ Bước 1: Định nghĩa địa chỉ các thanh ghi
Các thanh ghi cần dùng:
- RCC_CR (offset 0x00): Bật HSE và PLL.
- RCC_PLLCFGR (offset 0x04): Cấu hình PLL.
- RCC_CFGR (offset 0x08): Chọn SYSCLK và phân chia tần số.
#define AHB1_BASE_ADDR 0x40020000
#define RCC_BASE_ADDR (AHB1_BASE_ADDR + 0x3800)
#define RCC_CR (RCC_BASE_ADDR + 0x00) // RCC_CR
#define RCC_PLLCFGR (RCC_BASE_ADDR + 0x04) // RCC_PLLCFGR
#define RCC_CFGR (RCC_BASE_ADDR + 0x08) // RCC_CFGR
➤ Bước 2: Tạo pointer cho các thanh ghi
volatile uint32_t* pRCC_CR = (volatile uint32_t*)RCC_CR;
volatile uint32_t* pRCC_PLLCFGR = (volatile uint32_t*)RCC_PLLCFGR;
volatile uint32_t* pRCC_CFGR = (volatile uint32_t*)RCC_CFGR;
➤ Bước 3: Cấu hình clock cho core = 84 MHz
- Enable HSE:
- Bit 16 (HSEON) trong RCC_CR.
- Cấu hình PLL:
- Trong RCC_PLLCFGR:
- PLLM = 8 (8 MHz / 8 = 1 MHz).
- PLLN = 168 (1 MHz * 168 = 168 MHz).
- PLLP = 2 (168 MHz / 2 = 84 MHz).
- PLLSRC = 1 (chọn HSE).
- Bật PLL bằng bit 24 (PLLON) trong RCC_CR.
- Chọn PLL làm SYSCLK:
- Bit 0-1 (SW) trong RCC_CFGR = 10.
- Không chia tần số HCLK:
- HPRE = 0000 (HCLK = SYSCLK = 84 MHz).
➤ Code hoàn chỉnh cho Ví dụ 3
#define AHB1_BASE_ADDR 0x40020000
#define RCC_BASE_ADDR (AHB1_BASE_ADDR + 0x3800)
#define RCC_CR (RCC_BASE_ADDR + 0x00)
#define RCC_PLLCFGR (RCC_BASE_ADDR + 0x04)
#define RCC_CFGR (RCC_BASE_ADDR + 0x08)
void Core_Clock_84MHz_Config(void)
{
volatile uint32_t* pRCC_CR = (volatile uint32_t*)RCC_CR;
volatile uint32_t* pRCC_PLLCFGR = (volatile uint32_t*)RCC_PLLCFGR;
volatile uint32_t* pRCC_CFGR = (volatile uint32_t*)RCC_CFGR;
// Enable HSE Clock
*pRCC_CR |= (1 << 16); // HSEON = 1
while (!(*pRCC_CR & (1 << 17))); // Wait for HSERDY = 1
// Configure PLL: 8 MHz -> 84 MHz
*pRCC_PLLCFGR &= ~(0x3F << 0 | 0x1FF << 6 | 0x3 << 16 | 1 << 22); // Clear bit PLLM, PLLN, PLLP, PLLSRC
*pRCC_PLLCFGR |= (8 << 0) | // PLLM = 8
(168 << 6) | // PLLN = 168
(0 << 16) | // PLLP = 2 (00)
(1 << 22); // PLLSRC = HSE
*pRCC_CR |= (1 << 24); // PLLON = 1
while (!(*pRCC_CR & (1 << 25))); // Wait for PLLRDY = 1
// Configure Flash latency (2 wait states for 84 MHz, It need when increase the SYSCLK)
*((volatile uint32_t*)0x40023C00) = 2; // FLASH_ACR = 0x40023C00, LATENCY = 2WS
// Configure PLL as SYSCLK, HCLK = 84 MHz
*pRCC_CFGR &= ~(3 << 0 | 0xF << 4); // Clear SW and HPRE
*pRCC_CFGR |= (2 << 0); // SW = 10: PLL as SYSCLK
while ((*pRCC_CFGR & (3 << 2)) != (2 << 2)); // Wait for SWS = 10
}
Kết quả: SYSCLK và HCLK (clock cho core) đạt 84 MHz. Lưu ý thêm cấu hình FLASH_ACR để tránh lỗi khi tăng tần số.
Ví dụ 4: Cấu hình clock cấp cho ngoại vi USART = 12 MHz
Mục tiêu: Cấu hình tần số clock cho USART (ví dụ USART2 trên APB1) là 12 MHz. Việc cấu hình này khá quan trọng khi cấu hình các ngoại vi yêu cầu thay đổi về mặt tốc độ hoạt động như UART (Baudrate), Timer, ADC, SPI, I2C, ...
➤ Bước 1: Định nghĩa địa chỉ các thanh ghi
- RCC_CR, RCC_PLLCFGR, RCC_CFGR: Để cấu hình SYSCLK và PCLK1.
- RCC_APB1ENR (offset 0x40): Bật clock cho USART2.
#define AHB1_BASE_ADDR 0x40020000
#define RCC_BASE_ADDR (AHB1_BASE_ADDR + 0x3800)
#define RCC_CR (RCC_BASE_ADDR + 0x00)
#define RCC_PLLCFGR (RCC_BASE_ADDR + 0x04)
#define RCC_CFGR (RCC_BASE_ADDR + 0x08)
#define RCC_APB1ENR (RCC_BASE_ADDR + 0x40)
➤ Bước 2: Tạo pointer đại diện cho các thanh ghi
volatile uint32_t* pRCC_CR = (volatile uint32_t*)RCC_CR;
volatile uint32_t* pRCC_PLLCFGR = (volatile uint32_t*)RCC_PLLCFGR;
volatile uint32_t* pRCC_CFGR = (volatile uint32_t*)RCC_CFGR;
volatile uint32_t* pRCC_APB1ENR = (volatile uint32_t*)RCC_APB1ENR;
➤ Bước 3: Cấu hình clock cho USART = 12 MHz
USART2 nằm trên bus APB1, clock từ PCLK1. Để PCLK1 = 12 MHz, cần:
- SYSCLK = 48 MHz (từ PLL).
- PPRE1 = 4 (SYSCLK / 4 = 12 MHz).
Cấu hình PLL:
- HSE = 8 MHz.
- PLLM = 8 (8 MHz / 8 = 1 MHz).
- PLLN = 96 (1 MHz * 96 = 96 MHz).
- PLLP = 2 (96 MHz / 2 = 48 MHz).
Phân chia tần số:
- HPRE = 1 (HCLK = 48 MHz).
- PPRE1 = 4 (PCLK1 = 48 MHz / 4 = 12 MHz).
➤ Code hoàn chỉnh cho Ví dụ 4
#define AHB1_BASE_ADDR 0x40020000
#define RCC_BASE_ADDR (AHB1_BASE_ADDR + 0x3800)
#define RCC_CR (RCC_BASE_ADDR + 0x00)
#define RCC_PLLCFGR (RCC_BASE_ADDR + 0x04)
#define RCC_CFGR (RCC_BASE_ADDR + 0x08)
#define RCC_APB1ENR (RCC_BASE_ADDR + 0x40)
void USART2_Clock_12MHz_Config(void)
{
volatile uint32_t* pRCC_CR = (volatile uint32_t*)RCC_CR;
volatile uint32_t* pRCC_PLLCFGR = (volatile uint32_t*)RCC_PLLCFGR;
volatile uint32_t* pRCC_CFGR = (volatile uint32_t*)RCC_CFGR;
volatile uint32_t* pRCC_APB1ENR = (volatile uint32_t*)RCC_APB1ENR;
// Enable HSE
*pRCC_CR |= (1 << 16);
while (!(*pRCC_CR & (1 << 17)));
// Wait for HSERDY = 1
// Configure PLL: 8 MHz -> 48 MHz
*pRCC_PLLCFGR &= ~(0x3F << 0 | 0x1FF << 6 | 0x3 << 16 | 1 << 22);
*pRCC_PLLCFGR |= (8 << 0) | // PLLM = 8
(96 << 6) | // PLLN = 96
(0 << 16) | // PLLP = 2
(1 << 22); // PLLSRC = HSE
*pRCC_CR |= (1 << 24); // PLLON = 1
while (!(*pRCC_CR & (1 << 25))); // Wait for PLLRDY = 1
//Configure Flash latency (1 wait state for 48 MHz)
*((volatile uint32_t*)0x40023C00) = 1; // FLASH_ACR, LATENCY = 1WS
// Configure PLL as SYSCLK, HCLK = 48 MHz, PCLK1 = 12 MHz
*pRCC_CFGR &= ~(3 << 0 | 0xF << 4 | 0x7 << 10); // Clear SW, HPRE, PPRE1
*pRCC_CFGR |= (2 << 0) | // SW = 10: PLL as SYSCLK
(0 << 4) | // HPRE = 0000: HCLK = SYSCLK / 1
(5 << 10); // PPRE1 = 101: PCLK1 = HCLK / 4
while ((*pRCC_CFGR & (3 << 2)) != (2 << 2)); // Wait for SWS = 10
// Enable Clock for USART2
*pRCC_APB1ENR |= (1 << 17); // USART2EN = 1
}
Kết quả: PCLK1 = 12 MHz, cung cấp clock cho USART2 (hoặc các USART khác trên APB1 như USART3 nếu có). Bạn có thể điều chỉnh bit trong RCC_APB1ENR nếu muốn dùng USART khác (ví dụ USART1 trên APB2 thì dùng RCC_APB2ENR).
❓Tại sao cần dùng từ khóa Volatile khi định nghĩa thanh ghi tại đây
>>>>>> Follow ngay <<<<<<
Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊
cho em hỏi phần clock configuration chỗ (uin32_t*) là gì vậy ạ
Trả lờiXóaĐây là ép kiểu sang uint32_t * nha em, thanh ghi anh đang định nghĩa kiểu con trỏ uint_32
XóaNhận xét này đã bị tác giả xóa.
XóaSao em code trên con f103c8 như thế mà nó có lỗi No section matches selector ạ
XóaCái define là một address, em muốn đọc giá trị của address này thì phải gán cho define này là một con trỏ (địa chỉ) => bằng cách ép kiểu nó sang (uint_32*)
XóaCác địa chỉ trên anh đang lấy từ con STM32F401, nếu dùng con F1 thì em phải xem lại địa chỉ nhé. Mỗi con có các địa chỉ khác nhau mà
XóaEm đổi hết sang địa chỉ của f1 rồi ạ
XóaCái địa chỉ base + offset nữa, nếu đổi chuẩn thì code ok rồi nha e
XóaNhận xét này đã bị tác giả xóa.
Trả lờiXóaa cho em hỏi là lúc em chọn HSE làm system clock,lúc em debug em thấy chỗ HSE bit 16 17 lên 1 rồi,nhưng chỗ HSI nó cũng là 1 có ảnh hưởng gì không a? mình có cần clear 2 bit HSI ko a?
Trả lờiXóaSTM32 có thanh ghi RCC_CFGR - trường bit SW (System Clock Switch) đó em. Chọn dùng clock nào.
Xóa