先贴出代码部分:
//main.c #include "stm32f10x.h" #include "Serial.h" #include "AD.h" #include "Timer.h" #include "wav.h" // 包含WAV生成头文件 #include <stdio.h> #include <stdint.h> #define BUFFER_SIZE 4096 // 双缓冲区大小 #define TOTAL_SAMPLES 8000 // 1秒的8kHz音频 extern volatile uint16_t adc_buffer[BUFFER_SIZE]; extern volatile uint8_t buffer_half_ready; extern volatile uint32_t sample_count; extern void ADC_DMA_Init(void); extern void Start_ADC_Conversion(void); //启动ADC转换 extern void Delay_ms(uint32_t xms); /** * 函 数:处理ADC数据(现在直接发送WAV格式) * 参 数:buffer_index 0=上半区, 1=下半区 * 返 回 值:无 */ void Process_ADC_Data(uint8_t buffer_index) { uint32_t start_index = buffer_index * (BUFFER_SIZE / 2); uint32_t samples_to_process = BUFFER_SIZE / 2; // 处理半区数据 for (uint32_t i = 0; i < samples_to_process; i++) { if (sample_count < TOTAL_SAMPLES) // 只采集8000个样本(1秒) { uint16_t adc_value = adc_buffer[start_index + i]; // 直接发送WAV格式的PCM数据 WAV_SendData(adc_value); sample_count++; } } } int main(void) { // 系统初始化 SystemInit(); // 初始化串口 Serial_Init(); // 重要提示:先发送WAV头,再开始采集! WAV_SendHeader(TOTAL_SAMPLES); // 发送44字节WAV头 // 初始化ADC和DMA ADC_DMA_Init(); // 初始化定时器 Timer_Init(); // 启动ADC转换 Start_ADC_Conversion(); // 延时确保ADC稳定 Delay_ms(100); // 启动定时器(最后启动,确保所有配置完成) TIM_Cmd(TIM3, ENABLE); // 主循环 while (1) { // 检查上半区是否就绪 if (buffer_half_ready == 0) { Process_ADC_Data(0); // 处理上半区数据 buffer_half_ready = 2; // 重置标志 } // 检查下半区是否就绪 if (buffer_half_ready == 1) { Process_ADC_Data(1); // 处理下半区数据 buffer_half_ready = 2; // 重置标志 } // 检查是否达到目标样本数 if (sample_count >= TOTAL_SAMPLES) { // 停止定时器和ADC TIM_Cmd(TIM3, DISABLE); break; } // 简单延时避免CPU占用过高 for (volatile uint32_t i = 0; i < 1000; i++); } // 无限循环 while (1); }//AD.c #include "stm32f10x.h" #include "AD.h" #include "Serial.h" // 包含串口头文件 #define BUFFER_SIZE 4096 // 双缓冲区大小,每半区8个样本 volatile uint16_t adc_buffer[BUFFER_SIZE]; // 双缓冲数组 volatile uint8_t buffer_half_ready = 2; // 2=未就绪, 0=上半区就绪, 1=下半区就绪 volatile uint32_t sample_count = 0; // 采样计数器 /** * 函 数:ADC和DMA初始化 * 参 数:无 * 返 回 值:无 */ void ADC_DMA_Init(void) { /*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); /*设置ADC时钟*/ RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 72MHz/6 = 12MHz /*GPIO初始化*/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // PA0接电位器 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /*规则组通道配置*/ ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); /*ADC初始化*/ ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; // TIM3 TRGO触发 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换 ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); /*使能ADC的DMA请求*/ ADC_DMACmd(ADC1, ENABLE); /*ADC使能*/ ADC_Cmd(ADC1, ENABLE); /*ADC校准*/ ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1) == SET); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1) == SET); /*DMA初始化 - 配置为ADC到内存的传输*/ DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // ADC数据寄存器地址 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buffer; // 内存缓冲区地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设到内存 DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE; // 缓冲区大小 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 16位数据 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 16位数据 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); /*使能DMA半传输和传输完成中断*/ DMA_ITConfig(DMA1_Channel1, DMA_IT_HT | DMA_IT_TC, ENABLE); /*NVIC配置DMA中断*/ NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); /*使能DMA通道*/ DMA_Cmd(DMA1_Channel1, ENABLE); } /** * 函 数:启动ADC转换 * 参 数:无 * 返 回 值:无 * 说明:即使配置为外部触发,也需要调用此函数使能转换 */ void Start_ADC_Conversion(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); } /** * 函 数:DMA1通道1中断服务函数 * 参 数:无 * 返 回 值:无 */ void DMA1_Channel1_IRQHandler(void) { /*半传输中断 - 上半区数据就绪*/ if (DMA_GetITStatus(DMA1_IT_HT1) != RESET) { DMA_ClearITPendingBit(DMA1_IT_HT1); buffer_half_ready = 0; // 上半区就绪 } /*传输完成中断 - 下半区数据就绪*/ if (DMA_GetITStatus(DMA1_IT_TC1) != RESET) { DMA_ClearITPendingBit(DMA1_IT_TC1); buffer_half_ready = 1; // 下半区就绪 } } uint16_t AD_GetValue(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); return ADC_GetConversionValue(ADC1); } //AD.h #ifndef __AD_H #define __AD_H #include "stm32f10x.h" #define BUFFER_SIZE 4096 // 双缓冲区大小,每半区8个样本 extern volatile uint16_t adc_buffer[BUFFER_SIZE]; // 双缓冲数组 extern volatile uint8_t buffer_half_ready; // 0=上半区就绪, 1=下半区就绪, 2=未就绪 extern volatile uint32_t sample_count; // 采样计数器 void ADC_DMA_Init(void); void Start_ADC_Conversion(void); // 新增:启动ADC转换 #endif//Timer.c #include "stm32f10x.h" #include "Timer.h" #include "Serial.h" /** * 函 数:定时器初始化 * 参 数:无 * 返 回 值:无 */ void Timer_Init(void) { /*开启时钟*/ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); /*配置时钟源*/ TIM_InternalClockConfig(TIM3); /*计算定时器参数 - 8kHz采样率*/ /* * 系统时钟72MHz * 目标频率8kHz * 预分频值 = (72000000 / 8000) - 1 = 9000 - 1 = 8999 * 自动重载值 = 0(每次计数到8999+1=9000时产生更新事件) */ /*时基单元初始化*/ TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStructure.TIM_Period = 0; // 自动重载值 = 0 TIM_TimeBaseInitStructure.TIM_Prescaler = 8999; // 预分频 = 72000000/9000 = 8000Hz TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); /*重要:先完成所有配置,再配置TRGO输出*/ TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); /*使能定时器 - 注意:在ADC配置完成后才使能*/ // TIM_Cmd(TIM3, ENABLE); // 暂时不使能,等ADC准备好后再使能 } //Timer.h #ifndef __TIMER_H #define __TIMER_H #include "stm32f10x.h" void Timer_Init(void); #endif//Serial.c #include "stm32f10x.h" // Device header #include <stdio.h> #include <stdarg.h> #include <stdint.h> char Serial_RxPacket[100]; //定义接收数据包数组,数据包格式"@MSG\r\n" uint8_t Serial_RxFlag; //定义接收数据包标志位 /** * 函 数:串口初始化 * 参 数:无 * 返 回 值:无 */ void Serial_Init(void) { /*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 /*GPIO初始化*/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA10引脚初始化为上拉输入 /*USART初始化*/ USART_InitTypeDef USART_InitStructure; //定义结构体变量 USART_InitStructure.USART_BaudRate = 115200; //波特率 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要 USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择 USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要 USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位 USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位 USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1 /*中断输出配置*/ USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断 /*NVIC中断分组*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2 /*NVIC配置*/ NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1 NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设 /*USART使能*/ USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行 } /** * 函 数:串口发送一个字节 * 参 数:Byte 要发送的一个字节 * 返 回 值:无 */ void Serial_SendByte(uint8_t Byte) { USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形 while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成 /*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/ } /** * 函 数:串口发送一个数组 * 参 数:Array 要发送数组的首地址 * 参 数:Length 要发送数组的长度 * 返 回 值:无 */ void Serial_SendArray(uint8_t *Array, uint16_t Length) { uint16_t i; for (i = 0; i < Length; i ++) //遍历数组 { Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据 } } /** * 函 数:串口发送一个字符串 * 参 数:String 要发送字符串的首地址 * 返 回 值:无 */ void Serial_SendString(char *String) { uint8_t i; for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止 { Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据 } } /** * 函 数:次方函数(内部使用) * 返 回 值:返回值等于X的Y次方 */ uint32_t Serial_Pow(uint32_t X, uint32_t Y) { uint32_t Result = 1; //设置结果初值为1 while (Y --) //执行Y次 { Result *= X; //将X累乘到结果 } return Result; } /** * 函 数:串口发送数字 * 参 数:Number 要发送的数字,范围:0~4294967295 * 参 数:Length 要发送数字的长度,范围:0~10 * 返 回 值:无 */ void Serial_SendNumber(uint32_t Number, uint8_t Length) { uint8_t i; for (i = 0; i < Length; i ++) //根据数字长度遍历数字的每一位 { Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次调用Serial_SendByte发送每位数字 } } /** * 函 数:使用printf需要重定向的底层函数 * 参 数:保持原始格式即可,无需变动 * 返 回 值:保持原始格式即可,无需变动 */ int fputc(int ch, FILE *f) { Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数 return ch; } /** * 函 数:自己封装的prinf函数 * 参 数:format 格式化字符串 * 参 数:... 可变的参数列表 * 返 回 值:无 */ void Serial_Printf(char *format, ...) { char String[100]; //定义字符数组 va_list arg; //定义可变参数列表数据类型的变量arg va_start(arg, format); //从format开始,接收参数列表到arg变量 vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中 va_end(arg); //结束变量arg Serial_SendString(String); //串口发送字符数组(字符串) } /** * 函 数:USART1中断函数 * 参 数:无 * 返 回 值:无 * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行 * 函数名为预留的指定名称,可以从启动文件复制 * 请确保函数名正确,不能有任何差异,否则中断函数将不能进入 */ void USART1_IRQHandler(void) { static uint8_t RxState = 0; //定义表示当前状态机状态的静态变量 static uint8_t pRxPacket = 0; //定义表示当前接收数据位置的静态变量 if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断 { uint8_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量 /*使用状态机的思路,依次处理数据包的不同部分*/ /*当前状态为0,接收数据包包头*/ if (RxState == 0) { if (RxData == '@' && Serial_RxFlag == 0) //如果数据确实是包头,并且上一个数据包已处理完毕 { RxState = 1; //置下一个状态 pRxPacket = 0; //数据包的位置归零 } } /*当前状态为1,接收数据包数据,同时判断是否接收到了第一个包尾*/ else if (RxState == 1) { if (RxData == '\r') //如果收到第一个包尾 { RxState = 2; //置下一个状态 } else //接收到了正常的数据 { Serial_RxPacket[pRxPacket] = RxData; //将数据存入数据包数组的指定位置 pRxPacket ++; //数据包的位置自增 } } /*当前状态为2,接收数据包第二个包尾*/ else if (RxState == 2) { if (RxData == '\n') //如果收到第二个包尾 { RxState = 0; //状态归0 Serial_RxPacket[pRxPacket] = '\0'; //将收到的字符数据包添加一个字符串结束标志 Serial_RxFlag = 1; //接收数据包标志位置1,成功接收一个数据包 } } USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位 } } //Serial.h #ifndef __SERIAL_H #define __SERIAL_H #include <stdio.h> extern char Serial_RxPacket[]; extern uint8_t Serial_RxFlag; void Serial_Init(void); void Serial_SendByte(uint8_t Byte); void Serial_SendArray(uint8_t *Array, uint16_t Length); void Serial_SendString(char *String); void Serial_SendNumber(uint32_t Number, uint8_t Length); void Serial_Printf(char *format, ...); #endif//WAV.c #include <string.h> #include <stdint.h> #include "wav.h" #include "Serial.h" // WAV文件头结构(44字节) typedef struct { char riff[4]; // "RIFF" uint32_t chunk_size; // 文件大小-8 char wave[4]; // "WAVE" char fmt[4]; // "fmt " uint32_t fmt_size; // 16 fmt块大小 uint16_t format; // 1=PCM 音频格式 uint16_t channels; // 1=单声道 uint32_t sample_rate; // 8000 采样率 uint32_t byte_rate; // 16000 字节率 uint16_t block_align; // 2 块对齐 uint16_t bits_per_sample; // 16 位深度 char data[4]; // "data" uint32_t data_size; // 音频数据大小 } __attribute__((packed)) WAVHeader; // 发送WAV文件头 void WAV_SendHeader(uint32_t total_samples) { WAVHeader header; // 填充标准WAV头 memcpy(header.riff, "RIFF", 4); memcpy(header.wave, "WAVE", 4); memcpy(header.fmt, "fmt ", 4); memcpy(header.data, "data", 4); header.fmt_size = 16; header.format = 1; // PCM格式 header.channels = 1; // 单声道 header.sample_rate = 8000; // 8kHz采样率 header.bits_per_sample = 16; // 16位深度 // 计算其他参数 header.block_align = header.channels * header.bits_per_sample / 8; // 2 header.byte_rate = header.sample_rate * header.block_align; // 16000 header.data_size = total_samples * header.block_align; // 数据大小 header.chunk_size = 36 + header.data_size; // 文件总大小-8 // 发送44字节的WAV头 Serial_SendArray((uint8_t*)&header, sizeof(WAVHeader)); } // 发送单个ADC样本(转换为16位PCM) void WAV_SendData(uint16_t adc_value) { // 转换公式:(ADC值 - 2048) * 128 int32_t sample = (int32_t)(adc_value - 2048) * 128; // 限幅到16位范围 if (sample < -32768) sample = -32768; if (sample > 32767) sample = 32767; // 转换为16位有符号整数 int16_t pcm_sample = (int16_t)sample; // 发送2字节(小端格式) uint8_t data[2]; data[0] = (uint8_t)(pcm_sample & 0xFF); // 低字节 data[1] = (uint8_t)((pcm_sample >> 8) & 0xFF); // 高字节 Serial_SendArray(data, 2); } //WAV.h #ifndef __WAV_H #define __WAV_H #include "stm32f10x.h" #include <stdint.h> void WAV_SendHeader(uint32_t total_samples); void WAV_SendData(uint16_t adc_value); #endif我做这个也是为了学习,大体包括以下知识点:
1.双缓冲数组的工作原理
2.项目架构(定时器其触发ADC、ADC触发DMA)
3.定时器触发ADC的各种方式(外部触发,软件触发,硬件触发)的区别
4.ADC转换的详细过程
5.DMA转运的详细过程
6.三个外设配置的要点
7.WAV文件格式和生成逻辑
一,整体架构为:定时器3 ---> 触发ADC1 ---> 触发DMA1 ---> 双缓冲 ---> 串口输出WAV
定时器3:产生紧缺的8kHz时钟信号
ADC1:再定时器触发下进行模数转换
DMA通道1:自动将ADC转换结果搬运到内存
双缓冲数组:实现数据处理和采集的并行
串口:输出WAV格式的音频数据
二,双缓冲机制
#define BUFFER_SIZE 4096
volatile uint16_t adc_buffer[BUFFER_SIZE]; // 4096个16位样本
volatile uint8_t buffer_half_ready = 2; // 2=未就绪, 0=上半区就绪, 1=下半区就绪
上半区:adc_buffer[0]到adc_buffer[2047](2048个样本)
下半区:adc_buffer[2048]到adc_buffer[4095](2048个样本)
工作过程:
时间(μs) | 事件 | DMA地址 | buffer_half_ready | 主循环动作 |
|---|---|---|---|---|
0 | 开始采集 | 指向adc_buffer[0] | 2 | 等待 |
125×2048=256ms | DMA半传输中断 | 指向adc_buffer[2048] | 0 | 检测到0,处理上半区 |
256ms+125μs | 开始处理数据 | - | 2(重置) | 处理[0-2047]样本 |
512ms | DMA传输完成中断 | 指向adc_buffer[0] | 1 | 检测到1,处理下半区 |
512ms+125μs | 开始处理数据 | - | 2(重置) | 处理[2048-4095]样本 |
768ms | DMA半传输中断 | 指向adc_buffer[2048] | 0 | 检测到0,处理上半区 |
... | ... | ... | ... | ... |
DMA循环模式:DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
半传输中断:当DMA传输到缓冲区中间时触发
传输完成终端:当DMA传输到缓冲区末尾时触发
标志变量:buffer_half_ready用于同步主循环和中断
三,定时器触发ADC的机制
触发方式 | 配置代码 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
软件触发 |
| 单次转换 | 简单 | 无法精确控制采样率 |
定时器TRGO |
| 高精度录音 | 精确同步 | 配置复杂 |
外部引脚 |
| 外部信号同步 | 灵活 | 需要外部信号 |
本项目使用的是:TIM3的TRGO触发
// ADC配置
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;
// TIM3配置
TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); // 在更新事件时产生TRGO
TRGO信号产生条件:
每次TIM3计数到自动重载值(ARR = 0)时
产生一个脉冲信号,该信号连接到ADC1的外部触发输入
8kHz采样率配置详解:
TIM_TimeBaseInitStructure.TIM_Period = 0; // ARR = 0
TIM_TimeBaseInitStructure.TIM_Prescaler = 8999; // PSC = 8999
系统时钟:72MHz
定时器时钟:72MHz(APB1没有分频)
计数频率:72MHz / (8999 + 1) = 8kHz
TRGO频率:每计数到ARR+1=1时产生一次,即8kHz
四,ADC转换过程详解
- 触发条件:TIM3的TRGO信号上升沿
- 采样阶段:
- 模拟开关连接PA0到采样电容
- 采样时间:
ADC_SampleTime_55Cycles5= 55.5个ADC时钟周期 - ADC时钟:12MHz (72MHz/6)
- 采样时间:55.5/12MHz = 4.625μs
- 转换阶段:
- 12位逐次逼近转换
- 转换时间:12.5个ADC时钟周期 = 1.04μs
- 结果存储:
- 转换结果存入ADC1->DR寄存器
- 设置EOC(转换结束)标志
- 触发DMA请求
// 采样时间选择
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
// 外部触发配置
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换模式
// DMA请求使能
ADC_DMACmd(ADC1, ENABLE);
五,DMA转运机制详解
- 触发条件:ADC转换完成(EOC标志置位)
- 数据搬运:
- 从源地址:
&ADC1->DR(ADC数据寄存器) - 到目的地址:
adc_buffer(内存缓冲区) - 每次搬运16位数据(半字)
- 从源地址:
- 地址更新:
- 外设地址不变(ADC->DR固定)
- 内存地址自增(指向下一个缓冲区位置)
- 计数器递减:
- 每次搬运后,DMA计数器减1
- 计数器为0时,根据模式决定是否重载
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式
DMA_ITConfig(DMA1_Channel1, DMA_IT_HT | DMA_IT_TC, ENABLE); // 使能半传输和传输完成中断
- 循环模式:当计数器减到0时,自动重载为初始值,从头开始传输
- 半传输中断(HT):当传输了缓冲区一半数据时触发
- 传输完成中断(TC):当传输了整个缓冲区数据时触发
六,优先级分配
外设 | 抢占优先级 | 子优先级 | 原因 |
|---|---|---|---|
DMA1_Channel1 | 0 | 0 | 最关键:数据丢失会导致音频断续 |
TIM3 | 1 | 0 | 重要但可稍延迟 |
USART1 | 1 | 1 | 可以缓冲,不影响实时性 |
其他中断 | 2+ | - | 低优先级 |
七,WAV文件格式详解
WAV文件结构(44字接+音频数据)
偏移 大小 描述 值示例
0 4 RIFF标识 "RIFF"
4 4 文件大小-8 0x00003E2C (15916)
8 4 WAVE标识 "WAVE"
12 4 fmt 标识 "fmt "
16 4 fmt块大小 16 (0x00000010)
20 2 音频格式 1 (PCM)
22 2 声道数 1 (单声道)
24 4 采样率 8000 (0x00001F40)
28 4 字节率 16000 (0x00003E80)
32 2 块对齐 2 (1声道×16位/8)
34 2 位深度 16
36 4 data标识 "data"
40 4 音频数据大小 15872 (0x00003E00)
44 - 音频数据 (8000样本×2字节=16000字节)
八,总结
- 定时器精确触发:TIM3的TRGO输出
- ADC同步采样:外部触发+连续转换
- DMA零拷贝传输:循环模式+双缓冲
- 实时数据处理:中断与主循环协同
- 标准文件格式:WAV头生成+PCM数据转换
点击保存窗口,然后点击audio.wav进行播放