Skip to content

Commit

Permalink
Add SPI 25xx EEPROM support. (qmk#8780)
Browse files Browse the repository at this point in the history
  • Loading branch information
tzarc authored May 19, 2020
1 parent 4604c70 commit 54b04d9
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 20 deletions.
7 changes: 6 additions & 1 deletion common_features.mk
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
SRC += $(QUANTUM_DIR)/pointing_device.c
endif

VALID_EEPROM_DRIVER_TYPES := vendor custom transient i2c
VALID_EEPROM_DRIVER_TYPES := vendor custom transient i2c spi
EEPROM_DRIVER ?= vendor
ifeq ($(filter $(EEPROM_DRIVER),$(VALID_EEPROM_DRIVER_TYPES)),)
$(error EEPROM_DRIVER="$(EEPROM_DRIVER)" is not a valid EEPROM driver)
Expand All @@ -85,6 +85,11 @@ else
COMMON_VPATH += $(DRIVER_PATH)/eeprom
QUANTUM_LIB_SRC += i2c_master.c
SRC += eeprom_driver.c eeprom_i2c.c
else ifeq ($(strip $(EEPROM_DRIVER)), spi)
OPT_DEFS += -DEEPROM_DRIVER -DEEPROM_SPI
COMMON_VPATH += $(DRIVER_PATH)/eeprom
QUANTUM_LIB_SRC += spi_master.c
SRC += eeprom_driver.c eeprom_spi.c
else ifeq ($(strip $(EEPROM_DRIVER)), transient)
OPT_DEFS += -DEEPROM_DRIVER -DEEPROM_TRANSIENT
COMMON_VPATH += $(DRIVER_PATH)/eeprom
Expand Down
29 changes: 24 additions & 5 deletions docs/eeprom_driver.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
# EEPROM Driver Configuration
# EEPROM Driver Configuration :id=eeprom-driver-configuration

The EEPROM driver can be swapped out depending on the needs of the keyboard, or whether extra hardware is present.

Driver | Description
-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
`EEPROM_DRIVER = vendor` (default) | Uses the on-chip driver provided by the chip manufacturer. For AVR, this is provided by avr-libc. This is supported on ARM for a subset of chips -- STM32F3xx, STM32F1xx, and STM32F072xB will be emulated by writing to flash. STM32L0xx and STM32L1xx will use the onboard dedicated true EEPROM. Other chips will generally act as "transient" below.
`EEPROM_DRIVER = i2c` | Supports writing to I2C-based 24xx EEPROM chips. See the driver section below.
`EEPROM_DRIVER = spi` | Supports writing to SPI-based 25xx EEPROM chips. See the driver section below.
`EEPROM_DRIVER = transient` | Fake EEPROM driver -- supports reading/writing to RAM, and will be discarded when power is lost.

## Vendor Driver Configuration
## Vendor Driver Configuration :id=vendor-eeprom-driver-configuration

#### STM32 L0/L1 Configuration :id=stm32l0l1-eeprom-driver-configuration

!> Resetting EEPROM using an STM32L0/L1 device takes up to 1 second for every 1kB of internal EEPROM used.

No configurable options are available.
`config.h` override | Description | Default Value
------------------------------------|--------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------
`#define STM32_ONBOARD_EEPROM_SIZE` | The size of the EEPROM to use, in bytes. Erase times can be high, so it's configurable here, if not using the default value. | Minimum required to cover base _eeconfig_ data, or `1024` if VIA is enabled.

## I2C Driver Configuration
## I2C Driver Configuration :id=i2c-eeprom-driver-configuration

Currently QMK supports 24xx-series chips over I2C. As such, requires a working i2c_master driver configuration. You can override the driver configuration via your config.h:

Expand All @@ -41,7 +46,21 @@ MB85RC256V FRAM | `#define EEPROM_I2C_MB85RC256V` | <https://www.adafruit.com/p

?> If you find that the EEPROM is not cooperating, ensure you've correctly shifted up your EEPROM address by 1. For example, the datasheet might state the address as `0b01010000` -- the correct value of `EXTERNAL_EEPROM_I2C_BASE_ADDRESS` needs to be `0b10100000`.

## Transient Driver configuration
## SPI Driver Configuration :id=spi-eeprom-driver-configuration

Currently QMK supports 25xx-series chips over SPI. As such, requires a working spi_master driver configuration. You can override the driver configuration via your config.h:

`config.h` override | Description | Default Value
-----------------------------------------------|--------------------------------------------------------------------------------------|--------------
`#define EXTERNAL_EEPROM_SPI_SLAVE_SELECT_PIN` | SPI Slave select pin in order to inform that the EEPROM is currently being addressed | _none_
`#define EXTERNAL_EEPROM_SPI_CLOCK_DIVISOR` | Clock divisor used to divide the peripheral clock to derive the SPI frequency | `64`
`#define EXTERNAL_EEPROM_BYTE_COUNT` | Total size of the EEPROM in bytes | 8192
`#define EXTERNAL_EEPROM_PAGE_SIZE` | Page size of the EEPROM in bytes, as specified in the datasheet | 32
`#define EXTERNAL_EEPROM_ADDRESS_SIZE` | The number of bytes to transmit for the memory location within the EEPROM | 2

!> There's no way to determine if there is an SPI EEPROM actually responding. Generally, this will result in reads of nothing but zero.

## Transient Driver configuration :id=transient-eeprom-driver-configuration

The only configurable item for the transient EEPROM driver is its size:

Expand Down
6 changes: 3 additions & 3 deletions drivers/eeprom/eeprom_driver.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@
#include "eeprom_driver.h"

uint8_t eeprom_read_byte(const uint8_t *addr) {
uint8_t ret;
uint8_t ret = 0;
eeprom_read_block(&ret, addr, 1);
return ret;
}

uint16_t eeprom_read_word(const uint16_t *addr) {
uint16_t ret;
uint16_t ret = 0;
eeprom_read_block(&ret, addr, 2);
return ret;
}

uint32_t eeprom_read_dword(const uint32_t *addr) {
uint32_t ret;
uint32_t ret = 0;
eeprom_read_block(&ret, addr, 4);
return ret;
}
Expand Down
30 changes: 19 additions & 11 deletions drivers/eeprom/eeprom_i2c.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ static inline void init_i2c_if_required(void) {
}

static inline void fill_target_address(uint8_t *buffer, const void *addr) {
intptr_t p = (intptr_t)addr;
uintptr_t p = (uintptr_t)addr;
for (int i = 0; i < EXTERNAL_EEPROM_ADDRESS_SIZE; ++i) {
buffer[EXTERNAL_EEPROM_ADDRESS_SIZE - 1 - i] = p & 0xFF;
p >>= 8;
Expand All @@ -60,20 +60,28 @@ static inline void fill_target_address(uint8_t *buffer, const void *addr) {
void eeprom_driver_init(void) {}

void eeprom_driver_erase(void) {
#ifdef CONSOLE_ENABLE
uint32_t start = timer_read32();
#endif

uint8_t buf[EXTERNAL_EEPROM_PAGE_SIZE];
memset(buf, 0x00, EXTERNAL_EEPROM_PAGE_SIZE);
for (intptr_t addr = 0; addr < EXTERNAL_EEPROM_BYTE_COUNT; addr += EXTERNAL_EEPROM_PAGE_SIZE) {
eeprom_write_block(buf, (void *)addr, EXTERNAL_EEPROM_PAGE_SIZE);
for (uint32_t addr = 0; addr < EXTERNAL_EEPROM_BYTE_COUNT; addr += EXTERNAL_EEPROM_PAGE_SIZE) {
eeprom_write_block(buf, (void *)(uintptr_t)addr, EXTERNAL_EEPROM_PAGE_SIZE);
}

#ifdef CONSOLE_ENABLE
dprintf("EEPROM erase took %ldms to complete\n", ((long)(timer_read32() - start)));
#endif
}

void eeprom_read_block(void *buf, const void *addr, size_t len) {
uint8_t complete_packet[EXTERNAL_EEPROM_ADDRESS_SIZE];
fill_target_address(complete_packet, addr);

init_i2c_if_required();
i2c_transmit(EXTERNAL_EEPROM_I2C_ADDRESS((intptr_t)addr), complete_packet, EXTERNAL_EEPROM_ADDRESS_SIZE, 100);
i2c_receive(EXTERNAL_EEPROM_I2C_ADDRESS((intptr_t)addr), buf, len, 100);
i2c_transmit(EXTERNAL_EEPROM_I2C_ADDRESS((uintptr_t)addr), complete_packet, EXTERNAL_EEPROM_ADDRESS_SIZE, 100);
i2c_receive(EXTERNAL_EEPROM_I2C_ADDRESS((uintptr_t)addr), buf, len, 100);

#ifdef DEBUG_EEPROM_OUTPUT
dprintf("[EEPROM R] 0x%04X: ", ((int)addr));
Expand All @@ -85,14 +93,14 @@ void eeprom_read_block(void *buf, const void *addr, size_t len) {
}

void eeprom_write_block(const void *buf, void *addr, size_t len) {
uint8_t complete_packet[EXTERNAL_EEPROM_ADDRESS_SIZE + EXTERNAL_EEPROM_PAGE_SIZE];
uint8_t *read_buf = (uint8_t *)buf;
intptr_t target_addr = (intptr_t)addr;
uint8_t complete_packet[EXTERNAL_EEPROM_ADDRESS_SIZE + EXTERNAL_EEPROM_PAGE_SIZE];
uint8_t * read_buf = (uint8_t *)buf;
uintptr_t target_addr = (uintptr_t)addr;

init_i2c_if_required();
while (len > 0) {
intptr_t page_offset = target_addr % EXTERNAL_EEPROM_PAGE_SIZE;
int write_length = EXTERNAL_EEPROM_PAGE_SIZE - page_offset;
uintptr_t page_offset = target_addr % EXTERNAL_EEPROM_PAGE_SIZE;
int write_length = EXTERNAL_EEPROM_PAGE_SIZE - page_offset;
if (write_length > len) {
write_length = len;
}
Expand All @@ -110,7 +118,7 @@ void eeprom_write_block(const void *buf, void *addr, size_t len) {
dprintf("\n");
#endif // DEBUG_EEPROM_OUTPUT

i2c_transmit(EXTERNAL_EEPROM_I2C_ADDRESS((intptr_t)addr), complete_packet, EXTERNAL_EEPROM_ADDRESS_SIZE + write_length, 100);
i2c_transmit(EXTERNAL_EEPROM_I2C_ADDRESS((uintptr_t)addr), complete_packet, EXTERNAL_EEPROM_ADDRESS_SIZE + write_length, 100);
wait_ms(EXTERNAL_EEPROM_WRITE_TIME);

read_buf += write_length;
Expand Down
231 changes: 231 additions & 0 deletions drivers/eeprom/eeprom_spi.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/* Copyright 2020 Nick Brassel (tzarc)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include <stdint.h>
#include <string.h>

/*
Note that the implementations of eeprom_XXXX_YYYY on AVR are normally
provided by avr-libc. The same functions are reimplemented below and are
rerouted to the external SPI equivalent.
Seemingly, as this is compiled from within QMK, the object file generated
during the build overrides the avr-libc implementation during the linking
stage.
On other platforms such as ARM, there are no provided implementations, so
there is nothing to override during linkage.
*/

#include "wait.h"
#include "spi_master.h"
#include "eeprom.h"
#include "eeprom_spi.h"

#define CMD_WREN 6
#define CMD_WRDI 4
#define CMD_RDSR 5
#define CMD_WRSR 1
#define CMD_READ 3
#define CMD_WRITE 2

#define SR_WIP 0x01

// #define DEBUG_EEPROM_OUTPUT

#ifndef EXTERNAL_EEPROM_SPI_TIMEOUT
# define EXTERNAL_EEPROM_SPI_TIMEOUT 100
#endif

#ifdef CONSOLE_ENABLE
# include "print.h"
#endif // CONSOLE_ENABLE

static void init_spi_if_required(void) {
static int done = 0;
if (!done) {
spi_init();
done = 1;
}
}

static bool spi_eeprom_start(void) { return spi_start(EXTERNAL_EEPROM_SPI_SLAVE_SELECT_PIN, EXTERNAL_EEPROM_SPI_LSBFIRST, EXTERNAL_EEPROM_SPI_MODE, EXTERNAL_EEPROM_SPI_CLOCK_DIVISOR); }

static spi_status_t spi_eeprom_wait_while_busy(int timeout) {
uint32_t deadline = timer_read32() + timeout;
spi_status_t response;
do {
spi_write(CMD_RDSR);
response = spi_read();
if (timer_read32() >= deadline) {
return SPI_STATUS_TIMEOUT;
}
} while (response & SR_WIP);
return SPI_STATUS_SUCCESS;
}

static void spi_eeprom_transmit_address(uintptr_t addr) {
uint8_t buffer[EXTERNAL_EEPROM_ADDRESS_SIZE];

for (int i = 0; i < EXTERNAL_EEPROM_ADDRESS_SIZE; ++i) {
buffer[EXTERNAL_EEPROM_ADDRESS_SIZE - 1 - i] = addr & 0xFF;
addr >>= 8;
}

spi_transmit(buffer, EXTERNAL_EEPROM_ADDRESS_SIZE);
}

//----------------------------------------------------------------------------------------------------------------------

void eeprom_driver_init(void) {}

void eeprom_driver_erase(void) {
#ifdef CONSOLE_ENABLE
uint32_t start = timer_read32();
#endif

uint8_t buf[EXTERNAL_EEPROM_PAGE_SIZE];
memset(buf, 0x00, EXTERNAL_EEPROM_PAGE_SIZE);
for (uint32_t addr = 0; addr < EXTERNAL_EEPROM_BYTE_COUNT; addr += EXTERNAL_EEPROM_PAGE_SIZE) {
eeprom_write_block(buf, (void *)(uintptr_t)addr, EXTERNAL_EEPROM_PAGE_SIZE);
}

#ifdef CONSOLE_ENABLE
dprintf("EEPROM erase took %ldms to complete\n", ((long)(timer_read32() - start)));
#endif
}

void eeprom_read_block(void *buf, const void *addr, size_t len) {
init_spi_if_required();

//-------------------------------------------------
// Wait for the write-in-progress bit to be cleared
bool res = spi_eeprom_start();
if (!res) {
dprint("failed to start SPI for WIP check\n");
memset(buf, 0, len);
return;
}

spi_status_t response = spi_eeprom_wait_while_busy(EXTERNAL_EEPROM_SPI_TIMEOUT);
spi_stop();
if (response == SPI_STATUS_TIMEOUT) {
dprint("SPI timeout for WIP check\n");
memset(buf, 0, len);
return;
}

//-------------------------------------------------
// Perform read
res = spi_eeprom_start();
if (!res) {
dprint("failed to start SPI for read\n");
memset(buf, 0, len);
return;
}

spi_write(CMD_READ);
spi_eeprom_transmit_address((uintptr_t)addr);
spi_receive(buf, len);

#ifdef DEBUG_EEPROM_OUTPUT
dprintf("[EEPROM R] 0x%08lX: ", ((uint32_t)(uintptr_t)addr));
for (size_t i = 0; i < len; ++i) {
dprintf(" %02X", (int)(((uint8_t *)buf)[i]));
}
dprintf("\n");
#endif // DEBUG_EEPROM_OUTPUT

spi_stop();
}

void eeprom_write_block(const void *buf, void *addr, size_t len) {
init_spi_if_required();

bool res;
uint8_t * read_buf = (uint8_t *)buf;
uintptr_t target_addr = (uintptr_t)addr;

while (len > 0) {
uintptr_t page_offset = target_addr % EXTERNAL_EEPROM_PAGE_SIZE;
int write_length = EXTERNAL_EEPROM_PAGE_SIZE - page_offset;
if (write_length > len) {
write_length = len;
}

//-------------------------------------------------
// Wait for the write-in-progress bit to be cleared
res = spi_eeprom_start();
if (!res) {
dprint("failed to start SPI for WIP check\n");
return;
}

spi_status_t response = spi_eeprom_wait_while_busy(EXTERNAL_EEPROM_SPI_TIMEOUT);
spi_stop();
if (response == SPI_STATUS_TIMEOUT) {
dprint("SPI timeout for WIP check\n");
return;
}

//-------------------------------------------------
// Enable writes
res = spi_eeprom_start();
if (!res) {
dprint("failed to start SPI for write-enable\n");
return;
}

spi_write(CMD_WREN);
spi_stop();

//-------------------------------------------------
// Perform the write
res = spi_eeprom_start();
if (!res) {
dprint("failed to start SPI for write\n");
return;
}

#ifdef DEBUG_EEPROM_OUTPUT
dprintf("[EEPROM W] 0x%08lX: ", ((uint32_t)(uintptr_t)target_addr));
for (size_t i = 0; i < write_length; i++) {
dprintf(" %02X", (int)(uint8_t)(read_buf[i]));
}
dprintf("\n");
#endif // DEBUG_EEPROM_OUTPUT

spi_write(CMD_WRITE);
spi_eeprom_transmit_address(target_addr);
spi_transmit(read_buf, write_length);
spi_stop();

read_buf += write_length;
target_addr += write_length;
len -= write_length;
}

//-------------------------------------------------
// Disable writes
res = spi_eeprom_start();
if (!res) {
dprint("failed to start SPI for write-disable\n");
return;
}

spi_write(CMD_WRDI);
spi_stop();
}
Loading

0 comments on commit 54b04d9

Please sign in to comment.