Как управлять длиной чтения переменных I2C, требующей увеличения адреса (эмуляция IC Wire/I2C/EEPROM)

Вопрос заключается в том, как получить информацию/управление чтением переменной длины для Arduino, действующего как ведомое устройство.

Конкретный контекст таков: я хочу эмулировать микросхему EEPROM. Запись байта по адресу EEPROM устанавливает внутренний адрес EEPROM, с которого начинается чтение. Последующее чтение по адресу EEPROM начнет возвращать байты с этого внутреннего адреса, который увеличивается при каждом чтении байта.

"Wire.onRequest(requestEvent);" позволяет нам настроить обработчик, но, похоже, он вызывается только один раз в начале чтения.

В обработчике можно предоставить ответ из нескольких байтов. Однако я не нашел документации, позволяющей узнать, сколько байт было эффективно отправлено мастеру.

Исходный код я получил на форуме Arduino . Я адаптировал его под это:

#include <Wire.h>
//#определить отладку
byte XEEPROM[256] = {
  0x5D, 0x26, 0x31, 0x91, 0x3e, 0x12, 0x8e, 0x70, 0xa5, 0x57, 0x3d, 0xed, 0x91, 0x99, 0xb4, 0x11,
  0x38, 0xde, 0x1b, 0xd6, 0x4c, 0xd9, 0xf5, 0x13, 0x23, 0x94, 0xb8, 0x3d, 0x9c, 0xb0, 0xc4, 0x54,
  0xf0, 0x69, 0xca, 0xb3, 0x28, 0x84, 0xa5, 0xc1, 0x80, 0x53, 0x69, 0x0c, 0x38, 0x4f, 0x0c, 0x74,
  0xa1, 0x5b, 0x8c, 0x71, 0x34, 0x30, 0x38, 0x30, 0x30, 0x38, 0x31, 0x35, 0x33, 0x31, 0x30, 0x35,
  0x00, 0x12, 0x5a, 0x1e, 0xbd, 0x9e, 0x00, 0x00, 0x02, 0x82, 0x30, 0xaa, 0x10, 0xb7, 0x7f, 0x8d,
  0x0a, 0xcb, 0x2e, 0xc1, 0xeb, 0x52, 0x20, 0xdc, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x97, 0x55, 0x57, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x47, 0x4d, 0x54, 0x00, 0x42, 0x53, 0x54, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x05, 0x00, 0x02, 0x03, 0x05, 0x00, 0x01,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc4, 0xff, 0xff, 0xff,
  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
  0x68, 0xaa, 0xdd, 0x02, 0x46, 0x45, 0x8f, 0x8f, 0x78, 0x56, 0xbb, 0x69, 0x01, 0x04, 0x46, 0x45,
  0x8f, 0x8f, 0x78, 0x56, 0xbb, 0x69, 0x01, 0x04, 0x46, 0x45, 0x8f, 0x8f, 0x78, 0x56, 0xbb, 0x69,
  0x12, 0x03, 0x46, 0x45, 0x8f, 0x8f, 0x78, 0x56, 0xbb, 0x69, 0x02, 0x02, 0x46, 0x45, 0x8f, 0x8f,
  0x78, 0x56, 0xbb, 0x69, 0x12, 0x02, 0x84, 0x79, 0x80, 0x00, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00
};
static volatile byte rQ;

void setup()
{
 Wire.begin(0x50);
 Wire.onReceive(receiveEvent);
 Wire.onRequest(requestEvent);
 Serial.begin(115200);
 Serial.println("I2C EEPROM EMULATION\n");
}

void loop()
{
}

void requestEvent(){ 
  //Wire.write(XEEPROM[rQ]); // Только 1
  Wire.write(XEEPROM+rQ,6); /// Более 1
  #ifdef DEBUG
  Serial.print("ADR:");
  Serial.print(rQ,HEX);
  Serial.print("R:");
  Serial.print(XEEPROM[rQ],HEX);  // печатаем символ
  Serial.println();
  #endif
  rQ++; // Приращение должно зависеть от количества операций чтения — не знаю, как это определить.
}

void receiveEvent(int iData){
    rQ = Wire.read();
    Serial.print("ADR:");
    Serial.print(rQ,HEX);
    bool dwHeader=false;
    // перебираем все, кроме последнего
    while(1 < Wire.available()) 
    {
      if(!dwHeader) {
          dwHeader=true;
          Serial.print("DATAW:");
      }
      XEEPROM[rQ]=Wire.read();;
      Serial.print(XEEPROM[rQ],HEX);  // печатаем символ
    }
    Serial.println(); // новая линия
}

Вопрос касается requestEvent, а именно:

void requestEvent(){ 
  //Wire.write(XEEPROM[rQ]); // Только 1
  Wire.write(XEEPROM+rQ,6); /// Более 1
  rQ++; // Приращение должно зависеть от количества операций чтения - не знаю, как это сделать
}

Я хотел бы отправлять по одному или, по крайней мере, иметь возможность увеличивать rQ в соответствии с количеством эффективно отправленных байтов.

Если вы хотите просмотреть техническое описание, вы можете просмотреть техническое описание AT24C02. Что касается увеличения внутреннего адреса EEPROM, спецификация EEPROM гласит:

ЧТЕНИЕ ТЕКУЩЕГО АДРЕСА: внутренний счетчик адреса слова данных поддерживает последний адрес, к которому обращались во время последней операции чтения или записи, увеличено на единицу. Этот адрес остается действительным между операциями, поскольку пока сохраняется мощность чипа. Адрес «переворачивается» во время чтение происходит от последнего байта последней страницы памяти до первого байта первой страницы. Адрес, «переворачивающийся» во время записи, взят из последнего байта текущей страницы до первого байта той же страницы.

Вот почему код Arduino, эмулирующий EEPROM, должен точно знать, сколько байт было прочитано.

Спасибо.

, 👍0

Обсуждение

Пожалуйста, добавьте больше информации к вашему вопросу. Является ли главное устройство также платой Arduino? Если нет, то у вас проблемы, потому что Arduino в качестве подчиненного устройства — это не то же самое, что EEPROM. Какую плату Arduino вы используете в качестве ведомого и ведущего устройства? «Цикл по всем, кроме последнего» — это ненормально, это из примера, но это плохой пример. Если вы используете Arduino Unos, то размер буфера i2c равен 32, и вы можете выполнить Wire.write с 32 байтами. При использовании библиотеки Arduino ведомое устройство не знает, сколько байтов будет прочитано или прочитано ведущим устройством., @Jot

Мастером также является Arduino для теста, но обычно это не так, и это не имеет значения. * Я согласен, что есть и другие ошибки, я просто хотел быстро начать * Я использую Arduino Nanos * Arduino должен действовать как EEPROM — это позволяет избежать его использования для теста, но я удивлен, что нет способа узнать это. сколько байт отправляется в конце транзакции I2C * Я только что посмотрел Wire.cpp и twi.c * Единственный способ узнать, сколько байт отправлено, - это полностью заполнить буфер, а затем заполнять его постоянно ( в цикле) с помощью twi_transmit() и посчитайте единицу, когда она вернет 0., @le_top

Очень важно, является ли мастер ардуино или нет. Arduino в качестве ведомого устройства использует растяжение тактового импульса, а EEPROM — нет. Поэтому невозможно действовать так же, как EEPROM. Можете ли вы найти пример Arduino, который точно таким же образом имитирует EEPROM? Это невозможно. Для программного обеспечения можно изменить библиотеку или использовать библиотеку, генерирующую прерывания для каждого отдельного байта (название этой библиотеки я забыл). Мастер часто записывает адрес регистра перед чтением данных, тогда не проблема заполнить буфер на 32 байта., @Jot

Ок, я понимаю, что идеального быть не может. EEPROM не выполняет растяжение тактовой частоты, поскольку он достаточно быстр на максимальной частоте I2C. Arduino, эмулирующая EEPROM, может быть функционально эквивалентной, и этого достаточно., @le_top


2 ответа


0

Это именно то, на что я наткнулся, пытаясь создать подчиненное устройство I2C с интерфейсом на основе памяти.

Вы правы, обратный вызов requestEvent вызывается только один раз. Поэтому вам придется подготовить его заранее. Однако положительным моментом является то, что функция write отбросит все неиспользуемые байты.

Итак, что вы можете сделать? Ну, во-первых, подведение итогов «сообщений» I2C. Вы, как мастер, можете

  • Отправить команду записи с полезной нагрузкой
  • Отправьте команду чтения без полезной нагрузки, и ведомое устройство будет отвечать байтами, пока вы продолжаете отправлять ACK.

На мой взгляд, существовало два разных подхода:

  • Команда «запись» должна содержать начальный регистр и количество регистров для чтения; однако этот подход усложняет различие между командами «чтение из EEPROM» и «запись в EEPROM».
  • Команда записи должна отправлять только начальный регистр для чтения из EEPROM и начальный регистр + содержимое для записи в EEPROM.

В своем проекте я выбрал второй подход. По этой причине, когда мастер что-то записывает по шине I2C, первым байтом полезной нагрузки является номер регистра, а затем, если других данных нет, слейв готовится к отправке содержимого регистра и последующих, а если есть какие-либо данные, это рассматривается как команда записи.

Единственная проблема с этим подходом заключается в том, что ведомое устройство должно подготовить буфер, а размер буфера ограничивает максимальное количество возвращаемых регистров. В своем коде я установил это ограничение равным 8; если мастер запрашивает более 0, возвращаются только 0.

Вот библиотека, которую я написал для этого проекта.

Файл I2CInterface.h:

#ifndef I2CINTERFACE_H
#define I2CINTERFACE_H

#include <stdint.h>

class I2CInterface
{
  public:
    static I2CInterface instance;

  public:
    void start();

  private:
    I2CInterface();

    static void requestEvent();
    static void receiveEvent(int howMany);
    void resetSendingBuffer();

  private:
    static const uint8_t Address = 0x46;

    static const uint8_t MaxBufferLength = 8; // (макс.) 8 байт
    uint8_t sendingBuffer[MaxBufferLength];
};

#endif // I2CINTERFACE_H

Файл I2CInterface.cpp

#include <Arduino.h>
#include <Wire.h>
#include "I2CInterface.h"

I2CInterface I2CInterface::instance;

I2CInterface::I2CInterface()
{

}

void I2CInterface::start()
{
  Wire.begin(Address);
  Wire.onRequest(requestEvent);
  Wire.onReceive(receiveEvent);
  resetSendingBuffer();
}

void I2CInterface::requestEvent()
{
  Wire.write(instance.sendingBuffer, MaxBufferLength);
  instance.resetSendingBuffer();
}

void I2CInterface::receiveEvent(int howMany)
{
  if (howMany <= 0)
    return;
  else if (howMany == 1)
  { // Запрос на чтение
    uint8_t regist = Wire.read();
    for (uint8_t i = 0; i < MaxBufferLength; i++)
      instance.sendingBuffer[i] = Configuration.getRegister(regist++);
  }
  else
  { // Запись запроса
    uint8_t regist = Wire.read();
    while (Wire.available())
    {
      Configuration.setRegister(regist++, Wire.read());
    }
    instance.resetSendingBuffer();
  }
}

void I2CInterface::resetSendingBuffer()
{
  for (uint8_t i = 0; i < MaxBufferLength; i++)
    sendingBuffer[i] = 0x00;
}

Обратите внимание, что Configuration.getRegister и Configuration.setRegister — это функции, которые я использовал для получения и установки некоторых регистров; вы можете заменить их прямым доступом к EEPROM или собственными функциями.

Вам просто нужно добавить в основной файл

#include "I2CInterface.h"
...

void setup()
{
  ...
  I2CInterface::instance.start();
  ...
}
,

Спасибо за ваш ответ. Примерно это я делаю с «Wire.write(XEEPROM+rQ,6);», где 6 в настоящее время увеличено до 16, и это все, что мне нужно в моей настройке. * Однако счетчик адреса не обновляется правильно при чтении, поэтому, хотя для отправки мастеру могут быть подготовлены 16 или более байтов, мне нужно знать, сколько из них было фактически отправлено... Либо во время, либо после передачи. Самое главное, вы подтверждаете, что requestEvent вызывается только один раз для чтения с мастера., @le_top

@le_top Ну, в моем случае новый запрос (который представляет собой запись одним байтом) будет иметь новый начальный адрес, поэтому вам не следует увеличивать rQ. Последовательность чтения, например, 20 байт из байта 5 вашего мастера должна быть такой: записать адрес 5; прочитать 6 байт; напишите адрес 11; прочитать 6 байт; напишите адрес 17; прочитать 4 байта. За приращением следит мастер, а не подчиненный. Что касается единственного раза, то я обнаружил это экспериментируя (я не углублялся в библиотеку Wire, чтобы это подтвердить), @frarugi87

При изменении протокола ведомое устройство больше не будет вести себя как EEPROM. Добавляю к вопросу выдержку из спецификации EEPROM., @le_top

@le_top извините, я всегда использовал EEPROM таким образом (всегда запрашивая определенный адрес при каждой операции чтения, а не полагаясь на внутренний счетчик), поэтому я совершенно забыл об использовании этой функции. Если вам нужна эта функция, я думаю, что библиотеку следует изменить (я думаю, как предлагает Крисл), поскольку информация о количестве записанных байтов недоступна AFAIK, @frarugi87

Я понимаю. В целях безопасности лучше всегда сначала отправлять адрес. Однако при точной имитации поведения придется следовать спецификации. Мой главный вопрос связан с правильным методом определения количества байтов, на которые был получен ответ, или с тем, как отвечать побайтно — именно это и вызвало вопрос. Пример EEPROM представляет собой конкретное приложение. Поскольку основной вопрос показался мне новым, я решил открыть эту тему., @le_top


1

Обратите внимание, что я не тестировал приведенный ниже код, поскольку у меня сейчас нет на это достаточно времени. Но это может помочь вам добиться того, чего вы хотите.

Интерфейс I2C работает следующим образом: после каждого байта, прочитанного с ведомого устройства, ведущий отвечает ACK или NACK, в зависимости от того, хочет он прочитать больше данных или нет. Таким образом, ведомый заранее не знает, сколько байтов хочет прочитать ведущий.

Но вы можете изменить библиотеку Wire, чтобы вызывать обратный вызов onRequest для каждого отдельного байта, а не для каждого запроса. Важный код находится в пути к вашей установке Arduino по адресу hardware/arduino/avr/libraries/Wire/src/utility/twi.c в строке 575 (я имею в виду мою установку версии 1.8.5). ; у вас может быть немного по-другому):

case TW_ST_SLA_ACK:          // адресован, возвращено подтверждение
case TW_ST_ARB_LOST_SLA_ACK: // арбитраж проигран, возвращено подтверждение
      // вход в режим подчиненного передатчика
      twi_state = TWI_STX;
      // готовим индекс буфера передачи для итерации
      twi_txBufferIndex = 0;
      // устанавливаем длину буфера передачи равной нулю, чтобы проверить, изменил ли ее пользователь
      twi_txBufferLength = 0;
      // запрос на заполнение txBuffer и установку длины
      // примечание: для этого пользователь должен вызвать twi_transmit(bytes, length)
      twi_onSlaveTransmit();
      // если они не изменили буфер & длина, инициализируйте ее
      if(0 == twi_txBufferLength){
        twi_txBufferLength = 1;
        twi_txBuffer[0] = 0x00;
      }
      // передаем первый байт из буфера, падение
case TW_ST_DATA_ACK: // байт отправлен, подтверждение возвращено
      // копируем данные в выходной регистр
      TWDR = twi_txBuffer[twi_txBufferIndex++];
      // если есть что отправить, подтверждаем, иначе нет
      if(twi_txBufferIndex < twi_txBufferLength){
        twi_reply(1);
      }else{
        twi_reply(0);
      }
      break;

При получении запроса с правильным адресом будет выполнен первый case (конечно, после других действий с адресом получения и отправкой ACK). Функция twi_onSlaveTransmit() выполнит обратный вызов onRequest. Когда это будет сделано, код попадает непосредственно в следующий оператор case, который отправляет первый байт. Когда ведомое устройство получает подтверждение после этого байта, оно должно вернуться к этому case и отправить еще один байт.

Теперь мы можем поместить код заполнения буфера во второй оператор case, чтобы обратный вызов выполнялся для каждого запрошенного байта. В этом случае нам также необходимо после этого изменить оператор if, чтобы он запускал и отправлял NACK только в том случае, если пользователь не записывал ни байта в обратный вызов. Исходная библиотека, кажется, записывает минимум 1 байт (даже если в буфере ничего нет; он установлен на ноль). Поэтому мы также сделаем это, установив флаг в первом операторе case. Теперь код выглядит так:

case TW_ST_SLA_ACK:          // адресован, возвращено подтверждение
case TW_ST_ARB_LOST_SLA_ACK: // арбитраж проигран, возвращено подтверждение
      // вход в режим подчиненного передатчика
      twi_state = TWI_STX;
      twi_slave_transmit_first_byte = 1;
      // передаем первый байт из буфера, падение
case TW_ST_DATA_ACK: // байт отправлен, подтверждение возвращено
      // готовим индекс буфера передачи для итерации
      twi_txBufferIndex = 0;
      // устанавливаем длину буфера передачи равной нулю, чтобы проверить, изменил ли ее пользователь
      twi_txBufferLength = 0;
      // запрос на заполнение txBuffer и установку длины
      // примечание: для этого пользователь должен вызвать twi_transmit(bytes, length)
      twi_onSlaveTransmit();
      if(twi_slave_transmit_first_byte == 1){
          twi_slave_transmit_first_byte = 0;
          // если они не изменили буфер & длина, инициализируйте ее
          if(0 == twi_txBufferLength){
            twi_txBufferLength = 1;
            twi_txBuffer[0] = 0x00;
          }
      }
      // копируем данные в выходной регистр
      TWDR = twi_txBuffer[twi_txBufferIndex++];
      // если есть что отправить, подтверждаем, иначе нет
      if(twi_txBufferIndex < twi_txBufferLength){
        twi_reply(1);
      }else{
        twi_reply(0);
      }
      break;

И нам нужно добавить объявление флага в верхней части файла (где определены все остальные статические изменчивые переменные) как static изменчивые:

static volatile uint8_t twi_slave_transmit_first_byte = 0;

В обратном вызове onRequest вы теперь можете записать один байт в буфер и увеличить счетчик.


Думаю, это должно сработать. Если кто-то знает лучше, поправьте меня.

,

Привет, изменение библиотеки Wire — это один из способов сделать это: для переносимости ее нужно будет добавить как новую библиотеку Wire, возможно, просто расширив стандартную библиотеку. Я попробую это проверить., @le_top