A projects to learn the SPI API of the RP2040 C SDK, and to use it to control an external Flash IC. In this post, I set up the firmware and execute a first communication.
|
Hardware Initialisation
I'm setting the SPI pins functional and pull the nHOLD and nWRITEPROTECT high. I'm also pulling the nCS of the SD Card on the Pico Eurocard high, because I use the same SPI peripheral for the SPI flash.
Pre-reads:
- Add external SPI Flash Memory to Raspberry Pico - 1: hardware
- Raspberry Pico and CMake - create your own C lib or subdirectory with header files
I've written a helper function for GPIO, and one for SPI. Here's a repeat of the pin assignments of post 1:
pin | IC function | PICO function | GPIO | PICO pin | |
1 | S# | SPI1 CSn | IO9 | 12 | has to be different than the one used by the SD Card, if sharing SPI1 |
2 | DQ1 | SPI1 RX | IO12 | 16 | |
3 | W# | GPIO | IO14 | 19 | pulled high at this time, may revisit that |
4 | VSS | ground | 38 | ||
5 | DQ0 | SPI1 TX | IO11 | 15 | |
6 | C | SPI1 SCK | IO10 | 14 | |
7 | HOLD# | GPIO | IO15 | 20 | pulled high at this time, may revisit that |
8 | VCC | 3V3 (OUT) | 36 |
#define M25_SPI (spi1) #define M25_SPI_RX_PIN (12) #define M25_SPI_SCK_PIN (10) #define M25_SPI_TX_PIN (11) #define M25_SPI_CSN_PIN (9) void setGPIO() { // disable Eurocard SDCard CS gpio_init(13); gpio_put(13, 1); gpio_set_dir(13, GPIO_OUT); // hold gpio_init(15); gpio_put(15, 1); gpio_set_dir(15, GPIO_OUT); // write gpio_init(14); gpio_put(14, 1); gpio_set_dir(14, GPIO_OUT); } void setSPI() { spi_init(M25_SPI, 1000 * 1000); gpio_set_function(M25_SPI_RX_PIN, GPIO_FUNC_SPI); gpio_set_function(M25_SPI_SCK_PIN, GPIO_FUNC_SPI); gpio_set_function(M25_SPI_TX_PIN, GPIO_FUNC_SPI); // Chip select is active-low, so we'll initialise it to a driven-high state gpio_init(M25_SPI_CSN_PIN); gpio_put(M25_SPI_CSN_PIN, 1); gpio_set_dir(M25_SPI_CSN_PIN, GPIO_OUT); }
These are called in main(), before the real business starts.
In the initial design, I'll manually control the nCS.
m25 c++ Class, and the a bit of CMake
This first firmware can execute one command: read the Flash IC's Read Identification (RDID). I've written the code in c++, to show that the SDK works in that context too.
To get the RDID, you submit the command 9Fh to the IC, with ~CS low. Then, keeping the ~CS low, you clock out 32 bits. See the structure below:
Code
m25 class header:
#ifndef _M25_H #define _M25_H #include "hardware/gpio.h" #include "hardware/spi.h" class m25 { public: m25(spi_inst_t * spi, uint cs) : _spi(spi), _cs(cs) {} ~m25() {} uint8_t rdid(); private: spi_inst_t * _spi; uint _cs; void setDataBits(uint data_bits); void cs_select() { asm volatile("nop \n nop \n nop"); gpio_put(this->_cs, 0); // Active low asm volatile("nop \n nop \n nop"); } void cs_deselect() { asm volatile("nop \n nop \n nop"); gpio_put(this->_cs, 1); asm volatile("nop \n nop \n nop"); } }; #endif // _MM25_H
The source code:
#include "m25.h" #define FLASH_CMD_RDID 0x9F void m25::setDataBits(uint data_bits) { spi_set_format( this->_spi, // SPI instance data_bits, // Number of bits per transfer SPI_CPOL_0, // Polarity (CPOL) SPI_CPHA_0, // Phase (CPHA) SPI_MSB_FIRST); } uint8_t m25::rdid() { uint8_t retval = 0U; uint8_t buf[3]; // command this->setDataBits(8); uint8_t cmdbuf[] = { FLASH_CMD_RDID, }; this->cs_select(); spi_write_blocking(this->_spi, cmdbuf, 1); spi_read_blocking(this->_spi, 0, buf, 1); // Manufacturer Identification spi_read_blocking(this->_spi, 0, buf + sizeof buf[0], 2); // Device Identification (Memory Type || Memory Capacity) this->cs_deselect(); return retval; }
In the main file, this is how this gets called:
#include "hardware/gpio.h" #include "hardware/spi.h" #include "m25.h" int main() { m25 *eeprom = nullptr; setGPIO(); setSPI(); eeprom = new m25(M25_SPI, M25_SPI_CSN_PIN); uint8_t rdid = eeprom->rdid(); delete eeprom; eeprom = nullptr; while(1) { } return 0;; }
When you set a breakpoint at the last line of the m25::rdid() method, this is the result:
You find the 3 attributes of RDID back, as specified in the datasheet.
CMake setup
In this post, I'm introducing a feature of CMake: you can divide your projects in directories, while keeping the main CMake file simple. This way you can abstract the intrinsics of your submodules. It's also a starting point if you want to turn them into reusable libraries later.
Directory structure:
./CMakeList.txt:
cmake_minimum_required(VERSION 3.13) # Pull in SDK (must be before project) include(pico_sdk_import.cmake) project(spi_flash C CXX ASM) set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 17) set(PICO_DEOPTIMIZED_DEBUG=1) pico_sdk_init() add_subdirectory(m25) add_executable(spi_flash spi_flash.cpp ) target_link_libraries(spi_flash pico_stdlib hardware_spi m25) pico_add_extra_outputs(spi_flash)
This CMake file knows that we want to use the ./m25 directory in our project. It does not know how it has to be built. CMake will check the CMake file in that dir, and include those instructions in the make cycle.
./m25/CMakeList.txt:
add_library( m25 m25.h m25.cpp ) target_link_libraries(m25 hardware_clocks hardware_resets hardware_spi ) target_include_directories(m25 PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
This CMake file just knows how to build the ./m25 directory. Using these two files will also take care that VSCode finds the includes, and get the usual autocomplete and inspect functionality.
VSCode project, with binary UF2 firmware, is attached: