AVR. Учебный Курс. Программирование на Си. Работа с памятью, адреса и указателиPrint This Post

Автор DI HALT
Опубликовано 09 Янв 2010 
Рубрики: AVR. Учебный курс
Метки: , , ,

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

Итак, начну по порядку.
Инфа, любая инфа (команды, данные) лежит в памяти по ячейкам. У каждой ячейки есть порядковый номер — адрес.

Мы можем напрямую сказать процессору — возьми данные из ячейки с адресом 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])));

А на самом деле все просто:

А целиком код выглядит так:

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. Учебный Курс. Программирование на Си. Работа с памятью, адреса и указатели»


  1. KeFiR 09 Янв 2010 2:08

    Мля. Наверное ASM мой потолок. Что-то мне подсказывает, что СИ мне не освоить …

    DI HALT

    На самом деле Си проще изучается чем асм — более абстрактный. Тут просто иной стиль мышления. Перестраиваться с одного на другое сложно. А потом, когда вкуришь, то один фиг на чем писать.

    KeFiR

    Я надеюсь. Будем перестраивать мышление)))
    А то ASM_а без Си, что Си без ASM_а …

    DI HALT

    Немного обновил пост. Добавил ассемблерные аналогии. Посмотри, может так будет понятней :)

    KeFiR

    Пасиба))))

    gremlinable.livejournal.com

    Вот меня, почему-то, всё время такие заявления ассемблерщиков ставят в ступор. А иногда даже ассоциируются с киданием понтов. Если человек сумел освоить асм - что ему стоит писать на сях? Си ведь на порядок проще в использовании, на нём пишешь, как на естественном языке (по сравнению с асмом, который машинно-ориентирован, конечно).

    DI HALT

    Перейти с Си на асм намного сложней чем кажется. Даже я, зная Си с превеликими матюгами и спотыканиями сделал это.

    Кому как, для меня ассемблер естественный язык :) Что написал - то и получил. А тут еще пойми что этому компилятору надо :)

    nathos

    В пространство:
    – … Вот бы ещё яву к авркам прикрутили… И было бы объектно-ориентированное микро-счастье. :)

    DI HALT

    Я тут делаю небольшую такую виртуальную машину :)

    KeFiR

    Проще??? Кому как! Асм это конкретное пинание битов по регистрам. А СИ мне напоминает магию. Мне, как любителю, асм кажеться проще (с учётом того, что ничего сложного я и близко не делаю).

    gremlinable.livejournal.com

    Конкретное пинание битов по регистрам подразумевает, что Вы знаете какие биты в каких регистрах за что отвечают, а также знаете какими инструкциями это всё пинать и для чего. Для того, что бы нормально писать на асме нужно знать хотя бы пол-сотни инструкций (а лучше все полторы сотни) и аспекты их применения, архитектуру конкретного процессора, структуру памяти, внутреннюю организацию программы и прочие мелочи. Когда пишешь на ЯВУ, то добрую половину этого берёт на себя компилятор, а на тебя остаётся только работа с переферией и общий алгоритм.
    Собственно, я к чему - я вовсе не хочу сказать, что если человек пишет на асме, то он автоматом может сходу начать писать на C; нет, я хочу сказать, что если у человека достаточно мозгов что бы писать на асме, то значит у него вполне достаточно мозгов и для того, что бы освоить C - планка установлена ниже. Ну, может, нужно чуть больше абстрактного мышления, что бы понять теже указатели, или структуры, но не сильно - все категории языка C весьма логичны и интуитивны (чего не скажешь, например, о C++ - там уровень абстрактного мышления должен быть уже на порядок выше).

    DI HALT

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

    Поэтому на асме легче понимается что у тебя происходит в коде. Т.е. у тебя либо все понятно и это работает. Либо не понятно и не работает. Другого не дано. Тут нет места магии :)

    openid.yandex.ruvgacich

    Асм проще чем С (или, тем более, С++). Да, в асме много справочных данных и гораздо больше писанины, но в изучении он гораздо проще.


  2. mth 09 Янв 2010 2:26

    Ты как всегда все отлично расписал! Читаю и радуюсь что такой доступный материал теперь есть в свободном доступе. Один моментик: позволю себе покритиковать - на мой взгляд самое удачное в указателях - кастинг типов… Достаточно странно для понимания, особенно в первые разы, но когда вкуриваешь КАК то начинаешь любить Си и появляется отвращение к Делфи например, где работа с указателями возможна, но неудобно там все сделано.

    DI HALT

    Ну я не говорил что это плохо. Типизация указателей это величайший рулез!

    mth

    Я тоже не говорил - просто ты этот момент не описал, а вещь достаточно серъезная и мощная :) Но я на этом умолкаю. Сорри за критику ))

    P.S. Тоже ночами не спишь?

    DI HALT

    Опять режим меняю. Потому и не сплю. :)

    azcrc

    А в делфе почти так же всё, только местами задом наперед по отношению к си.

    delphi:
    a : integer; // переменная - целое число
    b : ^integer; // указатель на переменную целого числа
    b := @a; // или ADDR(a) или ещё пару способов получения указателя
    b^ := 12345; // залезли внутрь указателя (записать в ‘a’ через бэ)

    c:
    int a;
    int *b;
    b = &a;
    *b = 12345;

    azcrc

    Те же йайца, только сбоку. И снова 4 строчки.

    dword a;
    dword b;
    mov b, offset a
    mov dword [b], 12345

    (asm x86)

    Orcinus Orca

    Паскалистам (ака Дэльфятникам) проще понять Гарвардскую архитектуру памяти, чем Сишникам. Си изначально написан на втыкание переменных не задумываясь о том в коде он идет или за кодом. А подход к поинтерам (указателям) тот же что и в Си. Легкое отличие в синтаксисе вводит в заблуждение и людям которые всегда пользуются только одним языком кажется, что в другом языке все неправильно и что там неудобно.

    openid.yandex.ruvgacich

    В каком смысле кастинг типов? Типизированные указатели в дельфи есть. Вот арифметика указателей в дельфи сделана неочевидно - при желании делать нечто в духе a=*(p+10) указатель в дельфи нужно привести к указателю на массив требуемого типа, например:
    p: PIntegerArray; //а не Pointer или ^Integer
    a:=p[10];
    PSomethingArray объявляется как указатель на массив Something нулевой длины (в этом случае придется отключить проверку на выход за границы массива) или размером в 2ГБ-1 (предел для дельфи32).
    Сделано так для соответсвия pascal way.
    А вообще-то я сам матерился, когда не знал как это правильно делается и писал здоровенные строчки из приведения указателя к целому, арифметики и приведения обратно.

    DI HALT

    Он имел ввиду скорей всего приведение указателей к разным типам, дабы компилятор не матерился. Я то на это по привычке ложу болт :)

    openid.yandex.ruvgacich

    В этом разницы практически нет. В C - (byte*)p, в дельфи - PByte(p) (не помню, можно ли написать ^Byte(p) или обязательно нужно объявить тип указателя на требуемые данные, впрочем для стандартных типы PSomething и PSomethingArray уже есть в RTL).

    youmych

    Это что! Пришлось делать один проект на делфях (у заказчика на них все было написано, мой модуль интегрировался на уровне кодов). Обнаружил в делфяках жуткую вешь:
    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);

    DI HALT

    О_О

    Maccimo

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

    var m: packed array [0..0] of byte;спас бы отца русской демократии.
    А ещё лучше было сделать m типа PByte

    youmych

    хех.. ну да, такое возможно. Пример писАл по памяти, сейчас поднял-таки исходник и вот что выяснилось:
    function checksumm(buf: PChar, zise: integer): WORD;
    begin
    //тут тело функции (контрольная сумма в протоколе IP);
    end;
    то, что передавалось в функцию было выровнено на границы байтов. И уже в функции надо было работать с массивом и как с байтовым и как с массивом слов.


  3. Fodin 09 Янв 2010 2:38

    Я пока не уяснил для себя, когда учил С, что “указатель - это переменная, хранящая адрес”, не втек в смысл указателей. Было бы здорово, если б ты это прямо написал, хотя из твоих слов это следует, конечно. Но иногда для понимания приходится идти от вывода к рассуждениям, т.е. наоборот.


  4. Николай 09 Янв 2010 2:40

    Побольше практических применений указателей бы…
    А так хоть буду экономить память с помощью pgmspace.h :) thx!

    DI HALT

    Ну потом, в дальнийших примерах я буду это часто использовать.


  5. kalobyte.com 09 Янв 2010 2:49

    когда-то это было выше моего понимания, но училка у нас в пту вполне толково рассказала

    зы
    подкидываю тему для следущей статьи - пищание динамиком на простом выводе и через шим

    DI HALT

    Не, рано браться за периферию, коль скелета нету. Сначала про организацию программ вообще.


  6. azcrc 09 Янв 2010 3:30

    >>Чтобы было проще для понимания для ассемблерщиков я буду давать ассемблерную аналогию работы с указателем.

    Моя имха - ассемблирщикам и так понятно как никому. Ко мне вот как раз после практики на Асме пришло понимание указателей (как в Си так и на ООП в целом), т.к. реально начинаешь понимать что просиходит внутри при работе с указателями и классами. До этого возможно только чисто “энциклопедическое” (как изьъяснения с иностранцем по разговорнику-путеводителю) использование средств ооп.

    DI HALT

    Ну не скажи. Да, ассемблерщик все эти адресные замуты вдоль и поперек знает. Но вот пока ему не скажешь, что указатель это косвенная адресация и не более может тупить. По крайней мере я долго тупил на указателях, пока не достал дизассемблер и не посмотрел что же это такое. Увидев косвенные обращения сразу все понял :)

    azcrc

    Так и я тупил, пока не достал дизаассемблер.
    Но прежде был по-любому просто ассемблер :)


  7. bevice 09 Янв 2010 3:32

    Ну еще неплохо бы упомянуть, что могут быть указатели на указатели (для работы с массивом строк часто требуется)

    DI HALT

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


  8. Murav 09 Янв 2010 4:11

    В статье есть ошибка:
    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 .

    Murav

    И ещё добавлю, что код
    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), а не для того, чтобы мешаться программисту.

    DI HALT

    Ну за основу бралась документация на WinAVR
    http://www.nongnu.org/avr-libc/user-manual/pgmspace.html
    где PROGMEM был именно после имени переменной. И везде где бы я не находил примеры работы с флешем было именно так.

    И когда он не в main то записывает именно строку из флеша, не копируя ее в RAM.

    “И ещё, тип указателя вообще-то нужен для того, чтобы знать тип переменной на которую он указывает и соответсвенно использовать его во всех обращениях к ней.”
    Во, ценное дополнение. Сейчас впишу. Совсем забыл. Мне, как ассемблерщику, обычно до лампочки на тип данных =)

    Murav

    >Ну за основу бралась документация на 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 *), которая в неё записывается. И это расширение работает только с глобальными переменными.

    DI HALT

    “другие спецификаторы принято писать перед типом”
    Возможно, однако в WinAVR почему то принято иначе. :/ Иного я даже и не встречал.

    DI HALT

    Ваш же вариант:
    PROGMEM char StringP[] = PSTR(”Hello in FLASH Pinboard User”);
    даже компилиться не захотел. Заявляя что:

    Pinboard_1.c:26: error: invalid initializer

    DI HALT

    Убрав PSTR оно скомпилилось. Но появился варнинг
    ../Pinboard_1.c:26: warning: ‘__progmem__’ attribute ignored
    Свидетельствующий о том, что на прогмем мы забили и пихнули все в рам.

    Murav

    PSTR(s) объявлен как ((const PROGMEM char *)(s)), так что здесь забыт const, который показывает, что переменную под указателем менять нельзя(только чтение).

    DI HALT

    Добавление const не помогает. Ругается на то же самое. Убрав PSTR получаем опять же игнор progmem и строку в RAM. Еще варианты?

    Murav

    Как выяснелось надо использовать тип 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, то есть предпоследняя).

    DI HALT

    Аналогично:
    char * a=PSTR(”01234567890123456789″);
    компилится и нормально работает (т.е во флеше, где и положено ей быть). Версия последняя.

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

    PSTR разворачивается в итоге в:

    PSTR(s)(__extension__({static char __c[] PROGMEM = (s); &__c[0];})

    DI HALT

    Вот только одного понять не могу. Какой смысл указывать спецификатор прогмем в функциях?

    Ведь в функцию передается просто адрес. И не важно какой он и откуда, главное его тип (дискретность). А так что там, что тут он двухбайтный. И лишь при взятии адреса и укладке данных во флеш мы должны иметь ввиду откуда мы его берем. Функции же пофиг что на нее засунули — ее процедура pgm_read_*** иного и не считает.

    Компилятор на это даже варнинг не дает. Соответственно о том что это защищает от ошибок говорить не приходится - все в голове надо держать в любом случае.

    Murav

    В C в отличии от assembler’а есть проверка типов, так что PROGMEM нужно указывать, чтобы было видно(в том числе компилятору, хотя в GCC такое нормально не сделано) что это ссылка на программную память и обращаться к ней *имя переменной нельзя и в нормальном компиляторе это должно вызвать ошибку(в GCC, который как видно, был наскоро переделан под AVR энтузиастами этого нет).

    Murav

    И ещё заменил:
    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;

    DI HALT

    “объявления char String[] и char *String”
    О! Точняк, чет я затупил тут мощно. Работать то оно работает, но меня варнингом кормит, что дескать типы не совместимые.

    DI HALT

    “Ну и ещё вместо условия (переменная!=”) принято записывать (переменная).
    И переменные можно инициализовать в той же строке: unsigned char *u=&i;”

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

    DI HALT

    *u++ - это сначала разименовать указатель, а затем увеличить его на 1.

    Тут просто идет его увеличение на 1. Даже без чтения. Т.к. чтения по факту и не требуется.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    42:       k=&i;
    +000000B0:   01CE        MOVW      R24,R28        
    +000000B1:   96C2        ADIW      R24,0x32       
    +000000B2:   839A        STD       Y+2,R25       ; Сохранили указатель 
    +000000B3:   8389        STD       Y+1,R24       ; в индексную пару У
    44:       *k++;
    +000000B4:   8189        LDD       R24,Y+1       ; а тут достали его
    +000000B5:   819A        LDD       R25,Y+2      
    +000000B6:   9601        ADIW      R24,0x01    ; инкрементировали 
    +000000B7:   839A        STD       Y+2,R25       ; И вернули обратно 
    +000000B8:   8389        STD       Y+1,R24        
    46:         SendStr(u);
    +000000B9:   818D        LDD       R24,Y+5        Load indirect with displacement
    Murav

    А вот в статье строчка LD R17,Z есть и стоит не там, где ей положено.

    DI HALT

    Я там просто предположил возможное действие компилятора. Как бессмысленный забор значения без дальнейшего действа с ним.

    Murav

    И, кстати, код
    u=u*z+j;
    работать не должен(по крайней мере по стандарту) - указатель это не число и умножать его нельзя. А если очень нужно, то надо привести его к типу int:
    u=(char*)(((int)u)*z+j);
    Конечно громоздко, но указатели очень редко(я не могу вспомнить не одного случая, так как адрес в указателе может меняться при добавлении нового кода) нужно умножать и вообще производить с ними другие операции кроме сложения с числом и вычитания числа.

    DI HALT

    “Адрес в указателе может меняться при добавлении нового кода”
    Ну так при компиляции и линковке он же все равно пересчитывается заново, скольк бы там кода ни добавилось.

    “я не могу вспомнить не одного случая”
    Да хотя бы потрошить здоровеннный многомерный массив. Впрочем да, вы правы. Тут лучше предварительно вычислить смещение, а потом сложить с указателем.

    openid.yandex.ruvgacich

    В случае многомерного (для примера двухмерного) массива арифметика чуть другая:
    u=u+i*width+j
    Умножение указателя таки бессмысленно.

    openid.yandex.ruvgacich

    Впрочем, учитывая C way, операция умножения применительно к указателю вполне может означать что-нибудь совершенно другое.

    Murav

    В 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"]++;

    openid.yandex.ruvgacich

    Ну в принципе да, С менее извращенный :) Хотя извращенность С++ позволяет делать очень интересные вещи, выносящие моск кому угодно, кроме Александреску :)
    Впрочем, я дельфист и различия С/С++ кроме ООП не очень помню.

    Murav

    >Хотя извращенность С++ позволяет делать очень интересные вещи, выносящие моск кому угодно, кроме Александреску :)

    Если с этими вещами разобраться, то они не кажутся извращёнными. Напрмер перегрузка операторов - довольно удобная вешь, если пользоваться ей правильно. А шаблоны - одна из важнейших возможностей C++(и кстати одна из причин по которой я перешёл с Delphi на C++).
    Те примеры, которые я привёл в предыдущем посте - это нормальный C++ код.
    >Впрочем, я дельфист и различия С/С++ кроме ООП не очень помню.
    Много ещё чего по-мелочи, например спецификатор const и возможность задания переменных не только в начале блока, а так же в циклах(for (int a=0;a<10;a++)). Хотя некоторые компиляторы позволяют использовать их и в C.

    semak

    имхо, тогда уж
    u=first+i*width+j;
    (где first - указатель на первый элемент массива)


  9. bevice 09 Янв 2010 4:13

    А, ну и самое главное - указатели на функции.
    void foo1(void){;}
    void foo2(void){;}
    int main(void){
    void (*f)(void); // указатель на функцию
    f = foo1;
    f(); //выполнит функции foo1()
    f = foo2;
    f(); //теперь выполнит функцию foo2()
    }

    azcrc

    А смысл?

    DI HALT

    Еще какой! Например диспетчер задач через такие указатели работает.

    azcrc

    Пардон, применить точно можно.

    Я почему-то подумал - зачем ссылаться вручную на свои же статические процедуры, разве что для хитрых “эмуляций” классов и всяких там инъекций в код с хитрыми вывертами.

    А в диспетчере задач где применяется? Что-то не догоняю.

    DI HALT

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

    azcrc

    Точно точно.

    Те же вызовы библиотек так происходят.

    Это меня “зациклило” в рамках одного модуля, где такие финты без надобности в общем.

    bevice

    Это просто пример был, конешн как в примере ссылаться на функции смысла не имеет. А вот например когда указатель на функцию возвращает какая-то другая функция - уже имеет. т.е.
    f = foo();
    f();
    Модель с событиями как раз опирается, на указатели на функции.
    Да, в планировщике тоже нужны.

    youmych

    Часто таблицу указателей на функции (массив указателей) удобно использовать для реализации конечных автоматов:

    // тит указателя на функции вида 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;
    }

    на самом деле,я тут не показал как происходит изменение состояния автомата при переходах. Я часто делаю это непосредственно в функциях-обработчиках.

    DI HALT

    Или для виртуальной машины. Тоже, кстати, удобно. Гоним байт код в наш обработчик и по смещению вытаскиваем функции.

    bevice

    Кстати сказать в нашем случае виртуальная машины никакая не виртуальная, а вполне реальная, просто немного софтварная


  10. gremlinable.livejournal.com 09 Янв 2010 16:43

    Можно пару придирок?
    Ну, во-первых, снова трындю на тему WinAVR. Насколько я понимаю, цикл статей ориентирован на “непосвящённых”. Так вот, непосвящённые юниксоиды могут не понять, что речь идёт про простой gcc и avr-libc. =)
    Во-вторых, раз мы пишем про строки в прогмеме, то было бы неплохо упомянуть про массивы строк в прогмеме, хотя бы в виде ссылки на документацию, а то люди начнут делать по аналогии и будут ломать голову, почему это строка работает, а массив строк - нет.
    Ну и в-третьих, зачем изобретать неких тип u16? Чем не устраивают стандартные предопределённые типы uint16_t?

    DI HALT

    А тут возможны варианты??? Вроде как речь идет о AVR и только об AVR.

    Да, можно. Видимо понадобится еще одна статья про указатели. Т.к. есть еще немало что сказать.

    u08 и u16 это уже ставший почти стандартом тип данных для проектов на avr-libc. Почти вся библиотека

    http://www.procyonengineering.com/avr/avrlib/index.html

    и чуть более чем все проекты под WinAVR оперируют именно такой формой типов.

    А uint16_t практически не встречается. Почему? Видимо лень писать лишние буковки.

    gremlinable.livejournal.com

    Ну AVR-то да, он один, а вот компиляторов для него явно больше одного. Многие, кто используют winavr в сочетании с AvrStudio даже не подозреваются, что это фактически тот же самый gcc, который, например, в поставку почти любой юниксовой системы.
    Про u08 и u16 первый раз слышу - надо будет повнимательнее изучить этот вопрос… А (u)int(…)_t - это часть стандарта языка C, так же как size_t, time_t, ptr_t. Их удобно использовать хотя бы потому, что многие редакторы подсвечивают их как встроенные типы.

    Murav

    Типы (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, что заметно уменьшит размер кода(это вообще-то говоря неправильно, так как целочисленная константа при задании имеет тип в котором она будет использоваться дальше).

    gremlinable.livejournal.com

    >>Типы (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 - это нестандартное расширение языка =)).

    Murav

    >Во-первых, не только в 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 и т.п.

    bevice

    GCC поддерживает, Open Watcom поддерживает, Sun Studio поддерживает, intel поддерживает. Какой из компиляторов значительной части я забыл? MS?
    Ну так MS равно как и Borland сосредоточены на C++, а на ansi C они глубоко забили, и не рекомендуют его использовать. Поэтому насчет популярности именно ansi С компилятора от MS я готов поспорить.

    Murav

    >Какой из компиляторов значительной части я забыл? MS?
    Да, MSVC и C++ Builder. Этого достаточно.

    >Ну так MS равно как и Borland сосредоточены на C++, а на ansi C они глубоко забили, и не рекомендуют его использовать.
    MS вполне нормально поддерживает свой C компилятор(между прочим Windows они именно им и компилят). Да и в C++, где C код нормально компилиться этого нет.

    bevice

    А компилятор С++ и не должен поддерживать С99, хотя бы потому, что отпочковался сильно раньше.
    MSVC и С++ Builder это не С компиляторы. И нормально они компилят только С89. Писать на С и не использовать С99 уже довольно глупо.
    Думаю Ваш, процессор, например, помимо инструкций, что были в 8088 использует и MMX,SSE и пр. Не то, чтобы без них можно было бы обойтись, но удобней

    DI HALT

    Ну вот, добавил и про прогмем. Глянь свежим взглядом. А то у меня уже глаз замылился.

    gremlinable.livejournal.com

    Хорошо написано - кратко и ясно. В офф. доках запутаннее.
    Кстати, по теме: у меня тоже недавно была заметка про указатели в контексте оптимизации - http://gremlinable.livejournal.com/11757.html


  11. Dmitry 09 Янв 2010 19:49

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

    DI HALT

    Добавил. Оцени креатиф :)

    Murav

    Это ещё ничего по сравнению с тем, как на тиньках иногда работает оптимизатор:
    В таком коде
    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 и ещё столько же пока не трогал) ещё осталось около сотни байт места, а всё что я хотел сделать было сделано.

    Dmitry

    Оптимизация небось по скорости была? ))

    Murav

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

    Dmitry

    Отлично :)

    Есть небольшое методологическое предложение. В принципе, когда освоил, что это все указатели, только на разные области памяти, то все становится понятно. Это для тех, кто идет от асма к сям. А для сишников, я бы все-таки разделил эти два типа указателей. Не зря же ввели дефиницию 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;
    }

    DI HALT

    Можно и так, только тогда SendStr_P надо будет определить как PGM_P. Я пошел по принципу минимальных изменений :)


  12. dzed 09 Янв 2010 23:05

    SUBI ZL, Low(-1) ; Z++;
    SBCI ZH, High(-1) ; Инкремент указателя
    LD R17,Z ; и что дальше?

    А почему вычитается -1, а не прибавляется 1 ? В этом есть какой-то таинственный смысл?

    DI HALT

    SUBI это команда вычитания 1-(-1)=2 ;)

    dzed

    Это понятно…А почему нельзя INC ZL или ADIW ZL,1 ?

    DI HALT

    С INC ZL мы потеряем перенос.
    А ADIW есть не на всех AVR

    dzed

    Понятно. Спасибо! А сложения с переносом с константой нету в AVR, наверно, потому что RISC, да?

    DI HALT

    А нафига? Если сложение легко делается через вычитание?

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

    dzed

    Вот это неожиданно для меня! =)


  13. shamaxan 11 Янв 2010 3:38

    Уважаемый DI HALT! давно читаю ваш сайт и очень многое нахожу полезного в ваших трудах!
    Респект вам и уважуха!!
    у меня и моего сына появилась идея собрать бегущую строку на светодиодах, перерыв весь инет нормальных схем ненашол , есть одна гуляюшая по инету только хозяин неотвечает чтоб купить у него прошивку пика.
    Не могли бы вы в очередных ваших статьях описать сие чудо!
    С уважением и надеждой Денис.

    DI HALT

    Пока нет особого желания. Толку от нее немного. =)

    Если скажешь какое разрешение строки нужно, то могу подсказать по реализации.


  14. shamaxan 11 Янв 2010 3:55

    примерно 8на 96

    DI HALT

    думаю тут оптимально будет делать матрицы 8 на 8 по два сдвиговых на каждую и дрючить их в динамической индикации. Т.е. переключая байт поочерди зажигать строки. Яркости должно хватит.

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

    Вариант второй проще намного. Тут мы имеем 8 цепочек из сдвиговых регистров (по одной цепочке на строку, и по 12 регистров в цепочку). Всего 96 регистров по 5 рублей за каждый. Тогда их можно грузить напрямую из порта МК. Прошивка будет очень простой в реализации. Вполне хватит какой нибудь тини2313


  15. R_ura 02 марта 2010 5:28

    Понадобилось нам сделать станок с ЧПУ. Электронную часть сделал я, а программу должен был написать программист. Но оказался раздолбаем. Пришлось мне самому, на старости лет, разбираться с программированием. В общем с ASMoм разобрался за неделю, пока в голове что-то отложилось и стуктурировалось. В итоге программа написана, все работает. Но вот сколько ни пытался разобраться с СИ - темный лес. Сложно въехать даже в саму структуру программы, где там все эти циклы бегают? Да и вас тут всех почитал сейчас - у каждого своя интерпретация написанного кода. Т.е. похоже никакой конкретики в СИ нет. И так может быть и эдак. Напрашивается вывод, если нет необходимости иметь выигрыш во времени, который дает СИ ( что тоже сомнительно для непрофи), то человеку, не живущему программированием, за глаза хватит простого в освоении асма. Как я понял, кроме логики пишущейся программы, надо еще постоянно держать в голове загадочную логику самого Си, кучу понятий, определений и как все это между собой взаимодействует.
    На асме, если написать нормальные макросы и функции, тоже можно писать практически литературным языком. Вот только с математикой головняк. Что есть, то есть.
    P.S. Программист, который меня вынудил асм изучать, с WinAVR после асма разбирался полгода. Правда к асму возвращаться теперь не хочет.)))

    DI HALT

    Ну да. Си сложней для понимания если его не знать до этого. Простую автоматику на асме писать ничуть не сложней чем на Си, а отлаживать проще.

Оставьте свой отзыв

Вы должны войти, чтобы оставлять комментарии.


Материалы сайта являются авторскими. Копирование и публикация материалов без активной ссылки на первоисточник запрещено.

Реклама: