↑↓ move↵ open[] previous / next lesson? all shortcuts
Appearance
Theme
Type size
Reading

Focus mode hides the sidebars. Press f to toggle it anywhere.

Keyboard shortcuts
/
Search
[ ]
Previous / next lesson
t
Jump to the table of contents
r
Review queue
f
Focus mode
?
This panel
SkillA weekend

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.

AssumesTool: The Web Serial plotterSpineAnalog / mixed-signal hardware
On this pageA weekend
  1. SPI in one paragraph
  2. Step one: read the ID register
  3. Step two: configure
  4. Step three: read frames on data-ready
  5. Step four: scale and ship
  6. Do not lose samples

SPI in one paragraph

Figure 1. Four steps, in this order, because each one can be proved on its own. If you write all four before testing any, a single bug looks like four bugs.

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 says which clock edge it samples on and whether the clock idles high or low; the 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 and the reference , 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 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 / ) / 2²⁴. With VREF 4.5 V and 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 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 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 that drops one frame in a thousand produces spectra that are subtly wrong, and you will not know until the head tells you.

Recall
Why should the DRDY interrupt handler only set a flag?
Doing SPI or serial work inside an interrupt risks missing the next DRDY, blocking other interrupts, and race conditions. The main loop reads the frame when it sees the flag.
Recall
How do you convert a 24-bit two's-complement sample into a signed integer, and what goes wrong if you forget?
Assemble the three bytes into a 32-bit value and, if bit 23 is set, OR in 0xFF000000 to sign-extend. Forgetting makes negative voltages appear as large positive numbers, producing a cliff in the trace.
Recall
Why include a sample counter in every frame?
So the receiver can detect dropped frames. Missing samples silently distort spectra and timing.