Skip to content

Instantly share code, notes, and snippets.

@clydebarrow
Last active January 25, 2021 23:39
Show Gist options
  • Save clydebarrow/8bcad81ea6ecf45750738df2a9527f25 to your computer and use it in GitHub Desktop.
Save clydebarrow/8bcad81ea6ecf45750738df2a9527f25 to your computer and use it in GitHub Desktop.
SPI Flash read/write/erase functions for SiLabs EFR32 and friends.
//
// Created by Clyde Stubbs on 13/1/21.
//
#include <spiFlash.h>
#include <em_gpio.h>
#include <em_usart.h>
#include <em_ldma.h>
// Command bytes for the serial flash S25FL164K
#define WAKEUP_CMD 0xAB
#define WRITE_CMD 0x02 // write data command
#define READ_CMD 0x03 // read data command
#define READ_STATUS_1 0x05
#define READ_STATUS_2 0x35
#define READ_STATUS_3 0x33
#define WRITE_ENABLE 0x06
#define SECTOR_ERASE 0x20 // erase a 4K sector
#define RESET_ENABLE 0x66 // enable SW_RESET
#define SW_RESET 0x99 // reset chip to defaults
#define JEDEC_ID_CMD 0x9F
#define POWER_DOWN_CMD 0xB9
#define CS_PORT gpioPortC
#define CS_PIN 7 // chip select output
#define WP_PORT gpioPortC // write protect port
#define WP_PIN 6 // write protect output
#define HOLD_PORT gpioPortC // write protect port
#define HOLD_PIN 11 // write protect output
#define SO_PORT gpioPortC // Serial output port (RX to us)
#define SO_PIN 8 // Serial output pin
#define SI_PORT gpioPortC // serial input port (TX to us)
#define SI_PIN 9 // serial input (to ROM)
#define CLK_PORT gpioPortC // clock port
#define CLK_PIN 10 // clock (to ROM)
enum {
dmaChannelSpiPrimary,
dmaChannelSpiSecondary,
dmaChannelCount
};
static const LDMA_TransferCfg_t txConfig = LDMA_TRANSFER_CFG_PERIPHERAL(ldmaPeripheralSignal_USART1_TXBL);
static const LDMA_TransferCfg_t rxConfig = LDMA_TRANSFER_CFG_PERIPHERAL(ldmaPeripheralSignal_USART1_RXDATAV);
static LDMA_Descriptor_t txDescriptor[2] = {
LDMA_DESCRIPTOR_LINKREL_M2P_BYTE(NULL, &USART1->TXDATA, 1, 1),
LDMA_DESCRIPTOR_SINGLE_M2P_BYTE(NULL, &USART1->TXDATA, 1),
};
static LDMA_Descriptor_t rxDescriptor[2] = {
LDMA_DESCRIPTOR_LINKREL_P2M_BYTE(&USART1->RXDATA, NULL, 1, 1),
LDMA_DESCRIPTOR_SINGLE_P2M_BYTE(&USART1->RXDATA, NULL, 1)
};
static const LDMA_Init_t dmaInit = LDMA_INIT_DEFAULT;
static bool dmaBusy = false;
// initialise the USART
static void spiInit() {
USART_InitSync_TypeDef usartInit = USART_INITSYNC_DEFAULT;
usartInit.enable = usartDisable;
usartInit.master = true; // master mode
usartInit.msbf = true; // send msb first
usartInit.baudrate = 10000000; // 10MHz bit rate. This is the maximum value that works.
usartInit.autoCsEnable = true; // cs is auto-controlled
usartInit.autoCsHold = 1; // probably unnecessary, but doesn't hurt much.
usartInit.autoCsSetup = 0;
USART_InitSync(USART1, &usartInit);
USART1->ROUTEPEN = USART_ROUTEPEN_TXPEN
| USART_ROUTEPEN_RXPEN
| USART_ROUTEPEN_CLKPEN
| USART_ROUTEPEN_CSPEN;
USART1->ROUTELOC0 |=
(14 << _USART_ROUTELOC0_TXLOC_SHIFT) //PC9
| (12 << _USART_ROUTELOC0_RXLOC_SHIFT) // PC8
| (13 << _USART_ROUTELOC0_CLKLOC_SHIFT) // PC10
| (9 << _USART_ROUTELOC0_CSLOC_SHIFT);// PC7
USART1->CTRL |= USART_CTRL_TXBIL_HALFFULL; // enable double buffering.
GPIO_PinModeSet(WP_PORT, WP_PIN, gpioModePushPull, 1);
GPIO_PinModeSet(HOLD_PORT, HOLD_PIN, gpioModePushPull, 1);
GPIO_PinModeSet(SI_PORT, SI_PIN, gpioModePushPull, 0);
GPIO_PinModeSet(SO_PORT, SO_PIN, gpioModeInputPull, 0);
GPIO_PinModeSet(CLK_PORT, CLK_PIN, gpioModePushPull, 0);
GPIO_PinModeSet(CS_PORT, CS_PIN, gpioModePushPull, 1);
txDescriptor[0].xfer.doneIfs = 0;
rxDescriptor[0].xfer.doneIfs = 0;
txDescriptor[1].xfer.doneIfs = 1;
rxDescriptor[1].xfer.doneIfs = 1;
rxDescriptor[0].xfer.dstInc = ldmaCtrlSrcIncNone; // descriptor 1 is used as a sink for unwanted bytes
rxDescriptor[1].xfer.dstInc = ldmaCtrlSrcIncOne; // data is read into here.
txDescriptor[0].xfer.srcInc = ldmaCtrlSrcIncOne; // this is the command descriptor, always has valid data.
USART_Enable(USART1, usartEnable);
LDMA_Init(&dmaInit);
}
/**
* Wait until the transmitter is idle, then clear the RX
*/
static inline bool waitForIdle() {
unsigned timeout = 5000;
while (--timeout != 0 && (dmaBusy || !(USART1->STATUS & USART_STATUS_TXIDLE)))
continue;
if (timeout == 0) {
int cnt = LDMA_TransferRemainingCount((int) (dmaChannelSpiPrimary));
return false;
}
USART1->CMD = USART_CMD_CLEARRX | USART_CMD_CLEARTX;
return true;
}
/**
* Receive a single byte from the SPI
* @return The byte
*/
static inline uint8_t readByte() {
while (!(USART1->STATUS & USART_STATUS_RXDATAV))
continue;
return (uint8_t) USART1->RXDATA;
}
/**
* Write a 1 byte command, return the response.
* @param cmd The command to send
*/
static uint8_t cmdWithResult(uint8_t cmd) {
if (!waitForIdle())
return 0;
USART1->TXDATA = (uint32_t) cmd;
USART1->TXDATA = (uint32_t) 0;
readByte();
return readByte();
}
/**
* Write a 1 byte command, no response required.
*/
static inline bool cmd(uint8_t cmd) {
if (!waitForIdle())
return false;
USART1->TXDATA = (uint32_t) cmd;
return true;
}
/**
* Initialise the SPI and flash chip
*/
void spiFlashInit(void) {
spiInit();
// wake up the chip, in case it's in deep sleep.
cmdWithResult(WAKEUP_CMD);
// now do a software reset
cmd(RESET_ENABLE);
cmd(SW_RESET);
waitForIdle();
}
/**
* Wait until the flash is not busy
* @return false if timeout occurred.
*/
static bool spiFlashWaitIdle() {
unsigned timeout = 0;
while (cmdWithResult(READ_STATUS_1) & 1)
if(++timeout == 100000) {
return false;
}
return true;
}
/**
* Read a block of data from the flash at a given address
* @param address The byte address to read data from
* @param buffer A buffer to hold the result
* @param buflen The length of the buffer.
* @return True if the read was successful
*/
bool spiFlashReadData(uint32_t address, uint8_t *buffer, size_t buflen) {
uint8_t cmdBuf[4];
if (buflen == 0)
return false;
spiFlashWaitIdle();
cmdBuf[0] = READ_CMD;
cmdBuf[1] = address >> 16;
cmdBuf[2] = address >> 8;
cmdBuf[3] = address;
// use 2 linked descriptors. The first sends the command and address.
txDescriptor[0].xfer.xferCnt = sizeof(cmdBuf) - 1;
txDescriptor[0].xfer.srcAddr = (uint32_t) cmdBuf;
// the second sends dummy bytes to clock in the read bytes
txDescriptor[1].xfer.xferCnt = buflen - 1;
txDescriptor[1].xfer.srcAddr = (uint32_t) cmdBuf;
txDescriptor[1].xfer.srcInc = ldmaCtrlSrcIncNone;
// first rx descriptor soaks up the bytes clocked out during command write
rxDescriptor[0].xfer.xferCnt = sizeof(cmdBuf) - 1;
rxDescriptor[0].xfer.dstAddr = (uint32_t) buffer;
// then read the data directly to the buffer
rxDescriptor[1].xfer.xferCnt = buflen - 1;
rxDescriptor[1].xfer.dstAddr = (uint32_t) buffer;
dmaBusy = true;
LDMA_StartTransfer(dmaChannelSpiPrimary, &rxConfig, rxDescriptor);
LDMA_StartTransfer(dmaChannelSpiSecondary, &txConfig, txDescriptor);
return waitForIdle(); // wait for completion
}
static bool writeEnable() {
if(!spiFlashWaitIdle())
return false;
cmd(WRITE_ENABLE);
return waitForIdle();
}
/**
* Erase a sector, starting at the given address.
* @param address The linear address.The lower 12 bits of the address must be zero.
*/
bool spiFlashEraseSector(uint32_t address) {
uint8_t cmdBuf[4];
if(!writeEnable())
return false;
cmdBuf[0] = SECTOR_ERASE;
cmdBuf[1] = address >> 16;
cmdBuf[2] = address >> 8;
cmdBuf[3] = address;
txDescriptor[1].xfer.xferCnt = sizeof(cmdBuf) - 1;
txDescriptor[1].xfer.srcAddr = (uint32_t) cmdBuf;
txDescriptor[1].xfer.srcInc = ldmaCtrlSrcIncOne;
dmaBusy = true;
LDMA_StartTransfer(dmaChannelSpiPrimary, &txConfig, &txDescriptor[1]);
return true;
}
/**
* Write data to the flash
* @param address // the address to write to
* @param buffer // the data
* @param buflen // length of the data
* @return true if the write succeeded
*/
bool spiFlashWriteData(uint32_t address, const uint8_t *buffer, size_t buflen) {
uint8_t cmdBuf[4];
while (buflen != 0) {
if(!writeEnable())
return false;
unsigned len = buflen;
unsigned maxChunk = 256 - (address & 0xFF);
if (len > maxChunk)
len = maxChunk;
cmdBuf[0] = WRITE_CMD;
cmdBuf[1] = address >> 16;
cmdBuf[2] = address >> 8;
cmdBuf[3] = address;
// linked descriptors write the command followed by the data.
txDescriptor[0].xfer.xferCnt = sizeof(cmdBuf) - 1;
txDescriptor[0].xfer.srcAddr = (uint32_t) cmdBuf;
txDescriptor[1].xfer.xferCnt = len - 1;
txDescriptor[1].xfer.srcAddr = (uint32_t) buffer;
txDescriptor[1].xfer.srcInc = ldmaCtrlSrcIncOne;
dmaBusy = true;
LDMA_StartTransfer(dmaChannelSpiPrimary, &txConfig, &txDescriptor[0]);
buffer += len;
address += len;
if (address == SPI_FLASH_SIZE) {
address = 0;
}
buflen -= len;
}
// don't return until the transfer is complete, since the caller may not preserve the buffer.
return waitForIdle();
}
void LDMA_IRQHandler(void) {
/* Get all pending and enabled interrupts. */
uint32_t pending = LDMA_IntGetEnabled();
/* Iterate over all LDMA channels. */
for (uint32_t ch = 0; ch != dmaChannelCount; ch++) {
uint32_t mask = 0x1u << ch;
if (pending & mask) {
/* Clear interrupt flag. */
LDMA->IFC = mask;
if (ch == dmaChannelSpiPrimary)
dmaBusy = false;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment