Firmware and SPI for a biopotential chip
Reading a register, reading a frame on data-ready, unpacking 24-bit two's complement, and shipping samples to a computer without losing any. The four things every acquisition firmware does.
You are skimming: the title, the first figure, and the short version. Switch to Read in the header for the full page, or Deep to open every deep dive.
SPI in one paragraph
Four wires. The microcontroller drives a clock. On each clock edge one bit goes out on one wire and one bit comes in on another. A fourth wire, chip select, tells the chip that the conversation is for it. The chip’s datasheet says which clock edge it samples on and whether the clock idles high or low; the ADS1299 wants clock idle low, data captured on the falling edge, which in the usual notation is mode 1. Get that wrong and you read garbage that looks almost right, which is worse than nothing.
Step one: read the ID register
Everything else waits on this.
// ESP32 with the Arduino core; pins are examples
#include <SPI.h>
const int CS = 5, DRDY = 4, RST = 16, START = 17;
SPISettings ads(1000000, MSBFIRST, SPI_MODE1);
uint8_t readReg(uint8_t addr) {
SPI.beginTransaction(ads); digitalWrite(CS, LOW);
SPI.transfer(0x20 | addr); // RREG command: 001r rrrr
SPI.transfer(0x00); // read 1 register
uint8_t v = SPI.transfer(0x00);
digitalWrite(CS, HIGH); SPI.endTransaction();
return v;
}
void setup() {
Serial.begin(500000); SPI.begin();
pinMode(CS, OUTPUT); pinMode(DRDY, INPUT); pinMode(RST, OUTPUT); pinMode(START, OUTPUT);
digitalWrite(CS, HIGH); digitalWrite(START, LOW);
digitalWrite(RST, LOW); delay(1); digitalWrite(RST, HIGH); delay(10);
sendCmd(0x11); // SDATAC: stop continuous mode so registers can be read
Serial.printf("ID = 0x%02X (expect 0x3E)\n", readReg(0x00));
}
Start the clock slow, a megahertz. Speed comes later.
Step two: configure
Write the configuration registers with the values from the datasheet and the reference firmware, then read them back and print them. A register that does not read back what you wrote means a write that did not land (timing) or a bit that is read-only. Do not proceed until every one reads back.
Step three: read frames on data-ready
The chip pulls DRDY low when a sample set is ready. Attach an interrupt to that pin that sets a flag; in the main loop, when the flag is set, clock out the frame. For the ADS1299 that is 27 bytes: 3 status bytes then 8 channels of 3 bytes each, most significant byte first.
volatile bool ready = false;
void IRAM_ATTR onDrdy() { ready = true; }
int32_t chan[8];
void readFrame() {
uint8_t b[27];
SPI.beginTransaction(ads); digitalWrite(CS, LOW);
for (int i = 0; i < 27; i++) b[i] = SPI.transfer(0x00);
digitalWrite(CS, HIGH); SPI.endTransaction();
for (int c = 0; c < 8; c++) {
int32_t v = ((int32_t)b[3 + 3*c] << 16) | ((int32_t)b[4 + 3*c] << 8) | b[5 + 3*c];
if (v & 0x800000) v |= 0xFF000000; // sign-extend 24-bit two's complement
chan[c] = v;
}
}
The sign extension line is the one everyone gets wrong once. Without it, negative voltages appear as huge positive numbers and your trace has a cliff in it.
Step four: scale and ship
Counts to volts: one count is (2 × VREF / gain) / 2²⁴. With VREF 4.5 V and gain 24 that is about 22.35 nV. Multiply in floating point on the microcontroller, or ship raw counts and scale on the computer; shipping counts is fewer bytes and loses nothing.
Ship as binary, not text, once you are past the debugging stage: a start byte, a sample counter, 24 bytes of channels, an end byte. The sample counter is essential; it is how the computer knows if a frame was dropped. At 250 samples per second times 27 bytes that is under 7 kilobytes per second, trivial for USB or Wi-Fi. Match the OpenBCI Cyton’s frame format if you want every existing tool to read your board.
Do not lose samples
The interrupt sets a flag and nothing else; reading SPI inside the interrupt causes trouble. The main loop reads the frame promptly. Serial or Wi-Fi transmission happens after the frame is read, and if the link is slow, buffer frames in a ring and send in batches. Test by counting: run for ten minutes and check the sample counter never skips. A firmware that drops one frame in a thousand produces spectra that are subtly wrong, and you will not know until the phantom head tells you.