Full hardware UART driver with config-driven per-instance setup. Non-blocking SendBuffer supports DMA (zero-CPU) and ISR (FIFO-fill) modes selectable per instance in MCU_UART_cfg.h. Both modes and blocking SendBufferBlocking invoke a configurable TX-complete callback (STD_NULL to ignore). All public functions take a u8 instance parameter. Config uses struct-of-arrays pattern for runtime state, designated-initializer array for per-instance settings, and value enums in the public header for parity/data bits/stop bits/async mode. Added hardware_dma to CMake link list.
388 lines
15 KiB
C
388 lines
15 KiB
C
/******************************************************************************
|
|
* File: MCU_UART_prg.c
|
|
* Component: MCU_UART
|
|
* Description: Program (implementation) file for the MCU_UART driver.
|
|
* Wraps the Pico SDK's hardware_uart and hardware_dma libraries
|
|
* to provide blocking and non-blocking send APIs for the RP2040
|
|
* PL011 UART peripheral(s).
|
|
*
|
|
* Non-blocking TX supports two mechanisms (per-instance config):
|
|
* - DMA: hardware DMA channel transfers bytes autonomously
|
|
* - ISR: UART TX interrupt feeds one byte per interrupt
|
|
* Both invoke the configured TX-complete callback when done.
|
|
*
|
|
* Layer: MCU (hardware abstraction)
|
|
*****************************************************************************/
|
|
|
|
#include "MCU_UART.h"
|
|
#include "MCU_UART_priv.h"
|
|
#include "MCU_UART_cfg.h"
|
|
|
|
/* Pico SDK headers */
|
|
#include "hardware/uart.h"
|
|
#include "hardware/gpio.h"
|
|
#include "hardware/dma.h"
|
|
#include "hardware/irq.h"
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* INSTANCE LOOKUP TABLE */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* @brief Maps a config array index to the corresponding Pico SDK uart
|
|
* instance pointer (0 = uart0, 1 = uart1).
|
|
*/
|
|
static uart_inst_t * const apstrInstances[] =
|
|
{
|
|
uart0, /* index 0 = RP2040 uart0 peripheral */
|
|
uart1, /* index 1 = RP2040 uart1 peripheral */
|
|
};
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* RUNTIME STATE */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* @brief Single static control structure holding all per-instance TX state.
|
|
*
|
|
* NOTE: abTxBusy is written from interrupt context (DMA/UART ISR) and
|
|
* read from main context (MCU_UART_bIsTxBusy, MCU_UART_enuSendBuffer).
|
|
* On the single-core RP2040 with interrupts disabled during read-modify-
|
|
* write sequences, this is safe. If dual-core access is ever needed,
|
|
* add volatile qualifiers or use a spin lock.
|
|
*/
|
|
static MCU_UART_tstrControl strControl;
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* INTERNAL HELPERS */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* @brief Call the TX-complete callback for a given instance if configured.
|
|
* Safe to invoke from ISR context.
|
|
*
|
|
* @param u8Instance Index into MCU_UART_astrConfig[].
|
|
*/
|
|
static void vCallTxCallback(u8 u8Instance)
|
|
{
|
|
STD_tpfCallbackFunc pfCallbackLoc = MCU_UART_astrConfig[u8Instance].pfTxCompleteCallback;
|
|
|
|
if (pfCallbackLoc != STD_NULL)
|
|
{
|
|
pfCallbackLoc();
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* DMA IRQ HANDLER (INSTANCE 0) */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* @brief DMA completion interrupt handler for UART instance 0.
|
|
*
|
|
* Fires when the DMA channel finishes transferring the TX buffer.
|
|
* Flow: clear IRQ flag -> set abTxBusy to STD_FALSE -> call callback.
|
|
*/
|
|
static void vDmaIrqHandler0(void)
|
|
{
|
|
s8 s8ChannelLoc = strControl.as8DmaChannel[MCU_UART_INSTANCE_0];
|
|
u32 u32StatusLoc;
|
|
|
|
u32StatusLoc = dma_channel_get_irq0_status((u32)s8ChannelLoc);
|
|
|
|
if (u32StatusLoc != 0U)
|
|
{
|
|
/* Step 1: clear the DMA interrupt flag */
|
|
dma_irqn_acknowledge_channel(0, (u32)s8ChannelLoc);
|
|
|
|
/* Step 2: mark instance as idle (written here in ISR, read in main) */
|
|
strControl.abTxBusy[MCU_UART_INSTANCE_0] = STD_FALSE;
|
|
|
|
/* Step 3: notify the application */
|
|
vCallTxCallback((u8)MCU_UART_INSTANCE_0);
|
|
}
|
|
}
|
|
|
|
/* Note: when UART1 DMA is added, define vDmaIrqHandler1 here and
|
|
* use DMA_IRQ_1 to avoid sharing a single IRQ. */
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* UART TX ISR HANDLER */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* @brief UART TX interrupt handler logic — fills the FIFO on each invocation.
|
|
*
|
|
* The PL011 TX interrupt fires when the FIFO level drops at or below the
|
|
* programmable threshold. This handler writes as many bytes as the FIFO
|
|
* can accept before returning, minimizing the number of interrupts needed
|
|
* for a given transfer (one interrupt per FIFO drain cycle, not per byte).
|
|
*
|
|
* When all bytes are sent, disables the TX interrupt, marks the instance
|
|
* as idle, and invokes the TX-complete callback.
|
|
*
|
|
* @param u8Instance Index of the UART instance that fired the interrupt.
|
|
*/
|
|
static void vTxIsrHandler(u8 u8Instance)
|
|
{
|
|
uart_inst_t *pstrUartLoc = apstrInstances[u8Instance];
|
|
STD_tBool bFifoReady = STD_FALSE;
|
|
STD_tBool bDataLeft = STD_FALSE;
|
|
|
|
/* Fill the FIFO with as many bytes as it can accept */
|
|
bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE;
|
|
bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE;
|
|
|
|
while ((bFifoReady == STD_TRUE) && (bDataLeft == STD_TRUE))
|
|
{
|
|
uart_putc_raw(pstrUartLoc,
|
|
strControl.apu8TxBuffer[u8Instance][strControl.au16TxIndex[u8Instance]]);
|
|
strControl.au16TxIndex[u8Instance]++;
|
|
|
|
bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE;
|
|
bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE;
|
|
}
|
|
|
|
/* If all bytes have been sent, shut down the interrupt */
|
|
if (bDataLeft == STD_FALSE)
|
|
{
|
|
uart_set_irqs_enabled(pstrUartLoc, false, false);
|
|
|
|
strControl.abTxBusy[u8Instance] = STD_FALSE;
|
|
|
|
vCallTxCallback(u8Instance);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief UART0 TX interrupt entry point.
|
|
* Dispatches to the common handler with instance 0.
|
|
*/
|
|
static void vUart0IrqHandler(void)
|
|
{
|
|
vTxIsrHandler((u8)MCU_UART_INSTANCE_0);
|
|
}
|
|
|
|
/* Note: when UART1 ISR mode is added, define vUart1IrqHandler here. */
|
|
|
|
/* ========================================================================= */
|
|
/* INIT */
|
|
/* ========================================================================= */
|
|
|
|
STD_tenuResult MCU_UART_enuInit(void)
|
|
{
|
|
STD_tenuResult enuResultLoc = STD_OK;
|
|
u8 u8IndexLoc;
|
|
|
|
/* Zero-initialize all control state */
|
|
for (u8IndexLoc = 0U; u8IndexLoc < (u8)MCU_UART_NUM_INSTANCES; u8IndexLoc++)
|
|
{
|
|
strControl.apu8TxBuffer[u8IndexLoc] = STD_NULL;
|
|
strControl.au16TxLength[u8IndexLoc] = 0U;
|
|
strControl.au16TxIndex[u8IndexLoc] = 0U;
|
|
strControl.abTxBusy[u8IndexLoc] = STD_FALSE;
|
|
strControl.as8DmaChannel[u8IndexLoc] = -1;
|
|
}
|
|
|
|
/* Configure each UART instance */
|
|
for (u8IndexLoc = 0U; u8IndexLoc < (u8)MCU_UART_NUM_INSTANCES; u8IndexLoc++)
|
|
{
|
|
const MCU_UART_tstrConfig *pstrCfgLoc = &MCU_UART_astrConfig[u8IndexLoc];
|
|
uart_inst_t *pstrUartLoc = apstrInstances[u8IndexLoc];
|
|
|
|
/* Enable the UART peripheral and set the baud rate */
|
|
uart_init(pstrUartLoc, pstrCfgLoc->u32BaudRate);
|
|
|
|
/* Assign TX and RX GPIO pins to the UART function */
|
|
gpio_set_function(pstrCfgLoc->u8TxPin, GPIO_FUNC_UART);
|
|
gpio_set_function(pstrCfgLoc->u8RxPin, GPIO_FUNC_UART);
|
|
|
|
/* Set data format: data bits, stop bits, parity */
|
|
uart_set_format(pstrUartLoc,
|
|
pstrCfgLoc->enuDataBits,
|
|
pstrCfgLoc->enuStopBits,
|
|
pstrCfgLoc->enuParity);
|
|
|
|
/* Set up the async TX mechanism based on config */
|
|
if (pstrCfgLoc->enuAsyncMode == MCU_UART_ASYNC_DMA)
|
|
{
|
|
/* Claim a free DMA channel. true = panic if none available. */
|
|
s8 s8ChannelLoc = (s8)dma_claim_unused_channel(true);
|
|
strControl.as8DmaChannel[u8IndexLoc] = s8ChannelLoc;
|
|
|
|
/* Enable DMA completion interrupt for this channel */
|
|
dma_channel_set_irq0_enabled((u32)s8ChannelLoc, true);
|
|
|
|
/* Install the per-instance DMA IRQ handler */
|
|
if (u8IndexLoc == (u8)MCU_UART_INSTANCE_0)
|
|
{
|
|
irq_set_exclusive_handler(DMA_IRQ_0, vDmaIrqHandler0);
|
|
irq_set_enabled(DMA_IRQ_0, true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* ISR mode: install the per-instance UART TX interrupt handler.
|
|
* The TX interrupt itself is NOT enabled here — it gets enabled
|
|
* in SendBuffer and disabled by the ISR when transfer completes. */
|
|
if (u8IndexLoc == (u8)MCU_UART_INSTANCE_0)
|
|
{
|
|
irq_set_exclusive_handler(UART0_IRQ, vUart0IrqHandler);
|
|
irq_set_enabled(UART0_IRQ, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
return enuResultLoc;
|
|
}
|
|
|
|
/* ========================================================================= */
|
|
/* SEND BYTE (BLOCKING) */
|
|
/* ========================================================================= */
|
|
|
|
STD_tenuResult MCU_UART_enuSendByte(u8 u8Instance, u8 u8Byte)
|
|
{
|
|
STD_tenuResult enuResultLoc = STD_OK;
|
|
|
|
/* Blocking single-byte send. uart_putc_raw waits until the TX FIFO
|
|
* has space, then writes the byte. */
|
|
uart_putc_raw(apstrInstances[u8Instance], u8Byte);
|
|
|
|
return enuResultLoc;
|
|
}
|
|
|
|
/* ========================================================================= */
|
|
/* SEND BUFFER (NON-BLOCKING, DEFAULT) */
|
|
/* ========================================================================= */
|
|
|
|
STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16Length)
|
|
{
|
|
STD_tenuResult enuResultLoc = STD_OK;
|
|
STD_tBool bBusyLoc = strControl.abTxBusy[u8Instance];
|
|
MCU_UART_tenuAsyncMode enuModeLoc = MCU_UART_astrConfig[u8Instance].enuAsyncMode;
|
|
|
|
if (pu8Data == STD_NULL)
|
|
{
|
|
enuResultLoc = STD_NULL_POINTER_ERROR;
|
|
}
|
|
else if (bBusyLoc == STD_TRUE)
|
|
{
|
|
/* A previous async transfer is still in progress */
|
|
enuResultLoc = STD_NOK;
|
|
}
|
|
else
|
|
{
|
|
/* Store buffer info in the control structure.
|
|
* abTxBusy is set here (main context) and cleared by the ISR/DMA
|
|
* handler (interrupt context) when the transfer completes. */
|
|
strControl.apu8TxBuffer[u8Instance] = pu8Data;
|
|
strControl.au16TxLength[u8Instance] = u16Length;
|
|
strControl.au16TxIndex[u8Instance] = 0U;
|
|
strControl.abTxBusy[u8Instance] = STD_TRUE;
|
|
|
|
if (enuModeLoc == MCU_UART_ASYNC_DMA)
|
|
{
|
|
/* --- DMA mode --- */
|
|
s8 s8ChannelLoc = strControl.as8DmaChannel[u8Instance];
|
|
uart_inst_t *pstrUartLoc = apstrInstances[u8Instance];
|
|
|
|
/* Configure DMA: memory -> UART TX FIFO, 1 byte at a time,
|
|
* paced by UART TX DREQ so we never overflow the FIFO. */
|
|
dma_channel_config strDmaCfgLoc = dma_channel_get_default_config((u32)s8ChannelLoc);
|
|
channel_config_set_transfer_data_size(&strDmaCfgLoc, DMA_SIZE_8);
|
|
channel_config_set_read_increment(&strDmaCfgLoc, true);
|
|
channel_config_set_write_increment(&strDmaCfgLoc, false);
|
|
channel_config_set_dreq(&strDmaCfgLoc, uart_get_dreq(pstrUartLoc, true));
|
|
|
|
/* Start transfer. DMA IRQ fires on completion ->
|
|
* vDmaIrqHandler0 clears abTxBusy -> calls callback. */
|
|
dma_channel_configure(
|
|
(u32)s8ChannelLoc,
|
|
&strDmaCfgLoc,
|
|
&uart_get_hw(pstrUartLoc)->dr, /* dest: UART data register */
|
|
pu8Data, /* source: caller's buffer */
|
|
u16Length, /* transfer count */
|
|
true /* start immediately */
|
|
);
|
|
}
|
|
else
|
|
{
|
|
/* --- ISR mode --- */
|
|
uart_inst_t *pstrUartLoc = apstrInstances[u8Instance];
|
|
STD_tBool bFifoReady = STD_FALSE;
|
|
STD_tBool bDataLeft = STD_FALSE;
|
|
|
|
/* Kick-start: fill the FIFO with as many bytes as it can
|
|
* accept right now. This gets the first bytes transmitting
|
|
* immediately without waiting for an interrupt context switch.
|
|
* Identical logic to what the ISR does on subsequent fills. */
|
|
bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE;
|
|
bDataLeft = (strControl.au16TxIndex[u8Instance] < u16Length) ? STD_TRUE : STD_FALSE;
|
|
|
|
while ((bFifoReady == STD_TRUE) && (bDataLeft == STD_TRUE))
|
|
{
|
|
uart_putc_raw(pstrUartLoc,
|
|
pu8Data[strControl.au16TxIndex[u8Instance]]);
|
|
strControl.au16TxIndex[u8Instance]++;
|
|
|
|
bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE;
|
|
bDataLeft = (strControl.au16TxIndex[u8Instance] < u16Length) ? STD_TRUE : STD_FALSE;
|
|
}
|
|
|
|
/* If all bytes fit into the FIFO in one go, the transfer is
|
|
* already complete — no interrupt needed. */
|
|
if (bDataLeft == STD_FALSE)
|
|
{
|
|
strControl.abTxBusy[u8Instance] = STD_FALSE;
|
|
vCallTxCallback(u8Instance);
|
|
}
|
|
else
|
|
{
|
|
/* Bytes remain — enable the TX interrupt. The ISR fills
|
|
* the FIFO each time it drains below threshold until the
|
|
* entire buffer is transmitted. */
|
|
uart_set_irqs_enabled(pstrUartLoc, true, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
return enuResultLoc;
|
|
}
|
|
|
|
/* ========================================================================= */
|
|
/* SEND BUFFER (BLOCKING) */
|
|
/* ========================================================================= */
|
|
|
|
STD_tenuResult MCU_UART_enuSendBufferBlocking(u8 u8Instance, const u8 *pu8Data, u16 u16Length)
|
|
{
|
|
STD_tenuResult enuResultLoc = STD_OK;
|
|
u16 u16IndexLoc;
|
|
|
|
if (pu8Data == STD_NULL)
|
|
{
|
|
enuResultLoc = STD_NULL_POINTER_ERROR;
|
|
}
|
|
else
|
|
{
|
|
/* Send each byte blocking — waits for TX FIFO space per byte */
|
|
for (u16IndexLoc = 0U; u16IndexLoc < u16Length; u16IndexLoc++)
|
|
{
|
|
uart_putc_raw(apstrInstances[u8Instance], pu8Data[u16IndexLoc]);
|
|
}
|
|
|
|
/* Invoke callback synchronously (no ISR in blocking mode) */
|
|
vCallTxCallback(u8Instance);
|
|
}
|
|
|
|
return enuResultLoc;
|
|
}
|
|
|
|
/* ========================================================================= */
|
|
/* TX BUSY CHECK */
|
|
/* ========================================================================= */
|
|
|
|
STD_tBool MCU_UART_bIsTxBusy(u8 u8Instance)
|
|
{
|
|
return strControl.abTxBusy[u8Instance];
|
|
}
|