414 lines
13 KiB
C++
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 {};
|
|
}
|
|
}
|