Controlling a DAC with Max

personal_username's icon

Hi guys,

I'm trying to see if I can control a DAC directly from Max and an Arduino.
I'm basically trying to port a Python application that is giving me some headache, but I'm a little stuck now.
Basically it's about the kind of data that Arduino expects.

Data looks like this:

b'#\\n'
b'\\x0b\\xe6'
b'\\x19\\xe4'

I'm wondering if there is a way from Max to convert an int into this kind of format.
I know there is a way to convert ints into bytes (and hex)
like this:

Max Patch
Copy patch and select New From Clipboard in Max.


but really have no idea if there is an external or something that allows me to do the required conversion

Just to give you an idea, this is a list where I collected

1) the datatype the DAC is expecting
2) the hexadecimal correspondent
3) the value as an int

******
0, b'#\\n' 22dc 8924;
1, b'#7' 230a 8970;
2, b'#e' 2337 9015;

634, - b'\\x0c5' 0c50 3152;
635, - b'\\x0c\\x1a' 0c35 3125;
636, - b'\\x0c\\x00' 0c1a 3098;
637, - b'\\x0b\\xe6' 0c00 3072;

923, - b'\\x19\\x8d' 1962 6498;
924, - b'\\x19\\xb8' 198d 6541;
925, - b'\\x19\\xe4' 19b8 6584;
*****

Soooo, any chance I can do this within Max?
Thanks!

PS for the fearless, here's the DAC I'm using:
https://eu.mouser.com/ProductDetail/595-DAC7311IDCKT?r=595-DAC7311IDCKT

personal_username's icon

Addendum:

in Python that conversion is done like this (example with int 34)

**********

int(34).to_bytes(2,byteorder="big",signed=False)

**********

Source Audio's icon

You should rather post what arduino is expecting, and not what python syntax is.
All that quotas and backslashes is python part of serial print,
and won't work if sent via max serial object.
For example :
int 34 python = b'\x22'
in max one would send 34 to atoi and than to serial object.
Data is sent as ascii 50 50
Arduino reads 34 in both cases.

Max Patch
Copy patch and select New From Clipboard in Max.

personal_username's icon

Oh, I see...
Here!

/*

* CONFIRMED OPERATIONAL: 2017-02-17

* SYSTEM DEVELOPED ON SOFTWARE VERSIONS:

* Arduino 1.8.1

* Python 3.2.5

* PyGame 1.9.2pre

* PySerial 2.6

* SYSTEM DEVELOPED ON ARDUINO HARDWARE:

* "MINI USB Nano V3.0 ATmega328P CH340G 5V 16M"

*/

//This ring buffer takes data from USB > serial and stores it.

//The byte "0xFF" is transmitted out over serial > USB when

//there's no incoming data and there's space in the buffer

//for another 256 bytes.

//Output samples are pushed to the DAC at 1,953.125 Hz (every 512us).

//In my tests, Timer 2 CTC mode, which would (theoretically) allow a nice

//round-number sample rate, DOES NOT PLAY NICELY with the other

//libraries used here, <SPI.h>, and Serial.h (implicit in Arduino IDE).

//------------------------------------

//After many hours of poking and prodding, I have decided that this code,

//which appears stable, is sufficient.

#include <SPI.h> // necessary library for SPI data transmission to DAC7311

static uint8_t cs = 10; // using digital pin 10 for DAC7311 chip select

volatile uint8_t loadpoint = 0; //the index of the location in databuffer[]

//that our loaded data goes up to (moves up as we write)

volatile uint8_t readpoint = 0; //the index of the location in databuffer[]

//that we haven't read above (moves up as we read)

volatile uint16_t databuffer[256]; //our buffer array of unsigned 16-bit integers

volatile uint8_t should_send = 0; //this gets set to 1 every 512us by an interrupt.

//It's polled/reset to 0 in loop()

//-------------------------------------------------------------------------------

// INTERRUPT SERVICE ROUTINE (FANCY SPECIAL FUNCTION):

//

// WHEN TIMER2 OVERFLOWS, NO MATTER WHERE WE ARE**, WE GO HERE,

// SET THE VOLATILE GLOBAL VARIABLE "should_send" TO 1,

// AND THEN WE GO BACK TO WHATEVER WE WERE DOING BEFORE

//

// **except within certain parts of the Serial library. Unless

// you feel like going through a lot of TIMSK register descriptions in the

// 660-page ATMEGA328P datasheet, and cross-referencing them with the

// interrupt register bits set in the Arduino Serial library files,

// just... don't worry about it.

// However, as a result of interference by all the hidden interrupt

// timing/permissions/disabling/enabling that takes place in the Arduino Serial library...

// Don't set timer2 any faster than it's set in setup() !

// (timer2 overflow interrupt every 512us)

// SPEEDING UP TIMER2 WILL NOT INCREASE THE OUTPUT SAMPLE RATE AS YOU WOULD EXPECT!!

//-------------------------------------------------------------------------------

ISR(TIMER2_OVF_vect){ //When timer2 overflows,

should_send = 1; //it's time to send more data to the DAC.

}

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

// REQUEST DATA IF READY

//-------------------------------------------------------------------------------

void request_data_if_ready(void){

//(readpoint - loadpoint) == number of buffer addresses available.

//Total number of buffer addresses is 256, and we take data in 256-byte blocks.

//Our buffer addresses each require two bytes to fill.

uint8_t buffer_locations_available = readpoint - loadpoint;

if((loadpoint == readpoint) || ((buffer_locations_available >= 96) && ((buffer_locations_available % 16) == 0)) ){

//If we have space for more data in the buffer, either (buffer is completely empty), or (buffer has space for another 256-byte block)

Serial.write(0xFF); //Request more data for our buffer! (Writing the byte 0xFF tells the computer we're ready for more data.)

//the"magic number" 96 comes from: 128 buffer locations will be written, but given 115200 baud serial,

//((256bytes to receive *8bits per byte*(1/115200bits per second))/0.000512seconds per buffer pop = 34.72222222buffer locations lost during receiving

// so 93.277 "actual" locations are written per 128-location data block, ideally, ignoring USB latency...

//in my empirical tests on my system USB latency is 16 ms one-way, so 32ms latency round-trip (63 ticks)

}

}

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

// LOAD DATA IF AVAILABLE

//-------------------------------------------------------------------------------

void load_data_if_available(void){ //Loads data, as long as there is data to load.

uint8_t buffer_locations_filled = loadpoint - readpoint;

//###############################################################

// BE WARNED!! THIS FUNCTION DOES NOT CHECK IF

// LOADING MORE DATA WILL OVERWRITE YOUR BUFFER!

// IT DIRECTLY LOADS WHATEVER DATA IS AVAILABLE!

//###############################################################

//It is assumed that you will request data using

//request_data_if_ready(), which checks that you have

//enough remaining buffer capaciity...

//*before* it asks for more data.

if(Serial.available() > 31){ //As long as there are thirty-two or more bytes to load,

for(uint8_t i=0;i<16;i++){

uint8_t highbyte = Serial.read(); //First we save the high byte, then

uint8_t lowbyte = Serial.read(); //We save the low byte, and

loadpoint++; //We increment the load pointer.

//"RING BUFFER" : Incrementing moves loadpoint up one slot,

//away from readpoint -- the space below loadpoint

//but above readpoint is where the buffer lives; therefore

//note, with care: haphazardly writing data from the computer

//to the Arduino, thus overfilling the buffer until loadpoint

//wraps around to the same location (aka value) as readpoint, will

//cause output_data_if_ready() to think that the buffer is empty!

//The buffer is a ring because loadpoint and readpoint are both uint8_t,

//so they automatically wrap around when they go above 255, and no extra

//test/reset/whatever code is needed to make the buffer behave

//like a nice ring.

databuffer[loadpoint] = //To convert our two 8-bit bytes into a single uint16_t:

((uint16_t)highbyte << 8) //We cast highbyte (let's pretend its value is 0xFA) from

//unsigned 8-bit to unsigned 16-bit integer, yielding 0x00FA,

//then perform a logical shift left 8 places, yielding 0xFA00.

| (uint16_t)lowbyte; //We then cast lowbyte (we'll pretent lowbyte is 0xCE) to uint16_t

//as well, yielding 0x00CE. We perform a bitwise OR between

//lowbyte (now 0x00CE) and shifted highbyte (0xFA00) to obtain

//0xFACE, a uint16_t we can store in databuffer[loadpoint].

}

}

if((buffer_locations_filled == 0) && (Serial.available() > 1)){ //if the buffer's completely empty, and there are at lest two bytes available...

uint8_t highbyte = Serial.read(); //First we save the high byte, then

uint8_t lowbyte = Serial.read(); //We save the low byte, and

loadpoint++; //We increment the load pointer.

databuffer[loadpoint] =

((uint16_t)highbyte << 8)

| (uint16_t)lowbyte;

}

}

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

// OUTPUT DATA IF READY

//-------------------------------------------------------------------------------

void output_data_if_ready(void){ //Outputs data, as long as:

if(should_send == 1){ //The timer2 overflow interrupt service routine says it's time

//to send data (it's been 512 microseconds, buddy, you gonna stand here all day?),

if((loadpoint - readpoint) != 0){ //and we actually have data to send. If we do,

readpoint++; //move the pointer up one,

DACwrite(databuffer[readpoint]); //we output the 16 bits at the current location of the read pointer,

should_send = 0; //and note that we shouldn't send more data until timer2 overflows again.

}

}

}

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

// DAC WRITE FUNCTION

//-------------------------------------------------------------------------------

void DACwrite(uint16_t value){ //Why is this a separate function? So you can easily play with

//the DAC without following my specific read/write/buffer protocol!

digitalWrite(cs, LOW); //pull *CS LOW ("Hey, DAC, listen! We're talking to you!")

SPI.transfer16(value); //write the value to the DAC

digitalWrite(cs, HIGH); //pull *CS HIGH ("DAC, we're done talking to you. Output the value we sent you.")

}

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

// SETUP FUNCTION

//-------------------------------------------------------------------------------

void setup(){ //This function configures our timer, buffer, and serial communications.

cli(); //first we disable interrupts,

//because an interrupt happening during this setup stuff could really interfere with our code!

//-------------------------------------------------------------------------------

//set up data buffer:

for(uint16_t i=0;i<256;i++){ //for each value in the data buffer,

databuffer[i]= 0; //store a zero in the data buffer.

} //(if we don't do this, the compiler might not allocate any memory for the buffer)

//-------------------------------------------------------------------------------

Serial.begin(115200); //start serial at 115200 baud, no fancy stuff

//-------------------------------------------------------------------------------

//SPI setup steps:

pinMode(cs, OUTPUT); //we use this pin (D10) as an output for *CS ( * means active low) chip select pin

SPI.begin(); //start SPI

SPI.beginTransaction(SPISettings(50000000, MSBFIRST, SPI_MODE1));

//max SPI transmit speed is 50MHz, most significant bit first, SPI mode 1 (CPOL = 0, CPHA = 1)

//-------------------------------------------------------------------------------

//setting up timer2 to generate an overflow interrupt every 512us:

TCCR2A &= 0xFC; //TCCR2A configuration:

//set timer2 to normal (not CTC) mode.

//timer2 simply always runs, overflows at >255 (it's a uint8_t), and sets an interrupt flag when it overflows.

TCCR2B |= 0x03; //TCCR2B configuration:

TCCR2B &= 0xF3; //set clock prescaler (divider) to 32, so effective frequency is:

//16000000_CPU_SPEED/(32_PRESCALER * 256_CLOCKS_TO_OVERFLOW) = 1,953.125 Hz, overflow every 512us

TIMSK2 |= (1 << TOIE2); //TIMSK2 (Timer Interrupt Mask 2) configuration:

//enable overflow interrupt. Timer Overflow Inerrupt Enable 2(as in, the one for timer2)

TCNT2 = 0x00; //TCNT2 (Timer/Counter register 2) configuration:

//set the timer2 counter to zero, to be sure it ACTUALLY STARTS at zero when our loop starts.

//Who knows what value it powered on with?

//-------------------------------------------------------------------------------

//all our sensitive one-time setup stuff is done, so we can now safely

sei(); //enable interrupts

}

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

// MAIN LOOP FUNCTION

//-------------------------------------------------------------------------------

void loop(){ //Loop forever:

output_data_if_ready(); //If we have data to output and it's time to send it, send it!

//-------------------------------------------------------------------------------

request_data_if_ready(); //If we have enough space in the buffer, and we're

//not in the process of loading more data already,

//request another 256 bytes.

//-------------------------------------------------------------------------------

load_data_if_available(); //If we have data coming in, load it into the buffer.

}

//-------------------------------------------------------------------------------

//-------------------------------------------------------------------------------

/* ALL HAIL MACHINE EMPIRE */

Source Audio's icon

From a short look at the code :
Arduino requests data by sending 0xFF or decimal 255.
After 256 bytes are received, or buffer filled, data is sent to DAC
and next data chunk requested via serial.
So all You need is to listen to 255 and send 256 values to arduino.
Expected are simply pairs of 8 bit ints 0 -255. high than low

personal_username's icon

Hi SourceAudio,

and thanks a lot for your support! ;-)
As you understand I'm quite new to serial communications protocols, and these days I'm fighting on two fronts! ;-)

Soo... yes, you are right, it looks like the Arduino is waiting for 256 two-bytes values.
Two things I still cannot figure out:

1) it looks like that this buffer has to be filled every 512 microseconds...! So basically I'm really starting to think if Max is designed to reach this very high speeds.
I mean, as I remember it is not recommended to instantiate a [metro] under 20 msecs, and now we are talking to have it at least 0.512 milliseconds.

2) I have no experience on sending the string, in the following patch I created two type of communication, one with [zl group] that sends all the data at once once it receives the request from the serial, and the second one by tweaking the "chunk" parameter of the serial, setting it to 256. At the moment I have no way to check if the device will be working with this setting, maybe tomorrow I'll be able to test it...

Max Patch
Copy patch and select New From Clipboard in Max.

Source Audio's icon

Maybe it is a time to state what is the goal of this project.
If You are just experimenting to see if audio samples can be sent
over serial in max and played back on arduino/DAC...
Then I think just go on trying and see if You can get it work, which I really doubt.
If You need a sample playback where computer is envolved than this is just nonesense.
If You need sinewave playback, as in the example patch You posted,
than You cold solve it by sending frequency to arduino, and creating sinewaves
directly from there.
One thing is certain - it is not possible to mix signal world with serial communication like that.
Your only chance would be to for example use peek~ to read the samples from the buffer,
scale the -1. - 1. float output to 12 bit integer, and progress 256 samples on every request from arduino.
Maybe Jasch's comport object could be more responsive regarding polling data from serial port.
Other option would be to stream data from max continiously , and rewrite arduino sketch to proceed the data to dac without requesting data.
------------

personal_username's icon

Hey, sorry for the late reply,
and thanks again for your interest.
Yes sure, I'm happy to share the goal of the project.

I'm having a sleep device on one side, which give me some information about the sleeping phase of a subject. I analyse it with Max and than would like to use Max to set some parameters to a TACS device, which is basically an electric stimulator. As the wave type is going to be a wave, my main focus is on how to have a sample-accurate resolution of a signal, so I can pack it and send it in real-time to the dac.

But when you say "One thing is certain - it is not possible to mix signal world with serial communication like that" I guess that it is technically not possible.
I was almost sure I could grab one way or another every sample of wave transforming a signal into a stream of data.

I guess I'll try the Arduino way then...

Source Audio's icon

Yes, audio or signal domain can't talk directly to nonsignal world in max,
it does jump over at vector size rate.
So getting audio to serial would not be possible like that.
If You would have audio in a buffer, and use uzi/peek~ combo
to send sample values to serial port, than it could do,
but not in the way arduino sketch You posted is set to.
----------
Maybe best way would really be to use arduino with toneAC library to create sinewaves, if that is what You need.
What is the input of the TACS looking like ?
I mean is it audio jack, serial port ...

personal_username's icon

Great, thanks a lot, I'll look into it
As for the Tacs' input it is a serial port

Source Audio's icon

toneAC produces squarewave, but with steep low pass it would sound almost as
sine.
But if You need pure sinewave, You can try
https://github.com/sensorium/Mozzi
or simpler :
https://github.com/cmasenas/SineWave/archive/master.zip