My previous article covered JEDEC (Joint Electron Device Engineering Council) standards and their Flash commands. In embedded systems, JEDEC-compliant serial flash memory, such as NOR Flash used with QSPI or SPI, is commonly used for tasks like storing code, updating firmware, or saving parameters.
In this article, we will design a flexible and scalable JEDEC memory access system in modern C++. The design follows SOLID principles and focuses on creating a protocol-independent architecture, making it easier to maintain and extend.
What We Aim to Achieve:
In this article, we aim to achieve the following,
✅ Support for Multiple Memory Types: Seamlessly integrate NOR, NAND, and EEPROM flash memory.
✅ Protocol-Agnostic: Easily work with different communication protocols, including SPI, QSPI, OSPI, and more.
✅ Abstraction Through Interface-Based Design: Leverage interfaces for flexible and scalable memory access.
✅ Clear Separation of Responsibilities: Ensure each component has a well-defined role, making the architecture cleaner and more maintainable.
✅ Extendable and Maintainable: Build a system that is both easy to extend with new features and simple to maintain over time.
So let’s break the implementation into the following steps. You can enhance it as per your requirements and existing code base implementation.
Step 1: Define a Common JEDEC Memory Interface (IJEDECDevice.hpp):
#pragma once #include <cstdint> /** * @file IJEDECDevice.hpp * @brief Abstract interface for JEDEC-compliant memory devices. */ /** * @class IJEDECDevice * @brief Interface for JEDEC-compatible memory devices like NOR, NAND, EEPROM. */ class IJEDECDevice { public: virtual ~IJEDECDevice() {} /** * @brief Initializes and powers up the memory device. * @return true if initialization was successful. */ virtual bool start() = 0; /** * @brief Puts the memory device in low-power or shutdown mode. * @return true if shutdown was successful. */ virtual bool stop() = 0; /** * @brief Reads JEDEC ID (manufacturer ID, memory type, capacity). * @param id Pointer to a buffer of 3 bytes to receive the ID. * @return true if successful. */ virtual bool readId(uint8_t* id) = 0; /** * @brief Reads data from a memory address. * @param address 24-bit memory address (for most JEDEC-compliant devices like NOR Flash). * @param buffer Pointer to buffer to receive data. * @param length Number of bytes to read. * @return true if read is successful. */ virtual bool readData(uint32_t address, uint8_t* buffer, uint32_t length) = 0; /** * @brief Writes data to memory. * @param address Address to write data to. * @param data Pointer to source data. * @param length Number of bytes to write. * @return true if write is successful. */ virtual bool writeData(uint32_t address, const uint8_t* data, uint32_t length) = 0; /** * @brief Erases a memory sector containing the given address. * @param address Any address within the sector to be erased. * @return true if erase is successful. */ virtual bool eraseSector(uint32_t address) = 0; /** * @brief Checks whether memory is busy with a previous operation. * @return true if device is busy. */ virtual bool isBusy() = 0; };
Step 2: Define the IComBus.hpp – Abstract Protocol Layer:
#pragma once #include <cstdint> /** * @file IComBus.hpp * @brief Interface for protocol layer (SPI/QSPI/etc) communication. */ /** * @class IComBus * @brief Abstracts the low-level protocol (SPI, QSPI) used by JEDEC devices. */ class IComBus { public: virtual ~IComBus() {} /** * @brief Transmit data to device. * @param data Pointer to data to send. * @param length Number of bytes to send. * @return true if successful. */ virtual bool transmit(const uint8_t* data, uint32_t length) = 0; /** * @brief Transmit command and receive data back. * @param tx Pointer to command or data to send (can be nullptr). * @param rx Pointer to receive buffer. * @param length Number of bytes to receive. * @return true if successful. */ virtual bool transmitReceive(const uint8_t* tx, uint8_t* rx, uint32_t length) = 0; };
Step 3: Define MacronixNorFlash.hpp for NOR flash implementation of JEDEC interface:
#pragma once #include "IJEDECDevice.hpp" #include "IComBus.hpp" /** * @file MacronixNorFlash.hpp * @brief Macronix-specific NOR flash implementation of JEDEC interface. */ /** * @class MacronixNorFlash * @brief Driver for Macronix JEDEC-compatible NOR flash memory. */ class MacronixNorFlash : public IJEDECDevice { public: /** * @brief Constructor. * @param bus Reference to protocol bus (SPI/QSPI/etc). */ explicit MacronixNorFlash(IComBus& bus) : m_bus(bus) {} bool start() override { // Startup sequence, wake-up, etc. return true; } bool stop() override { // Deep power-down mode uint8_t cmd = POWER_DOWN_CMD; return m_bus.transmit(&cmd, 1); } bool readId(uint8_t* id) override { uint8_t cmd = READ_ID_CMD; return m_bus.transmitReceive(&cmd, id, 3); } bool readData(uint32_t address, uint8_t* buffer, uint32_t length) override { uint8_t cmd[4] = {READ_CMD, static_cast<uint8_t>((address >> 16) & 0xFF), static_cast<uint8_t>((address >> 8) & 0xFF), static_cast<uint8_t>(address & 0xFF)}; return m_bus.transmit(cmd, 4) && m_bus.transmitReceive(nullptr, buffer, length); } bool writeData(uint32_t address, const uint8_t* data, uint32_t length) override { uint8_t cmd[4] = {WRITE_CMD, static_cast<uint8_t>((address >> 16) & 0xFF), static_cast<uint8_t>((address >> 8) & 0xFF), static_cast<uint8_t>(address & 0xFF)}; if (!writeEnable()) return false; return m_bus.transmit(cmd, 4) && m_bus.transmit(data, length); } bool eraseSector(uint32_t address) override { uint8_t cmd[4] = {ERASE_CMD, static_cast<uint8_t>((address >> 16) & 0xFF), static_cast<uint8_t>((address >> 8) & 0xFF), static_cast<uint8_t>(address & 0xFF)}; if (!writeEnable()) return false; return m_bus.transmit(cmd, 4); } bool isBusy() override { uint8_t cmd = READ_STATUS_CMD; uint8_t status = 0; return m_bus.transmitReceive(&cmd, &status, 1) && (status & 0x01); } private: IComBus& m_bus; bool writeEnable() { uint8_t cmd = WRITE_ENABLE_CMD; return m_bus.transmit(&cmd, 1); } static constexpr uint8_t READ_ID_CMD = 0x9F; static constexpr uint8_t READ_CMD = 0x03; static constexpr uint8_t WRITE_CMD = 0x02; static constexpr uint8_t ERASE_CMD = 0x20; static constexpr uint8_t READ_STATUS_CMD = 0x05; static constexpr uint8_t WRITE_ENABLE_CMD = 0x06; static constexpr uint8_t POWER_DOWN_CMD = 0xB9; };
Step 4: Define JEDECDeviceFactory.hpp, it is the factory class to create static instances of supported JEDEC flash drivers:
#pragma once #include "IJEDECDevice.hpp" #include "MacronixNorFlash.hpp" /** * @file JEDECDeviceFactory.hpp * @brief Factory to create static instances of supported JEDEC flash drivers. */ /** * @class JEDECDeviceFactory * @brief Provides static creation of JEDEC devices based on JEDEC ID. */ class JEDECDeviceFactory { public: /** * @brief Creates a device driver based on JEDEC ID. * @param bus Reference to the protocol bus used by device. * @return Pointer to supported JEDEC device or nullptr. */ static IJEDECDevice* create(IProtocolBus& bus) { uint8_t id[3] = {0}; uint8_t cmd = 0x9F; if (!bus.transmitReceive(&cmd, id, 3)) { return nullptr; } if (id[0] == 0xC2) { // Macronix NOR static MacronixNorFlash flash(bus); return &flash; } // Extend here with more vendors if needed. return nullptr; } };
Step 5: Define DummyComBus, it is the factory class to create static instances of supported JEDEC flash drivers:
#pragma once #include "IComBus.hpp" /** * @class DummyComBus * @brief A dummy protocol bus implementation for simulating flash communication. */ class DummyComBus : public IComBus { public: /** * @brief Simulates writing data to a device. * * @param data Pointer to the data to write. * @param length Length of data. * @return true if successful. */ bool write(const uint8_t* data, uint32_t length) { bool ret; // Your code return ret; } /** * @brief Simulates reading data from a device. * * @param rx Pointer to receive buffer. * @param length Number of bytes to read. * @return true if successful. */ bool read(uint8_t* rx, uint32_t length) { bool ret; // Your code return ret; } };
Below is an example to demonstrate how the above code will work :
- Use the JEDEC factory to automatically detect the memory device and create a memory device instance.
- Start the memory device to prepare it for operations.
- Read the JEDEC ID to identify the connected flash chip.
- Perform memory operations such as write, read, and erase.
- Stop the device gracefully after operations are complete.
int main() { DummyComBus bus; // Create device using factory IJEDECDevice* device = JEDECDeviceFactory::create(bus); if (!device) { // Handle failure return -1; } // Start the device if (!device->start()) { return -2; } // Read JEDEC ID uint8_t id[3]; if (device->readId(id)) { // Normally print or log these values // printf("JEDEC ID: %02X %02X %02X\n", id[0], id[1], id[2]); } // Write example uint8_t writeData[4] = {0xAA, 0xBB, 0xCC, 0xDD}; if (!device->writeData(0x000100, writeData, sizeof(writeData))) { return -3; } // Read back uint8_t readBuffer[4]; if (!device->readData(0x000100, readBuffer, sizeof(readBuffer))) { return -4; } // Erase sector if (!device->eraseSector(0x000100)) { return -5; } // Stop device device->stop(); return 0; }