Dedicated GPIO: add examples implementing software I2C, UART and SPI on RISC-V targets.
Use dedicated GPIOs in examples to show how to emulate a UART, I2C and SPI bus. (Using assembly and C)
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
set(srcs "soft_uart.c")
|
||||
|
||||
if(CONFIG_IDF_TARGET_ARCH_RISCV)
|
||||
list(APPEND srcs "riscv/soft_uart.S")
|
||||
elseif(CONFIG_IDF_TARGET_ARCH_XTENSA)
|
||||
message(FATAL_ERROR "Xtensa targets not supported yet")
|
||||
endif()
|
||||
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES driver
|
||||
LDFRAGMENTS linker.lf)
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Enumeration for the usable baudrates by the software UART
|
||||
*/
|
||||
typedef enum {
|
||||
SOFT_UART_115200,
|
||||
SOFT_UART_230400,
|
||||
SOFT_UART_460800,
|
||||
SOFT_UART_921600,
|
||||
SOFT_UART_BAUD_END
|
||||
} soft_uart_baudrate_t;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Structure defining the configuration for the software UART port
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t tx_pin;
|
||||
uint32_t rx_pin;
|
||||
soft_uart_baudrate_t baudrate;
|
||||
} soft_uart_config_t;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Abstract type representing a software UART port.
|
||||
*/
|
||||
typedef struct soft_uart_port_impl_t* soft_uart_port_t;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Create and configure the software UART port.
|
||||
*
|
||||
* @param config Configuration to apply to the initialized port.
|
||||
* @param port Output structure representing the freshly initialized software UART port.
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t soft_uart_new(soft_uart_config_t *config, soft_uart_port_t *port);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Delete a previously initialized software UART port.
|
||||
*
|
||||
* @param port Port to delete, must have been initialized with `soft_uart_new` first.
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t soft_uart_del(soft_uart_port_t port);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Send the given bytes on the software UART port.
|
||||
*
|
||||
* @param port Software UART port to send data on.
|
||||
* @param write_buffer Buffer containing the bytes to send on the buffer. Must not be NULL.
|
||||
* @param write_size Size of the write buffer. Must not be 0.
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t soft_uart_send(soft_uart_port_t port, const uint8_t* write_buffer, size_t write_size);
|
||||
|
||||
/**
|
||||
* @brief Receive bytes from the software UART port.
|
||||
*
|
||||
* @param port Software UART port to receive data from.
|
||||
* @param read_buffer Buffer that will contain the bytes received. Must not be NULL.
|
||||
* @param read_size Size of the read buffer. Must not be 0.
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t soft_uart_receive(soft_uart_port_t port, uint8_t* read_buffer, size_t read_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,4 @@
|
||||
[mapping:main_default]
|
||||
archive: libsoft_uart.a
|
||||
entries:
|
||||
* (noflash)
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* RISC-V fast GPIO special registers, taken from "hal/dedic_gpio_cpu_ll.h" */
|
||||
#define CSR_GPIO_IN_USER 0x804
|
||||
#define CSR_GPIO_OUT_USER 0x805
|
||||
/* Special register for machine cycle count */
|
||||
#define CSR_PCCR_MACHINE 0x7e2
|
||||
|
||||
.section .text
|
||||
|
||||
/**
|
||||
* @brief Send bytes on the emulated UART.
|
||||
*
|
||||
* @param tx_buffer (a0) Buffer to send on the TX line. Guaranteed not NULL by the caller.
|
||||
* @param tx_size (a1) Size of tx_buffer. Guaranteed not 0 by the caller.
|
||||
* @param tx_bit (a2) Offset of TX I/O in the dedicated GPIO register.
|
||||
* @param baudrate (a3) CPU clock cycles taken by each bit.
|
||||
*
|
||||
* The C signature of this routine would be:
|
||||
* void emulate_uart_send(const uint8_t* tx, uint32_t tx_size, uint32_t tx_bit, uint32_t baudrate_delay);
|
||||
*/
|
||||
.global emulate_uart_send
|
||||
.type emulate_uart_send, @function
|
||||
emulate_uart_send:
|
||||
/* "Convert" tx_bit to an actual mask. Thus, use 1 << tx_bit instead.
|
||||
* rx_bit is not modified as we need the bit offset controlling the RX I/O and not a bit mask. */
|
||||
li t0, 1
|
||||
sll a2, t0, a2
|
||||
/* Save return address in a4 */
|
||||
mv a4, ra
|
||||
/* As UART is very time sensitive, we want each bit sent to be very precise in terms of duration.
|
||||
* The first toggle of the fast GPIO register may be slow, ~1us, so let's send a dummy byte here
|
||||
* before the actual UART emulation start, else the first byte sent would be corrupted.*/
|
||||
li t0, 0
|
||||
call uart_send_byte
|
||||
/* Reading the characters 4 by 4 would be much faster, but in our case, we don't need
|
||||
* the process to be fast as the bottleneck is the UART speed */
|
||||
uart_read_next:
|
||||
lb t0, (a0)
|
||||
/* Output the next character on the TX line */
|
||||
call uart_send_byte
|
||||
/* Go to the next character and repeat */
|
||||
addi a0, a0, 1
|
||||
addi a1, a1, -1
|
||||
/* If we don't have more bytes to send, return */
|
||||
bnez a1, uart_read_next
|
||||
uart_ret:
|
||||
/* Restore the return address */
|
||||
mv ra, a4
|
||||
ret
|
||||
|
||||
/**
|
||||
* In theory, we would need to respect the calling convention and receive the parameter
|
||||
* in a0, but as this routine is private and won't interact with any C function, we don't need
|
||||
* to respect it, so we can only use registers, and not the stack.
|
||||
*
|
||||
* The C signature of this routine would be:
|
||||
* void uart_send_byte(uint8_t byte, uint32_t tx_bitmask, uint32_t baudrate_delay);
|
||||
*/
|
||||
uart_send_byte:
|
||||
/* a0, a1, a3, a4 are used by the caller.
|
||||
* Parameters:
|
||||
* t0 - Character to send
|
||||
* a2 - Bit mask of GPIO_OUT_USER controlling TX
|
||||
* a3 - Delay to wait between each bit
|
||||
*/
|
||||
mv t1, ra
|
||||
/* Setup t3 to as we will send all 8 bits of the parameter (t0) */
|
||||
li t3, 8
|
||||
/* Start bit, clear/reset TX bit */
|
||||
csrrc zero, CSR_GPIO_OUT_USER, a2
|
||||
/* Wait a bit, depends on the baudrate configured */
|
||||
call uart_delay
|
||||
uart_send_byte_loop:
|
||||
/* Get the lowest bit of t0 (parameter), store the result in t2 */
|
||||
andi t2, t0, 1
|
||||
/* We could avoid using a branch, but writing a 0 or 1 would have different timings.
|
||||
* Using branches, we can arrange the code to have roughly the same timings in both cases. */
|
||||
beqz t2, uart_send_bit_zero
|
||||
/* The following will set the GPIO pointed by the lowest bit to 1 */
|
||||
csrrs zero, CSR_GPIO_OUT_USER, a2
|
||||
j uart_send_bit_after
|
||||
uart_send_bit_zero:
|
||||
/* If the bit was 0, we have to "clear" the GPIO */
|
||||
csrrc zero, CSR_GPIO_OUT_USER, a2
|
||||
uart_send_bit_after:
|
||||
call uart_delay
|
||||
/* Shift the parameter right */
|
||||
srli t0, t0, 1
|
||||
/* Decrement the loop index and continue if needed */
|
||||
addi t3, t3, -1
|
||||
bnez t3, uart_send_byte_loop
|
||||
/* Stop bit, set bit to 1 */
|
||||
csrrs zero, CSR_GPIO_OUT_USER, a2
|
||||
call uart_delay
|
||||
/* Restore return address before returning */
|
||||
mv ra, t1
|
||||
ret
|
||||
|
||||
/**
|
||||
* @brief Receive bytes from the emulated UART.
|
||||
*
|
||||
* @param rx_buffer (a0) Buffer to store the received bytes in. Guaranteed not NULL by the caller.
|
||||
* @param rx_size (a1) Size of rx_buffer. Guaranteed not 0 by the caller.
|
||||
* @param rx_bit (a2) Offset of RX I/O in the dedicated GPIO register.
|
||||
* @param baudrate (a3) CPU clock cycles taken by each bit.
|
||||
*
|
||||
* The C signature of this routine would be:
|
||||
* void emulate_uart_receive(uint8_t *rx_buffer, uint32_t tx_size, uint32_t rx_bit, uint32_t baudrate_delay);
|
||||
*/
|
||||
.global emulate_uart_receive
|
||||
.type emulate_uart_receive, @function
|
||||
emulate_uart_receive:
|
||||
/* Save return address in a4 */
|
||||
mv a4, ra
|
||||
uart_receive_iterate:
|
||||
/* Receive characters on RX line now */
|
||||
call uart_receive_char
|
||||
/* Character received in a5. Store it in the buffer */
|
||||
sb a5, (a0)
|
||||
addi a0, a0, 1
|
||||
/* Decrement the size */
|
||||
addi a1, a1, -1
|
||||
/* Iterate until we don't have space in the buffer */
|
||||
bnez a1, uart_receive_iterate
|
||||
/* Restore the return address */
|
||||
mv ra, a4
|
||||
ret
|
||||
|
||||
/* Routine to receive a character from the RX line and return it in a0.
|
||||
* For the same reasons as above, we can use temporary registers.
|
||||
*
|
||||
* The C signature of this routine would be:
|
||||
* uint8_t uart_receive_char(uint32_t rx_bit, uint32_t baudrate_delay);
|
||||
*/
|
||||
uart_receive_char:
|
||||
/* a0, a1, a3, a4 are used by the caller.
|
||||
* Parameters:
|
||||
* a2 - Bit offset of GPIO_OUT_USER controlling RX. For example, 0 if RX is mapped to BIT(0).
|
||||
* a3 - Delay (CPU cycles) to wait between each bit.
|
||||
*/
|
||||
mv t1, ra
|
||||
li a5, 0
|
||||
uart_receive_wait:
|
||||
/* Wait for the start bit. The input GPIO is bound to the lowest bit of CSR_GPIO_IN_USER */
|
||||
csrr t0, CSR_GPIO_IN_USER
|
||||
sra t0, t0, a2
|
||||
andi t0, t0, 1
|
||||
/* Check that the input pin is 0 (start bit) */
|
||||
bnez t0, uart_receive_wait
|
||||
/* t2 will go from 0 to 7 as we will receive 8 bits */
|
||||
li t2, 0
|
||||
/* Start bit arrived, wait a bit:
|
||||
* Wait half a UART-bit period here, the rest when we enter the loop, this will let us
|
||||
* sample the bits in the middle of the period */
|
||||
srli t6, a3, 1
|
||||
call uart_delay_t6
|
||||
uart_receive_next_bit:
|
||||
call uart_delay
|
||||
/* Read the next bit of RX */
|
||||
csrr t0, CSR_GPIO_IN_USER
|
||||
sra t0, t0, a2
|
||||
andi t0, t0, 1
|
||||
/* Add the bit we've just received to a5 */
|
||||
sll t0, t0, t2
|
||||
add a5, a5, t0
|
||||
/* Check if we have received all the bits */
|
||||
addi t2, t2, 1
|
||||
li t0, 8
|
||||
bne t0, t2, uart_receive_next_bit
|
||||
/* We have received all the bits, we have to wait for the stop bit, in theory.
|
||||
* In practice, just wait and return */
|
||||
call uart_delay
|
||||
/* Restore return address that was saved in t1 */
|
||||
mv ra, t1
|
||||
ret
|
||||
|
||||
|
||||
/* Routine to wait few microseconds. The delay depends on the baudrate configured. */
|
||||
uart_delay:
|
||||
/* Default baudrate to wait in a3 register */
|
||||
mv t6, a3
|
||||
|
||||
/* Specify a delay, in machine cycles, to wait */
|
||||
uart_delay_t6:
|
||||
/* t4, t5, t6 are available.
|
||||
* Use t4 to store the "end" point to wait.
|
||||
* Use t5 to get the current machine cycle. */
|
||||
csrr t4, CSR_PCCR_MACHINE
|
||||
/* In a real use case, we would need to check for a potential overflow,
|
||||
* In this example, there should be any issue. */
|
||||
add t4, t4, t6
|
||||
uart_delay_loop:
|
||||
csrr t5, CSR_PCCR_MACHINE
|
||||
bltu t5, t4, uart_delay_loop
|
||||
ret
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
#include "sdkconfig.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_check.h"
|
||||
#include "driver/dedic_gpio.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "soft_uart.h"
|
||||
|
||||
#define ERR_CHECK_OR_GOTO(ret, label) do { if((ret) != ESP_OK ) goto label; } while (0)
|
||||
|
||||
/* Forward declaration of static functions */
|
||||
void IRAM_ATTR emulate_uart_send(const uint8_t* tx_msg, uint32_t tx_size, uint32_t tx_bit, uint32_t baudrate);
|
||||
void IRAM_ATTR emulate_uart_receive(uint8_t* rx_msg, uint32_t rx_size, uint32_t rx_bit, uint32_t baudrate);
|
||||
|
||||
static uint32_t baudrate_to_cycles(soft_uart_baudrate_t baudrate);
|
||||
|
||||
/* Mutex required to enter critical sections */
|
||||
static portMUX_TYPE g_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
const char* __attribute__((used)) SOFT_UART_TAG = "soft_uart";
|
||||
|
||||
/***** Public API implementation *****/
|
||||
struct soft_uart_port_impl_t {
|
||||
uint32_t tx_bit;
|
||||
uint32_t rx_bit;
|
||||
uint32_t cycles;
|
||||
dedic_gpio_bundle_handle_t tx_bundle;
|
||||
dedic_gpio_bundle_handle_t rx_bundle;
|
||||
};
|
||||
|
||||
|
||||
esp_err_t soft_uart_new(soft_uart_config_t *config, soft_uart_port_t *port)
|
||||
{
|
||||
esp_err_t ret;
|
||||
struct soft_uart_port_impl_t *port_impl = NULL;
|
||||
|
||||
/* Check the parameters */
|
||||
ESP_GOTO_ON_FALSE(config != NULL && port != NULL, ESP_ERR_INVALID_ARG, error, SOFT_UART_TAG,
|
||||
"Parameters must not be NULL");
|
||||
ESP_GOTO_ON_FALSE(config->baudrate < SOFT_UART_BAUD_END, ESP_ERR_INVALID_ARG, error, SOFT_UART_TAG,
|
||||
"Invalid baudrate");
|
||||
|
||||
int tx = config->tx_pin;
|
||||
int rx = config->rx_pin;
|
||||
|
||||
/* In order to prevent the receiver to get garbage while we configure the GPIOs, pull the pins up to
|
||||
* reflect a UART idle state. */
|
||||
ret = gpio_set_pull_mode(tx, GPIO_PULLUP_ENABLE);
|
||||
ERR_CHECK_OR_GOTO(ret, error);
|
||||
ret = gpio_set_pull_mode(rx, GPIO_PULLUP_ENABLE);
|
||||
ERR_CHECK_OR_GOTO(ret, error);
|
||||
ret = gpio_set_direction(tx, GPIO_MODE_OUTPUT);
|
||||
ERR_CHECK_OR_GOTO(ret, error);
|
||||
ret = gpio_set_direction(rx, GPIO_MODE_INPUT);
|
||||
ERR_CHECK_OR_GOTO(ret, error);
|
||||
|
||||
/**
|
||||
* Before actually calling any assembly routine, we need to configure the GPIOs
|
||||
* We can do this in C. Using dedic_gpio API will do this for us, it will route
|
||||
* the instruction-controlled signals to the GPIO pads thanks to the GPIO matrix.
|
||||
*
|
||||
* Use one GPIO as output, for TX, and one as input, for RX.
|
||||
* Create the configuration for each.
|
||||
*/
|
||||
dedic_gpio_bundle_config_t tx_config = {
|
||||
.gpio_array = &tx,
|
||||
.array_size = 1,
|
||||
.flags = {
|
||||
.out_en = 1
|
||||
}
|
||||
};
|
||||
dedic_gpio_bundle_config_t rx_config = {
|
||||
.gpio_array = &rx,
|
||||
.array_size = 1,
|
||||
.flags = {
|
||||
.in_en = 1
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* Allocate the master port structure now that we need it */
|
||||
port_impl = malloc(sizeof(struct soft_uart_port_impl_t));
|
||||
ESP_GOTO_ON_FALSE(port_impl != NULL, ESP_ERR_NO_MEM, error, SOFT_UART_TAG, "No more memory available in the system");
|
||||
|
||||
/* Initialize the dedicated GPIO bundles */
|
||||
ret = dedic_gpio_new_bundle(&tx_config, &port_impl->tx_bundle);
|
||||
ERR_CHECK_OR_GOTO(ret, error);
|
||||
ret = dedic_gpio_new_bundle(&rx_config, &port_impl->rx_bundle);
|
||||
ERR_CHECK_OR_GOTO(ret, error_rx);
|
||||
|
||||
/**
|
||||
* Before executing the assembly routine, get the offset of TX/RX in the dedicated GPIO registers
|
||||
*/
|
||||
ret = dedic_gpio_get_out_offset(port_impl->tx_bundle, &port_impl->tx_bit);
|
||||
ERR_CHECK_OR_GOTO(ret, error_offset);
|
||||
ret = dedic_gpio_get_out_offset(port_impl->rx_bundle, &port_impl->rx_bit);
|
||||
ERR_CHECK_OR_GOTO(ret, error_offset);
|
||||
|
||||
port_impl->cycles = baudrate_to_cycles(config->baudrate);
|
||||
|
||||
*port = port_impl;
|
||||
|
||||
return ret;
|
||||
|
||||
error_offset:
|
||||
dedic_gpio_del_bundle(port_impl->rx_bundle);
|
||||
error_rx:
|
||||
dedic_gpio_del_bundle(port_impl->tx_bundle);
|
||||
error:
|
||||
if (port_impl != NULL) {
|
||||
free(port_impl);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
esp_err_t soft_uart_del(soft_uart_port_t port)
|
||||
{
|
||||
esp_err_t ret;
|
||||
ESP_GOTO_ON_FALSE(port != NULL, ESP_ERR_INVALID_ARG, error, SOFT_UART_TAG, "Bus must not be NULL");
|
||||
|
||||
dedic_gpio_del_bundle(port->tx_bundle);
|
||||
dedic_gpio_del_bundle(port->rx_bundle);
|
||||
|
||||
free(port);
|
||||
error:
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
esp_err_t soft_uart_send(soft_uart_port_t port, const uint8_t* write_buffer, size_t write_size)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ESP_GOTO_ON_FALSE(port != NULL, ESP_ERR_INVALID_ARG, error, SOFT_UART_TAG, "Bus must not be NULL");
|
||||
ESP_GOTO_ON_FALSE(write_buffer != NULL, ESP_ERR_INVALID_ARG, error, SOFT_UART_TAG, "Buffer must not be NULL");
|
||||
ESP_GOTO_ON_FALSE(write_size != 0, ESP_ERR_INVALID_ARG, error, SOFT_UART_TAG, "Buffer size must not be 0");
|
||||
|
||||
portENTER_CRITICAL(&g_lock);
|
||||
emulate_uart_send(write_buffer, write_size, port->tx_bit, port->cycles);
|
||||
portEXIT_CRITICAL(&g_lock);
|
||||
|
||||
error:
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t soft_uart_receive(soft_uart_port_t port, uint8_t* read_buffer, size_t read_size)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ESP_GOTO_ON_FALSE(port != NULL, ESP_ERR_INVALID_ARG, error, SOFT_UART_TAG, "Bus must not be NULL");
|
||||
ESP_GOTO_ON_FALSE(read_buffer != NULL, ESP_ERR_INVALID_ARG, error, SOFT_UART_TAG, "Buffer must not be NULL");
|
||||
ESP_GOTO_ON_FALSE(read_size != 0, ESP_ERR_INVALID_ARG, error, SOFT_UART_TAG, "Buffer size must not be 0");
|
||||
|
||||
portENTER_CRITICAL(&g_lock);
|
||||
emulate_uart_receive(read_buffer, read_size, port->rx_bit, port->cycles);
|
||||
portEXIT_CRITICAL(&g_lock);
|
||||
|
||||
error:
|
||||
return ret;
|
||||
}
|
||||
|
||||
/***** Private helpers *****/
|
||||
|
||||
static uint32_t baudrate_to_cycles(soft_uart_baudrate_t baudrate)
|
||||
{
|
||||
/**
|
||||
* Calculate the delay to wait between each bit depending on the UART baudrate and the CPU frequency.
|
||||
* For each delay, subtract a small amount of clock cycles which compensate for the instructions
|
||||
* used to prepare the next bits (loop, shifts, logic...).
|
||||
*/
|
||||
switch(baudrate) {
|
||||
case SOFT_UART_115200: // 115200, 8.63us per bit
|
||||
return ((CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ * 863)/100 - 20);
|
||||
case SOFT_UART_230400: // 4.34us per bit
|
||||
return ((CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ * 434)/100 - 24);
|
||||
case SOFT_UART_460800: // 2.17us per bit
|
||||
return ((CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ * 217)/100 - 20);
|
||||
case SOFT_UART_921600: // 1.085us per bit
|
||||
return ((CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ * 108)/100 - 23);
|
||||
default:
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user