嵌入式开发常用的C语言技巧
函数指针的巧妙使用
我们在平时的开发过程中,通常用指针来指向整型变量,字符串,数组…然而很多人也许忽略了函数指针的使用。
函数指针的定义
函数指针就是指向函数的指针。也就是将函数的入口地址赋值给指针。这样我们在访问函数的时候可以用指针访问。
函数指针可以当成参数传递,下面给出实例
#include <stdio.h>
/*比较函数*/
int max(int a,int b)
{
return (a>b ? a:b);
}
/*指向函数的指针声明(此刻指针未指向任何一个函数)*/
int (*test)(int a,int b);
int main(int argc,char* argv[])
{
int maxNum;
/*将max函数的入口地址赋值给
*函数指针test
*/
test=max;
/*通过指针test调用函数max实
*现比较大小
*/
maxNum = (*test)(1,2);
printf("maxNum = %d\n", maxNum);
return 0;
}
根据注释大家应该很容易地去理解,函数指针其实和平时我们用的其他指针没什么太大的区别。
看懂了上面的程序,我们来看一下s3c2440的NandFlash源码
typedef struct {
void (*nand_reset)(void);
void (*wait_idle)(void);
void (*nand_select_chip)(void);
void (*nand_deselect_chip)(void);
void (*write_cmd)(int cmd);
void (*write_addr)(unsigned int addr);
unsigned char (*read_data)(void);
}t_nand_chip;
static t_nand_chip nand_chip;
/* NAND Flash操作的总入口, 它们将调用S3C2410或S3C2440的相应函数 */
static void nand_reset(void);
static void wait_idle(void);
static void nand_select_chip(void);
static void nand_deselect_chip(void);
static void write_cmd(int cmd);
static void write_addr(unsigned int addr);
static unsigned char read_data(void);
/* S3C2410的NAND Flash处理函数 */
static void s3c2410_nand_reset(void);
static void s3c2410_wait_idle(void);
static void s3c2410_nand_select_chip(void);
static void s3c2410_nand_deselect_chip(void);
static void s3c2410_write_cmd(int cmd);
static void s3c2410_write_addr(unsigned int addr);
static unsigned char s3c2410_read_data();
/* S3C2440的NAND Flash处理函数 */
static void s3c2440_nand_reset(void);
static void s3c2440_wait_idle(void);
static void s3c2440_nand_select_chip(void);
static void s3c2440_nand_deselect_chip(void);
static void s3c2440_write_cmd(int cmd);
static void s3c2440_write_addr(unsigned int addr);
static unsigned char s3c2440_read_data(void);
/* 初始化NAND Flash */
void nand_init(void)
{
#define TACLS 0
#define TWRPH0 3
#define TWRPH1 0
/* 判断是S3C2410还是S3C2440 */
if ((GSTATUS1 == 0x32410000) || (GSTATUS1 == 0x32410002))
{
nand_chip.nand_reset = s3c2410_nand_reset;
nand_chip.wait_idle = s3c2410_wait_idle;
nand_chip.nand_select_chip = s3c2410_nand_select_chip;
nand_chip.nand_deselect_chip = s3c2410_nand_deselect_chip;
nand_chip.write_cmd = s3c2410_write_cmd;
nand_chip.write_addr = s3c2410_write_addr;
nand_chip.read_data = s3c2410_read_data;
/* 使能NAND Flash控制器, 初始化ECC, 禁止片选, 设置时序 */
s3c2410nand->NFCONF = (1<<15)|(1<<12)|(1<<11)|(TACLS<<8)|(TWRPH0<<4)|(TWRPH1<<0);
}
else
{
nand_chip.nand_reset = s3c2440_nand_reset;
nand_chip.wait_idle = s3c2440_wait_idle;
nand_chip.nand_select_chip = s3c2440_nand_select_chip;
nand_chip.nand_deselect_chip = s3c2440_nand_deselect_chip;
nand_chip.write_cmd = s3c2440_write_cmd;
#ifdef LARGER_NAND_PAGE
nand_chip.write_addr = s3c2440_write_addr_lp;
#else
nand_chip.write_addr = s3c2440_write_addr;
#endif
nand_chip.read_data = s3c2440_read_data;
/* 设置时序 */
s3c2440nand->NFCONF = (TACLS<<12)|(TWRPH0<<8)|(TWRPH1<<4);
/* 使能NAND Flash控制器, 初始化ECC, 禁止片选 */
s3c2440nand->NFCONT = (1<<4)|(1<<1)|(1<<0);
}
/* 复位NAND Flash */
nand_reset();
}
这段代码是用于操作s3c2440的NandFlash的源代码。下面我们来对这段代码进行分析
开始时,定义了一个结构体,结构体内部存放着一堆函数指针
随后定义了一个结构体的实例化变量nand_chip
再往下就是一堆函数的声明,这些函数声明作为入口等待被进入
然后跳入nand_init函数里面进行分析:该函数根据不同的芯片型号(s3c2410和s3c2440)来选择结构体里的函数指针赋值该赋值哪些函数。把对应的函数入口地址赋值给函数指针。
这么写的优势有两点
1.我们定义的结构体变量nand_chip在被赋值之后,里面存放着多个函数指针作为入口可供调用。要访问函数,仅需要访问这个结构体即可。
2.如果我们把芯片型号从2440换成2410,那么不必重新写一份代码,我们只需要在nand_chip函数里面加入条件判断,然后给结构体变量重新赋值即可。增加了可移植性。
寄存器操作
violatile是干嘛的
很多人在被面试时,很容易被问到violatile修饰是什么意思。相信很多人能朗朗上口地回答上来“防止被优化”,但是并不知道具体的使用场景是什么,下面来举出具体的例子来说明
#define GSTATUS1 (*(volatile unsigned int *)0x560000B0)
这是一个GPIO口引脚状态寄存器的宏定义,我们现在模拟一个场景:加入现在的GPIO口处于输入模式,我要在程序中实时地获取GPIO口的引脚信息。如果不加volatile修饰,那么cpu获取这个引脚的信息默认会在cache中获取,这里简单描述下cache的功能。
cache的功能
cache是处于CPU边上的高速缓存区,CPU通过SDRAM读取数据,如果CPU在一段时间内需要重复地访问一段数据,系统会将这段数据从SDRAM中将数据搬运到cache里面,这样大大增加了访问的速度。
然而我刚刚在说GPIO口引脚的模式是输入模式,它的数据是从外部不断获取到SDRAM中的。然而我们把某一时间的数据存储到cache后,那么CPU在访问的时候不会再去访问SDRAM中存储的数据,而是去访问cache中存储的数据。然而我们最新的数据永远是存储在SDRAM中,这样就造成我们无法获取到最新的数据。所以我们要避免这段数据存储到cache中,所以这个时候使用violatile进行修饰可以避免数据被存储到cache中,从而让CPU老老实实地去SDRAM中去读取我们的最新数据。
指针的巧妙运用
#define GSTATUS1 (*(volatile unsigned int *)0x560000B0)
在这里(volatile unsigned int )是一个指针,它存储的地址在0x560000B0,最后在通过“ * ”来访问这片地址里面的值。所以代码变成了((volatile unsigned int *)0x560000B0)。
这样,在后面的代码中可以直接通过访问GSTATUS1来访问寄存器的值。
下面再举个例子
/* NAND FLASH (see S3C2410 manual chapter 6) */
typedef struct {
S3C24X0_REG32 NFCONF;
S3C24X0_REG32 NFCMD;
S3C24X0_REG32 NFADDR;
S3C24X0_REG32 NFDATA;
S3C24X0_REG32 NFSTAT;
S3C24X0_REG32 NFECC;
} S3C2410_NAND;
static S3C2410_NAND * s3c2410nand = (S3C2410_NAND *)0x4e000000;
volatile unsigned char *p = (volatile unsigned char *)&s3c2410nand->NFSTAT;
这里就比较有意思了,首先定义了一个结构体,里面都是些32位的变量。
然后定义了一个结构体指针,并且让他指向0x4e000000这块地址。
最后通过先前定义的s3c2410nand这个结构体指针来访问结构体内部的NFSTAT变量。但是这里左边是一个指针,所以要取出它的地址,最后在前面加上(volatile unsigned char *)进行强制转换来赋值给左边的指针p。这样就可以直接通过p这个指针来给这个结构体里面的某个变量来赋值。
如何操作寄存器
#define GPFCON (*(volatile unsigned long *)0x56000050)
GPFCON &=~ (0x1<<3);
GPFCON |= (0x1<<3);
结合刚刚讲的指针,我们通过指针可以轻而易举地访问寄存器的地址。这里用个宏定义了起来,然后可以通过宏直接对它进行位操作。
拓展:带参宏的应用
刚刚我们在寄存器里面用到过宏定义,这里拓展一个宏定义的用法,直接上代码吧。
#include <stdio.h>
#define MAX(a,b) (a>b) ? a : b
int main()
{
int x;
int y;
int max;
printf(“input two numbers: “);
scanf("%d %d", &x, &y);
max = MAX(x, y);
printf("max=%d\n", max);
return 0;
}
运行结果
1
2
max = 2
由此可见,宏定义也可以像函数一样传入参数。只要在宏定义的右边加入程序逻辑即可。
为了方便理解,再举个例子
#include <stdio.h>
#define SQ(y) (y)*(y)
int main()
{
int a, sq;
printf(“input a number: “);
scanf("%d", &a);
sq = SQ(a+1);
printf("sq=%d\n", sq);
return 0;
}
运行结果
input a number: 10
sq=121
下面整理一下带参宏和函数传参的区别
1.函数调用时,先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。
2.函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
3.对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
4.调用函数只可得到一个返回值,而用宏可以设法得到几个结果。
5.使用宏次数多时,宏展开后源程序变长,因为每展开一次都使程序增长,而函数调用不使源程序变长。
6.宏替换不占运行时间,只占编译时间;而函数调用则占运行时间(分配单元、保留现场、值传递、返回)。
总的来说,用宏来代表简短的表达式比较合适。