Обзор анализатора ЛВС.

Всем привет!
Даная статья посвящена обзору анализатора ЛВС, его внутреннему устройству и программной части. Я хочу поделиться опытом разработки, рассказать про этапы проектирования и показать некоторые детали реализации для повышения общей квалификации. Полностью все выложить не могу, ибо коммерческая тайна, но ключевые моменты постараюсь изложить насколько возможно подробно. Итак…



Содержание



Введение


Зачем это нужно?

При прокладке ЛВС на основе витой пары по офисам, зданиям, квартирам в домах, либо где-нибудь еще, возникает необходимость убедиться в правильности выполненных работ, отсутствия неисправностей и работоспособности сети в целом.

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

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

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

Для выполнения всех этих действий, упрощения наладки и устранения неисправностей сети и был разработан данный прибор. В нем собраны воедино измеритель физических характеристик кабеля и сетевой тестер. И все оформлено для удобного использования.

Особенностью является использование частотного рефлектометра и поддержка протокола IPv6.

Жирным шрифтом выделены ключевые слова, на которые стоит обратить внимание.

Функции

Что в нем реализовано?

По части кабельных измерений:
  • Определять целостность и правильность разводки кабеля
  • Измерять расстояние до неоднородности (обрыв, замыкание, рассогласование)
  • Измерять следующие физические параметры кабеля по TIA/EIA TSB-67
    • Погонное затухание (Attenuation)
    • Перекрёстные помехи (NEXT)
    • Уровень шума (Noise)
    • Сопротивление пар


По части сетевых измерений:
  • Пинг удаленного хоста, включая протокол IPv6
  • Проверка работы DHCP сервера
  • Мониторинг трафика
  • Статистика по протоколам: ARP, IGMP, ICMP, TCP, UDP и др.
  • Статистика по IP и MAC адресам
  • Определение порта
  • Тест доступности сервера (FTP, HTTP, SSH и др.)


Прочее:
  • Автоматическая проверка кабеля
  • Создание отчетов измерений
  • Задание псевдонимов для IP адресов
  • Обновление микропрограммы
  • Зарядка аккумуляторов

Железо


В данном разделе описаны используемые в приборе микросхемы и устройства.

Упрощенная блок-схема устройства, совмещенная с разводкой земли.
Блок схема

PCB

MCU

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

Знакомьтесь, LPC2368. Он построен на базе ядра ARM7TDMI-S работающий на тактовой частоте до 72 МГц, снабжен двойной высокоскоростной шиной (AHB). Интерфейсы: 10/100 Ethernet MAC, UART, CAN, SPI, SSP, I2C, I2S, USB 2.0 Device/Host, ЦАП 10 бит, АЦП 10 бит. Режим прямого доступа к памяти (DMA). Флэш-память: 512 кБ, ОЗУ: 58 кБ. Корпус LQFP100.
LPC2368

DDS

Блок частотного рефлектометра построен на микросхеме AD9859.
Рефлектометр

AD9859 – это прямой цифровой синтезатор, DDS (direct digital synthesizer), с 10-разрядным цифро-аналоговым преобразователем, работающим со скоростью до 400 MSPS. AD9859 способен генерировать выходные аналоговые синусоидальные сигналы на частоте до 200 МГц с возможностью быстрой перестройки частоты.
AD9859

Кратко, работу частотного рефлектометра можно описать так: в кабель запускается сигнал определенной частоты и одновременно измеряется уровень на выходе. При совпадении длины волны с длиной кабеля происходит падение уровня сигнала. Это эффект стоячей волны, см. рисунок ниже. Зная частоту сигнала можно посчитать длину кабеля. На самом деле здесь сделано гораздо сложнее, о чем говорить не могу ибо ©.


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


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


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


Сопротивление пары косвенно показывает длину кабеля, при условии подключения дальнего конца к сетевому оборудованию или замыканию накоротко данной пары. Электрическое сопротивление двух последовательно соединенных токопроводящих жил пары можно взять из характеристик конкретного кабеля. Примерно 18 Ом на 100м.

Ethernet

Сетевой интерфейс реализован на микросхеме W5100.

W5100 является функционально законченным 10/100 Ethernet — контроллером с аппаратной реализацией стека протоколов TCP/IP. W5100 совместима со стандартами IEEE 802.3 10BaseT и 802.3u 100BaseTX.

В виду отсутствия возможности подключить W5100 по параллельной шине как внешнюю память (в данном контроллере такая возможность не предусмотрена) она подключена по SPI. А грабли в виде невозможности одновременной работы по SPI других микросхем с W5100 были успешно обойдены тем, что в целях экономии энергии было сделано два домена питания, которые не работают одновременно, либо оба отключены. То есть, когда работает Ethernet, то включена только W5100, а когда работает рефлектометр, только AD9859. Поэтому инвертор для входа SEN не понадобился.

В ядре W5100 реализован только IPv4. Поэтому для разбора пакетов IPv6 сделан свой драйвер, основываясь на RFC и подглядывая сниффером.

W5100W5100

LCD

Использовалось два дисплея: WG14432A и WG12232E. После исчезновения на складах одного просто перешли на второй. При этом, как будет сказано далее в разделе Софт->Вывод, пришлось всего лишь разобраться в контроллере экрана и сделать к нему драйвер. И все!

WG14432A и WG12232E — жидкокристаллические графические индикаторы с разрешением 144х32 и 122х32 точек соответственно. Контроллер в WG14432A — ST7920, а в WG12232E — SED1520.

WG14432A
WG12232E

Клавиатура

Простая пленочная клавиатура на 16 кнопок. На шлейфе 8 контактов: 4 столбца и 4 строки. Чтобы постоянно не опрашивать клавиатуру, к ней прицеплена микросхема 74HC148D, которая при нажатии клавиши формирует прерывание контроллера, после чего уже идет считывание. Минусом данного способа является сложность реализации автоповтора нажатия.



Софт


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

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


Для загрузчика и основной программы было сделано два проекта в IAR. Для первого, область кода указана с самого начала FLASH памяти, а для второго — с некоего определенного адреса. При этом загрузчик использует таблицу прерываний, расположенную в начале FLASH памяти, а основная программа размещает векторы прерываний в RAM и настраивает VIC на работу с ними. Переход из загрузчика в основную программу делается простым переходом на адрес её размещения. Туда, где находится код, отвечающий за начальную инициализацию микроконтроллера.

Процесс настройки и первой прошивки готовой платы выглядит так: прошиваем через UART загрузчик, включаем плату, убеждаемся и в отсутствии сигналов ошибок, подключаем Ethernet кабель и заливаем основную программу, настройки и др. посредством TFTP по соответствующим файлам.

Структура программ обоих проектов сделана следующей.


Диспетчер

В этом разделе должен выразить благодарность DIHALT за его статьи из цикла AVR. Учебный курс. Архитектура Программ. Проанализировав код, я понял, что сам написал эквивалент диспетчера задач, но с отличиями. Во-первых, это немного иной способ организации очереди задач. А во-вторых, у меня Idle это указатель на функцию, в которую можно пихнуть измериловку, для её фоновой работы. Пример использования смотри ниже в статье.

dispatch.с
#include "dispatch.h"

UserFunc_t Tasks[DISPATCH_BUF_LEN];
UserFunc_t DispatchIdleTask;
volatile uint32 TaskIndex;
//==============================================================================
void Dispatch_Idle(void)
{}
//==============================================================================
void Dispatch_Init(void)
{
    TaskIndex = 0;
    for(uint32 i=0;i<DISPATCH_BUF_LEN;i++)
        Tasks[i] = Dispatch_Idle;
    DispatchIdleTask = Dispatch_Idle;
}
//==============================================================================
void Dispatch_SetIdle(UserFunc_t Idle)
{
    DispatchIdleTask = Idle;
}
//==============================================================================
UserFunc_t Dispatch_GetTask(void)
{
    UserFunc_t RetTask;
    DISPATCH_ISR_DISABLE();
    RetTask = DispatchIdleTask;
    if(TaskIndex){
        RetTask = Tasks[0];
#if (DISPATCH_BUF_LEN > 1)
        for(uint32 i=0;i<DISPATCH_BUF_LEN-1;i++)
            Tasks[i] = Tasks[i+1];
        Tasks[DISPATCH_BUF_LEN-1] = DispatchIdleTask;
#endif
        TaskIndex--;
    }
    DISPATCH_ISR_ENABLE();
    return RetTask;
}
//==============================================================================
void Dispatch_AddTask(UserFunc_t Task)
{
    DISPATCH_ISR_DISABLE();
    if (TaskIndex < DISPATCH_BUF_LEN){
        Tasks[TaskIndex] = Task;
        TaskIndex++;
    }
    DISPATCH_ISR_ENABLE();
}
//==============================================================================


dispatch.h
#include "main.h"
#ifndef    __DISPATCH_H
#define    __DISPATCH_H

#define DISPATCH_ISR_DISABLE()     __disable_interrupt();
#define DISPATCH_ISR_ENABLE()      __enable_interrupt();

#define DISPATCH_BUF_LEN          8

typedef void (* UserFunc_t)();

void Dispatch_Init(void);
void Dispatch_SetIdle(UserFunc_t Idle);
UserFunc_t Dispatch_GetTask(void);
void Dispatch_AddTask(UserFunc_t Task);
void Dispatch_Idle(void);

#endif //__DISPATCH_H


Ввод

Ввод в основном посредством клавиатуры. При нажатии кнопки формируется прерывание EINT0. В прерывании сканирование клавиатуры не производится, а только ставится в очередь диспетчера задач функция сканирования. При очередном цикле диспетчера он вызовет функцию сканирования, которая в зависимости от нажатой кнопки добавит в очередь диспетчера функцию — обработчик события. Почему обработчик события, а не кнопки? Потому, что сделана система переназначения действий на возникающие события. И событий может быть больше, чем кнопок, например есть событие для отрисовки экрана.

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

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

uint32 KeybLastTime;
uint32 KeybDelay;

void DEV_SCAN_KEY(void)
{
    // сканируем клавиатуру
    uint32 Key = Keyb_ScanKey();

    // T0TC - текущее значение таймера
    if((T0TC - KeybLastTime) >= KeybDelay){
            Dispatch_AddTask(Events_GetFunc(Key));
            KeybLastTime = T0TC;
        }//if
}


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

Вывод

Интерфейс с пользователем сделан, конечно, на экране. Но отладочные сообщения и другая информация выводятся через UART. Тут тоже спасибо статье AVR. Учебный Курс. Работа на прерываниях. Однако на эту статью есть замечание, которое свело на нет все полезности предложенного метода. Если необходимо вывести не одну, а несколько строк, то пока выводится первая, буфер можно затереть другой строкой. В результате получаем кашу. Пришлось все же сделать ожидание окончания вывода всей строки…

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

Приведу последовательность команд инициализации дисплеев, как точку отсчета для тех, кто тоже будет их использовать.
ST7920
    LCD_INIT_DELAY;

#if (LCD_PARALLEL_TYPE == LCD_PARALLEL_TYPE_8BIT)
    ST7920_wr_cmd(0x30);//Mode 8 bit
    ST7920_wr_cmd(0x30);//Mode 8 bit
#endif //#if (LCD_CONNECT_TYPE == LCD_PARALLEL_TYPE_8BIT)

#if (LCD_PARALLEL_TYPE == LCD_PARALLEL_TYPE_4BIT)
    ST7920_wr_cmd(0x20);//Mode 4 bit
    ST7920_wr_cmd(0x20);//Mode 4 bit
#endif //#if (LCD_CONNECT_TYPE == LCD_PARALLEL_TYPE_4BIT)

    ST7920_wr_cmd(0x0C);//Display on, blink and cursor off
    ST7920_wr_cmd(0x01);//Clear
    LCD_CLEAR_DELAY;
    ST7920_wr_cmd(0x14);//Shift cursor right
    ST7920_wr_cmd(0x06);//Mode set shift right
    ST7920_wr_cmd(0x36);//Mode 8 bit + Extended instruction


SED1520
    LCD_RUN_CLOCK();
    LCD_INIT_DELAY;

    LCD_CS1_OFF();
    LCD_CS2_ON();

    SED1520_wr_cmd(0xE2);//Reset
    SED1520_wr_cmd(0xAE+0x01);//Display         0x00:OFF        0x01:ON
    SED1520_wr_cmd(0xC0+0x00);//Start line      0x00
    SED1520_wr_cmd(0xB8+0x00);//Page address    0x00
    SED1520_wr_cmd(0x00+0x00);//Column address  0x00
    SED1520_wr_cmd(0xA0+0x00);//Counter         0x00:forward    0x01:reverse
    SED1520_wr_cmd(0xA4+0x00);//Static drive    0x00:OFF        0x01:ON
    SED1520_wr_cmd(0xA8+0x01);//Select duty     0x00:1/16       0x01: 1/32

    LCD_CS1_ON();
    LCD_CS2_OFF();

    SED1520_wr_cmd(0xE2);//Reset
    SED1520_wr_cmd(0xAE+0x01);//Display         0x00:OFF        0x01:ON
    SED1520_wr_cmd(0xC0+0x00);//Start line      0x00
    SED1520_wr_cmd(0xB8+0x00);//Page address    0x00
    SED1520_wr_cmd(0x00+0x00);//Column address  0x00
    SED1520_wr_cmd(0xA0+0x00);//Counter         0x00:forward    0x01:reverse
    SED1520_wr_cmd(0xA4+0x00);//Static drive    0x00:OFF        0x01:ON
    SED1520_wr_cmd(0xA8+0x01);//Select duty     0x00:1/16       0x01: 1/32

    LCD_CS1_ON();
    LCD_CS2_ON();


Меню

Выполнение какой-либо задачи состоит из реакций на кнопки, отображения на экране и дополнительных действий (измерений, коммуникации и др.). Для выполнения нескольких задач, нужно уметь переключать обработчики кнопок, отрисовку экрана и дополнительных действий с одних на другие. Для этого был сделана та самая система переназначения действий. Коротко говоря, это просто таблица с адресами функций под разные события для разных задач и набор функций для заполнения таблицы. Имеется в виду, что нет жестко заданной таблицы с адресами функций сразу под все задачи. Таблица заполняется заново при запуске каждой задачи. Меняется назначение клавиш, отрисовка экрана и функция диспетчера Idle с функционалом задачи.

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



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

Сеть

Как было сказано выше, в данном приборе реализован IPv6, являющийся новой версией протокола IP. Я не буду рассказывать про отличия от старого. Да, он чем-то схож, но он другой. Потому пришлось покопаться в RFC и родить некий свой обработчик данного протокола. Вся работа сводится к анализу заголовков и передаче данных следующему обработчику, ответы на ICMP запросы, обнаружение соседей вместо ARP и др.

Хочется отметить то, что сделана система нуль-копирования или, по-другому, Zero Copy. Традиционно можно поступить так: сделать буфер для данных. Для пинга заполняем его некими сгенерированными данными. Потом перед ними нужно добавить заголовок ICMP, для этого нужно переместить эту кучу данных. В свою очередь, перед ним ставится заголовок IPv6, снова перемещаем. Теперь заголовок Ethernet. Перемещаем. Не забыть ещё посчитать контрольную сумму с псевдозаголовком… Гораздо проще сделать буфер побольше и заполнять его не с начала, а с некоторого смещения, достаточного для размещения всех заголовков перед данными.


Разное

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

Многоязычность
Все выводимые строки находятся в массивах, а не в параметрах функций. На каждый язык свой массив строк. Чтобы переключаться с одного на другой используется указатель. Строки заданы через макросы, а индексы строк через enum.
char * const menu_msgs_RU[STRING_COUNT] = {
    MSG_RU_PROGRAM_NAME,
    MSG_RU_ERROR,
    MSG_RU_DONE,
    MSG_RU_MEASURING,
};
char * const menu_msgs_EN[STRING_COUNT] = {
    MSG_EN_PROGRAM_NAME,
    MSG_EN_ERROR,
    MSG_EN_DONE,
    MSG_EN_MEASURING,
};

enum {
    IND_MSG_PROGRAM_NAME = 0,
    IND_MSG_ERROR,
    IND_MSG_DONE,
    IND_MSG_MEASURING,
    STRING_COUNT         // STRING_COUNT стоит последним и будет содержать число равное количеству сток
};

char * const * menu_msgs = menu_msgs_RU;

printf("%s",menu_msgs_RU[IND_MSG_PROGRAM_NAME]);



Файловая система.
Из-за отсутствия EEPROM в контроллере пришлось хранить настройки во FLASH памяти, записывая их с помощью механизма IAP. Для этого нужно было определить место во FLASH памяти и придумать формат хранения. Но тут пришла мысль сделать собственную простую файловую систему и разметить всю FLASH память на файлы. В самом начале размещен загрузчик, далее основная программа., а потом настройки. А так как Ethernet уже на борту, то процесс обновления сделан через TFTP, путем заливки нужного файла. При этом вшитый в контроллер загрузчик в последних секторах был тоже помечен как файл и успешно считывался. Файловая система предоставляет функции чтения, записи и добавления данных к существующему файлу. Запись сделана кэшированной. И опять же, как и с дисплеем, файловая система легко отделима от функций работы с FLASH памятью (IAP) и заменима на другую.

Работа с памятью.
Для контроля динамического выделения памяти сделаны два макроса. Первый макрос выводит данные о размере буфера, его адреса, имени переменной и вызывающей функции. Второй, об освобождении данного участка памяти. И после, запустив одно и тоже действие (функцию, измерение) несколько раз можно убедиться в отсутствии утечек памяти.

//Макрос для работы с памятью
//Раскомментировать для отладки
//#define MENU_USER_MEM_DEBUG

#ifdef MENU_USER_MEM_DEBUG
#undef MENU_USER_MEM_DEBUG
#define MENU_USER_MEM_DEBUG(S,V,SZ)       dmesg(S,V,SZ)
#else
#define MENU_USER_MEM_DEBUG(S,V,SZ)
#endif

...

//Пробуем выделить память
str = (uint8*)malloc(strlen_);

//Проверяем результат
if (str != NULL){

    //Выводим отладочное сообщение
    MENU_USER_MEM_DEBUG("Settings:malloc str addr:%x size:%x\r\n",str,strlen_);
    
    //работаем с str
    ...
}

//Освобождаем память и присваиваем NULL
free(str);
str = NULL;

//Выводим отладочное сообщение
MENU_USER_MEM_DEBUG("Settings:free: str\r\n",,);

Я сделал для себя негласное правило освобождать память в обратном порядке выделения, по принципу FILO. А так же обязательно присвоение NULL освобожденному указателю. Первое правило соблюдать необязательно, но второе необходимо. В следующем примере, несмотря на то, что два буфера выделяются, считай, одновременно, освобождение происходит в обратном порядке (даже если части кода с выделением и освобождением памяти разнесены по разным функциям).
FILO выделения памяти
str1 = (uint8*)malloc(strlen_);
    str2 = (uint8*)malloc(strlen_);
    ...
    free(str2);
    str2 = NULL;
free(str1);
str1 = NULL;


Подсчет контрольных сумм/хешей.
Обычно, когда идет речь о подсчете контрольных сумм приводят примеры такого вида
char input[N] = "сообщение";
int outputCRC;

outputCRC = CRC(input,N);

Однако так делать не стоит. Потому, что входные данные могут состоять из разных кусков памяти. Следует сделать раздельные функции: инициализации, добавления и окончания расчетов. Например:
char input1[N] = "начало";
char input2[N] = "конец";
int outputCRC;
CRC Checksum;

CRC_Init(Checksum);

CRC_Update(Checksum,input1,N);
CRC_Update(Checksum,input2,N);

outputCRC = CRC_Finish(Checksum);

Вот реальный пример. Это контрольная сумма, используемая в TCP/IP. Строго говоря, это 16-битное дополнение к сумме всех 16-битных слов данных.
checksum.c

typedef struct {
    u_long lsum;
    u_long len;
} CRC;

void checksum_init(CRC * crc)
{
    crc->lsum = 0;
    crc->len = 0;
}

void checksum_update(CRC * crc,
                     u_char * src,      /**< pointer to stream  */
                     u_long len         /**< size of stream */
                         )
{
    u_long i, j, len2;
    u_long  tsum;
    u_long lsum;

    if (len == 0)
        return;

    i = 0;
    lsum = crc->lsum;

    if (crc->len % 2){
        tsum = src[i++];
        lsum += tsum;
    }

    len2 = len - i;
    j = (len2 >> 1) << 1;

    while(i < j){
        tsum = src[i++];
        tsum = tsum << 8;
        lsum += tsum;

        tsum = src[i++];
        lsum += tsum;
    }

    if (len2 % 2){
        tsum = src[i++];
        tsum = tsum << 8;
        lsum += tsum;
    }

    crc->lsum = lsum;
    crc->len += len;
}

u_short checksum_finish(CRC * crc)
{
    u_long sum;
    u_long lsum;

    lsum = crc->lsum;

    sum = lsum;
    sum = ~(sum + (lsum >> 16));
    return (u_short) sum;
}


Структура исходников проекта.
Я использую следующий способ организации заголовочных файлов:
  • Все .h файлы собраны в одном, названом main.h.
  • В каждом .c файле ставится только одна директива: #include «filename.h».
  • В каждом .h файле ставится вначале директива: #include «main.h».
  • Директива extern для глобальных переменных заменена макросом DECLARE в файле main.h


Директива #define DECLARE находится только в main.c.

main.c
#define DECLARE
#include "main.h"

__irq __arm void Timer_ISR(void)
{
    if(T1IR_bit.MR0INT){
        Dispatch_AddTask(MENU_REFRESH);
        T1IR_bit.MR0INT = 1;
    }
    VICADDRESS = 0x00;
}

__irq __arm void Keyb_ISR(void)
{
    Dispatch_AddTask(DEV_SCAN_KEY);
    EXTINT_bit.EINT0 = 1;
    VICADDRESS = 0x00;
}

void main(void)
{
    Init();
    ...
    while(1){
        Dispatch_GetTask()();
    }//while(1)
}


main.h
#ifndef __MAIN_H
#define __MAIN_H

#ifndef DECLARE                 // if DECLARE is not defined
#define DECLARE extern          // variables are extern
#else
#define DECLARE
#endif

#include <math.h>
#include <stdio.h>
#include <ctype.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>

...

#endif // __MAIN_H


Все остальные файлы сделаны по этому примеру.

OTHER_FILE.c
#include "OTHER_FILE.h"

...


OTHER_FILE.h
#include "main.h"
#ifndef __OTHER_FILE_H
#define __OTHER_FILE_H

...

#endif //__OTHER_FILE_H


При таком способе упрощается структура каждого исходника. У всех объявлений глобальных переменных вместо extern пишется DECLARE и можно не следить за их определениями. Новые исходники можно делать шаблоном, задавая только имя файла. Нет необходимости следить за включением нужных .h файлов в каждый .c файл — они все в main.h и будут обрабатываться именно в этом порядке. Минусом является то, что изменение в любом .h файле автоматически затрагивает весь проект и IDE полностью его перекомпилирует. Подробнее про директивы препроцессора можно почитать тут.

Кстати о шаблонах. Структура исходных файлов тоже унифицирована по рекомендациям C Coding Standard Application Note AN-2000 от Micrium. В IAR-е сделана быстрая вставка шаблонов, куда я загнал полные шаблоны .c и .h файлов. в них сделана правильная расстановка последовательности описания переменных, функций, макросов и т.д.

CodeTemplates.ENU.txt
#TEMPLATE C_header,"&File name in upcase"
#define %1_MODULE
/*
********************************************************************************
* INCLUDE FILES
********************************************************************************
*/
#include "%1.h"
/*
********************************************************************************
* LOCAL DEFINES
********************************************************************************
*/
/*
********************************************************************************
* LOCAL CONSTANTS
********************************************************************************
*/
/*
********************************************************************************
* LOCAL DATA TYPES
********************************************************************************
*/
/*
********************************************************************************
* LOCAL TABLES
********************************************************************************
*/
/*
********************************************************************************
* LOCAL GLOBAL VARIABLES
********************************************************************************
*/
/*
********************************************************************************
* LOCAL FUNCTION PROTOTYPES
********************************************************************************
*/
/*
********************************************************************************
* LOCAL CONFIGURATION ERRORS
********************************************************************************
*/
#TEMPLATE H_header,"&File name in upcase"
/*
********************************************************************************
*                                 MODULE
********************************************************************************
*/
#include "main.h"
#ifndef __%1_H
#define __%1_H
/*
********************************************************************************
                                INCLUDE FILES
********************************************************************************
*/
/*
********************************************************************************
                      EXTERNS (if a matching code file exists)
********************************************************************************
*/
/*
********************************************************************************
                       DEFAULT CONFIGURATION (if necessary)
********************************************************************************
*/
/*
********************************************************************************
                                 DEFINES
********************************************************************************
*/
/*
********************************************************************************
                               DATA TYPES
********************************************************************************
*/
/*
********************************************************************************
                          GLOBAL VARIABLES
********************************************************************************
*/
/*
********************************************************************************
                              MACRO’S
********************************************************************************
*/
/*
********************************************************************************
                           FUNCTION PROTOTYPES
********************************************************************************
*/
/*
********************************************************************************
                           CONFIGURATION ERRORS
********************************************************************************
*/
#ifndef SOME_DEFINE
#error "SOME_DEFINE not #define'd in '%1.h'"
#error " [MUST be DEF_DISABLED] "
#error " [ || DEF_ENABLED ] "
#endif
/*
********************************************************************************
*                               MODULE END
********************************************************************************
*/
#endif //__%1_H


Заключение


Много сказано, ещё больше умолчено. Надеюсь, некоторые мысли помогут будущим инженерам достичь своих целей.
Если что-то интересует — обращайтесь.

Список сокращений


ЛВС — Локальная Вычислительная Сеть
IPv4 — Internet Protocol version 4
IPv6 — Internet Protocol version 6
RFC — Request for Comments
DHCP — Dynamic Host Configuration Protocol
IP — Internet Protocol
MAC — Media Access Address
FTP — File Transfer Protocol
HTTP — Hyper Text Transfer Protocol
SSH — Secure SHell
ARP — Address Resolution Protocol
IGMP — Internet Group Management Protocol
ICMP — Internet Control Message Protocol
TCP — Transmission Control Protocol
UDP — User Datagram Protocol
TFTP — Trivial File Transfer Protocol
DDS — Direct Digital Synthesizer
NEXT — Near End Crosstalk
FILO — First In Last Out
RAM — Random Access memory
IAP — In Application Programming
VIC — Vectored Interrupt Controller

Литература




Спасибо за внимание!
  • +21
  • 09 октября 2012, 20:30
  • DmitryFx

Комментарии (29)

RSS свернуть / развернуть
А зачем Ethernet-контроллер? У LPC2368 свой же есть.
Пришлось все же сделать ожидание окончания вывода всей строки…
Для этого есть буферизация. Заводим выделенный кольцевой буфер для UART, в функции отправки копируем отправляемые данные в конец буфера и возвращаемся. Затем драйвер сам отошлет содержимое буфера по прерываниям, а новые строку будут просто добавлены в буфер. Проблема возникнет только при заполнении буфера. Здесь есть три варианта — подождать опустошения в функции записи, вернуть ошибку «нету места» или отправить только то, что влезло. Последние два варианта можно объединить, сделав функцию в стиле виндовых функций записи/чтения файлов, которые записывают сколько могут и возвращают количество успешно записанных данных, которое можно использовать как индикатор ошибки.
Много сказано, ещё больше умолчено.
Сказано, как раз, таки мало.
0
  • avatar
  • Vga
  • 09 октября 2012, 21:11
Сказано, как раз, таки мало.

Присоединяюсь. Статья отлично оформлена, но о самом интересном Вы умолчали (я понимаю, «коммерческая тайна», и т. д.).

Помимо озвученных вопросов интересует следящие:

Все .h файлы собраны в одном, названом main.h
В каждом .h файле ставится вначале директива: #include «main.h».

Зачем искусственно создавать рекурсию. Да, это можно разрулить, но не логично что *.h включен в main.h, и, в тоже время *.h включает в себя main.h. Получаются какие-то перекрестные зависимости, из которых тяжело будет выделить отдельный *.h, если Вы захотите перенести код в другой проект.

Директива extern для глобальных переменных заменена макросом DECLARE в файле main.h

Тоже не особо понял смыл данного действия.

З.Ы. Спасибо за статью.
0
1. Про Ethernet. В начале был вопрос ставить PHY или W5100. Решили в пользу W5100. Цена такая же, но PHY с RGMII тогда доступные не нашлись. Плюс драйвера W5100 уже написаны, заживить проще… но, конечно, скорость не та. Теперь бы я стал делать на PHY.
2. «Сказано, как раз, таки мало.» Да, очень много хотелось рассказать, но либо никак либо это уже незначительные детали.
3. main.h. При таком способе создается рекурсия, однако неглубокая. Плюсом является то, что все заголовочные файлы собраны в одном файле. И в каждом исходном файле включается только один заголовочный файл с именем как у исходника. Если сделать пошаговую ручную обработку исходника, как это сделал бы препроцессор (или вывод исходника после препроцессора в отдельный файл, IAR такое умеет), то получится, что перед каждым кодом исходника вставится содержимое всех заголовочных файлов именно в том порядке как они описаны в main.h.
4. DECLARE. Это чтобы не следить объявлена переменная где-то явно, без extern, или нет. То-есть просто пишешь у всех объявлений глобальных переменных DECLARE и все. А если использовать слово extern, то нужно чтобы в одном и только одном файле эта переменная была без extern. Данный маневр применим там, где есть несколько исходников, использующих одну глобальную переменную. И эти исходники нужно включать или временно выключать из проекта. Где должна быть описана переменная явно без extern? Неизвестно. DECLARE в таком случае поможет.

Пожалуй, последние два пункта нужно будет оформить отдельной статьей.
0
Плюсом является то, что все заголовочные файлы собраны в одном файле. И в каждом исходном файле включается только один заголовочный файл с именем как у исходника.
Обычно, при декомпозиции кода пытаются сделать модули максимально автономными, избежать перекрестных ссылок и рекурсии. А Вы ее специально создаете. То, что в каждом *.C файле есть только один #include – это просто синтаксический сахар. Зато внутри *.h файлов у Вас получается спагетти…
DECLARE. Это чтобы не следить объявлена переменная где-то явно, без extern, или нет. То-есть просто пишешь у всех объявлений глобальных переменных DECLARE и все. А если использовать слово extern, то нужно чтобы в одном и только одном файле эта переменная была без extern.
Не понял Вашу идею. У Вас есть модуль, который экспортирует (целесообразность прямого экспорта рассматривать не будем) переменную. В *.h файле для этого модуля вы объявляете эту переменную как «extern» и включаете этот файл в модули, которые должны иметь к доступ к данной переменной.Чем не устраивает такой подход?
То-есть просто пишешь у всех объявлений глобальных переменных DECLARE
И все они, в зависимости от значения дефайна DECLARE вдруг станут «extern». Но это как-то неправильно, получается что у Вас либо все глобальные переменные «extern», либо ни одна из глобальных переменных не «extern».
0
«И все они, в зависимости от значения дефайна DECLARE вдруг станут «extern»». Как раз нет — так как директива #define DECLARE находится только в файле main.c (в самом начале), то в нем все переменные описанные c DECLARE будут без extern, а в остальных исходниках с extern.
0
А почему именно в main.c. А если есть два модуля, которым (по каким-то причинам нужна одна «общая» переменная)?

Управление экспортом функций/переменных (видимостью на уровне объектных файлов) специально сделано на уровне отдельных именованных элементов. А у Вас получается две крайности: либо «все обще» либо «все мое».
0
Да, действительно. При таком способе, все глобальные переменные и все функции общие для всех модулей.
Наверно мне не встречался случай, когда было нужно, чтобы какая-то глобальная переменная была видна только нескольким модулям и не видна остальным. Чем это плохо? Число глобальных переменных невелико, а имена уникальны.

Хотел уточнить насчет «Зато внутри *.h файлов у Вас получается спагетти…». Внутри .h файлов тоже чисто. «Спагетти» из кучи инклудов только в main.h.
0
Чем это плохо?

Плохо выдавать «наружу» больше информации, чем того требует программный интерфейс.

Например, 2 программиста пишут разные модули. Каждый из них «опубликовал» все свои глобальные переменные как «extern». И, случайно, название одной из перечных у них совпало. Каждый (по отдельности) модуль реализован правильно, и (по отдельности) они работают. Но если собрать их в одном проекте – получается конфликт. Нет смыла «публиковать» все внутренности реализации, если того не требует API.

Есть другая причина — для оптимизации лучше, ели переменная/функция «ограничена» модулем. Компилятор может ее использование оптимизировать как угодно (он «видит» как используется данный код внутри модуля). А вот если переменная/функция экспортируется – то компилятор не может применить ряд возможностей по оптимизации – он не знает, как будет использоваться данный объект из других модулей.

Хотел уточнить насчет «Зато внутри *.h файлов у Вас получается спагетти…». Внутри .h файлов тоже чисто. «Спагетти» из кучи инклудов только в main.h.

Но, насколько я понял описание, в каждый *.h Вы включаете main.h. Вот, в конечном итоге и получается «рекурсивное спагетти»…

Или я неправильно Вас понял?

З.Ы. На всякий случай, я извиняюсь за критику.
0
Вы все правильно поняли насчет .h файлов. Я напишу отдельную статью и разберу как это работает. И спасибо за объяснение.
0
Да нет, как это работает понятно, от рекурсивного вложения *.h файлов можно избавится через
#ifndef SOME_H
#define SOME_H
…
#endif

или
pragma once


Непонятно, какой смыл с таком построении проекта
0
Млин, какое-то кривое описание получилось. Я хотел сказать, что лучше «прятать» переменные/функции внутри модуля (через static), если их экспорт явно не требуется.
0
Я поддержу e_mc2 . Так обычно не делается, и тому есть причины. Начиная с того, что перекомпилировать при каждом изменении весь проект неинтересно.
Плюсом является то, что все заголовочные файлы собраны в одном файле.
Это ничего особо не дает. Единственный случай, когда так делается (насколько я знаю) — это при использовании PCH. В этом случае в один файл (std_afx.h для MSVC, например) собираются все общие инклюды (как правило системные, так как обычно именно они включают огромное количество кода, тормозящего компиляцию) и он компилируется в PCH-файл. Но это не твой случай.
Это чтобы не следить объявлена переменная где-то явно, без extern, или нет.
Глобальная переменная должны быть объявлена в заголовочнике модуля, к которому относится (как правило, можно выделить «главный» модуль, к которому относится переменные, а остальные ее оттуда используют), а реализована (выделена) — в .c файле этого модуля.
В твоем же варианте получается, что все глобальные переменные выделяются модулем main.
0
Прикольно, я сейчас тоже схожий девайс разрабатываю, только более специализированный.
0
1. Очень радует когда кто-то рассказывает про реальный проект, такие посты мне лично наиболее интересны.
2. На разработку софта, судя по количеству блоков, похоже ушло много времени.
+1
1. Спасибо. Самое главное, я понял то, что чтобы понять что же ты на самом деле сделал — напиши об этом статью. (Хочешь что-то понять — объясни это другому)
2. Действительно, даже самое незначительное действие, требует создания нескольких модулей, логически связанных, но делающих принципиально разные вещи.
Например, загрузка/сохранение настроек, это не просто прочитать/записать данные в памяти. Это связка файловой системы, TFTP, загрузка дефолтных настроек, парсера файла настроек с обработчиком-исправителем ошибок (ведь у пользователя не должен виснуть прибор, если что-то не так записалось), инициализация внутренних структур для отображения и редактирования параметров и сохранение настроек в файл. Целая эпопея.
0
пленочная клавиатура и жидкокристаллический графический индикатор
… а вы не рассматривали модные сегодня TFT дисплеи с сенсорным экраном?, если да, то почему отказались.
0
Из-за корпуса. Он у нас серийный.
А следующий прибор конечно хочу делать на дисплее с сенсорным экраном.
0
А следующий прибор конечно хочу делать на дисплее с сенсорным экраном.

Ну, решать конечное Вам, но ИМХО – сенсорный экран здесь не к месту.

Это проф. прибор, которым пользуются суровые дядьки-монтажники, лазят с ним по чердакам и кабельным шахтам. Соответственно прибор должен быть «дубовым». А сенсорный экран это красиво, но хрупко и не всегда удобно.
0
Или я не понял, и речь идет о совсем другом приборе, а не о модификации данного устройства?
0
Насчет дубовости прибора это Вы абсолютно правильно сказали. У нас был случай, когда прибор подобного назначения сделали полностью сенсорным, а потом удивлялись почему такое решение не понравилось пользователям. Вариант с кнопками пошел на ура.

Этот прибор переделывать под сенсорный не буду. Придумаю что-нибудь другое.
0
У меня просто был опыт создания подобного прибора (скажем так, продвинутого тестера/анализатора для MODBUS). И у меня есть живой опыт общения с теми самыми суровыми монтажниками.

В моем случае, за несколько версий прибор выродился в коробку с парой светодиодов и парой кнопок. Он лишился 80% первоначального функционала. Но, именно эта примитивная модель стала массовой, а продвинутый и нашпигованный фичами прибор сняли с производства. Вот такие, в моем случае, реалии.
0
Прочитал, как аннотацию к детективу. Глядишь, если копирайт «отвалится», то получится целый роман :) А то и биография.
0
  • avatar
  • hexus
  • 10 октября 2012, 00:48
Рисунки красивие, нарисованы в Visio?
0
  • avatar
  • Nemo
  • 10 октября 2012, 11:51
Блок схемы делал в онлайн редакторе www.gliffy.com
Для сохранения результатов там просят зарегистрироваться, а я просто содрал PrintScreen-ом.
0
Где-то я уже это читал, слово в слово...)
0
а насколько точно меряет длину кабеля и затухания?
приборк конкурентноспособный или был какой-то спецзаказ?
0
У кабелей есть такой параметр как коэффициент укорочения — это отношение скорости света в вакууме к скорости распространения сигнала. Он варьируется для разных конструкций кабелей. Поэтому сразу ткнув прибор в кабель с известной длиной и увидев не ту цифру можно подумать, что все плохо. Но на самом деле все не так. Для UTP данный коэффициент приблизительно равен 1.45 — 1.55. Подобрав его под используемый кабель можно добиться точности 0.1 м на 20 метрах и 0.5 до сотни.
Правда я сделал отображение длин до 20 метров с точностью 0.1 м, а длин больше 20 метров, до 1 метра (целое число). На практике этого вполне достаточно. Максимум, что мерял — бухту 350 м.
Затухание и NEXT в позиции курсора показывается с точностью 0.1 Дб (см. структуру меню), а в отчете округляется до целого числа Дб.
0
Стоимость прибора и где его можно купить?
0
Надеюсь DI меня за это не отпинает. Ищи Тверь Связьприбор LanTest. Там еще и фотка моя есть ))
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.