Is Part of the SPI Interface Missing in buildroot?

I’m having trouble getting SPI to work on the Duo. I have code that reads from the MCP3008 ADC chip over SPI. The code uses the kernel user-space tools as described in the Milkv-Duo Peripheral Driver Operation Guide (pdf) under 6.3.2 SPI Read-Write Program Example In User Space pp. 26.

However the interface specified in the guide is missing in the image created from the buildroot. Specifically, the peripheral guide specifies the following interface for the SPI controllers:

Controller       spidev Interface
-----------  --------------------------
    0          "/dev/spidev32766.0"
    1          "/dev/spidev32765.0"
    2          "/dev/spidev32764.0"
    3          "/dev/spidev32763.0"

None of those devfs interface files are present in the buildroot image. The only interfaces present are:

# ls -al /dev/spi*
crw-------    1 root     root      153,   0 Jan  1  1970 /dev/spidev0.0
crw-------    1 root     root      153,   1 Jan  1  1970 /dev/spidev1.0

Those character interfaces do not seem to correspond to the SPI2_xxx functions specified in the Milkv-Duo Schematic v1.2 (pdf) under VDDIO_SD1. (bottom of page 6 of 9).

Attempting to use /dev/spidev0.0 with pins GP6-GP9 (configured for SPI with duo-pinmux), miscellaneous incorrect values are given on the MCP3008 channels 1, 4 & 5. All other channels are zero. (voltages should be shown on channels 1-4 and zero on 5-8)

Attempting to use /dev/spidev1.0 with pins GP6-GP9 simply results in all channels being zero.

Confusion From The Peripheral Guide and Schematic

According to the schematic v1.2 (same page 6 of 9) in the VDDIO_SD0_EMMC block the "/dev/spidev0.0" lines SPI0_SCK, SPI0_SDO, SPI0_SDI, and SPI0_CS_X have no GPpin interface. Instead and interface through SD_SD0_....

Likewise, the SPI1 interface described on the schematic (top of same page) in the USB & ETH & MIPI block has no GPpin interface to connect through.

The user-space SPI interface described in the Peripheral Guide simply does not seem to exist. While there does appear to be an SPI interface through GPpins for SPI2, there is no devfs character interface for it.

This is in addition to the probles with overflow warnings in the duo-sdk system files used by the spidev (and GPIO) ioctl() interfaces described more fully in this forum post Is GPIO V2 ABI Supported Through /dev/gpiochipX? (see note added at bottom related to SPI - same warnings)

If I’m simply confused (happens), then what /dev/spidevX.X should I use to talk to an SPI chip with GP pins GP6 - GP9. If there is a problem with the interface, is a new issue needed on github? Let me know, I’m happy to help. I’m eager to get this ADC chip working on the DUO.


This may also be tangentially related to SPI2 Multiple cs-gpios posted recently that speaks of configuring part of SPI through the cv1800b_milkv_duo_sd.dts file – however, that isn’t mentioned anywhere in the Peripheral Guide and I’m not sure if that relates to the kernel user-space issue here. Digging I find Enabling spidev2 instructions #71 related to that issue, but little else to go on.

The problem I have with the onboard ADC (ADC1 / ADC2) is they cannot work for voltages with internal resistance due to the 1.8V voltage divider that implements the onboard saradc. Any resistance upstream ends up creating a 3-Resistor Voltage Divider scenario where ~90% of the voltage to be measured is lost across the internal resistor due to the two - 10K Ohm resistors that make up the onboard ADC voltage divider. (a 100K Ohm internal resistor) I have a working ADS1115 4-channel ADC over II2C, but it doesn’t support high-frequency sampling.

All thoughts and help welcomed.

1 Like

It looks like SPI numbers are dynamically generated from 32766 (bottom of this link devicetree - document using aliases to set spi bus number. - Patchwork )
I suspect that the docs were earlier in development. spidev1.x refers to the SPI2 pins on the board which are pins GP6 - GP9.

I got my information on SPI config from this wiki page and the linked repo/patch Introduction | Milk-V

1 Like

Thank you for the links. I’ll see if I can get it working. Next problem is how do I transfer all the config changes and tweaks from my running image on the Duo into the buildroot so the config is there when I generate a new image? Do I just hunt and peck and find where each part of the image lives in the buildroot, or is there some established method for doing so? There are not that many changes, but I would sure like for the new image to connect via ethernet instead of RNDIS when first booted.

RNDIS has been removed from most running kernels provided by Linux distros as it is a security issue.

note: the patch isn’t for generic SPI2, but rather for the ST7789V chip LCD screen. But it should contain the information for getting GP6-9 pins working for SPI2.

Result - the patch duo-kernel-fb_st7789v.patch is entirely specific to the ST7789V chip LCD screen. It does provide the identity of the files that need to be changed. The question is – what should a generic config look like to enable SPI2 for general use in cv1800b_milkv_duo_sd.dts?

For the cv1800b it already included/enabled in the buildroot sdk



&spi2 {
	status = "okay";

	spidev@0 {
		status = "okay";

and here

	// SPI

Thank you! Yes, I had found both files, but was not clear that SPINOR_MISO, etc… actually directly translated to SPI2_MISO (or SPI2_DI). I know it’s not enabled by default - so what is needed to enable by default, just changes to line 230 in duo-buildroot-sdk/u-boot-2021.10/board/cvitek/cv180x/board.c


So there is no need to make further changes to build/boards/cv180x/cv1800b_milkv_duo_sd/dts_riscv/cv1800b_milkv_duo_sd.dts?

Also, Enable SPI2 and the corresponding spidev on Milk-V Duo shows adding cs-gpios = <&porta 18 0>; to do dts – is that necessary?

What about the modification to cvitek_cv1800b_sophpi_duo_sd_defconfig shown in that post?

Here is my setup - the same setup that works without issue through /dev/spidev0.0 on Pi, etc…

The MCP3008 chip:


The chip configuration on breadboard:

Connection on GP6 - GP9 on the Milkv-Duo:

The problem is when read through /dev/spidev0.0 there are nothing but stray voltages on various channels:

# mcp3008spi "/dev/spidev0.0" 100
opened SPI device: /dev/spidev0.0 on file descriptor: 3
  mode    : 0
  bits    : 8
  clock   : 1350000
  delay   : 5
  samples : 100  (~20 seconds)

all channel output:

  chan[0] :  0.00
  chan[1] :  0.41
  chan[2] :  0.00
  chan[3] :  0.00
  chan[4] :  0.83
  chan[5] :  0.41
  chan[6] :  0.00
  chan[7] :  0.00

Where the voltages should be:

  chan[0] :  1.69
  chan[1] :  2.71
  chan[2] :  1.36
  chan[3] :  3.29
  chan[4] :  0.00
  chan[5] :  0.00
  chan[6] :  0.00
  chan[7] :  0.00

As shown by the ADS1115 ADC chip on II2C (same voltages going to the MCP3008 chip channels 0-3 just in different order) shown below:

# ads1115

ADS1115 each input channel (AINx) configured for
single-shot, 4.096V gain, 128 SPS

  Outputting 100 analog input samples at 5Hz  (~20 sec)

  channel, reg-values and voltages:

  channel[0] : 0x66d6  (3.29 V)
  channel[1] : 0x34b8  (1.69 V)
  channel[2] : 0x54b8  (2.71 V)
  channel[3] : 0x2a5c  (1.36 V)

So if everything is provided in the buildrood-sdk for SPI2 on /dev/spidev0.0 why is nothing working?

It just seems that there should be a /dev/spidev2.0 (bus 2 channel 0). Or do I just not understand how the Duo is configured?

For completeness, I’ll post the code as well:

#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>

typedef struct {          /* SPI device struct for mcp3008 */
  struct spi_ioc_transfer spi_xfer;
  uint8_t buf[6];
  int fd;
  uint8_t mode,
} spidev;

/* SPI and mcp3008 constants  */
#define SPIDEVFS      "/dev/spidev0.0"
#define BITS          8
#define CLOCK         1350000
#define DELAY         5

#ifdef MILKVFS
/* 3rd attempt with GPIOXX numbers - failed same vals as 2nd attempt
#define SPI_CLK      23
#define SPI_MOSI     22
#define SPI_MISO     21
#define SPI_CE       18
/* 2nd attempt - very low voltage readings on wrong pins
 * next try enabling spidev2 in cv1800b_milkv_duo_sd.dts?
#define SPI_CLK       6
#define SPI_MOSI      7
#define SPI_MISO      8
#define SPI_CE        9

/* 1st attempt through /dev/spidev0.0 - failed - no readings at all
#define SPI_CE       17
#define SPI_MISO     16
#define SPI_MOSI     19
#define SPI_CLK      18
#define SPI_CE        8
#define SPI_MISO      9
#define SPI_MOSI     10
#define SPI_CLK      11

 * @brief initialize the SPI system (e.g. open "/dev/spidev0.0") and configure
 * the device struct for reading from the mcp3008.
 * @param spidevfs filesystem device node for SPI.
 * @param dev pointer to struct holding mcp3008 configuration.
 * @param delay microsecond delay for mcp3008 conversion (sample switching).
 * @param clock SPI bus speed in Hz.
 * @param bits bits per-bytes (e.g. CHAR_BITS).
 * @param mode SPI_MODE_x (single-ended or pseudo-differential pairs).
 * @param cepin CS/CE GPIO pin number.
 * @return returns 0 on success, -1 otherwise.
int spi_device_init (const char *spidevfs, spidev *dev, uint16_t delay,
                      uint32_t clock, uint8_t bits, uint8_t mode, uint8_t cepin)
  /* initialize struct members */
  dev->spi_xfer.tx_buf = (unsigned long)dev->buf;
  dev->spi_xfer.rx_buf = (unsigned long)(dev->buf + 3);
  dev->spi_xfer.len = 3;
  dev->spi_xfer.delay_usecs = delay;
  dev->spi_xfer.speed_hz = clock;
  dev->spi_xfer.bits_per_word = bits;

  dev->mode = mode;
  dev->cepin = cepin;

  /* open spi device in Linux sysfs */
  if ((dev->fd = open (spidevfs, O_RDWR)) == -1) {
    perror ("open spidevfs");
    return -1;
  /* set device mode */
  if (ioctl (dev->fd, SPI_IOC_WR_MODE, &dev->mode) == -1) {
    perror ("error init SPI_IOC_WR_MODE");
    return -1;
  /* set number of bits per word (byte) */
  if (ioctl (dev->fd, SPI_IOC_WR_BITS_PER_WORD,
            &dev->spi_xfer.bits_per_word) == -1) {
    perror ("error init SPI_IOC_WR_BITS_PER_WORD");
    return -1;
  /* send requested SPI bus speed */
  if (ioctl (dev->fd, SPI_IOC_WR_MAX_SPEED_HZ,
            &dev->spi_xfer.speed_hz) == -1) {
    perror ("error init SPI_IOC_WR_MAX_SPEED_HZ");
    return -1;
  /* read configured SPI bus speed */
  if (ioctl (dev->fd, SPI_IOC_RD_MAX_SPEED_HZ,
            &dev->spi_xfer.speed_hz) == -1) {
    perror ("error init SPI_IOC_RD_MAX_SPEED_HZ");
    return -1;

  return 0;

 * @brief set channel to psuedo-differential compare mode.
 * @param channel channel to set control bits on (SGL/DIF = 0, D2=D1=D0=0).
 * @return returns control bits set for channel.
uint8_t channel_cfg_differential (uint8_t channel)
  return (channel & 7) << 4;

 * @brief set channel to single-ended input mode.
 * @param channel channel to set control bits on (SGL/DIF = 1, D2=D1=D0=0).
 * @return returns control bits set for channel.
uint8_t channel_cfg_single (uint8_t channel)
  return (0x8 | channel) << 4;

 * @brief read sample from mcp3008 for channel.
 * @param dev pointer to mcp3008 device struct.
 * @param channel analog input channel to read.
 * @return returns raw 10-bit value for sample.
int spi_read_adc (spidev *dev, uint8_t channel)
  dev->buf[0] = 1;
  dev->buf[1] = channel_cfg_single (channel);
  dev->buf[2] = 0;

  if (ioctl (dev->fd, SPI_IOC_MESSAGE(1), &dev->spi_xfer) == -1) {
    perror ("ioctl SPI_IOC_MESSAGE()");

  return ((dev->buf[4] & 0x03) << 8) |
          (dev->buf[5] & 0xff);

 * @brief simple output of all mcp3008 channels using ANSI escape
 * to overwrite values in place.
 * @param f array of float values to display.
 * @param n numer of elements in f.
void prn_all_channels (float *f, int n)
  char fmt[32] = "";

  for (int i = 0; i < n; i++) {
    printf ("  chan[%d] : % 5.2f\n", i, f[i]);

  sprintf (fmt, "\033[%dA", n);
  printf (fmt);

int main (int argc, char **argv) {

  const char *spidevfs = SPIDEVFS;
  uint8_t   channel = 0;
  unsigned  nsamples = 300;
  spidev    dev = { .spi_xfer = {0}};

  if (argc > 1) {   /* set SPI devfs pathname (if given) */
    spidevfs = argv[1];

  if (argc > 2) {   /* set the number of samples, (default 300) */
    unsigned tmp;
    if (sscanf (argv[2], "%u", &tmp) != 1) {
      fprintf (stderr, "error: invalid unsigned count for argv[2] (%s)\n",
      return 1;
    nsamples = tmp;

  if (argc > 3) {   /* set single channel to read, (default all channels) */
    uint8_t tmp;
    if (sscanf (argv[3], "%hhu", &tmp) != 1 || tmp > 7) {
      fprintf (stderr, "error: invalid channel given on argv[3] (%s)\n",
      return 1;
    channel = tmp;

  /* initialize spidev interface and validate */
  if (spi_device_init (spidevfs, &dev, DELAY, CLOCK, BITS,
                        SPI_MODE_0, SPI_CE) == -1) {
    return 1;

  printf ("opened SPI device: %s on file descriptor: %d\n"
          "  mode    : %d\n"
          "  bits    : %hhu\n"
          "  clock   : %u\n"
          "  delay   : %hu\n"
          "  samples : %u  (~%u seconds)\n\n",
          spidevfs, dev.fd, dev.mode, dev.spi_xfer.bits_per_word,
          dev.spi_xfer.speed_hz, dev.spi_xfer.delay_usecs,
          nsamples, nsamples / 5);

  if (argc > 3) { /* if channel argument provides display single-channel */
    puts ("single channel output:\n");

    /* do single-channel ADC read at 5Hz */
    for (unsigned i = 0; i < nsamples; i++) {
      int val = spi_read_adc (&dev, channel);
      float res = (float)val / 1023.f * 3.3f;
      printf ("  chan[%d] : % 5.2f\n\033[1A", channel, res);
      usleep (200000);
  else {  /* otherwise display sample values for all channels */
    puts ("all channel output:\n");

    /* do read of all channels at 5Hz */
    for (unsigned i = 0; i < nsamples; i++) {
      float fvals[8] = {0};
      for (uint8_t chan = 0; chan < 8; chan++) {
        int val = spi_read_adc (&dev, chan);
        fvals[chan] = (float)val / 1023.f * 3.3f;
      prn_all_channels (fvals, 8);
      usleep (200000);
    fputs ("\033[8B\n", stdout);

  close (dev.fd);

  return 0;

The following is a short Makefile I use with most test code project on the Duo (changed as needed):

TARGET = mcp3008spi


CFLAGS += -I$(SYSROOT)/usr/include -I./include
CFLAGS += -Wall -Wextra -pedantic -Wshadow -std=c11

LDFLAGS += -L$(SYSROOT)/usr/lib -L$(SYSROOT)/lib -lrt

SOURCE = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SOURCE))

	$(CC) -o $@ $(OBJS) $(LDFLAGS)

%.o: %.c
	$(CC) $(CFLAGS) -o $@ -c $<

.PHONY: clean
	@rm -rf *.o
	@rm -rf $(OBJS)
	@rm -rf $(TARGET)