Chapter 03 · ARM 嵌入式 C 开发

时钟系统与定时器

理解 STM32 时钟树的层级结构,掌握 SysTick、通用定时器 PWM 输出与输入捕获的完整实现。

课程进度30%

关键概念

时钟源:HSE / HSI / LSE / LSI
HSE(High Speed External):外部晶振,通常 8MHz,频率精确稳定,是 PLL 的首选输入源。HSI(High Speed Internal):片内 RC 振荡器,16MHz,无需外部元件,但精度较差(±1%)。LSE:32.768kHz 外部晶振,专供 RTC 实时时钟。LSI:片内低速 RC,32kHz,供看门狗和 RTC 备份。
PLL(锁相环)
Phase-Locked Loop,将低频时钟源倍频到高频。STM32F4 主 PLL 公式:f_VCO = HSE × (N/M)SYSCLK = f_VCO / P。典型配置:HSE=8MHz,M=8,N=336,P=2 → SYSCLK = 8×(336/8)/2 = 168MHz。M 是 HSE 预分频,确保 PLL 输入为 1-2MHz。
AHB / APB 总线分频
SYSCLK 经过 AHB 预分频器(最多/512),得到 HCLK(AHB 总线时钟),供 CPU/DMA/GPIO 使用。HCLK 再经过 APB1 预分频(/4 → 42MHz)和 APB2 预分频(/2 → 84MHz),供片上外设使用。定时器时钟是 APB 时钟的 2 倍(若 APB 分频>1)。
SysTick 定时器
Cortex-M 内核内置的 24 位递减计数器,专门用于操作系统心跳和精确延时。HAL 库用它产生 1ms 中断,HAL_Delay()HAL_GetTick() 依赖它。FreeRTOS 也用 SysTick 做任务调度心跳。
基本定时器 / 通用定时器 / 高级定时器
基本定时器(TIM6/7):只有计数和更新中断,常驱动 DAC。通用定时器(TIM2-5, TIM9-14):有捕获/比较通道,支持 PWM 输出、输入捕获、编码器接口。高级定时器(TIM1/8):在通用功能基础上增加死区控制(互补 PWM),用于三相电机控制。
PWM(脉冲宽度调制)
通过改变方波的占空比(高电平时间/周期)来等效控制输出电压或功率。占空比 0% = 始终低,50% = 平均 1.65V,100% = 始终高。电机控速、LED 调光、音频输出等都依赖 PWM。定时器 PWM 输出频率 = TIMx 时钟 / (PSC+1) / (ARR+1)。
输入捕获(Input Capture)
定时器通道检测到引脚上升沿/下降沿时,自动将当前计数值 CNT 锁存到 CCR 寄存器中,CPU 读取两次边沿的时间差,即可计算脉宽或频率。测量超声波距离传感器(HC-SR04)是经典应用。
预分频器(PSC)与自动重载值(ARR)
PSC 是分频系数,将定时器时钟降低 (PSC+1) 倍。ARR 是计数上限,CNT 从 0 计数到 ARR 后溢出,产生更新事件。定时器周期 = (PSC+1)×(ARR+1) / 定时器时钟频率。PWM 比较值 CCR 决定占空比:占空比 = CCR/(ARR+1)。

STM32F4 时钟树图解

STM32F407 时钟树(168MHz 配置) ═════════════════════════════════════════════════════════════════════ HSE(8MHz) ──┐ ├──► /M(÷8) ──► PLL VCO ×N(×336) = 336MHz HSI(16MHz) ──┘ └──► /P(÷2) ──► SYSCLK = 168MHz └──► /Q(÷7) ──► USB/SDIO = 48MHz SYSCLK(168MHz) │ ├──► AHB 预分频(/1) ──► HCLK = 168MHz │ │ │ ├──► CPU Core 168MHz │ ├──► DMA1/DMA2 168MHz │ ├──► GPIO A-K 168MHz │ ├──► FLASH (最大100MHz有效访问) │ │ │ ├──► APB1 预分频(/4) ──► PCLK1 = 42MHz │ │ └──► TIM 时钟: 84MHz(×2 因为APB1分频>1) │ │ 外设: UART2-5, I2C1-3, SPI2/3, TIM2-7, TIM12-14 │ │ │ └──► APB2 预分频(/2) ──► PCLK2 = 84MHz │ └──► TIM 时钟: 168MHz(×2) │ 外设: UART1/6, SPI1, TIM1/8-11, ADC(最大36MHz) │ └──► SysTick = HCLK/8 = 21MHz(或直接 HCLK = 168MHz) 定时器时钟规则: 若 APBx 预分频 = 1 → 定时器时钟 = HCLK 若 APBx 预分频 > 1 → 定时器时钟 = APBx 时钟 × 2

时钟配置代码(STM32CubeMX 生成)

/**
 * @brief 系统时钟配置:HSE 8MHz → PLL → SYSCLK 168MHz
 *        这段代码通常由 CubeMX 自动生成,了解参数含义很重要
 */
void SystemClock_Config(void) {
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /* 使能电源控制时钟,并设置 VOS = Scale1(最高性能) */
  __HAL_RCC_PWR_CLK_ENABLE();
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

  /* ① 配置 HSE + PLL */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState       = RCC_HSE_ON;     /* 开启 HSE */
  RCC_OscInitStruct.PLL.PLLState   = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource  = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 8;    /* HSE/8 = 1MHz 进 VCO */
  RCC_OscInitStruct.PLL.PLLN = 336;  /* VCO = 1×336 = 336MHz */
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;  /* SYSCLK = 336/2 = 168MHz */
  RCC_OscInitStruct.PLL.PLLQ = 7;    /* USB = 336/7 = 48MHz */
  HAL_RCC_OscConfig(&RCC_OscInitStruct);

  /* ② 配置总线分频 */
  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;    /* HCLK = 168MHz */
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;      /* APB1 = 42MHz */
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;      /* APB2 = 84MHz */
  /* Flash 延迟:168MHz 时需要 5 等待周期 */
  HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5);
}

SysTick 定时器原理

SysTick 工作原理(24位递减计数器) ┌───────────────────────────────────────────────┐ │ SysTick(Cortex-M 内核内置) │ │ │ │ LOAD: 设置重载值(如 168000-1 → 1ms中断) │ │ VAL: 当前计数值(从 LOAD 递减至 0) │ │ CTRL: bit0=使能, bit1=中断使能, bit2=时钟源 │ │ │ │ 168000 → 167999 → ... → 1 → 0 → 触发中断 │ │ └──────────────────────────────────┘ │ │ 自动重载 LOAD,继续递减 │ └───────────────────────────────────────────────┘ HAL_Init() 中的配置: - 时钟源 = HCLK = 168MHz - LOAD = 168000 - 1 - 每 168000 个时钟周期 = 1ms 中断一次 - HAL_GetTick() 返回中断次数(毫秒计数)

通用定时器 PWM 输出

用 TIM3 CH1(PA6)输出 1kHz PWM,初始占空比 50%,然后通过修改 CCR 值改变亮度:

/**
 * @brief TIM3 CH1 PWM 初始化
 *        引脚: PA6(AF2 = TIM3_CH1)
 *        TIM3 挂在 APB1,时钟 = 84MHz
 *        PWM 频率 = 84MHz / (PSC+1) / (ARR+1) = 84M/84/1000 = 1kHz
 */
TIM_HandleTypeDef htim3;

void TIM3_PWM_Init(void) {
  TIM_OC_InitTypeDef sConfigOC = {0};

  /* 开启时钟 */
  __HAL_RCC_TIM3_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  /* 配置 PA6 为 TIM3_CH1 复用推挽输出 */
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin       = GPIO_PIN_6;
  GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;     /* 复用推挽 */
  GPIO_InitStruct.Pull      = GPIO_NOPULL;
  GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;       /* TIM3 是 AF2 */
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /* 配置定时器基参数 */
  htim3.Instance               = TIM3;
  htim3.Init.Prescaler         = 84 - 1;    /* 84MHz/84 = 1MHz 计数时钟 */
  htim3.Init.CounterMode       = TIM_COUNTERMODE_UP;
  htim3.Init.Period            = 1000 - 1;  /* ARR=999, 1MHz/1000 = 1kHz PWM */
  htim3.Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  HAL_TIM_PWM_Init(&htim3);

  /* 配置 PWM 通道1 */
  sConfigOC.OCMode       = TIM_OCMODE_PWM1;   /* CNT
  sConfigOC.Pulse        = 500;              /* CCR=500 → 50% 占空比 */
  sConfigOC.OCPolarity   = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode   = TIM_OCFAST_DISABLE;
  HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);

  /* 启动 PWM 输出 */
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}

/**
 * @brief 动态改变 PWM 占空比(0-100)
 *        在运行时直接修改 CCR 寄存器即可,无需重新初始化
 */
void PWM_SetDuty(uint8_t duty_percent) {
  uint32_t pulse = ((uint32_t)duty_percent * (__HAL_TIM_GET_AUTORELOAD(&htim3) + 1)) / 100;
  __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse);
  /* 也可直接写寄存器:TIM3->CCR1 = pulse; */
}
PWM 波形与参数关系 ARR = 999(计数 0 到 999,共 1000 步) PSC = 83(84MHz/84 = 1MHz 计数时钟) PWM 周期 = 1000 / 1MHz = 1ms → 频率 1kHz CCR = 500,占空比 50%: ───────────────────────────────────────────── CNT: 0──────────────499│500──────────────999│0... ↑ CNT < CCR,高 ↑│↑ CNT ≥ CCR,低 ↑│ OUT: ████████████████████│────────────────│████... ───────────────────────────────────────────── CCR = 250,占空比 25%: OUT: █████│──────────────────────────────│█████... CCR = 0,占空比 0%(始终低) CCR = 999,占空比 ≈100%(始终高) 应用: - LED 调光:人眼对 ≥50Hz 的 PWM 感知平均亮度 - 电机调速:驱动电路对 PWM 进行功率放大 - 舵机控制:1-2ms 脉宽 / 20ms 周期(50Hz)

输入捕获测量脉宽

/* 用 TIM2 CH1(PA0)输入捕获,测量 HC-SR04 超声波 Echo 脉宽 */
TIM_HandleTypeDef htim2;
volatile uint32_t ic_val1 = 0, ic_val2 = 0;
volatile uint8_t  ic_done = 0;

void TIM2_IC_Init(void) {
  TIM_IC_InitTypeDef sConfigIC = {0};
  __HAL_RCC_TIM2_CLK_ENABLE();

  htim2.Instance               = TIM2;
  htim2.Init.Prescaler         = 84 - 1;   /* 1MHz 计数 */
  htim2.Init.CounterMode       = TIM_COUNTERMODE_UP;
  htim2.Init.Period            = 0xFFFFFFFF; /* TIM2 是32位,不溢出 */
  HAL_TIM_IC_Init(&htim2);

  sConfigIC.ICPolarity  = TIM_ICPOLARITY_BOTHEDGE;  /* 双边沿都捕获 */
  sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
  sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
  sConfigIC.ICFilter    = 0;
  HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);
  HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);  /* 中断模式 */
}

/* 捕获中断回调 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
  static uint8_t edge = 0;
  if (htim->Instance == TIM2) {
    if (!edge) {
      ic_val1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
      edge = 1;
    } else {
      ic_val2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
      edge = 0; ic_done = 1;
    }
  }
}

/* 计算距离:脉宽(μs) / 58 = 距离(cm) */
float HCSR04_GetDistance_cm(void) {
  ic_done = 0;
  uint32_t timeout = HAL_GetTick() + 100;
  while (!ic_done && HAL_GetTick() < timeout);
  if (!ic_done) return -1.0f;  /* 超时 */
  uint32_t pulse_us = ic_val2 - ic_val1;
  return pulse_us / 58.0f;
}
HAL_Delay 与 SysTick 的坑

HAL_Delay() 依赖 SysTick 中断(优先级最高)。若在高优先级中断服务程序中调用 HAL_Delay(),而该中断优先级高于 SysTick,则 SysTick 永远得不到执行,HAL_Delay() 将永久阻塞。在 ISR 中应使用 DWT 计数器做微秒延时,或重构代码避免在 ISR 中延时。

定时器时钟频率确认方法

在 STM32CubeMX 的 Clock Configuration 界面,可以直观看到每个定时器的时钟频率。记住:APB1 挂的定时器(TIM2-7,TIM12-14)时钟频率是 APB1 时钟的 2 倍(若 APB1 有分频);APB2 挂的定时器(TIM1,TIM8-11)时钟频率是 APB2 时钟的 2 倍。168MHz 系统下:APB1 TIM = 84MHz,APB2 TIM = 168MHz。