2、STM32平台基础与裸机开发:时钟树、GPIO、中断系统、定时器、DMA
好,咱们正式开始啃STM32这块硬骨头。说实话,很多新手一上来就怼HAL库、怼RTOS,结果出了问题连基本的外设都调不明白。我个人习惯是,先把裸机玩透了,再去碰那些封装好的东西。这一章,咱们就把STM32的五个基本功——时钟树、GPIO、中断、定时器、DMA,一个一个捋清楚。
2.1 时钟树——芯片的心脏起搏器
时钟,说白了就是芯片的“心跳”。没有时钟,CPU、外设全都歇菜。STM32的时钟树比51单片机复杂得多,我第一次看参考手册的时钟树图时,说实话有点懵。但后来发现,只要抓住几个关键节点,其实没那么玄乎。
2.1.1 时钟源有哪些?
STM32有四个主要的时钟源:
- HSI(高速内部时钟):8MHz RC振荡器,上电默认就用它。精度一般,温漂大,但胜在启动快。
- HSE(高速外部时钟):通常接4-16MHz晶振,精度高,适合对时序敏感的场景。
- LSI(低速内部时钟):40kHz左右,专门给独立看门狗和RTC用。
- LSE(低速外部时钟):32.768kHz,RTC的标配,走时准。
2.1.2 PLL锁相环——超频的秘密
STM32内部有个PLL,可以把时钟倍频上去。比如F103,HSE是8MHz,通过PLL可以倍频到72MHz。配置PLL时要注意:
- PLL的输入频率必须在2-16MHz之间(不同系列略有差异)
- 倍频系数不能乱设,要保证输出不超过芯片最大频率
- APB1和APB2总线时钟要分别配置,APB1最高36MHz,APB2最高72MHz
// 以STM32F103为例,配置系统时钟为72MHz
void SystemClock_Config(void)
{
// 1. 开启HSE并等待就绪
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY));
// 2. 配置PLL:HSE 8MHz * 9 = 72MHz
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
// 3. 切换系统时钟到PLL输出
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
// 4. 配置APB1和APB2分频
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = 36MHz
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // APB2 = 72MHz
}
2.2 GPIO——最基础也最容易出错的外设
GPIO,每个嵌入式工程师的入门课。但你真的会用吗?STM32的GPIO有8种工作模式,不是简单的输入输出就完事了。
2.2.1 八种模式速览
| 模式 | 用途 | 典型场景 |
|---|---|---|
| 浮空输入 | 电平不确定时用 | 外部中断输入 |
| 上拉输入 | 默认高电平 | 按键检测(按下为低) |
| 下拉输入 | 默认低电平 | 按键检测(按下为高) |
| 模拟输入 | ADC专用 | 采集模拟信号 |
| 推挽输出 | 最常用输出模式 | LED、蜂鸣器 |
| 开漏输出 | 需要外部上拉 | I2C总线 |
| 复用推挽 | 外设功能输出 | USART_TX、SPI_SCK |
| 复用开漏 | 外设功能开漏 | I2C_SCL、I2C_SDA |
2.2.2 配置GPIO的步骤
配置一个GPIO,其实就三步:开时钟、设模式、写数据。但顺序不能乱。
// 配置PA5为推挽输出,驱动LED
void GPIO_Config(void)
{
// 第一步:开启GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// 第二步:配置模式寄存器
// PA5对应CRL的位20-23,设置为通用推挽输出,50MHz
GPIOA->CRL &= ~(0xF << 20); // 先清零
GPIOA->CRL |= (0x2 << 20); // 0x2 = 推挽输出,50MHz
// 第三步:输出高电平
GPIOA->BSRR = GPIO_BSRR_BS5; // 置位
}
2.3 中断系统——让CPU学会“分心”
轮询太浪费CPU了。你想想看,如果CPU一直在那死等一个按键按下,那其他事还干不干了?中断就是让CPU能“分心”处理多任务的关键。
2.3.1 NVIC——中断的调度中心
STM32用NVIC(嵌套向量中断控制器)来管理中断。每个中断都有优先级,高优先级可以打断低优先级。优先级分组是个容易搞混的地方:
- 抢占优先级:数字越小,优先级越高,可以打断正在执行的低优先级中断
- 响应优先级:数字越小,优先级越高,但不能打断,只能排队
// 配置外部中断EXTI0,优先级分组为2(4位抢占,4位响应)
void NVIC_Config(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
2.3.2 外部中断EXTI
EXTI可以检测GPIO的上升沿、下降沿或双边沿。配置时要注意:
- 每个EXTI线只能对应一个GPIO引脚
- EXTI0-4有独立的中断号,EXTI5-9共用,EXTI10-15共用
- 中断标志位要手动清除,否则会一直触发
// 配置PA0为下降沿触发的外部中断
void EXTI_Config(void)
{
// 1. 开启GPIOA和AFIO时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;
// 2. 配置PA0为浮空输入
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= (0x4 << 0); // 0x4 = 浮空输入
// 3. 配置EXTI0连接到PA0
AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0;
AFIO->EXTICR[0] |= AFIO_EXTICR1_EXTI0_PA;
// 4. 配置EXTI0为下降沿触发
EXTI->IMR |= EXTI_IMR_MR0; // 取消屏蔽
EXTI->FTSR |= EXTI_FTSR_TR0; // 下降沿触发
// 5. 配置NVIC(略)
}
2.4 定时器——精确的时间控制
定时器是飞控里最常用的外设之一。PWM输出、输入捕获、编码器接口,全都靠它。STM32的定时器分三类:基本定时器、通用定时器、高级定时器。
2.4.1 定时器的工作原理
说白了,定时器就是一个不断累加的计数器。当计数值达到预设的自动重装载值时,就会产生溢出事件。配置定时器就是配置三个参数:
- 预分频器(PSC):对时钟源进行分频
- 自动重装载值(ARR):计数的上限
- 计数模式:向上、向下、中央对齐
// 配置TIM2为1ms中断一次(72MHz时钟)
void TIM2_Config(void)
{
// 1. 开启TIM2时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// 2. 配置预分频器:72MHz / 72 = 1MHz
TIM2->PSC = 71; // 注意:实际分频系数是PSC+1
// 3. 配置自动重装载值:1MHz / 1000 = 1kHz,即1ms
TIM2->ARR = 999; // 注意:实际计数值是ARR+1
// 4. 清除更新标志,使能更新中断
TIM2->SR = ~TIM_SR_UIF;
TIM2->DIER |= TIM_DIER_UIE;
// 5. 使能定时器
TIM2->CR1 |= TIM_CR1_CEN;
}
2.4.2 PWM输出——飞控的肌肉
飞控控制电机靠的就是PWM。STM32的高级定时器可以输出带死区插入的互补PWM,非常适合驱动无刷电机。
// 配置TIM1_CH1输出PWM,频率50Hz,占空比可调
void PWM_Config(void)
{
// 1. 开启TIM1和GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_TIM1EN | RCC_APB2ENR_IOPAEN;
// 2. 配置PA8为复用推挽输出
GPIOA->CRH &= ~(0xF << 0);
GPIOA->CRH |= (0xB << 0); // 0xB = 复用推挽输出,50MHz
// 3. 配置TIM1
TIM1->PSC = 71; // 72MHz / 72 = 1MHz
TIM1->ARR = 19999; // 1MHz / 20000 = 50Hz
TIM1->CCR1 = 1500; // 占空比 = 1500/20000 = 7.5%
// 4. 配置PWM模式1
TIM1->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; // PWM模式1
TIM1->CCER |= TIM_CCER_CC1E; // 使能输出
// 5. 使能TIM1
TIM1->BDTR |= TIM_BDTR_MOE; // 主输出使能(高级定时器特有)
TIM1->CR1 |= TIM_CR1_CEN;
}
2.5 DMA——解放CPU的搬运工
DMA,全称直接存储器访问。说白了,就是让数据在内存和外设之间自动搬运,CPU只需要告诉DMA“从哪搬、搬到哪、搬多少”,然后就可以去干别的事了。
2.5.1 DMA的传输模式
| 模式 | 说明 | 典型应用 |
|---|---|---|
| 外设到内存 | 外设数据自动存入内存 | ADC采集、USART接收 |
| 内存到外设 | 内存数据自动发送到外设 | DAC输出、USART发送 |
| 内存到内存 | 内存数据拷贝 | 图像处理、数据搬移 |
2.5.2 实战:DMA+USART实现无中断发送
飞控中经常需要发送大量数据到地面站。如果用CPU逐字节发送,会占用大量时间。用DMA就轻松多了。
// 配置USART1的DMA发送
void DMA_USART_Config(void)
{
// 1. 开启DMA1时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 2. 配置DMA通道4(USART1_TX)
DMA1_Channel4->CPAR = (uint32_t)&USART1->DR; // 外设地址
DMA1_Channel4->CMAR = (uint32_t)tx_buffer; // 内存地址
DMA1_Channel4->CNDTR = sizeof(tx_buffer); // 传输数据量
// 3. 配置控制寄存器
DMA1_Channel4->CCR |= DMA_CCR_MINC; // 内存地址递增
DMA1_Channel4->CCR |= DMA_CCR_DIR; // 内存到外设
DMA1_Channel4->CCR |= DMA_CCR_CIRC; // 循环模式(可选)
DMA1_Channel4->CCR |= DMA_CCR_EN; // 使能DMA通道
// 4. 使能USART1的DMA发送
USART1->CR3 |= USART_CR3_DMAT;
}
2.6 本章小结
嗯,这一章内容不少。时钟树、GPIO、中断、定时器、DMA,这五个是STM32裸机开发的核心。我个人觉得,把这五个玩透了,后面学什么外设都很快。你想想看,串口、I2C、SPI,哪个不是基于GPIO和中断?PWM、输入捕获,哪个不是基于定时器?
下一章,咱们开始讲串口通信和I2C/SPI总线协议。到时候我会结合飞控的实际应用,讲讲怎么用这些协议和传感器通信。咱们下章见。
- 用定时器实现一个1ms的软件定时器,可以同时管理5个不同的超时事件
- 用DMA+USART实现一个环形缓冲区,接收不定长数据
- 配置一个外部中断,检测飞控的紧急降落开关(下降沿触发)