点阵墨水屏的使用以及图像预处理

 

    我们电子日历的产品,选用的屏幕尺寸为5.83寸,分辨率为648*480。屏幕本身支持黑白红三色,我们使用黑白两色,单色位图表示的话,每个位都能表示一个像素点。所以对于这个屏幕而言,要显示一整幅图,需要的字节数为 38880。然后,由于屏幕需要的只是像素点,所以不能直接将一个位图数据写进去,需要预先转换一下。然后屏幕本身对于像素点的处理方式的不同,也会导致图片的预处理过程不一样。

    对于这个墨水屏的屏幕而言,非专业人员读他们这个datasheet难度较大,各种术语和参数意义不是很容易看懂。我开发主要是先按照官方给的示例程序和图像跑一遍,然后基本跑通了大致的操作也就清楚了,然后开始看datasheet这么个流程。

    我们前后接触了三个厂商,一个是大连奇云电子,这个是纯从淘宝上找的一个,他们这个有四灰度的屏幕,我是用他们这个demo刷了一副太祖的画像进去想要膜拜一下,由于像素密度不太够,看起来效果不是很好,这个后边没有继续接触了。后边使用了龙宁科技和威锋科技的屏幕,我们之前很多产品也使用了他们俩的段码墨水屏,算是比较熟悉的了。电子日历这个产品,这两家的屏幕都调好了,现在可以通用。

    不论是哪个厂商,其上游技术都是元太科技垄断,厂家再进行二次开发。因此不论是硬件上还是软件上,基本都差不太多。屏幕本身带有一个简单的驱动IC,我们应用开发的话,主要是和这个IC打交道。IC提供了二十多个各种接口供选择,除了电源和GND之外,我们还使用了SPI(由于不需要读取屏幕的数据,少了一根MISO),RST复位、BUSY状态、BS选择SPI类型(这个一般只会采用一个,要么三线要么四线,如果硬件设计上直接拉低应该就不用这个了),CD命令/数据输入。   

    比较曲折的是,一开始技术方案没有考虑好,选择了最难的那种,想要实现根据文本样式和内容生成图像数据这样的复杂方案,研究FreeType怎么渲染文本,折腾了三个多星期,倒是把这玩意儿差不多给搞出来了。不过对于一个裸奔的MCU程序而言,自己渲染的实现过于复杂了。最后采用的简化方案是:直接下载一整副图像或者将小图标的数据进行组合显示出来。

    最开始厂家提供了一个软件,用于帮助从图像生成数据文件。    就是下面这货:

点阵墨水屏的使用以及图像预处理_第1张图片

    按照厂家的说明可以使用这个软件快速提取一副图像的数据,刷入到demo中即可以运行。

    需要注意的是,用于生成数据的图像必须是单色位图,且分辨率必须和屏幕的分辨率严格对应,480*648或者648*480也可以。

    然后开始研究我们自己的应用场景。因为如果每出一张图都要手工使用这个软件生成数据再转换成bin文件放到服务器上,太麻烦了!所以得有一个可以自动根据图片生成h文件或者二进制文件的程序,因为都是我在研究屏幕相关的技术,所以由我自己来写预处理的程序。

    IC的分辨率为648*480,带FPC的那一边为下方。根据datasheet,扫描数据的时候,每个字节8位共可以表示8个像素,从左到右(或反过来,UD参数)逐个扫描直至填满本行所有的像素,扫完一行之后,根据SHL参数可以向上或者向下扫描第二行。

    点阵墨水屏的使用以及图像预处理_第2张图片

    我们这个日历,是竖着的,和屏厂默认的横着有90度的差。所以这里如果不想要美工MM每次都给出横着的日历,我就得在程序中处理这种转换。

点阵墨水屏的使用以及图像预处理_第3张图片

    对于符合屏厂默认方向的数据而言,不论是从左到右从上到下还是反过来,写入一整个屏幕的数据为 每行81个字节*480行,所以如果应用的UI是648*480的分辨率的,提取图像的时候,可以直接按照逐行扫描每8个像素合并一个字节即可。但是如果是像我们这种竖着的,则不太一样。要符合屏幕的扫描逻辑,则应该改为每一列共8行合并一个字节,然后是从左向右从上向下还是反过来根据参数决定。这里我前期研究的时候,选定的方向是更适合理解的从左到右从上到下,不过实际上这不符合Bitmap的扫描方向,Bitmap是从左到右从下往上扫描的,导致处理图像的时候我需要额外翻转一下数据。

    然后Bitmap格式的图像本身也不全是像素。其文件格式为 文件头+信息头+调色板 三部分组成。其中文件头固定为14个字节,信息头为40个字节,然后颜色表的长度根据图片的颜色模式决定:24位或36位真彩色模式无颜色表,黑白单色图的颜色表大小是8字节,16色图像的颜色表大小是64字节,256色图像的颜色表大小是1024字节。每4字节表示一种颜色,并以B(蓝色)、G(绿色)、R(红色)、alpha(像素的透明度值,一般不需要)。即首先4字节表示颜色号0的颜色,接下来表示颜色号1的颜色,依此类推。

    以下定义为使用Visualstudio研究FreeType渲染Bitmap的时候梳理的Bitmap文件格式:

//位图文件头定义:
typedef struct  tagBITMAPFILEHEADER {
	WORD bfType;//位图类别,根据不同的操作系统而不同,在Windows中,此字段的值总为‘BM’

	DWORD bfSize; // 位图文件的大小,以字节为单位(3-6字节)

	WORD bfReserved1; // 位图文件保留字,必须为0(7-8字节)

	WORD bfReserved2; // 位图文件保留字,必须为0(9-10字节)

	DWORD bfOffBits; // 位图数据的起始位置,以相对于位图(11-14字节)
	// 文件头的偏移量表示,以字节为单位
}BITMAPFILEHEADER;  //14字节
//BMP位图信息头数据用于说明位图的尺寸等信息:
typedef struct tagBITMAPINFOHEADER {
	DWORD biSize; // 本结构所占用字节数(15-18字节)

	LONG biWidth; // 位图的宽度,以像素为单位(19-22字节)

	LONG biHeight; // 位图的高度,以像素为单位(23-26字节)

	WORD biPlanes; // 目标设备的级别,必须为1(27-28字节)

	WORD biBitCount;// BMP图像的色深,即一个像素用多少位表示,常见有1、4、8、16、24和32,分别对应单色、16色、256色、16位高彩色、24位真彩色和32位增强型真彩色

	// 4(16色),8(256色)或24(真彩色)之一

	DWORD biCompression; // 压缩方式,0表示不压缩,1表示RLE8压缩,2表示RLE4压缩,3表示每个像素值由指定的掩码决定

	// 1(BI_RLE8压缩类型)或2(BI_RLE4压缩类型)之一

	DWORD biSizeImage; // BMP图像数据大小,必须是4的倍数,图像数据大小不是4的倍数时用0填充补足

	LONG biXPelsPerMeter; // 位图水平分辨率,每米像素数(39-42字节)

	LONG biYPelsPerMeter; // 位图垂直分辨率,每米像素数(43-46字节)

	DWORD biClrUsed;// 	BMP图像使用的颜色,0表示使用全部颜色,对于256色位图来说,此值为100h = 256

	DWORD biClrImportant;// 重要的颜色数,此值为0时所有颜色都重要,对于使用调色板的BMP图像来说,当显卡不能够显示所有颜色时,此值将辅助驱动程序显示颜色
}BITMAPINFOHEADER; //位图信息头定义,40字节
#if (BMP_BIT_COUNT<24)
typedef struct tagRGBQUAD {
	BYTE colors[(2 << (BMP_BIT_COUNT - 1)) * 4];
	//BYTE rgbBlue;// 蓝色的亮度(值范围为0-255)

	//BYTE rgbGreen; // 绿色的亮度(值范围为0-255)

	//BYTE rgbRed; // 红色的亮度(值范围为0-255)

	//BYTE rgbReserved;// 保留,必须为0

} RGBQUAD;
#endif
//BMP整体信息:
typedef struct tagBMP_BUFFER
{
	BITMAPFILEHEADER    hand;
	BITMAPINFOHEADER    info;
	#if (BMP_BIT_COUNT<24)
	RGBQUAD rgbQuad;
	#endif
	BYTE* BUFFER;
}BMP_BUFFER;

   然后根据以上信息来处理一个480*648分辨率的单色图,里边需要注意的一个问题是,对于非4字节对齐的尺寸,Bitmap会对行进行填充。比如15*18分辨率的图像,在数据上看,是有4字节*18行的,每行多余的字节为填充位。屏幕设计上没有这种对齐填充的说法,处理的时候,需要将填充的数据去掉。

   对于单色图的颜色表,只有黑和白两种情况。颜色表也有两种:1表是白色0表示黑色,或者反过来0表示白色1表示白色。这两种情况都存在,我用的Windows10 和设计师的Mac就刚好反过来了。由于屏幕没有颜色表,0和1代表黑还是白依靠一个参数来确定,所以这里需要将不同的颜色表统一生成同样的像素数据。 

   对于屏幕的C语言处理程序,我定义了一个文件头,规定每一幅要显示的图像的数据必须包含这么一个文件头,类似于Bitmap的文件头。这里有个Keil的强制编译对齐的操作,是由于早期设计的文件头长度不足以自动对齐。图像的日期yDays包含年份(从2000年开始)和当年的第几天,由于不能单独使用1个字节表示,这里合并使用两个字节。早期的格式中,没有定义crc,所以后边的python代码中也没有生成crc的相关内容。

#pragma pack(1)
typedef struct{
    uint32_t magic_number;//for quickly check data valid.
    uint16_t data_length;
    uint8_t res_ver;      //version,the last ver is 2 with crc section.
    uint8_t compress_flag;
    uint16_t yDays;
    uint16_t width;
    uint16_t height;
    uint16_t crc;
}res_header;
#pragma pack()

#define GET_RES_YEAR(yDays)    ((((uint16_t)yDays)>>10)&0x3F)
#define GET_RES_DAYS(yDays)    (((uint16_t)yDays)&0x3FF)
#define RES_YEAR_FROM          2000

   然后因为我们还有一些小图标,这些小图标尺寸不固定,所以需要根据大小记录存放到NandFlash中的位置,会生成一个小图标专用的索引。对于占用一整副图的日历而言,地址都是固定的,不需要索引。对于一整副的日历,由于不能自动识别bitmap中这张日历代表哪一天,最好的处理方式是图片的文件名字中包含有那一天的信息,程序读取后自动写入到头文件中,我这里早期由于日历图片不多没有这么处理。

   索引的结构为:    

//4 bytes pack to one word.
typedef struct{
    uint16_t nand_page_num;//page num.
    uint16_t pack;//invalid.
}icon;

  索引,mcu程序根据这个索引在flash中定位小图标的地址:

//save index file in MCU 
const icon res_init_icons[116]={
    
{0x0,0},/** res: beautiful ,data size:38896, pages:19 **/
{0x13,0},/** res: a_mmc_footer_56x16 ,data size:128, pages:1 **/
{0x14,0},/** res: icon_batt ,data size:112, pages:1 **/
{0x15,0},/** res: icon_drunk ,data size:88, pages:1 **/
{0x16,0},/** res: icon_medical ,data size:88, pages:1 **/
{0x17,0},/** res: icon_time_separate ,data size:40, pages:1 **/
{0x18,0},/** res: icon_tomato ,data size:88, pages:1 **/
{0x19,0},/** res: icon_unit_C ,data size:112, pages:1 **/
{0x1a,0},/** res: icon_wifi_1_32x24 ,data size:112, pages:1 **/
{0x1b,0},/** res: icon_wifi_2_32x24 ,data size:112, pages:1 **/
{0x1c,0},/** res: icon_wifi_3_32x24 ,data size:112, pages:1 **/
//...
}

   下边是图像预处理程序,如果是用于生成小图标索引,START_BLOCK的值应该修改为NandFlash存储小图标的起始block。 然后这里我使用的NandFlash一共1024个block,每个block为64个page,每个page为2kB。

    由于对python不是特别的熟悉,图片预处理代码里边对于全局变量的处理不是很规范,后边我把预处理的代码给服务器的同事参考使用的时候,遭到了他们的一致鄙视。回头我找着机会了也会鄙视回去的。

   这里边主要的关键步骤是读取bitmap文件信息,然后删除填充的字节,翻转数据顺序,将横向8字节一扫描改为每一列8行一扫描。

#!/usr/bin/python

import sys
import os
from shutil import copyfile
import glob
import json
from datetime import datetime

BMP_CONFIG = "release_config.json"  #配置图像的保存位置,生成的数据文件的保存位置等

FILE_SIZE_POS = 2  #文件大小
FILE_SIZE_BYTES = 4 # 4bytes
DATA_OFFSET_POS = 0x0a  # 像素数据的开始地址
DATA_OFFSET_BYTES = 4

PX_WIDTH_POS = 0x12  #度的偏移量
PX_HEIGHT_POS = 0x16 #高度的偏移量
BMP_SIZE_BYTES = 4   #宽和高各自4个字节


PX_RGBQUAD_FIRST = 0x36  #颜色表第一个颜色索引的位置
PX_GRBQUAD_SECOND = 0x3a #颜色表第二个颜色索引的位置
PX_RGB_BYTES = 3  # 4 bytes for r.g.b.alpha.

PX_RGBQUAND_THRESHOLD = 0x808080 #用于区分颜色表中1和0哪个在前边

#生成的h文件的格式和注释,前期研究的时候我生成的是h文件直接烧录,后边放服务器上供下载使用的时候要改为生成bin文件
H_FILE_PREFIX_COMMENT1 = "/**--------- File:%s.bmp ------------**/\r\n"
H_FILE_PREFIX_COMMENT2 = "/**--------width X height: %s X %s **/\r\n"
H_FILE_PREFIX_1 = "const unsigned char "
H_FILE_PREFIX_2 = "[]={\r\n"
H_FILE_UINT8_PRE = "(unsigned char) "
H_FILE_SUFFIX = "};\r\n"
H_FILE_FORMAT = ".h"

H_FILE_VALID_FLAG = 1      
H_FILE_COMPRESS_FLAG = 0

H_FILE_RECORD_VALID_FLAG = 0xabcd  
HEADER_BYTES = 14

START_DATE = datetime(1970, 1, 1)


# index file
BLOCK_SIZE = 64
PAGE_SIZE = 2048
START_BLOCK = 5  # block index
START_PAGE = 0  # page index in a block
INDEX_FILE = "___res_data_index.h"
NAME_ADDR_REPLACE = "const icon name_replace[]={\r\n"
INDEX_COMMON_RES_FILE = "/****for img res: %s *****/"
INDEX_ADDR_GROUP = "{%s,0},/** res: %s ,data size:%s, pages:%s **/\r\n"
block_acc = START_BLOCK
page_acc = START_PAGE      # current page in a block
##################

destPath = None          #数据保存的位置
resPath = None           #图像保存的位置
rbgQuandReverse = False  #是否翻转颜色
px_bytes = None
patch_bytes = None       #用于对齐的填充字节数

#移除填充的字节
def remove_filling_invalid_data(datas, px_width, px_height):
    if(px_bytes == patch_bytes):
        return datas

    buffer = []
    for i in range(0, px_height):
        # item = list((datas[i*px_width//8])[0:px_bytes])
        item = datas[i*patch_bytes:i*patch_bytes+px_bytes]
        buffer.extend(item)
    return buffer

#翻转数据的顺序,Bitmap从下往上扫描,改为从上往下扫描
def data_sort_to_epd(datas, px_width, px_height):
    for i in range(px_height-1):
        datas.extend(datas[((px_height-2-i)*px_width // 8)                           :((px_height-1-i)*px_width//8)])

    del datas[0:((px_height-1)*px_width//8)]
    return

#判断var从左往右的第bit位的值是0还是1
# var and (1 left shift bit) ,return 0 or 1.
def bit_x_to_bit01(var, bit):
    return 0 if(var & (1 << (7-bit)) == 0) else 1

#将0或1的颜色值填充到var从左到右的第bit位
# var( 1 or 0) left shift bit.
# 1-var:reverse 0/1.
def bit01_to_bit_x(var, bit):
    global rbgQuandReverse
    return ((1-var) << (7-bit)) if(rbgQuandReverse) else (var << (7-bit))

#每张图像都会生成一个h文件,并且向索引文件添加一行索引
def create_h_file(destPath, fileName, datas, px_width, px_height, file_index):
    print("h file path:"+destPath)
    print("h file name:"+fileName)
    global block_acc
    global page_acc
    days = datetime.now().__sub__(START_DATE).days
    # data length
    data_len = HEADER_BYTES+px_width*px_height//8
    # page nums
    page_nums = data_len//PAGE_SIZE+(0 if(data_len % PAGE_SIZE == 0) else 1)

    if(BLOCK_SIZE-page_acc % (BLOCK_SIZE+1) < page_nums):
        block_acc += 1
        page_acc = 0
    file_index.write(bytes(INDEX_ADDR_GROUP % (hex(
        block_acc*BLOCK_SIZE+page_acc), fileName, str(data_len), str(page_nums)), encoding='utf-8'))
    page_acc += page_nums

    with open(os.path.join(destPath, (fileName+H_FILE_FORMAT)), 'wb') as file_res:
        # header prefix
        file_res.write(bytes(H_FILE_PREFIX_COMMENT1 %
                             fileName, encoding='utf-8'))
        file_res.write(bytes(H_FILE_PREFIX_COMMENT2 %
                             (px_width, px_height), encoding='utf-8'))
        file_res.write(bytes(H_FILE_PREFIX_1, encoding='utf-8'))
        file_res.write(bytes(fileName, encoding='utf-8'))
        file_res.write(bytes(H_FILE_PREFIX_2, encoding='utf-8'))
        
        #后期的应用中,这个字节改为了数据格式的版本,早期的数据文件的文件头中没有crc
        # record_valid_flag
        file_res.write(
            bytes(hex(H_FILE_RECORD_VALID_FLAG & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(
            bytes(hex(H_FILE_RECORD_VALID_FLAG >> 8 & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(
            bytes(hex(H_FILE_RECORD_VALID_FLAG >> 16 & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(
            bytes(hex(H_FILE_RECORD_VALID_FLAG >> 24 & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(b"/*magic number.*/")
        file_res.write(b"\r\n")

        # H_FILE_RECORD_VALID_FLAG
        # data_len
        file_res.write(bytes(hex(data_len & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(
            bytes(hex(data_len >> 8 & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(b"/*data length.*/")
        file_res.write(b"\r\n")

        # valid.
        file_res.write(bytes(hex(H_FILE_VALID_FLAG), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(b"/*res valid flag.*/")
        file_res.write(b"\r\n")

        # compress
        file_res.write(
            bytes(hex(H_FILE_COMPRESS_FLAG), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(b"/*res compress flag.*/")
        file_res.write(b"\r\n")

        # days from 1970
        file_res.write(bytes(hex(days & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(bytes(hex(days >> 8 & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(b"/*res days from 1970.*/")
        file_res.write(b"\r\n")

        # width.

        file_res.write(bytes(hex(px_width & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(
            bytes(hex(px_width >> 8 & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(b"/*res width.*/")
        file_res.write(b"\r\n")
        # height
        file_res.write(bytes(hex(px_height & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(
            bytes(hex(px_height >> 8 & 0xFF), encoding='utf-8'))
        file_res.write(b",")
        file_res.write(b"/*res height.*/")
        file_res.write(b"\r\n")
        file_res.write(b"/*******now is res px data.**********/\r\n")
        col = 0
        row = 0
        lines = 0
        temp = 0
        writed_cnt = 0

        cvt_index = 0

        print("h_px_height:"+str(px_height))

        for i in range(px_height//8):
            for j in range(px_width):
                temp = 0
                for k in range(8):
                    temp |= bit01_to_bit_x(
                        bit_x_to_bit01(datas[(i*8+k)*(px_width//8)+j//8], j % 8), k)

                file_res.write(bytes(hex(temp), encoding='utf-8'))
                file_res.write(b",")
                writed_cnt += 1
                if writed_cnt % 16 == 0:
                    file_res.write(b"\r\n")
        file_res.write(bytes(H_FILE_SUFFIX, encoding='utf-8'))
    return


def auto_release_check():
    global destPath
    global resPath
    if(os.path.exists(BMP_CONFIG)):
        with open(BMP_CONFIG, 'r') as file_read:
            json_str = json.load(file_read)
            destPath = json_str["destPath"]
            resPath = json_str["resPath"]
            print("destPath:"+destPath)
            print("resPath:"+resPath)
            if(os.path.exists(destPath) and os.path.exists(resPath)):
                return True
    return False


def run_convert(resPath, destPath):
    global rbgQuandReverse
    global px_bytes
    global patch_bytes
    fs = os.listdir(resPath)
    with open(os.path.join(destPath, (INDEX_FILE)), 'wb') as file_index:
        for f in fs:
            # header include.
            file_index.write(bytes("#include \"%s.h\"\r\n" %
                                   (f[:-4]), encoding='utf-8'))
        file_index.write(bytes(NAME_ADDR_REPLACE, encoding='utf-8'))
        for f in fs:
            with open(os.path.join(resPath, f), 'rb') as file_read:
                file_read.seek(FILE_SIZE_POS, 0)
                file_size = int.from_bytes(file_read.read(
                    FILE_SIZE_BYTES), byteorder='little', signed=False)
                file_read.seek(DATA_OFFSET_POS, 0)
                data_offset = int.from_bytes(file_read.read(
                    DATA_OFFSET_BYTES), byteorder='little', signed=False)
                file_read.seek(PX_WIDTH_POS, 0)
                px_width = int.from_bytes(file_read.read(
                    BMP_SIZE_BYTES), byteorder='little', signed=False)
                file_read.seek(PX_HEIGHT_POS, 0)
                px_height = int.from_bytes(file_read.read(
                    BMP_SIZE_BYTES), byteorder='little', signed=False)
                file_read.seek(PX_RGBQUAD_FIRST, 0)
                rgbQuadFirst = int.from_bytes(file_read.read(
                    PX_RGB_BYTES), byteorder='little', signed=False)
                file_read.seek(PX_GRBQUAD_SECOND, 0)
                rgbQuadSecond = int.from_bytes(file_read.read(
                    PX_RGB_BYTES), byteorder='little', signed=False)
                px_bytes = px_width//8
                patch_bytes = ((px_bytes+3)//4)*4
                file_read.seek(data_offset)
                resDatas = file_read.read(patch_bytes*px_height)
                print("rgbQuadFirst:"+str(rgbQuadFirst))
                print("rgbQuadSecond:"+str(rgbQuadSecond))
                if(rgbQuadFirst < PX_RGBQUAND_THRESHOLD and rgbQuadSecond > PX_RGBQUAND_THRESHOLD):
                    rbgQuandReverse = True
                else:
                    rbgQuandReverse = False

                midDatas = list(resDatas)
                midDatas = remove_filling_invalid_data(
                    midDatas, px_width, px_height)
                data_sort_to_epd(midDatas, px_width, px_height)
                create_h_file(destPath, f[:-4], midDatas,
                              px_width, px_height, file_index)
        file_index.write(bytes(H_FILE_SUFFIX, encoding='utf-8'))
    return


# start application
if __name__ == "__main__":
    os.system('cls')  # clear screen
    if(auto_release_check()):
        run_convert(resPath, destPath)
    else:
        print("path error!")

 

你可能感兴趣的:(物联网)