最近忙于辞职、搬家、打扫卫生、请客吃饭、找工作等杂事,少有机会能够静下心来学习一些东西。趁着投出去的简历还没有动静的这段时间,决定开始着手一早就想做的事情,那就是LCD的编程控制。
本来买的这块板子就带一块128x64的单色点阵LCD,不玩岂不可惜了。于是乎大概重温了一下之前做的东西(其实也就是看看自己的blog),翻出光盘上的资料,一头扎了进去。
功夫不负苦心人,也正好这段时间比较闲(NEET就是爽啊,可惜口袋里的米……),没花几天时间就有点成果了,要不是资料不全以及资料不准确,应该会比现在进展得更快才对。
OK,苦水就吐到这里,下面来看看实在的东西。
首先我是第一次接触LCD的开发,可以说无处下手。应该有很多人都知道S3C44B0X是集成了LCD控制器的,所以我也自然而然的从它看起。很不幸,我看了一整天后七荤八素的,反而更糊涂了。为什么呢?我手上的LCM(LCD模块)的手册(名为SG12864J501C2,但不知为何实物上却印着 HG12864-32A3,我找到HG12864-32A3的资料却又发现它与实物不符,目前来看这个SG12864J501C2和实物还是较为接近的XD)的内容没办法和S3C44B0X文档所述LCD控制器相关内容对应上,完全是莫名其妙。
不过没关系,做菜鸟最重要的一点就是虚心,先看配套光盘的范例程序吧。先在ADS下硬件仿真一遍,确认LCD无故障、范例程序无问题,然后认真RTFSC(Linus名言,Read The Fucking Source Code)。
代码阅毕,首先确定了一个关键性问题。该LCM(LCD模块组)不使用S3C44B0X的LCD控制器,而是采用I/O的方式直接操作。后来我才领悟到 S3C44B0X的LCD控制器是针对LM057QC1T01这样的中高端STN、TFT LCD设计的,SG12864J501C2这种低端产品,是没有办法使用的(硬件接口不同)。
于是问题就简单化了,全部都是I/O的工作了。翻开原理图,有图如下:
看起来很整齐,但其实完全不是那么回事。请看LCM的各引脚说明:
由于我并没有LCD接口板的原理图,所以无法直接得知板子上的引脚最后都是怎么接到LCM上的。于是我一边向卖家索要缺失的原理图(至今仍未拿到……)一边看范例程序来试图猜出我所需要的信息(没资料真是苦啊,感觉自己此刻的身份都不再是一个开发者而是一个黑客orz)。
经过反复的RTFSC和推断我基本确认了:
MPU signal J10-CON20 LCM
GPD0 VD0 pin1 DB0
GPD1 VD1 pin2 DB1
GPD2 VD2 pin3 DB2
GPD3 VD3 pin4 DB3
GPC7 VD4 pin5 RS
GPC6 VD5 pin6 R/W
GPC5 VD6 pin7 E
GPD7 VFRAME pin9 DB7
GPD4 VCLK pin11 DB4
GPD5 VLINE pin12 DB5
GPD6 VM pin14 DB6
nRESET nRESET pin15 nRST
-- VCC pin17 PSB
不要被signal名字所迷惑,那名字是为高端LCM准备的,我们只要关心第一列的名字,重新组织后如下:
MPU J10-CON20 LCM
GPC5 pin7 E
GPC6 pin6 R/W
GPC7 pin5 RS
GPD0 pin1 DB0
GPD1 pin2 DB1
GPD2 pin3 DB2
GPD3 pin4 DB3
GPD4 pin11 DB4
GPD5 pin12 DB5
GPD6 pin14 DB6
GPD7 pin9 DB7
nRESET pin15 nRST
-- pin17 PSB
能看出些名堂了吧?最重要的是PGD0~7对应DB0~7,所以我们只要访问PDATD寄存器就可以从LCM中读写数据了(大概LCM接口板子的设计者就是考虑到这一点才如此接线的)。另外很重要的就是GPC5~7,它们对应着极其重要的3个控制信号E、R/W和RS。它们的具体作用请参见LCM手册。还有就是PSB信号,接VCC暗示着该LCM始终工作在并行模式下。
接下从程序员角度来说说该LCM的特性。
该LCM的可编程接口完全是I/O方式的,不产生任何中断信号。此类设备的驱动程序通常采用查询的方式来处理异步事务。例如忙等待,我们需要不断的问设备芯片是否就绪,如果就绪才继续对设备进行操作,否则继续等待(自旋或睡眠等待)。但如果是中断方式的设备则可以通过设备的中断信号来唤醒,在中断处理程序中陷入设备驱动来继续对设备的操作。
ST7920是这款LCM的核心控制芯片,LCM的可编程接口基本上都是围绕着它的。
ST7920主要有4个寄存器,分别是IR(8bit)、DR (8bit)、AC(7bit)、BF(1bit)。其中IR只写,DR可读写,AC和BF只读。它们复用DB0~7这几根信号线来同外界通讯,而其读写模式的选择信号就是RS及R/W。要注意只有在信号E生效后,指定的传送操作才会执行。E为下降边缘触发。具体请参见LCM手册。
ST7920芯片拥有两套指令集,基本指令集和扩展指令集。分别在两种工作方式下使用。当前芯片处于何种工作方式是由芯片内部状态RE来确定的,0x20指令可以用来切换芯片工作方式。有一点很不幸,我发现似乎没有办法能直接得到芯片的当前工作方式,如果是写驱动程序的话,大概只能用一个静态变量来存它了。
有了这些,可以动手写程序了。这里我采用上一篇文章《简单的S3C44B0X Bootloader》中描述的Bootloader来引导该程序,只要把其中的main.c文件替换掉就行了。
该程序大致流程如下:
1. 设置PGIO。主要是PC5-7需初始化,始终作为输出,作为对RS、R/W、E信号的控制。
2. 图形模式清屏。实际意义是GDRAM清零,刚刚上电的GDRAM的数据是随机的,如果不复位的话在进入图形模式后会看到花屏现象,因此需要在这之前清屏。如果你只用文字模式可以不做这一步。
3. 测试代码。就是在屏幕上胡写胡画,看看是否OK。主要包含了位图输出和文字输出,以及图形模式和文字模式的切换。
main.c
#define PORT(addr) (*(volatile int *)(addr))
#define INTCON PORT(0x1e00000)
#define INTMSK PORT(0x1e0000c)
#define PCONC PORT(0x1d20010)
#define PDATC PORT(0x1d20014)
#define PCOND PORT(0x1d2001c)
#define PDATD PORT(0x1d20020)
/* LCM读写使能 */
#define lcm_enable() do { PDATC |= 0x20; PDATC &= ~0x20; } while (0) /* E为下降边缘触发 */
/* MPU Port D作为输入 */
#define lcm_input() do { PCOND = 0x0000; } while (0) /* PD0~7=input */
/* MPU Port D作为输出 */
#define lcm_output() do { PCOND = 0x5555; } while (0) /* PD0~7=output */
/* 测试用单色位图数据 */
static unsigned short bmp1[] = { 0x2000, 0x5000, 0xa800, 0x7000, 0x5000 };
static unsigned short bmp2[] = { 0x9900, 0x2400, 0x5a00, 0xa500, 0x5a00, 0x2400, 0x9900 };
static unsigned short bmp3[] = { 0x2000, 0x5000, 0x8800, 0x5000, 0x2000 };
static void delay(int times) /* 延时函数 */
{
volatile int i;
for (i = 0; i < (times << 10); ++i);
}
static void lcm_wait(void) /* LCM忙等待函数 */
{
lcm_input();
PDATC &= ~0x80; /* RS=0 */
PDATC |= 0x40; /* R/W=1 */
lcm_enable();
/* 使用自旋方式等待 */
while (PDATD & 0x80) {
delay(1);
lcm_enable();
}
}
static void lcm_cmd(unsigned char cmd) /* 向LCM发送命令 */
{
lcm_output();
PDATC &= ~0xc0; /* RS=0, R/W=0 */
PDATD = cmd;
lcm_enable();
lcm_wait();
}
static unsigned char lcm_read(void) /* 从LCM读出显存数据 */
{
lcm_input();
PDATC |= 0xc0; /* RS=1, R/W=1 */
lcm_enable();
lcm_wait();
return (unsigned char)(PDATD);
}
static void lcm_write(unsigned char data) /* 写显存数据到LCM */
{
lcm_output();
PDATC |= 0x80; /* RS=1 */
PDATC &= ~0x40; /* R/W=0 */
PDATD = data;
lcm_enable();
lcm_wait();
}
static void lcm_tpos(int x, int y) /* 文本输出定位(文字模式坐标系) */
{
unsigned char pos;
y <<= 1;
if (y & 0x04)
y |= 0x1;
pos = ((y & 0x03) << 3) | (x & 0x07);
lcm_cmd(0x80 | pos);
}
static void lcm_gpos(int x, int y) /* 图像输出定位(图像模式坐标系) */
{
lcm_cmd(0x80 | (y & 0x7f));
lcm_cmd(0x80 | (x & 0x0f));
}
static void lcm_text(int clear) /* LCM进入文字模式 */
{
lcm_cmd(0x30); /* 功能设定: 基本指令集,8位模式 */
lcm_cmd(0x0c); /* 显示状态: 整体显示开,光标显示关,光标高亮关 */
lcm_cmd(clear ? 0x01 : 0x02); /* 文字模式复位 */
}
static void lcm_graphic(void) /* LCM进入图形模式 */
{
lcm_cmd(0x32); /* 功能设定: 先确保扩展指令集模式为关闭,再设G位 */
lcm_cmd(0x36); /* 功能设定: 扩展指令集,8位模式,图像模式开 */
}
static void lcm_print(int x, int y, const char* s) /* LCM文字模式下在指定位置输出字符串 */
{
lcm_tpos(x, y);
for (; *s; s++)
lcm_write(*s);
}
static void lcm_clear(void) /* LCM图形模式清屏 */
{
int i, j;
for (j = 0; j < 64; j++) {
lcm_gpos(0, j);
for (i = 0; i < 32; i++)
lcm_write(0); /* 连续写零清屏 */
}
}
static void lcm_draw(int x, int y, int w, int h, const unsigned short *p) /* LCM图形模式下在指定坐标输出位图图像 */
{
int i, j;
for (; h > 0; h--, y++) {
j = w;
lcm_gpos(x, y);
for (i = x; j > 0; j--, i++, p++) {
lcm_write(*p >> 8);
lcm_write(*p);
}
}
}
static void lcm_init(void) /* LCM相关初始化 */
{
PCONC = 0xaaa555aa; /* PC4~9=output */
lcm_cmd(0x34); /* 功能设定: 扩展指令集,8位模式,图像模式关 */
lcm_clear(); /* 图形模式清屏 */
}
static void init(void)
{
INTCON = 0x7; /* 非向量模式,禁止IRQ,禁止FIQ */
INTMSK = 0x07ffffff; /* 屏遮所有中断 */
}
void entry(void)
{
init();
lcm_init(); /* 初始化LCM */
lcm_graphic(); /* 进入图形模式 */
lcm_draw(0, 0, 1, 5, bmp1); /* 绘制位图1 */
lcm_draw(1, 3, 1, 7, bmp2); /* 绘制位图2 */
delay(10000);
lcm_text(0); /* 进入文字模式,不清屏 */
lcm_print(3, 0, "LCD!");
lcm_print(2, 1, "axx1611");
lcm_print(1, 2, "SG12864J5");
lcm_print(0, 3, "ARM7-S3C44B0X");
delay(10000);
lcm_graphic(); /* 返回图形模式 */
lcm_draw(0, 16, 1, 5, bmp3); /* 绘制位图3 */
for (;;);
}
呼,以上代码是没问题的,测试结果如图:
最后要注意lcm_tpos这个函数,该函数所写的内容和手册的描述是不同的。这里就是我前面所说的资料不准确的地方,这段代码是我自己试出来的,在此诅咒那个写手册的人,让我浪费了不少时间。
首先这段:
y <<= 1;
if (y & 0x04)
y |= 0x1;
可能会比较费解,但你仔细看其实就是交换y的第0位和第1位。为什么要这么做?如果按照手册上所说的话,文字模式一共4行,y = 0就是第1行,y = 1是第2行,y = 2是第3行,y = 3是第4行。可实际上根本不是那么回事,经过本人试验,y = 0和y = 3没错,y = 1和y = 2的情况是反的。所以我通过交换第0位和第1位来解决这个问题。
然后:
pos = ((y & 0x03) << 3) | (x & 0x07);
也和手册描述不一致。按照手册的说法应该是:
pos = ((y & 0x03) << 4) | (x & 0x0f);
即y应该是第4~5位,可实际上却是第3~4位;x的值范围应该为0~15,可实际上却是0~7。我本以为x丢掉的那一位被移到了第5位上,但是试验后发现第5为必须为0,1的话什么都显示不出来……
虽然很曲折,但总算是把这款LCD给拿下了,鼓掌……
下一步的打算就是给这个LCD写驱动在uCLinux上跑了,我也终于有机会跳出单片机开发的范畴走向真正的嵌入式开发了。敬请关注,同时祝愿自己早日找到满意的工作……