การเขียนโปรแกรมบอร์ด STM32F411 BlackPill โดยใช้ Arm CMSIS และ PlatformIO#

Keywords: STM32F4, BlackPill Microcontroller Boards, Arm CMSIS API


บอร์ด BlackPill STM32F411CE#

บอร์ด BlackPill STM32F411CE (Arm 32-bit Cortex-M4 CPU with FPU, 512 KB of Flash, 128KB of SRAM, up to 100MHz ) ของบริษัท WeAct Studio จากประเทศจีน เป็นบอร์ดไมโครคอนโทรลเลอร์ที่มีราคาไม่แพง สามารถนำมาใช้ฝึกเขียนโค้ดสำหรับชิปตระกูล STM32F4 หรือใช้เป็นตัวเลือกแทนที่บอร์ด STM32F401RE Nucleo

การอัปโหลดไฟล์เฟิร์มแวร์ไปยังบอร์ด STM32F4 ทำได้หลายวิธี เช่น

  • การใช้ USB DFU Bootloader โดยใช้เพียงสาย USB เชื่อมต่อกับพอร์ต USB ของคอมพิวเตอร์ผู้ใช้
  • การใช้อุปกรณ์ STLink/v2 USB Dongle
  • การใช้อุปกรณ์ SEGGER J-Link Programmer / Debugger
  • การใช้อุปกรณ์ CMSIS-DAP / DAPLink compatible USB Debug Probe

บอร์ด BlackPill STM32F411CE ไม่มีวงจร ST-Link/v2 เหมือนบอร์ด STM32 Nucleo ผู้ใช้สามารถอัปโหลดไฟล์เฟิร์มแวร์ผ่านทางพอร์ต USB Type-C ได้ โดยใช้สาย USB เพียงเส้นเดียว ด้วยวิธี USB DFU Bootloader ซึ่งเป็นโปรแกรมที่ถูกใส่ไว้ภายในหน่วยความจำของชิป STM32F4 จากโรงงานแล้ว (กดปุ่ม BOOT0 ค้างไว้ แล้วกดปุ่ม RESET แล้วปล่อย ก็จะทำให้ชิป STM32F4 เข้าสู่โหมด DFU Bootloader)

อีกทางเลือกหนึ่งคือ การใช้อุปกรณ์ USB CMSIS-DAP Adapter เชื่อมต่อเข้าที่ขา SWD Interface (3.3V, DIO, SCK, GND Pins)

รูป: BlackPill STM32F411CE v2.0 Pinout

รูป: ผังวงจรของบอร์ด BlackPill STM32F411CE v2.0

ตัวเลือกในการเขียนโค้ดภาษา C/C++ สำหรับ STM32F4 เช่น

การเขียนโปรแกรมสำหรับ STM32 (platform = ststm32) โดยใช้ PlatformIO (PIO) มีตัวเลือกอยู่หลายวิธี (เรียกว่า ST-STM32 Framworks) เช่น

บทความนี้สาธิตการใช้งาน VS Code IDE + PlatformIO (ใช้ PlatformIO IDE v3.2.0 และ PIO Core v6.1.7 กับระบบปฏิบัติการ Windows) และมีตัวอย่างการเขียนโค้ด โดยใช้ ARM CMSIS 5.x เพื่อนำไปทดลองใช้กับบอร์ด BlackPill STM32F411CE (v2.0) ขั้นตอนการติดตั้งและใช้งาน PlatformIO สามารถดูได้จาก "Installation of PlatformIO IDE for VSCode"

ในการทดลองกับอุปกรณ์ฮาร์ดแวร์ นอกจากจะได้ลองใช้วิธี DFU แล้ว อีกหนึ่งวิธี คือ การอัปโหลดเฟิร์มแวร์ด้วย SWD โดยใช้โมดูล เช่น

รูป: อุปกรณ์ WCHLink Mini CH549F DAPLink

รูป: อุปกรณ์ MuseLab - Mini DAPLink-HS Debug Probe

รูป: ตัวอย่างอุปกรณ์ที่ได้นำมาทดลองใช้งาน

 

การเขียนโค้ดด้วย Arm CMSIS 5.x API ถือว่า เป็นระดับต่ำกว่า การใช้ STM32Cube HAL หรือ Arduino Core (STM32duino) หรืออาจเรียกรูปแบบการเขียนโค้ดในลักษณะนี้ว่า *Bare-Metal Programming ดังนั้นผู้ใช้จะต้องเข้าใจหลักการทำงานของไมโครคอนโทรลเลอร์ในระดับฮาร์ดแวร์ เช่น การกำหนดหรือตั้งค่าบิตในรีจิสเตอร์ต่าง ๆ ของวงจรภายในที่ต้องการใช้งาน

แนะนำให้ศึกษารายละเอียดจากเอกสาร: "RM0383: Reference Manual STM32F411xC/E Advanced Arm-based 32-bit MCUs" (local copy)

การกำหนดชื่อของรีจิสเตอร์ต่าง ๆ ของ STM32F411xE และบิตของรีจิสเตอร์ สามารถดูได้จากไฟล์ stm32f411xe.h

รูป: ตัวอย่างโครงสร้างข้อมูลสำหรับบิตในรีจิสเตอร์ของ GPIO (บางส่วน)

รูป: ตัวอย่างโครงสร้างข้อมูลสำหรับบิตในรีจิสเตอร์ของ USART (บางส่วน)

รูป: ตัวอย่างค่าคงที่สำหรับรีจิสเตอร์ ODR (Output Data Register) ของพอร์ต GPIO

รูป: ตัวอย่างค่าคงที่สำหรับรีจิสเตอร์ MODERS (GPIO Mode Register)

 


การสร้างโปรเจกต์เพื่อเขียนโค้ด C/C++ ด้วย Arm CMSIS Framework#

เริ่มต้นด้วย VS Code IDE แล้วคลิกเลือกที่ไอคอนของ PlatformIO (PIO) เพื่อเข้าสู่หน้า PIO Home เลือกทำคำสั่ง "+New Project"

รูป: VS Code IDE + PIO Home

ในหน้าต่าง Project Wizard ให้ตั้งชื่อโปรเจกต์ เลือกบอร์ดไมโครคอนโทรลเลอร์ และเลือก Framework: CMSIS ตามลำดับ

รูป: การสร้างโปรเจกต์ใหม่ด้วย PIO Project Wizard

จากนั้นให้เพิ่มไฟล์ main.c ลงในไดเรกทอรี src/ ของโปรเจกต์ แล้วลองเขียนโค้ดตามตัวอย่างต่อไปนี้ ซึ่งจะทำให้ LED (active-low) บนบอร์ด (ตรงกับขา PC13 ) กระพริบได้ ฟังก์ชัน Init_GPIO() จะใช้สำหรับการตั้งค่าเพื่อใช้งานขา PC13 เป็นขาเอาต์พุต

#include <stdint.h>
#include "stm32f4xx.h"

// The onboard LED is connected to the PC13 pin.
#define LED1       (13)
#define GPIOMODER  (GPIO_MODER_MODER13_0)

// Global variables
volatile uint32_t ticks = 0;
uint32_t saved_ticks = 0;

void SysTick_Handler(void) { // SysTick interrupt handler
    ticks++; // Increment the tick count.
}

void Init_SysTick() {
    // Disable SysTick.
    SysTick->CTRL = 0;
    // Set SysTick period to 1msec.
    SysTick->LOAD = (SystemCoreClock/1000UL) - 1UL;
    // Set interrupt priority of SysTick to least urgency.
    NVIC_SetPriority( SysTick_IRQn, (1UL<<__NVIC_PRIO_BITS)-1UL ); 
    // Reset the SysTick counter value.
    SysTick->VAL = 0UL;
    // Select CPU clock, enable SysTick interrupt and SysTick.
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk   |
                    SysTick_CTRL_ENABLE_Msk;
}

void Init_GPIO() {
    // Enable the clock to GPIOC.
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;
    // Set PC13 pin to be a digital output.
    GPIOC->MODER |= GPIOMODER;
}

int main(void) {
    SystemInit(); // Initialize the system.
    SystemCoreClockUpdate(); // Update the system core clock.

    Init_SysTick(); // Initialize SysTick (24-bit SysTick Timer).
    Init_GPIO();    // Initialize GPIO (for LED pin).

    for (;;) { // Endless loop
        if ( ticks - saved_ticks >= 500 ) { // every 500 msec
            GPIOC->ODR ^= (1<<LED1); // Toggle LED pin.
            saved_ticks = ticks;     // Save the tick value.
        }
    }
    return 0;
}

ในตัวอย่างที่มีการเปิดใช้งานวงจรภายในที่มีชื่อว่า SysTick Timer ซึ่งเป็นวงจรตัวนับ ที่มีขนาด 24 บิต ของซีพียูตระกูล Arm Cortex-M โดยจะต้องมีการตั้งค่าให้นับถอยหลังจนถึง 0 แล้วจะเกิดอินเทอร์รัพท์และเริ่มต้นนับถอยหลังใหม่ ฟังก์ชัน Init_SysTick() จะใช้สำหรับการตั้งค่าใช้งานวงจร SysTick ตามที่ได้กล่าวไป

การกำหนดช่วงเวลานับถอยหลัง ตั้งค่าคาบเวลาไว้เท่ากับ 1 msec ในโค้ดตัวอย่างนี้ และมีการเปิดใช้งานอินเทอร์รัพท์สำหรับวงจรนี้ด้วย ดังนั้นเมื่อนับถึง 0 จะเกิดเหตุการณ์อินเทอร์รัพท์ และจะมีการเรียกหรือทำคำสั่งในฟังก์ชันชื่อ SysTick_Handler() โดยอัตโนมัติ ซึ่งจะเกิดขึ้นทุก ๆ 1 มิลลิวินาที

ฟังก์ชัน SysTick_Handler() ทำหน้าที่เป็น Interrupt Handler ในตัวอย่างนี้ เมื่อฟังก์ชันดังกล่าวทำงานในแต่ละครั้ง จะมีการเพิ่มค่าของตัวแปรภายนอก ticks เป็นการนับขึ้น หรือเพิ่มค่าตัวเลขครั้งละหนึ่ง

รูป: การสร้างและเพิ่มไฟล์ main.c เป็นส่วนหนึ่งของโปรเจกต์

 


การทำขั้นตอน PIO Build & Debug#

ไฟล์ platformio.ini จะใช้สำหรับการตั้งค่าต่าง ๆ ของโปรเจกต์ เช่น การเลือกบอร์ดเป้าหมาย รูปแบบการเขียนโค้ด การเลือกรูปแบบหรือวิธีการอัปโหลดไฟล์เฟิร์มแวร์ วิธีการดีบักการทำงานของโปรแกรมในฮาร์ดแวร์ เป็นต้น ในตัวอย่างนี้ได้เลือกใช้วิธี Upload Protocol เป็น cmsis-dap สำหรับการโปรแกรมและเครื่องมือสำหรับการดีบัก (Debug Tool) ก็ใช้เป็น cmsis-dap เช่นกัน

[env:blackpill_f411ce]
platform = ststm32
board = blackpill_f411ce
framework = cmsis

; set build type to debug
build_type = debug
; set debug tool
debug_tool = cmsis-dap

; set upload protocol
; upload_protocol = dfu
; upload_protocol = stlink
; upload_protocol = jlink
upload_protocol = cmsis-dap

; monitor speed (baudrate)
monitor_speed = 115200

 

รูป: ตัวอย่างการตั้งค่าในไฟล์ platformio.ini ของโปรเจกต์

ถัดไปให้ลองทำขั้นตอน PIO Build ซึ่งจะได้ไฟล์ชื่อ firmware.bin และ PIO Debug ตามลำดับ โดยจะต้องเชื่อมต่อบอร์ด BlackPill กับคอมพิวเตอร์ผู้ใช้ ผ่านทางพอร์ต USB และมีอุปกรณ์ CMSIS / DAPlink Debug Probe เป็นตัวกลางในการเชื่อมต่อ ก่อนเริ่มขั้นตอนอัปโหลดและดีบัก

รูป: การทำขั้นตอน PIO Build

เมื่อทำขั้นตอน PIO Build และ PIO Upload ได้สำเร็จแล้ว ให้ลองคลิกเลือก RUN AND DEBUG จากแถบเมนูซ้ายมือ ผู้ใช้สามารถคลิกเลือกหมายเลขบรรทัดในโค้ดของไฟล์ main.c เพื่อใช้เป็นตำแหน่งหยุดชั่วคราว หรือเรียกว่า Breakpoints (จะมองเห็นสัญลักษณ์วงกลมสีแดง)

รูป: การทำขั้นตอน PIO Debug (มาหยุดที่ Breakpoint ในตำแหน่งแรก)

รูป: การทำขั้นตอน PIO Debug (มาหยุดที่ Breakpoint ในตำแหน่งถัดไป)

 


การส่งข้อความแบบ Serial โดยใช้วงจร USART2#

ตัวอย่างโค้ดถัดไป สาธิตการเปิดใช้งานวงจรภายในที่มีชื่อว่า USART ซึ่งมีให้เลือกใช้ 2 ชุด สำหรับ STM32F411CE ในตัวอย่างนี้ ได้เลือกใช้ USART2 และจะต้องกำหนดขา GPIO ที่เกี่ยวข้อง จำนวน 2 ขา เพื่อใช้เป็นขา TX2 และ RX2 ให้แก่วงจร USART2 (เลือกใช้ขา PA2 / PA3 ตามลำดับ) ฟังก์ชัน Init_USART() ทำหน้าที่ตั้งค่าการใช้งานวงจร USART2 โดยสามารถกำหนดค่า Baudrate ได้ เช่น เลือกใช้ค่า 115200

หากต้องการจะใช้คำสั่ง printf() เพื่อส่งข้อความออกทาง USART จะต้องมีการสร้างฟังก์ชัน int _write(int file, char *ptr, int len) และการทำงานของฟังก์ชันนี้ จะต้องใช้วงจร USART2 เพื่อส่งข้อความทีละตัวอักขระออกทางขา TX2

ในโค้ดตัวอย่างนี้ ได้มีการเรียกใช้ฟังก์ชัน printf() เพื่อแสดงข้อความระบุค่าความถี่งของซีพียู (หน่วยเป็น MHz) และสถานะเอาต์พุตสำหรับ LED ที่มีการอัปเดตในแต่ละครั้งของการวนลูป

#include <stdint.h>
#include <stdio.h>
#include "stm32f4xx.h"

// The onboard LED is connected to the PC13 pin.
#define LED1       (13)
#define GPIOMODER  (GPIO_MODER_MODER13_0)
#define USART      (USART2)

// Global variables
volatile uint32_t ticks = 0;
uint32_t saved_ticks = 0;

void SysTick_Handler(void) { // SysTick interrupt handler
    ticks++; // Increment the tick count.
}

void Init_SysTick() {
    // Disable SysTick.
    SysTick->CTRL = 0;
    // Set SysTick period to 1msec.
    SysTick->LOAD = (SystemCoreClock/1000UL) - 1UL;
    // Set interrupt priority of SysTick to least urgency.
    NVIC_SetPriority( SysTick_IRQn, (1UL<<__NVIC_PRIO_BITS)-1UL ); 
    // Reset the SysTick counter value.
    SysTick->VAL = 0UL;
    // Select CPU clock, enable SysTick interrupt and SysTick.
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk   |
                    SysTick_CTRL_ENABLE_Msk;
}

void Init_GPIO() {
    // Configure the Reset & Clock Control (RCC) register
    // to enable the clock to GPIOC.
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;
    // Set PC13 pin to be a digital output.
    GPIOC->MODER |= GPIOMODER;
}

void Init_USART( uint32_t baudrate ) {
    // Set pins PA2 (TX) and PA3 (RX) for serial communication.
    // Enable RCC for port A.
    RCC->AHB1ENR  |= RCC_AHB1ENR_GPIOAEN; 

    // Use PA2 in alternate function mode (Serial TX).
    GPIOA->MODER  |= GPIO_MODER_MODER2_1; 
    GPIOA->AFR[0] |= (7 << GPIO_AFRL_AFSEL2_Pos); 

    // Use PA3 in alternate function mode (Serial RX).
    GPIOA->MODER  |= GPIO_MODER_MODER3_1; 
    GPIOA->AFR[0] |= (7 << GPIO_AFRL_AFSEL3_Pos); 

    // Enable RCC for USART2.
    RCC->APB1ENR  |= RCC_APB1ENR_USART2EN; 

    // Set the baud rate.
    // Note: When the source clock is the HSI (16MHz) and 
    // the prescaler for the APB1 is set to 1.
    // USART2 is attached to the APB1 bus.
    uint16_t uart_div = (SystemCoreClock / baudrate);
    USART->BRR = ( ((uart_div / 16) << USART_BRR_DIV_Mantissa_Pos) |
                   ((uart_div % 16) << USART_BRR_DIV_Fraction_Pos) );

    // Enable the USART peripheral (Enable both Rx and Tx).
    USART->CR1 |= USART_CR1_RE | USART_CR1_TE | USART_CR1_UE;
}

// Redirect/Retarget printf() output.
void __io_putchar(uint8_t c) {
    // Wait until we are able to transmit
    while( !(USART->SR & USART_SR_TXE) ) {}; 
    USART->DR = c; // Transmit the character
}

int _write(int file, char *ptr, int len) {
    for (int idx=0; idx < len; idx++) {
        __io_putchar(*ptr++);
    }
    return len;
}

int main(void) {
    SystemInit(); // Initialize the system.
    SystemCoreClockUpdate(); // Update the system core clock.

    Init_SysTick(); // Initialize SysTick (24-bit SysTick Timer).
    Init_GPIO();    // Initialize GPIO (for LED pin).
    Init_USART( 115200 ); // Initialize USART (with baudrate=115200).
    // Disable I/O buffering, use unbuffered stream for output. 
    setbuf( stdout, NULL ); 

    printf( "CPU freq.: %lu MHz\r\n", (uint32_t)(SystemCoreClock/1e6) );
    for (;;) { // Endless loop
        if ( ticks - saved_ticks >= 500 ) { // every 500 msec
            saved_ticks = ticks;     // Save the tick value.
            GPIOC->ODR ^= (1<<LED1); // Toggle LED pin.
            printf( "LED: %lu\r\n", (GPIOC->ODR >> LED1)&1 );
        }
    }
    return 0;
}

เมื่อทำขั้นตอน PIO Build & Upload แล้ว ให้เปิด PIO Serial Monitor และกดปุ่มรีเซตการทำงานของบอร์ดไมโครคอนโทรลเลอร์ สังเกตข้อความที่ได้รับ

รูป: ตัวอย่างข้อความที่ได้รับผ่านทาง PIO Serial Monitor (baudrate: 115200)

จากข้อความเอาต์พุต จะเห็นได้ ความถี่ของระบบคือ 16MHz ซึ่งเกิดจากการใช้วงจร HSI - Internal RC Oscillator สร้างสัญญาณ Clock ที่อยู่ภายในชิป

 


การกำหนดความถี่ของระบบ#

ถัดไปเป็นตัวอย่างโค้ดสำหรับการตั้งค่าความถี่สำหรับการทำงานของซีพียูให้สูงขึ้น และสำหรับ STM32F411CE จะได้สูงสุด 100MHz และในตัวอย่างนี้จะลองตั้งค่าให้ได้ความถี่ 96MHz โดยรับสัญญาณ Clock จากวงจรภายนอก 25MHz External Crystal และใช้เป็นสัญญาณอินพุตเพื่อสร้างสัญญาณ Clock ด้วยวงจร PLL ให้ได้สัญญาณความถี่สูงขึ้น

ไดอะแกรมต่อไปนี้เป็นผังวงจรภายในของ STM32F411 แสดงเส้นทางการเลือกใช้สัญญาณ Clock ความถี่สูงจากภายนอก (HSE) เข้ามา แล้วนำไปผ่านวงจร PLL ซึ่งจะต้องมีการกำหนดตัวคูณ และตัวหารความถี่ (ได้แก่ PLL_M, PLL_N, PLL_P และ PLL_Q) เพื่อให้ได้ความถี่ของสัญญาณสำหรับวงจรภายในตามที่ต้องการ

รูป: STM32F4 Clock Path & Configuration Settings

ฟังก์ชัน void Init_SysClkConfig() จะใช้สำหรับการตั้งค่าของระบบเพื่อให้ได้สัญญาณความถี่ตามที่ต้องการ และ Arm CMSIS ได้ระบุว่า ตัวแปรภายนอกที่มีชื่อว่า SystemCoreClock จะเป็นตัวระบุค่าความถี่ของซีพียู (System Clock Frequency)

#include <stdint.h>
#include <stdio.h>
#include "stm32f4xx.h"

#define LED1       (13)
#define GPIOMODER  (GPIO_MODER_MODER13_0)
#define USART      (USART2)

#define PLL_M      (25)
#define PLL_N      (192)
#define PLL_P      (2)
#define PLL_Q      (4)

volatile uint32_t ticks = 0;
uint32_t saved_ticks = 0;

void Init_SysClkConfig() {  // Use 25MHz external crystal (HSE)
    // Clear the PLLCFGR register
    RCC->PLLCFGR = 0x00000000;
    // Enable HSE in the RCC->CR register and wait until it’s ready to use.
    RCC->CR |=RCC_CR_HSEON;
    while ((RCC->CR & RCC_CR_HSERDY)==0);
    // Enable the APB1 bus
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
    // Adjust the voltage scaling options to suit thr operating frequency.
    PWR->CR |= PWR_CR_VOS;

    // PLL Source HSE = 25MHz 
    RCC->PLLCFGR |= RCC_PLLCFGR_PLLSRC_HSE;

    //  PLL clock = HSE clock / PLL_M * PLL_N / PLL_P
    //            = 25 / 25 * 192 / 2 = 96 MHz
    //  USB clock = HSE clock / PLL_M * PLL_N / PLL_Q
    //            = 25 / 25 * 192 / 2 / 2 = 48 MHz !!!
    //  AHB clock = PLL clock / HPRE  = 96MHz/1 = 96MHz
    // APB1 clock = PLL clock / PPRE1 = 96MHz/2 = 48MHz
    // APB2 clock = PLL clock / PPRE2 = 96MHz/1 = 96MHz

    // Set clock prescalers for AHB, APB1 and APB2 busses
    RCC->CFGR |= RCC_CFGR_HPRE_DIV1;
    RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;
    RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;

    RCC->PLLCFGR = (RCC_PLLCFGR_PLLSRC_HSE)
                 | (PLL_M<<0) 
                 | (PLL_N<<6) 
                 | (((PLL_P>>1) - 1)<<16)
                 | (PLL_Q<<24);
    // Enable main PLL and wait until PLL is ready.
    RCC->CR |= RCC_CR_PLLON;  
    while((RCC->CR & RCC_CR_PLLRDY)==0);

    // Set the Flash latency (3 wait cycles) and 
    // enable the Data, Instruction, and Prefetch Cache.
    FLASH->ACR |= (FLASH_ACR_ICEN  | FLASH_ACR_PRFTEN | FLASH_ACR_DCEN);
    FLASH->ACR |= FLASH_ACR_LATENCY_3WS;

    // Turn on the PLL and wait till PLL is ready.
    RCC->CR |=RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY)==0);

    // Switch the system clock to PLL and wait until it is ready.
    RCC->CFGR |= RCC_CFGR_SW_PLL;

    // Update SystemCoreClock variable
    SystemCoreClockUpdate(); 
}

void SysTick_Handler(void) { // SysTick interrupt handler
    ticks++; // Increment the tick count.
}

void Init_SysTick() {
    // Disable SysTick.
    SysTick->CTRL = 0;
    // Set SysTick period to 1msec.
    SysTick->LOAD = (SystemCoreClock/1000UL) - 1UL;
    // Set interrupt priority of SysTick to least urgency.
    NVIC_SetPriority( SysTick_IRQn, (1UL<<__NVIC_PRIO_BITS)-1UL ); 
    // Reset the SysTick counter value.
    SysTick->VAL = 0UL;
    // Select CPU clock, enable SysTick interrupt and SysTick.
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk   |
                    SysTick_CTRL_ENABLE_Msk;
}

void Init_GPIO() {
    // Enable the clock to GPIOC.
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;
    // Set PC13 pin to be a digital output.
    GPIOC->MODER |= GPIOMODER;
}

void Init_USART( uint32_t baudrate ) {
    // Set pins PA2 (TX) and PA3 (RX) for serial communication.
    // Enable RCC for port A.
    RCC->AHB1ENR  |= RCC_AHB1ENR_GPIOAEN; 

    // Use PA2 in alternate function mode (Serial TX).
    GPIOA->MODER  |= GPIO_MODER_MODER2_1; 
    GPIOA->AFR[0] |= (7 << GPIO_AFRL_AFSEL2_Pos); 

    // Use PA3 in alternate function mode (Serial RX).
    GPIOA->MODER  |= GPIO_MODER_MODER3_1; 
    GPIOA->AFR[0] |= (7 << GPIO_AFRL_AFSEL3_Pos); 

    // Enable RCC for USART2.
    RCC->APB1ENR  |= RCC_APB1ENR_USART2EN; 

    // Set the baud rate.
    // Note: The prescaler for the APB1 is set to 2.
    // The APB1 clock is SYSCLK/2.
    uint16_t uart_div = (SystemCoreClock/2/baudrate);
    USART->BRR = ( ((uart_div / 16) << USART_BRR_DIV_Mantissa_Pos) |
                   ((uart_div % 16) << USART_BRR_DIV_Fraction_Pos) );

    // Enable the USART peripheral (Enable both Rx and Tx).
    USART->CR1 |= USART_CR1_RE | USART_CR1_TE | USART_CR1_UE;
}

// Redirect/Retarget printf() output.
void __io_putchar(uint8_t c) {
    // Wait until we are able to transmit
    while( !(USART->SR & USART_SR_TXE) ) {}; 
    USART->DR = c; // Transmit the character
}

int _write(int file, char *ptr, int len) {
    for (int idx=0; idx < len; idx++) {
        __io_putchar(*ptr++);
    }
    return len;
}

int main(void) {
    //SystemInit();
    //SystemCoreClockUpdate();

    Init_SysClkConfig();  // Initialize System Clock Configuration.
    Init_SysTick(); // Initialize SysTick (24-bit SysTick Timer).
    Init_GPIO();    // Initialize GPIO (for LED pin).
    Init_USART( 115200 ); // Initialize USART (with baudrate=115200).
    // Disable I/O buffering, use unbuffered stream for output. 
    setbuf( stdout, NULL ); 

    printf( "CPU freq.: %lu MHz\r\n", (uint32_t)(SystemCoreClock/1e6) );
    for (;;) { // Endless loop
        if ( ticks - saved_ticks >= 500 ) { // every 500 msec
            saved_ticks = ticks;     // Save the tick value.
            GPIOC->ODR ^= (1<<LED1); // Toggle LED pin.
            printf( "LED: %lu\r\n", (GPIOC->ODR >> LED1)&1 );
        }
    }
    return 0;
}

รูป: ตัวอย่างข้อความเอาต์พุต (แสดงความถี่ 96MHz)

รูป: ตัวอย่างการทำขั้นตอน PIO Run & Debug และแสดงค่าของตัวแปร เช่น SystemCoreClock

 


การอ่านค่าอินพุตจากปุ่มกด KEY บนบอร์ด#

บอร์ด BlackPill มีปุ่มกด (Push Button) ที่ทำงานแบบ Active-Low และตรงกับขา PA0 ของชิป STM32F411CE

ตัวอย่างต่อไปนี้ สาธิตการอ่านค่าสถานะลอจิกจากปุ่ม เพื่อนำมากำหนดหรือสร้างเงื่อนไขในการสลับสถานะลอจิกของ LED เช่น LED กระพริบด้วยอัตราคงที่ ถ้าไม่กดปุ่มค้างไว้

#include <stdint.h>
#include <stdio.h>
#include "stm32f4xx.h"

#define LED1       (13)
#define KEY        (0)
#define USART      (USART2)

#define PLL_M      (25)
#define PLL_N      (192)
#define PLL_P      (2)
#define PLL_Q      (4)

volatile uint32_t ticks = 0;
uint32_t saved_ticks = 0;

void Init_SysClkConfig() {  // Use 25MHz external crystal (HSE)
    // Clear the PLLCFGR register
    RCC->PLLCFGR = 0x00000000;
    // Enable HSE in the RCC->CR register and wait until it’s ready to use.
    RCC->CR |=RCC_CR_HSEON;
    while ((RCC->CR & RCC_CR_HSERDY)==0);
    // Enable the APB1 bus
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
    // Adjust the voltage scaling options to suit thr operating frequency.
    PWR->CR |= PWR_CR_VOS;

    // PLL Source HSE = 25MHz 
    RCC->PLLCFGR |= RCC_PLLCFGR_PLLSRC_HSE;

    //  PLL clock = HSE clock / PLL_M * PLL_N / PLL_P
    //            = 25 / 25 * 192 / 2 = 96 MHz
    //  USB clock = HSE clock / PLL_M * PLL_N / PLL_Q
    //            = 25 / 25 * 192 / 2 / 2 = 48 MHz !!!
    //  AHB clock = PLL clock / HPRE  = 96MHz/1 = 96MHz
    // APB1 clock = PLL clock / PPRE1 = 96MHz/2 = 48MHz
    // APB2 clock = PLL clock / PPRE2 = 96MHz/1 = 96MHz

    // Set clock prescalers for AHB, APB1 and APB2 busses
    RCC->CFGR |= RCC_CFGR_HPRE_DIV1;
    RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;
    RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;

    RCC->PLLCFGR =  (RCC_PLLCFGR_PLLSRC_HSE)
                 | (PLL_M<<0) 
                 | (PLL_N<<6) 
                 | (((PLL_P >> 1)-1) << 16)
                 | (PLL_Q<<24);
    // Enable main PLL and wait until PLL is ready.
    RCC->CR |= RCC_CR_PLLON;  
    while((RCC->CR & RCC_CR_PLLRDY)==0);

    // Set the Flash latency (3 wait cycles) and 
    // enable the Data, Instruction, and Prefetch Cache.
    FLASH->ACR |= (FLASH_ACR_ICEN  | FLASH_ACR_PRFTEN | FLASH_ACR_DCEN);
    FLASH->ACR |= FLASH_ACR_LATENCY_3WS;

    // Turn on the PLL and wait till PLL is ready.
    RCC->CR |=RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY)==0);

    // Switch the system clock to PLL and wait until it is ready.
    RCC->CFGR |= RCC_CFGR_SW_PLL;

    // Update SystemCoreClock variable
    SystemCoreClockUpdate(); 
}

void SysTick_Handler(void) { // SysTick interrupt handler
    ticks++; // Increment the tick count.
}

void Init_SysTick() {
    // Disable SysTick.
    SysTick->CTRL = 0;
    // Set SysTick period to 1msec.
    SysTick->LOAD = (SystemCoreClock/1000UL) - 1UL;
    // Set interrupt priority of SysTick to least urgency.
    NVIC_SetPriority( SysTick_IRQn, (1UL<<__NVIC_PRIO_BITS)-1UL ); 
    // Reset the SysTick counter value.
    SysTick->VAL = 0UL;
    // Select CPU clock, enable SysTick interrupt and SysTick.
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk   |
                    SysTick_CTRL_ENABLE_Msk;
}

void Init_GPIO() {
    // Enable the clock to GPIOC.
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;
    // Enable the clock to GPIOA.
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    // Set PC13 pin to be a digital output.
    GPIOC->MODER |= GPIO_MODER_MODER13_0;
    // Set PA0 pin as input with internal pullup.
    GPIOA->PUPDR |= GPIO_PUPDR_PUPDR0_0;
}

void Init_USART( uint32_t baudrate ) {
    // Set pins PA2 (TX) and PA3 (RX) for serial communication.
    // Enable RCC for port A.
    RCC->AHB1ENR  |= RCC_AHB1ENR_GPIOAEN; 

    // Use PA2 in alternate function mode (Serial TX).
    GPIOA->MODER  |= GPIO_MODER_MODER2_1; 
    GPIOA->AFR[0] |= (7 << GPIO_AFRL_AFSEL2_Pos); 

    // Use PA3 in alternate function mode (Serial RX).
    GPIOA->MODER  |= GPIO_MODER_MODER3_1; 
    GPIOA->AFR[0] |= (7 << GPIO_AFRL_AFSEL3_Pos); 

    // Enable RCC for USART2.
    RCC->APB1ENR  |= RCC_APB1ENR_USART2EN; 

    // Set the baud rate.
    // Note: The prescaler for the APB1 is set to 2.
    // The APB1 clock is SYSCLK/2.
    uint16_t uart_div = (SystemCoreClock/2/baudrate);
    USART->BRR = ( ((uart_div / 16) << USART_BRR_DIV_Mantissa_Pos) |
                   ((uart_div % 16) << USART_BRR_DIV_Fraction_Pos) );

    // Enable the USART peripheral (Enable both Rx and Tx).
    USART->CR1 |= USART_CR1_RE | USART_CR1_TE | USART_CR1_UE;
}

// Redirect/Retarget printf() output.
void __io_putchar(uint8_t c) {
    // Wait until we are able to transmit
    while( !(USART->SR & USART_SR_TXE) ) {}; 
    USART->DR = c; // Transmit the character
}

int _write(int file, char *ptr, int len) {
    for (int idx=0; idx < len; idx++) {
        __io_putchar(*ptr++);
    }
    return len;
}

int main(void) {
    //SystemInit();
    //SystemCoreClockUpdate();

    Init_SysClkConfig();  // Initialize System Clock Configuration.
    Init_SysTick(); // Initialize SysTick (24-bit SysTick Timer).
    Init_GPIO();    // Initialize GPIO (for LED pin and PA0).
    Init_USART( 115200 ); // Initialize USART (with baudrate=115200).
    // Disable I/O buffering, use unbuffered stream for output. 
    setbuf( stdout, NULL ); 

    printf( "CPU freq.: %lu MHz\r\n", (uint32_t)(SystemCoreClock/1e6) );
    for (;;) { // Endless loop
        if ( ticks - saved_ticks >= 100 ) { // every 100 msec
            saved_ticks = ticks;     // Save the tick value.
            if ( (GPIOA->IDR & (1<<KEY)) ) { // KEY button not pressed
               GPIOC->ODR ^= (1<<LED1); // Toggle LED pin.
               printf( "LED: %lu\r\n", (GPIOC->ODR >> LED1)&1 );
            }
        }
    }
    return 0;
}

 


กล่าวสรุป#

บทความนี้ได้นำเสนอการใช้งานซอฟต์แวร์ VS Code IDE ร่วมกับ PlatformIO เพื่อนำมาใช้ในการเขียนโค้ดสำหรับ STM32F411CE และเลือกใช้รูปแบบการเขียนโค้ดแบบ Arm CMSIS พร้อมตัวอย่างโค้ดเพื่อนำไปทดลองใช้งานกับบอร์ดไมโครคอนโทรลเลอร์ในเบื้องต้น

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2023-06-18 | Last Updated: 2023-06-18