最新日志

发表于:2008-4-25 17:02:45
标签:c  

0

C可变长参数表问题

原文地址:http://www.programfan.com/club/showbbs.asp?id=82615

一、什么是可变参数
我们在C语言编程中有时会遇到一些参数个数可变的函数,例如printf()函数,其函数原型为:
int printf( const char* format, ...);
它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的(用三个点“…”做参数占位符),实际调用时可以有以下的形式: printf("%d",i);
printf("%s",s);
printf("the number is %d ,string is:%s", i, s);    
以上这些东西已为大家所熟悉。但是究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实现,这个问题却一直困扰了我好久。本文就这个翁饨幸恍┨教?希望能对大家有些帮助.
long sum(int i,...)
{
  int *p,j;
  long s = 0;
  p = &i+1;
  for (j=0;j<i;j++)
    s += p[j];
   return s;
}
long Sum = sum(3,1,2,3);
printf("%ld",Sum);
Sum == 6
二、写一个简单的可变参数的C函数
先看例子程序。该函数至少有一个整数参数,其后占位符…,表示后面参数的个数不定. 在这个例子里,所有的输入参数必须都是整数,函数的功能只是打印所有参数的值.
函数代码如下:
//示例代码1:可变参数函数的使用
#include "stdio.h"
#include "stdarg.h"
void simple_va_fun(int start, ...)
{
    va_list arg_ptr;
    int nArgValue =start;
    int nArgCout="0";     //可变参数的数目
    va_start(arg_ptr,start); //以固定参数的地址为起点确定变参的内存起始地址。
    do
    {
        ++nArgCout;
        printf("the %d th arg: %d",nArgCout,nArgValue);     //输出各参数的值
        nArgValue = va_arg(arg_ptr,int);                      //得到下一个可变参数的值
    } while(nArgValue != -1);                
    return;
}
int main(int argc, char* argv[])
{
    simple_va_fun(100,-1);
    simple_va_fun(100,200,-1);
       return 0;
}
下面解释一下这些代码
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
⑴由于在程序中将用到以下这些宏:
void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr );
va在这里是variable-argument(可变参数)的意思.
这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.
⑵函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变
量是存储参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。
⑶然后用va_start宏初始化⑵中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,即最后一个固定参数.
⑷然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。
⑸设定结束条件,这里的条件就是判断参数值是否为-1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。至于为什么它不会知道参数的数目,读者在看完这几个宏的内部实现机制后,自然就会明白。




--------------------------------------------------------------------------------


(二)可变参数在编译器中的处理
我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下面看一下VC++6.0中stdarg.h里的代码(文件的路径为VC安装目录下的\vc98\ include\stdarg.h)
typedef char *  va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)      ( ap = (va_list)0 )
下面我们解释这些代码的含义:
1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的
2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大?gt;>T谖业幕魃现苯佑胹izeof运朔创妫猿绦虻脑诵薪峁挂裁挥杏跋臁#ê笪慕吹轿易约旱氖迪郑?
3、va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start (ap, v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。
这里要知道两个事情:
    ⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
    (2)在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
|——————————————————————————|
|  最后一个可变参数             |   ->高内存地址处
|——————————————————————————|
   ...................
|——————————————————————————|
|  第N个可变参数              |     ->va_arg(arg_ptr,int)后arg_ptr所指的地方,
|                               |     即第N个可变参数的地址。
|——————————————— |     
   ………………………….
|——————————————————————————|
|  第一个可变参数               |     ->va_start(arg_ptr,start)后arg_ptr所指的地方
|                               |     即第一个可变参数的地址
|——————————————— |     
|———————————————————————— ——|
|                               |
|  最后一个固定参数             |    -> start的起始地址
|—————————————— —|       .................
|—————————————————————————— |
|                               |  
|——————————————— |  -> 低内存地址处

(4) va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到弦桓霾问钠鹗嫉刂贰?
因此,现在再来看va_arg()的实现就应该心中有数了:
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏做了两个事情,
       ①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
   ②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。
(5)va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.

(三)可变参数在编程中要注意的问题
因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢, 可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能 地识别不同参数的个数和类型. 有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数 printf是从固定参数format字符串来分析出参数的类型,再调用va_arg 的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的. 例如,在C的经典教材《the c programming language》的7.3节中就给出了一个printf的可能实现方式,由于篇幅原因这里不再叙述。
(四)小结:
1、标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。
2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如
⑴在固定参数中设标志—— printf函数就是用这个办法。后面也有例子。
⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法.
无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。
3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
①函数栈的生长方向
②参数的入栈顺序
③CPU的对齐方式
④内存地址的表达方式
结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现,将不再使用的指针设为NULL,这样可以防止以后的误操作。
4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。理解了以上要点,相信稍有经验的读者就可以写出适合于自己机器的实现来。下面臼且桓隼?
(五)扩展——自己实现简单的可变参数的函数。
下面是一个简单的printf函数的实现,参考了<The C Programming Language>中的156页的例子,读者可以结合书上的代码与本文参照。
#include "stdio.h"
#include "stdlib.h"
void myprintf(char* fmt, ...)        //一个简单的类似于printf的实现,//参数必须都是int 类型
{
    char* pArg="NULL";               //等价于原来的va_list
    char c;
    
    pArg = (char*) &fmt;          //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值
    pArg += sizeof(fmt);         //等价于原来的va_start          

    do
    {
        c =*fmt;
        if (c != '%')
        {
            putchar(c);            //照原样输出字符
        }
        else
{
//按格式字符输出数据
            switch(*++fmt)
{
            case 'd':
                printf("%d",*((int*)pArg));           
                break;
            case 'x':
                printf("%#x",*((int*)pArg));
                break;
            default:
                break;
            }
            pArg += sizeof(int);               //等价于原来的va_arg
        }
        ++fmt;
    }while (*fmt != '\0');
    pArg = NULL;                               //等价于va_end
    return;
}
int main(int argc, char* argv[])
{
    int i = 1234;
    int j = 5678;
    
    myprintf("the first test:i=%d",i,j);
    myprintf("the secend test:i=%d; %x;j=%d;",i,0xabcd,j);
    system("pause");
    return 0;
}
在intel+win2k+vc6的机器执行结果如下:
the first test:i=1234
the secend test:i=1234; 0xabcd;j=5678;
#include <stdarg.h>//不定数目参数需要的宏
int max(int n,int num,...)
{
va_list x;//说明变量x
va_start(x,num);//x被初始化为指向num后的第一个参数
int m="num";
for(int i="1";i<n;i++)
{
//将变量x所指向的int类型的值赋给y,同时使x指向下一个参数
int y="va"_arg(x,int);
if(y>m)m=y;
}
va_end(x);//清除变量x
return m;
}
main()
{
    printf("%d,%d",max(3,5,56),max(6,0,4,32,45,533));
}

点击此处查看原文 >>

系统分类: 软件开发   |    用户分类:    |    来源: 整理

评论(0) | 阅读(167)
发表于:2008-4-10 22:29:56
标签:dsp  

0

在TMS320F28XXX DSP上实现从flash拷贝整个程序到ram运行的方法探讨

TMS320F2812上实现从flash拷贝整个程序到RAM上运行的方法探讨

1.     前言

TMS320F2812 DSP 里,代码从内部flash里运行,比从内部RAM里运行要慢30%左右,所以对运行时间苛刻的程序直接在flash里运行,往往不能满足要求。故而,需要将代码拷贝到RAM以提高运行速度。TI文档只提供了部分代码从flash拷贝到RAM中的方法。然而,在一些应用中,需要将整个代码段都拷贝到RAM中执行,以提高整体运行速度。本文通过对TMS320F2812 的启动代码研究,来探讨如何在从FLASH启动后将整个代码段拷贝到RAM中,然后在RAM中运行的方法。

2.     TMS320F2812 启动过程

TMS320F2812的内部存储器中,0x3F F000  ----0x3FFFC04K*16Boot ROM

  1  2812内部BootRom地址图

CPU向量表位于北部ROM的底端(0x3FFFC0 ---- 0x3FFFF)。当VMAP=1,ENPIE=0 ,MPNMC=0时,该向量表被激活。复位向量出厂时被编程指向函数InitBoot。这个函数开始启动过程。当然,启动过程完成之后,我们需要初始化PIE中断向量表,同时使能PIE block。初始化PIE中断向量表之后,除了复位,所有中断向量均从PIE获得。

 

TMS320F2812启动代码固化在该内部ROM中。当TMS320F2812上电或者热复位后,首先由芯片本身将一些寄存器初始化:

PIE disabled(ENPIE=0,VMAP=1,OBJMDE=0,AMODE=0,MOM1MAP=1),

然后dsp芯片会对XMPNMC管脚采样,根据采样值的高低,来决定启动模式是“微处理器模式”还是“微计算机模式”。当XMPNMC=0时,为“微计算机模式”,此时,启动ROM存储器被使能而XINTF Zone 7被禁止。复位向量从内部启动ROM获取,启动ROM在复位期间一直被使能。

       启动ROM里的复位向量(位于0x3FFFC0)指向InitBoot函数(位于0x3FFC00)。在完成器件初始化(InitBoot)之后,Boot loader将检查GPIO管脚的状态,然后再决定选用的启动模式。启动模式有4种:跳转到flash,跳转到H0 SARAM,跳转到OTP或者调用片上启动程序。

       InitBoot Function 所做工作有:1.初始化状态寄存器;2.将堆栈指针设为0x4000x400 - 0x44F作为启动过程中的堆栈);3.CSM密码保护部分;4.调用SelectBootMode5.调用ExitBoot

       在完成选择启动模式过程之后,根据选择的启动模式,dsp会跳到相应的启动入口。也可以自己选择启动入口。这些入口地址都在这之前已经被dsp定义好的。

       如果从flash启动,那么我们的管脚状态应该是

      

GPIOF4

GPIOF12

GPIOF3

GPIOF2

 

(SCITXDA)

(MDXA)

(SPISTEA)

(SPICLK)

 

内部上拉

无内部上拉

无内部上拉

无内部上拉

Mode Selected

1

x

x

x

Jump to Flash address 0x3F 7FF6

下图为BootROM 函数的流程图

2  BootROM 函数的流程图

对于内部flash启动,如图

 

3跳转到Flash启动的流程图

0x3F7FF6必须放置一个跳转指令,该指令跳转到你自己的启动代码或者应用程序。

3.     搬移思路

根据上节2的启动过程,flash启动过后,跳到0x3F7FF6,然后根据其内容再跳转到应用程序。我们在这里稍微修改一下,就可以将PC指针跳到RAM首地址,程序就能在RAM里运行了。

将“搬移程序”烧在flash上,从flash启动之后,“搬移程序”会被执行。“搬移程序”做的工作就是将“应用主程序”代码拷贝到内部RAM,然后经过初始化环境(InitBootExitBoot),将PC指针指向RAM里代码首地址。这样,你的程序就在RAM中运行起来了。也就是说,我们需要一个“搬移程序”,启动时运行,用来拷贝flash上的代码到RAM中;当然,还需要一个你的“应用主程序”,该主程序被“搬移程序”从flash里“挪”到RAM后在RAM中运行。我们还要做的工作就是,把“应用主程序”烧写到flash里的某一块,这个块又不会影响flash启动时运行“搬移程序”。完成这个烧写过程的程序,我们称之为flash烧写程序。

总结一下,完成整个搬移过程,一共需要三个程序。“搬移程序”和“应用主程序”被固化到flash里“烧写flash程序”将“应用主程序”烧入flash中指定的块中。TI的烧写flash插件用来烧写“搬移程序”。

4.     搬移方法

1)首先你要用的主程序必须编译通过,并且通过仿真器在RAM里运行无问题。将主程序的CMD文件进行改写,保证程序段(.text)分配在连续的存储空间。程序从flash启动,所有初始化段链接在非易失存储器里,而非初始化段必须链接在易失存储器。我们可以把初始化段都放在一个连续的内部RAM空间,而非初始化段放在另一个内部RAM空间。如果你的代码不是很大,也可以都放在连续的RAM空间。但在实际项目中,通常你会遇到存储空间不够的问题。这时就要考虑将无关紧要的段放在另外的非程序空间了。

 

.cinit

Flash

.cio

RAM

.const

Flash

.econst

Flash

.pinit

Flash

.switch

Flash

.text

Flash

.bss

RAM

.ebss

RAM

.stack

Lower 64Kw RAM

.sysmem

RAM

.esysmem

RAM

.reset

RAM1

 

例如:

MEMORY

{

PAGE 0 :

   RAMH0      : origin = 0x3F8000, length = 0x002000

         

PAGE 1 :

   /* SARAM                     */    

   RAMM0M1    : origin = 0x000000, length = 0x000800

   RAML0L1    : origin = 0x008000, length = 0x002000

}

 

 

SECTIONS

{

   /* Allocate program areas: */

   .reset              : > RAMH0     PAGE = 0

   vectors             : > RAMH0     PAGE = 0

   .cinit              : > RAMH0     PAGE = 0

   .text               : > RAMH0     PAGE = 0

   .const              : > RAMH0     PAGE = 0

   .econst             : > RAMH0     PAGE = 0

   .switch                               : > RAMH0     PAGE = 0

  

   /* Allocate data areas: */

   .stack              : > RAMM0M1     PAGE = 1

   .bss                : > RAML0L1     PAGE = 1

   .ebss               : > RAML0L1     PAGE = 1

   .sysmem             : > RAML0L1     PAGE = 1

}

从上面的CMD可知,主程序代码均放在RAMH0中,长度为0x2000

2)其次,将该工程编译成功后,加载到内部ram,仿真器自动完成必要的初始化环境之后,pc指针应该指向_c_init00,记下现在PC指针的位置,在Boot.asm中会用到。

3)自制一个flash烧写程序,或者从网上下载其他网友的flash烧写程序,将目标地址放在除flashJ块以外的块中。烧写的长度不能小于被烧写的主程序长度。该烧写程序在RAM中运行。,其代码段不能和被烧写的主程序用的代码段存储区域相同,否则会破坏主程序在ram中的代码。

       烧写之后,可以用CCSSave data功能,来查看flash中的数值是否和ram里主程序空间数值一致。

4 TI的烧写插件烧写“搬移程序”。注意,该“搬移程序”要能在dsp启动后执行。并且,烧写的时候,不能将上一步烧到flash上的主程序代码擦除。“搬移程序”具体见下节。

 

 

5.     “搬移程序”具体实现方法

 

Boot.asm文件内容:

 

    .def _InitBoot

    .ref _EntryAddr_H

    .ref _EntryAddr_L

 

    .sect ".InitBoot"

;

; _InitBoot

;

; 1) Initalizes the stack pointer

; 2) Sets the device for C28x operating mode

; 3) Calls the main boot functions

; 4) Calls an exit routine

;

_InitBoot:

; Initalize the stack pointer.

  MOV SP, #0 ; Initalize the stack pointer

; Initalize the device for running in C28x mode.

  C28OBJ ; Select C28x object mode

  C28ADDR ; Select C27x/C28x addressing

  C28MAP ; Set blocks M0/M1 for C28x mode

  CLRC PAGE0 ; Always use stack addressing mode

  MOVW DP,#0 ; Initialize DP to point to the low 64 K

  CLRC OVM

; Set PM shift of 0

  SPM 0