|
了解如何使用图像编辑器设计字符,并将它们显示在由EFM8微控制器控制的LCD上。
前言 在上一篇文章中,我们介绍了如何使用EFM8微控制器的SPI功能在128x128像素LCD上显示滚动水平线。在本篇文章中,我们将使用相同的底层接口来更新液晶显示器的所有大写字母;更加有趣的是,您将学习到如何有效设计和显示任何可以用10个垂直像素和8个水平像素充分表示的字符或符号的技术。
所需的硬件/软件 ● SLSTK2000A EFM8评估板 ● Simplicity Studio集成开发环境 ● SCILAB
项目概况 其目标是设计并在128×128像素的LCD上显示所有大写字母。文本以典型的文字处理器风格打印到LCD屏幕上:光标跟踪下一个字符将被显示的位置,并且您只需通过调用一个名为InsertCharacter()的函数来“键入”一个新字符即可。
字体是使用下一节中介绍的技术创建的定制设计的固定宽度(和固定高度)字符集。固定字符大小为10个垂直像素乘以8个水平像素(尽管字符实际上限制为9×7像素,因为包含空白行和空白列以提供相邻字符之间的分隔)。这个大小被选择了几个不同的原因。 LCD的像素宽度为128像素,因此8个水平像素可确保偶数个字符适合整个屏幕。我们使用10个垂直像素而不是8个,因为生成的更薄的字母看起来更悦目。这些尺寸也使得字母足够大,可以很容易辨认,但又足够小,这样LCD一次可以显示有意义的字符数。然而,最重要的原因可能是以下几点:用于将像素数据从字符数组复制到LCD数据缓冲区的代码要简单得多,宽度为8像素,因为表示像素状态的位与字节边界对齐。这意味着我们可以使用干净的字节操作适当地更新LCD数据,而不是使用一个字节的一部分和另一个字节的一部分复制一个字节的位的尴尬(低效)位操作。
设计字符 将视觉符号转换为与LCD兼容的数据是一个严重不直观的过程:谁可能通过为字节数组分配一系列十六进制值来绘制字母G?所以我们需要一种方便的方式来直观地设计一个字符,将这些可视信息转换为可以并入我们的微控制器代码并传输到LCD模块的数据字节。该过程从图像编辑软件开始;这里我们使用Paint.NET,一个强大而免费的图像和照片编辑应用程序。
获取空白画布并将其大小调整为10像素高8个像素宽,然后一路放大。使用铅笔工具填充单个像素,直到字母看起来正确,然后将图像保存为灰度.bmp文件。
现在是Scilab的时候了,你可以在这里下载。 Scilab是免费的数值计算软件,类似于MATLAB。与MATLAB一样,Scilab对于处理数字非常有用。在这个项目中,我们将使用它来分析字符图像并将它们转换为C代码,我们可以将其复制并粘贴到EFM8源文件中。除了Scilab本身,您还需要安装图像处理设计工具箱;这很容易通过Scilab的ATOMS模块管理器完成:
使用Scilab的最基本方式是将单个命令输入到控制台窗口中。不过,当你开始使用SciNotes文本编辑器来组合和组织这些命令时,事情会变得很激动人心,因为它允许您将Scilab指令合并到一个连贯的源代码文件中,该文件就像用高级编程语言编写的应用程序一样执行。以下是用于该项目的SciNotes脚本(LCD_image_handler.sce)中的一些代码:
代码 - ImageFilename = uigetfile(["*.bmp"]);
-
- if(length(ImageFilename) == 0)
- break
- end
-
- CurrentImage = ReadImage(ImageFilename);
-
- CurrentImage = SegmentByThreshold(CurrentImage, 100);
-
- CurrentImage = flipdim(CurrentImage, 2);
-
- PixelData = uint8(zeros(10,1));
-
- for row = 1:10
- for column = 1:8
- if (CurrentImage(row,column) == %T)
- PixelData(row) = bitset(PixelData(row), column, 1);
- else
- PixelData(row) = bitset(PixelData(row), column, 0);
- end
- end
- end
复制代码
LCD_image_handler.sce执行如下:首先它打开一个文件对话框;选择您想要处理的.bmp文件。
图像被转换为黑色和白色(如果任何像素值不完全是0或255),并且根据相应的像素是黑色还是白色来设置或清除10字节数组中的每一位。 10字节数组表示一个字符的大小(即,10个垂直像素乘以8个水平像素):一个字节提供8个水平像素值,因此10个字节数组包含10个线,每个线具有8个水平像素。最后,使用printf()例程将数组名称(从文件名中提取)和像素数据输出到Scilab控制台。这个过程重复,直到你点击文件对话框中的“取消”。
这些变量定义可以直接复制并粘贴到EFM8项目的源文件中。该脚本还生成用于在项目的头文件中声明这些变量的代码,因此最终输出如下所示:
正如你所看到的,这个过程是高效而且完全灵活的。使用这个SciNotes脚本,您可以快速创建任何符号的LCD兼容像素数据,您可以将它们绘制成10行8列图像。
端口I / O 端口I / O配置与我们在上一个SPI项目中使用的相同。
SPI信号映射到适当的端口引脚,除了芯片选择信号,我们通过P0.1手动驱动。
外设和中断 外设和中断设置与我们以前使用的类似:SPI配置为与LCD模块通信,Timer2中断管理帧速率,Timer4用于短延迟。唯一的区别是Timer2配置为5 Hz的中断频率,而不是60 Hz。在这个项目中,每次定时器2溢出时,液晶屏上都会打印一个新字母,并且较慢的更新频率使字符在被覆盖前更容易观察。
固件 在之前的项目中,我们使用了LCD的单线更新模式:在将片选选择为逻辑高电平后,EFM8发送模式选择字节,然后发送行地址,然后发送128位像素数据。传输完成时有两个虚拟字节,芯片选择返回逻辑低电平。这种模式适用于不适宜的应用程序,但新固件不同。整个LCD的像素值存储在一个单一的二维阵列中,只要Timer2中断告诉固件更新显示,所有这些数据位就会传输到LCD。换句话说,我们正在向LCD发送更多数据。 (注意:在这个特定的实现中,微控制器只控制前60条LCD线路,而不是全部128条线路,因为只有1024字节的片上RAM可供应用使用;足够大的整个LCD阵列需要2048字节。通过选择具有4352字节RAM的代码兼容EFM8设备,可以在定制设计中轻松修复此限制。)
所有这些额外的数据都会传送到LCD上,这意味着使用“多线更新模式”是正确的,顾名思义,该模式允许我们在一次SPI传输期间更新多条线的像素值。这意味着我们需要一个新的状态机在SPI中断例程中:
固件的另一个重要功能是将来自Scilab生成阵列的像素数据插入保存整个LCD的像素数据的二维阵列中:
代码 - void InsertCharacter(unsigned char *LCD_Character) //the input to this function is a pointer to a pixel data array
- {
- unsigned char n;
- unsigned char row_pixel;
- unsigned char column_byte;
- //rows are handled on a pixel-by-pixel basis
- row_pixel = LCDCursor[ROW];
- //columns are handled on a byte-by-byte basis, because one character width is 8 bits
- column_byte = LCDCursor[COL];
- /*each byte from the pixel data array is copied to LCDDisplayData,
- starting with the current cursor position*/
- for(n = 0; n < CHAR_HEIGHT; n++)
- {
- LCDDisplayData[row_pixel][column_byte] = *LCD_Character;
- LCD_Character++; //point to the next byte in the array
- row_pixel++; //the next byte corresponds to pixel data for the next line
- }
- LCDCursor[COL]++; //move the cursor one character width to the right
- /*if the cursor has reached the end of the line,
- return the cursor to the far left and move it
- down by one character height*/
- if(LCDCursor[COL] == NUM_LINE_DATA_BYTES)
- {
- LCDCursor[COL] = 0;
- LCDCursor[ROW] = LCDCursor[ROW] + CHAR_HEIGHT;
- if(LCDCursor[ROW] == NUM_LINES) //if the cursor has reached the end of the display area,
- LCDCursor[ROW] = 0; //return the cursor to the top line
- }
- while(UPDATE_LCD == FALSE); //wait here until Timer2 initiates an LCD update
- UPDATE_LCD = FALSE;
- UpdateAllLCDLines();
- }
复制代码
如果你检查上面的代码并阅读评论,你应该能够很好地了解它的工作原理。请注意,该例程既更新了LCD像素数据数组,又管理光标位置。
该项目的整体功能是重复打印所有大写字母,从A到Z再从A到Z等等。基本的程序流程如下: ● 微控制器清除LCD。 ● Timer2已启用。 ● 程序进入一个无限循环并使InsertCharacter()函数将A,B和C等依次打印到Z,然后再次打印到A。 ● InsertCharacter()函数(参见上面的代码摘录)更新LCD像素数据数组,管理光标,等待Timer2为LCD更新设置标志,然后通过UpdateAllLCDLines()函数启动更新。 ● 只有当SPI状态变量指示SPI接口空闲时,Timer2 ISR才会设置更新标志。这确保InsertCharacter()函数在上次传输完成之前不会尝试启动新的SPI传输。 |