228 lines
6.4 KiB
C
228 lines
6.4 KiB
C
/*
|
|
* SPDX-License-Identifier: MIT
|
|
*
|
|
* This tidbit demonstrates out-of-band SPI transfers controlled from
|
|
* user-space, using the extension the EVL core adds to the common
|
|
* SPIDEV interface. Typical use case: closed-loop control systems
|
|
* having stringent requirements, i.e. high frequency and/or low
|
|
* jitter. The magic starts with the ioctl(SPI_IOC_ENABLE_OOB_MODE)
|
|
* request, check out the code below.
|
|
*
|
|
* Using a Raspberry PI2/3/4, you can easily demo this code with a
|
|
* simple loopback test, by shorting PIN #19 (SPI_MOSI) and #21
|
|
* (SPI_MISO) on the GPIO 40 pin header, using the SPI0 controller
|
|
* (bcm2835). You need the following features to be enabled in your PI
|
|
* kernel configuration:
|
|
*
|
|
* - CONFIG_SPI=[y|m]
|
|
* - CONFIG_SPI_OOB=y
|
|
* - CONFIG_SPI_SPIDEV=[y|m]
|
|
* - CONFIG_SPI_SPIDEV_OOB=y
|
|
* - CONFIG_DMA_BCM2835=[y|m]
|
|
* - CONFIG_DMA_BCM2835_OOB=y
|
|
*
|
|
* Usage:
|
|
* ~# oob-spi [/dev/spidevX.Y]
|
|
*
|
|
* What's so interesting with this test? Check the delay reported for
|
|
* the transfer, with and without load. Typically, you could run the
|
|
* following stress load in the background:
|
|
*
|
|
* ~# dd if=/dev/zero of=/dev/null bs=128M &
|
|
* ~# while :; do hackbench; done&
|
|
*
|
|
* TROUBLESHOOTING:
|
|
*
|
|
* - if you get -ENODEV on return to ioctl(SPI_IOC_ENABLE_OOB_MODE),
|
|
* then you should suspect the DMA settings for the SPI
|
|
* controller. DMA support is required for out-of-band transfers.
|
|
*
|
|
* - if you have no /dev/spidev* devices listed although the proper
|
|
* SPI driver is enabled in the kernel configuration, then the device
|
|
* tree for the SoC may be missing a SPI(dev) slave device declaration
|
|
* in the proper SPI bus stanza. You may want to set the compatible
|
|
* string to "spidev,loopback", which is detected by the EVL-enabled
|
|
* spidev driver.
|
|
*
|
|
* spi0 {
|
|
* ...
|
|
* spidev_0: spidev@0{
|
|
* compatible = "spidev,loopback";
|
|
* reg = <0>;
|
|
* #address-cells = <1>;
|
|
* #size-cells = <0>;
|
|
* spi-max-frequency = <12500000>;
|
|
* status = "okay";
|
|
* };
|
|
* };
|
|
*/
|
|
|
|
#include <sys/ioctl.h>
|
|
#include <sys/types.h>
|
|
#include <sys/mman.h>
|
|
#include <time.h>
|
|
#include <stdint.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <error.h>
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <pthread.h>
|
|
#include <sched.h>
|
|
#include <evl/thread.h>
|
|
#include <evl/clock.h>
|
|
#include <evl/proxy.h>
|
|
#include <linux/spi/spidev.h>
|
|
#include <uapi/evl/devices/spidev.h>
|
|
|
|
static const char *device = "/dev/spidev0.0";
|
|
static uint32_t mode = SPI_MODE_0;
|
|
static uint8_t bits = 8;
|
|
static uint32_t speed = 2500000;
|
|
static int len = 140;
|
|
|
|
static void timespec_sub(struct timespec *__restrict r,
|
|
const struct timespec *__restrict t1,
|
|
const struct timespec *__restrict t2)
|
|
{
|
|
r->tv_sec = t1->tv_sec - t2->tv_sec;
|
|
r->tv_nsec = t1->tv_nsec - t2->tv_nsec;
|
|
if (r->tv_nsec < 0) {
|
|
r->tv_sec--;
|
|
r->tv_nsec += 1000000000;
|
|
}
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
struct spi_ioc_oob_setup oob_setup;
|
|
struct timespec begin, end, delta;
|
|
struct sched_param param;
|
|
int tfd, devfd, ret, n;
|
|
char *tx, *rx;
|
|
void *iobuf;
|
|
|
|
if (argc > 1)
|
|
device = argv[1];
|
|
|
|
/*
|
|
* This is usual SPI configuration stuff using the spidev
|
|
* interface.
|
|
*/
|
|
devfd = open(device, O_RDWR);
|
|
if (devfd < 0)
|
|
error(1, errno, "can't open device %s", device);
|
|
|
|
ret = ioctl(devfd, SPI_IOC_WR_MODE32, &mode);
|
|
if (ret)
|
|
error(1, errno, "ioctl(SPI_IOC_WR_MODE32)");
|
|
|
|
ret = ioctl(devfd, SPI_IOC_WR_BITS_PER_WORD, &bits);
|
|
if (ret)
|
|
error(1, errno, "ioctl(SPI_IOC_WR_BITS_PER_WORD)");
|
|
|
|
ret = ioctl(devfd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
|
|
if (ret)
|
|
error(1, errno, "ioctl(SPI_IOC_WR_MAX_SPEED_HZ)");
|
|
|
|
/*
|
|
* This part switches the device to out-of-band operation
|
|
* mode. In this case, I/O is performed via the DMA engine
|
|
* exclusively, directly from/to buffers (m)mapped into the
|
|
* address space of the client.
|
|
*/
|
|
oob_setup.frame_len = len;
|
|
oob_setup.speed_hz = speed;
|
|
oob_setup.bits_per_word = bits;
|
|
ret = ioctl(devfd, SPI_IOC_ENABLE_OOB_MODE, &oob_setup);
|
|
if (ret)
|
|
error(1, errno, "ioctl(SPI_IOC_ENABLE_OOB_MODE)");
|
|
|
|
printf("mapping %d bytes, tx@%d, rx@%d, frame_len=%d\n",
|
|
oob_setup.iobuf_len, oob_setup.tx_offset, oob_setup.rx_offset, len);
|
|
|
|
/*
|
|
* We may map the I/O area now, it is composed of two adjacent
|
|
* buffers of @len bytes (plus alignment). CAUTION: the
|
|
* mapping is always on coherent DMA memory (i.e. non-cached).
|
|
*/
|
|
iobuf = mmap(NULL, oob_setup.iobuf_len, PROT_READ|PROT_WRITE,
|
|
MAP_SHARED, devfd, 0);
|
|
if (iobuf == MAP_FAILED)
|
|
error(1, errno, "mmap()");
|
|
|
|
/*
|
|
* Trick: we want evl_attach_self() to inherit the SCHED_FIFO
|
|
* policy and priority for scheduling by the EVL core. We
|
|
* could have used evl_set_schedattr() explicitly once
|
|
* attached instead.
|
|
*/
|
|
param.sched_priority = 1;
|
|
ret = pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m);
|
|
if (ret)
|
|
error(1, ret, "pthread_setschedparam()");
|
|
|
|
/*
|
|
* The core told us where to read and write data on return to
|
|
* ioctl(SPI_IOC_ENABLE_OOB_MODE), which is at some offset
|
|
* from the I/O buffer we received from mmap().
|
|
*/
|
|
tx = iobuf + oob_setup.tx_offset;
|
|
memset(tx, 0x42, len);
|
|
rx = iobuf + oob_setup.rx_offset;
|
|
memset(rx, 0, len);
|
|
|
|
/* Let's attach to the EVL core. */
|
|
tfd = evl_attach_self("oob-spi:%d", getpid());
|
|
if (tfd < 0)
|
|
error(1, -tfd, "cannot attach to the EVL core");
|
|
|
|
/*
|
|
* The actual I/O request: sending from @tx, receiving to
|
|
* @rx. Do some trivial test, running a single
|
|
* transaction. You may want to try making this a loop.
|
|
*/
|
|
evl_read_clock(EVL_CLOCK_MONOTONIC, &begin);
|
|
|
|
ret = oob_ioctl(devfd, SPI_IOC_RUN_OOB_XFER);
|
|
if (ret)
|
|
error(1, errno, "oob_ioctl(SPI_IOC_RUN_OOB_XFER)");
|
|
|
|
evl_read_clock(EVL_CLOCK_MONOTONIC, &end);
|
|
|
|
/*
|
|
* Do some visual control of the input buffer we received, it
|
|
* should be filled with value 0x42. We could have used
|
|
* printf(3) in the dump loop below, but were you to include
|
|
* this in an out-of-band loop for obtaining latency figures,
|
|
* then you would want evl_printf() to handle the printouts,
|
|
* so that no delay is incurred.
|
|
*/
|
|
timespec_sub(&delta, &end, &begin);
|
|
evl_printf("transfer done in %ld s, %ld us:",
|
|
delta.tv_sec, delta.tv_nsec / 1000);
|
|
|
|
/* Dump the contents of the input buffer. */
|
|
for (n = 0; n < len; n++) {
|
|
if (!(n % 16))
|
|
evl_printf("\n");
|
|
evl_printf("%.2x ", rx[n]);
|
|
}
|
|
|
|
evl_printf("\n");
|
|
|
|
/* All done, wrap it up. */
|
|
|
|
munmap(iobuf, oob_setup.iobuf_len);
|
|
|
|
ret = ioctl(devfd, SPI_IOC_DISABLE_OOB_MODE);
|
|
if (ret)
|
|
error(1, errno, "ioctl(SPI_IOC_DISABLE_OOB_MODE)");
|
|
|
|
close(devfd);
|
|
|
|
return 0;
|
|
}
|