On Teensy 4.x, you can use
MCP3208 via 24-byte transfers (or multiples thereof), with SPI clock at 1 MHz, giving you the state of all 8 MCP3208 analog inputs in 0.2 milliseconds (more specifically, 192 µs plus setup overhead). Using VCC=3.3V, you'll probably want to use 1k linear potentiometers; each potentiometer then dissipates 3.3mA. (The ends of the pot go to 3.3V and GND, and the wiper to the analog input on the MCP3208. You may wish to filter that 3.3V line and use it for both VDD and VREF on the MCP3208.) The MCP3208 itself consumes less than a milliamp, so round up to say 30mA at 3.3V per fully populated MCP3208 with 1k linear potentiometers. (It's not much, something like two bright indicator LEDs.)
The content of the 3 bytes sent and received via SPI per ADC reading are described in figure 6-1 (if using mode0) and 6-2 (mode1) in the datasheet I linked at the beginning of this post.
For output, the first byte will have value 6 for the four first channels, and 7 for four next channels; the next byte will be 0 for channels 1 and 5, 64 for channels 2 and 6, 128 for channels 3 and 7, and 192 for channels 4 and 8. In other words, the send buffer for all eight channels will be
Code:
const unsigned char mcp3208_out[24] = {
0x06,0x00,0x00, 0x06,0x40,0x00, 0x06,0x80,0x00, 0x06,0xC0,0x00,
0x07,0x00,0x00, 0x07,0x40,0x00, 0x07,0x80,0x00, 0x07,0xC0,0x00,
};
For the corresponding input, the third byte contains the low byte, and the second contains the high bits. For example,
Code:
unsigned int mcp3208_get_14bit(const unsigned char *inbuf, unsigned char channel)
{
const unsigned char *b = inbuf + 3 * (channel & 7);
return (b[2] << 2) | ((b[1] & 0x0F) << 10);
}
where
inbuf is the 24-byte input buffer received via SPI when the
mcp3208_out buffer is transferred. The function returns the value shifted left by two bits, so that the result is 14 bit.
Another option is to convert all eight in a loop, and return a bit mask of which values were changed:
Code:
unsigned int mcp3208_decode(const unsigned char *inbuf, unsigned int *state)
{
const unsigned char *const inend = inbuf + 24;
unsigned char changed = 0;
while (inbuf < inend) {
/* New 14-bit state for this analog input */
unsigned int newstate = (inbuf[2] << 2) | ((inbuf[1] & 0x0F) << 10);
inbuf += 3;
/* Since we want the first entry at LSB of changed, we need to shift right. */
changed >>= 1;
/* If state changes, set the corresponding bit (lowest bit = first channel) */
if (newstate != *state) {
changed |= 128;
*state = newstate;
}
state++;
}
return changed;
}
Given the 24 bytes received via SPI from an MCP3208, and the current 14-bit values of the potentiometers, calling the above function will update the 14-bit values and return which ones changed. For example, if the first and third channels have changed, it will return 0x01 + 0x04 = 0x05 = 5 in decimal.
Even if you use 32 potentiometers (using four separate output pins for the /CS line of each MCP3208), it'll still take less than a millisecond, total, to read all their states. Therefore, it does not matter here whether you use DMA or not. DMA will let you do other stuff while waiting for the conversions and SPI transfers to complete, though.
With Teensy 4.x, I really don't see why one would worry about the latency between sampling the ADCs and being ready to send a MIDI message, as that latency is on the order of 25µs per analog channel using MCP3208 at 1 MHz SPI clock. It is much more worthwhile to make sure one does not send unnecessary MIDI messages, because it very much seems to me that it is the MIDI data channel that is the bottleneck here, not the ADC at all.