AVR. Учебный Курс. Программирование на Си. Работа с памятью, адреса и указатели
Автор DI HALT
Опубликовано 09 Янв 2010
Рубрики: AVR. Учебный курс
Метки: AVR, C, Программатор, Язык Си
Указатель. Один из самых мутных для понимания и в то же время совершенно необходимый инструмент любого языка программирования. Вызывает массу вопросов и непонимания на начальном этапе обучения.
Итак, начну по порядку.
Инфа, любая инфа (команды, данные) лежит в памяти по ячейкам. У каждой ячейки есть порядковый номер — адрес.
Мы можем напрямую сказать процессору — возьми данные из ячейки с адресом 0xA0 и положи его в ячейку с адресом 0×11. Это будет прямая адресация. Здесь адреса 0xA0 и 0×11 содержатся напрямую в машинном коде. Это очень быстро, просто и не требует никаких дополнительных телодвижений. Один минус — адреса 0xA0 и 0×11 нельзя изменить, как мы их впишем в код, так они там и останутся.
Но может быть и другой способ. Когда у нас есть еще две ячейки памяти. Например, А и Б в которые мы предварительно положим числа 0xA0 и 0×11 соответственно. И тогда предыдущая операция будет выглядеть так.
Возьми число из ячейки адрес который лежит в А и положи в ячейку адрес которой узнаешь из Б.
Результат тот же, но возникло множество дополнительных телодвижений. Во первых положить первоначальные адреса 0xA0 и 0×11 в ячейки А и Б. Потом, при совершеннии операции, используая данные ячеек А и Б как адреса, взять уже оттуда нужные нам данные и совершить обмен.
Но прелесть вся в том, что при этом мы можем как угодно менять А и Б (ведь это такие же переменные как и любые другие) и они будут указывать на разные данные.
А один и тот же кусок кода становится универсальным. Он может работать с любыми данными адреса которых нам укажут переменные А и Б.
А сами эти переменные и будут указателями.
Вот и вся премудрость.
Чтобы было проще для понимания для ассемблерщиков я буду давать ассемблерную аналогию работы с указателем. Конечно компилятор делает все не так прозрачно — начинает тасовать ячейками памяти и регистрами как заправский фокусник колодой карт. На этом этапе разглядывать ассемблерный листинг сишного кода можно лишь с целью уловить “Общие тенденции в городе”. Поэтому я дам не реальный пример из скомпиленного кода, как делал раньше, а упрощенный аналог.
В Си, для работы с указателями есть операнд звездочка (*). Инициализируется указатель так же и там же где и обычная переменная.
Собственно, указатель переменной и является. Только специфичной — хранящей в себе адрес.
Заведем две переменные i и z, а еще один указатель u
1 2 | unsigned char i,z; unsigned char *u; |
Свежесозданный указатель изначально указывает в никуда. Точнее может быть куда нибудь и показывает, но явно не туда куда надо. Так что его надо нацелить. Для нацеливания на какую либо ячейку памяти или переменную используется амперсанд “&“.
Это операнд взятия адреса.
1 | u=&i; |
Вот таким простым образом мы взяли и нацелили на переменную i указатель u.
На ассемблере это будет выглядеть примерно так:
Вначале две переменные в памяти. Назову их по другому, чтобы не путались с сишными.
1 2 3 | .DSEG Mode1: .byte 1 Mode2: .byte 1 |
Вначале, как водится, грузим адреса переменных в индексные регистры. Собственно регистровые пары X,Y,Z и есть самые настоящие указатели!
Так что:
1 2 | LDI ZL,low(Mode1) ; Z=&Mode1; LDI ZH,high(Mode1) ;Это нацеливание указателя, его инициализация. |
Теперь мы можем сделать с указателем две вещи.
1) Изменить сам указатель.
Например так:
1 | u++; |
При этом указатель перестанет указывать на i и будет показывать на следующую после i ячейку памяти. Это может быть полезно при обработки строк и массивов. Мы указываем на начало строки, а потом, увеличивая указатель, проходимся по всей строке.
На асме:
1 2 | SUBI ZL, Low(-1) ; Z++; SBCI ZH, High(-1) ; Инкремент указателя |
или
1 | u=u+j*k; |
На асме тут придется взять адрес из Z и всяко его математически изменить, а потом сунуть обратно в Z.
Так, например, можно брать данные из массивов.
Указатель можно складывать и вычитать с целочисленной константой ведь с точки зрения компилятора это всего лишь переменная. Нам лишь надо следить чтобы мы не нацелили указатель куда нибудь не туда, к примеру, за границы нашего массива. Иначе все может плохо кончится. Будет хитрая ошибка которую сложно найти.
2) Изменить или пощупать данные на которые указывает указатель.
1 | (*u)++; //Инкремент переменной на которую показывает указатель |
На асме:
1 2 3 4 | ; (*Z)++; LD R17,Z ; Вначале берем в регистр данные на которые указывает Z INC R17 ; Инкремент данных на которые указывает указатель. ST Z,R17 ; А потом сохраняем обратно, оттуда где взяли |
или так
1 | *u=m; |
На асме:
1 2 | MOV R17, m ; Взяли откуда нибудь нашу m ST Z,R17 ; и запислаи ее по адресу который в Z |
При этом надо учитывать приоритет операций. Дело в том, что если мы запишем нашу операцию как
1 | *u++; |
На асме:
1 2 3 | SUBI ZL, Low(-1) ; Z++; SBCI ZH, High(-1) ; Инкремент указателя LD R17,Z ; и что дальше? |
То ничего не с данными произойдет. Операция ++ имеет тот же приоритет что и обращение через указатель * , а выполняются они у нас справа на лево. И у нас произойдет сначала увеличение указателя u, а потом уже мы по новому указателю обратимся в память и ничего там не сделаем. Подробней про приоритеты операций
Тип данных указателя
Если указатель это всего лишь адрес, то как же он может иметь тип? В AVR адрес двухбайтный, а на какие данные мы бы не ссылались указателем на длинну адреса это не влияет. Зачем тогда тип указателя?
А затем, чтобы компилятор знал на сколько этот адрес изменять при операциях с указателем.
В ассемблере, ясно дело, типов никаких нету. Поэтому нам надо самим думать на сколько изменять индексные регистры в каждом конкретном случае.
Еще тип позволяет отслеживать правильность обращения к данным через указатель. Например, чтобы мы не пытались записать в байт слово, или вместо структуры загнать всего один байт. Без типа, по одному адресу, мы не сможем понять что нас ждет на том конце указателя. А значит можно наделать кучу багов. А так нас компилятор предупредит Warning’ом. Мол чего это ты, товарищь, пургу гонишь? Так что Warning с указателем можешь смело приравнивать к критической ошибке и искать ее причину. :)
Чтобы соответствовать всем правилам, указатель должен быть того же типа, что и данные на которые он показывает.
То есть на u16 value; должен быть u16 *pointer;
А если нам надо двубайтные данные разобрать по байтам?
Конечно, можно взять и нацелить на то же двубайтное значение однобайтный указатель. И это будет прекрасно работать.
1 2 3 4 | u16 value; u08 *pointer_8; pointer_8=&value; |
Но компилятор перекосит от возмущения и он завалит тебя Warning’ами вида: “Pinboard_1.c:43: warning: assignment from incompatible pointer type”. Что дескать тип не совпадает и ты часом там не ошибся? В этом случае надо делать преобразование типа указателя. Задав нужный тип явным образом.
1 2 3 4 5 6 | u16 value; // Двубайтная переменная value u08 *pointer_8; // Указатель на однобайтные данные. // Даем явно понять, что несмотря на то, что данные в value двубайтные // Мы будем с ними работать как с байтом pointer_8=(u08 *)&value; |
Это успокоит компилятор.
Также бывают указатели вообще без типа. С типом void
1 | void *addres; |
Это просто адрес. Без типа, без размерности. Тупо два байта указывающие куда то в память. Все.
Для того чтобы с ним сделать какую нибудь гнусную вещь надо сначала решить мальчег это или девАчка. В смысле куда он нацелен и что мы с этим будем делать. Поэтому при использовании указателя типа void мы каждый раз должны явно обозначать тип данных на той стороне провода. Вот так:
1 2 3 | (u16 *)addres++; //Увеличили указатель как будто он u16, т.е. на 2 байта (u08 *)addres++; // А теперь словно он u08 -- на один байт |
Приведу пример.
Есть у нас массив двубайтных слов данных str типа u16 (aka unsigned short). И есть указатель u типа u16 который указывает на первый элемент строки.
1 2 3 | u16 str[10]; u16 *u; u=str; |
А потом сделали инкремент указателя:
1 | u++; |
Куда покажет указатель? Значение его адреса увелчится на два. Т.е. он укажет на следующее слово. Потому что его тип двухбайтный.
А если бы указатель был однобайтного типа, а указывал на двубайтные данные, то инкремент указателя дал бы нам не следующий двубайтный элемент массива, а на второй байт первого элемента. Таким образом, выбирая тип указателя мы можем задавать дискретность его изменения, выбирая те данные что нам нужны.
Дам один простенький пример на работу со строкам через указатель.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | // Задаем всякие переменные. Не стал выносить их в хидер #define F_CPU 8000000L #define XTAL 8000000L #define baudrate 9600L #define bauddivider (XTAL/(16*baudrate)-1) #define HI(x) ((x)>>8) #define LO(x) ((x)& 0xFF) // Подключаем библиотеку ввода вывода #include <avr/io.h> //Прототипы функций void SendStr(char *string); void SendByte(char byte); // Поехали!!! int main(void) { char String[] = "Hello Pinboard User!!!"; // Организуем в памяти массив-строку char *u; // И про указатель не забываем. // Инициализация периферии UBRRL = LO(bauddivider); UBRRH = HI(bauddivider); UCSRA = 0; UCSRB = 1<<RXEN|1<<TXEN|0<<RXCIE|0<<TXCIE; UCSRC = 1<<URSEL|1<<UCSZ0|1<<UCSZ1; // Главный код u=String; // Присваиваем указателю адрес начала строки // Где оператор взятия адреса "&"? А он в данном случае и не нужен // Дело в том, что все сложные типы вроде массивов и строк это и есть // Указатели в чистом виде. Поэтому мы просто присваиваем один // Указатель к другому. И все. А вот передавай мы один байт или слово // Пришлось бы брать адрес операндом "&". SendStr(u); // Печатаем строку в USART // А тут еще прикольней. Мы инлайново обьявили строку прям в параметре функции) // Так тоже можно. Работает точно также, строка размещается в RAM и в функцию // Передается указатель на ее заголовок. SendStr(" Hello inline String"); return 0; } // Отправка строки void SendStr(char *string) // А вот как все тут работает { while (*string!='\0') // Пока первый байт строки не 0 (конец ASCIIZ строки) { SendByte(*string); // Мы шлем байты из строки string++; // Не забывая увеличивать указатель, } // Выбирая новую букву из строки. } // Отправка одного символа void SendByte(char byte) // Тут тоже элементарно. На входе байт { while(!(UCSRA & (1<<UDRE))); // Ждем флага готовности USART UDR=byte; // Засылаем его в USART } |
Крастота? Прелесть?
Ну да, вот только есть тут одно западло. Используя таким образом строки и переменные мы совершенно варварским образом расходуем оперативку. А она у нас тут маленькая, всего килобайт. Причем западло тут двойное. Строки “Hello Pinboard User!!!” и ” Hello inline String” вначале вкомпиливаются в флеш, а потом, при старте МК, борзо копируются в ОЗУ и висят там мертвым грузом.
В микроконтроллерах AVR есть несколько видов памяти. Первая это, конечно же, ОЗУ. Оперативка. В ней хранятся все переменные и стек. Доступ к ней осуществляется из Си безо всяких ухищрений. Завел переменную или массив — и вот тебе обращение. То же самое и насчет указателей. Второй тип памяти до которого мы можем дотянуться — флеш. Вот туда бы эти строки и засунуть, точнее, раз уж они там уже есть, брать их сразу оттуда.
Как это сделать?
Тут нам поможет библиотека pgmspace.h из стандартной поставки WinAVR.
Осталось только определить наши строки с аттрибутом PROGMEM
1 | char String[] PROGMEM = "Hello in FLASH Pinboard User"; |
А для инлайновых строк есть удобный макрос PSTR
1 | SendStr(PSTR(" Hello FLAHS inline String")); |
Все замечательно, но вот только это работать не будет :(
Дело в том, что память программ адресуется совсем по другому и доступна только через спец команду процессора LPM. Поэтому обычное чтение по указателю тут работать не будет :( (впрочем, возможно это косяк именно GCC т.к. он изначально заточен на Неймановскую архитектуру, а на Гарвардской его перекашивает. Как там в IAR и CVAVR с обращением во флеш через указатели?)
Для этого в pgmspace.h есть функции чтения из памяти программ:
1 2 3 4 | pgm_read_byte(data); pgm_read_word(data); pgm_read_dword(data); pgm_read_float(data); |
Как говорится, на любой тип и размер.
Где data это передаваемый адрес во флеше.
Поэтому нашу функцию отправки строк придется переделать:
1 2 3 4 5 6 7 8 | void SendStr_P(char *string) // А вот как все тут работает { while (pgm_read_byte(string)!='\0') // Пока первый байт строки не 0 (конец ASCIIZ строки) { SendByte(pgm_read_byte(string)); // Мы шлем байты из строки string++; // Не забывая увеличивать указатель, } // Выбирая новую букву из строки. } |
Казалось бы все. Теперь работает. Ан нет. Тут у нас есть еще одно западло, стоившее мне когда то часа возни.
Дело в том, что если мы определим нашу PROGMEM строку в функции main, то компилятор посчитает ее локальной переменной, а все наши ярлыки проигнорирует. Выдаст Warning, a строка будет по прежнему предательски скопирована в RAM. Разумеется через pgm_read_*** до нее уже будет не достучаться. И вся наша программа полетит в тартар.
Чтобы такого не случилось надо все PROGMEM константы размещать за пределами main. Разумеется это не касается макроса инлайновой вставки PSTR, тут можно не париться — он сам все разместит где надо.
Да, кстати, когда мы обьявляем строку (или массив), то ее имя по факту и есть указатель. Так что вполне работает и такой механизм:
1 | SendStr_P(StringP); |
Ну а вся прога, со всеми заморочками, стала такого вида:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | #define F_CPU 8000000L #define XTAL 8000000L #define baudrate 9600L #define bauddivider (XTAL/(16*baudrate)-1) #define HI(x) ((x)>>8) #define LO(x) ((x)& 0xFF) #include <avr/io.h> #include <avr/pgmspace.h> //Прототипы функций void SendStr(char *string); void SendStr_P(char *string); void SendByte(char byte); //___________________________ char StringP[] PROGMEM = " Hello_IN_FLASH"; int main(void) { char String[] = " Hello_IN_RAM"; char *u,*z; // Инициализация периферии UBRRL = LO(bauddivider); UBRRH = HI(bauddivider); UCSRA = 0; UCSRB = 1<<RXEN|1<<TXEN|0<<RXCIE|0<<TXCIE; UCSRC = 1<<URSEL|1<<UCSZ0|1<<UCSZ1; // Главный код u=String; //Копируем указатель на RAM z=StringP; // И он ничем не отличается от указателя на FLASH // И в том и в другом случае это двубайтный адрес. // Так что работа с ним одинаковая. Разница лишь в том Из какой памяти черпать эти // данные? Для этого и нужны все эти pgm_read_***. SendStr(u); //Печатаем данные по первому указателю SendStr(" Hello_INLINE_IN_RAM"); // Печатаем инлайную строку. Она копируется и в RAM // и во FLASH!!! Не оптимально!!! SendStr_P(z); // Печатаем по указателю из флеша SendStr_P(PSTR("Hello_INLINE_IN_FLASH")); // Инлайновая флеш строка SendStr_P(StringP); // Печатаем по прямому адресу строки. // Он ведь тоже такой же указатель как и u return 0; // Конец программы. } // Отправка строки void SendStr(char *string) { while (*string!='\0') { SendByte(*string); string++; } } // Отправка строки из флеша void SendStr_P(char *string) { while (pgm_read_byte(string)!='\0') { SendByte(pgm_read_byte(string)); string++; } } // Отправка одного символа void SendByte(char byte) { while(!(UCSRA & (1<<UDRE))); UDR=byte; } |
А если нам надо разместить в памяти здоровенный массив строк? Как быть? Обычный прикол вида:
1 2 3 4 5 6 7 8 | char *string_table[] = { "String 1", "String 2", "String 3", "String 4", "String 5" }; |
Для PROGMEM не прокатит. Тут надо делать в памяти отдельные стркои и увязывать их массивом укзателей.
1 2 3 4 5 6 | char MenuItem0[] PROGMEM = "Menu Item 0"; // Это наши строки char MenuItem1[] PROGMEM = "Menu Item 1"; char MenuItem2[] PROGMEM = "Menu Item 2"; // А это наш массив указателей. Обрати внимание, что тип такой же как у строк. char *MenuItemPointers[] PROGMEM = {MenuItem0, MenuItem1, MenuItem2}; |
Строки самые обычные. Их можно напечатать нашей функцией SendStr_P если скормить ей указатель(хотя бы имя строки, как мы это делали выше).
1 | SendStr_P(MenuItem0); |
И строка “Menu Item 0” улетит в UART. Но нам то надо совсем другое! Нам надо брать строки по индексам из списка MenuItemPointers.
Казалось бы, в чем проблема то? Берем да запихиваем в нашу функцию SendStr_P элемент массива MenuItemPointers[1].
1 | SendStr_P(MenuItemPointers[1]); |
Компилятор эту матрешку развернет и достанет из нее указатель на искомую строку и зашлет все в USART. Да не тут то было! Проблема опять в том, что MenuItemPointers опять лежит во флеше и так просто до ее элементов не добраться. Только через pgm_read_***.
В результате получаем такую вот загогулину:
1 | SendStr_P((char*)pgm_read_word(&(MenuItemPointers[1]))); |
А на самом деле все просто:
- Массив указателей лежит во флеше. Указатель это двубайтный адрес — word. Чтобы его достать оттуда нам нужна pgm_read_word которой мы скармливаем адрес ячейки MenuItemPointers[1].
- Сам заголовок массива MenuItemPointers это указатель в чистом виде, поэтому ему бы & не потребовался. Но вот конкретный элемент массива (в частности [1], хотя может быть и [i]) это уже переменная и на нее нужно узнавать адрес через &. Поэтому и делаем вычисление адреса
- pgm_read_word нам достает из флеша word который уже является адресом MenuItem1 в чистом виде. И можно было бы так и оставить, скормив его нашей функции SendStr_P. Она все равно ничего другого не ест. Но вот только компилятор тут дико взвоет Warning!!! ЧТО ЭТО ТЫ ТАМ ИЗ БЕЗДНЫ ДОСТАЛ??? ЙА БОЙУСЯЯЯЯ!!! А ты ему — “Не сцы, это наш старина однойбайтный указатель (char*)” и делаешь явное указание типа.
А целиком код выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | #include <avr/io.h> #include <avr/pgmspace.h> //Прототипы функций void SendStr(char *string); void SendStr_P(char *string); void SendByte(char byte); //___________________________ // Строки флеша размещать ЗА пределами main!!! char MenuItem0[] PROGMEM = "Menu Item 0"; char MenuItem1[] PROGMEM = "Menu Item 1"; char MenuItem2[] PROGMEM = "Menu Item 2"; char *MenuItemPointers[] PROGMEM = {MenuItem0, MenuItem1, MenuItem2}; int main(void) { // Инициализация периферии UBRRL = LO(bauddivider); UBRRH = HI(bauddivider); UCSRA = 0; UCSRB = 1<<RXEN|1<<TXEN|0<<RXCIE|0<<TXCIE; UCSRC = 1<<URSEL|1<<UCSZ0|1<<UCSZ1; // Главный код SendStr_P(MenuItem0); // Вывод строк по прямому адресу строки SendStr_P((char*)pgm_read_word(&(MenuItemPointers[1]))); // Вывод по таблице SendStr_P((char*)pgm_read_word(&(MenuItemPointers[2]))); return 0; } // Отправка строки void SendStr(char *string) { while (*string!='\0') { SendByte(*string); string++; } } // Отправка строки из флеша void SendStr_P(char *string) { while (pgm_read_byte(string)!='\0') { SendByte(pgm_read_byte(string)); string++; } } // Отправка одного символа void SendByte(char byte) { while(!(UCSRA & (1<<UDRE))); UDR=byte; } |
Ну как? Постиг Дзен Си? Казалось бы, все просто. Все логично. Однако, не зная с чего начать тут можн очень долго ходить кругами. А как хорошо на ассемблере — у нас есть указатель Z и забойная команда LPM. А дальше Ать-ать-ать и сами накруичиваем любые матрешки из вложенных подропрограмм, потрошащих флеш вдоль, поперек и по диагонали. Благодать!
Указатель может быть не только на данные, но и на функцию. Ведь что такое функция? Это всего лишь кусок кода с адресом во флеше. Не более того! А все вызовы функций, все эти myfunc(); это указание компилятору скакнуть через CALL/ICALL на адрес myfunc. Так что нам мешает сделать на этот адрес указатель, а потом по нему перейти? Правильно — ничего. Делаем:
1 2 3 4 5 6 7 8 9 10 11 12 13 | void func1(void) {;} void func2(void) {;} int main(void) { void (*f)(void); // указатель на функцию f = func1; f(); //выполнит функции func1() f = func2; f(); //теперь выполнит функцию func2() } |
Зачем такой изврат? Ну много применений можно придумать. Например, байт код виртуальной машины. Где у тебя есть скрипт в виде массива действий, где каждое действие это адрес на функцию это действо выполняющее. А твоя программа, виртуальная машина, хватает из массива адреса и переходит на функции. А те, в свою очередь, делают что то, меняют порядок действий (если нужно). В общем, программа в программе получается :)
Или, второй пример, очередь задач в диспетчере ОС. Где у нас не прямые вывозовы функций, а заброс их в конвеер, где они выполняются в порядке очереди. Об этом я еще расскажу после.
Комментарии
105 комментариев на «AVR. Учебный Курс. Программирование на Си. Работа с памятью, адреса и указатели»
Оставьте свой отзыв
Вы должны войти, чтобы оставлять комментарии.





Мля. Наверное ASM мой потолок. Что-то мне подсказывает, что СИ мне не освоить …
На самом деле Си проще изучается чем асм — более абстрактный. Тут просто иной стиль мышления. Перестраиваться с одного на другое сложно. А потом, когда вкуришь, то один фиг на чем писать.
Я надеюсь. Будем перестраивать мышление)))
А то ASM_а без Си, что Си без ASM_а …
Немного обновил пост. Добавил ассемблерные аналогии. Посмотри, может так будет понятней :)
Пасиба))))
Вот меня, почему-то, всё время такие заявления ассемблерщиков ставят в ступор. А иногда даже ассоциируются с киданием понтов. Если человек сумел освоить асм - что ему стоит писать на сях? Си ведь на порядок проще в использовании, на нём пишешь, как на естественном языке (по сравнению с асмом, который машинно-ориентирован, конечно).
Перейти с Си на асм намного сложней чем кажется. Даже я, зная Си с превеликими матюгами и спотыканиями сделал это.
Кому как, для меня ассемблер естественный язык :) Что написал - то и получил. А тут еще пойми что этому компилятору надо :)
В пространство:
– … Вот бы ещё яву к авркам прикрутили… И было бы объектно-ориентированное микро-счастье. :)
Я тут делаю небольшую такую виртуальную машину :)
Проще??? Кому как! Асм это конкретное пинание битов по регистрам. А СИ мне напоминает магию. Мне, как любителю, асм кажеться проще (с учётом того, что ничего сложного я и близко не делаю).
Конкретное пинание битов по регистрам подразумевает, что Вы знаете какие биты в каких регистрах за что отвечают, а также знаете какими инструкциями это всё пинать и для чего. Для того, что бы нормально писать на асме нужно знать хотя бы пол-сотни инструкций (а лучше все полторы сотни) и аспекты их применения, архитектуру конкретного процессора, структуру памяти, внутреннюю организацию программы и прочие мелочи. Когда пишешь на ЯВУ, то добрую половину этого берёт на себя компилятор, а на тебя остаётся только работа с переферией и общий алгоритм.
Собственно, я к чему - я вовсе не хочу сказать, что если человек пишет на асме, то он автоматом может сходу начать писать на C; нет, я хочу сказать, что если у человека достаточно мозгов что бы писать на асме, то значит у него вполне достаточно мозгов и для того, что бы освоить C - планка установлена ниже. Ну, может, нужно чуть больше абстрактного мышления, что бы понять теже указатели, или структуры, но не сильно - все категории языка C весьма логичны и интуитивны (чего не скажешь, например, о C++ - там уровень абстрактного мышления должен быть уже на порядок выше).
Это пол сотни простых, как напильник, инструкций. У них никаких подводных камней и заморочек. И их не надо учить, достаточно перед тем как что либо сделать пробежать глазами по списку инструкций, чтобы выбрать нужную.
Поэтому на асме легче понимается что у тебя происходит в коде. Т.е. у тебя либо все понятно и это работает. Либо не понятно и не работает. Другого не дано. Тут нет места магии :)
Асм проще чем С (или, тем более, С++). Да, в асме много справочных данных и гораздо больше писанины, но в изучении он гораздо проще.
Ты как всегда все отлично расписал! Читаю и радуюсь что такой доступный материал теперь есть в свободном доступе. Один моментик: позволю себе покритиковать - на мой взгляд самое удачное в указателях - кастинг типов… Достаточно странно для понимания, особенно в первые разы, но когда вкуриваешь КАК то начинаешь любить Си и появляется отвращение к Делфи например, где работа с указателями возможна, но неудобно там все сделано.
Ну я не говорил что это плохо. Типизация указателей это величайший рулез!
Я тоже не говорил - просто ты этот момент не описал, а вещь достаточно серъезная и мощная :) Но я на этом умолкаю. Сорри за критику ))
P.S. Тоже ночами не спишь?
Опять режим меняю. Потому и не сплю. :)
А в делфе почти так же всё, только местами задом наперед по отношению к си.
delphi:
a : integer; // переменная - целое число
b : ^integer; // указатель на переменную целого числа
b := @a; // или ADDR(a) или ещё пару способов получения указателя
b^ := 12345; // залезли внутрь указателя (записать в ‘a’ через бэ)
c:
int a;
int *b;
b = &a;
*b = 12345;
Те же йайца, только сбоку. И снова 4 строчки.
dword a;
dword b;
mov b, offset a
mov dword [b], 12345
(asm x86)
Паскалистам (ака Дэльфятникам) проще понять Гарвардскую архитектуру памяти, чем Сишникам. Си изначально написан на втыкание переменных не задумываясь о том в коде он идет или за кодом. А подход к поинтерам (указателям) тот же что и в Си. Легкое отличие в синтаксисе вводит в заблуждение и людям которые всегда пользуются только одним языком кажется, что в другом языке все неправильно и что там неудобно.
В каком смысле кастинг типов? Типизированные указатели в дельфи есть. Вот арифметика указателей в дельфи сделана неочевидно - при желании делать нечто в духе a=*(p+10) указатель в дельфи нужно привести к указателю на массив требуемого типа, например:
p: PIntegerArray; //а не Pointer или ^Integer
a:=p[10];
PSomethingArray объявляется как указатель на массив Something нулевой длины (в этом случае придется отключить проверку на выход за границы массива) или размером в 2ГБ-1 (предел для дельфи32).
Сделано так для соответсвия pascal way.
А вообще-то я сам матерился, когда не знал как это правильно делается и писал здоровенные строчки из приведения указателя к целому, арифметики и приведения обратно.
Он имел ввиду скорей всего приведение указателей к разным типам, дабы компилятор не матерился. Я то на это по привычке ложу болт :)
В этом разницы практически нет. В C - (byte*)p, в дельфи - PByte(p) (не помню, можно ли написать ^Byte(p) или обязательно нужно объявить тип указателя на требуемые данные, впрочем для стандартных типы PSomething и PSomethingArray уже есть в RTL).
Это что! Пришлось делать один проект на делфях (у заказчика на них все было написано, мой модуль интегрировался на уровне кодов). Обнаружил в делфяках жуткую вешь:
m: Array [0..0] of BYTE; // ужасть! никогда б не подумал, что ТАК можно объявлять указатель!
pw: ^WORD;
pc: ~BYTE;
…
GetMem(m, ARRAY_MAX_SIZE); // выделяем память под массив
…
m[0]=$55; m[1]:= $aa; m[2] := $22; m[3] := $33; // не суть
pc := @m[0]; // получили указатель на первый (по счету) элемент массива
pw := WORD(pc); // не помню уже точно, как приводятся типы в делфях, но суть в копировании указателей.
так вот.. ^pw == 0×0055!!!! даже при разыменовании указателя! Пипец и создание вместо одной строчки пяти, в одной из которых слово собирается из байтов: w := b0 + (b1 shl 8);
О_О
Надо было не городить фигню со сбором байтов, а матчасть подучить.
Судя по всему было включено выравнивание элемента на границу слова или двойного слова.
var m: packed array [0..0] of byte;спас бы отца русской демократии.
А ещё лучше было сделать m типа PByte
хех.. ну да, такое возможно. Пример писАл по памяти, сейчас поднял-таки исходник и вот что выяснилось:
function checksumm(buf: PChar, zise: integer): WORD;
begin
//тут тело функции (контрольная сумма в протоколе IP);
end;
то, что передавалось в функцию было выровнено на границы байтов. И уже в функции надо было работать с массивом и как с байтовым и как с массивом слов.
Я пока не уяснил для себя, когда учил С, что “указатель - это переменная, хранящая адрес”, не втек в смысл указателей. Было бы здорово, если б ты это прямо написал, хотя из твоих слов это следует, конечно. Но иногда для понимания приходится идти от вывода к рассуждениям, т.е. наоборот.
Побольше практических применений указателей бы…
А так хоть буду экономить память с помощью pgmspace.h :) thx!
Ну потом, в дальнийших примерах я буду это часто использовать.
когда-то это было выше моего понимания, но училка у нас в пту вполне толково рассказала
зы
подкидываю тему для следущей статьи - пищание динамиком на простом выводе и через шим
Не, рано браться за периферию, коль скелета нету. Сначала про организацию программ вообще.
>>Чтобы было проще для понимания для ассемблерщиков я буду давать ассемблерную аналогию работы с указателем.
Моя имха - ассемблирщикам и так понятно как никому. Ко мне вот как раз после практики на Асме пришло понимание указателей (как в Си так и на ООП в целом), т.к. реально начинаешь понимать что просиходит внутри при работе с указателями и классами. До этого возможно только чисто “энциклопедическое” (как изьъяснения с иностранцем по разговорнику-путеводителю) использование средств ооп.
Ну не скажи. Да, ассемблерщик все эти адресные замуты вдоль и поперек знает. Но вот пока ему не скажешь, что указатель это косвенная адресация и не более может тупить. По крайней мере я долго тупил на указателях, пока не достал дизассемблер и не посмотрел что же это такое. Увидев косвенные обращения сразу все понял :)
Так и я тупил, пока не достал дизаассемблер.
Но прежде был по-любому просто ассемблер :)
Ну еще неплохо бы упомянуть, что могут быть указатели на указатели (для работы с массивом строк часто требуется)
Да много чего еще можно упомянуть. Массивы, таблицы переходов. Математическое вычисление прееходов. Возвращаемое значение в виде указателя. Всего не опишешь. Так что я планирую когда этот материал станет рутинным и привычным выдать вторую статью по укзателям.
В статье есть ошибка:
char String[] PROGMEM = “Hello in FLASH Pinboard User”;
объявляет указатель на char со спецификатором PROGMEM(причём спецификатор указан после имени переменной, что тоже не правильно) и записывает в него указатель на строку в обычной памяти.
Правильный код:
PROGMEM char String[] = PSTR(”Hello in FLASH Pinboard User”);
Собственно из-за этого такой код внутри main и не работал.
Вообще PROGMEM - это спецификатор для типа переменной(то есть её тип - PROGMEM char*), поэтому его нужно указывать везде где используется строка из программной памяти. В том числе в аргументах функции - вместо
void SendStr_P(char *string)
должно быть
void SendStr_P(PROGMEM char *string)
Кстати можно объявить и тип для PROGMEM char *
typedef PROGMEM char *progstr
И ещё, тип указателя вообще-то нужен для того, чтобы знать тип переменной на которую он указывает и соответсвенно использовать его во всех обращениях к ней.
Например объявление указателя на структуру
struct {
int a,b;
} *ptr1;
позволит обращаться к её членам: ptr1->a и prt1->b, и при этом не даст обратиться к ней как к числу - *ptr1 = 5 работать не будет. А если это очень нужно, можно сделать так: (*(char *)ptr1) = 5 .
И ещё добавлю, что код
volatile unsigned char i,z;
unsigned char *u;
u=&i;
так же не совсем правильный - надо или
volatile unsigned char i,z;
volatile unsigned char *u; //volitile относится к типу под указателем, а не к самой переменной
u=&i;
или
volatile unsigned char z;
unsigned char i;
unsigned char *u;
u=&i;
или если так нужно, чтобы i была volatile, а u указывал на не-volatile переменную то можно и так
volatile unsigned char i,z;
unsigned char *u;
u=(unsigned char *)&i;
Вообще такая проверка типов переменных(которой в принципе нет в assembler’е) нужна для того, чтобы не делать ошибок связанных с типами(как например *ptr1 = 5 или ptr1 = 5 вместо ptr1->b = 5), а не для того, чтобы мешаться программисту.
Ну за основу бралась документация на WinAVR
http://www.nongnu.org/avr-libc/user-manual/pgmspace.html
где PROGMEM был именно после имени переменной. И везде где бы я не находил примеры работы с флешем было именно так.
И когда он не в main то записывает именно строку из флеша, не копируя ее в RAM.
“И ещё, тип указателя вообще-то нужен для того, чтобы знать тип переменной на которую он указывает и соответсвенно использовать его во всех обращениях к ней.”
Во, ценное дополнение. Сейчас впишу. Совсем забыл. Мне, как ассемблерщику, обычно до лампочки на тип данных =)
>Ну за основу бралась документация на WinAVR
> http://www.nongnu.org/avr-libc/user-manual/pgmspace.html
>где PROGMEM был именно после имени переменной. И везде где бы я не находил >примеры работы с флешем было именно так.
PROGMEM - это __attribute__((__progmem__)) , а __attribute__ так же как и volatile, const и другие спецификаторы принято писать перед типом.
>И когда он не в main то записывает именно строку из флеша, не копируя ее в RAM.
По-видимому это какое-то расширение языка C в GCC - вообще тип переменной не должен влиять на константу(любая строка в C, записанная таким способом, - это указатель на const char *), которая в неё записывается. И это расширение работает только с глобальными переменными.
“другие спецификаторы принято писать перед типом”
Возможно, однако в WinAVR почему то принято иначе. :/ Иного я даже и не встречал.
Ваш же вариант:
PROGMEM char StringP[] = PSTR(”Hello in FLASH Pinboard User”);
даже компилиться не захотел. Заявляя что:
Pinboard_1.c:26: error: invalid initializer
Убрав PSTR оно скомпилилось. Но появился варнинг
../Pinboard_1.c:26: warning: ‘__progmem__’ attribute ignored
Свидетельствующий о том, что на прогмем мы забили и пихнули все в рам.
PSTR(s) объявлен как ((const PROGMEM char *)(s)), так что здесь забыт const, который показывает, что переменную под указателем менять нельзя(только чтение).
Добавление const не помогает. Ругается на то же самое. Убрав PSTR получаем опять же игнор progmem и строку в RAM. Еще варианты?
Как выяснелось надо использовать тип prog_char:
prog_char *a=PSTR(”01234567890123456789″);
который в свою очередь определён через typedef(причём без const):
typedef char PROGMEM prog_char;
Без этого __attribute(__progmem__) по-видимому(точнее можно узнать только в исходниках GCC - это open-source, так что нормальной документации не прилагается) относится к самой переменной указателя(которая является локальной и быть в программной памяти не может и соответственно отбрасывается).
Ещё один способ изменить приоритеты - поставить __attribute__ после *(например, если сonst указан между * и именем, то он относится к самой переменной, а не к указателю):
char * PROGMEM a=PSTR(”01234567890123456789″);
Кстати у меня на char * a=PSTR(”01234567890123456789″); ошибки почему-то не возникала(версия WinAVR - 20081205, то есть предпоследняя).
Аналогично:
char * a=PSTR(”01234567890123456789″);
компилится и нормально работает (т.е во флеше, где и положено ей быть). Версия последняя.
А вот со стрингом такое уже не прокатывает. Хотя стринг это тот же указатель.
PSTR разворачивается в итоге в:
PSTR(s)(__extension__({static char __c[] PROGMEM = (s); &__c[0];})
Вот только одного понять не могу. Какой смысл указывать спецификатор прогмем в функциях?
Ведь в функцию передается просто адрес. И не важно какой он и откуда, главное его тип (дискретность). А так что там, что тут он двухбайтный. И лишь при взятии адреса и укладке данных во флеш мы должны иметь ввиду откуда мы его берем. Функции же пофиг что на нее засунули — ее процедура pgm_read_*** иного и не считает.
Компилятор на это даже варнинг не дает. Соответственно о том что это защищает от ошибок говорить не приходится - все в голове надо держать в любом случае.
В C в отличии от assembler’а есть проверка типов, так что PROGMEM нужно указывать, чтобы было видно(в том числе компилятору, хотя в GCC такое нормально не сделано) что это ссылка на программную память и обращаться к ней *имя переменной нельзя и в нормальном компиляторе это должно вызвать ошибку(в GCC, который как видно, был наскоро переделан под AVR энтузиастами этого нет).
И ещё заменил:
char String[] = ” Hello_IN_RAM”;
char *u,*z;
u=&String;
Тоже неправильный(объявления char String[] и char *String - это одно и то же)
А так же
*u++ - это сначала разименовать указатель, а затем увеличить его на 1(и, между прочим, если результат разименовывыния нигде не используется, то чтения и не произоидёт)
Или на асме:
LD R17,Z
SUBI ZL, Low(-1)
SUBCI ZH, High(-1)
Ну и ещё вместо условия (переменная!=”) принято записывать (переменная).
И переменные можно инициализовать в той же строке: unsigned char *u=&i;
“объявления char String[] и char *String”
О! Точняк, чет я затупил тут мощно. Работать то оно работает, но меня варнингом кормит, что дескать типы не совместимые.
“Ну и ещё вместо условия (переменная!=”) принято записывать (переменная).
И переменные можно инициализовать в той же строке: unsigned char *u=&i;”
Я для наглядности стараюсь сейчас писать код пусть и более густой, но зато понятней для не посвященных.
*u++ - это сначала разименовать указатель, а затем увеличить его на 1.
Тут просто идет его увеличение на 1. Даже без чтения. Т.к. чтения по факту и не требуется.
А вот в статье строчка LD R17,Z есть и стоит не там, где ей положено.
Я там просто предположил возможное действие компилятора. Как бессмысленный забор значения без дальнейшего действа с ним.
И, кстати, код
u=u*z+j;
работать не должен(по крайней мере по стандарту) - указатель это не число и умножать его нельзя. А если очень нужно, то надо привести его к типу int:
u=(char*)(((int)u)*z+j);
Конечно громоздко, но указатели очень редко(я не могу вспомнить не одного случая, так как адрес в указателе может меняться при добавлении нового кода) нужно умножать и вообще производить с ними другие операции кроме сложения с числом и вычитания числа.
“Адрес в указателе может меняться при добавлении нового кода”
Ну так при компиляции и линковке он же все равно пересчитывается заново, скольк бы там кода ни добавилось.
“я не могу вспомнить не одного случая”
Да хотя бы потрошить здоровеннный многомерный массив. Впрочем да, вы правы. Тут лучше предварительно вычислить смещение, а потом сложить с указателем.
В случае многомерного (для примера двухмерного) массива арифметика чуть другая:
u=u+i*width+j
Умножение указателя таки бессмысленно.
Впрочем, учитывая C way, операция умножения применительно к указателю вполне может означать что-нибудь совершенно другое.
В C он не обозначает ничего(будет ошибка) - в нём операции могут делать только один тип действия(арефметические - только с числами, сложение, вычитание и сравнение - ещё и с указателем и целым числом, логические, сдвиги и взятие по модулю - с целыми числами, взятие указателя & - только с переменной, разименовывание * - только с указателем, условный оператор ?: - с числом-условием и любыми двумя одинаковыми типами значений, оператор , - с чем угодно). Только умножение и разъименовывание имеют одинаковое обозначение.
А вот в C++ для многих типов(а вот для двух фундаментальных типов - чисел или указателей - нельзя) сделать перегрузку оператора:
struct1 operator+(const struct1 &a,const struct1 &b);
позволит складывать между собой две переменных типа struct1
Отсюда появляются такие конструкции:
std:cout << “Some value:” << a << endl
std::string s1 = “abc”;
s1+=”def”;
и
std::map map1;
map1["Text"]++;
Ну в принципе да, С менее извращенный :) Хотя извращенность С++ позволяет делать очень интересные вещи, выносящие моск кому угодно, кроме Александреску :)
Впрочем, я дельфист и различия С/С++ кроме ООП не очень помню.
>Хотя извращенность С++ позволяет делать очень интересные вещи, выносящие моск кому угодно, кроме Александреску :)
Если с этими вещами разобраться, то они не кажутся извращёнными. Напрмер перегрузка операторов - довольно удобная вешь, если пользоваться ей правильно. А шаблоны - одна из важнейших возможностей C++(и кстати одна из причин по которой я перешёл с Delphi на C++).
Те примеры, которые я привёл в предыдущем посте - это нормальный C++ код.
>Впрочем, я дельфист и различия С/С++ кроме ООП не очень помню.
Много ещё чего по-мелочи, например спецификатор const и возможность задания переменных не только в начале блока, а так же в циклах(for (int a=0;a<10;a++)). Хотя некоторые компиляторы позволяют использовать их и в C.
имхо, тогда уж
u=first+i*width+j;
(где first - указатель на первый элемент массива)
А, ну и самое главное - указатели на функции.
void foo1(void){;}
void foo2(void){;}
int main(void){
void (*f)(void); // указатель на функцию
f = foo1;
f(); //выполнит функции foo1()
f = foo2;
f(); //теперь выполнит функцию foo2()
}
А смысл?
Еще какой! Например диспетчер задач через такие указатели работает.
Пардон, применить точно можно.
Я почему-то подумал - зачем ссылаться вручную на свои же статические процедуры, разве что для хитрых “эмуляций” классов и всяких там инъекций в код с хитрыми вывертами.
А в диспетчере задач где применяется? Что-то не догоняю.
Ну например, есть у нас очередь выполянемых задач. Которая представляет собой строку из указателей на задачи. Диспетчер хватает эти указатели и переходит по ним.
Точно точно.
Те же вызовы библиотек так происходят.
Это меня “зациклило” в рамках одного модуля, где такие финты без надобности в общем.
Это просто пример был, конешн как в примере ссылаться на функции смысла не имеет. А вот например когда указатель на функцию возвращает какая-то другая функция - уже имеет. т.е.
f = foo();
f();
Модель с событиями как раз опирается, на указатели на функции.
Да, в планировщике тоже нужны.
Часто таблицу указателей на функции (массив указателей) удобно использовать для реализации конечных автоматов:
// тит указателя на функции вида void func(void)
typedef void (*f)(void) to_func_t;
// прототипы функций, вызываемых при реакции на события
void event1(void);
void event2(void);
…
void eventN(void);
// массив указателей на функции - обработчики событий
to_func_t const events[] = { &event1, &event2, … &eventN };
uint8 event; // номер события
uint8 state; // текущее состояние автомата
…
main(void) {
…
while(1) {
event = detectEvent(); // тут определяется очередное событие
to_func_t pf = events[event];
(pf)(); // вызов обработчика
}
return 0;
}
на самом деле,я тут не показал как происходит изменение состояния автомата при переходах. Я часто делаю это непосредственно в функциях-обработчиках.
Или для виртуальной машины. Тоже, кстати, удобно. Гоним байт код в наш обработчик и по смещению вытаскиваем функции.
Кстати сказать в нашем случае виртуальная машины никакая не виртуальная, а вполне реальная, просто немного софтварная
Можно пару придирок?
Ну, во-первых, снова трындю на тему WinAVR. Насколько я понимаю, цикл статей ориентирован на “непосвящённых”. Так вот, непосвящённые юниксоиды могут не понять, что речь идёт про простой gcc и avr-libc. =)
Во-вторых, раз мы пишем про строки в прогмеме, то было бы неплохо упомянуть про массивы строк в прогмеме, хотя бы в виде ссылки на документацию, а то люди начнут делать по аналогии и будут ломать голову, почему это строка работает, а массив строк - нет.
Ну и в-третьих, зачем изобретать неких тип u16? Чем не устраивают стандартные предопределённые типы uint16_t?
А тут возможны варианты??? Вроде как речь идет о AVR и только об AVR.
Да, можно. Видимо понадобится еще одна статья про указатели. Т.к. есть еще немало что сказать.
u08 и u16 это уже ставший почти стандартом тип данных для проектов на avr-libc. Почти вся библиотека
http://www.procyonengineering.com/avr/avrlib/index.html
и чуть более чем все проекты под WinAVR оперируют именно такой формой типов.
А uint16_t практически не встречается. Почему? Видимо лень писать лишние буковки.
Ну AVR-то да, он один, а вот компиляторов для него явно больше одного. Многие, кто используют winavr в сочетании с AvrStudio даже не подозреваются, что это фактически тот же самый gcc, который, например, в поставку почти любой юниксовой системы.
Про u08 и u16 первый раз слышу - надо будет повнимательнее изучить этот вопрос… А (u)int(…)_t - это часть стандарта языка C, так же как size_t, time_t, ptr_t. Их удобно использовать хотя бы потому, что многие редакторы подсвечивают их как встроенные типы.
Типы (u)int(размер)_t - это стандартные типы только в Linux(и объявлены они в его заголовочном файле stdint.h ). Другие компиляторы такие типы не понимают. В Windows, например, используются типы CHAR, WORD, DWORD, UINT и т.д.
В стандарте C указаны типы char, short и long, к которым может быть добавлено signed или unsigned(short и long по умолчанию - signed, а вот char - в зависимости от реализации компилятора) и ничего не значащий int - unsigned int = unsigned, signed int = signed, short int = short(по крайней мере по стандарту, хотя некоторые компиляторы считают по другому). Так же есть типы с плавающей точкой - float, double и long double.
Кстати в AVR GCC можно изменить размер int с 16бит на 8(и соответсвенно short - с 16 на 8,long с 32 на 16, long long - с 64 на 32) параметром -mint8. При этом в качестве типа констант по умолчанию GCC будет использовать int размером 8бит, а не 16, что заметно уменьшит размер кода(это вообще-то говоря неправильно, так как целочисленная константа при задании имеет тип в котором она будет использоваться дальше).
>>Типы (u)int(размер)_t - это стандартные типы только в Linux(и объявлены они в его заголовочном файле stdint.h ).
Простите, но ЭТО бред. Во-первых, не только в Linux, но и, как минимум в FreeBSD, MacOS, Windows и AVR. Заметьте, я перечислил лишь те платформы, на которых я лично писал софт и использовал эти типы. Проблемы были только с IAR и то только пока не подключишь нормальный CLIB, вместо кастрированного DLIB (или как они там? за верность не ручаюсь, давно это было).
Как я понял из ваших комментов в этом треде, Вы позиционируете себя, как программиста хорошо знающего стандарт С. Если я прав, то я вынужден спросить, каким местом Вы читали стандарты и какие именно?
Следуя вашей логике, вслед за “типами (u)int(размер)_t” к нестандартным Linux-only поделям следует отнести различные функции, типа printf/scanf, str*, malloc/free и тд. - они же тоже объявлены где-то в заголовочных файлах!
Вся хитрость в том, что в стандартах описывается не только сам язык (то, что реализовано в компиляторе), но и набор стандартных библиотек. В частности, типы данных делятся на строенные и стандартные. Первые зашиты в компилятор, а вторые - объявляются в стандартных библиотеках, но и те и другие являются стандартными.
А то, что некоторые до сих пор не до конца поддерживают стандарты - это их личные половые проблемы (то, что многие компиляторы до сих пор не реализовали поддержку стандарта C99 не означает, что C99 - это нестандартное расширение языка =)).
>Во-первых, не только в Linux, но и, как минимум в FreeBSD, MacOS, Windows и AVR. Заметьте, я перечислил лишь те платформы, на которых я лично писал софт и использовал эти типы. Проблемы были только с IAR и то только пока не подключишь нормальный CLIB, вместо кастрированного DLIB (или как они там? за верность не ручаюсь, давно это было).
Во-первых стоит указывать не платформы, а компиляторы(или, как я указал, набор заголовочных файлов API), например в Microsoft Visual C, который является самым распространённым компилятором для Windows, типов (u)int***_t нет.
>Следуя вашей логике, вслед за “типами (u)int(размер)_t” к нестандартным Linux-only поделям следует отнести различные функции, типа printf/scanf, str*, malloc/free и тд. - они же тоже объявлены где-то в заголовочных файлах!
Я этого не говорил. Эти части стандартной библиотеки есть везде, так что при переходе с одного компилятора на другой проблем с совместимостью не возникнет.
Хотя на младших AVR многие из них использовать не стоит, так как они совершенно не оптимизированы под них(например printf).
>А то, что некоторые до сих пор не до конца поддерживают стандарты - это их личные половые проблемы (то, что многие компиляторы до сих пор не реализовали поддержку стандарта C99 не означает, что C99 - это нестандартное расширение языка =)).
То что значительная часть компиляторы не имеет поддержки C99 обозначает что этот стандарт не является общепринятым. Поэтому не нужно говорить, что uint08_t лучше u08 и т.п.
GCC поддерживает, Open Watcom поддерживает, Sun Studio поддерживает, intel поддерживает. Какой из компиляторов значительной части я забыл? MS?
Ну так MS равно как и Borland сосредоточены на C++, а на ansi C они глубоко забили, и не рекомендуют его использовать. Поэтому насчет популярности именно ansi С компилятора от MS я готов поспорить.
>Какой из компиляторов значительной части я забыл? MS?
Да, MSVC и C++ Builder. Этого достаточно.
>Ну так MS равно как и Borland сосредоточены на C++, а на ansi C они глубоко забили, и не рекомендуют его использовать.
MS вполне нормально поддерживает свой C компилятор(между прочим Windows они именно им и компилят). Да и в C++, где C код нормально компилиться этого нет.
А компилятор С++ и не должен поддерживать С99, хотя бы потому, что отпочковался сильно раньше.
MSVC и С++ Builder это не С компиляторы. И нормально они компилят только С89. Писать на С и не использовать С99 уже довольно глупо.
Думаю Ваш, процессор, например, помимо инструкций, что были в 8088 использует и MMX,SSE и пр. Не то, чтобы без них можно было бы обойтись, но удобней
Ну вот, добавил и про прогмем. Глянь свежим взглядом. А то у меня уже глаз замылился.
Хорошо написано - кратко и ясно. В офф. доках запутаннее.
Кстати, по теме: у меня тоже недавно была заметка про указатели в контексте оптимизации - http://gremlinable.livejournal.com/11757.html
обязательно надо разжевать про массив указателей на строки в PROGMEM, который сам находится в PROGMEM, и как с ним работать. Помню, много времени убил )
Добавил. Оцени креатиф :)
Это ещё ничего по сравнению с тем, как на тиньках иногда работает оптимизатор:
В таком коде
unsigned char val2=0;
for (;val>=10;val-=10,val2++);
он сначала догадался, что цикл - это деление на 10 и получение остатка от деления, и заменил его операциями деления и умножения(которые являются функицями, так как аппаратного умножения нет), а затем сделал эту функцию инлайновой(кроме этой строки там было ещё несколько строк, в том числе работа с 16-битным числом) и вставил её в пять мест. В результате успешно потратил больше четверти из 2кб.
Вообще оптимизатор на тиньках любит генерить кривой код при умножении(даже неявном типа a<<5+a<<3+a<<2, которое он преобразует в явное), делении и работе с 16-битными числами(особенно, если он умножает или делит 16-битное число). А если int - 16-битный(по умолчанию это так), то таких мест становится заметно больше.
Кстати после утрамбовывания кода(а таких мест набралось 2 или 3 и ещё столько же пока не трогал) ещё осталось около сотни байт места, а всё что я хотел сделать было сделано.
Оптимизация небось по скорости была? ))
По размеру. Но раскрутка циклов и вставка функций умножения и деления(без раскрутки их циклов, так как они написаны на ассемблере) от этого не зависят.
Просто оптимизатор не знает, что таких инструкций нет, а затем на стадии ассемблирования эти операции заменяются на немаленькие конструкции. Если внимательно посмотреть на ассемблерный листинг, там хотошо видно что в промежуточном коде это были элементарные операции, которые затем были заменены заранее написанными блоками.
Отлично :)
Есть небольшое методологическое предложение. В принципе, когда освоил, что это все указатели, только на разные области памяти, то все становится понятно. Это для тех, кто идет от асма к сям. А для сишников, я бы все-таки разделил эти два типа указателей. Не зря же ввели дефиницию PGM_P. То есть везде, где приводишь к (char *) я бы, исключительно в целях обучения, приводил бы к (PGM_P). Например:
PGM_P string_table[] PROGMEM =
{
string_1,
string_2,
string_3,
string_4,
string_5
};
void foo(void)
{
char buffer[10];
for (unsigned char i = 0; i < 5; i++)
{
strcpy_P(buffer, (PGM_P)pgm_read_word(&(string_table[i])));
// Display buffer on LCD.
…
}
return;
}
Можно и так, только тогда SendStr_P надо будет определить как PGM_P. Я пошел по принципу минимальных изменений :)
SUBI ZL, Low(-1) ; Z++;
SBCI ZH, High(-1) ; Инкремент указателя
LD R17,Z ; и что дальше?
А почему вычитается -1, а не прибавляется 1 ? В этом есть какой-то таинственный смысл?
SUBI это команда вычитания 1-(-1)=2 ;)
Это понятно…А почему нельзя INC ZL или ADIW ZL,1 ?
С INC ZL мы потеряем перенос.
А ADIW есть не на всех AVR
Понятно. Спасибо! А сложения с переносом с константой нету в AVR, наверно, потому что RISC, да?
А нафига? Если сложение легко делается через вычитание?
Вообще, если посмотришь машинные коды команд, то узнаешь что на самом деле чуть ли не треть инструкций AVR в природе не существует. Это лишь разные мнемоники одного и того же :)
Вот это неожиданно для меня! =)
Уважаемый DI HALT! давно читаю ваш сайт и очень многое нахожу полезного в ваших трудах!
Респект вам и уважуха!!
у меня и моего сына появилась идея собрать бегущую строку на светодиодах, перерыв весь инет нормальных схем ненашол , есть одна гуляюшая по инету только хозяин неотвечает чтоб купить у него прошивку пика.
Не могли бы вы в очередных ваших статьях описать сие чудо!
С уважением и надеждой Денис.
Пока нет особого желания. Толку от нее немного. =)
Если скажешь какое разрешение строки нужно, то могу подсказать по реализации.
примерно 8на 96
думаю тут оптимально будет делать матрицы 8 на 8 по два сдвиговых на каждую и дрючить их в динамической индикации. Т.е. переключая байт поочерди зажигать строки. Яркости должно хватит.
Сами сдвиговые регистры грузить контроллером. Вот только не знаю как тут со скоростью.
Эта реализация сложна софтверная, но не требует большого числа элементов.
Вариант второй проще намного. Тут мы имеем 8 цепочек из сдвиговых регистров (по одной цепочке на строку, и по 12 регистров в цепочку). Всего 96 регистров по 5 рублей за каждый. Тогда их можно грузить напрямую из порта МК. Прошивка будет очень простой в реализации. Вполне хватит какой нибудь тини2313
Понадобилось нам сделать станок с ЧПУ. Электронную часть сделал я, а программу должен был написать программист. Но оказался раздолбаем. Пришлось мне самому, на старости лет, разбираться с программированием. В общем с ASMoм разобрался за неделю, пока в голове что-то отложилось и стуктурировалось. В итоге программа написана, все работает. Но вот сколько ни пытался разобраться с СИ - темный лес. Сложно въехать даже в саму структуру программы, где там все эти циклы бегают? Да и вас тут всех почитал сейчас - у каждого своя интерпретация написанного кода. Т.е. похоже никакой конкретики в СИ нет. И так может быть и эдак. Напрашивается вывод, если нет необходимости иметь выигрыш во времени, который дает СИ ( что тоже сомнительно для непрофи), то человеку, не живущему программированием, за глаза хватит простого в освоении асма. Как я понял, кроме логики пишущейся программы, надо еще постоянно держать в голове загадочную логику самого Си, кучу понятий, определений и как все это между собой взаимодействует.
На асме, если написать нормальные макросы и функции, тоже можно писать практически литературным языком. Вот только с математикой головняк. Что есть, то есть.
P.S. Программист, который меня вынудил асм изучать, с WinAVR после асма разбирался полгода. Правда к асму возвращаться теперь не хочет.)))
Ну да. Си сложней для понимания если его не знать до этого. Простую автоматику на асме писать ничуть не сложней чем на Си, а отлаживать проще.