Chapter 03 · ARM 嵌入式 C 开发
时钟系统与定时器
理解 STM32 时钟树的层级结构,掌握 SysTick、通用定时器 PWM 输出与输入捕获的完整实现。
理解 STM32 时钟树的层级结构,掌握 SysTick、通用定时器 PWM 输出与输入捕获的完整实现。
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。HAL_Delay() 和 HAL_GetTick() 依赖它。FreeRTOS 也用 SysTick 做任务调度心跳。/** * @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); }
用 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; /* CNTsConfigOC.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; */ }
/* 用 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,则 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。