9. DAC-波形-声音的真相¶
够用的硬件
能用的代码
实用的教程
屋脊雀工作室编撰 -20190101
愿景:做一套能用的开源嵌入式驱动(非LINUX)
官网:www.wujique.com
github: https://github.com/wujique/stm32f407
淘宝:https://shop316863092.taobao.com/?spm=2013.1.1000126.2.3a8f4e6eb3rBdf
技术支持邮箱:code@wujique.com、github@wujique.com
资料下载:https://pan.baidu.com/s/12o0Vh4Tv4z_O8qh49JwLjg
QQ群:767214262
人机交互的电子产品经常需要语音提示。如果没有语音外设,可以通过一个DAC输出波形,经简单放大后就能发出声音。 如果音源干净清晰,电路设计好,音质还是非常不错的。 台系(华邦等)的语音芯片通常就是DAC输出音乐。大家小时候用的音乐贺卡,就是用这些芯片制作的。
9.1. DAC是什么?¶
DAC是数字模拟转换器(英语:Digital to analog converter,英文缩写:DAC),是一种将数字信号转换为模拟信号(以电流、电压或电荷的形式)的设备。 上面的定义比较抽象,在单片机来说,形象的说法是:
给一个在DAC位数范围内的值,这个值就是数字量,DAC就根据参考电压,将其转换为电压值,在指定的管脚上输出一个电压,也即是模拟量。
- 参考电压Vref:DAC转换后输出的最高电压,DAC输出范围0~Vref。精度也是根据参考电压计算。
- DAC位数:DAC的关键性能,位数即是DAC精度,也是DAC的输出步进。通常有8位、10位、12位等。例如12位,即是说可以将输出精确到:Vref/0xfff。一个12位的DAC在3.3V参考电压下,输出可以精确到0.805mv。将0X01送到DAC,管脚将输出0.805mv;将0x02送到DAC,管脚将输出1.61mv;将0xfff送到DAC,将输出Vref。
9.2. STM32 DAC¶
查看《STM32F4xx中文参考手册.pdf》 STM32F4系列DAC功能特性如下: DAC特性
功能框图如下,从图可以看出:
- DAC可以用软件触发、定时器触发、外部IO触发。
- DAC可以有DMA。
- 最下方的数模转换器,在控制逻辑控制之下,根据输入电压,在DAC_OUT上输出DAC电压。
DAC通道框图 硬件上使用PA5作为DAC输出测试。 在《STM32F407_数据手册.pdf》管脚描述表格中有说明PA5是DAC2的输出管脚。 PA5DAC
9.3. 声音¶
声音是一种波。在电子上,波,就是不同电压值在时间上的序列。 因此,在DAC管脚上,一直持续输出不同的电压值,即可形成一列波,这列波通过放大,通过喇叭转换,震动空气,就变成了声波。 通常的CD音乐采样频率时44.1K,属于高保真。但是实际上,只要8K的采样频率,声音还原质量就很好了。儿童玩具、声音贺卡的声音通常就是8K。 8K采样频率,每个样点间隔就是1s/8k=125us。 因此,将一个8K采样的声音文件,每125us读取一个声音文件里面的样点,在dac上输出,就可以还原声音了。
9.4. 编码调试¶
调试分三步:
- 先调试DAC输出正确电压。
- 再调试播放一段内嵌在程序的声音。
- 最后调试播放一个WAV声音文件(这一步暂时不做,等文件系统跟SD卡驱动做好后再调试,反正是纯软件调试,不影响验证硬件)。
9.4.1. DAC调试¶
首先要让DAC能输出指定电压值。 添加mcu_dac.c和mcu_dac.h到工程。
- 初始化
/**
*@brief: mcu_dac_open
*@details: 打开DAC控制器
*@param[in] void
*@param[out] 无
*@retval:
*/
s32 mcu_dac_open(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
DAC_InitTypeDef DAC_InitType;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);//----使能 PA 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);//----使能 DAC 时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;//---模拟模式
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;//---下拉
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//---初始化 GPIO
DAC_InitType.DAC_Trigger=DAC_Trigger_None; //---不使用触发功能 TEN1=0
DAC_InitType.DAC_WaveGeneration=DAC_WaveGeneration_None; //---不使用波形发生
DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude=DAC_LFSRUnmask_Bit0;
DAC_InitType.DAC_OutputBuffer=DAC_OutputBuffer_Disable ; //---输出缓存关闭
//DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude = DAC_TriangleAmplitude_4095; //噪声生成器
DAC_Init(DAC_Channel_2,&DAC_InitType); //---初始化 DAC 通道 2
DAC_Cmd(DAC_Channel_2, ENABLE); //---使能 DAC 通道 2
DAC_SetChannel2Data(DAC_Align_12b_R, 0); //---12 位右对齐数据格式 输出0
return 0;
}
上面函数是打开DAC代码,其实也就是初始化配置DAC。 和前面的定时器输出和定时器输入一样,除了使用DAC外设,还需要用IO口。
16~20行,将IO口PA5配置为模拟功能。 22行,配置DAC不使用触发。 23行,不使用波形发生器,DAC可以产生三角波等波形。 24行设置屏蔽/幅值选择,只有用波形发生器才有用。 25行禁止输出缓存。 27行执行配置。
- 输出电压
/**
*@brief: mcu_dac_output
*@details: 设置DAC输出值
*@param[in] u16 vol, 电压,单位MV,0-Vref
*@param[out] 无
*@retval:
*/
s32 mcu_dac_output_vol(u16 vol)
{
u32 temp;
temp = (0xfff*vol)/3300;
MCU_DAC_DEBUG(LOG_DEBUG, "\r\n---test dac data:%d-----\r\n", temp);
DAC_SetChannel2Data(DAC_Align_12b_R, temp);//12 位右对齐数据格式
return 0;
}
13行是电压计算,原理是:
配置值/电压 = 0XFFF/3.3V 配置值是我们要写到DAC的值,电压就是输入参数,单位是mv。 3300也就是3300mv; 0xFFF,因为我们的DAC是12位,也即是说,当我们设置DAC为0XFFF时,DAC输出3.3V。
17行调用函数将配置值写到DAC。
- 测试程序
s32 mcu_dac_test(void)
{
uart_printf("\r\n---test dac!-----\r\n");
mcu_dac_open();
mcu_dac_output_vol(1500);//1.5v
while(1);
}
程序设计输出1.5V,测试输出管脚,电压为1.492V,基本准确,偏差0.01V,这个偏差有可能是基准电压,也就是我们的3.3V有偏差。 实测3.3V,只有3.28V,偏差0.02V。如果要求不是很严格的场合,基本算正常。
9.4.2. 播放语音调试¶
在写语音播放代码之前要记住以下几点:
- 是app调用DAC声音驱动播放声音,还是DAC声音驱动去找语音数据。
- 根据1,请问是APP提供声音数据给DAC驱动还是DAC声音去找声音数据?
- 要播放一个保存在SD卡中的8K采样频率的WAV文件,请问:SD卡,8K采样,WAV,这三个参数跟DAC声音驱动是否有关?
对于这几点,个人看法如下:
DAC声音驱动只实现将一定格式的声音数据转换为声音。 格式包含什么呢?采样频率,单声道还是多声道,多少位,这三个参数是DAC需要的。 文件格式是WAV还是PCM还是MP3,跟DAC声音驱动没关系,至于你是放在SD卡还是U盘,那更加没关系了。 这些事情,应该由语音播放中间层处理。
那么 DAC sound驱动要提供什么接口呢?
init—-初始化设备 open—-打开设备,意味则要用这个设备 close—-关闭设备 setting—设置,采样频率,声道,位宽(当然,对于DACsound来说只支持单声道,位宽也是固定的) 提供一个控制接口—-控制启动播放,暂停,停止,查询状态 最后一个接口就是填充数据,如何填充?请思考。
以上的问题在本节暂时不处理,后面等我们做完WM8978的驱动,两个声音驱动一起分析,对于一个声音驱动应该做成什么样子,就更加清晰明了了。 现在先使用最快的速度编写一套代码,让硬件发出声音,以便硬件改版,软件架构问题后续慢慢优化。
- 语音播放流程
播放DAC语音,就是使用DAC和IO口还有定时器的配合。
- 初始化DAC和IO。
- 定时器设置为125us中断一次。
- 在定时器中断中读取语音数据,并用DAC输出电压。
- 重启定时器,循环3,直到语音播放结束。
- 声音数据准备
现在还没有调试WAV解码,也没有完成SD卡文件系统。只好将一段声音内嵌到代码内,这样也可以避免其他模块干扰,只验证DAC播放语音功能。
如何将一段声音内嵌到代码?
- 代码驱动说明
在board_dev文件夹创建dacsound驱动源码文件:dev_dacsound.c、dev_dacsound.h
在mcu_timer驱动中增加定时器3初始化和中断处理函数,定时125us。 定时器前面已经学习,不再累赘 在中断中调用dev_dacsound_timerinit函数输出DAC电压。
调用dev_dacsound_open初始化dacsound功能。 调用dev_dacsound_play开始播放,函数内开启了定时器。 然后进入主要处理函数dev_dacsound_timerinit,这个函数在定时中断中调用,125us执行一次。
s32 dev_dacsound_timerinit(void)
{
u8 data1 = 0, data2 = 0;
s16 data = 0;
u16 tmp;
data1 = BeepData[soundindex++];
data2 = BeepData[soundindex++];
/*要注意,读到的数据是S16,正负值*/
data = (s16)((data2 << 8) | data1);
tmp = (data+0X7FFF)>>4;//12位DAC
//uart_printf("%04x ", tmp);
mcu_dac_output(tmp);
if(soundindex >= BEEP_DATA_LEN)
{
uart_printf("dac sound play finish!");
/*停止定时器*/
mcu_tim3_stop();
}
}
处理过程并不复杂,读取两个字节数据,组成一个16位数据,丢到DAC。 需要注意的是:
- 声音数据是s16,也就是最高位是正负标志。但是我们的DAC可不支持负数,因此需要将波形直流电平(波形水平中间线,类似X轴),由0V抬高,抬高多少呢?抬高到最高电压的一半,也就是0X7FFF,我们直接加上0X7FFF的偏移。
- 我们的DAC是12位的,数据是16位的,数据右移4位匹配。
- 本算法有音频失真,请问原因是什么?最新处理方法请查看github上持续更新的代码。
还要记得在stm32f4xx_it.c添加中断入口
void TIM3_IRQHandler(void)
{
mcu_tim3_IRQhandler();
}
- 测试 修改main.c,第3行打开dacsound,第12行,当按下按键时,播放语音。
/* Infinite loop */
mcu_uart_open(3);
wjq_log(LOG_INFO, "hello word!\r\n");
mcu_i2c_init();
dev_key_init();
//mcu_timer_init();
dev_buzzer_init();
dev_tea5767_init();
dev_dacsound_init();
dev_key_open();
dev_dacsound_open();
//dev_tea5767_open();
//dev_tea5767_setfre(105700);
while (1)
{
/*驱动轮询*/
dev_key_scan();
/*应用*/
u8 key;
s32 res;
res = dev_key_read(&key, 1);
if(res == 1)
{
if(key == DEV_KEY_PRESS)
{
//dev_buzzer_open();
dev_dacsound_play();
GPIO_ResetBits(GPIOG, GPIO_Pin_0
| GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
//dev_tea5767_search(1);
}
else if(key == DEV_KEY_REL)
{
//dev_buzzer_close();
GPIO_SetBits(GPIOG, GPIO_Pin_0
| GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
}
}
Delay(1);
/*测试触摸按键*/
//dev_touchkey_task();
//dev_touchkey_test();
}
现在应该能听到声音了。