I'm back again with the resistor DAC on an Arduino Uno as a simple audio generator. I'm not clear how I came to do so many blogs on it, but this is going to be the last one.
Here's the latest sketch with commands for setting the output level as well as the frequency. This sketch is for a Uno, so may need adaption for other boards. I've changed the command to be 'FUNCtion' rather than 'SOURce' simply because I can (though I probably ought to read the SCPI spec and see what you should use, in case they recommend command usage).
#include <scpiparser.h>
#include <Arduino.h>
#include #include struct scpi_parser_context ctx; float frequency; float voltage; scpi_error_t identify(struct scpi_parser_context* context, struct scpi_token* command); scpi_error_t get_frequency(struct scpi_parser_context* context, struct scpi_token* command); scpi_error_t set_frequency(struct scpi_parser_context* context, struct scpi_token* command); scpi_error_t get_voltage(struct scpi_parser_context* context, struct scpi_token* command); scpi_error_t set_voltage(struct scpi_parser_context* context, struct scpi_token* command); unsigned int tableOffset = 0; // output count unsigned int tableStep = 5120; unsigned char amplitudeSelect = 0; // // --- Sine table - generated by sineTable.exe // unsigned int sineTable[512] = { 0x8000,0x8192,0x8324,0x84b6,0x8647,0x87d9,0x896a,0x8afb,0x8c8b,0x8e1b,0x8fab,0x9139,0x92c8,0x9455,0x95e2,0x976d, 0x98f8,0x9a82,0x9c0b,0x9d93,0x9f19,0xa09f,0xa223,0xa3a6,0xa528,0xa6a8,0xa826,0xa9a3,0xab1f,0xac98,0xae11,0xaf87, 0xb0fb,0xb26e,0xb3de,0xb54d,0xb6ba,0xb824,0xb98c,0xbaf2,0xbc56,0xbdb8,0xbf17,0xc073,0xc1ce,0xc325,0xc47a,0xc5cd, 0xc71c,0xc869,0xc9b4,0xcafb,0xcc3f,0xcd81,0xcebf,0xcffb,0xd133,0xd269,0xd39b,0xd4ca,0xd5f5,0xd71d,0xd842,0xd964, 0xda82,0xdb9d,0xdcb4,0xddc7,0xded7,0xdfe3,0xe0ec,0xe1f1,0xe2f2,0xe3ef,0xe4e8,0xe5dd,0xe6cf,0xe7bd,0xe8a6,0xe98c, 0xea6d,0xeb4a,0xec24,0xecf9,0xedca,0xee96,0xef5f,0xf023,0xf0e2,0xf19e,0xf255,0xf307,0xf3b5,0xf45f,0xf504,0xf5a5, 0xf641,0xf6d9,0xf76c,0xf7fa,0xf884,0xf909,0xf98a,0xfa05,0xfa7d,0xfaef,0xfb5d,0xfbc5,0xfc29,0xfc89,0xfce3,0xfd39, 0xfd8a,0xfdd6,0xfe1d,0xfe5f,0xfe9d,0xfed5,0xff09,0xff38,0xff62,0xff87,0xffa7,0xffc2,0xffd8,0xffe9,0xfff6,0xfffd, 0xffff,0xfffd,0xfff6,0xffe9,0xffd8,0xffc2,0xffa7,0xff87,0xff62,0xff38,0xff09,0xfed5,0xfe9d,0xfe5f,0xfe1d,0xfdd6, 0xfd8a,0xfd39,0xfce3,0xfc89,0xfc29,0xfbc5,0xfb5d,0xfaef,0xfa7d,0xfa05,0xf98a,0xf909,0xf884,0xf7fa,0xf76c,0xf6d9, 0xf641,0xf5a5,0xf504,0xf45f,0xf3b5,0xf307,0xf255,0xf19e,0xf0e2,0xf023,0xef5f,0xee96,0xedca,0xecf9,0xec24,0xeb4a, 0xea6d,0xe98c,0xe8a6,0xe7bd,0xe6cf,0xe5dd,0xe4e8,0xe3ef,0xe2f2,0xe1f1,0xe0ec,0xdfe3,0xded7,0xddc7,0xdcb4,0xdb9d, 0xda82,0xd964,0xd842,0xd71d,0xd5f5,0xd4ca,0xd39b,0xd269,0xd133,0xcffb,0xcebf,0xcd81,0xcc3f,0xcafb,0xc9b4,0xc869, 0xc71c,0xc5cd,0xc47a,0xc325,0xc1ce,0xc073,0xbf17,0xbdb8,0xbc56,0xbaf2,0xb98c,0xb824,0xb6ba,0xb54d,0xb3de,0xb26e, 0xb0fb,0xaf87,0xae11,0xac98,0xab1f,0xa9a3,0xa826,0xa6a8,0xa528,0xa3a6,0xa223,0xa09f,0x9f19,0x9d93,0x9c0b,0x9a82, 0x98f8,0x976d,0x95e2,0x9455,0x92c8,0x9139,0x8fab,0x8e1b,0x8c8b,0x8afb,0x896a,0x87d9,0x8647,0x84b6,0x8324,0x8192, 0x8000,0x7e6d,0x7cdb,0x7b49,0x79b8,0x7826,0x7695,0x7504,0x7374,0x71e4,0x7054,0x6ec6,0x6d37,0x6baa,0x6a1d,0x6892, 0x6707,0x657d,0x63f4,0x626c,0x60e6,0x5f60,0x5ddc,0x5c59,0x5ad7,0x5957,0x57d9,0x565c,0x54e0,0x5367,0x51ee,0x5078, 0x4f04,0x4d91,0x4c21,0x4ab2,0x4945,0x47db,0x4673,0x450d,0x43a9,0x4247,0x40e8,0x3f8c,0x3e31,0x3cda,0x3b85,0x3a32, 0x38e3,0x3796,0x364b,0x3504,0x33c0,0x327e,0x3140,0x3004,0x2ecc,0x2d96,0x2c64,0x2b35,0x2a0a,0x28e2,0x27bd,0x269b, 0x257d,0x2462,0x234b,0x2238,0x2128,0x201c,0x1f13,0x1e0f,0x1d0d,0x1c10,0x1b17,0x1a22,0x1930,0x1842,0x1759,0x1673, 0x1592,0x14b5,0x13db,0x1306,0x1235,0x1169,0x10a0,0x0fdc,0x0f1d,0x0e61,0x0daa,0x0cf8,0x0c4a,0x0ba0,0x0afb,0x0a5a, 0x09be,0x0926,0x0893,0x0805,0x077b,0x06f6,0x0675,0x05fa,0x0582,0x0510,0x04a2,0x043a,0x03d6,0x0376,0x031c,0x02c6, 0x0275,0x0229,0x01e2,0x01a0,0x0162,0x012a,0x00f6,0x00c7,0x009d,0x0078,0x0058,0x003d,0x0027,0x0016,0x0009,0x0002, 0x0000,0x0002,0x0009,0x0016,0x0027,0x003d,0x0058,0x0078,0x009d,0x00c7,0x00f6,0x012a,0x0162,0x01a0,0x01e2,0x0229, 0x0275,0x02c6,0x031c,0x0376,0x03d6,0x043a,0x04a2,0x0510,0x0582,0x05fa,0x0675,0x06f6,0x077b,0x0805,0x0893,0x0926, 0x09be,0x0a5a,0x0afb,0x0ba0,0x0c4a,0x0cf8,0x0daa,0x0e61,0x0f1d,0x0fdc,0x10a0,0x1169,0x1235,0x1306,0x13db,0x14b5, 0x1592,0x1673,0x1759,0x1842,0x1930,0x1a22,0x1b17,0x1c10,0x1d0d,0x1e0e,0x1f13,0x201c,0x2128,0x2238,0x234b,0x2462, 0x257d,0x269b,0x27bd,0x28e2,0x2a0a,0x2b35,0x2c64,0x2d96,0x2ecc,0x3004,0x3140,0x327e,0x33c0,0x3504,0x364b,0x3796, 0x38e3,0x3a32,0x3b85,0x3cda,0x3e31,0x3f8c,0x40e8,0x4247,0x43a9,0x450d,0x4673,0x47db,0x4945,0x4ab2,0x4c21,0x4d91, 0x4f04,0x5078,0x51ee,0x5367,0x54e0,0x565c,0x57d9,0x5957,0x5ad7,0x5c59,0x5ddc,0x5f60,0x60e6,0x626c,0x63f4,0x657d, 0x6707,0x6892,0x6a1d,0x6baa,0x6d37,0x6ec6,0x7054,0x71e4,0x7374,0x7504,0x7695,0x7826,0x79b8,0x7b49,0x7cdb,0x7e6d}; void setup() { struct scpi_command* scpiCommand; /* First, initialise the parser. */ scpi_init(&ctx); /* * After initialising the parser, we set up the command tree. Ours is * * *IDN? -> identify * :FUNC * :FREQuency -> set_frequency * :FREQuency? -> get_frequency * :VOLTage -> set_amplitude * :VOLTage? -> get_amplitude */ scpi_register_command(ctx.command_tree, SCPI_CL_SAMELEVEL, "*IDN?", 5, "*IDN?", 5, identify); scpiCommand = scpi_register_command(ctx.command_tree, SCPI_CL_CHILD, "FUNCTION", 8, "FUNC", 4, NULL); scpi_register_command(scpiCommand, SCPI_CL_CHILD, "FREQUENCY", 9, "FREQ", 4, set_frequency); scpi_register_command(scpiCommand, SCPI_CL_CHILD, "FREQUENCY?", 10, "FREQ?", 5, get_frequency); scpi_register_command(scpiCommand, SCPI_CL_CHILD, "VOLTAGE", 7, "VOLT", 4, set_voltage); scpi_register_command(scpiCommand, SCPI_CL_CHILD, "VOLTAGE?", 8, "VOLT?", 5, get_voltage); frequency = 1e3; // set the pins used as outputs: PORTD = 0; pinMode(0, OUTPUT); pinMode(1, OUTPUT); pinMode(2, OUTPUT); pinMode(3, OUTPUT); pinMode(4, OUTPUT); pinMode(5, OUTPUT); pinMode(6, OUTPUT); pinMode(7, OUTPUT); pinMode(A0, OUTPUT); pinMode(A1, OUTPUT); pinMode(A2, OUTPUT); pinMode(A3, OUTPUT); digitalWrite(A0, HIGH); digitalWrite(A1, HIGH); digitalWrite(A2, HIGH); digitalWrite(A3, LOW); // timer 2 set up cli(); // disable interrupts TCCR2A = 0; // control register all 0 TCCR2B = 0; // control register all 0 TCNT2 = 0; // set count to 0 OCR2A = 159; // period = 160 x 1/16MHz = 10uS TCCR2A |= (1 << WGM21); // mode is clear on match TCCR2B |= (1 << CS20); // no prescaler TIMSK2 |= (1 << OCIE2A); // enable interrupt on match sei(); // enable interrupts Serial.begin(9600); } // timer 2 interrupt routine ISR(TIMER2_COMPA_vect) { tableOffset = tableOffset + tableStep; PORTD = (sineTable[tableOffset >> 7]) >> 8; } // main loop void loop() { // char line_buffer[256]; char line_buffer[128]; unsigned char read_length; while(1) { /* Read in a line and execute it. */ read_length = Serial.readBytesUntil('\n', line_buffer, 256); if(read_length >0) if(line_buffer[read_length-1]=='\r') read_length = read_length - 1; if(read_length > 0) { scpi_execute_command(&ctx, line_buffer, read_length); } } } /* * Respond to *IDN? */ scpi_error_t identify(struct scpi_parser_context* context, struct scpi_token* command) { scpi_free_tokens(command); Serial.println("E14,Arduino Audio Signal Generator,1,10"); return SCPI_SUCCESS; } /** * Read the current frequency setting. */ scpi_error_t get_frequency(struct scpi_parser_context* context, struct scpi_token* command) { // float voltage; Serial.println(frequency,4); scpi_free_tokens(command); return SCPI_SUCCESS; } /** * Set the audio output frequency. */ scpi_error_t set_frequency(struct scpi_parser_context* context, struct scpi_token* command) { struct scpi_token* args; struct scpi_numeric output_numeric; // unsigned char output_value; args = command; while(args != NULL && args->type == 0) { args = args->next; } output_numeric = scpi_parse_numeric(args->value, args->length, 1e3, 20, 2e4); if(output_numeric.length == 0 || (output_numeric.length == 2 && output_numeric.unit[0] == 'H' && output_numeric.unit[1] == 'z')) { tableStep = (unsigned int)(65536.0 * output_numeric.value / 100000.0); frequency = (unsigned long)constrain(output_numeric.value, 20, 2e4); } else { scpi_error error; error.id = -200; error.description = "Command error;Invalid unit"; error.length = 26; scpi_queue_error(&ctx, error); scpi_free_tokens(command); return SCPI_SUCCESS; } scpi_free_tokens(command); return SCPI_SUCCESS; } /** * Read the current voltage setting. */ scpi_error_t get_voltage(struct scpi_parser_context* context, struct scpi_token* command) { Serial.println(voltage,4); scpi_free_tokens(command); return SCPI_SUCCESS; } /** * Set the audio output amplitude. */ scpi_error_t set_voltage(struct scpi_parser_context* context, struct scpi_token* command) { struct scpi_token* args; struct scpi_numeric output_numeric; float testLevel; args = command; while(args != NULL && args->type == 0) { args = args->next; } output_numeric = scpi_parse_numeric(args->value, args->length, 0.4, 0.050, 0.8); if(output_numeric.length == 0 || (output_numeric.length == 1 && output_numeric.unit[0] == 'V')) { // tableStep = (unsigned int)(65536.0 * output_numeric.value / 100000.0); voltage = constrain(output_numeric.value, 0.05, 0.8); amplitudeSelect = 0; testLevel = 0.05; while(amplitudeSelect < 16) { if(voltage <= testLevel) break; testLevel = testLevel + 0.05; amplitudeSelect = amplitudeSelect + 1; } voltage = testLevel; amplitudeSelect = 15 - amplitudeSelect; } else { scpi_error error; error.id = -200; error.description = "Command error;Invalid unit"; error.length = 26; scpi_queue_error(&ctx, error); scpi_free_tokens(command); return SCPI_SUCCESS; } digitalWrite(A0, (amplitudeSelect & 0x01)); digitalWrite(A1, (amplitudeSelect & 0x02)>>1); digitalWrite(A2, (amplitudeSelect & 0x04)>>2); digitalWrite(A3, (amplitudeSelect & 0x08)>>3); scpi_free_tokens(command); return SCPI_SUCCESS; }
Here it is working with PuTTY:
The output level is in volts and is constrained to the range 50mV to 800mV in steps of 50mV. That's the peak-to-peak voltage; here's what 2kHz and 800mV looks like on the 'scope:
The firmware will take the next highest level - you can see that in the line where I try to set it to 23mV and it returns 50mV for the current level.
I had one problem with doing all this. Initially, when I put in the two new commands, it didn't work and the firmware locked up - it didn't even respond to the IDN request. That turned out to be because the SCPI library uses a fair amount of memory when the program runs; with my sine table (which uses up half of the processor's RAM) in there it was running out of space. I've temporarily cured that by reducing the line buffer from 256 to 128 characters. The sine table doesn't have to be ints (I made it int originally because I was thinking I might be scaling the values) and so reducing that to unsigned chars would be a better solution, but I didn't want to have to redo the table at this stage. If you use the library keep an eye on the dynamic memory allocated - it's a bit confusing to debug if you don't realise what's going on.
I'm going to leave this here, rather than finish it off as a full project. If you wanted to take it further, issues to think about include:
a) Power rails - using the +5V rail from the Arduino Uno leaves the resulting waveforms somewhat noisy.
b) There is some frequency variation evident on the higher frequency waveforms. I haven't investigated further, but it probably means either that there's something with a higher priority than the timer 2 interrupt or something is disabling all interrupts for short periods.
c) Trying to use an LM324 wan't very inspired.
Part one: Arduino: R-2R Experiment
Part two: Arduino: R-2R: Sine On You Crazy Diamond
Part three: Arduino: R-2R: Buffer, Attenuate, and Filter
Part four: Arduino: R-2R: "We Interrupt This Programme..."
Part five: Arduino: R-2R: "Resistance is..."?
Part six: Arduino: R-2R: Setting the Output Frequency
Part seven: Arduino: R-2R; "A Sweep is as Lucky, as Lucky Can Be..."
Top Comments