Программирование ARM LwIP: распространенные ошибки Thu, March 28 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

LwIP: распространенные ошибки Печать
Добавил(а) microsin   

Наиболее часто встречающийся источник проблем при использовании lwIP - несколько контекстов выполнения кода внутри библиотеки lwIP (обычно при использовании многопоточных операционных систем наподобие RTOS).

Библиотека lwIP может использоваться в двух разных режимах работы: Mainloop mode (NO_SYS, когда на целевой системе нет операционной системы с несколькими потоками) или OS mode (потоки TCPIP, на целевой системе работает операционная система).

[Mainloop Mode]

В этом режиме можно использовать только API с функциями обратного вызова (Callback-style API). У пользователя есть две возможности обеспечить наличие в библиотеке lwIP только одного контекста выполнения:

1) Перенаправлять принимаемые пакеты (RX ethernet) в lwIP непосредственно в контексте прерывания, путем вызова в ISR функции netif->input. Это подразумевает, что все callback-функции библиотеки lwIP вызываются в контексте прерывания, что может вызвать дополнительные проблемы в коде приложения: IRQ блокируются на длительное время, несколько контекстов выполнения в коде приложения и т. д. Когда приложение захочет вызвать lwIP, ему только необходимо отключить прерывания во время вызова. Если используются таймеры, то потребуется даже больше блокирующего кода, чтобы исключить влияние друг на друга прерываний IRQ таймера и Ethernet IRQ предполагая, что они могут быть вложенными.

2) Запуск lwIP в главном цикле main. Во врезке ниже рассмотрен как раз пример такого случая, в нем lwIP вызывается ТОЛЬКО из тела главного бесконечного цикла. Ethernet IRQ должен помещать принятые пакеты в очередь, которая опрашивается в главном цикле. Гарантируйте, чтобы lwIP никогда не вызывалась из прерывания, например некоторые SPI IRQ хотят перенаправить данные в udp_send() или tcp_write()!

Используйте этот режим, если не используется операционная система. Задайте #define NO_SYS 1. Направляйте приходящие пакеты в функцию netif->input(pbuf, netif), вызываемую из главного цикла, не в контексте прерываний. Вы можете выделить буферы пакетов (packet buffers, PBUF) в контексте прерываний, и помещать их в очередь, которая обрабатывается в главном цикле.

Вызывайте периодически из главного цикла sys_check_timeouts().

Портирование: реализуйте все функции Time [2], критических секций [3] и абстракции компилятора/платформы [4].

В этом режиме API можно использовать только callback-стиля.

Пример кода:

void eth_mac_irq()
{
   /* Здесь обрабатывается прерывание MAC */
   /* Буфер pbuf выделяется из пула (чтобы избежать использования
      кучи в прерываниях) */
   struct pbuf* p = pbuf_alloc(PBUF_RAW, eth_data_count, PBUF_POOL);
   if(p != NULL)
   {
      /* Копирование фрейма Ethernet в pbuf */
      pbuf_take(p, eth_data, eth_data_count);
      /* Поместить данные в очередь, которая обрабатывается
         главном цикле main */
      if(!queue_try_put(&queue, p))
      {
         /* Очередь переполнена -> пакет потерян */
         pbuf_free(p);
      }
   }
}
 
static err_t netif_output(struct netif *netif, struct pbuf *p)
{
   LINK_STATS_INC(link.xmit);
   /* Обновление состояния SNMP (только если SNMP используется) */
   MIB2_STATS_NETIF_ADD(netif, ifoutoctets, p->tot_len);
   int unicast = ((p->payload[0] & 0x01) == 0);
   if (unicast)
   {
      MIB2_STATS_NETIF_INC(netif, ifoutucastpkts);
   }
   else
   {
      MIB2_STATS_NETIF_INC(netif, ifoutnucastpkts);
   }
   lock_interrupts();
   pbuf_copy_partial(p, mac_send_buffer, p->tot_len, 0);
   /* Здесь запускается передача через MAC */
   unlock_interrupts();
   return ERR_OK;
}
 
static void netif_status_callback(struct netif *netif)
{
   printf("netif status changed %s\n", ip4addr_ntoa(netif_ip4_addr(netif)));
}
 
static err_t netif_init(struct netif *netif)
{
   netif->linkoutput = netif_output;
   netif->output     = etharp_output;
   netif->output_ip6 = ethip6_output;
   netif->mtu        = ETHERNET_MTU;
   netif->flags      = NETIF_FLAG_BROADCAST |
                       NETIF_FLAG_ETHARP    |
                       NETIF_FLAG_ETHERNET  |
                       NETIF_FLAG_IGMP      |
                       NETIF_FLAG_MLD6;
   MIB2_INIT_NETIF(netif, snmp_ifType_ethernet_csmacd, 100000000);
   SMEMCPY(netif->hwaddr, your_mac_address_goes_here, sizeof(netif->hwaddr));
   netif->hwaddr_len = sizeof(netif->hwaddr);
   return ERR_OK;
}
 
void main(void)
{
   struct netif netif;
   lwip_init();
   netif_add(&netif,
             IP4_ADDR_ANY,
             IP4_ADDR_ANY,
             IP4_ADDR_ANY,
             NULL,
             netif_init,
             netif_input);
   netif.name[0] = 'e';
   netif.name[1] = '0';
   netif_create_ip6_linklocal_address(&netif, 1);
   netif.ip6_autoconfig_enabled = 1;
   netif_set_status_callback(&netif, netif_status_callback);
   netif_set_default(&netif);
   netif_set_up(&netif);
 
   /* Запуск DHCP и HTTPD */
   dhcp_init();
   httpd_init();
   while(1)
   {
      /* Проверка состояния сетевого соединения, например через
         MDIO-обмен с PHY: */
      if(link_state_changed())
      {
         if(link_is_up())
         {
            netif_set_link_up(&netif);
         }
         else
         {
            netif_set_link_down(&netif);
         }
      }
      /* Проверка на принятые фреймы, отправка их в lwIP */
      lock_interrupts();
      struct pbuf* p = queue_try_get(&queue);
      unlock_interrupts();
      if(p != NULL)
      {
         LINK_STATS_INC(link.recv);
 
         /* Обновление состояния SNMP (только если это используется) */
         MIB2_STATS_NETIF_ADD(netif, ifinoctets, p->tot_len);
         int unicast = ((p->payload[0] & 0x01) == 0);
         if (unicast)
         {
            MIB2_STATS_NETIF_INC(netif, ifinucastpkts);
         }
         else
         {
            MIB2_STATS_NETIF_INC(netif, ifinnucastpkts);
         }
         if(netif.input(p, &netif) != ERR_OK)
         {
            pbuf_free(p);
         }
      }
      /* Циклическая проверка таймеров lwIP */
      sys_check_timeouts();
      /* Здесь должны располагаться другие вызовы для
         функционала приложения: */
      ...
  }
}

[OS Mode]

В этом режиме можно использовать как Callback-style API, так и Sequential-style API. Sequential-style API разработан для вызова из потоков, отличных от потока TCPIP, так что здесь рассматривать нечего. Однако функции Callback-style API должны вызываться только из потока TCPIP. Общая ошибка заключается в том, что вызовы происходят из других потоков или из контекстов IRQ. Ethernet RX должен корректным способом, путем отправки сообщения передавать приходящие пакеты в поток TCPIP, это реализовано в tcpip_input(). И снова, гарантируйте, чтобы lwIP никогда не вызывалась из прерывания, например некоторые SPI IRQ хотят перенаправить данные в udp_send() или tcp_write()!

1) Может использоваться tcpip_callback() для обратных вызовов из потока TCPIP, отсюда можно безопасно вызывать любые функции Callback-style API.

2) Используйте LWIP_TCPIP_CORE_LOCKING (#define LWIP_TCPIP_CORE_LOCKING 1, это состояние по умолчанию, если опция не определена в lwipopts.h). Все функции Callback-style API могут быть вызваны, когда взята блокировка ядра lwIP, см. макросы LOCK_TCPIP_CORE() и UNLOCK_TCPIP_CORE(). Эти макросы нельзя использовать в контексте прерывания! Обратите внимание, что OS должна для этого корректно обрабатывать инверсию приоритета.

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

Портирование: реализуйте все функции Porting [5].

Вы можете использовать Callback-style API вместе с tcpip_callback, и все вызовы Sequential-style API.

[Портирование: слой абстракции системы, интерфейс sys_arch для lwIP]

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

Модуль sys_arch для lwIP предоставляет семафоры, почтовые ящики (mailboxes) и мьютексы and mutexes to lwIP. Для полной функциональности lwIP поддержка нескольких потоков может быть реализована в sys_arch, однако для базового функционала lwIP. Планировщик на таймере реализован в lwIP, однако может быть реализован в портом sys_arch (LWIP_TIMERS_CUSTOM==1).

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

Семафоры могут быть либо счетными, либо двоичными, lwIP работает с ними обоими. Mailboxы-должны быть реализованы как очередь, которая позволяет поместить в неё несколько сообщений (реализация в качестве точки ожидания, когда одновременно может быть помещено только одно сообщение, может крайне негативно сказаться на производительности). Сообщение в mailbox это просто указатель, и ничего больше.

Семафоры представлены типом sys_sem_t, который определен через typedef в файле sys_arch.h. Mailboxe-ы эквивалентно представлены типом sys_mbox_t. Мьютексы представлены типом sys_mutex_t. Библиотека lwIP не накладывает никаких ограничений на то, как эти типы представлены внутри системы.

Начиная с lwIP 1.4.0 семафоры, мьютексы и майлбоксы прототипированы так, что позволяют использовать и указатели, и реальные структуры OS. Таким образом память, необходимая для этих типов, может быть выделена либо по месту (глобально или в стеке), либо из кучи (выделенной внутренне функциями *_new()).

В sys_arch должно быть реализовано следующее:

void sys_init(void)

Вызывается, чтобы инициализировать слой sys_arch.

err_t sys_sem_new(sys_sem_t *sem, u8_t count)

Создает новый семафор. Семафор выделяется в памяти, на которую указывает параметр sem (который может быть как указателем, так и реальной структурой OS). Аргумент count задает начальное состояние семафора (0 или 1). Если семафор был создан, должно быть возвращено значение ERR_OK. Возврат любого другого значения ошибки даст подсказку, что пошло не так, однако кроме assert не реализована реальная обработка ошибок.

void sys_sem_free(sys_sem_t *sem)

Удаляет семафор и освобождает память, выделенную для него.

void sys_sem_signal(sys_sem_t *sem)

Посылает сигнал семафору.

u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout)

Блокирует поток в ожидании сигнала на семафоре. Если аргумент timeout не нулевой, то поток должен заблокирован только на указананное время (в миллисекундах). Если аргумент timeout нулевой, то поток должен быть заблокирован, пока не появится сигнал на семафоре.

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

Обратите внимание, что lwIP реализует функцию с похожим именем sys_sem_wait(), которая использует функцию sys_arch_sem_wait().

int sys_sem_valid(sys_sem_t *sem)

Вернет 1, если семафор допустимый, иначе 0. При использовании указателей простейший способ проверить допустимость семафора - проверить его указатель, что он != NULL. Когда напрямую используются структуры OS, реализация проверки может быть более сложная. Это также может быть определение #define, и в этом случае функция не является прототипом.

void sys_sem_set_invalid(sys_sem_t *sem)

Делает семафор недопустимым, чтобы проверяющая его sys_sem_valid() функция возвращала 0.

ВНИМАНИЕ: это НЕ означает, что семафор должен быть удален (deallocated): перед вызовом этой функции всегда вызывается функция sys_sem_free()!

Это также может быть определение #define, и в этом случае функция не является прототипом.

void sys_mutex_new(sys_mutex_t *mutex)

Создает новый мьютекс. Он выделяется в памяти, на которую указывает параметр mutex (это может быть указатель или реальная структура OS). Если мьютекс был создан, должно быть возвращено значение ERR_OK. Возврат любого другого значения ошибки даст подсказку, что пошло не так, однако кроме assert не реализована реальная обработка ошибок.

void sys_mutex_free(sys_mutex_t *mutex)

Удаляет мьютекс и освобождает память, выделенную для него.

void sys_mutex_lock(sys_mutex_t *mutex)

Блокирует поток до тех пор, пока мьютекс не может быть захвачен.

void sys_mutex_unlock(sys_mutex_t *mutex)

Освобождает мьютекс, ранее заблокированный через sys_mutex_lock().

void sys_mutex_valid(sys_mutex_t *mutex)

Вернет 1, если мьютекс допустимый, иначе вернет 0. Когда используются указатели, простой способ такой проверки - проверить указатель на мьютекс, что он != NULL. Когда напрямую используются структуры OS, реализация проверки может быть более сложная. Это также может быть определение #define, и в этом случае функция не является прототипом.

void sys_mutex_set_invalid(sys_mutex_t *mutex)

Делает мьютекс недопустимым, чтобы проверяющая его функция sys_mutex_valid() возвращала 0.

ВНИМАНИЕ: это НЕ означает, что мьютекс должен быть удален (deallocated): перед вызовом этой функции всегда вызывается функция sys_mutex_free()!

Это также может быть определение #define, и в этом случае функция не является прототипом.

err_t sys_mbox_new(sys_mbox_t *mbox, int size)

Создает пустой mailbox для максимального количества size элементов. Элементы, сохраняемые в mailbox-ах, это указатели. Вы должны определить макрос _MBOX_SIZE в своем файле настроек lwipopts.h, или игнорировать этот параметр в своей реализации, и использовать размер по умолчанию.

Если mailbox был создан, то должно быть возвращено ERR_OK. Возврат любого другого значения ошибки даст подсказку, что пошло не так, однако кроме assert не реализована реальная обработка ошибок.

void sys_mbox_free(sys_mbox_t *mbox)

Удалит mailbox. Если при удалении mailbox в нем все еще есть сообщения, то это означает ошибку программирования в lwIP, о чем следует оповестить разработчика.

void sys_mbox_post(sys_mbox_t *mbox, void *msg)

Помещает сообщение msg в mailbox. Эта функция блокирует выполнение кода, пока msg не будет действительно помещено в ящик.

err_t sys_mbox_trypost(sys_mbox_t *mbox, void *msg)

Пробует поместить msg в mailbox. Вернет ERR_MEM, если ящик заполнен, иначе вернет ERR_OK, если сообщение msg было помещено в ящик.

u32_t sys_arch_mbox_fetch(sys_mbox_t *mbox, void **msg, u32_t timeout)

Блокирует поток, пока сообщение не поступит в mailbox, но не дольше, чем на время timeout в миллисекундах (подобно тому, как это сделано в функции sys_arch_sem_wait()). Если timeout указан 0, то поток должен заблокироваться на неопределенное время, пока не поступит сообщение. Аргумент msg это параметр результата, который устанавливается этой функцией (например действием *msg = ptr). Значение msg может быть NULL, чтобы показать, что сообщение должно быть отброшено.

Возвращаемые значения такие же, как и у функции sys_arch_sem_wait(): количество миллисекунд, потраченных на ожидание, или SYS_ARCH_TIMEOUT, если был таймаут.

Обратите внимание, что в lwIP реализована функция с похожим именем, sys_mbox_fetch().

u32_t sys_arch_mbox_tryfetch(sys_mbox_t *mbox, void **msg)

Эта функция подобна sys_arch_mbox_fetch, однако если сообщения нет в mailbox, она немедленно вернет код результата SYS_MBOX_EMPTY. В случае успеха будет возвращено значение 0.

Для эффективной реализации эта функция может быть реализована в sys_arch.h имитирующим функцию макросом вместо обычной функции. Например, наивной реализацией могла бы быть:

#define sys_arch_mbox_tryfetch(mbox,msg) \
   sys_arch_mbox_fetch(mbox,msg,1)

... хотя такая реализация будет вводить нежелательные задержки.

int sys_mbox_valid(sys_mbox_t *mbox)

Вернет 1, если mailbox допустимый, иначе 0. При использовании указателей простой способ проверки - проверить указатель на ящик, что он != NULL. Когда напрямую используются структуры OS, реализация проверки может быть более сложная. Это так же может быть определение #define, и в этом случае функция не является прототипом.

void sys_mbox_set_invalid(sys_mbox_t *mbox)

Делает mailbox недопустимым, так что проверяющая его функция sys_mbox_valid() вернет 0.

ВНИМАНИЕ: это НЕ означает, что mailbox должен быть удален (deallocated): перед вызовом этой функции всегда вызывается функция sys_mutex_free()!

Это также может быть определение #define, и в этом случае функция не является прототипом.

Если потоки поддерживаются нижележащей операционной системой, и если такая функциональность нужна в lwIP, то также должна быть реализована функция:

sys_thread_t sys_thread_new(char *name, void (* thread)(void *arg), void *arg, int stacksize, int prio)

Запустит новый поток с именем name, с приоритетом prio, тело которого начнет выполняться в функции thread(). Аргумент arg будет передан как аргумент в функцию thread(). Размер стека, используемого этим потоком, задается в параметре stacksize. Функция вернет идентификатор нового потока. И идентификатор, и приоритет зависят от системы.

Когда lwIP используется больше чем в одном контексте (например из нескольких потоков, или из main-цикла и прерываний), то ДОЛЖНА быть разрешена защита SYS_LIGHTWEIGHT_PROT!

sys_prot_t sys_arch_protect(void)

Эта опциональная функция делает "быструю" защиту критического региона, и возвратить предыдущий уровень защиты. Эта функция должна вызываться только на очень коротких критических регионах. Встраиваемая система, которая поддерживает драйверы на основе ISR, может захотеть реализовать эту функцию для запрета прерываний. Системы, основанные на задачах, могут захотеть реализовать это на основе мьютекса или запрета переключения задач. Эта функция должна поддерживать рекурсивные вызовы из одной и той же задачи или прерывания. Другими словами, sys_arch_protect() могла быть безопасно вызвана, когда уже включена защита. Тогда возвращенное значение покажет, что защита уже активна.

sys_arch_protect() требуется только если Ваш порт поддерживает операционную систему.

void sys_arch_unprotect(sys_prot_t pval)

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

Для некоторых конфигураций также понадобится функция:

u32_t sys_now(void)

Эта опциональная функция вернет текущее время в миллисекундах (не беспокойтесь о переполнении этого значения, оно используется только для отслеживания разницы между абсолютными значениями времени). Если эта функция не реализована, то это значит, что Вы не сможете использовать некоторые модули (например метки TCP, внутренние таймауты для NO_SYS==1).

Замечание: будьте осторожны при использовании mem_malloc() в sys_arch. Когда malloc() ссылается на mem_malloc(), Вы можете столкнуться с проблемой циклического вызова функции. В модуле mem.c функция mem_init() пытается выделить семафор с использованием mem_malloc, что конечно же не может быть выполнено, когда sys_arch использует mem_malloc.

[Дополнительные файлы, которые нужны для слоя эмуляции "OS support"]

cc.h - архитектурное окружение, в некотором роде специфика компилятора, и в некотором роде специфика окружения (вероятно следует перенести код поддержки окружения в sys_arch.h.)

Определения типа, используемые lwIP:

u8_t, s8_t, u16_t, s16_t, u32_t, s32_t, mem_ptr_t

Подсказки компилятору для упаковки структур lwIP:

PACK_STRUCT_FIELD(x)
PACK_STRUCT_STRUCT
PACK_STRUCT_BEGIN
PACK_STRUCT_END

Вывод диагностики, специфичный для платформы:

LWIP_PLATFORM_DIAG(x)      - не фатальная проблема, печать сообщения.
LWIP_PLATFORM_ASSERT(x)  - фатально, вывод сообщения и остановка выполнения кода.

Определения портирования для форматтеров printf:

U16_F, S16_F, X16_F, U32_F, S32_F, X32_F, SZT_F

Облегченные механизмы синхронизации:

SYS_ARCH_DECL_PROTECT(x) - декларация переменной состояния защиты.
SYS_ARCH_PROTECT(x)          - вход в режим защиты.
SYS_ARCH_UNPROTECT(x)      - выход из режима защиты.

Если компилятор не предоставляет memset(), то этот файл должен включать определение для этого, или подключать файл, где есть соответствующее определение.

Этот файл должен либо подключать локальный для системы заголовок < errno.h>, где определены стандартные коды ошибок *nix, или должен использовать #define LWIP_PROVIDE_ERRNO, чтобы использовать коды #define в lwip/arch.h.

perf.h     - измерение производительности, специфичное для архитектуры. Измерение может выполняться повсюду в lwip, эти макросы также могут быть определены как пустота.

PERF_START                - начать измерение чего-либо.
PERF_STOP(x)             - остановить измерение и записать результат.

sys_arch.h - заголовок, привязанный к sys_arch.c.

Следующие типы зависят от архитектуры:

sys_sem_t, sys_mbox_t, sys_thread_t,

и опционально:

sys_prot_t

Определения для установки переменных sys_mbox_t и sys_sem_t в NULL:

SYS_MBOX_NULL NULL
SYS_SEM_NULL  NULL

[Ссылки]

1. Multiple Execution Contexts in lwIP code site:nongnu.org.
2. Time sys_now() site:nongnu.org.
3. Critical sections site:nongnu.org.
4. Compiler/platform abstraction site:nongnu.org.
5. Porting (system abstraction layer) site:nongnu.org.

 

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


Защитный код
Обновить

Top of Page