Skip to main content

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.

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.

RegionAddress rangeCached
SPI flash0x000000000x00FFFFFCYes
Flash OTP0x080000000x080001FCYes
LRAM0x081000000x0814FFFCYes
CSR registers0x082000000x08214003No
MMIO buffer0x083000000x0830FFFCNo
Device identity0x0FFFE0000x0FFFE004No
DMA registers0x100000000x1000001CNo

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

FunctionPurpose
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 codeEffectRequirements
kDMAOpCopyCopy lengthBytes from source to destination.Both addresses aligned to 4 bytes.
kDMAOpByteSetFill destination with the low byte of setValue.Destination aligned to 4 bytes.
kDMAOpWordSetFill destination with the whole 32 bits of setValue.Destination aligned, length a multiple of 4.

The functions returns one of three results:

ResultMeaning
kDmaDoneThe engine performed the whole operation and a cacheable destination is already invalidated.
kDmaNotRunNo 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.
kDmaErrorThe 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

FunctionPurpose
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 rstn drives, so its state survives a trap and survives a host stopping your core. If the engine is left in the Done state, it ignores the next operation you write to CONTROL.
  • CONTROL is sampled only while the DMA engine is idle. A write in any other state is dropped, and the stale Done then reads back as success.
  • Only an acknowledge returns the DMA engine to idle state. Writing CONTROL does 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