External RAM using FSMC (STM32Fxxx)

The FSMC interface of the STM32 microcontroller family allows it to interface external devices with a (wide) data and address bus. Although the system is designed for RAM, Flash and PC cards it's often used as an elegant means to address LCD and TFT screens that are capable of high bandwidths.

The following guide will show you how to interface an SRAM device.

Used system

In this tutorial, we'll look at two examples running ChibiOS/RT 2.6.2.

  • Example 1: Interface the 512kB SRAM to the STM32F407ZGT on the Olimex P407 board. The SRAM is a K6R4016V1D-TC10:

example1ram.jpg

  • Example 2: Drive 2 devices with an STM32F103ZET6: an LCD with an SSD1963 driver and a 1Mbyte SRAM. The SRAM is a ISWV51216BLL. As in example 1 there's a pull-up on the nCE pin but it's not shown in this schematic:

example2ram.jpg

These examples are provided as a guide for common cases, it will work with an SRAM of any size and any STM32F controller which provides the necessary interface. Only a subset of this controller family implements this interface.

Step 0: Gather information

It's essential that you understand how the FSMC interface works in order to figure out the proper timing values. Read through the FSMC chapter of the reference manual of your controller to learn about it's architecture and parameters. Then make sure that you got a proper datasheet of the RAM you want to interface, there needs to be a table about timing information somewhere.

Application note AN2784 focusses specifically on FSMC for high-density STM32F10xxx devices. Much (if not all) can be translated to an STM32 of other series.

Step 1: Peripheral configuration

Before we can talk to the SRAM, we first have to initialise and set up the FSMC peripheral. From the reference manual we learn that we have to set all the pins (data, address and control lanes) to alternative mode 12 (a.k.a. PAL_MODE_STM32_ALTERNATE_PUSHPULL in older Chibios versions). The easiest way to do this is using the IO-Bus structure of ChibiOS/RT. Afterwards we have to care about the FSMC timings and this will actually be the hardest part.

The FSMC memory map of both the STM32F407 and STM32F103ZET6 controller looks like this:

FSMC memory map of STM32F407

In both examples we're only interested in bank 1 (from 0x60000000 to 0x6FFFFFF) because this bank is specifically designed for our devices.

Each bank is divided into four equal sub-banks, this is denoted by the “4 x 64MB” comments. Bank 1 subbanks are addressed starting at 0x60000000h, 0x64000000h, 0x68000000h and 0x6C000000h.

Let's look at the physical signals for these banks:

FSMC signal map

Some signals are tied to a specific bank and FSMC device (e.g. FSMC_NEx is used to talk to NOR/PSRAM) while others are shared (e.g. data and address).

The chip-select pins for the four sub-banks in bank 1 are named FSMC_NE[1:4].

In the first example the chip select of the SRAM on the Olimex-P407 board is connected to NE1 and therefore our memory starts at 0x6000000.

In the second example the LCD is mapped to bank 1 sub-bank 1 (0x60000000h) and we'll use sub-bank 2 for the SRAM (chip enable on FSMC_NE2 and addressed at base 0x64000000h).

A working FSMC initialisation for the Olimex P407 board (example 1) would look like the following:

static void fsmc_setup(void) {
	unsigned char FSMC_Bank;
 
	// STM32F4 FSMC init
	rccEnableAHB3(RCC_AHB3ENR_FSMCEN, 0);
 
	// Group pins
	IOBus busD = {GPIOD, (1 << 0) | (1 << 1) | (1 << 4) | (1 << 5) | (1 << 7) | (1 << 8) | (1 <<  9) | (1 << 10) | (1 << 11) | (1 << 12) | (1 << 14) | (1 << 15), 0};
	IOBus busE = {GPIOE, (1 << 0) | (1 << 1) | (1 << 3) | (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 11) | (1 << 12) | (1 << 13) | (1 << 14) | (1 << 15), 0};
	IOBus busF = {GPIOF, (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 12) | (1 << 13) | (1 << 14) | (1 << 15), 0};
	IOBus busG = {GPIOG, (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5), 0};
 
	// FSMC is an alternate function 12 (AF12)
	palSetBusMode(&busD, PAL_MODE_ALTERNATE(12));
	palSetBusMode(&busE, PAL_MODE_ALTERNATE(12));
	palSetBusMode(&busF, PAL_MODE_ALTERNATE(12));
	palSetBusMode(&busG, PAL_MODE_ALTERNATE(12));
 
        // The SRAM is connected to the first FSMC BANK (NE1)
	FSMC_Bank = 0;
 
	// FSMC timing. Use BTRx where x is the sub-bank.
	FSMC_Bank1->BTCR[FSMC_Bank+1] = (FSMC_BTR1_ADDSET_1 | FSMC_BTR1_ADDSET_3) | (FSMC_BTR1_DATAST_1 | FSMC_BTR1_DATAST_3) | (FSMC_BTR1_BUSTURN_1 | FSMC_BTR1_BUSTURN_3);
 
	// Bank1 NOR/SRAM control register configuration. Use BCRx where x is the sub-bank.
	FSMC_Bank1->BTCR[FSMC_Bank] = FSMC_BCR1_MWID_0 | FSMC_BCR1_WREN | FSMC_BCR1_MBKEN;
}

A working FSMC initialisation for the STM32F103ZET6 with LCD and SRAM (example 2) would look like the following:

#define SRAMSIZE	1048576	//SRAM 1Mb bytes
#define SRAMBUFSIZE	(SRAMSIZE - 8)	//red tape consumes a few bytes (8?), keep it below SRAMSIZE
 
static MemoryHeap ext_heap;
static uint8_t * buffer;
 
static void display_fsmc_setup(void) {
	/* Initialise the display */
	/* set pins to FSMC mode */
	IOBus busD = {GPIOD, (1 << 0) | (1 << 1) | (1 << 4) | (1 << 5) | (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 11) | (1 << 14) | (1 << 15), 0};
	IOBus busE = {GPIOE, (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 11) | (1 << 12) | (1 << 13) | (1 << 14) | (1 << 15), 0};
	const unsigned char FSMC_Bank = 0;
	/* FSMC setup for F1/F3 */
	rccEnableAHB(RCC_AHBENR_FSMCEN, 0);
 
	palSetBusMode(&busD, PAL_MODE_ALTERNATE(12));
	palSetBusMode(&busE, PAL_MODE_ALTERNATE(12));
	palSetPadMode(GPIOG, GPIOG_LCD_RESET, PAL_MODE_OUTPUT_PUSHPULL);
 
	/* FSMC timing */
	FSMC_Bank1->BTCR[FSMC_Bank+1] = (FSMC_BTR1_ADDSET_0) \
			| (FSMC_BTR1_DATAST_0) \
			| (FSMC_BTR1_BUSTURN_0) ;
 
	/* Bank1 NOR/SRAM control register configuration
	 * This is actually not needed as already set by default after reset */
	//FSMC_Bank1->BTCR[FSMC_Bank] = FSMC_BCR1_MWID_0 | FSMC_BCR1_WREN | FSMC_BCR1_MBKEN;
}
 
static void SRAM_fsmc_setup(void) {
	unsigned char FSMC_Bank;
	IOBus busD = {GPIOD, (1 << 0)|(1 << 1)|(1 << 4)|(1 << 5)|(1 << 7)|(1 << 8)|(1 <<  9)|(1 << 10)|(1 << 11)|(1 << 12)|(1 << 13)|(1 << 14)|(1 << 15), 0};
	IOBus busE = {GPIOE, (1 << 0)|(1 << 1)|(1 << 7)|(1 << 8)|(1 << 9)|(1 << 10)|(1 << 11)|(1 << 12)|(1 << 13)|(1 << 14)|(1 << 15), 0};
	IOBus busF = {GPIOF, (1 << 0)|(1 << 1)|(1 << 2)|(1 << 3)|(1 << 4)|(1 << 5)|(1 << 12)|(1 << 13)|(1 << 14)|(1 << 15), 0};
	IOBus busG = {GPIOG, (1 << 0)|(1 << 1)|(1 << 2)|(1 << 3)|(1 << 4)|(1 << 5)|(1 << 9), 0};
 
	//set all pin functions
	palSetBusMode(&busD, PAL_MODE_ALTERNATE(12));
	palSetBusMode(&busE, PAL_MODE_ALTERNATE(12));
	palSetBusMode(&busF, PAL_MODE_ALTERNATE(12));
	palSetBusMode(&busG, PAL_MODE_ALTERNATE(12));
	//The SRAM is connected to the second FSMC BANK (NE2)
	FSMC_Bank = 2;
	// FSMC timing. Use BTRx where x is the sub-bank.
	FSMC_Bank1->BTCR[FSMC_Bank+1] = (FSMC_BTR2_ADDSET_1) | (FSMC_BTR2_DATAST_0 | FSMC_BTR2_DATAST_1);
 
	// Bank1 NOR/SRAM control register configuration. Use BCRx where x is the sub-bank.
	FSMC_Bank1->BTCR[FSMC_Bank] = FSMC_BCR2_MWID_0 | FSMC_BCR2_WREN | FSMC_BCR2_MBKEN;
}

In our initialisation code we'd first call display_fsmc_setup() and follow it with a call to SRAM_fsmc_setup(); The LCD FSMC setup is taken from µGFX.

Notice a couple of things:

  • example 1 (STM32F407) uses rccEnableAHB3() while example 2 (STM32F103) employs rccEnableAHB().
  • we're not obliged to define all pins. If you only need 20 address pins you don't need to consume the remaining 6 (FSMC_A[20:25]). If your memory device doesn't implement FSMC_CLK you don't need to use it.
  • The structure behind FSMC_Bank1 looks like this:
  typedef struct
{
  __IO uint32_t BTCR[8];    // NOR/PSRAM chip-select control register(BCR) and chip-select timing register(BTR)   
} FSMC_Bank1_TypeDef; 

… so for each sub bank we have 2 BTCR entries. One deals with access timing while the other defines general configuration like “write enable” and data width.

In the first example we connect RAM to sub bank 1 and we configure it with FSMC_Bank1→BTCR[0] and FSMC_Bank1→BTCR[1]. In the second example we need FSMC_Bank1→BTCR[2] and FSMC_Bank1→BTCR[3] to configure the RAM.

To calculate the timing for your RAM you need the RAM's datasheet and it's best to also look at AN2784 section 3.2

As an example let's look at the 1Mbyte RAM in example 2 (IS66WV51216BLL). From AN2784 we get 3 equations that need to be satisfied:

  1. ( (ADDSET + 1) + (DATAST + 1) ) × tHCLK ≥ max (tWC)
  2. DATAST × tHCLK = tPWE1
  3. DATAST ≥ (tAA + tsu(Data_NE) + tv(A_NE))/tHCLK – ADDSET – 4

Where …

  • tHCLK is determined by your system: in example 2 the clock is 72Mc which leaves us with tHCLK = 14 ns
  • tWC is the write cycle time, it's maximum is probably an error in the application note and should be a minimum: 55 ns
  • tPWE1 is the Write Enable low pulse width = 45 ns
  • tAA is the Address access time = 55 ns
  • tsu(Data_NE) is the Data to FSMC_NEx high setup time (called tHZCS1 in our datasheet) = 20 ns
  • tv(A_NE) is the FSMC_NEx low to FSMC_A valid time (called tACS1 in our datasheet) = 55 ns

Formula 2: DATAST = tPWE1 / tHCLK = 45 / 14 = 3

Formula 1: ( (ADDSET + 1) + (DATAST + 1) ) × tHCLK ≥ max (tWC)

so (ADDSET + 1) ≥ 55 / 14 - ( 3 + 1 ) ⇒ ADDSET ≥ 1 ⇒ ADDSET = 2

We verify using Formula 3:

3 ≥ (55 + 20 + 55) / 14 - 2 - 4 which checks out if we apply rounding to the nearest integer. In this case we'd try with ADDSET = 2 but if it fails we'd go with ADDSET = 3

Lastly, we need to consider are the BCRx bits. In the examples we enable FSMC_BCR2_MWID_0 to allow 16 bit data width, both RAM examples support this data width. We enable write access by setting FSMC_BCR2_WREN and the bank itself is enabled with FSMC_BCR1_MBKEN.

Step 2: Create a heap

From the hardware point of view, we are now able to communicate with the SRAM. The next step is to create a heap so we can use the ChibiOS/RT memory management to use the RAM. For this purpose, the ChibiOS/RT API provides us with a routine called chHeapInit():

void chHeapInit(MemoryHeap *heapp, void *buf, size_t size);
  • heapp - This is the heap pointer. It's the thing that actually gets initialised by calling this function. We'll use the heap pointer to use the newly created memory in the next step.
  • buf - The pointer to the memory. In example 1 the SRAM is on Bank 1 sub-bank 1, the start address will be 0x60000000. In example 2 it's 0x64000000
  • size - The mount of memory. We're going to use the entire SRAM as a new heap and therefore our size will be 524288 (512kB) in example 1 and 1048576 (1MB) in example 2

An important note is given to this function:

Both the heap buffer base and the heap size must be aligned to the stkalign_t type size.

The stkalign_t type for the STM32 microcontroller is 8. Therefore both our values (0x60000000 & 524288 and 0x64000000 & 1048576) are aligned and we don't have to care any further.

Our code looks like the following:

MemoryHeap ext_heap;
chHeapInit(&ext_heap, (void*)0x60000000, 524288); //example 1

Step 3: Use the created heap

The external memory has now been “registered” by the ChibiOS/RT memory management system. From now on we can allocate and free memory use chHeapAlloc() and chHeapFree(). The first argument of the chHeapAlloc() routine is the pointer to our memory heap:

uint8_t *buffer;
 
buffer = chHeapAlloc(&ext_heap, BUF_SIZE);
 
if(buffer == NULL) {
	chprintf(shell, "couldn't allocate buffer!\r\n");
} else {
	chprintf(shell, "buffer allocated at address 0x%X\r\n", buffer);
}	

The maximum size you can allocate from the heap is smaller than the actual heap size. That's because a few bytes are used by the memory manager to keep track of the allocations and so on.

Instead of defining buffer as an 8-bit pointer you can define it as a 16- or 32-bit pointer. Take care however to keep an eye on the addressing range: if your uint8_t* buffer can go to buffer[N] then your uint16_t* buffer should be limited to buffer[N/2]. If you don't limit it the address will just wrap around.

Secondly, for performance reasons you might want to stick to the native data width of the RAM. Addressing a 16-bit RAM with a uint32_t* buffer will result in 2 write cycles per assignment.

Troubleshooting

In case of your SRAM is not working as expected, check the following things:

  • Pin assignments
  • Peripheral clock
  • Timings

The most straight-forward way to check your interface is by hooking up your logic analyser. You don't need to connect all pins, just the control lines (nCE, nOE, nWE, nBHE and nBLE). Additionally, assign an extra unused pin as timing indicator: before calling the FSMC init routine you set the pin (palSetPad(GPIOx, DEBUG_PINy)) and clear it (palClearPad) after the init routine. This gives you a clear indicator where the control activity should be located.

The most obvious error is a missing/incorrect pin assignment in your IOBus declaration. This causes the system to hang.

Even when you have a fully functioning system it's still advised to check the timing. You can do this with the logic analyser or by timing a routine that writes constants to the entire buffer:

static uint32_t endCounter, i, duration;
static uint32_t startCounter = halGetCounterValue();

for(i = 0; i < SRAMBUFFERSIZE; i++){
   buffer[i] = 0xAA;// b10101010 for uint8_t* buffer
}
endCounter = halGetCounterValue();
if(endCounter > startCounter){
   duration = endCounter - startCounter
}else{
   duration = 0xFFFFFFFF - startCounter + endCounter;//counter overflowed
}
//duration can be queried via serial connection

Such a routine shouldn't take seconds to run. Calculate the time per transaction from the duration and this will give you a ballpark figure per transaction.

But even if the timing checks out, you should still do a RAM sanity check in your initialization code and keep it as part of your standard boot procedure:

static uint8_t badRAM = FALSE;

for(i = 0; i < SRAMBUFFERSIZE; i++){
   if(buffer[i] != 0xAA){// b10101010
      badRAM = TRUE;
   }
}

If you couldn't successfully use your SRAM by now, please contact the community through the discussion forum and leave all details so we may help you as best as we can.

 
chibios/community/plans/external_ram.txt · Last modified: 2014/03/04 12:52 by tectu
 
Except where otherwise noted, content on this wiki is licensed under the following license:GNU Free Documentation License 1.3