Одним из самых простых и полезных SPI-устройств является последовательная EEPROM (электрически стираемая программируемая память), которая часто используется во встроенных устройствах в качестве небольшого энергонезависимого хранилища для параметров конфигурации и постоянного состояния (например, результатов в игре). Хранение данных для EEPROM измеряется десятилетиями. Одним из ограничений является то, что они могут пережить только ограниченное количество циклов записи (возможно, миллион) и, следовательно, должны использоваться для относительно медленно меняющейся информации. SPI EEPROM доступны от многих производителей с общими интерфейсами. Емкости варьируются от 1K бит до 1M бит.
Наш выбор EEPROM для первого примера приложения SPI был продиктован одним соображением — базовая операция интуитивно понятна (чтение / запись) и, следовательно, относительно легко определить, правильно ли работает код интерфейса. Пришло время использовать этот код для создания простого приложения.
В следующем обсуждении мы будем используем EEPROM от ST M95080-W ( 8-Kbit serial SPI bus EEPROM with high-speed clock ) в корпусе DIP.
Устройства реализуют простой протокол сообщений через шину SPI, где каждая транзакция состоит из одного или нескольких байтов инструкций, отправляемых на устройство, за которыми следуют один или несколько байтов данных на устройство или с него. Например, EEPROM содержит регистр состояния, который можно использовать для установки различных режимов защиты, а также для определения того, занято ли устройство. Устройствам EEPROM требуется много времени для выполнения операций записи! Транзакция чтения состояния показана ниже. Ведущий (STM32) выбирает EEPROM, понижая SS, затем передает 8-битный код RDSR (0x05) и получает 8-битное значение состояния.
Команда для реализации чтения статуса легко реализуется:
uint8_t eepromReadStatus() {
uint8_t cmd[] = {cmdRDSR , 0xff};
uint8_t res[2];
GPIO_WriteBit(EEPROM_PORT , EEPROM_CS , 0);
spiReadWrite(EEPROM_SPI , res, cmd, 2, EEPROM_SPEED);
GPIO_WriteBit(EEPROM_PORT , EEPROM_CS , 1);
return res[1];
}
Формат регистра статуса показан на рисунке ниже. На данный момент нас интересуют только два бита — WIP (выполняется запись) и WEL (блокировка включения записи). BP [1: 0] — это биты защиты блоков, которые, если установлены, защищают части EEPROM от записи. Бит WIP особенно важен — если он установлен, то выполняется запись, и все команды, кроме RDSR, не будут выполнены. Как мы видим, важно подождать, пока этот бит будет очищен, прежде чем предпринимать какие-либо другие действия. Бит WEL должен быть установлен для выполнения записи данных, и он автоматически сбрасывается при любой записи — позже мы увидим, как установить этот бит.
enum eepromCMD { cmdREAD = 0x03, cmdWRITE = 0x02
cmdWREN = 0x06, cmdWRDI = 0x04,
cmdRDSR = 0x05, cmdWRSR = 0x01 };
Самыми простыми командами являются WRDI (отключение записи) и WREN (включение записи), которые очищают и устанавливают бит WEL регистра состояния соответственно.
Каждый реализуется путем записи одного байта — команды — в EEPROM. Данные не возвращаются. Помните, что обе эти команды не будут выполнены, если EEPROM занята. Например, мы реализуем включение EEPROM следующим образом (обратите внимание на опрос регистра статуса!):
#define WIP(x) ((x) & 1)
void eepromWriteEnable(){
uint8_t cmd = cmdWREN;
while (WIP(eepromReadStatus()));
GPIO_WriteBit(EEPROM_PORT , EEPROM_CS , 0);
spiReadWrite(EEPROM_SPI , 0, &cmd, 1, EEPROM_SPEED);
GPIO_WriteBit(EEPROM_PORT , EEPROM_CS , 1);
}
Наиболее сложными для реализации инструкциями являются чтение и запись — обе могут читать или записывать несколько байтов. Важно отметить, что эти операции должны вести себя по-разному для EEPROM различного размера. Для больших EEPROM, таких как 25LC160, адрес передается в виде пары последовательных байтов, в то время как для 25AA040 старший значащий бит адреса кодируется командой. Здесь мы предполагаем 16-битный адрес. Операция чтения может получить доступ к любому количеству байтов, включая всю EEPROM. Операция начинается с кода инструкции, двух байтов адреса (сначала старший байт!), а затем 1 или более операций чтения. Передача адреса удобнее всего воспринимается как 16-битная передача — если она выполняется с двумя 8-битными передачами, для адреса нужно будет поменять местами байты.
Операция записи более сложна из-за архитектуры EEPROM. Массив памяти организован в «страницы», и можно записать любой или все байты на странице за одну операцию (гораздо эффективнее записать целые страницы одновременно). Размер страницы зависит от устройства — 25LC160 имеет 16-байтовые страницы, в то время как большие могут иметь 64-байтовые страницы. Невозможно записать байты на более чем одной странице за одну операцию. На практике любые байты, записанные за пределами страницы, выбранной адресными битами A [16: 5], будут перенесены на страницу. Таким образом, код записи EEPROM должен проверять границы страниц. Самая простая реализация — вернуть количество записанных байтов и ограничить эти байты одной страницей. Некоторые устройства также ограничивают чтение до границ страницы — важно прочитать таблицу данных для устройства, которое вы планируете использовать!
EEPROM Module:
void eepromInit(); void eepromWriteEnable(); void eepromWriteDisable(); uint8_t eepromReadStatus(); void eepromWriteStatus(uint8_t status); int eepromWrite(uint8_t *buf, uint8_t cnt, uint16_t offset); int eepromRead(uint8_t *buf, uint8_t cnt, uint16_t offset);
Подключим нашу микросхему EEPROM по SPI к следующим пинам:
| EEPROM Pin | EEPROM Signal | STM32 Pin |
| 1 | CS | A4 |
| 2 | Q (MOSI) | A6 |
| 3 | W (WP) | 3.3V |
| 4 | VSS (GND) | GND |
| 5 | D (MISO) | A7 |
| 6 | C (SCK) | A5 |
| 7 | HOLD | 3.3V |
| 8 | Vcc | 3.3V |
Пример кода №1:
#include <stm32f10x.h>
#include <stdint.h>
#include <stddef.h>
#include "gpio.h"
#include "stm32f10x_sdio.h"
#include "stdint.h"
#include "bits/bits.h"
#include "defs/defs.h"
#define EEP_SPI SPI1
#define EEP_CS_GPIO GPIOA
#define EEP_CS_PIN GPIO_PIN_4
//Инструкции по работе с EEPROM
typedef enum _EepCmd {
WREN = 0b00000110,
WRDI = 0b00000100,
RDSR = 0b00000101,
WRSR = 0b00000001,
READ = 0b00000011,
WRITE = 0b00000010,
RDID = 0b10000011,
WRID = 0b10000010,
RDLS = 0b10000011,
LID = 0b10000010
} eep_cmd_t;
#define EEP_STATUS_WIP 0x1 //занятость процессом записи
static uint16_t spi_transmit(SPI_TypeDef* spi, uint16_t data)
{
while(!(spi->SR & SPI_SR_TXE));
spi->DR = data;
while(!(spi->SR & SPI_SR_RXNE));
return spi->DR;
}
static void spi_write(SPI_TypeDef* spi, const uint8_t* data, size_t size)
{
size_t i;
for(i = 0; i < size; i ++){
spi_transmit(spi, data[i]);
}
}
static void spi_read(SPI_TypeDef* spi, uint8_t* data, size_t size)
{
size_t i;
for(i = 0; i < size; i ++){
data[i] = spi_transmit(spi, 0xffff);
}
}
static uint8_t eep_read_status(SPI_TypeDef* spi)
{
gpio_reset(EEP_CS_GPIO, EEP_CS_PIN);
spi_transmit(spi, RDSR);
uint8_t status = spi_transmit(spi, 0xffff);
gpio_set(EEP_CS_GPIO, EEP_CS_PIN);
return status;
}
static void eep_write_enable(SPI_TypeDef* spi)
{
gpio_reset(EEP_CS_GPIO, EEP_CS_PIN);
spi_transmit(spi, WREN);
gpio_set(EEP_CS_GPIO, EEP_CS_PIN);
}
static void eep_wait_write(SPI_TypeDef* spi)
{
uint8_t status = 0;
do {
status = eep_read_status(spi);
} while(status & EEP_STATUS_WIP);
}
static void eep_read(SPI_TypeDef* spi, uint16_t address, uint8_t* data, size_t size)
{
gpio_reset(EEP_CS_GPIO, EEP_CS_PIN);
spi_transmit(spi, READ);
spi_transmit(spi, address >> 8);
spi_transmit(spi, address & 0xff);
spi_read(spi, data, size);
gpio_set(EEP_CS_GPIO, EEP_CS_PIN);
}
static void eep_write(SPI_TypeDef* spi, uint16_t address, const uint8_t* data, size_t size)
{
eep_write_enable(spi);
gpio_reset(EEP_CS_GPIO, EEP_CS_PIN);
spi_transmit(spi, WRITE);
spi_transmit(spi, address >> 8);
spi_transmit(spi, address & 0xff);
spi_write(spi, data, size);
gpio_set(EEP_CS_GPIO, EEP_CS_PIN);
eep_wait_write(spi);
}
static void init_spi_gpio(void)
{
//CS.
gpio_init(EEP_CS_GPIO, EEP_CS_PIN, GPIO_MODE_OUT_50MHz, GPIO_CONF_OUT_GP_PP);
gpio_set(EEP_CS_GPIO, EEP_CS_PIN);
// SCK.
gpio_init(GPIOA, GPIO_PIN_5, GPIO_MODE_OUT_50MHz, GPIO_CONF_OUT_AF_PP);
// MISO.
gpio_init(GPIOA, GPIO_PIN_6, GPIO_MODE_IN, GPIO_CONF_IN_FLOATING);
// MOSI.
gpio_init(GPIOA, GPIO_PIN_7, GPIO_MODE_OUT_50MHz, GPIO_CONF_OUT_AF_PP);
}
static void init_spi(void)
{
init_spi_gpio();
EEP_SPI->CR1 = SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_SPE | SPI_CR1_MSTR | SPI_CR1_BR_1;
}
static void init_rcc(void)
{
// AFIO.
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
// GPIOA.
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// SPI1.
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
}
int main(void)
{
SystemInit();
init_rcc();
init_spi();
uint8_t readed_data[8] = {0};
eep_write(EEP_SPI, 0x0, (uint8_t*)"12345678", 8);
eep_read(EEP_SPI, 0x0, readed_data, 8);
if(readed_data[0] != '1') {
while(1);
}
for(;;){}
return 0;
}
Файлы проекта:
Пример кода №2:
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_spi.h"
#define CS_ON() GPIO_ResetBits(GPIOA, GPIO_Pin_3)
#define CS_OFF() GPIO_SetBits(GPIOA, GPIO_Pin_3)
uint8_t SPIData = 0;
//Команды работы с EEPROM
enum eepromCMD
{ cmdREAD = 0x03, cmdWRITE = 0x02,
cmdWREN = 0x06, cmdWRDI = 0x04,
cmdRDSR = 0x05, cmdWRSR = 0x01 };
int main(void) {
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
// Тактирование модуля SPI1 и порта А
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
/**SPI1 GPIO Configuration
PA3 ------> SPI1_CS
PA5 ------> SPI1_SCK
PA6 ------> SPI1_MISO
PA7 ------> SPI1_MOSI
*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
CS_OFF();//Выставили CS в 1
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13|GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//Заполняем структуру с параметрами SPI модуля
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //полный дуплекс
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // передаем по 8 бит
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // Полярность и
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // фаза тактового сигнала
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // Управлять состоянием сигнала NSS программно
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; // Предделитель SCK
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // Первым отправляется старший бит
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // Режим - мастер
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI2, &SPI_InitStructure); //Настраиваем SPI1
SPI_Cmd(SPI2, ENABLE); // Включаем модуль SPI1....
// Поскольку сигнал NSS контролируется программно, установим его в единицу
// Если сбросить его в ноль, то наш SPI модуль подумает, что
// у нас мультимастерная топология и его лишили полномочий мастера.
SPI_NSSInternalSoftwareConfig(SPI2, SPI_NSSInternalSoft_Set);
while(1) {
//SPI_I2S_SendData(SPI1, 0x05); //Передаем байт 0x93 через SPI1
// while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET); //Передатчик занят? значит ничего не делаем
CS_ON();
SPI_I2S_SendData(SPI2, cmdRDSR);//Запрашиваем...
while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) == SET){}
SPIData = SPI_I2S_ReceiveData(SPI2);
while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) == SET){}
SPI_I2S_SendData(SPI2, 0x01);//Запрашиваем...
while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY) == SET){}
SPIData = SPI_I2S_ReceiveData(SPI2);
CS_OFF();
}
}






