1. STM32裸机开发快速入门指南
作为一名嵌入式开发工程师,我经常需要快速验证硬件功能或搭建原型系统。STM32裸机开发是最基础也是最高效的方式之一。本文将基于实际代码,带你快速掌握STM32裸机开发的核心要点。
1.1 为什么选择裸机开发?
裸机开发(Bare-metal Programming)是指不依赖任何操作系统,直接在硬件上编写程序的方式。相比RTOS或Linux等系统,裸机开发具有以下优势:
- 资源占用极小:不需要额外的内存和CPU开销
- 响应速度快:没有任务调度开销,中断响应更快
- 开发简单:不需要复杂的系统配置
- 适合小型项目:对于简单控制任务非常高效
在资源受限的单片机项目中,裸机开发仍然是主流选择。下面我将从最基础的延时函数开始,逐步介绍STM32裸机开发的关键技术。
2. 基础功能实现
2.1 精确延时函数实现
延时是嵌入式开发中最基础的功能之一。STM32提供了SysTick定时器,可以用来实现精确的延时功能。
c复制void Delay_us(uint32_t xus) {
SysTick->LOAD = 72 * xus; // 设置定时器重装值
SysTick->VAL = 0x00; // 清空当前计数值
SysTick->CTRL = 0x00000005; // 设置时钟源为HCLK,启动定时器
while(!(SysTick->CTRL & 0x00010000)); // 等待计数到0
SysTick->CTRL = 0x00000004; // 关闭定时器
}
2.1.1 工作原理详解
这个延时函数使用了STM32内核自带的SysTick定时器,它是一个24位递减计数器。工作原理如下:
- 设置LOAD寄存器为72*xus:72MHz时钟下,1us需要72个时钟周期
- 清空VAL寄存器:确保从LOAD值开始计数
- 配置CTRL寄存器:选择时钟源并启动定时器
- 轮询等待COUNTFLAG标志位:当计数器减到0时,该标志位会被置1
- 关闭定时器:防止影响后续使用
2.1.2 注意事项
- 时钟频率依赖:此代码假设系统时钟为72MHz,如果时钟频率不同,需要调整72这个系数
- 最大延时限制:24位计数器最大值为16,777,215,72MHz下最大延时约233ms
- 阻塞式延时:CPU会一直等待延时结束,不适合实时性要求高的场景
2.2 GPIO控制LED闪烁
LED控制是嵌入式开发的"Hello World"。下面是一个简单的LED闪烁程序:
c复制#include "stm32f10x.h"
#include "Delay.h"
int main(void) {
// 开启GPIOC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
// GPIO初始化
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_StructInit(&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; // PC13
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
while(1) {
GPIO_ResetBits(GPIOC, GPIO_Pin_13); // LED亮
Delay_ms(500);
GPIO_SetBits(GPIOC, GPIO_Pin_13); // LED灭
Delay_ms(500);
}
}
2.2.1 GPIO模式选择
STM32的GPIO有多种输出模式,常用的有:
- 推挽输出(GPIO_Mode_Out_PP):可以输出高/低电平,驱动能力强
- 开漏输出(GPIO_Mode_Out_OD):只能拉低,需要外接上拉电阻
- 复用推挽输出(GPIO_Mode_AF_PP):用于外设功能如USART、SPI等
- 复用开漏输出(GPIO_Mode_AF_OD):用于I2C等总线通信
对于LED控制,推挽输出是最合适的选择。
2.2.2 开发板LED连接特点
在常见的STM32开发板上:
- LED通常连接在PC13引脚
- 采用低电平点亮的方式(LED阳极接VCC,阴极接PC13)
- PC13驱动能力较弱,不适合直接驱动大电流负载
3. 外设驱动开发
3.1 按键输入检测
按键检测是交互式设备的基础功能。下面是一个简单的按键检测实现:
c复制void Key_Init(void) {
// 开启GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置PB1和PB11为上拉输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
uint8_t Key_GetNum(void) {
uint8_t KeyNum = 0;
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) {
Delay_ms(20); // 消抖
while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0); // 等待释放
Delay_ms(20); // 消抖
KeyNum = 1;
}
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0) {
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0);
Delay_ms(20);
KeyNum = 2;
}
return KeyNum;
}
3.1.1 按键硬件连接
通常按键采用如下连接方式:
code复制VDD
|
[R] (上拉电阻)
|
PBx --- 按键 --- GND
- 未按下时:PBx通过上拉电阻保持高电平
- 按下时:PBx被拉低到GND
3.1.2 软件消抖原理
机械按键在按下和释放时会产生抖动,通常持续5-20ms。软件消抖通过延时跳过这段不稳定期:
- 首次检测到低电平后延时20ms
- 再次检测确认按键状态
- 等待按键释放
- 释放后再延时20ms
3.1.3 改进空间
这个实现有几个可以改进的地方:
- 阻塞式检测:会卡住主循环
- 不支持同时按键检测:后按的会覆盖先按的
- 消抖时间固定:可能不适合所有按键类型
3.2 外部中断应用
外部中断可以实现快速响应外部事件,如传感器信号等:
c复制volatile uint16_t CountSensor_Count = 0;
void CountSensor_Init(void) {
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 配置PB14为上拉输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 外部中断线映射
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
// 配置EXTI
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}
void EXTI15_10_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line14) == SET) {
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0) {
CountSensor_Count++;
}
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
3.2.1 外部中断配置要点
- 开启AFIO时钟:必须开启才能配置外部中断
- GPIO配置:设置为输入模式
- 中断线映射:将GPIO引脚映射到对应的EXTI线
- EXTI配置:选择触发边沿和模式
- NVIC配置:使能中断并设置优先级
3.2.2 volatile关键字的重要性
在中断服务程序(ISR)和主程序之间共享的变量必须声明为volatile:
c复制volatile uint16_t CountSensor_Count = 0;
这告诉编译器不要优化对此变量的访问,确保每次都能读取最新值。
3.3 定时器中断应用
定时器中断是周期性任务的理想选择,如定时采样、状态检测等:
c复制void Timer_Init(void) {
// 开启TIM2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 时基单元配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; // 自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; // 预分频值
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
// 中断配置
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
NVIC_EnableIRQ(TIM2_IRQn);
// 启动定时器
TIM_Cmd(TIM2, ENABLE);
}
void TIM2_IRQHandler(void) {
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) {
// 中断处理代码
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
3.3.1 定时器中断周期计算
定时器中断周期由以下公式决定:
code复制中断周期 = (Prescaler + 1) × (Period + 1) / 定时器时钟频率
对于上述配置:
- 定时器时钟:72MHz
- Prescaler:7200-1 → 分频后时钟:10kHz
- Period:10000-1 → 中断周期:1秒
3.3.2 定时器使用注意事项
- 清除中断标志:必须在ISR中清除中断标志,否则会不断触发中断
- 中断优先级:根据任务重要性合理设置NVIC优先级
- 中断处理时间:ISR应尽可能简短,避免影响其他中断响应
4. 高级外设应用
4.1 PWM输出配置
PWM广泛应用于电机控制、LED调光等场景:
c复制void PWM_Init(void) {
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 配置PA0为复用推挽输出
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 时基单元配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; // ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; // PSC
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
// PWM模式配置
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 启动定时器
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompare1(uint16_t Compare) {
TIM_SetCompare1(TIM2, Compare);
}
4.1.1 PWM参数计算
PWM频率和占空比计算公式:
code复制PWM频率 = 定时器时钟 / ((Prescaler + 1) × (Period + 1))
占空比 = Pulse / (Period + 1)
对于上述配置:
- PWM频率:72MHz / (720 × 100) = 1kHz
- 占空比范围:0-100%
4.1.2 PWM模式选择
STM32支持两种PWM模式:
- PWM模式1:
- CNT < CCR时输出有效电平
- CNT ≥ CCR时输出无效电平
- PWM模式2:
- 与模式1相反
通常使用PWM模式1,配合极性设置可以灵活控制输出波形。
4.2 输入捕获功能
输入捕获可用于测量脉冲宽度、频率等:
c复制void IC_Init(void) {
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 配置PA6为上拉输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 时基单元配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; // 最大计数值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; // 1MHz计数频率
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
// 输入捕获配置
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0xF;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
// 触发源和从模式配置
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
// 启动定时器
TIM_Cmd(TIM3, ENABLE);
}
uint32_t IC_GetFreq(void) {
return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
4.2.1 输入捕获工作原理
- 配置定时器时基:决定时间测量精度
- 配置输入捕获通道:选择触发边沿和滤波参数
- 配置从模式为Reset:每次捕获后复位计数器
- 计算频率:频率 = 1 / 周期
4.2.2 测量范围和精度
- 最大测量频率:取决于定时器时钟和捕获处理时间
- 最小测量频率:取决于计数器位数和预分频设置
- 测量精度:由定时器时钟决定,72MHz下理论精度可达13.8ns
5. 开发技巧与常见问题
5.1 调试技巧
- 使用GPIO翻转调试:在关键位置翻转GPIO,用示波器观察执行时间
c复制GPIO_SetBits(GPIOA, GPIO_Pin_0); // 要测试的代码 GPIO_ResetBits(GPIOA, GPIO_Pin_0); - 利用断点和Watch窗口:在IDE中设置断点观察变量
- 使用串口打印调试信息:简单但有效的调试方法
5.2 常见问题解决
-
外设不工作:
- 检查时钟是否开启
- 确认GPIO模式配置正确
- 验证外设是否已使能
-
中断不触发:
- 检查NVIC配置
- 确认中断标志是否清除
- 验证中断优先级设置
-
程序跑飞:
- 检查堆栈大小是否足够
- 确认没有数组越界等问题
- 验证中断处理时间是否过长
5.3 性能优化建议
-
减少中断处理时间:
- 只做必要的操作
- 将耗时操作放到主循环
-
合理使用DMA:
- 用于数据传输密集型任务
- 减少CPU开销
-
优化时钟配置:
- 根据需求选择适当时钟频率
- 不使用的外设时钟可以关闭
6. 项目实战建议
在实际项目中,建议采用模块化编程:
- 为每个外设创建单独的.h/.c文件
- 定义清晰的接口函数
- 使用宏定义配置硬件参数
- 编写详细的注释说明
例如,LED模块可以这样组织:
c复制// led.h
#ifndef __LED_H
#define __LED_H
#include "stm32f10x.h"
void LED_Init(void);
void LED_On(void);
void LED_Off(void);
void LED_Toggle(void);
#endif
c复制// led.c
#include "led.h"
void LED_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
}
void LED_On(void) {
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
}
void LED_Off(void) {
GPIO_SetBits(GPIOC, GPIO_Pin_13);
}
void LED_Toggle(void) {
GPIOC->ODR ^= GPIO_Pin_13;
}
这种模块化设计可以提高代码可读性、可维护性和可移植性。
