Merge pull request #11352 from fhessel/native_spi

cpu/native: Allow Access to Hardware SPI Bus on Linux
This commit is contained in:
Martine Lenders 2019-10-16 15:49:44 +02:00 committed by GitHub
commit 31228bc0fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 550 additions and 3 deletions

View File

@ -19,4 +19,5 @@ OS/RIOT/images/Native.jpg)
- LEDs: One red and one green LED - state changes are printed to the UART - LEDs: One red and one green LED - state changes are printed to the UART
- PWM: Dummy PWM - PWM: Dummy PWM
- QDEC: Emulated according to PWM - QDEC: Emulated according to PWM
- SPI: Runtime configurable - `/dev/spidev*` are supported (Linux host only)
*/ */

3
cpu/native/Makefile.dep Normal file
View File

@ -0,0 +1,3 @@
ifneq (,$(filter periph_spi,$(USEMODULE)))
USEMODULE += periph_spidev_linux
endif

View File

@ -5,3 +5,8 @@ FEATURES_PROVIDED += periph_cpuid
FEATURES_PROVIDED += periph_hwrng FEATURES_PROVIDED += periph_hwrng
FEATURES_PROVIDED += periph_pm FEATURES_PROVIDED += periph_pm
FEATURES_PROVIDED += periph_pwm FEATURES_PROVIDED += periph_pwm
# Access to hardware SPI bus is only supported on Linux hosts
ifeq ($(OS),Linux)
FEATURES_PROVIDED += periph_spi
endif

View File

@ -17,7 +17,7 @@
#define PERIPH_CONF_H #define PERIPH_CONF_H
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
/** /**
@ -50,7 +50,6 @@
/** /**
* @brief xtimer configuration * @brief xtimer configuration
* @{
*/ */
#define XTIMER_OVERHEAD 14 #define XTIMER_OVERHEAD 14
@ -86,6 +85,44 @@
#define QDEC_NUMOF (8U) #define QDEC_NUMOF (8U)
#endif #endif
/**
* @name SPI configuration (Linux host only)
* @{
*/
#if !defined(SPI_NUMOF) || defined(DOXYGEN)
/**
* @brief Amount of SPI devices
*
* Allows up to SPI_NUMOF SPI devices with each having up to SPI_MAXCS hardware
* cable select lines. Assignment to hardware devices can be configured at
* runtime using the `--spi` startup parameter.
*
* Can be overriden during compile time with a `-DSPI_NUMOF=n` flag.
*/
#define SPI_NUMOF (1U)
#endif
#if !defined(SPI_MAXCS) || defined(DOXYGEN)
/**
* @brief Maximum amount of chip select lines per bus
*
* Allows up to SPI_MAXCS hardware cable select lines per SPI device. The n-th
* hardware select line can be used with the SPI_HWCS macro.
*/
#define SPI_MAXCS (4U)
#endif
/**
* @brief Hardware chip select access macro.
*
* The amount of available hardware chip select lines depends on the SPI_MAXCS
* parameter. If the line is actually available at runtime depends of whether a
* `--spi` startup parameter with the corresponding SPI device and HWCS-line
* parameter has been given.
*/
#define SPI_HWCS(x) (x)
/** @} */
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -45,6 +45,52 @@ extern "C" {
#define PROVIDES_PM_SET_LOWEST #define PROVIDES_PM_SET_LOWEST
/** @} */ /** @} */
/* Configuration for the wrapper around the Linux SPI API (periph_spidev_linux)
*
* Needs to go here, otherwise the SPI_NEEDS_ are defined after inclusion of
* spi.h.
*/
#if defined(MODULE_PERIPH_SPIDEV_LINUX) || defined(DOXYGEN)
/**
* @name SPI Configuration
*/
/**
* @brief Use the common `transfer_byte` SPI function
*/
#define PERIPH_SPI_NEEDS_TRANSFER_BYTE
/**
* @brief Use the common `transfer_reg` SPI function
*/
#define PERIPH_SPI_NEEDS_TRANSFER_REG
/**
* @brief Use the common `transfer_regs` SPI function
*/
#define PERIPH_SPI_NEEDS_TRANSFER_REGS
/**
* @brief Use a custom clock speed type
*/
#define HAVE_SPI_CLK_T
/**
* @brief SPI clock speed values
*
* The Linux userspace driver takes values in Hertz, which values are available
* can only be determined at runtime.
* @{
*/
typedef enum {
SPI_CLK_100KHZ = (100000U),
SPI_CLK_400KHZ = (400000U),
SPI_CLK_1MHZ = (1000000U),
SPI_CLK_5MHZ = (5000000U),
SPI_CLK_10MHZ = (10000000U)
} spi_clk_t;
/** @} */
#endif /* MODULE_PERIPH_SPI | DOXYGEN */
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -0,0 +1,134 @@
/*
* Copyright (C) 2019 Frank Hessel <frank@fhessel.de>
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @defgroup drivers_spidev_linux Linux User Mode SPI Driver
* @ingroup cpu_native
* @brief Implementation of SPI access from Linux User Space
*
* This module allows to connect a RIOT application that runs on a Linux host to
* the physical SPI bus(ses) of that host. To do so, the application has to be
* compiled for the native board in a Linux environment.
*
* SPI support is automatically included if either a module requiring the
* `PERIPH_SPI` feature is added to the application or if it is explicitly
* listed as `FEATURES_REQUIRED` in the application's Makefile.
*
* At runtime, the process has to be connected to a specific bus on the host
* machine. SPI busses are exposed as `/dev/spidevB.D` character files, where B
* is the Bus ID (MISO, MOSI and SCLK lines) and D denotes the connected device
* or hardware chip select line. Ideally, this structure should be reflected
* when mapping the device files to RIOT SPI busses.
*
* Example:
*
* ```
* $ ./riot_native_app --spi=0:0:/dev/spidev0.0 --spi=0:1:/dev/spidev0.1
* ```
*
* This will add `/dev/spidev0.0` and `/dev/spidev0.1` as SPI_DEV(0) in RIOT.
* The first device can be used with SPI_HWCS(0) as CS parameter, the second one
* with SPI_HWCS(1) as CS parameter.
*
* Multiple SPI busses can be added by increasing SPI_NUMOF in the Makefile:
* ```
* CFLAGS += -DSPI_NUMOF=n
* ```
*
* The sames goes for the SPI_MAXCS parameter that defines the maximum number of
* SPI_HWCS values per bus.
*
* Busses that aren't assigned during startup will return either SPI_NODEV or
* SPI_NOCS when accessed.
*
* If the SPI API is called with SPI_CS_UNDEF as CS parameter, the driver will
* select the file descriptor with the lowest HWCS id for that bus, but the
* actual CS line will not be pulled low (if the hardware supports this). This
* would (in principle) allow to control CS manually.
*
* @{
*
* @file
* @brief Implementation of SPI access from Linux User Space
*
* @author Frank Hessel <frank@fhessel.de>
*/
#ifndef SPIDEV_LINUX_H
#define SPIDEV_LINUX_H
#if defined(__linux__) || defined(DOXYGEN) /* Linux-only */
#include "periph/spi.h"
#include "mutex.h"
#include "periph_conf.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Status codes for SPI device setup
*/
enum {
SPI_SETUP_OK = 0, /**< parameters are sound */
SPI_SETUP_INVALID = -1, /**< invalid params or duplicate definition */
};
/**
* @brief Static runtime configuration for SPI port + CS line
*
* Contains the information that is passed by command line on startup
*/
typedef struct spidev_linux_conf {
/** Filename for a specific SPI device + CS line (like /dev/spidev0.0) */
char *device_filename[SPI_MAXCS];
} spidev_linux_conf_t;
/**
* @brief Dynamic runtime state for SPI port + CS line
*
* Contains state of the line (whether if it's opened, in use, available, ...)
*/
typedef struct spidev_linux_state {
/** Mutex for the whole bus (all CS lines) */
mutex_t lock;
/** File descriptors for each CS line on the bus */
int fd[SPI_MAXCS];
} spidev_linux_state_t;
/**
* @brief register `/dev/spidev*` device to be used for SPI
*
* @param[in] bus SPI bus id of the device
* @param[in] cs CS line to configure
* @param[in] name path name for `/dev/spidev*` device
* @return SPI_SETUP_OK On success
* @return SPI_SETUP_INVALID On invalid parameters
*/
int spidev_linux_setup(spi_t bus, spi_cs_t cs, const char *name);
/**
* @brief Close open SPI file descriptors
*/
void spidev_linux_teardown(void);
#ifdef __cplusplus
}
#endif
#else
/* Create the error in the header file as spi.c will be compiled to late to show it */
#ifdef MODULE_PERIPH_SPIDEV_LINUX
#error "MODULE periph_spidev_linux is only available on Linux"
#endif
#endif /* defined(__linux__) || defined(DOXYGEN) */
#endif /* SPIDEV_LINUX_H */
/** @} */

View File

@ -26,6 +26,11 @@
#include "async_read.h" #include "async_read.h"
#include "tty_uart.h" #include "tty_uart.h"
#ifdef MODULE_PERIPH_SPIDEV_LINUX
/* Only manage SPI if it is part of the build */
#include "spidev_linux.h"
#endif
#define ENABLE_DEBUG (0) #define ENABLE_DEBUG (0)
#include "debug.h" #include "debug.h"
@ -44,6 +49,9 @@ void pm_set_lowest(void)
void pm_off(void) void pm_off(void)
{ {
puts("\nnative: exiting"); puts("\nnative: exiting");
#ifdef MODULE_PERIPH_SPIDEV_LINUX
spidev_linux_teardown();
#endif
real_exit(EXIT_SUCCESS); real_exit(EXIT_SUCCESS);
} }
@ -52,6 +60,9 @@ void pm_reboot(void)
printf("\n\n\t\t!! REBOOT !!\n\n"); printf("\n\n\t\t!! REBOOT !!\n\n");
native_async_read_cleanup(); native_async_read_cleanup();
#ifdef MODULE_PERIPH_SPIDEV_LINUX
spidev_linux_teardown();
#endif
if (real_execve(_native_argv[0], _native_argv, NULL) == -1) { if (real_execve(_native_argv[0], _native_argv, NULL) == -1) {
err(EXIT_FAILURE, "reboot: execve"); err(EXIT_FAILURE, "reboot: execve");

View File

@ -0,0 +1,274 @@
/*
* Copyright (C) 2019 Frank Hessel <frank@fhessel.de>
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup cpu_native
* @ingroup drivers_spidev_linux
* @{
*
* @file
* @brief Implementation of SPI access from Linux User Space
*
* @author Frank Hessel <frank@fhessel.de>
* @}
*/
#ifdef MODULE_PERIPH_SPIDEV_LINUX
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/limits.h>
#include <linux/spi/spidev.h>
/* Linux' SPI_MODE_N collide with RIOT's spi_mode_t enum */
#undef SPI_MODE_0
#undef SPI_MODE_1
#undef SPI_MODE_2
#undef SPI_MODE_3
#include "assert.h"
#include "native_internal.h"
#include "spidev_linux.h"
#define ENABLE_DEBUG (0)
#include "debug.h"
/**
* @brief Holds the configuration for each SPI device (pathnames)
*/
static spidev_linux_conf_t device_conf[SPI_NUMOF];
/**
* @brief Holds the current state for each SPI device (file descriptors, mutex)
*/
static spidev_linux_state_t device_state[SPI_NUMOF];
/**
* @brief Returns the fd of the first valid cs line
*/
static int spidev_get_first_fd(spidev_linux_state_t *state);
/**
* @brief Initializes a spidev_linux_state_t structure
*/
static void spidev_init_device_state(spidev_linux_state_t *state);
/**
* @brief Applies bus parameters
*
* @param[in] fd File descriptor for the bus
* @param[in] hwcs true if the hardware chip select line should be used
* @param[in] mode SPI mode (0..3)
* @param[in] clk Clock rate in Hertz
*
* @return SPI_OK If everything went well
* @return SPI_NOMODE If setting the mode didn't work
* @return SPI_NOCLK If setting the clock didn't work
*/
static int spi_set_params(int fd, bool hwcs, spi_mode_t mode, spi_clk_t clk);
static void spidev_init_device_state(spidev_linux_state_t *state)
{
mutex_init(&(state->lock));
for (spi_cs_t cs = 0; cs < SPI_MAXCS; cs++) {
state->fd[cs] = -1;
}
}
static int spidev_get_first_fd(spidev_linux_state_t *state)
{
int fd = -1;
for (spi_cs_t cs = 0; cs < SPI_MAXCS && fd < 0; cs++) {
fd = state->fd[cs];
}
return fd;
}
int spidev_linux_setup(spi_t bus, spi_cs_t cs, const char *name)
{
if (bus >= SPI_NUMOF || cs >= SPI_MAXCS) {
return SPI_SETUP_INVALID;
}
spidev_linux_conf_t *conf = &(device_conf[bus]);
if (conf->device_filename[cs] != NULL) {
return SPI_SETUP_INVALID;
}
device_conf[bus].device_filename[cs] = strndup(name, PATH_MAX - 1);
return SPI_SETUP_OK;
}
void spidev_linux_teardown(void)
{
for (spi_t bus = 0; bus < SPI_NUMOF; bus++) {
spidev_linux_state_t *state = &(device_state[bus]);
for (spi_cs_t cs = 0; cs < SPI_MAXCS; cs++) {
if (state->fd[cs] >= 0) {
real_close(state->fd[cs]);
}
}
spidev_init_device_state(state);
}
}
int spi_acquire(spi_t bus, spi_cs_t cs, spi_mode_t mode, spi_clk_t clk)
{
DEBUG("spi_acquire(%d, %d, 0x%02x, %d)\n", bus, cs, mode, clk);
if (bus >= SPI_NUMOF) {
return SPI_NODEV;
}
mutex_lock(&(device_state[bus].lock));
bool use_hwcs = false;
int fd = -1;
if (cs != SPI_CS_UNDEF) {
use_hwcs = true;
if (cs > SPI_MAXCS || device_state[bus].fd[cs] < 0) {
DEBUG("spi_acquire: No fd for %d:%d\n", bus, cs);
return SPI_NOCS;
}
fd = device_state[bus].fd[cs];
DEBUG("spi_acquire: Using %d:%d with HWCS (-> fd 0x%x)\n", bus, cs, fd);
}
else {
fd = spidev_get_first_fd(&(device_state[bus]));
if (fd < 0) {
return SPI_NOCS;
}
DEBUG("spi_acquire: Using SPI_NO_CS (-> fd 0x%x)\n", fd);
}
int res = spi_set_params(fd, use_hwcs, mode, clk);
if (res < 0) {
DEBUG("spi_acquire: set_params failed for %d:%d\n", bus, cs);
mutex_unlock(&(device_state[bus].lock));
}
DEBUG("spi_acquire: %d:%d acquired\n", bus, cs);
return SPI_OK;
}
void spi_init(spi_t bus)
{
assert(bus < SPI_NUMOF);
spidev_linux_state_t *state = &(device_state[bus]);
spidev_linux_conf_t *conf = &(device_conf[bus]);
spidev_init_device_state(state);
DEBUG("spi_init: init bus %d\n", bus);
for (spi_cs_t cs = 0; cs < SPI_MAXCS; cs++) {
if (conf->device_filename[cs] != NULL) {
int fd = real_open(conf->device_filename[cs], O_RDWR);
if (fd < 0) {
/* Add a printf instead of only asserting to show invalid bus */
real_printf(
"Cannot acquire %s for spidev%d:%d\n",
conf->device_filename[cs],
bus,
cs
);
assert(false);
}
DEBUG("spi_init: %d:%d %s (fd 0x%x)\n", bus, cs,
conf->device_filename[cs], fd);
state->fd[cs] = fd;
}
else {
DEBUG("spi_init: %d:%d Unused\n", bus, cs);
}
}
DEBUG("spi_init: done\n");
}
int spi_init_cs(spi_t bus, spi_cs_t cs)
{
if (bus >= SPI_NUMOF) {
return SPI_NODEV;
}
else if (cs != SPI_CS_UNDEF && cs >= SPI_MAXCS) {
return SPI_NOCS;
}
else if (device_state[bus].fd[cs] < 0) {
return SPI_NOCS;
}
return SPI_OK;
}
void spi_init_pins(spi_t bus)
{
(void)bus;
/* Nothing to do here, as the kernel driver does the pin management */
}
void spi_release(spi_t bus)
{
DEBUG("spi_release(%d)\n", bus);
if (bus < SPI_NUMOF) {
mutex_unlock(&(device_state[bus].lock));
}
}
static int spi_set_params(int fd, bool hwcs, spi_mode_t mode, spi_clk_t clk)
{
uint8_t spi_mode = mode | (hwcs ? 0 : SPI_NO_CS);
uint32_t ioctl_clk = clk;
if (real_ioctl(fd, SPI_IOC_WR_MODE, &spi_mode) < 0) {
return SPI_NOMODE;
}
if (real_ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &ioctl_clk) < 0) {
return SPI_NOCLK;
}
return SPI_OK;
}
void spi_transfer_bytes(spi_t bus, spi_cs_t cs, bool cont,
const void *out, void *in, size_t len)
{
if (bus >= SPI_NUMOF || (cs != SPI_CS_UNDEF && cs >= SPI_MAXCS)) {
return;
}
int fd = (cs == SPI_CS_UNDEF) ?
spidev_get_first_fd(&(device_state[bus])) :
device_state[bus].fd[cs];
if (fd < 0) {
return;
}
intptr_t out_addr = (intptr_t)out;
intptr_t in_addr = (intptr_t)in;
struct spi_ioc_transfer spi_tf = {
.bits_per_word = 8,
/*
* The kernel documentation is a bit ambiguous about how to use the
* cs_change value ("True to deselect device"). It seems like
* setting it to true leaves the CS line actually low (=selected)
* after transmission.
*/
.cs_change = cont,
.len = len,
.rx_buf = (uint64_t)in_addr,
.tx_buf = (uint64_t)out_addr,
/* Leaving speed_hz as zero uses the value from spi_acquire */
.speed_hz = 0,
};
if (real_ioctl(fd, SPI_IOC_MESSAGE(1), &spi_tf) < 0) {
DEBUG("spi_transfer_bytes: ioctl failed\n");
}
else {
DEBUG("\nspi_transfer_bytes: transfered %d bytes\n", len);
}
}
#endif /* MODULE_PERIPH_SPIDEV_LINUX */

View File

@ -76,7 +76,9 @@ netdev_tap_params_t netdev_tap_params[NETDEV_TAP_MAX];
#ifdef MODULE_CAN_LINUX #ifdef MODULE_CAN_LINUX
#include "candev_linux.h" #include "candev_linux.h"
#endif #endif
#ifdef MODULE_PERIPH_SPIDEV_LINUX
#include "spidev_linux.h"
#endif
#ifdef MODULE_SOCKET_ZEP #ifdef MODULE_SOCKET_ZEP
#include "socket_zep_params.h" #include "socket_zep_params.h"
@ -92,6 +94,9 @@ static const char short_opts[] = ":hi:s:deEoc:"
#endif #endif
#ifdef MODULE_SOCKET_ZEP #ifdef MODULE_SOCKET_ZEP
"z:" "z:"
#endif
#ifdef MODULE_PERIPH_SPIDEV_LINUX
"p:"
#endif #endif
""; "";
@ -112,6 +117,9 @@ static const struct option long_opts[] = {
#endif #endif
#ifdef MODULE_SOCKET_ZEP #ifdef MODULE_SOCKET_ZEP
{ "zep", required_argument, NULL, 'z' }, { "zep", required_argument, NULL, 'z' },
#endif
#ifdef MODULE_PERIPH_SPIDEV_LINUX
{ "spi", required_argument, NULL, 'p' },
#endif #endif
{ NULL, 0, NULL, '\0' }, { NULL, 0, NULL, '\0' },
}; };
@ -251,6 +259,9 @@ void usage_exit(int status)
real_printf(" -z <laddr>:<lport>,<raddr>:<rport>\n"); real_printf(" -z <laddr>:<lport>,<raddr>:<rport>\n");
} }
#endif #endif
#ifdef MODULE_PERIPH_SPIDEV_LINUX
real_printf(" [-p <b>:<d>:<spidev>]\n");
#endif
real_printf(" help: %s -h\n\n", _progname); real_printf(" help: %s -h\n\n", _progname);
@ -292,6 +303,15 @@ void usage_exit(int status)
" -n <ifnum>:<ifname>, --can <ifnum>:<ifname>\n" " -n <ifnum>:<ifname>, --can <ifnum>:<ifname>\n"
" specify CAN interface <ifname> to use for CAN device #<ifnum>\n" " specify CAN interface <ifname> to use for CAN device #<ifnum>\n"
" max number of CAN device: %d\n", CAN_DLL_NUMOF); " max number of CAN device: %d\n", CAN_DLL_NUMOF);
#endif
#ifdef MODULE_PERIPH_SPIDEV_LINUX
real_printf(
" -p <b>:<d>:<spidev>, --spi=<b>:<d>:<spidev>\n"
" specify Linux SPI device to use for CS line d on bus b (in RIOT)\n"
" Example: --spi=0:1:/dev/spidev0.0 will assign the file spidev0.0 to\n"
" SPI_DEV(0) and SPI_HWCS(1).\n"
" Supports up to %d busses with %d CS lines each.\n", SPI_NUMOF, SPI_MAXCS
);
#endif #endif
real_exit(status); real_exit(status);
} }
@ -455,6 +475,22 @@ __attribute__((constructor)) static void startup(int argc, char **argv, char **e
case 'z': case 'z':
_zep_params_setup(optarg, zeps++); _zep_params_setup(optarg, zeps++);
break; break;
#endif
#ifdef MODULE_PERIPH_SPIDEV_LINUX
case 'p': {
long bus = strtol(optarg, &optarg, 10);
if (*optarg != ':') {
usage_exit(EXIT_FAILURE);
}
long cs = strtol(++optarg, &optarg, 10);
if (*optarg != ':') {
usage_exit(EXIT_FAILURE);
}
if (spidev_linux_setup(bus, cs, ++optarg) < 0) {
usage_exit(EXIT_FAILURE);
}
}
break;
#endif #endif
default: default:
usage_exit(EXIT_FAILURE); usage_exit(EXIT_FAILURE);