tft彩屏spi驱动编程——基于stc32g12k128的spi_dma功能的例程分享: spi接口的接线,分四线制和三线制,据说spi协议没有确定的文字规定,只有实事的实现方案,而行业内的现状则是众说纷纭,我大概把它们分为两类,一类是按实际用了几条线实现传输,一类是按信号传递格式来划分。具体到屏显驱动来讲,凡是接口有独立的数据/命令(rs.dc.a0等叫法都有)接线的,属于我们适用的范围,具体用了几条线,我数学不好,就不去数了。这不是说没有数据/命令选择线的不能用,反而是那种可能更有用这种驱动模式的需要,但我手里没有这样的屏,对不熟悉,没验证的东西,就不去评论了。 spi接口的tft彩屏在mcu工程里是常见的。其优点是接线方便,占用io 口少,色彩丰富。缺点是速度会慢些,一方面是因为彩屏显示需要的数据量大,单色屏一个字节可以包涵8个点的显示信息,而彩屏一个点就需要二个字节的信息。另一方面是它属于串口传输,并口传输一次可以送出8到16位信息,而串口一次只能送出一位信息,信息量大加传输慢,就成了spi接口彩屏的一个短板。特别是在屏的显示点数多时,尤其明显。 记得dma技术刚问世时是用在pc机上,当时经销商把它当卖点,用户则花钱买功能。只要花了钱,就能用上这个功能了。 但在mcu领域有些不同,dma技术不仅需要芯片功能上支持,还需要技术人员愿意去用。一些学成的技术人员因为工作忙,面对的问题又能用传统手段解决,使用dma的动力就不足了。而新人由于感觉这东西有些难弄,也不愿用。 本文说的tft彩屏spi驱动的编程,是基于dma功能的spi彩屏驱动程序,因为spi接口慢,所以使用dma的意义更大。由于stc32g12k128芯片具有dam功能,所以就以这个芯片为依托,写了一组spi接口的驱动程序,这个模式的程序,能加快刷屏速率,很大地提高芯片的工作效率。 下面先看一下spi接口驱动中最基本的函数,数据传送函数。分别是软件模拟spi传送,硬件spi数据传送和基于spi_dma的数据传送函数 第一个:软件模拟spi数据传送函数 void transfer_data(unsigned int data1)//需要传送命令时加dc=0 { char i; cs=0; for(i=0;i<8;i ) { scl=0;//sclk=0; //delay(4); if((data1&0x80)==0) sda=0;//sid=1; else sda=1;//sid=0; scl=1;//sclk=1; data1=data1<<=1; // delay(4); } // cs=1; } 第二个:硬件spi数据传送函数 void transfer_data(unsigned int data1)//需要传送命令时加dc=0 { cs=0; spstat=0xc0; spdat=data1; while(!spif);//等待发送完成 } 第三个:基于spi_dma的数据传送函数 void transfer_data(unsigned int dat1) { cs=0; dmaspi_initial();//这一句不是必须的,为了保险,我喜欢加上这一句 *(unsigned char *)(0x10000)=dat1;//待传送数据送专用缓存区,就是buffer1 dma_spi_cr=0xc1;//开始执行,采用主机模式,并清空fifo, while(!(dma_spi_sta&0x01)); } 显然,软件模拟spi传送函数比较麻烦,速度也慢很多,但它的优点是对mcu硬件依赖少,基本可以在任何io口实现,所以屏的卖家愿意用它写的测试驱动。 硬件spi的数据传送函数就很给力了。与spi_dma的数据传送比,写入数据后自动开始传送,而后者写入数据与启动传送是两个语句,并且硬件spi也是外设接口,就是说它运行时有相对的独立性,对mcu的依赖较少,所以很棒,而基于spi_dma的数据传送函数,语句多了,而且还要有专门缓冲区配合,在单个数据传送上,不如硬件spi的传送函数便捷。 需要说明,基于dma的spi接口彩屏驱动,必须使用(开启)硬件spi功能。这是前提条件。 在具体的应用中,传送函数用硬件spi的和用spi_dam的都可以,但既然硬件spi的传送函数很好用了,spi_dma的传送函数写出来更多是象征性的了。表示可行,但不表示必须用。spi_dma的长项在批量数据传送上。 这里说了半天一是介绍一下情况,二是表明两个函数的兼容性: 基于spi_dma数据传送函数的程序,使用硬件spi传送函数都可以顺畅运行 反过来 基于硬件spi数据传送函数的程序,在spi_dma传送函数下不一定能运行 原因是缓冲区可能出问题。也可以说话程序写的不够好。我尽量采用spi_dma做依托写函数,运行时使用硬件spi支持。 下面是图像显示函数,这是spi_dma技术的强项。 void spi_dma_disp(unsigned int x,unsigned int y,unsigned int x_width,unsigned int y_height,unsigned int total_num,unsigned int once_num,unsigned char *p,unsigned char *q) { unsigned int ii,jj; unsigned long kee; unsigned char *kkee; kkee=q;//保存入口地址,方便指针复位时使用 lcd_address(x,y,x_width-1,y_height-1);//通知屏幕需要刷图的位置,这个指令要尽量往前放,因为其中用到数据传送,会影响传送参数的设置。 kee=(unsigned long)(q);//取缓冲区首地址的值,也就是打算发送给屏幕的数据源地址的值(缓冲区首地址是1:0000h。所以习惯用long类型变量) dma_spi_txah=(kee)>>8;//把源地值高位赋给地址寄存器 dma_spi_txal=kee;//在xdata的起始地址01:0002h dma_spi_amth=(once_num-1)>>8; dma_spi_amt=(once_num-1); for(ii=0;ii<(total_num/once_num);ii )//total_num/once_num必须是个整数,否则要做处理 { for(jj=0;jj把要送屏的数据写入指定的缓冲数组 { *q=*p;//传送图像数据,p指向code区的图像数组,q指向缓冲区buffer2 p ; q ; } q=kkee;//传送一轮后,缓冲区指针复位。 //把缓冲区的数据送到屏上显示 dma_spi_sta=0;//清中断标志位及错误标志位 dma_spi_cr=0xc1;//开始执行,采用主机模式,并清空fifo, while(!(dma_spi_sta&0x01));//等待吧。这时可以做其它事,比如去读一个ad值, } dmaspi_initial();//这是多余的指令,原想加了它可以省去数据传送函数中的初始化 } 程序的思路是设置了两个指针,一个指向存放图像信息数组的头文件*p;一个指向缓冲区存放临时数据*q。后者也是向屏输出图像信息的源地址。缓冲区必须设在xdata区。由于空间限制,要分几次才能传输完总数据。所以设置了总数据量和每次数据量这两个参数。运行时先把数据送到缓冲区,再启动spi_dma功能,把数据送屏显示。完成一组再进行第二组。直到全部传送完。 程序的优点是启动spi_dma后,mcu基本处于空闲状态,可以去做其它事了,传送由外设自己管理,你只要不去干涉它用到的资源就好(相关总线,特别是缓冲区)也就是说在它完成任务前,不要下达与屏显有关的指令。下一个spi_dma指令,也要在前面的确定执行完后才能下达。 这个程序特点是每传送完一轮,缓冲区指针q都要复位一次,而p不需要,因为缓冲区是重复使用的。 这个程序能给mcu节省多少时间?我没测量过,只是用delay();函数看了一下,要用多大的参数值,能保证spi_dma完成任务。就是使用延时函数替代程序中的等待查询指令。看看需要多少延时能替代那个等待。 void delay(unsigned int ms) { int j,k; for(j=0;j for(k=0;k<60;k ); } 结果在delay(11111)时,图像完全不显示(说明传送过程被完全打乱了),在delay(33333)时显示基本正常了。我认为,这就是spi_dma节省出来的时间,而且是一轮传送节省的时间。120x120的图像用了五轮传送。 接下来是字模显示函数。 先贴一个使用普通字模数组的字模显示函数: void word16x32_bydma_spi(unsigned int x,unsigned int y,unsigned char segin,int font_color,int back_color,char *q) { unsigned char column=0; unsigned char tm=0,temp; unsigned long kee; char *point; char *kkee; kkee=q;//这是保留缓冲区指针初值 point=digit_code[segin];//把要显示的数字(segin)转换成对应的字模地址point //先把字模数据转换成屏显所用数据存到缓冲区,因为写入缓冲区是并行处理,且没有其它操作。所以比写屏快很多 for(column=0;column<64;column )//字节数循环,逐个字节进行转换 { temp=*point;//*point是原字模数据指针,取出原字模数值 //把字节信息换算成屏需要的数据,送到缓冲区 for(tm=0;tm<8;tm ) { if(temp&0x01)//注意这是低位先出模式,如果与字模不符,可以考虑改为高位先出模式 { *q=(font_color>>8);//q是缓冲区指针 q ; *q=(font_color); q ; } else { *q=(back_color>>8); q ; *q=(back_color); q ; } temp>>=1; } point ; } //开始刷屏,因为由dma_spi操作,基本不需要占用mcu时间。 lcd_address(x,y,x 15,y 31); kee=(unsigned int)(kkee);//取缓冲区首地址的值,也就是打算发送给屏幕的数据源地址的值 dma_spi_txah=(kee)>>8;//把源地值高位赋给地址寄存器 dma_spi_txal=kee;//地址是xdata的起始地址 dma_spi_amth=3;//0x03,表示的传送总数据量是3ffh 1,也就是1024 dma_spi_amt=255;//0xff q=kkee;//缓冲区指针复位 dma_spi_sta=0;//清中断标志位及错误标志位 dma_spi_cr=0xc1;//开始执行,采用主机模式,并清空fifo,这句明显要跟着程序走的 while(!(dma_spi_sta&0x01));//等待吧。这时可以做其它事 dmaspi_initial();// } 字模显示函数的编写思路是,字模数组放在code区的头文件数组中。使用时查到需要字模,读取后转换成屏显需要的数据,再存到缓冲区。然后开启spi_dma刷到屏幕上。读取和转换程序与其它人写的程序一样,照抄的。区别就是转换完后不是直接送屏显示,而是送到了缓冲区,再由spi_dma统一送屏。spi_dma刷屏则与前面的图像显示一样。这个函数的优点是不需要专门制作字模,过去的字模软件可以直接用(脸红的说一句,我没用过硬件字库)。 既然用了spi_dma,就会考虑能不能最大限度的发挥其作用,于是写了一个spi_dma专用的字模显示程序。由于没有准备字模,又写了一个专用字模数据的生成函数。生成专用字模后,放在缓冲区,就可以由专用显示函数使用了。 void matrix_produce8x16(unsigned int font_color,unsigned int back_color) { char *q; unsigned char *point; unsigned char column; unsigned char temp,tm; q=&buffer3[0][0];//缓冲指针指向数字字模专用缓冲区buffer3 point=&number0_9_8x16[0][0];//8x16数字字模数组 for(column=0;column<160;column )//字节数循环,逐个字节进行转换 { temp=*point;//*point是原字模数据指针,取出原字模数值 //把字节信息换算成屏需要的数据,送到缓冲区 for(tm=0;tm<8;tm ) { if(temp&0x01)//注意这是低位先出模式,如果与字模不符,可以考虑改为高位先出模式 { *q=(back_color>>8);//q是缓冲区指针 q ; *q=(back_color); q ; } else { *q=(font_color>>8); q ; *q=(font_color); q ; } temp>>=1; } point ; } dmaspi_initial();// } 专用字模显示函数其实就是把普通字模全部转换成显示所用的字模数据,放在缓冲区专用位置,供需要时读取使用,由于缓冲区空间有限,只能供占空间不大,需要频繁调用的字模采用。例程中是把0-9这十个8x16的数字转换进去了。屏幕上很小的那个字就是用它显示的。 下面是专用字模调用程序 void word8x16_bydma_spi(unsigned int x,unsigned int y,unsigned char segin) { unsigned long kee; lcd_address(x,y,x 7,y 15); kee=(unsigned long)(&buffer3[segin][0]);//取缓冲区首地址的值,也就是打算发送给屏幕的数据源地址的值 dma_spi_txah=(kee)>>8;//把源地值高位赋给地址寄存器 dma_spi_txal=kee;//地址是xdata中对应数字的起始地址 dma_spi_amth=0;//0x00,表示的传送总数据量是ffh 1,也就是256 dma_spi_amt=255;//0xff dma_spi_sta=0;//清中断标志位及错误标志位 dma_spi_cr=0xc1;//开始执行,采用主机模式,并清空fifo, while(!(dma_spi_sta&0x01));//等待吧。这时可以做其它事, dmaspi_initial();//冗余指令 } 这程序不仅形式上简洁多了,速度也快,特别是极少使用mcur 。 有了图像和字符显示,缺少的就是曲线了。橫平竖直的线太容易,不想写了。曲线由于个人水平原因,没找到能提高效率的思路,只好作罢。 使用spi_dma技术,最大的不同在于要规划和使用缓冲区。就是xdata区的那些空间, 我为了做验证,在例程中设了三个数组,做为缓冲区;buffer1两个字节,作用是占位,在整个程序中没有出现调用它的指令,占位的意思是不让其它函数使用这个空间,留给数据传输函数专用,(其实这样做是没必要的,只是验证实验中期望把数据传输函数效率做到最高,才给它留了一个最佳缓冲地址)c251内存空间是自动安排的,不占位的话其它程序可能会来使用,使整个程序乱序。数据传送函数使用buffer1时的语句是: *(unsigned char *)(0x10000)=dat1;//待传送数据送专用缓存区,就是buffer1 这样做是因为buffer1要占住位置,只能设置在mai.c程序的模块里,而我采用模块化编程,数据传送函数是在驱动模块中的。这时不方便用数组名buffer1访问。 第二个数组buffer2用了5600字节用来临时存放传送的图像数据,其实实际使用时临时数据是5760字节,超过了安排的缓冲区,也就是侵占了后面的空间,例程中用做开机图像传送时,后面的缓冲区还没使用,所以侵占一下也没什么。但如果后面的用上了。侵占就导致后面的数据错误。这也是使用指针的优点。 第三个数组是buffer3,安排了2560字节,用来放10个8x16数字的显示数据,每个占256字节。使显示速度达到最快。实际应用中如果有需要频繁刷屏的字符,可以采用这种模式处理。 buffer3定义的是一个二维数组,写入数据时只当一维数组,按顺序写,但读出时按二维读。编程方便许多。 例程中常规字模显示函数用了buffer3做缓冲区,这会影响专用字模显示功能的使用,实际上常规字模显示应该使用buffer2,但技术原因有不方便之处,做为实验使用,就保留了这个失误,这样常规显示和专用字模显示只能保留一个了(因为它们的缓冲区设重复了) 在例程中有开启高速高级spi的语句(在mcu设置函数mcu_initial.c里) clksel &= ~0x80; //默认选择 pll 的 96m 作为 pll 的输出时钟 usbclk |= 0x20; //pll 输入时钟 2 分频 ,因为stc-isp设定频率为24m usbclk |= 0x80; //使能 pll 倍频 delay(222);//等待pll锁频 clksel &= ~0x40; //默认 hspwm/hsspi 选择主时钟为时钟源 hsclkdiv = 0; //hspwm/hsspi 时钟源不分频 可以屏蔽掉,看一下对比效果。 例程验证时使用的是stc32g12k128dip40芯片,焊了一个小洞洞板。 ftf彩屏使用的是128x128分辨率。0.85吋,我手里大点的屏都是并口的。 调试中使用了stc-usb link1d硬件仿真器,个头不大,但很给力。让我这个第一次玩dma的人能清楚地看到问题出在哪里,程序卡在哪里。对症处理,节约了n多时间。 程序主要内容和编程思路已经说完了。完整的程序放在附件中可以下载查看。欢迎指导,欢迎吐槽。 分享这个程序的目的是希望使用mcu芯片的朋友们能更好的使用dma功能,更多地发挥出它的应有效能。也为初学的朋友们提供一个实例借鉴。mcu技术的发展越来越复杂,例程对学习者的作用是不能低估的。
|