• 带你梳理下ARM代码编译链接的工作流程

     梳理下下ARM代码编译链接的工作流程,以及过程中需要的相关概念信息,不具体关注编译链接的具体命令。 一、编译过程 编译过程就是把源代码编译生成目标代码的过程。而采用ARM编译命令,可以将源代码编译成带有ELF格式的目标文件。除了编译命令可以选择相应的编译选项之外,源代码中的pragmas以及特别的关键字也会对编译过程/结果产生一定影响。 1、makefile文件 Makefiile类似一个脚本文件,这个文件用来定义了编译过程,其中包含了需要编译的文件、文件顺序,编译的宏定义等等,可以看做完整编译需要的信息及过程的集合。 2、ELF格式文件 ELF文件:(Executable and Linkable Format) ELF文件出了包含编译出的二进制代码,还包含其他链接需要信息,ELF格式提供了相应代码/数据对应编译出的地址信息、文件信息等内容。 二、链接过程 链接就是把编译生成的目标文件和链接库处理成为相应ELF格式的映像文件(image),最终的文件可以写入嵌入式系统的ROM/FLASH中。映像文件中包含:分组信息和定位信息,亦即输出段/域及地址定位信息。链接器同时可以生成相应与域有关的符号来指示关于加载时地址、运行时地址、加载时长度限制、运行时长度限制等信息。同时链接器也具有优化的功能,删除不必要的代码、段域等。 1、映像文件的组成 一个映像文件包含一个或多个域;一个域包含一个或多个输出段;一个输出段包含一个或多个输入段;输入段中包含了目标文件的代码和数据。 输入段的内容:代码、已初始化的数据、未初始化的数据、初始化0的存储区域。 输出段和域中包含:RO、RW、ZI域。 2、映像文件的地址映射 加载时地址和运行时地址的区别:加载时地址是运行前的地址,简单理解在FLASH中固定存储即为加载时地址,而若代码载入RAM运行时,新的地址为运行时地址;而程序在FLASH中运行,加载时地址和运行时地址就一致了。当地址映射复杂时,可以通过scatter配置文件进行设置。 3、初始入口点和普通入口点 初始入口点:运行时的入口点,初始入口点必须位于映像文件运行时入口点,而它的加载时地址和运行时地址一致(称为固定域)。可以通过-entry指定映像文件的初始入口点。 普通入口点:用ENTRY伪操作定义在程序中,一般为中断服务程序的入口点。 4、scatter文件中包含的信息 加载时域描述、运行时域描述、输入段描述、输入段选择符;加载时域包括:名称、起始地址、属性、最大尺寸和一个运行时域;运行时域包括:名称、起始地址、属性、最大尺寸和一个输入段集合;输入段选择符描述了输入段名称或属性的匹配方式。 输入段属性:RO-CODE/CODE、RO_DATA/CONST、RO/TEXT包括前两项、RW_DATA、RW/DATA(RW_CODE+RW_DATA)、BSS、ZI;FIRST\LAST来指定运行时域的开头结尾,.ANY可以根据实际情况安排到合适的运行时域。 可以使用FIXED属性将域放置在ROM中固定位置,加载时域和固定时域即相同。 小知识: 1、程序断点 断点可以分为:软件断点和硬件断点,软件断点可以相应地址插入相应的指令实现,而硬件断点必须要需要相应硬件支持才能实现。 2、链接库的概念 链接库分为:静态链接库和动态链接库,而动态链接库又分为加载时动态链接库和运行时动态链接库;其差别:静态链接库的使用内容包含在生成的目标代码中,加载时动态链接库是程序载入内存时知道相应的动态链接库调用内容同时调入内存中,而运行时动态链接库只有在运行到需要调用时才调入使用。 3、JTAG Joint Test Action Group?

    时间:2018-05-21 关键词: ARM 程序

  • 关于存储器的一些基础知识整理

     RAM Random-Access-Memory,随机存储存储器,可读可写,分为SRAM和DRAM,即静态随机存储器和动态随机存储器,理解上静动态主要体现是否需要刷新,通常DRAM需要刷新,否则数据将丢失;SRAM的效率较好,而成本较高,通常将SRAM作为cache使用。 PSRAM Pseudo static random access memory,伪SRAM伪随机存储器,内部和DRAM相似,接口和SRAM相似,具有自刷新功能,不需要外部刷新。而其成本介于SRAM与DRAM之间。 单\双端口RAM 单端口RAM同一时刻,只能满足读或写某一动作,而双端口RAM存在两套独立的地址、数据、读写控制等,可以同时进行两个操作,当然为避免冲突,存在一定的仲裁控制,成本也更高。伪双口RAM是只有两访问接口,单一个端口只读,另一个端口只能写。 ROM Read-Only-Memory,只读存储器,通常使用时一次写好,使用时只能进行读操作,而不能进行写操作。 CACHE 高速缓冲存储器,由于存储器DDR/DRAM等相对于处理器访问速度较慢,增加的一级缓冲存储空间,当需要处理器需要访问内存某一块区域时,先缓存cache中,处理器访问cache速度较快;但同时也需要增加处理DDR和CACHE中数据同步、替换等问题。 TCM Tightly-Coupled-Memory 紧密耦合(链接)的存储器,是指和处理器链接紧密,基本可以看做和CACHE同一等级连接的存储空间(印象中ARM结构上和L2 CACHE同一层次),其存储空间的内容不会像CACHE处理一样经常替换。 EEPROM Electrically Erasable Programmable read only memory电可擦可编程只读存储器,掉电非易失的存储芯片,在特殊高电压模式下可以插写,普通模式下只读ROM。 FLASH 闪存,和EEPROM一样可擦除可重写,差别EEPROM总是按字节操作,FLASH可以按照字节块擦除。FLASH有分Nand-Flash、Nor-Flash,Nor-flash可以按照字节读取,而NandFlash只能按块读取,两者同样可以按照字节块擦除。Nor-Flash需要支持随机读取的地址、数据线,成本比Nand-Flash高,而其可擦写次数低于NAND FLASH,一般嵌入系统中刚boot需要初始化的代码需要放置在Nor-Flash中。 对于FLASH的读取总线可以有I2C、SPI串行型,也可以采用并行Parallel;同样Flash可以和处理器集成在一起或是通过总线外部访问。 eMMC embedded multi media card,集成了NAND FLASH和控制部分的集成电路,提供像SD、TF(trans-flash)卡一样的使用接口。 硬盘 传统硬盘采用磁材料作为存储介质,固态硬盘使用FLASH,访问速度性能较好。

    时间:2018-05-21 关键词: 存储器 单片机

  • 嵌入式C代码优化

     之前刚开始工作时,参与做过嵌入式代码优化,除了最基本的函数实现细节算法优化外,还有一些细节的处理。当然之前优化时,也是借助了分析工具来分析哪些函数调用频繁,哪些开销比较大。对于具体细节的处理记得不一定全面了,当然也有部分操作在编译时,工具也有可能自动进行优化。 函数展开 类似inline,减少函数出栈入栈开销 结构体比较 相应数据结构具有不同的比特位含义,而高位更具有意义,比较时无需将成员一一比较,强转32/64位格式比较。 相同操作提取 提取相同深度的指针指向,如下A、C、D为指针,E为具体成员操作。 A->C->D->E1,A->C->D->E2,A->C->D->E3... ...则可将A1 = A->C->D,然后使用A1->E1、A1->E2、A1->E3进行替换操作。 消息合并 线程之前多个消息发送会导致开销变大,可以合并成单个消息,同时处理多个事情,当然前提是这些事件可进行合并。 时间空间的转换(动态申请用静态变量替代) 频繁使用的消息可以改为一次性申请或是静态方式,以减少频繁申请释放的开销;而同一逻辑部分可能申请多次的情况,可以采用半静态半动态的方式,可以通过统计经常同时使用的次数来确定静态内存的大小。 寄存器(变量定义)问题 在arm上汇编可以看到当定义uint8 i;for(i = 1; i< 255; i++)时寄存器为32bit,所以在处理8bit数据时,需要额外的移位等操作来放置溢出超过8位情况,此时使用uint32定义反而可以减少MIPS开销。

    时间:2018-05-21 关键词: 嵌入式 C语言

  • 教你给51单片机扩展片外RAM

     上一文中扩展了单片机的程序存储器,4KB存储空间提升到64KB。其实,4K的代码空间还凑合,但是51自带的256B数据存储空间使用起来还真紧张,其中留给用户的连128B都不到,所以不得不扩展片外RAM。扩展RAM方法和扩展ROM差不多,都是占用P0/P2口做地址线,同时P0用锁存器74373分时复用地址和数据信号。 以前扩展RAM是用汇编语言访问存储器,好处是定位精准,指哪打哪,坏处就是:程序规模一大就有点难维护了,所以还得改用C实现。 对应于汇编语言用R1,R0/DPTR访问外部RAM,keil C扩展了存储类型,增加了如pdata(等同用movx @Rn访问方式)/xdata(等同于movx @DPTR访问方式)存储类型用于访问片外ram。同时,还提供了绝对地址访问的宏,如PBYTE/XBYTE,查看定义: [cpp] view plain copy#define PBYTE((unsigned char volatile pdata*)0); #define XBYTE((unsigned char volatile xdata*)0); 其实,也还是定义相应存储类型的指针~。 上仿真图和代码前,整理一下keil c提供访问绝对地址的方法: 1._at_定义变量: 变量类型 [存储类型] 变量名 _at_ 常数,指定变量存放在常数所指定的ram位置,注意bit型变量不能使用_at_指定位置; 2.绝对地址访问: 头文件absacc.h提供了绝对地址访问宏,用于字节/字寻址,如 val=XBYTE[0x0000];读片外ram 0x00处内存值 XBYTE[0x0200]=val;写片外ram 0x0200处内存值 扩展ram仿真图: c语言版本: 最后看下程序运行后6264内部存储的数据: 后记: 本来想自己扩展ram的,可是ram芯片属于高速信号,自己布板就省了,还是玩泥巴去了。。。

    时间:2018-05-21 关键词: 51单片机 RAM

  • 51单片机内存扩展:从片内ROM跳转到片外ROM

    源于一年前想自己动手给51写个OS,编译选Large模式,调试时整个流程都跑的好好的,可是烧写到片上后得不到预期的效果,后来查书才知道51单片机片上只有4KRom,如果没有扩展片外Rom,当访问4K以外的程序空间,程序指针又会回到最开始执行。参考手册扩展片外Rom后,能访问达64K的程序空间。网上能搜索到的扩展方式都是将EA引脚接地,让MCU上电后从外部ROM开始执行。但查看芯片手册,明明说EA为高时,程序从片内ROM执行,当执行到0x1000以上地址时(标准51单片机),会跳转到片外ROM执行。按网上的做法,为了扩展个片外ROM,片内的基本ROM都不用了,有点浪费了,于是开始找资料如何从片内跳转到片外执行。 射人先射马,发帖先上图,仿真图如下: 此处EA脚没有接地。如果想简单粗暴的加电时从片外ROM执行,EA引脚接地,双击U2(27C64)Image File选Hex然后就可以了,这不是本文的重点,略过,后面可能会写到。 跳转,最简单的方式用LJMP,当然也可以用把跳转地址压入栈,然后ret过去,不过这种方式我没尝试成功。 考虑到汇编写代码太苦逼,写规模大一点的代码还得靠C,因此程序的效果是:main函数在片内执行,流水灯代码存放在片外Rom,main函数跳转到流水灯中执行。 因为是一种尝试,所以从写汇编代码开始(加载地址容易控制:ORG指定即可) 1)用汇编代码跳转: AT89C51中的代码: ORG 0000H LJMP 1000H END ##################### 27C64中代码: ORG 1000H STAR: MOV A,#0AAH MOV P1,A MOV A,#55H MOV P1,A SJMP STAR END 程序运行起来后,PC寄存器指向0x0000处的LJMP 0x1000,然后跳到27C64处执行。起初,在27C64 0x0000处搜索编码,没找到,查阅手册后知,当PC超过0FFFH时,会转向片外程序存储空间1000H-FFFFH执行程序。 [27C64处的内容] 2)用C代码跳转: #include int main() { int i=0; i++; /* 执行一些初始化逻辑,或者接受交互内容,按不同的输入,跳转到片外ROM */ #pragma asm LJMP 0x1000 #pragma endasm while(1); } C代码中嵌入汇编,做跳转。 这个连接中有相关的设置 http://bbs.ednchina.com/BLOG_ARTICLE_1721.HTM 如果不做设置,连接时会有警告找不到C_STARTUP,也不会运行到代码中。 调试运行,由于KEIL C加了启动代码,在protues仿真时有一长段麻烦的初始化堆栈的过程,因为没有源码,连设置断点都不行,只能按着F11傻等着。最终当然也是能跳转到片外ROM执行的。 3)片外ROM存放由KEIL C编写的HEX文件 这个摸索了很久才摸索出来!代码如下: #include int main() { while(1) { P1 = 0x33; P1 = 0xcc; } } 首先,由于KEIL C创建的新工程会添加启动代码(startup.a51),这个前面说过用来初始化C语言运行的堆栈。因为我的程序是从片内ROM跳转过来运行的,至少已经被初始化了一次,再初始化一次,原本保留的变量全没了,因此在创建工程的时候,跳过添加startup.a51这个文件。带来的不便是:程序没有C环境,想要在调试是不可能了。 hex文件是生成了,加载,但是从片内ROM跳转过来后,P1口的内容不是0x33/0xCC而是上一次运行时的0x55/0xAA,why?代码写错了? 查看27C64的内存印象: 0x0000H的内容是: 75 90 33和75 90 CC是往P1端口写入0x33/0xCC---就是现在的代码 再查看0x1000H的内容: 74 AA对应MOV A,#0AAH,F5 90 对应MOV 90,A,明显是上次仿真时的结果! 好吧,现在得想办法把代码加载到0x1000的位置,ORG是用不上了,得用其他办法。 在我的另一篇文章 中提到,INTEL HEX文件格式中每个规则开始处都有地址,那好先看看这段代码的地址: :08000F007590337590CC80F868 :03000000020003F8 :0C000300787FE4F6D8FD75810702000F3D :00000001FF 080000F007 08是这行的长度8字节,后面的0000是这行加载位置,从0x0000开始。shit,难怪加载补上。先手动修改地址,修改玩以后,protues提示HEX校验码不对,仿真失败。无奈,只能想其他办法了。加载地址一般是由连接器在连接阶段确定的(<程序员的自我修养>一书中有提到),既然这样,看看keil c在链接时有没有什么参数可以设置: BL51是KEIL C的连接器,Code这个位置好像是,那就试试填入0x1000,然后再编译连接: :08100C007590337590CC80F85B :03000000021000EB :0C100000787FE4F6D8FD75810702100C23 :00000001FF 这次生成的HEX文件,链接地址部分已经被改为0x100C。再仿真一次,不过这次仿真前要把片内ROM的跳转地址改为LJMP 0x1003,要不然指不准执行了非法指令。 27C64 0x100C处的内容75 90 33对应汇编语句 MOV 90,#33H 75 90 CC对应汇编语句MOV 90,#0CCH这正是c代码的内容,而且P1口的内容也是CC。   至此,从片内ROM跳转到片外ROM结束。另外估计ISP烧写器可能也是类似的工作原理

    时间:2018-05-21 关键词: 51单片机 内存扩展

  • 你知道单片机的片内存储器片外存储器都是干什么的吗?

    单片机的分为数据存储器和程序存储器。单片机内部的存储器称为片内存储器,片外扩展的存储器成为片外存储器。比如8031内部有数据存储器而没有程序存储器,所以它一般要外接一块程序存储芯片,内部的数据存储器叫做9031的片内存储器,外部扩展的存储芯片叫做片外存储器。 早期,片内存储器,还是片外存储器,确实是根据:他们是不是 在同一块 集成电路芯片上,来区分的。 数据存储器的传送指令,也有区别:片内传送,使用MOV,涉及片外了,就要用MOVX指令。 但是,科技发展了,有些单片机芯片,在同一块芯片上,还集成了少量的“片外存储器”,针对这些存储单元操作,就必须使用MOVX指令。 这样看来,片内,还是片外,区分的方法应该是使用什么指令,而不是他们是否分离成两块芯片。

    时间:2018-05-21 关键词: 存储器 单片机

  • 单片机C51位运算应用技巧

     位运算应用口诀: 清零取位要用与,某位置一可用或,若要取反和交换,轻轻松松用异或! 移位运算要点 1 它们都是双目运算符,两个运算分量都是整形,结果也是整形。 2 "<<" 左移:右边空出的位上补0,左边的位将从字头挤掉,其值相当于乘2。 3 ">>"右移:右边的位被挤掉。对于左边移出的空位,如果是正数则空位补0,若为负数,可能补0或补1,这取决于所用的计算机系统。 4 ">>>"运算符,右边的位被挤掉,对于左边移出的空位一概补上0。 位运算符的应用 (源操作数s 掩码mask) (1) 按位与-- & 1 清零特定位 (mask中特定位置0,其它位为1,s=s&mask) 2 取某数中指定位 (mask中特定位置1,其它位为0,s=s&mask) (2) 按位或-- | 常用来将源操作数某些位置1,其它位不变。 (mask中特定位置1,其它位为0 s=s|mask) (3) 位异或-- ^ 1 使特定位的值取反 (mask中特定位置1,其它位为0 s=s^mask) 2 不引入第三变量,交换两个变量的值 (设 a=a1,b=b1) 目标 操作 操作后状态 a=a1^b1 a=a^b a=a1^b1,b=b b=a1^b1^b1 b=a^b a=a1^b1,b=a a=b1^a1^a1 a=a^b a=b1,b=a 二进制补码运算公式: -x = ~x + 1 = ~(x-1) ~x = -x- -(~x) = x+ ~(-x) = x- x+y = x - ~y - 1 = (x|y)+(x&y) x-y = x + ~y + 1 = (x|~y)-(~x&y) x^y = (x|y)-(x&y) x|y = (x&~y)+y x&y = (~x|y)-~x x==y: ~(x-y|y-x) x!=y: x-y|y-x x< y: (x-y)^((x^y)&((x-y)^x)) x<=y: (x|~y)&((x^y)|~(y-x)) x< y: (~x&y)|((~x|y)&(x-y))//无符号x,y比较 x<=y: (~x|y)&((x^y)|~(y-x))//无符号x,y比较 应用举例 (1) 判断int型变量a是奇数还是偶数 a&1 = 0 偶数 a&1 = 1 奇数 (2) 取int型变量a的第k位 (k=0,1,2……sizeof(int)),即a>>k& (3) 将int型变量a的第k位清0,即a=a&~(1< (4) 将int型变量a的第k位置1, 即a=a|(1< (5) int型变量循环左移k次,即a=a< (6) int型变量a循环右移k次,即a=a>>k|a<<16-k (设sizeof(int)=16) (7)整数的平均值 对于两个整数x,y,如果用 (x+y)/2 求平均值,会产生溢出,因为 x+y 可能会大于INT_MAX,但是我们知道它们的平均值是肯定不会溢出的,我们用如下算法: int average(int x, int y) //返回X,Y 的平均值 { return (x&y)+((x^y)>>1); } (8)判断一个整数是不是2的幂,对于一个数 x >= 0,判断他是不是2的幂 boolean power2(int x) { return ((x&(x-1))==0)&&(x!=0); } (9)不用temp交换两个整数 void swap(int x , int y) { x ^= y; y ^= x; x ^= y; } (10)计算绝对值 int abs( int x ) { int y ; y = x >> 31 ; return (x^y)-y ; //or: (x+y)^y } (11)取模运算转化成位运算 (在不产生溢出的情况下):a % (2^n) 等价于 a & (2^n - 1) (12)乘法运算转化成位运算 (在不产生溢出的情况下):a * (2^n) 等价于 a<< n (13)除法运算转化成位运算 (在不产生溢出的情况下):a / (2^n) 等价于 a>> n 例: 12/8 == 12>>3 (14) a % 2 等价于 a & 1 (15) if (x == a) x= b; else x= a; 等价于 x= a ^ b ^ x; (16) x 的 相反数表示为 (~x+1) (17) 实现最低n位为1,其余位为0的位串信息:~(~0 << n) (18)截取变量x自p位开始的右边n位的信息:(x >> (1+p-n)) & ~(~0 << n) (19)截取old变量第row位,并将该位信息装配到变量new的第15-k位:new |= ((old >> row) & 1) << (15 – k) (20)设s不等于全0,代码寻找最右边为1的位的序号j: for(j = 0; ((1 << j) & s) == 0; j++) ;

    时间:2018-05-16 关键词: C51 单片机

  • 单片机c51和一般的c语言有何不同之处?

     c语言和c51大部分的地方都是相同的,他们的语句,结构,顺序都是很相似的,只是c51相比与c语言,多了很多变量类型和其他的东西,下面是总结c语言和c51的一些不同之处。 变量类型 位变量声明 bit c51中特有的一种变量声明,bit变量位域只有0和1,长度也只有1 存储类型 很多不管学过还是没学过c语言对于这个词都会有一些陌生,其实我们在学习c语言的时候接触过这个东西,在c语言里面,存储结构有四种,分别是auto,static,extern,register这四种,这里不再一一说明,下面讲一下c51里面的集中存储结构。 data型,直接寻址片内数据存储区,访问速度快,128字节 bdata型,可以位寻址片内数据存储区,允许位于字节混合访问16字节 idata型,可以间接被片内数据存储区访问,可以访问片内所有RAM空间,256字节 pdata型,分页寻址片外数据存储区,有MOVX@RI访问,256字节 xdata型,寻址片外数据存储区,由movx@dptr访问,64k字节 code型,寻址代码存储区,由movx@dptr访问,64k字节 存储模式 这个我实在是不懂,看说明都看不明白,直接放图吧。

    时间:2018-05-16 关键词: C51 单片机 C语言

  • KEIL C51之绝对地址定位详解

     单片机空间分配看*.M51文件,ARM,DSP空间分配看*.map文件 1、函数定位: 假如要把C源文件 tools.c 中的函数 int BIN2HEX(int xx) { ... } 放在CODE MEMORY的0x1000处,先编译该工程,然后打开该工程的M51文件,在 * * * C O D E M E M O R Y * * * 行下找出要定位的函数的名称,应该形如: CODE xxxxH xxxxH UNIT ?PR?_BCD2HEX?TOOLS 然后在: Project->Options for Target ...->BL51 Locate:Code 中填写如下内容: ?PR?_BCD2HEX?TOOLS(0x1000) 再次Build,在M51中会发现该函数已放在CODE MEMORY的0x1000处了 2.赋初值的变量定位 要将某变量定位在一绝对位置且要赋初值,此时用 _at_ 不能完成,则如下操作: 在工程中建立一个新的文件,如InitVars.c,在其中对要处理的变量赋初值(假设是code变 量): char code myVer = {"COPYRIGHT 2001-11"}; 然后将该文件加入工程,编译,打开M51文件,若定义的是code型,则在 * * * C O D E M E M O R Y * * * 下可找到: CODE xxxxH xxxxH UNIT ?CO?INITVARS 然后在: Project->Options for Target ...->BL51 Locate:Code 中填入: ?CO?INITVARS(0x200) 再次编译即可。 相应地,如为xdata变量,则InitVars.c中写: char xdata myVer = {"COPYRIGHT 2001-11"}; 然后将该文件加入工程,编译,打开M51文件,在 * * * X D A T A M E M O R Y * * * 下可找到: XDATA xxxxH xxxxH UNIT ?XD?INITVARS 然后在: Project->Options for Target ...->BL51 Locate:Xdata 中填入: ?XD?INITVARS(0x200) 再次编译即可。相应地,若定义的是data/idata等变量,则相应处理即可。 3、若有多个变量或函数要进行绝对地址定位,则应按地址从低到高的顺序 使用KeilC51软件,可以很方便地将代码或者数据绝对定位到某个地址。 1、代码定位: 方法1:使用伪指令CSEG。比如要将MyFunc1定位到代码区C:0x1000,则新建一个A51文件,添加以下内容: PUBLIC MYFUNC1 CSEG AT 1000H MYFUNC1: ;其它代码 RET 在其它源文件中,就可以调用MyFunc()函数了。需要注意的是,编译器不检测传递参数的数目,仅检测函数是否有返回值。 方法2:使用BL51 Locate选项。比如在main.c中定义了一个MyFunc2函数,并且要将该函数定位到代码区C:0x2000,则从菜单中选择Project->Options for Target 'Target1',在弹出的对话框中选择BL51 Locate页,在下面的code栏中写上?PR?MYFUNC2?MAIN(0x2000)即可。 如果想定位多个函数,也可以使用*通配符。 2、变量定位: 只有全局变量可以绝对定位,局部变量无法实现绝对定位。 方法1:使用_at_关键字。声明一个全局变量unsigned char data MyBuf1[8] _at_ 0x20; 方法2:使用BL51 Locate选项。比如将main.c中定义的所有data型的全局变量定位到数据区D:0x28开始的空间,则从菜单中 选择Project->Options for Target 'Target1',在弹出的对话框中选择BL51 Locate页,在下面的data栏中写上?DT?MAIN(0x28)即可。 如果是idata,则使用?ID?MAIN(0x28),如果是xdata,则使用?XD?MAIN(0x28),如果是pdata,则使用?PD?MAIN(0x28) 3、堆栈定位: 在STARTUP.A51文件中定义了堆栈区?STACK,其起始地址同样可以在BL51 Locate页中设置,在Stack栏写上?STACK(0x80) 还可以通过汇编实现 // my.a51 public my_flash_var cseg at 0F100H my_flash_var: db 55h end 然后C声明 // flash.c extern unsigned char code my_flash_var; BL51 locate 选项卡中 code range 和 xdata range如果不填写,编译默认将程序中相应代码和变量从空间前面取起 网上看到有人提到在keil中使用_at_进行绝对地址定位问题,我简单介绍一下它的用法。 使用_at_关键字对存储器进行绝对地址定位程序如下 #i nclude char xdata LED_Data[50] _at_ 0x8000; main() { LED_Data[0] = 0x23; } 在keil中运行以上程序可以在存储器窗口中输入 x:0x8000 可以看到0x8000地址中的值为0x23. 值得指出的几点是 1.在给变量LED_Data[50]定位绝对地址空间时,不能对其赋初值。 2.char xdata LED_Data[50] _at_ 0x8000;这条语句不能主函数中。有些网友提到在按着keil说明中用_at_进行绝对地址定位时,编译会出现错误274,就是将这条语句放在主函数中的原因。 3.keil中地址是自动分配的,所以除非特殊情况否则不提倡使用绝对地址定位。初学者因帖别注意。不要把c当作汇编使用。 对需要/RST复位后要保持变量不变,防止意外改变(比如升级到新程序,变量地址可能被编译器优化到其他地方),比较有用!!!!

    时间:2018-05-16 关键词: C51 keil

  • 单片机C51中的NOP指令使用经验

     方法1: 在keil C51中,直接调用库函数: #include // 声明了void _nop_(void); _nop_(); // 产生一条NOP指令 作用:对于延时很短的,要求在us级的,采用“_nop_”函数,这个函数相当汇编NOP指令,延时几微秒。NOP指令为单周期指令,可由晶振频率算出延时时间,对于12M晶振,延时1uS。对于延时比较长的,要求在大于10us,采用C51中的循环语句来实现。 方法2: 插入方式: __asm //是两个下划线 { nop; }

    时间:2018-05-16 关键词: C51 单片机

  • stdarg的用法(可变参数的用法)

     stdarg宏: 可变参数列表是通过宏来实现的,这些宏定义于stdarg.h头文件,它是标准库的一部分。 这个头文件声明的一个va_list的类型,和三个宏va_start,va_arg,va_end。我们可以生明一个va_list类型的变量,配合三个宏使用。 va_start(arg, last have name arg); 初始化之后,arg将指向第一个无名参数。 va_arg(arg, next arg type); va_arg将会返回当前的arg的va_list变量所指向的无名变量。并使它指向下一个无名变量。 注意,当访问所有变量之后记得调用va_end(arg); 来释放这个va_list类型的变量。 #include int nsum(int n,...) { va_list num; // va_list 是一个宏定义类型 int sum=0; va_start(num,n); //开始取参,是num指向第一个参数 for(;n>1;n--) { sum += va_arg(num,int); // 这个函数返回当前指向的参数,并指向下一个参数 } va_end(num); //用完释放 return sum; }

    时间:2018-05-14 关键词: C语言 stdarg

  • c标准文件io函数的原型和注意点

     fopen() 需要头文件:#include 函数原型:FILE *fopen(const char *path,const char *mode) 函数参数:path:要打开的文件的路径及文件名 mode:文件打开方式,见下 函数返回值:成功:指向文件的FILE类型指针 失败:NULL 以下是mode参数允许使用的取值及说明: r或rb 以只读的方式打开文件,该文件必须存在 r+或r+b 以可读可写的方式打开文件,该文件必须存在 w或wb 以只写的方式打开文件,若文件不存在则创建该文件;若文件存在则擦除文件原始内容,从文件开头开始操作文件 w+或w+b 以可读可写的方式打开文件,若文件不存在则创建该文件;若文件存在则擦除文件原始内容,从文件开头开始操作文件 a或ab 以附加的方式打开只写文件,若文件不存在则创建该文件;若文件存在,写入的数据追加在文件尾,即文件的原始内容会被保留 a+或a+b 以附加的方式打开可读可写文件,若文件不存在则创建该文件;若文件存在,写入的数据追加在文件尾,即文件的原始内容会被保留 } gets()、fgets() 需要头文件:#include 函数原型:char *gets(char *s) char *fgets(char *s,int size,FILE *stream) 函数功能: 假设buff长度为MAX;那么使fgetc停止的方式就有两种: 1. 当读到/n时,就把/n的ascii写入buff便停止,在后面补上一个'\0'; 2. 当读到 size-1 个字符都没读到/n时那么就结束,后面补上一个'\0'; 函数参数:s:存放输入字符的缓冲区地址 size:输入的字符串长度 stream:输入文件流 函数返回值: 成功:s 失败或读到文件尾:NULL puts()、fputs() 需要头文件:#include 函数原型:int puts(const char *s) int fputs(const char *s,FILE *stream) 函数参数:s:存放输出字符的缓冲区地址 stream:输出文件流 函数返回值:成功:非负数 失败:EOF scanf()、fscanf()、sscanf() 需要头文件:#include 函数原型:int scanf(const char *format,...); int fscnaf(FILE *fp,const char *format,...); int sscanf(char *buf,const char *format,...); 函数参数:format:输入的格式 fp:待输入的流 buf:待输入的缓冲区 函数返回值:成功:读到的数据个数 失败:EOF printf()、fprintf()、sprintf() 需要头文件:#include 函数原型:int printf(const char *format,...); int fprintf(FILE *fp,const char *format,...); int sprintf(char *buf,const char *format,...); 函数参数:format:输出的格式 fp:待输出的流 buf:待输出的缓冲区 函数返回值:成功:输出的字符数 失败:EOF fread() 需要头文件:#include 函数原型:size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 函数参数:ptr:存放读入数据的缓冲区 size:读取的每个数据项的大小(单位字节) nmemb:读取的数据个数 stream:要读取的流 函数返回值: 成功:实际读到的nmemb数目 失败:0 fwrite() 需要头文件:#include 函数原型:size_t fwrite(void *ptr,size_t size,size_t nmemb,FILE *stream); 函数参数:ptr:存放写入数据的缓冲区 size:写入的每个数据项的大小(单位字节) nmemb:写入的数据个数 stream:要写入的流 函数返回值: 成功:实际写入的nmemb数目 失败:0 feof()/ferror()可以函数判断是因为读操作错误哈市读到文件尾部。 在打开流的时候,偏移位置为0(即文件开头) fseek() 需要头文件:#include 函数原型:int fseek(FILE *stream,long offset,int whence); 函数参数:stream:要定位的流 offset:相对于基准点whence的偏移量 whence:基准点(取值见下) 函数返回值:成功:0,改变读写位置 失败:EOF,不改变读写位置 其中第三个参数whence的取值如下: SEEK_SET:代表文件起始位置,数字表示为0 SEEK_CUR:代表文件当前的读写位置,数字表示为1 SEEK_END:代表文件结束位置,数字表示为2 函数ftell() 需要头文件:#include 函数原型:int ftell(FILE *stream); 函数参数:stream:要定位的流 函数返回值:成功:返回当前的读写位置 失败:EOF

    时间:2018-05-14 关键词: C语言 io函数

  • Keil C51重定向printf到串口

     概述 进行C/C++开发的时候我们都会需要打印调试信息,打印调试信息时我们习惯使用printf函数,但是在Keil C51环境下,由于我们的程序是下载到单片机里,使用printf函数时不能直接打印到串口上,这个时候就需要我们对printf函数输出重定向。 重定向 重定向printf很简单,我们知道,printf函数是调用putchar实现字符数据传送的。我们只要重写putchar函数,就可以对printf进输出重定向。 代码清单 下面是自己在Keil 5环境下,使用单片机STC12测试printf重定向功能的代码清单 #include #include //UART1 初始化 void Uart1Init(void) //115200bps@11.0592MHz { PCON &= 0x7F; //波特率不倍速 SCON = 0x50; //8位数据,可变波特率 AUXR |= 0x04; //1T模式 BRT = 0xFD; //设置独立波特率发生器重装值 AUXR |=0X01; //串口1选择独立发生器为波特率发生器 AUXR |=0X10; //启动独立波特率发生器 ES = 1; //使能串口1中断 } //UART1 发送串口数据 void UART1_SendData(char dat) { ES=0; //关串口中断 SBUF=dat; while(TI!=1); //等待发送成功 TI=0; //清除发送中断标志 ES=1; //开串口中断 } //UART1 发送字符串 void UART1_SendString(char *s) { while(*s)//检测字符串结束符 { UART1_SendData(*s++);//发送当前字符 } } //重写putchar函数 char putchar(char c) { UART1_SendData(c); return c; } void main(void) { Uart1Init(); UART1_SendString("Hello World!\r\n"); printf("printf Test!\r\n"); printf("Complie Time:%s\r\n", __TIME__); while(1) { } }12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152 打开串口把波特率调成115200bps,可以看到串口打印如下信息: Hello World! printf Test! Complie Time:11:12:36

    时间:2018-05-14 关键词: C51 keil

  • 格式化io与非格式化io的对比理解

     测试函数1 #include #include int main() { int a[5]={65,66,67,68}; char value[10],i; FILE *pf; if(!(pf = fopen("printf.txt","w+"))) { perror("open file:"); return -1; } fprintf(pf,"%d %c %d %c",a[0],a[1],a[2],a[3]); sprintf(value,"%d %c %d %c",a[0],a[1],a[2],a[3]); puts(value); printf("%d",strlen(value)); fclose(pf); return 0; }123456789101112131415161718192021222324 这个程序的输出是: 65 B 67 D 9 从这个程序发现scanf家族对这个函数所作的事情就是将所有格式的变量以 一个文本的形式输出到我们指定的地方,不论是数组还是文件; sprintf(value,”%d %c %d %c”,a[0],a[1],a[2],a[3]); 这个调用, 即先读到一个%d,对应a[0]是65, 就在value[0]放入6的ascii码然后在value[1]存入5的ascii码,下一个是空格就在value[2]保存空格的ascii 下一个是%c匹配a[2]就把a[2]的值直接当作一个ascii码存入value[3],以此类推…. 所以最后得到字符串长度为9 测试程序2 #include int main() { FILE *fp; int a; char b,c[100]; if(!(fp = fopen("scanf.txt","r+"))) { perror("opend file:"); return -1; } fscanf(fp,"%d %c %s",&a,&b,c); printf("%d %d %d\n",(int)a,(int)b,(int)*c); fclose(fp); return 0; }12345678910111213141516171819202122 scanf.txt的内容是: 123 a abc 输出是: 123 97 97 scanf家族的原理也和printf相似:第一个参数类型是%d即读入一个整形赋值给a; 用一个空格分开123和a 就是为了让程序了解空格前1,2,3这三个字符都属于变量a; 所以a的值是经过: (‘1’-‘0’) * 100 * +(‘2’-‘0’) * 10 + (‘3’-‘0’) * 1得到的; 得出结论: 理解二进制保存的方式和文本保存的方式: 在linux下:所有的文件都是按文本的方式存储和读取的,所以所有的文件打开的时候默认为是文本文件,当我们保存一些数字进入文档时,只能按字节翻译数字为对应的符号,这就是为什么打开一个可执行文件时会出现乱码的情况.因为可执行文件都是一些二进制机器码. 文件存储是一个个字节存储的,而且存储的都是一个二进制数,scanf和print只是用了字符数字互转的方法对文件或者数组元素 进行取放的. 对于格式化输入输出函数 文件存储是一个个字节存储的,而且存储的都是一个二进制数,scanf和print只是用了字符数字互转的方法对文件或者数组元素 进行取放的. 即:scanf按格式把 ascii码->所需格式保存至变量. printf按格式把 某些格式变量->ascii码 方便文本软件按ascii码显示内容 至于非格式化io,即不格式化直接将数据原封不动一个个字节输出输入; fgetc和getchar;fputc和putchar:即一个个字节输入输出; fgets和gets;fputs和puts:即一次一行’\n’一个个字节输入输出; fread和fwrite:即一次可控字节数输入输出;(直接忽略变量类型直接对内存按字节操作)

    时间:2018-05-14 关键词: C语言 格式化

  • C语言可变参数函数的使用方法讲解

     本文主要介绍可变参数的函数使用,然后分析它的原理,程序员自己如何对它们实现和封装,最后是可能会出现的问题和避免措施。 VA函数(variable argument function),参数个数可变函数,又称可变参数函数。C/C++编程中,系统提供给编程人员的va函数很少。*printf()/*scanf()系列函数,用于输入输出时格式化字符串;exec*()系列函数,用于在程序中执行外部文件(main(int argc,char*argv[]算不算呢,与其说main()也是一个可变参数函数,倒不如说它是exec*()经过封装后的具备特殊功能和意义的函数,至少在原理这一级上有 很多相似之处)。由于参数个数的不确定,使va函数具有很大的灵活性,易用性,对没有使用过可变参数函数的编程人员很有诱惑力;那么,该如何编写自己的va函数,va函数的运用时机、编译实现又是如何。作者借本文谈谈自己关于va函数的一些浅见。 一、 从printf()开始 从大家都很熟悉的格式化字符串函数开始介绍可变参数函数。 原型:int printf(const char * format, ...); 参数format表示如何来格式字符串的指令,… 表示可选参数,调用时传递给"..."的参数可有可无,根据实际情况而定。 系统提供了vprintf系列格式化字符串的函数,用于编程人员封装自己的I/O函数。 int vprintf / vscanf(const char * format, va_list ap); // 从标准输入/输出格式化字符串 int vfprintf / vfsacanf(FILE * stream, const char * format, va_list ap); // 从文件流 int vsprintf / vsscanf(char * s, const char * format, va_list ap); // 从字符串 // 例1:格式化到一个文件流,可用于日志文件 FILE *logfile; int WriteLog(const char * format, ...) { va_list arg_ptr; va_start(arg_ptr, format); int nWrittenBytes = vfprintf(logfile, format, arg_ptr); va_end(arg_ptr); return nWrittenBytes; } … // 调用时,与使用printf()没有区别。 WriteLog("%04d-%02d-%02d %02d:%02d:%02d %s/%04d logged out.", nYear, nMonth, nDay, nHour, nMinute, szUserName, nUserID); 同理,也可以从文件中执行格式化输入;或者对标准输入输出,字符串执行格式化。在上面的例1中,WriteLog()函数可以接受参数个数可变的输入,本质上,它的实现需要vprintf()的支持。如何真正实现属于自己的可变参数函数,包括控制每一个传入的可选参数。 二、 va函数的定义和va宏 C语言支持va函数,作为C语言的扩展--C++同样支持va函数,但在C++中并不推荐使用,C++引入的多态性同样可以实现参数个数可变的函数。不过,C++的重载功能毕竟只能是有限多个可以预见的参数个数。比较而言,C中的va函数则可以定义无穷多个相当于C++的重载函数,这方面C++是无能为力的。va函数的优势表现在使用的方便性和易用性上,可以使代码更简洁。C编译器为了统一在不同的硬件架构、硬件平台上的实现,和增加代码的可移植性,提供了一系列宏来屏蔽硬件环境不同带来的差异。 ANSI C标准下,va的宏定义在stdarg.h中,它们有:va_list,va_start(),va_arg(),va_end()。 // 例2:求任意个自然数的平方和: int SqSum(int n1, ...) { va_list arg_ptr; int nSqSum = 0, n = n1; va_start(arg_ptr, n1); while (n > 0) { nSqSum += (n * n); n = va_arg(arg_ptr, int); } va_end(arg_ptr); return nSqSum; } // 调用时 int nSqSum = SqSum(7, 2, 7, 11, -1); 可变参数函数的原型声明格式为: type VAFunction(type arg1, type arg2, … ); 参数可以分为两部分:个数确定的固定参数和个数可变的可选参数。函数至少需要一个固定参数,固定参数的声明和普通函数一样;可选参数由于个数不确定,声明时用"…"表示。固定参数和可选参数公同构成一个函数的参数列表。 借助上面这个简单的例2,来看看各个va_xxx的作用: va_list arg_ptr:定义一个指向个数可变的参数列表指针; va_start(arg_ptr, argN):使参数列表指针arg_ptr指向函数参数列表中的第一个可选参数,说明:argN是位于第一个可选参数之前的固定参数,(或者说,最后一个固定参数;…之前的一个参数),函数参数列表中参数在内存中的顺序与函数声明时的顺序是一致的。如果有一va函数的声明是void va_test(char a, char b, char c, …),则它的固定参数依次是a,b,c,最后一个固定参数argN为c,因此就是 va_start(arg_ptr, c)。 va_arg(arg_ptr, type):返回参数列表中指针arg_ptr所指的参数,返回类型为type,并使指针arg_ptr指向参数列表中下一个参数。 va_copy(dest, src):dest,src的类型都是va_list,va_copy()用于复制参数列表指针,将dest初始化为src。 va_end(arg_ptr):清空参数列表,并置参数指针arg_ptr无效。 说明:指针arg_ptr被置无效后,可以通过调用va_start()、va_copy()恢复arg_ptr。每次调用va_st art() / va_copy()后,必须得有相应的va_end()与之匹配。参数指针可以在参数列表中随意地来回移动,但必须在va_start() … va_end()之内。

    时间:2018-05-14 关键词: C语言 函数

发布文章