DAC-波形-声音的真相


人机交互的电子产品经常需要语音提示。如果没有语音外设,可以通过一个DAC输出波形,经简单放大后就能发出声音。 如果音源干净清晰,电路设计好,音质还是非常不错的。 台系(华邦等)的语音芯片通常就是DAC输出音乐。大家小时候用的音乐贺卡,就是用这些芯片制作的。

DAC是什么?

DAC是数字模拟转换器(英语:Digital to analog converter,英文缩写:DAC),是一种将数字信号转换为模拟信号(以电流、电压或电荷的形式)的设备。 上面的定义比较抽象,在单片机来说,形象的说法是:

给一个在DAC位数范围内的值,这个值就是数字量,DAC就根据参考电压,将其转换为电压值,在指定的管脚上输出一个电压,也即是模拟量

  1. 参考电压Vref:DAC转换后输出的最高电压,DAC输出范围0~Vref。精度也是根据参考电压计算。
  2. 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。

STM32 DAC

查看《STM32F4xx中文参考手册.pdf》 STM32F4系列DAC功能特性如下: DAC特性

功能框图如下,从图可以看出: * DAC可以用软件触发、定时器触发、外部IO触发。 * DAC可以有DMA。 * 最下方的数模转换器,在控制逻辑控制之下,根据输入电压,在DAC_OUT上输出DAC电压。

DAC通道框图 硬件上使用PA5作为DAC输出测试。 在《STM32F407_数据手册.pdf》管脚描述表格中有说明PA5是DAC2的输出管脚。 PA5DAC

声音

声音是一种波。在电子上,波,就是不同电压值在时间上的序列。 因此,在DAC管脚上,一直持续输出不同的电压值,即可形成一列波,这列波通过放大,通过喇叭转换,震动空气,就变成了声波。 通常的CD音乐采样频率时44.1K,属于高保真。但是实际上,只要8K的采样频率,声音还原质量就很好了。儿童玩具、声音贺卡的声音通常就是8K。 8K采样频率,每个样点间隔就是1s/8k=125us。 因此,将一个8K采样的声音文件,每125us读取一个声音文件里面的样点,在dac上输出,就可以还原声音了

编码调试

调试分三步: 1. 先调试DAC输出正确电压。 2. 再调试播放一段内嵌在程序的声音。 3. 最后调试播放一个WAV声音文件(这一步暂时不做,等文件系统跟SD卡驱动做好后再调试,反正是纯软件调试,不影响验证硬件)。

DAC调试

首先要让DAC能输出指定电压值。 添加mcu_dac.c和mcu_dac.h到工程。 * 初始化 ```c {.line-numbers} / @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行执行配置。
* 输出电压
```c {.line-numbers}
/**
 *@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。如果要求不是很严格的场合,基本算正常。

播放语音调试

在写语音播放代码之前要记住以下几点:

  1. 是app调用DAC声音驱动播放声音,还是DAC声音驱动去找语音数据。
  2. 根据1,请问是APP提供声音数据给DAC驱动还是DAC声音去找声音数据?
  3. 要播放一个保存在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口还有定时器的配合。 1. 初始化DAC和IO。 2. 定时器设置为125us中断一次。 3. 在定时器中断中读取语音数据,并用DAC输出电压。 4. 重启定时器,循环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执行一次。 ```c {.line-numbers} 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。
需要注意的是:
>1. **声音数据是s16,也就是最高位是正负标志。但是我们的DAC可不支持负数,因此需要将波形直流电平(波形水平中间线,类似X轴),由0V抬高,抬高多少呢?抬高到最高电压的一半,也就是0X7FFF,我们直接加上0X7FFF的偏移。**
>2. 我们的DAC是12位的,数据是16位的,数据右移4位匹配。
>3. **本算法有音频失真,请问原因是什么?最新处理方法请查看github上持续更新的代码。**

还要记得在stm32f4xx_it.c添加中断入口
```c
void TIM3_IRQHandler(void)
{
    mcu_tim3_IRQhandler();
}

  • 测试 修改main.c,第3行打开dacsound,第12行,当按下按键时,播放语音。 ```c {.line-numbers} / 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();
    

    } ``` 现在应该能听到声音了。

    思考

    现在我们仅仅是验证了DAC播放语音功能。 如何解码WAV? dacsound提供什么接口? 使用什么机制播放? 这些问题我们等WM8978调试之后优化完成。


end