Есть еще один вопрос, требующий подробного разжевывания.

Вернемся к старой проблеме. Без двоичной системы счисления делать нечего. Читаем статью «популярно про таймеры» . Важно не только понять, что двоичная система счисления это 1 и 0. Важно понять сколько бит в байте, надеюсь помним, что восемь. Надеюсь понятно, что байт больше бит, потому что в слове байт больше букв 🙂

Теперь перейдем к шестнадцатеричной системе счисления. Почему Вы так ее боитесь? Да я и сам не помню чему равно 0xEE или 0xEC, открыл виндовский калькулятор, перевел из одной системы в другую. Почему она удобна? Тем что два символа дают 1 байт. Вот вы можете сказать, сходу не задумываясь сколько байт в числе 15658717? Большинство сходу не ответит, зато сколько занимает 0xEEEEDD все сразу ответят, что 3 байта, хотя это одно и тоже число. Поэтому не стесняемся, юзаем hex по полной.

Еще раз, в сотый раз повторим, размерность = количество бит в переменной, дает нам понять количество значений переменной, которое определяется как двойка в степени количества бит. Запомнить основные не составит труда 8 бит = 2^8 = 256 значений, 16 бит = 2^16 = 65536 значений. Обычно принято считать с нуля, поэтому максимальное переменной на единицу меньше 8 бит = 255 = 0xFF, 16 бит = 65535 = 0xFFFF.

Теперь немаловажный момент, то как хранятся байты. Ключевое слово тут «байты», т.е. не один, а несколько. Одно и тоже число может храниться на микроконтроллере так: 0xFF55, а на компьютере так 0x55FF. При том, что это будет одно и тоже число. Называются little endian и big endian, честно я так и не запомнил какие кто использует, поэтому спасаюсь отладчиком, проще посмотреть.

Итак первый важный момент, который всех вводит в заблуждение. Это размер переменной. Взглянем на две таблицы.

Codevision AVR
data_types

Keil
data_types2

Как видно, типы данных, несмотря на одно и тоже название не одинаковы, например unsigned int. Поэтому пинайте ссаными тряпками тех кто использует встроенные типы данных. Пользуйтесь типами из библиотеки stdint.h. Первая часть uint или int означает беззнаковое число или со знаком, дальше идет размер данных в битах. Наконец _t указывает что это тип. Таким образом uint32_t x означает что переменная x беззнаковая, 4 байтная. int16_t двухбайтное знаковое.

Для каждого из типов существует понятие приведение типов, оно бывает явным и неявным. Неявное когда из любого большого числа можно получить меньшее, отсечением старших байт. Допустим есть переменная uint32_t x = 0x31FF32EE, вы всегда можете получить ее младшие байты просто присваивая значение uint16_t y = x в этом случае в y окажутся 2 младших байта 0x32EE.

Пример явного приведения,

uint16_t x = 0x0102;
uint16_t y;
 
y = (uint8_t)x;

В этом случае в переменной y будет только младшая часть переменной х, т.е. 0х02

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

Типичные операции, присвоить указателю адрес переменной

uint8_t *adc_data;
uint8_t data;
 
adc_data = &data;

И получить значение по адресу

uint8_t *adc_data;
uint8_t data;
 
data = *adc_data;

Что нам это дает? Допустим я принял в массив 4 байта и хочу прочитать только 3 байт, который например хранит значение АЦП, то можно поставить указатель на 3 байт и прочитать его.

uint8_t buff[4] = {0x01,0x02,0x03,0x04};
uint8_t *adc_data;
 
adc_data = &buf[2];

Теперь мы можем читать значение указателя

uint8_t V_bat = *adc_data;

Не убедительно, ок, представим что нужное значение АЦП было бы не 1 байт, а два

uint8_t buff[4] = {0x01,0x02,0x03,0x04};
uint16_t *adc_data;
 
adc_data = (uint16_t *)&buf[3];

Теперь в переменной V_bat окажется 0х0403. Как видно ценность указателя повышается.

uint16_t V_bat = *adc_data;

Зачем указателю размерность? Во первых как было показано выше, чтобы понимать сколько байт он вернет при операции взятия значения, во вторых на сколько байт двигать адрес указателя. Сравним два куска кода, в первый момент указатель на нулевом элементе массива. Что же окажется в переменной x в первом случае и во втором.

uint8_t buff[4] = {0x01,0x02,0x03,0x04};
uint8_t *adc_data;
uint8_t x;
 
adc_data = &buf[0];
adc_data++;
x = *adc_data;
uint8_t buff[4] = {0x01,0x02,0x03,0x04};
uint16_t *adc_data;
uint8_t x;
 
adc_data = (uint16_t *)&buf[0];
adc_data++;
x = *adc_data;

В первом случае, после увеличения адреса указателя adc_data++, в x окажется 0x02, т.е. второй элемент массива. Во втором случае указатель двухбайтный, поэтому после adc_data++ он шагнет на 2 байта, т.е. в х окажется 0x03, т.е. третий элемент массива.

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

uint8_t buff[4] = {0x01,0x02,0x03,0x04};
float adc_data;
 
adc_data = *(float *)&buf[0];

Угадайте что будет в adc_data? Правильно либо 0x01020304 либо 0x04030201, в зависимости от little endian или big endian используется в камне или компе.

Что же насчет отправки? Да все тоже самое, размер 4 байта, отправляем побайтно, поэтому заведем промежуточный указатель, который будет прыгать по байту внутри adc_data.

float adc_data = 0.333;
uint8_t i = 0;
uint8_t *ptr = (uint8_t *)&adc_data;
 
while(i < 4)
{
  uart_putchar(*ptr); //отправили байт
  ptr++; //перешли на следующий
  i++; //увеличили счетчик
}

Очень удобно передавать указатели в функции. Допустим вы не знаете сколько байт должно быть обработано или оно может меняться по ходу программы, тогда передаете в функцию указатель, и по ходу выполнения программы вычисляете размер буфера. Очень популярный вопрос, как отправить строку в уарт или вывести строку на дислей, если есть только функция вывести один символ. Простой пример, как сделать из функции выводящей один символ lcd_putchar(), функцию выводящую строку.

void lcd_puts(uint8_t *buff, uint8_t buff_size)
{
  uint8_t char_numb = 0;
  while(char_numb < buff_size)
  {
    lcd_putchar(*buff);
    buff++;
  }
}
 
void main()
{
uint8_t string[] = "hello";
lcd_puts(&string[0], sizeof(string))
}

Следующий вопрос, который выносит мозг, это таблица кодировки. Если вы поняли наконец, что память это ячейки, которые имеют адреса и по ним можно двигаться с помощью указателя, то следующий вопрос должен стать более понятным. Итак, номер ячейки/адрес может быть равен значению, которое внутри нее записано. Почему бы по адресу 0x05, не записать значение 0x05? Никто ведь нам не мешает это сделать. Номер дома 5, в его почтовый ящик кидаем письмо с надписью 5, но это совсем не одно и тоже?

Итак, большинство систем используют различные кодировки, это значит что каждому символу соответствует какой либо номер. Допустим, когда вы шлете по уарту uart_putchar(0x31) это значит что вы отправили номер, когда этот номер приходит на терминальную программу компьютера, она автоматически преобразовывает этот номер, в соответствии с кодировкой. Что у нее записано по этому номеру, то она и выведет это может быть все что угодно, зависит только от используемой кодировки. Обычно в микроконтроллерах используют ASCII, можете загуглить и посмотреть, что под каким номером идет.

Обычно камнем преткновения становится то, когда путают номер символа в кодировке и значение. Это как спутать номер дома и то что в письме написано. Масла в огонь подливает упрощение uart_puchar(‘1’), которое никак не равно uart_puchar(1). В первом случае мы отправляем 0x31, в ASCII кодировке под этим номером символ 1, компилятор сам преобразует ‘1’ в 0x31, во втором же случае мы действительно отправляем 0x01.

Поэтому, когда говорят мне нужно вывести на дисплей значение, то нужно сразу подразумевать, что в символьном дисплее зашит алфавит, поэтому если вы прочитали температуру 27 градусов, и хотите ее вывести на дисплей, то lcd_putchar(27) выведет не 27 градусов, а тот символ что идет под номером 27. Если число двухзначное, то нужно его разбить на отдельные символы т.е. на 2 и 7, и каждый вывести отдельно. Чтобы вывести десятки нужно разделить число на 10, чтобы вывести единицы взять остаток x = 27%10.

Пример вывода числа 567,4

    float x = 567.4;
    int y = x*10;
 
     unsigned char buff[4];
 
    //сотни
    buff[0] = y/1000;
    //десятки
    buff[1] = (y%1000)/100;
    //единицы
    buff[2] = (y%100)/10;
    //дробная часть
    buff[3] = (y)%10;
 
    for(int i = 0; i < sizeof(buff); i++) {
    putchar(buff[i]+0x30);
    }

Абсолютно тоже самое касается уарта, если хотите выводить число в терминале, сначала разбиваем на 2 символа 2 и 7, к каждому прибавляем 0x30, т.к. в ASCII нумерация чисел начинается в 0x30 и выводите побайтно/посимвольно. Если лень делать все это руками то можно использовать
sprintf — преобразовать число в массив,

sprintf(lcd_buf,"t=%.1f\xdfC",temper);

printf — преобразовать число и вывести в уарт.

printf("t=%d", temper);

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

54 комментария: Студентам. О переменных. Популярно.

  • Благодарю! ))


  • while(char_numb < buff_size)
    {
    lcd_putchar(*buff);
    buff++;
    }

    А как тут программа из цикла выйдет? char_numb не нужно инкрементировать?

  • while(*buff!=0)
    {
    lcd_putchar(*buff);
    buff++;
    }

    я бы так сделал. Как дошёл до конца строки выход.

  • да, char_numb надо увеличивать, ваш вариант будет работать только для строк расположенных во флеш

  • Здравствуй админ.
    Я немного запутался, поправь если не прав.
    Переменные (как глобальные так и локальные) во время работы храняться в RAM кроме тех к которым добавлено eeprom и константы.
    Так?
    Спасибо!

  • Так, только есть разница в том, что предложил Илья и то о чем говорил я. Когда объявляешь массив во флеше const char mass[] = «hello»; то к нему автоматом в конце добавляется символ 0x00, в этом случае вариант Ильи работать будет. А если допустим я принял строку из уарта в оперативную память, то в конце строки будет мусор. Можно добавлять символ окончания строки руками, но на практике, мне доводилось видеть что передают буфер и размер.

  • Ну в то что что вы обсуждали я не сильно углубляюсь пока… ))
    Я чисто про своё…
    Вы не однократно мне говорили про константы, но ведь им не можно присваивать значения в годе выполнения программы, если я правильно понял…

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

  • можно сохранять в eeprom

  • Вы правы. Я перепутал вывод строки в уарт и на экран в своей программе.
    void usart_puts( char *str )
    /*****************************************************************************/
    {
    uint8_t c;
    while( ( c = *str++ ) != 0 ) {
    usart_putchar( c );
    }
    }

    Сам пример, я так понял, это вывод из буфера в который помещаются данные из уарта?

  • Админ, понравилась реализация вывода float, но…. Но как быть с отрицательными значениями?

  • как отдельный символ выводите

  • Спасибо огромное за статью. Словно солнце из за туч, а то бродишь тут, в сумерках…

  • Прошу прощения но, попытался почитать незнакомую тему и подумал, а почему бы не пользоваться всегда прописью, вместо цифр ? Например, пишите получил 4 байта и хочу прочитать 3 байт. Хорошо что склонением можно догадаться, что здесь говорят третий , а не три. А ведь не всегда можно… Уже не раз на этом спотыкался, а вы всё одно продолжаете так писать… Я не программист, поэтому прошу снисхождения, за свои дилетантские мысли.

  • Вы почти везде инициализируете буфер как buff ,а используете buf. Кого то может сбить.

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

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

Последние комментарии
  • Загрузка...
Счетчик
Яндекс.Метрика