定时器练习报告
任务分析:
本任务要求设计并实现一个"智能报警节点"。该节点平时处于静默状态,通过串口 DMA 持续上报系统状态;当接收到上位机指令时,需驱动蜂鸣器发出指定频率的报警音,并能根据指令动态调节音量。
- 报警输出:无源蜂鸣器
- 通信接口:USART,波特率 115200
目标功能
- 蜂鸣器底层驱动
功能描述:配置定时器产生PWM波形驱动无源蜂鸣器
具体数据:- 频率要求:蜂鸣器发声的频率应该保持在2Khz
- 分辨率要求:为了保证音量调节细腻,要求 PWM 的自动重装载值 (ARR) 设定为 499(即周期计数值为 500)
- 默认状态:上电初始化后,蜂鸣器应处于静音状态(占空比 0%)
- 通信协议解析与控制
功能描述: 通过串口中断接收上位机指令,解析指令并控制蜂鸣器音量
通行协议:- 指令格式:VOL:xxx (定长 7 字节字符)
- VOL: 为固定的指令头
- xxx 为 3 位数字,代表音量百分比 (000 - 100)
具体指标:
- 必须使用串口接收中断处理数据
- 程序需具备基本的容错性(例如:若收到 VOL:200,应自动限幅为 100%)
- DMA状态上报
功能描述:为了监控设备运行状态,系统需自动周期性地上报数据,且不能阻塞 CPU
具体指标:- 定时要求:配置 TIM2 产生频率为 2Hz (即每 0.5 秒一次) 的定时中断
- 数据内容:每次中断触发时,向串口发送当前系统状态字符串,格式如下: "STATUS:RUN, VOL:xx\r\n"(其中 xx 为当前设置的音量值)
- 传输方式:必须使用 DMA 模式发送数据,严禁在定时器中断中使用阻塞式发送
程序设计
本次开发所使用的开发版主控芯片为GD32F407,程序中使用的是标准库开发
蜂鸣器模块
根据扩展版原理图可知蜂鸣器对应的引脚为PB9
串口选择
查询GD32F4的DM功能表后最终选择串口0进行数据的收发操作,其中串口0的输入断端对应的时是DMA1的通道1,串口0的输出端对应DMA1的通道7
查询串口引脚图可得串口0对应接收引脚为PA10,发送引脚为PA9
代码编写
本次所使用的开发环境为vscode+keil,即使用Keil5进行项目配置,在vscode中通过EIDE插件进行代码编写和程序烧录
蜂鸣器驱动代码
蜂鸣器主要需要实现的功能函数有:初始化蜂鸣器,修改蜂鸣器输出音量(即修改对应定时器占空比),停止发声
- 蜂鸣器所使用的定时器以及通道为TIMER1的通道一,时钟配置代码如下
cpp
static void timer_init_config(uint16_t t_perscaler, uint32_t t_period)
{
timer_parameter_struct initpara;
timer_struct_para_init(&initpara);
// 配置预分频系数, 可以实现更小的Timer频率
initpara.prescaler = t_perscaler - 1;
//配置时钟周期
initpara.period = t_period - 1;
timer_init(TIMER_PERIPH, &initpara);
timer_enable(TIMER_PERIPH);
}
当前函数通过用户传入的形参:t_perscaler(预分频系数)和t_period(周期)对定时器进行了初步配置,主要作用为初始化并配置定时器的基本参数,包括预分频系数和自动重装载周期值
- 引脚配置代码如下
cpp
static void timer_gpio_config(rcu_periph_enum rcu, uint32_t port, uint32_t pin, uint32_t alt_func_num)
{
// GPIO ----------------------------------------
//初始化定时器时钟
rcu_periph_clock_enable(rcu);
//设置对应引脚复用
gpio_mode_set(port, GPIO_MODE_AF, GPIO_PUPD_NONE, pin);
//输出参数配置(推挽输出模式)
gpio_output_options_set(port, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, pin);
// 复用设置
gpio_af_set(port, alt_func_num, pin);
}
该函数用于初始化并配置GPIO引脚,使其作为定时器外设的复用功能引脚
- 蜂鸣器初始化:
c
void Buzzer_init()
{
// gpio初始化
timer_gpio_config(RCU_GPIOB, GPIOB, GPIO_PIN_9, GPIO_AF_1);
// timer初始化
rcu_periph_clock_enable(TIMER_RCU);
/* deinit a TIMER */
timer_deinit(TIMER_PERIPH);
/* 升级所有Timer相关的时钟频率 */
rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4);
timer_init_config(FIXED_PSC, FIXED_ARR + 1);
// 输出通道配置 ---------------------------------
timer_oc_parameter_struct ocpara;
/* 初始化结构体参数 initialize TIMER channel output parameter struct */
timer_channel_output_struct_para_init(&ocpara);
/* 启用TIMER3_CH2的正极(OP) */
ocpara.outputstate = TIMER_CCX_ENABLE;
/* 配置通道输出参数 configure TIMER channel output function */
timer_channel_output_config(TIMER_PERIPH, TIMER_CH, &ocpara);
/* 配置通道输出比较模式 configure TIMER channel output compare mode */
timer_channel_output_mode_config(TIMER_PERIPH, TIMER_CH, TIMER_OC_MODE_PWM0);
/* 配置通道输出的脉冲值(占空比)configure TIMER channel output pulse value */
timer_channel_output_pulse_value_config(TIMER_PERIPH, TIMER_CH, 0);
/* enable a TIMER */
timer_disable(TIMER_PERIPH);
}
当前代码用于初始化蜂鸣器的硬件控制链路,主要分三步走,分别是配置GPIO引脚(将物理引脚映射到定时器PWM输出通道), 配置定时器基础时钟(设定PWM信号输出的频率),以及配置PWm输出通道(设定工作模式与初始占空比)
- 音量控制函数
c
void Buzzer_set_volume(uint8_t volume_percent)
{
if (volume_percent == 0)
{
timer_channel_output_pulse_value_config(TIMER_PERIPH, TIMER_CH, 0);
timer_disable(TIMER_PERIPH);
}
else
{
timer_enable(TIMER_PERIPH);
uint16_t max_effective_pulse = FIXED_ARR * 0.5;
// 公式:Pulse = (volume_percent / 100.0) * (FIXED_ARR + 1)
uint16_t pulse = (uint16_t)((volume_percent / 100.0) * max_effective_pulse);
if (pulse > FIXED_ARR)
{
pulse = FIXED_ARR;
}
timer_channel_output_pulse_value_config(TIMER_PERIPH, TIMER_CH, pulse);
}
}
当前函数作用为通过控制PWm信号的占比来控制蜂鸣器音量的大小,但是其中需要注意的点为蜂鸣器的最佳发声区间的PWM信号占空比大约在50%到70%左右,因此在函数中设置最大占空比为50%
- 蜂鸣器停止发声函数
c
/停止蜂鸣器发声
void Buzzer_stop()
{
//设置占空比为0
timer_channel_output_pulse_value_config(TIMER_PERIPH, TIMER_CH, 0);
//禁用定时器输出
timer_disable(TIMER_PERIPH);
}
当前函数作用为控制蜂鸣器停止发声,两句代码分别实现了将PWM信号输出占空比设置为0以及关闭定时器的操作
串口驱动代码
本次串口使用的是串口0进行数据收发,数据发送的方向为从内存到外设,数据接收的方向为从外设到内存
- 串口输出配置函数
c
static void USART0_DMA_TX_config(){
// 数据的发送,搬运方向:内存 -> 外设
// SRC: memory0_addr
// DST: periph_addr
// RCU ---------------------------------------------
rcu_periph_clock_enable(USART0_TX_DMA_RCU);
// DMA ---------------------------------------------
// 重置DMA和通道
dma_deinit(USART0_TX_DMA_PERIPH_CH);
dma_single_data_parameter_struct init_struct;
/* 初始化结构体参数 */
dma_single_data_para_struct_init(&init_struct);
// 配置拷贝方向
init_struct.direction = DMA_MEMORY_TO_PERIPH;
// 配置源头地址
init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
// 配置目标地址
init_struct.periph_addr = USART0_DATA_ADDR;
init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
// 数据的个数,宽度(每个数据的BIT数)
init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;
// 循环模式(关闭)
init_struct.circular_mode = DMA_CIRCULAR_MODE_DISABLE;
// 优先级
init_struct.priority = DMA_PRIORITY_LOW;
// 执行DMA的初始化
dma_single_data_mode_init(USART0_TX_DMA_PERIPH_CH, &init_struct);
// 设置通道子集
dma_channel_subperipheral_select(USART0_TX_DMA_PERIPH_CH, DMA_SUBPERI4);
}
当前函数的作用为初始化DMA通道,将其配置为从内存SART0发送数据寄(SRAM)到U存器(外设)的单次数据传输链路,以实现一下功能
- 自动搬运数据:将指定内存区域的数据自动传输至USART0的发送寄存器
- 减少CPU资源占用:数据传输过程无需CPU干预,适合高速或大数据量串口通信场景
- 性能提升:通过DMA控制器直接操作外设,避免传统中断发送的频繁上下文切换
该函数内部主要配置了数据的传输方向,数据传输的起始地址,数据的宽度与长度
- 串口输入配置函数
c
static void USART0_DMA_RX_config()
{
// 数据的发送,搬运方向:外设 -> 内存
// 传输:USART0外设 -> 固定内存
// DMA1,CH2 子集100 -> USART0_RX
// SRC: periph_addr :USART0_DATA_ADDR
// DST: memory0_addr:g_rx_buffer
// RCU ---------------------------------------------
rcu_periph_clock_enable(USART0_RX_DMA_RCU);
// DMA ---------------------------------------------
// 重置DMA和通道
dma_deinit(USART0_RX_DMA_PERIPH_CH);
dma_single_data_parameter_struct init_struct;
// 初始化结构体参数
dma_single_data_para_struct_init(&init_struct);
// 配置拷贝方向
init_struct.direction = DMA_PERIPH_TO_MEMORY;
// 配置源头地址
init_struct.periph_addr = USART0_DATA_ADDR;
init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
// 配置目标地址
init_struct.memory0_addr = (uint32_t)g_rx_buffer;
init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
// 数据的个数,宽度(每个数据的BIT数)
init_struct.number = RX_BUFFER_LEN;
init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;
// 循环模式(关闭)
init_struct.circular_mode = DMA_CIRCULAR_MODE_DISABLE;
// 优先级
init_struct.priority = DMA_PRIORITY_LOW;
// 执行DMA的初始化
dma_single_data_mode_init(USART0_RX_DMA_PERIPH_CH, &init_struct);
// 设置通道子集
dma_channel_subperipheral_select(USART0_RX_DMA_PERIPH_CH, DMA_SUBPERI4);
// 启动用DMA接收
dma_channel_enable(USART0_RX_DMA_PERIPH_CH);
}
当前函数作用为配置USART0串口的DMA接收功能,实现外设到内存的自动数据传输,配置过程与串口输出函数类似
- 串口初始化函数
c
void USART0_init() {
// 初始化DMA TX 发送---------------------------------------
USART0_DMA_TX_config();
// 初始化DMA RX 接收---------------------------------------
USART0_DMA_RX_config();
// GPIO 初始化 -----------------------------------------TX PB6, RX PB7 -> AF
// 启用GPIO时钟
rcu_periph_clock_enable(USART0_TX_RCU);
// 引脚模式配置
gpio_mode_set(USART0_TX_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, USART0_TX_PIN);
// 设置输出选项
gpio_output_options_set(USART0_TX_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, USART0_TX_PIN);
// 设置AF复用功能
gpio_af_set(USART0_TX_PORT, GPIO_AF_7, USART0_TX_PIN);
rcu_periph_clock_enable(USART0_RX_RCU);
// 引脚模式配置
gpio_mode_set(USART0_RX_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, USART0_RX_PIN);
// 设置输出选项
gpio_output_options_set(USART0_RX_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, USART0_RX_PIN);
// 设置AF复用功能
gpio_af_set(USART0_RX_PORT, GPIO_AF_7, USART0_RX_PIN);
// USART 初始化 ----------------------------------------
// 启用USART时钟
rcu_periph_clock_enable(RCU_USART0);
// 重置
usart_deinit(USART0);
// 配置串口参数:波特率*,数据位,校验位,停止位
/* configure usart baud rate value 波特率 */
usart_baudrate_set(USART0, USART0_BAUDRATE);
/* configure usart word length 数据位长度 */
usart_word_length_set(USART0, USART_WL_8BIT);
/* configure usart parity function 校验位 */
usart_parity_config(USART0, USART_PM_NONE);
/* configure usart stop bit length 停止位*/
usart_stop_bit_set(USART0, USART_STB_1BIT);
/* 先发送高位还是低位 LSB低位先发*/
usart_data_first_config(USART0, USART_MSBF_LSB);
/* configure USART transmitter 启用发送功能 */
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
/* configure USART receiver 启用接收功能 */
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
/* 启用DMA发送 configure USART DMA for transmission */
usart_dma_transmit_config(USART0, USART_TRANSMIT_DMA_ENABLE);
/* 启用DMA接收 configure USART DMA for reception */
usart_dma_receive_config(USART0, USART_RECEIVE_DMA_ENABLE);
/* 配置中断优先级 */
nvic_irq_enable(USART0_IRQn, 2, 2);
/* 启用串口接收中断 */
// 接收缓冲区不为空中断
usart_interrupt_enable(USART0, USART_INT_RBNE);
// 串口空闲中断
usart_interrupt_enable(USART0, USART_INT_IDLE);
/* enable usart 启用*/
usart_enable(USART0);
}
该函数的作用为对USART0串口进行总体配置,主要通过以下四步实现了构建一个通过DMA进行数据收发的全双工通信链路
- DMA通道配置: 内存与串口间的零CPU干预数据传输
- GPIO复用配置 → 物理引脚映射到串口功能
- USART参数设置 → 定义通信协议
- 中断系统配置 → 实现数据帧检测与异常处理
- 串口0中断函数
c
void USART0_IRQHandler(){
if(SET == usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)){
// 只能通过再次接收一个数据,来清理标记
usart_data_receive(USART0); // 必须读一次数据,读出来的数据不可用
/* 获取剩余未传输的数据数量 get the number of remaining data to be transferred by the DMA */
uint32_t remaining_cnt = dma_transfer_number_get(USART0_RX_DMA_PERIPH_CH);
//printf("remaining_cnt: %d\n", remaining_cnt);
g_rx_cnt = RX_BUFFER_LEN - remaining_cnt;
#if USART0_RECV_CALLBACK
// 添加字符串结束标记,避免打印字符串出错
g_rx_buffer[g_rx_cnt] = '\0';
USART0_on_recv(g_rx_buffer, g_rx_cnt);
#endif
// 停止搬运
dma_channel_disable(USART0_RX_DMA_PERIPH_CH);
// 清除标记FTF
dma_flag_clear(USART0_RX_DMA_PERIPH_CH, DMA_FLAG_FTF);
// 重启搬运
dma_channel_enable(USART0_RX_DMA_PERIPH_CH);
}
}
当前中断函数的作用为精确读取用户输入的字符串并对其进行处理,此函数中存在一个细节就是GD32的空闲标志清除需要 读取数据寄存器(读出的数据无效),而stm32则可以直接通过状态寄存器去除
定时器配置
本次采用TIMER3定时器以实现每0.5秒输出一次音量信息到上位机,因此需要将TIMER3定时器的频率配置为2HZ,每次在中断函数中进行信息输出操作,TIMER3的定时器配置如下
c
// 启用TIMER3的情况下,才需要配置如下参数
#define TM3_PRESCALER 10000
#define TM3_FREQ 2
#define TM3_PERIOD (SystemCoreClock / TM3_PRESCALER / TM3_FREQ)
在中断函数中TIMER3则会输出当前的音量信息,中断处理函数代码如下
c
// TIMER3中断服务函数
void TIMER3_IRQHandler(void)
{
// 检查是否是更新中断
if (timer_interrupt_flag_get(TIMER3, TIMER_INT_FLAG_UP) != RESET)
{
// 清除中断标志
timer_interrupt_flag_clear(TIMER3, TIMER_INT_FLAG_UP);
// 输出当前音量信息
if (voluem != -1)
{
if (voluem > 0)
{
printf("STATUS:RUN,VOL:%d\r\n", voluem);
}
else if (voluem == 0)
{
printf("STATUS:STOP,VOL:%d\r\n", voluem);
}
}
}
}
程序中的音量变量voluem是一个定义在USART0.h中的全局变量,在mian.c、TIMER.c以及USART.c中被共享,每次用户输入音量控制信息后串口会从中提取出相应的音量信息并写入voluem变量中以供其它文件调用,每次当用户输入信息后串口对应的回调函数就会回显相关信息,并更新音量变量中的信息,回调函数内容如下
c
void USART0_on_recv(uint8_t *buffer, uint32_t len)
{
voluem = Get_vloume(buffer, len);
printf("recv[%d]-> %s\n", len, buffer);
if (voluem >= 0)
{
Buzzer_set_volume(voluem);
}
}
总结
整体程序的运行逻辑为,上电后TIMER3每0.5秒触发一次中断,中断每次触发都会执行输出当前蜂鸣器音量的操作,用户可以通过上位机与单片机进行串口通信,用户可以输入指定格式的信息控制蜂鸣器的音量大小,每次串口接收到信息都会触发中断,在中断函数中会从用户输入的信息中提取相应的音量信息,随后便会自动调用回调函数,在回调函数中回显输入信息并更新音量信息并更新TIMER1输出PWM信息的占空比,以此改变输出的音量信息