查看: 1263|回复: 4

Arduino Leonardo 自带的“显示屏”

[复制链接]

该用户从未签到

发表于 2019-1-10 21:19 | 显示全部楼层 |阅读模式
电子的巨大魅力在于无限的可能性。比如说用3IO端口驱动6 LED 或者用三极管、电感、电阻制作能榨干电池剩余电力的“焦耳小偷”。百思不得其解之后看到最终的解决方案总会有醍醐灌顶的感觉,也会非常钦佩第一个想到这样用法的人。并且因为电子和生活息息相关,学会了这样的招数转头也可以用在自己的设计上。
本文的起因是某天在网上看到有人用 Teensy 2.X 制作的摄像头【参考1】,2.0版本使用32u4也是Leonardo同款主控芯片,因此,这个项目完全可以使用在Leonardo上。
Arduino Leonardo 是很常见的Arduino开发板,它使用了 32U4 的主控芯片,其中带有了USB Device,因此我们有机会将视频直接投送到PC上,而具体的方法就是将设备报告为USB Camera,再将要显示的内容生成视频发送出去。Windows 内置了 USBMass Storage 驱动,因此用户可以直接使用 U盘而无需额外安装驱动。同样的,目前的 Win10内置了UVCUSBvideo device class)的驱动,对于符合这个协议定义的USB 设备可以直接在“摄像头”程序中显示出来。
上面介绍了基本原理,接下来就是具体的实验。为了能够更好的展现内容,实验的目标是滚动显示“祝新年快乐”字样。在实验验证上有很多经验之谈,比如:不要用4个字做实验,因为4刚好是22倍,同时也是2的平方。很多适用于此的技巧实际上只是巧合。因此,这次使用5个字。另外还有就是测试音频设备尽量不要使用纯音乐而要使用歌曲,后者更容易让测试人员得知当前的音调是否正常。
先研究一下字模的问题。为了将汉字的字形显示输出,汉字信息处理系统还需要配有汉字字模库,也称字形库,它集中了全部汉字的字形信息。需要显示汉字时,根据汉字内码向字模库检索出该汉字的字形信息,然后输出,再从输出设备得到汉字。汉字点阵字模有16*16点、24*24点、32*32点,48*48点几种,每个汉字字模分别需要3272128288个字节存放,点数愈多,输出的汉字愈美观。从经验上来说,16x16是普通人能够接受的最小字形,虽然这个尺寸的字形信息也有缺少笔画的问题(比如:“量”字,在这个尺寸下会丢掉上面 “曰”的最下面一横),但是少于16X16的汉字字形信息会让观看者有明显的缺少笔画的的观感,有如“第二次简化字”死灰复燃。24x24的字形信息则是完全不丢失笔画的最小尺寸。但是缺点很明显,每个汉字要比16x16的字形多花1倍的空间来进行存储。这对于内存和处理能力有限的单片机来说,着实是一个负担。因此,大多数情况下,单片机使用最多的是16x16的自字形库HZK16。这个字库是符合GB2312国家标准的16×16点阵字库,HZK16GB2312-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
有了上面的知识,可以很容易的从字库中取得汉字的字形,比如,用程序取得字的字形信息:
Untitled.png
其中的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.     介于第一个字和第二个字之间,需要取得第一个字形的高位信息,然后和第二个字的低位信息拼接即可;
image001.png
                              
上面解决了显示内容的问题,下面就是如何显示。模拟出来的摄像头将一帧帧的图像发送给PC,其中采用的是YV12的编码格式。通常我们接触到的都是 RGB 格式,每一个点由Red/Green/Blue 三个颜色信息组成,最常见的是每一个颜色各占1个字节。YV12则使用的是另外的颜色表示方法,使用Y命令度(Luminance),U色度(Chrominance)和 浓度(Chroma)。这种表示方法是 是历史原因导致的,它出现在Y'UV的发明是由于彩色电视与黑白电视的过渡时期。黑白视频只有YLumaLuminance)视频,也就是灰阶值。到了彩色电视规格的制定,是以YUV的格式来处理彩色电视图像,把UV视作表示彩度的CChrominanceChroma),如果忽略C信号,那么剩下的YLuma)信号就跟之前的黑白电视频号相同,这样一来便解决彩色电视机与黑白电视机的兼容问题。另外,这种编码方式也会使用视觉特性来节省空间。每一个点的 Y 信号是独立的,但是相邻的四个点会共享同一个U和同一个V信息。比如:之前空间上存在相邻的四个点 Ri,Gi,Bi 通过某种算法变换后得到 Yi,U1,V1)这样的四个点信息。之前存放四个点需要 3*4=12Byte;变化之后只需要存储 Y1/Y2/Y3/Y4/U1/V16Byte的信息即可。减少了一半的数据量。
  
R1,G1,B1
  
  
R2,G2,B2
  
  
Y1,U1,V1
  
  
Y2,U1,V1
  
  
R3,G3,B3
  
  
R4,G4,B4
  
  
Y3,U1,V1
  
  
Y4,U1,V1
  
将上面的过程放在一个帧的图像上来看是下面这样
image002.png
代码很长,但大部分都只是框架,send_yv12_frame() 是最关键的函数,具体的输出帧(借用计算机图形学的概念可以说是渲染的过程)是在其中完成计算的:

kittenblock中小学创客名师推荐的图形化编程软件


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]=0;//这个点位置有信息

          }

        else  board[j]=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 即可完成编译。
image003.png
使用和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 即可查看结果:
image004.png
为了简单起见,只实现了黑白显示,有兴趣的朋友可以发挥想象力显示彩色的汉字,相信这只是一个开始,后面会有更多的玩法。

参考:

动态视频稍后放出..........

评分

参与人数 1贡献 +1 收起 理由
coloz + 1

查看全部评分

打赏作者鼓励一下!

该用户从未签到

 楼主| 发表于 2019-1-10 21:20 | 显示全部楼层
完整代码下载


ccshow.zip

129.71 KB, 下载次数: 1

打赏作者鼓励一下!

该用户从未签到

 楼主| 发表于 2019-1-12 11:00 | 显示全部楼层
打赏作者鼓励一下!
  • TA的每日心情
    擦汗
    2019-7-29 00:08
  • 签到天数: 47 天

    [LV.5]常住居民I

    发表于 2019-1-23 13:06 来自手机 | 显示全部楼层
    这样看来可以开发更为复杂的应用了,上位机直接就是camera app
    您需要登录后才可以回帖 登录 | 立即注册  

    本版积分规则

    热门推荐

    OLED 128*64自制可达10000000个选项的菜单(已更新)
    OLED 128*64自制可达10000
    OLED 128*64自制可达10000000个选项的菜单 温馨提示: 建议占个楼再食用本帖子
    这个怎么整?标点都是英文的
    这个怎么整?标点都是英文
    a=a+1改成a++也不行
    [WiFiduino-8266开发板测试]三、测试IO口
    [WiFiduino-8266开发板测
    首先测试IO口的关系,板载正面的D0~D15[D14,D15没有,相同的位置写的是D4,D5],反面
    求助,arduino nano发热严重
    求助,arduino nano发热严
    我有一块arduino nano板,装在一个扩展板上(如下图),用扩展板上的外接电源接口(资
    [限时福利]5分钟带你快速了解新一代开发板:M5STACK
    [限时福利]5分钟带你快速
    一、什么是M5Stack M5Stack是一种模块化、可堆叠扩展的开发板,每个模块
    Copyright   ©2015-2016  Arduino中文社区  Powered by©Discuz!   ( 蜀ICP备14017632号-3 )
    快速回复 返回顶部 返回列表