| /* |
| * Copyright (c) 2021 Marc Reilly - Creative Product Design |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT zephyr_spi_bitbang |
| |
| #define LOG_LEVEL CONFIG_SPI_LOG_LEVEL |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(spi_bitbang); |
| |
| #include <zephyr/sys/sys_io.h> |
| #include <zephyr/drivers/spi.h> |
| #include <zephyr/drivers/spi/rtio.h> |
| #include "spi_context.h" |
| |
| struct spi_bitbang_data { |
| struct spi_context ctx; |
| int bits; |
| int wait_us; |
| int dfs; |
| }; |
| |
| struct spi_bitbang_config { |
| struct gpio_dt_spec clk_gpio; |
| struct gpio_dt_spec mosi_gpio; |
| struct gpio_dt_spec miso_gpio; |
| }; |
| |
| static int spi_bitbang_configure(const struct spi_bitbang_config *info, |
| struct spi_bitbang_data *data, |
| const struct spi_config *config) |
| { |
| if (config->operation & SPI_OP_MODE_SLAVE) { |
| LOG_ERR("Slave mode not supported"); |
| return -ENOTSUP; |
| } |
| |
| if (config->operation & (SPI_TRANSFER_LSB | SPI_LINES_DUAL |
| | SPI_LINES_QUAD)) { |
| LOG_ERR("Unsupported configuration"); |
| return -ENOTSUP; |
| } |
| |
| const int bits = SPI_WORD_SIZE_GET(config->operation); |
| |
| if (bits > 16) { |
| LOG_ERR("Word sizes > 16 bits not supported"); |
| return -ENOTSUP; |
| } |
| |
| data->bits = bits; |
| data->dfs = ((data->bits - 1) / 8) + 1; |
| if (config->frequency > 0) { |
| /* convert freq to period, the extra /2 is due to waiting |
| * twice in each clock cycle. The '2000' is an upscale factor. |
| */ |
| data->wait_us = (1000000ul * 2000ul / config->frequency) / 2000ul; |
| data->wait_us /= 2; |
| } else { |
| data->wait_us = 8 / 2; /* 125 kHz */ |
| } |
| |
| data->ctx.config = config; |
| |
| return 0; |
| } |
| |
| static int spi_bitbang_transceive(const struct device *dev, |
| const struct spi_config *spi_cfg, |
| const struct spi_buf_set *tx_bufs, |
| const struct spi_buf_set *rx_bufs) |
| { |
| const struct spi_bitbang_config *info = dev->config; |
| struct spi_bitbang_data *data = dev->data; |
| struct spi_context *ctx = &data->ctx; |
| int rc; |
| const struct gpio_dt_spec *miso = NULL; |
| const struct gpio_dt_spec *mosi = NULL; |
| gpio_flags_t mosi_flags = GPIO_OUTPUT_INACTIVE; |
| |
| rc = spi_bitbang_configure(info, data, spi_cfg); |
| if (rc < 0) { |
| return rc; |
| } |
| |
| if (spi_cfg->operation & SPI_HALF_DUPLEX) { |
| if (!info->mosi_gpio.port) { |
| LOG_ERR("No MOSI pin specified in half duplex mode"); |
| return -EINVAL; |
| } |
| |
| if (tx_bufs && rx_bufs) { |
| LOG_ERR("Both RX and TX specified in half duplex mode"); |
| return -EINVAL; |
| } else if (tx_bufs && !rx_bufs) { |
| /* TX mode */ |
| mosi = &info->mosi_gpio; |
| } else if (!tx_bufs && rx_bufs) { |
| /* RX mode */ |
| mosi_flags = GPIO_INPUT; |
| miso = &info->mosi_gpio; |
| } |
| } else { |
| if (info->mosi_gpio.port) { |
| mosi = &info->mosi_gpio; |
| } |
| |
| if (info->miso_gpio.port) { |
| miso = &info->miso_gpio; |
| } |
| } |
| |
| if (info->mosi_gpio.port) { |
| rc = gpio_pin_configure_dt(&info->mosi_gpio, mosi_flags); |
| if (rc < 0) { |
| LOG_ERR("Couldn't configure MOSI pin: %d", rc); |
| return rc; |
| } |
| } |
| |
| spi_context_buffers_setup(ctx, tx_bufs, rx_bufs, data->dfs); |
| |
| int clock_state = 0; |
| int cpha = 0; |
| bool loop = false; |
| |
| if (SPI_MODE_GET(spi_cfg->operation) & SPI_MODE_CPOL) { |
| clock_state = 1; |
| } |
| if (SPI_MODE_GET(spi_cfg->operation) & SPI_MODE_CPHA) { |
| cpha = 1; |
| } |
| if (SPI_MODE_GET(spi_cfg->operation) & SPI_MODE_LOOP) { |
| loop = true; |
| } |
| |
| /* set the initial clock state before CS */ |
| gpio_pin_set_dt(&info->clk_gpio, clock_state); |
| |
| spi_context_cs_control(ctx, true); |
| |
| const uint32_t wait_us = data->wait_us; |
| |
| while (spi_context_tx_buf_on(ctx) || spi_context_rx_buf_on(ctx)) { |
| uint16_t w = 0; |
| |
| if (ctx->tx_len) { |
| switch (data->dfs) { |
| case 2: |
| w = *(uint16_t *)(ctx->tx_buf); |
| break; |
| case 1: |
| w = *(uint8_t *)(ctx->tx_buf); |
| break; |
| } |
| } |
| |
| int shift = data->bits - 1; |
| uint16_t r = 0; |
| int b = 0; |
| bool do_read = false; |
| |
| if (miso && spi_context_rx_buf_on(ctx)) { |
| do_read = true; |
| } |
| |
| while (shift >= 0) { |
| const int d = (w >> shift) & 0x1; |
| |
| b = 0; |
| |
| /* setup data out first thing */ |
| if (mosi) { |
| gpio_pin_set_dt(mosi, d); |
| } |
| |
| k_busy_wait(wait_us); |
| |
| if (!loop && do_read && !cpha) { |
| b = gpio_pin_get_dt(miso); |
| } |
| |
| /* first (leading) clock edge */ |
| gpio_pin_set_dt(&info->clk_gpio, !clock_state); |
| |
| k_busy_wait(wait_us); |
| |
| if (!loop && do_read && cpha) { |
| b = gpio_pin_get_dt(miso); |
| } |
| |
| /* second (trailing) clock edge */ |
| gpio_pin_set_dt(&info->clk_gpio, clock_state); |
| |
| if (loop) { |
| b = d; |
| } |
| |
| r = (r << 1) | (b ? 0x1 : 0x0); |
| |
| --shift; |
| } |
| |
| if (spi_context_rx_buf_on(ctx)) { |
| switch (data->dfs) { |
| case 2: |
| *(uint16_t *)(ctx->rx_buf) = r; |
| break; |
| case 1: |
| *(uint8_t *)(ctx->rx_buf) = r; |
| break; |
| } |
| } |
| |
| LOG_DBG(" w: %04x, r: %04x , do_read: %d", w, r, do_read); |
| |
| spi_context_update_tx(ctx, data->dfs, 1); |
| spi_context_update_rx(ctx, data->dfs, 1); |
| } |
| |
| spi_context_cs_control(ctx, false); |
| |
| spi_context_complete(ctx, dev, 0); |
| |
| return 0; |
| } |
| |
| #ifdef CONFIG_SPI_ASYNC |
| static int spi_bitbang_transceive_async(const struct device *dev, |
| const struct spi_config *spi_cfg, |
| const struct spi_buf_set *tx_bufs, |
| const struct spi_buf_set *rx_bufs, |
| spi_callback_t cb, |
| void *userdata) |
| { |
| return -ENOTSUP; |
| } |
| #endif |
| |
| int spi_bitbang_release(const struct device *dev, |
| const struct spi_config *config) |
| { |
| struct spi_bitbang_data *data = dev->data; |
| struct spi_context *ctx = &data->ctx; |
| |
| spi_context_unlock_unconditionally(ctx); |
| return 0; |
| } |
| |
| static const struct spi_driver_api spi_bitbang_api = { |
| .transceive = spi_bitbang_transceive, |
| .release = spi_bitbang_release, |
| #ifdef CONFIG_SPI_ASYNC |
| .transceive_async = spi_bitbang_transceive_async, |
| #endif /* CONFIG_SPI_ASYNC */ |
| #ifdef CONFIG_SPI_RTIO |
| .iodev_submit = spi_rtio_iodev_default_submit, |
| #endif |
| }; |
| |
| int spi_bitbang_init(const struct device *dev) |
| { |
| const struct spi_bitbang_config *config = dev->config; |
| struct spi_bitbang_data *data = dev->data; |
| int rc; |
| |
| if (!gpio_is_ready_dt(&config->clk_gpio)) { |
| LOG_ERR("GPIO port for clk pin is not ready"); |
| return -ENODEV; |
| } |
| rc = gpio_pin_configure_dt(&config->clk_gpio, GPIO_OUTPUT_INACTIVE); |
| if (rc < 0) { |
| LOG_ERR("Couldn't configure clk pin; (%d)", rc); |
| return rc; |
| } |
| |
| if (config->mosi_gpio.port != NULL) { |
| if (!gpio_is_ready_dt(&config->mosi_gpio)) { |
| LOG_ERR("GPIO port for mosi pin is not ready"); |
| return -ENODEV; |
| } |
| rc = gpio_pin_configure_dt(&config->mosi_gpio, |
| GPIO_OUTPUT_INACTIVE); |
| if (rc < 0) { |
| LOG_ERR("Couldn't configure mosi pin; (%d)", rc); |
| return rc; |
| } |
| } |
| |
| if (config->miso_gpio.port != NULL) { |
| if (!gpio_is_ready_dt(&config->miso_gpio)) { |
| LOG_ERR("GPIO port for miso pin is not ready"); |
| return -ENODEV; |
| } |
| |
| |
| rc = gpio_pin_configure_dt(&config->miso_gpio, GPIO_INPUT); |
| if (rc < 0) { |
| LOG_ERR("Couldn't configure miso pin; (%d)", rc); |
| return rc; |
| } |
| } |
| |
| rc = spi_context_cs_configure_all(&data->ctx); |
| if (rc < 0) { |
| LOG_ERR("Failed to configure CS pins: %d", rc); |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| #define SPI_BITBANG_INIT(inst) \ |
| static struct spi_bitbang_config spi_bitbang_config_##inst = { \ |
| .clk_gpio = GPIO_DT_SPEC_INST_GET(inst, clk_gpios), \ |
| .mosi_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, mosi_gpios, {0}), \ |
| .miso_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, miso_gpios, {0}), \ |
| }; \ |
| \ |
| static struct spi_bitbang_data spi_bitbang_data_##inst = { \ |
| SPI_CONTEXT_INIT_LOCK(spi_bitbang_data_##inst, ctx), \ |
| SPI_CONTEXT_INIT_SYNC(spi_bitbang_data_##inst, ctx), \ |
| SPI_CONTEXT_CS_GPIOS_INITIALIZE(DT_DRV_INST(inst), ctx) \ |
| }; \ |
| \ |
| DEVICE_DT_INST_DEFINE(inst, \ |
| spi_bitbang_init, \ |
| NULL, \ |
| &spi_bitbang_data_##inst, \ |
| &spi_bitbang_config_##inst, \ |
| POST_KERNEL, \ |
| CONFIG_SPI_INIT_PRIORITY, \ |
| &spi_bitbang_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(SPI_BITBANG_INIT) |