// // Created by Kurosu Chan on 2024/1/18. // #include #include #include #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 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 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 read_stream(uint8_t cmd, std::span data, const size_t timeout_ms) { std::array _rx_buf; std::array _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(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(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 write_stream(uint8_t cmd, std::span data, const size_t timeout_ms) { std::array _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(data.size() * 8), .rxlength = static_cast(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 read_register(uint16_t reg, std::span data, const size_t timeout_ms) { std::array _rx_buf; std::array _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(rx_buffer.size() * 8), .rxlength = static_cast(rx_buffer.size() * 8), .tx_buffer = tx_buffer.data(), .rx_buffer = rx_buffer.data(), }, .command_bits = static_cast(CMD_BIT_SIZE), .address_bits = static_cast(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 write_register(uint16_t offset, std::span data, const size_t timeout_ms) { std::array _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(data.size() * 8), .rxlength = static_cast(rx_buffer.size() * 8), .tx_buffer = data.data(), .rx_buffer = rx_buffer.data(), }, .command_bits = static_cast(CMD_BIT_SIZE), .address_bits = static_cast(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, error_t> read_buffer(uint8_t offset, uint8_t size, const size_t timeout_ms) { const auto size_required = static_cast(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(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(rx_buffer.size() * 8), .rxlength = static_cast(rx_buffer.size() * 8), .tx_buffer = tx_buffer.data(), .rx_buffer = rx_buffer.data(), }, .command_bits = static_cast(CMD_BIT_SIZE), .address_bits = static_cast(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 write_buffer(uint8_t offset, std::span 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(data_from_host.size() * 8), .rxlength = static_cast(rx_buffer.size() * 8), .tx_buffer = data_from_host.data(), .rx_buffer = rx_buffer.data(), }, .command_bits = static_cast(CMD_BIT_SIZE), .address_bits = static_cast(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 {}; } }