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

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

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

Мы можем напрямую сказать процессору — возьми данные из ячейки с адресом 0xA0 и положи его в ячейку с адресом 0x11. Это будет прямая адресация. Здесь адреса 0xA0 и 0x11 содержатся напрямую в машинном коде. Это очень быстро, просто и не требует никаких дополнительных телодвижений. Один минус — адреса 0xA0 и 0x11 нельзя изменить, как мы их впишем в код, так они там и останутся.

Но может быть и другой способ. Когда у нас есть еще две ячейки памяти. Например, А и Б в которые мы предварительно положим числа 0xA0 и 0x11 соответственно. И тогда предыдущая операция будет выглядеть так.

Возьми число из ячейки адрес который лежит в А и положи в ячейку адрес которой узнаешь из Б.

Результат тот же, но возникло множество дополнительных телодвижений. Во первых положить первоначальные адреса 0xA0 и 0x11 в ячейки А и Б. Потом, при совершеннии операции, используая данные ячеек А и Б как адреса, взять уже оттуда нужные нам данные и совершить обмен.

Но прелесть вся в том, что при этом мы можем как угодно менять А и Б (ведь это такие же переменные как и любые другие) и они будут указывать на разные данные.

А один и тот же кусок кода становится универсальным. Он может работать с любыми данными адреса которых нам укажут переменные А и Б.

А сами эти переменные и будут указателями.

Вот и вся премудрость.

Чтобы было проще для понимания для ассемблерщиков я буду давать ассемблерную аналогию работы с указателем. Конечно компилятор делает все не так прозрачно — начинает тасовать ячейками памяти и регистрами как заправский фокусник колодой карт. На этом этапе разглядывать ассемблерный листинг сишного кода можно лишь с целью уловить «Общие тенденции в городе». Поэтому я дам не реальный пример из скомпиленного кода, как делал раньше, а упрощенный аналог.

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

Заведем две переменные 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()
}

Зачем такой изврат? Ну много применений можно придумать. Например, байт код виртуальной машины. Где у тебя есть скрипт в виде массива действий, где каждое действие это адрес на функцию это действо выполняющее. А твоя программа, виртуальная машина, хватает из массива адреса и переходит на функции. А те, в свою очередь, делают что то, меняют порядок действий (если нужно). В общем, программа в программе получается :)
Или, второй пример, очередь задач в диспетчере ОС. Где у нас не прямые вывозовы функций, а заброс их в конвеер, где они выполняются в порядке очереди. Об этом я еще расскажу после.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      Pinboard_1.c:26: error: invalid initializer

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

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

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

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

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

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

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

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

                1. Тред с ярким примером работы на Си, программисту вместо того чтобы писать исполняемый код ручаясь табличными данными приходится ковыряться в дополнительной прослойке знаний зачастую ошибаясь и экспериментируя.

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

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

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

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

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

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

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

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

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

      3. *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
          1. Я там просто предположил возможное действие компилятора. Как бессмысленный забор значения без дальнейшего действа с ним.

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

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

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

                1. В 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»]++;

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

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

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

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

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

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

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

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

            1. Точно точно.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      2. Отлично :)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    2. Да с математикой головняк еще тот. Но я себе наковырял с разных сайтов куски кода, деление, умножение. Целых и дробных чисел. Преобразование в bcd формат И т.п. И теперь просто к любому своему проекту подключаю эту библиотеку и дело в шляпе. Кому надо могу скинуть на мыло.

  13. Привет DI, прочитал сейчас все твои статъи про СИ, много полезного извлек. Но пока решил наслаждаться асмом. Но у меня вопросец возник.В WINAVR есть Programmers Notepad. В котором можно писать код на СИ. У тебя о нем ни слова не сказано. О его возможностях или о его необходимости. Программу написанную в нем можно потом спокойно прогнать в студии. В принципе это все одно и тоже или есть некоторые нюансы? Что думаеш об этом?

  14. Я чуть чаем не подавился, когда прочитал комменты людей, которые говорят, что С сложнее, чем асм. Мое мнение прямо противоположное — порог вхождения в асм значительно выше, чем в С.
    Дело в том, что у каждой отдельной архитектуры свой асм и соответственно при переходе на другой МК заново приходится разбираться в новой(хотя и местами похожей) системе команд. С позволяет абстрагироваться от системы команд данной конкретной архитектуры и максимум внимания уделить логике программы, хотя эта самая абстракция тянет за собой костыли в виде pgm_read*, потому эти переходы обычно получаются не такими гладкими как хотелось бы:)
    Оптимальный вариант — это разбираться и в С и в асме. Это позволит облегчить переход между архитектурами и писать быстрые маленькие куски кода на асме, если нужно.

    1. Сложней сложней. Для того чтобы начать писать на сях надо обложится мануалами по языку. Да еще мануалами по реализации си для конретной архитектуры.

      Для написания на асме нужна ТОЛЬКО система команд и мозги. Усе :) Там все прозрачно и явно. Негде запутаться (ну разве что в своем коде) и нет места непониманию. Да язык специфичный, но он не требует больших количеств информации. Я начал писать на ассемблере (тогда это еще был i8080) за два часа :). Мне дали систему команд с описанием того что и как делается и я пошел :))))

      Тогда как на то чтобы начать писать на си (не тупо повторять примеры из учебника), а понимать что я делаю и как и зачем у меня ушло пара месяцев. С хорошими учебниками (К&R и еще весьма толковое пособие с массой примеров).

      А смена асма с архитектуры на архитектуру занимает буквально пару часов. Это проще чем с Си на паскаль перейти. Я когда то писал только под С51 на асме. Тут мне надо было написать прогу на AVR. Купил контроллер, открыл даташит и за вечер написал =)

      ОДно плохо — свои наработки приходится заново переписывать.

    2. Ты не забывай еще что к сям приходят обычно программисты, которые начинали на всяких бейсиках. А к ассемблеру приходят со стороны электроники, когда нужную схему проще на триггерах и логике всякой спаять чем запрограммировать. :)

  15. А операторы «new» и «delete» использовать пробовали?
    Компилятор пишет «error: ‘new’ undeclared». Странно вроде GCC (MinGW).

    1. Вероятно проблема в том, что вы ты сохранил код в файл с расширением .c, что заставляет gcc думать, что это c-код. В c нет оператора new, поэтому для компилятора символ new является не определенным.

      В файле с расширением cpp gcc нормально воспримет new, но для него лучше вызывать не gcc а g++, потому что последний также подключит необходимые библиотеки с++.

  16. о_О. Еще с самого паскаля нелюбил указатели…
    И Асм почему то неполюбил, наверное потому что о нем ходили такие темные слухи, что на нем проггят только 70 летние деды, которые потратили 50 лет своей жизни на его изучение)

  17. Можно ли в С как в ассемблере назначить с какого адреса начинается программа или определение функции? т.е. если ли что-то аналогичное директиве .org в ассемблере?

  18. Большое спасибо за статью — практические примеры бесценная вещь -)Но вопросы возникают, тоже практический пример: простая программа выдающая на PORTB элементы заранее заданного массива в зависимости от поступающих на PIND тоже заранее заданных элементов — чисел, если никакому заранее заданному не соответствуют то на PORTB — выдается 0. Сделано на операторе if; прикол в том что если использовать pgmspace.h то размер программы вырастает в два раза что весьма странно, все вроде нормально собирается…
    Вариант1:
    #include
    int main (void)
    {
    int in [] = {1,2,3,4,5,6,1,2,3,4,5,6};
    int sravn = 0;
    int buf0 = 0;
    DDRB = 0xFF;
    int *f;
    f = &in[0];
    while(1)
    {
    buf0 = PIND;
    if (sravn ^ buf0)
    {
    if (buf0 == *f) PORTB = *(f+6); else asm(«nop»::);
    if (buf0 == *(f+1)) PORTB = *(f+7); else asm(«nop»::);
    if (buf0 == *(f+2)) PORTB = *(f+8); else asm(«nop»::);
    if (buf0 == *(f+3)) PORTB = *(f+9); else asm(«nop»::);
    if (buf0 == *(f+4)) PORTB = *(f+10); else asm(«nop»::);
    if (buf0 == *(f+5)) PORTB = *(f+11); else asm(«nop»::);
    if ((buf0 == *f) || (buf0 == *(f+1)) || (buf0 == *(f+2)) || (buf0 == *(f+3)) || (buf0 == *(f+4)) || (buf0 == *(f+5))) asm(«nop»::); else
    PORTB = 0;
    }
    else asm(«nop»::);
    sravn = buf0;
    }

    }
    Вариант 2:
    #include
    #include
    int in [] PROGMEM= {1,2,3,4,5,6,1,2,3,4,5,6};
    int main (void)
    {

    int sravn = 0;
    int buf0 = 0;
    DDRB = 0xFF;
    int *f;
    f = &in[0];
    while(1)
    {
    buf0 = PIND;
    if (sravn ^ buf0)
    {
    if (buf0 == pgm_read_byte(f)) PORTB = pgm_read_byte(f+6); else asm(«nop»::);
    if (buf0 == pgm_read_byte(f+1)) PORTB = pgm_read_byte(f+7); else asm(«nop»::);
    if (buf0 == pgm_read_byte(f+2)) PORTB = pgm_read_byte(f+8); else asm(«nop»::);
    if (buf0 == pgm_read_byte(f+3)) PORTB = pgm_read_byte(f+9); else asm(«nop»::);
    if (buf0 == pgm_read_byte(f+4)) PORTB = pgm_read_byte(f+10); else asm(«nop»::);
    if (buf0 == pgm_read_byte(f+5)) PORTB = pgm_read_byte(f+11); else asm(«nop»::);
    if ((buf0 == pgm_read_byte(f)) || (buf0 == pgm_read_byte(f+1)) || (buf0 == pgm_read_byte(f+2)) || (buf0 == pgm_read_byte(f+3)) || (buf0 == pgm_read_byte(f+4)) || (buf0 == pgm_read_byte(f+5))) asm(«nop»::); else
    PORTB = 0;
    }
    else asm(«nop»::);
    sravn = buf0;
    }

    }
    Я чего-то жостко туплю или одно из двух? Спасибо-)

  19. Да, тут вроде .dseg не появляется, когда до main какие-нибудь данные тогда появляется так вроде выходит? Да и еще не окажешь любезность не мог бы ты подсказать какой аналог директивы .eseg?

  20. Еще чуть-чуть покопался и вариант 3 ввел меня в ступор ))) Whats going on?? То есть без pgm_read_byte не считывается по указателям из подпрограмм ? Кстати размер программы уменьшился на треть.

    1. Без пгм_рид_байт ты не сможешь считать из памяти флеша. Т.к. адресное пространство разное, а GCC универсальный компилятор который на такие задротства с рождения не заточен, то вот эта пгм_рид является указанием откуда брать данные.

  21. На всякий случай Вариант 3 постом:
    Вариант 3:
    #include
    #include
    int in [] PROGMEM = {1,2,3,4,5,6,1,2,3,4,5,6};
    int main (void)
    {

    int sravn = 0;
    int buf0 = 0;
    DDRB = 0xFF;
    int *f;
    f = &in[0];
    while(1)
    {
    buf0 = PIND;
    if (sravn ^ buf0)
    {
    if (buf0 == (int)f) PORTB = (int)(f+6); else asm(«nop»::);
    if (buf0 == (int)(f+1)) PORTB = (int)(f+7); else asm(«nop»::);
    if (buf0 == (int)(f+2)) PORTB = (int)(f+8); else asm(«nop»::);
    if (buf0 == (int)(f+3)) PORTB = (int)(f+9); else asm(«nop»::);
    if (buf0 == (int)(f+4)) PORTB = (int)(f+10); else asm(«nop»::);
    if (buf0 == (int)(f+5)) PORTB = (int)(f+11); else asm(«nop»::);
    if ((buf0 == (int)(f)) || (buf0 == (int)(f+1)) || (buf0 == (int)(f+2)) || (buf0 == (int)(f+3)) || (buf0 == (int)(f+4)) || (buf0 == (int)(f+5))) asm(«nop»::); else
    PORTB = 0;
    }
    else asm(«nop»::);
    sravn = buf0;
    }

    }

  22. Прошу прощения за такие здоровые посты, еще вопрос -). Никак не могу понять почему ты формально объявил прототипы функций во первых с переменными (в книжках только типы данных без указания самих переменных), а во вторых именно с указателями хотя при вызове ты передаешь все равно адрес, ну никак не понятно в чем дело при попытке сделать по книжке т.е. в прототипе объявить только тип и в функции при описании в аргументах указать переменную(адрес) без звездочки все работает, адрес передается но вылазит варнинг: ../ukaz-v-func-log.c:19: warning: passing argument 3 of ‘out’ makes integer from pointer without a cast , по твоему варнинга нет но непонятно).

      1. Почему у тебя так:
        //Прототипы функций
        void SendStr(char *string);
        void SendStr_P(char *string);
        void SendByte(char byte);
        //___________________________
        …………………

        …………………
        // Отправка строки
        void SendStr(char *string)
        …………………

        …………………

        // Отправка строки из флеша
        void SendStr_P(char *string)
        …………………
        а не так :
        //Прототипы функций
        void SendStr(char);
        void SendStr_P(char);
        void SendByte(char);
        //___________________________
        …………………

        …………………
        // Отправка строки
        void SendStr(char string)
        …………………

        …………………

        // Отправка строки из флеша
        void SendStr_P(char string)
        …………………

        ты же вызываешь функции с параметрами u и z — SendStr(u); и SendStr_P(z), а не SendStr(*u); и SendStr_P(*z) понятно что так работает , но не понятно почему))

        1. В прототипе указывается тип данных, а не то что туда подсовывается. Конкретно

          void SendStr(char *string); и
          void SendStr(char);

          это принципиально разные вещи. Т.к. char *string это двухбайтный указатель типа char. А char просто это однобайтная переменная. И может быть передана тупо в регистре, но не может быть указателем.

  23. Привет, опять у меня вопрос ) Не могу понять как оперировать с массивами! Например, есть массив в ипром color состоит из 3 данных по 1 байту. Мне нужно вычитать из каждого данного в этом массиве по единице. Получается, надо каждое данное из массива поместить в обычную переменную, потом делать все что захочется с ней и опять записать в массив. Но как его разделить? Плз опиши как поместить данные из массива по отдельным ячейкам, а потом обратно из них в массив?

    1. Массив в еепром пишется же в строку. А само понятие массива это абстракция. Нет никаких массивов — память линейная :)))) Поэтому тебе чтобы взять выборку из конкретной ячейки надо вычислить ее адрес, сделать чтение из еепрома по этому адресу. Если она была изменена — то запись в еепром по этому адресу.

      1. А если не вычислять, то на примере асма скажем выражение вычесть единицу из третьей ячейки массива color правильно будет: color.3 — 1?

        1. а что такое color.3-1 ????

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

  24. Помогите разрешить кашу в голове. Как правильно задавать указатель на строку из FLASH?
    Просто прочитал комменты к статье и так и не понял какой вариант окончательно верный:
    1. char string[] PROGMEM=»0123456″;
    2. PROGMEM char string[]=PSTR(«0123456»);
    3. char *PROGMEM a=PSTR(«0123456»);
    4. prog_char*a=PSTR(«0123456»);
    И нужно ли везде(имеется ввиду в ф-циях) тащить этот спецификатор PROGMEM или нет?
    И что это за тип prog_char?
    Вообщем если не трудно не могли бы по-подробнее это объяснить?

  25. Не могу понять эту конструкцию (языком владею не достаточно свободно):

    u16 value; // Двубайтная переменная value
    u08 *pointer_8; // Указатель на однобайтные данные.

    // Даем явно понять, что несмотря на то, что данные в value двубайтные
    // Мы будем с ними работать как с байтом
    pointer_8=(u08 *)&value;

    Где:
    «value» — имя 16-битной переменной,
    «pointer_8» — указатель на 8-битные данные

    А что это за u16 и u08? (это первый вопрос)

    И если перейти от абстракционизЬма к конкретике, то должно получиться так:
    unsigned short int value;
    unsigned char *pointer_8;

    pointer_8=(unsigned char *)&value;

    Верно? (это второй вопрос)

    1. u16 и u08 это ранее определенные типы данных как unsigned 8 бит и unsigned 16 бит. Т.к. использовать классические int и прочие в МК черевато, т.к. стандарт Си не декларирует сколько бит должна быть, например, int (в одной архитектуре инт это 8 бит, в другой 16 или даже 32). То лучше создать явные типы указывающие на разрядность. Легче будет таскать код с проца на проц.

      1. Я, конечно, догадывался о чем-то эдаком, но это меня, как новичка, все-таки запутало. Ведь в тексте данного урока определения этих типов отсутствуют…
        Мне как раз потребовалось преобразование типов для получения старшей части 16-битного числа, но компилятор ругается на какие-то ошибки, понять которые я надеялся, переложив кусок приведенного кода на свой, а тут непонятные «u16» и «u08».

        Еще вопрос: если «unsigned short int» будет иметь разную длину (но не менее 16 бит в любом случае) в зависимости от МК, то как тогда определять длину строго в 16 бит вне зависимости от МК, чтобы точно знать где в памяти старшая часть лежит?

  26. Доброго дня, опыта слишком мало, Си уже давненько не видел, прошу помощи в этом коде
    struct lcdSymbol** lcdSymbols = txSymbols


  27. typedef unsigned char byte;
    struct lcdSymbol {
    byte val;
    byte row;
    byte line;
    byte nbValues;
    byte maxLen;
    byte* caption; // 0 if unused; e.g. "ID "
    byte** line1Values; // e.g. "Full", "1/2", "1/4"
    byte** line2Values; // 0 if unused
    };
    struct lcdSymbol* rxSymbols[ ] = {&groupSym, &deviceSym, &powerRxSym, &blSym, &idSym, &modeSym };
    struct lcdSymbol** lcdSymbols = txSymbols

    // и дальше есть строки
    main {
    prevVal = lcdSymbols[cursorPos]->val;
    currVal = (prevVal+1) % (lcdSymbols[cursorPos]->nbValues);
    }

    1) не пойму
    byte*
    byte** ... что означают звздочки в описании структуры lcdSymbol

    2) как используется lcdSymbols и уже две "**" сразу

  28. DI привет. Кстати в AVRSTUDIO 5 и 6 уже не работают такие определения данных во флешь как:
    char String[] PROGMEM = «Hello in FLASH Pinboard User»; или
    uint8_t test[] PROGMEM = {0x01,0x02,0x03,0x04};
    работает только так
    const char String[] PROGMEM = «Hello in FLASH Pinboard User»; или
    const uint8_t test[] PROGMEM = {0x01,0x02,0x03,0x04};
    И уазатель тоже надо указывать const uint8_t *UKAZATEL;
    UKAZATEL=test;
    data=pgm_read_byte (UKAZATEL);
    UKAZATEL++;
    только так.

  29. Спасибо вам за ваши статить, и с целью сделать их еще лучше осмелюсь сказать следующее

    «DI HALT, ты забыл про EEPROM!!!» Как работать с ним на асме статья есть, а вот си оказался недостоин этого =)

  30. Уточню по поводу

    pgm_read_byte(data);
    pgm_read_word(data);
    pgm_read_dword(data);
    pgm_read_float(data);

    В библиотеке pgmspace.h они ссылаются соответственно на


    pgm_read_byte_near(address_short)
    pgm_read_word_near(address_short)
    pgm_read_dword_near(address_short)
    pgm_read_float_near(address_short)

    Таким же образом я вывожу в девайсах сообщения из flash в uart. И все бы ничего, да вот появись проблема!
    В очередной раз пишу, а девайс выводит кракозябры вместо того, что надо. Голову ломал не мало что да как. Догадавшись - посмотреть либу и узрел, что функции вместе с address_short ссылаются на __LPM((uint16_t)(address_short)), а это, извините меня, всего 65535 максимальный адрес ячейки. А размер моей программа уже перевалила за эту цифру. И в итоге старший бит в адресе просто зарезался.

    Решается просто: в этой же либе есть дополнительные функции:

    pgm_read_byte_far(address_long)
    pgm_read_word_far(address_long)
    pgm_read_dword_far(address_long)
    pgm_read_float_far(address_long)

    которые соответственно ссылаются на __ELPM((uint32_t)(address_long)), где адрес идет уже в 4 байта.

  31. Спасибо за статью! Я уже привык искать инфк сначала на easyelectronics, а только потом на других сайтах ))
    Вопрос на примере ATMega8A. У него 8 кило флеша, но код программы можно писать только в первых 4к, т.к. счетчик инструкций всего 12-ти разрядный. Поэтому логично было бы инструкции писать в первые 4 килобайта, а данные во вторые 4 килобайта (т.к. данные, в отличие от инструкций, можно адресовать из всей флеш памяти, а не только из первой половины).
    Но я что-то не понял, как можно компилятору указать, что данную конкретную переменную
    char StringP[] PROGMEM = " Hello_IN_FLASH"; надо положить по адресу не менее 4к.
    Как быть?

    1. Кажется я сам понял ответ на свой вопрос.
      Счетчик команд действительно только 12-ти разрядный. Но сами-то команды минимум двухбайтные. Поэтому уникальных адресов может быть не 8КБ, а 4КБ, то есть аккурат 2^12.
      Видимо, микроконтроллер просто умножает значение счетчика команда на 2.

  32. DiHalt, привет! Хотел вот что спросить.
    А в каком состоянии сейчас находится программирование на си искусственного интеллекта? Распознавание, принятие решений, роботы-пылесосы… Собираешься писать статью по этой теме?

      1. Может исходник выложить. Ну прям очень хочется доработать проект а мозгов уже не хватает.
        Делаю на интерес, потому пытаюсь перебороть собственное невежество сидя уже вторые сутки над проектом. И даже не брился =)

  33. Я просто уже не знаю, кого и спросить. Уже много часов сижу в раздумьях.
    Я делаю таймер обратного счета.
    Есть несколько проблем, которых сам не могу решить. Весь интернет облазил, выключал комп и сам думал и ничего…
    Что я сделал… Таймер для меня и точность не имеет значения. Поэтому питаю от внутренней частоты 8Мгц и делителем 64. Таймер по переполнению и прерывание ISR(TIMER0_OVF_vect). Это примерно каждые 2мс срабатывает прерывание. (448 раз в секунду). Чего я хочу.
    Вот мои переменные:
    volatile unsigned short int tic=0;
    unsigned short int h = 0, m = 0, s = 0, hms=0;

    hms это сотня миллисекунд…
    Я хочу ввести переменную tic+=2 вTIMER0_OVF_vect.
    А в главном цикле прописать функцию….
    //функция счета сотни миллисекнд
    unsigned short int hundred_millisec(unsigned short int a, unsigned short int b)
    {
    a=b/100; //досчитай до 100 и верни 1
    }

    // в цикле пишем
    hms = hundred_millisec(hms,tic)

    Получается, что моя переменная hms обновляется при проходе через цикл while(1) в main()

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

    Второе.
    ISR(PCINT0_vect) //Прерывание на PCINT Кнопки на адресах 0x04, 0x02, 0x01.
    {
    if ((PINB & 0x04)==0x00)
    {

    _delay_ms (50);
    if ((PINB & 0x04)==0x00)
    {

    Что надо написать сюда, чтобы подпрограмма включалась от удержания кнопки?

    }

    }
    Маска порта DDRB = 0b00000000;
    Подтягиваем резисторы PORTB = 0b00000111

    1. Из-за первой функции студия выдает предупреждения, мол ф-ция должна возвращать что-то…
      control reaches end of non-void function [-Wreturn-type]

      1. Ну так у тебя она обьявлена как unsigned short int hundred_millisec(unsigned short int a, unsigned short int b) т.е. она должна вернуть unsigned short int. А где return инструкция на выходе? Нету. Вот и матерится.

    2. Второе. Нахрена у тебя в прерывании цикл в 50мс? В результате у тебя и основной таймер перестает работать. Выкидывай все задержки из прерываний. Сделай на конечном автомате все.

      Т.е. есть нажатие — переходим в состояние нажато, запускаем таймер.
      В состоянии «нажато» проверяем кнопку. Если отжали — переходим в исходное. Если протикал таймер, а отжатия не было то запускаем подпрограмму и переходим дальше.

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

      1. Все заново переписал.
        Сделал счет в плюс в самом прерывании
        Значение s-секунд почему-то не выводится в разряд семисегментника data[3]=codes[s];

        http://pastebin.com/abgSVGKq

        Из-за чего такая хрень =(

        1. А че ты гадаешь то. Запусти под эмулятором или отладчиком и посмотри что происходит в регистрах. Такие вещи просто трассировкой решаются. Прежде чем научиться программировать контроллеры надо сначала научиться их отлаживать. Тогда не будет вопросов «почему не работает» берешь и смотришь почему не работает.

  34. Ты про нулевой таймер чтоли? А как я из одного прерывания перейду в таймер? Я 10 статей прочитал — в голове уже каша.
    Может я выложу всетаки исходник и посмотришь натурэль.
    Может я вообще не то делаю… https://yadi.sk/d/CagbV22ihiBcF

  35. if ((PIND & 0×04) == 0) // Опять проверяем нажатие
    e++; //Увеличиваем на 1
    …… ; // Здесь выполняется
    необходимая нам команда
    while ((PIND & 0×04) == 0) //Все ждем отпускание кнопки { Вот сдесь можем сделать проверку и код на
    долгое удержание }

    Так, чтоль?
    Без этой задержки у меня перестали отвечать кнопки =(((

    1. У тебя алгоритм концептуально неправильный.

      1) Зачем тебе прерывание по изменению вывода, если ты проверяешь один хрен опросом кнопки? Не морочься с прерываниями, сделай цикл опроса, проходящий через главный цикл, но без задержек. И там уже можешь прикрутить счетчик, который будет считать тики таймера и по их количеству определять длительность.

      2) Если же делать на прерываниях, то тут делается как:
      1) Ловим прерывание по нажатию. В его обработчике делаем следующее:
      !) определяем каким путем мы пришли сюда. По нажатию или отпусканию, например чтением конфигурационных байтов прерывания.
      а) запускаем таймер отслеживания дребезга (5-10мс)
      б) запрещаем прерывания по нажатию.

      2) в обработчике таймера дребезга (тот что сработал через 5-10мс)
      а) запускаем таймер снова, но уже конфигурируем его на 1с, например длительное нажатие, типа.
      б) разрешаем прерывания по нажатию, но в этот раз конфигурируем их так, чтобы они были уже на отпускание. Т.е. прерывание на отпускание кнопки.

      и теперь у нас два исхода. Исход 1

      Первым сработает таймер, тот что на секунду. Это будет означать, что мы кнопку не отпустили. А Значит там:
      а) Выключаем прерывание по отпусканию, включаем прерывание по нажатию (возможно придется также давить дребезг отпускания очередным выкрутасом с таймером, додумай сам)
      б) Фиксируем длительное нажатие, выставив флажок который считает главная программа.

      Первым сработает прерывание по отпусканию. Это значит, что нажатие у нас короткое. Мы отпустили раньше таймера. А значит:
      а) Выключаем наш таймер. Он уже не актуален.
      б) Переводим прерывание по отпусканию в прерывание по нажатию (возможно придется также давить дребезг отпускания очередным выкрутасом с таймером, додумай сам)
      в) Фиксируем короткое нажатие, выставив флажок короткого нажатия.

      Ну, а в главной программе анализируем флажки. На все про все уйдет один таймер и одно прерывание по нажатию/отпусканию. Которое мы постоянно переконфигурируем на новую задачу.

      А задержки в прерываниях делать нельзя. Т.к. пока в прерывании крутится задержка вся остальная программа стоит. А другие прерывания вообще не могут запуститься и ждут. В результате у тебя вся логика нахер идет. Учись на лету жонглировать разными конфигурациями периферии. Прочитай уже про конечные автоматы у меня. Сразу же множество вопросов отпадет.

      1. Я с кнопками еще ничего не делал.
        Товарищь, что помогал мне с таймером обратного счета слился. Видимо почувствовал, что дело пахнет киросином. =)
        Теперь я пытаюсь его оживить…
        Выкладывал последнее сюда.
        http://electronix.ru/forum/index.php?showtopic=129109&st=0&gopid=1350359&#entry1350359
        И тоже самое сюда. Но первый по новее.
        Там я пробовал по флагу прерывания тактировать. У меня с этим таймером затык. Я не понимаю, откуда брать тиканье и как использовать алгоритм отсчета времени.
        Если из таймера0 брать переменную и делить её в главном цикле программы — таймер считает медленно.
        Пишу типа
        if ((TIFR0)&(1<<TOV0)) // Если флаг таймера Т0 поднят и запущено прерывание по переполнению…
        {
        unsigned int temp;
        temp = ++tmsec;
        s = temp; // получаем наш тик и суём его в …
        data[3]=codes[s];
        }

        Кнопки, это не самое геморное. Как разбить все прерывания, чтобы и на индикацию, и на кнопки, и на счет ЧЧММСС хватало. Даже пробовал писать в прерывании это самое ччммсс. Работает, но сделать ничего нельзя с ним дальше. Не первую ночь так сижу. Фриковские проекты ломаю. Пока все не очень хорошо.

      2. Помнится, когда я кольцевой буфер осваивал, тоже все волосы повыпали. Ваши статьи тоже читал. Освоил, кое как.
        Потом PCINT был… Но он проще. Плохо, когда не знаешь, как надо…

  36. Пойду помоюсь =))) не могу придумать ничего лучше этого.
    Еще и задержки нельзя… хм….

  37. По сути…
    А зачем нужно прерывания, если они ничего не решает?
    Вроде аппаратно — круто. Работает сразу.
    Но значение вытянуть нельзя. Нажал ты кнопку, определил, откуда, и чо. Зачем нужен PCINT если записать значение, набранное в прерывании никуда нельзя?
    Тоже с таймером. Хочешь, чтобы отсечка была по количеству в переменной, чтобы капилось там значение до определенного момента, чтобы тактировать этим количеством 1 секунду (пускай хоть не точно — не атомно). Дык тоже засада. Не вырвать от туда это значение одной секунды в главный цикл.
    Фуфло какое-то все это программирование получается.
    Знал бы, что такая засада у Risc архитектуры, в руки бы не брал. Дык дешево… Вот и попробуй придумай к этому всему кастыль.
    Дело не в отладке, а в подходе к делу. Зачем такая засада вообще нужна?
    Подарок от заморской фирмы? Что-ж, теперь ищи кастыль, чтобы запустить простую считалку с выводом в один разряд семисегментника.
    Кошмар.

    1. Зачем нужны прерывания? Чтобы предельно быстро среагировать.

      Зачем нужен PCINT если записать значение, набранное в прерывании никуда нельзя?

      Кто вам такое сказал? Можно конечно же. Тупить в прерывании нельзя, а выносить из них данные через глобальные или статичные переменные или флаги — пожалуйста.

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

      У вас тотальное непонимание как надо писать программу в микроконтроллере. На уровне подхода, концепеции. Почему я и говорил почитать про архитектуру программ у меня был целый цикл статей по этой теме. Как создается структура. Тогда не будет вопросов как отмерить секунду, как считать значение и прочее прочее. В частности все делается либо на конечных автоматах, либо на подобии событийной ОС. А чаще гибрид и того и другого. Это наиболее оптимальные подходы.

  38. А как правильно читать такую конструкцию?

    if ((j=100) !=0)
    {если i = 100 И не равно нулю, то выполняем это условие}
    else
    {если i не равно 100 и равно нулю}
    ???
    Правильно ли?

  39. веди проверка — это знак ==, а тут одно =…
    Получается, что мы присваиваем переменной i значение 100.
    А это переменная может быть файловой?
    или она только локальная?,
    Ну тогда надо писать что-то вроде…
    if (( int j=100) !=0)
    определяя тип локальной переменной

  40. > z=StringP; // И он ничем не отличается от указателя на FLASH
    // И в том и в другом случае это двубайтный адрес.

    Все же надо заметить, что указатель на что-либо во флеш будет трех байтным для данных, лежащих выше 64Кб.

    Для получения адреса такой строки придется воспользоваться макросом pgm_get_far_address(StringP), потому что обычные указатели в gcc двухбайтовые. Читать указатели из флэш по этой же причине, вероятно лучше функцией вроде pgm_read_ptr_far(&(MenuItemPointers[1]));

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

Ваш e-mail не будет опубликован.

Перед отправкой формы:
Human test by Not Captcha