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

Arduino UNO R3

В уроке узнаем, что такое указатели, и как они позволяют оптимизировать код программы, научимся преобразовывать сложные типы данных (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);

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

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

 

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

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

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

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

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

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