— 前言 —
前言往往要解释写文章的动机和原因,同时给作者一个正题以外灌水的机会——本文也不例外。 1、为什么我要写这篇文章。 不可否认,我的确受到了Armok的利诱影响,但是最近发生的一些事情却使我觉得写这篇文章是非常有必要的。在OurAVR上看到很多版本的LCD驱动程序,几乎每一个版本都只是简单的将全部或部分的显示数据Cover到LCD的显存上,完成一个字或者是图片的显示就等着大家喊“牛”了。其实要走的路还很远。对一个工程项目来说,增加n多的成本来提供一个点阵屏作为用户接口,不是一两幅欢迎图片和Now Loading...Please Standy By的提示能糊弄的过去的。用户希望你提供的是友好的图形界面GUI,虽然比不过XP和Apple的华丽,但是由各种基本图形组成的窗口界面还是需要的。 当我们真的想实现一个图形界面的时候,很快就会发现,我们需要的不仅仅是一个被喊了“牛”的初级驱动,我们需要的是一个图形引擎——一个自定义的图形函数包,没有DirectX的华丽,但是能绘制一个任意的直线或是矩形就够了——结果往往发现无所适从。这个时候,我们遇到的就是一个门槛,真正的嵌入式工程师和一个业余电子爱好者之间的门槛。 2、我如何写这篇文章 考虑到本人老王卖瓜的习惯,所以请大家一定无比在吃饭前看本人写的技术文章,同时保持耐心等待续集(绝对有续集)。本人现单身,个人问题众多,学习任务重,所以可能有时候写文章象羊拉屎,不对大家胃口,请见谅。 硬件平台:AVR Mega8级 LCD: 不带字库的12864 软件平台:ICC 规范: 符合基本的C编程规范 3、何时开始正文 广告之后,马上回来…… 怎样在点阵屏上绘图——基于LCD12864
Chapter Zero
— 预备知识 — 其实,本文应该算是计算机图形学的一个具体分支,所以,计算机图形学的基本要求就是本文的基本要求,考虑到各位兄弟的胃口,我就多罗嗦下。 1、位操作 向LCD12864这种二值屏幕,我们习惯于用1个字节表示连续的8个点,1对应对应位被点亮,0表示不亮,所以对图形的操作最基本的手段就是位操作。 复习下,常用的位操作,假设Dis表示某一个现存地址的内容 Dis = Dis~ 黑白颠倒 Dis &= ~(1<<n) 第n处被擦去, Dis |= (1<<n) 第n处被画了一个点 Dis ^= (1<<n) 如果n处是亮的,就变被擦掉;如果n处是空白的,就被点亮…… …… 差不多就这些 2、作图原理 点是一切光栅显示设备的基本要素,所有的操作都是以点为基础的,所以学会如何利用点构成线、圆、填充就是必须掌握的——几何不能太差哦。 还有,结合屏幕的硬件特点,对算法进行优化的一些方法也是需要掌握的。比方说,如何填充之类的……后面会针对LCD12864作详细介绍的。 3、人机交互学 虽然很多人都没有实实在在学过这门功课,但是多多少少对于界面应该怎样有些许了解。如何利用手中的基本操作函数做出一些特效,如何安排窗体,如何绘制图形界面的一些基本元素如按钮,甚至如何显示汉字,都是人际交互学需要教会你的——总之,如果你没有学过这门课程,你的产品只有你自己用的话——跟着感觉走,没错的。 4、最最重要的物质基础 你要掌握一种单片机,掌握一种点阵屏幕。 AVR Inside Not Only For LCD12864 怎样在点阵屏上绘图——基于LCD12864
Chapter One
— 从点滴开始 — 前面已经说过,对于栅格显示设备来说,点是一切图形的基础,说白了,不会在液晶屏幕的任意位置随心所欲的画点就不能说你已经掌握了某种液晶屏幕的使用了。事实上,根据笔者的经验,尝试画点往往能暴露出很多问题。就拿LCD12864来说吧,一段时间,笔者总是发现,在片切换的地方有一点点错误…… 在说明如何画点之前先说明一个概念:显示缓冲区。 所谓显示缓冲区,顾名思义,就是显示数据的一个缓冲地带。一般情况下,我们绘图也是直接对这块缓冲区域进行操作的。 很多朋友就要提问了:我们为什么不直接对屏幕操作呢? 事实上,LCD本身存在一块显示存储器,也可以被认为是一个显示缓存,直接写在这个存储器的数据并非直接显示出来,而多半需要一个显示指令来影射一下。我们有时候也通过这种类似的技术来实现很大的图片的显示——先画好,再拿给大家看,让人以为你是一下就画好的,当然如果你刻意需要那种图片被“画”出来的效果,则不在讨论之列。问题就在于,LCD是片外资源,对其存储器的访问可以被认为是对片外存储器的访问,其速度显然没有对片内SRAM的操作速度要快。如果我们使用那种常用的串行方式来作图(所谓串行方式作图,就是绘图指令的执行和系统的其他操作时串行的,指令不完成,其它操作就不回被执行),那么对于一些实时性要求高的系统来说就会造成重大隐患——甚至是不符合要求的;如果开辟一段片内存储空间和LCD的存储器一一对应,在相同的时间段内,花费相同的资源来保持着两个存储空间的一致性,那么就可以保证实时系统的稳定和可靠,保证画面显示的正常(因为允许跳帧嘛^_^)。这就是我们为什么需要另外在片内选取一个显示缓冲区的原因。 以后,我们的操作都是针对显示缓冲区的。显示缓冲区将成为一个概念,无论这个缓冲区位于SRAM内还是LCD内部。比方说,我们需要一个低成本的游戏机 ——如做一个贪食蛇游戏机送给老师作为课程设计,或者送给小侄子,那么,M8甚至是M48成为首选,问题是,M8绝对无法开辟出一个8 * 128 = 1K大小的数组,所以,这个时候,认定LCD的片内存储器作为显示缓冲区就是不二的选择——好在贪食蛇速度太快了也没有办法玩哈。 绕了这么多的弯子,究竟如何画点呢?我还想多补充一个原因,关于,为什么需要显示缓冲区,由于点阵屏幕使用的是1个字节来表示8个点的,如果你想使得一个字节中的某一个点被操作而不影响别的点,你起码要知道原来这个位置是什么内容,对于有些使用595级联的LCD设备来说,无法直接从LCD读取现存数据,所以开辟一个缓冲区,从中获得信息加以加工以后再放回去,是这种设备处理显示图形的唯一方法——当然我们没有那么惨,但是从LCD中获取所需点所在字节的内容还是必须的,不然你画一个点就会破坏一条线上所有的数据。 好的,那么我们实际上已经明确了如何去画一个点。 首先,根据用户给出的坐标计算出所要画的点所在现存内的地址偏移量; 然后,我们读出该地址内的数据; 再次,根据用户所需画点的类型(擦除、画点、反相点)来进行相应的操作获得一个新的数据; 最后,将该数据写回原来的地址。 不是很难吧? --------------------------------------------- LCD12864补充知识 1、关于坐标系。很多人,包括笔者,一开始都看不懂LCD12864的内存影射方式,感觉X、Y似乎不是那么回事。后来才发现,只要把屏幕竖着放一切就好懂了。X还是横轴,Y还是竖轴……但是这显然不符合我们的习惯,我们习惯于长的那个边作为横轴,所以需要一点点坐标之间的转换。 假设输入的是正常习惯的坐标 X,Y DX DY就是LCD上的坐标,那么转换关系是 char DX = (Y >> 3); //计算出属于哪个字节 char BX = Y - (DX << 3); //属于该字节的哪个位 char DY = X; 2、读取12864的数据的时候,一定要注意,E信号要在一个下降延之后持续拉高,然后才能正常独处数据;假设直接拉高,的确也能读出数据,但是,等着抓头皮,发帖子“[跪求]大侠帮忙关于12864——请使用明确的大标题……”^_^ --------------------------------------------- 废话少说(说的不少了),看源代码: # define LCD12864_Graphic_Draw 0x01 # define LCD12864_Graphic_Clear 0x00 # define LCD12864_Graphic_Not 0x02 …… void LCD12864Draw(char X,char Y,char Type) { char DX = (Y >> 3); //计算出属于哪个字节 char BX = Y - (DX << 3); //计算出属于字节哪一位 char TempData = 0; LCD12864_ChooseBoth; setX(DX); if (X > 63) { LCD12864_ChooseCS2; X -= 64; } else { LCD12864_ChooseCS1; } setY(X); TempData = getLCD12864Data(); switch (Type) { case LCD12864_Graphic_Clear: TempData &= ~(1<<BX); break; case LCD12864_Graphic_Not: TempData ^= (1 << BX); break; default: TempData |= (1 << BX); } setY(X); sendDataToLCD(TempData); } 怎样在点阵屏上绘图——基于LCD12864
特别说明一下,关于贪食蛇范例的问题,这篇文章里面只会简单得提及一下。
作为嵌入式系统开发的一个范例,我会另外开一个帖子详细说明开发过程。 这个范例将作为介绍嵌入式系统开发方法的一个很好的例子,用于解释一个系统和一段表示您调通了某一个功能的代码之间有什么区别,同时也将介绍嵌入式开发系统的几种模式(超级循环、调度器),顺便侃一侃时间驱动的系统RTOS (Real Time Operation System实时操作系统)和RTS(Real Time System)实时系统。 怎样在点阵屏上绘图——基于LCD12864
[本章导读]
直线由点构成,更精确的说,直线是由靠近这条线的像素构成。这就引出一个问题,究近那些点算是靠近一条直线;哪些点不算是靠近一条直线,这必须使用一种算法作为依据。实际上,图形学算法和纯几何算法还是有很大差别的,问题就出在一个离散化上面,说白了,你画出的直线很可能是一组波动厉害的锯齿象素群而不是一条看起来有规则变化的直线。 当然,太过于理论的东西对我们是没有多少实际价值的。下面,我就介绍两种常见的画线思路,一种就是最容易被想到的直线方程计算的方法,另外一种则是被称为布兰森汉姆(Bresenham)的计算机图形学主流算法。 在介绍完这两种算法以后,我们会针对LCD12864的硬件结构为例子,介绍,具体算法的实现和优化。 怎样在点阵屏上绘图——基于LCD12864
首先,我们从最基本的数学算法说起。
如果我们使用公式y = kx + b来作为绘图的依据,那么就需要分3种情况:水平直线,斜率为0;垂直直线,斜率为五穷达(或者说k不存在);普通直线。 假设我们已经知道直线的起始坐标点(Xbegin,Ybegin)和终点(Xend,Yend),x,y,是当前的坐标点,如果我们通过增加x反算出y的方法的话,这个公式就可以很容易转换为伪代码。 LineMode 为直线的类型:水平,垂直,普通 if Xbegin == Xend then LineMode = 水平 elseif Ybegin = Yend then LineMode = 垂直 else k = (Yend - Ybegin) / (Xend - Xbegin) switch LineMode case 水平 for x = Xbegin to Xend 在x,Ybegin处画点 case 垂直 for y = Ybegin to Yend 在Ebegin,y处画点 default: for x = Xbegin to Xend { y = kx + b 在x,y处画点 } 非常简单,不是么?注意,大部分情况下,这个算法存在很多乘法和除法,对单片机系统来说,可能不是那么合适哦。画出的点也基本令人满意。 怎样在点阵屏上绘图——基于LCD12864
有了上面的代码垫底,我想很多人都可以放心了,因为大不了跑一个高速晶振也能快速的画出直线,否则你还需要耐心的阅读下面的算法。
首先,我们给出这个算法的伪代码。 在(x1,y1)到(x2,y2)之间画一条直线 dx 是x到终点横坐标的距离 dy 是y到终点纵坐标的距离 ix 是dx的绝对值 iy 是dy的绝对值 inc是dx和dy中较大的那个 plot是是否要画一个点的标志位,boolean变量 plotx 是当前点所在的横坐标 ploty 是当前点所在的纵坐标 plotx = x1 ploty = y1 x = 0 y = 0 在 plotx,ploty画一个点——起点 for i = 0 to inc 增量1 x += ix y += iy plot = false if x > inc then plot = true x -= inc if dx > 0 then plotx ++ if dx < 0 then plotx -- if y > inc then plot = true y -= inc if dy > 0 then ploty ++ if dy < 0 then ploty -- if plot == true then 在(plotx,ploty)处画点 这就是计算机图形学中流行的布兰森汉姆(Bresenham)算法,他的意图就是采用离散的整数增量来代替斜率增量计算,学习这个算法,最好的方法不是看多少理论,而是按照上面的伪代码自己完成一条直线的绘制工作,你就能心领神会了——不是小弟我偷懒。 所有的计算都是简单得整数计算,代码效率自然不用小弟我罗嗦哈。 怎样在点阵屏上绘图——基于LCD12864
俗语说,巧妇难为无米之炊,有了点再有了直线算法,距离窗体的绘制不远了,But Stop! “没有最好,但求更好”这是我们做系统开发应该谨记的一条准则。
有了上面的算法还不够,毕竟,他们只是一些伪代码,针对不同的屏幕——准确地说,是针对不同的显存映射方式,有不同的算法优化方法。究竟有哪些优化方法暂且不论,其实他们都是一个原理,我想先解释一下,为什么我们需要优化算法,或者说,我们需要先弄清楚是什么地方产生了冗余。 还记得黑白点阵屏幕的显存映射方式么?它简单的使用1个字节表示8个坐标点,同时这1个字节是沿着屏幕的短边方向映射的,所以当我们想画一条垂直的直线时,对于每一个牵涉到的字节都有可能要重复的操作8次之多,这种操作不是简单的画线,而是要先读取再计算最后再写这样的复合操作,重复8次只是为了把整个字节变黑显然是一种超级不可容忍的冗余——大家都知道,直接把这个字节读取一次,计算一次,再写一次就完成了。基于这种思想,我们需要把这种特殊情况单独提取出来,重新优化代码……具体优化代码就留给大家做作业了哈。 同时补充一下,这样做,不是只对画线作了优化,事实上,在后面,矩形的填充里面,这种优化会极大地提高速度,因为填充的本质就是画线(点)…… |