硬件平台:友善之臂 Tiny4412
软件平台:Ubuntu16.04
源码位置:https://github.com/lian494362816/Tiny4412/tree/master/SourceCode/Driver/003_spi
本文的重点放在如何使用字库上,因此不过多的介绍OLED模块
OLED 连接到Tin4412 的SPI接口,
D/C 连接 GPB_5,当D/C 为高电平时传输的是数据,为低电平时传输的是命令
RESET 连接 GPB_4, 当RESET 为低电平时OLED复位, 为高电平取消复位状态
对OLED来说,使用的为4-wire SPI
OLED 总共有128x64 个像素点, 内部按Page 和 Seg 来区别, 总共有8个Page, 每个Page 有128个Seg。相当于分成8行、128列, 每行占8个像素点, 每列占1个像素点
地址的增长总共有3种模式:page addressing mode, horizontal addressingmode and vertical addressing mode
这里是介绍第1种page addressing mode。
page addressing mode: 从某个Page Seg的低地址到高地址增长, 到了最高地址后Page不变, Seg又变为低地址。
注意前面说的,一个Page 占8个像素点,因此一个byte的数据刚好可以写完一个Seg
当设置完了起始Page和Seg后,继续写数据Seg会自动增加
因为OLED模块不带字库,因此只能通过描点的方式来显示中文和英文,英文字母网上已经有一大堆16X16的字库,而中文的则没有,因此要在OLED上显示中文需要使用特定的字库或者自己使用取模软件。
要显示中文可以使用16X16的像素点来表示,当然是可以显示一些简单的文字,太复杂的文字还是显示不了的
根据前面对OLED模块的介绍,显示16X16需要使用到2个Page, 16个Seg。 16X16 bit = 2x16Bytes, 因此可以把数据分成2排,
每排占16个Bytes, 数据的排列方式如下
字库的介绍不多说,只介绍一句HZK16字库是符合GB2312国家标准的16×16点阵字库, 16x16是重点
字库的详细介绍:https://www.cdsy.xyz/electronic/ped/240126/cd60642.html
在linux下使用的是UTF-8编码, 一个中文字符由个3Byte表示,而HZK16字库是GB2312表示,一个中文字符有2个Byte组成。因此就需要把UTF-8转化为GB2312
注: (代码实际用的GBK, GBK向下兼容GB2312)
UTF8转成GBK使用的是iconv库, 虚拟机中本身就有这个库和头文件,因此可以直接使用里面的函数。
/*
need include <iconv.h> <string.h>
convert UTF-8 to GBK (GBK is compatible GB2313)
input_buf:UTF-8中文字符串,一个中文字符占3个字节
output_buf:将中文字符串转化成GBK编码后的输出, 一个中文字符占2个字节
*/
int UTF8_to_GBK(unsigned char *input_buf, long input_len, unsigned char *output_buf, long output_len)
{
char *encTo = "GBK//IGNORE";
char *encFrom = "UTF-8";
char *srcstart = input_buf;
char *tempoutbuf = output_buf;
int i = 0;
long ret = 0;
iconv_t fd = iconv_open (encTo, encFrom);
if (fd == (iconv_t)-1)
{
printf ("iconv_open fail \n");
return -1;
}
memset(output_buf, 0, output_len);
ret = iconv (fd, &srcstart, &input_len, &tempoutbuf, &output_len);
if (ret == -1)
{
printf ("iconv fail \n");
}
iconv_close (fd);
return 0;
}
通过3.1 已经可以获取GKB编码的中文字符了, 接下来就是在字库HZK16中找到对应的32 Byte数据。(16x16 bit= 32byte)
原理很简单, 打开HZK16文件, 通过2字节的GBK数据算出中文字符对应位置,然后读到某个buff中,再把buff返回来即可。
计算位置的原理请看:https://www.cdsy.xyz/electronic/ped/240126/cd60642.html
/*
one_chinese_buf:input, only one chinese-character(UTF-8), 3 bytes
hex_buf:output, HZK16 code, 32 bytes
can use print_HZK16_string to print hex_buf result
*/
int Get_GBK_Code(unsigned char *one_chinese_buf, unsigned char *hex_buf)
{
FILE* fphzk = NULL;
unsigned char tmp_buf[10];
int offset;
//这里把前面的UTF8_to_GBK 函数也封装进来了,因此可以直接输出UTF8的中文字符
//one chinese-character(UTF-8) is 3 bytes
UTF8_to_GBK(one_chinese_buf, 3, tmp_buf, 10);
fphzk = fopen("HZK16", "rb");
if(fphzk == NULL)
{
fprintf(stderr, "error hzk16\n");
return 1;
}
offset = (94*(unsigned int)(tmp_buf[0]-0xa0-1)+(tmp_buf[1]-0xa0-1))*32;
fseek(fphzk, offset, SEEK_SET);
fread(hex_buf, 1, 32, fphzk);
//print_HZK16_string(hex_buf, 32);
fclose(fphzk);
return 0;
}
3.1 和 3.2 已经获取了 GBK编码对应的32Byte数据, 接下来需要验证,看数据是否正确。
因此封装了print_HZK16_string 来打印获取到的数据。这里数据的打印顺序与前面说的OLED的排列顺序不一样。OLED的叫列行式, print_HZK16_string的叫逐行式。
/*
buf: HZK16 buf,
len: buf len,should be multiple of 32
*/
int print_HZK16_string(unsigned char *buf, int len)
{
int i, j, k, n, z;
int flag;
unsigned char key[8] = {0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01};
if (len < 32 || 0 != (len % 32))
{
printf("len is not multiple of 32 \n");
return -1;
}
n = 0;
do{
for(k=0; k < 16; k++)
{
for(j=0; j < 2; j++)
{
for(i=0; i < 8; i++)
{
flag = buf[k*2+j]&key[i];
printf("%s", flag?"●":"○");
}
}
printf("\n");
}
for (i = 0; i <32; i++)
{
printf("0x%-2x ", buf[i]);
}
n++;
buf += 32;
printf(" \n");
printf(" \n");
}while(n < len / 32);
return 0;
}
测试代码:https://github.com/lian494362816/C/tree/master/TEST/2019_05
直接编译即可运行
因为HZK16库中的数据是按逐行式排列的,而OLED需要使用行列式的数据排列,因此需要把逐行式的数据转换为行列式的数据。
原来的数据排列是逐行式,可以看做是16x16的矩阵, 现在要从这个矩阵里面去提取数据。按照从上往下,从左往右的顺序来获取每一个bit, 再组成一个byte.
先获取第一个byte的数据, 由Byte0的第7位, Byte2的第7位, Byte4的第7位, Byte6的第7位,Byte8的第7位,Byte10的第7位,Byte12的第7位,Byte14的第7位。
再获取第二个byte的数据, 由Byte0的第6位, Byte2的第6位, Byte4的第6位, Byte6的第6位,Byte8的第6位,Byte10的第6位,Byte12的第6位,Byte14的第6位。
依次类推
int change_arrangement_mode(unsigned char *pc_buf, unsigned char *oled_buf, int len)
{
int i, j, k;
if (len % 32)
{
printf("len is not multiple of 32 \n");
return -1;
}
k = 0;
do {
for (i = 0; i < 8; i++)
{
for (j = 0; j < 8; j++)
{
oled_buf[i] |= ( ((pc_buf[j * 2] & (0x1 << 7 - (i % 8))) >> (7 - (i % 8)) ) << j );
oled_buf[i + 8] |= ( ((pc_buf[j * 2 + 1] & (0x1 << 7 - (i % 8))) >> (7 - (i % 8)) ) << j );
oled_buf[i + 16] |= ( ((pc_buf[j * 2 + 16] & (0x1 << 7 - (i % 8))) >> (7 - (i % 8)) ) << j );
oled_buf[i + 24] |= ( ((pc_buf[j * 2 + 1 + 16] & (0x1 << 7 - (i % 8))) >> (7 - (i % 8)) ) << j );
//printf("buf[%d] bit[%d] = byte[%d] & %0x << %d \n", i, j, j * 2, (0x1 << 7 - (i % 8)), j);
}
}
k += 32;
pc_buf += 32;
oled_buf += 32;
}while(k < len);
pc_buf -= k;
oled_buf -= k;
return 0;
}
当我把所有程序在PC上调试过后,通过交叉编,放到开发板上执行时,程序报错。
提示我的参数有问题,不过我再虚拟机上测试过是ok的,然后把encTo 和 encFrom 各种修改,发现程序不支持GBK这个参数,
如果不转化成GBK, 后面的程序都无法执行, 真是恼火。
细想一下,虚拟机支持,但是交叉编译后不支持,应该是交叉编译器自带的库的问题。于是我找到了libiconv的网站,下载了libiconv的源码。
http://www.gnu.org/software/libiconv/#downloading
将源码通过交叉编译器编译成了静态库(我的交叉编译器无法编译动态库),最后使用静态库解决了代码运行的错误。
上层的测试函数,中间有驱动层的代码,这里不做介绍
本来使用OLED模块我只是想测试一下SPI总线的,可是OLED模块不带字库,无法显示中文,让我写测试很不爽。
只能显示英文字符,这不符合国情。随后便想到以前使用LCD12864模块时,模块是自带文字库的,因此想通过动态查找字库来在OLED模块上显示中文,继而找到了 HZK16字库。然后linux 的字符编码为UTF-8, 因此需要转换成GBK(GB2313), 结果交叉编译器自带的库又不支持GBK的转换,被迫自己去找libiconv的源码,自己编译静态库。最后HZK16字模的排列方式与OLED所需的不一样,因此又要转换排列方式,最后才成功的显示出中文。
过程如下:
1)点亮OLED
2)为显示中文,找到HZK16字库
3)linux使用UTF-8编码, 需转换成GBK(GB2313) 编码
4)交叉编译器自带的库无法正常运行,网上找到libiconv的源码, 手动编译静态库
5)HZK16字模排列顺序与OLED所需的不一样, 写代码转换。
本来只是想显示中文字符,没想到要经过这么多步骤,特写此文给后面的人,让后人少走点弯路。