发新帖我要提问
12
返回列表
打印
[应用相关]

以太网配合FreeRTOS实现socket通信!实战STM32F4以太网DP83848配...

[复制链接]
楼主: keaibukelian
手机看帖
扫描二维码
随时随地手机跟帖
21
keaibukelian|  楼主 | 2021-8-5 10:31 | 只看该作者 |只看大图 回帖奖励 |倒序浏览
3.代码简介(以CubeMx生成代码做说明)

我们来直接说下代码架构跟流程,具体细项不做说明


大概不的部分分为几个:

1)MX_LWIP_Init,包含lwip的初始化,网卡添加,设置默认网卡,启动网卡等

2)在main的while循环中判断是否有数据进来,如果有数据进来,那么通过ETH读出数据,叫给lwip处理


使用特权

评论回复
22
keaibukelian|  楼主 | 2021-8-5 10:31 | 只看该作者
六. 在STM32F407使用freertos移植lwip
在确定每个配置之前,建议别修改其他参数,因为我修改了head size不能正常PING通,这个我会在下面总结问题说明

1.Cubemx配置过程
其中RCC/SYS/UART/ETH/LWIP勾选跟noos完全一模一样,所以在这里我们就不做介绍了,我们主要介绍下勾选FreeRTOS以及上述勾选的差异点

1.1 勾选FreeRTOS
主要牵扯到两点,我用的CMSIS_V1,另外开启了浮点运算,防止有现成用到浮点后宕机情况



使用特权

评论回复
23
keaibukelian|  楼主 | 2021-8-5 10:32 | 只看该作者
1.2 勾选FreeRTOS

SYS/ETH/LWIP的差异

SYS cubemx强烈建议别用systick,所以我们选择TIM1

ETH mode由轮询改为中断(只可选择中断)

LWIP我们改为有RTOS


使用特权

评论回复
24
keaibukelian|  楼主 | 2021-8-5 10:33 | 只看该作者
2.自己移植过程
LwIP 不仅能在裸机上运行,也能在操作系统环境下运行,而且在操作系统环境下,用户能使用NETCONN API 与 Socket API 编程,相比 RAW API 编程会更加简便。操作系统环境下,这意味着多线程环境,一般来说 LwIP 作为一个独立的处理线程运行,用户程序也独立为一个/多个线程,这样子在操作系统中就相互独立开,并且借助操作系统的 IPC 通信机制,更好地实现功能的需求。

LwIP 在设计之初,设计者无法预测 LwIP 运行的环境是怎么样的,而且世界上操作系统那么多,根本没法统一,而如果 LwIP 要运行在操作系统环境中,那么就必须产生依赖,即 LwIP 需要依赖操作系统自身的通信机制,如信号量、互斥量、消息队列(邮箱)等,所以 LwIP 设计者在设计的时候就提供一套与操作系统相关的接口,由用户根据操作系统的不同进行移植,这样子就能降低耦合度,让 LwIP 内核不受其运行的环境影响,因为往往用户并不能完全了解内核的运作,所以只需要用户在移植的时候对 LwIP 提供的接口根据不同操作系统进行完善即可。

2.1 移植FreeRTOS到工程
这个牵扯到RTOS的移植,不是本**范畴,可以自行百度,或者网络上现成的工程一堆,可以自己百度使用

2.2 配置lwip的宏文件lwipopts.h(可以直接把这个copy到你的rtos的lwip中)
#ifndef __LWIPOPTS__H__
#define __LWIPOPTS__H__
#include "main.h"
/*-----------------------------------------------------------------------------*/
/* Current version of LwIP supported by CubeMx: 2.1.2 -*/
/*-----------------------------------------------------------------------------*/
/* Within 'USER CODE' section, code will be kept by default at each generation */
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
#ifdef __cplusplus
extern "C" {
#endif
/* STM32CubeMX Specific Parameters (not defined in opt.h) ---------------------*/
/* Parameters set in STM32CubeMX LwIP Configuration GUI -*/
/*----- WITH_RTOS enabled (Since FREERTOS is set) -----*/
#define WITH_RTOS 1  /* 带RTOS */
/*----- CHECKSUM_BY_HARDWARE enabled -----*/
#define CHECKSUM_BY_HARDWARE 1
/*-----------------------------------------------------------------------------*/
/* LwIP Stack Parameters (modified compared to initialization value in opt.h) -*/
/* Parameters set in STM32CubeMX LwIP Configuration GUI -*/
/*----- Value in opt.h for MEM_ALIGNMENT: 1 -----*/
#define MEM_ALIGNMENT 4
/*----- Value in opt.h for LWIP_ETHERNET: LWIP_ARP || PPPOE_SUPPORT -*/
#define LWIP_ETHERNET 1
/*----- Value in opt.h for LWIP_DNS_SECURE: (LWIP_DNS_SECURE_RAND_XID | LWIP_DNS_SECURE_NO_MULTIPLE_OUTSTANDING | LWIP_DNS_SECURE_RAND_SRC_PORT) -*/
#define LWIP_DNS_SECURE 7
/*----- Value in opt.h for TCP_SND_QUEUELEN: (4*TCP_SND_BUF + (TCP_MSS - 1))/TCP_MSS -----*/
#define TCP_SND_QUEUELEN 9
/*----- Value in opt.h for TCP_SNDLOWAT: LWIP_MIN(LWIP_MAX(((TCP_SND_BUF)/2), (2 * TCP_MSS) + 1), (TCP_SND_BUF) - 1) -*/
#define TCP_SNDLOWAT 1071
/*----- Value in opt.h for TCP_SNDQUEUELOWAT: LWIP_MAX(TCP_SND_QUEUELEN)/2, 5) -*/
#define TCP_SNDQUEUELOWAT 5
/*----- Value in opt.h for TCP_WND_UPDATE_THRESHOLD: LWIP_MIN(TCP_WND/4, TCP_MSS*4) -----*/
#define TCP_WND_UPDATE_THRESHOLD 536
/*----- Value in opt.h for LWIP_NETIF_LINK_CALLBACK: 0 -----*/
#define LWIP_NETIF_LINK_CALLBACK 1
/*----- Value in opt.h for TCPIP_THREAD_STACKSIZE: 0 -----*/
#define TCPIP_THREAD_STACKSIZE 1024 /* TCP/IP线程的栈大小为1024 */
/*----- Value in opt.h for TCPIP_THREAD_PRIO: 1 -----*/
#define TCPIP_THREAD_PRIO osPriorityNormal /* TCP/IP线程的优先级为normal */
/*----- Value in opt.h for TCPIP_MBOX_SIZE: 0 -----*/
#define TCPIP_MBOX_SIZE 6  /* 消息邮箱大小为6个 */
/*----- Value in opt.h for SLIPIF_THREAD_STACKSIZE: 0 -----*/
#define SLIPIF_THREAD_STACKSIZE 1024
/*----- Value in opt.h for SLIPIF_THREAD_PRIO: 1 -----*/
#define SLIPIF_THREAD_PRIO 3
/*----- Value in opt.h for DEFAULT_THREAD_STACKSIZE: 0 -----*/
#define DEFAULT_THREAD_STACKSIZE 1024
/*----- Value in opt.h for DEFAULT_THREAD_PRIO: 1 -----*/
#define DEFAULT_THREAD_PRIO 3
/*----- Value in opt.h for DEFAULT_UDP_RECVMBOX_SIZE: 0 -----*/
#define DEFAULT_UDP_RECVMBOX_SIZE 6
/*----- Value in opt.h for DEFAULT_TCP_RECVMBOX_SIZE: 0 -----*/
#define DEFAULT_TCP_RECVMBOX_SIZE 6
/*----- Value in opt.h for DEFAULT_ACCEPTMBOX_SIZE: 0 -----*/
#define DEFAULT_ACCEPTMBOX_SIZE 6
/*----- Value in opt.h for RECV_BUFSIZE_DEFAULT: INT_MAX -----*/
#define RECV_BUFSIZE_DEFAULT 2000000000
/*----- Value in opt.h for LWIP_STATS: 1 -----*/
#define LWIP_STATS 0
/*----- Value in opt.h for CHECKSUM_GEN_IP: 1 -----*/
#define CHECKSUM_GEN_IP 0
/*----- Value in opt.h for CHECKSUM_GEN_UDP: 1 -----*/
#define CHECKSUM_GEN_UDP 0
/*----- Value in opt.h for CHECKSUM_GEN_TCP: 1 -----*/
#define CHECKSUM_GEN_TCP 0
/*----- Value in opt.h for CHECKSUM_GEN_ICMP: 1 -----*/
#define CHECKSUM_GEN_ICMP 0
/*----- Value in opt.h for CHECKSUM_GEN_ICMP6: 1 -----*/
#define CHECKSUM_GEN_ICMP6 0
/*----- Value in opt.h for CHECKSUM_CHECK_IP: 1 -----*/
#define CHECKSUM_CHECK_IP 0
/*----- Value in opt.h for CHECKSUM_CHECK_UDP: 1 -----*/
#define CHECKSUM_CHECK_UDP 0
/*----- Value in opt.h for CHECKSUM_CHECK_TCP: 1 -----*/
#define CHECKSUM_CHECK_TCP 0
/*----- Value in opt.h for CHECKSUM_CHECK_ICMP: 1 -----*/
#define CHECKSUM_CHECK_ICMP 0
/*----- Value in opt.h for CHECKSUM_CHECK_ICMP6: 1 -----*/
#define CHECKSUM_CHECK_ICMP6 0
/*-----------------------------------------------------------------------------*/
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
#ifdef __cplusplus
}
#endif
#endif /*__LWIPOPTS__H__ */


使用特权

评论回复
25
keaibukelian|  楼主 | 2021-8-5 10:34 | 只看该作者
2.3 sys_arch.c/h 文件的编写
操作系统环境下,LwIP 移植的核心就是编写与操作系统相关的接口文件 sys_arch.c 和 sys_arch.h,这两个文件可以自己创建也可以从 contrib 包中获取,路径分别为“contrib-2.1.0\ports\freertos”与“contrib-2.1.0\ports\freertos\includearch”,用户在移植的时候必须根据操作系统的功能为协议栈提供相应的接口,如邮箱(因为本次移植以 FreeRTOS 为例子, FreeRTOS 中没有邮箱这种概念,但是可以使用消息队列替代,为了迎合 LwIP 中的命名,下文统一采用邮箱表示)、信号量、互斥量等,这些 IPC 通信机制是保证内核与上层 API 接口通信的基本保障,也是内核实现管理的继承,同时在 sys.h 文件中声明了用户需要实现的所有函数框架,这些函数具体见表格





看到那么多函数,是不是头都大了,其实这些函数的实现都是很简单的,首先讲解一下邮箱函数的实现。在 LwIP 中,用户代码与协议栈内部之间是通过邮箱进行数据的交互的,邮箱本质上就是一个指向数据的指针,API 将指针传递给内核,内核通过这个指针访问数据,然后去处理,反之内核将数据传递给用户代码也是通过邮箱将一个指针进行传递。在操作系统环境下,LwIP 会作为一个线程运行,线程的名字叫tcpip_thread,在初始化 LwIP 的时候,内核就会自动创建这个线程,并且在线程运行的时候阻塞在邮箱上,等待数据进行处理,这个邮箱数据的来源可能在底层网卡接收到的数据或者上层应用程序的数据,总之,tcpip_thread线程在获取到邮箱中的数据时候,就会退出阻塞态,去处理数据,在处理完毕数据后又进入阻塞态中等待数据的到来,如此反复。信号量与互斥量的实现为内核提供同步与互斥的机制,比如当用户想要发送一个数据的时候,就会调用上层 API 接口,API 接口就会去先发送一个数据给内核去处理,然后尝试获取一个信号量,因为此时是没有信号量的,所以就会阻塞用户线程;内核在知道用户想要发送数据后,就会调用对应的网卡去发送数据,当数据发送完成后就释放一个信号量告知用户线程发送完成,这样子用户线程就得以继续执行。


使用特权

评论回复
26
keaibukelian|  楼主 | 2021-8-5 10:35 | 只看该作者
所以这些函数的接口都必须由用户实现,下面我们先来看下sys_arch.h

#ifndef __SYS_ARCH_H__
#define __SYS_ARCH_H__
#include "lwip/opt.h"
#if (NO_SYS != 0)
#error "NO_SYS need to be set to 0 to use threaded API"
#endif
#include "cmsis_os.h"
#ifdef  __cplusplus
extern "C" {
#endif
#if (osCMSIS < 0x20000U)
#define SYS_MBOX_NULL (osMessageQId)0
#define SYS_SEM_NULL  (osSemaphoreId)0
typedef osSemaphoreId sys_sem_t;
typedef osSemaphoreId sys_mutex_t;
typedef osMessageQId  sys_mbox_t;
typedef osThreadId    sys_thread_t;
#else
#define SYS_MBOX_NULL (osMessageQueueId_t)0
#define SYS_SEM_NULL  (osSemaphoreId_t)0
typedef osSemaphoreId_t     sys_sem_t;
typedef osSemaphoreId_t     sys_mutex_t;
typedef osMessageQueueId_t  sys_mbox_t;
typedef osThreadId_t        sys_thread_t;
#endif
#ifdef  __cplusplus
}
#endif
#endif /* __SYS_ARCH_H__ */


使用特权

评论回复
27
keaibukelian|  楼主 | 2021-8-5 10:36 | 只看该作者
接下来我们来下sys_arch.c的实现

/* lwIP includes. */
#include "lwip/debug.h"
#include "lwip/def.h"
#include "lwip/sys.h"
#include "lwip/mem.h"
#include "lwip/stats.h"
#if !NO_SYS
#include "cmsis_os.h"
#if defined(LWIP_PROVIDE_ERRNO)
int errno;
#endif
/*-----------------------------------------------------------------------------------*/
//  Creates an empty mailbox.
err_t sys_mbox_new(sys_mbox_t *mbox, int size)
{
#if (osCMSIS < 0x20000U)
  osMessageQDef(QUEUE, size, void *);
  *mbox = osMessageCreate(osMessageQ(QUEUE), NULL);
#else
  *mbox = osMessageQueueNew(size, sizeof(void *), NULL);
#endif
#if SYS_STATS
  ++lwip_stats.sys.mbox.used;
  if(lwip_stats.sys.mbox.max < lwip_stats.sys.mbox.used)
  {
    lwip_stats.sys.mbox.max = lwip_stats.sys.mbox.used;
  }
#endif /* SYS_STATS */
  if(*mbox == NULL)
    return ERR_MEM;
  return ERR_OK;
}
/*-----------------------------------------------------------------------------------*/
/*
  Deallocates a mailbox. If there are messages still present in the
  mailbox when the mailbox is deallocated, it is an indication of a
  programming error in lwIP and the developer should be notified.
*/
void sys_mbox_free(sys_mbox_t *mbox)
{
#if (osCMSIS < 0x20000U)
  if(osMessageWaiting(*mbox))
#else
  if(osMessageQueueGetCount(*mbox))
#endif
  {
    /* Line for breakpoint.  Should never break here! */
    portNOP();
#if SYS_STATS
    lwip_stats.sys.mbox.err++;
#endif /* SYS_STATS */
  }
#if (osCMSIS < 0x20000U)
  osMessageDelete(*mbox);
#else
  osMessageQueueDelete(*mbox);
#endif
#if SYS_STATS
  --lwip_stats.sys.mbox.used;
#endif /* SYS_STATS */
}
/*-----------------------------------------------------------------------------------*/
//   Posts the "msg" to the mailbox.
void sys_mbox_post(sys_mbox_t *mbox, void *data)
{
#if (osCMSIS < 0x20000U)
  while(osMessagePut(*mbox, (uint32_t)data, osWaitForever) != osOK);
#else
  while(osMessageQueuePut(*mbox, &data, 0, osWaitForever) != osOK);
#endif
}
/*-----------------------------------------------------------------------------------*/
//   Try to post the "msg" to the mailbox.
err_t sys_mbox_trypost(sys_mbox_t *mbox, void *msg)
{
  err_t result;
#if (osCMSIS < 0x20000U)
  if(osMessagePut(*mbox, (uint32_t)msg, 0) == osOK)
#else
  if(osMessageQueuePut(*mbox, &msg, 0, 0) == osOK)
#endif
  {
    result = ERR_OK;
  }
  else
  {
    // could not post, queue must be full
    result = ERR_MEM;
#if SYS_STATS
    lwip_stats.sys.mbox.err++;
#endif /* SYS_STATS */
  }
  return result;
}
/*-----------------------------------------------------------------------------------*/
//   Try to post the "msg" to the mailbox.
err_t sys_mbox_trypost_fromisr(sys_mbox_t *mbox, void *msg)
{
  return sys_mbox_trypost(mbox, msg);
}
/*-----------------------------------------------------------------------------------*/
/*
  Blocks the thread until a message arrives in the mailbox, but does
  not block the thread longer than "timeout" milliseconds (similar to
  the sys_arch_sem_wait() function). The "msg" argument is a result
  parameter that is set by the function (i.e., by doing "*msg =
  ptr"). The "msg" parameter maybe NULL to indicate that the message
  should be dropped.
  The return values are the same as for the sys_arch_sem_wait() function:
  Number of milliseconds spent waiting or SYS_ARCH_TIMEOUT if there was a
  timeout.
  Note that a function with a similar name, sys_mbox_fetch(), is
  implemented by lwIP.
*/
u32_t sys_arch_mbox_fetch(sys_mbox_t *mbox, void **msg, u32_t timeout)
{
#if (osCMSIS < 0x20000U)
  osEvent event;
  uint32_t starttime = osKernelSysTick();
#else
  osStatus_t status;
  uint32_t starttime = osKernelGetTickCount();
#endif
  if(timeout != 0)
  {
#if (osCMSIS < 0x20000U)
    event = osMessageGet (*mbox, timeout);
    if(event.status == osEventMessage)
    {
      *msg = (void *)event.value.v;
      return (osKernelSysTick() - starttime);
    }
#else
    status = osMessageQueueGet(*mbox, msg, 0, timeout);
    if (status == osOK)
    {
      return (osKernelGetTickCount() - starttime);
    }
#endif
    else
    {
      return SYS_ARCH_TIMEOUT;
    }
  }
  else
  {
#if (osCMSIS < 0x20000U)
    event = osMessageGet (*mbox, osWaitForever);
    *msg = (void *)event.value.v;
    return (osKernelSysTick() - starttime);
#else
    osMessageQueueGet(*mbox, msg, 0, osWaitForever );
    return (osKernelGetTickCount() - starttime);
#endif
  }
}
/*-----------------------------------------------------------------------------------*/
/*
  Similar to sys_arch_mbox_fetch, but if message is not ready immediately, we'll
  return with SYS_MBOX_EMPTY.  On success, 0 is returned.
*/
u32_t sys_arch_mbox_tryfetch(sys_mbox_t *mbox, void **msg)
{
#if (osCMSIS < 0x20000U)
  osEvent event;
  event = osMessageGet (*mbox, 0);
  if(event.status == osEventMessage)
  {
    *msg = (void *)event.value.v;
#else
  if (osMessageQueueGet(*mbox, msg, 0, 0) == osOK)
  {
#endif
    return ERR_OK;
  }
  else
  {
    return SYS_MBOX_EMPTY;
  }
}
/*----------------------------------------------------------------------------------*/
int sys_mbox_valid(sys_mbox_t *mbox)
{
  if (*mbox == SYS_MBOX_NULL)
    return 0;
  else
    return 1;
}
/*-----------------------------------------------------------------------------------*/
void sys_mbox_set_invalid(sys_mbox_t *mbox)
{
  *mbox = SYS_MBOX_NULL;
}
/*-----------------------------------------------------------------------------------*/
//  Creates a new semaphore. The "count" argument specifies
//  the initial state of the semaphore.
err_t sys_sem_new(sys_sem_t *sem, u8_t count)
{
#if (osCMSIS < 0x20000U)
  osSemaphoreDef(SEM);
  *sem = osSemaphoreCreate (osSemaphore(SEM), 1);
#else
  *sem = osSemaphoreNew(UINT16_MAX, count, NULL);
#endif
  if(*sem == NULL)
  {
#if SYS_STATS
    ++lwip_stats.sys.sem.err;
#endif /* SYS_STATS */
    return ERR_MEM;
  }
  if(count == 0)        // Means it can't be taken
  {
#if (osCMSIS < 0x20000U)
    osSemaphoreWait(*sem, 0);
#else
    osSemaphoreAcquire(*sem, 0);
#endif
  }
#if SYS_STATS
  ++lwip_stats.sys.sem.used;
  if (lwip_stats.sys.sem.max < lwip_stats.sys.sem.used) {
    lwip_stats.sys.sem.max = lwip_stats.sys.sem.used;
  }
#endif /* SYS_STATS */
  return ERR_OK;
}
/*-----------------------------------------------------------------------------------*/
/*
  Blocks the thread while waiting for the semaphore to be
  signaled. If the "timeout" argument is non-zero, the thread should
  only be blocked for the specified time (measured in
  milliseconds).
  If the timeout argument is non-zero, the return value is the number of
  milliseconds spent waiting for the semaphore to be signaled. If the
  semaphore wasn't signaled within the specified time, the return value is
  SYS_ARCH_TIMEOUT. If the thread didn't have to wait for the semaphore
  (i.e., it was already signaled), the function may return zero.
  Notice that lwIP implements a function with a similar name,
  sys_sem_wait(), that uses the sys_arch_sem_wait() function.
*/
u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout)
{
#if (osCMSIS < 0x20000U)
  uint32_t starttime = osKernelSysTick();
#else
  uint32_t starttime = osKernelGetTickCount();
#endif
  if(timeout != 0)
  {
#if (osCMSIS < 0x20000U)
    if(osSemaphoreWait (*sem, timeout) == osOK)
    {
      return (osKernelSysTick() - starttime);
#else
    if(osSemaphoreAcquire(*sem, timeout) == osOK)
    {
        return (osKernelGetTickCount() - starttime);
#endif
    }
    else
    {
      return SYS_ARCH_TIMEOUT;
    }
  }
  else
  {
#if (osCMSIS < 0x20000U)
    while(osSemaphoreWait (*sem, osWaitForever) != osOK);
    return (osKernelSysTick() - starttime);
#else
    while(osSemaphoreAcquire(*sem, osWaitForever) != osOK);
    return (osKernelGetTickCount() - starttime);
#endif
  }
}
/*-----------------------------------------------------------------------------------*/
// Signals a semaphore
void sys_sem_signal(sys_sem_t *sem)
{
  osSemaphoreRelease(*sem);
}
/*-----------------------------------------------------------------------------------*/
// Deallocates a semaphore
void sys_sem_free(sys_sem_t *sem)
{
#if SYS_STATS
  --lwip_stats.sys.sem.used;
#endif /* SYS_STATS */
  osSemaphoreDelete(*sem);
}
/*-----------------------------------------------------------------------------------*/
int sys_sem_valid(sys_sem_t *sem)
{
  if (*sem == SYS_SEM_NULL)
    return 0;
  else
    return 1;
}
/*-----------------------------------------------------------------------------------*/
void sys_sem_set_invalid(sys_sem_t *sem)
{
  *sem = SYS_SEM_NULL;
}
/*-----------------------------------------------------------------------------------*/
#if (osCMSIS < 0x20000U)
osMutexId lwip_sys_mutex;
osMutexDef(lwip_sys_mutex);
#else
osMutexId_t lwip_sys_mutex;
#endif
// Initialize sys arch
void sys_init(void)
{
#if (osCMSIS < 0x20000U)
  lwip_sys_mutex = osMutexCreate(osMutex(lwip_sys_mutex));
#else
  lwip_sys_mutex = osMutexNew(NULL);
#endif
}
/*-----------------------------------------------------------------------------------*/
                                      /* Mutexes*/
/*-----------------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------------*/
#if LWIP_COMPAT_MUTEX == 0
/* Create a new mutex*/
err_t sys_mutex_new(sys_mutex_t *mutex) {
#if (osCMSIS < 0x20000U)
  osMutexDef(MUTEX);
  *mutex = osMutexCreate(osMutex(MUTEX));
#else
  *mutex = osMutexNew(NULL);
#endif
  if(*mutex == NULL)
  {
#if SYS_STATS
    ++lwip_stats.sys.mutex.err;
#endif /* SYS_STATS */
    return ERR_MEM;
  }
#if SYS_STATS
  ++lwip_stats.sys.mutex.used;
  if (lwip_stats.sys.mutex.max < lwip_stats.sys.mutex.used) {
    lwip_stats.sys.mutex.max = lwip_stats.sys.mutex.used;
  }
#endif /* SYS_STATS */
  return ERR_OK;
}
/*-----------------------------------------------------------------------------------*/
/* Deallocate a mutex*/
void sys_mutex_free(sys_mutex_t *mutex)
{
#if SYS_STATS
      --lwip_stats.sys.mutex.used;
#endif /* SYS_STATS */
  osMutexDelete(*mutex);
}
/*-----------------------------------------------------------------------------------*/
/* Lock a mutex*/
void sys_mutex_lock(sys_mutex_t *mutex)
{
#if (osCMSIS < 0x20000U)
  osMutexWait(*mutex, osWaitForever);
#else
  osMutexAcquire(*mutex, osWaitForever);
#endif
}
/*-----------------------------------------------------------------------------------*/
/* Unlock a mutex*/
void sys_mutex_unlock(sys_mutex_t *mutex)
{
  osMutexRelease(*mutex);
}
#endif /*LWIP_COMPAT_MUTEX*/
/*-----------------------------------------------------------------------------------*/
// TODO
/*-----------------------------------------------------------------------------------*/
/*
  Starts a new thread with priority "prio" that will begin its execution in the
  function "thread()". The "arg" argument will be passed as an argument to the
  thread() function. The id of the new thread is returned. Both the id and
  the priority are system dependent.
*/
sys_thread_t sys_thread_new(const char *name, lwip_thread_fn thread , void *arg, int stacksize, int prio)
{
#if (osCMSIS < 0x20000U)
  const osThreadDef_t os_thread_def = { (char *)name, (os_pthread)thread, (osPriority)prio, 0, stacksize};
  return osThreadCreate(&os_thread_def, arg);
#else
  const osThreadAttr_t attributes = {
                        .name = name,
                        .stack_size = stacksize,
                        .priority = (osPriority_t)prio,
                      };
  return osThreadNew(thread, arg, &attributes);
#endif
}
/*
  This optional function does a "fast" critical region protection and returns
  the previous protection level. This function is only called during very short
  critical regions. An embedded system which supports ISR-based drivers might
  want to implement this function by disabling interrupts. Task-based systems
  might want to implement this by using a mutex or disabling tasking. This
  function should support recursive calls from the same task or interrupt. In
  other words, sys_arch_protect() could be called while already protected. In
  that case the return value indicates that it is already protected.
  sys_arch_protect() is only required if your port is supporting an operating
  system.
  Note: This function is based on FreeRTOS API, because no equivalent CMSIS-RTOS
        API is available
*/
sys_prot_t sys_arch_protect(void)
{
#if (osCMSIS < 0x20000U)
  osMutexWait(lwip_sys_mutex, osWaitForever);
#else
  osMutexAcquire(lwip_sys_mutex, osWaitForever);
#endif
  return (sys_prot_t)1;
}
/*
  This optional function does a "fast" set of critical region protection to the
  value specified by pval. See the documentation for sys_arch_protect() for
  more information. This function is only required if your port is supporting
  an operating system.
  Note: This function is based on FreeRTOS API, because no equivalent CMSIS-RTOS
        API is available
*/
void sys_arch_unprotect(sys_prot_t pval)
{
  ( void ) pval;
  osMutexRelease(lwip_sys_mutex);
}
#endif /* !NO_SYS */


使用特权

评论回复
28
keaibukelian|  楼主 | 2021-8-5 10:38 | 只看该作者
2.4 网卡底层的编写
在无操作性移植的时候,我们的网卡收发数据就是单纯的收发数据, ethernetif_input() 函数就是处理接收网卡数据的,但是使用了操作系统的话,我们一般将接收数据函数独立成为一个网卡接收线程,这样子在收到数据的时候才去处理数据,然后递交给内核线程,所以我们只需要稍作修改即可,将函数转换成线程就行了,并且在初始化网卡的时候创建网卡接收线程。当然,我们也能将发送函数独立成一个线程,我们暂时没有必要去处理它,此处只创建一个网卡接收线程,具体见

void ethernetif_input(void const * argument)
{
  struct pbuf *p;
  struct netif *netif = (struct netif *) argument;
  for( ;; )
  {
    if (osSemaphoreWait(s_xSemaphore, TIME_WAITING_FOR_INPUT) == osOK)
    {
      do
      {
        LOCK_TCPIP_CORE();
        p = low_level_input( netif );
        if   (p != NULL)
        {
          if (netif->input( p, netif) != ERR_OK )
          {
            pbuf_free(p);
          }
        }
        UNLOCK_TCPIP_CORE();
      } while(p!=NULL);
    }
  }
}



在网卡接收线程中需要留意一下以下内容:网卡接收线程是需要通过信号量机制去接收数据的,一般来说我们都是使用中断的方式去获取网络数据包,当产生中断的时候,我们一般不会在中断中处理数据,而是告诉对应的线程去处理,也就是我们的网卡接收线程去处理数据,那么就会通过信号量进行同步,当网卡接收到了数据就会产生中断释放一个信号量,然后线程从阻塞中恢复,去获取网卡的数据并且向上层递交。当然我们还需要在中断中对网卡底层进行编写!

void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth)
{
  osSemaphoreRelease(s_xSemaphore);
}



使用特权

评论回复
29
keaibukelian|  楼主 | 2021-8-5 10:39 | 只看该作者
这个函数是有ETH_IRQHandler中的HAL_ETH_IRQHandler调用!

网卡初始化函数 low_level_init()

static void low_level_init(struct netif *netif)
{
  uint32_t regvalue = 0;
  HAL_StatusTypeDef hal_eth_init_status;
/* Init ETH */
   uint8_t MACAddr[6] ;
  heth.Instance = ETH;
  heth.Init.AutoNegotiation = ETH_AUTONEGOTIATION_ENABLE;
  heth.Init.Speed = ETH_SPEED_100M;
  heth.Init.DuplexMode = ETH_MODE_FULLDUPLEX;
  heth.Init.PhyAddress = DP83848_PHY_ADDRESS;
  MACAddr[0] = 0x00;
  MACAddr[1] = 0x80;
  MACAddr[2] = 0xE1;
  MACAddr[3] = 0x00;
  MACAddr[4] = 0x00;
  MACAddr[5] = 0x00;
  heth.Init.MACAddr = &MACAddr[0];
  heth.Init.RxMode = ETH_RXINTERRUPT_MODE;
  heth.Init.ChecksumMode = ETH_CHECKSUM_BY_HARDWARE;
  heth.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII;
  /* USER CODE BEGIN MACADDRESS */
  /* USER CODE END MACADDRESS */
  hal_eth_init_status = HAL_ETH_Init(&heth);
  if (hal_eth_init_status == HAL_OK)
  {
    /* Set netif link flag */
    netif->flags |= NETIF_FLAG_LINK_UP;
  }
  /* Initialize Tx Descriptors list: Chain Mode */
  HAL_ETH_DMATxDescListInit(&heth, DMATxDscrTab, &Tx_Buff[0][0], ETH_TXBUFNB);
  /* Initialize Rx Descriptors list: Chain Mode  */
  HAL_ETH_DMARxDescListInit(&heth, DMARxDscrTab, &Rx_Buff[0][0], ETH_RXBUFNB);
#if LWIP_ARP || LWIP_ETHERNET
  /* set MAC hardware address length */
  netif->hwaddr_len = ETH_HWADDR_LEN;
  /* set MAC hardware address */
  netif->hwaddr[0] =  heth.Init.MACAddr[0];
  netif->hwaddr[1] =  heth.Init.MACAddr[1];
  netif->hwaddr[2] =  heth.Init.MACAddr[2];
  netif->hwaddr[3] =  heth.Init.MACAddr[3];
  netif->hwaddr[4] =  heth.Init.MACAddr[4];
  netif->hwaddr[5] =  heth.Init.MACAddr[5];
  /* maximum transfer unit */
  netif->mtu = 1500;
  /* Accept broadcast address and ARP traffic */
  /* don't set NETIF_FLAG_ETHARP if this device is not an ethernet one */
  #if LWIP_ARP
    netif->flags |= NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP;
  #else
    netif->flags |= NETIF_FLAG_BROADCAST;
  #endif /* LWIP_ARP */
/* create a binary semaphore used for informing ethernetif of frame reception */
  osSemaphoreDef(SEM);
  s_xSemaphore = osSemaphoreCreate(osSemaphore(SEM), 1);
/* create the task that handles the ETH_MAC */
/* USER CODE BEGIN OS_THREAD_DEF_CREATE_CMSIS_RTOS_V1 */
  osThreadDef(EthIf, ethernetif_input, osPriorityRealtime, 0, INTERFACE_THREAD_STACK_SIZE);
  osThreadCreate (osThread(EthIf), netif);
/* USER CODE END OS_THREAD_DEF_CREATE_CMSIS_RTOS_V1 */
  /* Enable MAC and DMA transmission and reception */
  HAL_ETH_Start(&heth);
/* USER CODE BEGIN PHY_PRE_CONFIG */
/* USER CODE END PHY_PRE_CONFIG */
  /**** Configure PHY to generate an interrupt when Eth Link state changes ****/
  /* Read Register Configuration */
  HAL_ETH_ReadPHYRegister(&heth, PHY_MICR, &regvalue);
  regvalue |= (PHY_MICR_INT_EN | PHY_MICR_INT_OE);
  /* Enable Interrupts */
  HAL_ETH_WritePHYRegister(&heth, PHY_MICR, regvalue );
  /* Read Register Configuration */
  HAL_ETH_ReadPHYRegister(&heth, PHY_MISR, &regvalue);
  regvalue |= PHY_MISR_LINK_INT_EN;
  /* Enable Interrupt on change of link status */
  HAL_ETH_WritePHYRegister(&heth, PHY_MISR, regvalue);
/* USER CODE BEGIN PHY_POST_CONFIG */
/* USER CODE END PHY_POST_CONFIG */
#endif /* LWIP_ARP || LWIP_ETHERNET */
/* USER CODE BEGIN LOW_LEVEL_INIT */
/* USER CODE END LOW_LEVEL_INIT */
}


其中low_level_output/low_level_input跟noos函数编写一致即可

使用特权

评论回复
30
keaibukelian|  楼主 | 2021-8-5 10:40 | 只看该作者
3.代码简介(以CubeMx生成代码做说明)

我大概分析了一下init以及收数据的代码流程(红线)


使用特权

评论回复
31
keaibukelian|  楼主 | 2021-8-5 10:41 | 只看该作者
4.使用lwip的socket通信
4.1 socket概念
Socket 英文原意是“孔”或者“插座”的意思,在网络编程中,通常将其称之为“套接字”,当前网络中的主流程序设计都是使用 Socket 进行编程的,因为它简单易用,更是一个标准,能在不同平台很方便移植。本章讲解的是 LwIP 中的 Socket 编程接口,因为 LwIP 作者为了能让更多开发者直接上手 LwIP 的编程,专门设计了 LwIP 的第三种编程接口——Socket API,它兼容 BSDSocket。

Socket 虽然是能在多平台移植,但是 LwIP 中的 Socket 并不完善,因为 LwIP 设计之初就是为了在嵌入式平台中使用,它只实现了完整 Socket 的部分功能,不过,在嵌入式平台中,这些功能早已足够。

在 Socket 中,它使用一个套接字来记录网络的一个连接,套接字是一个整数,就像我们操作文件一样,利用一个文件描述符,可以对它打开、读、写、关闭等操作,类似的,在网络中,我们也可以对 Socket 套接字进行这样子的操作,比如开启一个网络的连接、读取连接主机发送来的数据、向连接的主机发送数据、终止连接等操作。

4.2 socket API介绍
4.2.1 socket() API介绍

这个函数的功能是向内核申请一个套接字,lwip中的socket最终实现是lwip_socket,但是为了兼容BSD socket,所以重新define了一下,我们来看下

/** @ingroup socket */
#define socket(domain,type,protocol)              lwip_socket(domain,type,protocol)
参数:

domain: 表示该套接字使用的协议簇,对于 TCP/IP 协议来说,该值始终为 AF_INET。

type: 指定了套接字使用的服务类型,可能的类型有 3 种:

1)SOCK_STREAM:提供可靠的(即能保证数据正确传送到对方)面向连接的 Socket 服务,多用于资料(如文件)传输,如 TCP 协议。

2)SOCK_DGRAM:是提供无保障的面向消息的 Socket 服务,主要用于在网络上发广播信息,如 UDP 协议,提供无连接不可靠的数据报交付服务。

3)SOCK_RAW:表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少,暂时不用理会它。

protocol :指定了套接字使用的协议,在 IPv4 中,只有 TCP 协议提供 SOCK_STREAM 这种可靠的服务,只有 UDP 协议提供 SOCK_DGRAM 服务,对于这两种协议,protocol 的值均为 0。当申请套接字成功的时候,该函数返回一个 int 类型的值,也是 Socket 描述符,用户通过这个值可以索引到一个 Socket 连接结构——lwip_sock,当申请套接字失败时,该函数返回-1。

4.2.2 bind() API介绍

用于服务器端绑定套接字与网卡信息,lwip中的bind最终实现是lwip_bind,但是为了兼容BSD bind,所以重新define了一下,我们来看下

/** @ingroup socket */
#define bind(s,name,namelen)                      lwip_bind(s,name,namelen)
参数:

s :是表示要绑定的 Socket 套接字,注意了,这个套机字必须是从 socket() 函数中返回的索引,否则将无法完成绑定操作。

name: 是一个指向 sockaddr 结构体的指针,其中包含了网卡的 IP 地址、端口号等重要的信息,LwIP 为了更好描述这些信息,使用了 sockaddr 结构体来定义了必要的信息的字段,它常被用于Socket API 的很多函数中,我们在使用 bind() 的时候,只需要直接填写相关字段即可

namelen: 指定了 name 结构体的长度。

下面我们来说下name的struct,如下:

struct sockaddr {
  u8_t        sa_len;
  sa_family_t sa_family;
  char        sa_data[14];
};
为了便于理解,lwip重新定义了一下这个结构体

/* members are in network byte order */
struct sockaddr_in {
  u8_t            sin_len;
  sa_family_t     sin_family; /* 协议簇 */
  in_port_t       sin_port; /* 端口号 */
  struct in_addr  sin_addr; /* IP地址 */
#define SIN_ZERO_LEN 8
  char            sin_zero[SIN_ZERO_LEN];
};
4.2.3 connect() API介绍

它用于客户端中,将 Socket 与远端 IP 地址、端口号进行绑定,在 TCP 客户端连接中,调用这个函数将发生握手过程(会发送一个 TCP 连接请求),并最终建立新的 TCP 连接,而对于 UDP协议来说,调用这个函数只是在 UDP 控制块中记录远端 IP 地址与端口号,而不发送任何数据,参数信息与 bind() 函数是一样的

/** @ingroup socket */
#define connect(s,name,namelen)                   lwip_connect(s,name,namelen)
4.2.4 listen() API介绍

只能在 TCP 服务器中使用,让服务器进入监听状态,等待远端的连接请求, LwIP 中可以接收多个客户端的连接,因此参数 backlog 指定了请求队列的大小

/** @ingroup socket */
#define listen(s,backlog)                         lwip_listen(s,backlog)
4.2.5 accept() API介绍

用于 TCP 服务器中,等待着远端主机的连接请求,并且建立一个新的 TCP 连接,在调用这个函数之前需要通过调用 listen() 函数让服务器进入监听状态。accept() 函数的调用会阻塞应用线程直至与远程主机建立 TCP 连接。参数 addr 是一个返回结果参数,它的值由 accept() 函数设置,其实就是远程主机的地址与端口号等信息,当新的连接已经建立后,远端主机的信息将保存在连接句柄中,它能够唯一的标识某个连接对象。同时函数返回一个 int 类型的套接字描述符,根据它能索引到连接结构,如果连接失败则返回-1

/** @ingroup socket */
#define accept(s,addr,addrlen)                    lwip_accept(s,addr,addrlen)
4.2.6 read()、recv()、recvfrom() API介绍

read() 与 recv() 函数的核心是调用 recvfrom() 函数, recv() 与 read() 函数用于从 Socket 中接收数据,它们可以是 TCP 协议和 UDP 协议!

/** @ingroup socket */
#define recv(s,mem,len,flags)                     lwip_recv(s,mem,len,flags)
/** @ingroup socket */
#define recvmsg(s,message,flags)                  lwip_recvmsg(s,message,flags)
/** @ingroup socket */
#define recvfrom(s,mem,len,flags,from,fromlen)    lwip_recvfrom(s,mem,len,flags,from,fromlen)
men 参数记录了接收数据的缓存起始地址,len 用于指定接收数据的最大长度,如果函数能正确接收到数据,将会返回一个接收到数据的长度,否则将返回-1,若返回值为 0,表示连接已经终止,应用程序可以根据返回的值进行不一样的操作。recv() 函数包含一个 flags 参数,我们暂时可以直接忽略它,设置为 0 即可。注意,如果接收的数据大于用户提供的缓存区,那么多余的数据会被直接丢弃。

4.2.7 sendto() API介绍

这个函数主要是用于 UDP 协议传输数据中,它向另一端的 UDP 主机发送一个 UDP 报文,参数 data 指定了要发送数据的起始地址,而 size 则指定数据的长度,参数 flag 指定了发送时候的一些处理,比如外带数据等,此时我们不需要理会它,一般设置为 0 即可,参数 to 是一个指向 sockaddr 结构体的指针,在这里需要我们自己提供远端主机的 IP 地址与端口号,并且用 tolen 参数指定这些信息的长度!

/** @ingroup socket */
#define sendto(s,dataptr,size,flags,to,tolen)     lwip_sendto(s,dataptr,size,flags,to,tolen)
4.2.8 sendto() API介绍

send() 函数可以用于 UDP 协议和 TCP 连接发送数据。在调用 send() 函数之前,必须使用 connect()函数将远端主机的 IP 地址、端口号与 Socket 连接结构进行绑定。对于 UDP 协议,send() 函数将调用 lwip_sendto() 函数发送数据,而对于 TCP 协议,将调用 netconn_write_partly() 函数发送数据。相对于 sendto() 函数,参数基本是没啥区别的,但无需我们设置远端主机的信息,更加方便操作,因此这个函数在实际中使用也是很多的

/** @ingroup socket */
#define send(s,dataptr,size,flags)                lwip_send(s,dataptr,size,flags)
4.2.9 write() API介绍

这个函数一般用于处于稳定的 TCP 连接中传输数据,当然也能用于 UDP 协议中,它也是基于lwip_send 上实现的,但是无需我们设置 flag 参数

/** @ingroup socket */
#define write(s,dataptr,len)                      lwip_write(s,dataptr,len)
4.2.10 close() API介绍

close() 函数是用于关闭一个指定的套接字,在关闭套接字后,将无法使用对应的套接字描述符索引到连接结构,该函数的本质是对 netconn_delete() 函数的封装(真正处理的函数是 netconn_prepare_delete()),如果连接是 TCP 协议,将产生一个请求终止连接的报文发送到对端主机中,如果是 UDP 协议,将直接释放 UDP 控制块的内容!

/** @ingroup socket */
#define close(s)                                  lwip_close(s)


4.3 socket创建一个TCP server做一个回显实验
代码如下:

#include "tcpecho.h"
#include "lwip/opt.h"
#if LWIP_SOCKET
#include <lwip/sockets.h>
#include "lwip/sys.h"
#include "lwip/api.h"
/*-----------------------------------------------------------------------------------*/
#define RECV_DATA         (1024)
#define LOCAL_PORT 5001
static void  
tcpecho_thread(void *arg)
{
  int sock = -1,connected;
  char *recv_data;
  struct sockaddr_in server_addr,client_addr;
  socklen_t sin_size;
  int recv_data_len;

  printf("本地端口号是%d\n\n",LOCAL_PORT);

  recv_data = (char *)pvPortMalloc(RECV_DATA);
  if (recv_data == NULL)
  {
      printf("No memory\n");
      goto __exit;
  }

  sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 0)
  {
      printf("Socket error\n");
      goto __exit;
  }

  server_addr.sin_family = AF_INET;
  server_addr.sin_addr.s_addr = INADDR_ANY;
  server_addr.sin_port = htons(LOCAL_PORT);
  memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));

  if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
  {
      printf("Unable to bind\n");
      goto __exit;
  }

  if (listen(sock, 5) == -1)
  {
      printf("Listen error\n");
      goto __exit;
  }

  while(1)
  {
    sin_size = sizeof(struct sockaddr_in);
    connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size);
    printf("new client connected from (%s, %d)\n",
            inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
    {
      int flag = 1;

      setsockopt(connected,
                 IPPROTO_TCP,     /* set option at TCP level */
                 TCP_NODELAY,     /* name of option */
                 (void *) &flag,  /* the cast is historical cruft */
                 sizeof(int));    /* length of option value */
    }

    while(1)
    {
      recv_data_len = recv(connected, recv_data, RECV_DATA, 0);

      if (recv_data_len <= 0)  
        break;

      printf("recv %d len data\n",recv_data_len);

      write(connected,recv_data,recv_data_len);

    }
    if (connected >= 0)  
      closesocket(connected);

    connected = -1;
  }
__exit:
  if (sock >= 0) closesocket(sock);
  if (recv_data) free(recv_data);
}
/*-----------------------------------------------------------------------------------*/
void
tcpecho_init(void)
{
  sys_thread_new("tcpecho_thread", tcpecho_thread, NULL, 512, 4);
}
/*-----------------------------------------------------------------------------------*/

效果如下:

这个实验室我们STM32做TCP server,然后IP是静态IP(192.168.1.250),开启的TCP port是5001,





使用特权

评论回复
32
keaibukelian|  楼主 | 2021-8-5 10:42 | 只看该作者

4.4 socket创建一个TCP client间隔发送数据

#include "tcp_client.h"

#include "lwip/opt.h"

#include "lwip/sys.h"

#include "lwip/api.h"

#include <lwip/sockets.h>

#define DEST_IP_ADDR0 192

#define DEST_IP_ADDR1 168

#define DEST_IP_ADDR2 1

#define DEST_IP_ADDR3 102

#define DEST_PORT 5001

static void client(void *thread_param)

{

  int sock = -1;

  struct sockaddr_in client_addr;

  ip4_addr_t ipaddr;

  uint8_t send_buf[]= "This is a TCP Client test...\n";

  printf("目地IP地址:%d.%d.%d.%d \t 端口号:%d\n\n",      \

          DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3,DEST_PORT);

  printf("请将电脑上位机设置为TCP Server.在User/arch/sys_arch.h文件中将目标IP地址修改为您电脑上的IP地址\n\n");

  printf("修改对应的宏定义:DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3,DEST_PORT\n\n");

  IP4_ADDR(&ipaddr,DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3);

  while(1)

  {

    sock = socket(AF_INET, SOCK_STREAM, 0);

    if (sock < 0)

    {

      printf("Socket error\n");

      vTaskDelay(10);

      continue;

    }  

    client_addr.sin_family = AF_INET;      

    client_addr.sin_port = htons(DEST_PORT);   

    client_addr.sin_addr.s_addr = ipaddr.addr;

    memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));   

    if (connect(sock,  

               (struct sockaddr *)&client_addr,  

                sizeof(struct sockaddr)) == -1)  

    {

        printf("Connect failed!\n");

        closesocket(sock);

        vTaskDelay(10);

        continue;

    }                                            

   

    printf("Connect to server successful!\n");

   

    while (1)

    {

      if(write(sock,send_buf,sizeof(send_buf)) < 0)

        break;

   

      vTaskDelay(1000);

    }

   

    closesocket(sock);

  }

}

void

tcp_client_init(void)

{

  sys_thread_new("client", client, NULL, 512, 4);

}



使用特权

评论回复
33
keaibukelian|  楼主 | 2021-8-5 10:43 | 只看该作者

效果如下:


这个实验室我们STM32做TCP client,然后连接pc的tcp server(PC IP地址为:192.168.1.102),PC的TCP port为5001


使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则