我们电子日历的产品,选用的屏幕尺寸为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!")