【安富莱】【RL-TCPnet网络教程】第10章 RL-TCPnet网络协议栈移植(FreeRTOS)
第10章 RL-TCPnet网络协议栈移植(FreeRTOS)
本章教程为大家讲解RL-TCPnet网络协议栈的FreeRTOS操作系统移植方式,学习了第6章讲解的底层驱动接口函数之后,移植就比较容易了,主要是添加库文件、配置文件和驱动文件即可。另外,RL-TCPnet移植到FreeRTOS要重新配置RL-TCPnet的接口函数,以此来支持RL-TCPnet多任务运行。使用RTX无需重新配置,因为默认情况下就是采用RTX的API函数配置的。
本章教程含STM32F407开发板和STM32F429开发板的移植。
10.1 移植前准备工作说明
10.2 STM32F407移植RL-TCPnet协议栈
10.3 STM32F429移植RL-TCPnet协议栈
10.4 总结
10.1 移植前准备工作说明
1、学习本章节前,务必要优先学习第6章的底层驱动讲解。
2、RL-TCPnet只有库,没有源码。库分为两个版本,一个用于调试的版本TCPD_CM3.lib和一个正式版本TCP_CM3.lib,当前的例子统一使用调试版本。另外注意,虽然是CM3版本的,但可同时用于CM3和CM4内核的MCU,因为官方没有专门的CM4内核库。
3、测试时,请将网线接到路由器或者交换机上面测试,因为已经使能了DHCP,可以自动获取IP地址。
而且使能了NetBIOS局域网域名,用户只需在电脑端ping armfly,就可以获得板子的IP地址。
4、如果要使用固定IP进行测试,请看附件C。
5、网口使用的是DM9161/9162(挨着9帧串口座的网口),而不是DM9000。
6、找一个简单的工程,最好是跑马灯之类的,越简单越好,我们就在这个简单的工程上面移植即可。
10.2 STM32F407移植RL-TCPnet协议栈
10.2.1 RL-TCPnet网络协议栈移植
首先准备好一个简单的FreeRTOS工程模板,工程模板的制作就不做讲解了,这里的重点是教大家移植RL-TCPnet协议栈。准备好的工程模板如下图所示(大家也可以制作其它任意的工程模板,不限制):
准备好工程模板后,就可以开始移植了。首先要做的就是将所有需要的文件放到工程模板里面。下面分4步跟大家进行说明,当然,不限制必须使用下面的方法添加源码到工程,只要将需要的文件添加到工程模板即可。
第1步:将我们FreeRTOS模板中制作好的RL-ARM文件夹复制粘贴到大家准备好的工程模板中。
RL-ARM文件夹中有如下七个文件夹:
Config文件夹用于存放RTX及其中间件的配置文件。
Driver文件夹用于存放中间件的驱动文件,也就是底层移植文件。
RL-CAN文件夹用于存放CAN总线的源码文件。
RL-FlashFS文件夹用于存放文件系统RL-FlashFS的库文件。
RL-RTX文件夹用于存放RTX的源码文件。
RL-TCPnet文件夹用于存放网络协议栈RL-TCPnet的库文件。
RL-USB文件夹用于存放USB协议栈RL-USB的库文件。
也许有用户会问:我们不是仅仅需要移植RL-TCPnet的相关文件就行了吗,为什么把RTX及其所有中间件都添加进来了?这样做的目的是为了以后升级的方便,如果需要添加USB、文件系统、CAN等组件,直接添加到工程即可。
这些文件全部来自MDK4.74的安装目录,库文件位于路径:C:\Keil_v474\ARM\RV31下,而驱动和配置文件位于路径:C:\Keil_v474\ARM\RL下。
第2步:添加RL-TCPnet的库文件、配置文件和驱动文件到工程,添加完毕后的效果如下:
Net_lib.c,Net_Config.c和NET_Debug.c在RL-ARM文件夹的Config文件里面。
TCPD_CM3.lib在RL-ARM文件夹的RL-TCPnet文件里面。
ETH_STM32F4xx.c和ETH_STM32F4xx.h在RL-ARM文件夹的Driver文件里面。
第3步:添加相应的头文件路径,在原来工程模板的基础上新添加的几个路径:
第4步:也是最后一步,添加预定义宏,点击MDK的option -> c/c++选项,添加上__RTX(注意,字母RTX前面有两个下划线的),添加这个宏定义才可以使能RL-TCPnet的多任务支持。
至此,RL-TCPnet的移植工作就完成了,剩下就是系统配置和应用了。
10.2.2 RL-TCPnet配置说明(Net_Config.c)
RL-TCPnet的配置工作是通过配置文件Net_Config.c实现。在MDK工程中打开文件Net_Config.c,可以看到下图所示的工程配置向导:
RL-TCPnet要配置的选项非常多,我们这里把几个主要的配置选项简单介绍下。
System Definitions
(1)Local Host Name
局域网域名。
这里起名为armfly,使用局域网域名限制为15个字符。
(2)Memory Pool size
参数范围1536-262144字节。
内存池大小配置,单位字节。另外注意一点,配置向导这里显示的单位是字节,如果看原始定义,MDK会做一个自动的4字节倍数转换,比如我们这里配置的是8192字节,那么原始定义是#define MEM_SIZE 2048,也就是8192/4 = 2048。
(3)Tick Timer interval
可取10,20,25,40,50,100,200,单位ms。
系统滴答时钟间隔,也就是网络协议栈的系统时间基准,默认情况下,取值100ms。
Ethernet Network Interface
以太网接口配置,勾选了此选项就可以配置了,如果没有使能DHCP的话,将使用这里配置的固定IP。
(1)MAC Address
局域网内可以随意配置,只要不跟局域网内其它设备的MAC地址冲突即可。
(2)IP Address
IP地址。
(3)Subnet mask
子网掩码。
(4)Default Gateway
默认网关。
Ethernet Network Interface
以太网接口配置,这个配置里面还有如下两项比较重要的配置需要说明。
(1)NetBIOS Name Service
NetBIOS局域网域名服务,这里打上对勾就使能了。这样我们就可以通过前面配置的Local Host Name局域网域名进行访问,而不需要通过IP地址访问了。
(2)Dynaminc Host Configuration
即DHCP,这里打上对勾就使能了。使能了DHCP后,RL-TCPnet就可以从外接的路由器上获得动态IP地址。
UDP Sockets
UDP Sockets配置,打上对勾就使能了此项功能
(1)Number of UDP Sockets
用于配置可创建的UDP Sockets数量。
范围1 – 20。
TCP Sockets
TCP Sockets配置,打上对勾就使能了此项功能
(1)Number of TCP Sockets
用于配置可创建的TCP Sockets数量。
(2)Number of Retries
范围0-20。
用于配置重试次数,TCP数据传输时,如果在设置的重试时间内得不到应答,算一次重试失败,这里就是配置的最大重试次数。
(3)Retry Timeout in seconds
范围1-10,单位秒。
重试时间。学习了第6章讲解的底层驱动接口函数之后,移植就比较容易了。
(4)Default Connect Timeout in seconds
范围1-600,单位秒。
用于配置默认的保持连接时间,即我们常说的Keep Alive时间,如果时间到了将断开连接。常用于HTTP Server,Telnet Server等。
(5)Maximum Segment Size
范围536-1460,单位字节。
MSS定义了TCP数据包能够传输的最大数据分段。
(6)Receive Window Size
范围536-65535,单位字节。
TCP接收窗口大小。
10.2.3 RL-TCPnet调试说明(Net_Debug.c)
(重要说明,RL-TCPnet的调试是通过串口打印出来的)
RL-TCPnet的调试功能是通过配置文件Net_Debug.c实现。在MDK工程中打开文件Net_Debug.c,可以看到下图所示的工程配置向导:
Print Time Stamp
勾选了此选项的话,打印消息时,前面会附带时间信息。
其它所有的选项
默认情况下,所有的调试选项都是关闭的,每个选项有三个调试级别可选择,这里我们以Memory Management Debug为例,点击下拉列表,可以看到里面有Off,Errors only和Full debug三个调试级别可供选择,每个调试选项里面都是这三个级别。
Off:表示关闭此选项的调试功能。
Errors only:表示仅在此选项出错时,将其错误打印出来。
Full debug:表示此选项的全功能调试。
关于调试功能的使用会在第11章详细为大家讲解,移植阶段将其全部关闭即可。
10.2.4 RL-TCPnet的多任务驱动接口函数
要让RL-TCPnet支持多任务,就需要修改Net_lib.c文件。默认情况下,Net_lib.c文件是支持RTX操作系统的,现在要将其修改为支持FreeRTOS,需要修改的几个地方如下:
添加FreeRTOS的头文件。
#if (__RTX) #include "FreeRTOS.h" #include "task.h" #include "queue.h" #include "semphr.h" #endif
定义信号量和互斥信号量。
#if (BSD_ENABLE) static BSD_INFO bsd_scb[BSD_NUMSOCKS + BSD_SRVSOCKS]; #ifdef __RTX SemaphoreHandle_t bsd_mutex = NULL; SemaphoreHandle_t bsd_sem = NULL; #define BSD_INRTX __TRUE #else #define BSD_INRTX __FALSE #endif BSD_CFG bsd_config = { bsd_scb, BSD_NUMSOCKS + BSD_SRVSOCKS, BSD_INRTX, BSD_RCVTOUT * TICK_RATE }; #endif
创建互斥信号量和信号量。
/*--------------------------- init_system -----------------------------------*/ void init_system (void) { /* Initialize configured interfaces and applications. */ #if (ETH_ENABLE) eth_init_link (); #endif #if (PPP_ENABLE) ppp_init_link (); #endif #if (SLIP_ENABLE) slip_init_link (); #endif ip_init (); icmp_init (); #if (ETH_ENABLE && IGMP_ENABLE) igmp_init (); #endif #if (UDP_ENABLE) udp_init (); #endif #if (TCP_ENABLE) tcp_init (); #endif #if (BSD_ENABLE) bsd_init (); #if (BSD_GETHOSTEN) bsd_init_host (); #endif #endif #if (HTTP_ENABLE) http_init (); #endif #if (TNET_ENABLE) tnet_init (); #endif #if (TFTP_ENABLE) tftp_init (); #endif #if (TFTPC_ENABLE) tftpc_init (); #endif #if (FTP_ENABLE) ftp_init (); #endif #if (FTPC_ENABLE) ftpc_init (); #endif #if (ETH_ENABLE && NBNS_ENABLE) nbns_init (); #endif #if (ETH_ENABLE && DHCP_ENABLE) dhcp_init (); #elif (ETH_ENABLE) arp_notify (); #endif #if (DNS_ENABLE) dns_init (); #endif #if (SMTP_ENABLE) smtp_init (); #endif #if (SNMP_ENABLE) snmp_init (); #endif #if (SNTP_ENABLE) sntp_init (); #endif #if (BSD_ENABLE && __RTX) { bsd_mutex = xSemaphoreCreateMutex(); if(bsd_mutex == NULL) { /* ERR */ } bsd_sem = xSemaphoreCreateBinary(); if(bsd_sem == NULL) { /* ERR */ } } #endif }
系统运行时,使用互斥信号量。
/*--------------------------- run_system ------------------------------------*/ void run_system (void) { /* Run configured interfaces and applications. */ #if (BSD_ENABLE && __RTX) xSemaphoreTake(bsd_mutex, portMAX_DELAY); #endif #if (ETH_ENABLE) eth_run_link (); #endif #if (PPP_ENABLE) ppp_run_link (); #endif #if (SLIP_ENABLE) slip_run_link (); #endif ip_run_local (); icmp_run_engine (); #if (ETH_ENABLE && IGMP_ENABLE) igmp_run_host (); #endif #if (TCP_ENABLE) tcp_poll_sockets (); #endif #if (BSD_ENABLE) bsd_poll_sockets (); #endif #if (HTTP_ENABLE) http_run_server (); #endif #if (TNET_ENABLE) tnet_run_server (); #endif #if (TFTP_ENABLE) tftp_run_server (); #endif #if (TFTPC_ENABLE) tftpc_run_client (); #endif #if (FTP_ENABLE) ftp_run_server (); #endif #if (FTPC_ENABLE) ftpc_run_client (); #endif #if (ETH_ENABLE && DHCP_ENABLE) dhcp_run_client (); #endif #if (DNS_ENABLE) dns_run_client (); #endif #if (SMTP_ENABLE) smtp_run_client (); #endif #if (SNMP_ENABLE) snmp_run_agent (); #endif #if (SNTP_ENABLE) sntp_run_client (); #endif #if (BSD_ENABLE && __RTX) xSemaphoreGive(bsd_mutex); #endif }
使能BSD Socket的话,Socket挂起和恢复的实现。
/*--------------------------- bsd_suspend/resume ----------------------------*/ #if (BSD_ENABLE && __RTX) __used void bsd_suspend (U8 *tsk_id) { /* Suspend a socket owner task. */ xSemaphoreGive(bsd_mutex); xSemaphoreTake(bsd_sem, portMAX_DELAY); xSemaphoreTake(bsd_mutex, portMAX_DELAY); } __used void bsd_resume (U8 tsk_id) { /* Resume a task waiting for a socket event. */ xSemaphoreGive(bsd_sem); } #endif
使能BSD Socket的话,锁机制的实现。
/*--------------------------- bsd_lock/unlock -------------------------------*/ #if (BSD_ENABLE && __RTX) __used void bsd_lock (void) { /* Acquire mutex - Lock TCPnet functions. */ xSemaphoreTake(bsd_mutex, portMAX_DELAY); } __used void bsd_unlock (void) { /* Release mutex - Unlock TCPnet functions. */ xSemaphoreGive(bsd_mutex); } #endif
10.2.5 RL-TCPnet应用实例
为了验证移植的RL-TCPnet是否可以使用,需要添加测试代码。下面是编写的测试代码,配套的测试例子完整版是:V5-1003_RL-TCPnet实验_工程移植模板(FreeRTOS)。
FreeRTOS操作系统创建的任务
经过上面的移植和配置之后,在 main.c 文件中添加如下代码,代码中创建了5个用户任务:
vTaskTaskUserIF 任务: 按键消息处理。
vTaskLED 任务: LED闪烁。
vTaskMsgPro 任务: 消息处理,这里用作按键检测。
vTaskTCPnet 任务: RL-TCPnet测试任务。
vTaskStart 任务: 启动任务,实现RL-TCPnet的时间基准更新。
具体代码如下:
#include "includes.h" /* ********************************************************************************************************** 函数声明 ********************************************************************************************************** */ static void vTaskTaskUserIF(void *pvParameters); static void vTaskLED(void *pvParameters); static void vTaskMsgPro(void *pvParameters); static void vTaskTCPnet(void *pvParameters); static void vTaskStart(void *pvParameters); static void AppTaskCreate (void); static void AppObjCreate (void); static void App_Printf(char *format, ...); /* ********************************************************************************************************** 变量声明 ********************************************************************************************************** */ static TaskHandle_t xHandleTaskUserIF = NULL; static TaskHandle_t xHandleTaskLED = NULL; static TaskHandle_t xHandleTaskMsgPro = NULL; static TaskHandle_t xHandleTaskTCPnet = NULL; static TaskHandle_t xHandleTaskStart = NULL; static SemaphoreHandle_t xMutex = NULL; EventGroupHandle_t xCreatedEventGroup = NULL; /* ********************************************************************************************************* * 函 数 名: main * 功能说明: 标准c程序入口。 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ int main(void) { /* 在启动调度前,为了防止初始化STM32外设时有中断服务程序执行,这里禁止全局中断(除了NMI和HardFault)。 这样做的好处是: 1. 防止执行的中断服务程序中有FreeRTOS的API函数。 2. 保证系统正常启动,不受别的中断影响。 3. 关于是否关闭全局中断,大家根据自己的实际情况设置即可。 在移植文件port.c中的函数prvStartFirstTask中会重新开启全局中断。通过指令cpsie i开启,__set_PRIMASK(1) 和cpsie i是等效的。 */ __set_PRIMASK(); /* 硬件初始化 */ bsp_Init(); /* 1. 初始化一个定时器中断,精度高于滴答定时器中断,这样才可以获得准确的系统信息 仅供调试目的,实际项 目中不要使用,因为这个功能比较影响系统实时性。 2. 为了正确获取FreeRTOS的调试信息,可以考虑将上面的关闭中断指令__set_PRIMASK(1); 注释掉。 */ vSetupSysInfoTest(); /* 创建任务 */ AppTaskCreate(); /* 创建任务通信机制 */ AppObjCreate(); /* 启动调度,开始执行任务 */ vTaskStartScheduler(); /* 如果系统正常启动是不会运行到这里的,运行到这里极有可能是用于定时器任务或者空闲任务的 heap空间不足造成创建失败,此要加大FreeRTOSConfig.h文件中定义的heap大小: #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) */ while(); } /* ********************************************************************************************************* * 函 数 名: vTaskTaskUserIF * 功能说明: 接口消息处理。 * 形 参: pvParameters 是在创建该任务时传递的形参 * 返 回 值: 无 * 优 先 级: 1 (数值越小优先级越低,这个跟uCOS相反) ********************************************************************************************************* */ static void vTaskTaskUserIF(void *pvParameters) { uint8_t ucKeyCode; uint8_t pcWriteBuffer[]; while() { ucKeyCode = bsp_GetKey(); if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { /* K1键按下,直接发送事件标志给任务vTaskTCPnet,设置bit0 */ case KEY_DOWN_K1: App_Printf("K1键按下,直接发送事件标志给任务AppTaskTCPMain,bit0被设置\r\n"); xEventGroupSetBits(xCreatedEventGroup, KEY1_BIT0); break; /* K2键按下,直接发送事件标志给任务vTaskTCPnet,设置bit1 */ case KEY_DOWN_K2: App_Printf("K2键按下,直接发送事件标志给任务AppTaskTCPMain,bit1被设置\r\n"); xEventGroupSetBits(xCreatedEventGroup, KEY2_BIT1); break; /* K3键按下,直接发送事件标志给任务vTaskTCPnet,设置bit2 */ case KEY_DOWN_K3: App_Printf("K3键按下,直接发送事件标志给任务AppTaskTCPMain,bit2被设置\r\n"); xEventGroupSetBits(xCreatedEventGroup, KEY3_BIT2); break; /* 摇杆的OK键按下,打印任务执行情况 */ case JOY_DOWN_OK: App_Printf("=================================================\r\n"); App_Printf("任务名 任务状态 优先级 剩余栈 任务序号\r\n"); vTaskList((char *)&pcWriteBuffer); App_Printf("%s\r\n", pcWriteBuffer); App_Printf("\r\n任务名 运行计数 使用率\r\n"); vTaskGetRunTimeStats((char *)&pcWriteBuffer); App_Printf("%s\r\n", pcWriteBuffer); App_Printf("当前动态内存剩余大小 = %d字节\r\n", xPortGetFreeHeapSize()); break; /* 其他的键值不处理 */ default: break; } } vTaskDelay(); } } /* ********************************************************************************************************* * 函 数 名: vTaskLED * 功能说明: LED闪烁 * 形 参: pvParameters 是在创建该任务时传递的形参 * 返 回 值: 无 * 优 先 级: 2 ********************************************************************************************************* */ static void vTaskLED(void *pvParameters) { TickType_t xLastWakeTime; const TickType_t xFrequency = ; /* 获取当前的系统时间 */ xLastWakeTime = xTaskGetTickCount(); while() { bsp_LedToggle(); /* vTaskDelayUntil是绝对延迟,vTaskDelay是相对延迟。*/ vTaskDelayUntil(&xLastWakeTime, xFrequency); } } /* ********************************************************************************************************* * 函 数 名: vTaskMsgPro * 功能说明: 消息处理,这里用作按键检测 * 形 参: pvParameters 是在创建该任务时传递的形参 * 返 回 值: 无 * 优 先 级: 3 ********************************************************************************************************* */ static void vTaskMsgPro(void *pvParameters) { while() { /* 按键扫描 */ bsp_KeyScan(); vTaskDelay(); } } /* ********************************************************************************************************* * 函 数 名: vTaskTCPnet * 功能说明: RL-TCPnet测试任务 * 形 参: pvParameters 是在创建该任务时传递的形参 * 返 回 值: 无 * 优 先 级: 4 ********************************************************************************************************* */ static void vTaskTCPnet(void *pvParameters) { while() { TCPnetTest(); } } /* ********************************************************************************************************* * 函 数 名: vTaskStart * 功能说明: 启动任务,也是最高优先级任务,这里实现RL-TCPnet的时间基准更新 * 形 参: pvParameters 是在创建该任务时传递的形参 * 返 回 值: 无 * 优 先 级: 5 ********************************************************************************************************* */ static void vTaskStart(void *pvParameters) { TickType_t xLastWakeTime; const TickType_t xFrequency = ; /* 初始化RL-TCPnet */ init_TcpNet (); /* 获取当前的系统时间 */ xLastWakeTime = xTaskGetTickCount(); while() { /* RL-TCPnet时间基准更新函数 */ timer_tick (); /* vTaskDelayUntil是绝对延迟,vTaskDelay是相对延迟。*/ vTaskDelayUntil(&xLastWakeTime, xFrequency); } } /* ********************************************************************************************************* * 函 数 名: AppTaskCreate * 功能说明: 创建应用任务 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ static void AppTaskCreate (void) { xTaskCreate( vTaskTaskUserIF, /* 任务函数 */ "vTaskUserIF", /* 任务名 */ , /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ , /* 任务优先级*/ &xHandleTaskUserIF ); /* 任务句柄 */ xTaskCreate( vTaskLED, /* 任务函数 */ "vTaskLED", /* 任务名 */ , /* stack大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ , /* 任务优先级*/ &xHandleTaskLED ); /* 任务句柄 */ xTaskCreate( vTaskMsgPro, /* 任务函数 */ "vTaskMsgPro", /* 任务名 */ , /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ , /* 任务优先级*/ &xHandleTaskMsgPro ); /* 任务句柄 */ xTaskCreate( vTaskTCPnet, /* 任务函数 */ "vTaskTCPnet", /* 任务名 */ , /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ , /* 任务优先级*/ &xHandleTaskTCPnet ); /* 任务句柄 */ xTaskCreate( vTaskStart, /* 任务函数 */ "vTaskStart", /* 任务名 */ , /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ , /* 任务优先级*/ &xHandleTaskStart ); /* 任务句柄 */ } /* ********************************************************************************************************* * 函 数 名: AppObjCreate * 功能说明: 创建任务通信机制 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ static void AppObjCreate (void) { /* 创建互斥信号量 */ xMutex = xSemaphoreCreateMutex(); if(xMutex == NULL) { /* 没有创建成功,用户可以在这里加入创建失败的处理机制 */ } /* 创建事件标志组 */ xCreatedEventGroup = xEventGroupCreate(); if(xCreatedEventGroup == NULL) { /* 没有创建成功,用户可以在这里加入创建失败的处理机制 */ } } /* ********************************************************************************************************* * 函 数 名: App_Printf * 功能说明: 线程安全的printf方式 * 形 参: 同printf的参数。 * 在C中,当无法列出传递函数的所有实参的类型和数目时,可以用省略号指定参数表 * 返 回 值: 无 ********************************************************************************************************* */ static void App_Printf(char *format, ...) { char buf_str[ + ]; va_list v_args; va_start(v_args, format); (void)vsnprintf((char *)&buf_str[], (size_t ) sizeof(buf_str), (char const *) format, v_args); va_end(v_args); /* 互斥信号量 */ xSemaphoreTake(xMutex, portMAX_DELAY); printf("%s", buf_str); xSemaphoreGive(xMutex); }
硬件外设初始化
硬件外设的初始化是在 bsp.c 文件实现:
/* ********************************************************************************************************* * 函 数 名: bsp_Init * 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_Init(void) { /* 由于ST固件库的启动文件已经执行了CPU系统时钟的初始化,所以不必再次重复配置系统时钟。 启动文件配置了CPU主时钟频率、内部Flash访问速度和可选的外部SRAM FSMC初始化。 系统时钟缺省配置为168MHz,如果需要更改,可以修改 system_stm32f4xx.c 文件 */ /* 优先级分组设置为4,可配置0-15级抢占式优先级,0级子优先级,即不存在子优先级。*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); bsp_InitUart(); /* 初始化串口 */ bsp_InitKey(); /* 初始化按键变量 */ bsp_InitLed(); /* 初始LED指示灯端口 */ }
RL-TCPnet功能测试
这里专门创建了一个app_tcpnet_lib.c文件用于RL-TCPnet功能的测试,主要功能是创建了一个TCP Server。
#include "includes.h" /* ********************************************************************************************************* * 用于本文件的调试 ********************************************************************************************************* */ #if 1 #define printf_debug printf #else #define printf_debug(...) #endif /* ********************************************************************************************************* * 用于本文件的调试 ********************************************************************************************************* */ #define PORT_NUM 1001 /* TCP服务器监听端口号 */ /* ********************************************************************************************************* * 变量 ********************************************************************************************************* */ uint8_t socket_tcp; /* ********************************************************************************************************* * 函 数 名: tcp_callback * 功能说明: TCP Socket的回调函数 * 形 参: soc TCP Socket类型 * evt 事件类型 * ptr 事件类型是TCP_EVT_DATA,ptr指向的缓冲区记录着接收到的TCP数据,其余事件记录IP地址 * par 事件类型是TCP_EVT_DATA,记录接收到的数据个数,其余事件记录端口号 * 返 回 值: ********************************************************************************************************* */ U16 tcp_callback (U8 soc, U8 evt, U8 *ptr, U16 par) { char buf[]; uint16_t i; /* 确保是socket_tcp的回调 */ if (soc != socket_tcp) { return (); } switch (evt) { /* 远程客户端连接消息 1、数组ptr存储远程设备的IP地址,par中存储端口号。 2、返回数值1允许连接,返回数值0禁止连接。 */ case TCP_EVT_CONREQ: sprintf(buf, "远程客户端请求连接IP: %d.%d.%d.%d", ptr[], ptr[], ptr[], ptr[]); printf_debug("IP:%s port:%d\r\n", buf, par); return (); /* 连接终止 */ case TCP_EVT_ABORT: break; /* Socket远程连接已经建立 */ case TCP_EVT_CONNECT: printf_debug("Socket is connected to remote peer\r\n"); break; /* 连接断开 */ case TCP_EVT_CLOSE: printf_debug("Connection has been closed\r\n"); break; /* 发送的数据收到远程设备应答 */ case TCP_EVT_ACK: break; /* 接收到TCP数据帧,ptr指向数据地址,par记录数据长度,单位字节 */ case TCP_EVT_DATA: printf_debug("Data length = %d\r\n", par); for(i = ; i < par; i++) { printf_debug("ptr[%d] = %d\r\n", i, ptr[i]); } break; } return (); } /* ********************************************************************************************************* * 函 数 名: TCP_StatusCheck * 功能说明: 检测TCP的连接状态,主要用于网线插拔的判断 * 形 参: 无 * 返 回 值: __TRUE 连接 * __FALSE 断开 ********************************************************************************************************* */ uint8_t TCP_StatusCheck(void) { uint8_t res; switch (tcp_get_state(socket_tcp)) { case TCP_STATE_FREE: case TCP_STATE_CLOSED: res = tcp_listen (socket_tcp, PORT_NUM); printf_debug("tcp listen res = %d\r\n", res); break; case TCP_STATE_LISTEN: break; case TCP_STATE_CONNECT: return (__TRUE); default: break; } return (__FALSE); } /* ********************************************************************************************************* * 函 数 名: TCPnetTest * 功能说明: TCPnet应用 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void TCPnetTest(void) { int32_t iCount; uint8_t *sendbuf; uint8_t tcp_status; uint16_t maxlen; uint8_t res; const TickType_t xTicksToWait = ; /* 延迟2ms */ EventBits_t uxBits; /* 创建TCP Socket并创建监听,客户端连接服务器后,10秒内无数据通信将断开连接。 但是由于这里使能了TCP_TYPE_KEEP_ALIVE,会一直保持连接,不受10秒的时间限制。 */ socket_tcp = tcp_get_socket (TCP_TYPE_SERVER|TCP_TYPE_KEEP_ALIVE, , , tcp_callback); if(socket_tcp != ) { res = tcp_listen (socket_tcp, PORT_NUM); printf_debug("tcp listen res = %d\r\n", res); } while () { /* RL-TCPnet处理函数 */ main_TcpNet(); /* 用于网线插拔的处理 */ tcp_status = TCP_StatusCheck(); /* 等待所有任务发来事件标志 */ uxBits = xEventGroupWaitBits(xCreatedEventGroup, /* 事件标志组句柄 */ 0xFFFF, /* 等待0xFFFF某一位被设置 */ pdTRUE,/* 退出前0xFFFF位被清除,这里是任意0xFFFF位被设置就“退出”*/ pdFALSE, /* 设置为pdTRUE表示等待0xFFFF任意位被设置*/ xTicksToWait);/* 等待延迟时间 */ if((uxBits != )&&(tcp_status == __TRUE)) { switch (uxBits) { /* 接收到K1键按下,给远程TCP客户端发送8字节数据 */ case KEY1_BIT0: printf_debug("tcp_get_state(socket_tcp) = %d\r\n", tcp_get_state(socket_tcp)); iCount = ; do { main_TcpNet(); if (tcp_check_send (socket_tcp) == __TRUE) { maxlen = tcp_max_dsize (socket_tcp); iCount -= maxlen; if(iCount < ) { /* 这么计算没问题的 */ maxlen = iCount + maxlen; } sendbuf = tcp_get_buf(maxlen); sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; /* 测试发现只能使用获取的内存 */ tcp_send (socket_tcp, sendbuf, maxlen); } }while(iCount > ); break; /* 接收到K2键按下,给远程TCP客户端发送1024字节的数据 */ case KEY2_BIT1: printf_debug("tcp_get_state(socket_tcp) = %d\r\n", tcp_get_state(socket_tcp)); iCount = ; do { main_TcpNet(); if (tcp_check_send (socket_tcp) == __TRUE) { maxlen = tcp_max_dsize (socket_tcp); iCount -= maxlen; if(iCount < ) { /* 这么计算没问题的 */ maxlen = iCount + maxlen; } /* 这里仅初始化了每次所发送数据包的前8个字节 */ sendbuf = tcp_get_buf(maxlen); sendbuf[] = 'a'; sendbuf[] = 'b'; sendbuf[] = 'c'; sendbuf[] = 'd'; sendbuf[] = 'e'; sendbuf[] = 'f'; sendbuf[] = 'g'; sendbuf[] = 'h'; /* 测试发现只能使用获取的内存 */ tcp_send (socket_tcp, sendbuf, maxlen); } }while(iCount > ); break; /* 接收到K3键按下,给远程TCP客户端发送5MB数据 */ case KEY3_BIT2: printf_debug("tcp_get_state(socket_tcp) = %d\r\n", tcp_get_state(socket_tcp)); iCount = **; do { main_TcpNet(); if (tcp_check_send (socket_tcp) == __TRUE) { maxlen = tcp_max_dsize (socket_tcp); iCount -= maxlen; if(iCount < ) { /* 这么计算没问题的 */ maxlen = iCount + maxlen; } /* 这里仅初始化了每次所发送数据包的前8个字节 */ sendbuf = tcp_get_buf(maxlen); sendbuf[] = 'a'; sendbuf[] = 'b'; sendbuf[] = 'c'; sendbuf[] = 'd'; sendbuf[] = 'e'; sendbuf[] = 'f'; sendbuf[] = 'g'; sendbuf[] = 'h'; /* 测试发现只能使用获取的内存 */ tcp_send (socket_tcp, sendbuf, maxlen); } }while(iCount > ); break; /* 其他的键值不处理 */ default: break; } } } }
至此,FreeRTOS方式移植的RL-TCPnet就可以运行了。
10.2.6 RL-TCPnet实验测试和实验现象
测试前,先将开发板上面的DM9161/9162网口通过网线接到路由器或者交换机上面。
RJ45网络变压器插座上绿灯和黄灯现象
各种网卡、交换机等网络设备都不一样,一般来讲:绿灯分为亮或不亮(代表网络速度),黄灯分为闪烁或不闪烁(代表是否有数据收发)。
绿灯:长亮代表100M; 不亮代表10M。
黄灯:长亮代表无数据收发; 闪烁代表有数据收发。
也有些千兆网卡的灯以颜色区分,不亮代表10M / 绿色代表100M / 黄色代表1000M。现在10M的网络基本看不到了,如果一个灯长亮,基本可以说明100M网络或更高,而另一个灯时而闪烁,那代表有数据收发,具体要看你的网络设备了。甚至有些低等网卡如TP-LINK,只有一个灯,亮代表连通,闪烁代表数据收发。
对于STM32F407开发板上面的RJ45网络变压器插座上面的灯而言,绿灯代表数据收发,长亮的话表示无数据收发,闪烁代表有数据收发。黄灯代表网络速度,长亮代表100M,不亮代表10M。
底层驱动执行情况
为了验证RL-TCPnet底层驱动接口函数是否有问题,专门在ETH_STM32F4xx.c文件中配置了串口调试打印函数:
/* ********************************************************************************************************* * 用于本文件的调试 ********************************************************************************************************* */ #if 1 #define printf_eth printf #else #define printf_eth(...) #endif
如果底层驱动正常执行了,打印的效果如下:
ping是否正确
ping命令的主要作用是通过发送数据包并接收应答信息来检测两台设备之间的网络是否连通。ping命令成功说明当前主机与目的主机之间存在连通的路径。如果不成功,需要查看网线是否连通、网卡设置是否正确、IP地址是否可用等。测试方法如下:
(1)WIN+R组合键打开“运行”窗口,输入cmd。
(2)弹出的命令窗口中,输入ping armfly,因为在前面的配置中我们使能了NetBIOS局域网域名,并将名字设置为armfly,而且使能了DHCP,通过ping命令还可以获得板子自动获取的IP地址。
(3)输入ping armfly后,回车。
收发相同,没有数据丢失,说明ping命令也是成功的。
电脑端创建一个TCP Client与板子上面的TCP Server通信
具体测试方法,查看第13章的13.6小节即可,因为配套例子实现的功能是一样的。
10.3 STM32F429移植RL-TCPnet协议栈
10.3.1 RL-TCPnet网络协议栈移植
首先准备好一个简单的FreeRTOS工程模板,工程模板的制作就不做讲解了,这里的重点是教大家移植RL-TCPnet协议栈。准备好的工程模板如下图所示(大家也可以制作其它任意的工程模板,不限制):
准备好工程模板后,就可以开始移植了。首先要做的就是将所有需要的文件放到工程模板里面。下面分4步跟大家进行说明,当然,不限制必须使用下面的方法添加源码到工程,只要将需要的文件添加到工程模板即可。
第1步:将我们FreeRTOS模板中制作好的RL-ARM文件夹复制粘贴到大家准备好的工程模板中。
RL-ARM文件夹中有如下七个文件夹:
Config文件夹用于存放RTX及其中间件的配置文件。
Driver文件夹用于存放中间件的驱动文件,也就是底层移植文件。
RL-CAN文件夹用于存放CAN总线的源码文件。
RL-FlashFS文件夹用于存放文件系统RL-FlashFS的库文件。
RL-RTX文件夹用于存放RTX的源码文件。
RL-TCPnet文件夹用于存放网络协议栈RL-TCPnet的库文件。
RL-USB文件夹用于存放USB协议栈RL-USB的库文件。
也许有用户会问:我们不是仅仅需要移植RL-TCPnet的相关文件就行了吗,为什么把RTX及其所有中间件都添加进来了?这样做的目的是为了以后升级的方便,如果需要添加USB、文件系统、CAN等组件,直接添加到工程即可。
这些文件全部来自MDK4.74的安装目录,库文件位于路径:C:\Keil_v474\ARM\RV31下,而驱动和配置文件位于路径:C:\Keil_v474\ARM\RL下。
第2步:添加RL-TCPnet的库文件、配置文件和驱动文件到工程,添加完毕后的效果如下:
Net_lib.c,Net_Config.c和NET_Debug.c在RL-ARM文件夹的Config文件里面。
TCPD_CM3.lib在RL-ARM文件夹的RL-TCPnet文件里面。
ETH_STM32F4xx.c和ETH_STM32F4xx.h在RL-ARM文件夹的Driver文件里面。
第3步:添加相应的头文件路径,在原来工程模板的基础上新添加的几个路径:
第4步:也是最后一步,添加预定义宏,点击MDK的option -> c/c++选项,添加上__RTX(注意,字母RTX前面有两个下划线的),添加这个宏定义才可以使能RL-TCPnet的多任务支持。
至此,RL-TCPnet的移植工作就完成了,剩下就是系统配置和应用了。
10.3.2 RL-TCPnet配置说明(Net_Config.c)
RL-TCPnet的配置工作是通过配置文件Net_Config.c实现。在MDK工程中打开文件Net_Config.c,可以看到下图所示的工程配置向导:
RL-TCPnet要配置的选项非常多,我们这里把几个主要的配置选项简单介绍下。
System Definitions
(1)Local Host Name
局域网域名。
这里起名为armfly,使用局域网域名限制为15个字符。
(2)Memory Pool size
参数范围1536-262144字节。
内存池大小配置,单位字节。另外注意一点,配置向导这里显示的单位是字节,如果看原始定义,MDK会做一个自动的4字节倍数转换,比如我们这里配置的是8192字节,那么原始定义是#define MEM_SIZE 2048,也就是8192/4 = 2048。
(3)Tick Timer interval
可取10,20,25,40,50,100,200,单位ms。
系统滴答时钟间隔,也就是网络协议栈的系统时间基准,默认情况下,取值100ms。
Ethernet Network Interface
以太网接口配置,勾选了此选项就可以配置了,如果没有使能DHCP的话,将使用这里配置的固定IP。
(1)MAC Address
局域网内可以随意配置,只要不跟局域网内其它设备的MAC地址冲突即可。
(2)IP Address
IP地址。
(3)Subnet mask
子网掩码。
(4)Default Gateway
默认网关。
Ethernet Network Interface
以太网接口配置,这个配置里面还有如下两项比较重要的配置需要说明。
(1)NetBIOS Name Service
NetBIOS局域网域名服务,这里打上对勾就使能了。这样我们就可以通过前面配置的Local Host Name局域网域名进行访问,而不需要通过IP地址访问了。
(2)Dynaminc Host Configuration
即DHCP,这里打上对勾就使能了。使能了DHCP后,RL-TCPnet就可以从外接的路由器上获得动态IP地址。
UDP Sockets
UDP Sockets配置,打上对勾就使能了此项功能
(1)Number of UDP Sockets
用于配置可创建的UDP Sockets数量。
范围1 – 20。
TCP Sockets
TCP Sockets配置,打上对勾就使能了此项功能
(1)Number of TCP Sockets
用于配置可创建的TCP Sockets数量。
(2)Number of Retries
范围0-20。
用于配置重试次数,TCP数据传输时,如果在设置的重试时间内得不到应答,算一次重试失败,这里就是配置的最大重试次数。
(3)Retry Timeout in seconds
范围1-10,单位秒。
重试时间。如果发送的数据在重试时间内得不到应答,将重新发送数据。
(4)Default Connect Timeout in seconds
范围1-600,单位秒。
用于配置默认的保持连接时间,即我们常说的Keep Alive时间,如果时间到了将断开连接。常用于HTTP Server,Telnet Server等。
(5)Maximum Segment Size
范围536-1460,单位字节。
MSS定义了TCP数据包能够传输的最大数据分段。
(6)Receive Window Size
范围536-65535,单位字节。
TCP接收窗口大小。
10.3.3 RL-TCPnet调试说明(Net_Debug.c)
(重要说明,RL-TCPnet的调试是通过串口打印出来的)
RL-TCPnet的调试功能是通过配置文件Net_Debug.c实现。在MDK工程中打开文件Net_Debug.c,可以看到下图所示的工程配置向导:
Print Time Stamp
勾选了此选项的话,打印消息时,前面会附带时间信息。
其它所有的选项
默认情况下,所有的调试选项都是关闭的,每个选项有三个调试级别可选择,这里我们以Memory Management Debug为例,点击下拉列表,可以看到里面有Off,Errors only和Full debug三个调试级别可供选择,每个调试选项里面都是这三个级别。
Off:表示关闭此选项的调试功能。
Errors only:表示仅在此选项出错时,将其错误打印出来。
Full debug:表示此选项的全功能调试。
关于调试功能的使用会在第11章详细为大家讲解,移植阶段将其全部关闭即可。
10.3.4 RL-TCPnet的多任务驱动接口函数
要让RL-TCPnet支持多任务,就需要修改Net_lib.c文件。默认情况下,Net_lib.c文件是支持RTX操作系统的,现在要将其修改为支持FreeRTOS,需要修改的几个地方如下:
添加FreeRTOS的头文件。
#if (__RTX) #include "FreeRTOS.h" #include "task.h" #include "queue.h" #include "semphr.h" #endif
定义信号量和互斥信号量。
#if (BSD_ENABLE) static BSD_INFO bsd_scb[BSD_NUMSOCKS + BSD_SRVSOCKS]; #ifdef __RTX SemaphoreHandle_t bsd_mutex = NULL; SemaphoreHandle_t bsd_sem = NULL; #define BSD_INRTX __TRUE #else #define BSD_INRTX __FALSE #endif BSD_CFG bsd_config = { bsd_scb, BSD_NUMSOCKS + BSD_SRVSOCKS, BSD_INRTX, BSD_RCVTOUT * TICK_RATE }; #endif
创建互斥信号量和信号量。
/*--------------------------- init_system -----------------------------------*/ void init_system (void) { /* Initialize configured interfaces and applications. */ #if (ETH_ENABLE) eth_init_link (); #endif #if (PPP_ENABLE) ppp_init_link (); #endif #if (SLIP_ENABLE) slip_init_link (); #endif ip_init (); icmp_init (); #if (ETH_ENABLE && IGMP_ENABLE) igmp_init (); #endif #if (UDP_ENABLE) udp_init (); #endif #if (TCP_ENABLE) tcp_init (); #endif #if (BSD_ENABLE) bsd_init (); #if (BSD_GETHOSTEN) bsd_init_host (); #endif #endif #if (HTTP_ENABLE) http_init (); #endif #if (TNET_ENABLE) tnet_init (); #endif #if (TFTP_ENABLE) tftp_init (); #endif #if (TFTPC_ENABLE) tftpc_init (); #endif #if (FTP_ENABLE) ftp_init (); #endif #if (FTPC_ENABLE) ftpc_init (); #endif #if (ETH_ENABLE && NBNS_ENABLE) nbns_init (); #endif #if (ETH_ENABLE && DHCP_ENABLE) dhcp_init (); #elif (ETH_ENABLE) arp_notify (); #endif #if (DNS_ENABLE) dns_init (); #endif #if (SMTP_ENABLE) smtp_init (); #endif #if (SNMP_ENABLE) snmp_init (); #endif #if (SNTP_ENABLE) sntp_init (); #endif #if (BSD_ENABLE && __RTX) { bsd_mutex = xSemaphoreCreateMutex(); if(bsd_mutex == NULL) { /* ERR */ } bsd_sem = xSemaphoreCreateBinary(); if(bsd_sem == NULL) { /* ERR */ } } #endif }
系统运行时,使用互斥信号量。
/*--------------------------- run_system ------------------------------------*/ void run_system (void) { /* Run configured interfaces and applications. */ #if (BSD_ENABLE && __RTX) xSemaphoreTake(bsd_mutex, portMAX_DELAY); #endif #if (ETH_ENABLE) eth_run_link (); #endif #if (PPP_ENABLE) ppp_run_link (); #endif #if (SLIP_ENABLE) slip_run_link (); #endif ip_run_local (); icmp_run_engine (); #if (ETH_ENABLE && IGMP_ENABLE) igmp_run_host (); #endif #if (TCP_ENABLE) tcp_poll_sockets (); #endif #if (BSD_ENABLE) bsd_poll_sockets (); #endif #if (HTTP_ENABLE) http_run_server (); #endif #if (TNET_ENABLE) tnet_run_server (); #endif #if (TFTP_ENABLE) tftp_run_server (); #endif #if (TFTPC_ENABLE) tftpc_run_client (); #endif #if (FTP_ENABLE) ftp_run_server (); #endif #if (FTPC_ENABLE) ftpc_run_client (); #endif #if (ETH_ENABLE && DHCP_ENABLE) dhcp_run_client (); #endif #if (DNS_ENABLE) dns_run_client (); #endif #if (SMTP_ENABLE) smtp_run_client (); #endif #if (SNMP_ENABLE) snmp_run_agent (); #endif #if (SNTP_ENABLE) sntp_run_client (); #endif #if (BSD_ENABLE && __RTX) xSemaphoreGive(bsd_mutex); #endif }
使能BSD Socket的话,Socket挂起和恢复的实现。
/*--------------------------- bsd_suspend/resume ----------------------------*/ #if (BSD_ENABLE && __RTX) __used void bsd_suspend (U8 *tsk_id) { /* Suspend a socket owner task. */ xSemaphoreGive(bsd_mutex); xSemaphoreTake(bsd_sem, portMAX_DELAY); xSemaphoreTake(bsd_mutex, portMAX_DELAY); } __used void bsd_resume (U8 tsk_id) { /* Resume a task waiting for a socket event. */ xSemaphoreGive(bsd_sem); } #endif
使能BSD Socket的话,锁机制的实现。
/*--------------------------- bsd_lock/unlock -------------------------------*/ #if (BSD_ENABLE && __RTX) __used void bsd_lock (void) { /* Acquire mutex - Lock TCPnet functions. */ xSemaphoreTake(bsd_mutex, portMAX_DELAY); } __used void bsd_unlock (void) { /* Release mutex - Unlock TCPnet functions. */ xSemaphoreGive(bsd_mutex); } #endif
10.3.5 RL-TCPnet应用实例
为了验证移植的RL-TCPnet是否可以使用,需要添加测试代码。下面是编写的测试代码,配套的测试例子完整版是:V6-1003_RL-TCPnet实验_工程移植模板(FreeRTOS)。
FreeRTOS操作系统创建的任务
经过上面的移植和配置之后,在 main.c 文件中添加如下代码,代码中创建了5个用户任务:
vTaskTaskUserIF 任务: 按键消息处理。
vTaskLED 任务: LED闪烁。
vTaskMsgPro 任务: 消息处理,这里用作按键检测。
vTaskTCPnet 任务: RL-TCPnet测试任务。
vTaskStart 任务: 启动任务,实现RL-TCPnet的时间基准更新。
具体代码如下:
#include "includes.h" /* ********************************************************************************************************** 函数声明 ********************************************************************************************************** */ static void vTaskTaskUserIF(void *pvParameters); static void vTaskLED(void *pvParameters); static void vTaskMsgPro(void *pvParameters); static void vTaskTCPnet(void *pvParameters); static void vTaskStart(void *pvParameters); static void AppTaskCreate (void); static void AppObjCreate (void); static void App_Printf(char *format, ...); /* ********************************************************************************************************** 变量声明 ********************************************************************************************************** */ static TaskHandle_t xHandleTaskUserIF = NULL; static TaskHandle_t xHandleTaskLED = NULL; static TaskHandle_t xHandleTaskMsgPro = NULL; static TaskHandle_t xHandleTaskTCPnet = NULL; static TaskHandle_t xHandleTaskStart = NULL; static SemaphoreHandle_t xMutex = NULL; EventGroupHandle_t xCreatedEventGroup = NULL; /* ********************************************************************************************************* * 函 数 名: main * 功能说明: 标准c程序入口。 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ int main(void) { /* 在启动调度前,为了防止初始化STM32外设时有中断服务程序执行,这里禁止全局中断(除了NMI和HardFault)。 这样做的好处是: 1. 防止执行的中断服务程序中有FreeRTOS的API函数。 2. 保证系统正常启动,不受别的中断影响。 3. 关于是否关闭全局中断,大家根据自己的实际情况设置即可。 在移植文件port.c中的函数prvStartFirstTask中会重新开启全局中断。通过指令cpsie i开启,__set_PRIMASK(1) 和cpsie i是等效的。 */ __set_PRIMASK(); /* 硬件初始化 */ bsp_Init(); /* 1. 初始化一个定时器中断,精度高于滴答定时器中断,这样才可以获得准确的系统信息 仅供调试目的,实际项 目中不要使用,因为这个功能比较影响系统实时性。 2. 为了正确获取FreeRTOS的调试信息,可以考虑将上面的关闭中断指令__set_PRIMASK(1); 注释掉。 */ vSetupSysInfoTest(); /* 创建任务 */ AppTaskCreate(); /* 创建任务通信机制 */ AppObjCreate(); /* 启动调度,开始执行任务 */ vTaskStartScheduler(); /* 如果系统正常启动是不会运行到这里的,运行到这里极有可能是用于定时器任务或者空闲任务的 heap空间不足造成创建失败,此要加大FreeRTOSConfig.h文件中定义的heap大小: #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) */ while(); } /* ********************************************************************************************************* * 函 数 名: vTaskTaskUserIF * 功能说明: 接口消息处理。 * 形 参: pvParameters 是在创建该任务时传递的形参 * 返 回 值: 无 * 优 先 级: 1 (数值越小优先级越低,这个跟uCOS相反) ********************************************************************************************************* */ static void vTaskTaskUserIF(void *pvParameters) { uint8_t ucKeyCode; uint8_t pcWriteBuffer[]; while() { ucKeyCode = bsp_GetKey(); if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { /* K1键按下,直接发送事件标志给任务vTaskTCPnet,设置bit0 */ case KEY_DOWN_K1: App_Printf("K1键按下,直接发送事件标志给任务AppTaskTCPMain,bit0被设置\r\n"); xEventGroupSetBits(xCreatedEventGroup, KEY1_BIT0); break; /* K2键按下,直接发送事件标志给任务vTaskTCPnet,设置bit1 */ case KEY_DOWN_K2: App_Printf("K2键按下,直接发送事件标志给任务AppTaskTCPMain,bit1被设置\r\n"); xEventGroupSetBits(xCreatedEventGroup, KEY2_BIT1); break; /* K3键按下,直接发送事件标志给任务vTaskTCPnet,设置bit2 */ case KEY_DOWN_K3: App_Printf("K3键按下,直接发送事件标志给任务AppTaskTCPMain,bit2被设置\r\n"); xEventGroupSetBits(xCreatedEventGroup, KEY3_BIT2); break; /* 摇杆的OK键按下,打印任务执行情况 */ case JOY_DOWN_OK: App_Printf("=================================================\r\n"); App_Printf("任务名 任务状态 优先级 剩余栈 任务序号\r\n"); vTaskList((char *)&pcWriteBuffer); App_Printf("%s\r\n", pcWriteBuffer); App_Printf("\r\n任务名 运行计数 使用率\r\n"); vTaskGetRunTimeStats((char *)&pcWriteBuffer); App_Printf("%s\r\n", pcWriteBuffer); App_Printf("当前动态内存剩余大小 = %d字节\r\n", xPortGetFreeHeapSize()); break; /* 其他的键值不处理 */ default: break; } } vTaskDelay(); } } /* ********************************************************************************************************* * 函 数 名: vTaskLED * 功能说明: LED闪烁 * 形 参: pvParameters 是在创建该任务时传递的形参 * 返 回 值: 无 * 优 先 级: 2 ********************************************************************************************************* */ static void vTaskLED(void *pvParameters) { TickType_t xLastWakeTime; const TickType_t xFrequency = ; /* 获取当前的系统时间 */ xLastWakeTime = xTaskGetTickCount(); while() { bsp_LedToggle(); /* vTaskDelayUntil是绝对延迟,vTaskDelay是相对延迟。*/ vTaskDelayUntil(&xLastWakeTime, xFrequency); } } /* ********************************************************************************************************* * 函 数 名: vTaskMsgPro * 功能说明: 消息处理,这里用作按键检测 * 形 参: pvParameters 是在创建该任务时传递的形参 * 返 回 值: 无 * 优 先 级: 3 ********************************************************************************************************* */ static void vTaskMsgPro(void *pvParameters) { while() { /* 按键扫描 */ bsp_KeyScan(); vTaskDelay(); } } /* ********************************************************************************************************* * 函 数 名: vTaskTCPnet * 功能说明: RL-TCPnet测试任务 * 形 参: pvParameters 是在创建该任务时传递的形参 * 返 回 值: 无 * 优 先 级: 4 ********************************************************************************************************* */ static void vTaskTCPnet(void *pvParameters) { while() { TCPnetTest(); } } /* ********************************************************************************************************* * 函 数 名: vTaskStart * 功能说明: 启动任务,也是最高优先级任务,这里实现RL-TCPnet的时间基准更新 * 形 参: pvParameters 是在创建该任务时传递的形参 * 返 回 值: 无 * 优 先 级: 5 ********************************************************************************************************* */ static void vTaskStart(void *pvParameters) { TickType_t xLastWakeTime; const TickType_t xFrequency = ; /* 初始化RL-TCPnet */ init_TcpNet (); /* 获取当前的系统时间 */ xLastWakeTime = xTaskGetTickCount(); while() { /* RL-TCPnet时间基准更新函数 */ timer_tick (); /* vTaskDelayUntil是绝对延迟,vTaskDelay是相对延迟。*/ vTaskDelayUntil(&xLastWakeTime, xFrequency); } } /* ********************************************************************************************************* * 函 数 名: AppTaskCreate * 功能说明: 创建应用任务 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ static void AppTaskCreate (void) { xTaskCreate( vTaskTaskUserIF, /* 任务函数 */ "vTaskUserIF", /* 任务名 */ , /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ , /* 任务优先级*/ &xHandleTaskUserIF ); /* 任务句柄 */ xTaskCreate( vTaskLED, /* 任务函数 */ "vTaskLED", /* 任务名 */ , /* stack大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ , /* 任务优先级*/ &xHandleTaskLED ); /* 任务句柄 */ xTaskCreate( vTaskMsgPro, /* 任务函数 */ "vTaskMsgPro", /* 任务名 */ , /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ , /* 任务优先级*/ &xHandleTaskMsgPro ); /* 任务句柄 */ xTaskCreate( vTaskTCPnet, /* 任务函数 */ "vTaskTCPnet", /* 任务名 */ , /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ , /* 任务优先级*/ &xHandleTaskTCPnet ); /* 任务句柄 */ xTaskCreate( vTaskStart, /* 任务函数 */ "vTaskStart", /* 任务名 */ , /* 任务栈大小,单位word,也就是4字节 */ NULL, /* 任务参数 */ , /* 任务优先级*/ &xHandleTaskStart ); /* 任务句柄 */ } /* ********************************************************************************************************* * 函 数 名: AppObjCreate * 功能说明: 创建任务通信机制 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ static void AppObjCreate (void) { /* 创建互斥信号量 */ xMutex = xSemaphoreCreateMutex(); if(xMutex == NULL) { /* 没有创建成功,用户可以在这里加入创建失败的处理机制 */ } /* 创建事件标志组 */ xCreatedEventGroup = xEventGroupCreate(); if(xCreatedEventGroup == NULL) { /* 没有创建成功,用户可以在这里加入创建失败的处理机制 */ } } /* ********************************************************************************************************* * 函 数 名: App_Printf * 功能说明: 线程安全的printf方式 * 形 参: 同printf的参数。 * 在C中,当无法列出传递函数的所有实参的类型和数目时,可以用省略号指定参数表 * 返 回 值: 无 ********************************************************************************************************* */ static void App_Printf(char *format, ...) { char buf_str[ + ]; va_list v_args; va_start(v_args, format); (void)vsnprintf((char *)&buf_str[], (size_t ) sizeof(buf_str), (char const *) format, v_args); va_end(v_args); /* 互斥信号量 */ xSemaphoreTake(xMutex, portMAX_DELAY); printf("%s", buf_str); xSemaphoreGive(xMutex); }
硬件外设初始化
硬件外设的初始化是在 bsp.c 文件实现:
/* ********************************************************************************************************* * 函 数 名: bsp_Init * 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_Init(void) { /* 由于ST固件库的启动文件已经执行了CPU系统时钟的初始化,所以不必再次重复配置系统时钟。 启动文件配置了CPU主时钟频率、内部Flash访问速度和可选的外部SRAM FSMC初始化。 系统时钟缺省配置为168MHz,如果需要更改,可以修改 system_stm32f4xx.c 文件 */ /* 优先级分组设置为4,可配置0-15级抢占式优先级,0级子优先级,即不存在子优先级。*/ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); bsp_InitUart(); /* 初始化串口 */ bsp_InitKey(); /* 初始化按键变量 */ bsp_InitLed(); /* 初始LED指示灯端口 */ }
RL-TCPnet功能测试
这里专门创建了一个app_tcpnet_lib.c文件用于RL-TCPnet功能的测试,主要功能是创建了一个TCP Server。
#include "includes.h" /* ********************************************************************************************************* * 用于本文件的调试 ********************************************************************************************************* */ #if 1 #define printf_debug printf #else #define printf_debug(...) #endif /* ********************************************************************************************************* * 用于本文件的调试 ********************************************************************************************************* */ #define PORT_NUM 1001 /* TCP服务器监听端口号 */ /* ********************************************************************************************************* * 变量 ********************************************************************************************************* */ uint8_t socket_tcp; /* ********************************************************************************************************* * 函 数 名: tcp_callback * 功能说明: TCP Socket的回调函数 * 形 参: soc TCP Socket类型 * evt 事件类型 * ptr 事件类型是TCP_EVT_DATA,ptr指向的缓冲区记录着接收到的TCP数据,其余事件记录IP地址 * par 事件类型是TCP_EVT_DATA,记录接收到的数据个数,其余事件记录端口号 * 返 回 值: ********************************************************************************************************* */ U16 tcp_callback (U8 soc, U8 evt, U8 *ptr, U16 par) { char buf[]; uint16_t i; /* 确保是socket_tcp的回调 */ if (soc != socket_tcp) { return (); } switch (evt) { /* 远程客户端连接消息 1、数组ptr存储远程设备的IP地址,par中存储端口号。 2、返回数值1允许连接,返回数值0禁止连接。 */ case TCP_EVT_CONREQ: sprintf(buf, "远程客户端请求连接IP: %d.%d.%d.%d", ptr[], ptr[], ptr[], ptr[]); printf_debug("IP:%s port:%d\r\n", buf, par); return (); /* 连接终止 */ case TCP_EVT_ABORT: break; /* Socket远程连接已经建立 */ case TCP_EVT_CONNECT: printf_debug("Socket is connected to remote peer\r\n"); break; /* 连接断开 */ case TCP_EVT_CLOSE: printf_debug("Connection has been closed\r\n"); break; /* 发送的数据收到远程设备应答 */ case TCP_EVT_ACK: break; /* 接收到TCP数据帧,ptr指向数据地址,par记录数据长度,单位字节 */ case TCP_EVT_DATA: printf_debug("Data length = %d\r\n", par); for(i = ; i < par; i++) { printf_debug("ptr[%d] = %d\r\n", i, ptr[i]); } break; } return (); } /* ********************************************************************************************************* * 函 数 名: TCP_StatusCheck * 功能说明: 检测TCP的连接状态,主要用于网线插拔的判断 * 形 参: 无 * 返 回 值: __TRUE 连接 * __FALSE 断开 ********************************************************************************************************* */ uint8_t TCP_StatusCheck(void) { uint8_t res; switch (tcp_get_state(socket_tcp)) { case TCP_STATE_FREE: case TCP_STATE_CLOSED: res = tcp_listen (socket_tcp, PORT_NUM); printf_debug("tcp listen res = %d\r\n", res); break; case TCP_STATE_LISTEN: break; case TCP_STATE_CONNECT: return (__TRUE); default: break; } return (__FALSE); } /* ********************************************************************************************************* * 函 数 名: TCPnetTest * 功能说明: TCPnet应用 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void TCPnetTest(void) { int32_t iCount; uint8_t *sendbuf; uint8_t tcp_status; uint16_t maxlen; uint8_t res; const TickType_t xTicksToWait = ; /* 延迟2ms */ EventBits_t uxBits; /* 创建TCP Socket并创建监听,客户端连接服务器后,10秒内无数据通信将断开连接。 但是由于这里使能了TCP_TYPE_KEEP_ALIVE,会一直保持连接,不受10秒的时间限制。 */ socket_tcp = tcp_get_socket (TCP_TYPE_SERVER|TCP_TYPE_KEEP_ALIVE, , , tcp_callback); if(socket_tcp != ) { res = tcp_listen (socket_tcp, PORT_NUM); printf_debug("tcp listen res = %d\r\n", res); } while () { /* RL-TCPnet处理函数 */ main_TcpNet(); /* 用于网线插拔的处理 */ tcp_status = TCP_StatusCheck(); /* 等待所有任务发来事件标志 */ uxBits = xEventGroupWaitBits(xCreatedEventGroup, /* 事件标志组句柄 */ 0xFFFF, /* 等待0xFFFF某一位被设置 */ pdTRUE,/* 退出前0xFFFF位被清除,这里是任意0xFFFF位被设置就“退出”*/ pdFALSE, /* 设置为pdTRUE表示等待0xFFFF任意位被设置*/ xTicksToWait);/* 等待延迟时间 */ if((uxBits != )&&(tcp_status == __TRUE)) { switch (uxBits) { /* 接收到K1键按下,给远程TCP客户端发送8字节数据 */ case KEY1_BIT0: printf_debug("tcp_get_state(socket_tcp) = %d\r\n", tcp_get_state(socket_tcp)); iCount = ; do { main_TcpNet(); if (tcp_check_send (socket_tcp) == __TRUE) { maxlen = tcp_max_dsize (socket_tcp); iCount -= maxlen; if(iCount < ) { /* 这么计算没问题的 */ maxlen = iCount + maxlen; } sendbuf = tcp_get_buf(maxlen); sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; sendbuf[] = ''; /* 测试发现只能使用获取的内存 */ tcp_send (socket_tcp, sendbuf, maxlen); } }while(iCount > ); break; /* 接收到K2键按下,给远程TCP客户端发送1024字节的数据 */ case KEY2_BIT1: printf_debug("tcp_get_state(socket_tcp) = %d\r\n", tcp_get_state(socket_tcp)); iCount = ; do { main_TcpNet(); if (tcp_check_send (socket_tcp) == __TRUE) { maxlen = tcp_max_dsize (socket_tcp); iCount -= maxlen; if(iCount < ) { /* 这么计算没问题的 */ maxlen = iCount + maxlen; } /* 这里仅初始化了每次所发送数据包的前8个字节 */ sendbuf = tcp_get_buf(maxlen); sendbuf[] = 'a'; sendbuf[] = 'b'; sendbuf[] = 'c'; sendbuf[] = 'd'; sendbuf[] = 'e'; sendbuf[] = 'f'; sendbuf[] = 'g'; sendbuf[] = 'h'; /* 测试发现只能使用获取的内存 */ tcp_send (socket_tcp, sendbuf, maxlen); } }while(iCount > ); break; /* 接收到K3键按下,给远程TCP客户端发送5MB数据 */ case KEY3_BIT2: printf_debug("tcp_get_state(socket_tcp) = %d\r\n", tcp_get_state(socket_tcp)); iCount = **; do { main_TcpNet(); if (tcp_check_send (socket_tcp) == __TRUE) { maxlen = tcp_max_dsize (socket_tcp); iCount -= maxlen; if(iCount < ) { /* 这么计算没问题的 */ maxlen = iCount + maxlen; } /* 这里仅初始化了每次所发送数据包的前8个字节 */ sendbuf = tcp_get_buf(maxlen); sendbuf[] = 'a'; sendbuf[] = 'b'; sendbuf[] = 'c'; sendbuf[] = 'd'; sendbuf[] = 'e'; sendbuf[] = 'f'; sendbuf[] = 'g'; sendbuf[] = 'h'; /* 测试发现只能使用获取的内存 */ tcp_send (socket_tcp, sendbuf, maxlen); } }while(iCount > ); break; /* 其他的键值不处理 */ default: break; } } } }
至此,FreeRTOS方式移植的RL-TCPnet就可以运行了。
10.3.6 RL-TCPnet实验测试和实验现象
测试前,先将开发板上面的DM9161/9162网口通过网线接到路由器或者交换机上面。
RJ45网络变压器插座上绿灯和黄灯现象
各种网卡、交换机等网络设备都不一样,一般来讲:绿灯分为亮或不亮(代表网络速度),黄灯分为闪烁或不闪烁(代表是否有数据收发)。
绿灯:长亮代表100M; 不亮代表10M。
黄灯:长亮代表无数据收发; 闪烁代表有数据收发。
也有些千兆网卡的灯以颜色区分,不亮代表10M / 绿色代表100M / 黄色代表1000M。现在10M的网络基本看不到了,如果一个灯长亮,基本可以说明100M网络或更高,而另一个灯时而闪烁,那代表有数据收发,具体要看你的网络设备了。甚至有些低等网卡如TP-LINK,只有一个灯,亮代表连通,闪烁代表数据收发。
对于STM32F429开发板上面的RJ45网络变压器插座上面的灯而言,绿灯代表数据收发,长亮的话表示无数据收发,闪烁代表有数据收发。黄灯代表网络速度,长亮代表100M,不亮代表10M。
底层驱动执行情况
为了验证RL-TCPnet底层驱动接口函数是否有问题,专门在ETH_STM32F4xx.c文件中配置了串口调试打印函数:
/* ********************************************************************************************************* * 用于本文件的调试 ********************************************************************************************************* */ #if 1 #define printf_eth printf #else #define printf_eth(...) #endif
如果底层驱动正常执行了,打印的效果如下:
ping是否正确
ping命令的主要作用是通过发送数据包并接收应答信息来检测两台设备之间的网络是否连通。ping命令成功说明当前主机与目的主机之间存在连通的路径。如果不成功,需要查看网线是否连通、网卡设置是否正确、IP地址是否可用等。测试方法如下:
(1)WIN+R组合键打开“运行”窗口,输入cmd。
(2)弹出的命令窗口中,输入ping armfly,因为在前面的配置中我们使能了NetBIOS局域网域名,并将名字设置为armfly,而且使能了DHCP,通过ping命令还可以获得板子自动获取的IP地址。
(3)输入ping armfly后,回车。
收发相当,没有数据丢失,说明ping命令也是成功的。
电脑端创建一个TCP Client与板子上面的TCP Server通信
具体测试方法,查看第13章的13.6小节即可,因为配套例子实现的功能是一样的。
10.4 总结
本章节为大家讲解了RL-TCPnet网络协议栈的FreeRTOS操作系统移植方法,移植相对比较简单。另一个重要内容是Net_Config.c配置向导文件的说明,这个比较重要,初学者要好好熟悉下。
【安富莱】【RL-TCPnet网络教程】第10章 RL-TCPnet网络协议栈移植(FreeRTOS)的更多相关文章
- 【安富莱STM32H7教程】第1章 初学STM32H7的准备工作
完整教程下载地址:http://forum.armfly.com/forum.php?mod=viewthread&tid=86980 第1章 初学STM32H7的准备工作 俗话说万事开头 ...
- 【安富莱】RTX嵌入式操作系统教程发布,支持F103,F407和F429,含81个配套例程(2017-10-17)
前言说明:1. 首先感谢大家对我们安富莱电子一年来的支持,2016年我们会再接再厉推出更好的教程. 2. 估计也有网友会问RTX的优势在那里,针对这个问题,教程中第一章分为6条专门回答了这个问题,有兴 ...
- 【安富莱】【RL-TCPnet网络教程】第8章 RL-TCPnet网络协议栈移植(RTX)
第8章 RL-TCPnet网络协议栈移植(RTX) 本章教程为大家讲解RL-TCPnet网络协议栈的RTX操作系统移植方式,学习了第6章讲解的底层驱动接口函数之后,移植就比较容易了,主要 ...
- 【安富莱】【RL-TCPnet网络教程】第7章 RL-TCPnet网络协议栈移植(裸机)
第7章 RL-TCPnet网络协议栈移植(裸机) 本章教程为大家讲解RL-TCPnet网络协议栈的裸机移植方式,学习了上个章节讲解的底层驱动接口函数之后,移植就比较容易了,主要是添加库文 ...
- 【安富莱】【RL-TCPnet网络教程】第11章 RL-TCPnet调试方法
第11章 RL-TCPnet调试方法 本章节为大家讲解RL-TCPnet的调试方法,RL-TCPnet的调试功能其实就是通过串口打印实时监控运行状态.而且RL-TCPnet的调试设置比较简单 ...
- 【安富莱专题教程第3期】开发板搭建Web服务器,利用花生壳让电脑和手机可以外网远程监控
说明:1. 开发板Web服务器的设计可以看我们之前发布的史诗级网络教程:链接.2. 需要复杂些的Web设计模板,可以使用我们V6开发板发布的综合Demo:链接.3. 教程中使用的是花生壳免费版, ...
- 【安富莱TCPnet网络教程】HTTP通信实例
第41章 HTTP超文本传输协议基础知识 本章节为大家讲解HTTP(HyperText Transfer Protocol,超文本传输协议),从本章节开始,正式进入嵌入式Web的设计和学习. ...
- 【安富莱专题教程第7期】终极调试组件Event Recorder,各种Link通吃,支持时间和功耗测量,printf打印,RTX5及中间件调试
说明:1.继前面的专题教程推出SEGGER的RTT,JScope,Micrium的uC/Probe之后,再出一期终极调试方案Event Recoder,之所以叫终极解决方案,是因为所有Link通吃. ...
- 【安富莱二代示波器教程】第19章 附件E---参考资料
第19章 附件E---参考资料 DSP教程 http://forum.armfly.com/forum.php?mod=viewthread&tid=3886 . FreeRTOS教 ...
随机推荐
- Java后端学习,推荐书籍和学习路线
最近在学习Java和全栈开发,推荐一些有用的书籍 书架主要针对Java后端和全栈开发用的 书籍介绍 <Spring Boot 2.0企业级应用开发实战> 本书深入浅岀地讲解了 Spring ...
- unity iOS本地代码总结(一)
1. 项目能直接运行了,但是代码的实际数据流动任然会有问题. 2. unity的代码能这么简单的被调用简直是奇迹一样,不需要大的改动就能够使用. 3. 目前需要注意的问题就是,unity的内容还太少, ...
- Scyther-Semantics and verification of Security Protocol
1 .本书前一节主要是介作者自己的生平经历(读完感觉作者是个神童),目标明确作者13岁代码已经写的很溜了.自己也开了网络公司,但是后面又专注于自己的计算机基础理论,修了哲学的博士学位(不得不说很多专业 ...
- Shell执行*.sql
> mysql -uroot -p123456 > use db_test > source /root/temp.sql
- (转载)配置 Linux 操作系统的 JDK
系统Ubuntu,下载jdk-9.0.1 1,切换到root ,创建文件夹 xxxx@ubuntu:~$ sudo su root@ubuntu:~# mkdir /usr/java 2,找到下载 ...
- MySQL 远程连接问题
使用Workbench 无法远程连接Mysql服务器提示如下错误: 查找原因: 显示只能localhost 访问. 解决方法:修改授权远程访问 create user 'root'@'%' ident ...
- Python3学习笔记十五
---恢复内容开始--- 1. jquery的属性操作 $().attr(属性名) 取值 $().attr(属性名,属性值) 赋值 <!DOCTYPE html> &l ...
- Linux安装Tomcat-Nginx-FastDFS-Redis-Solr-集群——【第五集之补充-使用桥接模式实现虚拟机作为服务器,让同网段的其他主机远程连接】
参考:https://blog.csdn.net/qicheng777/article/details/73438045 https://www.cnblogs.com/hld123/p/650550 ...
- java PDF分页打印
将获取的pdf文件按页拆分:参考https://q.cnblogs.com/q/99944/ pdf文件有多页,第一页需设置横向打印,其他页设置为纵向打印. PDDocument document = ...
- Unreal 4 error 记录
1.打包出来的exe,黑屏 这种最大的可能是在“地图&模式中”将 Default Maps设置为自己的map,注意这里分为Editor Startup Map和Game Default Map ...