本次项目开发平台为正点原子的STM32F1战舰版
(本次博客也是对上一博客的具体说明)
参考文献
1.正点原子-FREERTOS的开发教程
2.正点原子-基于ucos系统的LWIP协议移植开发教程
3.STM32嵌入式开发教程指南—[李老师]
前言
FREERTOS是由safeRTOS衍生的一套操作系统,由Richard Barry于2002年开发完成的,具有源代码公开、可移植、易裁剪且功能全面的特点,能移植到很多内核中,它要求的配置低,但运行效率高。
LWIP是一款由瑞士计算机科学家的Adam等设计研发的轻量级TCP/IP协议栈,具备TCP/IP的主要功能,主要优点是内存使用率低、代码空间小,适用于资源紧张的嵌入式系统中使用。
本课题主要是在FREERTOS系统的上移植LWIP协议,并对其进行分析,最终测试PC端能否与开发板进行通信。
源码链接
https://github.com/zht1217/FREERTOS-and-LWIP
FREERTOS系统介绍
FREERTOS作为一个轻量级嵌入式操作系统,提供了一个高层次的可信任代码。源代码以C开发,系统实现的任务数量没有限制,FREERTOS内核支持优先级调度算法,每个任务可根据重要程度的不同赋予一定的优先级,CPU总是让处于就绪态的、优先级最高的任务先运行。FREERTOS内核同时支持轮换调度算法,系统允许不同的任务使用相同的优先级,在没有更高优先级任务就绪的情况下,同一优先级的任务共享CPU的使用时间。
此外,FREERTOS还具有强大的执行跟踪功能、堆栈溢出检测、互斥信号量、优先级继承权等特点,在嵌入式操作系统中是为数不多的同时具有实时性、开源性、可靠性、易用性、多平台支持等特点的嵌入式操作系统。
FREERTOS提供的功能包括:任务管理、时间管理、消息队列、内存管理、记录功能等,可基本满足较小系统的需要。
FREERTOS系统之API函数
本章节主要介绍FREERTOS中常用的几个API函数,读者不必深究函数的细节,只需要知道参数对任务行为的影响即可,即可满足平时的开发。
1.创建任务函数xTaskCreate()
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
FREERTOS采用xTaskCreate()来进行任务的创建。函数原型如上图所示,因为是永不退出的c函数,以死循环实现。参数pxTaskCode为指向任务的实现函数的指针,与函数名相同;pcName为任务名称,该参数对任务没影响;内核在创建任务时为其分配唯一的堆栈空间,usStackDepth指示内核为其分配空间容量,需要注意的是,该参数单位为字(4字节),例如,此处为10,那么实际分配的堆栈空间为40字节;pvParameters为传递任务函数的参数;uxPriority为任务执行的优先级,取值范围为0-configMAX_PRIORITIES-1);pxCreateTask为该任务的句柄,其他的API可以通过该句柄对该任务进行引用,例如改变任务优先级或删除任务。
2.删除任务函数xTaskDelete()
void vTaskDelete( TaskHandle_t xTaskToDelete )
函数原型如上图,被删除的任务不再存在,也就是说不再进入运行态。任务被删除后就不能再使用此任务句柄;此外,只有内核为任务分配的内存空间才会在任务被删除后自动回收。任务自己占用的内存或资源需要由应用程序自己显示的释放。
3.创建二值信号量函数xSemaphoreCreateBinary()
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
函数原型如上图,新版本采用上述函数来创建,此函数默认创建的二值信号量为空,可以看到,此函数也是一个宏。
4.获取信号量函数xSemaphoreTake()
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ), NULL, ( xBlockTime ), pdFALSE )
函数原型如上图,可以看到此函数也是一个宏,xSemaphore为要获取的信号量句柄,xBlockTime为阻塞时间。
5.释放信号量函数xSemaphoreGive()、xSemaphoreGiveISR()
#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
此函数用于释放二值信号量、计数、互斥信号量等,xSemaphore为要释放的句柄。
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )
此函数为上面函数的特殊形式,只用于中断例程中,xSemaphore为要释放的信号量;对于信号量来说,可能有不只一个任务处于阻塞状态,调用此函数会让信号量有效,所以会让其中一个等待任务切换切出阻塞态。如果调用此函数使一个任务解除阻塞态,并且此任务优先级高于当前任务(被中断的任务),那么pxHigherPriorityTaskWoken会设为pdTURE,如果已经设为pdTURE,则中断退出前应当进行一次上下文切换,这样才能保证中断直接返回就绪态任务中优先级最高的任务。
6.创建互斥信号量函数xSemaphoreCreateMutex()
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
函数原型如上图,互斥量是一种特殊的二值信号量,用于控制在两个或多个任务间访问共享资源。用于互斥的信号量用完之后必须归还,而二值信号量用于同步之后便丢弃。返回值为NULL,表示创建失败,原因是内存空间不足,返回非NULL创建成功。
FREERTOS系统移植
FREERTOS的实现主要由list.c、queue.c、croutine.c和task.c4个文件来完成。List.c是一个链表的实现,主要供给内核调度器使用;queue.c是一个队列的实现,支持中断环境和信号量控制;croutine.c和task.c是两种任务的组织实现。
因此,FREERTOS在STM32上的移植主要在三个文件实现,一个portmacro.h文件定义编译器相关的数据类型和中断处理的宏定义;一个port.c文件市场实现任务的堆栈初始化、系统心跳的管理和任务请求;一个portasm.s实现具体的任务切换。此移植具体教程不是本手册重点,具体移植过程请参考正点原子教程。
LWIP协议介绍
LWIP是瑞典计算机学院的Adam等开发的一个小型的TCP/IP协议栈。有无操作系统的支持都可以运行,LWIP协议在保证TCP协议主要功能的基础上减少对RAM的占用,它只需要十几KB的RAM和40K左右的ROM就可以运行,因此其适合在低端的嵌入式系统中使用。
LWIP在设计之初,设计者无法预测LWIP运行的环境是怎么样的,而且世界上操作系统那么多,根本没法统一,而如果 LWIP要运行在操作系统环境中,那么就必须产生依赖,即 LWIP需要依赖操作系统自身的通信机制,如信号量、互斥量、消息队列(邮箱)等,所以LWIP设计者在设计的时候就提供一套与操作系统相关的接口,由用户根据操作系统的不同进行移植,这样子就能降低耦合度,让 LWIP内核不受其运行的环境影响,因为往往用户并不能完全了解内核的运作,所以只需要用户在移植的时候对LWIP提供的接口根据不同操作系统进行完善即可。
LWIP协议提供了三种应用程序的API接口,即RAW接口,RAW API是LWIP的一大特色, 在没有操作系统支持的裸机环境中,只能使用这种 API 进行开发,同时这种 API 也可以用在操作系统环境中;NETCONN API 是基于操作系统的 IPC 机制(即信号量和邮箱机制)实现的,它的设计将LWIP内核代码和网络应用程序分离成了独立的线程;SOCKET API,即套接字,它对网络连接进行了高级的抽象,使得用户可以像操作文件一样操作网络连接,LWIP协议的SOCKET API是基于NETCONN API的。因此,本次开发中,会应用NETCONN API来进行协议的移植实现。
LWIP协议移植
本次LWIP协议移植分为两部分,即以太网接口ethernetif.c的移植和操作系统模拟层sys_arch.c的移植。
先看重头戏,sys_arch.c的移植,在LWIP协议源码\lwip-1.4.1\doc目录下的sys_arch.txt文档对相关的接口已经做出了说明解释。
1. SYS_ARCH.C文档讲解
现在,我们先看看具体的函数功能如何实现。具体函数功能如下:
- 创建新的消息邮箱
在sys_arch.txt中是这么说的,如上,我们需要为最大的元素size创建一个空邮箱,元素作为一个指针类型存储在邮箱中,在FREERTOS系统中没有邮箱这个概念,所以用消息队列替代,其实本质都一样,这里我们定义了邮箱大小为MAX_QUEUE_ENTRIES,如果邮箱被创建,那么会返回ERR_OK。直接看代码实现,比较容易理解。
err_t sys_mbox_new(sys_mbox_t *mbox,int size)
{
if(size>MAX_QUEUE_ENTRIES)size=MAX_QUEUE_ENTRIES; //消息队列最多容纳MAX_QUEUE_ENTRIES消息数目
mbox->xQueue = xQueueCreate(size, sizeof(void *)); //创建消息队列,该消息队列存放指针
LWIP_ASSERT("OSQCreate",mbox->xQueue!=NULL);
if(mbox->xQueue!=NULL)return ERR_OK; //返回ERR_OK,表示消息队列创建成功 ERR_OK=0
else
return ERR_MEM; //消息队列创建错误
}
2.等待邮箱中的消息,文档说明如下
从消息队列取出一条消息,该函数是一个阻塞函数。调用该函数的线程若未取到消息,则形参timeout所指顶的时间内,该线程被阻塞。当超时timeout所指定的时间后,该线程恢复至就绪态。若timeout为0,则调用该函数的线程一直被阻塞,直到收到消息。
(这里对FREERTOS中的几种任务状态简单的说明一下,首先运行态-----顾名思义,当前这个任务处于运行状态,单核情况下,当前是我在使用处理器,没你们的事。接着是就绪态-------表面意思,万事俱备只欠东风,就差cpu了,为啥没运行呢,前面有个比我NB的在运行呢(也就是优先级高或同一优先级),没轮到我。然后是阻塞态----就是这个任务正在等待某个事件,比如一个线程调用了阻塞式的I/O方法,调用了某个对象的wait()方法,或调用等,都会使线程进入阻塞状态,任务进入阻塞态以等待两种不同的事件即定时和同步。最后是挂起态-----显而易见,就是不做任何处理,除非其他任务或中断唤醒,当然,大部分应用程序不会用到挂起态。),具体的函数实现如下:
u32_t sys_arch_mbox_fetch (sys_mbox_t *mbox, void **msg, u32_t timeout)
{
void* dummyptr;
portTickType StartTime, EndTime, Elapsed;
StartTime = xTaskGetTickCount();
if (msg == NULL)
{
msg=&dummyptr;
}
if (timeout != 0)
{
if (pdTRUE == xQueueReceive(mbox->xQueue,&(*msg),timeout/portTICK_RATE_MS))
{
EndTime = xTaskGetTickCount();
Elapsed = (EndTime - StartTime) * portTICK_RATE_MS;
return Elapsed;
}
else //超时就退出
{
*msg = NULL;
return SYS_ARCH_TIMEOUT;
}
}
else
{
while (pdTRUE != xQueueReceive(mbox->xQueue, &(*msg),portMAX_DELAY))
{
}
EndTime = xTaskGetTickCount();
Elapsed = (EndTime - StartTime) * portTICK_RATE_MS;
return Elapsed;
}
}
3.尝试获取消息
从消息队列尝试取出一条消息,该函数是一个非阻塞函数,当取到消息返回成功,否则立即退出,返回队列为空。实现如下:
u32_t sys_arch_mbox_tryfetch(sys_mbox_t *mbox, void **msg)
{
void* dummyptr;
if (msg == NULL)
{
msg = &dummyptr;
}
if (pdTRUE == xQueueReceive(mbox->xQueue,&(*msg),0))
{
return ERR_OK;
}
else
{
return SYS_MBOX_EMPTY;
}
}
4.创建信号量
创建一个信号量,创建成功,返回ERR_OK,否则返回 ERR_MEM,函数实现如下:
err_t sys_sem_new(sys_sem_t* sem, u8_t count)
{
if (count <= 1)
{
*sem = xSemaphoreCreateBinary();
if (count == 1)
{
sys_sem_signal(sem);
}
}
else
{
*sem= xSemaphoreCreateCounting(count,count);
}
if (*sem == NULL)
{
return ERR_MEM;
}
else
{
return ERR_OK;
}
}
5.等待一个信息量,说明文档如下:
该函数是一个阻塞函数。调用该函数的线程在形参timeout指定的事件内阻塞。若timeout为0,则调用该函数的线程一直被阻塞,直到等待的信号量被释放。当该函数取得信号量时,它将返回取得信号量所用的时间。函数实现如下:
u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout)
{
portTickType StartTime, EndTime, Elapsed;
StartTime = xTaskGetTickCount();
if (timeout != 0)
{
if (xSemaphoreTake(*sem, timeout/portTICK_RATE_MS) == pdTRUE)
{
EndTime = xTaskGetTickCount();
Elapsed = (EndTime - StartTime) * portTICK_RATE_MS;
return Elapsed;
}
else
{
return SYS_ARCH_TIMEOUT;
}
}
else
{
while (xSemaphoreTake(*sem, portMAX_DELAY) != pdTRUE)
{
}
EndTime = xTaskGetTickCount();
Elapsed = (EndTime - StartTime) * portTICK_RATE_MS;
return Elapsed;
}
}
6.释放信号量,以及后面的删除等比较简单,故不在作说明。
void sys_sem_signal(sys_sem_t *sem)
{
xSemaphoreGive(*sem);
}
7.删除信号量,函数功能如下:
void sys_sem_free(sys_sem_t *sem)
{
vSemaphoreDelete(*sem);
*sem = NULL;
}
8.查询一个信号量的状态,无效或有效,函数功能如下:
int sys_sem_valid(sys_sem_t *sem)
{
if(*sem != NULL)
return 1;
else
return 0;
}
9.设置一个信号量无效,实现如下:
void sys_sem_set_invalid(sys_sem_t *sem)
{
*sem=NULL;
}
10.系统初始化函数:
void sys_init(void)
{
}
11.接下来是将LWIP单独作为一个线程,创建一个新线程:
其中形参name指定线程的名字,thread对应的该线程的函数,args为线程的形参,stacksize为该线程对应的堆栈的大小,prio对应的该线程的优先级。
TaskHandle_t LWIP_ThreadHandler;
sys_thread_t sys_thread_new(const char *name, void (* thread)(void *arg), void *arg, int stacksize, int prio)
{
taskENTER_CRITICAL(); //进入临界区
xTaskCreate((TaskFunction_t)thread,
name,
(uint16_t )stacksize,
(void* )NULL,
(UBaseType_t )prio,
(TaskHandle_t*)&LWIP_ThreadHandler);//创建TCP IP内核任务
taskEXIT_CRITICAL(); //退出临界区
return 0;
}
12.获取系统的时间:
u32_t sys_now(void)
{
u32_t lwip_time;
lwip_time=(xTaskGetTickCount()*1000/configTICK_RATE_HZ+1);//将节拍数转换为LWIP的时间MS
return lwip_time; //返回lwip_time;
}
13.保护临界区资源及访问临界区资源。
临界区资源就是指会被多个任务访问到的公共资源,各进程采取互斥的方式,实现共享的资源。,而临界区就是进程中访问临界资源的那段代码,也即是在 taskENTER_CRITICAL()和taskEXIT_CRITICAL()之间的代码,每次只允许一个进程进入临界区,无论硬件还是软件资源都必须互斥的进行访问。
函数实现如下:
sys_prot_t sys_arch_protect(void)
{
vPortEnterCritical();
return 1;
}
void sys_arch_unprotect(sys_prot_t pval)
{
(void) pval;
vPortExitCritical();
}
到这里,关于sys_arch.c文件内容全部完成。
2.dm9000.c文档讲解
这里不在赘述了,可参照正点原子教程在相应位置添加信号量,FREERTOS系统互斥信号量的添加在下方贴出。
Dm9000的中断处理函数也是参照正点原子在相应的位置修改即可,修改为的内容在下方已贴出。
3.ethernetif.c文档讲解
Ethnernetif.c是LWIP协议栈和STM32F103网络驱动程序之间的接口,它主要包含ethernetif_init、ethernetif_input、low_level_input、low_level_output等函数,接下来会逐渐介绍。
1.ethernetif_init函数
这个函数是LWIP底层网络接口的初始化函数,指定了网络接口netif对应的主机名及网卡描述,并指定了该网卡的MAC地址,同时,该函数还指定了netif的发送数据报文函数,并调用了网络底层驱动初始化函数low_level_init对网络底层进行初始化。源代码如下:
err_t ethernetif_init(struct netif *netif)
{
LWIP_ASSERT("netif!=NULL",(netif!=NULL));
#if LWIP_NETIF_HOSTNAME //LWIP_NETIF_HOSTNAME
netif->hostname="lwip"; //初始化名称
#endif
netif->name[0]=IFNAME0; //初始化变量netif的name字段
netif->name[1]=IFNAME1; //在文件外定义这里不用关心具体值
netif->output=etharp_output;//IP层发送数据包函数
netif->linkoutput=low_level_output;//ARP模块发送数据包函数
low_level_init(netif); //底层硬件初始化函数
return ERR_OK;
}
2.low_level_init函数
这个函数是网卡初始化函数,设定网卡的物理地址以及每帧最大传输字节数据等。源码如下:
static err_t low_level_init(struct netif *netif)
{
//INT8U err;
netif->hwaddr_len = ETHARP_HWADDR_LEN; //设置MAC地址长度,为6个字节
//初始化MAC地址,设置什么地址由用户自己设置,但是不能与网络中其他设备MAC地址重复
netif->hwaddr[0]=lwipdev.mac[0];
netif->hwaddr[1]=lwipdev.mac[1];
netif->hwaddr[2]=lwipdev.mac[2];
netif->hwaddr[3]=lwipdev.mac[3];
netif->hwaddr[4]=lwipdev.mac[4];
netif->hwaddr[5]=lwipdev.mac[5];
netif->mtu=1500; //最大允许传输单元,允许该网卡广播和ARP功能
netif->flags = NETIF_FLAG_BROADCAST|NETIF_FLAG_ETHARP|NETIF_FLAG_LINK_UP;
return ERR_OK;
}
3.ethernetif_input函数
因为使用了操作系统,我们在此处将接收数据函数独立成为一个网卡接收线程,这样子在收到数据时候采取处理数据,然后递交给内核线程。需要注意的是,网卡接收线程是需要通过信号量机制去接收数据的,一般来说我们都是使用中断的方式去获取网络数据包,当产生中断的时候,我们不会在中断中处理数据,一般在中断中打个标志位,告诉对应的线程去处理,也就是我们的网卡接收线程去处理数据,那么会通过信号量进行同步,当网卡收到数据就会产生中断然后释放信号量,然后线程从阻塞中恢复,从网卡中读取数据,向上递交。因此,该函数用于从底层物理网卡读取报文,并将报文上传LWIP协议栈函数ethernet_input进行处理,该函数会请求信号量dm900input,一旦请求到该信号量那么说明有数据收到,则会调用low_level_input()进行数据接收。
函数源码如下:
err_t ethernetif_input(struct netif *netif)
{
unsigned char _err;
err_t err;
struct pbuf *p;
while(1)
{
if(dm9000input!=NULL)
{
xSemaphoreTake(dm9000input,portMAX_DELAY);//死等dm9000input信号
}
else
{
vTaskDelay(100);
}
while(1)
{
p=low_level_input(netif); //调用low_level_input函数接收数据
if(p!=NULL)
{
err=netif->input(p, netif); //调用netif结构体中的input字段(一个函数)来处理数据包
if(err!=ERR_OK)
{
LWIP_DEBUGF(NETIF_DEBUG,("ethernetif_input: IP input error\n"));
pbuf_free(p);
p = NULL;
}
}else break;
}
}
}
4.low_level_input()函数
此函数主要是从DM9000中接受数据,通过DM9000_Receive_Packet()函数来实现。
static struct pbuf * low_level_input(struct netif *netif)
{
struct pbuf *p;
p=DM9000_Receive_Packet();
return p;
}
5.low_level_output()函数
用发送数据,通过DM9000发送数据。
static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
DM9000_SendPacket(p);//发送数据
return ERR_OK;
}
4.lwip_comm.c文档讲解
- 创建一个任务来接收数据如下:
2.接下来是lwip_comm_init()初始化函数:
参照正点原子教程在相应的位置添加如下:
创建DM9000任务:
最后创建lwip_comm_dhcp任务:
至此移植已经全部完成。
5.Main函数实现
最后,完成main函数的编写,主要完成亮灯任务和运行lwip获取地址任务。实现代码部分如下:
int main(void)
{
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置NVIC中断分组
uart_init(115200); //串口初始化为115200
LED_Init(); //LED端口初始化
LCD_Init(); //初始化LCD
KEY_Init(); //初始化按键
// usmart_dev.init(72); //初始化USMART
FSMC_SRAM_Init();//初始化外部SRAM
//DM9000_Init();
my_mem_init(SRAMIN); //初始化内部内存池
my_mem_init(SRAMEX); //初始化外部内存池
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
//开始任务任务函数
void start_task(void *pvParameters)
{
lwip_comm_init(); //lwip初始化
taskENTER_CRITICAL(); //进入临界区
#if LWIP_DHCP
lwip_comm_dhcp_creat(); //创建DHCP任务
#endif
//创建led任务
xTaskCreate((TaskFunction_t )led_task,
(const char* )"led_task",
(uint16_t )LED_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED_TASK_PRIO,
(TaskHandle_t* )&LedTask_Handler);
//创建display任务
xTaskCreate((TaskFunction_t )display_task,
(const char* )"display_task",
(uint16_t )DISPALY_STK_SIZE,
(void* )NULL,
(UBaseType_t )DISPALY_TASK_PRIO,
(TaskHandle_t* )&DisplayTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
xSemaphoreCreateMutex()
//显示地址等信息
void display_task(void *pdata)
{
while(1)
{
#if LWIP_DHCP //当开启DHCP的时候
if(lwipdev.dhcpstatus != 0) //开启DHCP
{
// show_address(lwipdev.dhcpstatus ); //显示地址信息
vTaskSuspend(DisplayTask_Handler); //显示完地址信息后挂起自身任务
}
#else
show_address(0); //显示静态地址
vTaskSuspend(DisplayTask_Handler); //显示完地址信息后挂起自身任务
#endif //LWIP_DHCP
vTaskDelay(1000);
}
}
//led任务
void led_task(void *pdata)
{
while(1)
{
LED0 = !LED0;
vTaskDelay(1000);
LED1 =!LED1;
vTaskDelay(1000);
}
}
最终测试结果
串口打印:
Pc端ping开发板如下:
同一网络内可以ping通,移植成功。
至此全篇结束。