ADS1256 - Improved code for faster acquisition
In this video I show how I improved my Arduino/STM32 code to read data from the ADS1256 24-bit 8-channel AD converter.
The two most important changes are the following:
I use an interrupt to listen to the status of the DRDY pin of the ADS1256. When it goes low, the ISR captures the change and modifies the value of a variable.
I use Serial.write() instead of Serial.print(). This makes data processing a bit more meticulous because we have to convert the bytes into 24-bit (3-byte) numbers on the computer again, but the whole process is much more faster.
Furthermore, I improved the serial communication in my client software and instead of using events (like I did before), I am constantly polling the serial port and fetching the available data in packets. An example of this can be found in my other blog post.
ADS1256 datasheet link: LINK
ADS1256 YouTube playlist link: LINK
I still need to improve the code because I cannot reach the absolute highest sampling rate of 30 ksps, but 24 ksps is already fast enough for me.
Arduino/STM32 source code
// Pin configuration - STM32F103C8T6 /* SPI default pins: MOSI - PA7 // DIN MISO - PA6 // DOUT SCK - PA5 // SCLK SS - PA4 // CS -------------------- MOSI: Master OUT Slave IN -> DIN MISO: Master IN Slave OUT -> DOUT -------------------- Other pins RST - PA3 DRDY - PA2 // this is an interrupt pin PDWN/SYNC - +3.3 V or PA1 */ //-------------------------------------------------------------------------------- //Clock rate /* f_CLKIN = 7.68 MHz tau = 130.2 ns */ //-------------------------------------------------------------------------------- #include <SPI.h> //SPI comunication void setup() { Serial.begin(115200); delay(1000); initialize_ADS1256(); delay(1000); reset_ADS1256(); userDefaultRegisters(); //printInstructions(); attachInterrupt(digitalPinToInterrupt(PA2), checkDReady, FALLING); //FALLING - DRDY goes low } //-------------------------------------------------------------------------------- //Variables //Booleans volatile boolean dataReady; //It is very important to have it defined as volatile! double VREF = 2.50; //VREF for converting the raw data to human-readable voltage //Pins const byte CS_pin = PA4; //goes to CS on ADS1256 const byte DRDY_pin = PA2; //goes to DRDY on ADS1256 const byte RESET_pin = PA3; //goes to RST on ADS1256 //const byte PDWN_PIN = PA1; //Goes to the PDWN pin - Alternatively can be permanently tied to 3.3 V //Values for registers uint8_t registerAddress; //address of the register, both for reading and writing - selects the register uint8_t registerValueR; //this is used to READ a register uint8_t registerValueW; //this is used to WRITE a register int32_t registerData; //this is used to store the data read from the register (for the AD-conversion) uint8_t directCommand; //this is used to store the direct command for sending a command to the ADS1256 String PrintMessage; //this is used to concatenate stuff into before printing it out. byte outputBuffer[3]; //3-byte (24-bit) buffer for the fast acquisition - Single-channel, continuous byte differentialBuffer[12]; //4x3-byte buffer for the fast differential-channel acquisition - byte singleBuffer[24]; //8x3-byte buffer for the fast single-ended-channel acquisition //float StartTime; //This only serves test purposes //-------------------------------------------------------------------------------- void loop() { if (Serial.available() > 0) { char commandCharacter = Serial.read(); //we use characters (letters) for controlling the switch-case switch (commandCharacter) //based on the command character, we decide what to do { case 'r': //this case is used to READ the value of a register while (!Serial.available()); //wait for the serial registerAddress = Serial.parseInt(); //parse the address of the register /* //Wait for the input while (!Serial.available()); Text before the print PrintMessage = "*Value of register " + String(registerAddress) + " is " + String(readRegister(registerAddress)); Serial.println(PrintMessage); PrintMessage = ""; //resetting value */ break; case 'w': //this case is used to WRITE the value of a register while (!Serial.available()); //wait for the serial registerAddress = Serial.parseInt(); //Store the register in registerAddress delay(100); while (!Serial.available()); registerValueW = Serial.parseInt(); //Store the value to be written delay(100); writeRegister(registerAddress, registerValueW); delay(500); break; case 't': //this case is used to print a message to the serial terminal. Just to test the connection...etc. Serial.println("*Test message triggered by serial command"); break; case 'O': //this case is used to read a single value from the AD converter readSingle(); break; case 'R': //this does a RESET on the ADS1256 reset_ADS1256(); break; case 's': //SDATAC - Stop Reading Data Continously SPI.transfer(B00001111); break; case 'A': //Single channel continous reading - MUX is manual, can be single and differential too readSingleContinuous(); break; case 'C': //Single-ended mode cycling cycleSingleEnded(); break; case 'D': //differential mode cycling cycleDifferential(); break; case 'd': //direct command while (!Serial.available()); directCommand = Serial.parseInt(); sendDirectCommand(directCommand); break; case 'U'://Set everything back to default userDefaultRegisters(); break; } } } //-------------------------------------------------------------------------------- //Functions void checkDReady() //Function for the ISR { dataReady = true; } unsigned long readRegister(uint8_t registerAddress) //Function for READING a selected register { SPI.beginTransaction(SPISettings(1920000, MSBFIRST, SPI_MODE1)); //SPI_MODE1 = output edge: rising, data capture: falling; clock polarity: 0, clock phase: 1. digitalWrite(CS_pin, LOW); //CS must stay LOW during the entire sequence [Ref: P34, T24] SPI.transfer(0x10 | registerAddress); //0x10 = 0001000 = RREG - OR together the two numbers (command + address) SPI.transfer(0x00); //2nd (empty) command byte delayMicroseconds(5); //see t6 in the datasheet registerValueR = SPI.transfer(0xFF); //read out the register value digitalWrite(CS_pin, HIGH); SPI.endTransaction(); return registerValueR; } void writeRegister(uint8_t registerAddress, uint8_t registerValueW) { SPI.beginTransaction(SPISettings(1920000, MSBFIRST, SPI_MODE1)); //SPI_MODE1 = output edge: rising, data capture: falling; clock polarity: 0, clock phase: 1. digitalWrite(CS_pin, LOW); //CS must stay LOW during the entire sequence [Ref: P34, T24] delayMicroseconds(5); //see t6 in the datasheet SPI.transfer(0x50 | registerAddress); // 0x50 = 01010000 = WREG SPI.transfer(0x00); SPI.transfer(registerValueW); digitalWrite(CS_pin, HIGH); SPI.endTransaction(); } void reset_ADS1256() { SPI.beginTransaction(SPISettings(1920000, MSBFIRST, SPI_MODE1)); // initialize SPI with clock, MSB first, SPI Mode1 digitalWrite(CS_pin, LOW); delayMicroseconds(7); SPI.transfer(0xFE); //Reset command delay(2); //Minimum 0.6ms required for Reset to finish. SPI.transfer(0x0F); //Issue SDATAC (any 8-bit value would complete the RESET command) delayMicroseconds(100); digitalWrite(CS_pin, HIGH); SPI.endTransaction(); } void initialize_ADS1256() //starting up the chip by making the necessary steps. This goes into the setup() later. { //Chip select pinMode(CS_pin, OUTPUT); //Chip select is an output digitalWrite(CS_pin, LOW); //DRDY pinMode(DRDY_pin, INPUT); //Reset pinMode(RESET_pin, OUTPUT); //We do a manual chip reset on the ADS1256 - Datasheet Page 27/ RESET digitalWrite(RESET_pin, LOW); delay(100); digitalWrite(RESET_pin, HIGH); //RESET is set to high delay(500); SPI.begin(); } void readSingle() //Reading a single value ONCE using the RDATA command { registerData = 0; // every time we call this function, this should be 0 in the beginning! SPI.beginTransaction(SPISettings(1920000, MSBFIRST, SPI_MODE1)); digitalWrite(CS_pin, LOW); //REF: P34: "CS must stay low during the entire command sequence" while (dataReady == false) {} //Wait for DRDY to go LOW SPI.transfer(B00000001); //Issue RDATA (0000 0001) command delayMicroseconds(7); //Wait t6 time (~6.51 us) REF: P34, FIG:30. //step out the data: MSB | mid-byte | LSB, registerData = SPI.transfer(0x0F); //MSB comes in, first 8 bit is updated registerData <<= 8; //MSB gets shifted LEFT by 8 bits registerData |= SPI.transfer(0x0F); //MSB | Mid-byte // '|=' compound bitwise OR operator registerData <<= 8; //MSB | Mid-byte gets shifted LEFT by 8 bits registerData |= SPI.transfer(0x0F); //(MSB | Mid-byte) | LSB - final result //After this, DRDY should go HIGH automatically Serial.print("Raw data: "); Serial.println(registerData); //prints the raw data (24-bit number) Serial.print("Voltage: "); convertToVoltage(registerData); //prints the converted data (PGA should be 0!) digitalWrite(CS_pin, HIGH); //We finished the command sequence, so we switch it back to HIGH SPI.endTransaction(); } void readSingleContinuous() //Reads the recently selected channel using RDATAC { SPI.beginTransaction(SPISettings(1920000, MSBFIRST, SPI_MODE1)); digitalWrite(CS_pin, LOW); //REF: P34: "CS must stay low during the entire command sequence" //These variables serve only testing purposes! //uint32_t loopcounter = 0; //StartTime = micros(); //-------------------------------------------- while (dataReady == false) {} //Wait until DRDY does low, then issue the command SPI.transfer(B00000011); //Issue RDATAC (0000 0011) command after DRDY goes low delayMicroseconds(7); //Wait t6 time (~6.51 us) REF: P34, FIG:30. while (Serial.read() != 's') { //while (GPIOA->regs->IDR & 0x0004){} //direct port access to A2 (DRDY) pin - less reliable polling alternative while (dataReady == false) {} //waiting for the dataReady ISR //Reading a single input continuously using the RDATAC //step out the data: MSB | mid-byte | LSB outputBuffer[0] = SPI.transfer(0); // MSB comes in outputBuffer[1] = SPI.transfer(0); // Mid-byte outputBuffer[2] = SPI.transfer(0); // LSB - final conversion result //After this, DRDY should go HIGH automatically Serial.write(outputBuffer, sizeof(outputBuffer)); //this buffer is [3] dataReady = false; //reset dataReady manually /* //These variables only serve test purposes! loopcounter++; //if(micros() - StartTime >= 5000000) //5 s if(loopcounter >= 150000) { Serial.print(" Loops: "); Serial.println(loopcounter++); Serial.println(micros() - StartTime); break; //exit the whole thing } */ } SPI.transfer(B00001111); //SDATAC stops the RDATAC - the received 's' just breaks the while(), this stops the acquisition digitalWrite(CS_pin, HIGH); //We finished the command sequence, so we switch it back to HIGH SPI.endTransaction(); } void cycleSingleEnded() { int cycle = 0; SPI.beginTransaction(SPISettings(1920000, MSBFIRST, SPI_MODE1)); digitalWrite(CS_pin, LOW); //CS must stay LOW during the entire sequence [Ref: P34, T24] while (Serial.read() != 's') { for (cycle = 0; cycle < 8; cycle++) { //we cycle through all the 8 single-ended channels with the RDATAC //INFO: //RDATAC = B00000011 //SYNC = B11111100 //WAKEUP = B11111111 //--------------------------------------------------------------------------------------------- /*Some comments regarding the cycling: When we start the ADS1256, the preconfiguration already sets the MUX to [AIN0+AINCOM]. When we start the RDATAC (this function), the default MUX ([AIN0+AINCOM]) will be included in the cycling which means that the first readout will be the [AIN0+AINCOM]. But, before we read the data from the [AIN0+AINCOM], we have to switch to the next register already, then start RDATA. This is demonstrated in Figure 19 on Page 21. Therefore, in order to get the 8 channels nicely read and formatted, we have to start the cycle with the 2nd input of the ADS1256 ([AIN1+AINCOM]) and finish with the first ([AIN0+AINCOM]). \ CH1 | CH2 CH3 CH4 CH5 CH6 CH7 CH8 \ CH1 | CH2 CH3 ... The switch-case is between the two '|' characters The output (one line of values) is between the two '\' characters. */ //------------------------------------------------------------------------------------------- //Steps are on Page 21 of the datasheet while (dataReady == false) {} //direct port access to A2 (DRDY) pin //Step 1. - Updating MUX switch (cycle) { //Channels are written manually case 0: //Channel 2 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B00011000); //AIN1+AINCOM break; case 1: //Channel 3 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B00101000); //AIN2+AINCOM break; case 2: //Channel 4 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B00111000); //AIN3+AINCOM break; case 3: //Channel 5 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B01001000); //AIN4+AINCOM break; case 4: //Channel 6 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B01011000); //AIN5+AINCOM break; case 5: //Channel 7 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B01101000); //AIN6+AINCOM break; case 6: //Channel 8 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B01111000); //AIN7+AINCOM break; case 7: //Channel 1 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B00001000); //AIN0+AINCOM break; } //Step 2. SPI.transfer(B11111100); //SYNC delayMicroseconds(4); //t11 delay 24*tau = 3.125 us //delay should be larger, so we delay by 4 us SPI.transfer(B11111111); //WAKEUP //Step 3. //Issue RDATA (0000 0001) command SPI.transfer(B00000001); delayMicroseconds(7); //Wait t6 time (~6.51 us) REF: P34, FIG:30. //step out the data: MSB | mid-byte | LSB singleBuffer[(3 * cycle)] = SPI.transfer(0x0F); //MSB comes in, first 8 bit is updated singleBuffer[(3 * cycle) + 1] = SPI.transfer(0x0F); //Mid-byte singleBuffer[(3 * cycle) + 2] = SPI.transfer(0x0F); //LSB - final result dataReady = false; //After this, DRDY should go HIGH automatically } //Dump the buffer after all 8 channels are read Serial.write(singleBuffer, 24); } SPI.transfer(B00001111); //SDATAC stops the RDATAC - the received 's' just breaks the while(), this stops the acquisition digitalWrite(CS_pin, HIGH); //We finished the command sequence, so we switch it back to HIGH SPI.endTransaction(); } void cycleDifferential() //APPROVED { SPI.beginTransaction(SPISettings(1920000, MSBFIRST, SPI_MODE1)); //Set the AIN0+AIN1 as inputs manually digitalWrite(CS_pin, LOW); //CS must stay LOW during the entire sequence [Ref: P34, T24] SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B00000001); //AIN0+AIN1 digitalWrite(CS_pin, HIGH); delay(50); digitalWrite(CS_pin, LOW); //CS must stay LOW during the entire sequence [Ref: P34, T24] while (Serial.read() != 's') { for (int cycle = 0; cycle < 4; cycle++) { //Steps are on Page21 //Step 1. - Updating MUX //DRDY has to go low while (dataReady == false) {} switch (cycle) { case 0: //Channel 2 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B00100011); //AIN2+AIN3 break; case 1: //Channel 3 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B01000101); //AIN4+AIN5 break; case 2: //Channel 4 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B01100111); //AIN6+AIN7 break; case 3: //Channel 1 SPI.transfer(0x50 | 1); // 0x50 = WREG //1 = MUX SPI.transfer(0x00); SPI.transfer(B00000001); //AIN0+AIN1 break; } SPI.transfer(B11111100); //SYNC delayMicroseconds(4); //t11 delay 24*tau = 3.125 us //delay should be larger, so we delay by 4 us SPI.transfer(B11111111); //WAKEUP //Step 3. SPI.transfer(B00000001); //Issue RDATA (0000 0001) command delayMicroseconds(7); //Wait t6 time (~6.51 us) REF: P34, FIG:30. differentialBuffer[(3 * cycle)] = SPI.transfer(0x0F); //MSB comes in, first 8 bit is updated // '|=' compound bitwise OR operator differentialBuffer[(3 * cycle) + 1] = SPI.transfer(0x0F); //Mid-byte differentialBuffer[(3 * cycle) + 2] = SPI.transfer(0x0F); //LSB - final result dataReady = false; } //Dump the buffer after all 4 channels are read Serial.write(differentialBuffer, 12); } SPI.transfer(B00001111); //SDATAC stops the RDATAC - the received 's' just breaks the while(), this stops the acquisition digitalWrite(CS_pin, HIGH); //We finished the command sequence, so we switch it back to HIGH SPI.endTransaction(); } void sendDirectCommand(uint8_t directCommand) { //Direct commands can be found in the datasheet Page 34, Table 24. SPI.beginTransaction(SPISettings(1700000, MSBFIRST, SPI_MODE1)); digitalWrite(CS_pin, LOW); //REF: P34: "CS must stay low during the entire command sequence" delayMicroseconds(5); SPI.transfer(directCommand); //Send Command delayMicroseconds(5); digitalWrite(CS_pin, HIGH); //REF: P34: "CS must stay low during the entire command sequence" SPI.endTransaction(); } void userDefaultRegisters() { // This function is "manually" updating the values of the registers then reads them back. // This function can be used in the setup() after performing an initialization-reset process /* REG VAL USE 0 54 Status Register, Everyting Is Default, Except Auto - Cal 1 1 Multiplexer Register, AIN0 POS, AIN1 POS 2 0 ADCON, Everything is OFF, PGA = 0 3 132 DataRate = 100 SPS */ //We update the 4 registers that we are going to use delay(500); writeRegister(0x00, B00110110); //STATUS: bit1: bufen=1; bit2: acal=1; rest is not important or factory default delay(200); writeRegister(0x01, B00000001); //MUX AIN0+AIN1 delay(200); writeRegister(0x02, B00000000); //ADCON - PGA = 0 (+/- 5 V) delay(200); writeRegister(0x03, B10000100); //100SPS delay(500); sendDirectCommand(B11110000); //Offset and self-gain calibration } void printInstructions() { //This function should be in the setup() and it shows the commands - not used PrintMessage = "*Use the following letters to send a command to the device:" + String("\n") + "*r - Read a register. Example: 'r1' - reads the register 1" + String("\n") + "*w - Write a register. Example: 'w1 8' - changes the value of the 1st register to 8." + String("\n") + "*O - Single readout. Example: 'O' - Returns a single value from the ADS1256." + String("\n") + "*A - Single, continuous reading with manual MUX setting." + String("\n") + "*C - Cycling the ADS1256 Input multiplexer in single-ended mode (8 channels). " + String("\n") + "*D - Cycling the ADS1256 Input multiplexer in differential mode (4 channels). " + String("\n") + "*R - Reset ADS1256. Example: 'R' - Resets the device, everything is set to default." + String("\n") + "*s - SDATAC: Stop Read Data Continously." + String("\n") + "*U - User Default Registers." + String("\n") + "*d - Send direct command."; Serial.println(PrintMessage); PrintMessage = ""; //Reset (empty) variable. } void convertToVoltage(int32_t registerData) { if (registerData >> 23 == 1) //if the 24th bit (sign) is 1, the number is negative { registerData = registerData - 16777216; //conversion for the negative sign //"mirroring" around zero } //This is only valid if PGA = 0 (2^0). Otherwise the voltage has to be divided by 2^(PGA) double voltage = ((2 * VREF) / 8388608) * registerData; //5.0 = Vref; 8388608 = 2^{23} - 1 Serial.println(voltage, 8); //print it on serial }