Урок 15. Указатели в C для Ардуино. Преобразование разных типов данных в байты.

 

Указатели в C++ для Ардуино

В уроке узнаем, что такое указатели, и как они позволяют оптимизировать код программы, научимся преобразовывать сложные типы данных (int, long, float…) в последовательность байтов.

Предыдущий урок     Список уроков     Следующий урок

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

 

Тема указателей в языке C++ важная и обширная. Сейчас я очень коротко расскажу об указателях. Подробно примеры их использования будут рассмотрены в следующих уроках по мере необходимости.

Указатели в C для Ардуино.

При разработке программы мы работаем с переменными разных типов, массивами, объектами, функциями…

  • Ко всем им мы обращаемся по именам, заданным при объявлении.
  • Все они хранятся в памяти, разбитой на ячейки - байты.
  • Все занимают разное число байтов в памяти.
  • Но у всех объектов есть адрес начала блока памяти, в котором они расположены.

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

  • Указатели могут быть использованы для передачи данных по ссылке. Это намного ускоряет обработку данных, т.к. нет необходимости в копировании данных, как это делается при передаче с использованием имени переменной.
  • Указатели используются для динамического распределения памяти, например, для массива неограниченного размера.
  • Указатели удобно использовать для преобразования различных типов данных в байтовые потоки.

 

Косвенная адресация.

Узнать адрес конкретной переменно в C++ можно операцией получения адреса &. Она выдает адрес переменной, перед которой написан символ &.

А для обращения к переменной по адресу (указателю) есть операция косвенной адресации *. Она выдает значение ячейки памяти по адресу, на который ссылается указатель.

cod = 15;                    // переменная cod = 15
ptr
Cod= &cod;         // переменая ptrCod = адрес переменной cod
vl = * ptrCod;  // переменная vl = значению по адресу из ptrCod, т.е. vl = cod = 15

Надо понимать, что &cod это число, конкретный адрес. А ptrCod это переменная типа указатель, т.е. переменная для адреса.

Виды указателей.

Бывают указатели:

  • на основные типы;
  • на массивы;
  • на составные объекты (описываемые классами);
  • на функции;
  • на указатели;
  • на void.

Указатели на основные типы.

Как и любая другая переменная, указатель должен быть объявлен перед использованием. При объявлении указателя перед его именем ставится *.

int *ptrdt;  // указатель на переменную типа int
float *ptrx, *ptry, *ptrz;  // указатели на переменные типа float

Чтобы отличать указатели от обычных переменных принято добавлять к имени символы ptr. Но это условие необязательно и многие его не придерживаются.

При объявлении указателей выделяется необходимое число байтов памяти, в зависимости от типа данных. Например, для dt (int) компилятор выделит 2 байта, а для x (float) будет зарезервировано 4 байта.

Указатели на массивы.

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

Имя массива это указатель на его первый элемент.

int weights[10];  // массив weights
weights == &weights[0];  // имя это адрес первого элемента массива

Удобно использовать указатели на массивы, т.е. имена массивов, как аргументы функций.

int weights[10];  // массив weights
calculateAll(weights);  // функция использует в качестве аргумента имя массива

Указатель на функцию.

Подобно имени массива, имя функции само по себе является указателем. Указатель на функцию хранит адрес памяти программы, по которому расположен ее код. По этому адресу передается управление при вызове функции. Такие указатели используются:

  • для вызова функции через обращение к переменной с ее адресом, а не через имя;
  • для передачи имени функции в качестве аргумента в другую функцию.

В роке 10 мы использовали функцию MsTimer2::set(). В качестве второго аргумента мы задали имя другой функции (timerInterupt)- обработчика прерывания.

MsTimer2::set(2, timerInterupt); // задаем период прерывания и имя обработчика прерывания

Объявляется указатель на функцию так:

тип (*имя)(аргументы).

По сравнению с объявлением функции добавились скобки и *.

int (*ptrCalc)(int, float);  // объявление указателя на функцию с аргументами int и float
ptrCalc = calculate;  // присвоение указателю ptrCalc  адреса функции calculate
Serial.printf(ptrCalc(x, 2.345)  );  // вызов функции через указатель

int calculate(int, float) {
// тело функции calculate
}

Указатели на void.

Указатели на void используются для ссылки на объект, тип которого не определен. Например, для хранения различных типов данных в одних и тех же ячейках памяти.

Перед использованием указателя на void для хранения конкретного типа данных надо выполнить явное преобразование к этому типу.

 

Динамические переменные.

Когда мы объявляем переменную, компилятор выделяет для нее нужное количество ячеек памяти. Эти ячейки не могут использоваться для других целей, даже если в этой переменной уже нет необходимости. Поэтому такая переменная называется статической. Если статическая переменная требует значительного размера памяти (например, большой массив), то может возникнуть необходимость удалить ее и освободить память для новых переменных.

Сделать это можно только при использовании динамических переменных. Динамические переменные могут создаваться и удаляться в ходе выполнения программы. Доступ к динамическим переменным производится только через указатели.

Выделение памяти для динамической переменной осуществляется с помощью оператора new:

тип_данных *имя_указателя = new тип_данных;

Например:

long *dt = new long;
Выделяется память, достаточная для типа long (4 байта). Адрес начала записывается в указатель dt.

int *weights = new int[50];
Выделяется память для 50 значений типа int (100 байтов). Адрес начала записывается в указатель weights, который может использоваться как имя массива.

Можно при объявлении выполнить инициализацию значения по адресу указателя:

long *dt = new long(102345);  // значение памяти по адресу dt = 102345

Освободить память, выделенную оператором new, можно с помощью оператора delete.

// выделение памяти
long *dt = new long;
int *weights = new int[50];

// код

// освобождение памяти
delete [] weights;
delete  dt;

Заметьте, что для освобождения памяти массивов надо использовать оператор delete []. Если забыть про скобки, то будет удален только первый элемент массива, а остальные будут недоступны.

 

Указатели на составные объекты (описываемые классами).

Для создания динамических объектов также используется оператор new.

Button *buttonPlus = new Button;  // выделение памяти под объект buttonPlus типа Button

При создании статического объекта, доступ к его свойствам и методам происходит операцией прямого обращения – ”.”.

buttonPlus.scanState();
buttonPlus.flagClick = false;

Для работы с динамическим объектом через указатель, для доступа к его свойствам и методам используется оператор косвенного обращения ”->”.

buttonPlus->scanState();
buttonPlus->flagClick = false;

Операции с указателями.

С указателями можно производить простые операции:

  • косвенное обращение (разадресация);
  • присваивание;
  • сложение с константой;
  • вычитание;
  • инкремент;
  • декремент;
  • сравнение;
  • явное приведение типов.

Операция разадресации позволяет получить доступ к величине, адрес которой хранится в указателе.  Конструкцию *имя_указателя можно считать именем переменной. С ней допустимы все действия, которые разрешены для типа, заданного при объявлении указателя.

Арифметические операции с указателями автоматически учитывают размер типа переменной. Т.е. для типа данных char инкремент указателя увеличит адрес памяти на 1 байт, а для типа long прибавление 1 к указателю прибавит к реальному адресу памяти 4 байта. Арифметические операции применяют в основном при работе с данными, размещенными в памяти последовательно, например, с массивами.

 

Преобразование разных типов данных в байты.

В прошлом уроке мы сохраняли данные в EEPROM платы Ардуино. Сохраняли мы данные типа byte. EEPROM хранит данные в байтах и наши данные в байтах. Все просто. Но, допустим, нам надо сохранить в EEPROM переменную типа int . Она занимает в памяти 2 байта и для записи в энергонезависимую память ее надо преобразовать в отдельные байты. Можно сделать так:

int dt = 0x1234;  // переменная типа int, которую надо преобразовать в байты
byte byteEeprom1 =     (byte)(dt & 0xff);  // младший байт = 0x34
byte byteEeprom2 =     (byte)(dt >> 8);     // старший байт = 0x12

Мы получили два байта, но пришлось выполнить несколько операций, в том числе сдвиг на 8 разрядов. Для обработки переменной типа long сдвигов и других операций будет значительно больше. А вот что делать с переменной типа float я вообще не представляю. Преобразовать ее в число с фиксированной запятой? Наверное, что-то можно придумать, но это будет сложное решение и потребует значительных ресурсов от микроконтроллера. А ведь все переменные хранятся в памяти, разбитой на байты. Надо и взять их из памяти в виде байтов. Сделать это можно с помощью указателя.

Преобразуем переменную типа int в байты таким способом.

int dt = 0x1234;  // переменная типа int, которую надо преобразовать в байты
byte byteEeprom1 =  * ((byte *)(& dt));    // младший байт = 0x34
byte byteEeprom2 =  * ((byte *)(& dt) + 1);     // старший байт = 0x12

Для чтения байта мы:

  • получили адрес переменной: & dt;
  • явно преобразовали адрес к указателю на тип byte: (byte *);
  • применили операцию косвенной адресации: *;
  • для чтения второго байта прибавили 1 к указателю.

В этом примере мы не объявляли указатель  вообще. В строках кода для чтения байтов операции получения и преобразования адреса повторились. Для преобразования данных с большими размерами лучше объявить указатель. Это значительно ускорит работу программы.

Пример преобразования переменной типа float в байты:

float dt = 2.58901;  // переменная типа float, которую надо преобразовать в байты
byte* ptrdt;  // указатель на тип byte
ptrdt =  (byte*)(& dt);  // получаем адрес переменной dt

byte byteEeprom1 = * ptrdt;    // считываем байты
byte byteEeprom2 = * (ptrdt+1);
byte byteEeprom3 = * (ptrdt+2);
byte byteEeprom4 = * (ptrdt+3);

Осталось записать байты в EEPROM. Конечно, это можно сделать без промежуточных переменных.

// записываем  байты в EEPROM
EEPROM.write(0, * ptrdt);
EEPROM.write(1, *(ptrdt+1));
EEPROM.write(2, *(ptrdt+2));
EEPROM.write(3, *(ptrdt+3));

Для чтения данных типа float из EEPROM - считываем байты и записываем их последовательно в область памяти по указателю ptrdt:

// чтение байтов из EEPROM и запись в область памяти для dt
* ptrdt = EEPROM.read(0);
* (ptrdt+1) = EEPROM.read(1);
* (ptrdt+2) = EEPROM.read(2);
* (ptrdt+3) = EEPROM.read(3);

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

Указатели – очень важная тема для практического программирования микроконтроллеров. В будущих уроках, по мере необходимости,  мы будем рассматривать способы их использования на конкретных примерах. Тогда, возможно, Вам придется вернуться к этому уроку.

 

В следующем уроке поговорим о надежности программ для Ардуино, узнаем, как использовать сторожевой таймер.

Предыдущий урок     Список уроков     Следующий урок

0

Автор публикации

не в сети 3 дня

Эдуард

8
Комментарии: 1128Публикации: 124Регистрация: 13-12-2015

25 комментариев на «Урок 15. Указатели в C для Ардуино. Преобразование разных типов данных в байты.»

  1. Очень здорово написанная статья! Спасибо огромное! Пересылал с платы на плату переменные long по последовательному порту — проблем не было, при использовании операторов сдвига. Как появились переменные float ,так начались проблемы. Специально искал внятную инфу по применению указателей в таких случаях, и нашёл! Более внятно написать наверное невозможно 🙂

    0
  2. Если позволите, оставлю здесь шпаргалку про передачу массива как аргумент по ссылке.

    const int in = 3;//число пинов входа
    byte KlaIn[in]={6, 7, 8}; // пины входа

    void setup() {
    Serial.begin(9600); // открываем Serial порт
    for (int i = 0, x=in-1; i <= x; i++){ // выставляем входы
    pinMode (KlaIn[i], INPUT);
    }
    }

    void loop() {
    Serial.print(" STR = ");
    Serial.println( read_pins( &KlaIn[0], in ) , BIN);
    delay(10);
    }

    int read_pins(byte *pins, int kol_vo) {
    int str = 0; // "строка строк" общий сигнал со строк кнопок
    int stepen = 1; // 2 в степени, чтоб не возиться со степенями 10
    for (int i = 0, x = kol_vo -1 ; i <= x; i++){ // пробегаем по входным пинам
    if ( digitalRead( pins[i]) ){ // если сигнал = 1 (5В)
    str = str + stepen; // запоминаем 100/010/001 в STR
    }
    stepen = stepen << 1; // возводить 10 в степень — дорого
    } // (for)
    return str;
    }

    0
    • при вызове функции read_pins. В качестве первого аргумента IMHO можно просто указать имя массива т.к. это и есть адрес первого элемента массива

      Serial.println(read_pins(KlaIn,in),BIN)

      0
  3. Эдуард, здравствуйте. Помогите. Мой проект упёрся в передачу массива из функции в основную программу. Написал упрощенную конструкцию для Вас и она заработает, а вот в настоящей программе выдает 13 беспорядочных символа перед заданными. Посмотрите, пожалуйста, может я и тут что-то не верно написал?

    void setup() {
    Serial.begin(9600);

    char data[13];
    char ptrName = func();
    data[0] == &ptrName;
    Serial.println(data);
    }

    void loop() {}

    char func() {
    char dataF[13] = «abcdefghigkl»;
    Serial.println(dataF);
    char *ptrDataF = &dataF[0];
    return ptrDataF;
    }

    0
  4. В общем-то вопрос вот в чем: как правильно передать массив типа char из функции в программу?

    0
    • Здравствуйте!
      Не понятно:
      data[0] == &ptrName;
      Функция возвращает char, а вы передаете указатель;
      указателем на первый элемент массива является его имя …

      void setup() {
      Serial.begin(9600);
      Serial.println(func());
      }

      void loop() {}

      char * func() {
      char dataF[] = «abcdefghigkl»;
      Serial.println(dataF);
      return dataF;
      }

      0
      • Да, Эдуард, Ваша конструкция заработала и в упрощенном виде и в моей основной программе. Но есть вопросы! Я же не могу из функции передать целый массив данных, а лишь только одно значение, не так ли? Поэтому я пытался передать из функции как раз указатель на место, куда функция сохранила массив, а из основной программы уже присвоить новому массиву значения по указателю. Как раз в «строчке data[0] == &ptrName;» я сделал попытку сделать это. Вот как я понимаю свой код. (поправьте меня, пожалуйста). В функции, в массив я сохранил значения (char dataF[13] = «abcdefghigkl»;). Адрес массива я сохранил в указатель (char *ptrDataF = &dataF[0];), указатель я вернул в программу (return ptrDataF;), а значением адреса, которая вернула функция я указал на новый массив data[0] == &ptrName;), чтобы сохранить данные туда, и дальше с ними дальше работать. В общем, задача перенести значение массива dataF в массив data, но через функцию.

        0
      • Эдуард, не могли бы Вы объяснить для чего функция Serial.println(); в вашем примере используется два раза. Первый раз в void setup, второй раз в описании самой функции?

        0
      • Я попробовал вот такой вариант

        void setup() {
        Serial.begin(9600);
        func();/вызов функции по указателю
        }

        void loop() {}

        char * func() {
        char dataF[] = «acdefghigkl»;
        Serial.println(dataF);
        return dataF;
        }

        он дает точно такой же результат. В чем разница?

        0
    • char dataF[13] = «abcdefghigkl»; вы создали локальную динамическую переменную, которая при выходе из функции может быть уничтожена. Чтобы не уничтожалось, надо использовать глобальную переменную или static char dataF[13] = «abcdefghigkl»;

      char *ptrDataF = &dataF[0]; имя массива является указателем на первый элемент char *ptrDataF = dataF;

      return ptrDataF; Ваша функция возвращает тип char, а вы возвращаете указатель.

      data[0] == &ptrName; операция сравнения.

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

      0
      • Тут я все понял. Но я не понимаю как раз самого главного — как по указателю из функции сохранить массив в новый массив. Как??? ))). Значение из функции Вы вывели в COM-порт (Serial.println(func());), а как сохранить это значение в новый массив, чтобы не утерять данные при выходе из функции?

        0
          • Немного не то, что я себе представлял, но в любом случае, Эдуард, Вы мне очень помогли и кое что прояснили с указателями. Буду думать. Спасибо за все и особенно за терпение!

            0
  5. [quote] При объявлении указателей выделяется необходимое число байтов памяти, в зависимости от типа данных. Например, для dt (int) компилятор выделит 2 байта, а для x (float) будет зарезервировано 4 байта. [/quote]
    Почему-то всегда считал что указатель в памяти занимает одинаковое количество байт вне зависимости от его типа. Ведь указатель — это просто адрес расположения данных.

    0
  6. Эдуард! Спасибо Вам за информацию про сдвиги и указатели! Я раньше думал, что умею программировать на ардуино (как я ошибался))) Недавно стала задача прочитать данные с шины CAN. Я подключил шилду MCP2515 и нашел в интернете скейч на нее. Но она не стала работать… Нашел другой скейч и тот же результат. Решил просмотреть досконально все и понять как оно должно работать и ужаснулся всем этим непонятным символам &, *, -> ))))))) теперь понимаю зачем разработчики их использовали)) Спасибо еще раз Вам за Вашу работу!
    А у Вас есть форум на котором можно общаться на темы ардуино и не только?))

    0
  7. Здравствуйте Эдуард. Можно ли оператором delite удалить указатель на составной объект описываемый классом, и освободиться ли память занимаемая кодом объекта в таком случае?

    0
  8. Большущее спасибо за материал! так наглядной информации по Преобразованию разных данных в байты и не расчитывал даже найти!Применил метод успешно, но по i2c почему-то абра-кадабра.
    Мастер
    #include // Лобавляем необходимую библиотеку
    #include
    #include
    iarduino_I2C_connect I2C2;
    LiquidCrystal lcd(12, 11, 5, 4, 3, 2); // (RS, E, DB4, DB5, DB6, DB7)

    short value=0; //Переменная для сборки из байтов
    byte* pot; // указатель на тип byte

    void setup () {
    Wire.begin();

    lcd.begin(16, 2); // Задаем размерность экрана
    lcd.setCursor(0, 0); // Устанавливаем курсор в начало 1 строки
    lcd.print(«hello»); // Выводим смайлик (символ под номером 1) — «\1»
    delay(200);
    lcd.clear();
    }

    void loop() {
    pot = (byte*)(& value); // получаем адрес переменной value
    * pot = I2C2.readByte(0x01,0); //собираем байт 1
    * (pot+1) = I2C2.readByte(0x01,1);

    delay(200);
    lcd.setCursor(0, 0); // Устанавливаем курсор в начало 1 строки
    lcd.print(value);
    delay(200);
    lcd.clear();
    }

    Слейв
    #include // Лобавляем необходимую библиотеку
    #include
    #include
    iarduino_I2C_connect I2C2;
    LiquidCrystal lcd(12, 11, 5, 4, 3, 2); // (RS, E, DB4, DB5, DB6, DB7)
    short dt= 1256; //Переменная для разбивки на байты
    byte* ptrdt; // указатель на тип byte

    short value=0; //Переменная для сборки из байтов
    byte* pot; // указатель на тип byte

    byte myArray[2]; // Массив буферный для сборки/разбрки делаем доступным с помощью библиотеки iarduino_I2C_connect.

    void setup () {
    Wire.begin(0x01);
    I2C2.begin(myArray);

    lcd.begin(16, 2); // Задаем размерность экрана
    lcd.setCursor(0, 0); // Устанавливаем курсор в начало 1 строки
    lcd.print(«hello»); // Выводим смайлик (символ под номером 1) — «\1»
    delay(200);
    lcd.clear();
    }

    void loop() {
    ptrdt = (byte*)(& dt); // получаем адрес переменной dt
    myArray[0] = * ptrdt; // считываем байт 1
    myArray[1] = * (ptrdt+1); // считываем байт 2

    pot = (byte*)(& value); // получаем адрес переменной value
    * pot = myArray[0]; //собираем байт 1
    * (pot+1) = myArray[1]; // собираем байт 1

    lcd.setCursor(0, 0); // Устанавливаем курсор в начало 1 строки
    lcd.print(value);
    delay(200);
    dt++;
    lcd.clear();
    }

    не поскажете?

    0

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *