H_On个人小站

个人站

这都被你发现了【惊
奖励你一朵小红花~


按键和定时器 PWM

目录

扫描按键

接线

按键一头接 PB12 一头接 GND,因为 PB12 默认是上拉 3.3V

按键初始化函数

void key_init(void) {
	// GPIO 结构体变量
	GPIO_InitTypeDef GPIO_InitStruct;

	// 使能 GPIOB
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12;
	// 设置 PA12 为上拉输入
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStruct);
}

按键扫描函数

首先在上面定义一个案件引脚状态检测的宏定义

#define USER_KEY GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12)

然后定义一个按键扫描和触发动作的函数

void key_scan(void) {
	// 检测按键被按下
	if (USER_KEY == 0) {
		// 延时 10ms 再次检测防抖
		delay(1, 10);
		if (USER_KEY == 0) {
			// 按键按下使板载 LED 闪烁一次
			GPIO_ResetBits(GPIOC, GPIO_Pin_13);
			delay(1, 100);
			GPIO_SetBits(GPIOC, GPIO_Pin_13);
			delay(1, 100);
		}
	}
}

主函数

这里面很简单,初始化之后,循环扫描按键即可

int main(void) {
	delay_init();
	led_init();
	key_init();
	while (1) {
		key_scan();
	}
}

编译下载运行之后,将 PB12 连接一下 GND 板载 LED 就闪烁一次,一直连着一直闪。

外部中断

首先需要添加一个外部中断固件库 EXTI

添加步骤

修改按键初始化函数

void key_init(void) {
	// GPIO 结构体变量
	GPIO_InitTypeDef GPIO_InitStruct;
	// 外部中断结构体变量
	EXTI_InitTypeDef EXTI_InitStruct;
	// 中断优先级结构体变量
	NVIC_InitTypeDef NVIC_InitStruct;

	// 使能 GPIOB 和 AFIO 复用时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO, ENABLE);

	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12;
	// 设置 PA12 为上拉输入
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStruct);
	
	// PB12 中断线映射
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource12);
	// 外部中断模式
	EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
	// 中断线为 12
	EXTI_InitStruct.EXTI_Line = EXTI_Line12;
	// 下降沿触发
	EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
	// 中断线使能
	EXTI_InitStruct.EXTI_LineCmd = ENABLE;
	// 外部中断初始化
	EXTI_Init(&EXTI_InitStruct);
	
	// 如果配置了中断,那么必须同时配置优先级
	// 配置外部中断通道为 10~15,因为我们使用的是 12,通道定义可以在 stm32f10x.h 中查看
	NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn;
	// 抢占优先级为 0
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	// 子优先级为 1
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
	// 使能外部中断通道
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	// 中断优先级初始化
	NVIC_Init(&NVIC_InitStruct);
}

修改主函数

int main(void) {
	// 设置抢占优先级为 2 位,可以在 misc.c 中查看此函数和参数
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	delay_init();
	led_init();
	// usart_init(115200);
	key_init();
	while (1) {
		// key_scan();
	}
}

添加中断服务函数

// 注释掉主函数中循环里的按键扫描,因为接下来会用中断服务函数触发
// 中断服务函数,函数名是固定的,根据设置的通道在 startup_stm3210x_md.s 中查看对应的终端服务函数名
void EXTI15_10_IRQHandler(void) {
	// 判断中断线 12 是否被触发
	if (EXTI_GetITStatus(EXTI_Line12) != RESET) {
		// 消抖
		delay(1, 30);
		// 闪一下灯
		GPIO_ResetBits(GPIOC, GPIO_Pin_13);
		delay(1, 100);
		GPIO_SetBits(GPIOC, GPIO_Pin_13);
		delay(1, 100);
	}
	// 清除 EXTI_Line12 上的中断标志位
	EXTI_ClearITPendingBit(EXTI_Line12);
}

根据教程中断触发的应该比扫描好一点,比如响应更准确,长按只触发一次。然而我的测试是长按还是会反复触发,甚至导致程序崩溃停止运行。

TIM 定时器

STM32 中有 4 个定时器(TIM)每个 TIM 包含 4 个独立通道。我们使用通用定时器来:

  • 测量输入信号的脉宽(输入捕获)
  • 产生输出波形(输出比较)
  • 产生脉冲宽度调制波(PWM)

因此在使用 PWM 之前要先了解一下通用定时器的使用方法。

首先添加定时器固件库 stm32f10x_tim.c

初始化定时器

// 周期和频率经常需要改变,所以用变量来传递
void tim_init(uint16_t per, uint16_t psc) {
	// 定时器结构体变量
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
	// 中断优先级结构体变量
	NVIC_InitTypeDef NVIC_InitStruct;
	
	// 使能 TIM3 的时钟,定时器寄存器名可以在 stm32f10x_rcc.h 中查看
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

	// 自动重装载周期
	TIM_TimeBaseInitStruct.TIM_Period = per;
	// 设置预分频值(频率)
	TIM_TimeBaseInitStruct.TIM_Prescaler = psc;
	// 向上计数模式,其他模式可以在 stmstm32f10x_tim.h 中查看定义
	TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
	// 设置时钟分割
	TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
	// 定时器初始化函数
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
	// 使能 TIM3 中断,stmstm32f10x_tim.c 中查看实现
	// 第二个参数表示中断源,此为中断更新
	TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
	
	// 如果配置了中断,那么必须同时配置优先级
	NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStruct);
	
	// TIM3 使能
	TIM_Cmd(TIM3, ENABLE);
}

修改主函数

int main(void) {
	// 设置抢占优先级为 2 位,可以在 misc.c 中查看此函数和参数
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	delay_init();
	led_init();
	// 定时器初始化(周期,分频)
	// 定时 1s = (9999 + 1)*(7199 + 1)/72MHz
	tim_init(9999, 7199);
	while (1) {
	}
}
  • 由于我们的使用定时器中断来执行功能,所以还得在初始化阶段设置抢占优先级之后,才能在 tim_init() 中设置中断的抢占优先级和子优先级
  • 主循环中依然不需要任何代码,下面的中断服务程序会编写我们要做的操作

定时器中断服务函数

void TIM3_IRQHandler(void) {
	if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
		// 直接对寄存器位操作使 PC13 电平翻转
		GPIOC->ODR ^= GPIO_Pin_13;
		// 依然需要清除中断标记
		TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
	}
}

PWM 输出

现在我们使用 “输出比较” 功能实现 PWM 输出功能。但是 PC13 是无法输出 PWM 信号的,因此我们需要查看对应的 开发手册 中的 “引脚定义” 章节,查看引脚功能表中哪个引脚复用了 TIM3 定时器:

引脚功能表

可以看到 PA6 PA7 PB0 PB1 分别对应定时器 TIM3 的四个通道。因此我们可以使用 PA6 来实现 PWM 呼吸灯的功能。

初始化函数

void breath_light_tim_init(uint16_t per, uint16_t psc) {
	GPIO_InitTypeDef GPIO_InitStruct;
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
	// 输出比较结构体变量
	TIM_OCInitTypeDef TIM_OCInitStruct;
	
	// 使能 GPIOA
	// 因为 TIM3 是 PA6 的复用功能,所以需要开启 AFIO 复用时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO, ENABLE);
	// 使能 TIM3 的时钟,定时器寄存器名可以在 stm32f10x_rcc.h 中查看
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
	// 设置 PA6 为推挽输出
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStruct);
	
	TIM_TimeBaseInitStruct.TIM_Period = per;
	TIM_TimeBaseInitStruct.TIM_Prescaler = psc;
	TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
	// 时钟分割依然是默认的 1 分割
	TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
	// 这次没有用到中断,所以不需要使能中断
	// TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
	
	// PWM 模式 1,其他模式可以在 stm32f10x_tim.h 中查看
	TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
	// 初始化占空比为 0
	TIM_OCInitStruct.TIM_Pulse = 0;
	// 输出比较极性低,stm32f10x_tim.h
	TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_Low;
	// 比较输出使能
	TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
	// 初始化在 stm32f10x_rcc.h 中找到定义
	// 因为我们使用的是 PA6 其复用的是 TIM3 的通道 1 CH1 所以我们使用 OC1 来进行初始化
	TIM_OC1Init(TIM3, &TIM_OCInitStruct);
	
	// TIM3 使能
	TIM_Cmd(TIM3, ENABLE);
}

修改主函数

int main(void) {
	uint8_t flag = 0;
	uint16_t pwmValue = 0;
	// 没有用到中断这里也不需要设置分组
	// NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	delay_init();
	led_init();
	// (899+1)*(0+1)/72MHz = 1/(8e4)s,也就是说频率为 80KHz
	breath_light_tim_init(899, 0);
	while (1) {
		// key_scan();
		if (flag == 0) {
			pwmValue++;
			if (pwmValue > 350) flag = 1;
		}
		if (flag == 1) {
			pwmValue--;
			if (pwmValue == 0) flag = 0;
		}
		TIM_SetCompare1(TIM3, pwmValue);
		delay(1, 10);
	}
}

参考视频