46.Linux-分析rc红外遥控平台驱动框架,修改内核的NEC解码函数BUG(1)
- 内核版本 : Linux 3.10.14
- rc红外接收类型: GPIO 类型的NEC红外编码
本章内容
- 1) rc体系结构分析
- 2) 分析红外platform_driver平台驱动框架
- 3) 分析内核自带的NEC红外解码过程
- 4) 修改内核自带的NEC红外解码BUG,实现按键重复按下
下章内容
- 1) 自己创建一个红外platform_device平台设备
- 2) 试验
在分析之前,先来复习下NEC红外编码的发送波形(在后面分析NEC解码会用到)
基本数据格式如下:
如果一直按住一个按钮时,会每隔100ms一直发送引导重复码.
一个完整的数据波形如下所示:
1.rc体系结构分析
rc相关文件位于kernel\drivers\media\rc
1.1首先来看kernel\drivers\media\rc\Makefile
如上图所示,由于我们板子上的红外接收编码是NEC格式,并且是GPIO类型
所以Make menuconfig配置宏:
->Device Drivers
-> Multimedia support (MEDIA_SUPPORT [=y])
-> Remote controller decoders (RC_DECODERS [=y])
[*] Enable IR raw decoder for the NEC protocol
//选择NEC协议, ,使CONFIG_IR_NEC_DECODER=y ->Device Drivers
-> Multimedia support (MEDIA_SUPPORT [=y])
-> Remote Controller devices (RC_DEVICES [=y])
[*] GPIO IR remote control
//选择GPIO接收类型,使CONFIG_IR_GPIO_CIR=y
1.2然后在drivers\media\rc\keymaps里存了各种不同的键映射文件
先来看看drivers\media\rc\keymaps\Makefile:
如上图所示,可以看到把keymaps文件夹里的文件全部包含了.
它们用途在于:
1) 当内核解码后,通过我们红外平台设备的dev.platform_data里map_name成员去匹配这些文件.
其中红外平台设备platform_data对应的结构体为:
struct gpio_ir_recv_platform_data {
int gpio_nr; //红外接收管对应的管脚
bool active_low; //数据是否低电平有效
u64 allowed_protos; //该红外允许接收的编码协议,比如有NEC, SANYO, RC5等,可以填0,表示支持所有
const char *map_name;
//该红外接收管对应的键值映射表名,内核会通过该名字去匹配keymaps文件夹里的编码对应的文件.从而注册该文件的键值映射表,以后解出来的编码则去找该键值映射表
};
2) 找到对应的文件,然后便通过该文件里的rc_map_list匹配编码
我们以rc-trekstor.c文件为例,该文件内容如下所示:
3)如果匹配到支持接收的编码,便会上报input事件按键.
PS: 在下章创建红外平台设备时,会详细讲解如何使用
2.分析红外platform_driver平台驱动框架
我们选择的是CONFIG_IR_GPIO_CIR宏,所以接下来分析GPIO类型的rc驱动框架,该宏对应的驱动文件为:
2.1 分析gpio-ir-recv.c的init入口函数
如上图所示,其中module_platform_driver()宏定义位于platform_device.h
最终module_platform_driver(gpio_ir_recv_driver)展开后等于:
static int __init gpio_ir_recv_driver_init(void)
{
return platform_driver_register(&gpio_ir_recv_driver);
}
module_init(gpio_ir_recv_driver_init);
//…
该平台驱动的.name定义如下所示:
#define GPIO_IR_DRIVER_NAME "gpio-rc-recv"
所以我们后面创建红外platform_device平台设备时, .name也要写成"gpio-rc-recv"
2.2 分析gpio-ir-recv.c的probe函数
PS:在probe函数里,主要是获取平台设备pdev->dev.platform_data内容.该内容在1.2小结讲解过了.
代码如下:
static int gpio_ir_recv_probe(struct platform_device *pdev)
{
struct gpio_rc_dev *gpio_dev;
struct rc_dev *rcdev;
const struct gpio_ir_recv_platform_data *pdata =pdev->dev.platform_data;
//获取gpio_ir_recv_platform_data结构体 int rc;
//… …
if (pdata->gpio_nr < ) //判断管脚有效性
return -EINVAL; gpio_dev = kzalloc(sizeof(struct gpio_rc_dev), GFP_KERNEL);
if (!gpio_dev)
return -ENOMEM;
rcdev = rc_allocate_device();
if (!rcdev) {
rc = -ENOMEM;
goto err_allocate_device;
} rcdev->priv = gpio_dev;
rcdev->driver_type = RC_DRIVER_IR_RAW;
rcdev->input_name = GPIO_IR_DEVICE_NAME;
rcdev->input_phys = GPIO_IR_DEVICE_NAME "/input0";
rcdev->input_id.bustype = BUS_HOST;
rcdev->input_id.vendor = 0x0001;
rcdev->input_id.product = 0x0001;
rcdev->input_id.version = 0x0100;
rcdev->dev.parent = &pdev->dev;
rcdev->driver_name = GPIO_IR_DRIVER_NAME;
if (pdata->allowed_protos)
rcdev->allowed_protos = pdata->allowed_protos;
else
rcdev->allowed_protos = RC_BIT_ALL; // allowed_protos==0,表示支持所有协议类型
rcdev->map_name = pdata->map_name ?: RC_MAP_EMPTY; gpio_dev->rcdev = rcdev;
gpio_dev->gpio_nr = pdata->gpio_nr;
gpio_dev->active_low = pdata->active_low; rc = gpio_request(pdata->gpio_nr, "gpio-ir-recv"); //申请IO管脚
if (rc < )
goto err_gpio_request;
rc = gpio_direction_input(pdata->gpio_nr); //设置为输入
if (rc < )
goto err_gpio_direction_input; rc = rc_register_device(rcdev);
if (rc < ) {
dev_err(&pdev->dev, "failed to register rc device\n");
goto err_register_rc_device;
}
platform_set_drvdata(pdev, gpio_dev); rc = request_any_context_irq(gpio_to_irq(pdata->gpio_nr),
gpio_ir_recv_irq,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"gpio-ir-recv-irq", gpio_dev);
//创建gpio_ir_recv_irq中断函数,为上下沿触发
return ;
//… …
}
接下来,我们来看看gpio_ir_recv_irq()函数,看看如何实现解码的
2.3 分析gpio-ir-recv.c的gpio_ir_recv_irq函数
static irqreturn_t gpio_ir_recv_irq(int irq, void *dev_id)
{
struct gpio_rc_dev *gpio_dev = dev_id;
int gval;
int rc = ;
enum raw_event_type type = IR_SPACE; //默认定义类型为IR_SPACE (红外接收的间隔信号) gval = gpio_get_value_cansleep(gpio_dev->gpio_nr); //获取GPIO的值
if (gval < )
goto err_get_value;
if (gpio_dev->active_low) //低电平有效
gval = !gval; //取反
if (gval == )
type = IR_PULSE; //收到的是脉冲信号 rc = ir_raw_event_store_edge(gpio_dev->rcdev, type); //通过内核时间,计算出当前波形的持续时间,并保存
if (rc < )
goto err_get_value; ir_raw_event_handle(gpio_dev->rcdev); //启动内核解码对应的线程,来处理波形
err_get_value:
return IRQ_HANDLED;
}
接下来分析ir_raw_event_handle()函数如何处理波形的.
2.4 gpio_ir_recv_irq ()->ir_raw_event_handle()函数
该函数如下所示:
如上图所示,最终会唤醒一个线程,该线程对应的函数为ir_raw_event_thread():
static int ir_raw_event_thread(void *data)
{
struct ir_raw_handler *handler;
… …
list_for_each_entry(handler, &ir_raw_handler_list, list)
// ir_raw_handler_list: 存储内核里注册的各个解码协议ir_raw_handler结构体,比如NEC, SANYO, RC5等
handler->decode(raw->dev, ev); //调用解码函数
… …
};
2.5 接下来,我们看看解码文件是如何添加到ir_raw_handler_list表的
由于我们选择的是NEC协议(CONFIG_IR_NEC_DECODER=y),所以以/drivers/media/rc/ir-nec-decoder.c为例
1)首先查看ir-nec-decoder.c的init函数:
如上图所示,可以看到通过ir_raw_handler_register()来注册.
2) 然后ir_raw_handler_register()里,则将该nec_handler添加到ir_raw_handler_list表:
3.接下来,我们来分析ir_nec_decode()解码函数如何解码的.
3.1分析ir_nec_decode()解码函数
static int ir_nec_decode(struct rc_dev *dev, struct ir_raw_event ev)
{
struct nec_dec *data = &dev->raw->nec;
u32 scancode;
u8 address, not_address, command, not_command;
bool send_32bits = false; if (!(dev->enabled_protocols & RC_BIT_NEC)) //判断协议是否支持
return ;
//… …
switch (data->state) { case STATE_INACTIVE:
if (!ev.pulse)
break; if (eq_margin(ev.duration, NEC_HEADER_PULSE, NEC_UNIT * )) { //判断ev.duration 是否等于9ms头引导码
data->is_nec_x = false; //标记当前格式不是NECX编码格式
data->necx_repeat = false;
} else if (eq_margin(ev.duration, NECX_HEADER_PULSE, NEC_UNIT / )) //另一种不常见的NECX引导码
data->is_nec_x = true; //标记是NECX编码格式
else
break; data->count = ;
data->state = STATE_HEADER_SPACE; //进入判断引导码间隔值,是4.5ms还是2.25ms ?
return ; case STATE_HEADER_SPACE:
if (ev.pulse)
break; if (eq_margin(ev.duration, NEC_HEADER_SPACE, NEC_UNIT)) { //如果ev.duration=4.5ms 间隔引导码
data->state = STATE_BIT_PULSE; //进入解析32bit模式
return ;
} else if (eq_margin(ev.duration, NEC_REPEAT_SPACE, NEC_UNIT / )) { //如果ev.duration=2.5ms ,表示重复引导码
if (!dev->keypressed) { //dev->keypressed是松开的,则放弃(这里有BUG,后面会分析到)
IR_dprintk(, "Discarding last key repeat: event after key up\n");
} else {
rc_repeat(dev); //dev->keypressed是未松开,则上报事件
IR_dprintk(, "Repeat last key\n");
data->state = STATE_TRAILER_PULSE;
}
return ;
}
break; case STATE_BIT_PULSE: //接收数据位的脉冲数据
if (!ev.pulse)
break;
if (!eq_margin(ev.duration, NEC_BIT_PULSE, NEC_UNIT / )) //不等于0.56ms,则忽略掉
break; data->state = STATE_BIT_SPACE; //等于0.56ms,接下来进入STATE_BIT_SPACE,开始解析数据bit
return ; case STATE_BIT_SPACE:
if (ev.pulse)
break; if (data->necx_repeat && data->count == NECX_REPEAT_BITS &&
geq_margin(ev.duration,
NEC_TRAILER_SPACE, NEC_UNIT / )) { //解析NECX编码格式
IR_dprintk(, "Repeat last key\n");
rc_repeat(dev);
data->state = STATE_INACTIVE;
return ; } else if (data->count > NECX_REPEAT_BITS)
data->necx_repeat = false; data->bits <<= ;
if (eq_margin(ev.duration, NEC_BIT_1_SPACE, NEC_UNIT / )) // 1.68ms 数据1
data->bits |= ;
else if (!eq_margin(ev.duration, NEC_BIT_0_SPACE, NEC_UNIT / )) // 既不等于1.68ms,也不等于0.56ms,则是无效数据
break;
data->count++; if (data->count == NEC_NBITS) //data->count == 32,则表示数据接收完成
data->state = STATE_TRAILER_PULSE;
else
data->state = STATE_BIT_PULSE;
return ; case STATE_TRAILER_PULSE:
if (!ev.pulse)
break;
if (!eq_margin(ev.duration, NEC_TRAILER_PULSE, NEC_UNIT / ))
break;
data->state = STATE_TRAILER_SPACE;
return ; case STATE_TRAILER_SPACE:
if (ev.pulse)
break; if (!geq_margin(ev.duration, NEC_TRAILER_SPACE, NEC_UNIT / ))
break; address = bitrev8((data->bits >> ) & 0xff);
not_address = bitrev8((data->bits >> ) & 0xff);
command = bitrev8((data->bits >> ) & 0xff);
not_command = bitrev8((data->bits >> ) & 0xff); if ((command ^ not_command) != 0xff) { //解析数据
IR_dprintk(, "NEC checksum error: received 0x%08x\n",
data->bits);
send_32bits = true;
} if (send_32bits) {
/* NEC transport, but modified protocol, used by at
* least Apple and TiVo remotes */
scancode = data->bits;
IR_dprintk(, "NEC (modified) scancode 0x%08x\n", scancode);
} else if ((address ^ not_address) != 0xff) {
/* Extended NEC */
scancode = address << |
not_address << |
command;
IR_dprintk(, "NEC (Ext) scancode 0x%06x\n", scancode);
} else {
/* Normal NEC */
scancode = address << | command;
IR_dprintk(, "NEC scancode 0x%04x\n", scancode);
} if (data->is_nec_x)
data->necx_repeat = true; rc_keydown(dev, scancode, ); //通过scancode编码来上报按键事件
data->state = STATE_INACTIVE;
return ;
}
//… …
}
3.2接下来分析ir_nec_decode ()->rc_keydown()如何通过scancode编码来上报按键事件
void rc_keydown(struct rc_dev *dev, int scancode, u8 toggle)
{
unsigned long flags;
u32 keycode = rc_g_keycode_from_table(dev, scancode); //从键映射表里找到编码对应的键值 spin_lock_irqsave(&dev->keylock, flags); if(keycode){ //如果找到键值
ir_do_keydown(dev, scancode, keycode, toggle); //上报按键事件 if (dev->keypressed) { //如果是按下,则启动timer_keyup定时器, IR_KEYPRESS_TIMEOUT(20ms)后上报key松开事件
dev->keyup_jiffies = jiffies + msecs_to_jiffies(IR_KEYPRESS_TIMEOUT);
mod_timer(&dev->timer_keyup, dev->keyup_jiffies);
}
}else{
dev->last_scancode = ;
dev->last_toggle = ;
dev->last_keycode = ;
}
spin_unlock_irqrestore(&dev->keylock, flags);
}
上个函数里的dev->timer_keyup定时器对应的函数为ir_timer_keyup(),该函数会去调用一次ir_do_keyup()函数,上报key松开事件,该函数如下:
如上图所示,我们发现dev->keypressed = false,这就是解码函数出现的BUG:
1)比如当遥控器当按下按键时,会上报一次按键按下事件,并启动20ms定时器,用来自动上报按键自动按起事件,并标记dev->keypressed = false.
2)然后,如果遥控器一直按下不松手的话,会隔110ms发送一次9ms+2.25ms重复引导码
3) 然后内核将会调用ir_nec_decode()进行解码2.25ms
4. 修改ir_nec_decode()函数
接下来,我们修改ir_nec_decode()函数,实现按键重复按下,并实现rc_map->repeat_key.
为什么要实现rc_map->repeat_key?
因为rc_map->scan里存储的键值表仅仅表示可支持按下的按键, 而rc_map->repeat_key里存储的才是表示可重复按下的按键.
修改之前需要将rc-map.h下的rc_map结构体里添加两个成员repeat_key和repeat_size(用来实现重复按下功能)
修改后的结构体如下所示:
struct rc_map {
struct rc_map_table *scan;
unsigned int size; /* Max number of entries */
unsigned int len; /* Used number of entries */
unsigned int alloc; /* Size of *scan in bytes */
enum rc_type rc_type;
const char *name;
spinlock_t lock;
struct rc_map_table *repeat_key; //add
unsigned int repeat_size; //add
};
然后修改ir_nec_decode(),如下所示:
static int ir_nec_decode(struct rc_dev *dev, struct ir_raw_event ev)
{
struct nec_dec *data = &dev->raw->nec;
u32 scancode=;
static unsigned char repet_flag=0; if (!(dev->enabled_protocols & RC_BIT_NEC))
return ; if (!is_timing_event(ev)) {
if (ev.reset)
data->state = STATE_INACTIVE;
return ;
} IR_dprintk(, "NEC decode started at state %d (%uus %s)\n",
data->state, TO_US(ev.duration), TO_STR(ev.pulse)); switch (data->state) { case STATE_INACTIVE:
if (!ev.pulse)
break; if (eq_margin(ev.duration, NEC_HEADER_PULSE, NEC_UNIT * )) {
data->is_nec_x = false;
data->necx_repeat = false;
} else if (eq_margin(ev.duration, NECX_HEADER_PULSE, NEC_UNIT / ))
{ data->is_nec_x = true;
}
else
break; data->count = ;
data->state = STATE_HEADER_SPACE;
return ; case STATE_HEADER_SPACE:
if (ev.pulse)
break; if (eq_margin(ev.duration, NEC_HEADER_SPACE, NEC_UNIT)) {
repet_flag = 0;
data->state = STATE_BIT_PULSE;
return ;
} else if (eq_margin(ev.duration, NEC_REPEAT_SPACE, NEC_UNIT / )) { //处理重复编码
if(!repet_flag) //避免第一次按下,出现两次编码
{
data->state = STATE_INACTIVE;
repet_flag =1;
}
else
data->state = STATE_TRAILER_SPACE;
IR_dprintk(, "Discarding last key repeat: event after key up\n");
return ;
}
else
break; case STATE_BIT_PULSE:
if (!ev.pulse)
break;
if (!eq_margin(ev.duration, NEC_BIT_PULSE, NEC_UNIT / ))
break; data->state = STATE_BIT_SPACE;
return ; case STATE_BIT_SPACE:
if (ev.pulse)
break; if (data->necx_repeat && data->count == NECX_REPEAT_BITS &&
geq_margin(ev.duration,
NEC_TRAILER_SPACE, NEC_UNIT / )) {
IR_dprintk(, "Repeat last key\n");
rc_repeat(dev);
data->state = STATE_INACTIVE;
return ;
} else if (data->count > NECX_REPEAT_BITS)
data->necx_repeat = false; data->bits <<= ; if (eq_margin(ev.duration, NEC_BIT_1_SPACE, NEC_UNIT / ))
data->bits |= ; else if (!eq_margin(ev.duration, NEC_BIT_0_SPACE, NEC_UNIT / ))
break; data->count++; if (data->count == NEC_NBITS)
data->state = STATE_TRAILER_SPACE;
else
data->state = STATE_BIT_PULSE;
return ; case STATE_TRAILER_SPACE:
{
struct rc_map *rc_map = &dev->rc_map;
struct rc_map_table *repeat_key = rc_map->repeat_key;
unsigned int repeat_size = rc_map->repeat_size; //获取 repeat_size,是否有支持重复按下的按键
scancode=data->bits; if (!ev.pulse)
break; if (!eq_margin(ev.duration, NEC_TRAILER_PULSE, NEC_UNIT / ))
break;
printk("NEC scancode=0x%x\n",scancode); if(!scancode)
break;
if (data->is_nec_x)
data->necx_repeat = true; rc_keydown(dev, scancode, ); //上报事件
if(repeat_key){
int i = ;
while(repeat_size){
if(scancode == repeat_key[i].scancode){
break;
}
repeat_size--;
i++;
} if(repeat_size==) //repeat_size==0,表示没找到有支持重复按键,则清空data->bits
data->bits = ; }
else
data->bits = ; return ;
}
} IR_dprintk(, "NEC decode failed at count %d state %d (%uus %s)\n",
data->count, data->state, TO_US(ev.duration), TO_STR(ev.pulse));
data->state = STATE_INACTIVE;
return -EINVAL;
}
接下来下章,自己创建一个红外platform_device平台设备
创建红外platform_device平台设备步骤为:
- 1) 创建一个platform_device设备,其中.name= "gpio-rc-recv",并注册设备
- 2) 在drivers\media\rc\keymaps\里创建一个名字为rc-my-text.c键值映射文件
下章链接: 46.Linux-创建rc红外遥控平台设备,实现重复功能(2)
46.Linux-分析rc红外遥控平台驱动框架,修改内核的NEC解码函数BUG(1)的更多相关文章
- 46.Linux-创建rc红外遥控平台设备,实现重复功能(2)
上章链接:46.Linux-分析rc红外遥控平台驱动框架,修改内核的NEC解码函数BUG(1) 在上章分析了红外platform_driver后,已经修改bug后,接下来我们自己创建一个红外platf ...
- 驱动开发:内核层InlineHook挂钩函数
在上一章<驱动开发:内核LDE64引擎计算汇编长度>中,LyShark教大家如何通过LDE64引擎实现计算反汇编指令长度,本章将在此基础之上实现内联函数挂钩,内核中的InlineHook函 ...
- Linux内核的LED设备驱动框架【转】
/************************************************************************************ *本文为个人学习记录,如有错 ...
- 基于STM32的红外遥控重点解析
本文有两个内容:一.红外遥控协议的的讲解:二.解码程序解析(参考正点原子的代码) 红外的介绍.优点.缺点就不给大家说了,进入正题 一.红外遥控协议的的讲解 红外遥控的编码目前广泛使用的是:NEC Pr ...
- ALSA声卡笔记2---ASoC驱动框架
1.简单了解一下ASOC 在嵌入式系统里面的声卡驱动为ASOC(ALSA System on Chip) ,它是在ALSA 驱动程序上封装的一层 分为3大部分,Machine,Platform和C ...
- linux驱动基础系列--linux spi驱动框架分析
前言 主要是想对Linux 下spi驱动框架有一个整体的把控,因此会忽略某些细节,同时里面涉及到的一些驱动基础,比如平台驱动.设备模型等也不进行详细说明原理.如果有任何错误地方,请指出,谢谢! spi ...
- linux驱动基础系列--linux spi驱动框架分析(续)
前言 这篇文章是对linux驱动基础系列--linux spi驱动框架分析的补充,主要是添加了最新的linux内核里设备树相关内容. spi设备树相关信息 如之前的文章里所述,控制器的device和s ...
- linux设备驱动程序--串行通信驱动框架分析
linux 串行通信接口驱动框架 在学习linux内核驱动时,不论是看linux相关的书籍,又或者是直接看linux的源码,总是能在linux中看到各种各样的框架,linux内核极其庞杂,linux各 ...
- Linux 驱动框架---dm9000分析
前面学习了下Linux下的网络设备驱动程序的框架inux 驱动框架---net驱动框架,感觉知道了一个机器的大致结构还是不太清楚具体的细节处是怎么处理的,所以今天就来以dm9000这个网上教程最多的驱 ...
随机推荐
- hadoop安装笔记
环境是ubuntu java啥的有yum apt-get install default-jdk update-alternatives --display Java hadoop解压缩就行 tar ...
- ActiveMQ 的安装与使用
消息中间件简介 消息中间件(MOM:Message Orient middleware) 消息中间件有很多的用途和优点: 1. 将数据从一个应用程序传送到另一个应用程序,或者从软件的一个模块传送到另外 ...
- 修改openstack用户配额
修改openstack用户配额 这是我在工作中遇到的一个很有趣的小问题,当时的场景是这样的: 公司的云产品要上线数据库服务(trove),因为每创建数据库实例都要占用一个虚拟机及相关资源的配额,尤其是 ...
- Kubenetes 核心概念理解
Kubernetes 是一个具有自动控制 .自动纠错功能的资源管理系统 可以把 Node , Pod , Replication Controller , Service 等都看做是一种 " ...
- iOS学习——(转)iOS中关于通知的使用
在移动端开打过程中,经常会用到通知和推送,例如有短信来了需要通知提示,手机横屏了需要通知提示,插上耳机了需要通知提示等等,我们可以根据这些通知采取对应的动作.iOS系统自身定义了很对通知,但是在开发过 ...
- Jade是变体的HTML
在这段HTML代码中,div 包含了一个 a 元素与一段没有标记包围的文本.若要用Jade表述这段HTML,div 元素和 a 元素都可以用前面所述的方法实现,但剩下的那个没有标记包围的文本就不能用前 ...
- 面试题:两种方法计算n!
直接上代码package com.face.test; public class Test { /** * 面试题:递归方法计算n! */ @org.junit.Test public void di ...
- 【分布式事务】基于RocketMQ搭建生产级消息集群?
导读 目前很多互联网公司的系统都在朝着微服务化.分布式化系统的方向在演进,这带来了很多好处,也带来了一些棘手的问题,其中最棘手的莫过于数据一致性问题了.早期我们的软件功能都在一个进程中,数据的一致性可 ...
- Java 容器 & 泛型:三、HashSet,TreeSet 和 LinkedHashSet比较
Writer:BYSocket(泥沙砖瓦浆木匠) 微博:BYSocket 豆瓣:BYSocket 上一篇总结了下ArrayList .LinkedList和Vector比较,今天泥瓦匠总结下Hash ...
- .Net程序员学用Oracle系列(19):导出、导入(备份、还原)
1.传统的导出/导入工具 1.1.EXP 命令详解 1.2.IMP 命令详解 1.3.EXP/IMP 使用技巧 2.新的导出/导入工具 2.1.EXPDP/IMPDP 参数说明 2.2.EXPDP/I ...