Files
esp_llcc68_driver/src/hal_spi.cpp

436 lines
14 KiB
C++

//
// Created by Kurosu Chan on 2024/1/18.
//
#include <cstdint>
#include <driver/spi_master.h>
#include <esp_log.h>
#include "esp_err.h"
#include "hal_error.hpp"
#include "hal_gpio.hpp"
#include "hal_spi.hpp"
#include "utils/app_utils.hpp"
#include "utils/app_instant.hpp"
// https://docs.espressif.com/projects/esp-idf/en/v5.3.2/esp32h2/api-reference/peripherals/spi_master.html
// https://github.com/espressif/esp-idf/blob/v5.3.2/examples/peripherals/spi_master/lcd/main/spi_master_example_main.c
namespace app::driver::hal::spi {
static constexpr uint8_t CMD_BIT_SIZE = 8;
static constexpr uint8_t OFFSET_BIT_SIZE = 8;
static constexpr uint8_t REG_ADDR_BIT_SIZE = 16;
static constexpr auto MAX_BUFFER_SIZE = 256;
namespace details {
static bool is_initialized = false;
static spi_device_handle_t spi_device;
static uint8_t _data_buffer[MAX_BUFFER_SIZE];
/**
* @brief only used in read/write buffer io
*/
static std::span<uint8_t> data_buffer = std::span{_data_buffer};
}
/**
* a traditional way to complete an synchronous SPI transaction
*
* @sa https://github.com/nopnop2002/esp-idf-sx126x/blob/bd26fd20b118c6d57d35b0554d8de80378d614a8/components/ra01s/ra01s.c#L123-L161
* @sa https://github.com/IanBurwell/DynamicLRS/blob/c26f7f8dcca0c1b70af0aa6aee3aba3a1652aba6/components/DLRS_LoRadio/radiolib_esp32s3_hal.hpp#L193-L201
*
*/
static constexpr auto verify_statuses = [](std::span<const uint8_t> statuses) {
uint8_t i = 0;
for (const auto st : statuses) {
if (const auto err = status_to_err(st); err != error::OK) {
ESP_LOGE(TAG, "failed to verify status 0x%02x at byte %u", st, i);
return err;
}
i += 1;
}
return error::OK;
};
void init(spi_config_t config) {
if (details::is_initialized) {
return;
}
// Configure SPI bus
spi_bus_config_t bus_config = {
.mosi_io_num = MOSI_PIN,
.miso_io_num = MISO_PIN,
.sclk_io_num = SCLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = 0,
.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS,
.intr_flags = 0,
};
// Configure SPI device
// https://github.com/nopnop2002/esp-idf-sx126x/blob/main/components/ra01s/ra01s.c
//
// SPI_DEVICE_HALFDUPLEX flag will turn the device into half-duplex
// See also the phase
// https://docs.espressif.com/projects/esp-idf/en/v5.3.2/esp32/api-reference/peripherals/spi_master.html#spi-transactions
spi_device_interface_config_t dev_config = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 128,
.clock_speed_hz = config.clock_speed_hz,
.spics_io_num = CS_PIN,
.flags = SPI_DEVICE_NO_DUMMY,
.queue_size = 1,
};
ESP_ERROR_CHECK(spi_bus_initialize(config.host_id, &bus_config, SPI_DMA_CH_AUTO));
ESP_ERROR_CHECK(spi_bus_add_device(config.host_id, &dev_config, &details::spi_device));
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << BUSY_PIN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
details::is_initialized = true;
}
// https://docs.espressif.com/projects/esp-idf/en/release-v5.3/esp32/api-reference/peripherals/spi_master.html
// https://github.com/espressif/arduino-esp32/blob/5ba4c21a990f46b61ae8c7913b81e15064b2a8ef/cores/esp32/esp32-hal-spi.c#L836-L843
// https://github.com/espressif/esp-idf/issues/5223
/*!
\brief wait for busy pin to go down
\return true if the pin goes down, return false if timeout
\note The LLCC68 uses the pin BUSY to indicate the status of the chip and
its ability (or not) to receive another command while internal processing
occurs. Prior to executing one of the generic functions, it is thus
necessary to check the status of BUSY to make sure the chip is in a state
where it can process another function.
*/
inline bool wait_for_not_busy(const size_t timeout_ms) {
if constexpr (BUSY_PIN == GPIO_NUM_NC) {
app::utils::delay_ms(timeout_ms);
return true;
} else {
const auto io_inst = app::utils::Instant<>{};
while (hal::gpio::digital_read(BUSY_PIN) == hal::gpio::HIGH) {
if (io_inst.elapsed_ms() > timeout_ms) {
return false;
}
}
return true;
}
}
// The communication with the LLCC68 is organized around generic functions which
// allow the user to control the device behavior. Each function is based on an
// Operational Command (refer throughout this document as “Opcode”), which is
// then followed by a set of parameters. The LLCC68 use the BUSY pin to indicate
// the status of the chip. In the following chapters, it is assumed that host
// micro-controller has an SPI and access to it via spi.write(data). Data is an
// 8-bit word. The SPI chip select is defined by NSS, active low.
// Through the SPI interface, the host can issue commands to the chip or access
// the data memory space to directly retrieve or write data. In normal
// operation, a reduced number of direct data write operations is required
// except when accessing the data buffer. The user interacts with the circuit
// through an API (instruction set).
/*! READ */
constexpr auto STACK_BUFFER_SIZE = 32;
constexpr auto PADDING = 0xFE;
Result<Unit, error_t>
read_stream(uint8_t cmd, std::span<uint8_t> data, const size_t timeout_ms) {
std::array<uint8_t, STACK_BUFFER_SIZE> _rx_buf;
std::array<uint8_t, STACK_BUFFER_SIZE> _tx_buf;
if (data.empty() or data.size() > STACK_BUFFER_SIZE - 1) {
return ue_t{error::INVALID_SIZE};
}
if (not wait_for_not_busy(timeout_ms)) {
return ue_t{error::TIMEOUT};
}
auto rx_buffer = std::span{_rx_buf}.subspan(0, data.size() + 1);
auto tx_buffer = std::span{_tx_buf}.subspan(0, rx_buffer.size());
std::ranges::fill(tx_buffer, PADDING);
spi_transaction_ext_t transaction{};
// during the Command and Address phases, the members spi_transaction_t::cmd
// and spi_transaction_t::addr are sent to the bus, nothing is read at this
// time.
transaction = spi_transaction_ext_t{
.base = {
.flags = SPI_TRANS_VARIABLE_CMD,
.cmd = cmd,
// total data length, in bits
.length = static_cast<size_t>(rx_buffer.size() * 8),
// total data length received, should be not greater than length in
// full-duplex mode (0 defaults this to the value of length).
.rxlength = static_cast<size_t>(rx_buffer.size() * 8),
.tx_buffer = tx_buffer.data(),
.rx_buffer = rx_buffer.data(),
},
.command_bits = CMD_BIT_SIZE,
.address_bits = 0,
.dummy_bits = 0,
};
esp_err_t err = spi_device_transmit(details::spi_device, &transaction.base);
if (err != ESP_OK) {
ESP_LOGE(TAG, "spi transmit: %s (%d)", esp_err_to_name(err), err);
return ue_t{error::FAILED};
}
auto status = rx_buffer[0];
if (const auto err = status_to_err(status); err != error::OK) {
return ue_t{err};
}
std::copy(rx_buffer.begin() + 1, rx_buffer.end(), data.begin());
return {};
}
/*! WRITE */
Result<Unit, error_t>
write_stream(uint8_t cmd, std::span<const uint8_t> data, const size_t timeout_ms) {
std::array<uint8_t, STACK_BUFFER_SIZE> _rx_buf;
if (data.size() > STACK_BUFFER_SIZE - 1) {
return ue_t{error::INVALID_SIZE};
}
if (not wait_for_not_busy(timeout_ms)) {
return ue_t{error::TIMEOUT};
}
const auto rx_buffer = std::span{_rx_buf}.subspan(0, data.size());
spi_transaction_ext_t transaction{};
transaction = spi_transaction_ext_t{
.base = {
.flags = SPI_TRANS_VARIABLE_CMD,
.cmd = cmd,
.length = static_cast<size_t>(data.size() * 8),
.rxlength = static_cast<size_t>(rx_buffer.size() * 8),
.tx_buffer = data.data(),
.rx_buffer = rx_buffer.data(),
},
.command_bits = CMD_BIT_SIZE,
.address_bits = 0,
.dummy_bits = 0,
};
esp_err_t err = spi_device_transmit(details::spi_device, &transaction.base);
if (err != ESP_OK) {
ESP_LOGE(TAG, "spi transmit: %s (%d)", esp_err_to_name(err), err);
return ue_t{error::FAILED};
}
if (const auto err = verify_statuses(rx_buffer); err != error::OK) {
return ue_t{err};
}
return {};
}
/*! READ REGISTER */
Result<Unit, error_t>
read_register(uint16_t reg, std::span<uint8_t> data, const size_t timeout_ms) {
std::array<uint8_t, STACK_BUFFER_SIZE> _rx_buf;
std::array<uint8_t, STACK_BUFFER_SIZE> _tx_buf;
if (data.size() > STACK_BUFFER_SIZE - 1) {
return ue_t{error::INVALID_SIZE};
}
if (not wait_for_not_busy(timeout_ms)) {
return ue_t{error::TIMEOUT};
}
// Note that the host has to send an NOP after sending the 2
// bytes of address to start receiving data bytes on the next NOP sent.
//
// NOTE: I'm not sure if ESP32 would send 0x00 (NOP) after MOSI phase
// if the tx_buffer is nullptr.
spi_transaction_ext_t transaction;
auto rx_buffer = std::span{_rx_buf}.subspan(0, data.size() + 1);
auto tx_buffer = std::span{_tx_buf}.subspan(0, rx_buffer.size());
std::ranges::fill(tx_buffer, PADDING);
transaction = spi_transaction_ext_t{
.base = {
.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR,
.cmd = SPI_READ_COMMAND,
.addr = reg,
.length = static_cast<size_t>(rx_buffer.size() * 8),
.rxlength = static_cast<size_t>(rx_buffer.size() * 8),
.tx_buffer = tx_buffer.data(),
.rx_buffer = rx_buffer.data(),
},
.command_bits = static_cast<uint8_t>(CMD_BIT_SIZE),
.address_bits = static_cast<uint8_t>(REG_ADDR_BIT_SIZE),
.dummy_bits = 0,
};
esp_err_t err = spi_device_transmit(details::spi_device, &transaction.base);
if (err != ESP_OK) {
ESP_LOGE(TAG, "spi transmit: %s (%d)", esp_err_to_name(err), err);
return ue_t{error::FAILED};
}
auto status = rx_buffer[0];
if (const auto err = status_to_err(status); err != error::OK) {
return ue_t{err};
}
std::copy(rx_buffer.begin() + 1, rx_buffer.end(), data.begin());
return {};
}
/*! WRITE REGISTER */
Result<Unit, error_t>
write_register(uint16_t offset, std::span<const uint8_t> data, const size_t timeout_ms) {
std::array<uint8_t, STACK_BUFFER_SIZE> _rx_buf;
if (data.size() > STACK_BUFFER_SIZE - 1) {
return ue_t{error::INVALID_SIZE};
}
if (not wait_for_not_busy(timeout_ms)) {
return ue_t{error::TIMEOUT};
}
auto rx_buffer = std::span{_rx_buf}.subspan(0, data.size());
spi_transaction_ext_t transaction;
transaction = spi_transaction_ext_t{
.base = {
.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR,
.cmd = SPI_WRITE_COMMAND,
.addr = offset,
.length = static_cast<size_t>(data.size() * 8),
.rxlength = static_cast<size_t>(rx_buffer.size() * 8),
.tx_buffer = data.data(),
.rx_buffer = rx_buffer.data(),
},
.command_bits = static_cast<uint8_t>(CMD_BIT_SIZE),
.address_bits = static_cast<uint8_t>(REG_ADDR_BIT_SIZE),
.dummy_bits = 0,
};
esp_err_t err = spi_device_transmit(details::spi_device, &transaction.base);
if (err != ESP_OK) {
ESP_LOGE(TAG, "spi transmit: %s (%d)", esp_err_to_name(err), err);
return ue_t{error::FAILED};
}
if (const auto err = verify_statuses(rx_buffer); err != error::OK) {
return ue_t{err};
}
return {};
}
/*! READ BUFFER */
Result<std::span<const uint8_t>, error_t>
read_buffer(uint8_t offset, uint8_t size, const size_t timeout_ms) {
const auto size_required = static_cast<size_t>(size) + 1;
if (size_required > details::data_buffer.size()) {
return ue_t{error::INVALID_SIZE};
}
auto rx_buffer = details::data_buffer.subspan(0, size_required);
auto _tx_buffer = std::make_unique<uint8_t[]>(rx_buffer.size());
auto tx_buffer = std::span{_tx_buffer.get(), rx_buffer.size()};
std::ranges::fill(tx_buffer, PADDING);
spi_transaction_ext_t transaction;
transaction = spi_transaction_ext_t{
.base = {
.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR,
.cmd = SPI_READ_BUFFER_CMD,
.addr = offset,
.length = static_cast<size_t>(rx_buffer.size() * 8),
.rxlength = static_cast<size_t>(rx_buffer.size() * 8),
.tx_buffer = tx_buffer.data(),
.rx_buffer = rx_buffer.data(),
},
.command_bits = static_cast<uint8_t>(CMD_BIT_SIZE),
.address_bits = static_cast<uint8_t>(OFFSET_BIT_SIZE),
.dummy_bits = 0,
};
esp_err_t err = spi_device_transmit(details::spi_device, &transaction.base);
if (err != ESP_OK) {
ESP_LOGE(TAG, "spi transmit: %s (%d)", esp_err_to_name(err), err);
return ue_t{error::FAILED};
}
const auto status = rx_buffer[0];
if (const auto err = status_to_err(status); err != error::OK) {
return ue_t{err};
}
return std::span{rx_buffer.begin() + 1, rx_buffer.end()};
};
/*! WRITE BUFFER */
Result<Unit, error_t>
write_buffer(uint8_t offset, std::span<const uint8_t> data_from_host, const size_t timeout_ms) {
if (data_from_host.size() > details::data_buffer.size()) {
return ue_t{error::INVALID_SIZE};
}
if (not wait_for_not_busy(timeout_ms)) {
return ue_t{error::TIMEOUT};
}
auto rx_buffer = details::data_buffer.subspan(0, data_from_host.size());
spi_transaction_ext_t transaction;
transaction = spi_transaction_ext_t{
.base = {
.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR,
.cmd = SPI_WRITE_BUFFER_CMD,
.addr = offset,
.length = static_cast<size_t>(data_from_host.size() * 8),
.rxlength = static_cast<size_t>(rx_buffer.size() * 8),
.tx_buffer = data_from_host.data(),
.rx_buffer = rx_buffer.data(),
},
.command_bits = static_cast<uint8_t>(CMD_BIT_SIZE),
.address_bits = static_cast<uint8_t>(OFFSET_BIT_SIZE),
.dummy_bits = 0,
};
esp_err_t err = spi_device_transmit(details::spi_device, &transaction.base);
if (err != ESP_OK) {
ESP_LOGE(TAG, "spi transmit: %s (%d)", esp_err_to_name(err), err);
return ue_t{error::FAILED};
}
if (const auto err = verify_statuses(rx_buffer); err != error::OK) {
return ue_t{err};
}
return {};
}
}