zephyr/drivers/spi/spi_sam0.c

522 lines
12 KiB
C

/*
* Copyright (c) 2017 Google LLC.
*
* SPDX-License-Identifier: Apache-2.0
*/
#define LOG_LEVEL CONFIG_SPI_LOG_LEVEL
#include <logging/log.h>
LOG_MODULE_REGISTER(spi_sam0);
#include "spi_context.h"
#include <errno.h>
#include <device.h>
#include <spi.h>
#include <soc.h>
/* Device constant configuration parameters */
struct spi_sam0_config {
SercomSpi *regs;
u32_t pads;
u32_t pm_apbcmask;
u16_t gclk_clkctrl_id;
};
/* Device run time data */
struct spi_sam0_data {
struct spi_context ctx;
};
static void wait_synchronization(SercomSpi *regs)
{
#if defined(SERCOM_SPI_SYNCBUSY_MASK)
/* SYNCBUSY is a register */
while ((regs->SYNCBUSY.reg & SERCOM_SPI_SYNCBUSY_MASK) != 0) {
}
#elif defined(SERCOM_SPI_STATUS_SYNCBUSY)
/* SYNCBUSY is a bit */
while ((regs->STATUS.reg & SERCOM_SPI_STATUS_SYNCBUSY) != 0) {
}
#else
#error Unsupported device
#endif
}
static int spi_sam0_configure(struct device *dev,
const struct spi_config *config)
{
const struct spi_sam0_config *cfg = dev->config->config_info;
struct spi_sam0_data *data = dev->driver_data;
SercomSpi *regs = cfg->regs;
SERCOM_SPI_CTRLA_Type ctrla = {.reg = 0};
SERCOM_SPI_CTRLB_Type ctrlb = {.reg = 0};
int div;
if (spi_context_configured(&data->ctx, config)) {
return 0;
}
if (SPI_OP_MODE_GET(config->operation) != SPI_OP_MODE_MASTER) {
/* Slave mode is not implemented. */
return -ENOTSUP;
}
ctrla.bit.MODE = SERCOM_SPI_CTRLA_MODE_SPI_MASTER_Val;
if ((config->operation & SPI_TRANSFER_LSB) != 0) {
ctrla.bit.DORD = 1;
}
if ((config->operation & SPI_MODE_CPOL) != 0) {
ctrla.bit.CPOL = 1;
}
if ((config->operation & SPI_MODE_CPHA) != 0) {
ctrla.bit.CPHA = 1;
}
ctrla.reg |= cfg->pads;
if ((config->operation & SPI_MODE_LOOP) != 0) {
/* Put MISO and MOSI on the same pad */
ctrla.bit.DOPO = 0;
ctrla.bit.DIPO = 0;
}
ctrla.bit.ENABLE = 1;
ctrlb.bit.RXEN = 1;
if (SPI_WORD_SIZE_GET(config->operation) != 8) {
return -ENOTSUP;
}
/* 8 bits per transfer */
ctrlb.bit.CHSIZE = 0;
/* Use the requested or next higest possible frequency */
div = (SOC_ATMEL_SAM0_GCLK0_FREQ_HZ / config->frequency) / 2 - 1;
div = max(0, min(UINT8_MAX, div));
/* Update the configuration only if it has changed */
if (regs->CTRLA.reg != ctrla.reg || regs->CTRLB.reg != ctrlb.reg ||
regs->BAUD.reg != div) {
regs->CTRLA.bit.ENABLE = 0;
wait_synchronization(regs);
regs->CTRLB = ctrlb;
wait_synchronization(regs);
regs->BAUD.reg = div;
wait_synchronization(regs);
regs->CTRLA = ctrla;
wait_synchronization(regs);
}
spi_context_cs_configure(&data->ctx);
data->ctx.config = config;
return 0;
}
static bool spi_sam0_transfer_ongoing(struct spi_sam0_data *data)
{
return spi_context_tx_on(&data->ctx) || spi_context_rx_on(&data->ctx);
}
static void spi_sam0_shift_master(SercomSpi *regs, struct spi_sam0_data *data)
{
u8_t tx;
u8_t rx;
if (spi_context_tx_buf_on(&data->ctx)) {
tx = *(u8_t *)(data->ctx.tx_buf);
} else {
tx = 0U;
}
while (!regs->INTFLAG.bit.DRE) {
}
regs->DATA.reg = tx;
spi_context_update_tx(&data->ctx, 1, 1);
while (!regs->INTFLAG.bit.RXC) {
}
rx = regs->DATA.reg;
if (spi_context_rx_buf_on(&data->ctx)) {
*data->ctx.rx_buf = rx;
}
spi_context_update_rx(&data->ctx, 1, 1);
}
/* Finish any ongoing writes and drop any remaining read data */
static void spi_sam0_finish(SercomSpi *regs)
{
while (!regs->INTFLAG.bit.TXC) {
}
while (regs->INTFLAG.bit.RXC) {
(void)regs->DATA.reg;
}
}
/* Fast path that transmits a buf */
static void spi_sam0_fast_tx(SercomSpi *regs, const struct spi_buf *tx_buf)
{
const u8_t *p = tx_buf->buf;
const u8_t *pend = (u8_t *)tx_buf->buf + tx_buf->len;
u8_t ch;
while (p != pend) {
ch = *p++;
while (!regs->INTFLAG.bit.DRE) {
}
regs->DATA.reg = ch;
}
spi_sam0_finish(regs);
}
/* Fast path that reads into a buf */
static void spi_sam0_fast_rx(SercomSpi *regs, const struct spi_buf *rx_buf)
{
u8_t *rx = rx_buf->buf;
int len = rx_buf->len;
if (len <= 0) {
return;
}
/* See the comment in spi_sam0_fast_txrx re: interleaving. */
/* Write the first byte */
regs->DATA.reg = 0;
len--;
while (len) {
/* Load byte N+1 into the transmit register */
regs->DATA.reg = 0;
len--;
/* Read byte N+0 from the receive register */
while (!regs->INTFLAG.bit.RXC) {
}
*rx++ = regs->DATA.reg;
}
/* Read the final incoming byte */
while (!regs->INTFLAG.bit.RXC) {
}
*rx = regs->DATA.reg;
spi_sam0_finish(regs);
}
/* Fast path that writes and reads bufs of the same length */
static void spi_sam0_fast_txrx(SercomSpi *regs,
const struct spi_buf *tx_buf,
const struct spi_buf *rx_buf)
{
const u8_t *tx = tx_buf->buf;
const u8_t *txend = (u8_t *)tx_buf->buf + tx_buf->len;
u8_t *rx = rx_buf->buf;
size_t len = rx_buf->len;
if (len == 0) {
return;
}
/*
* The code below interleaves the transmit writes with the
* receive reads to keep the bus fully utilised. The code is
* equivalent to:
*
* Transmit byte 0
* Loop:
* - Transmit byte n+1
* - Receive byte n
* Receive the final byte
*/
/* Write the first byte */
regs->DATA.reg = *tx++;
while (tx != txend) {
/* Load byte N+1 into the transmit register. TX is
* single buffered and we have at most one byte in
* flight so skip the DRE check.
*/
regs->DATA.reg = *tx++;
/* Read byte N+0 from the receive register */
while (!regs->INTFLAG.bit.RXC) {
}
*rx++ = regs->DATA.reg;
}
/* Read the final incoming byte */
while (!regs->INTFLAG.bit.RXC) {
}
*rx = regs->DATA.reg;
spi_sam0_finish(regs);
}
/* Fast path where every overlapping tx and rx buffer is the same length */
static void spi_sam0_fast_transceive(struct device *dev,
const struct spi_config *config,
const struct spi_buf_set *tx_bufs,
const struct spi_buf_set *rx_bufs)
{
const struct spi_sam0_config *cfg = dev->config->config_info;
size_t tx_count = 0;
size_t rx_count = 0;
SercomSpi *regs = cfg->regs;
const struct spi_buf *tx = NULL;
const struct spi_buf *rx = NULL;
if (tx_bufs) {
tx = tx_bufs->buffers;
tx_count = tx_bufs->count;
}
if (rx_bufs) {
rx = rx_bufs->buffers;
rx_count = rx_bufs->count;
} else {
rx = NULL;
}
while (tx_count != 0 && rx_count != 0) {
if (tx->buf == NULL) {
spi_sam0_fast_rx(regs, rx);
} else if (rx->buf == NULL) {
spi_sam0_fast_tx(regs, tx);
} else {
spi_sam0_fast_txrx(regs, tx, rx);
}
tx++;
tx_count--;
rx++;
rx_count--;
}
for (; tx_count != 0; tx_count--) {
spi_sam0_fast_tx(regs, tx++);
}
for (; rx_count != 0; rx_count--) {
spi_sam0_fast_rx(regs, rx++);
}
}
/* Returns true if the request is suitable for the fast
* path. Specifically, the bufs are a sequence of:
*
* - Zero or more RX and TX buf pairs where each is the same length.
* - Zero or more trailing RX only bufs
* - Zero or more trailing TX only bufs
*/
static bool spi_sam0_is_regular(const struct spi_buf_set *tx_bufs,
const struct spi_buf_set *rx_bufs)
{
const struct spi_buf *tx = NULL;
const struct spi_buf *rx = NULL;
size_t tx_count = 0;
size_t rx_count = 0;
if (tx_bufs) {
tx = tx_bufs->buffers;
tx_count = tx_bufs->count;
}
if (rx_bufs) {
rx = rx_bufs->buffers;
rx_count = rx_bufs->count;
}
if (!tx || !rx) {
return false;
}
while (tx_count != 0 && rx_count != 0) {
if (tx->len != rx->len) {
return false;
}
tx++;
tx_count--;
rx++;
rx_count--;
}
return true;
}
static int spi_sam0_transceive(struct device *dev,
const struct spi_config *config,
const struct spi_buf_set *tx_bufs,
const struct spi_buf_set *rx_bufs)
{
const struct spi_sam0_config *cfg = dev->config->config_info;
struct spi_sam0_data *data = dev->driver_data;
SercomSpi *regs = cfg->regs;
int err;
spi_context_lock(&data->ctx, false, NULL);
err = spi_sam0_configure(dev, config);
if (err != 0) {
goto done;
}
spi_context_cs_control(&data->ctx, true);
/* This driver special cases the common send only, receive
* only, and transmit then receive operations. This special
* casing is 4x faster than the spi_context() routines
* and allows the transmit and receive to be interleaved.
*/
if (spi_sam0_is_regular(tx_bufs, rx_bufs)) {
spi_sam0_fast_transceive(dev, config, tx_bufs, rx_bufs);
} else {
spi_context_buffers_setup(&data->ctx, tx_bufs, rx_bufs, 1);
do {
spi_sam0_shift_master(regs, data);
} while (spi_sam0_transfer_ongoing(data));
}
spi_context_cs_control(&data->ctx, false);
done:
spi_context_release(&data->ctx, err);
return err;
}
static int spi_sam0_transceive_sync(struct device *dev,
const struct spi_config *config,
const struct spi_buf_set *tx_bufs,
const struct spi_buf_set *rx_bufs)
{
struct spi_sam0_data *data = dev->driver_data;
spi_context_lock(&data->ctx, false, NULL);
return spi_sam0_transceive(dev, config, tx_bufs, rx_bufs);
}
#ifdef CONFIG_SPI_ASYNC
static int spi_sam0_transceive_async(struct device *dev,
const struct spi_config *config,
const struct spi_buf_set *tx_bufs,
const struct spi_buf_set *rx_bufs,
struct k_poll_signal *async)
{
struct spi_sam0_data *data = dev->driver_data;
spi_context_lock(&data->ctx, true, async);
return spi_sam0_transceive(dev, config, tx_bufs, rx_bufs);
}
#endif /* CONFIG_SPI_ASYNC */
static int spi_sam0_release(struct device *dev,
const struct spi_config *config)
{
struct spi_sam0_data *data = dev->driver_data;
spi_context_unlock_unconditionally(&data->ctx);
return 0;
}
static int spi_sam0_init(struct device *dev)
{
const struct spi_sam0_config *cfg = dev->config->config_info;
struct spi_sam0_data *data = dev->driver_data;
SercomSpi *regs = cfg->regs;
/* Enable the GCLK */
GCLK->CLKCTRL.reg = cfg->gclk_clkctrl_id | GCLK_CLKCTRL_GEN_GCLK0 |
GCLK_CLKCTRL_CLKEN;
/* Enable SERCOM clock in PM */
PM->APBCMASK.reg |= cfg->pm_apbcmask;
/* Disable all SPI interrupts */
regs->INTENCLR.reg = SERCOM_SPI_INTENCLR_MASK;
wait_synchronization(regs);
spi_context_unlock_unconditionally(&data->ctx);
/* The device will be configured and enabled when transceive
* is called.
*/
return 0;
}
static const struct spi_driver_api spi_sam0_driver_api = {
.transceive = spi_sam0_transceive_sync,
#ifdef CONFIG_SPI_ASYNC
.transceive_async = spi_sam0_transceive_async,
#endif
.release = spi_sam0_release,
};
#define SPI_SAM0_SERCOM_PADS(n) \
SERCOM_SPI_CTRLA_DIPO(DT_SPI_SAM0_SERCOM##n##_DIPO) | \
SERCOM_SPI_CTRLA_DOPO(DT_SPI_SAM0_SERCOM##n##_DOPO)
#define SPI_SAM0_DEFINE_CONFIG(n) \
static const struct spi_sam0_config spi_sam0_config_##n = { \
.regs = (SercomSpi *)DT_SPI_SAM0_SERCOM##n##_BASE_ADDRESS, \
.pm_apbcmask = PM_APBCMASK_SERCOM##n, \
.gclk_clkctrl_id = GCLK_CLKCTRL_ID_SERCOM##n##_CORE, \
.pads = SPI_SAM0_SERCOM_PADS(n) \
}
#define SPI_SAM0_DEVICE_INIT(n) \
SPI_SAM0_DEFINE_CONFIG(n); \
static struct spi_sam0_data spi_sam0_dev_data_##n = { \
SPI_CONTEXT_INIT_LOCK(spi_sam0_dev_data_##n, ctx), \
SPI_CONTEXT_INIT_SYNC(spi_sam0_dev_data_##n, ctx), \
}; \
DEVICE_AND_API_INIT(spi_sam0_##n, \
DT_SPI_SAM0_SERCOM##n##_LABEL, \
&spi_sam0_init, &spi_sam0_dev_data_##n, \
&spi_sam0_config_##n, POST_KERNEL, \
CONFIG_SPI_INIT_PRIORITY, &spi_sam0_driver_api)
#if DT_SPI_SAM0_SERCOM0_BASE_ADDRESS
SPI_SAM0_DEVICE_INIT(0);
#endif
#if DT_SPI_SAM0_SERCOM1_BASE_ADDRESS
SPI_SAM0_DEVICE_INIT(1);
#endif
#if DT_SPI_SAM0_SERCOM2_BASE_ADDRESS
SPI_SAM0_DEVICE_INIT(2);
#endif
#if DT_SPI_SAM0_SERCOM3_BASE_ADDRESS
SPI_SAM0_DEVICE_INIT(3);
#endif
#if DT_SPI_SAM0_SERCOM4_BASE_ADDRESS
SPI_SAM0_DEVICE_INIT(4);
#endif
#if DT_SPI_SAM0_SERCOM5_BASE_ADDRESS
SPI_SAM0_DEVICE_INIT(5);
#endif