Files
esp_llcc68_driver/src/hal_spi.cpp
2025-05-14 12:17:06 +08:00

414 lines
13 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 {
constexpr auto SPI_CMD_BIT_SIZE = 8;
constexpr auto SPI_BUFFER_OFFSET_BIT_SIZE = 8;
constexpr auto SPI_REGISTER_ADDR_BIT_SIZE = 16;
constexpr auto MAX_BUFFER_SIZE = 256;
namespace details {
static bool is_initialized = false;
static spi_device_handle_t spi_device;
/// should satisfy most of the control register write/read
/// without overlap with the data buffer
constexpr auto DATA_BUFFER_OFFSET = 16;
static_assert(DATA_BUFFER_OFFSET < MAX_BUFFER_SIZE,
"DATA_BUFFER_OFFSET must be less than MAX_BUFFER_SIZE");
static uint8_t _shared_buffer[MAX_BUFFER_SIZE];
/**
* @brief used in generic stream io
*/
static std::span<uint8_t> shared_buffer = std::span{_shared_buffer};
/**
* @brief only used in read/write buffer io
*/
static std::span<uint8_t> data_buffer = std::span{_shared_buffer + DATA_BUFFER_OFFSET, MAX_BUFFER_SIZE - DATA_BUFFER_OFFSET};
}
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;
};
std::span<uint8_t> mut_shared_buffer() {
return std::span{details::shared_buffer};
}
void init() {
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 = 8,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 128,
.clock_speed_hz = 10'000'000,
.spics_io_num = CS_PIN,
.flags = SPI_DEVICE_NO_DUMMY,
.queue_size = 1,
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus_config, SPI_DMA_CH_AUTO));
ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &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 */
Result<Unit, error_t>
read_stream(uint8_t cmd, std::span<uint8_t> data, const size_t timeout_ms) {
if (not wait_for_not_busy(timeout_ms)) {
return ue_t{error::TIMEOUT};
}
if (data.size() > MAX_BUFFER_SIZE - 1) {
return ue_t{error::INVALID_SIZE};
}
auto rx_buffer = details::shared_buffer.subspan(0, data.size() + 1);
spi_transaction_ext_t transaction;
transaction = spi_transaction_ext_t{
.base = {
.flags = SPI_TRANS_VARIABLE_CMD,
.cmd = cmd,
.length = static_cast<size_t>(rx_buffer.size() * 8),
.rxlength = static_cast<size_t>(rx_buffer.size() * 8),
.tx_buffer = nullptr,
.rx_buffer = rx_buffer.data(),
},
.command_bits = static_cast<uint8_t>(SPI_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, "failed to transfer %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) {
if (not wait_for_not_busy(timeout_ms)) {
return ue_t{error::TIMEOUT};
}
if (data.size() > MAX_BUFFER_SIZE) {
return ue_t{error::INVALID_SIZE};
}
const auto rx_buffer = details::shared_buffer.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 = static_cast<uint8_t>(SPI_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, "failed to transfer %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) {
if (not wait_for_not_busy(timeout_ms)) {
return ue_t{error::TIMEOUT};
}
if (data.size() > MAX_BUFFER_SIZE - 1) {
return ue_t{error::INVALID_SIZE};
}
// 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 = details::shared_buffer.subspan(0, data.size() + 1);
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 = nullptr,
.rx_buffer = rx_buffer.data(),
},
.command_bits = static_cast<uint8_t>(SPI_CMD_BIT_SIZE),
.address_bits = static_cast<uint8_t>(SPI_REGISTER_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, "failed to transfer %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) {
if (not wait_for_not_busy(timeout_ms)) {
return ue_t{error::TIMEOUT};
}
if (data.size() > MAX_BUFFER_SIZE) {
return ue_t{error::INVALID_SIZE};
}
auto rx_buffer = details::shared_buffer.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>(SPI_CMD_BIT_SIZE),
.address_bits = static_cast<uint8_t>(SPI_REGISTER_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, "failed to transfer %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);
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 = nullptr,
.rx_buffer = rx_buffer.data(),
},
.command_bits = static_cast<uint8_t>(SPI_CMD_BIT_SIZE),
.address_bits = static_cast<uint8_t>(SPI_BUFFER_OFFSET_BIT_SIZE),
.dummy_bits = 0,
};
esp_err_t err = spi_device_polling_transmit(details::spi_device, &transaction.base);
if (err != ESP_OK) {
ESP_LOGE(TAG, "failed to transfer %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 (not wait_for_not_busy(timeout_ms)) {
return ue_t{error::TIMEOUT};
}
if (data_from_host.size() > details::data_buffer.size()) {
return ue_t{error::INVALID_SIZE};
}
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>(SPI_CMD_BIT_SIZE),
.address_bits = static_cast<uint8_t>(SPI_BUFFER_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, "failed to transfer %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 {};
}
}