1. GPIO 配置
GPIO(General Purpose Input/Output,通用输入输出)配置是嵌入式开发中最基础也最关键的环节之一。合理的 GPIO 配置直接决定了外设能否正常工作,以及系统的稳定性和抗干扰能力。本节将从配置流程、关键参数和常见注意事项三个维度展开说明。
配置流程建议:
- 使能时钟:首先需要使能 GPIO 所在总线的时钟(如 RCC),这是后续所有寄存器操作的前提。
- 选择引脚模式:根据实际用途选择输入、输出、复用功能或模拟模式。
- 配置引脚属性:包括输出类型(推挽/开漏)、上下拉电阻、输出速度等参数。
- 初始化并验证:完成寄存器写入后,建议通过读取寄存器或实际电平测试验证配置是否生效。
案例补充:以 STM32 点亮 LED 为例,通常需要将 PA5 配置为推挽输出模式,并使能 GPIOA 时钟。若配置为开漏模式且外部未接上拉电阻,LED 将无法正常点亮,这是初学者最容易踩的坑之一。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能RCC时钟,开启GPIOA GPIO_InitTypeDef GPIO_InitStructure;//创建GPIO结构体 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//配置引脚模式为推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;//配置启用引脚,PA5 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//指定所选引脚的速度 GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化GPIOA外设2. EXTI中断配置
EXTI(External Interrupt,外部中断)是嵌入式系统中响应外部事件的重要手段。与轮询方式相比,中断机制可以让 CPU 在等待外部信号时执行其他任务,从而显著提升系统的实时性和资源利用率。本节将围绕 EXTI 的触发方式、配置流程和注意事项展开说明。
EXTI 触发方式:
- 上升沿触发:当引脚电平从低到高跳变时触发中断,适用于按键释放、传感器输出高电平脉冲等场景。
- 下降沿触发:当引脚电平从高到低跳变时触发中断,常用于按键按下、低电平有效的信号检测。
- 双边沿触发:无论电平上升还是下降都会触发中断,适合需要同时捕获两种边沿变化的场景,如编码器信号检测。
- 低电平触发:只要引脚保持低电平就会持续触发,适合需要持续响应的外部信号。
配置流程建议:
- 使能 GPIO 时钟:与 GPIO 配置相同,首先需要使能对应总线的时钟。
- 配置引脚为输入模式:EXTI 通常基于输入引脚,需将目标引脚配置为浮空输入或上拉/下拉输入。
- 配置 EXTI 中断源:将目标引脚与 EXTI 线关联,并选择触发方式(上升沿、下降沿或双边沿)。
- 配置 NVIC 优先级:在 NVIC 中使能对应的中断通道,并设置抢占优先级和子优先级。
- 编写中断服务函数:在中断服务函数中处理业务逻辑,并在退出前清除中断标志位。
案例补充:以 STM32 按键触发外部中断为例,通常将 PA0 配置为上拉输入模式,并关联到 EXTI0 线,选择下降沿触发。当按键按下时,引脚电平从高变低,触发中断进入服务函数。需要注意的是,按键在按下和释放瞬间会产生机械抖动,可能导致多次触发中断,因此建议在中断服务函数中加入软件消抖处理,例如延时 10ms 后再次确认电平状态。
EXTI中断配置
/*使能RCC时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能RCC时钟,开启GPIOA RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//使能RCC时钟,开启AFIO /*配置GPIO*/ GPIO_InitTypeDef GPIO_InitStructure;//定义GPIO结构体 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//配置引脚模式为上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//配置启用引脚,PA0 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//指定所选引脚的速度 GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化GPIOA外设 /*指定端口源为GPIOA,指定要配置的中断线路为EXTI_Line0*/ GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitTypeDef EXTI_InitStructure;//定义EXTI结构体 EXTI_InitStructure.EXTI_Line = EXTI_Line0;//选择EXTI中断线0 EXTI_InitStructure.EXTI_LineCmd= ENABLE;//使能中断线 EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//选择为中断模式 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//选择下降沿触发 EXTI_Init(&EXTI_InitStructure);//初始化EXTI外设 /*选择NVIC分组,配置优先级*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置抢占优先组 NVIC_InitTypeDef NVIC_InitStructure;//定义NVIC结构体 NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;//选择0通道 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//设置抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//设置分组优先级 NVIC_Init(&NVIC_InitStructure);//初始化NVIC外设中断函数编写
void EXTI0_IRQHandler(void)//中断函数名(此为固定名称) { if(EXTI_GetFlagStatus(EXTI_Line0) == SET)//获取中断标志位 { if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0) == 0)//获取引脚电平 { /*在此写函数内容*/ EXTI_ClearITPendingBit(EXTI_Line0);//清除中断标志位 } } }常见注意事项:
- 中断标志位清除:退出中断服务函数前必须清除对应的 EXTI 挂起标志位,否则中断会反复进入,导致系统卡死。
- NVIC 优先级配置:合理设置抢占优先级可以避免高优先级中断被低优先级中断打断,保证关键任务的实时性。
- 消抖处理:机械按键等信号源存在抖动,建议在硬件上增加 RC 滤波电路,或在软件中采用延时确认的方式消抖。
- 中断服务函数尽量简短:中断服务函数中不宜执行耗时操作,如延时、打印等,应尽快退出,将耗时任务放到主循环中处理。
3. TIM定时器&中断配置
TIM(Timer,定时器)是嵌入式系统中用于产生精确时间基准、计数、PWM 输出和输入捕获的核心外设。与 GPIO 和 EXTI 相比,定时器能够在硬件层面独立完成计时和计数任务,无需 CPU 持续参与,从而大幅提升系统的实时性和资源利用率。本节将围绕 TIM 的基本原理、定时中断的配置流程和常见注意事项展开说明。
TIM 定时器基本原理:
- 时基单元:定时器内部包含预分频器(PSC)、自动重装载寄存器(ARR)和计数器(CNT)。时钟信号经过预分频后驱动计数器递增,当计数值达到 ARR 设定值时产生更新事件。
- 定时中断:当计数器溢出或达到设定值时,定时器会产生更新中断(Update Interrupt),通知 CPU 执行相应的中断服务函数,从而实现周期性任务调度。
- 计数模式:定时器支持向上计数、向下计数和中心对齐计数三种模式,可根据实际应用场景灵活选择。
- 时钟源:定时器时钟通常来源于内部时钟(如 APB1/APB2 总线时钟),也可配置为外部时钟或编码器模式,满足不同测频和测速需求。
定时中断配置流程建议:
- 使能定时器时钟:首先需要使能 TIM 所在总线的时钟(如 RCC),这是后续所有寄存器操作的前提。
- 配置时基单元:设置预分频器(PSC)和自动重装载值(ARR),确定定时器的计数周期和中断频率。
- 使能更新中断:在定时器中断配置结构体中使能更新中断源,确保计数器溢出时能够产生中断请求。
- 配置 NVIC 优先级:在 NVIC 中使能对应的定时器中断通道,并设置抢占优先级和子优先级。
- 启动定时器:调用定时器使能函数启动计数,并编写中断服务函数处理周期性任务。
案例补充:以 STM32 使用 TIM2 产生 1 秒定时中断为例,假设系统时钟为 72MHz,可将预分频器设置为 7199(即 72MHz 分频后得到 10kHz 计数频率),自动重装载值设置为 9999,这样计数器每计数 10000 次溢出一次,正好产生 1 秒的更新中断。在中断服务函数中可执行 LED 翻转、状态刷新等周期性任务。
TIM定时器中断配置
/*使能RCC时钟*/ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//使能RCC时钟,开启TIM2 /*配置时基单元*/ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;//定义定时器结构体 TIM_TimeBaseStructure.TIM_Period = 10000 - 1;//设置自动重装载值,计数10000次溢出 TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1;//设置预分频值,72MHz/7200=10kHz TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//设置时钟分割 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//选择向上计数模式 TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//设定周期更新重复次数,0为不重复 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);//初始化TIM2时基单元 /*使能更新中断*/ TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);//使能TIM2更新中断 /*选择NVIC分组,配置优先级*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置抢占优先组 NVIC_InitTypeDef NVIC_InitStructure;//定义NVIC结构体 NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;//选择TIM2中断通道 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//设置抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//设置分组优先级 NVIC_Init(&NVIC_InitStructure);//初始化NVIC外设 /*启动定时器*/ TIM_Cmd(TIM2, ENABLE);//使能TIM2,开始计数中断函数编写
void TIM2_IRQHandler(void)//中断函数名(此为固定名称) { if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)//获取更新中断标志位 { /*在此写函数内容,例如LED翻转、状态刷新等*/ TIM_ClearITPendingBit(TIM2, TIM_IT_Update);//清除更新中断标志位 } }常见注意事项:
- 中断标志位清除:退出中断服务函数前必须清除对应的定时器更新标志位,否则中断会反复进入,导致系统卡死。
- 预分频与重装载值计算:定时周期 = (PSC+1) × (ARR+1) / 定时器时钟频率,配置前应先根据目标周期反推参数,避免溢出或精度不足。
- NVIC 优先级配置:合理设置抢占优先级可以避免高优先级中断被低优先级中断打断,保证关键任务的实时性。
- 中断服务函数尽量简短:中断服务函数中不宜执行耗时操作,如延时、打印等,应尽快退出,将耗时任务放到主循环中处理。
- 时钟源确认:不同定时器挂载的总线不同(APB1 或 APB2),使能时钟前务必确认目标定时器所在总线,避免时钟未开启导致定时器不工作。
4. TIM输出比较&PWM配置
输出比较(Output Compare)是定时器的核心功能之一,用于在计数值达到设定值时自动翻转或改变引脚电平,从而生成精确的方波信号。PWM(Pulse Width Modulation,脉冲宽度调制)正是基于输出比较机制实现的,通过调节高电平占空比来控制电机转速、LED 亮度、舵机角度等。与软件延时翻转引脚相比,输出比较由定时器硬件在后台自动完成,无需 CPU 持续参与,能够生成高精度、高稳定性的波形。本节将围绕输出比较与 PWM 的基本原理、配置流程、代码示例和常见注意事项展开说明。
输出比较与 PWM 基本原理:
- 输出比较通道:定时器的每个捕获/比较通道(如 CH1、CH2)都可配置为输出比较模式,当计数器(CNT)的值与比较寄存器(CCR)的值相等时,引脚电平会根据设定的输出模式自动变化。
- PWM 生成原理:在 PWM 模式下,计数器从 0 递增到自动重装载值(ARR),当计数值小于比较值(CCR)时输出高电平,大于等于比较值时输出低电平(或相反),从而形成周期固定、占空比可调的方波。
- 占空比调节:通过修改比较寄存器(CCR)的值即可改变高电平持续时间,进而调节占空比。占空比 = CCR / (ARR+1) × 100%。
- 输出极性:可配置输出极性为高电平有效或低电平有效,以适应不同的负载驱动需求。
- 输出模式:除 PWM 外,输出比较还支持强制输出高/低电平、翻转输出、冻结输出等多种模式,满足不同应用场景。
PWM 配置流程建议:
- 使能定时器时钟:首先需要使能 TIM 所在总线的时钟(如 RCC),这是后续所有寄存器操作的前提。
- 配置 GPIO 复用功能:将目标引脚配置为复用推挽输出模式,并连接到对应的定时器输出通道。
- 配置时基单元:设置预分频器(PSC)和自动重装载值(ARR),确定 PWM 的周期和频率。
- 配置输出比较通道:选择 PWM 模式,设置比较值(CCR)、输出极性和输出使能。
- 使能定时器主输出:对于高级定时器(如 TIM1、TIM8),还需使能 MOE(主输出使能)位,否则 PWM 信号无法输出到引脚。
- 启动定时器:调用定时器使能函数启动计数,PWM 波形即开始在引脚上输出。
案例补充:以 STM32 使用 TIM3 的 CH2 通道输出 PWM 控制 LED 亮度为例,假设系统时钟为 72MHz,可将预分频器设置为 71(即 72MHz 分频后得到 1MHz 计数频率),自动重装载值设置为 999,这样 PWM 频率为 1MHz / 1000 = 1kHz。若将比较值设置为 500,则占空比为 50%,LED 亮度为中等水平。通过在主循环中动态修改比较值,即可实现呼吸灯效果。
PWM 配置代码示例:
/*使能RCC时钟*/ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);//使能RCC时钟,开启TIM3 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能RCC时钟,开启GPIOA /*配置GPIO为复用推挽输出*/ GPIO_InitTypeDef GPIO_InitStructure;//定义GPIO结构体 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//配置引脚模式为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;//配置启用引脚,PA7(TIM3_CH2) GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//指定所选引脚的速度 GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA外设 /*配置时基单元*/ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;//定义定时器结构体 TIM_TimeBaseStructure.TIM_Period = 1000 - 1;//设置自动重装载值,PWM频率=1MHz/1000=1kHz TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;//设置预分频值,72MHz/72=1MHz TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//设置时钟分割 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//选择向上计数模式 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);//初始化TIM3时基单元 /*配置输出比较通道*/ TIM_OCInitTypeDef TIM_OCInitStructure;//定义输出比较结构体 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//选择PWM1模式 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//使能输出比较通道 TIM_OCInitStructure.TIM_Pulse = 500;//设置比较值,占空比=500/1000=50% TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//设置输出极性为高电平有效 TIM_OC2Init(TIM3, &TIM_OCInitStructure);//初始化TIM3的CH2输出比较 /*启动定时器*/ TIM_Cmd(TIM3, ENABLE);//使能TIM3,开始输出PWM波形动态调节占空比:
/*在主循环中动态修改比较值,实现呼吸灯效果*/ uint16_t duty = 0; uint8_t direction = 1; while(1) { TIM_SetCompare2(TIM3, duty);//设置TIM3_CH2的比较值,即占空比 delay_ms(10);//延时10ms if(direction == 1) { duty += 10;//占空比递增 if(duty >= 1000) direction = 0;//达到最大值后反向 } else { duty -= 10;//占空比递减 if(duty <= 0) direction = 1;//达到最小值后反向 } }常见注意事项:
- GPIO 复用功能配置:PWM 输出必须将引脚配置为复用推挽输出模式,并确保引脚与定时器通道正确对应,否则波形无法输出到引脚。
- 高级定时器主输出使能:对于 TIM1、TIM8 等高级定时器,必须使能 MOE(主输出使能)位,否则即使配置正确也无法输出 PWM 信号。
- 频率与占空比计算:PWM 频率 = 定时器时钟 / ((PSC+1) × (ARR+1)),占空比 = CCR / (ARR+1) × 100%,配置前应先根据目标频率和占空比反推参数。
- 比较值范围:比较值(CCR)不能超过自动重装载值(ARR),否则输出将一直为高电平或低电平,无法形成有效 PWM 波形。
- 动态调节注意:在主循环中动态修改比较值时,建议在更新事件发生后修改,避免在计数过程中写入导致波形异常。
- 时钟源确认:不同定时器挂载的总线不同(APB1 或 APB2),使能时钟前务必确认目标定时器所在总线,避免时钟未开启导致定时器不工作。
5. TIM输入捕获
输入捕获(Input Capture)是定时器的重要功能之一,用于精确测量外部信号的频率、周期、脉宽等时间参数。与轮询读取引脚电平相比,输入捕获由定时器硬件在信号边沿到来时自动记录当前计数值,无需 CPU 持续参与,能够实现高精度、低延迟的时间测量。本节将围绕输入捕获的基本原理、配置流程、代码示例和常见注意事项展开说明。
输入捕获基本原理:
- 捕获通道:定时器的每个捕获/比较通道(如 CH1、CH2)都可配置为输入捕获模式,用于检测外部信号的上升沿或下降沿。
- 捕获过程:当检测到设定的边沿信号时,定时器会自动将当前计数器(CNT)的值锁存到捕获寄存器(CCR)中,并产生捕获事件。
- 周期测量:通过连续两次捕获的计数值之差,结合定时器时钟频率,即可计算出信号的周期和频率。
- 脉宽测量:通过分别捕获上升沿和下降沿的计数值,可计算出高电平或低电平的持续时间,从而得到脉宽和占空比。
- 边沿选择:每个通道可独立配置捕获边沿(上升沿、下降沿或双边沿),以适应不同的测量需求。
输入捕获配置流程建议:
- 使能定时器时钟:首先需要使能 TIM 所在总线的时钟(如 RCC),这是后续所有寄存器操作的前提。
- 配置 GPIO 复用功能:将目标引脚配置为复用输入模式,并连接到对应的定时器捕获通道。
- 配置时基单元:设置预分频器(PSC)和自动重装载值(ARR),确定计数器的计数频率和最大可测量范围。
- 配置捕获通道:选择输入捕获模式,设置捕获边沿(上升沿、下降沿或双边沿),并使能捕获通道。
- 使能捕获中断:在定时器中断配置结构体中使能捕获中断源,确保边沿到来时能够产生中断请求。
- 配置 NVIC 优先级:在 NVIC 中使能对应的定时器中断通道,并设置抢占优先级和子优先级。
- 启动定时器并编写中断服务函数:调用定时器使能函数启动计数,在中断服务函数中读取捕获值并计算频率或脉宽。
案例补充:以 STM32 使用 TIM4 的 CH1 通道测量外部方波信号频率为例,假设系统时钟为 72MHz,可将预分频器设置为 71(即 72MHz 分频后得到 1MHz 计数频率),自动重装载值设置为 0xFFFF(65535),这样计数器每微秒递增一次。当检测到上升沿时,定时器将当前计数值锁存到 CCR1 寄存器。通过连续两次捕获的计数值之差,即可计算出信号的周期和频率。例如,若两次捕获的差值(考虑溢出补偿后)为 1000,则信号周期为 1000 微秒,频率为 1kHz。
输入捕获配置代码示例:
/*使能RCC时钟*/ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);//使能RCC时钟,开启TIM4 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能RCC时钟,开启GPIOB /*配置GPIO为复用输入*/ GPIO_InitTypeDef GPIO_InitStructure;//定义GPIO结构体 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//配置引脚模式为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//配置启用引脚,PB6(TIM4_CH1) GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//指定所选引脚的速度 GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB外设 /*配置时基单元*/ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;//定义定时器结构体 TIM_TimeBaseStructure.TIM_Period = 0xFFFF;//设置自动重装载值,最大计数范围 TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;//设置预分频值,72MHz/72=1MHz TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//设置时钟分割 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//选择向上计数模式 TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);//初始化TIM4时基单元 /*配置输入捕获通道*/ TIM_ICInitTypeDef TIM_ICInitStructure;//定义输入捕获结构体 TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//选择捕获通道1 TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//选择上升沿捕获 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//直接映射到TI1 TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//不分频,每次边沿都捕获 TIM_ICInitStructure.TIM_ICFilter = 0x0;//不配置滤波器 TIM_ICInit(TIM4, &TIM_ICInitStructure);//初始化TIM4输入捕获 /*使能捕获中断*/ TIM_ITConfig(TIM4, TIM_IT_CC1, ENABLE);//使能TIM4捕获比较1中断 /*选择NVIC分组,配置优先级*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置抢占优先组 NVIC_InitTypeDef NVIC_InitStructure;//定义NVIC结构体 NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;//选择TIM4中断通道 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//设置抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//设置分组优先级 NVIC_Init(&NVIC_InitStructure);//初始化NVIC外设 /*启动定时器*/ TIM_Cmd(TIM4, ENABLE);//使能TIM4,开始计数中断函数编写:
uint16_t capture_value = 0;//保存上一次捕获值 uint16_t capture_period = 0;//保存两次捕获的差值 void TIM4_IRQHandler(void)//中断函数名(此为固定名称) { if(TIM_GetITStatus(TIM4, TIM_IT_CC1) == SET)//获取捕获比较1中断标志位 { uint16_t current_value = TIM_GetCapture1(TIM4);//读取当前捕获值 if(current_value >= capture_value) { capture_period = current_value - capture_value;//计算差值 } else { capture_period = (0xFFFF - capture_value) + current_value;//考虑溢出补偿 } capture_value = current_value;//更新上一次捕获值 /*根据capture_period计算频率:频率 = 1MHz / capture_period*/ TIM_ClearITPendingBit(TIM4, TIM_IT_CC1);//清除捕获中断标志位 } }常见注意事项:
- GPIO 复用功能配置:输入捕获必须将引脚配置为复用输入模式,并确保引脚与定时器通道正确对应,否则无法检测到外部信号。
- 计数器溢出处理:当测量周期较长时,计数器可能发生溢出,需要在中断服务函数中判断并补偿,否则计算结果会出错。
- 捕获边沿选择:根据测量需求正确选择上升沿或下降沿捕获,测量脉宽时需同时配置双边沿捕获或使用两个通道配合。
- 输入滤波:对于存在噪声或抖动的信号,可配置输入滤波器(ICF)滤除毛刺,提高测量稳定性。
- 中断服务函数尽量简短:中断服务函数中不宜执行耗时操作,如延时、打印等,应尽快读取捕获值并退出,将计算任务放到主循环中处理。
- 时钟源确认:不同定时器挂载的总线不同(APB1 或 APB2),使能时钟前务必确认目标定时器所在总线,避免时钟未开启导致定时器不工作。
6. AD单通道&AD多通道
ADC(Analog-to-Digital Converter,模数转换器)是嵌入式系统中连接模拟世界与数字世界的桥梁,用于将电压、电流、温度、压力等连续变化的模拟信号转换为 CPU 能够处理的离散数字量。与 GPIO 的数字高低电平不同,ADC 能够精确量化信号的幅度信息,是传感器采集、电源监测、音频处理等场景的核心外设。本节将围绕 ADC 的基本原理、单通道与多通道的配置流程、代码示例和常见注意事项展开说明。
ADC 基本原理:
- 采样与量化:ADC 首先对模拟信号进行采样,将连续时间信号离散化,再通过量化将连续的幅度值映射为有限个数字等级。采样频率和量化位数(分辨率)共同决定了转换精度。
- 分辨率:STM32 常见 ADC 为 12 位分辨率,即输出范围为 0~4095。参考电压(如 3.3V)被划分为 4096 个等级,每个数字量对应一个电压区间。
- 转换模式:支持单次转换、连续转换、扫描模式等多种工作方式。单次转换适合采集单个通道,连续转换适合持续监测,扫描模式则用于多通道依次采集。
- 触发方式:ADC 转换可由软件触发,也可由定时器、外部引脚等硬件事件触发,以满足不同采样时序要求。
- 数据对齐:转换结果可配置为左对齐或右对齐,通常使用右对齐,便于直接读取 12 位有效数据。
单通道配置流程建议:
- 使能 ADC 与 GPIO 时钟:首先需要使能 ADC 所在总线(APB2)的时钟,同时使能目标引脚所在 GPIO 端口的时钟,这是后续所有寄存器操作的前提。
- 配置引脚为模拟输入:将目标引脚配置为模拟输入模式(GPIO_Mode_AIN),确保外部模拟信号能够无干扰地进入 ADC 采样电路。
- 配置 ADC 时钟分频:根据系统时钟频率设置 ADC 时钟分频系数,使 ADC 工作时钟处于合理范围(通常不超过 14MHz),以保证转换精度。
- 配置 ADC 工作模式:设置 ADC 为独立模式、单次转换、软件触发、数据右对齐,并指定转换通道数量。
- 配置转换通道与采样时间:选择目标通道并设置采样时间,采样时间越长精度越高,但转换速度会相应降低,需根据实际需求权衡。
- 使能 ADC 并校准:使能 ADC 后执行复位校准和校准流程,消除内部电容误差,提高转换结果的一致性。
- 启动转换并读取结果:通过软件触发启动转换,等待转换完成标志位置位后读取数据寄存器,再按公式换算为实际电压值。
案例补充:以 STM32 使用 ADC1 的通道 1(PA1)采集电位器电压为例,假设参考电压为 3.3V,ADC 为 12 位分辨率。将 PA1 配置为模拟输入,设置采样时间为 55.5 个周期以保证精度。启动单次转换后,读取数据寄存器得到数字量,通过公式 电压 = 数字量 × 3.3 / 4095 即可换算出实际电压值。例如,若读取到数字量为 2048,则对应电压约为 1.65V。若读取到数字量为 4095,则对应电压约为 3.3V,说明输入信号已达到满量程。
单通道配置代码示例:
/*使能RCC时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//使能RCC时钟,开启ADC1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能RCC时钟,开启GPIOA /*配置GPIO为模拟输入*/ GPIO_InitTypeDef GPIO_InitStructure;//定义GPIO结构体 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//配置引脚模式为模拟输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;//配置启用引脚,PA1(ADC1_CH1) GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//指定所选引脚的速度 GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA外设 /*配置ADC时钟分频*/ RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置ADC时钟分频,72MHz/6=12MHz /*配置ADC工作模式*/ ADC_InitTypeDef ADC_InitStructure;//定义ADC结构体 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//选择独立模式 ADC_InitStructure.ADC_ScanConvMode = DISABLE;//关闭扫描模式,单通道转换 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//关闭连续转换,单次转换 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//不使用外部触发 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据右对齐 ADC_InitStructure.ADC_NbrOfChannel = 1;//转换通道数为1 ADC_Init(ADC1, &ADC_InitStructure);//初始化ADC1 /*配置转换通道*/ ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5);//配置通道1,采样时间55.5周期 /*使能ADC并校准*/ ADC_Cmd(ADC1, ENABLE);//使能ADC1 ADC_ResetCalibration(ADC1);//复位校准寄存器 while(ADC_GetResetCalibrationStatus(ADC1));//等待复位校准完成 ADC_StartCalibration(ADC1);//开始校准 while(ADC_GetCalibrationStatus(ADC1));//等待校准完成 /*启动转换并读取结果*/ ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件触发转换 while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//等待转换完成 uint16_t adc_value = ADC_GetConversionValue(ADC1);//读取转换结果 float voltage = adc_value * 3.3f / 4095.0f;//换算为电压值多通道配置流程建议:
- 使能 ADC 与 GPIO 时钟:与单通道相同,首先使能 ADC 所在总线时钟以及多个目标引脚所在 GPIO 端口的时钟。
- 配置多个引脚为模拟输入:将参与采集的所有引脚统一配置为模拟输入模式,确保各通道信号都能正常进入采样电路。
- 配置 ADC 时钟分频:根据系统时钟设置合理的 ADC 时钟分频,保证多通道依次转换时整体采样时序稳定。
- 配置 ADC 工作模式:使能扫描模式(ADC_ScanConvMode = ENABLE),设置转换通道数量为实际通道数,并选择单次转换、软件触发和数据右对齐。
- 配置规则序列:通过 ADC_RegularChannelConfig 依次为每个通道指定其在规则序列中的位置(第 1 位、第 2 位等),并分别设置采样时间。
- 使能 ADC 并校准:使能 ADC 后执行复位校准和校准流程,确保多通道转换结果的准确性。
- 启动转换并依次读取结果:软件触发启动转换,等待转换完成标志位后读取数据寄存器。扫描模式下每次转换完成一个通道,需多次触发并读取,才能获得所有通道的转换结果。
案例补充:以 STM32 使用 ADC1 的通道 0(PA0)和通道 1(PA1)分别采集两路电位器电压为例,将 PA0 和 PA1 配置为模拟输入,使能扫描模式并设置转换通道数为 2。将通道 0 设为规则序列第 1 位,通道 1 设为第 2 位,采样时间均设为 55.5 个周期。启动转换后,ADC 会依次转换两个通道,转换结果分别存入 ADC1 的 DR 寄存器(按顺序更新)。通过两次读取数据寄存器,即可获得两路电压对应的数字量。例如,若第一次读取到数字量为 2048,第二次读取到数字量为 1024,则两路电压分别约为 1.65V 和 0.825V。
多通道配置代码示例:
/*使能RCC时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//使能RCC时钟,开启ADC1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能RCC时钟,开启GPIOA /*配置GPIO为模拟输入*/ GPIO_InitTypeDef GPIO_InitStructure;//定义GPIO结构体 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//配置引脚模式为模拟输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;//配置启用引脚,PA0和PA1 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//指定所选引脚的速度 GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA外设 /*配置ADC时钟分频*/ RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置ADC时钟分频,72MHz/6=12MHz /*配置ADC工作模式*/ ADC_InitTypeDef ADC_InitStructure;//定义ADC结构体 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//选择独立模式 ADC_InitStructure.ADC_ScanConvMode = ENABLE;//使能扫描模式,多通道转换 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//关闭连续转换,单次转换 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//不使用外部触发 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据右对齐 ADC_InitStructure.ADC_NbrOfChannel = 2;//转换通道数为2 ADC_Init(ADC1, &ADC_InitStructure);//初始化ADC1 /*配置转换通道*/ ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);//配置通道0为第1位,采样时间55.5周期 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);//配置通道1为第2位,采样时间55.5周期 /*使能ADC并校准*/ ADC_Cmd(ADC1, ENABLE);//使能ADC1 ADC_ResetCalibration(ADC1);//复位校准寄存器 while(ADC_GetResetCalibrationStatus(ADC1));//等待复位校准完成 ADC_StartCalibration(ADC1);//开始校准 while(ADC_GetCalibrationStatus(ADC1));//等待校准完成 /*启动转换并读取结果*/ ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件触发转换 while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//等待转换完成 uint16_t adc_value0 = ADC_GetConversionValue(ADC1);//读取通道0转换结果 ADC_SoftwareStartConvCmd(ADC1, ENABLE);//再次触发转换 while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//等待转换完成 uint16_t adc_value1 = ADC_GetConversionValue(ADC1);//读取通道1转换结果 float voltage0 = adc_value0 * 3.3f / 4095.0f;//换算为通道0电压值 float voltage1 = adc_value1 * 3.3f / 4095.0f;//换算为通道1电压值常见注意事项:
- GPIO 模拟输入配置:ADC 采集引脚必须配置为模拟输入模式(GPIO_Mode_AIN),不能使用推挽输出或浮空输入,否则外部模拟信号会被内部电路干扰,导致转换结果失真。
- ADC 时钟频率限制:STM32 的 ADC 时钟频率通常不能超过 14MHz,过高的时钟会导致采样精度下降。配置分频系数前应先确认系统时钟频率,再选择合适的 PCLK2 分频。
- 采样时间选择:采样时间越长,内部采样电容充电越充分,转换精度越高,但转换速度会降低。对于高阻抗信号源,应适当增大采样时间以保证精度。
- 校准流程不可省略:使能 ADC 后必须执行复位校准和校准流程,否则转换结果可能存在较大偏差。校准完成后应等待校准状态位清零再启动转换。
- 多通道读取顺序:扫描模式下,每次转换完成一个通道后数据寄存器会被更新,必须按规则序列顺序依次读取,否则会得到错误的通道对应关系。
- 参考电压稳定性:ADC 转换精度直接依赖参考电压(VREF)的稳定性,建议在参考电压引脚附近增加去耦电容,避免电源纹波影响转换结果。
- 转换结果换算:12 位 ADC 的满量程数字量为 4095,换算电压时使用公式 电压 = 数字量 × VREF / 4095,其中 VREF 为实际参考电压值。
7. DMA配置
DMA(Direct Memory Access,直接存储器访问)是嵌入式系统中用于在内存与外设之间、内存与内存之间高速搬运数据的重要外设。与 CPU 逐字节搬运数据相比,DMA 能够在硬件层面独立完成数据转移,无需 CPU 持续参与,从而大幅提升系统的数据吞吐量和实时性。本节将围绕 DMA 的基本原理、配置流程、代码示例和常见注意事项展开说明。
DMA 基本原理:
- 数据传输方向:DMA 支持外设到内存、内存到外设、内存到内存三种传输方向,可灵活满足不同场景的数据搬运需求。
- 传输单位:DMA 可配置传输数据宽度(字节、半字、字),并支持突发传输,提高搬运效率。
- 传输模式:支持单次传输和循环传输两种模式。单次传输在完成指定次数后停止,循环传输则自动重新开始,适合持续采集等场景。
- 通道与请求:每个 DMA 通道可关联不同的外设请求(如 ADC、USART、SPI、TIM 等),通过配置通道与外设的映射关系实现自动搬运。
- 中断与标志:DMA 支持传输完成、半传输、传输错误等中断,可在中断服务函数中处理数据或进行状态管理。
DMA 配置流程建议:
- 使能 DMA 时钟:首先需要使能 DMA 所在总线的时钟(如 RCC),这是后续所有寄存器操作的前提。
- 配置 DMA 通道:选择目标 DMA 通道,设置传输方向、数据宽度、传输模式(单次或循环)以及优先级。
- 配置源地址与目标地址:设置外设寄存器地址和内存缓冲区地址,并指定传输数据量(以传输单位计)。
- 使能 DMA 中断(可选):如需在传输完成时通知 CPU,可在 DMA 中断配置结构体中使能传输完成中断,并配置 NVIC 优先级。
- 使能 DMA 通道:调用 DMA 使能函数启动传输,DMA 将按配置自动搬运数据。
- 验证传输结果:通过读取 DMA 标志位或检查内存缓冲区数据,确认传输是否按预期完成。
案例补充:以 STM32 使用 DMA 将 ADC1 转换结果自动搬运到内存缓冲区为例,可将 ADC1 配置为连续转换模式,并将 DMA1 通道 1 关联到 ADC1。配置 DMA 为外设到内存方向、循环传输模式,源地址设为 ADC1 的数据寄存器地址,目标地址设为内存缓冲区。启动后,ADC 每次转换完成都会自动触发 DMA 搬运,无需 CPU 干预即可持续采集数据。在 DMA 传输完成中断中可对缓冲区数据进行处理,例如计算平均值或触发其他任务。
DMA 配置代码示例:
/*使能RCC时钟*/ RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能RCC时钟,开启DMA1 /*配置DMA通道*/ DMA_InitTypeDef DMA_InitStructure;//定义DMA结构体 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//设置外设基地址为ADC1数据寄存器 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buffer;//设置内存基地址为缓冲区 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//设置传输方向为外设到内存 DMA_InitStructure.DMA_BufferSize = 100;//设置传输数据量 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设地址不递增 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设数据宽度为半字 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//内存数据宽度为半字 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//选择循环传输模式 DMA_InitStructure.DMA_Priority = DMA_Priority_High;//设置通道优先级为高 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//关闭内存到内存传输 DMA_Init(DMA1_Channel1, &DMA_InitStructure);//初始化DMA1通道1 /*使能DMA传输完成中断*/ DMA_ITConfig(DMA1_IT_TC1, ENABLE);//使能DMA1通道1传输完成中断 /*选择NVIC分组,配置优先级*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置抢占优先组 NVIC_InitTypeDef NVIC_InitStructure;//定义NVIC结构体 NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;//选择DMA1通道1中断 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//设置抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//设置分组优先级 NVIC_Init(&NVIC_InitStructure);//初始化NVIC外设 /*使能DMA通道*/ DMA_Cmd(DMA1_Channel1, ENABLE);//使能DMA1通道1,开始传输中断函数编写:
void DMA1_Channel1_IRQHandler(void)//中断函数名(此为固定名称) { if(DMA_GetITStatus(DMA1_IT_TC1) == SET)//获取传输完成中断标志位 { /*在此写函数内容,例如处理缓冲区数据、更新标志位等*/ DMA_ClearITPendingBit(DMA1_IT_TC1);//清除传输完成中断标志位 } }常见注意事项:
- 外设与通道映射:每个 DMA 通道只能关联特定的外设请求,配置前务必查阅芯片手册确认通道与外设的对应关系,否则 DMA 无法正确触发。
- 地址对齐:源地址和目标地址需按传输数据宽度对齐(半字按 2 字节对齐、字按 4 字节对齐),否则可能导致总线错误或数据错乱。
- 缓冲区大小:内存缓冲区大小必须不小于 DMA 配置的传输数据量,否则会发生缓冲区溢出,破坏其他内存数据。
- 循环模式注意:循环传输模式下,传输完成后地址自动回到起始位置,适合持续采集,但需注意缓冲区数据会被不断覆盖,应及时处理。
- 中断标志位清除:退出中断服务函数前必须清除对应的 DMA 中断标志位,否则中断会反复进入,导致系统卡死。
- 与外设配合:DMA 传输通常需要外设配合触发,例如 ADC 转换完成、USART 接收数据等,需确保外设已正确配置并使能。
- 优先级配置:多个 DMA 通道同时工作时,合理设置通道优先级可避免数据冲突,保证关键传输的实时性。