3. STM32裸机工程创建:新建标准库工程、配置时钟、GPIO与串口
好,咱们正式开始动手。这一章的目标很明确——从零搭建一个STM32的裸机工程,把时钟、GPIO和串口都跑通。别小看这一步,我见过不少朋友移植FreeRTOS时出问题,最后发现是底层工程就没搭稳。
说白了,裸机工程就是操作系统的地基。地基歪了,上面盖什么楼都得塌。咱们今天就用STM32F103系列 + 标准库来演示,这套组合我用了好多年,稳定可靠。
3.1 新建标准库工程——我的固定套路
新建工程这事儿,每个人都有自己的习惯。我个人的做法是:不依赖IDE自动生成的模板,手动搭建文件夹结构。为什么?因为这样你清楚每个文件是干什么的,出了问题能快速定位。
3.1.1 文件夹结构
我在项目里通常这样组织目录:
Project/
├── User/ # 用户代码(main.c, stm32f10x_it.c 等)
├── CMSIS/ # 内核相关文件(core_cm3.h, system_stm32f10x.c)
├── FWLIB/ # 标准库外设驱动(src + inc)
├── Startup/ # 启动文件(startup_stm32f10x_hd.s)
└── Output/ # 编译输出(hex, map 等)
嗯,这里要注意:启动文件必须选对。STM32F103C8T6是中等容量,用 startup_stm32f10x_md.s;如果是ZET6这种高容量,就用 startup_stm32f10x_hd.s。选错了,芯片可能直接跑飞。
3.1.2 添加文件到工程
在Keil里新建工程后,按上面的分组添加文件:
- CMSIS组:core_cm3.c, system_stm32f10x.c
- FWLIB组:你需要的库文件,比如gpio.c, usart.c, rcc.c
- User组:main.c, stm32f10x_it.c, stm32f10x_conf.h
3.2 配置系统时钟——别用默认的
STM32上电默认用的是HSI(内部8MHz振荡器),精度一般,而且跑不到最高速度。咱们做项目,第一件事就是把时钟切到HSE + PLL,让主频跑到72MHz。
我曾经在一个项目中,忘了配置时钟,结果串口波特率怎么算都不对,折腾了两天才发现是时钟源没切……
3.2.1 时钟树概览
先看一眼STM32F103的时钟树结构,心里有个谱:
3.2.2 时钟配置代码
标准库提供了 SystemInit() 函数,但它默认用的是HSI。我们需要在 system_stm32f10x.c 里修改宏定义:
/* 在 system_stm32f10x.c 中找到下面这行 */
// #define SYSCLK_FREQ_72MHz 72000000 // 取消注释这一行
/* 或者在 main.c 中手动配置 */
void SystemClock_Config(void)
{
ErrorStatus HSEStartUpStatus;
/* 1. 复位RCC配置 */
RCC_DeInit();
/* 2. 使能HSE */
RCC_HSEConfig(RCC_HSE_ON);
HSEStartUpStatus = RCC_WaitForHSEStartUp();
if (HSEStartUpStatus == SUCCESS)
{
/* 3. 配置FLASH预取指和等待周期 */
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
FLASH_SetLatency(FLASH_Latency_2);
/* 4. 配置AHB、APB1、APB2预分频 */
RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB = SYSCLK
RCC_PCLK2Config(RCC_HCLK_Div1); // APB2 = AHB
RCC_PCLK1Config(RCC_HCLK_Div2); // APB1 = AHB/2
/* 5. 配置PLL:HSE 8MHz * 9 = 72MHz */
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
RCC_PLLCmd(ENABLE);
/* 6. 等待PLL就绪 */
while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
/* 7. 切换系统时钟到PLL */
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
while (RCC_GetSYSCLKSource() != 0x08); // 0x08表示PLL作为系统时钟
}
else
{
/* HSE启动失败,可以在这里加入错误处理 */
while (1); // 我习惯在这里点亮一个错误指示灯
}
}
3.3 配置GPIO——点亮LED,验证工程
时钟配好了,咱们先点亮一个LED,验证工程能不能跑。我习惯用PB0接一个LED(正极接3.3V,负极接PB0,低电平点亮)。
3.3.1 GPIO配置步骤
标准库配置GPIO分三步:
- 使能GPIO时钟:RCC_APB2PeriphClockCmd()
- 定义GPIO初始化结构体:GPIO_InitTypeDef
- 调用初始化函数:GPIO_Init()
void LED_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能GPIOB时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
/* 配置PB0为推挽输出,50MHz */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 默认熄灭LED(高电平) */
GPIO_SetBits(GPIOB, GPIO_Pin_0);
}
然后在main函数里调用:
int main(void)
{
SystemClock_Config(); // 配置时钟
LED_GPIO_Config(); // 配置LED引脚
while (1)
{
GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 点亮
Delay(0xFFFFF);
GPIO_SetBits(GPIOB, GPIO_Pin_0); // 熄灭
Delay(0xFFFFF);
}
}
看到LED闪烁,说明工程搭建成功,时钟和GPIO都正常工作了。
3.4 配置串口——调试的“眼睛”
做嵌入式开发,串口就是你的眼睛。没有串口打印,出了问题你只能靠猜。我个人的习惯是:每个工程都标配一个串口调试模块,哪怕最终产品不用,开发阶段也必须留。
3.4.1 串口配置代码
咱们用USART1,PA9(TX)和PA10(RX),波特率115200:
void USART1_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
/* 使能时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
/* 配置TX(PA9)为复用推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 配置RX(PA10)为浮空输入 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* USART1配置 */
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStructure);
/* 使能USART1 */
USART_Cmd(USART1, ENABLE);
}
3.4.2 实现printf重定向
有了串口,咱们还得把printf函数映射到串口上,这样调试信息打印起来方便。在工程里添加一个 retarget.c 文件:
#include <stdio.h>
int fputc(int ch, FILE *f)
{
/* 等待发送缓冲区空 */
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
3.5 验证——串口打印测试
所有配置完成后,在main函数里加入打印:
int main(void)
{
SystemClock_Config();
LED_GPIO_Config();
USART1_Config();
printf("STM32 Bare-metal Project Started!\r\n");
printf("System Clock: %d MHz\r\n", SystemCoreClock / 1000000);
while (1)
{
printf("LED Toggle\r\n");
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
Delay(0xFFFFF);
GPIO_SetBits(GPIOB, GPIO_Pin_0);
Delay(0xFFFFF);
}
}
打开串口助手,波特率115200,你应该能看到:
STM32 Bare-metal Project Started!
System Clock: 72 MHz
LED Toggle
LED Toggle
LED Toggle
...
看到这个输出,恭喜你——STM32裸机工程搭建完成!时钟、GPIO、串口全部就位。这个工程就是咱们后续移植FreeRTOS的基础,一定要确保它稳定可靠。
本章核心要点:
- 手动搭建工程结构,清楚每个文件的作用
- 系统时钟必须切换到HSE+PLL,主频72MHz
- GPIO配置三步走:时钟使能 → 结构体定义 → 初始化
- 串口是调试利器,每个工程都建议标配
- printf重定向用MicroLIB,省Flash空间