• ↓
  • ↑
  • ⇑
 
Записи с темой: программирование (список заголовков)
23:58 

Парадокс близнецов

Отлаживал программу при помощи отладочных сообщений. То есть, сообщения по ходу выполнения программы пишутся в лог-файл.

Время на часах -- 14:00. Выполняю в программе операцию, которую хотел проверить. Захожу в лог -- а там последняя запись -- 13:52. Что такое?! Как так?! Проделал ещё раз операцию -- то же.

Потом меня стали терзать смутные сомнения. Я отодвинул окошко, загораживающее часы в трее.

13:53.

Работал на целевом компьютере я по удалёнке. И смотрел время по часам основного компа. Часы целевого компа отставали на 8 минут.

@темы: Борьба с техникой, Программирование

23:59 

То лапы ломит, то хвост отваливается

Перевели один из проектов из-под MSVS 2008 под MSVS 2015. Экзешник вырос в два раза и перестал работать под Windows XP! (наше ПО эксплуатируется в т.ч. на машинах с XP)

Потом, правда, оказалось, что есть специальный набор legacy-библиотек.

@темы: Борьба с техникой, Программирование

23:58 

Лакуна

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

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

Однако поток при старте очищал входной буфер виртуального COM-порта! На всякий случай -- вдруг там мусор какой от предыдущих передач остался?

Если мне везло, то устройство не успевало ответить до очистки буфера, и ошибка не возникала. Поскольку очистка производилась только при старте потока, дальнейшие запросы и ответы воспринимались нормально. Если же мне не везло, то часть ответа (или даже весь ответ) оказывалась стёрта.

@темы: Борьба с техникой, Программирование

23:59 

signed char и шестнадцатеричное представление

Допустим, мы хотим увидеть char в шестнадцатеричном виде при помощи printf или аналогичной функции. Это бывает нужно для организации логирования дампов двоичных массивов при приёме/передаче данных, скажем.

Нас ждёт разочарование. char по умолчанию signed. Спецификатор для шестнадцатеричных чисел -- %x. Но он работает с числами размером с int. signed char будет преобразован в unsigned int, и все старшие биты будут забиты единичками из-за дополнительного кода. То есть, вместо 0x2F мы увидим 0xFFFFFF2F (при форматное строке "0x%02x").

Что же делать?
1. Забыть про массивы char. Использовать либо unsigned char, либо новомодный (лет пять-десять как) uint8_t.
2. Использовать двойное преобразование типов. Это лол. Я не ожидал, что это сработает:

printf( "0x%02x ",(unsigned int)(unsigned char)buffer[offset]);

@темы: Программирование

23:59 

Опасность enum

enum -- что может быть лучше?

(для тех кто не в теме)

Чем же это опасно? При любом изменении списка, исключая добавление новых констант в конец, меняется фактическая нумерация! И значение, которое было раньше 2, теперь становится 3. Это совершенно некритично, если доступ к переменным завязан ТОЛЬКО на применении символических значений. Как только происходит выход за пределы данного лягушатника -- начинается беда-печаль. Вот примеры:
1. Бинарное общение с другой программой, у которой список констант немного отличается.
2. Общение самого с собой через файлы. Сохраняем, к примеру, настройки в файл. Обновляем программу, файл считывается, а значение уже не то!
3. Использование символических констант в качестве индексов предопределённых массивов. Например:

enum TypeName {SYMBOLIC_CONSTANT1, SYMBOLIC_CONSTANT2, SYMBOLIC_CONSTANT3};
char SymbolicNames[][80]={"Name1", "Name2", "Name3"};

printf("Name: %s\n",SymbolicNames[SYMBOLIC_CONSTANT2]);

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

Что с этим делать:
1. Не модифицировать список в середине и в начале. Никогда. (иногда может показаться, что это можно сделать безболезненно... переупорядочить константы по группам, к примеру)
2. Присваивать принудительные номера. Всегда. (как это делать, см. под катом в начале)
3. Использовать #define .

@темы: Программирование, Говнокод

23:58 

Как провести рефакторинг кода и не облажаться?

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

Но раз уж я написал заголовок, я, пожалуй, всё же отвечу на него.

Так как же провести рефакторинг кода и не облажаться? Ответ: никак.

Но кое-от-чего защититься можно.

Итак, пусть у нас в программе есть об'ект A. Нам надо добавить новый об'ект -- A2, того же класса. Из текущих ситуаций использования A половина должны остаться за ним, а половина -- перейти к A2 (правила определения, кто чем будет заниматься, известны заранее). Ситуации встречаются по всей программе. Об'ект практически глобальный. Как же технически провести эту работу?

Самое очевидное решение -- пролистать все употребления A и часть перевести на A2 (предварительно создав его). Но так есть риск пропустить что-нибудь, даже если использовать глобальный поиск.

Я предлагаю способ, благодаря которому пропустить ни одного употребления просто не выйдет. Об'явление об'екта A надо исключить из программы! Либо удалить A, а вместо него создать массив A0[2], либо переименовать A в определении (и только там!) в A1.

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

Хотя и тут есть возможность сфейлиться. В какой-нибудь функции может быть одноимённый локальный об'ект того же класса. Он раньше перекрывался A, а теперь -- нет. В этом случае ошибки не будет.

@темы: Программирование, Лайфхак, Говнокод

23:59 

Вавилонская башня

Микроконтроллер выдает HardFault (аппаратная ошибка). После какой конкретно строчки программы он туда вываливается по техническим причинам выследить невозможно.

Смотрю -- происходит это по адресу 0x1800 0000. Там как раз заканчивается настоящая оперативная память и начинает пустое адресное пространство. Ставлю точку останова по адресу 0x17FF FFFA -- недалеко от конца. Но она не срабатывает. Вывод -- в этот адрес упирается не программа, а данные.

Нахожу в коде следующий текст:

char out_buf[200];
char temp_buf[20];
int x;
int a,b,r[10];
// ---

//sprintf(out_buf,"%d\n",a);
sprintf(temp_buf,"%d\n",b);
strcat(out_buf,temp_buf);
for(x=0;x<10;x++)
{
sprintf(temp_buf,"%d\n",r[x]);//напечатать очередной кусок текста в строку
strcat(out_buf,temp_buf);//добавить к основной результирующей строке
}
//...


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

sprintf печатает в строку, strcat сшивает (конкатенирует) две строки.

Данный кусок выполняется в основном бесконечном цикле прошивки.
Первый вызов sprintf закомментирован. Он попал под раздачу, т.к. отвечал за одну из функций прошивки, которая была больше не нужна. Вместо её удаления, я её закомментировал. И правильно сделал. Благодаря этому я легко смог опознать, в чём же было дело.

Эта закомментированная строка писала в выходной буфер с его начала. А остальные операторы дописывали к его концу. Когда же я строку закомментировал, при каждой следующей итерации новые данные дописывались в конец строки. Которая всё удлинялась и удлинялась. Пока не упёрлась в конец памяти!

Решением в данном случае было дописать после закомментированной строки:

out_buf[0]='\0';

Тогда заполнение стало происходить с нуля каждый раз.

P.S. Только что подумал, что можно оптимизировать, сделав:

sprintf(out_buf+strlen(out_buf),"%d\n",r[x]);

@темы: Программирование, Борьба с техникой

23:59 

Вас здесь не стояло

При входе в main() микроконтроллерной программы:
а) МК вылетает в исключение HardFault, если запустить программу на автоматическое выполнение до брейкпоинта.
б) если выполнять пошагово до него же, то всё работает.
в) после каждой строчки начиная с первой выдаётся предупреждение, что указатель стека вне допустимого диапазона.

Я изучил в дизассемблере пролог функции main(), т.е. в промежуток между её началом и первой командой:

int main()
{
// <- тут пролог... его не видно, потому что его делает компилятор.
func1();
//...
return 0;
}


Обнаружил там странную команду:

SUB.W SP, #7306

То есть, вычесть из указателя стека семь тысяч с гаком. А стека было всего две тысячи. То есть, программа не работала потому, что новый указатель стека выходил за пределы стека, о чём и выдавалось предупреждение. Но это ещё не всё -- указатель оказывался В ОБЛАСТИ КОДА и переписывал его данными на ходу. Это МК, тут нету DEP. Этим и был вызван HardFault.

Но почему же вычитались семь тысяч? Я думал, это безумие какое-то. А потом мне коллега напомнил, что локальные переменные выделяются в стеке. Эта команда как раз выделяла место под них. А у меня там среди прочего массивы на пару тысяч чисел.

@темы: Программирование, Борьба с техникой

23:59 

И прочие ингредиенты

Есть древняя-древняя функция puts. Она выводит на экран строку. И переводит каретку на новую.

У меня был список чисел, надо было записать в файл. При работе с текстовыми файлами я и в Си и в пхп предпочитаю олдскульный способ -- fopen/fcolse, fprintf/fscanf и прочее.

И вот я зачем-то решил воспользоваться fputs. Не знаю, зачем.

Короче говоря совершенно неожиданно я выяснил, что fputs символ новой строки в конец не добавляет. А обнаружил я это узрев сплошную строку цифр в выходном файле.

Это одна из плохих вещей в Си: что функции разных семейств, делающие по сути одно и то же, различаются в существенной особенности.

@темы: Программирование, Говнокод

23:59 

Кратчайшее руководство по отладке при помощи дампов памяти

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

Сегодня я научился использовать дампы памяти в целях отладки. In a nutshell:

0. Отладка с помощью дампа используется, когда возможностей отладочной печати и удалённой отладки недостаточно (у меня удалённая отладка вообще не работала).
1. Не используйте команду "создать дамп" в диспетчере задач (или как там он сейчас называется). Дамп будет только частичный.
2. Качайте программу ProcDump.exe от Sysinternals и печально известного Марка Руссиновича.
3. В командной строке пишите ProcDump.exe -pa image.exe, где image.exe -- имя программы, дамп которой создаёте (желательно запускать отладочную версию). Есть ещё много параметров -- можно делать дампы при зависании, при превышении использования памяти/процессора, при падении программы и пр. Данная выше команда сделает дамп при её (команды) исполнении.
4. Перекачайте дамп на комп, где стоит вижуал студия. Дамп может занимать больше 100 мегабайт.
5. Откройте его. Для полноценной работы надо подгрузить символы (symbol, PDB-file). Для этого есть пункт в меню либо гиперссылка (зависит от версии).
6. Жмёте "запуск" (гиперссылка или пункт меню с зелёной стрелкой).
7. ???
8. ВУАЛЯ! Вы видите текущую точку работы на время создания дампа. Можно посмотреть все переменные. В окошечке "потоки" можно посмотреть точки выполнения всех потоков. Также доступны все стеки вызовов и все переменные в функциях более высоких уровней. Исполнять программу дальше, к сожалению, нельзя.

@темы: Программирование, Лайфхак

23:58 

Что посеешь, то и пожмёшь

Одним проектом я не занимался год. Потом открыл. Ох, как всё запущенно.

Я знал и знаю некоторые правила хорошего стиля программирования, которые я сам иногда нарушаю. И открыв этот проект я огрёб по полной. Я не знаю, почему я тогда нарушил парочку базовых правил, но сейчас мне это сильно добавило работы. Правила все давно знают, но всё же их укажу:

1. У переменных должны быть понятные имена. Это касается не только переменных типа _, asdbsd и Burrrrp (хотя игроку в НетХак последняя будет весьма понятна) -- тут всё ясно. Название может иметь отношение к реальной ситуации, но имеет шанс быть воспринятым неправильно. Так, у меня в программе была переменная по имени need_count. Я долго думал, что это число раз, которые надо (need) посчитать (count) что-то. Однако на самом деле эта переменна обозначало то, НАДО считать (1) или НЕ НАДО (0).

2. Отсюда вытекает второе правило -- тип переменной должен соответствовать её назначению. Переменная из п.1 имела тип int. Хотя гораздо понятнее всё было бы, если бы я сделал её типа _Bool (это было в той части проекта, которая работала на микроконтроллере, а она написана на голом Си-99, поэтому именно _Bool, а не bool). Для МК, это, правда, простительно, т.к. int будет быстрее обрабатываться в ряде случаев.
Примечание. Беззнаковые типы при совместном использовании со знаковыми таят в себе гремучую бомбу: zhz00.diary.ru/p211118163.htm

3. А третье правило -- не следует повторно использовать ту же переменную для других целей. Хотя для переменных типа x, y, i особой разницы нету, сколько раз их использовать. Но повторное использование более специальных переменных часто ведёт к нарушению п.1. То, на чём конкретно напоролся я -- WriteFile возвращает через указатель число записанных байтов. Для этого я использовал переменную с подходящим именем written. Но потом мне надо было читать данные при помощи ReadFile. Как не трудно догадаться, он тоже через указатель возвращает число байтов, но уже прочтённых. Видимо тогда я решил сэкономить четыре байта. Ну вот нафига? Да-да, именно так. Я написал --

ReadFile(hFile,size,&written,NULL);

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

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

Но кое-от-чего чего я пока избавиться не смог -- так это от применения операции ?: . Очень её люблю, хотя она сильно усложняет чтение текста.

@темы: Программирование, Очевидное-невероятное, Говнокод

23:59 

Босохождение

Начальник: Наша программа в таком-то месте выдаёт ошибку.
Я: Расскажите подробнее, как вы с ней столкнулись.
Начальник: Я поставил машину на стоянку, заглушил двигатель, включил ручной тормоз, поставил сигнализацию, зашёл в здание, поднялся в комнату, снял уличные ботинки -- и практически сразу получил ошибку!
Я: Попробуйте надеть уличные ботинки обратно.

@темы: Викторика, Программирование, Случай из жизни

23:59 

Стой там, иди сюда

Посылаю данные через последовательный интерфейс (SPI) из контроллера. Для тестовых целей посылаю по очереди 0xFFFFFF и 0xAAAAAA. По системе:

for(;;)
{
SendData(0xFFFFFF);
Pause(1);//us
SendData(0xAAAAAA);
Pause(1);//us
}


Но на осциллографе вижу какую-то хрень: вроде данные видны, но через несколько тактовых импульсов (а там есть вторая линия, где вместо данных -- тактовые импульсы) происходит сбивка длительности импульса (т.е. они все должны быть одинаковой длины, а очередной импульс оказывается другой длины). И данные совсем не те. А число импульсов... ОДИННАДЦАТЬ (а должно быть 24, я так задал). Вообще ни в какие ворота не лезет.

ОКАЗАЛОСЬ, что блок контроллера, отвечающий за SPI занимается посылкой данных независимо, т.е. как только я данные ему отправил, он сразу возвращает управление. А функция SendData не ждёт, пока завершится передача. А 11 импульсов проходит за время паузы как раз. И блок SPI бросает все дела и начинает посылать следующие данные, недопослав предыдущие.

@темы: Борьба с техникой, Программирование

23:59 

Как не надо обрабатывать ошибки

Если у вас обработка ошибок сделана через коды ошибок, то должны соблюдаться следующие правила:
1а. Код, возвращаемый из каждой ветки управления, должен быть уникальным.
ЛИБО
1б. При выводе сообщения об ошибке должен указываться не только код, но и место возникновения ошибки (файл с исходным текстом, строка; в си/пхп для этого есть __FILE__, __LINE__, а в пхп ещё и __FUNCTION__).

2а. Сообщения об ошибках должны быть уникальными.
ЛИБО
2б. В сообщения должен быть включён код ошибки.

Я же встретил следующую вещь:
switch(nError)
{
//...
case 100:
case 101:
case 102:
AfxMessageBox("Разгерметизация скафандра!");
break;
//...
}


А ошибки 100, 101 и 102 выпадали из десяти разных мест.

После этого разработчикам становится понятно, в чём дело, но становится непонятно, куда копать.

@темы: Говнокод, Программирование

23:53 

Добровольно-принудительно


Необязательная галочка, которую нельзя сбросить! И это не вижал студио экспресс.

@темы: Программирование, Программы

23:58 

О лицензиях

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

Или не использовать библиотеки, которые под ней распространяются.

По-настоящему свободная лиценизя только одна. Public Domain.

@темы: Мысли, Программирование

23:59 

Тупоконечники

Ошибка в документации. Написано, что устройство должно в качестве конца сообщения присылать сигнатуру 0xA5A4A3A2. И особо указано, что менять порядок байт запрещается!

Хорошо, что приведён пример пакета. В пакете указано:
байт 60: 0xA5
байт 61: 0xA4
байт 62: 0xA3
байт 63: 0xA2

Однако и устройство и контроллер используют обратный порядок байт (это известно по описанию некоторых команд). То есть, с обоих точек зрения это число 0xA2A3A4A5. А я думаю, почему это оно не реагирует.

@темы: Программирование

23:59 

Ещё про упаковку

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

Сначала несколько важных замечаний:
  • Встретил я указанную проблему в компиляторе GCC под ARM.
  • По документации результат #pragma pack распространяется на упаковку полей структуры, описание которой идёт сразу после директивы. То есть, по идее, значение упаковки не должно было переноситься на дальнейшие структуры. Однако оно переносилось.
  • #pragma pack -- микрософтовская штучка и была добавлена в GCC для совместимости. В компиляторах от микрософт она может работать несколько иначе. В GCC есть более мощный аналог -- __attribute__((packed(...)))


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

#pragma pack(push)
#pragma pack()
//#pragma pack(N)
struct A
{
//...
};
#pragma pack(pop)


pack(push) в начале сохранит, а pack(pop) в конце восстановит старое значение упаковки (вдруг оно будет кому-то нужно?).

pack() без параметров установит значение упаковки по-умолчанию, которое можно настроить перед началом компиляции (подробнее тут), что позволит гибко настраивать упаковку для различных архитектур. Если же нужна конкретная упаковка для данной структуры, можно написать #pragma pack (N), где вместо N должно стоять число 1, 2 или 4 (компиляторы микрософт для 64-битных систем также поддерживают 8 и 16). Идентификатор, сделанный через #define, вместо N подставить нельзя.

@темы: Программирование

23:58 

Первый -- шаг вперёд, и в рай

Удивительную вещь встретил. Передаю в функцию указатель на экземпляр структуры, а приходит вместо исходных значений -- мусор.

Смотрю, адреса в памяти совпадают, т.к. указатель передался правильно. Смотрю значения в памяти -- не меняются. Так почему же я передаю одни значения, а получаю другие?

Значит, они по-разному интерпретируются! Но структура одна же. Я включил её определение в .h файле в два .c файла -- в главный файл и в файл с определением функции, которую я вызываю.

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

Короче говоря, дело было в выравнивании данных в памяти. В главном файле где-то в начале (или в одном из заголовков) было установлено #pragma pack(2), а во втором файле -- #pragma pack(4). И уже ПОСЛЕ этого был включен мой заголовочный файл и описанием структуры. В итоге главный файл думал, что выравнивать поля надо по 2 байта на переменную, а второй файл, что по четыре! Т.е. структуры имели одинаковое имя, но были несовместимы двоично! Я непосредственно перед описанием структуры в заголовочном файле поставил #pragma pack(4) и всё стало зашибись.

(пример, иллюстрирующий #pragma pack для тех, кто не в курсе)

@темы: Программирование

23:36 

Фейл-моногатари [05]

В прошивке контроллера написано выдавать сигнал на выход, а осциллограф этого не видит. Никак не мог понять, в чём дело.

Решил -- контроллер работает на частоте 200 МГц. Возможно, сигнал проскакивает так быстро, что осциллограф просто не срабатывает? (он работает в режиме single sequence: при появлении сигнала он начинает измерение данных, а потом показывает статичную картинку начиная с момента появления сигнала)

Думаю, надо для теста уменьшить частоту. Так, какие у нас ещё есть источники для тактовой частоты? О, тактовый генератор 32 кГц! Его и поставлю!

И заменил строчку:
REGISTERS->PERIPHERAL_CLOCK=9<<24;//основной кварцевый генератор; в 24-28 битах должна быть 9,
//остальные биты 0; << -- логический побитовый сдвиг влево.

На строчку:
REGISTERS->PERIPHERAL_CLOCK=1<<24;//генератор 32 кГц на RC-цепочке

Контроллер после этого прошился, но при попытке выполнить пару строк кода завис. И больше он теперь не прошивается!

@темы: Фейлы, Программирование, Борьба с техникой

Untitled

главная