:o Finally got a DAC that's able to keep up with the sample rate.(See mcp4922) The DAC on the Due was nice for testing but analysis showed the output was not very good and didn't have "rail to rail" output.
The output use to go from the built in DAC -> lm386 -> speaker... now goes -> MCP4922 -> PAM8403 -> speakers via a variable resister.
The output is inside an interrupt so I needed it as fast as possible. I didn't use hardware SPI as DMA mode SPI on the SD card clashed with regular SPI and the DMA mode is > 2x faster than regular SPI library.
Another side note... in all my code I use variables like uintX_t, uintX16_t. On an 8 bit processor the 2 will resolve to uint8_t and uint16_t... but on a 32 bit platform they would both be uint32_t. I could be wrong for arm chips... but on x86's they didn't like being 32 bit and reading byte values... especially if not dword aligned. From what I can tell... this eliminates a lot of thrashing around converting 8<->16<->32 bit when not needed.
Main class to driver the DAC
/*
MCP4922 12 bit 2 channel DAC driver
https://www.futurlec.com/Mini_DACa.shtml
*/
class cMCP4922
{
public :
cMCP4922(uintX_t cs, uintX_t sclk, uintX_t mosi);
void outputDAC(uintX16_t val);
private:
// These fastpin macros define all the variables needed to write a value directly to port in a single instruction
fastpin(m_mosi);
fastpin(m_sclk);
fastpin(m_cs);
};
cMCP4922::cMCP4922(uintX_t cs, uintX_t sclk, uintX_t mosi)
{
// macro copies variables, does pinMode and sets default high or low
fastpinInit(m_mosi, mosi, OUTPUT, LOW);
fastpinInit(m_sclk, sclk, OUTPUT, LOW);
fastpinInit(m_cs, cs, OUTPUT, HIGH);
}
// inlined macro as timing critical ... esp between the setting and unsetting of m_sclk
// The chip can handle 20Mhz clock... so a due of 84Mhz to run full speed here only needs a few 2 cycle nop
// Too much waiting just slows everything down... too fast and bits start getting mucked up
#define write_bit(x) (x) ? fastpinSetHigh(m_mosi) : fastpinSetLow(m_mosi); fastpinSetHigh(m_sclk); \
__asm__ __volatile__("nop\n\t"); \
__asm__ __volatile__("nop\n\t"); \
__asm__ __volatile__("nop\n\t"); \
fastpinSetLow(m_sclk);
// Byte and bit order... MSBFIRST
void cMCP4922::outputDAC(uintX16_t val)
{
// No interrupts here could be good
fastpinSetLow(m_cs);
// 4 control bits
write_bit(0); // 16 - DAC A=0 B=1
write_bit(1); // 15 - 0=unbuffered 1=buffered
write_bit(1); // 14 - gain = 0=2x 1=1x (2x clips and sounds crap if values go over 4095)
write_bit(1); // 13 - 1=active 0=shutdown
// 12 data bits (bits 12 -> 1)
write_bit(val & 2048);
write_bit(val & 1024);
write_bit(val & 512);
write_bit(val & 256);
write_bit(val & 128);
write_bit(val & 64);
write_bit(val & 32);
write_bit(val & 16);
write_bit(val & 8);
write_bit(val & 4);
write_bit(val & 2);
write_bit(val & 1);
fastpinSetHigh(m_cs);
}
cMCP4922 gDAC(8, 9, 10);
void fn_play_dac(void) // called by interrupt
{
PCM.sendSample();
register int32_t left = 2048 + PCM.getLevelLeft() / 16; // /16 because need 16 bit -> 12 bit
if (left < 0) left = 0;
else if (left >= 4096) left = 4095;
gDAC.outputDAC(left);
}
I have a header I include with all my projects that defines all the low level code specific to boards. Which defines some of the macros used above.
// These variables are the native 8/32 bit types and used for things 0-127 and 0-255
#define uintX_t uint32_t
#define intX_t int32_t
#define uintX16_t uint32_t
#define intX16_t int32_t
#define fastpin(var) uintX_t var = 0; uint32_t var##_mask ; WoReg *var##_set; WoReg *var##_unset;
#define fastpinInit(var, pin, mode, deft) if (pin < digitalPinCount) { var##_mask = g_APinDescription[pin].ulPin; var = pin; var##_set = &g_APinDescription[pin].pPort->PIO_SODR; var##_unset = &g_APinDescription[pin].pPort->PIO_CODR; setPinMode(pin, mode); fastDigitalWrite(pin, deft); }
#define fastpinSet(adr, value) if ((value)) { adr##_set = adr##_mask ; } else { adr##_unset = adr##_mask; }
#define fastpinSetHigh(adr) *adr##_set = adr##_mask
#define fastpinSetLow(adr) *adr##_unset = adr##_mask