应用笔记 613

DS80C400/DS80C410/DS80C411的Keil C语言编程

By: Kris Ardis

摘要 : 本应用笔记介绍了如何从Keil®的µVision2®开发套件着手构建一个DS80C400微控制器的C语言应用程序,通过实现一个简单的HTTP服务器演示如何使用DS80C400的ROM功能。所有开发都采用了TINIm400验证模块和包含7.05版C编译器“C51”的Keil µVision2 2.37版。这些DS80C400例程可以方便地移植到DS80C410或DS80C411微控制器应用中。

另请参考:

引言

DS80C400/DS80C410/DS80C411 ROM提供多个可通过8051汇编、C或Java®语言访问的功能。ROM是构建C和汇编程序的起始存储区,这些程序可提供经过TINI®接口验证的网络堆栈、进程调度器和存储器管理器。诸如网络扬声器这样的简单程序可以用汇编语言很容易地实现,C语言可用于更为复杂的程序,如:与文件系统交互作用的HTTP服务器。

从Keil的µVision2开始

你可以使用Keil µVision2开发套件,构建一个简单的HelloWorld型C语言程序。按照以下步骤完成你的第一个用于DS80C400的C语言应用程序。

选择Project Create New Project。 输入项目名称。

屏幕上将弹出Select Device for Target对话框。在Data base中选择MaximDS80C400。选择Use Extended LinkerUse Extended Assembler。点击OK继续。图1所示为该对话框的正确配置。

图1. 为一个新的Keil µVision2项目选择器件
图1. 为一个新的Keil µVision2项目选择器件

软件将询问:Copy Maxim80C390 Startup Code to Project Folder and Add File to Project? 选择No。我们将提供自己的启动代码。

当项目窗口在左侧打开时,打开Target 1。右击Source Group 1,并选择Add files to group 'Source Group 1'。在弹出的文件对话框中,将files of type改为Asm Source file。添加文件startup400.a51。该文件可以在http://files.dalsemi.com/tini/ds80c400/c_libraries/Keil_C_Libraries.zip上的zip文件中找到。

双击打开文件startup400.a51。找到段声明?C_CPURESET?0。确保代码段在400000h处声明:
      ?C_CPURESET?0        SEGMENT CODE AT 400000h .
另外,应有一个“DB 'TINI'”行,后跟另一个DB行,带有注释“Target bank”。这样就可以确保应用建立地址为400000h,对应于TINIm400上flash的起始地址。请确认该行为:
      DB     40h           ; Target bank
创建一个新的文件,将其另存为“main.c”。在该文件中写入如下代码:
      #include

      void main()
      {
             printf("Test 400 Program\r\n");
             while (1)
             {
             }
      }
保存文件内容。右击Source Group 1,并添加源文件main.c。现在就将该源文件添加到项目中了。

右击左侧的Target 1。选择Options for Target 'Target 1'打开选项对话框。第一个选择标签应该为Target。将Memory Model改为Large: variables in XDATA,将Code Rom Size改为Contiguous Mode: 16MB program。选中检查框Use multiple DPTR registers'far' memory type support。在Off-chip Code memory项目下加入第一个入口:Start为0x400000,Size为0x80000。在Off-chip Xdata memory项目下加入一个入口:Start为0x10000,Size为0x4000。图2所示为配置完成的对话框。确认检查框Use On-Chip Arithmetic Accelerator被清除—多线程应用很难共用算术累加器。

图2. 步骤7目标选项对话框(注意,'Eprom:Start' 应为0x400000,最后一个'0'未显示)。
图2. 步骤7目标选项对话框(注意,'Eprom:Start' 应为0x400000,最后一个'0'未显示)。

选择Output标签。点击Create HEX File,并在下拉框中选择HEX-386。按F7键建立应用程序。如果每一步都正确完成,建立过程应不产生错误或警告信息,之后会生成一个hex文件。现在就可以将该应用程序装载到你的电路板上了。

将应用实例装载到TINIm400模块

这部分介绍如何使用JavaKit工具将Keil编译器生成的hex文件装载到TINIm400验证模块中。使用JavaKit之前,必须首先安装Java Runtime Environment¹ (1.2版以上)和Java Communications API²。JavaKit工具包含在TINI软件开发套件中。运行JavaKit的说明可以在TINI软件开发套件的docs目录下的Running_JavaKit.txt文件中找到。如果你在运行JavaKit时遇到任何技术问题,很可能其他人曾经遇到过类似的问题,这些问题被收集在TINI主题列表中。你可以在http://lists.dalsemi.com/search/search.html上搜索有关该列表的文档。

通过以下命令行建立JavaKit与TINIm400的通话:
      java JavaKit -400 -flash 40
图3显示了JavaKit窗口。

图3. JavaKit界面
图3. JavaKit界面

运行JavaKit后,选择用来与TINIm400进行通信的串口。使用'Open Port'按钮打开该串口。然后按'Reset'按钮。DS80C400装载器将提示如下信息:
     DS80C400 Silicon Software - Copyright (C) 2002 Maxim Integrated
     Detailed product information available at http://www.maximintegrated.com

     Welcome to the TINI DS80C400 Auto Boot Loader 1.0.1
     >
JavaKit顶部的'File'菜单中,选择Load HEX File as TBIN。找到并选择我们已经创建的helloworld.hex文件。Load HEX File as TBIN选项先将输入的hex文件转换为TBIN文件,然后装载。这样的操作比直接装载hex文件速度快,因为对于同样的数据,ASCII编码的hex文件的尺寸是二进制文件的二倍多。

用户程序装载后,有两种执行方法。由于程序装载到存储区40中,所以可以直接键入:
      > B40
      > X
要选择存储区40,并运行这里的代码,也可以键入:
      > E
这将使ROM查找可执行代码,有一个特殊标记符用来表示当前存储区中有可执行代码。该标记符由字符'TINI'和紧随其后的当前区号组成,它位于当前区的地址0002处。我们的HelloWorld程序在startup400.a51文件中对此标记符声明,如下所示:
      ?C_STARTUP:   SJMP   STARTUP1
                    DB     'TINI'               ; Tag for TINI Environment 1.02c
                                                ; or later (ignored in 1.02b)
                    DB     40h                  ; Target bank
注意SJMP STARTUP1语句位于40区的地址0000处。由于sjmp语句占两个字节,所以紧随其后的执行标记{'T', 'I', 'N', 'I', 40h}位于地址0002。当键入'E'时,ROM在FEh存储区中向下查找可执行代码。如果键入'E'后执行了其他代码,就说明ROM在高于400000h (装载你的代码的位置)的地址处找到了执行标记,你可能需要找到该标记,并删除该存储区中的内容。

接口至ROM和ROM库

调用ROM函数的步骤请参见高速微控制器用户指南:网络微控制器补充资料(English only)³。然而,从C中调用ROM函数比较复杂,必须将Keil C编译器规范的参数转换成ROM所使用的规范。Keil编译器以XDATA地址和寄存器组合的方式传递参数。而ROM函数采用不同的方式接收参数。例如,套接字函数接受存放在单个参数缓冲器中的参数,而许多其他应用函数接受特殊功能寄存器或直接存储器地址传来的参数。Maxim编写了访问ROM函数的库,可完成Keil调用规范与ROM参数规范间的翻译工作。

要在C程序中使用ROM函数,只需要导入相应的库,并包含一个头文件。为了在项目中导入一个库,在Keil项目窗口中右击Source Group 1,并选择Add Files to Group 'Source Group 1'。将文件过滤器改为'*.lib',选择你需要的库,然后在源代码顶部加入头文件,你可以使用任何一个库函数。这些ROM库包括:
  • ROM初始化程序
  • DHCP客户端操作
  • 进程调度器
  • 套接字函数(TCP、UDP、Multicast)
  • TFTP客户端操作
  • 实用函数(CRC16和随机数等)

使用扩展库

除了ROM库,还有许多其他库(还有更多正在编写中)提供了很多ROM中没有的实用功能。这些库包括:
  • 文件系统:从TINI文件系统移植,在stdio.h中声明了实现方法
  • DNS客户端操作
  • 1-Wire®:采用公用软件包中定义的API
  • I²C:与使用TINI的设计实现方式相似
  • CAN:与使用TINI的设计实现方式相似
为DS80C400提供的C库项目(包括文档、应用实例和发行说明)可以在http://files.dalsemi.com/tini/ds80c400/c_libraries/index.html上找到。

一个简单应用示例:HTTP服务器和SNTP客户端

我们编写了一个小的应用程序来演示这些库的功能,特别是文件系统、套接字、进程调度器和TFTP库等。应用实例中包括一个SNTP客户端和一个只响应'GET'请求的HTTP服务器。它使用Maxim提供的核心库调用套接字和调度器函数。它还使用文件系统保存了几个网页。

该应用由两个进程组成:HTTP服务器作为一个新进程被创建并用来处理端口80上的连接;主进程位于一个循环中,约每60秒尝试进行一次时间同步。

文件系统的初始化

启动HTTP服务器之前,必须初始化文件系统。演示程序确保两个静态文件(主页(index.html)和程序源码(source.html)),在服务器启动之前已位于文件系统中。可以用多种方法将这些文件安装到文件系统。方法之一是在程序代码数据中包含这些文件的文本,然后,在启动时将文件数据写到文件系统。这是最简单的方法,而且我们的演示程序也有空闲的代码空间可供使用。

本演示程序通过TFTP服务器查找它所需要的文件并初始化其文件系统。这是一种更为有趣、并且更能展示DS80C400内置功能的方法。在我们这个实例中,TFTP服务器在一个已知的IP地址上运行。采用以下代码从TFTP服务器上申请获取文件index.htmlsource.html
void initialize_filesystem()
      {
             struct sockaddr address;
             unsigned int i;
             unsigned int result;
             void* start = (void*)FS_START;

             // initialize the file system
             int x = finit(FOPEN_MAX, FS_BLOCKS, start);
             printf("Result of FS init: %d \r\n", x);

             if ((x==0) && (fexists("index.html")==0) && (fexists("source.html")==0))
             {
                    printf("File system OK, skip TFTP init.\r\n");
                    return;
             }

             // lets get the files we want off a TFTP server
             // initialize TFTP server setting
             for (i=0;i<18;i++)
                    address.sin_addr[i] = 0;

             // since the DS80C400 supports IPv6, the address is 16 bytes long
             // however, since we are only using IPv4 addresses, only the last
             // 4 bytes are meaningful
             address.sin_addr[12] = TFTP_IP_MSB;
             address.sin_addr[13] = TFTP_IP_2;
             address.sin_addr[14] = TFTP_IP_3;
             address.sin_addr[15] = TFTP_IP_LSB;

             result = settftpserver(&address, sizeof(struct sockaddr));
             printf("Set TFTP server to selected server, result: %u\r\n", result);
             result = tftp_init();
             printf("Result of TFTP init: %u \r\n", result);

             get_tftp_file("source.html");
             get_tftp_file("index.html");
      }


      void get_tftp_file(char* filename)
      {
             unsigned int result;
             unsigned char* TFTP_MSG;
             FILE* file;

             printf("Free FS RAM: %ld\r\n", getFreeFSRAM());
             TFTP_MSG = getTFTPData();
             file = fopen(filename, "w");
             result = tftp_first(filename);
             if (result==0xFFFF)
             {
                    printf("Error in TFTP_FIRST...\r\n");
                    return;
             }
             printf("Result of first segment: %u\r\n", result);
             fwrite(TFTP_MSG, 1, result, file);

             while (result >= 512)
             {
                    result = tftp_next(TFTP_MORE_DATA);
                    if (result==0xFFFF)
                    {
                           printf("Error in TFTP_NEXT...\r\n");
                           return;
                    }
                    printf("Result of next segment: %u\r\n", result);
                    TFTP_MSG[result] = 0;
                    fwrite(TFTP_MSG, 1, result, file);
             }
             tftp_next(TFTP_LAST_SEGMENT);

             fclose(file);
             printf("Done with TFTP server.\r\n");
      }
注意在每次启动应用时都须调用finit函数,以确保文件系统已正确安装并能正常工作。如果文件系统已正确初始化(返回0),并且我们所要的文件已全部到位,则函数直接退出,不去尝试下载文件。否则,它会尝试从TFTP服务器下载文件,并将它们写入文件系统,正如函数get_tftp_file中所显示的。

SolarWinds为Windows平台提供了一个免费的TFTP服务器,它被应用于该演示程序的开发中。在SolarWinds的网站跟随Downloads - Free Software菜单可找到TFTP服务器下载。安装以后,使用File菜单下的Configure选项来配置TFTP服务器上可供下载的文件。确保程序使用你的TFTP服务器IP地址(TFTP_IP_MSBTFTP_IP_2TFTP_IP_3TFTP_IP_LSB)。

简单的HTTP服务器

该应用中的HTTP服务器是RFC 2068所描述的HTTP服务器的一个简化版实现。在该版本下,只支持'GET'方法。输入头被忽略,只给出很少的输出头。

服务器套接字通过调用Berkeley型套接字函数来创建,这样使得服务器套接字容易建立。以下代码说明了这个简单的HTTP服务器是如何创建、绑定并接受新连接的。
      struct sockaddr local;
      unsigned int socket_handle, new_socket_handle, temp;

      socket_handle = socket(0, SOCKET_TYPE_STREAM, 0);
      local.sin_port = 80;
      bind(socket_handle, &local, sizeof(local));
      listen(socket_handle, 5);

      printf("Ready to accept HTTP connections...\r\n");

      // here is the main loop of the HTTP server
      while (1)
      {
             new_socket_handle = accept(socket_handle, &address, sizeof(address));
             handleRequest(new_socket_handle);
             closesocket(new_socket_handle);
      }
请注意,接受了新的套接字后,这个简单的应用并不启动新的线程或进程来处理请求。而是在同一进程中处理该请求。任何非演示版的HTTP服务器都会在新的线程中处理收到的请求,这样就允许多个连接出现并被同时处理。请求处理完后,关闭套接字,等待另一个收到的连接。

handleRequest方法从收到的请求中解析出一个文件名,并确定该方法是'GET'。其它方法(甚至是'POST'、'HEAD'或'OPTIONS')均不被接受。两个文件名被作为特例处理。当请求文件为time.html时,服务器动态产生一个响应,其中包含来自时间服务器的最新结果,以及自上一次查询时间服务器以来的秒数。当请求文件为stats.html时,将显示服务器的正常运行时间和请求次数统计结果。

如果找不到文件、发出的是无效的请求方法或者文件系统失效,HTTP服务器报告错误码。

SNTP客户端

时间服务器应用的第二个主要部分是Simple Network Time Protocol (SNTP)客户端,参见RFC 1361的描述。它是Network Time Protocol (RFC 1305)的一个简化版本。SNTP要求UDP从服务器的侦听端口123请求时戳。我们的时间服务器使用以下代码周期性地与服务器time.nist.gov同步。请注意,在写这篇应用笔记时,DNS检索还不被支持,因此服务器的IP地址须手动设置。
      socket_handle = socket(0, SOCKET_TYPE_DATAGRAM, 0);

      // set a timeout of about 2 seconds
      buffer[0] = 0x0;
      buffer[1] = 0x0;
      buffer[2] = 0x8;
      buffer[3] = 0x0;
      setsockopt(socket_handle, 0, SO_TIMEOUT, buffer, 200);

      buffer[2] = 0;                            // reset since we used this in call to setsockopt
      buffer[0] = 0x23;                         // No warning/NTP Ver 4/Client

      address.sin_addr[12] = TIME_NIST_GOV_IP_MSB;
      address.sin_addr[13] = TIME_NIST_GOV_IP_2;
      address.sin_addr[14] = TIME_NIST_GOV_IP_3;
      address.sin_addr[15] = TIME_NIST_GOV_IP_LSB;
      address.sin_port = NTP_PORT;

      sendto(socket_handle, buffer, 48, 0, &address, sizeof(struct sockaddr));
      recvfrom(socket_handle, buffer, 256, 0, &address, sizeof(struct sockaddr));

      timeStamp = *(unsigned long*)(&buffer[40]);
      timeStamp = timeStamp - NTP_UNIX_TIME_OFFSET;
      // now we have time since Jan 1 1970

      formatTimeString(timeStamp, "London", last_time_reading_1);
      last_reading_seconds = getTimeSeconds();
      closesocket(socket_handle);
首先生成一个数据报套接字,并给定一个约2秒的超时(0x800==2048ms)。这样可以确保在与我们选择的服务器通信失败时,不必无限期地等待下去。

下一行设定请求选项。关于这些位的描述参见RFC 1361第三节。值0x23要求闰秒时无需告警,要求采用NTP版本4,并声明模式为'Client'。当我们使用公共数据报函数sendtorecvfrom发出请求并收到回答后,时戳的秒部被赋给变量timeStamp,然后调整到参考时间1970年1月1日。formatTimeString函数将时戳转换为易读的字符串,如“In London it is 15:37:37 on March 31, 2003”。

getTimeSeconds函数以DS80C400内部时钟为基础确定上一次更新。由于该程序大约每60秒才更新一次,HTML页time.html使用这个数值来报告自上次更新以来的时间间隔。最后,关闭套接字,SNTP客户端在下面的60秒内进入休眠。

有关同步的说明

在LARGE存储模式下,Keil编译器通过在进程交换中安全的存储器传递有限数量的参数。这就意味着有些函数不能由多个程序同时调用。尽管已专为400开发了C库,其中的所有变量都通过在进程交换中安全的直接存储器传递,但是有些函数仍然是危险的。例如,Berkeley式的套接字头要求一些较长的方法签名,它会涉及到一些通过非安全存储器传递的数据。因此,针对套接字有两个库。

一个库(rom_sock.lib)遵循Berkeley式的字头。但是,两个进程同时用这个库调用某个函数是不安全的。不过,如果一个进程正在使用UDP函数而另一个正在使用TCP函数就不会有问题。为了对并发访问非安全存储器提供真正的保护,开发了另外一个套接字库(rom_sock_synch.lib)。该库中的函数类似于Berkeley型函数,但具有更少或重新安排的变量,以使Keil编译器通过安全存储区传递参数。无论何种情况,请参考相关文档,以确认函数是否在多进程中是安全的。

定时访问寄存器和Keil C编译器

受保护的位只能通过定时访问程序进行写操作,该程序完成后,受保护的位将在三个时钟周期内保持可写状态。以下代码演示了如何在汇编程序中复位看门狗定时器。
RWT	BIT	0D8h		; Bit 0 of the WDCON register
TA	EQU	0C7h		; Timed Access register

ORG	00h

MOV	TA, #0AAh		; Enable the timed access
MOV	TA, #055h

SETB	RWT			; Reset the watchdog timer count

END
如果使用Keil C编译器,同样的代码可改写如下:
sfr TA = 0xC7; 		// Timed Access register
sfr WDCON = 0xD8; 	// Watchdog Control register

sbit RWT = WDCON^0;	// Bit 0 of the WDCON register

TA = 0xAA;		// Enable the timed access
TA = 0x55;

RWT = 1;		// Reset the watchdog timer count
Keil C编译器是一款可优化的编译器。如定时访问等重复序列,通常被编译器所优化,使得两次定时写操作和对指定寄存器的写操作不连续进行。这种情况下,占用3个时钟周期的定时访问可能会在软件写指定寄存器之前终止,寄存器的内容保持不变。

为防止上述情况的发生,用户必须为每一次定时访问写操作创建一个函数,应始终设置函数的优化等级。之前使用定时访问复位看门狗定时器的例子可改写如下:
sfr TA = 0xC7;			// Timed Access register
sfr WDCON = 0xD8;		// Watchdog Control register

sbit RWT = WDCON^0;		// Bit 0 of the WDCON register

#pragma SAVE			// Save current optimization level
#pragma OPTIMIZE(8)		// Prevent common subroutine optimization
#pragma DISABLE			// Disable interrupts for this function

void reset_watchdog(void){
	TA = 0xAA;		// Enable the timed access
	TA = 0x55;

	RWT = 1;		// Reset the watchdog timer
}

#pragma RESTORE			// Restore previous optimization level

有关指针传递的说明

Keil文档提供了如何用8051汇编语言编写可从你的C程序中调用的方法。如果你选择这样做,请注意,由C程序传递到8051汇编的指针在DS80C400中不是立即可用的。因为传统的8051架构是16位,Keil指针由两字节的指针和一字节的存储类型组成。当采用Maxim的24位8051微控制器时,存储类型字节为指针高字节所用,但采用一种变化的形式。在当前版本的Keil编译器中,高指针字节有其高位设置并按1递增。以下来自rom_offsets.inc的宏在Maxim库中被用于纠正被更改的指针:
      FIXKEILPOINTER  MACRO DIRECT_DPX
             LOCAL  must_be_null
             mov    a, DIRECT_DPX
             jz     must_be_null
             dec    a
             anl    a, #7Fh
             mov    DIRECT_DPX, a
     must_be_null:
             ENDM
Keil编译器通过寄存器r3:r2:r1 (r3是存储类型字节)或XDATA存储区传递指针。该宏将工作于任何寄存器或其他直接存储器值,传给它存储类型字节,它会在同一位置返回高指针字节。以下代码演示了它的用法:
      ;
      ; Keil passes pointers as r3:r2:r1...
      ;---- Variable 'buffer1?972' assigned to Register 'R1/R2/R3' ----
      ;
      FIXKEILPOINTER r3
      ;
      ; r3:r2:r1 is now usable as a pointer value.
      ;

      ;
      ; ...or in XDATA.
      ;---- use dpx1:dph1:dpl1 for buffer pointer ----
      ;
      mov     dptr, #buffer2?1078
      GETX
      mov     dpx1, a
      inc     dptr
      GETX
      mov     dph1, a
      inc     dptr
      GETX
      mov     dpl1, a
      FIXKEILPOINTER   dpx1
      ;
      ; Data pointer 1 is now usable as a pointer.
      ;
注意,还有一种与FIXKEILPOINTER宏相反的情况,可以使函数将其所需的指针转换为Keil编译器生成代码可以理解的形式。在此情况下,可以用UNFIXKEILPOINTER宏,这个宏的用法和FIXKEILPOINTER宏相同。一点不同是,当你从一个用汇编写的方法中返回一个指针时,指针必须保存在寄存器r3r2r1中,高指针字节在r3中。因此,在退出一个需要返回指针的函数前,必须调用宏:
      UNFIXKEILPOINTER r3
      ret
      ; End of the assembly function

保持你的Keil为最新版本

Keil会随时发布对其µVision2工具套件的更新版本。网站www.keil.com/uvision/contains上有关于最新版本的C51编译器和µVision2 IDE的信息。从该网页你可以选择你需要的下载,并看到所发生的变化。

更新应该是一个可执行的InstallShield,运行后,应用将显示一个标题为Setup µVision2的窗口。选择Update Current Installation选项执行更新。程序可以检测你当前的安装目录,点击Next继续。在下一屏上选择是否要保留原来的µVision2配置,并再次点击Next。最后,确认你选择的选项并开始安装。

结论

Keil C编译器和Maxim提供的库允许C语言编写的应用也可方便地使用以前只能通过TINI的Java环境访问的功能和函数。C语言程序现在可以访问网络堆栈、存储管理器、进程调度器、文件系统、以及DS80C400的许多其他特性。另外,与TINI运行环境相比,用C语言编写的应用程序可以选择使用和包含哪些库,为用户代码和数据提供了更多的空间。使用C语言的DS80C400开发者可轻易编写出极为精简的应用,有充裕的速度、能力、代码空间来应付各种问题。



参考文献
  1. 可从http://java.sun.com/j2se/downloads.html下载
  2. 可从http://java.sun.com/products/javacomm/下载
  3. 高速微控制器用户指南:网络微控制器补充资料请参见china.maximintegrated.com/products/microcontrollers/pdfs/network_microcontroller_supplement.pdf
相关链接: