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:
Omar Chebib
2022-11-02 19:31:18 +08:00
parent dcaa753f37
commit 623bffaab8
37 changed files with 1928 additions and 7 deletions
@@ -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)
@@ -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;
}
}