65.9K
CodeProject 正在变化。 阅读更多。
Home

dsPIC33FJ128GP802 上的 16 位立体声音频 DAC

2023年4月4日

CPOL

5分钟阅读

viewsIcon

3287

dsPIC33FJ128GP802 上的 16 位立体声音频 DAC

dsPIC33FJ128GP802dsPIC33F 系列中少数集成 16 位立体声 DAC 模块的器件之一。DAC 可以用来播放存储在 SD 卡上的 WAV 音频文件。我从 Microchip 收到了免费的样品(当然是 DIP 封装的),并花了一些时间试用了 dsPICDAC 模块。

根据数据手册,这款 PIC 的 DAC 模块支持 16 位分辨率(14 位精度),输入频率高达 45kHz。DAC 可以通过数据手册第 22.0 节中描述的 DACXXXX 寄存器进行管理。还有一个 DACDFLT 寄存器,用于指定当 DAC 缓冲区变空时将使用的默认输出值。有趣的是,尽管 DACDFLT 的描述说默认值对于工业控制应用很有用,其中 DAC 输出控制着重要的处理器或机械设备,但数据手册的同一页面却指出 DAC 模块专门为音频应用设计,不推荐用于控制类应用!这类矛盾的信息可能会误导许多用户。在我进行的实验中,DAC 输出引脚上的最大电压约为 1.6V。此外,输出波形似乎经过了某种低通滤波器,并且与输入值的变化不是线性的。因此,正如数据手册中所述,这款 PIC 上的 DAC 模块应严格仅用于音频。

以下代码使用 PLL 输出 (FVCO) 作为时钟源来初始化 DAC

void initDAC()
{
    ACLKCONbits.APSTSCLR = 0b111; 	// Auxiliary Clock Output Divider (divide by 1)
    ACLKCONbits.SELACLK = 0;    	// 1 = Auxiliary Oscillators provides the 
                                    // source clock for Auxiliary Clock Divider
				                    // 0 = PLL output (Fvco) provides the source clock 
                                    // for the Auxiliary Clock Divider for the DAC clock

    DAC1STATbits.LOEN = 1; 	        // Left Channel DAC Output Enabled
    DAC1STATbits.ROEN = 1; 	        // Right Channel DAC Output Enabled

    DAC1STATbits.LITYPE = 0;        // Left Channel Interrupt Type 
                                    // (1 = Interrupt if FIFO is empty, 
                                    //  0 = Interrupt if FIFO is not full)
    DAC1STATbits.RITYPE = 0; 	    // Right Channel Interrupt Type

    DAC1CONbits.AMPON = 0; 	// Amplifier Disabled During Sleep and Idle Modes

    DAC1DFLT = 0x00; 		// Default value When DAC buffer is empty

    IFS4bits.DAC1LIF = 0; 	// Clear Left Channel Interrupt Flag
    IFS4bits.DAC1RIF = 0; 	// Clear Right Channel Interrupt Flag

    IEC4bits.DAC1LIE = 0; 	// Left Channel Interrupt   (0 = Disabled, 1 = Enabled)
    IEC4bits.DAC1RIE = 0; 	// Right Channel Interrupt 

    DAC1CONbits.DACEN = 1; 	// DAC1 Module Enabled
}

下一步是设置 DAC 时钟,该时钟必须等于采样率乘以 256。DACCLK 是通过将高速振荡器(辅助时钟或系统时钟)除以指定值生成的。除数比由 DAC 控制 (DACxCON register<6:0>) 中的时钟分频器位 (DACFDIV<6:0>) 指定。生成的 DACCLK 不能超过 25.6 MHz。dsPIC DSC PLL 可以配置为提供系统时钟,该系统时钟是 DAC 时钟速率的整数倍。DAC 模块的系统时钟源指定为 FVCO,它是 PLL 在后 PLL 分频器 (PLLPOST 或 N2) 之前的输出。假设 PIC 使用内部振荡器,M(等于 PLLFBD + 2)设置为 40,N1(等于 PLLPRE)设置为 2,我们得到:

FVCO = (7.3728 * 10^6 * 40) / 2 = 147456000 Hz

SELACLK (ACLKCON<13>) 位设置为 ‘0’,并将 DACFDIV 寄存器值设置为将 FVCO 时钟除以 72,我们得到的 DAC 时钟为:

147456000 Hz / 72 = 2048000 Hz 

这就是输入频率所需的 DAC 时钟:

2048000 Hz / 256 = 8000 Hz 

以下代码展示了如何为特定输入频率设置 DACFDIV 值。

unsigned int div = (FINT * (MCONST + 2) / freq / N1 / 256) - 1;
DAC1CONbits.DACFDIV = div; 

其中 FINT 是 PIC 振荡器频率(内部振荡器为 7372800),MCONST 是时钟设置期间分配给 PLLFBD 的值,N1 是分配给 PLLPRE 的值。有关更多信息,请参阅 Microchip 的 DS70211, 音频数模转换器 (DAC) 的第 33.4 部分,以及数据手册的“振荡器配置”部分。

请注意,DACFDIV 是一个 16 位整数,因此对于某些频率(如 11025Hz、22050Hz 或 44100Hz),可能会存在一些不准确性,具体取决于时钟源。在任何情况下,这些不准确性都可以忽略不计,并且不应影响输出音频质量。

Microchip 的 DS70211 中有一个明显的排版错误。FVCO 的公式如下(重点是我加的):

Microchip DS70211 wrong FVCO formula

好了,如果你想让 DAC 工作,那么上面的排版错误应该很明显。147.456 * 106 MHz 的速度比今天最快的计算机还要快。如此低级的错误竟然会出现在 Microchip 的文档中,这真是讽刺。无论如何,我问了一位拥有信息处理博士学位的 EE 教授朋友,她也没能发现这个错误。这也许能说明一些问题……

要将音频样本发送到 DAC,只需为主通道设置 DAC1LDAT 的值,为右通道设置 DAC1RDAT 的值。如果你的输入样本是 8 位,你需要将其转换为 16 位,并考虑样本是带符号还是无符号。按照惯例,8 位音频样本是无符号的,而 16 位样本是带符号的。使用以下代码将 DAC 配置为工作在有符号或无符号模式:

DAC1CONbits.FORM = 0;    // Data Format (0 = Unsigned, 1 = Signed)

以下代码演示了如何使用 FatFs 来使用 8kHz WAV 文件中检索到的 8 位无符号音频样本播放 DAC。文件头(仅包含 44 字节)尚未考虑在内。

FSFILE * pointer;
unsigned int i;

#define BUFFER_LENGTH 64
unsigned char buffer[BUFFER_LENGTH];

pointer = FSfopen ("AUDIO.WAV", FS_READ);
if (pointer == NULL)
{
	SendUARTStr("Error opening WAV");
}
else {
	SendUARTStr("Playing WAV");

	unsigned int bytesRead = 0;
	do
	{
		bytesRead = FSfread(buffer, 1, BUFFER_LENGTH, pointer);
		for (i=0; i<bytesRead;i++)
		{
			DAC1LDAT = (unsigned int)(buffer[i]) << 8;
			delay_us(125);
		}
	}
	while (bytesRead > 0);

	FSfclose(pointer);
	SendUARTStr("Finished");
} 

这里,为 8kHz 的输入频率在每个样本后添加了 125µs 的延迟。更好的实现方式是有一个小的 PCM 播放缓冲区,配置一个以 8kHz 运行的定时器中断,该中断从缓冲区检索数据并将其放入 DAC,然后主例程负责将音频数据转储到此缓冲区。这种实现方式可以让你的主例程在等待播放完成的同时处理其他事情。我将把它留给读者作为练习。

如果你的代码无法及时填充 DAC 缓冲区,则将 DACDFLT 设置为最后播放的样本可能很有用,可以减少缓冲区欠载的影响。此外,通过将 DAC1LIE/DAC1RIE 设置为 1 并配置 LITYPE/RITYPE,你可以使用以下中断例程来指示 DAC FIFO 缓冲区何时为空或已满,并相应地处理情况。

void __attribute__((interrupt, no_auto_psv))_DAC1LInterrupt(void)
{
    // Clear Left Channel Interrupt Flag
   IFS4bits.DAC1LIF = 0;
}

void __attribute__((interrupt, no_auto_psv))_DAC1RInterrupt(void)
{
    // Clear Right Channel Interrupt Flag
    IFS4bits.DAC1RIF = 0;
} 

要在示波器上计算当前的 DAC 时钟频率,你可能需要在 DAC 中断例程中切换一个引脚(例如,LATBbits.LATB5 = !LATBbits.LATB5),并在该引脚上测量输出波形。你可能还想在离开中断例程之前设置 DAC1LDAT/DAC1RDAT,否则直到 DAC 缓冲区接收到另一个值之前,中断都不会再次调用。数据手册说 DAC 有一个 4 字节深的 FIFO 缓冲区,在通过此方法计算 DAC 频率时必须考虑到这一点。在我的项目中,使用 DAC 中断不是必需的,因为音频播放是从配置为以输入音频频率运行的定时器中断完成的。

© . All rights reserved.