4. ADC数据读取与校准
时钟,是单片机的心脏。没有时钟,芯片就是一块死硅片。我刚开始学单片机那会儿,总觉得时钟配置是件麻烦事,能用默认的就行。直到有一次,我调一个ADC采样,数据老是跳,后来才发现是时钟没配好,采样时序全乱了。从那以后,我养成了一个习惯——上电第一件事,先把时钟理清楚。
这一章,我们就来聊聊STM32的时钟系统。说白了,就是搞清楚你的芯片到底跑多快,以及怎么精确地“等”一段时间。
4.1 时钟源的选择:HSE vs HSI
STM32的时钟源有好几个,但最常用的就是两个:HSE(外部高速晶振)和HSI(内部高速RC振荡器)。
| 特性 | HSE(外部晶振) | HSI(内部RC) |
|---|---|---|
| 精度 | 高(通常±10~30ppm) | 低(约±1%~2%) |
| 频率 | 常用4~16MHz | 固定8MHz(STM32F1系列) |
| 启动时间 | 较慢(ms级) | 极快(μs级) |
| 成本 | 需要外部晶振+电容 | 无需外部元件 |
| 温漂 | 小 | 大 |
我个人习惯:只要对时序有要求,比如串口通信、ADC采样,我一定用HSE。HSI虽然省事,但精度不够,波特率容易跑偏。我见过一个项目,用HSI跑115200波特率,结果丢包率高达10%,换成HSE后问题立刻消失。
不过,HSI也有它的用武之地。比如低功耗模式下,或者系统刚上电需要快速启动时,先用HSI顶着,等HSE稳定了再切过去。这种“双保险”的做法,我在电池供电的产品里用过很多次。
4.2 SysTick定时器原理
SysTick,全称System Tick Timer,是ARM Cortex-M内核自带的一个24位递减计数器。它不依赖外设,所以哪怕你换了个型号的STM32,SysTick的用法基本不变。
它的工作原理很简单:
- 从你设定的重装载值开始,每个时钟周期减1。
- 减到0时,触发一次中断,同时自动重新装载。
- 如此循环,就像一个节拍器。
你想想看,如果我们把SysTick的中断频率设为1kHz,那每次中断就是1ms。HAL库里的HAL_Delay(),就是靠这个实现的。
核心公式:
SysTick中断频率 = SysTick时钟频率 / (重装载值 + 1)
例如:时钟72MHz,重装载值设为71999,则中断频率 = 72MHz / 72000 = 1kHz,即1ms中断一次。
这里有个坑,我踩过。SysTick的时钟源可以选处理器时钟(AHB),也可以选AHB/8。如果你改了系统时钟,忘了调整重装载值,那延时就不准了。我曾经因为这个,让一个电机多转了半圈,差点把机械结构打坏。
4.3 HAL_Delay的实现与局限
HAL库的HAL_Delay(),本质上就是一个基于SysTick的忙等待延时。它的代码大致是这样的:
__weak void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick();
while ((HAL_GetTick() - tickstart) < Delay)
{
// 空转等待
}
}
HAL_GetTick()返回的是从系统启动到现在经过的毫秒数,这个值由SysTick中断每1ms加1。
局限很明显:
- 精度只有1ms,微秒级延时做不到。
- 是阻塞的,CPU一直在那空转。
- 如果在中断里调用,SysTick中断优先级不够高时,会导致延时不准。
我记得有一次调试一个I2C传感器,时序要求微秒级。用HAL_Delay(1),结果实际等了1.2ms,传感器直接不响应。后来我不得不自己写微秒延时。
4.4 微秒级延时实现
要实现微秒级延时,有几种方法。我常用的是定时器微秒延时和指令周期延时。
方法一:使用通用定时器
这种方法精度高,不占用CPU(可以用中断),但需要占用一个定时器资源。
// 假设TIM2时钟为72MHz,预分频器设为71,则计数频率为1MHz
// 即每计数一次为1μs
void delay_us(uint32_t us)
{
__HAL_TIM_SET_COUNTER(&htim2, 0);
while (__HAL_TIM_GET_COUNTER(&htim2) < us);
}
方法二:指令周期延时(简单粗暴)
这种方法不占用外设,但精度受编译器优化影响,移植性差。
void delay_us(uint32_t us)
{
uint32_t count = us * (SystemCoreClock / 1000000) / 4;
while (count--)
{
__NOP(); // 空操作指令
}
}
注意:指令周期延时强烈依赖编译器优化等级。在-O0下可能准确,但-O2下可能被优化掉。我建议只在调试阶段用,正式产品还是用定时器方案。
4.5 时钟树与配置流程
STM32的时钟树,看起来复杂,其实就三步:
- 选源:决定用HSE还是HSI。
- 走路径:经过PLL倍频,得到系统时钟SYSCLK。
- 分频:给AHB、APB1、APB2各总线分配时钟。
下面这张图,是我画的一个简化版时钟配置流程,你看一眼就能明白:
实际配置时,我一般用STM32CubeMX图形化工具。选好HSE频率,填上目标系统时钟,它会自动算好PLL参数。但你要理解背后的逻辑,不然出了问题不知道怎么调。
我的经验:配置时钟时,先确认HSE是否起振。可以在HAL_RCC_ClockConfig()之后,检查__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY)。如果HSE没起振,系统会自动切回HSI,这时候你以为是72MHz,实际只有8MHz,所有外设时序全乱。
4.6 实战:配置系统时钟为72MHz
下面是一个完整的时钟配置代码,我把它放在SystemClock_Config()函数里。这是HAL库的标准写法:
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 1. 配置HSE为8MHz,打开PLL,目标72MHz
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz * 9 = 72MHz
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// 2. 配置SYSCLK、AHB、APB1、APB2分频
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB = 72MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // APB1 = 36MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2 = 72MHz
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}
这段代码,我几乎每个项目都会用到。你把它当成模板,改改PLL倍频系数就能适配不同频率的晶振。
嗯,时钟配置和延时函数,是单片机开发的基石。你把这章吃透了,后面ADC采样、串口通信的时序问题,就都不是问题了。