SPI and hardware/software CS issue

Thread Starter

janbert

Joined Aug 6, 2025
18
Hi,

I’m having a bit of an issue with my current setup. Unfortunately, I’ve already printed a custom PCB and soldered everything.

Here’s my Raspberry Pi 4b setup:
  • SPI0, CS0: SPI display
  • SPI0, CS1: MCP3008 (ADC)
  • SPI0 with software CS on GPIO 5: MCP3008

The problem is that the software chip select is causing artifacts on my display. I also can’t use SPI1 on the Raspberry Pi since it’s already occupied by a soundcard HAT. I’ve connected some potentiometer to the MCP3008, they’re supposed to send values that show up on the display.

Is there anything I can do on the programming side?
I’ve already tried reducing the SPI speed, but that didn’t help. Both hardware CS0 and CS1 work perfectly together, but the software CS causes these artifacts.:(
 
Last edited:

Thread Starter

janbert

Joined Aug 6, 2025
18
Hi Irving. Thank you so much for your help on this. My schematic is unfortunately pretty big and messy, I tried cleaning it up a bit. Please let me know if that is still hard to read. I made 4 layer pcb with one layer dedicated to power and one to ground.
The display is an OLED 2.4" 2.42 Inch 128x64 LCD Display SSD1309.

1761167183334.png

This is the code I am currently using(with chatgpt help), I am not really the best coder.
Also, its just using the MCP3008 with the software CS for now, the other MCP3008 on CS1 doesnt cause any issues:

Code:
#!/usr/bin/env python3
import time
import RPi.GPIO as GPIO
import spidev
from luma.core.interface.serial import spi
from luma.oled.device import ssd1309
from luma.core.render import canvas
from PIL import ImageFont

# -----------------------------
# SIMPLE ENCODER CLASS
# -----------------------------
class Encoder:
    def __init__(self, channels=(0,1), cs_gpio=5, spi_bus=0, spi_device=0):
        self.ch_a, self.ch_b = channels
        self.spi = spidev.SpiDev()
        self.spi.open(spi_bus, spi_device)
        self.spi.max_speed_hz = 1000000

        self.cs_pin = cs_gpio
        GPIO.setmode(GPIO.BCM)           # <- Set pin numbering mode here
        GPIO.setup(self.cs_pin, GPIO.OUT)
        GPIO.output(self.cs_pin, GPIO.HIGH)

    def read_channel(self, channel):
        GPIO.output(self.cs_pin, GPIO.LOW)
        r = self.spi.xfer2([1, (8 + channel) << 4, 0])
        GPIO.output(self.cs_pin, GPIO.HIGH)
        return ((r[1] & 3) << 8) + r[2]

    def read(self):
        return self.read_channel(self.ch_a), self.read_channel(self.ch_b)

    def cleanup(self):
        self.spi.close()
        GPIO.cleanup(self.cs_pin)

# -----------------------------
# OLED SETUP
# -----------------------------
DC_PIN = 13
RST_PIN = 6
GPIO.setmode(GPIO.BCM)              # <- Also needed before using RST_PIN
GPIO.setup(RST_PIN, GPIO.OUT)
GPIO.output(RST_PIN, GPIO.LOW)
time.sleep(0.1)
GPIO.output(RST_PIN, GPIO.HIGH)
time.sleep(0.1)

serial = spi(port=0, device=0, gpio_DC=DC_PIN, gpio_RST=RST_PIN, bus_speed_hz=1000000)
device = ssd1309(serial, width=128, height=64)
font = ImageFont.load_default()

# -----------------------------
# ENCODER
# -----------------------------
encoder = Encoder(channels=(0,1), cs_gpio=5)

# -----------------------------
# MAIN LOOP
# -----------------------------
try:
    while True:
        a, b = encoder.read()
        with canvas(device) as draw:
            draw.text((0,0), f"Ch0: {a}", fill="white", font=font)
            draw.text((0,12), f"Ch1: {b}", fill="white", font=font)
        time.sleep(0.05)

except KeyboardInterrupt:
    print("Exiting...")

finally:
    device.clear()
    encoder.cleanup()
 
Last edited:

Thread Starter

janbert

Joined Aug 6, 2025
18
I am sorry, I was still figuring out what was really the issue.
But it turns out "spi.no_cs = True" did the trick. I can`t fully explain it though how it works.
But I think its basically that even the hardware CS I can now toggle in software and sync it better to the software cs.
At least that's how I understood it.
 

Irving

Joined Jan 30, 2016
5,065
I am sorry, I was still figuring out what was really the issue.
But it turns out "spi.no_cs = True" did the trick. I can`t fully explain it though how it works.
But I think its basically that even the hardware CS I can now toggle in software and sync it better to the software cs.
At least that's how I understood it.
I don't see where you put that...
 

Irving

Joined Jan 30, 2016
5,065
spi.no_cs seems to be an implementation of the SPI definition 'No Slave Select' as stated in the SPI Wiki:

"No slave select
Some devices do not use slave select, and instead manage protocol state machine entry/exit using other methods. "

This relies on software to manipulate the CS - or do something else that's peripheral specific - outside of the SPI implementation. Since each instantiation of an SPI object would have its own flag, without that two SPI channels might clash - one using the predefined CS pin when it wasn't meant to be using it.

Personally I'd have used the predefined SPI CS pins for the ADCs and used the software one for the display, to keep the encoder implementation consistent.
 

simozz

Joined Jul 23, 2017
170
I remember Linux SPI C driver spidev has a struct member to use or not the chip select.

I think the TS is using a kind of wrapper for this.

I am wondering about execution performances of using Python instead of C.
 

nsaspook

Joined Aug 27, 2009
16,271
I remember Linux SPI C driver spidev has a struct member to use or not the chip select.

I think the TS is using a kind of wrapper for this.

I am wondering about execution performances of using Python instead of C.
The actual CS hardware code is in the spi hardware driver, not the spidev protocol driver. Most of the hardware today (since the early 2000's) is defined in the device-tree (using the device tree-compiler) instead of in kernel code, along with what's needed for a specific protocol driver for a SoC like the RPi and similar boards.
https://elinux.org/Device_Tree_Usage
https://docs.kernel.org/devicetree/usage-model.html
The most interesting hook in the DT context is .init_machine() which is primarily responsible for populating the Linux device model with data about the platform. Historically this has been implemented on embedded platforms by defining a set of static clock structures, platform_devices, and other data in the board support .c file, and registering it en-masse in .init_machine(). When DT is used, then instead of hard coding static devices for each platform, the list of devices can be obtained by parsing the DT, and allocating device structures dynamically.
spi hardware driver device-tree source
Code:
&spi0  {
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&spi0_pins>, <&spi0_cs0_pin>;

        spidev@0 {
                status = "disabled";
                compatible = "rohm,dh2228fv";
                reg = <0>;
                spi-max-frequency = <1000000>;
        };

        flash@0 {
                status = "okay";
                #address-cells = <1>;
                #size-cells = <1>;
                compatible = "jedec,spi-nor";
                reg = <0>;
                spi-max-frequency = <40000000>;
        };
};

&spi1 {
        status = "disabled";
        #address-cells = <1>;
        #size-cells = <0>;
        pinctrl-names = "default";
        pinctrl-0 = <&spi1_pins>, <&spi1_cs1_pin>;

        spidev@1 {
                compatible = "rohm,dh2228fv";
                status = "disabled";
                reg = <1>;
                spi-max-frequency = <1000000>;
        };
};
spi protocol driver device-tree source
sun50i-h616-spi1-cs0-cs1-spidev.dtbo
Code:
/dts-v1/;
/plugin/;

/ {
        fragment@0 {
                target-path = "/aliases";
                __overlay__ {
                        spi1 = "/soc/spi@5011000";
                };
        };

        fragment@1 {
                target = <&spi1>;
                __overlay__ {
                        status = "okay";
                        #address-cells = <1>;
                        #size-cells = <0>;

                        pinctrl-names = "default";
                        pinctrl-0 = <&spi1_pins>, <&spi1_cs0_pin>, <&spi1_cs1_pin>;

                        spidev@0 {
                                compatible = "rohm,dh2228fv";
                                status = "okay";
                                reg = <0>;
                                spi-max-frequency = <50000000>;
                        };

                        spidev@1 {
                                compatible = "rohm,dh2228fv";
                                status = "okay";
                                reg = <1>;
                                spi-max-frequency = <50000000>;
                        };
                };
        };
};
The driver-tree source for my daq_bmc spi protocol driver.
sun50i-h616-spi1-spibmc.dtbo
Code:
/dts-v1/;
/plugin/;

/ {
        fragment@0 {
                target-path = "/aliases";
                __overlay__ {
                        spi1 = "/soc/spi@5011000";
                };
        };

        fragment@1 {
                target = <&spi1>;
                __overlay__ {
                        status = "okay";
                        #address-cells = <1>;
                        #size-cells = <0>;

                        pinctrl-names = "default";
                        pinctrl-0 = <&spi1_pins>, <&spi1_cs1_pin>;

                        spibmc@1 {
                                compatible = "orangepi,spi-bmc";
                                status = "okay";
                                reg = <1>;
                                spi-max-frequency = <4000000>;
                        };

                };
        };
};
Yes, the wrapper is Python.
Python will be fine as all of the actual driver code is in C, Python is just glue here.

There are non-Linux uses of device-trees for hardware specifications like Zephyr. IMO seems over-kill for most small embedded applications.

 
Last edited:

nsaspook

Joined Aug 27, 2009
16,271
No. I was not referring to the device-tree, but to the kernel user space driver. spidev (it is not a protocol) driver is here:
https://github.com/torvalds/linux/blob/master/drivers/spi/spidev.c
https://github.com/torvalds/linux/blob/master/include/uapi/linux/spi/spidev.h

And yes, it has a flag SPI_NO_CS and a struct member cs_change.
Your links point to this: https://github.com/torvalds/linux/blob/master/drivers/spi/spidev.c
Code:
MODULE_AUTHOR("Andrea Paterniani, <a.paterniani@swapp-eng.it>");
MODULE_DESCRIPTION("User mode SPI device interface");
MODULE_LICENSE("GPL");
MODULE_ALIAS("spi:spidev");
spidev is a IOCTL kernel Protocol driver (user mode SPI device driver support). It converts raw spi (using the spi framework to abstract various hardware controllers to a common API) to a file in the /dev folder that can be opened and used like any other userland ioctl device.
https://www.kernel.org/doc/html/v4.9/driver-api/spi.html
The programming interface is structured around two kinds of driver, and two kinds of device. A “Controller Driver” abstracts the controller hardware, which may be as simple as a set of GPIO pins or as complex as a pair of FIFOs connected to dual DMA engines on the other side of the SPI shift register (maximizing throughput). Such drivers bridge between whatever bus they sit on (often the platform bus) and SPI, and expose the SPI side of their device as a struct spi_master. SPI devices are children of that master, represented as a struct spi_device and manufactured from struct spi_board_info descriptors which are usually provided by board-specific initialization code. A struct spi_driver is called a “Protocol Driver”, and is bound to a spi_device using normal driver model calls.

The I/O model is a set of queued messages. Protocol drivers submit one or more struct spi_message objects, which are processed and completed asynchronously. (There are synchronous wrappers, however.) Messages are built from one or more struct spi_transfer objects, each of which wraps a full duplex SPI transfer. A variety of protocol tweaking options are needed, because different chips adopt very different policies for how they use the bits transferred with SPI.
https://docs.kernel.org/driver-api/ioctl.html
There are Master controller drivers for various SoC boards.
1761576426550.png
This is the SPI framework the spidev protocol driver connects to: https://github.com/torvalds/linux/blob/master/drivers/spi/spi.c
and this is a typical controller (talks to hardware) driver for the SPI framework: https://github.com/torvalds/linux/blob/master/drivers/spi/spi-apple.c
The controller driver is where hardware related auto CS capabilities and limitations are set.
C:
static void apple_spi_init(struct apple_spi *spi)
{
    /* Set CS high (inactive) and disable override and auto-CS */
    reg_write(spi, APPLE_SPI_PIN, APPLE_SPI_PIN_CS);
    reg_mask(spi, APPLE_SPI_SHIFTCFG, APPLE_SPI_SHIFTCFG_OVERRIDE_CS, 0);
    reg_mask(spi, APPLE_SPI_PINCFG, APPLE_SPI_PINCFG_CS_IDLE_VAL, APPLE_SPI_PINCFG_KEEP_CS);

    /* Reset FIFOs */
    reg_write(spi, APPLE_SPI_CTRL, APPLE_SPI_CTRL_RX_RESET | APPLE_SPI_CTRL_TX_RESET);

    /* Configure defaults */
    reg_write(spi, APPLE_SPI_CFG,
          FIELD_PREP(APPLE_SPI_CFG_FIFO_THRESH, APPLE_SPI_CFG_FIFO_THRESH_8B) |
          FIELD_PREP(APPLE_SPI_CFG_MODE, APPLE_SPI_CFG_MODE_IRQ) |
          FIELD_PREP(APPLE_SPI_CFG_WORD_SIZE, APPLE_SPI_CFG_WORD_SIZE_8B));

    /* Disable IRQs */
    reg_write(spi, APPLE_SPI_IE_FIFO, 0);
    reg_write(spi, APPLE_SPI_IE_XFER, 0);

    /* Disable delays */
    reg_write(spi, APPLE_SPI_DELAY_PRE, 0);
    reg_write(spi, APPLE_SPI_DELAY_POST, 0);
}
My custom Comedi Protocol Master driver spibmc is listed above on a modified kernel build next to the usual IOCTL Protocol driver spidev.
https://github.com/nsaspook/mqtt_comedi/blob/fixes/daq_bmc/daq_bmc.c
Code:
MODULE_AUTHOR("NSASPOOK <nsaspooksma2@gmail.com");
MODULE_DESCRIPTION("OPi DI/DO/AI/AO SPI Driver");
MODULE_VERSION("6.1.31");
MODULE_LICENSE("GPL");
MODULE_ALIAS("spi:spibmc");
https://github.com/torvalds/linux/blob/master/drivers/comedi/drivers/ni_daq_700.c
Another Protocol driver. This one uses the old PCMCIA card interface.
C:
MODULE_DEVICE_TABLE(pcmcia, daq700_cs_ids);

static struct pcmcia_driver daq700_cs_driver = {
    .name        = "ni_daq_700",
    .owner        = THIS_MODULE,
    .id_table    = daq700_cs_ids,
    .probe        = daq700_cs_attach,
    .remove        = comedi_pcmcia_auto_unconfig,
};
module_comedi_pcmcia_driver(daq700_driver, daq700_cs_driver);

MODULE_AUTHOR("Fred Brooks <nsaspook@nsaspook.com>");
MODULE_DESCRIPTION(
    "Comedi driver for National Instruments PCMCIA DAQCard-700 DIO/AI");
MODULE_LICENSE("GPL");
https://forum.allaboutcircuits.com/threads/raspberry-pi-daq-system.75543/post-866607
 
Last edited:

nsaspook

Joined Aug 27, 2009
16,271
Correct, cs_change, there are lots of framework CS modifiers that some simplistic controller drivers don't implement.

https://docs.kernel.org/driver-api/spi.html
struct spi_device
Controller side proxy for an SPI target device

Definition:
struct spi_device {
struct device dev;
struct spi_controller *controller;
u32 max_speed_hz;
u8 bits_per_word;
bool rt;
#define SPI_NO_TX BIT(31);
#define SPI_NO_RX BIT(30);
#define SPI_TPM_HW_FLOW BIT(29);
#define SPI_MODE_KERNEL_MASK (~(BIT(29) - 1));
u32 mode;
int irq;
void *controller_state;
void *controller_data;
char modalias[SPI_NAME_SIZE];
const char *driver_override;
struct spi_statistics __percpu *pcpu_statistics;
struct spi_delay word_delay;
struct spi_delay cs_setup;
struct spi_delay cs_hold;
struct spi_delay cs_inactive;
u8 chip_select[SPI_DEVICE_CS_CNT_MAX];
u8 num_chipselect;
u32 cs_index_mask : SPI_DEVICE_CS_CNT_MAX;
struct gpio_desc *cs_gpiod[SPI_DEVICE_CS_CNT_MAX];
};

word_delay
delay to be inserted between consecutive words of a transfer

cs_setup
delay to be introduced by the controller after CS is asserted

cs_hold
delay to be introduced by the controller before CS is deasserted

cs_inactive
delay to be introduced by the controller after CS is deasserted. If cs_change_delay is used from spi_transfer, then the two delays will be added up.

chip_select
Array of physical chipselect, spi->chipselect gives the corresponding physical CS for logical CS i.

num_chipselect
Number of physical chipselects used.

cs_index_mask
Bit mask of the active chipselect(s) in the chipselect array

cs_gpiod
Array of GPIO descriptors of the corresponding chipselect lines (optional, NULL when not using a GPIO line)
I usually implement my own SPI exchange CS delays with a high resolution timer in Protocol drivers as the SPI framework delays are not dependable.
C:
static const struct spi_delay CS_CHANGE_DELAY_USECS0 = {
    .value = 1,
    .unit = SPI_DELAY_UNIT_USECS,
};
static const struct spi_delay CS_CHANGE_DELAY_USECS10 = {
    .value = 10,
    .unit = SPI_DELAY_UNIT_USECS,
};
...
/*
* called for each listed spibmc SPI device
* SO THIS RUNS FIRST, setup basic spi comm parameters here
*/
static int32_t spibmc_spi_probe(struct spi_device * spi)
{
    struct comedi_spibmc *pdata;
    int32_t ret;

    pdata = kzalloc(sizeof(struct comedi_spibmc), GFP_KERNEL | GFP_DMA);
    if (!pdata)
        return -ENOMEM;

    /*
     * default SPI delays
     */
    pdata->delay = CS_CHANGE_DELAY_USECS0;
    pdata->cs_delay = CS_CHANGE_DELAY_USECS0;
    pdata->word_delay = CS_CHANGE_DELAY_USECS0;

    spi->dev.platform_data = pdata;
    reinit_completion(&done);
    pdata->ping_pong = false;
    pdata->upper_lower = 0;
    pdata->tx_buff = kzalloc(SPI_BUFF_SIZE_NOHUNK, GFP_KERNEL);
    if (!pdata->tx_buff) {
        ret = -ENOMEM;
        goto kfree_exit;
    }
    pdata->rx_buff = kzalloc(SPI_BUFF_SIZE_NOHUNK, GFP_KERNEL);
    if (!pdata->rx_buff) {
        ret = -ENOMEM;
        goto kfree_tx_exit;
    }

    dev_info(&spi->dev, "spi link %s\n", spibmc_version);
    /*
     * Do only one chip select for the BMCboard
     */
    dev_info(&spi->dev,
        "BMCboard default: do_conf=%d, di_conf=%d, daqbmc_conf=%d\n",
        do_conf, di_conf, daqbmc_conf);

    if (spi->chip_select == CSnA) {
        /*
         * get a copy of the slave device 0 to share with Comedi
         * we need a device to talk to the Q84
         *
         * create entry into the Comedi device list
         */
        INIT_LIST_HEAD(&pdata->device_entry);
        pdata->slave.spi = spi;
        /*
         * put entry into the Comedi device list
         */
        list_add_tail(&pdata->device_entry, &device_list);
        spi->mode = daqbmc_devices[daqbmc_conf].spi_mode;
        spi->max_speed_hz = daqbmc_devices[daqbmc_conf].max_speed_hz;
        spi->word_delay = CS_CHANGE_DELAY_USECS0;
        spi->cs_setup = CS_CHANGE_DELAY_USECS0;
        spi->cs_hold = CS_CHANGE_DELAY_USECS0;
        spi->cs_inactive = CS_CHANGE_DELAY_USECS0;
    }
...
}


/*
* My standard 9 byte SPI data packet with ~20us spacing between bytes
*/
static int32_t bmc_spi_exchange(struct comedi_device *dev, struct bmc_packet_type * packet)
{
    struct comedi_subdevice *s = dev->read_subdev;
    struct spi_param_type *spi_data = s->private;
    struct spi_device *spi = spi_data->spi;
    int32_t ret = 0;
    ktime_t slower = SPI_GAP_LONG;

    if (spi == NULL) {
        ret = -ESHUTDOWN;
        return ret;
    }
    /*
     * use nine spi transfers for the complete SPI transaction
     * we need the inter-byte processing time on the slave side
     * with only a two byte FIFO
     */
    packet->one_t.speed_hz = spi->max_speed_hz;

    /*
     * send the cmd and 4-bit channel data
     */
    packet->one_t.tx_buf = &packet->bmc_byte_t[BMC_CMD];
    packet->one_t.rx_buf = &packet->bmc_byte_r[BMC_CMD];
    packet->one_t.cs_change = false;
    packet->one_t.len = 1;
    spi_message_init_with_transfers(packet->m, &packet->one_t, 1); //one transfer per message
    spi_bus_lock(spi->master);
    ret = spi_sync_locked(spi, packet->m);
    spi_bus_unlock(spi->master);
    if (ret == 0) {
        ret = packet->m->actual_length;
    }
    __set_current_state(TASK_UNINTERRUPTIBLE);
    schedule_hrtimeout_range(&slower, 0, HRTIMER_MODE_REL_PINNED);

    /*
     * send the data
     */
    packet->one_t.tx_buf = &packet->bmc_byte_t[BMC_D0];
    packet->one_t.rx_buf = &packet->bmc_byte_r[BMC_D0];
    packet->one_t.cs_change = false;
    packet->one_t.len = 1;
    spi_message_init_with_transfers(packet->m, &packet->one_t, 1); //one transfer per message
    spi_bus_lock(spi->master);
    spi_sync_locked(spi, packet->m);
    spi_bus_unlock(spi->master);
    slower = SPI_GAP;
    __set_current_state(TASK_UNINTERRUPTIBLE);
    schedule_hrtimeout_range(&slower, 0, HRTIMER_MODE_REL_PINNED);
...
}
 
Top