неділю, 29 липня 2012 р.

Робота з EEPROM пам'яттю 24CXX -- Software, Arduino

А це ніжки, за які смикати ;-)
Мухоловка.
Щоб краще відчути "дух" I2C, реалізуємо його чисто програмно, самостійно смикаючи контролер за ніжки. Для початку, з використанням засобів Ардуіно ("мови" Wiring -- фактично С із "ардуїноспецифічними" бібліотеками).

В якій послідовності це слід робити, описано раніше: теорія та простенька практика.


Нагадаю, що до 1 (до напруги Vcc) лінія підтягуватиметься самостійно, за допомогою резисторів. Тому ніжки контролера працюватимуть так: 
  • Переключені на вхід, відповідають сигналу 1 на лінії. Зразу варто прочитати значення на вході. Наприклад, якщо ваш пристрій, будучи Master, відпустив SCL, а вона в 1 "не прийшла", значить хтось на шині притиснув лінію до 0, слід почекати.
  • Переключені на вивід, виводимо нуль -- відповідає 0 лінії.
УВАГА! Ні в якому випадку не можна в режимі виводу подавати 1 на лінію! Якщо в цей момент хтось на шині притискатиме її до нуля, відбудеться коротке замикання! Зокрема, спочатку вказуємо виводити на пін 0, і лише потім переключаємо його в режим виводу. Нагадаємо, що запис 0 в регістр, включений на ввід, просто вимикає внутрішній резистор зсуву до позитивної напруги (pull-up, подтягивающий резистор) (*1), тому проблем не буде (*2).

Цеглинки роботи з I2C


Отож, приступимо. Сигнали Start та Stop виглядають так:
(Взято з атмелівського даташіту на AT24XXX)
 Детальніше -- див. попередню нотатку.

Щоб зробити старт:
  1. Ніжки контролера, до яких підключено SDA та SCL -- на вивід, їх підтягне до 1.
  2. Затримка (масштабу мілісекунд, див. далі).
  3. Переконавшись, що pull-up-резистор вимкнено, переводимо пін, до якого підключено SDA, в режим виводу. При цьому він притягне лінію до 0.
  4. Затримка.
  5. Переконавшись, що pull-up-резистор вимкнено, переводимо пін, до якого підключено SCL, в режим виводу. При цьому він притягне лінію до 0. Сигнал Start подано.
Запрограмуємо його, для початку, засобами  Ардуїно (wiring):

  digitalWrite(SDA_pin,LOW); // Встановлюємо в нуль, для гарантії
  digitalWrite(SCL_pin,LOW); // Встановлюємо в нуль, для гарантії

  pinMode(SDA_pin, INPUT); // Лінію притягне до 1
  //Перевіримо, чи притягнуло:
  if(digitalRead(SDA_pin)!=HIGH)  
    // Лінію SDA насильно притиснуто до нуля - помилка
  pinMode(SCL_pin, INPUT); // Лінію притягне до 1
  //Перевіримо, чи притягнуло:
  if(digitalRead(SCL_pin)!=HIGH)  
    // Лінію SCL насильно притиснуто до нуля - помилка

  // Шина наша, починаємо працювати. (Вважаємо, що інших Master 
  // на шині немає, для простоти не реалізовуємо арбітраж)  
  // Чекаємо на завершення перехідних процесів і все таке
  delayMicroseconds(def_delay);
  pinMode(SDA_pin, OUTPUT);  // Опускаємо лінію SDA
  delayMicroseconds(def_delay);
  pinMode(SCL_pin, OUTPUT);  // Опускаємо лінію SCL
  //Сигнал Start подано


Спробуємо оцінити частоту шини, яку можна досягнути описаним вище методом.
  • Час виконання digitalWrite, згідно цього обговорення, порядку 5 мікросекунд (мої виміри "на коліні" дали 5.2мкс). 
  • Припустимо, що час виконання pinMode співмірний.
Тоді, час подачі сигналу Start буде:

T = def_delay + 5мкс + def_delay + 5мкс = 10 мкс + 2 def_delay   

В кінці затримку не даємо, нехай кожна функція сама робить затримки на своєму початку.
Мінімальна затримка --  1 мкс (*3), мінімальний період T=12 мкс, а значить максимальна доступна частота: 83.3 кГц. Мало. Ардуїнівські засоби Wiring прості, але ціна теж немала - швидкодія.

Сигнал Stop виглядатиме аналогічно, тільки в зворотньому порядку. Вважатимемо, що лінія SCL має бути на момент початку в нулі (щоб зайвий раз її не смикати).
  1. SDA-pin в режим виводу, лінія притискається до 0.
  2. SCL-pin в режим вводу, SCL підтягне до 1.
  3. Затримка.
  4. SDA-pin в режим вводу, SDA підтягне до 1. Сигнал Stop подано.
Конкретний код тут не наводжу, але якщо дуже цікаво, далі є повний текст програми.

Передати біт просто:
(Взято з атмелівського даташіту на AT24XXX


Після описаної вище процедури старту, SCL залишиться в нулі, в такому дусі і продовжимо, опускаючи її після передачі кожного біта:
  1. Виставляємо потрібний нам біт на SDA-пін. (Якщо нуль - переключаємо його на вивід, якщо 1 - на ввід).
  2. Виставляємо 1 на SCL, переключивши SCL-пін на вивід. 
  3. Затримка.
  4. Виставляємо 0 на SCL, переключивши SCL-пін на ввід.
  if(bit_val)
  {// Відпускаємо лінію - підтягнеться до 1
    pinMode(SDA_pin, INPUT);
  }else{
    pinMode(SDA_pin, OUTPUT); // Притискаємо лінію до 0    
  }
  // Відпускаємо SCL до 1
  pinMode(SCL_pin, INPUT);
  // Чекаємо:
  delayMicroseconds(def_delay);
  //Завершуємо передачу, SCL=0
  pinMode(SCL_pin, OUTPUT);

Читання біта аналогічне. Поки SCL в нулі, Slave виставляє дані:
  1. Переключаємо SDA-пін на ввід -- нам з нього читати.
  2. Виставляємо 1 на SCL, переключивши SCL-пін на ввід. 
  3. Перевіряємо, чи лінія піднялася (раптом Slave ще не закінчив свої справи). Якщо ні -- чекаємо, поки підніметься.
  4. Читаємо, що подано на  SDA-пін -- дані для нас.
  5. Виставляємо 1 на SCL, переключивши SCL-пін на вивід.
Запис байта відбувається так:
  1. Послідовно передаємо біти байта, починаючи із старшого.
  2. Очікуємо, що у відповідь приймач дасть ACK -- притисне SDA до нуля (або, іншими словами, передасть нуль).
for(int8_t i=7;i>=0;--i)
  {    
    // Зсуваємо потрібний біт на 0-ву позицію
    uint8_t cur_bit = byte_for_send >> i;
    // Зануляємо всі решта
    cur_bit &= 0x01;    
    
    //Відсилаємо
    send_bit(cur_bit);
  }
  // Перевіряємо, чи є ACK
  uint8_t in_bit;
  get_bit(&in_bit);
  if(in_bit!=0)
  { // Маємо ситуацію NACK
    // Реагуємо якось на неї
  }

Читання -- аналогічно.
  1. Послідовно читаємо біти, починаючи з старшого.
  2. Якщо хочемо продовжити передачу, посилаємо ACK (0 на SDA), якщо ні -- NACK (1 на SDA).
Як відбувається запис та читання даних, детально описано в попередніх нотатках. Якщо вміти посилати сигнали Start, Stop, передавати та отримувати байти, реалізація проста. Наприклад, якщо працювати з "великою" мікросхемою -- тією що приймає два байти адреси даних,  записати байт можна так:

// Посилаємо сигнал Start  
  send_start();

  uint8_t address = device_id << 1;
  // Біт R/W автоматично, після зсуву - нуль, запис.
  // Посилаємо апаратну адресу
  send_byte(address);

  // Посилаємо старший байт адреси
  send_byte(byte_address >> 8);
  // Посилаємо молодший байт адреси
  send_byte(byte_address & 0xFF);

  // Посилаємо байт для запису
  send_byte(byte_value);

  // Посилаємо сигнал Stop  
  send_stop();

Ціла програма


Використовуючи написане вище, реалізуємо просту програму, яка записуватиме байт у "велику" мікросхему, потім читатиме його. Реалізувати все решта, що зроблено в попередній нотатці за допомогою Wire, тривіально, хай це буде домашнім завданням.

Код, наведений вище, доповнено перевіркою на всі можливі нештатні ситуації, які просто детектуються (найбільш поширено -- перевірка, чи лінії піднялися в 1, після того як їх відпустили). Також, додано вивід інформації про роботу програми в серіальний порт.

Зверніть увагу, що для такої програмної емуляції можна використовувати практично будь які цифрові виводи контролера! Немає прив'язки до єдиної пари TWI-пінів.

const int write_enable=23; // Пін, до якого під'єднано WC

uint8_t device_id=0x52; // Адреса пристрою на шині.
                        // Працюємо з "великою" мікросхемою

uint16_t byte_address=0x3; // Адреса, за якою записуватимемо
uint8_t byte_value=0xCD; // Значення, яке записуватимемо 

uint8_t byte_readed=0; // Прочитаний байт зберігаємо тут

const int SDA_pin=20;
const int SCL_pin=21;

// Затримка, в мікросекундах.
const unsigned long int def_delay=4;

void setup() {                
  Serial.begin(9600); // Готуємо бібліотеку Serial для виводу
                      // відладочної інформації
  // Вказуємо, що пін write_enable має працювати у режимі виводу
  pinMode(write_enable, OUTPUT);      

  digitalWrite(write_enable,LOW); // Дозволяємо запис 
}

// Посилає сигнал Start
// Якщо все ОК, повертає 0
// Якщо лінія не піднялася для SDA -- повертає 1
// Якщо лінія не піднялася для SCL -- повертає 2
int send_start()
{
  digitalWrite(SDA_pin,LOW); // Встановлюємо в нуль, для гарантії
  digitalWrite(SCL_pin,LOW); // Встановлюємо в нуль, для гарантії

  pinMode(SDA_pin, INPUT); // Лінію притягне до 1
  //Перевіримо, чи притягнуло:
  if(digitalRead(SDA_pin)!=HIGH)  
    return 1; // Лінію SDA насильно притиснуто до нуля
  pinMode(SCL_pin, INPUT); // Лінію притягне до 1
  //Перевіримо, чи притягнуло:
  if(digitalRead(SCL_pin)!=HIGH)  
    return 2; // Лінію SCL насильно притиснуто до нуля

  // Шина наша, починаємо працювати. (Вважаємо, що інших Master 
  // на шині немає, для простоти не реалізовуємо арбітраж)  
  // Чекаємо на завершення перехідних процесів і все таке
  delayMicroseconds(def_delay);
  pinMode(SDA_pin, OUTPUT);  // Опускаємо лінію SDA
  delayMicroseconds(def_delay);
  pinMode(SCL_pin, OUTPUT);  // Опускаємо лінію SCL
  //Сигнал Start подано
  return 0;
}

// Посилає сигнал Stop
// Якщо все ОК, повертає 0
// Якщо лінія не піднялася для SDA -- повертає 3
// Якщо лінія не піднялася для SCL -- повертає 4
int send_stop()
{
  pinMode(SDA_pin, OUTPUT); // Лінію притискає до 0

  pinMode(SCL_pin, INPUT); // Лінію притягне до 1
  //Перевіримо, чи притягнуло:
  if(digitalRead(SCL_pin)!=HIGH)  
    return 4; // Лінію SCL насильно притиснуто до нуля
  delayMicroseconds(def_delay);

  pinMode(SDA_pin, INPUT); // Лінію притягне до 1
  //Перевіримо, чи притягнуло:
  if(digitalRead(SDA_pin)!=HIGH)  
    return 3; // Лінію SDA насильно притиснуто до нуля

  //Сигнал Stop подано
  return 0;
}

// Записуємо один біт
// Вважає, що SCL=0 на початку
// Повертає 0, якщо все добре
// 5 -- не вдалося виставити одиничний біт на SDA
// 6 -- не вдалося виставити одиничний біт на SCL
int send_bit(uint8_t bit_val)
{
  bit_val &=0x01; // Про всяк випадок обнуляємо старші біти
  if(bit_val)
  {// Відпускаємо лінію - підтягнеться до 1
    pinMode(SDA_pin, INPUT);
    // Користуючись нагодою, перевіримо, чи вдалося підняти
    if(digitalRead(SDA_pin)!=HIGH) 
      return 5; // Лінію SDA насильно притиснуто до нуля
  }else{
    pinMode(SDA_pin, OUTPUT); // Притискаємо лінію до 0    
  }
  // Відпускаємо SCL до 1
  pinMode(SCL_pin, INPUT);
  // Перевіряємо, чи вдалося. Сильно сповільнює процес!
  if(digitalRead(SCL_pin)!=HIGH) 
    return 6; // Лінію SCL насильно притиснуто до нуля
  // Чекаємо:
  delayMicroseconds(def_delay);
  //Завершуємо передачу, SCL=0
  pinMode(SCL_pin, OUTPUT);
  return 0;
}

// Читає один біт
// Вважає, що SCL=0 на початку
// Повертає 0, якщо все добре
// 7 -- таймаут очікування на лінію SCL
int get_bit(uint8_t *bit_val)
{
  //Переключаємо SDA-пін на ввід -- нам з нього читати.
  pinMode(SDA_pin, INPUT);
  //Виставляємо 1 на SCL
  pinMode(SCL_pin, INPUT);
  const int timeout_counter=20;
  int i;
  for(i=0;i<timeout_counter;++i)
  {
    if(digitalRead(SCL_pin)!=HIGH)
    {
      delayMicroseconds(def_delay);
      ++i;
    }else{
      break;
    }
  }
  
  if(i==timeout_counter)
  { // Лінію нам не віддали - таймаут
    return 7;
  }
  // Читаємо, що там нам передали
  *bit_val=digitalRead(SDA_pin);
  // Повертаємо SCL в нуль
  pinMode(SCL_pin, OUTPUT);
  return 0;
}

// Записує байт, перевіряє ACK
// Повертає 0, якщо все ОК
// Повертає коди помилок функцій, що викликає,
// або 8, якщо у відповідь на передані дані
// отримав NACK
// Коди помилок спеціально зроблені скрізьними і різними
// щоб зразу було видно, де збій.
int send_byte(uint8_t byte_for_send)
{
  int res;
  //Serial.print("  Byte to send: "); 
  //Serial.println(byte_for_send, BIN); 
  
  for(int8_t i=7;i>=0;--i)
  {    
    // Зсуваємо потрібний біт на 0-ву позицію
    uint8_t cur_bit = byte_for_send >> i;
    // Зануляємо всі решта
    cur_bit &= 0x01;    
    //Serial.print("  Bit number: "); 
    //Serial.print(i); 
    //Serial.print(" is equal to: "); 
    //Serial.println(cur_bit); 
    
    //Відсилаємо
    res=send_bit(cur_bit);
    if(res!=0)
    {
      send_stop();//Помилка. Пробуємо послати Stop
      return res;          
    }
  }
  // Перевіряємо, чи є ACK
  uint8_t in_bit;
  res=get_bit(&in_bit);
  if(res!=0)
  {
    send_stop();//Помилка. Пробуємо послати Stop
    return res;          
  }
  if(in_bit!=0)
  { // Маємо ситуацію NACK
    return 8;
  }
      
  return 0;
}

// Читаємо байт, в залежності від аргумента посилає ACK
// Повертає 0, якщо все ОК
// Повертає коди помилок функцій, що викликає,
// або 8, якщо у відповідь на передані дані
// Коди помилок спеціально зроблені скрізьними і різними
// щоб зразу було видно, де збій.
int receive_byte(uint8_t *in_byte, int sendACK)
{

  int res;
  *in_byte=0;
  for(int8_t i=7;i>=0;--i)
  { 
    uint8_t in_bit;
    res=get_bit(&in_bit);
    if(res!=0)
    {
      send_stop();//Помилка. Пробуємо послати Stop
      return res;          
    }
    //Serial.print("  Received bit: "); 
    //Serial.println(in_bit, BIN);    
    
    // Зсуваємо вже отримані біти
    *in_byte = *in_byte << 1;
    // Встановлюємо новий отриманий
    *in_byte+=in_bit;    
  }
    //Serial.print("  Received: "); 
    //Serial.println(*in_byte, BIN); 
  
  if(sendACK)
  {
    res=send_bit(0);
    if(res!=0)
    {
      send_stop();//Помилка. Пробуємо послати Stop
      return res;          
    }
  }else{ // Посилаємо NACK
    res=send_bit(1);
    if(res!=0)
    {
      send_stop();//Помилка. Пробуємо послати Stop
      return res;          
    }  
  }
      
  return 0;
  
}


void loop() {

  Serial.print("Starting\n"); // Відсилаємо по COM-over-USB
                              // серіальному монітору

//-Записуємо байт--------------------------------------------------
  Serial.print("Writing byte\n"); 
  
  int res; 
  uint8_t address_write = device_id << 1;
  // Біт R/W автоматично, після зсуву - нуль, запис.
  // Посилаємо сигнал Start
  res=send_start();
  Serial.print("Sent Start. Returned code: "); 
  Serial.println(res);   
  
  // Посилаємо апаратну адресу
  res=send_byte(address_write);
  Serial.print("Sent device_id. Returned code: "); 
  Serial.println(res); 
  // Посилаємо старший байт адреси
  res=send_byte(byte_address >> 8);
  Serial.print("High address byte sent. Returned code: "); 
  Serial.println(res); 
  // Посилаємо молодший байт адреси
  res=send_byte(byte_address & 0xFF);
  Serial.print("Low address byte sent. Returned code: "); 
  Serial.println(res); 

  // Посилаємо байт для запису
  res=send_byte(byte_value);
  Serial.print("Data byte sent. Returned code: "); 
  Serial.println(res); 

  // Посилаємо сигнал Stop  
  res=send_stop();
  Serial.print("Sent Stop. Returned code: "); 
  Serial.println(res);   
  

//-Читаємо байт--------------------------------------------------
  Serial.print("Reading byte\n"); 

  // Посилаємо сигнал Start
  res=send_start();
  Serial.print("Sent Start. Returned code: "); 
  Serial.println(res);   
  
  // Посилаємо апаратну адресу
  res=send_byte(address_write);
  Serial.print("Sent device_id. Returned code: "); 
  Serial.println(res); 
  // Посилаємо старший байт адреси
  res=send_byte(byte_address >> 8);
  Serial.print("High address byte sent. Returned code: "); 
  Serial.println(res); 
  // Посилаємо молодший байт адреси
  res=send_byte(byte_address & 0xFF);
  Serial.print("Low address byte sent. Returned code: "); 
  Serial.println(res); 

  // Адресу передали, читаємо:
  
  uint8_t address_read = device_id << 1;
  // Встановлюємо молодший біт, R/W -- читання
  address_read |= 0x01;

  res=send_start();
  Serial.print("Sent repeated Start. Returned code: "); 
  Serial.println(res);   

  // Посилаємо апаратну адресу
  res=send_byte(address_read);
  Serial.print("Sent device_id. Returned code: "); 
  Serial.println(res); 

  // Отримуємо байт, посилаємо NACK -- досить
  res=receive_byte(&byte_readed, (int)false);
  Serial.print("Received data, returned code: "); 
  Serial.println(res); 
  Serial.print("Received data: "); 
  Serial.println(byte_readed, HEX); 

  // Посилаємо сигнал Stop  
  res=send_stop();
  Serial.print("Sent Stop. Returned code: "); 
  Serial.println(res);   


//---------------------------------------------------------------
  // "Чистимося", про всяк випадок, щоб не було короткого замикання
  pinMode(SDA_pin, INPUT);
  pinMode(SCL_pin, INPUT);

  // Зациклюємося, передаючи прочитаний байт.
  // Зациклення потрібне, бо запис крутити в циклі дуже
  // швидко вичерпає можливості EEPROM по кількості записів
  while(true){
    delay(1000);               
  }
}


Підсумок


Описана тут програмна реалізація грубувата, зокрема частота на SCL відверто гулятиме, але для ілюстрації зійде. Та й засобами Wiring суттєво краще не зробиш. Однак, вона цілком працездатна і непогано ілюструє принципи роботи з I2C.

В наступній нотатці буде повторено зроблене тут за допомогою безпосередніх звертань до засобів контролера архітектури AVR8 (в нашому випадку -- ATmega2560).


Виноски


(*1) Згідно e2u, так воно і перекладається...

(*2) Зокрема бібліотека Ардуїно поки нічого не хімічить, якщо вірити документації:
Note also that the pullup resistors are controlled by the same registers (internal chip memory locations) that control whether a pin is HIGH or LOW. Consequently a pin that is configured to have pullup resistors turned on when the pin is an INPUT, will have the pin configured as HIGH if the pin is then swtiched to an OUTPUT with pinMode(). This works in the other direction as well, and an output pin that is left in a HIGH state will have the pullup resistors set if switched to an input with pinMode().
тут правда сказано, що трохи таки хімічить, але не мало б заважати:
As of Arduino 1.0.1, it is possible to enable the internal pullup resistors with the mode INPUT_PULLUP. Additionally, the INPUT mode explicitly disables the internal pullups.

(*3) Реально, затримка буде трішки більшою, див. коментарі коду delayMicroseconds з Wiring:

/* Delay for the given number of microseconds.  Assumes a 8 or 16 MHz clock. */
void delayMicroseconds(unsigned int us)
{
    // calling avrlib's delay_us() function with low values (e.g. 1 or
    // 2 microseconds) gives delays longer than desired.
    //delay_us(us);
#if F_CPU >= 20000000L
    // for the 20 MHz clock on rare Arduino boards

    // for a one-microsecond delay, simply wait 2 cycle and return. The overhead
    // of the function call yields a delay of exactly a one microsecond.
    __asm__ __volatile__ (
        "nop" "\n\t"
        "nop"); //just waiting 2 cycle
    if (--us == 0)
        return;

    // the following loop takes a 1/5 of a microsecond (4 cycles)
    // per iteration, so execute it five times for each microsecond of
    // delay requested.
    us = (us<<2) + us; // x5 us

    // account for the time taken in the preceeding commands.
    us -= 2;

#elif F_CPU >= 16000000L
    // for the 16 MHz clock on most Arduino boards

    // for a one-microsecond delay, simply return.  the overhead
    // of the function call yields a delay of approximately 1 1/8 us.
    if (--us == 0)
        return;

    // the following loop takes a quarter of a microsecond (4 cycles)
    // per iteration, so execute it four times for each microsecond of
    // delay requested.
    us <<= 2;

    // account for the time taken in the preceeding commands.
    us -= 2;
#else
    // for the 8 MHz internal clock on the ATmega168

    // for a one- or two-microsecond delay, simply return.  the overhead of
    // the function calls takes more than two microseconds.  can't just
    // subtract two, since us is unsigned; we'd overflow.
    if (--us == 0)
        return;
    if (--us == 0)
        return;

    // the following loop takes half of a microsecond (4 cycles)
    // per iteration, so execute it twice for each microsecond of
    // delay requested.
    us <<= 1;
    
    // partially compensate for the time taken by the preceeding commands.
    // we can't subtract any more than this or we'd overflow w/ small delays.
    us--;
#endif

    // busy wait
    __asm__ __volatile__ (
        "1: sbiw %0,1" "\n\t" // 2 cycles
        "brne 1b" : "=w" (us) : "0" (us) // 2 cycles
    );
}


Немає коментарів:

Дописати коментар