Last active
January 25, 2021 23:39
-
-
Save clydebarrow/8bcad81ea6ecf45750738df2a9527f25 to your computer and use it in GitHub Desktop.
SPI Flash read/write/erase functions for SiLabs EFR32 and friends.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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