From 51194b9c0db9bd4e5c893f7fb561a571925adf95 Mon Sep 17 00:00:00 2001 From: Frank Hessel Date: Sat, 6 Apr 2019 06:24:22 +0200 Subject: [PATCH 1/2] cpu/native: Implement hardware SPI access (Linux) --- cpu/native/Makefile.dep | 3 + cpu/native/Makefile.features | 5 + cpu/native/include/periph_conf.h | 41 ++++- cpu/native/include/periph_cpu.h | 46 +++++ cpu/native/include/spidev_linux.h | 134 +++++++++++++++ cpu/native/periph/pm.c | 11 ++ cpu/native/periph/spidev_linux.c | 274 ++++++++++++++++++++++++++++++ cpu/native/startup.c | 38 ++++- 8 files changed, 549 insertions(+), 3 deletions(-) create mode 100644 cpu/native/Makefile.dep create mode 100644 cpu/native/include/spidev_linux.h create mode 100644 cpu/native/periph/spidev_linux.c diff --git a/cpu/native/Makefile.dep b/cpu/native/Makefile.dep new file mode 100644 index 000000000000..4923a58c5914 --- /dev/null +++ b/cpu/native/Makefile.dep @@ -0,0 +1,3 @@ +ifneq (,$(filter periph_spi,$(USEMODULE))) + USEMODULE += periph_spidev_linux +endif diff --git a/cpu/native/Makefile.features b/cpu/native/Makefile.features index 638a8f0f0697..5efff466cf71 100644 --- a/cpu/native/Makefile.features +++ b/cpu/native/Makefile.features @@ -5,3 +5,8 @@ FEATURES_PROVIDED += periph_cpuid FEATURES_PROVIDED += periph_hwrng FEATURES_PROVIDED += periph_pm FEATURES_PROVIDED += periph_pwm + +# Access to hardware SPI bus is only supported on Linux hosts +ifeq ($(OS),Linux) + FEATURES_PROVIDED += periph_spi +endif diff --git a/cpu/native/include/periph_conf.h b/cpu/native/include/periph_conf.h index 6931fcbd3c10..70a0921c438f 100644 --- a/cpu/native/include/periph_conf.h +++ b/cpu/native/include/periph_conf.h @@ -17,7 +17,7 @@ #define PERIPH_CONF_H #ifdef __cplusplus - extern "C" { +extern "C" { #endif /** @@ -50,7 +50,6 @@ /** * @brief xtimer configuration - * @{ */ #define XTIMER_OVERHEAD 14 @@ -86,6 +85,44 @@ #define QDEC_NUMOF (8U) #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 } #endif diff --git a/cpu/native/include/periph_cpu.h b/cpu/native/include/periph_cpu.h index 6141790d6b8b..b76444731fd6 100644 --- a/cpu/native/include/periph_cpu.h +++ b/cpu/native/include/periph_cpu.h @@ -45,6 +45,52 @@ extern "C" { #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 } #endif diff --git a/cpu/native/include/spidev_linux.h b/cpu/native/include/spidev_linux.h new file mode 100644 index 000000000000..afc21d3679ff --- /dev/null +++ b/cpu/native/include/spidev_linux.h @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2019 Frank Hessel + * + * 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 + */ + +#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 */ +/** @} */ diff --git a/cpu/native/periph/pm.c b/cpu/native/periph/pm.c index 1b3b23c1a55a..72c2d8a989e5 100644 --- a/cpu/native/periph/pm.c +++ b/cpu/native/periph/pm.c @@ -26,6 +26,11 @@ #include "async_read.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) #include "debug.h" @@ -44,6 +49,9 @@ void pm_set_lowest(void) void pm_off(void) { puts("\nnative: exiting"); +#ifdef MODULE_PERIPH_SPIDEV_LINUX + spidev_linux_teardown(); +#endif real_exit(EXIT_SUCCESS); } @@ -52,6 +60,9 @@ void pm_reboot(void) printf("\n\n\t\t!! REBOOT !!\n\n"); native_async_read_cleanup(); +#ifdef MODULE_PERIPH_SPIDEV_LINUX + spidev_linux_teardown(); +#endif if (real_execve(_native_argv[0], _native_argv, NULL) == -1) { err(EXIT_FAILURE, "reboot: execve"); diff --git a/cpu/native/periph/spidev_linux.c b/cpu/native/periph/spidev_linux.c new file mode 100644 index 000000000000..a07c2b717eb4 --- /dev/null +++ b/cpu/native/periph/spidev_linux.c @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2019 Frank Hessel + * + * 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 + * @} + */ + +#ifdef MODULE_PERIPH_SPIDEV_LINUX + +#include +#include +#include + +#include +#include +/* 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 */ diff --git a/cpu/native/startup.c b/cpu/native/startup.c index 620b8fa48d8f..d9a69d780f08 100644 --- a/cpu/native/startup.c +++ b/cpu/native/startup.c @@ -76,7 +76,9 @@ netdev_tap_params_t netdev_tap_params[NETDEV_TAP_MAX]; #ifdef MODULE_CAN_LINUX #include "candev_linux.h" #endif - +#ifdef MODULE_PERIPH_SPIDEV_LINUX +#include "spidev_linux.h" +#endif #ifdef MODULE_SOCKET_ZEP #include "socket_zep_params.h" @@ -92,6 +94,9 @@ static const char short_opts[] = ":hi:s:deEoc:" #endif #ifdef MODULE_SOCKET_ZEP "z:" +#endif +#ifdef MODULE_PERIPH_SPIDEV_LINUX + "p:" #endif ""; @@ -112,6 +117,9 @@ static const struct option long_opts[] = { #endif #ifdef MODULE_SOCKET_ZEP { "zep", required_argument, NULL, 'z' }, +#endif +#ifdef MODULE_PERIPH_SPIDEV_LINUX + { "spi", required_argument, NULL, 'p' }, #endif { NULL, 0, NULL, '\0' }, }; @@ -251,6 +259,9 @@ void usage_exit(int status) real_printf(" -z :,:\n"); } #endif +#ifdef MODULE_PERIPH_SPIDEV_LINUX + real_printf(" [-p ::]\n"); +#endif real_printf(" help: %s -h\n\n", _progname); @@ -292,6 +303,15 @@ void usage_exit(int status) " -n :, --can :\n" " specify CAN interface to use for CAN device #\n" " max number of CAN device: %d\n", CAN_DLL_NUMOF); +#endif +#ifdef MODULE_PERIPH_SPIDEV_LINUX + real_printf( +" -p ::, --spi=::\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 real_exit(status); } @@ -455,6 +475,22 @@ __attribute__((constructor)) static void startup(int argc, char **argv, char **e case 'z': _zep_params_setup(optarg, zeps++); 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 default: usage_exit(EXIT_FAILURE); From 40fe3154b459cb8e1db5493475b4ed5c5b1db06f Mon Sep 17 00:00:00 2001 From: Frank Hessel Date: Sat, 6 Apr 2019 15:05:07 +0200 Subject: [PATCH 2/2] boards/native Add SPI feature to doc.txt --- boards/native/doc.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/boards/native/doc.txt b/boards/native/doc.txt index 4d8bc49b08d1..9161b3a8bf27 100644 --- a/boards/native/doc.txt +++ b/boards/native/doc.txt @@ -19,4 +19,5 @@ OS/RIOT/images/Native.jpg) - LEDs: One red and one green LED - state changes are printed to the UART - PWM: Dummy PWM - QDEC: Emulated according to PWM +- SPI: Runtime configurable - `/dev/spidev*` are supported (Linux host only) */