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

Вернемся к старой проблеме. Без двоичной системы счисления делать нечего. Читаем статью «популярно про таймеры» . Важно не только понять, что двоичная система счисления это 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 комментария: Студентам. О переменных. Популярно.

  • Спасибо Админ, отличная статья. Многие вопросы, которые тормозили работу решились!

  • Я правильно понял насчет ссылки? Создаем не переменную а ссылку. Потом ее используем на те значения переменных которые нам нужны. Сколько занимает памяти ссылка? Если мы присвои значения переменной, то при обновлении значений в переменной останутся старые значения. По ссылки всегда получаем только свежие данные.

  • все вроде так, размер можете посмотреть с помощью sizeof

  • Прошу поправить ошибку в тексте в максимальном значении числа, которое хранится в 1 или 2 байтах. А именно: (1^8 — 1) = 0xFF = 255 и (1^16 — 1) = 0xFFFF = 65535.
    Кстати, в таблице из Codevision AVR — правильно 🙂

  • Да получилось неоднозначно, думал одно писал другое.

  • сделайте отдельно вкладку СТУДЕНТУ….у вас уже 2 статейки для них есть…а так потеряются они среди ваших статей здесь..) и опять появятся вопросы ) у людей, а вы…ну а вы не злой ))..Думаю тогда у вас будет шанс и не одного программера вытащить из веб..)

  • мысля хорошая

  • Спасибо за пример. Была мысль с массивом, но я её не пытался сделать. Зациклился на указателе.

  • Осталось раскурить, как подключить 2 датчика ds18b20.

  • Админ, день добрый. Идем дальше, правильно ли я понял. для вивода переменного значения на lcd я должен использовать указатель. Указатель это переменная. Обявляя переменную ми как это сказать обявляем тип данних какой ми хочем от нее принять. Правильно? и указатель занимает только помоєму 2 байта. Но запутиваешься потому что есть такой указатель «*» и «&». Первий указатель показывает, «разыменовивает», значения которое записано в некой области памяти. А второй «&» показывает адрес переменной. Так вот, я хочу вивести переменную например t которая постоянно меняеться. обявляем : usigned int T; дальше обявляем наш указатель usigned *ptemp; Но если я понял нам ничего это не даст так как надо написать *ptemp=&t; тоесть происходит инициализация. Я так понял или может что упустил. Почемуту с «&» информация на сайтах теряеться.

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

  • Вы меня еще больше запутали. 🙁

  • разберитесь с тем, какая разница между lcd_putchar(1) и lcd_putchar(‘1’)

  • ну если я правильно понял то первое функция отправит число а вторая функция отправит символ. Вот с вольтметром вы писали так int *pvolt; pvt=&adc; volt = *vt;
    потом вывод на лсд функцией printf («volt=» volt, %10) я точно не помню. Но оставим sprintf, в библ. до лсд можно как символ так и строку виводить. например у меня переменная t которая меняет свое значения. виводим строку lcd_strong («число=» t, 0,0) или я снова не туда иду. Я просто не поймц какая запись должна быть. если снова же я правильно понял то все что в кавичках то идет как символ, если нет кавичек то значения.

  • что то я вообще не пойму. Читаю снова статью и вижу все новую информацию .

  • Еще раз напишу, вы не правильно подходите к проблеме. Исходить нужно из того, что есть байт, это минимальный размер информации = 1 ячейка памяти. Любая ячейка имеет адрес. С чем бы вы не работали: дисплеем/уартом, интерфейсом или внешним устройством, все равно обычно минимум это байт. Надеюсь это понятно, есть байт, который лежит где то, т.е. имеет какой то адрес. Все строки, массивы, структуры указатели все равно состоят из байт, которые расположены по какому то адресу. Важно то, что на дисплей передаете вы именно байты, поэтому совершенно не важно КАК вы передаете эти байты, по указателю или по имени переменной, важно передать именно правильные значения. Правильные значения определяются кодировкой дисплея.

  • Ладно, составлю ход своих мислей. и действий. Спасибо что помогаете. Сложно самому все разбирать. И еще такой вопрос, я так и запустил свой лсд 16х2 по вашему алгоритму (в как написать свою библиотеку) и он не заработал. Где то писали что в hd4478 и ks006 немного разная инициализация. Верно ли это

  • Вроде было что то такое, но уже не вспомнить надо даташиты смотреть

  • беда какая то с этим лсд. уже разние инициализации пробывал, времена и так далее. Что за китайские лсд где нет информации никакой( я тут поигрался с калькулятором , оказываеться в байт можно 99 записать. хорошо показано 1 бит.

  • инициализация прописана в даташите на контроллер дисплея, а не на сам дисплей

  • я имел ввиду микроконтроллер. и кстати на мой дисплей в даташите инициализации нет.

  • Админ, Добрый день. В общем что я уже понял. Отправляем байт в лсд (если 8 бит режим, если 4 бит то по пол бита). хочем что бы лсд показал на 5 пишем код который в дш соотве. 5 например 0b00110011 (это для примера) и отсилаем. На лсд видем 5 . хочем ‘с’ смотрим в дш и байт который отвечает за него. Сдесь я уже понял. Дальше, я не могу понять или что то еще не знаю как написать что бы перемення которая меняет свое значения , например 6 , ето у нас число, нам нужно перевести в символ, тоесть в бити которые в лсд записани как симво 6. Но как?

  • все очень просто 1 символ всегда занимает одно знакоместо (по размеру шрифта), т.е. числа от 0 до 9 это 1 символ, от 10 до 99 — 2 символа и т.п. Далее все очень просто — нужно число разложить на 1 или несколько символов, в зависимости от того какое это число. Как это сделать пример есть в статье

  • Вот мы расклали цифри в буфер. А как же нам запятую вивести?

  • запятая тоже имеет свой символ, смотрите ASCII таблицу под каким номером

  • правильно я понял, например есть у нас температура + 25,5 градусов. 1. делаем в масив число 255, .2 Виводим байтом на дисплей +, потом байтом виводим значения первого масива.3 Потом значения второго значения масива. 4. Потом код символа , 5. Потом последнее значения масива (у нас 5). А если у нас 5 градусов будет то на дисплее такой формат будет + 0 5 , 0 градусов. Правильно? И еще один вопрос, а где в протеусе пошагово посмотреть работу програмы. И еще, В атмел студио 6.2 (да и в других версиях) при оптимизации может вибрасиваться код компилятором. Если отключить оптимизацию то функция задержки _delay_ms не будет работать. Как же быть?

  • все верно, если отключить оптимизацию то все должно нормально работать.

  • Админ, работать не будет так как отключаеться функция задержки. Вобщем запустил преобразования числа в троку функцией itoa, еще нашол фунццию gcvt, которая как раз и работает с плавающим значением. Но она не подключена в атмел студио 🙄 . Значит судьба такая, разобраться в азах

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

  • А тут ошибка или нет? Переменная buff[4]
    uint8_t buff[4] = {0x01,0x02,0x03,0x04};
    a тут &buf[3];
    uint8_t *adc_data;
    adc_data = &buf[3];

  • поправил

  • привет админ. вопрос, каким языком полюзуетесь, си или си++?

  • для микроконтроллеров си достаточно

  • ну смотря что делать 😉

  • admin, ты конечно загнул с формулировкой на указатели, даже я сейчас смотрю и мозг крутится. Во первых когда создаем указатель (а точнее переменная которая указывает на тип) , должны его инициализировать. Что это значит, а значит нужно присвоить адресу бутто переменной или массива (короче что нас интересует). По большому счету указатель это адрес которий должен содержать(!) адрес переменной или массива. Это как дверь.

  • не, дверь как то не вяжется. скорее уж тогда кнопки в лифте, которые указывают на каком этаже нам остановиться. сам этаж это уже адрес того что нам нужно.

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

  • для переменных которые пишутся во флеш есть ключевое слово const, например const int x = 0; если речь про авр, то в можно еще писать в еепром, например eeprom int x = 0;

  • Я понял, спасибо!
    То что надо.
    А где об этом пишут?
    (Просто вы хорошую литературу советуете, только там где вы Дейтела рекомендуете стоит указать что надо четвертое издание)

  • если речь о codevision то в codevision user manual

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

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

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