dsPIC33FJ128GP802 上的 16 位立体声音频 DAC





0/5 (0投票)
dsPIC33FJ128GP802 上的 16 位立体声音频 DAC
dsPIC33FJ128GP802
是 dsPIC33F
系列中少数集成 16 位立体声 DAC 模块的器件之一。DAC 可以用来播放存储在 SD 卡上的 WAV 音频文件。我从 Microchip 收到了免费的样品(当然是 DIP 封装的),并花了一些时间试用了 dsPIC
的 DAC
模块。
根据数据手册,这款 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
的公式如下(重点是我加的):
好了,如果你想让 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
中断不是必需的,因为音频播放是从配置为以输入音频频率运行的定时器中断完成的。