Спробуємо скористатися описаною теорією та попрацювати з мікросхемками EEPROM пам'яті на практиці. Для простоти почнемо із бібліотеки Wire для Arduino, яка енкапсулює більшість технічних подробиць роботи із апаратурою.
Працюватимемо із (єдиним наявним в мене :) Arduino Mega 2560 rev3. Суттєвих відмінностей для більш поширеного у нас Arduino Uno, крім як номерів пінів, до яких підключатися, немає.
Орієнтуватимемося на Arduino IDE 1 (1.0.1, якщо строго, але думаю це несуттєво). Надалі вважатиметься, що проблем з тим, щоб скачати оболонку, запустити її, під'єднати до комп'ютера Arduino, встановити відповідні драйвери, і заливати хоча б найпростіші прошивки, немає.
Arduino Mega 2560 побудований навколо контролера ATmega2560 (Uno -- ATmega328). Ці контролери мають апаратну підтримку I2C (TWI). Для зручності, за кожної події на шині генеруються переривання, щоб контролер не мусив чекати, поки відбуватимуться доволі повільні операції шини (*1). Детальніше розглянемо її в наступних нотатках, а почнемо експериментувати з 24Cxx за допомогою бібліотеки Wire, що є частиною оболонки Arduino. Вона значно простіша у використанні, ніж безпосередня робота з апаратурою, однак синхронна -- поки відбувається обмін по шині, контролер чекає.
Опис бібліотеки Wire
Бібліотека може працювати як у режимі Master, так і у режимі Slave. Поки що Slave не розглядатимемо, для роботи з пам'яттю це не потрібно. (Як зауважив diHalt, бути слейвом складніше, тому треба буде і до цієї теми потім звернутися.)
Wire -- C++ бібліотека, реалізовує клас TwoWire. Безпосередньо з цим класом працювати потреби немає, бібліотека створює його глобальний екземпляр з іменем Wire, до якого і звертатимемося.
Для роботи використовуються внутрішні буфери (на які використовуватиметься певний об'єм і так куцої оперативної (не плутати з програмною!) пам'яті контролера). По замовчуванню вони розміром 32 байти. Тобто, більше прийняти/передати за одну операцію вона не може. Іноді цей розмір потрібно змінити (наприклад, для ATtiny, у яких пам'яті мало). Щоб це зробити, слід у файлі libraries/Wire/utility/twi.h (відраховуючи від розташування Arduino IDE) змінити розмір в наступному рядку:
#define TWI_BUFFER_LENGTH 32
(Написати такий #define перед включенням заголовку бібліотеки -- Wire.h, бажаного ефекту не дасть, через те, що оболонка Arduino IDE насправді здійснює включення раніше.) Для ATtiny, згідно документації на сайті, можна встановити значення 6.
Розглянемо основні методи класу TwoWire:
Wire.begin()
Функція, яку слід викликати перед будь-якими операціями в режимі Master. Налаштовує внутрішні змінні бібліотеки, вмикає апаратуру підтримки TWI в мікроконтролері. Зокрема, переключає піни 20 і 21 (для Mega2560, 4 і 5 для Uno) у режим SDA i SCL відповідно. Встановлює швидкість обміну 100кГц.
Слід викликати в частині "Setup" ардуїнівського скетчу -- повторні виклики небажані.
Wire.begin(uint8_t address)
Wire.begin(int address)
Служить для ініціалізації бібліотеки в ролі Slave, з адресою address (використовуються молодші 7 бітів). Поки не розглядатимемо.
uint8_t requestFrom(uint8_t address, uint8_t count, uint8_t sendStop)
uint8_t requestFrom(int address, int count)
uint8_t requestFrom(int address, int count, int sendStop)
uint8_t requestFrom(int address, int count, int sendStop)
Використовується в режимі Master, дає запит на читання count байтів від пристрою з адресою address. Починає запис на початку буфера, тому може прочитати максимум TWI_BUFFER_LENGTH, 32 по замовчуванню, байт. Якщо count більше 32, воно автоматично і мовчки робиться рівним 32. З адреси, зрозуміло, використовуються тільки 7 молодших біт.До них всередині бібліотеки додається біт R/W.
Читання здійснюється типово -- подається сигнал Start (див. попередню нотатку), коли прочитано задану кількість байт, відповідає NACK, закінчуючи процедуру читання. Якщо в якийсь момент Slave-пристрій повернув NACK, сигналізуючи, що не має більше що сказати, теж закінчить виконання (не зависне, тобто).
Повертає кількість прочитаних байт.
По замовчуванню, після завершення передачі подається сигнал Stop. Це можна заборонити, явно вказавши третім аргументом false. Увага, бібліотека не відслідковує, чи було послано сигнал Stop. Залишена в підвішеному вигляді лінія може приводити до різноманітних збоїв.
void beginTransmission(uint8_t address)
void beginTransmission(int address)
Використовується в режимі Master, підготовлює бібліотеку до передачі даних. Зокрема, встановлює вказівники буферів на 0. Апаратних дій не виконує.
size_t write(uint8_ value);
size_t write(const uint8_t *data, size_t quantity);
Заповнює буфер даними. Кожен наступний виклик додає дані в буфер передачі. Все, що більше розміру буфера, просто ігноруватиметься. Не запихайте забагато даних підряд.
Існують варіанти, що приймають int та long, однак обидва кладуть в буфер лише молодший байт! В попередніх версіях IDE/бібліотеки називалися send, додатково вміла
працювати із стандартними стрічками С, виду char*, що закінчувалися
нульовим символом. (Це щоб був зрозумілим код трішки старіших програм.)
Апаратних дій не виконує.
uint8_t endTransmission(void)
uint8_t endTransmission(uint8_t sendStop)
Використовується в режимі Master. Намагається захопити лінію, якщо вдалося, то по адресі, вказаній в beginTransmission, передає вміст буфера, заповненого викликами write. Повертає код результату:
- 0 -- передано успішно.
- 1 -- буфер завеликий для TWI. Не повинно траплятися, бібліотека себе конфігурує самоузгоджено. (Можливо певний legacy-код, або я чогось не зрозумів).
- 2 -- отримано NACK у відповідь на послану адресу. Фактично -- не отримано відповіді. Або немає пристрою із такою адресою, або він несправний, або просто не хоче говорити. Слід виконати Stop, звільнивши лінію.
- 3 -- Отримано дані та NACK, Slave не має більше даних (поки?). Слід або виконати Stop, або повторити Start.
- 4 -- Інша помилка TWI.
int available(void)
Повертає об'єм даних, які знаходяться в буфері бібліотеки для читання -- отриманих. В режимі Master заповнюються requestFrom, в режимі Slave -- onReceiveService.
Апаратних дій не виконує.
int read(void)
Повертає наступний байт з внутрішнього буферу бібліотеки. Не більше, ніж там є, згідно available. Раніше називалася receive.
Апаратних дій не виконує.
int peek(void)
Повертає поточний байт з буфера, не переміщаючи вказівник на наступний (на відміну від read).
Апаратних дій не виконує.
void onReceive( void (*)(int) )
void onRequest( void (*)(void) )
Дві функції, що використовуються для роботи в режимі Slave. Встановлюють функції, що будуть викликатися, коли до Slave звернулися, передаючи йому, або очікуючи від нього, дані. Поки що їх не розглядатимемо.
Підключення - одна мікросхема
Використаємо схему, таку яка наводилася в попередній нотатці, або ж аналогічну, з AT24C128, та WC під'єднаним до піна 23, заземленим A0 (логічний нуль) та приєднаним до живлення A1 (логічна одиниця).
Див. також фрагмент фото Arduino Mega 2560 на початку поста.
Програмуємо - одна "велика" мікросхема
Робота з EEPROM розміром 128/256 кбіт простіша -- апаратний адрес не заплутується із адресами даних. Тому спершу подивимося, як працювати із ними. Почнемо з простого -- запишемо один байт.
int write_enable=23; // Пін, до якого під'єднано WC
uint8_t device_id=0x52; // Адреса пристрою на шині.
// Так як A1 - 1, A0 - 0, 01010010b
// Див. також попередню нотатку.
uint16_t byte_number=0x355; // Адреса, за якою записуватимемо uint8_t byte_value=0x1A; // Значення, яке записуватимемо Wire.begin(); // Ініціалізуємо Wire та, відповідно, I2C (TWI) digitalWrite(write_enable,LOW); // Дозволяємо запис Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою Wire.write(byte_number >> 8); // Старший байт адреси - в буфер Wire.write(byte_number & 0xFF); // Молодший байт адреси - в буфер Wire.write(byte_value); // Дані для запису - в буфер int result = Wire.endTransmission(true); // Передаємо!
// Вказано дати сигнал Stop
// Код результату в змінну result
Все просто, процес повністю описаний в коментарях. Варто зауважити, що реально з апаратурою працюватимуть лише команди Wire.begin (ввімкнувши апаратну підтримку TWI) та Wire.endTransmission (передасть Start, адресу пристрою, два байти адреси даних, значення байту для запису, та сигнал Stop -- в припущенні, що пам'ять відповідатиме ACK).
Якщо щось піде не так -- невірна адреса, збій мікросхеми пам'яті, запис заборонено високим рівнем на WC, буде повернуто код помилки, відмінний від 0. 2 для невірної адреси, 3 -- якщо запис заборонено, і т.д.
Увага! Після запису слід зробити невелику паузу, щоб мікросхема встигла його здійснити! (кількох мілісекунд повинно вистачити). Вона звичайно сигналізує, що ще не готова, якщо що -- не відпускаючи від нуля лінію SCL, але чи то бібліотека некоректно обробляє, чи то сама апаратура... (*2)
Увага! Wire.begin слід робити тільки раз, тому, бажано -- в Setup().
Прочитати байт трішки складніше. Вважатимемо, що всі константи -- адреса пристрою, адреса даних, номер піна WC, ті ж, що й в попередньому прикладі.Тобто, пробуємо читати щойно записані дані.
uint8_t byte_readed=0; // Прочитаний байт зберігаємо тут Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою Wire.write(byte_number >> 8); // Старший байт адреси - в буфер Wire.write(byte_number & 0xFF); // Молодший байт адреси - в буфер result=Wire.endTransmission(true); // Записуємо адресу байта Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою Wire.requestFrom(device_id,(uint8_t)1); //Очікуємо 1 байт (потім даємо) // Slave відповідь NACK -- досить. int readed_bytes=Wire.available(); // Перевіряємо, скільки байт ми отримали if(readed_bytes) byte_readed=Wire.read(); // Читаємо, що ж отримали
Які описано раніше, спочатку записуємо адресу байта, потім робимо до нього запит.
Що цікаво, тут насправді використано певний трюк. Ми записуємо адресу байта, однак зразу після того закриваємо з'єднання. Потім здійснюємо читання за поточною адресою.
Однак, можна зробити і повторний старт. Для цього під час виклику:
result=Wire.endTransmission(false); // Записуємо адресу байта, не робимо Stop!даємо вказівку не посилати Stop. Мікросхема все зрозуміє правильно.
Ціла програма, "скетч", може виглядати якось так:
#include <Wire.h> const int write_enable=23; // Пін, до якого під'єднано WC uint8_t device_id=0x52; // Адреса пристрою на шині. // Так як A1 - 1, A0 - 0, 01010010b // Див. також попередню нотатку. uint16_t byte_number=0x345; // Адреса, за якою записуватимемо uint8_t byte_value=0x1A; // Значення, яке записуватимемо uint8_t byte_readed=0; // Прочитаний байт зберігаємо тут void setup() { Serial.begin(9600); // Готуємо бібліотеку Serial для виводу // відладочної інформації // Вказуємо, що пін write_enable має працювати у режимі виводу pinMode(write_enable, OUTPUT); digitalWrite(write_enable,LOW); // Дозволяємо запис Wire.begin(); // Ініціалізуємо Wire та, відповідно, I2C (TWI) } void loop() { Serial.print("Starting\n"); // Відсилаємо по COM-over-USB // серіальному монітору Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою Wire.write(byte_number >> 8); // Старший байт адреси - в буфер Wire.write(byte_number & 0xFF); // Молодший байт адреси - в буфер Wire.write(byte_value); // Дані для запису - в буфер int result = Wire.endTransmission(true); // Передаємо! // Вказано дати сигнал Stop // Код результату в змінну result // Відсилаємо код результату Serial.print("Write transmission status: "); Serial.print(result); Serial.print("\n"); delay(1000); // Чекаємо секунду (для зручності того, хто на // серіальний монітор дивиться :) // Перевіряємо записане Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою Wire.write(byte_number >> 8); // Старший байт адреси - в буфер Wire.write(byte_number & 0xFF); // Молодший байт адреси - в буфер result=Wire.endTransmission(false); // Записуємо адресу байта // Відсилаємо код результату Serial.print("Read transmission, stage 1 - write address, status: "); Serial.print(result); Serial.print("\n"); delay(1000); // Чекаємо секунду Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою Wire.requestFrom(device_id,(uint8_t)1, (uint8_t)true); //Очікуємо 1 байт (потім даємо // Slave відповідь NACK -- досить) int readed_bytes=Wire.available(); // Перевіряємо, скільки байт ми отримали //Повідомляємо користувача про це Serial.print("Readed bytes: "); Serial.print(readed_bytes); Serial.print("\n"); if(readed_bytes) byte_readed=Wire.read(); // Читаємо, що ж отримали // Зациклюємося, передаючи прочитаний байт. // Зациклення потрібне, бо запис крутити в циклі дуже // швидко вичерпає можливості EEPROM по кількості записів while(true){ if(readed_bytes) { Serial.print("Readed: "); Serial.print((int)byte_readed); Serial.print("\n"); } else { Serial.print("0 bytes readed. \n"); } delay(1000); } }
На додачу до роботи з пам'яттю, реалізовано вивід усіх важливих етапів у серіальний COM-over-USB порт (*3). Запустивши серіальний монітор оболонки Ардуїно, можна наглядно спостерігати роботу програми. Таке собі ерзац-відлагодження.
Щоб писати/читати не по одному байту, можна в коді вище зробити прості заміни, скориставшись конструкціями типу:
Wire.write((uint8_t*)string_for_write, strlen(string_for_write)); Wire.requestFrom(device_id,(uint8_t)strlen(string_for_write), (uint8_t)true); for(int i=0; i<readed_bytes;++i) string_readed[i]=Wire.read();
Тільки пам'ятаємо, що:
- Запис сторінками відбувається тільки в межах сторінки -- якщо тільки 6 (для 128/256 кбіт)/4 (1-16 кбіт) молодших біт адреси змінюються.
- Розмір внутрішнього буфера бібліотеки, по замовчуванню -- 32 байти, більше за раз бібліотека переварити не може.
00|111010b -- 0x3A 00|111011b
00|111100b
00|111101b
00|111110b
00|111111b -- 0x3F
00|000000b -- !!!
00|000001b
00|000010b -- і т.д.
Програмуємо - одна "мала" мікросхема
Для "малих" мікросхем, типу нашої жертви M24C04, старші біти адреси задаються в апаратній адресі пристрою під час ініціалізації. Насправді, це не на багато складніше. Наприклад, фрагмент коду для запису байта, наведений вище, виглядатиме так:
int write_enable=22; // Пін, до якого під'єднано WC uint8_t device_id=0x50; // Адреса пристрою на шині. // Так як E1 i E2 заземлені, // задіяно 01010000b і 01010001b (0x50, 0x51) // Див. також попередню нотатку. uint16_t byte_number=0x355; // Адреса, за якою записуватимемо uint8_t byte_value=0x1A; // Значення, яке записуватимемо Wire.begin(); // Ініціалізуємо Wire та, відповідно, I2C (TWI) digitalWrite(write_enable,LOW); // Дозволяємо запис uint8_t A9; A9=(byte_number >> 8) & 0x01; // Виділяємо 9-й біт // Молодший біт device_id для 24C04/08/16 має бути рівним нулю ! Wire.beginTransmission(device_id | A9); // Звертатимемося до нашого пристрою Wire.write(byte_number & 0xFF); // Молодших 8 біт байт адреси - в буфер Wire.write(byte_value); // Дані для запису - в буфер int result = Wire.endTransmission(true); // Передаємо! // Вказано дати сигнал Stop
Замість передавати два байти адреси, ми виділяємо 9-й біт (зауважте, 9 біт якраз достатньо для адресації 521 байт -- 4 кбіт), і засуваємо його в апаратну адресу. Молодших вісім біт передаємо звичним чином.
Пам'ятаємо, що розмір сторінок для цих мікросхем -- 16 байт (можливо, для молодших - 1 і 2 кбіт -- 8 байт, про це пишеться в атмелівському даташіті, але не згадується в в ST-шному).
Важливо також, що послідовне читання цілком нормально проходить границю між верхніми і нижніми 256 байтами -- тими, для звертання до яких слід використовувати різні апаратні адреси. Тобто, звернувшись за апаратною адресою 0x50, адреса байта 0, можна послідовно прочитати весь вміст мікросхеми, всіх 512 байт.
Підключення - дві мікросхеми
Програмуємо - дві мікросхеми
За такого підключення, як на схемі вище, адреси мікросхем будуть 0x50 (+0x51) і 0x52.Щоб не говорити багато, подивимося на дві програмки, одна читає вміст обох мікросхем і передає його в серіальний порт, друга записує задану стрічку стільки раз, скільки вона вміститься.
Запис всієї пам'яті
Програма записує стрічку, вказану в змінній output_string стільки раз, скільки вона вміститься. Для максимальної швидкодії пише найбільшими можливими шматками (див. коментарі в тексті програми, функція find_optimal_buffer_size). Вона від душі коментована, особливих пояснень не потребує, однак після тексту декілька зауважень.
#include <Wire.h> //Лише для того, щоб знати, який зараз TWI_BUFFER_LENGTH // Статичних змінних не містить, тому проблем не мало б бути. #include <utility/twi.h> const int write_enable_1=22; // Пін, до якого під'єднано WC-1 const int write_enable_2=23; // Пін, до якого під'єднано WC-2 uint8_t device_id_1=0x50; // Адреса пристрою на шині. // Так як E1 i E2 заземлені, // задіяно 01010000b і 01010001b uint8_t device_id_2=0x52; // Адреса пристрою на шині. // Так як A0=0 i A1=1, 01010010b // Див. також попередню нотатку. // Розміри пам'яті в байтах const uint16_t m24c04_size=512; //4*1024/8; const uint16_t at24c128_size=16*1024; //128*1024/8; -- так буде переповнення // Розмір сторінок const uint8_t m24c04_pagesize=16; const uint8_t at24c128_pagesize=64; // Розмір буфера бібліотеки const int wiring_lib_buffer_size = TWI_BUFFER_LENGTH; char output_string[]="0123456789ABCDEFGHKLMNOPQRSTUW"; void setup() { Serial.begin(9600); // Готуємо бібліотеку Serial для виводу // відладочної інформації // Вказуємо, що пін write_enable має працювати у режимі виводу pinMode(write_enable_1, OUTPUT); pinMode(write_enable_2, OUTPUT); digitalWrite(write_enable_1,LOW); // Дозволяємо запис мікросхемі 1 digitalWrite(write_enable_2,LOW); // Дозволяємо запис мікросхемі 2 Wire.begin(); // Ініціалізуємо Wire та, відповідно, I2C (TWI) // Забігаючи на перед, TWBR -- один із регістрів Atmel-івського TWI // Задає частоту шини -- SCL // SCL = CPU clock / (16 + 2(TWBR)*4^TWPS ) // Де TWPS -- prescaler, може мати значення 0, 1, 2, 3. // По замовчуванню Wire встановлює нуль, тому частота шини визначається // SCL = CPU clock / (16 + 2(TWBR) ) // Для вибраної Wire за замовчуванням частоти 100кГц, і частоти // процесора 16МГц, TWBR=72 TWBR = 72; } // Функція звертається до шини, і чекає до її звільнення, або завершення // таймаута. Якщо дочекалася, повертає нуль, якщо ні -- 0x55, таймаут int wait_for_line(uint8_t device_id) { int result=0; uint16_t max_counter=20; uint16_t counter=0; uint16_t delay_ms=1; while(counter<max_counter) { Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою int tmp_result = Wire.endTransmission(true); if(tmp_result!=0) delay(delay_ms); else break; ++counter; } //Serial.print("timeout counter: "); //Serial.println(result, HEX); if(counter==max_counter) {// Маємо таймаут result=0x55; } return result; } // Функція, яка записує задану кількість байт в "малу" мікросхему M24C04, // по заданій адресі. Кількість даних для передачі, quantity, повинна // бути не більшою за менше з розміру сторінки та розміру буфера бібліотеки // Сама функція не здійснює перевірки! // На додачу до кодів Wire повертає код 0x55 -- таймаут int write_small(uint8_t device_id, uint16_t address, uint8_t quantity, char *buffer) { uint8_t A9=(address >> 8) & 0x01; // Виділяємо 9-й біт A9=(address >> 8) & 0x01; // Виділяємо 9-й біт // Молодший біт device_id для 24C04/08/16 має бути рівним нулю ! Wire.beginTransmission(device_id | A9); // Звертатимемося до нашого пристрою Wire.write(address & 0xFF); // Молодших 8 біт адреси - в буфер for(uint8_t i=0; i<quantity;++i) Wire.write(buffer[i]); // Буферизуємо дані int result = Wire.endTransmission(true); // Передаємо адресу та записуємо // Чекаємо, поки лінія звільниться - дозволить опустити SCL int waited=wait_for_line(device_id | A9); if(waited!=0) result=waited; return result; } // Функція, яка записує задану кількість байт в "велику" мікросхему AT24C128, // по заданій адресі. Кількість даних для передачі, quantity, повинна // бути не більшою за менше з розміру сторінки та розміру буфера бібліотеки // Сама функція не здійснює перевірки! // На додачу до кодів Wire повертає код 0x55 -- таймаут int write_large(uint8_t device_id, uint16_t address, uint8_t quantity, char *buffer) { Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою Wire.write(address >> 8); // Старший байт адреси - в буфер Wire.write(address & 0xFF); // Молодший байт адреси - в буфер for(uint8_t i=0; i<quantity;++i) Wire.write(buffer[i]); // Буферизуємо дані int result = Wire.endTransmission(true); // Передаємо адресу та записуємо // Чекаємо, поки лінія звільниться - дозволить опустити SCL int waited=wait_for_line(device_id); if(waited!=0) result=waited; return result; } // Знаходить оптимальну порцію для запису за раз. Вона повинна вміщатися в буфер // бібліотеки і бути кратною сторінці -- якщо буфер бібліотеки (мінус розмір адреси) // більший за сторінку, то просто сторінка за раз, якщо // менший -- то так, щоб запис (який йтиме частинами сторінки) відбувався, порціями, // на які розмір сторінки ділиться націло -- щоб не відбувався перехід // границь сторінки, інакше відбудеться згортання запису на її початок. // Реалізація вульгарна, я знаю :-) uint16_t find_optimal_buffer_size(uint8_t pagesize, uint8_t max_buffersize) { uint16_t buffer_size; if(pagesize<max_buffersize) { buffer_size=pagesize; }else{ buffer_size=max_buffersize; for(int i=0;i<max_buffersize-1;++i) // Менше одного байта писати - дурне заняття { if(pagesize % buffer_size == 0) { break; }else{ --buffer_size; } } } return buffer_size; } void loop() { //Затримка півсекунди, щоб в серіальний монітор не потрапляло //сміття з попередньої сесії delay(500); Serial.print("Starting. Library buffer size: "); Serial.print(wiring_lib_buffer_size); Serial.print("\n"); // Відсилаємо деяку інформацію по COM-over-USB серіальному монітору //------------------------------------------------------------------- //---"мала"-мікросхема,-M24C04--------------------------------------- //------------------------------------------------------------------- Serial.print("Writing to M24C04 \n"); //-1 бо один байт в буфері займе адреса uint8_t buffer_size_small = find_optimal_buffer_size(m24c04_pagesize,wiring_lib_buffer_size-1); Serial.print("Write buffer size: "); Serial.print(buffer_size_small); Serial.print("\n"); char write_buffer_small[buffer_size_small]; //uint16_t last_buffered=0; uint16_t curr_address=0; uint8_t buffer_pointer=0; uint16_t string_pointer=0; const uint16_t string_size=strlen(output_string); //Зберігаємо час роботи Ардуіно в мікросекундах unsigned long lapsed=micros(); for(uint16_t i=0; i<m24c04_size;++i) { write_buffer_small[buffer_pointer]=output_string[string_pointer]; ++buffer_pointer; if(buffer_pointer==buffer_size_small) { // Маємо цілу сторінку для запису, записуємо. int result = write_small(device_id_1,curr_address,buffer_size_small, write_buffer_small ); if(result!=0) { Serial.print("Writing small micro failed, error: "); Serial.print(result, HEX); Serial.print(", curr_address: "); Serial.print(curr_address, HEX); Serial.print(", i: "); Serial.print(i, HEX); Serial.print("\n"); break; } curr_address+=buffer_size_small; buffer_pointer=0; } ++string_pointer; if(string_pointer==string_size) { //Повертаємося до початку стрічки string_pointer=0; } } // Розраховуємо, скільки мікросекунд виконувався запис lapsed=micros()-lapsed; Serial.print("Written in: "); Serial.print(lapsed); Serial.print(" microsecond\n"); //------------------------------------------------------------------- //---"велика"-мікросхема,-AT24C128----------------------------------- //------------------------------------------------------------------- Serial.print("Writing to AT24C128 \n"); //-2 бо два байти в буфері займе адреса uint8_t buffer_size_large = find_optimal_buffer_size(at24c128_pagesize,wiring_lib_buffer_size-2); Serial.print("Write buffer size: "); Serial.print(buffer_size_large); Serial.print("\n"); char write_buffer_large[buffer_size_large]; //uint16_t last_buffered=0; curr_address=0; buffer_pointer=0; string_pointer=0; //Зберігаємо час роботи Ардуіно в мікросекундах lapsed=micros(); for(uint16_t i=0; i<at24c128_size;++i) { write_buffer_large[buffer_pointer]=output_string[string_pointer]; ++buffer_pointer; if(buffer_pointer==buffer_size_large) { // Маємо цілу сторінку для запису, записуємо. int result = write_large(device_id_2,curr_address,buffer_size_large, write_buffer_large ); if(result!=0) { Serial.print("Writing large micro failed, error: "); Serial.print(result, HEX); Serial.print(", curr_address: "); Serial.print(curr_address, HEX); Serial.print(", i: "); Serial.print(i, HEX); Serial.print("\n"); break; } curr_address+=buffer_size_large; buffer_pointer=0; } ++string_pointer; if(string_pointer==string_size) { //Повертаємося до початку стрічки string_pointer=0; } } // Розраховуємо, скільки мікросекунд виконувався запис lapsed=micros()-lapsed; Serial.print("Written in: "); Serial.print(lapsed); Serial.print(" microsecond\n"); //------------------------------------------------------------------ // Зациклюємося. // Зациклення потрібне, бо запис крутити в циклі дуже // швидко вичерпає можливості EEPROM по кількості записів while(true){ delay(1000); } }
Функції для читання "малої" та "великої" мікросхем реалізовано окремо, для простоти уніфікація не проводилася.
Ключові величини, такі як розміри пам'яті мікросхем, їх сторінок, буфера бібліотеки Wiring винесено в окремі константи.
Після запису -- пробуємо звернутися до шини, щоб перевірити, чи мікросхема вже відпустила SCL -- тримавши дані, вона потребує певний час щоб записати їх із внутрішнього буфера на EEPROM. Взагалі, там зроблено цикл, однак поки що кількість його повторів завжди була рівна нулю... Це трішки дивно, бо Wire.endTransmission мала б просто повернути код помилки, щось типу 2 або 4, і не мала б блокувати, очікуючи звільнення лінії. Може часу просто досить, затримка, у вигляді цього виклику, достатня? Хоча, навіть на частоті 888кГц (нештатній для однієї з мікросхем, але про це далі) -- все рівно, результат той же.
Програма виводить в серіальний порт етап свого виконання та час, що витрачено на роботу з мікросхемою. Команди деякого додаткового відладочного друку закоментовані, але якщо цікаво -- можна розкоментувати.
Забігаючи трішки наперед, за допомогою регістра контролера ATmega2560, TWBR (TWI Bit Rate Register), можна керувати частотою шини. Бібліотека встановлює по замовчуванню TWBR=72, що відповідає швидкодії 100 кГц. Змінивши його зразу після виклику Wire.begin(), можна отримати інші частоти. Див. також коментарі в тексті програми, в кінці функції setup. Формула відповідності між TWBR та частотою наступна: \[SCL frequency = \frac{CPU clock}{ 16 + 2 TWBR 4^{TWPS} }.\] Зокрема, можна дивитися, як змінюється для різних частот час запису чи читання.
В серіальному моніторі ми побачимо щось таке (*4):
Завершивши, програма зациклюється, тому що, якщо їй дозволити писати знову і знову, ресурс пам'яті на цикли запису доволі швидко вичерпається (від днів до десятків днів для великої, в десять раз менше для малої).
Читання всієї пам'яті
Програма читає весь вміст пам'яті обох мікросхем. Для покращення швидкодії читається максимальна кількість байт за раз -- розмір буфера бібліотеки. Якщо визначена змінна препроцесора MEASURE_TIME, заміряє час виконання, якщо ні -- виводить прочитане в серіальне з'єднання (*4).
#include <Wire.h> //Лише для того, щоб знати, який зараз TWI_BUFFER_LENGTH // Статичних змінних не містить, тому проблем не мало б бути. #include <utility/twi.h> const int write_enable_1=22; // Пін, до якого під'єднано WC-1 const int write_enable_2=23; // Пін, до якого під'єднано WC-2 uint8_t device_id_1=0x50; // Адреса пристрою на шині. // Так як E1 i E2 заземлені, // задіяно 01010000b і 01010001b uint8_t device_id_2=0x52; // Адреса пристрою на шині. // Так як A0=0 i A1=1, 01010010b // Див. також попередню нотатку. // Розміри пам'яті в байтах const uint16_t m24c04_size=512; //4*1024/8; const uint16_t at24c128_size=16*1024; //128*1024/8; -- так буде переповнення // Розмір буфера бібліотеки const int wiring_lib_buffer_size = TWI_BUFFER_LENGTH; // Раз на скільки символів вставляти line-feed при виводі в // серіальний порт const int symbols_per_line = 16; void setup() { Serial.begin(9600); // Готуємо бібліотеку Serial для виводу // відладочної інформації // Вказуємо, що пін write_enable має працювати у режимі виводу pinMode(write_enable_1, OUTPUT); pinMode(write_enable_2, OUTPUT); digitalWrite(write_enable_1,LOW); // Дозволяємо запис мікросхемі 1 digitalWrite(write_enable_2,LOW); // Дозволяємо запис мікросхемі 2 Wire.begin(); // Ініціалізуємо Wire та, відповідно, I2C (TWI) // Забігаючи на перед, TWBR -- один із регістрів Atmel-івського TWI // Задає частоту шини -- SCL // SCL = CPU clock / (16 + 2(TWBR)*4^TWPS ) // Де TWPS -- prescaler, може мати значення 0, 1, 2, 3. // По замовчуванню Wire встановлює нуль, тому частота шини визначається // SCL = CPU clock / (16 + 2(TWBR) ) // Для вибраної Wire за замовчуванням частоти 100кГц, і частоти // процесора 16МГц, TWBR=72 // TWBR = 1; } // Функція, яка читає задану кількість байт із "малої" мікросхеми M24C04, // по заданій адресі. Має виконуватися: quantity<=wiring_lib_buffer_size // Більше wiring_lib_buffer_size все рівно не читатиме // Для простоти код не робиться загальним, таким що підходитиме // бідь-яким із серії 24C01/02/04/08/16 int read_small(uint8_t device_id, uint16_t address, uint8_t quantity, char *buffer, uint8_t *received) { if(quantity>wiring_lib_buffer_size) quantity=wiring_lib_buffer_size; if(quantity==0) { *received=0; return 0; } uint8_t A9=(address >> 8) & 0x01; // Виділяємо 9-й біт A9=(address >> 8) & 0x01; // Виділяємо 9-й біт // Молодший біт device_id для 24C04/08/16 має бути рівним нулю ! Wire.beginTransmission(device_id | A9); // Звертатимемося до нашого пристрою Wire.write(address & 0xFF); // Молодших 8 біт байт адреси - в буфер int result = Wire.endTransmission(false); // Передаємо адресу. Не даємо Stop. if(result == 0) { Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою Wire.requestFrom(device_id, quantity, (uint8_t)true); // Посилаємо Stop *received=Wire.available(); // Перевіряємо, скільки байт ми отримали // Зберігаємо отримані дані собі for(uint8_t i=0; i<*received;++i) buffer[i]=Wire.read(); }else{ *received=0; } return result; } // Функція, яка читає задану кількість байт із "великої" мікросхеми AT24C128, // по заданій адресі. Стосуються всі обмеження, актуальні для read_small int read_large(uint8_t device_id, uint16_t address, uint8_t quantity, char *buffer, uint8_t *received) { if(quantity>wiring_lib_buffer_size) quantity=wiring_lib_buffer_size; if(quantity==0) { *received=0; return 0; } Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою Wire.write(address >> 8); // Старший байт адреси - в буфер Wire.write(address & 0xFF); // Молодший байт адреси - в буфер int result=Wire.endTransmission(false); // Передаємо адресу. Не даємо Stop. if(result == 0) { Wire.beginTransmission(device_id); // Звертатимемося до нашого пристрою Wire.requestFrom(device_id, quantity, (uint8_t)true); // Посилаємо Stop *received=Wire.available(); // Перевіряємо, скільки байт ми отримали // Зберігаємо отримані дані собі for(uint8_t i=0; i<*received;++i) buffer[i]=Wire.read(); }else{ *received=0; } return result; } // Використовуємо, якщо хочемо заміряти час роботи, // якщо не визначена - виводимо прочитане //#define MEASURE_TIME 1 void loop() { Serial.print("Starting. Buffer size: "); Serial.print(wiring_lib_buffer_size); Serial.print("\n"); // Відсилаємо деяку інформацію по COM-over-USB серіальному монітору char string_readed[wiring_lib_buffer_size]; // Прочитане зберігаємо тут //------------------------------------------------------------------- //---"мала"-мікросхема,-M24C04--------------------------------------- //------------------------------------------------------------------- Serial.print("Reading M24C04 \n"); uint16_t curr_address=0; uint16_t printed_char=0; //Зберігаємо час роботи Ардіїно в мікросекундах unsigned long lapsed=micros(); while(curr_address<m24c04_size) { int quantity=wiring_lib_buffer_size; // На випадок, якщо розмір буфера і мікросхеми не кратні. if(curr_address+quantity>=m24c04_size) { quantity=m24c04_size-curr_address; } uint8_t received; int result = read_small(device_id_1, curr_address, quantity, string_readed, &received); if(result!=0) { Serial.print("Reading small micro failed, error: "); Serial.print(result, HEX); /* Serial.print(", curr_address: "); Serial.print(curr_address); Serial.print(", quantity: "); Serial.print(quantity); Serial.print(", received: "); Serial.print(received); */ Serial.print("\n"); break; } #ifndef MEASURE_TIME for(uint8_t i=0;i<received;++i) { if(printed_char % symbols_per_line == 0) { // Розпочинаємо новий рядок виводу Serial.print(curr_address+i, HEX); Serial.print(": "); } Serial.print(string_readed[i]); Serial.print("("); Serial.print((uint8_t)string_readed[i], HEX); Serial.print("h), "); ++printed_char; if(printed_char % symbols_per_line == 0) { // Завершуємо попередній рядок виводу Serial.print("\n"); } // Без цього десь так на виводі 0x1FA-го символа // починало посилати сміття Serial.flush(); } #endif curr_address+=quantity; } // Розраховуємо, скільки мікросекунд виконувалося читання // Так як вивід по серіальному порту повільний, буде // хоч якось адекватним тільки коли визначено MEASURE_TIME, // яка заборонить вкомпільовувати вивід. lapsed=micros()-lapsed; Serial.print("Readed in: "); Serial.print(lapsed); Serial.print(" microsecond\n"); //------------------------------------------------------------------- //---"велика"-мікросхема,-AT24C128----------------------------------- //------------------------------------------------------------------- Serial.print("Reading AT24C128 \n"); curr_address=0; printed_char=0; //Зберігаємо час роботи Ардіїно в мікросекундах lapsed=micros(); while(curr_address<at24c128_size) { int quantity=wiring_lib_buffer_size; // На випадок, якщо розмір буфера і мікросхеми не кратні. if(curr_address+quantity>=at24c128_size) { quantity=at24c128_size-curr_address; } uint8_t received; int result = read_large(device_id_2, curr_address, quantity, string_readed, &received); if(result!=0) { Serial.print("Reading large micro failed, error: "); Serial.print(result, HEX); /* Serial.print(", curr_address: "); Serial.print(curr_address); Serial.print(", quantity: "); Serial.print(quantity); Serial.print(", received: "); Serial.print(received); */ Serial.print("\n"); break; } #ifndef MEASURE_TIME for(uint8_t i=0;i<received;++i) { if(printed_char % symbols_per_line == 0) { // Розпочинаємо новий рядок виводу Serial.print(curr_address+i, HEX); Serial.print(": "); } Serial.print(string_readed[i]); Serial.print("("); Serial.print((uint8_t)string_readed[i], HEX); Serial.print("h), "); ++printed_char; if(printed_char % symbols_per_line == 0) { // Завершуємо попередній рядок виводу Serial.print("\n"); } // Без цього десь так на виводі 0x1FA-го символа // починало посилати сміття Serial.flush(); } #endif curr_address+=quantity; } // Розраховуємо, скільки мікросекунд виконувалося читання // Так як вивід по серіальному порту повільний, буде // хоч якось адекватним тільки коли визначено MEASURE_TIME, // яка заборонить вкомпільовувати вивід. lapsed=micros()-lapsed; Serial.print("Readed in: "); Serial.print(lapsed); Serial.print(" microsecond\n"); // Зациклюємося. while(true){ delay(1000); } }
Виглядатиме вивід програми (у режимі, коли MEASURE_TIME не визначена) якось так:
Надрукований на скріншоті час великий, так як вивід по повільному серіальному порту -- це довго. Тому під час замірів продуктивності його вимикаємо, визначивши MEASURE_TIME.
Ігрища із швидкодією
Користуючись можливістю змінювати TWBR, а значить і частоту синхронізації на SCL, провів виміри продуктивності. Вони, звичайно, достатньо поверхневі та не дуже точні, але загальне враження скласти можна. Нагадаємо, що згідно даташітів: обидві мікросхеми мають декларовану мінімальну частоту 100 кГц, максимальна, за напруги 5В для AT24C128 -- 1 МГц, 400кГц -- 2.5В, для M24C04 -- 400 кГц, час фізичного циклу запису - 10 мс для AT24C128, 5 мс для M24C04.
Напруга живлення, яка на них подавалася -- 3.3 В.
Для орієнтування, час виконання коду без виклику функцій бібліотеки Wire -- 100мс, що на два порядки менше, ніж найменший час читання.
В перерахунку на один байт (адже абсолютні часи дуже відрізнятимуться, ємність однієї мікросхемки 512 байт, іншої 16 кілобайт, відрізняються в 32 рази) отримані результати виглядали так:
TWBR | M24C04 читання, мс/байт | AT24C128 читання, мс/байт | M24C04 запис, мс/байт | AT24C128 запис, мс/байт |
255 | -- | -- | 546,28 | 566,38 |
200 | -- | -- | 463,08 | 479,72 |
165 | -- | -- | 409,48 | 423,44 |
164 | 225,39 | 231,80 | 407,94 | 421,79 |
163 | 224,02 | 230,52 | 406,23 | 419,87 |
160 | 220,54 | 226,67 | 401,79 | 414,96 |
150 | 208,02 | 213,87 | 386,16 | 398,82 |
100 | 145,90 | 149,82 | 309,56 | 318,57 |
72 | 111,02 | 114,10 | 338,00 | 273,83 |
64 | 101,12 | 103,89 | 325,19 | 261,06 |
32 | 61,27 | 62,88 | 273,125 | 209,31 |
16 | 41,5 | 42,56 | 247,51 | 183,87 |
10 | 34,07 | 34,92 | 237,88 | 174,27 |
9 | 32,80 | 33,64 | 236,30 | 175,66 |
8 | 31,56 | 32,41 | 234,43 | 181,68 |
7 | 30,38 | 31,06 | 232,93 | 184,76 |
6 | 29,11 | 29,87 | 231,42 | 196,08 |
5 | 27,88 | 28,58 | 229,64 | 217,05 |
4 | 26,65 | 27,30 | 228,12 | 217,44 |
3 | 25,40 | 26,01 | 226,55 | 220,99 |
2 | 24,15 | 24,71 | 224,56 | 224,23 |
1 | 23,52 | 24,11 | 224,02 | 224,28 |
Для TWBR=0, що відповідає частоті 1МГц, працювати на напрузі як 3.3В, так і 5В, відмовились обоє. Можливо, знаходячись на шині сама і з живленням +5В, AT24C128 і погодилася б працювати -- не перевіряв. TWBR=1 відповідає 889кГц, що і є найвищою досягнутою частотою.
Прочерки для читання за великих TWBR означають, що мікросхеми відмовилася працювати на настільки низьких частотах. На 46.5-46.8 кГц (TWBR: 164-163) ще читалися (на 46.5 - не кожного разу), на 46.2 (165) -- лише кілька перших десятків байт, потім намертво зависали, на ще нижчих не було і цього. Запис відбувався успішно на як завгодно малих частотах із доступного діапазону.
Перерахувавши від TWBR до частоти, та відобразивши графічно, отримаємо:
Найяскравіший результат -- час запису/читання одного байта нелінійно залежить від частоти. До цього повернемося трішки пізніше, а поки ще декілька спостережень.
Час читання, в перерахунку на байт, в обох мікросхем приблизно однаковий, ледь-ледь (лічені проценти), хоча і гарантовано, швидше працює M24C04.
Швидкодія запису на краях діапазону частот приблизно однакова, плюс вона трішки (проценти) гуляє для AT24C128 (З точністю до десятка мілісекунд стабільна для M24C04), однак в діапазоні між 100 і 600кГц лідирує AT24C128, на 20-25%.
Нагадуємо ще раз, що M24C04, згідно документації на частотах, близьких до 1МГц працювати не мала б, однак вона умудряється.
Швидкість запису відрізняється майже в десять на високих частотах, однак всього в 1.8 на низьких.
Найдивніше для мене ось що. Якщо побудувати графік швидкодії не як функцію частоти, а як функцію TWBR, він буде лінійним:
Хто-небудь більш досвідчений може пояснити в чому справа? Не вірю, що це співпадіння. :-)
Додатки
Раптом кому згодиться...
- Якщо заборонити запис (подати на write_enable високий логічний рівень - 1), то M24C04 писати, безперечно, не буде, але читатиметься нормально. AT24C128 відмовляється і читати -- збоїть запис адреси для читання. Читати за поточною адресою, звичайно, можна. Детальніше вникати, чому так і що там пишуть даташіти, не став.
- Навіть якщо відключити одну із мікросхем від живлення, знаходячись на шині фізично вона цілком може заважати. Зокрема, за коректного підключення (щоб не було колізій по адресах), все працюватиме успішно, а якщо відключити живлення однієї із них -- перестане. Очевидно, якісь внутрішні їх схеми приводять до інтереференції.
Інформація, що допомогла написати цю нотатку, походила з:
- Даташіти AT24C128, M24C04, (а також, консультативно, AT24C04). (З часом посилання змінюються, документи оновлюються, але знайти їх просто).
- "Офіційний" reference manual бібліотеки Wire. Російський переказ тут.
- Детальний. трішки довгий, але доступний, опис внутрішньої кухні Wire.
- Дві статті-мануали про роботу зі шиною I2C з ардуїно. Охоплюють різні датчики, згадують і про EEPROM: "Tutorial: Arduino and the I2C bus – Part One", "Tutorial: Arduino and the I2C bus – Part Two".
- Детальний опис шини I2C від Di Halt.
- Опис апаратури TWI атмелівських контролерів від нього ж.
- Ще один хороший опис шини.
- Вікіпедія, російська, англійська.
- Різноманітні бібліотеки, що можуть мати відношення: на Arduino Playground, також альтернативна бібліотека для роботи із EEPROM та альтернативна бібліотека для роботи з I2C.
Виноски
(*1) Наприклад, на максимальній стандартній швидкості 400кГц, тривалість, скажімо, сигналу Start -- 2.5 мікросекунди. За той час ардуїнівський ATmega2560 робить 40 тактів.(*2) Я зауважив, що моя M24C04, якщо її живити 3.3В цілком встигає записати, без всіляких додаткових затримок, до наступного читання, а якщо живити 5В -- не встигає.
(*3) Нагадую, що у стандартного Ардуїно, ініціація зв'язку по COM-over-USB приводить до перезавантаження контролера, і скетч починає виконуватися з початку. Тому ловити моменти в програмі не треба -- її вивід будемо бачити від самого початку.
(*4) Іноді, перед виводом програми, в канал може потрапляти всіляке сміття з попередніх сесій зв'язку (можливо -- глюк драйвера?). Цього лякатися не слід :-)
Немає коментарів:
Дописати коментар