电子的巨大魅力在于无限的可能性。比如说用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)&amp;0xFF)<<8)+(*(p+i*2+1)&amp;0xFF);
        }
      else //指针不在一个字上,就要用两个字来拼成一个字进行显示
        {
          //指向要显示的第一个汉字
          p=wordaddr[k/16];
          //取出第一个汉字的一行
          low=(((*(p+i*2)&amp;0xFF)<<8)+(*(p+i*2+1)&amp;0xFF))&0xFFFF;           
          //指向要显示的第二个汉字
          p=wordaddr[(k/16+1)%sizeof(wordaddr)];
          //取出第二个汉字的一行
          high=(((*(p+i*2)&amp;0xFF)<<8)+(*(p+i*2+1)&amp;0xFF));
          //用取得的信息拼出来要显示的一行
          c=low<<(k%16)|(high&amp;0xFFFF)>>(16-k%16);
        }

    for (int j=0;j<16;j++)
      {
        if ((c&amp;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&amp;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&amp;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&amp;1);
                    if(h==HEIGHT/2-1 &amp;&amp; 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

Leave a Reply

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

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>