Arduino Leonardo 自带的“显示屏

电子的巨大魅力在于无限的可能性。比如说用3个IO端口驱动6个 LED 或者用三极管、电感、电阻制作能榨干电池剩余电力的“焦耳小偷”。百思不得其解之后看到最终的解决方案总会有醍醐灌顶的感觉,也会非常钦佩第一个想到这样用法的人。和解决数学问题之后的快乐不同,因为电子和生活息息相关,学会了这样的招数转头也可以用在自己的设计上。

本文的起因是某天在网上看到有人用 Teensy 2.X 制作的摄像头【参考1】,2.0版本使用32u4也是Leonardo同款主控芯片,因此,这个项目完全可以使用在Leonardo上。

Arduino Leonardo 是很常见的Arduino开发板,它使用了 32U4 的主控芯片,其中带有了USB Device,因此我们有机会将视频直接投送到PC上,而具体的方法就是将设备报告为 USB Camera,再将要显示的内容生成视频发送出去。Windows 内置了 USB Mass Storage 驱动,因此用户可以直接使用 U盘而无需额外安装驱动。同样的,目前的 Win10 内置了 UVC(USB video device class)的驱动,对于符合这个协议定义的USB 设备可以直接在“摄像头”程序中显示出来。

上面介绍了基本原理,接下来就是具体的实验。为了能够更好的展现内容,实验的目标是滚动显示“祝新年快乐”字样。在实验验证上有很多经验之谈,比如:不要用4个字做实验,因为4刚好是2的2倍,同时也是2的平方。很多适用于此的技巧实际上只是巧合。因此,这次使用5个字。另外还有就是测试音频设备尽量不要使用纯音乐而要使用歌曲,后者更容易让测试人员得知当前的音调是否正常。

先研究一下字模的问题。为了将汉字的字形显示输出,汉字信息处理系统还需要配有汉字字模库,也称字形库,它集中了全部汉字的字形信息。需要显示汉字时,根据汉字内码向字模库检索出该汉字的字形信息,然后输出,再从输出设备得到汉字。汉字点阵字模有16*16点、24*24点、32*32点,48*48点几种,每个汉字字模分别需要32、72、128、288个字节存放,点数愈多,输出的汉字愈美观。从经验上来说,16x16是普通人能够接受的最小字形,虽然这个尺寸的字形信息也有缺少笔画的问题(比如:“量”字,在这个尺寸下会丢掉上面 “曰”的最下面一横),但是少于16X16的汉字字形信息会让观看者有明显的缺少笔画的的观感,有如“第二次简化字”死灰复燃。24x24 的字形信息则是完全不丢失笔画的最小尺寸。但是缺点很明显,每个汉字要比16x16的字形多花1倍的空间来进行存储。这对于内存和处理能力有限的单片机来说,着实是一个负担。因此,大多数情况下,单片机使用最多的是 16x16的自字形库HZK16。这个字库是符合GB2312国家标准的16×16点阵字库,HZK16的GB2312-80支持的汉字有6763个,符号682个。其中一级汉字有 3755个,按声序排列,二级汉字有3008个,按偏旁部首排列。取得字形的过程如下:

  1. 取得欲查询汉字的GB2312编码,每个汉字由2个Byte 进行编码,第一个字节被称作              “区码”;第二个字节被称作“位码”;
  2. 接下来计算汉字在HZK16中的绝对偏移位置:offset = (94*(区码-1)+(位码-1))*32
  3. 读取 HZK16 中 offset 给出的位置连续 32 Bytes 即可得到字形信息

例如:查询得到“奈”的区位码为3646,那么计算  offset=(94*(36-1)+(46-1))*32=106720(D)=0x1A0E0

○○○○○○○●○○○○○○○○   →   0x01,0x00
○○○○○○○●○○○○●○○○   →   0x01,0x08
○●●●●●●●●●●●●●○○   →   0x7F,0xFC
○○○○○○●○●○○○○○○○   →   0x02,0x80
○○○○○●○○○●○○○○○○   →   0x04,0x40
○○○○●○○○○○●●○○○○   →   0x08,0x30
○○○●○○○○○●○○●●●○   →   0x10,0x4E
●●●○●●●●●●●○○●○○   →   0xEF,0xE4
○○○○○○○○○○○○○○○○   →   0x00,0x00
○○○○○○○○○○○●○○○○   →   0x00,0x10
○○●●●●●●●●●●●○○○   →   0x3F,0xF8
○○○○○○○●○○○○○○○○   →   0x01,0x00
○○○○●○○●○○●○○○○○   →   0x09,0x20
○○○●○○○●○○○●●○○○   →   0x11,0x18
○●●○○●○●○○○○●○○○   →   0x65,0x08
○○○○○○●○○○○○○○○○   →   0x02,0x00

有了上面的知识,可以很容易的从字库中取得汉字的字形,比如,用程序取得”祝”字的字形信息:

其中的key[32] = {0x20,0x08,0x13,0xFC,0x12,0x08,0x02,0x08,0xFE,0x08,0x0A,0x08,0x12,0x08,0x3B,0xF8,0x56,0xA8,0x90,0xA0,0x10,0xA0,0x11,0x20,0x11,0x22,0x12,0x22,0x14,0x1E,0x18,0x00}; 就是我们需要的字形信息。

用同样的方法,可以依次取得“新年快乐”的字形信息。滚动的原理可以想象成一个窗户,不断在向右侧滑动。窗户的大小为  16 Bits ,落在这个里面的内容就是需要显示出来的内容。

出现在窗口中的数值有两种情况:

  1. 刚好是一个完整的字,那么直接输出这个字的信息即可;
  2. 介于第一个字和第二个字之间,需要取得第一个字形的高位信息,然后和第二个字的低位信息拼接即可;

上面解决了显示内容的问题,下面就是如何显示。模拟出来的摄像头将一帧帧的图像发送给PC,其中采用的是YV12的编码格式。通常我们接触到的都是 RGB 格式,每一个点由Red/Green/Blue 三个颜色信息组成,最常见的是每一个颜色各占1个字节。YV12 则使用的是另外的颜色表示方法,使用Y 命令度(Luminance),U色度(Chrominance)和 浓度(Chroma)。这种表示方法是 是历史原因导致的,它出现在Y'UV的发明是由于彩色电视与黑白电视的过渡时期。黑白视频只有Y(Luma,Luminance)视频,也就是灰阶值。到了彩色电视规格的制定,是以YUV的格式来处理彩色电视图像,把UV视作表示彩度的C(Chrominance或Chroma),如果忽略C信号,那么剩下的Y(Luma)信号就跟之前的黑白电视频号相同,这样一来便解决彩色电视机与黑白电视机的兼容问题。另外,这种编码方式也会使用视觉特性来节省空间。每一个点的 Y 信号是独立的,但是相邻的四个点会共享同一个U和同一个V信息。比如:之前空间上存在相邻的四个点 (Ri,Gi,Bi) 通过某种算法变换后得到 (Yi,U1,V1)这样的四个点信息。之前存放四个点需要 3*4=12Byte;变化之后只需要存储 Y1/Y2/Y3/Y4/U1/V1 6Byte的信息即可。减少了一半的数据量。

R1,G1,B1 R2,G2,B2 Y1,U1,V1 Y2,U1,V1
R3,G3,B3 R4,G4,B4 Y3,U1,V1 Y4,U1,V1

将上面的过程放在一个帧的图像上来看是下面这样

代码很长,但大部分都只是框架,send_yv12_frame() 是最关键的函数,具体的输出帧(借用计算机图形学的概念可以说是“渲染的过程”)是在其中完成计算的:

static inline void send_yv12_frame(void)
{
    uint16_t h,w,lastw;
    uint8_t write_hdr = 1;
    uint8_t hdr;
    uint8_t color, colcnt;
    //uint8_t br = LSB(brightness);
    uint8_t board[16][16];
    int c,low,high;
    char *p;
  
    usb_wait_in_ready();

    if (k==(sizeof(wordaddr)/2-1)*16) {k=0;}
    else k++;
    
    //显示一个完整的字形
    for (int i=0;i<16;i++)
    {
      //k给出当前要显示的窗口起始位置      
      if (k%16==0)  //如果当前指针刚好在一个字上,那么直接取出这个字进行显示
        {
          //指向要显示的汉字起始地址
          p=wordaddr[k/16];
          //取出这个字的一行信息
          c=((*(p+i*2)&0xFF)<<8)+(*(p+i*2+1)&0xFF);
        }
      else //指针不在一个字上,就要用两个字来拼成一个字进行显示
        {
          //指向要显示的第一个汉字
          p=wordaddr[k/16];
          //取出第一个汉字的一行
          low=(((*(p+i*2)&0xFF)<<8)+(*(p+i*2+1)&0xFF))&0xFFFF;           
          //指向要显示的第二个汉字
          p=wordaddr[(k/16+1)%sizeof(wordaddr)];
          //取出第二个汉字的一行
          high=(((*(p+i*2)&0xFF)<<8)+(*(p+i*2+1)&0xFF));
          //用取得的信息拼出来要显示的一行
          c=low<<(k%16)|(high&0xFFFF)>>(16-k%16);
        }

    for (int j=0;j<16;j++)
      {
        if ((c&0x8000)==0) {
              board[j][i]=0;//这个点位置有信息
          }
        else  board[j][i]=1;//这个点位无信息
        c=c<<1;
      }        
     }
     
    //下面发送一帧信息 
    /* Y plane (h*w bytes) */ 
    for(h=0; h < HEIGHT; h++) {
        w=0;
        color = cur_start_col;
        colcnt = COLUMN_WIDTH - cur_col_offset;
        do {
            if(!usb_rw_allowed()) {
                usb_ack_bank();                
                if(usb_wait_in_ready_timeo(10) < 0)                
                    return;                
                usb_ack_in();
                lastw = w;
                write_hdr = 1;
            } else {
                if(write_hdr) {
                    /* Write header */
                    hdr = UVC_PHI_EOH | (fid&1);
                    UEDATX = 2; // write header len
                    UEDATX = hdr;
                    write_hdr = 0;
                }
                //为了美观,上下各空出12
                if ((h<12)||(h>107)) {
                       UEDATX=0; 
                }
                else {
                   //检查当前的点阵信息输出黑或者白  
                   //Y:255 U:128 V:128 是白色
                   //Y:0   U:128 V:128 是黑色
                   if (board[w/10][(h-12)/6]==1) {UEDATX=0xFF;} // Y                
                   else {UEDATX=0;}
                }
                w++;
                if(--colcnt == 0) {
                    color = next_color(color);
                    colcnt = COLUMN_WIDTH;
                }
            }            
        } while(w < WIDTH);
    }

    /* U plane (h/2*w/2 bytes) */ 
    for(h=0; h < HEIGHT/2; h++) {
        w=0;
        color = cur_start_col;
        colcnt = COLUMN_WIDTH - cur_col_offset;
        do {
            if(!usb_rw_allowed()) {
                usb_ack_bank();                
                if(usb_wait_in_ready_timeo(10) < 0)                
                    return;                
                usb_ack_in();
                lastw = w;
                write_hdr = 1;
            } else {
                if(write_hdr) {
                    /* Write header */
                    hdr = UVC_PHI_EOH | (fid&1);
                    UEDATX = 2; // write header len
                    UEDATX = hdr;
                    write_hdr = 0;
                }
                UEDATX=128;                
                w++;
                if(colcnt <= 2) {
                    color = next_color(color);
                    colcnt = COLUMN_WIDTH;
                } else {
                   colcnt -= 2; 
                }

            }            
        } while(w < WIDTH/2);
    }

    /* V plane (h/2*w/2 bytes) */ 
    for(h=0; h < HEIGHT/2; h++) {
        w=0;
        color = cur_start_col;
        colcnt = COLUMN_WIDTH - cur_col_offset;
        do {
            if(!usb_rw_allowed()) {
                usb_ack_bank();                
                if(usb_wait_in_ready_timeo(10) < 0)                
                    return;                
                usb_ack_in();
                lastw = w;
                write_hdr = 1;
            } else {
                if(write_hdr) {
                    /* Write header */
                    hdr = UVC_PHI_EOH | (fid&1);
                    if(h==HEIGHT/2-1 && w < UVC_TX_SIZE)
                        hdr |= UVC_PHI_EOF; 
                    UEDATX = 2; // write header len
                    UEDATX = hdr;
                    write_hdr = 0;
                }
                UEDATX=128;
                w++;
                if(colcnt <= 2) {
                    color = next_color(color);
                    colcnt = COLUMN_WIDTH;
                } else {
                    colcnt-=2;
                }
            }            
        } while(w <WIDTH/2);
    }

    if(lastw != w) {
        usb_ack_bank();
    }
    fid = ~fid; // flip frame id bit
}

和之前的项目一样,这个需要基于 Lufa 库支持,使用  WinAvr 进行编译。使用 make 即可完成编译。

使用和Arduino刷新一样的的命令,要根据你的Arduino板子重启时占用的串口号调整 –PCOMn 参数。同时,每次刷新时,需要先按下 Arduino Reset键,然后运行刷写命令。

D: \arduino-1.8.4\hardware\tools\avr\bin\avrdude -v –d:\arduino-1.8.4\hardware\tools\avr\etc\avrdude.conf -patmega32u4 -cavr109 -PCOM7 -b57600 -D -V -Uflash:w:./uvc.hex:i

烧写成功后,运行 Camera 即可查看结果:

为了简单起见,只实现了黑白显示,有兴趣的朋友可以发挥想象力显示彩色的汉字,相信这只是一个开始,后面会有更多的玩法。

参考:

1. https://github.com/avivgr/teensy_uvc

《Arduino Leonardo 自带的“显示屏》有一个想法

  1. I second this motion. SparkFun should make a separate inventors kit for the Leonardo. (Except for the fact that they just switched from Uno to RedBoard, so they're probably not permitted to use another arduino board.)

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注