понедельник, 30 сентября 2019 г.

STM32. Maple Mini. I2C. Подключаем внешнюю EEPROM 24C08.


Маленькая EEPROM платка.

Сегодня попробуем подключить внешнюю EEPROM к Maple Mini.
Когда-то давно для экспериментов с AVR были куплены несколько микросхем 24C08WP EEPROM памяти. Maple Mini содержит два независимых I2C интерфейса. В своей задаче мы будем использовать первый I2C.
Цель будет следующей. Запишем во внешнюю память любое число от 1 до 10. Затем считаем его обратно и мигнем встроенным светодиодом тем количеством, которое было получено из памяти. Прерывания I2C интерфейса использовать не будем.

Что нам потребуется.

Во-первых почитать вики про I2C и общие принципы работы. Во-вторых, скачать StdPeriph_Lib (или скачайте с оф. сайта), где есть библиотека-драйвер для I2C с подробнейшими комментариями. В-третьих, не забыть подключить резисторы на SDA, SCL линии. Если на линиях они уже есть, дополнительно подключать не стоит - при параллельном соединении сопротивление уменьшается.
Желательно использовать 24C08WP с рабочим напряжением от 2.5 до 5.5 вольт или найти похожую, которая будет поддерживать рабочее напряжение МК 3.3 вольта. Для того, чтобы сразу на прямую подключать к МК.
Для I2C1 будем использовать 15 - SDA и 16 - SCL выводы Maple Mini. На плате они подписаны I2C1.
Выглядит все примерно так...

Как записать/прочитать данные из внешней EEPROM.

Адрес памяти в I2C шине зависит от вывода E2.
Этот вывод влияет на значение бита №3 (счет начинается с 0) в байте адреса микросхемы. Если подключен к земле, тогда бит будет сброшен, если к Vcc - установлен. Вывод E2 подключил к земле, поэтому байт адреса будет равен 0xA0.
Адрес микросхемы состоит из 7 битов. Последний 8-ой бит - это бит чтения/записи (R/W).

Нас интересует запись и чтение только одного байта. В мануале все прекрасно расписано.
Для записи нужно послать следующую последовательность байтов и битов.
Dev select - адрес памяти в шине и равен A0.
R/W  - бит сброшен, что означает запись.
Byte address - адрес в памяти, где будет записан байт.
Data in - записываемый байт.
ACK - байт подтверждения приема байта - его отправляет микросхема памяти.

Для чтения нужно отправить адрес считываемого байта, сгенерировать повторный СТАРТ и принять байт.

Dev select - адрес памяти в шине и равен A0.
R/W - бит сброшен, что означает запись.
R/W - бит установлен, что означает чтение (прием данных).
Byte address - адрес в памяти, который будет считан.
Data out - считываемый байт.
ACK - байт подтверждения приема байта - его отправляет микросхема памяти, после повторного СТАРТ (второй start), его отправляет МК. После последнего принятого байта, чтобы закончить передачу, его не нужно отсылать.

MSB и LSB.

Старший и младший значащий бит соответственно - это по сути название первого и последнего бита. Например, один байт состоит из 8 битов. Если эти 8 битов записать на бумажке - 10101010, то старшим значащим битом будет самая левая единичка, а младшим - самый правый нолик. В контексте I2C, MSB - старший значащий бит который будет передан первый, а LSB - младший значащий  бит который будет передан последним.

Режимы I2C.

Бывает 4 основных режима (modes): slave receiver - ведомый принимает данные; slave transmitter - ведомый шлет данные; master receiver - ведущий принимает данные; master transmitter - ведущий шлет данные.

По умолчанию активирован slave mode. I2C интерфейс автоматически переходит из slave в master mode, когда МК генерирует START состояние. Так же режим автоматически меняется из master в slave mode, когда теряется арбитраж или после активации STOP состояния. Это позволяет работать нескольким master'ам на одной линии.

Я сэкономил бы огромное количество времени, если бы знал что разработку следует начинать не только с datasheet, но и вместе с errata. Errata - такой документ, написанный производителем, в котором описаны известные аппаратные баги микроконтроллера и их решение.

Переназначение выводов I2C1.

За переназначение выходов I2C1 отвечает AFIO->MAPR бит. Если он сброшен тогда переназначения не будет и будут использоваться PB6 - (16 вывод платы) и PB7 (15) выводы. Если бит установлен, тогда будут использоваться PB8 и PB9 выводы. На плате Maple Mini выводы PB8 и PB9, не выведены, но вывод PB8 используется кнопкой, поэтому переназначать, без переделок, нет смысла.
Переназначение выводов I2C2 в МК не предусмотрено.

Включаем I2C интерфейс.

После того как подключили внешнюю EEPROM, переходим к написанию программы. Для настройки I2C я использовал StdPeriph библиотеку. Алгоритм следующий:
  1. Включить тактирование для необходимых интерфейсов и настроить GPIO:
    1. Включить тактирование для AFIO;
    2. Включить тактирование для I2C интерфейса;
    3. Настроить remapping - какие линии внешних портов будут использоваться для SDA/SCL;
    4. Включить тактирование для выбранного GPIO, где будут SDA/SCL линии;
    5. Настроить выбранные в 3 шаге GPIO линии как альтернативный выход с открытым стоком и максимальной выходной частотой в 50 МГц;
  2. Настроить работу I2C интерфейса (StdPeriph).
    1. Настроить частоту SCL линии;
    2. Включить режим чистого I2C, без SMBus;
    3. Рассчитать и установить скважность.
    4. Заполнить адрес МК.
    5. Активировать подтверждение ACK битом. Отсылает ACK бит после каждого успешно принятого байта.
    6. Настроить длину адреса = 7 бит на шине I2C;
Напишем функцию которая подготовит I2C к работе.

void I2CMainConfig(uint16_t clockSpeed) {
        I2C_InitTypeDef I2CInitConfig;

        /* --== Настраиваем выходы I2C интерфейса: ==-- */
        //включаем тактирование для AFIO
        RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;

        //включаем тактирование для I2C1. Используем APB1
        RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;

        //настроим AFIO не переназначать выходы I2C1 интерфейса.
        //PB6 = SCL, PB7 = SDA
        AFIO->MAPR &=~ AFIO_MAPR_I2C1_REMAP;

        //включаем тактирование для GPIO B
        RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

        //устанавливаем PB6 и PB7 as Alternate function open drain
        GPIOB->CRL |= GPIO_CRL_CNF6_0;
        GPIOB->CRL |= GPIO_CRL_CNF6_1;
        GPIOB->CRL |= GPIO_CRL_CNF7_0;
        GPIOB->CRL |= GPIO_CRL_CNF7_1;

        //устанавливаем макс. частоту = 50 MHz
        GPIOB->CRL |= GPIO_CRL_MODE6_1;
        GPIOB->CRL |= GPIO_CRL_MODE6_0;
        GPIOB->CRL |= GPIO_CRL_MODE7_1;
        GPIOB->CRL |= GPIO_CRL_MODE7_0;

        /* --== Настраиваем I2C интерфейс: ==-- */
        I2C_DeInit(I2C1);
        I2CInitConfig.I2C_ClockSpeed      = clockSpeed * 1000;
        I2CInitConfig.I2C_Mode                = I2C_Mode_I2C;
        I2CInitConfig.I2C_DutyCycle        = I2C_DutyCycle_16_9;
        I2CInitConfig.I2C_OwnAddress1  = 10;
        I2CInitConfig.I2C_Ack                  = I2C_Ack_Enable;
        I2CInitConfig.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

        I2C_Init(I2C1, &I2CInitConfig);
}

I2C1 - это библиотечная структура всех регистров первого I2C интерфейса. Используй эту структуру всегда, когда хочешь как-либо взаимодействовать с интерфейсом.
Для настройки I2C интерфейса используется структура I2CInitConfig.
I2C_Init() - настраивает I2C1 интерфейс используя значения I2CInitConfig структуры.

Рассчитываем значения регистров.

Если не использовать StdPeriph библиотеку, то необходимо понять как рассчитываются значения некоторых регистров.
Частота APB1 используется для тактирования I2C. Она настраивается битами RCC_CFGR->PPRE1, которые содержат значение делителя системной частоты (72 МГц). Обычно после SystemInit(); частота APB1 равна 36 МГц и не должна превышать это значение. Она обозначается как FPCLK1. Значение периода обозначают как TPCLK1 = 1 / FPCLK1.

Значение CR2->FREQ в регистре указывается в МГц. Это значит, что его значение выставляют равным значению FPCLK1 / 1000 000.

Есть два стандарта скорости I2C: Standard Mode = 100 кГц и Fast Mode = 400 кГц в SCL линии.

I2C_CCR->DUTY битом выставляется скважность сигнала для Fast Mode. Она может быть либо 2 к 1 либо 16 к 9. Значение скважности для Standard Mode равен 1 к 1. Этот параметр настраивает соотношение низкого TLOW и высокого уровня THIGHT сигнала в SCL линии.
T - период, t1 - высокий уровень, t2 - низкий уровень.
При выборе значения скважности следует учитывать два критерия. Первый - нужно посмотреть минимальные значения скважности у подключаемого устройства. Второй - значение скважности делит частоту FPCLK1 для максимальной частоты SCL. Если выбран режим 2 к 1, то 2 + 1 = 3, значит FSCL частота будет в 3 раза меньше FPCLK1. Точно также и для 16 к 9 = 25 означает что FSCL будет в 25 раз меньше FPCLK1. Как я понимаю, скважность нужна для реализации Fast Mode plus, у которой FSCL равна 1 МГц. В наших экспериментах значение скважности не критично. Выбранное значение скважности нужно учитывать при расчете значения I2C_CCR->CCR и при расчете максимального времени нарастания высокого уровня I2C_TRISE->TRISE.

В Standard Mode, значение CCR = FPCLK1 / 2 * FSCL.

TLOW +  THIGHT = CCR * TPCLK1 +  CCR * TPCLK1;
1 / FSCL = 2 * CCR * TPCLK1;
1 / FSCL = 2 * CCR / FPCLK1;
CCR = FPCLK1 / 2 * FSCL;

В Fast Mode, если скважность 2 к 1, то CCR = FPCLK1 / 3 * FSCL.
Если скважность 16 к 9, тогда CCR = FPCLK1 / 25 * FSCL.

Значение TRISE для Standard Mode рассчитывается: (1000 ns / TPCLK1) + 1.
Для Fast Mode: (300 ns / TPCLK1) + 1.

Fast Mode включается установкой бита I2C_CCR->F/W.

Способы взаимодействия с I2C интерфейсом.

Когда отправлен/получен байт данных/адреса или произошла ошибка или изменилось состояние линии, то все это отображается в I2C->SR1 и I2C->SR2 регистрах. Они называются регистры статуса. Помимо отображения в регистрах статуса, будут вызваны события/прерывания, если установлены ITBUFEN, ITEVTEN, ITERREN биты. Любое такое изменение называется событие.

В StdPeriph библиотеке предлагают 3 способа взаимодействия с I2C.
Первый способ - Basic state monitoring. В библиотеке прописаны константы событий (см. секцию I2C_Events в stm32f10x_i2c.h). И разработчик предлагает использовать I2C_CheckEvent() функцию, которая сравнивает регистры статуса с ожидаемым событием, которое указывается в параметрах функции. В случае, если ожидаемое событие совпало, то она вернет 1. Этот способ лучше всего подходит для ознакомления. Будем его использовать.

Второй способ - Advanced state monitoring. Необходимо использовать I2C_GetLastEvent() функцию, которая просто возвращает конкатенацию регистров статуса. Удобно тем, что можно самостоятельно сравнивать отдельные биты статуса или сравнивать со своими константами событий. Этот способ более гибок чем первый.

Третий способ - Flag-based state monitoring. Используется I2C_GetFlagStatus() функция, которая возвращает текущий статус флаг, а не событие.

Первый способ будем реализовывать следующим образом: даем интерфейсу задачу и ждем событие, которое подтвердит выполнение поставленной задачи.

Отсылаем данные.

Напишем функцию, которая будет отсылать адрес и данные используя Basic state monitoring способ.

Алгоритм следующий:
  1. Генерируем СТАРТ состояние на шине. Ждем выполнения.
  2. Отправляем Адрес slave устройства и указываем что будем отправлять данные. Ждем выполнения.
  3. Отправляем байты данных. Ждем выполнения.
  4. Генерируем СТОП или СТАРТ, если необходимо сгенерировать повторный СТАРТ.

void sendViaI2cNI(uint8_t address, uint8_t *requestData, uint8_t requestLength, uint8_t callReStartNotStopFlag) {
        //генерируем СТАРТ состояние
        I2C_GenerateSTART(I2C1, ENABLE);
        //ждем когда СТАРТ состояние будет сгенерировано
        while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
        //отправляем адрес slave устройства
        I2C_Send7bitAddress(I2C1, address, I2C_Direction_Transmitter);
        //ждем когда адрес будет отправлен
        while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
        //отправляем данные
        for (uint16_t i = 0; i < requestLength; i++) {
                I2C_SendData(I2C1, requestData[i]);
                //ждем когда байт данных будет отправлен
                while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
        }

        if (callReStartNotStopFlag) {
                I2C_GenerateSTART(I2C1, ENABLE);
        } else {
                I2C_GenerateSTOP(I2C1, ENABLE);
        }
}

Параметры функции: address - адрес slave устройства; requestData - массив отправляемых байтов; requestLength - длина массива requestData; callReStartNotStopFlag - если 1, тогда в конце генерируем пов. СТАРТ вместо СТОП.

Принимаем данные.

Теперь напишем функцию принимающую данные. В начале следует отправить запрос (например, запрос с адресом считываемого байта во внешней памяти), генерируем пов. СТАРТ и только потом принимаем данные. Алгоритм:
  1. Включаем подтверждение принятых байтов. Отправляем запрос на запрашиваемые данные используя предыдущую функцию. В конце отправляем повторный СТАРТ. Ждем выполнения.
  2. Отправляем адрес и указываем что будем принимать данные. Ждем выполнения.
  3. Принимаем байты данных. Ждем выполнения.
  4. Отменяем подтверждение приема последнего принятого байта с помощью ACK бита.
  5. И принимаем последний байт.
  6. Генерируем СТОП состояние.

void receiveViaI2cNI(uint8_t address, uint8_t *requestData, uint8_t requestLength,  uint8_t *responseData, uint8_t responseLength) {
        //возобновляем подтверждение ACK
        I2C_AcknowledgeConfig(I2C1, ENABLE);
        //Отправляем запрос
        sendViaI2cNI(address, requestData, requestLength, 1);

        //Ждем выполнения, когда будет сгенерирован повторный СТАРТ
        while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
        //Отправляем адрес и указываем что будем принимать данные
        I2C_Send7bitAddress(I2C1, address, I2C_Direction_Receiver);
        while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

        //Принимаем побайтово данные
        for (uint16_t i = 0; i < responseLength - 1; i++) {
                //Сначала ждем когда будет принят байт
                while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED));
                //Считываем его
                responseData[i] = I2C_ReceiveData(I2C1);
        }
        //Отключаем подтверждение ACK для последнего байта
        I2C_AcknowledgeConfig(I2C1, DISABLE);
        while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED));
        responseData[responseLength - 1] = I2C_ReceiveData(I2C1);

        //Генерируем СТОП
        I2C_GenerateSTOP(I2C1, ENABLE);
}

Параметры функции: address - адрес slave устройства; requestData - массив отправляемых байтов; requestLength - длина массива requestData, responseData - массив принятых байтов; responseLength - длина массива responseData.

Эксперимент №1.

Когда у нас есть написанные функции инициализации и приема/передачи данных, нужно их опробовать.
Алгоритм:
  1. Инициализация.
  2. Мигнем светодиодом один раз. Это будет означать что инициализация прошла успешно.
  3. Записываем число во внешнюю память.
  4. Ждем 5 мс.
  5. Считываем число из внешней памяти.
  6. Мигаем встроенным светодиодом тем количеством раз, которое было получено из памяти. 

#include <stdio.h>
#include <stm32f10x.h>

//tact in one ms
uint32_t tactsInOneMs = 1;

//delay in ms
void delay(uint16_t waitInMs) {
  uint32_t countTacts = waitInMs * tactsInOneMs;

  for(; countTacts != 0; countTacts--) {

  }
}

/*
        ...

        Здесь должны быть функции I2CMainConfig, sendViaI2cNI, receiveViaI2cNI...

        ...
*/


int main(void) {
        uint8_t requestData[32], requestByteAddress[1], responseData[32];
        uint8_t receivedByte = 0;

        SystemInit();

        tactsInOneMs = SystemCoreClock / 10000;

        I2CMainConfig(100);//скорость 100 кГц

        //настраиваем LED линию (PB1)
        GPIOB->CRL &=~(GPIO_CRL_CNF1_0);
        GPIOB->CRL &=~(GPIO_CRL_CNF1_1);
        GPIOB->CRL |= (GPIO_CRL_MODE1_1);
        GPIOB->CRL |= (GPIO_CRL_MODE1_0);

        //мигнем один раз, как конец инициализации
        GPIOB->ODR |= GPIO_ODR_ODR1;
        delay(1000);
        GPIOB->ODR &=~ GPIO_ODR_ODR1;
        delay(1000);

        //готовим запрос для записи байта
        requestData[0] = 0x01;//адрес байта в памяти
        requestData[1] = 0x06;//значение байта
        //записываем байт во внешнюю память
        sendViaI2cNI(0xA0, requestData, 2, 0);

        //ждем 5 мс, чтобы внешняя память успела записать новое значение
        delay(5);

        //считываем байт из памяти
        requestByteAddress [0] = 0x01;// адрес считываемого байт в памяти
        receiveViaI2cNI(0xA0, requestByteAddress, 1, responseData, 1);

        receivedByte = responseData[0];
        //мигаем встроенным светодиодом
        for(uint8_t i = 0; i < receivedByte; i++){
                GPIOB->ODR |= GPIO_ODR_ODR1;
                delay(500);
                GPIOB->ODR &=~ GPIO_ODR_ODR1;
                delay(500);
        }

        while (1) {
        }
}

#ifdef  USE_FULL_ASSERT
void assert_failed(uint8_t* file, uint32_t line) {

}
#endif  

Если определить макрос USE_FULL_ASSERT в stm32f10x_conf.h (по умолчанию не определен), то необходимо реализовать функцию assert_failed(). Она вызывается когда макрос assert_param возвращает ошибку. В параметрах этой функции передается имя файла и строка в которой произошла ошибка. Если макрос не определен, тогда assert_param возвращает 0 в случае неудачи. По умолчанию он не определен, поэтому функцию можно не реализовывать, но для учебной цели о ней стоило упомянуть.
Макрос assert_param(expr) используется для проверки параметров функций StdPeriph библиотеки, где expr - это выражение проверки параметра.

Заливаем прошивку. Светодиод мигнет один долгий раз после инициализации, а потом мигнет 6 "быстрых" раз. Нажимаем reset на плате и повторно убеждаемся что все работает.

Комментариев нет:

Отправить комментарий