•  本文主要讲解使用2440中的SPI控制器来控制SPI传输命令和数据,以实现对OLED和FLASH的控制。同时本文会分为两部分,第一部分主要介绍2440的SPI控制器。而第二部分我们将结合代码,看是如何实现对OLED和FLASH的控制...

    简介:

        本文主要讲解使用2440中的SPI控制器来控制SPI传输命令和数据,以实现对OLED和FLASH的控制。同时本文会分为两部分,第一部分主要介绍2440的SPI控制器。而第二部分我们将结合代码,看是如何实现对OLED和FLASH的控制。

    所用开发板:JZ2440 V3

    所用OLED 屏幕:韦东山老师淘宝所用屏幕

    所用OLED 驱动芯片:SSD1306

    FLASH:W25Q16DV

    声明:

        我在前面两篇文章:嵌入式Linux——SPI总线(1):2440裸机GPIO模拟SPI驱动OLED嵌入式Linux——SPI总线(2):2440裸机GPIO模拟SPI控制FLASH中已经讲解了对OLED和FLASH的设置。而前面两篇文章的重点就是OLED手册和flash手册的说明,因此,我在这篇文章中将主要讲解2440中SPI控制器实现收发函数的步骤,而关于OLED和flash部分的知识将简略介绍。

    第一部分:2440SPI控制器

     

        S3C2440A的串行外设接口(SPI)可以作为串行数据传输的接口。S3C2440A有两个SPI接口,每个接口有两个8位的移位寄存器分别用来传送和接收数据。当一个SPI接口在传输时,数据即发送(串行移出)又接收(串行移入)。8位的串行数据的传输频率是由他相应的控制寄存器设置的。如果你仅仅想要传送,输出数据可以忽略,同样如果你仅仅只是想接收。你应该发送1 。

        SPI传输中有4个I/O引脚:时钟SCK (SPICLK0,1), 主机输入MISO (SPIMISO0,1), 主机输出MOSI(SPIMOSI0,1)和片选/SS (nSS0,1)。

    SPI操作:

           使用SPI接口,S3C2440A可以同时与一个外设发送/接收8位数据。而串行时钟线与用于发送和接收信息的数据线同步。当SPI为主机时,可以通过给SPPREn寄存器设置一个合适的值来设置传输频率。而当SPI为从机时,另一个主机控制时钟。当程序员向SPTDATn寄存器写入数据时,SPI发送/接收操作就开始了。通常,片选nSS要在向SPIDATn写数据前拉低。

     

    SPI传输格式:

           S3C2440A支持4种不同的传输格式,如下图。

     

        在这里我先讲解一下这四种不同格式的传输有什么区别。

     

        首先我们先讲CPOL=0与CPOL=1的区别。当CPOL=0时,时钟平时将处于低电平,他的第一个时钟沿一定是上升沿,而当完成传输后时钟会回到低电平状态。而当CPOL=1时,时钟平时将处于高电平,他的第一个时钟沿一定是下降沿,而当完成传输后时钟会回到高电平状态。

     

        下面我们讲CPHA=0与CPHA=1的区别。当CPHA=0时,SPI在第一个时钟沿时触发数据的发送或读取。而当CPHA=1时,SPI在第二个时钟沿时触发数据的发送或读取。

        而Format A与Format B的区别是,Format A是当这个时钟周期传完,而下一个时钟周期还没有到来前,在MISO线上当前位电平应该与上一时钟周期的最高有效位电平保持一致。而Format B是当这个时钟周期传完,而下一个时钟周期还没有到来前,在MISO线上当前位电平应该与下一时钟周期的最低有效位电平保持一致。如下图:

    下面就是SPI寄存器:

     

    SPI控制寄存器:

    寄存器

    地址

    R/W

    描述

    重启值

    SPCON0

    0x59000000

    R/W

    SPI通道0控制寄存器

    0x00

    SPCON1

    0x59000020

    R/W

    SPI通道1控制寄存器

    0x00

     

    SPCONn

    描述

    初始值

    SPI模式选择(SMOD)

    [6:5]

    决定SPTDAT怎样读写:

    00 = 轮询      01 = 中断模式

    10 = DMA模式  11 = 保留

    00

    时钟使能(ENSCK)

    [4]

    决定你是否想要使能CSK(只能是主机)

    0 = 不使能     1 = 使能

    0

    主机/从机选择(MSTR)

    [3]

    决定想要的模式(主机或者从机)

    0 = 从机       1 = 主机

    主要:在从机模式时,由主机来设置初始化发送接收时间

    0

    时钟极性选择(CPOL)

    [2]

    决定一个高电平或低电平时钟

    0 = 高电平     1 = 低电平

    0

    时钟相位选择(CPHA)

    [1]

    从两个不同的传输模式中选出一个

    0 = 模式A      1 = 模式B

    0

    传输自动Garbage数据模式使能(TAGD)

    [0]

    决定是否请求接收数据

    0 = 正常模式    1 = 传输自动Garbage数据模式

    注意:在正常模式,如果你仅仅只是想接收数据,你应该传输0xff

    0

     

    SPI状态寄存器:

    寄存器

    地址

    R/W

    描述

    重启值

    SPSTA0

    0x59000004

    R

    SPI通道0状态寄存器

    0x01

    SPSTA1

    0x59000024

    R

    SPI通道1状态寄存器

    0x01

     

    SPSTAn

    描述

    初始值

    保留

    [7:3]

     

     

    数据冲突错误标志(DCOL)

    [2]

    当一个传输正在进行时,如果SPTDATn被写入或者SPRDATNT被读,将使该位置1。通过读SPSTAn清零

    0 = 没有检测到  1 = 检测到冲突错误

    0

    多主机错误标志(MULF)

    [1]

    当片选nSS信号选中时,SPI被配置为主机该位就被设置为1,SPPINn的ENMUL位就检测到多主机错误。MULF通过读SPSTAn清零

    0 = 没有检测到  1 = 检测到错误

    0

    传输就绪标志位(REDY)

    [0]

    该位表示SPTDATn或者SPRDATn准备去发送或者接收。通过读SPSTAn清零

    0 = 没有就绪    1 = 就绪

    1

     

    SPI波特率预分频寄存器:

    寄存器

    地址

    R/W

    描述

    重启值

    SPPRE0

    0x5900000C

    R/W

    SPI通道0波特率预分频寄存器

    0x00

    SPPRE1

    0x5900002C

    R/W

    SPI通道1波特率预分频寄存器

    0x00

     

    SPPREn

    描述

    初始值

    预分频值

    [7:0]

    决定SPI时钟速率

    波特率 = PCLK / 2 / (预分频值+ 1)

    0x00

     

    注意:波特率应该小于25MHz

    SPI发送数据寄存器:

    寄存器

    地址

    R/W

    描述

    重启值

    SPTDAT0

    0x59000010

    R/W

    SPI通道0发送数据寄存器

    0x00

    SPTDAT1

    0x59000030

    R/W

    SPI通道1发送数据寄存器

    0x00

     

    SPTDATn

    描述

    初始值

    发送数据寄存器

    [7:0]

    包含发送到SPI通道中的数据

    0x00

     

    SPI接收数据寄存器:

    寄存器

    地址

    R/W

    描述

    重启值

    SPRDAT0

    0x59000014

    R

    SPI通道0接收数据寄存器

    0xff

    SPRDAT1

    0x59000034

    R

    SPI通道1接收数据寄存器

    0xff

     

    SPRDATn

    描述

    初始值

    接收数据寄存器

    [7:0]

    包含从SPI通道中接收的数据

    0xff

     

     

    讲解完这些我们就要按着2440手册SPI中所讲的步骤来写SPI模块了:

     

     

    1.    设置波特率预分频寄存器(SPPREn)。

    2.    设置SPCONn来配置合适的SPI模块

    3.    向SPTDATn寄存器写10次0xff 来初始化MMC或者SD卡

    4.    设置GPIO引脚,他们将拉低nSS来激活MMC或者SD卡

    5.    传送数据 :检查传输就绪标志(REDY=1)的状态,然后将数据写入SPTDATn

    6.    接收数据(1):SPCONn的TAGD为正常模式(normal mode)

    7.    写0xff到SPTDATn寄存器,然后确定REDY位被设置,最后就可以从读缓存中读数据了

    8.    接收数据(2):SPCONn的TAGD为Tx auto garbage data mode

    9.    然后确定REDY位被设置,最后就可以从读缓存中读数据了

    10.  拉高片选引脚

     

    第二部分:结合代码讲解SPI控制OLED和FLASH

        我们首先先完成设置波特率预分频寄存器(SPPREn)设置SPCONn来配置合适的SPI模块。因为这两步实现的是对SPI控制器的初始化,代码为:

    static void spi_cnt_init(void)
    {
    	/* 
    	*第一步:设置波特率
    	*oled 最大速率为10MHz,而flash最大速率为104MHz,
    	*所以选OLED的10MHz为波特率,PCLK为50MHz
    	*由于Baud rate = PCLK / 2 / (Prescaler value + 1)
    	*所以Prescaler为2
    	*/
    	SPPRE0 = 2;
    	SPPRE1 = 2;
    
    	/* 
    	*第二步:设置spi控制寄存器SPCON
    	*[6:5] :00  轮询模式
    	*[4]   :1 使能时钟
    	*[3]   :1 主机模式
    	*[2]   :0 选择默认高电平
    	*[1]   :0 默认模式A
    	*[0]   :0 正常模式
    	*/
    	SPCON0 = (1<<4) | (1<<3);
    	SPCON1 = (1<<4) | (1<<3);
    	
    }

        从上面的代码可以看出,波特率预分频系数为2,所以波特率为8.3MHz。我们设置的SPI为主机,并设置传输方式为轮询。而SPI的传输格式为CPOL=0,CPHA=0,即时钟平时将处于低电平,他的第一个时钟沿一定是上升沿,而当完成传输后时钟会回到低电平状态。SPI在第一个时钟沿时触发数据的发送或读取。同时我们还设置为正常模式。

        由于我们没有使用MMC和SD卡,所以我们不用步骤3,接着我们按步骤4操作,即对GPIO进行初始化

    static void spi_gpio_init(void)
    {
    	/* GPF1 OLED_CS0 output */
    	GPFCON &= ~(0x3<<(1*2));
    	GPFCON |= (0x1<<(1*2));
    	GPFDAT |= (0x1<<1);
    
    	/* GPG2 FLASH_CS0 output
    	*  GPG4 OLED_D/C  output
    	*  GPG5 SPI_MISO  
    	*  GPG6 SPI_MOSI  
    	*  GPG7 SPI_CLK   
    	*/
    	GPGCON &= ~((0x3<<(2*2)) | (0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)) | (0x3<<(7*2)));
    	GPGCON |= ((0x1<<(2*2)) | (0x1<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)) | (0x3<<(7*2)));
    	GPGDAT |= (0x1<<2);
    }

        完成上面这些准备工作,我们将在下面开始开始做数据收发的工作。我们先看步骤5,传送数据 :检查传输就绪标志(REDY=1)的状态,然后将数据写入SPTDATn。而它对应的函数为:

    void spi_send_byte(unsigned char val)
    {
    	while(!(SPSTA1 & 1)); //检验REDY=1
    	SPTDAT1 = val;
    }

        如上面SPI状态寄存器所示:REDY表示SPTDATn或者SPRDATn准备去发送或者接收。通过读SPSTAn清零。当REDY = 0时表示没有就绪    当REDY =1时表示就绪。当SPI准备就绪后,数据写入SPI数据传输寄存器SPTDAT1。

     

        由于我们在上面SPI控制寄存器中配置SPI为正常模式,所以我们按步骤6和步骤7来接收数据,即接收数据(1):SPCONn的TAGD为正常模式(normal mode),写0xff到SPTDATn寄存器,然后确定REDY位被设置,最后就可以从读缓存中读数据了。而接收函数为:

     

    unsigned char spi_recv_byte(void)
    {
    	SPTDAT1 = 0xff;
    	while(!(SPSTA1 & 1)); //检验REDY=1
    	return SPRDAT1;
    }

        好了,现在我们就讲完SPI控制器的设置,以及收发函数的编写。下面我们就要将他们运用到OLED和flash上去。我们先看在OLED上的运用。

    SPI在OLED上的运用:

        由于我们的spi并没有对片选引脚/CS的控制,所以我们要自己编写对片选引脚/CS控制。同时又由于在OLED中要使用数据/命令引脚的高低电平来区分我们所发送的是命令还是数据。所以我们要再定义两个引脚:

    /* 数据/命令引脚 */
    void spi_set_DC(char val)
    {
    	if(val)
    		GPGDAT |= (0x1<<4);
    	else
    		GPGDAT &= ~(0x1<<4);
    }
    
    /* 片选引脚 */
    static void spi_set_CS(char val)
    {
    	if(val)
    		GPFDAT |= (0x1<<1);
    	else
    		GPFDAT &= ~(0x1<<1);
    }

        下面我们要完成的是对数据和命令的写函数,因为操作OLED其实就是对OLED进行写命令和写数据操作而已。下面我们看写命令和写数据函数

    /* 写命令函数 */
    void spi_oled_write_cmd(unsigned char cmd)
    {
    	spi_set_DC(0);
    	spi_set_CS(0);
    	spi_send_byte(cmd);
    	spi_set_CS(1);
    	spi_set_DC(0);
    }
    
    /* 写数据函数 */
    void spi_oled_write_dat(unsigned char dat)
    {
    	spi_set_DC(1);
    	spi_set_CS(0);
    	spi_send_byte(dat);
    	spi_set_CS(1);
    	spi_set_DC(1);
    }

        而写完写数据和写命令函数后,我们就可以对OLED进行操作了,如下面的初始化OLED函数,设置OLED显示位置函数,显示单字符函数,显示字符串函数以及清屏函数

    /* 初始化OLED函数 */
    void oled_init(void)
    {
    	spi_oled_write_cmd(0xae);//--display off
    	spi_oled_write_cmd(0x00);//---set low column address
    	spi_oled_write_cmd(0x10);//---set high column address
    	spi_oled_write_cmd(0x40);//--set start line address  
    	spi_oled_write_cmd(0xb0);//--set page address
    	spi_oled_write_cmd(0x81); // contract control
    	spi_oled_write_cmd(0xff);//--128   
    	spi_oled_write_cmd(0xa1);//set segment remap 
    	spi_oled_write_cmd(0xa6);//--normal / reverse
    	spi_oled_write_cmd(0xa8);//--set multiplex ratio(1 to 64)
    	spi_oled_write_cmd(0x3f);//--1/32 duty
    	spi_oled_write_cmd(0xc8);//com scan direction
    	spi_oled_write_cmd(0xd3);//-set display offset
    	spi_oled_write_cmd(0x00);//
    
    	spi_oled_write_cmd(0xd5);//set osc division
    	spi_oled_write_cmd(0x80);//
             
    	spi_oled_write_cmd(0xd8);//set area color mode off
    	spi_oled_write_cmd(0x05);//
                
    	spi_oled_write_cmd(0xd9);//set pre-charge period
    	spi_oled_write_cmd(0xf1);//
                
    	spi_oled_write_cmd(0xda);//set com pin configuartion
    	spi_oled_write_cmd(0x12);//
              
    	spi_oled_write_cmd(0xdb);//set vcomh
    	spi_oled_write_cmd(0x30);//
              
    	spi_oled_write_cmd(0x8d);//set charge pump enable
    	spi_oled_write_cmd(0x14);//
    
    	oled_set_pageAddr_mode();
    	oled_clear();
    	
    	spi_oled_write_cmd(0xaf);//--turn on oled panel
    
    }
    /* 设置OLED显示位置函数 */
    void oled_set_pos(int page,int col)
    {
    	spi_oled_write_cmd(0xb0+page);
    	spi_oled_write_cmd(col&0x0f);
    	spi_oled_write_cmd(((col&0xf0)>>4)|0x10);	
    }
    /* 显示单字符函数 */
    static void oled_put_char(int page,int col,char c)
    {
    	int i;
    	/* 得到字模 */
    	const unsigned char *val = F8X16[c-' '];
    
    	/* 发送OLED */
    	oled_set_pos(page,col);
    
    	/* 发送8字节数据 */
    	for(i=0;i<8;i++){
    		spi_oled_write_dat(val[i]);
    	}
    
    	/* 发送OLED */
    	oled_set_pos(page+1,col);
    
    	/* 发送8字节数据 */
    	for(i=0;i<8;i++){
    		spi_oled_write_dat(val[i+8]);
    	}
    	
    }
    /* 显示字符串函数 */
    void oled_print_string(int page,int col,char *str)
    {
    	int i = 0;
    	while(str[i]){
    		oled_put_char(page,col,str[i]);
    		col += 8;
    		if(col > 127){
    			col = 0;
    			page += 2;
    		}
    		i++;
    	}
    }
    
    /* 清屏函数 */
    void oled_clear(void)
    {
    	int page,col;
    	for(page=0;page<8;page++){
    		oled_set_pos(page,0);
    		for(col=0;col<128;col++){
    			spi_oled_write_dat(0);
    		}
    	}
    }

        讲完在OLED中的运用,我们现在讲解在flash中运用spi控制器的收发函数。

    flash中的运用:

        同样,由于spi没有对片选引脚的控制,所以我们要在这里加对片选的控制,片选函数为:

    static void spi_set_CS(char val)
    {
    	if(val)
    		GPGDAT |= (0x1<<2);
    	else
    		GPGDAT &= ~(0x1<<2);
    }

        下面我们来完成写命令函数,其实写命令函数就是我们的SPI发送函数,这里只是调用一下就可以了:

    static void flash_write_cmd(unsigned char cmd)
    {	
    	spi_send_byte(cmd);
    }

        而由于我们的flash内存为16Mbit,所以我们需要用24位的地址来访问内存中的任意位置,而写地址就要连续的使用三次SPI发送数据,所以发地址函数为:

    static void flash_write_addr(unsigned int addr)
    {
    	spi_send_byte((addr>>16)&0xff);
    	spi_send_byte((addr>>8)&0xff);
    	spi_send_byte(addr&0xff);
    }

        而flash接收函数与SPI的接收函数一样,所以这里也只是调用一下:

    unsigned char flash_read_byte(void)
    {
    	return spi_recv_byte();
    }

        有了上面的函数我们就可以按着操作flash的步骤写flash了:

    1. 去保护

        a. 写保护

        b. 写寄存器

    2. 擦除扇区(擦除指定地址内存)

    3. 写页(即页编程,向指定的位置 写入信息)

    4. 读数据(从指定的位置读信息)

        我们先完成步骤1:

    /* 写使能 */
    static void spi_flash_write_enable(int enable)
    {
    	if(enable){
    		spi_set_CS(0);
    		flash_write_cmd(0x06);
    		spi_set_CS(1);
    	}else{
    		spi_set_CS(0);
    		flash_write_cmd(0x04);
    		spi_set_CS(1);
    	}
    }
    
    /* 读状态寄存器1 */
    static unsigned char spi_flash_read_status_Reg1(void)
    {
    	unsigned char val = 0;
    	spi_set_CS(0);
    	flash_write_cmd(0x05);
    	val = flash_read_byte();
    	spi_set_CS(1);
    
    	return val;
    }
    
    /* 读状态寄存器2 */
    static unsigned char spi_flash_read_status_Reg2(void)
    {
    	unsigned char val = 0;
    	spi_set_CS(0);
    	flash_write_cmd(0x35);
    	val = flash_read_byte();
    	spi_set_CS(1);
    
    	return val;
    }
    /* 等待就绪 */
    static void spi_flash_wait_when_busy(void)
    {
    	while(spi_flash_read_status_Reg1() & 1);
    }
    
    /* 写状态寄存器 */
    static void spi_flash_write_status_Reg(unsigned char reg1,unsigned char reg2)
    {
    	spi_flash_write_enable(1);
    	spi_set_CS(0);
    	flash_write_cmd(0x01);
    	flash_write_cmd(reg1);
    	flash_write_cmd(reg2);
    	spi_set_CS(1);	
    
    	spi_flash_wait_when_busy();
    }
    
    /* 去状态寄存器保护 */
    static void spi_flash_clear_protect_for_status_Reg(void)
    {
    	unsigned char reg1,reg2;
    	
    	reg1 = spi_flash_read_status_Reg1();
    	reg2 = spi_flash_read_status_Reg2();
    	reg1 &= ~(1<<7);
    	reg2 &= ~(1<<0);
    	spi_flash_write_status_Reg(reg1,reg2);
    
    	spi_flash_wait_when_busy();
    }
    
    /* 去内存保护 */
    static void spi_flash_clear_protect_for_data(void)
    {
    	unsigned char reg1,reg2;
    	
    	reg1 = spi_flash_read_status_Reg1();
    	reg2 = spi_flash_read_status_Reg2();
    	reg1 &= ~(7<<2);
    	reg2 &= ~(1<<6);
    	spi_flash_write_status_Reg(reg1,reg2);	
    
    	spi_flash_wait_when_busy();
    }

        然后是擦除扇区,页编程和读数据

    /* 擦除扇区,erase 4K */
    void spi_flash_erase_sector(unsigned int addr)
    {
    	spi_flash_write_enable(1);
    
    	spi_set_CS(0);
    	flash_write_cmd(0x20);
    	flash_write_addr(addr);
    	spi_set_CS(1);
    
    	spi_flash_wait_when_busy();
    }
    
    /* 页编程 */
    void spi_flash_program(unsigned int addr,unsigned char *buf,int len)
    {
    	int i;
    	spi_flash_write_enable(1);
    
    	spi_set_CS(0);
    	flash_write_cmd(0x02);
    	flash_write_addr(addr);
    
    	for(i=0;i<len;i++){
    		flash_write_cmd(buf[i]);
    	}
    	
    	spi_set_CS(1);
    
    	spi_flash_wait_when_busy();	
    }
    
    /* 读数据 */
    void spi_flash_read(unsigned int addr,unsigned char *buf,int len)
    {
    	int i;
    
    	spi_set_CS(0);
    	flash_write_cmd(0x03);
    	flash_write_addr(addr);
    
    	for(i=0;i<len;i++){
    		buf[i] = flash_read_byte();
    	}
    	
    	spi_set_CS(1);
    
    }

        讲到这里就讲完了,谢谢。

        我将详细的代码放到了:2440SPI控制器控制OLED和FLASH

     

    展开全文
  • 上一篇文章,描述的是如何驱动spi的屏幕,嵌入式Linux驱动笔记(二十四)------framebuffer之使用spi-tft屏幕 但是是使用的是在内核里开一个线程来不停的绘制图形,CPU占用率非常高,效率低。 有种较为方便的办法,...

    你好!这里是风筝的博客,

    欢迎和我一起交流。


    上一篇文章,描述的是如何驱动spi的屏幕,嵌入式Linux驱动笔记(二十四)------framebuffer之使用spi-tft屏幕(上)
    但是是使用的是在内核里开一个线程来不停的绘制图形,CPU占用率非常高,效率低。
    有种较为方便的办法,就是局部刷新,每次只重绘“脏区”即可。
    参考了github里几个9225芯片的驱动,对本驱动进行了一些改进,主要是加入fb_deferred_io_init函数,建立延定时工作队列。

    标准frambuffer操作api->mkdirty->fbtft_mkdirty()->schedule_delayed_work(&info->deferred_work, fbdefio->delay);->deferred_io()->fbtft_deferred_io()
    故只要定期执行deferred_io()即可完成FPS

    参考:ili9225.c

    ili9225fb.c

    #include <linux/init.h>
    #include <linux/fs.h>
    #include <linux/slab.h>
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/device.h>
    #include <sound/core.h>
    #include <linux/spi/spi.h>
    #include <asm/uaccess.h>
    #include <linux/cdev.h>
    #include <linux/sched.h>
    #include <linux/vmalloc.h>
    #include <linux/fb.h>
    #include <linux/delay.h>
    
    #include <linux/gpio.h>
    #include <linux/errno.h>
    #include <linux/sched.h>
    #include <linux/wait.h>
    #include <asm/mach/map.h>
    #include <linux/dma-mapping.h>
    
    
    //#define USE_HORIZONTAL
    //#define __DEBUG__ 1
    //#define OPEN_FPS 1
    
    
    #ifdef __DEBUG__
    #define DEBUG(format,...) \
            printk("DEBUG::"format,  ##__VA_ARGS__)
    #else
    #define DEBUG(format,...)
    #endif
    
    
    #define LCD_X_SIZE          176
    #define LCD_Y_SIZE          220
    
    #ifdef USE_HORIZONTAL//如果定义了横屏
    #define X_MAX_PIXEL         LCD_Y_SIZE
    #define Y_MAX_PIXEL         LCD_X_SIZE
    #else//竖屏
    #define X_MAX_PIXEL         LCD_X_SIZE
    #define Y_MAX_PIXEL         LCD_Y_SIZE
    #endif
    
    static struct timer_list frame_timer;
    
    static int tft_lcdfb_setcolreg(unsigned int regno, unsigned int red,
    			     unsigned int green, unsigned int blue,
    			     unsigned int transp, struct fb_info *info);
    
    struct tft_lcd{
        struct gpio_desc *reset_gpio;   
    	struct gpio_desc *rs_gpio;
    };
    
    struct fb_page {
    	unsigned short x;
    	unsigned short y;
    	u32 *buffer;//unsigned short
    	unsigned short len;
    };
    
    struct tft_lcd_fb{
    	struct spi_device *spi; //记录fb_info对象对应的spi设备对象
    	struct fb_info *fbi;
    	unsigned int pages_count;
    	struct fb_page *pages;
    	u32 *last_buffer_start;//unsigned short
    	struct task_struct *thread; //记录线程对象的地址,此线程专用于把显存数据发送到屏的驱动ic
    };
    
    static struct fb_ops fops = {
    	.owner		= THIS_MODULE,
    	.fb_setcolreg	= tft_lcdfb_setcolreg,	/*设置颜色寄存器*/
    	.fb_fillrect	= cfb_fillrect,			/*用像素行填充矩形框,通用库函数*/
    	.fb_copyarea	= cfb_copyarea,			/*将屏幕的一个矩形区域复制到另一个区域,通用库函数*/
    	.fb_imageblit	= cfb_imageblit,		/*显示一副图像,通用库函数*/
    };
    
    struct regdata_t{
            u8  reg;
            u16 data;
            int delay_ms;
    }regdatas[] = {
        {0x10, 0x0000, 0}, {0x11, 0x0000, 0}, {0x12, 0x0000, 0},
        {0x13, 0x0000, 0}, {0x14, 0x0000, 40},
    
        {0x11, 0x0018, 0}, {0x12, 0x1121, 0}, {0x13, 0x0063, 0},
        {0x14, 0x3961, 0}, {0x10, 0x0800, 10}, {0x11, 0x1038, 30},
    
        {0x02, 0x0100, 0}, 
    #ifdef USE_HORIZONTAL//如果定义了横屏
    	{0x01, 0x001c, 0}, {0x03, 0x1038, 0},
    #else//竖屏
    	{0x01, 0x011c, 0}, {0x03, 0x1030, 0},
    #endif
    
        {0x07, 0x0000, 0}, {0x08, 0x0808, 0}, {0x0b, 0x1100, 0},
        {0x0c, 0x0000, 0}, {0x0f, 0x0501, 0}, {0x15, 0x0020, 0},
        {0x20, 0x0000, 0}, {0x21, 0x0000, 0},
    
        {0x30, 0x0000}, {0x31, 0x00db}, {0x32, 0x0000}, {0x33, 0x0000},
        {0x34, 0x00db}, {0x35, 0x0000}, {0x36, 0x00af}, {0x37, 0x0000},
        {0x38, 0x00db}, {0x39, 0x0000},
    
        {0x50, 0x0603}, {0x51, 0x080d}, {0x52, 0x0d0c}, {0x53, 0x0205},
        {0x54, 0x040a}, {0x55, 0x0703}, {0x56, 0x0300}, {0x57, 0x0400},
        {0x58, 0x0b00}, {0x59, 0x0017},
    
        {0x0f, 0x0701}, {0x07, 0x0012, 50}, {0x07, 0x1017},
    }; 
    
    int frames = 0;
    #ifdef 	OPEN_FPS
    static void frames_timer_function(unsigned long data)
    {
    	printk("frames is %d fps\n",frames);
    	frames = 0;
    	//mod_timer(&frame_timer, jiffies+HZ/1000);
    	mod_timer(&frame_timer, jiffies + (1*HZ));
    }
    #endif
    
    static void Lcd_WriteIndex(struct spi_device *spi, u8 Index)
    {
    	struct tft_lcd *pdata = spi_get_drvdata(spi);
    
    	gpiod_set_value(pdata->rs_gpio, 0); //高电平
    	spi_write(spi, &Index, 1);
    }
    
    static void Lcd_WriteData_16Bit(struct spi_device *spi, u16 Data)
    {   
    	u8 buf[2];
    	struct tft_lcd *pdata = spi_get_drvdata(spi);
    
    	buf[0] = ((u8)(Data>>8));
    	buf[1] = ((u8)(Data&0x00ff));
    
    	gpiod_set_value(pdata->rs_gpio, 1); //高电平
    	spi_write(spi, &buf[0], 1);
    	spi_write(spi, &buf[1], 1);   
    }
    
    static void LCD_WriteReg(struct spi_device *spi, u8 Index, u16 Data)
    {
    	int addr;
    	addr = Index;
        Lcd_WriteIndex(spi, addr);
        Lcd_WriteData_16Bit(spi, Data);
    }
    
    static void Lcd_SetRegion(struct spi_device *spi, u8 xStar, u8 yStar,u8 xEnd,u8 yEnd)
    {
    #ifdef USE_HORIZONTAL//如果定义了横屏 
    		LCD_WriteReg(spi,0x38,xEnd);
    		LCD_WriteReg(spi,0x39,xStar);
    		LCD_WriteReg(spi,0x36,yEnd);
    		LCD_WriteReg(spi,0x37,yStar);
    		LCD_WriteReg(spi,0x21,xStar);
    		LCD_WriteReg(spi,0x20,yStar);
    #else//竖屏   
    		LCD_WriteReg(spi,0x36,xEnd);
    		LCD_WriteReg(spi,0x37,xStar);
    		LCD_WriteReg(spi,0x38,yEnd);
    		LCD_WriteReg(spi,0x39,yStar);
    		LCD_WriteReg(spi,0x20,xStar);
    		LCD_WriteReg(spi,0x21,yStar);
    #endif
    		Lcd_WriteIndex(spi,0x22);	
    
    }
    
    static int lcd_dt_parse(struct spi_device *spi, struct tft_lcd *lcd_data)
    {
    
        lcd_data->reset_gpio = devm_gpiod_get(&spi->dev, "rest", GPIOD_OUT_HIGH);
        if (IS_ERR(lcd_data->reset_gpio))
            goto err0;
    	gpio_direction_output(desc_to_gpio(lcd_data->reset_gpio), 1);
    
    	lcd_data->rs_gpio = devm_gpiod_get(&spi->dev, "rs", GPIOD_OUT_HIGH);
        if (IS_ERR(lcd_data->rs_gpio))
            goto err1;
    	gpio_direction_output(desc_to_gpio(lcd_data->rs_gpio), 1);
    	
    	return 0;
    
    err1:
    	devm_gpiod_put(&spi->dev, lcd_data->reset_gpio);
    err0:
    	DEBUG("[%s]:failed\n", __FUNCTION__);
    	return -1;
    }
    
    static void lcd_init(struct spi_device *spi, struct tft_lcd *pdata)
    {
    	int i =0;
    	gpiod_set_value(pdata->reset_gpio, 0); //设低电平
        msleep(100);
        gpiod_set_value(pdata->reset_gpio, 1); //设高电平
        msleep(50);
    
    	for (i = 0; i < ARRAY_SIZE(regdatas); i++)
        {
            LCD_WriteReg(spi, regdatas[i].reg, regdatas[i].data);
            if (regdatas[i].delay_ms)
                msleep(regdatas[i].delay_ms);
        }
    
    }
    
    void show_fb(struct fb_info *fbi, struct spi_device *spi)
    {
        int x, y;
        u32 k;
        u32 *p = (u32 *)(fbi->screen_base);
        u16 c;
        u8 *pp;
    
        //从屏的0,0坐标开始刷
        Lcd_SetRegion(spi, 0,0,X_MAX_PIXEL-1,Y_MAX_PIXEL-1);
     	DEBUG("[%s] \n",__FUNCTION__);
        for (y = 0; y < fbi->var.yres; y++)
        {
            for (x = 0; x < fbi->var.xres; x++)
            {
                k = p[y*fbi->var.xres+x];//取出一个像素点的32位数据
                // rgb8888 --> rgb565       
                pp = (u8 *)&k;
                c = pp[0] >> 3; //蓝色
                c |= (pp[1]>>2)<<5; //绿色
                c |= (pp[2]>>3)<<11; //红色
    
                //发出像素数据的rgb565
                //write_data(spi, c >> 8);
                //write_data(spi, c & 0xff);
    			Lcd_WriteData_16Bit(spi, c);
            }
        }
    
    }
    static void fb_update(struct fb_info *fbi, struct list_head *pagelist);
    
    int thread_func_fb(void *data)
    {
        struct fb_info *fbi = (struct fb_info *)data;
        struct tft_lcd_fb *lcd_fb = fbi->par;
    
        while (1)
        {
    		if (kthread_should_stop())
    			break;
            show_fb(fbi, lcd_fb->spi);
            //fb_update(fbi, &fbi->fbdefio->pagelist);
            frames++;
        }
    
        return 0;
    }
    
    static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
    {
    	chan &= 0xffff;
    	chan >>= 16 - bf->length;
    	return chan << bf->offset;
    }
    
    static u32 pseudo_palette[16];
    static int tft_lcdfb_setcolreg(unsigned int regno, unsigned int red,
    			     unsigned int green, unsigned int blue,
    			     unsigned int transp, struct fb_info *info)
    {
    	unsigned int val;
    	
    	if (regno > 16)
    	{
    		DEBUG("[%s] the regno is %d !!\n",__FUNCTION__, regno);
    		return 1;
    	}
    
    	/* 用red,green,blue三原色构造出val  */
    	val  = chan_to_field(red,	&info->var.red);
    	val |= chan_to_field(green, &info->var.green);
    	val |= chan_to_field(blue,	&info->var.blue);
    	
    	//((u32 *)(info->pseudo_palette))[regno] = val;
    	pseudo_palette[regno] = val;
    	return 0;
    }
    
    static void fb_copy(struct tft_lcd_fb *item, unsigned int index, struct spi_device *spi)
    {
    	unsigned short x;
    	unsigned short y;
    	u32 *buffer;
    	unsigned int len;
    	int i = 0;
    	u32 k;
    	u16 c;
        u8 *pp;
    	//  unsigned int count;
    	//unsigned int diff;
    				 
    	x = item->pages[index].x;
    	y = item->pages[index].y;		 
    	buffer = item->pages[index].buffer;
    	len = item->pages[index].len;
    				 
    	// check if we should change the chip framebuffer pointer
    	if (item->last_buffer_start!=buffer)
    	{
    		/*
    		if (x!=0)	 // The ssd1963 needs to start from X=0 to keep drawing in next lines 
    		{
    			diff = x*(item->info->var.bits_per_pixel / 16);
    			buffer-=diff;
    			len+=diff;
    		}
    		*/
    		//dev_dbg(item->dev, "%s: page[%u]: x=%3hu y=%3hu buffer=0x%p len=%3hu diff=%3hu\n",
    		//		__func__, index, x, y, buffer, len,diff);
    
    		Lcd_SetRegion(spi, x,y,X_MAX_PIXEL-1,Y_MAX_PIXEL-1);
    	}
    	
    	for (i = 0; i < len; i++)
        {
    		k = buffer[i];//取出一个像素点的32位数据
            // rgb8888 --> rgb565       
    		pp = (u8 *)&k;
    		c = pp[0] >> 3; //蓝色
    		c |= (pp[1]>>2)<<5; //绿色
    		c |= (pp[2]>>3)<<11; //红色
    
            //发出像素数据的rgb565
    		Lcd_WriteData_16Bit(spi, c);
    	}
    	item->last_buffer_start=&buffer[len];
    }
    
    static void fb_update(struct fb_info *fbi, struct list_head *pagelist)
    {
    	struct tft_lcd_fb *lcd_fb = (struct tft_lcd_fb *)fbi->par;
    	struct page *page;
    
    	DEBUG("[%s]:fb_update\n", __FUNCTION__);
    	list_for_each_entry(page, pagelist, lru) {
    		DEBUG("[%s]:the pages x is %d, y is %d \n",__func__, lcd_fb->pages[page->index].x, lcd_fb->pages[page->index].y);
    		fb_copy(lcd_fb, page->index, lcd_fb->spi);
    	}
    	frames++;
    }
    
    static struct fb_deferred_io fb_defio = {
    	.delay 		 = HZ/20,
    	.deferred_io	 = &fb_update,
    };
    
    static struct fb_var_screeninfo fb_var __initdata = {
        .xres 			= LCD_X_SIZE,
        .yres 			= LCD_Y_SIZE,
        .xres_virtual 	= LCD_X_SIZE,
        .yres_virtual 	= LCD_Y_SIZE,
        .bits_per_pixel = 32, // 屏是rgb565, 但QT程序只能支持32位.还需要在刷图时把32位的像素数据转换成rgb565
        .red.offset 	= 16,
        .red.length 	= 8,
        .green.offset 	= 8,
        .green.length 	= 8,
        .blue.offset 	= 0,
        .blue.length 	= 8,
    	.activate       = FB_ACTIVATE_NOW,
    	.vmode			= FB_VMODE_NONINTERLACED,
    };
    
    static struct fb_fix_screeninfo fb_fix __initdata= {
    	.type 			= FB_TYPE_PACKED_PIXELS,
        .visual 		= FB_VISUAL_TRUECOLOR,
        .line_length 	= LCD_X_SIZE*4,//32 bit = 4 byte
    	.accel 			= FB_ACCEL_NONE,
    	.id				= "myfb",
    };
    
    static int fb_video_alloc(struct tft_lcd_fb *item)
    {
    	unsigned int frame_size;
    				 
    	frame_size = item->fbi->fix.line_length * item->fbi->var.yres;
    	item->pages_count = frame_size / PAGE_SIZE;
    	if ((item->pages_count * PAGE_SIZE) < frame_size) {
    		item->pages_count++;
    	}
    	
    	item->fbi->fix.smem_len = item->pages_count * PAGE_SIZE;			 
    	item->fbi->fix.smem_start =
    						 (u32)vmalloc(item->fbi->fix.smem_len);	
    	if (!item->fbi->fix.smem_start) {
    		dev_err(&item->spi->dev, "[%s]: unable to vmalloc\n", __func__);
    		return -ENOMEM;
    	}
    	memset((void *)item->fbi->fix.smem_start, 0, item->fbi->fix.smem_len);
    				 
    	return 0;
    }
    
    static int fb_pages_alloc(struct tft_lcd_fb *item)
    {
    	u32 pixels_per_page;
    	u32 yoffset_per_page;
    	u32 xoffset_per_page;
    	u32 index;
    	unsigned short x = 0;
    	unsigned short y = 0;
    	u32 *buffer;
    	unsigned int len;
    
    	item->pages = kmalloc(item->pages_count * sizeof(struct fb_page), GFP_KERNEL);
    	if (!item->pages) {
    		dev_err(&item->spi->dev, "[%s]: unable to kmalloc for fb_page\n", __func__);
    		return -ENOMEM;
    	}
    
    	pixels_per_page = PAGE_SIZE / (item->fbi->var.bits_per_pixel / 8);
    	yoffset_per_page = pixels_per_page / item->fbi->var.xres;
    	xoffset_per_page = pixels_per_page -
    	    (yoffset_per_page * item->fbi->var.xres);
    	
    	DEBUG("[%s]: pixels_per_page=%hu "
    		"yoffset_per_page=%hu xoffset_per_page=%hu\n",
    		__func__, pixels_per_page, yoffset_per_page, xoffset_per_page);//5 144
    
    	buffer = (u32 *)item->fbi->fix.smem_start;
    	for (index = 0; index < item->pages_count; index++) {
    		len = (item->fbi->var.xres * item->fbi->var.yres) -
    		    (index * pixels_per_page);
    		if (len > pixels_per_page) {
    			len = pixels_per_page;
    		}
    
    		printk("%s: page[%d]: x=%3hu y=%3hu buffer=0x%p len=%3hu\n",
    			__func__, index, x, y, buffer, len);
    		item->pages[index].x = x;
    		item->pages[index].y = y;
    		item->pages[index].buffer = buffer;
    		item->pages[index].len = len;
    
    		x += xoffset_per_page;
    		if (x >= item->fbi->var.xres) {
    			y++;
    			x -= item->fbi->var.xres;
    		}
    		y += yoffset_per_page;
    		buffer += pixels_per_page;
    	}
    
    	/* tell the copy routines that the LCD controller is ready to receive data
     	  from the start of the buffer X,Y = (0,0) */
    	item->last_buffer_start=(u32 *)item->fbi->fix.smem_start;
    
    	return 0;
    }
    
    int tft_lcd_fb_register(struct spi_device *spi) //此函数在probe函数里被调用
    {
    	struct fb_info *fbi;
        //u8 *v_addr;
        //u32 p_addr;
    	struct tft_lcd_fb *lcd_fb;
    	int ret = 0;
    
    	//v_addr = dma_alloc_coherent(NULL, LCD_X_SIZE*LCD_Y_SIZE*4, &p_addr, GFP_KERNEL);
    
    	//额外分配tft_lcd_fb类型空间
        fbi = framebuffer_alloc(sizeof(struct tft_lcd_fb), &spi->dev);
    	if(fbi == NULL){
    		dev_err(&spi->dev, "[%s]: framebuffer_alloc failed\n", __func__);
    		goto out;
    	}
        lcd_fb = fbi->par; //data指针指向额外分配的空间
    	lcd_fb->spi = spi;
    	lcd_fb->fbi = fbi;
    
    	fbi->pseudo_palette = pseudo_palette;
    	fbi->var    = fb_var;
    	fbi->fix = fb_fix;
    	fbi->fbops = &fops;
    	fbi->flags = FBINFO_FLAG_DEFAULT | FBINFO_VIRTFB;
    
        //fbi->fix.smem_start = p_addr; //显存的物理地址
        //fbi->fix.smem_len = LCD_X_SIZE*LCD_Y_SIZE*4; 
    	
        //fbi->screen_base = v_addr; //显存虚拟地址
        //fbi->screen_base = dma_alloc_writecombine(NULL, fbi->fix.smem_len, &fbi->fix.smem_start, GFP_KERNEL);
        //fbi->screen_size = LCD_X_SIZE*LCD_Y_SIZE*4; //显存大小
    
    	ret = fb_video_alloc(lcd_fb);
    	if (ret) {
    		dev_err(&spi->dev, "[%s]: unable to fb_video_alloc\n", __func__);
    		goto out_info;
    	}
    	fbi->screen_base = (char __iomem *)lcd_fb->fbi->fix.smem_start;
    
    	ret = fb_pages_alloc(lcd_fb);
    	if (ret) {
    		dev_err(&spi->dev, "[%s]: unable to fb_pages_init\n", __func__);
    		goto out_info;
    	}
    
    	fbi->fbdefio = &fb_defio;
    	fb_deferred_io_init(fbi);
    	
        ret = register_framebuffer(fbi);
    	if (ret < 0) {
    		dev_err(&spi->dev, "[%s]: unable to register_frambuffer\n", __func__);
    		goto out_info;
    	}
    	
        //lcd_fb->thread = kthread_run(thread_func_fb, fbi, spi->modalias);
    	
        return 0; 
    
    out_info:
    	framebuffer_release(fbi);
    out:
    	return ret;
    
    }
    
    static void tft_fb_test(struct spi_device *spi)
    {
    	int i,j;
    	u16 color = 0x001f; /* rgb565,  蓝色 */
    	
    	Lcd_SetRegion(spi, 0,0,X_MAX_PIXEL-1,Y_MAX_PIXEL-1); //设置从屏哪个坐标开始显示,到哪个坐标结束
    
    	#define rgb(r, g, b)  ((((r>>3)&0x1f)<<11) | (((g>>2)&0x3f)<<5) | ((b>>3)&0x1f))
        for(i=0 ; i<Y_MAX_PIXEL/2 ; i++)
        {
    		
            color = rgb(255, 0, 255); 
            for(j=0; j<X_MAX_PIXEL/2; j++)
                Lcd_WriteData_16Bit(spi, color);//(u8 *)&
            color = rgb(255, 255, 0);
            for(j=X_MAX_PIXEL/2; j<X_MAX_PIXEL; j++)
                Lcd_WriteData_16Bit(spi, color);
        }
        for(i=Y_MAX_PIXEL/2 ; i<Y_MAX_PIXEL; i++)
        {
            color = rgb(0, 255, 255);
            for(j=0; j<X_MAX_PIXEL/2; j++)
                Lcd_WriteData_16Bit(spi, color);
    
            color = rgb(255, 0,0);
            for(j=X_MAX_PIXEL/2; j<X_MAX_PIXEL; j++)
                Lcd_WriteData_16Bit(spi, color);
        }
    }
    
    static int tft_lcd_probe(struct spi_device *spi)
    {
    	int ret;
    	
    	struct tft_lcd *lcd_data = devm_kzalloc(&spi->dev, sizeof(struct tft_lcd), GFP_KERNEL);
    
    	ret = lcd_dt_parse(spi, lcd_data);
    	if(ret !=0)
    		goto err0;
    
    	DEBUG("[%s]:success\n", __FUNCTION__);
    	spi_set_drvdata(spi, lcd_data);
    	lcd_init(spi, lcd_data);
    
    	tft_fb_test(spi);
    
    	ret = tft_lcd_fb_register(spi); //fb设备初始化
    	
    #ifdef 	OPEN_FPS
    	init_timer(&frame_timer);
    	frame_timer.expires = jiffies+(1*HZ);
    	frame_timer.function = frames_timer_function;
    	add_timer(&frame_timer);
    #endif
    
        return 0;
    
    err0:
    	devm_gpiod_put(&spi->dev, lcd_data->rs_gpio);
    	dev_err(&spi->dev, "[%s]:failed\n", __func__);
    	return ret;
    	
    }
    
    int tft_lcd_remove(struct spi_device *spi)
    {
        struct tft_lcd *pdata = spi_get_drvdata(spi);
    
        DEBUG("[%s]:success\n", __FUNCTION__);
    	devm_gpiod_put(&spi->dev, pdata->rs_gpio);
        devm_gpiod_put(&spi->dev, pdata->reset_gpio);
    
    	//kthread_stop(data->thread); //让刷图线程退出container_of(&temp.j,struct test,j);
        //unregister_framebuffer(fbi);
        //dma_free_coherent(NULL, fbi->screen_size, fbi->screen_base, fbi->fix.smem_start);
    
        return 0;
    }
    
    
    struct of_device_id tft_lcd_ids[] = {
        {.compatible = "nanopi,tft_lcd_spi"},
        {},
    };
    
    struct spi_driver tft_lcd_drv = {
            .probe	= tft_lcd_probe,
            .remove = tft_lcd_remove,
    
            .driver = {
                .owner = THIS_MODULE,
                .name = "tft_lcd_drv",
                .of_match_table = tft_lcd_ids,
            },
    };
    
    module_spi_driver(tft_lcd_drv);
    MODULE_LICENSE("GPL");
    MODULE_DESCRIPTION("TFT LCD SPI driver");
    

    这样即可做到局部刷新,哪里改变就刷新它的page即可。
    其实把,最好还是使用内核里的fbtft驱动,如果内核没有fbtft驱动,可以去github下载fbtft的驱动,地址:https://github.com/notro/fbtft
    Linux内核里也是有这个fbtft这个驱动的,比如我的Linux4.14就在drivers/staging/fbtft/目录下。
    在make menuconfig里选择对应的芯片驱动为m,作为模块,最后:
    make ARCH=arm CROSS_COMPILE=arm-linux- modules SUBDIRS=/work/linux/drivers/staging/fbtft/
    即可编译出ko文件,insmod即可使用。

    测试用例可以使用fbtft里的应用测试一下:https://github.com/notro/fbtft/wiki/Testing

    或者网上找一个参考试一试:一个简单的framebuffer的显示使用例子
    当然这个例子我没使用过,网上随便找的。

    最后,给个framebuffer驱动分析

    展开全文
  • 原文首发于跟韦东山学嵌入式Linux 韦东山嵌入式 学嵌入式Linux,就看韦东山视频。 我是1999年上的大学,物理专业。在大一时,我们班里普遍弥漫着对未来的不安,不知道学习了物理后出去能做什么。你当下的...

    原文首发于跟韦东山学嵌入式Linux

    【超详细】韦东山:史上最全嵌入式Linux学习路线图

     

    韦东山嵌入式

    韦东山嵌入式

    学嵌入式Linux,就看韦东山视频。

    我是1999年上的大学,物理专业。在大一时,我们班里普遍弥漫着对未来的不安,不知道学习了物理后出去能做什么。你当下的经历、当下的学习,在未来的一天肯定会影响到你。毕业后我们也各自找到了自己的职业:出国深造转行做金融、留校任教做科研、设计芯片、写程序、创办公司等等,这一切都离不开在校时学到的基础技能(数学、IT、电子电路)、受过煅炼的自学能力。

    所以,各位正在迷茫的在校生,各位正在尝试转行的程序员,未来一定有你的位置,是好是坏取决于你当下的努力与积累。

    我不能预言几年后什么行业会热门,也不能保证你照着本文学习可以发财。我只是一个有十几年经验的程序员,给对编程有兴趣的你,提供一些建议。

    程序员的三大方向

    程序员的方向,一般以分为3:专业领域、业务领域、操作系统领域。你了解它们后,按兴趣选择吧。对于专业领域,我提供不了建议。业务,也就是应用程序,它跟操作系统并不是截然分开的:

    ① 开发实体产品时,应用程序写得好的人,有时候需要操作系统的知识,比如调度优先级

    的设置、知道某些函数可能会令进程休眠。

    ② 写应用程序的人进阶为系统工程师时,他需要从上到下都了解,这时候就需要有操作

    系统领域的知识了,否则,你怎么设计整个系统的方案呢?

    ③ 做应用程序的人,需要了解行业的需求,理解业务的逻辑。所以,当领导的人,多是

    做应用的。一旦钻入了某个行业,很难换行业。

    ④ 而操作系统领域,做好了这是通杀各行业:他只负责底层系统,在上面开发什么业务跟

    他没关系。这行很多是技术宅,行业专家。

    ⑤ 操作系统和业务之间并没有一个界线。有操作系统经验,再去做应用,你会对系统知

    根知底,碰到问题时都有解决思路。有了业务经验,你再了解一下操作系统,很快就可组成一个团队自立门户,至少做个CTO没问题。

     

    专业领域

    它又可以分为下面2类。

    学术研究

    比如语音、图像处理、人工智能,这类工作需要你有比较强的理论知识,我倾向于认为这类人是“科学家”,他们钻研多年,很多时候是在做学术研究。

    在嵌入式领域,需要把他们的成果用某种算法表达出来,针对某种芯片进行优化,这部分工作也许有专人来做。

    工程实现

    也有这样一类人,他们懂得这些专业领域的概念,但是没有深入钻研。可以使用各类开源资料实现某个目标,做出产品。比如图像处理,他懂得用opencv里几百个复杂函数来实现头像识别。有时候还可以根据具体芯片来优化这些函数。

    “专业领域”不是我的菜,如果你要做这一块,我想最好的入门方法是在学校学习研究生、博士课程。

    业务领域

    换句话说,就是应用程序,这又可以分为下面2类。

    界面显示

    做产品当然需要好的界面,但是,不是说它不重要,是没什么发展后劲。

    现在的热门词是Android
    APP和IOS APP开发。你不要被Android、IOS两个词骗了,它们跟以前的VC、VB是同一路货色,只是、仅仅是一套GUI控件的实现。

     

    希望没有冒犯到你,我有理由。

    一个程序需要有GUI界面,但是程序的内在逻辑才是核心。Android、IOS的开发工具给我们简化了GUI的开发,并提供了这些控件的交互机制,封装并提供了一些服务(比如网络传输)。

    但是程序内部的业务逻辑、对视频图像声音的处理等等,这才是核心。另外别忘了服务器那边的后台程序:怎样更安全地保存数据、保护客户的隐私,怎样处理成千上万上百万的并发访问,等等,这也是核心。

    但是,从Android、IOS APP入门入行,这很快!如果你是大四,急于找到一份工作,那么花上1、2个月去学习Android或IOS,应该容易找到工作,毕竟APP的需求永远是最大的,现在这两门技术还算热门。在2011、2012年左右,Android程序员的起薪挺高,然后开始下滑。Android APP的入门基本只要1个月,所以懂的人也越来越多。2013、2014年,IOS开发的工资明显比Android高了,于是各类IOS培训也火曝起来。中华大地向来不缺速成人才,估计再过一阵子IOS工程师也是白菜价了。会Android、IOS只是基本要求,不信去51job搜搜Android或IOS,职位要求里肯定其他要求。

     

    业务逻辑

    举个简单例子,做一个打卡软件,你需要考虑这些东西:

    ① 正常流程是上班下班时都要打卡

    ② 有人忘记了怎么办?作为异常记录在案,推送给管理员

    ③ 请假时怎么处理?

    ④ 加班怎么处理?

    对于更复杂的例子,视频会议系统里,各个模块怎么对接,各类协议怎么兼容,你不深入这个行业,你根本搞不清楚。

    应用开发的职位永远是最多的,入门门槛也低。基本上只要你会C语言,面试时表现比较得体,一般公司都会给你机会。因为:

    ① 你进公司后,还需要重新培训你:熟悉它们的业务逻辑。

    ② 你要做的,基本也就是一个个模块,框架都有人给你定好了,你去填代码就可以了。

     

    说点让你高兴的事:软件公司里,做领导的基本都是写应用程序的(当然还有做市场的)。写应用程序的人,对外可以研究市场接待客户,对内可以管理程序员完成开发,不让他做领导让谁做?如果你的志向是写应用程序,那么我建议你先练好基本功:数据结构、算法是必备,然后凭兴趣选择数据库、网络编程等等进行深入钻研。最后,选择你看好的、感兴趣的行业深耕个10年吧。做应用开发的人选择了某个行业,后面是很难换行业的,选行很重要!

     

    操作系统领域

    UCOS太简单,VxWorks太贵太专业,Windows不玩嵌入式了,IOS不开源,所以对于操作系统领域我们也只能玩Linux了。

    在嵌入式领域Linux一家独大!

    Android呢?Android跟QT一样,都是一套GUI系统。只是Google的实力太强了,现在Android无处不在,所以很多时候Linux+Android成了标配。注意,在这里我们关心的是Android的整个系统、里面的机制,而不是学习几个API然后开发界面程序。

    操作系统领域所包含的内容,简单地说,就是制作出一台装好系统的专用“电脑”,可以分为:

    ① 为产品规划硬件:

    按需求、性能、成本选择主芯片,搭配周边外设,交由硬件开发人员设计。

    ② 给单板制作、安装操作系统、编写驱动

    ③ 定制维护、升级等系统方案

    ④ 还可能要配置、安装Android等GUI系统:

    ⑤ 为应用开发人员配置开发环境

    ⑥ 从系统角度解决疑难问题

    这个领域,通常被称为“底层系统”或是“驱动开发”。

    先解决2个常见误区:

    ① 这份工作是写驱动程序吗?

     

    看看上面罗列的6点,应该说,它包含驱动开发,但远远不只有驱动开发。

    ② 我们还需要写驱动吗?不是有原厂吗?或者只需要改改就可以?

    经常有人说,芯片原厂都做好驱动了,拿过来改改就可以了。如果,你的硬件跟原厂的公板完全一样,原厂源码毫无BUG,不想优化性能、削减成本,不想做一些有特色的产品,那这话是正确的。

     

    但是在这个不创新就是找死的年代,可能吗?!原因有二:

    ① 即使只是修改代码,能修改的前提是能理解;能理解的最好煅炼方法是从零写出若干

    驱动程序

    ② 很多时候,需要你深度定制系统。

    以前做联发科手机只需要改改界面就可以出货了,现在山寨厂一批批倒下。大家都使用原厂的方案而不加修改时,最后只能拼成本。举个例子,深圳有2家做交通摄像头、监控摄像头的厂家,他们曾经找我做过4个项目:

    ① 改进厂家给的SD卡驱动性能,使用DMA。

    ② 换了Flash型号后,系统经常出问题,需要修改驱动BUG。

    ③ 触摸屏点击不准,找原因,后来发现是旁路电容导致的。

    ④ 裁减成本,把4片DDR换为2片DDR,需要改bootloader对DDR的初始化。

    这些项目都很急,搞不定就无法出货,这时候找原厂?除非你是中兴华为等大客户,否则谁理你?

    我在中兴公司上班时,写驱动的时间其实是很少的,大部分时间是调试:系统调优,上帮APP工程师、下帮硬件工程师查找问题。我们从厂家、网上得到的源码,很多都是标准的,当然可以直接用。但是在你的产品上也许优化一下更好。比如我们可以把摄像头驱动和DMA驱动揉合起来,让摄像头的数据直接通过DMA发到DSP去。

     

    我们可以在软件和硬件之间起桥梁作用,对于实体产品,有可能是软件出问题也可能是硬件出问题,一般是底层系统工程师比较容易找出问题。当硬件、软件应用出现问题,他们解决不了时,从底层软件角度给他们出主意,给他们提供工具。再比如方案选择:芯片性能能否达标、可用的BSP是否完善等等,这只能由负责整个方案的人来考虑,他必须懂底层。

    在操作系统领域,对知识的要求很多:

    ① 懂硬件知识才能看懂电路图

    ② 英文好会看芯片手册

    ③ 有编写、移植驱动程序的能力

    ④ 对操作系统本身有一定的理解,才能解决各类疑难问题

    ⑤ 理解Android内部机制

    ⑥ 懂汇编、C语言、C++、JAVA

     

    它绝对是一个大坑,没有兴趣、没有毅力的人慎选。

    ① 这行的入门,绝对需要半年以上,即使全天学习也要半年。

    ② 它的职位,绝对比APP的职位少

    ③ 并且你没有1、2年经验,招你到公司后一开始你做的还是APP。

     

    优点就是

    ① 学好后,行业通杀,想换行就换行;想自己做产品就自己做产品。

    ② 相比做应用程序的人,不会被经常变动的需求搞得天天加班。

    ③ 门槛高,当然薪水相对就高。

    操作系统领域,我认为适合于这些人:

    ① 硬件工程师想转软件工程师,从底层软件入门会比较好

    ② 单片机工程师,想升级一下。会Linux底层的人肯定会单片机,会单片机的人不一定

    会Linux。

    ③ 时间充足的学生:如果你正读大二大三,那么花上半年学习嵌入式Linux底层多有益处。

    ④ 想掌握整个系统的人,比如你正在公司里写APP,但是想升为系统工程师,那么底层

    不得不学。

    ⑤ 想自己创业做实体产品的工程师,你有钱的话什么技术都不用学,但是如果没钱又想

    做产品,那么Linux底层不得不学。

    ⑥ 做Linux APP的人,没错,他们也要学习。

    这部分人不需要深入,了解个大概就可以:bootloader是用来启动内核,Linux的文件系统(第1个程序是什么、做什么、各目录干嘛用)、APP跟驱动程序的调用关系、工具链,有这些概念就可以了

     

    本文中,就把操作系统默认为Linux,讲讲怎么学习嵌入式Linux+Android系统。

     

    嵌入式Linux+Android系统包含哪些内容

    嵌入式Linux系统包含哪些东西?不要急,举一个例子你就知道了。

    ① 电脑一开机,那些界面是谁显示的?

    是BIOS,它做什么?一些自检,然后从硬盘上读入windows,并启动它。

    类似的,这个BIOS对应于嵌入式Linux里的bootloader。这个bootloader要去Flash上读入Linux内核,并启动它。

    ② 启动windows的目的是什么?

    当然运行应用程序以便上网、聊天什么的了。

    这些上网程序、聊天程序在哪?

    在C盘、D盘上。

    所以,windows要先识别出C盘、D盘。在Linux下我们称之为根文件系统。

    ③ windows能识别出C盘、D盘,那么肯定有读写硬盘的能力。

    这个能力我们称之为驱动程序。当然不仅仅是操作硬盘,还有网卡、USB等等其他硬件。

    嵌入式Linux能从Flash上读出并执行应用程序,肯定也得有Flash的驱动程序啊,当然也不仅仅是Flash。

     

    简单地说,嵌入式LINUX系统里含有bootloader、内核、驱动程序、根文件系统、应用程序这5大块。而应用程序,我们又可以分为:C/C++、Android。

    所以,嵌入式Linux+Android系统包含以下6部分内容:

    ① bootloader

    ② Linux内核

    ③ 驱动程序

    ④ 使用C/C++编写的应用程序

    ⑤ Android系统本身

    ⑥ Android应用程序

    Android跟Linux的联系实在太大了,它的应用是如此广泛,学习了Linux之后没有理由停下来不学习Android。在大多数智能设备中,运行的是Linux操作系统;它上面要么安装有Android,要么可以跟Android手机互联。现在,Linux+Android已成标配

     

    怎么学习嵌入式Linux操作系统

    本文假设您是零基础,以实用为主,用最快的时间让你入门;后面也会附上想深入学习时可以参考的资料。在实际工作中,我们从事的是“操作系统”周边的开发,并不会太深入学习、修改操作系统本身。

    ① 操作系统具有进程管理、存储管理、文件管理和设备管理等功能,这些核心功能非常

    稳定可靠,基本上不需要我们修改代码。我们只需要针对自己的硬件完善驱动程序

    ② 学习驱动时必定会涉及其他知识,比如存储管理、进程调度。当你深入理解了驱动程

    序后,也会加深对操作系统其他部分的理解

    ③ Linux内核中大部分代码都是设备驱动程序,可以认为Linux内核由各类驱动构成

    但是,要成为该领域的高手,一定要深入理解Linux操作系统本身,要去研读它的源代码。

    在忙完工作,闲暇之余,可以看看这些书:

    ① 赵炯的《linux内核完全注释》,这本比较薄,推荐这本。他后来又出了《Linux 内核

    完全剖析》,太厚了,搞不好看了后面就忘记前面了。

    ② 毛德操、胡希明的《LINUX核心源代码情景分析》,此书分上下册,巨厚无比。当作

    字典看即可:想深入理解某方面的知识,就去看某章节。

    ③ 其他好书还有很多,我没怎么看,没有更多建议

     

    基于快速入门,上手工作的目的,您先不用看上面的书,先按本文学习

     

    入门路线图

    假设您是零基础,我们规划了如下入门路线图。

     

     

    前面的知识,是后面知识的基础,建议按顺序学习。每一部分,不一定需要学得很深入透彻,下面分章节描述。

     

    学习驱动程序之前的基础知识

    C语言

    只要是理工科专业的,似乎都会教C语言。我见过很多C语言考试90、100分的一上机就傻了,我怀疑他们都没在电脑上写过程序。

    理论再好,没有实践不能干活的话,公司招你去干嘛?

    反过来,实践出真知,学习C语言,必须练练练、写写写!

    当你掌握基本语法后,就可以在电脑上练习一些C语言习题了;

    当你写过几个C程序后,就可以进入下一阶段的裸机开发了。

    ① 不需要太深入

    作为快速入门,只要你会编写“Hello, world!”,会写冒泡排序,会一些基础的语法操作,暂时就够了。指针操作是重点,多练习;

    不需要去学习过多的数据结构知识,只需要掌握链表操作,其他不用学习,比如:队列、二叉树等等都不用学;不需要去学习任何的函数使用,比如文件操作、多线程编程、网络编程等等;这些知识,在编写Linux应用程序时会用,但是在操作系统特别是驱动学习时,用不着!永往直前吧,以后碰到不懂的C语言问题,我们再回过头来学习。

    在后续的“裸机开发”中,会让你继续练习C语言,那会更实战化。

    C语言是在写代码中精进的。

     

    ② 可以在Visual Studio下学习,也可以在Linux下学习,后者需要掌握一些编译命令

    我们暂时没有提供C语言的教程,找一本C语言书,网上找找免费的C语言视频(主要看怎么搭建环境),就可以自学了。

    PC Linux基本操作:

    对于PC Linux,我们推荐使用Ubuntu,在它上面安装软件非常简便。

    我们的工作模式通常是这样:在Windows下阅读、编写代码,然后把代码上传到PC Linux去编译。实际上,Ubuntu的桌面系统已经很好用了,我们拿到各种智能机可以很快上手,相信Ubuntu的桌面系统也可以让你很快上手。为了提高工作效率,我们通常使用命令行来操作Ubuntu。不用担心,你前期只需要掌握这几条命令就可以了,它们是如此简单,我干脆列出它们:

    ① cd : Change
    Directory(改变目录)

    cd 目录名 // 进入某个目录

    cd .. // cd “两个点”:返回上一级目录

    cd - // cd “短横”:返回上一次所在目录

     

    ② pwd : Print Work Directory(打印当前目录 显示出当前工作目录的绝对路径)

    ③ mkdir : Make
    Directory(创建目录)

    mkdir abc // 创建文件夹abc

    mkdir -p a/b/c // 创建文件夹a,再a下创建文件夹b,再在b下创建文件夹c

    ④ rm : Remove(删除目录或文件)

    rm file // 删除名为file的文件

    rm -rf dir // 删除名为dir的目录

     

    ⑤ ls : List(列出目录内容)

    ⑥ mount : 挂载

    mount -t nfs -o nolock,vers=2 192.168.1.123:/work/nfs_root /mnt

    mount -t yaffs /dev/mtdblock3 /mnt

     

    ⑦ chown : Change
    owner(改变文件的属主,即拥有者)

    chown book:book /work -R // 对/work目录及其下所有内容,属主改为book用户,组改为book

    ⑧ chmod : Change mode(改变权限),下面的例子很简单粗暴

    chmod 777 /work -R // 对/work目录及其下所有内容,权限改为可读、可写、可执行

    ⑨ vi : Linux下最常用的编辑命令,使用稍微复杂,请自己搜索用法。

    要练习这些命令,你可以进入Ubuntu桌面系统后,打开终端输入那些命令;或是用SecureCRT、putty等工具远程登录Ubuntu后练习。

     

    硬件知识

    我们学习硬件知识的目的在于能看懂原理图,看懂通信协议,看懂芯片手册;不求能设计原理图,更不求能设计电路板。对于正统的方法,你应该这样学习:

    ① 学习《微机原理》,理解一个计算机的组成及各个部件的交互原理。

    ② 学习《数字电路》,理解各种门电路的原理及使用,还可以掌握一些逻辑运算(与、或等)。

    ③ 《模拟电路》?好吧,这个不用学,至少我在工作中基本用不到它,现在全忘光了。

     

    就我个人经验来说,这些课程是有用的,但是:

    ① 原理有用,实战性不强。

    比如《微机原理》是基于x86系统,跟ARM板子有很大差别,当然原理相通。

    我是在接触嵌入式编程后,才理解了这些课程。

    ② 每本书都那么厚,内容都很多,学习时间过长,自学有难度。

     

    针对这些校园教材的不足,并结合实际开发过程中要用到的知识点,我们推出了《学前班_怎么看原理图》的系列视频:

    学前班第1课第1节_怎么看原理图之GPIO和门电路.wmv

    学前班第1课第2.1节_怎么看原理图之协议类接口之UART.wmv

    学前班第1课第2.2节_怎么看原理图之协议类接口之I2C.wmv

    学前班第1课第2.3节_怎么看原理图之协议类接口之SPI.wmv

    学前班第1课第2.4节_怎么看原理图之协议类接口之NAND Flash.wmv

    学前班第1课第2.5节_怎么看原理图之协议类接口之LCD.wmv

    学前班第1课第3节_怎么看原理图之内存类接口.wmv

    学前班第1课第4.1节_怎么看原理图之分析S3C2410开发板.wmv

    学前班第1课第4.2节_怎么看原理图之分析S3C2440开发板.wmv

    学前班第1课第4.3节_怎么看原理图之分析S3C6410开发板.wmv

    即使你只具备初中物理课的电路知识,我也希望能通过这些视频,让你可以看懂原理图,理解一些常见的通信协议;如果你想掌握更多的硬件知识,这些视频也可以起个索引作用,让你知道缺乏什么知识。这些视频所讲到的硬件知识,将在《裸板开发》系列视频中用到,到时可以相互对照着看,加深理解。

     

    要不要专门学习Windows下的单片机开发

    很多学校都开通了单片机的课程,很多人都是从51单片机、AVR单片机,现在比较新的STM32单片机开始接触嵌入式领域,并且使用Windows下的开发软件,比如keil、MDK等。

    问题来了,要不要专门学习Windows下的单片机开发?

    ① 如果这是你们专业的必修课,那就学吧

    ② 如果你的专业跟单片机密切相关,比如机械控制等,那就学吧

    ③ 如果你只是想从单片机入门,然后学习更广阔的嵌入式Linux,那么放弃在Windows

    学习单片机吧!理由如下:

    ① Windows下的单片机学习,深度不够

    Windows下有很好的图形界面单片机开发软件,比如keil、MDK等。

    它们封装了很多技术细节,比如:

    你只会从main函数开始编写代码,却不知道上电后第1条代码是怎么执行的;

    你可以编写中断处理函数,但是却不知道它是怎么被调用的;

    你不知道程序怎么从Flash上被读入内存;

    也不知道内存是怎么划分使用的,不知道栈在哪、堆在哪;

    当你想裁剪程序降低对Flash、内存的使用时,你无从下手;

    当你新建一个文件时,它被自动加入到工程里,但是其中的机理你完全不懂;

    等等等。

     

    ② 基于ARM+Linux裸机学习,可以学得更深,并且更贴合后续的Linux学习。

    实际上它就是Linux下的单片机学习,只是一切更加原始:所有的代码需要你自己来编写;哪些文件加入工程,需要你自己来管理。在工作中,我们当然倾向于使用Windows下更便利的工具,但是在学习阶段,我们更想学习到程序的本质。一切从零编写代码、管理代码,可以让我们学习到更多知识:

    你需要了解芯片的上电启动过程,知道第1条代码如何运行;

    你需要掌握怎么把程序从Flash上读入内存;

    需要理解内存怎么规划使用,比如栈在哪,堆在哪;

    需要理解代码重定位;

    需要知道中断发生后,软硬件怎么保护现场、跳到中断入口、调用中断程序、恢复现场;

    你会知道,main函数不是我们编写的第1个函数;

    你会知道,芯片从上电开始,程序是怎么被搬运执行的;

    你会知道,函数调用过程中,参数是如何传递的;

    你会知道,中断发生时,每一个寄存器的值都要小心对待;

    等等等。

    你掌握了ARM+Linux的裸机开发,再回去看Windows下的单片机开发,会惊呼:怎么那么简单!并且你会完全明白这些工具没有向你展示的技术细节。

    驱动程序=Linux驱动程序软件框架+ARM开发板硬件操作,我们可以从简单的裸机开发入手,先掌握硬件操作,并且还可以:

    ① 掌握如何在PC Linux下编译程序、把程序烧录到板子上并运行它

    ② 为学习bootloader打基础:掌握了各种硬件操作后,后面一组合就是一个bootloader

    为什么选择ARM9 S3C2440开发板,而不是其他性能更好的?

    有一个错误的概念:S3C2440过时了、ARM9过时了。

    这是不对的,如果你是软件工程师,无论是ARM9、ARM11、A8还是A9,对我们来说是没有差别的。一款芯片,上面有CPU,还有众多的片上设备(比如UART、USB、LCD控制器)。我们写程序时,并不涉及CPU,只是去操作那些片上设备。

    所以:差别在于片上设备,不在于CPU核;差别在于寄存器操作不一样。

    因为我们写驱动并不涉及CPU的核心,只是操作CPU之外的设备,只是读写这些设备的寄存器。之所以推荐S3C2440,是因为它的Linux学习资料最丰富,并有配套的第1、2期视频。

     

    怎么学习ARM+Linux的裸机开发

    学习裸机开发的目的有两个:

    ① 掌握裸机程序的结构,为后续的u-boot作准备

    ② 练习硬件知识,即:怎么看原理图、芯片手册,怎么写代码来操作硬件

    后面的u-boot可以认为是裸机程序的集合,我们在裸机开发中逐个掌握各个部件,再集合起来就可以得到一个u-boot了。后续的驱动开发,也涉及硬件操作,你可以在裸机开发中学习硬件知识。

    注意:如果你并不关心裸机的程序结构,不关心bootloader的实现,这部分是可以先略过的。在后面的驱动视频中,我们也会重新讲解所涉及的硬件知识。

    推荐两本书:杜春蕾的《ARM体系结构与编程》,韦东山的《嵌入式Linux应用开发完全手册》。后者也许是国内第1本涉及在PC Linux环境下开发的ARM裸机程序的书,如果我说错了,请原谅我书读得少。

     

    对于裸机开发,我们提供有2部分视频:

    ① 环境搭建

    第0课第1节_刚接触开发板之接口接线.wmv

    第0课第2节_刚接触开发板之烧写裸板程序.wmv

    第0课第3节_刚接触开发板之重烧整个系统.wmv

    第0课第4节_刚接触开发板之使用vmware和预先做好的ubuntu.wmv

    第0课第5节_刚接触开发板之u-boot打补丁编译使用及建sourceinsight工程.wmv

    第0课第6节_刚接触开发板之内核u-boot打补丁编译使用及建sourceinsight工程.wmv

    第0课第7节_刚接触开发板之制作根文件系统及初试驱动.wmv

    第0课第8节_在TQ2440,MINI2440上搭建视频所用系统.wmv

    第0课第9节_win7下不能使用dnw烧写的替代方法.wmv

     

    ② 裸机程序开发

    第1课 环境搭建及工具、概念介绍.wmv

    第2课 GPIO实验.wmv

    第3课 存储管理器实验.wmv

    第4课 MMU实验.wmv

    第5课 NAND FLASH控制器.wmv

    第6课 中断控制器.wmv

    第7课 系统时钟和UART实验.wmv

    第8课 LCD实验.wmv

     

    要声明的是:

     

    录制上述《裸机程序开发》视频时,本意是结合《嵌入式Linux应用开发完全手册》的《第2篇 ARM9嵌入式系统基础实例篇》来讲解,所以视频里没有完全从零编写代码,需要结合书本来学习。

    ① 书和视频并不是完全配套的,不要照搬,其中的差异并不难解决。

    《嵌入式Linux应用开发完全手册》发表于2008年,使用了很多款开发板,并且那时的开发板配置较低(Nand Flash是64M);《裸机程序开发》视频使用JZ2440开发板录制。

    ② 书和视频,适用于所有S3C2440开发板,包括mini2440、tq2440等

    天下S3C2440配置都是相似的,基本也就是LED、按键所用引脚不同,LCD型号不同;你学习了书、视频,如果连这些差异都搞不定的话,那就是你我的失败了。

     

    学习方法是这样的:

    ① 先看《环境搭建》视频来搭建开发环境

    ② 书(第2篇)和视频(裸机程序开发)结合,看完一章,练习一章

    一定要编写代码,即使是照抄也要写。

    ③ 如果对于ARM架构相关的知识,觉得模糊或是想了解得更深入,参考《ARM体系结构与编程》

     

    学习程度:

    ① 理解一个裸机程序的必要结构:异常向量、硬件初始化、代码重定位、栈

    ② 知道如何操作GPIO、Flash、LCD、触摸屏等硬件

    ③ 很多人觉得MMU难以理解,可以放过它

     

    bootloader的学习

    bootloader有很多种,vivi、u-boot等等,最常用的是u-boot。

    u-boot功能强大、源码比较多,对于编程经验不丰富、阅读代码经验不丰富的人,一开始可能会觉得难以掌握。但是,u-boot的主要功能就是:启动内核。它涉及:读取内核到内存、设置启动参数、启动内核。按照这个主线,我们尝试自己从零编写一个bootloader,这个程序相对简单,可以让我们快速理解u-boot主要功能的实现。

     

    从零编写bootloader的视频有:

    毕业班第1课第1.1节_自己写bootloader之编写第1阶段.wmv

    毕业班第1课第1.2节_自己写bootloader之编写第2阶段.wmv

    毕业班第1课第2节_自己写bootloader之编译测试.wmv

    毕业班第1课第3节_自己写bootloader之改进.wmv

    分析u-boot 1.1.6的视频有:

    第9课第1节
    u-boot分析之编译体验.wmv

    第9课第2节
    u-boot分析之Makefile结构分析.wmv

    第9课第3节
    u-boot分析之源码第1阶段.wmv

    第9课第3节
    u-boot分析之源码第2阶段.wmv

    第9课第4节
    u-boot分析之u-boot命令实现.wmv

    第9课第5节
    u-boot分析_uboot启动内核.wmv

     

    移植一个全新u-boot的视频有:

    毕业班第2课第1节_移植最新u-boot之初试.wmv

    毕业班第2课第2.1节_移植最新u-boot之分析启动过程之概述.wmv

    毕业班第2课第2.2节_移植最新u-boot之分析启动过程之内存分布.wmv

    毕业班第2课第2.3节_移植最新u-boot之分析启动过程之重定位.wmv

    毕业班第2课第3.1节_移植最新u-boot之修改代码之建新板_时钟_SDRAM_UART.wmv

    毕业班第2课第3.2节_移植最新u-boot之修改代码支持NAND启动.wmv

    毕业班第2课第3.3节_移植最新u-boot之修改代码支持NorFlash.wmv

    毕业班第2课第3.4节_移植最新u-boot之修改代码支持NandFlash.wmv

    毕业班第2课第3.5节_移植最新u-boot之修改代码支持DM9000网卡.wmv

    毕业班第2课第4.1节_移植最新u-boot之裁剪和修改默认参数.wmv

    毕业班第2课第4.2节_移植最新u-boot支持烧写yaffs映象及制作补丁.wmv

     

    《嵌入式Linux应用开发完全手册》上对u-boot的讲解有如下章节:

    15.1 Bootloader简介

    15.1.1 Bootloader的概念

    15.1.2 Bootloader的结构和启动过程

    15.1.3 常用Bootloader介绍

    15.2 U-Boot分析与移植

    15.2.1 U-Boot工程简介

    15.2.2 U-Boot源码结构

    15.2.3 U-Boot的配置、编译、连接过程

    15.2.4 U-Boot的启动过程源码分析

    15.2.5 U-Boot的移植

    15.2.6 U-Boot的常用命令

    15.2.7使用U-Boot来执行程序

     

    学习方法如下:

     

    ① 先学习《从零编写bootloader的视频》,这可以从最少的代码理解bootloader的主要功能

    ② 再看书上对u-boot的讲解,并结合《分析u-boot 1.1.6的视频》来理解

    ③ 最后,有时间有兴趣的话,看《移植一个全新u-boot的视频》,这不是必须的。

     

    学习程度:

    ① 理解u-boot的启动过程,特别是u-boot代码重定位:怎么从Flash上把自己读入内存

    ② 理解u-boot的核心:命令

    ③ 知道bootloader如何给内核传递参数

    ④ 知道bootloader是根据“bootcmd”指定的命令启动内核

    ⑤ 作为入门:只求理解,不要求能移植u-boot

     

    Linux内核的学习

    前面说过,内核本身不是我们学习的重点,但是了解一下内核的启动过程,还是很有必要的:工作中有可能要修改内核以适配硬件,掌握了启动过程才知道去修改哪些文件。

     

    分析内核的视频有:

    第10课第1节 内核启动流程分析之编译体验.wmv

    第10课第2节 内核启动流程分析之配置.wmv

    第10课第3节 内核启动流程分析之Makefile.wmv

    第10课第4节 内核启动流程分析之内核启动.wmv

     

    移植内核的视频有:

    业班第3课第1节_移植3.4.2内核之框架介绍及简单修改.wmv

    毕业班第3课第2节_移植3.4.2内核之修改分区及制作根文件系统.wmv

    毕业班第3课第3节_移植3.4.2内核之支持yaffs文件系统.wmv

    毕业班第3课第4节_移植3.4.2内核之裁剪及ECC简介及制作补丁.wmv

     

    《嵌入式Linux应用开发完全手册》上对内核的讲解有如下章节:

    16.1 Linux版本及特点

    16.2 Linux移植准备

    16.2.1 获取内核源码

    16.2.2 内核源码结构及Makefile分析

    16.2.3 内核的Kconfig分析

    16.2.4 Linux内核配置选项

    16.3 Linux内核移植

    16.3.1 Linux内核启动过程概述

    16.3.2 修改内核以支持S3C2410/S3C2440开发板

    16.3.3 修改MTD分区

    16.3.4 移植YAFFS文件系统

    16.3.5 编译、烧写、启动内核

     

    学习方法如下:

    ① 先看书,并结合《分析内核的视频》进行理解

    ② 如果有兴趣,再根据《移植内核的视频》自己掌握移植内核,这不是必须的

     

    学习程度:

    ① 知道机器ID的作用,根据机器ID找到单板对应的文件

    ② 知道Makefile、Kconfig的作用,知道怎么简单地配置内核

    ③ 知道怎么修改分区

    ④ 作为入门:只求理解,不要求能移植

     

    根文件系统

    在驱动程序开发阶段,我们喜欢搭建一个最小根文件系统来调试驱动;

    在开发应用程序时,也需要搭建文件系统,把各种库、配置文件放进去;

    在发布产品时,你还需要修改配置文件,使得产品可以自动运行程序;

    甚至你想实现插上U盘后自动启动某个程序,这也要要修改配置文件;

    这一切,都需要你理解根文件系统的构成,理解内核启动后是根据什么配置文件来启动哪些应用程序。

     

    分析根文件系统的视频有:

    第11课第1节 构建根文件系统之启动第1个程序.wmv

    第11课第2节 构建根文件系统之init进程分析.wmv

    第11课第3节 构建根文件系统之busybox.wmv

    第11课第4节 构建根文件系统之构建根文件系统.wmv

     

    《嵌入式Linux应用开发完全手册》上对文件系统的讲解有如下章节:

    17.1 Linux文件系统概述

    17.1.1 Linux文件系统的特点

    17.1.2 Linux根文件系统目录结构

    17.1.3 Linux文件属性介绍

    17.2 移植Busybox

    17.2.1 Busybox概述

    17.2.2 init进程介绍及用户程序启动过程

    17.2.3 编译/安装Busybox

    17.3 使用glibc库

    17.3.1 glibc库的组成

    17.3.2 安装glibc库

    17.4 构建根文件系统

    17.4.1 构建etc目录

    17.4.2 构建dev目录

    17.4.3 构建其他目录

    17.4.4 制作/使用yaffs文件系统映象文件

    17.4.5 制作/使用jffs2文件系统映象文件

    学习方法:结合书和视频学习。

    学习程度:

    ① 理解配置文件的作用

    ② 知道根文件系统中lib里的文件来自哪里

    ③ 可以制作、烧写文件系统映象文件

     

    驱动程序的学习

    《嵌入式Linux应用开发完全手册》对驱动程序的讲解不多,我们推出的“韦东山Linux视频第2期_驱动现场编写调试”,可以认为完全脱离了书本。

    所以,驱动程序的学习完全按照视频来就可以了。

     

    第2期的视频,对每一个驱动,先讲解硬件原理,然后从零写代码,从简单到复杂,逐渐完善它的功能。我们不会罗列专业术语,会参考日常生活的例子,力争用最形象的比喻让你轻松入门,同时又会很深入。

    注意:我们可以让你入门时很轻松,但是要深入理解的话,这需要你跟着视频练习代码,这是个要慢慢思考的过程,不会轻松。

    轻松的话,凭什么拿高工资?

    再次申明:即使照抄也要写代码!很多人视频看得很高兴,但是写代码时就傻了。

     

    经典字符设备驱动程序

    视频中以LED、按键驱动为例,讲解并练习开发过程中碰到的机制:查询、休眠-唤醒、中断、异步通知、poll、同步、互斥等等。后续更复杂的驱动程序,就是在这些机制的基础上,根据硬件特性设计出精巧的软件框架。相关的视频有(文件名中带“_P”的属于第2期加密视频):

    第12课第1节 字符设备驱动程序之概念介绍.wmv

    第12课第2.1节 字符设备驱动程序之LED驱动程序_编写编译.wmv

    第12课第2.2节 字符设备驱动程序之LED驱动程序_测试改进.wmv

    第12课第2.3节 字符设备驱动程序之LED驱动程序_操作LED.wmv

    第12课第3节 字符设备驱动程序之查询方式的按键驱动程序.wmv

    第12课第4.1节 字符设备驱动程序之中断方式的按键驱动_Linux异常处理结构.wmv

    第12课第4.2节 字符设备驱动程序之中断方式的按键驱动_Linux中断处理结构.wmv

    第12课第4.3节 字符设备驱动程序之中断方式的按键驱动_编写代码.wmv

    第12课第5节 字符设备驱动程序之poll机制.wmv

    第12课第6节 字符设备驱动程序之异步通知.wmv

    第12课第7节 字符设备驱动程序之同步互斥阻塞.wmv

    第12课第8节 字符设备驱动程序之定时器防抖动_P.wmv

    第13课第1节 输入子系统概念介绍_P.wmv

    第13课第2节 输入子系统第编写驱动程序_P.wmv

     

    《嵌入式Linux应用开发完全手册》上对字符设备驱动程序的讲解有如下章节:

    第19章 字符设备驱动程序

    19.1 Linux驱动程序开发概述

    19.1.1 应用程序、库、内核、驱动程序的关系

    19.1.2 Linux驱动程序的分类和开发步骤

    19.1.3 驱动程序的加载和卸载

    19.2 字符设备驱动程序开发

    19.2.1 字符设备驱动程序中重要的数据结构和函数

    19.2.2 LED驱动程序源码分析

    第20章 Linux异常处理体系结构

    20.1 Linux异常处理体系结构概述

    20.1.1 Linux异常处理的层次结构

    20.1.2 常见的异常

    20.2 Linux中断处理体系结构

    20.2.1 中断处理体系结构的初始化

    20.2.2 用户注册中断处理函数的过程

    20.2.3 中断的处理过程

    20.2.4 卸载中断处理函数

    20.3 使用中断的驱动程序示例

    20.3.1 按键驱动程序源码分析

    20.3.2 测试程序情景分析

     

    学习方法

    ① 沿着数据流向,从应用程序的对驱动程序的使用进行情景分析。

    所谓情景分析,就是假设应用程序发起某个操作,你去分析其中的运作过程。比如应用程序调用open、read、ioctl等操作时涉及驱动的哪些函数调用。你要思考一个问题:一个应用程序,怎么获得按键信息,怎么去控制LED。把其中数据的流向弄清楚了,对字符驱动程序也就基本理解了。

    ② 学习异常和中断时,可以结合书和视频;对于驱动程序中其他内容的学习,可以不看书。

     

    工作中各类驱动程序

    我们的视频中讲解的驱动程序非常多,目的有二:

    ① 在你工作中遇到同类驱动时提供借鉴

    ② 供你学习、练习,煅炼阅读驱动程序的“语感”,提升编写程序的能力,增加调试经验

    我们还打算扩充驱动视频,把它打造成“Linux驱动程序大全”视频,基本上都会采取从零现场编写的方式。也许有人说:在工作中我们基本上只是移植、修改驱动而已,很少从头编写。这话没错,但是能修改的前提是理解;想更好地理解,最好的方法是从零写一个出来。在学习阶段,不要怕耗费太多时间,从零开始编写,慢慢完善它,在这过程中你既理解了这个驱动,也煅炼了能力,做到触类旁通。

    如果有时间,建议你学完这些所有的视频,直到你自认为:

    ① 给你一个新板,你可以很快实现相关驱动

    ② 给你一个新硬件,你可以很快给它编写/移植驱动。

     

    我们录制的视频很多,下面只罗列到“课”,不罗列到“节”。

    第2期视频:

    第14课 驱动程序分层分离概念_总线驱动设备模型

    第15课 LCD驱动程序

    第16课 触摸屏驱动程序

    第17课 USB驱动程序

    第18课 块设备驱动程序

    第19课 NAND FLASH驱动程序

    第20课 NOR FLASH驱动程序

    第21课 网卡驱动程序

    第22课 移植DM9000C驱动程序

    第23课 I2C设备裸板程序

    第24课 I2C驱动程序 (不看此课,看第32课,第32课讲得更好)

    第26课 声卡驱动程序 (不看此课,看第3期的ALSA驱动,那讲得更好)

    第27课 DMA驱动程序

    第28课 hotplug_uevent机制

    第32课 3.4.2内核下的I2C驱动程序

    第3期的驱动视频:

    摄像头驱动_虚拟驱动vivi

    摄像头驱动_USB摄像头

    摄像头驱动_CMOS摄像头

    WIFI网卡驱动程序移植

    3G网卡驱动程序移植

    ALSA声卡驱动程序

    学习方法:

    ① 再次强调,不能光看不练:一定要写程序,即使照抄也得写

    ② 必学:LCD、触摸屏、NAND Flash、Nor
    Flash、hotplug_uevent机制

    ③ 学完之后,强烈建议换一个不同的开发板,尝试在新板上写驱动程序。

    按视频学习会一切顺利,很多问题你可能没想到、没想通,换一个新板会让你真正掌握。

     

    调试方法

    有一种说法,程序是三分写七分调,我们从操作系统的角度提供了一些很有用的调试方法。

    相关的视频有:

    第29课第1节_裸板调试之点灯法_P.wmv

    第29课第2节_裸板调试之串口打印及栈初步分析_P.wmv

    第29课第3.1节_裸板调试之JTAG原理_P.wmv

    第29课第3.2节_裸板调试之JTAG调试体验_P.wmv

    第29课第3.3节_裸板调试之JTAG调试命令行调试_P.wmv

    第29课第3.4节_裸板调试之JTAG调试源码级调试_P.wmv

    第30课第1.1节_驱动调试之printk的原理_P.wmv

    第30课第1.2节_驱动调试之printk的使用_P.wmv

    第30课第1.3节_驱动调试之打印到proc虚拟文件_P.wmv

    第30课第2.1节_驱动调试之段错误分析_根据pc值确定出错的代码位置_P.wmv

    第30课第2.2节_驱动调试之段错误分析_根据栈信息确定函数调用过程_P.wmv

    第30课第3节_驱动调试之自制工具_寄存器编辑器_P.wmv

    第30课第4节_驱动调试之修改系统时钟中断定位系统僵死问题_P.wmv

    第31课第1节_应用调试之使用strace命令跟踪系统调用_P.wmv

    第31课第2节_应用调试之使用gdb和gdbserver_P.wmv

    第31课第3节_配置修改内核打印用户态段错误信息_P.wmv

    第31课第4.1节_应用调试之自制系统调用_P.wmv

    第31课第4.2节_应用调试之使用自制的系统调用_P.wmv

    第31课第5.1节_应用调试之输入模拟器之设计思路_P.wmv

    第31课第5.2节_应用调试之输入模拟器之编写保存功能_P.wmv

    第31课第5.3节_应用调试之输入模拟器之编写测试模拟功能_P.wmv

     

    Linux应用程序的学习

    对于大多数人来说,第1个C程序是在Windows的Visual Studio C++(简称VC)上写的,所以你们关心的也许是:嵌入式Linux应用程序,跟VC应用程序之间的区别:

    ① 编译方法不同:

    在VC上点点鼠标即可编译,对于嵌入式Linux应用程序,我们需要“交叉编译”:程序要在PC Linux上编译,但是运行时要放到单板上。并且,它的编译环境需要你自己搭建:解压出工具链后设计PATH,还要自己构造一套Makefile系统。

    ② 调试方法不同:

    在VC上点点鼠标就可以调试,对于嵌入式Linux应用程序,你可以更喜欢用打印;或是在PC Linux上通过GDB观察应用程序在单板上的运行状况。

    ③ 可用的资源不同:

    对于VC程序,你可以直接使用微软公司提供的各种类库;对于嵌入式Linux应用程序,很多时候需要去寻找、下载、编译、使用开源库。

    ④ 功能不同:

    VC程序运行在PC上,一般是用来解决某些纯软件的问题,比如整理数据、修图、联网播放音乐之类。嵌入式Linux应用程序一般都要操作若干种硬件,比如监控设备中要操作摄像头、存储音视频,无人机中要操作GPS、螺旋桨,POS机中要操作银行卡等等。它跟单板上的硬件联系很大,很多时候需要你懂点硬件知识,至少是知道怎么通过驱动程序来操作这些

    硬件。

    上述4点的不同,花很少的时间就可以掌握。

    如果你有志于开发应用程序,那么一定要有算法、数据结构、网络编程等基础,然后再掌握一些设计模式,最后就是多参加一些实际项目的开发了。

     

    基于我们提供的视频,你可以这样学习:

    ① 先掌握第1期讲解的根文件系统:

    在后续学习中你会经常构建根文件系统,比如往里面添加库、修改配置文件让你的程序自动运行。

    ② 掌握怎么编译、烧写u-boot、内核:

    在实际工作中,一般来说不需要你去烧写u-boot、内核,但是在自学阶段还是自己掌握吧,免得去麻烦别人。按开发板手册即可操作,你甚至不用管里面的原理。

    ③ 掌握Makefile:

    可以看如下第3期视频,以后编译程序时只要执行make命令即可:

    第1课第4节_数码相框_编写通用的Makefile

    ④ 学习第1个项目:数码相框

    该项目不使用任何开源的GUI项目,完全是自己构建一套GUI系统,实现了文件浏览、文件显示(文本和图片)、图片操作(放大、缩小、自动播放)等功能;涉及网络编程、多线程编程、开源库使用等等。

    虽然数码相框作为一个产品已经落伍了,但是该项目所涉及的技术,特别是以面向对象的编程思想设计出一个模块化的、易扩展的系统,非常适合没有大型项目开发经验的人。很多同学,都是根据该项目所教会的编程思想找到了心怡的工作。

     

    第3期视频取名为“项目开发”,而非“应用开发”,它的第2、3个项目跟内核、驱动耦合很大。如果只关心应用开发,或是急于找一份工作,可以先看第1个项目。

     

    第2个项目涉及摄像头、ALSA声卡、WIFI网卡、3G网卡,这些都是在实际工作过程中经常用到的设备,比如我们后面补充的QQ物联就用到摄像头、声卡、WIFI网卡。

    第3个项目是电源管理,讲解怎么讲你的单板休眠以省电。



    另外本人还开设了个人公众号:JiandaoStudio ,会在公众号内定期发布行业信息,以及各类免费代码、书籍、大师课程资源。

     

                                                

    扫码关注本人微信公众号,有惊喜奥!公众号每天定时发送精致文章!回复关键词可获得海量各类编程开发学习资料!

    例如:想获得Python入门至精通学习资料,请回复关键词Python即可。

    展开全文
  • 注:该系列文章是我转载formycuteboy的博客的,在此非常...本系列文章对Linux设备模型中的SPI子系统进行讲解。SPI子系统的讲解将分为4个部分。  第一部分,将对SPI子系统整体进行描述,同时给出SPI的相关数据结构,
    注:因为之前没接触过spi驱动开发,该系列文章是我参照博主formycuteboy的文章并结合自己的平台添加了自己的见解,本系列文章对于每个结构体变量以及函数都会有详细的解释该在此非常感谢formycuteboy!(formycuteboy是基于S3C2440,本文是基于itop4412),在此表示感谢的同时还想激励自己可以做出更多原创作品。

    0.引言


    本系列文章对Linux设备模型中的SPI子系统进行讲解。SPI子系统的讲解将分为4个部分。

     第一部分,将对SPI子系统整体进行描述,同时给出SPI的相关数据结构,最后描述SPI总线的注册。

     第二部分,将对SPI的主控制器(master)驱动进行描述。     嵌入式Linux驱动——SPI子系统解读(二)

     第三部分,将对SPI设备驱动,也称协议(protocol) 驱动进行描述。 嵌入式Linux驱动——SPI子系统解读(三)

     第四部分,通过SPI设备驱动留给用户层的API,我们将从上到下描述数据是如何通过SPI的protocol 驱动,由bitbang中转,最后由master驱动将数据传输出去。     嵌入式Linux驱动——SPI子系统解读(四)

    1.SPI子系统综述

    SPI子系统从上到下分为:spi设备驱动层核心层master驱动层。其中master驱动抽象出spi控制器的相关操作,而spi设备驱动层抽象出了用户空间API。
    platform_device结构中描述了SPI控制器的相关资源,同时在板级信息中将会添加spi设备的相关信息。master驱动将以platform_driver形式体现出来,也就是说在主控制器(master)和主控制器驱动将挂载到platform总线上。platform_driver的probe函数中将注册spi_master,同时将会获取在板级信息中添加的spi设备,将该信息转换成spi_device,然后注册spi_device到spi总线上。spi_driver结构用于描述spi设备驱动,也将挂载到spi总线上。连同spi_driver一起注册的是字符设备,该字符设备将提供5个API给用户空间。通过API,用户空间可以执行半双工读、半双工写和全双工读写。

    2. SPI的相关数据结构

      这里将介绍内核所用到的关键数据结构,还有些结构将在用到时加以说明。

      2.1 spi_master

    该结构体主要用于描述spi主控器的信息。

    struct spi_master { 
          struct device dev;        //驱动的设备接口
          s16   bus_num;            //总线号(每个控制器的特定标识符)
          u16   num_chipselect;     //片选的设备数(可以挂载在该控制器上的从设备个数) 
          u16   dma_alignment;      //dma模式 
          int  (*setup)(struct spi_device *spi);     //更新SPI设备的模式和SPI设备的采样时钟
          int  (*transfer)(struct spi_device *spi,struct spi_message *mesg);//添加一个消息到控制器的传输队列
          void (*cleanup)(struct spi_device *spi); 
       }; 

      2.2 spi_device

      该结构用于描述SPI设备,也就是从设备的相关信息。(注:SPI子系统只支持主模式,也就是说S3C2440的SPI只能工作在master模式,外围设备只能为slave模式。)

    struct spi_device { 
        struct device     dev;            //spi设备
        struct spi_master *master;        //spi控制器master
        u32               max_speed_hz;   //最大允许的频率
        u8                chip_select;    //片选号
        u8                mode;           //spi从设备的模式
    #define SPI_CPHA   0x01               /* clock phase */ 
    #define SPI_CPOL   0x02               /* clock polarity */ 
    #define SPI_MODE_0 (0|0)              /* (original MicroWire) */ 
    #define SPI_MODE_1 (0|SPI_CPHA) 
    #define SPI_MODE_2 (SPI_CPOL|0) 
    #define SPI_MODE_3 (SPI_CPOL|SPI_CPHA) 
    #define SPI_CS_HIGH     0x04          /* chipselect active high? */ 
    #define SPI_LSB_FIRST   0x08          /* per-word bits-on-wire */ 
    #define SPI_3WIRE  0x10               /* SI/SO signals shared */ 
    #define SPI_LOOP   0x20               /* loopback mode */ 
        u8                bits_per_word;  //每次穿的位数
        int               irq;            //用到的中断号
        void              *controller_state; 
        void              *controller_data; 
        char              modalias[32];  
    }; 

      2.3 spi_board_info

      该结构也是对从设备的描述,只不过它是板级信息,最终该结构的所有信息将复制给spi_device。
    struct spi_board_info { 
        char          modalias[32]; 
        const  void   *platform_data; 
        void          *controller_data; 
        int           irq; 
        u32           max_speed_hz;  
        u16           bus_num; 
        u16           chip_select;  
        u8            mode; 
    }; 

      2.4 spi_driver

      该结构用于描述SPI设备驱动。(驱动核心将根据driver.name和spi_board_info 的modalias进行匹配,如过modalia和name相等,则绑定驱动程序和SPI设备)
    struct spi_driver{
        int    (*probe)(struct spi_device *spi);
        int    (*remove)(struct spi_device *spi);
        void   (*shutdown)(struct spi_device *spi);
        int    (*suspend)(struct spi_device *spi,pm_message_t mesg);
        int    (*resume)(stuct spi_device *spi);
        struct device_driver driver;
    };
     

      2.5 spi_transfer

      该数据结构是对一次完整的数据传输的描述。
    struct spi_transfer {  
        const void    *tx_buf; 
        void          *rx_buf; 
        unsigned      len; 
        dma_addr_t    tx_dma; 
        dma_addr_t    rx_dma; 
        unsigned      cs_change:1; 
        u8            bits_per_word; 
        u16           delay_usecs; 
        u32           speed_hz; 
        struct list_head transfer_list; 
    }; 

      2.6 spi_message

    该结构就是对多个spi_transfer的封装。
    struct spi_message { 
        struct list_head    transfers; 
        struct spi_device  	*spi; 
        unsigned            is_dma_mapped:1; 
        void              	(*complete)(void *context); 
        void              	*context; 
        unsigned            actual_length; 
        int              	status; 
        struct list_head    queue; 
        void              	*state; 
    }; 

      2.7 spi_bitbang

    struct spi_bitbang { 
        struct workqueue_struct     *workqueue; 
        struct work_struct          work; 
        spinlock_t                  lock; 
        struct list_head            queue; 
        u8                          busy; 
        u8                          use_dma; 
        u8                          flags;     
        struct spi_master           *master; 
        int (*setup_transfer)(struct spi_device *spi, struct spi_transfer *t); 
        void(*chipselect)(struct spi_device *spi, int is_on); 
    #define BITBANG_CS_ACTIVE  1   /* normally nCS, active low */ 
    #define BITBANG_CS_INACTIVE0 
        int (*txrx_bufs)(struct spi_device *spi, struct spi_transfer *t); 
        u32(*txrx_word[4])(struct spi_device *spi, unsigned nsecs, u32 word, u8 bits); 
    };

    3. 注册SPI总线

      下列函数位于drivers/spi/spi.c中。

    struct bus_type spi_bus_type = { 
        .name           = "spi", 
        .dev_attrs      = spi_dev_attrs, 
        .match          = spi_match_device, 
        .uevent         = spi_uevent, 
        .suspend        = spi_suspend, 
        .resume         = spi_resume, 
    }; 
    EXPORT_SYMBOL_GPL(spi_bus_type); 
     
    static struct class spi_master_class = { 
        .name           = "spi_master", 
        .owner          = THIS_MODULE, 
        .dev_release    = spi_master_release, 
    }; 
     
    /* portable code must never pass more than 32 bytes */ 
    #define    SPI_BUFSIZ    max(32,SMP_CACHE_BYTES)  
    static u8    *buf; 
    static int __init spi_init(void) 
    { 
        int status; 
        buf = kmalloc(SPI_BUFSIZ, GFP_KERNEL); 
        if (!buf) { 
            status = -ENOMEM; 
            goto err0; 
        } 
        status = bus_register(&spi_bus_type);       /*注册SPI总线*/ 
        if (status < 0) 
            goto err1; 
     
        status = class_register(&spi_master_class);/*注册SPI类*/ 
        if (status < 0) 
            goto err2; 
        return 0; 
     
    err2: 
        bus_unregister(&spi_bus_type); 
    err1: 
        kfree(buf); 
        buf = NULL; 
    err0: 
        return status; 
    } 
    postcore_initcall(spi_init); 

    spi_init函数注册SPI总线以及SPI类到内核中。该函数在内核初始化的postcore_initcall阶段被调用。

    下面是总线的匹配函数。(drivers/spi/spi.c)

    static int spi_match_device(struct device *dev,struct device_driver *drv) 
    { 
        const struct spi_device *spi = to_spi_device(dev); 
        return strcmp(spi->modalias, drv->name) == 0; 
    }

      从这里我们可以看出,SPI设备和驱动的匹配是通过device的modalias字段和driver的name字段,这两个字段相等则绑定设备和驱动。

    展开全文
  • 【开发环境构建】 ...:https://pan.baidu.com/s/1bpakJtP // env/嵌入式linux软件开发环境(不定期更新)  步骤整理&lt;博客&gt;:http://blog.csdn.net/sinat_36184075/article/detai...

    【开发环境构建】
    1. 开发环境搭建:操作系统或虚拟机Ubuntu安装、网络服务配置、工具安装等
        工具资源<云盘>:https://pan.baidu.com/s/1bpakJtP   // env/嵌入式linux软件开发环境(不定期更新)
        步骤整理<博客>:http://blog.csdn.net/sinat_36184075/article/details/71194832

    2. 编程基础内容:交叉编译使用、Makefile规则、常用汇编指令
        交叉编译工具链制作<博客>:http://blog.csdn.net/sinat_36184075/article/details/71195114
        Makefile编写<博客>:http://blog.csdn.net/sinat_36184075/article/details/54917518
        汇编指令快速查询<博客>:http://blog.csdn.net/sinat_36184075/article/details/55819869

    3. 常用工具使用:
        windows下工具的使用(SourceInsight、SecureCRT、keil、IAR等)
        linux下工具的使用(tftp、nfs、vi、man手册、基本命令行命令及grep/find/tar/diff/patch等)
        // 常用工具上手简单,可自查。

    【ARM9嵌入式系统基础】
    1. GPIO接口
    1) 嵌入式开发步骤:
        编程:主要以.c .h文件为主;
        编译:make命令调用Makefile中写好的交叉编译规则来编译出.bin二进制文件;
        烧写:PC并口与JTAG连接或PC的USB与串口连接,使用对应的工具和命令烧写;
        运行测试:命令运行或复位开发板,观察运行效果。
    2) 通过GPIO引脚实现软件如何控制硬件:
        单个引脚的操作有3种:输出高低电平、检测引脚状态、中断;
        对某个引脚的操作通过读、写寄存器来实现;
        读写寄存器方法:通过编程程序,读写寄存器的地址;
        // 需结合2点:目标板电路图(确定引脚)、ARM芯片手册Datasheet(操作说明)

    2. 存储控制器
    1)目标板地址空间布局:
        32位CPU虚拟地址4G,其中1G为CPU内核空间,3G为用户空间;
        // 1G:0x0000_0000 ~ 0x4000_0000
        // 3G:0x4000_0000 ~ 0xFFFF_FFFF
        // 表:目标板功能部件与寄存器地址对照表
        // 表:存储控制器与所接外设的访问地址对照表

    3. 内存管理单元MMU
    1) 虚拟地址和物理地址关系
        MMU:负责虚拟地址到物理地址的映射,提供硬件机制的内存访问权限检查。
        通过MMU使得各个用户进程都有自己独立的地址空间。
        ARM的CPU上地址转换有3个概念:虚拟地址VA、变换后的虚拟地址MVA、物理地址PA。
        没启动MMU时,CPU、cache、MMU、外设等所有部件使用的是物理地址;
        启动MMU之后,CPU对外发出虚拟地址VA,VA被转换为MVA供cache、MMU使用,MVA在这里被转换为PA,最后使用PA读写实际的硬件设备(内部寄存器或外接设备)。
        CPU核看到的、用到的只是虚拟地址VA。实际设备看不到VA和MVA,读写他们时使用的是物理地址PA。
    2) 通过设置MMU来控制虚拟地址到物理地址的转化
        虚拟地址VA >> 物理地址PA 方法有2:用一个确定的数学公式转换、用表格存储虚拟地址对应的物理地址(表格:页表Page Table)。
        页表:由一个个条目(Entry)组成,每个条目存储了一段VA对应的PA及其访问权限,或者下一页表的地址。
        ARM的CPU核使用的是第二种方法:页表。
    3) MMU的内存访问权限机制
        内存的访问权限检查是MMU的主要功能之一,决定了一块内存是否允许被读或被写。
        // 表:AP位、S、R位的访问权限对照表

    4. Nand Flash控制器
    1) Nand Flash芯片接口
        类似于PC上的硬盘,掉电不丢失,保存系统运行所必须的操作系统、应用程序、用户数据、其他数据等。
        容量:16M~512M
        与Nor Flash相比优点:Nand Flash性能高即擦写速度快3ms、可擦写次数多、生命周期10倍以上、容量大、价格低。
        与Nor Flash相比缺点:Nand Flash可靠性较低必须要有校验措施、易用性不如Nor Flash、不支持XIP(XIP:代码可以直接在Nor Flash上运行,无需加载到内存)。
        Flash存储器件的可靠性要考虑3点:位反转、坏块、可擦除次数。
        Nand Flash发生位反转的概率更高,推荐使用EDC/ECC进行错误检测和恢复。
        嵌入式linux对两种Flash的支持都很成熟,在Nor Flash上常用jffs2文件系统,在Nand Flash上常用yaffs文件系统。在更底层,有MTD驱动程序实现了对他们的读、写、擦除操作,也实现了EDC/ECC校验。
    2) 通过Nand Flash控制器访问Nand Flash的方法
        对Nand Flash进行访问控制时,需要先发出命令,然后发出地址序列,最后读写数据;需要使用各个使能信号来分辨是命令、地址、还是数据。

    5. 中断体系结构
    1) ARM的CPU中7种工作模式
        用户模式usr、快速中断模式fiq、中断模式irq、管理模式svc、数据访问终止模式abt、系统模式sys、未定义指令终止模式und。
        大多数程序运行与用户模式usr,进入其他6种特权模式是为了处理中断、异常、或者访问被保护的系统资源。
        ARM的CPU中有两种工作状态:32bit的ARM指令、16bit的Thumb指令。
        ARM9中有31个32bit寄存器 + 6个状态寄存器 = 37个寄存器。
    2) 中断服务程序编写方法
        CPU运行过程中,知道各类外设发生了某些事件如串口接收到了新数据、USB接口插入了设备、按下了某个按键等,主要通过2种方法:
        轮询:循环检查设备状态并作出相应反应。实现简单,常用在功能单一的系统中,如温控系统;缺点是占用CPU资源过高,不适用于多任务系统;
        中断:当某时间发生时,硬件会设置某个寄存器,中断当前程序,跳转执行此事件,最后返回被中断的程序。实现相对复杂,但效率很高,是常用的方法。
        使用中断的步骤:
        ①设置好中断模式和快速中断模式下的栈;
        ②准备好中断处理函数(ISR);
        ③设置中断优先级;
        ④进入、退出中断模式或快速中断模式时,需要保存、恢复被中断程序的运行环境;
        ⑤根据具体中断,设置相应外设;
        ⑥确定使用此中断的方式:FIQ或IRQ;
        ⑦使能中断。

    6. 系统时钟和定时器
    1) 系统时钟体系结构
        时钟控制逻辑给整个芯片提供三种时钟,
        FCLK:用于CPU核;
        HCLK:用于AHB总线(用高性能模块的连接)上的设备,如CPU核、存储器控制、中断控制器、LCD控制器、DMA和USB主机模块等;
        PCLK:用于APB总线(用于低带宽外设的连接)上的设备,如IIS、I2C、PWM定时器、MMC接口、ADC、UART、GPIO、RTC和SPI;
    2) 通过设置MPLL改变系统时钟的方法
        ①设置分频/倍频
        ②启动MPLL
        ③设置存储控制器SDRAM
        ④初始化定时器0
        ⑤定时器0中断使能及中断服务程序

    7. 通用异步收发器UART
    1) UART原理
        全双工方式串行双向数据收发。
        通信管脚跳线接线方式:
        RxD <=> TxD
        TxD <=> RxD
        GND <=> GND
        RS232逻辑电平:高电平(3~12V)表示0,低电平(-3~-12V)表示1。
        重要参数:
        >>数据位个数: 5 ~ 8 bit   (开发板那端定好的是 8 bit / 帧数据)
        >>验证方式:奇校验、偶校验、无校验
        >>停止位宽度:1~2bit
        >>通信的速率:bps (bit per second - 每秒传输bit位,即波特率)
        数据收发原理:
        发送数据时,CPU先将数据写入发送FIFO中,然后UART会自动将FIFO中的数据复制到发送移位器(TS)中,发送移位器将数据一位一位的发送到TxDn数据线上。
        接收数据时,接收移位器(RS)将RxDn数据线上的数据一位一位的接收进来,然后复制到接收FIFO中,CPU即可从中读取数据。
    2) UART使用
        ①将所涉及的UART通道管脚设为UART功能
        ②设置波特率
        ③设置传输格式
        ④设置时钟源、中断方式(或DMA模式)
        ⑤发送数据:往寄存器写入;接收数据:从寄存器读取
        ⑥UTRSTATn状态寄存器检测数据是否发送完毕、是否接收到数据
        结合PC上的串口工具windows下SecureCRT工具,linux下kermit(Page195)工具进行测试。

    8. I2C接口
    1) I2C总线协议
        飞利浦公司开发的串行总线,连接微控制器及其外围设备。
        两条总线:SDA数据线,SCL时钟线。
            多主机总线。
            主从设备通信方式。
            主机收发均可。
            多主机冲突仲裁。
            8bit双向数据传输,100kbit/s,400kbit/s,3.4Mbit/s。
            从设备地址唯一。
        三种类型信号:开始(S)、响应(ACK-8bit后的第9个时钟周期拉低SDA电平)、结束(P)
        数据传输格式:
        master:--S--从addr|W---接ACK---0x0d---接ACK---SR---从地址|R---接ACK---收---NACK---T--
        slave: --------接---------发ACK-----接---发ACK-----------接-------发ACK---
    2) I2C接口使用
        ①I2C控制器初始化
        ②主机发送函数
        ③主机接收函数
        ④中断服务程序i2c_Handle:清中断、写操作、读操作

    9. LCD控制器
    1) LCD显示器的接口及时序
        LCD,液晶显示器。
        CPU或显卡发出的信号时TTL信号,LCD本身接收的也是TTL信号。
        LCD控制器被用来传输LCD图像数据,并提供必要的控制信号。
        1幅图像为1帧frame。显示器从屏幕左上角,一行行的取得每个像素的数据并显示,沿着Z字形的路线进行扫描。
        分辨率:有效像素数据的 行数 × 列数。
    2) LCD控制器的使用方法
        ①main.c 提供菜单以选择不同的显示模式来操作LCD
        ②lcdlib.c 条用以下两个文件的函数操作LCD
        ③lcddrv.c 设置LCD控制器、调色板
        ④framebuffer.c 画点、画线、画同心圆、清屏

    10. ADC和触摸屏接口
    1) ADC和触摸屏结构
        ADC,数模转换。可以将模拟信号输入转换为二进制数据。
        // 图:ADC和触摸屏接口结构图
    2) ADC和触摸屏的使用方法
        ADC启动方法:手工启动、读结果时就自动启动下一次转换;
        是否已经结束:查询状态位、转换结束时发出中断。
        ADC操作只涉及三个寄存器:ADCCON、ADCTSC、ADCDAT0。
        ①设置ADCCON寄存器,选择输入信号通道,设置A/D转换器时钟;
        ②设置ADCTSC寄存器,使用设为普通转换模式,不适用触摸屏功能;
        ③设置ADCCON寄存器,启动A/D转换;
        ④转换结束时,读取ADCDAT0寄存器获得数值。
        // 拓展:电阻触摸屏原理(Page241)

    【嵌入式linux系统移植】
    1. 移植u-boot
    1) bootloader作用及工作流程
        作用:一小段程序,在系统上电时开始执行,初始化硬件设备、准备好软件环境、最后调用操作系统内核。
        也可以增强bootloader的功能,如增加网络功能、从PC上通过串口或网络下载文件、烧写文件、将Flash上压缩的文件解压后再运行等,强大的bootloader也叫Monitor。
        工作过程:仅对开发人员使用,在开发时通常需要各种命令操作bootloader,一般通过串口工具连接pc和目标板,可以在串口上输入各种命令,观察运行结果。
        嵌入式linux系统通常分为4个层次:bootloader、kernel、filesystem、Application
        bootloader启动流程分为两个阶段:
        第一阶段:
            1>硬件设备初始化
            2>为bootloader第二阶段准备RAM空间
            3>复制bootloader第二阶段代码到RAM中
            4>设置好栈
            5>跳转到第二阶段代码的C入口点
        第二阶段:
            1>初始化本阶段要使用到的硬件设备
            2>检测系统内存映射
            3>将内核映像和根文件系统映像从Flash上读到RAM空间中
            4>为内核设置启动参数
            5>调用内核
        u-boot是通用的bootloader,支持平台包括X86、ARM、PowerPC。
    2) u-boot代码结构及编译过程
        u-boot开源源码官方ftp下载地址:ftp://ftp.denx.de/pub/u-boot/
        以u-boot-1.1.6为例,根目录下有26个子目录,可以分为4类:
            1>平台相关(cpu/ lib_i386/)或开发板相关(board/)的;
            2>通用的函数(include/ lib_generic/ common/);
            3>通用的设备驱动程序(disk/ drivers/ dtt/ fs/ nand_spl/ net/ post/ rtc/);
            4>u-boot工具(tools/)、示例程序(examples/)、文档(doc/)。
        从readme和Makefile入手,根据目标开发板的名字board_name,通常执行2步进行编译:
            1> $:' make <board_name>_config
                // 配置对应目标板的配置文件
            2> $:' make all
                // 编译,生成u-boot.bin、u-boot、u-boot.srec三个文件
        注意:tools/目录下生成的mkimage文件,在编译内核时用来生成u-boot格式的内核映像文件。
    3) 移植u-boot流程
        使用网卡芯片硬件,tftp服务器软件进行移植:
        1> 配置开发板ip地址,服务器地址的环境变量
        2> 使用tftp命令进行指定起始地址位置下载u-boot.bin文件
        3> 修改相关文件使bootloader支持内核烧写、yaffs根文件系统移植;
        4> 修改默认配置参数方便使用,如linux启动参数、自动启动命令、默认网络设置
        (Page302)
    4) 常用u-boot命令
        help:查看所有u-boot中的命令及其作用,也可以help [命令]查看单个命令;也可以用?代替help进行执行。
        下载命令:loadb、loads、loadx、loady和tftpboot、nfs
            如loadb [off] [baud]
            如tftpboot [loadAddress] [bootfilename]
            如nfs [loadAddress] [host ip addr:bootfilename]
        内存操作命令:
            查看内存命令:md,如md[.b,.w,.l] address [count]
            修改内存命令:mm
            填充内存命令:mw
            复制内存命令:cp
        Nor Flash操作命令:
            查看Flash信息:flinfo
            加/解写保护命令:protect
            擦除命令:erase
        Nand Flash操作命令:
            nand命令,根据不同的参数进行不同的操作,如擦除、读取、烧写。
        启动命令:
            boot、bootm命令都是执行环境变量bootcmd所指定的命令。
    2. 移植linux内核
    1) 内核源码结构,内核启动过程
        获取内核源码:https://www.kernel.org/
        下载、解压得到linux内核源码文件。
        // 表:linux内核子目录结构
        Makefile:顶层的Makefile是所有Makefile文件的核心,总体控制内核的编译和连接。
        .config:配置文件,配置内核时产生。所有Makefile文件都是根据.config来决定使用哪些文件来编译。
        Linux内核启动过程:
            开始→确定内核是否支持该架构→确定内核是否支持该单板→建立一级页表→禁止ICache等→使能MMU→设置栈指针并调用start_kernel→输出linux版本信息→设置与体系结构相关的环境→初始化控制台→启动init进程
    2) 内核配置方法
        $:' make menuconfig
        System Type:系统类型子菜单,用来选择目标板类型。(Page324)
        Device Drivers:设备驱动程序子菜单。(Page327)
    3) 移植内核
        配置内核→编译内核→烧写内核→启动内核
    4) MTD设备分区方法
        MTD,内存技术设备,是Linux中ROM、Nor Flash、Nand Flash等存储设备抽象出来的一个设备层。
        分区方法:修改arch/arm/plat-s3c24xx/common-smdk.c文件中的smdk_default_nand_part结构即可。
    5) yaffs根文件系统移植
        yaffs,专门为Nand Flash设计的嵌入式文件系统,适用于大容量的存储设备。
        移植(Page346)
    3. 构建linux根文件系统
    1) linux文件系统层次标准
        FHS文件系统层次标准:http://www.pathname.com/fhs/
    2) 根文件系统下各目录的作用
        /bin 目录:存放所有用户都可以使用的、基本的命令。
        /sbin 目录:存放系统命令,即只有管理员能够使用的命令,用于启动系统、修复系统等。
        /dev 目录:存放设备文件(字符设备、块设备)。(Linux下一切皆文件)
        /etc 目录:存放各种配置文件。
        /lib 目录:存放共享库和可加载模块(即驱动程序)。
        /home 目录:用户目录,是可选的。目录下是以用户名命名的子目录。
        /root 目录:根用户(root)目录,普通用户即为/home下的子目录。
        /usr 目录:存放共享、只读的程序和数据。
        /var 目录:存放可变的数据,与/usr内容相反。
        /proc 目录:空目录,常作为proc文件系统的挂载点,proc文件系统是个虚拟的文件系统。
        /mnt 目录:空目录,用于临时挂载某个文件系统的挂载点,如U盘、光盘等。
        /tmp 目录:空目录,用于存放临时文件。
    3) 构建根文件系统:移植Busybox、构造各个目录/文件等
        Busybox开源工具下载地址:https://busybox.net/downloads/
        下载、解压、开始配置:
            // 表:Busybox配置选项分类(Page362)
        配置完成、编译:make
        安装并拷贝glibc库。
        构建根文件系统:(Page367)
            1> 创建etc/initab文件
            2> 创建etc/init.d/rcS脚本文件
            3> 创建etc/fstab文件(控制mount命令行为)
            4> 构建dev/目录
            5> 构建其他目录,如proc/ mnt/ tmp/ sys/ root/等
    4) 制作yaffs、jffs2文件系统映像文件
        yaffs根文件系统:Page367
        jffs2根文件系统:Page375
        烧写...
    4. linux内核调试技术
    1) 几种调试内核的方法:printk、kgdb
        【printk】调试驱动、内核最简单的方法,使用printk打印信息。与用户空间的printf函数格式完全相同。可在字符串内的头部加入<n>(0~7)表示记录级别。
        printk信息常常用于串口输出,这时串口被称为串口控制台。
        使用u-boot时,设置了命令行参数"console=ttySAC0",它使得printk的信息从串口0中输出。
        系统启动后,查看printk信息,直接运行dmesg命令即可。
        【kgdb】源码级别的linux内核调试器。使用kgdb时,需要结合gdb一起使用,使得调试内核就像调试应用程序一样,可在内核代码中设置断点、一步一步执行、观察变量的值。
        内核需要添加kgdb补丁才可以使用kgdb调试。
        kgdb补丁下载地址:http://kgdb.cvs.sourceforge.net/kgdb/kgdb-2/?pathrev=linux2_6_22_uprev
        使用cvs工具下载:(Page382)
        $:' cd /work/debug
        $:' cvs -z3 -d:pserver:anonymous@kgdb.cvs.sourceforge.net:/cvsroot/kgdb co -p -r linux2_6_22_uprev kgdb-2
        ...
        配置内核,使能kgdb功能。
        集合可视化图形前端ddd和gdb来调试。
    2) 调试工具:gdb、ddd
        【ddd】ddd调用gdb来调试内核,可以在图形界面上完成调试工作。
        安装ddd:$:' sudo apt-get install ddd
        调试方法(Page388)

    【嵌入式linux设备驱动开发】
    1. 字符设备驱动程序
    1) linux系统中驱动程序的地位和作用
        从下到上,一个软件系统能够运行的结构:硬件 → 驱动程序 → 操作系统(内核) → 库 → 应用程序
        linux驱动程序分类:字符设备、块设备、网络接口。
        编写驱动程序注意点:
            1> 驱动程序可能同时被多个程序使用,考虑并发问题;尽可能发挥硬件作用以提高性能。
            2> 硬盘驱动程序中使用DMA也可以不用,使用DMA的程序比较复杂,但是可以提高效率。
            3> 处理硬件的各种异常情况,即使概率很低,否则出错时可能导致整个系统崩溃。
    2) 驱动程序开发一般流程
        1> 查看原理图、数据手册,了解设备的操作方法;
        2> 在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从0开始;
        3> 实现驱动程序的初始化:比如向内核注册这个驱动程序,这样应用程序传入文件名时,内核才能找到相应的驱动程序;
        4> 设计所要实现的操作,比如open、close、read、write等函数;
        5> 实现中断服务(中断并不是每个设备驱动所必须的);
        6> 编译该驱动程序到内核中,或者使用insmod命令加载;
        7> 测试驱动程序。

    3) 简单字符设备驱动程序开发方法
        字符设备驱动程序操作函数集合,在结构体: struct file_operations {...}
        调用哪个驱动程序的操作函数集合中的函数呢?
            1> 设备文件有主/次设备号;
            2> 模块初始化时,将主设备号与file_operations结构一起向内核注册;
        编写字符设备驱动程序的流程:
            1> 编写驱动程序初始化函数;
            2> 构造file_operations结构中要用到的各个成员函数。
        常用的操作函数集合中的函数:open、ioctl、write、read...
        驱动程序编译:将.c文件放入内核的driver/char目录下,在driver/char/Makefile中增加对应模块编译的一个语句(obj-m +=...),然后在内核的根目录下执行命令make modules,就可以生成对应的.ko驱动程序模块文件。
        驱动程序测试(Page409)。
    2. 异常处理体系结构
    1) linux异常处理体系结构
        init/main.c中内核在start_kernel函数中调用trap_init和init_IRQ两个函数来设置异常的处理函数。
        trap_init() 函数(arch/arm/kernel/traps.c)被用来设置各种异常的处理向量,包括中断向量。
        init_IRQ() 函数(arch/arm/kernel/irq.c)被用来初始化中断的处理框架,设置各种中断的默认处理函数。当发生中断时,中断总入口函数asm_do_IRQ就可以调用这些函数做进一步处理。
        常见异常分类5种:未定义指令异常、指令预取中止异常、数据访问中止异常、中断异常、swi异常。
    2) 中断处理体系结构及重要的数据结构
        linux内核将所有的中断统一编号,使用一个结构数组来描述这些中断: struct irq_desc {...}
        其中结构体成员 handle_irq 是这个或这组中断的处理函数入口。
    3) 中断处理函数的注册、处理、卸载流程
        初始化中断处理体系结构函数: init_IRQ (void);
        用户向内核注册中断处理函数的函数: request_irq (...);
        中断的C语言总入口函数: asm_do_IRQ (...);
        用户从内核注销中断处理函数的函数: free_irq (...);
    4) 驱动程序中使用中断的方法
        按键驱动使用中断方式实现的实例。
    3. 扩展串口驱动程序移植
    1) 串口终端设备驱动程序的层次结构
        串口驱动程序从上到下分为4层:终端设备层、行规程、串口抽象层、串口芯片层、(硬件)。
    2) 移植标准串口驱动程序的方法(Page438)
    --------------------- 

     

    转载
    原文:https://blog.csdn.net/sinat_36184075/article/details/71305568 

    展开全文
  • Linux内核裁减 (1)安装新内核:  i)将新内核copy到/usr/src下并解压: #tar -zxvf linux-2.6.38.4.tar.gz ii) 将名为linux的符号链接删掉,这是旧版本内核的符号链接.  #ln -s linux-2.6.38.4 linux  (2)设置...
  • 网站/论坛:www.100ask.org ...微信公众号:baiwenkeji 公司 微博:百问科技 个人 微博:韦东山 版本 日期 作者 说明 V1 2016.07.29 ... 第1版本,Android部分未写 表格完毕我是1999年上的大学,物理专业
  • 1.嵌入式音频系统硬件连接 下图所示的嵌入式设备使用IIS将音频数据发送给编解码器。对编解码器的I/O寄存器的编程通过IIC总线进行。 2.音频体系结构-ALSA ALSA是Advanced Linux Sound Architecture 的缩写,目前...
  • 嵌入式Linux系统中的快速启动技术研究 1、嵌入式Linux系统启动时序 目前,嵌入式系统的硬件平台和应用方向区别很大,但总体启动流程一致的。这里的系统启动是指从用户执行上电/复位操作,到系统开始提供用户可接收...
  • 2. 怎么学习嵌入式Linux操作系统 本文假设您是零基础,以实用为主,用最快的时间让你入门;后面也会附上想深入学习时可以参考的资料。 在实际工作中,我们从事的是“操作系统”周边的开发,并不会太深入学习、修改...
  • 嵌入式linux中文站 旨在为广大嵌入式linux中文爱好者提供一个学习、讨论、研究嵌入式linux的优良平台,所有提供的服务永久免费,欢迎广大嵌入式linux爱好者经常光临本站 嵌入式linux中文站地址: ...
  • 飞星嵌入式LINUX视频课程表 技术交流QQ群 : 215875560 业务QQ : 32708168 购买地址 : http://linuxgtk.taobao.com 飞星嵌入式官网 : http://www.linuxgtk.com 基础知识...
  • 之前讲过i2c框架:通俗易懂式分析了解i2c框架 如果之前你看懂了,那其实spi框架也差不多。 同样的,先上张图: 老规则,从上往下看起,以kernel4.8.17为例: 在mach-smdk2440.c文件里:static struct platform_...
  • 文档名称:嵌入式linux usb wifi驱动移植 版本历史 版本号 时间 内容 v1.0b001 2012-6-18 初始版本,介绍在嵌入式linux方面如何移植usb wifi相关    嵌入式l
  • 一、系统时钟和定时器 ... B、HCLK: 用于AHB总线上设备,比如CPU核、存储控制器、中断控制器、LCD控 制器、DMA和USB主机模块等  C、PCLK: 用于APB总线上的设备,比如WATCHDOG、IIS、IIC、PWM控制器、MMC 接口...
  • 嵌入式Linux+Android学习路线图
  • 很不错的一个视频。    http://www.100ask.net/index.html  第一期免费的。 http://dl.dbank.com/c03o1ebwlo  第二期280块一套。http://dl.dbank.com/c06bbt0sxp  第三期180块一套。...
  • 本文目录 1. 程序员的三大方向 1.1 专业领域 ...1.4 嵌入式Linux+Android系统包含哪些内容 2. 怎么学习嵌入式Linux操作系统 2.1 入门路线图 2.2 学习驱动程序之前的基础知识 2.2.1 C语
1 2 3 4 5 ... 20
收藏数 1,885
精华内容 754