.zyz 发表于 2014-2-4 18:41:56

Ndless SDK 系列教程——LCD操作

(返回目录)
Ndless SDK 系列教程——LCD操作
本节内容:
自己操作nspire的显示屏!

在以前的帖子中,我们已经看到了如何使用nio(nspireio2.h)库进行字符的输出,不过其能力有限,也不能输出中文。要想拥有更高的自由度,在屏幕上显示更加复杂的图形,我们就要自己实现一些LCD函数。本节大多数例子都是基于黑白机的,大家稍作变通就可以用上彩屏机。

Ndless SDK 为我们提供了一个宏:SCREEN_BASE_ADDRESS。这就是nspire当前的显存首地址。通过指针操作,我们就可以对nspire的显存进行修改,比如如下代码(来自NdlessSDK _Samples文件夹,有改动,可在附件中找到):#include <os.h>
int main(void) {
      if (!has_colors) return 0;
      lcd_incolor();
      volatile unsigned char *scr_base = SCREEN_BASE_ADDRESS;
      volatile unsigned char *ptr;
      unsigned scr_size = SCREEN_BYTES_SIZE;                        // SCREEN_BYTES_SIZE为显存大小
      for (ptr = scr_base; ptr < scr_base + scr_size / 3; ptr += 2)
                *(volatile unsigned short*)ptr = 0b1111100000000000;
      for (; ptr < scr_base + scr_size * 2 / 3; ptr += 2)
                *(volatile unsigned short*)ptr = 0b0000011111100000;
      for (; ptr < scr_base + scr_size; ptr += 2)
                *(volatile unsigned short*)ptr = 0b0000000000011111;
      wait_key_pressed();
      return 0;
}
CX/CM运行编译后的程序,效果如图:
我们来好好理解一下这段代码:首先是解释一下16位彩屏的颜色问题。16位彩色一共可以显示2^16(65536)种颜色,因此可以用unsigned short来表示一个像素的颜色。关于颜色的组成,可以这样看:0bRRRRRGGGGGGBBBBB。即用二进制表示时,前5位为红色的值,中间6位为绿色(人眼对绿色更敏感),剩下的5位则是蓝色。实际上我们都更加习惯于使用使用0~255来表示一个像素里的一个颜色,所以我们可以这样写一个宏:#define RGB16(r,g,b)   (((unsigned short)(r>>3))<<11 | ((unsigned short)(g>>2))<<5 | ((unsigned short)(b>>3)))这样就可以使用RGB16(255,0,0)来表示红色。
每次都用SCREEN_BASE_ADDRESS会觉得很麻烦,于是我们可以简单的封装一个函数:void setpixel( int x , int y , unsigned short color )
{
      volatile unsigned char * ptr = SCREEN_BASE_ADDRESS;
      ptr += y*320*sizeof(short) + x*sizeof(short);
      *(volatile unsigned short*)ptr = color;
}
现在我们就可以利用这个setpixel来操作显示屏了!比如画一条水平蓝线。#include <os.h>
#define RGB16(r,g,b)   (((unsigned short)(r>>3))<<11 | ((unsigned short)(g>>2))<<5 | ((unsigned short)(b>>3)))

int main(void)
{
      if (!has_colors) return 0;//防止黑白机运行此程序
      lcd_incolor();
      int i;
      for( i=0 ; i<320 ; i++ )
      {
                setpixel( i , 100 , RGB16(0,0,255) );
      }
      wait_key_pressed();
      return 0;
}
对于黑白机,可以用unsigned char来表示两个像素,这里我们给出描点函数,原理可以自己理解。void setpixel(int x, int y, unsigned char color )
{
      if( x < 0 || x >= 320 || y < 0 || y >= 240 )
                return ;
      volatile unsigned char * p = SCREEN_BASE_ADDRESS + (x >> 1) + ( y << 7 ) + (y << 5 );
      *p = ( x & 1) ? ((*p & 0xf0 ) | color ) : (( *p & 0x0f ) | ( color << 4 ));
}
在使用这些函数时,务必要注意小心访问越界,否则会有不可意料的后果(通常情况是重启)。为了规避风险,可以在描点之前加上检查(如上一段代码)。
下面提供几个用于黑白机的显示函数,部分改变后就可以用上彩屏机。void   allclr ()
{      memset(SCREEN_BASE_ADDRESS,0xff,SCREEN_BYTES_SIZE);}

void   allfill (unsigned char color)
{
      //绝对不适用于彩屏机,彩屏机必须逐点描颜色!!
      unsigned char c = (color<<4)&(color);
      memset(SCREEN_BASE_ADDRESS,c,SCREEN_BYTES_SIZE);
}
下面再来个画线函数,里面有注释,就自己看吧:void   line (int x1 , int y1 , int x2 , int y2 , unsigned char color)
{
      float i ;
      if( x1 == x2 )                              //此时,斜率不存在
      {
                if( y1 > y2 ) { i = y1 ; y1 = y2 ; y2 = i ;}
                for( ; y1 <= y2 ; y1 += 1)
                        setpixel (x1 , y1 , color );
                return ;
      }
      float k , b ;
      k = ((float)y2 - (float)y1)/((float)x2 - (float)x1);      //计算斜率
      b = (float)y1 - k * (float)x1 ;                        //计算截距
      if( _abs(k) <= 1 )                        //当斜率小于1时,直线比较平缓,以x方向逐个画点
      {
                if( x1 > x2 )                        //交换x,y的值,如果需要的话
                {
                        i = y1 ; y1 = y2 ; y2 = i ;
                        i = x1 ; x1 = x2 ; x2 = i ;
                }
                for( ; x1 <= x2 ; x1 +=1 )
                        setpixel ( x1 , k * x1 + b , color);
                return ;
      }
      else                                                //当斜率大于1时,直线比较陡峭,以y方向逐个画点
      {
                if( y1 > y2 )                        //交换x,y的值,如果需要的话
                {
                        i = y1 ; y1 = y2 ; y2 = i ;
                        i = x1 ; x1 = x2 ; x2 = i ;
                }
                for( ; y1 <= y2 ; y1 +=1 )
                        setpixel (( y1 - b ) / k , y1 , color);
                return ;
      }
}
再有,画一个实心圆也是很简单的,可以用点到圆心的距离小于半径得到。void fillcircle(int x0,int y0,int r0,unsigned char color)
{
      int x, y;
      int xMax, yMax, yInit;
      
      x = x0 -r0;                         if( x < 0 )                x = 0;
      yInit = y0 - r0;                if( yInit < 0 ) yInit = 0;
      xMax = x0 + r0;                        if( xMax > 320) xMax = 320;
      yMax = y0 + r0;                if( yMax > 240 ) yMax = 240;
      for( ; x <= xMax; ++x )
      {
                for( y = yInit; y <= yMax; ++y )
                {
                        if( abs( (x-x0)*(x-x0) + (y-y0)*(y-y0 ) ) <r0*r0 )
                              setpixel(x, y, color);
                }
      }
}(CPASS)
好了,有了以上这些函数,我们可以来点有意思的!
还记得Wudy在9860GII的C解释器(WSC&FVM)里做的演示程序吗?http://www.cncalc.org/forum.php?mod=viewthread&tid=7572我们来在nspire上做一下那个BALL吧。(来源于WSC&FVM V1.2Sample,有改动)#include "os.h"
#define BALL_RADIUS10
#define X_MAX       320
#define Y_MAX       240
#define LOST      -0.95

float x, y, vx, vy, ax, ay;
void update();
void fillcircle(int x0,int y0,int r0,unsigned char color);
void allclr ();
void setpixel(int x, int y, unsigned char color );

int main()
{
lcd_ingray();
x= 10; y= 10;
vx = 2;vy = 0;
ax = 0;ay = 0.2;
while( !isKeyPressed(KEY_NSPIRE_ESC) )
{
    allclr();
    fillcircle(x, y, BALL_RADIUS ,0 );
      sleep(30);
    if( isKeyPressed(KEY_NSPIRE_UP ) )
      vy -= 0.5;
    if( isKeyPressed(KEY_NSPIRE_DOWN ) )
      vy += 0.5;
    if( isKeyPressed(KEY_NSPIRE_LEFT ) )
      vx -= 0.5;
    if( isKeyPressed(KEY_NSPIRE_RIGHT ) )   
      vx += 0.5;
    update();
}
return 0;
}

void update()
{
vx += ax; vy += ay;
x += vx*0.75; y += vy*0.75;

if( x > X_MAX - BALL_RADIUS + 1 )
{
    x = X_MAX - BALL_RADIUS; vx *= LOST;
}
else if( x < BALL_RADIUS - 1 )
{
    x = BALL_RADIUS; vx *= LOST;
}

if( y > Y_MAX - BALL_RADIUS + 1 )
{
    y = Y_MAX - BALL_RADIUS; vy *= LOST;
}
else if( y < BALL_RADIUS - 1 )
{
    y = BALL_RADIUS; vy *= LOST;
}
}
void fillcircle(int x0,int y0,int r0,unsigned char color)
{
      int x, y;
      int xMax, yMax, yInit;
      
      x = x0 -r0;                         if( x < 0 )                x = 0;
      yInit = y0 - r0;                if( yInit < 0 ) yInit = 0;
      xMax = x0 + r0;                        if( xMax > 320) xMax = 320;
      yMax = y0 + r0;                if( yMax > 240 ) yMax = 240;
      for( ; x <= xMax; ++x )
      {
                for( y = yInit; y <= yMax; ++y )
                {
                        if( abs( (x-x0)*(x-x0) + (y-y0)*(y-y0 ) ) <r0*r0 )
                              setpixel(x, y, color);
                }
      }
}

void   allclr ()
{      memset(SCREEN_BASE_ADDRESS,0xff,SCREEN_BYTES_SIZE);}

void setpixel(int x, int y, unsigned char color )
{
      if( x < 0 || x >= 320 || y < 0 || y >= 240 )
                return ;
      volatile unsigned char * p = SCREEN_BASE_ADDRESS + (x >> 1) + ( y << 7 ) + (y << 5 );
      *p = ( x & 1) ? ((*p & 0xf0 ) | color ) : (( *p & 0x0f ) | ( color << 4 ));
}
经模拟器检验运行良好(球有闪烁现象),黑白机实际运行时存在严重BUG(显示位置偏移),这个问题到最后在解决。大家也可以拿WSC&FVM里的SPRING.c做实验。

下面我们来研究下字符(串)的描绘。在ndless程序的源代码中,几乎都存在着char.h或者类似的文件,里面有一个超大的二位数组,那些其实就是字符的图片。(char.h见本帖附件)这些数组(图片)的结构类似于9860的图片数据,想做更多了解的可以找diameter的老帖:“9860 SDK 教程”

为了解码这些图片,我们编写以下函数:void   writegraph(int x, int y , int width,int height,char * pimage,char cl_fg,char cl_bg)
{
      int i,j,k,pixel,rx=0,ry=0;
      unsigned char p;
      int iwidth = width/8 + (width % 8 ? 1:0);
      for (i=0;i<height;++i,pimage+=iwidth)
      {
                ry = y+i;
                if (ry>=240) return;
                else if (ry<0) continue;
                for (j=0;j<iwidth;++j)
                {
                        p = pimage;
                        for (k=0;k<8;++k)
                        {
                              rx = x+(8-k)+8*j;
                              pixel = p % 2;
                              p>>=1;
                              if (pixel)                        setpixel (rx-1,ry,cl_fg);
                              else                                 setpixel (rx-1,ry,cl_bg);
                        }
                }
      }
}
void   DrawAsciiChar (int x, int y, char c , int cl_fg, int cl_bg)
{
      writegraph ( x , y ,8 , 12 , (char *)charMap_ascii [(unsigned char)c] , cl_fg , cl_bg );
}
搞定,现在就可以在屏幕的任意位置输出一个字符了。要输出字符串,连续输出字符即可。void   DrawAsciiString (int x, int y, char *string,int cl_fg,int cl_bg)
{
      while(1)
      {
                DrawAsciiChar (x , y , *string , cl_fg , cl_bg );
                string ++; x += 8 ;
                if(*string == '\0' || x > 320 )return;
      }
}
不同char.h的字符输出方式可能不一样,比如NIO库的字符输出方式就比较非常态。大家可以自己找NIO库的源代码来研究下。
英文字符输出的问题解决了,下面就是汉字的描绘了。汉字很多,所以不能像ascii字符一样用数组就解决,所以我们外包一个文件,名为HZK16.tns。先写两个函数,用来打开汉字库和关闭汉字库。FILE * open_hzk ()
{
      return fopen("/documents/HZK16.tns","rb");
}(CPASS)

void close_hzk (FILE* file_hzk)
{
      fclose(file_hzk);
}
然后就是输出中文字符串。void PrintChStr(FILE *HZK , int x,int y,char *str,unsigned char cl_fg,unsigned char cl_bg)
{
      unsigned char c1,c2,mat;
      while(*str)
      {
                if(x>=304)
                {
                        x=0;y+=16;
                }
                c1=*str++;
                if( !(c1&0x80) )
                {
                        DrawAsciiChar (x,y+2,c1,cl_fg,cl_bg);
                        x+=8;
                        continue;
                }
                c2=*str++;
                fseek(HZK,(94*(c1-0xa1)+(c2-0xa1))*32,SEEK_SET);
                fread(mat,32,1, HZK);
                writegraph (x,y,16,16,(char *)mat, cl_fg,cl_bg);
                x+=16;
      }
}
我们来试试看int main(void)
{
      lcd_ingray();
      FILE *hzk = open_hzk()
      if( hzk==NULL )
                return -1;
      PrintChStr(hzk,0,0,"世界,你好!",0x0,0xf);
      close_hzk(hzk);
      wait_key_pressed();
      return 0;
}
中文描绘顺利解决,效果如图:
(貌似少了清空屏幕……)
值得一提的是,这种方法读取汉字库必须保证字符串是ANSI编码,而且据我所知部分日语和繁体中文(ANSI,如“電”)都会死,所以我写的nNovel暂不支持这些。要想使得输出的汉字更好看,你可以找nNovel的源代码,里面有灰度字库的输出方式。在教程文件整合包里的char.h,提供了3中不同的ascii字符:点阵大字符,点阵小字符,灰度大字符。

前面所提供的所有函数都是基于直接对显存的的操作,因此你会看到绘图的过程(虽然很短暂)。而实际上在复杂的绘图中,我们更加不希望别人看到这一过程。所以,我们可以在空闲的内存中分配一块,在这块内存里绘图,绘图完毕后用memcpy函数送到显存里去。这样还可以避免出现黑白机运行BALL时的显示位置偏移的BUG。下面我们试试这个方法来处理ball.c。#include "os.h"
#define BALL_RADIUS10
#define X_MAX       320
#define Y_MAX       240
#define LOST      -0.95

float x, y, vx, vy, ax, ay;
void update();
void fillcircle(char*VRAM,int x0,int y0,int r0,unsigned char color);
void allclr (char*VRAM);
void setpixel(char*VRAM,int x, int y, unsigned char color );
char *init_VRAM()
{
      return (char*)malloc(SCREEN_BYTES_SIZE);
}
void Close_VRAM(char *VRAM)
{
      free(VRAM);
}
void   PutDisp(char * VRAM)
{      memcpy(SCREEN_BASE_ADDRESS,VRAM,SCREEN_BYTES_SIZE);}

int main()
{
lcd_ingray();
char *VRAM=init_VRAM();
x= 10; y= 10;
vx = 2;vy = 0;
ax = 0;ay = 0.2;
while( !isKeyPressed(KEY_NSPIRE_ESC) )
{
    allclr(VRAM);
      fillcircle(VRAM ,x, y, BALL_RADIUS ,0 );
      PutDisp(VRAM);
      sleep(20);
    if( isKeyPressed(KEY_NSPIRE_UP ) )
      vy -= 0.5;
    if( isKeyPressed(KEY_NSPIRE_DOWN ) )
      vy += 0.5;
    if( isKeyPressed(KEY_NSPIRE_LEFT ) )
      vx -= 0.5;
    if( isKeyPressed(KEY_NSPIRE_RIGHT ) )   
      vx += 0.5;
    update();
}
Close_VRAM(VRAM);
return 0;
}

void update()
{
vx += ax; vy += ay;
x += vx*0.75; y += vy*0.75;

if( x > X_MAX - BALL_RADIUS + 1 )
{
    x = X_MAX - BALL_RADIUS; vx *= LOST;
}
else if( x < BALL_RADIUS - 1 )
{
    x = BALL_RADIUS; vx *= LOST;
}

if( y > Y_MAX - BALL_RADIUS + 1 )
{
    y = Y_MAX - BALL_RADIUS; vy *= LOST;
}
else if( y < BALL_RADIUS - 1 )
{
    y = BALL_RADIUS; vy *= LOST;
}
}
void fillcircle(char*VRAM ,int x0,int y0,int r0,unsigned char color)
{
      int x, y;
      int xMax, yMax, yInit;
      
      x = x0 -r0;                         if( x < 0 )                x = 0;
      yInit = y0 - r0;                if( yInit < 0 ) yInit = 0;
      xMax = x0 + r0;                        if( xMax > 320) xMax = 320;
      yMax = y0 + r0;                if( yMax > 240 ) yMax = 240;
      for( ; x <= xMax; ++x )
      {
                for( y = yInit; y <= yMax; ++y )
                {
                        if( abs( (x-x0)*(x-x0) + (y-y0)*(y-y0 ) ) <r0*r0 )
                              setpixel(VRAM,x, y, color);
                }
      }
}

void   allclr (char*VRAM)
{      memset(VRAM,0xff,SCREEN_BYTES_SIZE);}

void setpixel(char*VRAM ,int x, int y, unsigned char color )
{
      if( x < 0 || x >= 320 || y < 0 || y >= 240 )
                return ;
      volatile char * p = VRAM + (x >> 1) + ( y << 7 ) + (y << 5 );
      *p = ( x & 1) ? ((*p & 0xf0 ) | color ) : (( *p & 0x0f ) | ( color << 4 ));
}
好了,在教程文件整合包中的mylib文件夹内,免费送上了一个我自己用的,黑白机的LCD库,都是基于上述的“虚拟显存”方案。所有文件没有版权问题,大家拿了随便用~还有,整合包里面的文件不要直接编译,否则会出现多个main函数的错误。

写的略仓促,稍微有点乱,麻烦大家了……

如果当中出现错误,也希望大家能指出。有任何意见和建议,可以在下面回帖。



rphero 发表于 2014-2-12 12:31:14

支持LZ!!

.zyz 发表于 2014-3-3 22:59:58

本帖最后由 Zentauit 于 2014-3-3 23:01 编辑

来个SPRING.c和Wudy的解释器效果差不多,不过用了触摸板效果比较好。
暂时不兼容ClickPad等无触摸板机型。#include <os.h>
#include "graph.h"
#include "touchpad.h"

#define X_MAX 320
#define Y_MAX 240

#define BALL_RADIUS 10
#define LOST -0.95

void fillcircle(char*VRAM ,int x0,int y0,int r0,unsigned char color)
{
      int x, y;
      int xMax, yMax, yInit;
      
      x = x0 -r0;                         if( x < 0 )                x = 0;
      yInit = y0 - r0;                if( yInit < 0 ) yInit = 0;
      xMax = x0 + r0;                        if( xMax >= 320) xMax = 319;
      yMax = y0 + r0;                if( yMax >= 240 ) yMax = 239;
      for( ; x <= xMax; ++x )
      {
                for( y = yInit; y <= yMax; ++y )
                {
                        if( abs( (x-x0)*(x-x0) + (y-y0)*(y-y0 ) ) <r0*r0 )
                              DrawPoint_VRAM(VRAM,x, y, color);
                }
      }
}

int main(void) {
        char *VRAM = init_VRAM();
        AllClr_VRAM(VRAM);
        lcd_ingray();
        PutDisp_DDVRAM(VRAM);
        initTP();
        int mx, my, bx, by; // m = mouse, b = ball
        float vx, vy;
        char msg;
        mx = bx = X_MAX/2; my = by = 30;
        vx = vy = 0;
        int last_x=mx,last_y=my,curr_x=mx,curr_y=my,touching=0;
        while( !isKeyPressed( KEY_NSPIRE_ESC ) )
        {       
                readTP();
      if (isTPTouched())
      {
                        if(!touching)
                        {
                                curr_x = ((tpreport->x)*320)/tpinfo->width;
                                curr_y = 240-(((tpreport->y)*240)/tpinfo->height);
                                touching=1;
                        }
                        mx=last_x+((tpreport->x)*320)/tpinfo->width-curr_x;
            my=last_y+240-(((tpreport->y)*240)/tpinfo->height)-curr_y;
                }
                else if(touching)
                {
                        last_x = mx;//((tpreport->x)*320)/tpinfo->width;
                        last_y = my;//240-((tpreport->x)*320)/tpinfo->width;
                        touching=0;
                }
                float fx = mx - bx;
                float fy = my - by + 60;
                vx += (fx / 40);
                vy += (fy / 40);

                vx *= 0.98; vy *= 0.98;

                bx += vx; by += vy;

                AllClr_VRAM(VRAM);
                sprintf(msg,"Ball Information:X %4d|Y %4d|VX %4d|VY %4d|",bx,by,(int)vx,(int)vy);
                DrawMiniString_VRAM(VRAM,0,0,msg,BLACK,WHITE);
                fillcircle( VRAM , bx, by, BALL_RADIUS ,BLACK );
                DrawLine_VRAM( VRAM, mx-1, my, mx+1, my, BLACK );
                DrawLine_VRAM( VRAM, mx, my-1, mx, my+1, BLACK );
                DrawLine_VRAM( VRAM, mx, my, bx, by, BLACK );
                PutDisp_DDVRAM( VRAM );
                sleep(30);
        }
        Close_VRAM(VRAM);
        endTP();
        return 0;
}
页: [1]
查看完整版本: Ndless SDK 系列教程——LCD操作