2021.10.8:启动方式

有很多同学在移植u-boot时,都会对s3c2440从Nandflash启动的过程非常迷惑。这里发这个帖子给大家介绍一下它的启动流程。

大部分ARM9的CPU内部都集成有一个SRAM,SRAM是英文Static RAM的缩写,它是一种具有静止存取功能的内存,不需要刷新电路即能保存它内部存储的数据。这样他不需要初始化就能够直接使用。这与我们在外部扩展的大容量的SDRAM是不一样的,外部大容量的SDRAM是需要初始化后才能使用的,这点大家务必要搞清楚。这点在我做过移植的处理器:s3c2410(arm920t),s3c2440(arm920t),at91rm9200(arm920t),at91sam9260(arm926t)上都是这样的。在s3c2440这颗CPU上这个SRAM大小为4KB,datasheet里把它叫做Stepping Stone,江湖人称“起步石”。

Nandflash和Norflash是不同的:Norflash像内存一样是直接挂在系统总线上的,这样有足够多的地址线使得CPU能够寻址到每一个存储单元上去,这也意味着CPU能够直接通过总线访问Norflash上存储的内容,同时他还支持XIP(即片上执行,不用将代码搬到内存中,直接在Norflash上就能运行)。 而Nandflash它并不是直接挂载系统总线上,而是通过Nandflash控制器(这个一般集成在CPU内部)来完成读写操作的。如果我们把Norflash的那种寻址方式叫直接寻址的话(不是汇编里的那个直接寻址,这里指CPU能够直接通过地址线访问存储器的存储单元),那么这里的Nandflash就是间接寻址(这里需要Nandflash控制器来寻址)。所以我们在使用Nandflash之前,一定要初始化Nandflash控制器。

理解上面的这点后,就不难理解,为什么系统能够从Norflash直接启动,而不能直接从Nandflash启动。这是因为,ARM在CPU复位时,CPU默认会到0x0000 0000地址处去取指令,而如果我们是从Norflash启动的话(一般Norflash会挂到Bank0,nGCS0上),s3c2440 CPU就会把Norflash的空间挂接到0x0000 0000这段内存空间上。这时CPU就能够直接从Norflash上取指令运行,启动了。而如果是Nandflash, 因为Nandflash他不能直接挂到系统总线上,并且他的读写,擦除操作必须依赖Nandflash控制器,这也就意味着Nandflash的存储空间永远不能映射到0x0000 0000这个地址上去。另外,Nandflash的读写操作也不是这样直接寻址的,有兴趣的同学可以自己看看Nandlfash的datasheet,写一个RAW的Nandflash 擦除,读写操作程序就明白了。我就写过这么一个程序,对理解Nandflash究竟是怎么操作的非常有帮助。

而如果这些CPU要从Nandflash上启动,那该怎么办呢?这就要用到我之前提到的CPU的内部SRAM了。在S3C2440的datasheet里有提到,如果我们配置从Nandflash启动的话,那么CPU会自动将内部SRAM的地址映射到0x0000 0000这个地址空间上了,而如果不是从Nandflash启动,那么挂载Bank0(nGCS0)上的设备就会被映射到0x0000 0000地址空间上,如我们之前提到的Norflash。

简而言之就是:如果从Nandflash启动,那么CPU内部SRAM被映射到0x0000 0000地址空间上,这时Norflash就不可用了。而如果是从Norflash启动的话,那么Norflash被映射到0x0000 0000地址空间上。我们之前提到ARM CPU在复位时,会默认到0x0000 0000地址上取指令。这样也就是如果从Nandflash启动的话,那么CPU默认会从内部SRAM中取第一条指令;而如果从Norflash启动的话,那么CPU默认从Norflash中取第一条指令。

那如果从SRAM启动的话,那么SRAM中的指令(也就是代码)从哪里来的呢?在s3c2440处理器(arm920t和arm926t的核应该都是这样的,另外我看S3C6410也是如此)上电时,CPU会自动将Nandflash的前4K代码(或叫指令)拷贝到内部SRAM中,这是由CPU自动完成的,不需要我们干预。这也就意味着,SRAM中的内容就是我们Nandflash上前4K的代码了。

因此,我们的bootloader,如u-boot中就要确保前4K代码完成以下功能:
        1, 初始化CPU,外部SDRAM,Nandflash控制器等基本功能;
        2, 将Nandflash上剩余的u-boot代码拷贝到外部的SDRAM中
        3, 调到外部的SDRAM中来运行u-boot代码。

这样,U-boot就启动了。
   
综上所述:
s3c2440支持两种启动模式:NAND和非NAND(这里是nor flash)。
具体采用的方式取决于OM0、OM1两个引脚

OM[1:0]所决定的启动方式

OM[1:0]=00时,处理器从NAND Flash启动

OM[1:0]=01时,处理器从16位宽度的ROM启动

OM[1:0]=10时,处理器从32位宽度的ROM启动。

OM[1:0]=11时,处理器从Test Mode启动。

1、当引脚OM0跟OM1有一个是高电平时,这时地址0会映射到外部nGCS0片选的空间,也就是Norflash,程序就会从Norflash中启动,arm直接取Norflash中的指令运行。

2、当OM0跟OM1都为低电平,则0地址内部bootbuf(一段4k的SRAM)开始。系统上电,arm会自动把NANDflash中的前4K内容考到bootbuf(也就是0地址),然后从0地址运行。

Arm的启动都是从0地址开始,所不同的是地址的映射不一样。在arm开电的时候,要想让arm知道以某种方式(地址映射方式)运行,不可能通过你写的某段程序控制,因为这时候你的程序还没启动,这时候arm会通过引脚的电平来判断。

1) 从NorFlash启动时,与nGCS0相连的NorFlash就被映射到nGCS0片选的Bank0空间,其地址被映射为0x0000 0000;

2) 从NandFlash启动时,S3C2440芯片内部自带的一块容量为4K的被称为“Steppingstone”(起步石)的BootRAM被映射到nGCS0片选的Bank0空间,其地址被映射为0x0000 0000。当系统上电或复位时,程序会从0x0地址处开始执行,因此我们编写的启动代码要确保存储在0地址处。如果系统的所有程序在编译链接后的大小小于4K,那在系统的启动代码中无需考虑将程序从NandFlash搬运到SDRAM这个问题,因为所有的程序在启动时即全部由NandFlash拷贝至BootSRAM,程序在BootSRAM中运行即可;如果系统的所有程序在编译链接后的大小大于4K,那在系统的启动代码中就需要包含一段将系统的全部程序从NandFlash搬运到SDRAM的代码,因为系统启动时只将NandFlash的前4K拷贝到了BootSRAM中,还有部分程序在NandFlash中,而程序在NandFlash中是无法运行的,需要将所有程序拷贝至SDRAM并在其中运行,所以系统的启动代码中要包含这段有关程序拷贝的代码,并在所有程序拷贝完成后使程序跳转到SDRAM中运行。也就是说NandFlash启动时需要考虑到涉及的两次搬移,第一次搬运是S3C2440硬件机制自动实现的,无需干预,第二次搬运需要程序员来实现,搬运程序量大小是系统的所有程序。

NANDFLASH启动流程如图所示:

2021.10.9:衔接课程-->构建根文件系统-->004节

在用nfs网络挂载根文件系统的时候先在u-boot下设置bootargs。

①set bootargs noinitrd root=/dev/nfs nfsroot=192.168.10.100:/work/nfs_root/second_fs(空格)ip=192.168.10.150:192.168.10.100:192.168.10.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0

②save

其中ip=192.168.10.150:192.168.10.100:192.168.10.1:255.255.255.0::eth0

我猜测是用来设置所挂载的文件系统的eth0的ip地址等等,因为挂载成功后可以看到串口打印出:

实际上更加详细正规的说明在linux源码目录下的Documentation/nfsroot.txt,我懒就不去看了。

第二期-->字符驱动设备

cat /proc/devices目录下列举出了当前系统支持的设备。

自动创建设备节点的方法:

static struct class *firstdrv_class;
static struct class_device  *firstdrv_class_dev;firstdrv_class = class_create(THIS_MODULE, "firstdrv"); //创建类firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* 创建设备 /dev/xyz */

为什么可以自动创建设备节点:创建根文件系统时在rcS命令下使用了:echo /sbin/mdev > /proc/sys/kernel/hotplug,就是热插拔的意思。在驱动入口函数我们创建了类,并且在类中创建了设备(设备名xyz,设备号是major),我们的目的并不是去使用驱动入口函数中创建的类或设备,而是让相应的信息更新到/sys目录下(我们在类中创建的设备名是xyz,主设备号是major,他们实际上都是真正要创建的设备节点的信息,如上图/sys/class/firstdrv/xyz/dev的major=252、minor=0等)。当我们的注册驱动时相当于热拔插的动作,此时mdev机制就会采集/sys目录下的更新的信息,根据采集到的信息自动创建设备节点。同理卸载驱动。具体mdev机制是如何实现的我们暂且不去深究,只需了解大致的流程。

2021.10.10:

request_irq(irq中断号,handle中断处理函数,flags触发方式,name中断名字,dev_id)

①分配一个irqaction结构,这个结构体里面的参数就用参数填充

②把这个irqaction结构放入irq_desc[]这个数组项里面的action链表里面

③设置引脚

④使能中断

free_irq(irq中断号,dev_id)

①由irq中断号结合dev_id确定要卸载的irqaction结构,将他从irq_desc[]这个数组项里面的action链表里面删除。dev_id的详细作用以后再分析,好像有点复杂,暂且理解为同一个中断的不同的处理函数必须使用不同的dev_id区分。

②如果这个action链表中还有其他的irqaction结构的话(共享中断,比如一个引脚可以被多种中断共同使用),就不会禁止中断,如果链表空了,就会禁止这个中断。

Linux内核的中断框架现在对我来说还太过于复杂,有以上大概的抽象认知即可。

2021.10.11:第二期-->第12课-->字符驱动设备

一、在用户空间应用程序向驱动程序请求数据时,有以下几种方式:

1、不断查询,条件不满足的情况下就是死循环,非常耗cpu

2、休眠唤醒的方式,如果条件不满足,应用程序则一直睡眠下去

3、poll机制,如果条件不满足,休眠指定的时间,休眠时间内条件满足可以被唤醒继续往下执·     行,条件一直不满足则设定的超时时间到达被自动唤醒。

4、用程序注册信号处理函数,驱动程序发信号异步通知。

二、异步通知的流程:

①谁来发?--> 驱动程序发 --> kill_fasync(&button_async, SIGIO, POLL_IN);

参数button_async:包含接收者的进程id等,也就是包含要发给的接受对象的信息。

参数SIGIO:要发的信号。

参数POLL_IN:发送信号的原因。

②发给谁?    --> APP(接收者)

③怎么告诉驱动?--> APP中使用这三句话来设置异步通知的标志,来告诉驱动程序你要发给我。

 fcntl(fd,F_SETOWN,getpid());         //获取当前进程的pidOflags = fcntl(fd,F_GETFL);          //读出当前的标志fcntl(fd,F_SETFL, Oflags | FASYNC);  //设置当前进程为异步通知

执行fcntl(fd,F_SETFL, Oflags | FASYNC);时会调用到驱动中file_operations结构体中的.fasync   = xxx(), 进而在xxx()中会调用fasync_helper(fd,filp,on,&button_async);函数,这个函数会将APP的信息如pid等写入button_async结构体,也就和①中的参数button_async对应上了。

④APP注册信号处理函数 --> signal(SIGIO,my_signal_fun);  当接收到驱动发过来的信号为SIGIO时,就会执行所注册的信号处理函数my_signal_fun();

三、同步与互斥:

1、原子操作:使用内核提供的接口函数来访问原子变量,可以保证该访问过程是一个原子过程。

2、自旋锁:是一个二值的锁   0 1;是一个等待锁,当一个进程已经获得了自旋锁,另外一个进程也获得该自旋锁,则第二进程就会“原地自旋”,直到第一个进程释放该自旋锁;不是睡眠锁,不会产生阻塞。

3、信号量:是一个多值的锁;当一个进程得到信号量就可以访问临界资源,信号量的值就会减1。当信号量间减到0,再有进程想获得信号量,该进程就会产生阻塞,进入睡眠状态,当有人释放信号量的时候,按先后顺序唤醒睡眠的进程。

4、互斥锁:又叫互斥体,是信号量的特例;是一个二值的信号量,只有上锁和解锁两个状态;信号量有的特性,互斥锁也有;当一个进程得不到互斥锁(信号量),就会产生阻塞,进入睡眠状态。睡眠在该互斥锁的等待队列。当该进程得到了互斥锁,就会进入运行队列,在运行队列中等待系统的调度。

-->从3、4的描述中可以看出互斥锁和信号量很像,他们的具体异同日后再说。

2021.10.12:第二期-->第13课-->输入子系统input(按键为例)

①整体框架

软件层面(evdev.c/keyboard.c/mousedev.c)向核心层“ input.c”注册“ handler”(input_register_handler),这一边代表“软件”;另一边是“设备层面”,向核心层 “input.c”注册“device”(input_register_device),这一边代表“硬件”。

 从上图可知,不管是先注册软件层面的“handler”还是硬件层面的“device”。最终都会成对的调用“input_attach_handler()”。此函数会判断二者是否有能够支持的对象,有的话会建立连接。换句话说就是:注册 input_dev 或 input_handler 时,会两两比较硬件层面的input_dev 和软件层面的 input_handler,根据 input_handler 的 id_table 判断这个 input_handler 能否支持这个 input_dev,如果能支持,则调用 input_handler 的 connect 函数建立"连接。

②connect 函数具体如何建立连接: 

③建立连接后如何读数据,比如“按键值”:
应用程序来读,最终会导致与之建立连接的那个“软件层面”的“handler”里面的“读函数”被调用。如:“evdev.c”中的“evdev_handler”结构里面的成员“.fops=&evdev_fops”,在“evdev_fops”结构中有一个“读”函数“evdev_read。

搜索“evdev->wait”后找到在“evdev_event()”中有唤醒操作,函数是结构“evdev_handler”的“.event”事件成员。所以谁调用“evdev_event():

④谁调用“evdev_event():


也即:硬件相关的代码来唤醒睡眠,input_dev那层调用的。比如读取按键值,在设备的中断服务程序里,确定事件是什么,然后调用相应的input_handler的event处理函数上报键值,唤醒睡眠。

⑤系统注册input子系统时对应的file_operations结构体中为何只有一个open函数?

输入子系统的代码在drivers/input.c 这个 c 文件中。看一个驱动程序是从“入口函数”开始查看。

 分析“int input_open_file(struct inode *inode, struct file *file)”:

其中有一个“input_handler *” (“输入处理器”或“输入处理句柄” ):

这里这个“输入处理句柄”结构指向一个“input_table[]”数组。从这个数组里面根据这个“次设备号 iminor(inode) >> 5”找到一项给他赋值。


接着新的“ file_operations”结构"new_fops"等于上面的“ input_handler *”指针变量handler 的成员“ fops”(这是一个 file_operations 结构。 Input_handler 结构中有这个file_operations 成员)。

接着把这个新的 file_operations 结构赋给此函数“input_open_file”的形参“file”的 f_op。
然后再调用这个新的 file_operations 结构“new_fops”的 open(inode,file)函数,如下:

这样以后要来读按键时,用到的是“struct input_handler *handler”中的“new_fops”,也就是“input_table[]中与之匹配的handler的file_operations,里面各种函数齐全” ,也即Input.c中的open函数只是一个“中转”作用,并不是真的帮我们注册的设备只有一个open函数。

总结就是:我们的input的子系统,他的主设备号是13,我们知道一个设备,它对应的操作集,是由他的主设备号找到的,然而,我们的input的子系统下面所有的设备,他的主设备号都是13,那么不可能我主设备号是13,下面的所有次设备号都用一个操作集。那么我们怎么通过次设备号来区分不同的输入设备的操作集呢?答案就是把input子系统,也就是主设备号为13的那个操作集,只注册成一个open函数,以后我打开所有的input输入设备,首先都会通过主设备号13找到input注册的open函数,通过这个open函数,从一个事先注册好的input_table数组里面,通过次设备号取出对应的handler结构体,这个结构体里面就包含了真正这一类次设备号所需要的操作集了,里面read等函数齐全。简而言之,也就是说input里注册的file_operations那个唯一的open函数,只是一个中转站,用来更新成我们真正所需要用到的,根据次设备号配对的操作集file_operations。

⑥input_table[]的handler从何而来?为什么用次设备号匹配?用evdev.c举例:

回答问题1---> input_table[]的handler是注册handler的时候放进去的

看“evdev.c”中的注册过程:
static int __init evdev_init(void)-->return input_register_handler(&evdev_handler);-->input_table[handler->minor >> 5] = handler;  //evdev的次设备号是64
即:input_table[2] = &evdev_handler;

回答问题2---> 同一类input用不同的次设备号区分,但是最终都是用到同一个file_operations

input驱动程序的主设备号是13、次设备号的分布如下:

joystick游戏杆:0~16;mouse鼠标: 32~62;mice鼠标: 63;事件设备: 64~95

当我们设置好能产生哪一类input后,比如设置为事件设备,调用input_register_device注册一个新的事件设备的时候,就会从当前系统的事件设备的次设备号开始累加。比如第一个是系统自带的事件设备even0-->64,那么我们再次注册时候就会以65作为次设备号。

我们知道,应用层使用设备的第一步,是open(“/dev/event1”),因此这里event1的主设备号成为关键,因为主设备号将表明你是什么设备,我们知道一个设备是根据主设备号来找到对应的file_operations,输入命令cat /proc/devices查看主设备为13的是input设备,因此当我们的应用层执行open函数打开event1设备的时候,实际上就是调用input_init中注册的那个唯一的open函数,也即input_open_file(),这个open函数就会如上面分析的那样根据次设备号来更新成真正的操作函数齐全的file_operations

input.c--->input_open_file()中:
{input_handler *handler = input_table[iminor(inode) >> 5] = &evdev_handler;
-->  new_fops = fops_get(handler->fops)
即   new_fops = fops_get(&evdev_handler->fops);
也即 new_fops = fops_get(&evdev_handler->(fops=&evdev_fops));......
}

input_table[iminor(inode) >> 5] = &evdev_handler;(64~95)>> 5 都等于2。相当于事件设备都用了evdev_handler里面的evdev_fops操作集,里面open,read等函数齐全。假设我们注册了按键的事件设备,最终读取键值用到的就是evdev_handler里的read函数。理论上最多有32个事件设备。

⑥cat /dev/tty1为什么能从串口中显示键值?

如果没有启动 QT:
cat /dev/tty1
按:s2,s3,s4这种方式最后一定要按“回车”。就可以得到 ls

cat /dev/tty1 时,就用到了“tty”,这个更为复杂。tty1 的主设备号是 4,次设备号为 1.是通过“tty_io.c”中的驱动程序访问到“keyboard.c” ,“tty_io.c”远远比输入子系统要复杂。这里不分析。还是看下“keyboard.c”。从“入口函数”开始分析:

__init kbd_init(void) -->error = input_register_handler(&kbd_handler); //注册一个 input_handler 结构.

看这个“kbd_handler”中的“id_table”指明了它支持哪些设备。

.flags = INPUT_DEVICE_ID_MATCH_EVBIT, 中“MATCH”是指匹配哪些位,这里比较“EVBIT”。
只要这个输入设备,能够产生“EV_KEY”按键类事件,这个“keyboard.c"就能支持。

我们自已写的 buttons 驱动中支持“按键”类事件:

所以这个“keyboard.c”也支持我们的 buttons 驱动。当我们按下按键后,上报事件:这时就会从 input_device 结构的 h_list“链表”里面(会有多项 input_handle 结构)。
找出“evdev.c”的 handler 调用它的“input_handler”结构中的“event”函数。
找出“keyboard.c”的 handler 调用它的“input_handler”结构中的“event”函数。

如上为“keyboard.c”中 input_handler 结构变量 kbd_handler 中的"event"函数。在其中的“kbd_keycode()”或“kbd_rawcode()”代码中就和“tty”有了关系。可以看里面的代码有 tty。太复杂了就不深究了。因此cat /dev/tty1 时,不是从“输入子系统”中过来的,而是从 tty 有关的部分进来的。所以按键得到了键值就可以可以从串口显示了。

如果把标准输入改为“dev/tty1”,之前是从串口上看到(cat)按键值ls,但是并不会执行ls命令,现在是把串口的标准输入直接改成“ dev/tty1”

这时按按键“ls”和“回车”就可以当成在键盘,和在终端上敲写“ls"时一样显示目录下的文件与目录。

exec:通过exec将-sh进程下的描述符指向我们的驱动,exec 0</dev/tty1 //将本开发板的tty1(LCD)终端挂载到-sh进程下描述符0(标准输入),以后按下的键盘驱动就会打印在-sh进程上。
-sh:串口显示终端进程
-sh进程常用的命令调试描述符
0: 标准输入
1: 标准输出
2: 错误信息
5: 中断服务

12.Linux之输入子系统分析(详解) - 诺谦 - 博客园

⑦综上,怎么写符合一个输入子系统框架的驱动程序?
1. 分配一个input_dev结构体。
2. 设置这个结构体。如:能产生哪一类input、能产生这类input里的哪些东西......
3. 注册此结构体。
4. 硬件相关的代码,比如在中断服务程序里上报事件。

2021.10.14:第二期-->第14课-->驱动程序的分离分层概念_总线驱动设备模型

分离:把经常要更改的东西抽出来(硬件);把相对稳定的软件部分抽出来。

分层:如input.c 向上提供统一给 APP 操作的统一接口。每一层专注于自已的事件。

用platform驱动点灯来理解分离与分层:

 一、写“平台设备”:

①设置并注册一个platform_device结构体

static struct platform_device led_dev = {.name      = "my_led",.id           = -1,.num_resources    = ARRAY_SIZE(led_resources),.resource  = led_resources,.dev        = {   .release = led_dev_release,},
};
......
platform_device_register(&led_dev);    //注册这个平台设备

② .resource    = led_resources,中有led的硬件信息

static struct resource led_resources[] = {[0] = {.start    = 0x56000050,            //寄存器的物理地址.end    = 0x56000050 + 8 - 1,    //寄存器的地池长度.flags = IORESOURCE_MEM,        //这个信息是什么类型},[1] = {.start   = 4,                     //要控制哪一个led.end   = 4,                     //没啥用.flags   = IORESOURCE_IRQ,        //这个信息是什么类型},
}

③看看注册平台设备的过程:

platform_device_register (&led_dev);
-->platform_device_add(&led_dev);
-->device_add(&led_dev); 将device放到平台总线的“dev”链表中去。

二、接着写“平台驱动”:

①分配、设置、注册一个 platform_driver 结构体。 要注意的是因为平台总线的.match 函数比较的是"平台设备"和"平台驱动"的名字.所以两边名字要相同.这样才会认为这个 drv 能支持这个 dev。才会调用平台驱动里面的".probe"函数。

/* 分配并设置platform_driver*/
struct platform_driver leds_device_driver = {.probe        = led_probe,  //如果配对成功就会调用此函数,可以用来获取前面的led的硬件信息,将物理地址重映射等,取决于你自己想做什么。.remove      = led_remove, //如果配对取消就会调用此函数,与probe函数倒过来写.比如我们在probe函数中地址重映射了,注册了设备等,我们就在led_remove函数中取消重映射,注销设备等等.driver     = {.name    = "my_led",   //两边名字要相同}
};
/*在驱动入口函数中注册一个platform_driver*/
static int led_drv_init(void)
{platform_driver_register(&leds_device_driver);return 0;
}

②构造平台驱动结构中的“.probe”函数:

static int led_probe(struct platform_device *pdev)
{//根据 platform_device 的资源进行 ioremap .//注册字符设备驱动程序.return 0;
}

③构造平台驱动结构中的“.remove”函数:做与“.probe”相反的事件。

static int led_remove(struct platform_device *pdev)
{//根据 platform_device 的资源进行 iounmap .//卸载字符设备驱动程序.return 0;
}

④剩下的就和字符驱动设备一样,open、write;比如write,此处我们要点灯,就根据probe中采集到的硬件的信息,去操作这个灯。

注意:不管先注册“平台设备”还是先注册“平台驱动”,都会调用bus总线下的mach函数来通过名字比较有无匹配者,匹配成功的话就会调用“平台驱动”中的probe函数。以后我们要切换操作不同的灯的时候只需要更改platform_device中的硬件资源,也就相当于只要更改“平台设备”的代码。在大型项目上采用这种分离分层的思想,提取出经常要更改的代码,提取出比较稳定的代码,这样代码的结构就很优美。

2021.10.15:第二期-->第15课-->LCD驱动

fbmem.c 是内核自带的 LCD 驱动程序。基于分层的思想也是抽出了共性的内容。所以我们要想写出我们自己的LCD驱动程序,就应该基于内核的LCD驱动框架。从fbmem.c入手分析。

①fbmem.c 分析:

 //主设备号29

因为“ fbmem.c”是通用的文件,故并不能直接使用这个 file_operations 结构中的.read 等函数。这和前面的input子系统是一样的,都要通过核心层中转。 从入口函数分析:

 init __init fbmem_init(void)
-->register_chrdev(FB_MAJOR,"fb",&fb_fops)     //注册"file_operations”结构体“ fb_fops”。
-->fb_class = class_create(THIS_MODULE, "graphics");//创建类,但是先不创建设备节点。

这里没有在设备类下创建设备,只有真正有硬件设备时才有必要在这个类下去创建设备。

②如何才能真正操作到我们具体的屏幕设备呢?

假设 APP 打开一个主设备号: 29, 次设备号:0 的设备时,open("/dev/fb0", ...),会根据主设备号找到fbmem.c注册的file_operations  fb_fops 结构中的“ .open =fb_open,”函数

int fb_open(struct inode *inode, struct file *file)
-->int fbidx = iminor(inode);  //iminor(inode)得到这个设备节点的“次设备号”。
-->struct fb_info *info;       //帧缓冲区结构体。
-->info = registered_fb[fbidx];//即 struct fb_info *info =registered_fb[fbidx]; 假设“次设备号”为 0.即:struct fb_info *info = registered_fb[iminor(inode)] =registered_fb[0]。从这个 registered_fb[]数组里得到“以次设备号为 0 为下标”的一项。
-->info->fbops->fb_open //若这个 info = registered_fd[0] 的“ fbops”有“ fb_open”函数时就:
-->res = info->fbops->fb_open(info,1);//最终调用的就是以次设备号分配的操作函数中的open,如果没有这个函数,就执行系统的默认操作。
同理读、写等函数也是一样,fbmem.c的操作集file_operations只是一个中转。
简明操作:
fb_readint fbidx = iminor(inode);struct fb_info *info = registered_fb[fbidx];if (info->fbops->fb_read)return info->fbops->fb_read(info, buf, count, ppos);/*如果没有read函数,系统有他的默认操作*/src = (u32 __iomem *) (info->screen_base + p);dst = buffer;*dst++ = fb_readl(src++);copy_to_user(buf, buffer, c)

③那么registered_fb[fbidx]里面的信息是谁放进去的呢?

答案是我们用户自己注册的,当我们有一块LCD屏幕要写驱动的时候,我们需要得知他的屏幕信息参数,这时候最好的办法就是定义一个结构体,这个结构体不单单包含屏幕的参数,还有我们这块屏幕的read,write等函数。我们设置好了之后就去注册他。

static struct fb_ops s3c_lcd_fb_ops = {.owner       = THIS_MODULE,.fb_setcolreg    = s3c_lcdfb_setcolreg,//调色板.fb_fillrect    = cfb_fillrect,.fb_copyarea    = cfb_copyarea,.fb_imageblit   = cfb_imageblit,
};
/*1、定义fb_info结构体
static struct fb_info *s3c2440_4_3;
/*2、设置这个结构体*/
s3c2440_4_3->var.xres           = 480;//屏幕信息参数
s3c2440_4_3->var.yres           = 272;
s3c2440_4_3->fbops              = &s3c_lcd_fb_ops;设置操作的函数
/*3、注册这个fb_info结构体
register_framebuffer(s3c2440_4_3);

在执行register_framebuffer(s3c2440_4_3);注册时,会去找一个没用过的数组下标i,用这个下标把我们设置好的fb_info类型结构体s3c2440_4_3放到registered_fb[i]中,并且用这下标当作次设备号,在刚刚的fbmem_init创建的fb_class类下注册一个主设备号是29,次设备号是下标i的设备,这也就是为什么在fbmem_init函数中只创建类,不创建具体设备的原因。函数如下:

简化而言:
int register_framebuffer(struct fb_info *fb_info)
{int i;if (num_registered_fb == FB_MAX)return -ENXIO;for (i = 0 ; i < FB_MAX; i++)if (!registered_fb[i])break;fb_info->dev = device_create(fb_class, fb_info->device,MKDEV(FB_MAJOR, i), "fb%d", i);registered_fb[i] = fb_info;
}

 ④registered_fb[fbidx]里面的参数信息谁来用呢?换句话说谁来提取呢?

由fbmem.c的file_operations  fb_fops中的.ioctl = fb_ioctl,函数去读取,系统会用这个函数将我们的参数信息提取出来去设置,具体的事情就是内核做的了,我们的工作其实就是基于内核的LCD驱动框架去编写设配自己LCD驱动程序。

static const struct file_operations fb_fops = {.owner =    THIS_MODULE,.read =        fb_read,.write =   fb_write,.ioctl =  fb_ioctl,.open =       fb_open,......
};

⑤软件方面都配置注册好了,剩下的就是我们板子的硬件配置了,和裸机一样配置寄存器。

1、配置GPIO用于LCD
2、根据LCD手册设置LCD控制器,比如vclk的频率、水平,垂直方向的时间参数、信号的极性等
3、把显存的地址告诉LCD控制器......

⑥结合按键,让LCD成为控制台,按键成为标准输入

1、修改/etc/inittab,新增一行,tty1::askfirst:-/bin/sh
2、配合buttons驱动,让按键模拟l,s,enter,并显示输出到lcd屏幕上
3、为什么修改/etc/inittab,新增一行,tty1::askfirst:-/bin/sh就可以把lcd和按键结合?为什么echo hello > /dev/tty1时会将hello字符显示在lcd屏幕上?

这是启动一个“ -/bin/sh”程序,这个 shell 程序从“ s3c2410_serial0”串口 0 得到输入,把输出输出串口 0 上来。现在再启动一个“ shell”程序,这个 shell 程序开启 device“ tty1” (tty1 输入时就对应我们的键盘, device tty1 输出时就对应这里的“ LCD”)。

/dev/tty1 是用的如下驱动程序:
linux/drivers/video/fbcon.c        -- Low level frame buffer based console driver
这个驱动程序最终也会用到“ framebuffer” .所以它会用到我们的LCD驱动。而且tty和按键定义的事件有关,这一点在input子系统的⑥cat /dev/tty1为什么能从串口中显示键值?有分析。很复杂,知道这么回事就行, 按下按键时会向tty1写入数据,也会从结构体数组“ registered_fb”里面得到我们在LCD驱动中注册的LCD“ fb_info”结构体。显示文字时得到文字的字模,在 LCD 的显存里面描出这个文字,数据自然会被输出到LCD上了。 也同理解释了echo hello > /dev/tty1时会将hello字符显示在lcd屏幕上

总结上面的过程:抽象出驱动程序:怎么写 LCD 驱动程序?

1. 分配一个 fb_info 结构体: framebuffer_alloc
2. 设置这个结构体(包含屏幕参数,操作函数等信息)
3. 注册这个结构体: register_framebuffer
4. 硬件相关的配置

------> 实际上还可以像第14课那样,将驱动再次分离分层,匹配后在。在drv层的probe函数中完成对dev层资源的获取完成上述操作,具体代码在:第15课_LCD驱动程序\模仿platform分离LCD;    这样以后我们要更换屏幕,就在dev层的资源数组中更改参数信息即可,但是我的程序没有这么做,因为我懒,所以probe直接写死了。我只是做个实验可以这么分离。

Linux系统的文字登录界面tty1~tty6终端有什么区别?看不懂,先放着。

linux系统的文字登录界面tty1~tty6终端有什么区别?各有什么优劣之处_百度知道

1.各终端之间没有区别的,他就是为了方便用户的登录。比如说我可以同时利用其同一用户或其他用户同时登录,切换用户的时候,只需要使用alt+ctrl+fn切换即可,方便管理。

2,比如说,当用tty1 登录后,出现死机时,可切换到tty2(alt+ctrl+f2),利用另一个用户登录,比如利用root用户登录,ps -aux | grep program_name查询到刚才的那个让系统死机的进程,然后kill pid掉即可。这时系统就会恢复正常,还可通过service program_name start再次启动这个进程。

3.可以通过w命令,或who命令可以查看当前登录的用户。其中line那个字段就表示用户所使用的登录终端,tty1表示虚拟控制台,通过ALT+CTRL+FN(N在1-6之间)。pts/n(这个n理论上没有限制),远程登录的用户,就是使用securecrt,putty等远程登录工具登录的用户,他的终端显示的就是pts/n,比如说pts/0.在本机上,的xwindow下,打开的那个终端,也显示为pts/n哦。

4.可以更改ttyn虚拟控制台virtual console的数量,在/etc/inittab文件中,1:2345:respawn:/sbin/mingetty tty1
2:2345:respawn:/sbin/mingetty tty2
3:2345:respawn:/sbin/mingetty tty3
4:2345:respawn:/sbin/mingetty tty4
5:2345:respawn:/sbin/mingetty tty5
6:2345:respawn:/sbin/mingetty tty6
这6行,就表示可以启动的tty了。第一个字段(1-6)表示编号。第二个字段(看,全都是2345),表示在那种运行级别启动ttyn,其中2345就表示在runlevel为2345都启动此ttyn。如果你只想在指定的运行级别启动某个ttyn的话,可以修改这个字段的值,比如,tty4只能在运行级别为35时(不是35哦,是runlevel 3 和runlevel 5),删除24即可。如果想启动某个运行级别只需要注视掉对应的行即可(一定要注释掉,就是在行首加#,最好不要删除,方便以后添加,这是一个好习惯,凡是要删除配置文件中的某一行时,请都用#注释哦)。要重新开启时,删除#即可。

2021.10.17:关于LCD突然出现段错误的debug艰苦调试

起因是因为在学触摸屏驱动的时候,我们要配置去掉原来内核中触摸屏的驱动,重新编译内核之后用新的uImage启动板子,然后就是和往常一样:

当执行insmod lcd.ko的时候报错说Segmentation fault,我尝试的方法:

1.重新编译内核,开发板使用新的uImage启动

2.重新编译驱动,驱动makefile中内核目录设为步骤1的目录

3.也就是说现在我们的内核、驱动都是配套的,最新的。但是还是无果。

最后翻了无数的帖子,找到了解决方案

怎么编译到内核,这就要了解编译的流程:

2.6内核的源码树目录下一般都会有两个文件:Kconfig和Makefile。分布在各目录下的Kconfig构成了一个分布式的内核配置数据库,每个Kconfig分别描述了所属目录源文件相关的内核配置菜单。在内核配置make menuconfig(或xconfig等)时,从Kconfig中读出配置菜单,用户配置完后保存到.config(在顶层目录下生成)中。在内核编译时,主Makefile调用这个.config,就知道了用户对内核的配置情况。
  上面的内容说明:Kconfig就是对应着内核的配置菜单。假如要想添加新的驱动到内核的源码中,可以通过修改Kconfig来增加对我们驱动的配置菜单,这样就有途径选择我们的驱动,假如想使这个驱动被编译,还要修改该驱动所在目录下的Makefile。
  因此,一般添加新的驱动时需要修改的文件有两种(注意不只是两个)
  *Kconfig
  *Makefile
  要想知道怎么修改这两种文件,就要知道两种文档的语法结构。

以一个简单的例子说明:

①编写驱动程序hello.c

②修改Kconfig:Kconfig用来产生make menuconfig的菜单。打开drivers/char目录下的Kconifg,加入如下代码:

config HELLO_WORLDtristate "my hello world test"

在Kconfig中加入这项之后,执行make menuconfig ,在Device Drivers -> Character devices 中可以看到多了一项my hello world test:

如果选择将HELLO WORLD编译进内核,即将my hello world test前尖括号中配置为“*”,保存之后看内核代码的根目录下的.config,在字符设备中多了一行CONFIG_HELLO_WORLD=y:

③修改Makefile:

注意修改的是drivers/char下的Makefile,加入obj-$(CONFIG_HELLO_WORLD) += hello.o

重新编译内核,重启之后可以看到内核打印信息中有“ *****hello init test***** ”。

有了以上基础,试一下第一种方法:将我们的把lcd.c编译到内核中去。看能否解决段错误!

①先修改/driver/char/Kconfig,加入以下代码:

config MY_lcd_drivertristate "my lcd driver"

②再修改 /driver/char/Makefile,加入下面的语句:

obj-$(CONFIG_MY_LCD_driver)     += my_lcd_driver.o

③把我们的驱动程序my_lcd_driver.c放到 /driver/char/目录下

④在根目录下make menuconfig,配置菜单,他会根据各层目录下面的Kconfig去读取菜单目录。

1、开启我们刚刚加入的MY_lcd_driver,将他设置成编译进内核。

2、驱动程序要用到cfb*.ko,前面LCD实验,我们把它们都编译成了模块,现在我们自己的LCD驱动程序要编译进内核,所以我们要把cfb.ko也都编译进内核。

⑤编译:make     生成内核:make uImage

⑥用新的uImage启动板子,段错误是解决了,但是LCD并不显示控制台的信息,

echo hello >/dev/tty1也没反应。

这就和解决办法二出现了一样的事情,难道是我的Kconfig的写法,或者用法有错?

内核源码是最好的老师,直接模仿内核的LCD驱动的Kconfig编写

①内核中LCD相关的程序放在了/driver/video目录下,进入/driver/video

②打开/driver/video/Kconfig,搜索CFB相关的,因为内核的LCD也要用到CFB。找到了:

后续修改/driver/video/Makefile、把驱动程序放到/driver/video目录、删除前面在/driver/char 改变的东西,我怕会有影响。虽然我们在Kconfig中默认打开了我们的LCD驱动程序,但是还是要执行一下make menuconfig,用来更新.config,然后make的时候才会用这个最新的,包含我们加的LCD内容的.config去编译内核,最后用这次的uImage启动后,奇迹发生了:

以上事情告诉我们,源码是最好的老师,参考他,还有就是具体为什么会段错误,我也不知道。

2021.10.20:第二期-->第17课-->USB驱动

一、USB整体架构框架:

二、USB总线的流程:

当接上一个 USB 设备时,就会产生一个中断hub_irq(),在中断里会分配一个编号地址(choose_address(udev)。再然后把这个地址告诉 USB 设备hub_set_address()。接着发出各种命令获取 USB 设备的“设备描述符” usb_get_device_descriptor()。再然后注册一个 device(device_add()。这个被注册的 device 会被放到 USB 总线(usb_bus_type)的“设备链表”。并且会从总线(usb_bus_type)的“驱动链表”中取出"usb_driver" - USB 设备驱动结构,把 usb_interface 和 usb_driver 的 id_table 比较,若匹配就调用“usb_driver”结构中的“.probe”函数。

三、USB 驱动设备简单编写:

USB 总线驱动程序,在接入 USB 设备时,会帮我们识别,提取信息并构造一个新的 usb_dev 注册到“usb_bus_type”里去。这部分是内核做好的。我们要做的是,构造一个 usb_driver 结构体,注册到“usb_bus_type”中去。在“usb_driver”结构体中有“id_table”表示他能支持哪些设备,当 USB 设备能匹配 id_table 中某一个设备时,就会调用“usb_driver”结构体中的“.probe” (自已确定在 probe 中做的事情)等函数,当拔掉USB 设备时,就会调用其中的“.disconnect”函数

“usb_bus_type” USB 总线驱动设备模型只是提供了这一种框架而已。在".probe"函数里面,注册“字符设备”也好,注册一个“input_dev”结构体也好,再或注册一个块设备也好。再或只是加了句打印也好,都可以由自已确定。

目标:USB 鼠标用作按键: (相当于输入子系统)。
左键 -- L
右键 -- S
中键 -- Enter

①,分配一个 input_dev 结构。

②,设置这个 input_dev 结构

③,注册。

④,硬件相关操作:以前的驱动程序,数据是从中断里来,或读寄存器,引脚状态而来,确定是什么数据。现在这里数据要用底层“USB 总线驱动程序”提供的函数来收发 USB 数据。

四、具体看看“USB 总线驱动程序”提供的函数来收发 USB 数据:

A,数据传输 3 要素:源,目的,长度。
①,源: USB 设备的某个端点。每个 USB 设备接上后都有个编号地址:


pipe = usb_rcvintpipe (dev, endpoint->bEndpointAddress);

这个宏“usb_rcvintpipe(dev,endpoint)”就包含了 USB 设备的地址,和哪一号端点(端
点地址)。“PIPE_INTERRUPT”中断类型端点。“USB_DIR_IN”端点的方向。

“源-pipe”是个整数,这个整数里含有端点的类型--PIPE_INTERRUPT,和端点的方向--
USB_DIR_IN。“__create_pipe(dev,endpoint)”这里既含有设备地址,也含有端点地址。

“devnum”:就是 USB 设备的地址( USB 设备编号)。
“endpoint”:就是端点的地址( bEndpointAddress)也是端点的一个编号(也是整数)。

②,目的:从 USB 设备读数据,读到一个缓冲区

分配一个缓冲区,不能是静态缓冲区 ,必须使用kmalloc()来分配,用“usb_buffer_alloc()”

void *usb_buffer_alloc(
struct usb_device *dev,
size_t size, //参 2,分配多大的长度,是
“usb_endpoint_descriptor.wMaxPacketSize”
gfp_t mem_flags,
dma_addr_t *dma //参 4,这是物理地址的意思
)
他最终也是调用到kmalloc来分配的缓冲区

③, 长度:

端点描述符中有长度.usb_endpoint_descriptor.wMaxPacketSize(最大包大小),要作为
“usb_buffer_alloc()”的形参 2。

也就是:

//硬件相关操作--三要素:
//源:USB 设备某个端点。每个 USB 设备都有编号地址
pipe = usb_rcvintpipe (dev, endpoint->bEndpointAddress);
//长度:端点描述符中有长度.usb_endpoint_descriptor.wMaxPacketSize(最大包大小)
len = endpoint->wMaxPacketSize;
//目的:
usb_buf = usb_buffer_alloc(dev, len, GFP_ATOMIC, usb_buf_phys);

B,数据使用3 要素:

①,要分配一个“urb”: usb 请求块(usb request block),注意URB结构体也不宜静态创建,因为这可能破坏USB核心给URB使用的引用计数方法。要用:usb_alloc_urb
②,使用三要素前要设置 urb 。
 a, 使用Usb_fill_int_urb():fill(填充)中断类型的 urb。

inline void usb_fill_int_urb (struct urb *urb,
struct usb_device *dev,
unsigned int pipe, //三要素:源void *transfer_buffer, //三要素:目的
int buffer_length, //三要素:长度
usb_complete_t complete_fn, //complete_fn 完成函数。
void *context, //给 complete_fn 使用
int interval) //中断方式是通过查询,查询有多频繁(interval)。

鼠标是中断传输,实际上 USB 设备(鼠标)并没有主动通知“USB 主机控制器”的能力,即没有打断“USB 主机控制器”的能力。如何保证数据是“及时”的--用不断的查询。这个查询不是由 CPU 来查询,是由“USB 主机控制器”来不断查询,查询到数据后,USB 主机控制器发出中断(USB 主机控制器有中断 CPU 的能力)。 USB 设备却没有中断“USB 主机控制器”的能力。 USB 设备(USB 鼠标)所谓的“中断传输”是指“USB 主机控制器”不断的查询“USB 设备”是否有数据过来。当“USB 主机控制器”得到数据后,“USB 总线驱动程序”就会调用“complete_fn--完成函数”,“查询频率”:中断方式是通过查询,查询有多频繁(interval)。端点描述符中有个“interval--间隔、间隙”。

b,   USB 主机控制器得到“USB 设备”的数据后,要往某个内存去写。它需要的是物理地址。所以要告诉“USB 主机控制器”某个内存的物理地址。

c,   设置某些标记(不知道其意思)。

③,    使用以上设置好的传输urb,提交 urb.usb_submit_urb()。

也就是:

//使用三要素:
//分配一个 usb request block(USB 请求块)
uk_urb = usb_alloc_urb(0, GFP_KERNEL);
//使用三要素设置 urb
//填充中断类型的 urb。
usb_fill_int_urb(uk_urb, dev,pipe,usb_buf,len,usbmouse_as_key_irq,NULL,endpoint>bInterval);
//USB 主机控制收到 USB 设备数据要写到一个内存的物理地址.
uk_urb->transfer_dma = usb_buf_phys;
//设置某些标记
uk_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
//提交 urb,当接收到数据就会调用自己设置的中断函数(完成函数)
usb_submit_urb();

USB 总线驱动程序提供“识别设备”,“给设备找到驱动”,提供“读写数据”,只收发数据却并不知道数据的含意。USB 设备驱动程序知道数据的含意。数据的含意需要“USB 设备”驱动程序来解析。

总结USB驱动:

根据“usb_bus_type”总线驱动设备驱动模型,里面有个“.match”函数,左边是由“USB 总线驱动程序”帮我们来发现这个新“USB 设备”,会注册"usb_new_device()"一个"USB 设备",并且从右边“driver”链表里找了一个个 USB 驱动程序和左边注册进来的“USB 设备”比较.所谓的比较,是“usb_bus_type”中的“.match”函数,把左边“usb_interface”结构中的“接口”与右边“usb_driver”结构中的“id_table”比较。若能吻合,则调用“usb_driver”中的“.probe”函数。“usb_bus_type”提供了这套机制。在“.probe”函数中,可以只是打印,也可以注册字符设备,或注册“input_dev”结构。完全由自已确定。以前的驱动程序,数据是从“中断”(按键中断, ADC 中断)里面读寄存器得到。现在“USB 设备驱动程序”中的数据从“USB 总线”来,是 USB 总线驱动程序提供的函数(读写等)发起 USB 传输,从 USB 传输里得到那些数据,但是他不清楚数据的含义。(数据传输三要素:源,目的,长度。再构造一个“usb_urb = usb_alloc_urb(0,GFP_KERNEL)”后,接接着把“源,目的,长度”填充到“usb_urb”中,使用就是提交"usb_urb",提交 usb_sunmit_urb 函数是 USB 总线驱动程序提供的)。当“USB 主机控制器”接收到数据后,发生中断函数通知cpu(complete_fn 完成函数),在这个函数里根据"USB 设备"数据的含义去上报(input_event())。

学习s3c2440的随笔笔记相关推荐

  1. Yann Lecun纽约大学《深度学习》2020课程笔记中文版,干货满满!

    关注上方"深度学习技术前沿",选择"星标公众号", 资源干货,第一时间送达! [导读]Yann Lecun在纽约大学开设的2020春季<深度学习>课 ...

  2. 使用html记笔记,开始学习HTML,并记下笔记

    开始学习HTML,并记下笔记. 外边距(不影响可见框大小,影像盒子位置) margin-top(上) right(右) bottom(下) left(左) "外边距也可以为一个负值,元素会反 ...

  3. 学习Python的做笔记神器——Jupyter Notebook

    学习Python的做笔记神器--Jupyter Notebook 给想学好Python的同学们安利一波,Jupyter Notebook是学习Python最好的做笔记环境,没有之一哦. Jupyter ...

  4. 《Python深度学习》第一章笔记

    <Python深度学习>第一章笔记 1.1人工智能.机器学习.深度学习 人工智能 机器学习 深度学习 深度学习的工作原理 1.2深度学习之前:机器学习简史 概率建模 早期神经网络 核方法 ...

  5. 《学习geometric deep learning笔记系列》第一篇,Non-Euclidean Structure Data之我见

    <学习geometric deep learning笔记系列>第一篇,Non-Euclidean Structure Data之我见 FesianXu at UESTC 前言 本文是笔者在 ...

  6. 《Python学习手册》读书笔记

    原文地址为: <Python学习手册>读书笔记 之前为了编写一个svm分词的程序而简单学了下Python,觉得Python很好用,想深入并系统学习一下,了解一些机制,因此开始阅读<P ...

  7. 《Data Structures and Algorithm Analysis in C》学习与刷题笔记

    <Data Structures and Algorithm Analysis in C>学习与刷题笔记 为什么要学习DSAAC? 某个月黑风高的夜晚,下班的我走在黯淡无光.冷清无人的冲之 ...

  8. 《我的PaddlePaddle学习之路》笔记一——PaddlePaddle的安装

    原文博客:Doi技术团队 链接地址:https://blog.doiduoyi.com/authors/1584446358138 初心:记录优秀的Doi技术团队学习经历 环境 系统:Ubuntu 1 ...

  9. 训练大规模对比学习的一些小笔记

    训练大规模对比学习的一些小笔记 FesianXu 20210815 at Baidu Search Team 前言 笔者在公司中会面对数以亿计的用户历史行为数据,用好这些数据是非常关键的.而最近流行的 ...

  10. 《我的PaddlePaddle学习之路》笔记四——自定义图像数据集的识别

    原文博客:Doi技术团队 链接地址:https://blog.doiduoyi.com/authors/1584446358138 初心:记录优秀的Doi技术团队学习经历 *本篇文章基于 Paddle ...

最新文章

  1. 易宝典文章——如何将PST文件导入到Exchange 2010 的邮箱
  2. 虚拟机无法上网/连接失败原因及解决方法
  3. Spring的单元测试
  4. const深度总结(effective C++)
  5. 虚拟机连接网络_Parallels Desktop 16教程PD16虚拟机共享网络和桥接网络设置方法
  6. c语言实现线性表的算法,数据结构算法代码实现——线性表的定义(一)
  7. SQL必知必会-检索数据
  8. 勒索软件好多都使用恶意LNK链接文件欺骗用户 来看趋势科技分析新型LNK-PowerShell攻击...
  9. 2017总结:迷茫的一年
  10. 2020-04-17-E-prime常见问题汇总
  11. linux fastboot工具,Linux下使用Fastboot给手机刷机
  12. 小程序 · 引入企业微信中的「在小程序中加入群聊」插件
  13. 成功解决TypeError: distplot() got an unexpected keyword argument ‘y‘
  14. 事务的四大特性(ACID)
  15. 笔记本硬盘直接安装win7系统教程(不用U盘和PE)
  16. 光线cms,如何增加像百度一样的智能提示
  17. java还原混淆代码,android混淆 android如何将混淆代码还原?
  18. 装完黑苹果怎么装windows_黑苹果安装教程,小编教你黑苹果怎么安装
  19. 教你羊肉炒菠菜的做法
  20. 163.net是什么邮箱?这种个人邮箱你足够了解么?

热门文章

  1. 仿苹果 底部弹窗 选择列表
  2. javaScript用函数的方式计算体重是否是标准体重(代码)
  3. 阅读材料:信息技术年谱
  4. 宫颈癌预测--随机森林
  5. DTOJ #1079. 多项式展开 mult
  6. 软件测试自学还是培训?
  7. python 转义字符——学习笔记
  8. lbp2900打印机linux驱动下载,lbp2900打印机驱动下载x64 (canon lbp2900驱动canon lbp2900打印机驱动)下载 - 下载吧...
  9. IDEA设置版权信息
  10. 搭建内网BT服务器(转)