"); //-->
Altera公司推出的Nios软核CPU是一种可配置的通用精简指令集计算RISC(Reduced Instruction Set Computing)嵌入式处理器。它可以与各种外设相结合,构成一个定制的可编程片上系统SOPC(System on Programable Chip)。
嵌入式实时操作系统uC/OS-II是一个非常优秀的实时操作系统RTOS(Real Time Operating System),其性能已得到广泛认可。uC/OS-II的特点有:公开的源代码、可移植、可裁剪、可固化、抢占式内核。TCP/IP是Interenet的基本协议。嵌入式设备 要与Internet网络交换信息,就必须支持TCP/IP协议。
尽管uC/OS-II是一个开放源码的RTOS,但是目前它的第三方TCP/IP支持都是商业化的,很少给出源代码。用户需要付费才能获得。通过在Nios上移植uC/OS-II和开放源码的TCP/IP协议栈-LwIP轻量级网络协议(Light-weight Internet Protocol),就可以实现uC/OS-II的网络功能,并建立一套嵌入式网络开发平台。该系统模型示于图1。
uC/OS-II在Nios上的移植
uC/OS-II可以看作是一个多任务的调度器,在这个任务调度器上添加了和多任务操作系统相关的一些系统服务,如信号量、邮箱、消息队列等。uC/OS-II的设计分为与处理器类型无关的代码、与处理器类型相关的代码和与应用程序有关的配置代码三部分。这也是uC/OS-II具有良好的可移植性的原因。移植工作主要集中在多任务切换的实现上。这部分代码主要是用来保存和恢复处理器现场(即相关寄存器),因此不能用c语言,只能使用特定处理器的汇编语言完成。在Nios上移植uC/OS-II非常简单,只需修改三个和Nios体系结构相关的文件即可。下面分别介绍这三个文件的移植工作。
1.1 OS_CPU.H文件
数据类型定义 这部分的移植是和所用的编译器相关的,我们使用的编译器是nios-elf-gcc。需要定义的数据类型包括无符号和有符号的8位、16位和32位整型变量等。
堆栈单位 因为处理器现场的寄存器在任务切换时都将被保存在当前运行任务的堆栈中,所以OS_STK数据类型应该与处理器的寄存器长度一致。
typedef unsigned int OS_STK;
堆栈增长方向 堆栈由高地址向低地址增长,这和选择的编译器有关。
#define OS_STK_GROWTH 1
宏定义(包括开、关中断的宏定义,以及进行任务切换的宏定义)
#define OS_ENTER_CRITICAL() disable_interrupt();
#define OS_EXIT_CRITICAL() enable_interrupt()
#define OS_TASK_SW() OSCtxSw
1.2 OS_CPU_C.C文件
该文件必须实现任务初始化时的堆栈设计,也就是在堆栈增长方向上如何定义每个需要保存的寄存器的位置。我们将堆栈空间设计为按任务堆栈空间由高至低依次保存寄存器ra、ISTATUS、r1~r31。
该文件还需要实现几个操作系统规定的hook函数。通常都实现为空函数。
1.3 OS_CPU A.S文件(由汇编语言实现)
(1)OSStartHighRdy()函数 此函数是在OSStart()多任务启动后,负责从最高优先级任务的TCB控制块中获得该任务的堆栈指针sp,通过sp依次将CPU现场恢复。这时系统就将控制权交给用户创建的该任务进程,直到该任务被阻塞或者被其他更高优先级的任务抢占CPU。该函数仅仅在多任务启动时被执行一次,用来启动优先级最高的任务执行,以后多任务的调度和切换就由下面的函数来实现。
(2)OSCtxSw()函数 任务级的上下文切换。它是当任务因被阻塞而主动请求CPU调度时被执行的。它的工作是先将当前任务的CPU现场保存到该任务堆栈中,然后获得最高优先级任务的堆栈指针,从该堆栈中恢复此任务的CPU现场,使之继续执行。
(3)OSIntCtxSw()函数 中断级的任务切换,它是在ISR(中断服务例程)中执行任务切换。当发现有高优先级任务就绪,则在中断退出后并不返回被中断任务,而是直接调度就绪的最高优先级任务执行。这样做的目的是能够尽快地让高优先级的任务得到响应,保证系统的实时性。它的原理基本上与任务级的切换相同,但是由于进入中断时已经保存过被中断任务的CPU现场,因此这里就不用再保存。
(4)OSTickISR()函数 时钟中断处理函数。它的主要任务是负责处理时钟中断,调用系统实现的OSTimeTick函数,如果有等待时钟信号的高优先级任务,则需要在中断级别上调度其执行。
(5)OS_ENTER_CRITICAL()函数和OS_EXIT_CRITICAL()函数 分别是进入临界区和退出临界区的宏指令。主要用于在进入临界区之前关中断,在退出临界区的时候恢复原来的中断状态。
2 LwIP
LwIP是Light-weight Internet Protocol的缩写,即轻量级网络协议。LwIP是瑞典计算机科学院的Adam Dunkels等开发的用于嵌入式系统的TCP/IP协议栈。LwIP实现的重点是在保持TCP/IP协议主要功能的基础上减少对RAM的占用,一般它只需要几十KByte的RAM和40K左右的ROM就可以运行,适于在嵌入式系统中使用。
在LwIP中,所有TCP/IP协议栈都在一个进程当中。应用层程序既可以是单独的进程,也可以驻留在TCP/IP进程中。如果是单独的进程,可以通过操作系统的邮箱、消息队列等和TCP/IP进程进行通讯;如果驻留TCP/IP进程中,那么利用内部回调函数接口(Raw API)和TCP/IP协议栈通讯。对uC/OS-II来说,进程就是一个任务。LwIP的进程模型 (Process Model)示于图2。在图2中,整个TCP/IP协议栈都在同一个任务(tcpip_thread)中。应用层程序既可以是独立的任务(图中的tftp_thread和cpecho_thread),也可以在tcpip_thread中利用内部回调函数接口和TCP/IP协议栈通讯。
3 LwIP在uC/OS-II上的移植
在设计LwIP时,就考虑到移植问题,所有与操作系统、编译器相关的部分被独立出来,放在/src/arch目录下。因此,LwIP在uC/OS-II上的实现就是修改这个目录下的文件。下面分别说明相应文件的实现。
3.1 与CPU或编译器相关的include文件
在LwIP/src/arch/include/arch目录下,cc.h、cpu.h、perf.h中有一些与CPU或编译器相关的定义,如数据长度、字的高低位顺序等。这应该与用户实现uC/OS-II时定义的参数一致。通常,c语言的结构体(struct)是4字节对齐的,但是在处理数据包的时候,LwIP是通过结构体中不同数据的长度来读取相应的数据的,所以,一定要在定义struct的时候使用_packed关键字,让编译器放弃struct的字节对齐。LwIP也考虑到了这个问题,所以,在它的结构体定义中有几个PACK_STRUCT_xxx宏,在移植的时候添加编译器所对应的_packed关键字。比如在nios-eft-gcc上对应的定义为:
#define PACK_STRUCT_FIELD(x) x_attribute_((packed))
#define PACK_STRUCT_STRUCT _attribute_((packed))
3.2 sys_arch操作系统相关部分
sys_arch.h[c]中的内容是与OS相关的一些结构和函数,主要可以分为四个部分:
3.2.1 sys_sem_t 信号量
LwIP中需要使用信号量进行通信,所以在sys_arch中应实现信号量结构体和处理函数:
struct sys_sem_t
sys_sem_new() //创建一个信号量结构
sys_sem_free() //释放一个信号量结构
sys_sem_signal() //发送信号量
sys_arch_sem_wait() //请求信号量
由于uC/OS-II已经实现了信号量OS_EVENT的各种操作,并且功能和LwIP上面几个函数的功能是完全一样的,所以只要把uC/OS-II的函数重新封装成上面的函数就可以了。
3.2.2 sys_mbox_t消息
LwIP使用消息队列来缓冲、传递数据报文,因此要在sys_arch中实现消息队列结构
sys_mbox_t以及相应的操作函数:
sys_mbox_new() //创建一个消息队列
sys_mbox_free() //释放一个消息队列
sys_mbox_post() //向消息队列发送消息
sys_arch_mbox_fetch() //从消息队列中获取消息
uC/OS-II虽然实现了消息队列结构OS_Q及其操作,但是uC/OS-II没有对消息队列中的消息进行管理,因此不能直接使用,必须在uC/OS-II的基础上重新实现。为了实现对消息的管理,我们定义了以下结构:
typedef struct{
OS_EVENT *pQ:
void *pvQEntries[MAX_QUEUE_ENTRIES];
}sys_mbox_t;
typedef PQ_DESCR sys_mbox_t; //LwIP中的mbox是UCOS的消息队列
该结构包括OS_EVENT类型的队列指针(pQ)和队列内的消息(pvQEntries)两部分,对队列本身的管理利用uC/OS-II自己的消息队列相关函数来完成,然后使用uC/OS-II中的内存管理模块实现对消息的创建、使用和删除,两部分综合起来便实现了LwIP的消息队列功能。
3.2.3 sys_arch_timeout函数
LwIP中每个与外界网络连接的线程都有自己的timeout属性,即等待超时时间。这个属性表现为每个线程都对应一个sys_timeout结构体队列,它包括这个线程的timeout时间长度,以及超时后应调用的timeout函数,该函数会做一些释放连接、回收资源的工作。timeout结构体已经在sys.h中定义好了,而且对结构体队列的数据操作也由LwIP负责,我们所要实现的是如下函数:
struct sys_timeouts *sys_arch_timeouts(void)
这个函数的功能是返回目前正处于运行状态的线程所对应的timeout队列指针。timeout队列属于线程的属性,因此是与操作系统相关的函数,只能由用户实现。
3.2.4 sys_thread_new创建新线程函数
LwIP可以是单线程运行,即只有一个tcpip线程(tcpip_thread),负责处理所有的TCP(Transmission Control Protocol:传输控制协议)或UDP(User Datagram Protocol:用户数据报协议)连接,各种网络程序都通过tcpip线程与网络交互。它也可以多线程运行,以提高效率。这时就需要用户实现创建新线程的函数:
void sys_thread_new(void(*thread)(void *arg),void *arg);
在uC/OS-II中,没有线程(thread)的概念,只有任务(Task)。它提供了创建新任务的系统调用OSTaskCreate,因此只要把OSTaskCreate封装一下,就可以实现sys_thread_new。需要注意的是LwIP中的thread并没有uC/OS-II中优先级的概念,实现时,用户要事先为LwIP中创建的线程分配好优先级。
3.3 lib_arch中库函数的实现
LwIP用到8个外部函数,这些函数通常与用户使用的系统或编译器有关,因此要求用户实现。
u16_t htons(u16_t n);//16位数据高低字节交换
u16_t ntons(u16_t n);
u32_t htonl(u32_t n);//32位数据大小端对调
u32_ t ntonl(u32_t n);
int strlen(const char *str);
int strncmp(const char *str1,const char *str2,int len);
void bcopy(const void *src,void dest,int len);
void bzero(void *data,int n);
4 网络设备驱动程序
我们采用的网络芯片为Cirrus Logic公司的CS8900芯片。LwIP的网络驱动有一定的模型,/src/netif/ethernetif.c文件即为驱动的模板,用户为自己的网络设备实现驱动时应参照此模板。在LwIP中可以有多个网络接口,每个网络接口都对应了一个netif结构,该结构体包含了相应网络接口的属性、收发函数。LwIP调用netif的函数netif->input()及netif->output()进行以太网packet的收、发等操作。在驱动中主要做的就是实现网络接口的收、发、初始化以及中断处理函数。
void ethernetif_init(struct netif *netif) //网卡初始化函数
void ethernetif_input(struct netif *netif) //网卡接收函数
err_t ethernetif_output(struct netif *netif,struct pbuf *p,struct ip_addr *ipaddr) //网卡发送函数
void ethernetif_isr(void);//网卡中断处理函数
5 测试
完成上面的移植修改工作后,就可以在uC/OS-II中初始化LwIP,并创建TCP或UDP任务进行测试了。这部分是用c语言实现的。关键部分的代码和说明如下:
main(){
OSInit();
OSTaskCreate(lwip_init_task,&task1_data,&lwip_init_stk[TASK_STK_SIZE-1],0);
OSTaskCreate(user_task,&task2_data,&user_stk[TASK_STK_SIZE-1],1);
OSStart();
}
主程序中,创建了Lwip_init_task初始化LwIP任务(优先级0)和user_task用户任务(优先级1)。lwip_init_task任务中除了初始化硬件时钟和LwIP之外,还创建了tcpip_thread(优先级3)和tcpecho_thread(优先级4)。实际上tcpip_thread才是LwIP的主线程,多线程的Berkley API也是基于这个线程实现的,即上面的tcpecho_thread线程也要依靠tcpip_thread线程来与外界通信。tcpecho_thread是一个TCP echo服务,监听7号端口。程序框架如下:
void tcpecho_thread(void arg){
conn = netconn_new(NETCONN_TCP); //建立新的连接
netconn_bind(conn,NULL,7); //绑定到7号端口
netconn_listen(conn); //监听7号端口
while(1){
newconn = netconn_accept(conn); //接收外部连接
bur=netconn_recv(newconn) //获取数据
…… //处理数据
netconn_write(newconn,data,len,NETCONN_COPY);//发送数据
netconn_delete(newconn);//释放本次连接
}
}
编译下载运行,用ping ip地址命令可以得到ICMP reply响应,用telnet ip地址7(登录7号端口)命令可以看到echo server的回显效果。说明ARP、IP、ICMP、TCP协议都已正确运行。
6 结论
本文提出了在Nios软核CPU上移植嵌入式实时操作系统uC/OS-II,并且在uC/OS-II上移植LwIP,以构建嵌入式网络开发平台。在该网络平台的设计中,一方面,由于采用Nios软核CPU,而Nios开发软件SOPC Builder开发环境的完备功能使得硬件设计简单而快捷;另一方面,由于uC/OS-II和LwIP都是开放源代码的,并且在设计时就已经考虑了移植问题,因此使得移植很方便。通过测试,证明这种方案切实可行。
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。