Linux驱动
创始人
2024-05-10 21:35:29
0

Linux驱动

驱动
1.驱动课程大纲
 内核模块
 字符设备驱动
 中断
2.ARM裸机代码和驱动有什么区别?
 共同点:都能够操作硬件 (都操作寄存器)
 不同点:
 裸机就是用C语言给对应的寄存器里面写值,驱动是按照一定的框架格式往寄存器里面写值
 arm裸机单独编译单独执行,驱动依赖内核编译,依赖内核执行(根据内核指定好的架构和配置去实现)
 arm裸机同时只能执行一份代码,驱动可以同时执行多分代码(且当要操作串口的时候,内核写的一部分代码咱们程序员就不用去写了,比较方便)
 arm裸机只需要一个main就可以了,在main函数中写相应的逻辑代码即可驱动是依赖内核的框架和操作硬件的过程。
在这里插入图片描述
(驱动里面操作LED灯的寄存器)(驱动模块是依赖内核框架执行代码)
3.linux系统组成
在这里插入图片描述

 0-3G的用户空间是每个进程单独拥有0-3G的空间
 系统调用(软中断swi)----(应用层通过系统调用与底层交互,swi,将应用层切换到内核层。
在这里插入图片描述

注:1G的物理内存映射成04G的虚拟内存,每个进程都可以访问内核,03G是每个进程单独拥有的,3G~4G是所有的共有的。代码运行在物理内存上,向虚拟内存上面写值,其实是写在物理内存上面的

 kernel : 【3-4G】
内核5大功能:
 进程管理:进程的创建,销毁,调度等功能
注:可中断,不可中断,就是是否被信号打断。从运行状态怎样改到可中断等待态,和不可中断等待态操作系统开始会对每个进程分配一个时间片,当进程里面写了sleep函数,进程由运行到休眠态,但是此时CPU不可能等着。有两种方法,1:根据时间片,CPU自动跳转,2:程序里面自己写能引起CPU调度的代码就可以

 文件管理:通过文件系统ext2/ext3/ext4 yaff jiffs等来组织管理文件
 网络管理:通过网络协议栈(OSI,TCP)对数据进程封装和拆解过程(数据发送和接收是通过网卡驱动完成的,网卡驱动不会产生文件(在Linux系统dev下面没有相应的文件),所以不能用open等函数,而是使用的socket)。
 内存管理:通过内存管理器对用户空间和内核空间内存的申请和释放
 设备管理: 设备驱动的管理(驱动工程师所对应的)
 字符设备驱动: (led 鼠标 键盘 lcd touchscreen(触摸屏))
1.按照字节为单位进行访问,顺序访问(有先后顺序去访问)
2.会创建设备文件,open read write close来访问
 块设备驱动 :(camera u盘 emmc)
1.按照块(512字节)(扇区)来访问,可以顺序访问,可以无序访问
2.会创建设备文件,open read write close来访问
 网卡设备驱动:(猫)

  1. 按照网络数据包来收发的。
    4.宏内核、微内核 (了解)
     宏内核:将进程,网络,文件,设备,内存等功能集成到一个内核中
    特点:代码运行效率高。
    缺点:如果有一个部分出错整个内核就崩溃了。
    eg:ubuntu Android
     微内核:只将进程,内存机制集成到这个内核中,文件,设备,驱动在操作系统之外。通过API接口让整个系统运行起来。
    缺点:效率低 优点:稳定性强(华为手机)
    eg:鸿蒙

  2. 驱动模块(驱动三要素:入口;出口;许可证)
     入口:资源的申请 (安装)
     出口:资源的释放 (释放)
     许可证:GPL(写一个模块需要开源,因为Linux系统是开源的,所以需要写许可协议)
    #include
    #include
    在这里插入图片描述

    static int __init  hello_init(void) 
    

(__init可以不指定,及可以不写,但是正常是写的)
在这里插入图片描述

 //_init  _eixt 指定放到内存的那个位置。//__init将hello_init放到.init.text段中{return 0;}static void __exit hello_exit(void)//存储类型  数据类型 指定存放区域 函数名(形参)//__exit将hello_exit放到.exit.text段中{}module_init(hello_init);//告诉内核驱动的入口地址(函数名为函数首地址)module_exit(hello_exit);//告诉内核驱动的出口地址MODULE_LICENSE("GPL");//许可证

在这里插入图片描述

 Makefile:
KERNELDIR:= /lib/modules/KaTeX parse error: Expected 'EOF', got '#' at position 40: … //Ubuntu内核的路径 #̲KERNELDIR:= /ho…(shell pwd)//驱动文件的路径
(打开一个终端看终端的路径)
all: //目标
make -C (KERNELDIR)M=(KERNELDIR) M=(KERNELDIR)M=(PWD) modules
(-C:进入顶层目录)
注:进入内核目录下执行make modules这条命令
如果不指定 M=(PWD)会把内核目录下的.c文件编译生成.koM=(PWD) 会把内核目录下的.c文件编译生成.ko M=(PWD)会把内核目录下的.c文件编译生成.koM=(PWD) 想编译模块的路径
clean:
make -C (KERNELDIR)M=(KERNELDIR) M=(KERNELDIR)M=(PWD) clean
obj-m:=hello.o //指定编译模块的名字
在这里插入图片描述

make工具作用: 是什么 (使用格式) 什么特点 怎么用
make是工程管理器,对多个文件进行管理,可以根据文件的时间戳自动发现更新的文件

追代码
创建索引文件
ctags -R
在终端上
vi -t xxx
在代码中跳转
ctrl + ]
ctrl + t
在这里插入图片描述

Ubuntu内核所对应的内核路径
在这里插入图片描述

6.命令:sudo insmod hello.ko   安装驱动模块

在这里插入图片描述

	sudo rmmod  hello     卸载驱动模块

在这里插入图片描述

lsmod                  查看模块
dmesg                  查看消息
sudo dmesg -C          直接清空消息不回显
sudo dmesg -c          回显后清空

在这里插入图片描述

7.内核中的打印函数。

在这里插入图片描述

搜索函数,搜到以后,在里面任意找到一个,看函数原形就OK
printk(打印级别 “内容”)
printk(KERN_ERR “Fail%d”,a);
printk(KERN_ERR “%s:%s:%d\n”,FILE,func,LINE);
(驱动在哪一个文件,哪一个函数,哪一行)
printk(“%s:%s:%d\n”,FILE,func,LINE);/
vi -t KERN_ERR(查看内核打印级别)
include/linux/printk.h
#define KERN_EMERG "<0> /* system is unusable /(系统不用)
#define KERN_ALERT “<1>” /
action must be taken immediately /(被立即处理)
#define KERN_CRIT “<2>” /
critical conditions /(临界条件,临界资源)
#define KERN_ERR “<3>” /
error conditions /(出错) //kern_err
#define KERN_WARNING “<4>” /
warning conditions /(警告)
#define KERN_NOTICE “<5>” /
normal but significant condition /(提示)
#define KERN_INFO “<6>” /
informational /(打印信息时候的级别)
#define KERN_DEBUG “<7>” /
debug-level messages */ (调试级别)
0 ------ 7
最高的 最低的
在这里插入图片描述

Hq@ubuntu:~$ cat /proc/sys/kernel/printk 4 4 1 7
终端的级别 消息的默认级别 终端的最大级别 终端的最小级别
#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])
只有当消息的级别大于终端级别,消息才会被显示
但对与咱们的这个Ubuntu被开发者修改过来,所有消息不会主动回显。

修改系统默认的级别su root		echo 4 3 1 7 > /proc/sys/kernel/printk虚拟机的默认情况: 

在这里插入图片描述

板子的默认情况:
在这里插入图片描述

如果想修改开发板对应的打印级别
vi rootfs/etc/init.d/rcS //当系统重新启动,rcS中的命令会全部重新执行一遍
echo 4 3 1 7 > /proc/sys/kernel/printk

在rootfs/etc/init.d/rcS里面添加上以后再起板子,板子的级别就为如下:
rootfs/etc/init.d/rcS:一些启动虚拟机需要启动的东西都可以放在这个文件中,启动系统时同时启动。echo 4 3 1 7 > /proc/sys/kernel/printk 放到板子跟文件系统对应这个文件中。

安装驱动和卸载驱动时,消息会打印。

8.驱动多文件编译
hello.c add.c
Makefile

 obj-m:=demo.odemo-y+=hello.o add.o  

(-y作用:将hello.o add.o放到demo.o中)
最终生成demo.ko文件
在这里插入图片描述

9.模块传递参数
 命令传递的方式 (终端传递值)
sudo insmod demo.ko hello world

 * Standard types are:                                                                             *  byte, short, ushort, int, uint, long, ulong  (没有找到char!!!!!!!!)   //char -->byte*  charp: a character pointer      //一个字符指针*  bool: a bool, values 0/1, y/n, Y/N.*  invbool: the above, only sense-reversed (N = true).

在这里插入图片描述
在这里插入图片描述

 module_param(name, type, perm)
功能:接收命令行传递的参数
参数:
@name :变量的名字
@type :变量的类型
@perm :权限 0664 0775(其它用户对我的只有读和执行权限,没有写的权限)
modinfo hello.ko(查看变量情况)
 MODULE_PARM_DESC(_parm, desc) //module_parm_desc
功能:对变量的功能进行描述
参数:
@_parm:变量
@desc :描述字段
在这里插入图片描述

只能传十进制,不可以写十六进制
练习:
1.byte类型如何使用
2.如何给一个指针传递一个字符串

在这里插入图片描述

sudo insmod hello.ko a=20 b=30 c=65 p="hello_world"

在这里插入图片描述

注意:传字符的时候写ASCII码值;传递字符串的时候,不能有空格

 module_param_array(name, type, nump, perm)
功能:接收命令行传递的数组
参数:
@name :数组名
@type :数组的类型
@nump :保存输入元素个数的地址
@perm :权限
sudo insmod hello.ko a=121 b=10 c=65 p=“hello” ww=1,2,3,4,5

在这里插入图片描述
在这里插入图片描述

复习
1.模块
三要素
入口
static int __init hello_init(void)
{
return 0;
}
module_init(hello_init);
出口
static void __exit hello_exit(void)
{

		}module_exit(hello_exit)

许可证
MODULE_LICENSE(“GPL”);
 多文件编译
obj-m:=demo.o
demo-y+=hello.o add.o
 内核中的打印:
printk(打印级别 “打印的内容”);
printk(“打印的内容”);
/proc/sys/kernel/printk
4 4 1 7
在这里插入图片描述

出现这个错误,提示,说明scripts下没有生成相应的文件,cd到kernel所在目录,执行: make scripts
然后 make 就可以编译了

在这里插入图片描述

字符设备驱动
linux系统中一切皆文件
应用层: APP1 APP2 …
fd = open(“led驱动的文件”,O_RDWR);
read(fd);
write();
close();
内核层:
对灯写一个驱动
led_driver.c
driver_open();
driver_read();
driver_write();
driver_close();
struct file_operations {
in¬t (*open) (struct inode *, struct file *);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*release) (struct inode *, struct file *);(close)
}
cdev:
设备号1 设备号2 设备号n
设备驱动1 设备驱动2 … 设备驱动n
设备号:32位,无符号数字
高12位 :主设备号 :区分哪一类设备
低20位 :次设备号 :区分同类中哪一个设备

硬件层: LED uart ADC PWM
每个驱动里面都有对应的file_operations
 open的过程:
open打开文件,这个文件与底层的驱动的设备号有关系,
通过设备号访问设备驱动中的struct file_operations里面的open函数。
 read的过程:
open函数会有一个返回值,文件描述符fd,read函数通过fd
找到驱动中的struct file_operations里面的read函数。
字符设备驱动的注册
 int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
功能:注册一个字符设备驱动
参数:
@major:主设备号
:如果你填写的值大于0,它认为这个就是主设备号
:如果你填写的值为0,操作系统给你分配一个主设备号
@name :名字 cat /proc/devices
(
当注册一个字符设备驱动的时候。
如果成功的话,当你使用cat /proc/devices 命令查看的时候可以看到系统自动分配的主设备号和这个名字
@fops :操作方法结构体
返回值:major>0 ,成功返回0,失败返回错误码(负数) vi -t EIO
major=0,成功主设备号,失败返回错误码(负数)

 void unregister_chrdev(unsigned int major, const char *name)
功能:注销一个字符设备驱动
参数:
@major:主设备号
@name:名字
返回值:无
手动创建设备文件
sudo mknod hello (路径是任意) c/b 主设备号 次设备号
sudo –rf hello 删除的时候记得加-rf
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过字符设备驱动点亮板子上的led灯
app: test.c char buf[3]
1 0 0
0 1 0
0 0 1
------------------|------------------------
kernel: led_driver.c
-------------------|------------------------
hardware: RGB_led
 应用程序如何将数据传递给驱动(读写的方向是站在用户的角度来说的)
#include
 int copy_from_user(void *to, const void __user *from, int n)
功能:从用户空间拷贝数据到内核空间(用户需要写数据的时候)
参数:
@to :内核中内存的首地址
@from:用户空间的首地址
@n :拷贝数据的长度(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数
 int copy_to_user(void __user *to, const void *from, int n)
功能:从内核空间拷贝数据到用户空间(用户开始读数据)
参数:
@to :用户空间内存的首地址
@from:内核空间的首地址
@n :拷贝数据的长度(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数
 驱动如何操作寄存器
rgb_led灯的寄存器是物理地址,在linux内核启动之后,在使用地址的时候,操作的全是虚拟地址。需要将物理地址转化为虚拟地址。在驱动代码中操作的虚拟地址就相当于操作实际的物理地址。
物理地址<------>虚拟地址
 void * ioremap(phys_addr_t offset, unsigned long size)
在这里插入图片描述

(当__iomen告诉编译器,取的时候是一个字节大小)
功能:将物理地址映射成虚拟地址
参数:
@offset :要映射的物理的首地址
@size :大小(字节)(映射是以业为单位,一页为4K,就是当你小于4k的时候映射的区域都为4k)
返回值:成功返回虚拟地址,失败返回NULL((void *)0);
在这里插入图片描述

 void iounmap(void addr)
功能:取消映射
参数:
@addr :虚拟地址
返回值:无
#define ENOMEM 12 /
Out of memory */

释放资源是按申请资源的倒序来释放
Eg:点灯
 软件编程控制硬件的思想:
只需要向控制寄存器中写值或者读值,就可以让我们处理器完成一定的功能。
RGB_led
1》GPIOxOUT:控制引脚输出高低电平
RED_LED—>GPIOA28
GPIOAOUT —> 0xC001A000
GPIOA28输出高电平:
GPIOAOUT[28] <–写-- 1
GPIOA28输出低电平:
GPIOAOUT[28] <–写-- 0
2》GPIOxOUTENB:控制引脚的输入输出模式
GPIOAOUTENB —> 0xC001A004
设置GPIOA28引脚为输出模式:
GPIOAOUTENB[28] <–写-- 1
3》GPIOxALTFN:控制引脚功能的选择
GPIOAALTFN1 —> 0xC001A024
设置GPIOA28引脚为GPIO功能:
GPIOAALTFN1[25:24] <–写-- 0b00
00 = ALT Function0
01 = ALT Function1
10 = ALT Function2
11 = ALT Function3
GPIO引脚功能的选择:每两位控制一个GPIO引脚
red :gpioa28
GPIOXOUT :控制高低电平的 0xC001A000
GPIOxOUTENB:输入输出模式 0xC001A004
GPIOxALTFN1:function寄存器 0xC001A024
(一个寄存器36个字节)
green:gpioe13
0xC001e000
blue :gpiob12
0xC001b000
练习:
1.字符设备驱动实现流水灯(30分钟)
//读,改,写
writel(v,c)
功能:向地址中写一个值
参数:
@ v :写的值
@ c :地址
readl©
功能:读一个地址,将地址中的值给返回
参数:
@c :地址
设备节点创建问题(udev/mdev)
(mknod hello c 243 0,手动创建设备节点hello)
(宏有返回值:为最后一句话执行的结果)
#include
自动创建设备节点:
struct class *cls;
 cls = class_create(owner, name) /void class_destroy(struct class *cls)//销毁
功能:向用户空间提交目录信息(内核目录的创建)
参数:
@owner :THIS_MODULE(看到owner就添THIS_MODULE)
@name :目录名字
返回值:成功返回struct class *指针
失败返回错误码指针 int (-5)
if(IS_ERR(cls)){
return PTR_ERR(cls);(PTR_ERR:把错误码指针转换成错误码)
}
在这里插入图片描述

 struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, …)(内核文件的创建),每个文件对应一个外设(硬件设备)
/void device_destroy(struct class *class, dev_t devt)//销毁
功能:向用户空间提交文件信息
参数:
@class :目录名字
@parent:NULL
@devt :设备号 (major<<12 |0 < = > MKDEV(major,0))
@drvdata :NULL
@fmt :文件的名字
返回值:成功返回struct device *指针
失败返回错误码指针 int (-5)

#include 
#include 
#include 
#include 
#include 
#include 
#include #define NAME "led_dev"struct class *cls = NULL;
struct device *dev = NULL;// 定义宏名代替物理地址
#define RED_BASE 0xc001a000
#define GREEN_BASE 0xc001e000
#define BLUE_BASE 0xc001b000unsigned int major = 0; // 主设备号
char kbuf[32] = "";
int ret;// 定义指针保存映射后得到的虚拟地址
unsigned int *red_addr = NULL;
unsigned int *green_addr = NULL;
unsigned int *blue_addr = NULL;int myopen(struct inode *inode_t, struct file *file_t)
{printk("%s %s %d\n", __FILE__, __func__, __LINE__);return 0;
}ssize_t myread(struct file *file_t, char __user *ubuf, size_t size, loff_t *off)
{printk("%s %s %d\n", __FILE__, __func__, __LINE__);if (size > sizeof(kbuf))size = sizeof(kbuf);ret = copy_to_user(ubuf, kbuf, size);if (ret != 0){printk("kernel copy data to user failed.\n");return -EINVAL;}return 0;
}ssize_t mywrite(struct file *file_t, const char __user *ubuf, size_t size, loff_t *off)
{printk("%s %s %d\n", __FILE__, __func__, __LINE__);// 将用户空间数据拷贝到内核空间if (size > sizeof(kbuf))size = sizeof(kbuf);ret = copy_from_user(kbuf, ubuf, size);if (ret != 0){printk("user copy to kbuf failed.\n");return -EINVAL;}if (kbuf[0] == 1) // 红灯*(red_addr) |= 1 << 28;else*(red_addr) &= ~(1 << 28);if (kbuf[1] == 1) // 绿灯*(green_addr) |= 1 << 13;else*(green_addr) &= ~(1 << 13);if (kbuf[2] == 1) // 蓝灯*(blue_addr) |= 1 << 12;else*(blue_addr) &= ~(1 << 12);return 0;
}int myclose(struct inode *inode_t, struct file *file_t)
{printk("%s %s %d\n", __FILE__, __func__, __LINE__);return 0;
}
// 点等法结构体赋值
struct file_operations fops = {.open = myopen,.read = myread,.write = mywrite,.release = myclose,
};static int __init mycdev_init(void)
{// 注册字符设备驱动(得到字符设备驱动的框架)major = register_chrdev(major, NAME, &fops);if (major < 0){printk("register_chrdev error.\n");return -EINVAL;}// 建立led灯操作物理地址和虚拟地址之间的映射// 1.映射红灯red_addr = (unsigned int *)ioremap(RED_BASE, 40);if (red_addr == NULL){printk("ioremap red failed.\n");return -EINVAL;}// 2.映射绿灯green_addr = (unsigned int *)ioremap(GREEN_BASE, 40);if (green_addr == NULL){printk("ioremap green failed.\n");return -EINVAL;}// 3.映射蓝灯blue_addr = (unsigned int *)ioremap(BLUE_BASE, 40); // 这里40个字节和36个字节都行if (blue_addr == NULL){printk("ioremap blue failed.\n");return -EINVAL;}// 初始化灯操作寄存器// 红*(red_addr + 9) &= ~(3 << 24);*(red_addr + 1) |= 1 << 28;*(red_addr) &= ~(1 << 28); // 初始灭// 绿*(green_addr + 8) &= ~(3 << 26);*(green_addr + 1) |= 1 << 13;*(green_addr) &= ~(1 << 13); // 初始灭// 蓝*(blue_addr + 8) &= ~(1 << 24);*(blue_addr + 8) |= 1 << 25;*(blue_addr + 1) |= 1 << 12;*(blue_addr) &= ~(1 << 12); // 初始灭// 设置自动创建设备节点// 向用户空间提交目录信息cls = class_create(THIS_MODULE, NAME);if (IS_ERR(cls)){printk("class create failed.\n");return PTR_ERR(cls);}// 提交文件信息dev = device_create(cls, NULL, MKDEV(major, 0), NULL, NAME);if (IS_ERR(dev)){printk("device create failed.\n");return PTR_ERR(dev);}return 0;
}static void __exit mycdev_exit(void)
{device_destroy(cls, MKDEV(major, 0));class_destroy(cls);// 取消映射,就是释放的时候是和映射时倒过来的iounmap(blue_addr);iounmap(green_addr);iounmap(red_addr);// 注销字符设备驱动unregister_chrdev(major, NAME);
}module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");#include 
#include 
#include 
#include 
#include int main(int argc, char const *argv[])
{int fd = open("/dev/led_dev", O_RDWR);if (fd < 0){perror("open error.");return -1;}char buf[32] = ""; // 让初始值全部初始化为0while (1){write(fd, buf, 32);buf[0] = 1;write(fd, buf, 32);sleep(1);buf[0] = 0;buf[1] = 1;write(fd, buf, 32);sleep(1);buf[1] = 0;buf[2] = 1;write(fd, buf, 32);sleep(1);buf[2] = 0;buf[0] = 1;buf[1] = 1;write(fd, buf, 32);sleep(1);buf[0] = 0;buf[1] = 1;buf[2] = 1;write(fd, buf, 32);sleep(1);buf[1] = 0;buf[2] = 0;}close(fd);return 0;
}

ioctl函数 ******思想非常重要
注:用户程序所作的只是通过命令码告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。
如:前边我们对灯的控制是一个或两个灯,如果我们有一万个灯。没有办法统一控制。
引入ioctl的原因,从用户的角度出发,用户只需要告诉你指令,底层如何实现和我无关。
(功能:input output 的控制)
 user: ioctl是应用层使用的函数,底层需要识别实现功能
#include
 int ioctl(int fd, int request, …);(RED_ON)
(让点灯的代码变得简洁)
参数://请求码和打开的文件有关
@fd : 打开文件产生的文件描述符
@request: 请求码(读写|第三个参数传递的字节的个数),(如开灯关灯)
:在sys/ioctl.h中有这个请求码的定义方式。
@… :可写、可不写,如果要写,写一个内存的地址

 Kernel:
(在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情;)
fops: //这个函数在注册设备的时候file_operations结构体中
long (*unlocked_ioctl) (struct file *file, unsigned int request, unsigned long args);
//request:就是应用层发下来的请求码,需要传对应类型值。我能识别的是 _IO(type,lr)
计算后的值。
对于使用ioctl函数时,主要的就是请求码的设计,请求码主要在sys/ioctl.h文件里面进行了设计。
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);老版本的内核用这个
unsigned int (*poll) (struct file *, struct poll_table_struct *);//应用层的poll、epoll调用的是底层的poll函数。
//从宏定义可以看出,底层已经和应用层有了对应控制规定,相当于定义 了标志
在这里插入图片描述
在这里插入图片描述

表示我本次是读还是写的字节的大小;再往下看当调用_IOC的时候怎样把四个域组合在一起的。
在这里插入图片描述

一个一个看,鼠标放在_IOC_DIRSHIFT,进行跳转,出现下面的同学

在这里插入图片描述

#define _IO(type,nr)		_IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)	_IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))

#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define RDE_LED _IO(type,nr)
如果不涉及用copy_to_user和copy_from_user应用层和底层数据传递,单纯只是点灯,就用_IO最简单的就可以。
这些宏是帮助你完成请求码的封装的。

在这里插入图片描述

	#define _IOC(dir,type,nr,size) \(((dir)  << _IOC_DIRSHIFT) | \((type) << _IOC_TYPESHIFT) | \((nr)   << _IOC_NRSHIFT) | \((size) << _IOC_SIZESHIFT))		

dir << 30 | size<<16 | type << 8 | nr << 0
2 14 8 8
方向 大小 类型 序号
(方向:00 01 10 11读写相关,)
(大小:sizeof(变量名))
(类型:组合成一个唯一的不重合的整数,一般传一个字符)
(序号:表示同类型中的第几个,当开灯的时候写0,那关的时候就不写0)。
详解:https://blog.csdn.net/JCfyw/article/details/116349738
//通过一个数的某几位控制内容不同,来实现不同的操作命令
#define RLED_ON _IOWR(‘a’,0,int)//亮灯
#define RLED_OFF _IOWR(‘a’,1,int) //灭灯
//ioctl之后,read和write已经可以不用了,他们可以识别命令,这相当于进程间通信IPC通信中获取唯一key值一样,这的0 1开关灯也需要通过宏函数获取一个唯一操作指令。
经使用的命令码的域在如下文档中已经声明了。
vi kernel-3.4.39/Documentation/ioctl$ vi ioctl-number.txt
(2^32次方 = 4G的数字,所以可以使用,内核的想法是:每一个数字代表一个,功能和数字一一对应,但是不一样的驱动使用的时候相同也是可以的)
详解:https://blog.csdn.net/JCfyw/article/details/116349738
练习:

  1. ioctl函数的使用

用ioctl的实现电灯思路:站在用户角度

  1. ioctl实现点灯,首先需要输入命令码(应用层可以随便设计),但是命令码对应操作需要给底层发送对应的数字,这个数字不能随便发送,因为底层需要识别,那我使用的时候需要根据底层的提示传送对应的值。
  2. 编写驱动时:首先写应用层代码,调用ioctl。底层根据应用层输出做出对应操作。

在这里插入图片描述

 Linux内核中断
Eg:
ARM里当按下按键的时候,他首先会执行汇编文件start.s里面的异常向量表里面的irq,在irq里面进行一些操作,再跳转到C的do_irq(); //Linux内核和裸机特点一样,只是做了一些操做,需要跟着linux框架才能操作。通用的东西,系统会先自己写好。
进行操作:1)判断中断的序号;2)处理中断;3)清除中断;
Linux内核实现和ARM逻辑实现中断的原理是一样的。
内核:当按键按下后依然到异常向量表,再到handler_irq函数(写死的),在handler_irq里面定义了一个数组,数组中每个成员里面存放的是结构体,在结构体里面有个函数指针,这个函数指针就指向了咱们自己提交函数的名字;(数组的下标是Linux内核的软中断号,它和硬件中断号之间有个映射关系)。内核实现中断时,在handler_irq函数里面把中断的寄存器都初始化好了,咱们只需要拿到软中断号,绑定我的中断处理函数就可以
//终端寄存器都自己映射好了,你只需要在这个中断来你做什么样的操作就可以了
 int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char name, void dev)
功能:注册中断
参数:
@irq : 软中断号 (对于中断,如:按键,当按这个按键产生一个中断,我们知道按键所对应的真实的物理CPIONO号,即需要将真实的物理GPIONO号映射到内存中)
gpio的软中断号
软中断号 = gpio_to_irq(gpiono号); //将GPIONO号转为软中断号
gpiono = m
32+n(n:组内的序号)//GPIONO号 的计算方式 针对于按键
m:那一组 A B C D E(5组)
0 1 2 3 4
gpioa28 = 0
32+28
gpiob8 =132+8 gpiob16 = 132+16
Gpioc5=2*32+5

使用中断步骤:

  1. 先注册中断(有这个中断)
  2. 找到GPIONO号

控制器中断号(ADC):(控制器对应的中断、内核中已经给你定义好了)
find -name irqs.h(在内核源码中找)
find -name irqs.h ./arch/arm/mach-s5p6818/include/mach/irqs.h
find -name s5p6818_irq.h ./arch/arm/mach-s5p6818/include/mach/s5p6818_irq.h
#define IRQ_PHY_ADC (41 + 32) //IRQ_PHY_ADC软中断号
@handler: 中断的处理函数 //*****这个函数中实现逻辑
在这里插入图片描述

irqreturn_t (*irq_handler_t)(int irqno, void *dev);
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

		IRQ_NONE        //中断没有处理完成   IRQ_HANDLED     //中断正常处理完成//中断处理函数返回的时IRQ_NONE表示中断没有处理完成@flags :中断的触发方式

在这里插入图片描述
在这里插入图片描述

		#define IRQF_DISABLED	0x00000020 //快速中断(在处理函数里面写了他,就先处理这个中断)#define IRQF_SHARED		0x00000080     //共享中断(中断的接口较少,但是器件都想要中断,那管脚需要外接两个,寄存器里面有中断状态标志位,看中断状态标志位有没有置位。一个口不可以链接两个按键,按键没办法区分)#define IRQF_TRIGGER_RISING	0x00000001(上升沿触发)#define IRQF_TRIGGER_FALLING	0x00000002(下降沿触发)#define IRQF_TRIGGER_HIGH	0x00000004#define IRQF_TRIGGER_LOW	0x00000008@name :名字   cat /proc/interrupts@dev  :向中断处理函数中传递参数 ,不想传就写为NULL
返回值:成功0,失败返回错误码

 void free_irq(unsigned int irq, void *dev_id)
功能:注销中断
参数:
@irq :软中断号
@dev_id:向中断处理函数中传递的参数,不想传就写为NULL
 Eg:按键所对应的中断号是多少?及找所对应的GPIO;
 第一步:找底板原理图,找到按键
在这里插入图片描述

 第二步:拷贝网络标号,到核心板
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

及对应的软中断号为:gpio_to_irq (gpiob8 = 132+8);gpio_to_irq (gpiob16 = 132+16)
ARRAY_SIZE计算数组里面元素的个数; //代码完成后,按键一次可能会打印多次,硬件设备按键一次可能会设别多次
在这里插入图片描述
在这里插入图片描述

 问题解决方法
[root@farsight]#insmod farsight_irq.ko
[ 21.262000] request irq146 error
insmod: can’t insert ‘farsight_irq.ko’: Device or resource busy
在这里插入图片描述

	通过 cat /proc/interrupts146:        GPIO  nxp-keypad154:        GPIO  nxp-keypad说明中断号已经被占用了

 解决办法:在内核中将这个驱动删掉
如何确定驱动文件的名字是谁?
grep “nxp-keypad” * -nR
在这里插入图片描述

arch/arm/mach-s5p6818/include/mach/devices.h:48:
#define DEV_NAME_KEYPAD  "nxp-keypad"

grep “DEV_NAME_KEYPAD” * -nR

在这里插入图片描述

drivers/input/keyboard/nxp_io_key.c:324:		.name	= DEV_NAME_KEYPAD,
驱动文件的名字是nxp_io_key.c

在这里插入图片描述

找宏的名字,在Makefine里面知道;
在这里插入图片描述
在这里插入图片描述

如何从内核中将他去掉?
选项菜单的名字?Kconfig
config KEYBOARD_NXP_KEY
tristate "SLsiAP push Keypad support"make menuconfig<>SLsiAP push Keypad support

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

去掉图形化界面里面的*号后,可以把nxp_io_key.o删除掉,这样再次编译内核的时候就可以看出来nxp_io_key.c是否备编译,如果被编译就有对应的.o生成,如果不被编译,就不会生成nxp_io_key.o文件。
make uImage 重新编译内核
在这里插入图片描述
在这里插入图片描述

	cp  arch/arm/boot/uImage ~/tftpboot

重新启动板子;
安装驱动:
在这里插入图片描述

然后按键,进行测试;

在这里插入图片描述

eg:

  1. 按下按键将红灯状态取反
  2. 尝试使用定时器将中断的抖动消除
    为什么不用sleep(1)消抖?
    Linux内核定时器 (按键一次打印多次,消抖)
     定时器的当前时间如何获取?
    jiffies:内核时钟节拍数
    jiffies是在板子上电这一刻开始计数,只要
    板子不断电,这个值一直在增加(64位)(工业上直接使用,可以在设置板子时进行修改)。在驱动代码中直接使用即可。
    //每次当定时器中断发生时,内核内部通过一个64位的变量jiffies_64做加一计数

 定时器加1代表走了多长时间?
在内核顶层目录下有.config
CONFIG_HZ=1000
周期 = 1/CONFIG_HZ
周期是1ms;//1000ms=1s
//总步骤:有对象、初始化、添加对应逻辑、操作完成注销
 分配的对象
struct timer_list mytimer;
 对象的初始化
struct timer_list {
unsigned long expires; //定时的时间
void (*function)(unsigned long); //定时器的处理函数
unsigned long data; //向定时器处理函数中填写的值
};
void timer_function(unsigned long data) //定时器的处理函数
{

	}//初始化自己需要的三个:mytimer.expries = jiffies + 1000;  //1smytimer.function = timer_function;mytimer.data = 0;init_timer(&mytimer);  //内核帮你填充你未填充的对象	

 对象的添加定时器
void add_timer(struct timer_list *timer);
//同一个定时器只能被添加一次,
//在你添加定时器的时候定时器就启动了,只会执行一次

	int mod_timer(struct timer_list *timer, unsigned long expires)//再次启动定时器                          jiffies+1000

 4.对象的删除
int del_timer(struct timer_list *timer)
//删除定时器

  Int gpio_get_value(int gpiono);//通过gpiono获取当权gpio的所处状态

注:定时器的申请需要放在中断申请之前。重启定时器在检测到下降沿就可以重启。
当检测到下降沿进行中断处理,调用中断处理函数,同时检测到下降沿还需要重启定时器,所以重启定时器应该放在中断处理函数中。
定时器处理函数中检测是否是低电平,再进行对应操作。定时时间到检测一次。

模块导出符号表
思考1:应用层两个app程序,app1中拥有一个add函数,app1运行时app2是否可以调用app1中的add函数? 不行,因为应用层app运行的空间是私有的(0-3G)没有共享。
思考2:两个驱动模块,module1中的函数,module2是否可以调用?可以,他们公用(3-4G)内核空间,只是需要找到函数的地址就可以。好处:减少代码冗余性,代码不会再内存上被重复加载。代码更精简,一些代码可以不用写,直接调用别人写好的函数就可以。
编写驱动代码找到其他驱动中的函数,需要用模块导出符号表将函数导出,被人才可以使用这个函数。他是一个宏函数。
在驱动的一个模块中,向使用另外一个模块中的函数/变量,只需要使用EXPORT_SYMBOL_GPL将变量或者函数的地址给导出。使用者就可以用这个地址来调用它了。
EXPORT_SYMBOL_GPL(sym)
sym:变量名或函数名
代码举例1:两个独立的代码驱动模块
在这里插入图片描述

代码举例2:提供者为内核已经安装使用的驱动
在这里插入图片描述

总结:
编译:
1.先编译提供者,编译完成之后会产生一个Module.symvers
在这里插入图片描述

2.将Module.symvers拷贝到调用者的目录下
3.编译调用者即可

安装:
先安装提供者
再安装调用者
卸载:
先卸载调用者
再卸载提供者
如果调用者和提供者时两个独立(xx.ko)驱动模块,他们间传递地址的时候,是通过Module.symvers传递的。
如果提供者是内核的模块(uImage),此时调用者和提供者间就不需要Module.symvers文件传递信息。

补充-裸机实现按键中断
中断
按键中断实验:
1.打开按键原理图。
在这里插入图片描述

2.核心板
在这里插入图片描述

由于我们需要将外边的信号输入到芯片中,即将GPIOB8设置伪GPIO功能,同时设置为输入功能。
3.芯片手册
在这里插入图片描述

设置信号检测为下降沿触发中断,使能引脚检测功能。
在这里插入图片描述

将信号发给cpu(使能中断功能)。
在这里插入图片描述

按键产生中断信号代码部分:
在这里插入图片描述

中断管理器GIC:可以控制产生160种中断
中断是外部的硬件给cpu发送的一个信号,让cpu停下来,先完成自己的事情。如: 按键打断cpu让音响声音放大。

解决问题:产生的中断不可能直接给cpu运行的。
–》1.如果外部硬件同时产生中断,cpu没有办法同时处理。
2.cpu正在处理一个信号,现在其他硬件又产生了中断,这个中断信号cpu无法接收处理。
3.一个中断产生,如果有多个cpu,送个那个cpu。
4.中断信号送给cpu,cpu无法识别是那种中断。
即:产生的中断是不能直接送给cpu处理的,而是在中断管理器GIC中拦截注册分配优先级排队等待,中断管理器中给160种中断进行了优先级划分, 可以通过寄存去重新自己配置优先级,这个优先级只决定排队,高优先级不能打断低优先级的中断。可以任意打断关闭中断,不给cpu送过去。
/* 中断管理器-介于外部设备和CPU之间,能检测和接收外部设备产生的中断信号
并且对所有的中断信号进行统一管理和协调并将其转发给合适的CPU去处理*/

1.多个外设同时产生中断信号时中断管理器可以对多个信号进行排队,优先级高的先转发给CPU,优先级低的继续排队
2.中断管理器可以为每一个中断分配一个优先级
3.一个中断正在处理的同时另一个外设产生了中断信号中断管理器可以先将其进行注册等待,等上一个处理完成后再将其转发给CPU处理
4.中断管理器可以给每一个外设产生的中断信号选择一个合适的CPU来处理
5.CPU在接收到中断信号后并不知道是哪个外设产生的,这时CPU可以通过查询中断管理器来获取中断信息
6.任意打开或者关闭一个中断
… …
了解–》

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

重点

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

查看寄存器写代码:

情况1:led在一直闪烁着的情况下遇到中断灯灭,处理中断,完成之后会返回main函数继续执行main函数,但是发现一直在重复的执行中断处理。原因:
在这里插入图片描述

代码如下
#include"s5p6818.h"

void Delay(unsigned int Time)
{
unsigned int i,j;
for(i=0;i for(j=0;j<2000;j++);
}

void do_irq(void)
{
printf(“This is GPIOB8\n”);
Delay(1000);
/清除中断标志位,防止处理完中断后外设还向中断管理器发送中断信号,写1清0,写0不变/
GPIOB.DET = (1 << 8);//GPIOxDET
}
int main()
{
/外设层次 - 设置对应的外设(硬件),使其产生一个有效的中断信号/
/1.将GPIOB8设置成GPIO功能 GPIOB.ALTFN0/
GPIOB.ALTFN0 = GPIOB.ALTFN0 & (~(0x3 << 16));
/2.将GPIOB8设置成input功能 GPIOB.OUTENB/
GPIOB.OUTENB = GPIOB.OUTENB & (~(1 << 8));
/3.将GPIOB8的检测模式设置成检测下降沿 GPIOB.DETMODEEX + GPIOB.DETMODE0/
GPIOB.DETMODEEX = GPIOB.DETMODEEX & (~(1 << 8));
GPIOB.DETMODE0 = GPIOB.DETMODE0 & (~(0x3 << 16)) | (0x2 << 16);
/4.使能GPIOB8引脚的检测功能 GPIOB.DETENB/
GPIOB.DETENB = GPIOB.DETENB | (1 << 8);
/5.使能GPIOB8检测到有效信号后(下降沿)产生中断信号 GPIOB.INTENB/
GPIOB.INTENB = GPIOB.INTENB | (1 << 8);

/*中断管理器层次 -* 一方面检测和接收外部设备产生的中断信号,并对其统一管理排队,另一方面将其转发给CPU*/
/*6.全局使能中断管理器接收第0组的中断信号*/
GICD_CTRL |= 0x1;
/*7.在中断管理器中使能86号中断,使其能够转发到对应的CPU*/
GICD_ISENABLER.ISENABLER2 = GICD_ISENABLER.ISENABLER2 | (0x1 << 22);
/*8.在中断管理器中设置86号中断转发给CPU0处理*/
GICD_ITARGETSR.ITARGETSR21 = GICD_ITARGETSR.ITARGETSR21 & (~(0xFF << 16)) | (0x1 << 16);
/*9.使能中断信号能通过CPU接口连接到CPU*/
GICC_CTRL |= 0x01;/*初始化LED*/
GPIOA.ALTFN1 = GPIOA.ALTFN1 & (~(0x3 << 24)) | (0x0 << 24);
GPIOA.OUTENB = GPIOA.OUTENB | (1 << 28);
while(1)
{GPIOA.OUT = GPIOA.OUT | (1 << 28);Delay(200);GPIOA.OUT = GPIOA.OUT & (~(1 << 28));Delay(200);
}
return 0;

}
只要出现产生中断,不同的中断信号来,都会调转到do_irq执行这个中断处理函数,如何区分是哪一类中断信号?读寄存器GICC_IAR --》cpu不知道是那种中断信号,但是中断管理器转过来的中断信号它知道。

代码部分如下:

情况2:当第一次接收到中断信号,读GICC_IAR判断是那种中断进行处理,再次来中断不识别。–》原因:刚开始不出现这个情况是因为GICC_IAR没有读那种中断信号,所以中断处理器认为这个中断信号没有处理,继续给cpu送信号,而现在读了,中断处理器认为你还在处理这个中断还在忙。
中断处理器只负责送信号,不知道cpu有没有处理这个信号。但是cpu现在问中断处理器处理的是几号信号,这个时候中断处理器就认为你一直在处理这个信号,再来这个信号也不会往cpu送。哪怕cpu已经把这个信号处理完了。
代码部分如下:

最终代码中断管理器
#include"s5p6818.h"
void Delay(unsigned int Time)
{
unsigned int i,j;
for(i=0;i for(j=0;j<2000;j++);
}
void do_irq(void)
{
unsigned int IrqNum;
/从中获取当前的中断的中断号,从该指令执行后中断管理器就认为CPU要处理中断,所以就不再送入新的中断/
IrqNum = GICC_IAR & 0x000003FF;

switch(IrqNum)
{case 0:/*0号中断处理程序*/break;case 1:/*1号中断处理程序*/break;/* ... ... */case 86:   printf("This is GPIOB8\n");Delay(1000);/*清除中断标志位,防止处理完中断后外设还向中断管理器发送中断信号,写1清0,写0不变*/GPIOB.DET = (1 << 8);break;/* ... ... */case 159:/*159号中断处理程序*/break;/* ... ... */
}
/*告诉中断管理器当前的中断已经处理完成,可以送入新的中断*/
GICC_EOIR = GICC_EOIR & (~(0x3FF)) | IrqNum;

}

int main()
{
/外设层次 - 设置对应的外设(硬件),使其产生一个有效的中断信号/
/1.将GPIOB8设置成GPIO功能 GPIOB.ALTFN0/
GPIOB.ALTFN0 = GPIOB.ALTFN0 & (~(0x3 << 16));
/2.将GPIOB8设置成input功能 GPIOB.OUTENB/
GPIOB.OUTENB = GPIOB.OUTENB & (~(1 << 8));
/3.将GPIOB8的检测模式设置成检测下降沿 GPIOB.DETMODEEX + GPIOB.DETMODE0/
GPIOB.DETMODEEX = GPIOB.DETMODEEX & (~(1 << 8));
GPIOB.DETMODE0 = GPIOB.DETMODE0 & (~(0x3 << 16)) | (0x2 << 16);
/4.使能GPIOB8引脚的检测功能 GPIOB.DETENB/
GPIOB.DETENB = GPIOB.DETENB | (1 << 8);
/5.使能GPIOB8检测到有效信号后(下降沿)产生中断信号 GPIOB.INTENB/
GPIOB.INTENB = GPIOB.INTENB | (1 << 8);

/*中断管理器层次 -* 一方面检测和接收外部设备产生的中断信号,并对其统一管理排队,另一方面将其转发给CPU*/
/*6.全局使能中断管理器接收第0组的中断信号*/
GICD_CTRL |= 0x1;
/*7.在中断管理器中使能86号中断,使其能够转发到对应的CPU*/
GICD_ISENABLER.ISENABLER2 = GICD_ISENABLER.ISENABLER2 | (0x1 << 22);
/*8.在中断管理器中设置86号中断转发给CPU0处理*/
GICD_ITARGETSR.ITARGETSR21 = GICD_ITARGETSR.ITARGETSR21 & (~(0xFF << 16)) | (0x1 << 16);
/*9.使能中断信号能通过CPU接口连接到CPU*/
GICC_CTRL |= 0x01;/*初始化LED*/
GPIOA.ALTFN1 = GPIOA.ALTFN1 & (~(0x3 << 24)) | (0x0 << 24);
GPIOA.OUTENB = GPIOA.OUTENB | (1 << 28);while(1)
{GPIOA.OUT = GPIOA.OUT | (1 << 28);Delay(200);GPIOA.OUT = GPIOA.OUT & (~(1 << 28));Delay(200);
}
return 0;

}

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...