點陣墨水屏的使用以及圖像預處理

 

    我們電子日曆的產品,選用的屏幕尺寸爲5.83寸,分辨率爲648*480。屏幕本身支持黑白紅三色,我們使用黑白兩色,單色位圖表示的話,每個位都能表示一個像素點。所以對於這個屏幕而言,要顯示一整幅圖,需要的字節數爲 38880。然後,由於屏幕需要的只是像素點,所以不能直接將一個位圖數據寫進去,需要預先轉換一下。然後屏幕本身對於像素點的處理方式的不同,也會導致圖片的預處理過程不一樣。

    對於這個墨水屏的屏幕而言,非專業人員讀他們這個datasheet難度較大,各種術語和參數意義不是很容易看懂。我開發主要是先按照官方給的示例程序和圖像跑一遍,然後基本跑通了大致的操作也就清楚了,然後開始看datasheet這麼個流程。

    我們前後接觸了三個廠商,一個是大連奇雲電子,這個是純從淘寶上找的一個,他們這個有四灰度的屏幕,我是用他們這個demo刷了一副太祖的畫像進去想要膜拜一下,由於像素密度不太夠,看起來效果不是很好,這個後邊沒有繼續接觸了。後邊使用了龍寧科技和威鋒科技的屏幕,我們之前很多產品也使用了他們倆的段碼墨水屏,算是比較熟悉的了。電子日曆這個產品,這兩家的屏幕都調好了,現在可以通用。

    不論是哪個廠商,其上游技術都是元太科技壟斷,廠家再進行二次開發。因此不論是硬件上還是軟件上,基本都差不太多。屏幕本身帶有一個簡單的驅動IC,我們應用開發的話,主要是和這個IC打交道。IC提供了二十多個各種接口供選擇,除了電源和GND之外,我們還使用了SPI(由於不需要讀取屏幕的數據,少了一根MISO),RST復位、BUSY狀態、BS選擇SPI類型(這個一般只會採用一個,要麼三線要麼四線,如果硬件設計上直接拉低應該就不用這個了),CD命令/數據輸入。   

    比較曲折的是,一開始技術方案沒有考慮好,選擇了最難的那種,想要實現根據文本樣式和內容生成圖像數據這樣的複雜方案,研究FreeType怎麼渲染文本,折騰了三個多星期,倒是把這玩意兒差不多給搞出來了。不過對於一個裸奔的MCU程序而言,自己渲染的實現過於複雜了。最後採用的簡化方案是:直接下載一整副圖像或者將小圖標的數據進行組合顯示出來。

    最開始廠家提供了一個軟件,用於幫助從圖像生成數據文件。    就是下面這貨:

    按照廠家的說明可以使用這個軟件快速提取一副圖像的數據,刷入到demo中即可以運行。

    需要注意的是,用於生成數據的圖像必須是單色位圖,且分辨率必須和屏幕的分辨率嚴格對應,480*648或者648*480也可以。

    然後開始研究我們自己的應用場景。因爲如果每出一張圖都要手工使用這個軟件生成數據再轉換成bin文件放到服務器上,太麻煩了!所以得有一個可以自動根據圖片生成h文件或者二進制文件的程序,因爲都是我在研究屏幕相關的技術,所以由我自己來寫預處理的程序。

    IC的分辨率爲648*480,帶FPC的那一邊爲下方。根據datasheet,掃描數據的時候,每個字節8位共可以表示8個像素,從左到右(或反過來,UD參數)逐個掃描直至填滿本行所有的像素,掃完一行之後,根據SHL參數可以向上或者向下掃描第二行。

    

    我們這個日曆,是豎着的,和屏廠默認的橫着有90度的差。所以這裏如果不想要美工MM每次都給出橫着的日曆,我就得在程序中處理這種轉換。

秒秒測智能日曆

    對於符合屏廠默認方向的數據而言,不論是從左到右從上到下還是反過來,寫入一整個屏幕的數據爲 每行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!")

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章