Using the DMA Engine and the Cache
The Signaloid SoC in the C0-microSD+ comes with a DMA engine that moves data without the
RISC-V core, and two caches, one for instructions and one for data. Both caches cover every
address below the CSR base at 0x08200000. Your device application accesses the DMA engine
and the cache-invalidation operations through a minimal C API that the Signaloid UxHw Toolchain
links into every application for C0-microSD+.
The data cache is not coherent with the DMA engine, so anything the DMA engine writes into a cached address has to be invalidated before the core reads it. This page explains how to correctly use DMA and caching.
To find out more about the registers in the DMA API, see Host Interface and Protocol.
What the toolchain links in
Two drivers are compiled into every application running on C0-microSD+.
Include cache.h for cache manipulation and dma.h to programmatically use the DMA engine. You don't need to update the SOURCES build variable.
#include "cache.h"
#include "dma.h"
These are hardware drivers rather than parts of the hardware abstraction layer, so they are
separate from C0HAL.h. To use the HAL, see
Using the Hardware Abstraction Layer.
The caches
The core has two caches, one for instructions and one for data. They share the same geometry:
4 KiB each, in 128 lines of 32 bytes. A cache operation aligns down to a line boundary, so
invalidating address 0x10 invalidates the range 0x00 to 0x1F rather than 0x10 to
0x2F.
Both caches cover the same fixed addresses. Everything
below the CSR base at 0x08200000 is cached, i.e., is the SPI flash, the flash OTP, and the
LRAM. Nothing at or above the CSR base is cached.
| Region | Address range | Cached |
|---|---|---|
| SPI flash | 0x00000000 – 0x00FFFFFC | Yes |
| Flash OTP | 0x08000000 – 0x080001FC | Yes |
| LRAM | 0x08100000 – 0x0814FFFC | Yes |
| CSR registers | 0x08200000 – 0x08214003 | No |
| MMIO buffer | 0x08300000 – 0x0830FFFC | No |
| Device identity | 0x0FFFE000 – 0x0FFFE004 | No |
| DMA registers | 0x10000000 – 0x1000001C | No |
The registers and the MMIO buffer are uncached. A value the host writes into the input half of the MMIO buffer is visible to your next load, and a status value you write is visible to the host straight away.
The data cache is write-through, so the data-cache API only ever invalidates and never flushes. Invalidate before you read an address whose contents in memory no longer match what the cache holds.
The cache API
| Function | Purpose |
|---|---|
void cacheInvalidateRange(const void *base, size_t lengthBytes) | Invalidate every cache line overlapping the range. The usual entry point. |
void cacheInvalidateLine(uintptr_t address) | Invalidate the single 32-byte line containing address. |
void cacheInvalidateAll(void) | Invalidate the whole data cache. |
bool cacheIsCacheable(const void *address) | Report whether accesses to this address go through the cache. |
void cacheFlushInstruction(void) | Discard the cached instructions so the core fetches them again from memory. |
The functioncacheInvalidateRange() handles an unaligned start and end, and performs no operation
for zero input length. For a range of 4 KiB or more it invalidates the whole cache
instead, which costs less than invalidating more lines than the cache holds and has the same
effect.
When to invalidate
Invalidate when the contents of a cached address in memory no longer match what the cache holds. On C0-microSD+ that can happen only when your application writes to the SPI flash.
A write that starts on a 4 KiB-aligned address erases that whole sector before it writes. A write large enough to cross into the next sector erases that sector, too. The erase discards every byte stored in those sectors, so lines your application never wrote go stale alongside the ones it did. A write that starts part way into a sector erases nothing. Flash programming can only change a bit from 1 to 0, so the stored result is the bitwise AND of the old and the new contents rather than what you wrote. That is fine if you erased the sector in an earlier step, because an erased sector holds all ones. See SPI flash write and erase semantics.
#include "cache.h"
/*
* A write erased the whole sector, so drop every line covering it
* before reading any part of it back.
*/
cacheInvalidateRange((const void *) kSectorAddress, kSectorSizeBytes);
value = *(volatile uint32_t *) kSectorAddress;
Nothing outside the core can create this situation while your application is running. The SD interface cannot access the SPI flash or the flash OTP while the core is running, so a host cannot change the contents of a cached address while your application is running. See SD access while the core runs.
Two cases need nothing from you:
- A host reflashing the module: A host can access the flash only with the core stopped, and
the boot code invalidates the whole data cache before it calls
main, so your application always starts against a clean cache. - A
dmaTryRun()transfer into the LRAM: The function invalidates a cacheable destination before it returns. A transfer into the flash is the exception: the function invalidates the range it wrote, but the erase covers the whole surrounding sector. You then need to invalidate the rest of that sector.
Call cacheFlushInstruction() only when your application writes instructions that it then
executes, which most applications never do.
The DMA engine
The DMA engine copies a range of memory, or fills one with a repeated value, in 512-byte bursts that it chains itself. There is a single DMA engine, it is shared with everything else in your application The SD host cannot access it.
Run one operation
Function dmaTryRun() performs a single operation synchronously, only if the DMA engine is free.
DmaResult dmaTryRun(DMAOpCode op, uint32_t source, uint32_t destination, uint32_t lengthBytes, uint32_t setValue);
This functions takes as input one of three operation codes. The function ignores arguments not supported by an operation.
| Operation code | Effect | Requirements |
|---|---|---|
kDMAOpCopy | Copy lengthBytes from source to destination. | Both addresses aligned to 4 bytes. |
kDMAOpByteSet | Fill destination with the low byte of setValue. | Destination aligned to 4 bytes. |
kDMAOpWordSet | Fill destination with the whole 32 bits of setValue. | Destination aligned, length a multiple of 4. |
The functions returns one of three results:
| Result | Meaning |
|---|---|
kDmaDone | The engine performed the whole operation and a cacheable destination is already invalidated. |
kDmaNotRun | No data changed, because the engine was busy or an argument was rejected. You need to programmatically copy the data, without the use of the DMA engine. |
kDmaError | The DMA engine accepted the operation and then failed. The destination is undefined. |
#include <string.h>
#include "dma.h"
DmaResult result;
result = dmaTryRun(kDMAOpCopy, (uint32_t) source, (uint32_t) destination, lengthBytes, 0);
switch (result)
{
case kDmaDone:
/*
* Every byte moved, and the destination is already
* invalidated where that is meaningful.
*/
break;
case kDmaNotRun:
/*
* Nothing was accessed. The core needs to do the data transfer.
*/
memcpy(destination, source, lengthBytes);
break;
case kDmaError:
/*
* The destination holds neither the old data nor the new
* data. Report the failure instead of repeating the copy.
*/
handleTransferFailure();
break;
}
kDmaError is not a fallback condition. It means a bus transaction failed part way
through, so the destination holds neither its old contents nor its new contents. Repeating the
operation on the core writes correct data over a range you can no longer reason about and it
hides a fault that is worth reporting. Treat kDmaError as a failure and kDmaNotRun as the
one result you retry.
A length of zero returns kDmaDone without invoking the engine, because moving no bytes is a
completed operation.
Functions to inspect the DMA engine
| Function | Purpose |
|---|---|
uint32_t dmaStatusRaw(void) | Read the raw STATUS register. |
bool dmaIsIdle(void) | Report whether the engine is idle. |
uint32_t dmaRunCount(void) | Count of operations the engine completed. |
uint32_t dmaNotRunCount(void) | Count of operations declined, which is your fallback rate. |
uint32_t dmaErrorCount(void) | Count of operations that failed part way through. |
void dmaResetCounters(void) | Set all three counters back to zero. |
Both inspection functions are free of side effects, so they are safe to call on an engine that another operation left in an undefined state. The three counters are ordinary variables in the LRAM rather than hardware registers, so publish them through the MMIO buffer when you want a host to see them.
Rules the DMA engine does not enforce
Read these before you drive the DMA registers without the use of the provided functions. In case of a potential wrong configuration or usage we suggest using dmaTryRun().
- A core reset does not reset the engine. It responds to the module reset, not the core reset
that
rstndrives, so its state survives a trap and survives a host stopping your core. If the engine is left in theDonestate, it ignores the next operation you write toCONTROL. CONTROLis sampled only while the DMA engine is idle. A write in any other state is dropped, and the staleDonethen reads back as success.- Only an acknowledge returns the DMA engine to idle state. Writing
CONTROLdoes not, and this engine has no abort. - Never wait on an operation you did not start. Your application is the only thing that can start or acknowledge one, so a wait on a transfer you did not begin never ends, and acknowledging that transfer destroys the only record of its fault.
- A copy of a length that is not a multiple of 4 reads up to 3 bytes past the end of the source. That faults if the source ends at the top of a mapped region.
- The SPI flash controller ignores byte strobes. Every write that accesses the flash memory programs a full 32-bit word, both when the core or the DMA engine issues it. Only a word-aligned write of a whole number of words accesses the exact requested range.
Function dmaTryRun() is not reentrant and waits without a timeout for the operation it started,
so do not call it from a trap handler.
How your application starts
The boot code that the toolchain places ahead of main uses both of these facilities. It
takes ownership of the DMA engine, copies your writable image from the flash into the SRAM or
LRAM, zeroes .bss, invalidates the data cache, runs any C++ global
constructors, and calls main. The DMA engine is therefore idle and acknowledged by the time your
code runs.
If the boot code cannot bring the engine to idle, it lights the red and green LEDs and stops. See Troubleshooting and FAQ.
Next steps
- Using the SPI Flash, storing data that survives a power cycle, and reading it back without a stale cache line.
- Host Interface and Protocol, the DMA registers behind this API.
- Using the Hardware Abstraction Layer, the C library your device application uses to access the registers and buffers.