- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
使用AIR32的ADC, I2S 和 DMA 实现简单的语音录音和播放功能, 以及使用 ADPCM 编码提升录音时长. 使用的MCU型号为 AIR32F103CCT6. 如果用CBT6, 对应的音频数据数组大小需要相应减小. 。
加电后开始录音, 录音结束后循环播放 。
对中间每个环节的说明 。
首先是存储, MCU的内存有限, 如果不借助AT24C, MX25L这类外部存储, 只用内存存储的数据是有限的, AIR32F103CCT6 带 64K Byte内存, 如果按原始采样值存储, 录音时长为 。
使用AIR32的ADC, 配合定时器实现精确的每秒8K, 11K和16K采样. AIR32的ADC分辨率和STM32F103一样都是固定的12bit(STM32F4之后才可以用寄存器调节分辨率) 。
音频采集设备如果直接用驻极体话筒, 采样的信号很弱(不是没有, 但是非常小), 需要加一个三极管做放大. 也可以买成品的 MAX9814 模块. 两者的效果区别不大, 但是在调试阶段, 建议用 MAX9814, 因为不用担心信号是否过饱和和失真问题, 在调通之后, 再换回低成本的驻极体话筒和三极管. 。
驻极体话筒放大的电路和元件参数可以参考这一篇 https://www.cnblogs.com/milton/p/15315783.html 。
播放可以使用PWM转DAC, 也可以直接用I2S. 。
I2S模块可以用 MAX98357A 模块, 自带I2S解码和放大可以直连喇叭, 也可以买PT8211/TM8211/GH8211, 0.3元一片非常便宜还是双声道, 缺点是不带功放, 如果直连喇叭得贴着耳朵才能听到, 可以再加一个LM386或者PAM8403做放大, 都非常便宜. 。
接线 。
* AIR32F103 MAX98357A / PT8211
* PB13(SPI1_SCK/I2S_CK) -> BCLK, BCK
* PB15(SPI1_MOSI/I2S_SD) -> DIN
* PB12(SPI1_NSS/I2S_WS) -> LRC, WS
* GND -> GND
* VIN -> 3.3V
* + -> speaker
* - -> speaker
*
* AIR32F103 MAX9814
* PA2 -> Out
* 3.3V -> VDD
* GND -> GND
* GND -> A/R
* GAIN -> float:60dB, gnd:50dB, 3.3v:40dB
完整的示例代码 。
定义了全局变量 。
// 定义不同的AUDIO_FREQ值, 可以切换不同的采样频率, 8K, 11K, 16K, 越高的采样频率, 音质越好, 录音时长越短
#define AUDIO_FREQ 8000
//#define AUDIO_FREQ 11000
//#define AUDIO_FREQ 16000
// 定义存储的音频数据大小, CCT6用的是30000, CBT6 或 RPT6 可以相应的减小或增大
#define BUFF_SIZE 30000
// 音频数据数组, 同时用于DMA的接收地址
uint16_t dma_buf[BUFF_SIZE];
// I2S传输时, 用于记录传输的位置
uint32_t index;
// I2S传输时, 用于区分左右声道
__IO uint8_t lr = 0;
初始化GPIO, PA2是采样输入, PB12, PB13, PB15 用于I2S传输, PC13 是板载的LED, 用于指示录音开始和结束. 如果使用的不是Bluepill而是合宙的开发板, 可以修改为开发板对应的LED GPIO. 。
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// PA2 as analog input
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// PB12,PB13,PB15 as I2S AF output
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// PC13 as GPIO output
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
}
初始化ADC, 设置为外部触发模式, 这里使用TIM3的Update中断作为触发源, 初始化之后ADC并不会立即开始转换, 而是在TIM3的每次Update中断时进行转换. 所以如果要停止ADC, 需要先停掉TIM3 。
void ADC_Configuration(void)
{
ADC_InitTypeDef ADC_InitStructure;
// Reset ADC1
ADC_DeInit(ADC1);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
// 设置 TIM3 为外置触发源
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;
// 结果右对齐
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
// 只使用一个通道
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
// PA2对应的channel是 ADC_Channel_2
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 1, ADC_SampleTime_239Cycles5);
// 启用ADC1的外部触发源
ADC_ExternalTrigConvCmd(ADC1, ENABLE);
// 在 ADC1 上启用 DMA
ADC_DMACmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE);
// 校准
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1));
}
初始化DMA, 用 ADC1->DR 作为外设地址, dma_buf作为内存地址, 内存地址递增, 数据大小为16bit, 循环填充. 同时打开DMA的填充完成中断 DMA_IT_TC 。
//调用
DMA_Configuration(DMA1_Channel1, (uint32_t)&ADC1->DR, (uint32_t)dma_buf, BUFF_SIZE);
// 函数实现
void DMA_Configuration(DMA_Channel_TypeDef *DMA_CHx, uint32_t ppadr, uint32_t memadr, uint16_t bufsize)
{
DMA_InitTypeDef DMA_InitStructure;
DMA_DeInit(DMA_CHx);
DMA_InitStructure.DMA_PeripheralBaseAddr = ppadr;
DMA_InitStructure.DMA_MemoryBaseAddr = memadr;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = bufsize;
// Addresss increase - peripheral:no, memory:yes
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
// Data unit size: 16bit
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_Medium;
// Memory to memory: no
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA_CHx, &DMA_InitStructure);
// Enable 'Transfer complete' interrupt
DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
// Enable DMA
DMA_Cmd(DMA_CHx, ENABLE);
}
打开外设的中断控制, DMA用于转换结束, SPI2的中断用于每次的数据发送 。
void NVIC_Configuration(void)
{
// DMA1 interrupts
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// SPI2 interrupts
NVIC_InitStructure.NVIC_IRQChannel = SPI2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
初始化定时器TIM3, 根据MCU频率72MHz, 计算得到分别在8K, 11K, 16K时的定时器周期和预分频系数. 启用计时器的Update中断, 但是不启动定时器, 因为启动后就会产生中断, 就会触发ADC转换. 需要将计时器的启动放到main()中. 。
void TIM_Configuration(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 9 - 1;
#if AUDIO_FREQ == 8000
// Period = 72,000,000 / 8,000 = 1000 * 9
TIM_TimeBaseStructure.TIM_Prescaler = 1000 - 1;
#elif AUDIO_FREQ == 11000
// Period = 72,000,000 / 11,000 = 727 * 9
TIM_TimeBaseStructure.TIM_Prescaler = 727 - 1;
#else
// Period = 72,000,000 / 16,000 = 500 * 9
TIM_TimeBaseStructure.TIM_Prescaler = 500 - 1;
#endif
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// Enable TIM3 'TIM update' trigger for adc
TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update);
// Timer will be started in main()
}
初始化I2S, 如果使用的是PT8211, 需要将 I2S_Standard 设置为 I2S_Standard_LSB. 否则双声道传数据时工作不正常 。
void IIS_Configuration(void)
{
I2S_InitTypeDef I2S_InitStructure;
SPI_I2S_DeInit(SPI2);
I2S_InitStructure.I2S_Mode = I2S_Mode_MasterTx;
// PT8211:LSB, MAX98357A:Phillips
I2S_InitStructure.I2S_Standard = I2S_Standard_Phillips;
// 16-bit data resolution
I2S_InitStructure.I2S_DataFormat = I2S_DataFormat_16b;
#if AUDIO_FREQ == 8000
// 8K sampling rate
I2S_InitStructure.I2S_AudioFreq = I2S_AudioFreq_8k;
#elif AUDIO_FREQ == 11000
// 11K sampling rate
I2S_InitStructure.I2S_AudioFreq = I2S_AudioFreq_11k;
#else
// 16K sampling rate
I2S_InitStructure.I2S_AudioFreq = I2S_AudioFreq_16k;
#endif
I2S_InitStructure.I2S_CPOL = I2S_CPOL_Low;
I2S_InitStructure.I2S_MCLKOutput = I2S_MCLKOutput_Disable;
I2S_Init(SPI2, &I2S_InitStructure);
I2S_Cmd(SPI2, ENABLE);
}
中断处理 。
void DMA1_Channel1_IRQHandler(void)
{
// DMA1 Channel1 Transfer Complete interrupt
if (DMA_GetITStatus(DMA1_IT_TC1))
{
DMA_ClearITPendingBit(DMA1_IT_GL1);
// Stop ADC(by stopping TIM3)
TIM_Cmd(TIM3, DISABLE);
ADC_Cmd(ADC1, DISABLE);
GPIO_SetBits(GPIOC, GPIO_Pin_13);
}
}
void SPI2_IRQHandler(void)
{
// If TX Empty flag is set
if (SPI_I2S_GetITStatus(SPI2, SPI_I2S_IT_TXE) == SET)
{
// Put data to both channels
if (lr == 0)
{
lr = 1;
SPI_I2S_SendData(SPI2, (uint16_t)dma_buf[index] << 3);
}
else
{
lr = 0;
SPI_I2S_SendData(SPI2, (uint16_t)dma_buf[index++] << 3);
if (index == BUFF_SIZE)
{
index = 0;
// Disable the I2S1 TXE Interrupt to stop playing
SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_TXE, DISABLE);
}
}
}
}
主函数. 在主函数中, 先开启录音, 然后等待4秒(对应 3万个样本, 8K采样, 4秒之内就结束了), 然后开始播放. 每个循环等待5秒. 播放会在中断中判断是否结束, 结束就停止. 。
int main(void)
{
Delay_Init();
USART_Printf_Init(115200);
printf("SystemClk:%ld\r\n", SystemCoreClock);
RCC_Configuration();
GPIO_Configuration();
ADC_Configuration();
DMA_Configuration(DMA1_Channel1, (uint32_t)&ADC1->DR, (uint32_t)dma_buf, BUFF_SIZE);
NVIC_Configuration();
TIM_Configuration();
IIS_Configuration();
GPIO_SetBits(GPIOC, GPIO_Pin_13);
Delay_S(1);
// Start timer to start recording
printf("Start recording\r\n");
TIM_Cmd(TIM3, ENABLE);
// Turn on LED, DMA TC1 interrupt will turn it off
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
Delay_S(4);
printf("Start playing\r\n");
while (1)
{
// Restart the playing
SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_TXE, ENABLE);
Delay_S(5);
}
}
ADPCM 的原理和计算方式可以参考这一篇 https://www.cnblogs.com/milton/p/16914797.html . 。
使用ADPCM可以将16bit的数据压缩为4bit, 同时保持基本一致的听觉信息. 这样对于64kB的CCT6, 可以在12bit的效果下记录接近16秒的语音(64K = 16 * 8K * 0.5). 而且 ADPCM 的计算简单, AIR32这种M3核心的MCU处理起来非常轻松. 。
如果使用ADPCM, 需要对前面的例子进行一些调整. 硬件和前面的一致, 改动都在代码. 。
因为DMA必须是硬件到硬件, 如果想做成双缓冲, 比如做一个1K左右的DMA数组, 一半结束后批量编码, 再等另一半结束再编码? 这样其实不行, 因为集中编码时ADC也还在进行, 一边在计算一边在转换和中断, 会互相影响, 导致采样不均匀. 因为ADC转换使用定时器触发, 定时器两个中断之间, ADC转换的时间很短, 中间间隔的时间完全可以用于编码, 所以需要将DMA去掉, 改成使用ADC的转换完成中断, 在完成中断的处理函数中对采样值进行编码 。
为了计算方便, 将语音数组转换为uint8_t, 这样每个值记录的是两个采样点, 相应的数组大小扩充到了60000 。
因为每个值存储的是两个采样, 因此在I2S的TXE中断处理中, 原先的左右声道判断需要叠加4bit偏移判断, 变成4种情况. 。
完整的示例代码 。
ADC启用中断 。
void ADC_Configuration(void)
{
ADC_InitTypeDef ADC_InitStructure;
// Reset ADC1
ADC_DeInit(ADC1);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
// Select TIM3 trigger output as external trigger
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
// ADC_Channel_2 for PA2
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 1, ADC_SampleTime_7Cycles5);
// Enable ADC1 external trigger
ADC_ExternalTrigConvCmd(ADC1, ENABLE);
ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);
// Enable ADC1
ADC_Cmd(ADC1, ENABLE);
// Calibration
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1));
}
在ADC中断中, 对ADC结果值的编码 。
void Audio_Encode(void)
{
static uint32_t idx = 0;
static uint8_t msb = 0;
uint8_t val;
val = ADPCM_Encode((uint16_t)(ADC1->DR << 2)) & 0x0F;
if (msb == 0)
{
voice[idx] = val;
msb = 1;
}
else
{
voice[idx] |= (val << 4);
msb = 0;
idx++;
if (idx == BUFF_SIZE)
{
// Stop ADC(by stopping TIM3)
TIM_Cmd(TIM3, DISABLE);
ADC_Cmd(ADC1, DISABLE);
ADC_ExternalTrigConvCmd(ADC1, DISABLE);
GPIO_SetBits(GPIOC, GPIO_Pin_13);
idx = 0;
finish = 1;
}
}
}
在I2S传输中断中, 对值的解码. 每传输四个数据(低4位左右声道, 高4位左右声道)下标才加1, 传输结束后重置下标. 。
uint16_t Audio_Decode(void)
{
static uint32_t idx = 0;
static __IO uint8_t msb = 0, lr = 0;
static uint16_t val;
if (msb == 0)
{
// Put data to both channels
if (lr == 0)
{
val = ADPCM_Decode(voice[idx] & 0x0F);
lr = 1;
}
else if (lr == 1)
{
lr = 0;
msb = 1;
}
}
else
{
if (lr == 0)
{
val = ADPCM_Decode((voice[idx] >> 4) & 0x0F);
lr = 1;
}
else if (lr == 1)
{
lr = 0;
msb = 0;
idx++;
if (idx == BUFF_SIZE)
{
idx = 0;
ADPCM_Reset();
}
}
}
return val;
}
使用ADPCM后, 在8K采样下语音音质没有明显下降, 但是录音时长增长到了15秒, 提升明显. 。
以上说明了如何使用AIR32自带的内存实现简单的语音录制和播放功能, 以及使用 ADPCM 对音频数据进行压缩, 提高录制时长. 通过这些机制, 可以快速扩充为实用的录制设备, 例如外挂I2C或SPI存储, 或提升无线传输的音质, 在同样的码率下使用更高采样率. 。
最后此篇关于AIR32F103(六)ADC,I2S,DMA和ADPCM实现录音播放功能的文章就讲到这里了,如果你想了解更多关于AIR32F103(六)ADC,I2S,DMA和ADPCM实现录音播放功能的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
这个问题在这里已经有了答案: Why don't Java's +=, -=, *=, /= compound assignment operators require casting? (11 个
我搜索了很多,但没有一个链接能帮助我解决这个问题。我得到了 ORA-21500: internal error code, arguments: [%s], [%s], [%s], [%s], [%s
我正在做 RegexOne 正则表达式教程,它有一个 question关于编写正则表达式以删除不必要的空格。 教程中提供的解决方案是 We can just skip all the starting
([\s\S]+|\s?) 中 |\s? 的目的或作用是什么?如果没有它,表达式会不会与 ([\s\S]+) 相同? 最佳答案 这不是完全相同的。 ([\s\S]+|\s?) 会匹配空字符串,而 ([
这个正则表达式有一组还是两组? 我正在尝试使用第二组访问 bookTitle 但出现错误: Pattern pattern = Pattern.compile("^\\s*(.*?)\\s+-\\s+
在 C 中给定一个字符串指针 s,下面的迭代会做什么?即它以什么方式遍历字符串? for (++s ; *s; ++s); 最佳答案 for (++s ; *s;++s) 表示 将指针 s 递增到字符
我正在用一个 node.js 应用程序解析一个大列表并有这段代码 sizeCode = dbfr.CN_DESC.split('\s+-\s*|\s*-\s+') 这似乎不起作用,因为它返回了 [ '
我正在编写一个简单的字符串连接程序。 该程序按照我发布的方式运行。但是,我首先使用以下代码编写它来查找字符串的结尾: while (*s++) ; 但是,这个方法并没有奏效。我传递给它的字符串
这个问题已经有答案了: What does (?和aramchand来自Mohandas Karamchand G 因此,在使用这些匹配来分割字符串后,您最终会得到 {"M", "K", "G"} 注
我正在尝试转换 Map到 List使用 lambda。 本质上,我想将键和值与 '=' 连接起来之间。这看起来微不足道,但我找不到如何去做。 例如 Map map = new HashMap<>();
我正在经历 K & R,并且在递增指针时遇到困难。练习 5.3(第 107 页)要求您使用指针编写一个 strcat 函数。 在伪代码中,该函数执行以下操作: 将 2 个字符串作为输入。 找到字符串
在下面的代码中,pS 和 s.pS 在最后一行是否保证相等?也就是说,在语句S s = S();中,是否可以确定不会构造一个临时的S? #include using namespace std; s
演示示例代码: public void ReverseString(char[] s) { for(int i = 0, j = s.Length-1; i < j; i++, j--){
我一直在寻找类似于 .NET examples 中的示例的 PowerShell 脚本.取一个 New-TimeSpan 并显示为 1 天 2 小时 3 分钟 4 秒。排除其零的地方,在需要的地方添加
def func(s): s = s + " is corrected" return s string_list = ["She", "He"] for s in string_li
我是 python 的新手。当我在互联网上搜索 lambda 时。我在 lambda_functions 中找到了这个声明. processFunc = collapse and (lambda s:
我最近开始学习正则表达式,并试图为上面的问题写一个正则表达式。如果限制只放在一个字母上(例如不超过 2 个“b”),这并不困难。 那么答案就是:a* c*(b|ε)a* c*(b|ε)a* c* 但是
当我运行 npm install 时出现以下错误,但我无法修复它。 我试过:npm install -g windows-build-tools 也没有修复这个错误 ERR! configure
有很多有趣的haskell网上可以找到片段。 This post可以在 this (awesome) Stack Overflow question 下找到. The author写道: discou
我知道以下三行代码旨在将字符串提取到$ value中并将其存储在$ header中。但是我不知道$value =~ s/^\s+//;和$value =~ s/\s+$//;之间有什么区别。 $val
我是一名优秀的程序员,十分优秀!