As part of my deep dive into embedded Linux, I wanted to interface with real-world hardware. My previous projects with GPIO, UART, and I2C reflect my progress and learning of new Linux APIs. I chose to learn how to implement drivers using these tools because they are common standard communication protocols in embedded systems, whether for microcontrollers or single-board computers like the Raspberry Pi 4. The final common protocol to tackle was SPI. Out of the sensors available, the ADXL345 accelerometer caught my eye with its interrupt capabilities – learning about its built-in interrupt map and working with IRQs in kernel space presented an interesting challenge. One of my goals was also to expand the functionality of my device tree overlay in assigning pins, making my driver more hardware agnostic.
Getting basic SPI communication with the ADXL345 from a kernel driver involved a few key steps and some classic debugging.
Missing Probe function call:
Initially, my driver's probe() function wouldn't even get called! A quick check of ls /sys/class/spi_master/ confirmed it was empty, meaning the SPI0 bus itself wasn't active on my Raspberry Pi. The first fix was ensuring dtparam=spi=on was correctly set in /boot/firmware/config.txt. However, the bus still wouldn't appear consistently. The deeper issue lay in my Device Tree Overlay (DTO). I learned that my explicit assignment of the SPI pins (MOSI, MISO, SCLK) and using the cs-gpios property for the chip select within the DTO was conflicting with the SPI master's default hardware chip select handling. By removing these explicit SPI pin definitions from the DTO (relying on dtparam=spi=on for default pin muxing) and removing cs-gpios (instead using reg = <0>; in the DTO to select the default hardware CS0, which is GPIO8/CE0 on the Pi), the spi0 bus finally registered, and my probe() function was called. (While the spi_device struct has a cs_gpio member, using reg = <0>; proved to be the standard way for hardware CE0.)
Garbage Data - The Importance of SPI Mode & Communication Functions:
Once probe() was active, reading the ADXL345's Device ID (expected 0xE5) was frustratingly inconsistent, often returning garbage like 0x00 or 0xF2. I initially suspected wiring or SPI speed issues. To validate my physical setup, I successfully used the same wiring (with a logic shifter) to communicate between the Raspberry Pi and an ATmega328P via spidev in Python – this confirmed my pins and connections were sound. The fact I was receiving some data, albeit incorrect, was a good hint.
Delving into the spi_device struct, I noted the mode member. As kernel documentation states, this defines how data is clocked and can be changed by the driver. The ADXL345 datasheet, under "4-Wire SPI," clearly states: "The maximum SPI clock speed is 5 MHz... and the timing scheme follows clock polarity (CPOL) = 1 and clock phase (CPHA) = 1." This is SPI Mode 3. My initial DTO only had spi-max-frequency = <5000000>; as I did not think much about the latter part of the sentence. Before, I hadn't explicitly forced the SPI mode. The default mode being used was not Mode 3. The solution was to explicitly set the mode in my probe() function with spi->mode = 3. After calling spi_setup(), I was finally able to reliably read the correct 0xE5 Device ID! This experience reminded me of working on a UART serdev driver where incorrect baud rate settings led to similarly garbled characters.
For actual data transfer, I utilized several kernel SPI functions:
spi_write(): For writing single bytes to configuration registers (e.g., POWER_CTL, DATA_FORMAT). This simplified the code compared to manually setting up spi_transfer and spi_message for each write.
spi_w8r8(): For reading single byte registers like REG_DEVID or INT_SOURCE. This function writes an 8-bit command (the register address with the read bit set) and reads back an 8-bit value.
spi_sync() with struct spi_transfer and struct spi_message: For the multi-byte read of X,Y,Z acceleration data from REG_DATAX0 onwards. This provided the necessary control for a single transaction to fetch all 6 data bytes efficiently.
Interrupt init snippet
With basic communication established, the next goal was handling interrupts for data-ready and tap detection events.
GPIO Binding from Device Tree:
A key aim was to make the interrupt pin configurable. I achieved this by defining an int1-gpio property in my DTO: int1-gpio = <&gpio 1 0>; (to use BCM GPIO 1). In the driver, of_get_named_gpio(node, "int1-gpio", 0); reads this property. On my Raspberry Pi 4, the main GPIO controller (gpiochip512 or similar representing the BCM2711 GPIOs) often has a Linux GPIO number base offset (e.g., 512). So, BCM GPIO 1 correctly resolved to Linux GPIO 513 in my dmesg logs, which was then mapped to a kernel IRQ. This confirmed the DT binding for interrupt pin selection was working.
The Double Tap Echo & Debouncing:
After configuring the ADXL345 registers (REG_THRESH_TAP, REG_DUR, REG_LATENT, REG_WINDOW, REG_TAP_AXES) for tap detection, I successfully registered double tap events. However, I frequently observed an "echo" – a single tap interrupt firing immediately after a double tap. The ADXL345 can be quite sensitive to physical reverberations!
The fix involved a two-pronged approach:
Hardware Tuning: I iteratively adjusted the ADXL345's tap registers in probe(). Increasing REG_THRESH_TAP (e.g., to ~4g from an initial 3g) and REG_DUR (e.g., to ~20ms from 10ms) made the sensor itself less prone to registering these rapid echoes as new events.
Software Cooldown: While the Raspberry Pi Linux kernel doesn't have a generic "software debounce API" for GPIO IRQs in the same way some microcontrollers do, it provides timing mechanisms. Referencing my experience with debouncing in the BANSHEEUAV project and some research (StackExchange), I implemented a jiffies-based cooldown in my irq_handler. After a double tap is processed, its timestamp is recorded. Any subsequent single tap interrupt arriving within a defined DOUBLE_TAP_COOLDOWN_MS (e.g., 250-400ms) is logged as "ignored."
Single, Double Tap, and Data Ready handling in irq_handler function
This ADXL345 project was a fantastic culmination of my initial foray into Linux driver development. From wrestling with Device Tree intricacies and SPI modes to fine-tuning interrupt behavior and deep datasheet interpretation, it's been an invaluable learning experience. The driver now successfully reads accelerometer data via cdev, allows dynamic configuration of range and rate via sysfs, and detects data-ready, single, and double tap interrupts.
While there's always more to explore (like the ADXL345's FIFO or integrating with the IIO subsystem), this project marks a good point to shift my focus. My next embedded Linux adventure will be board bring-up, specifically with Das U-Boot!
-https://raspberrypi.stackexchange.com/questions/8544/gpio-interrupt-debounce
-https://docs.kernel.org/driver-api/spi.html
-https://docs.kernel.org/driver-api/driver-model/devres.html
-https://embetronicx.com/tutorials/linux/device-drivers/linux-kernel-spi-device-driver-tutorial/
-https://www.youtube.com/watch?v=oCTNuwO9_FA