5. 移植通用版LCD 驱动程序到另一颗MCU
将通用版的LCD 驱动程序移植到另外的MCU 上并不复杂,而需要做的工作也很少,在前面介绍驱动程序代码时已经介绍过了,基本上只需要修改驱动当中与MCU 相关的代码,主要就是LCD_Driver_User.c 和驱动的端口配置文件LCD_PortConfig.h 文件。本章将以凌阳公司的SPCE061A 单片机作为移植的对像,下面一步一步分析。
5.1. 修改驱动中的底层代码
与MCS51 系列MCU 不同,凌阳公司的SPCE061A 属于流行的SOC 概念的MCU,特征之一就是将众多以前放置在MCU 外部的硬件资源集成到MCU 当中,而且不将部线拉出MCU外部,不过这些都与本驱动程序没太多关系,感兴趣的读者可以在网络上查找相关的资料参考,不作过多介绍。
5.1.1. 修改LCD_PortConfig.h 的端口配置
SPCE061A 是不支持端口的位操作指令的,所以对于端口的操作只能利用对端口的读—改—写方式进行,即读回当前端口的输出状态,然后根据需要改变输出状态的端口来修改读回的值,然后再将改后的值输出至端口。
以下便是修改后的端口配置头文件:
//note:如果您使用C 语言编写LCD 的底层接口程序的话,这里的定义才会有用
// this file for MCU I/O port or the orther`s hardware config
// for LCD Display
//Define the MCU Register
#define P_IOA_Data (volatile unsigned int *)0x7000
#define P_IOA_Buffer (volatile unsigned int *)0x7001
#define P_IOA_Dir (volatile unsigned int *)0x7002
#define P_IOA_Attrib (volatile unsigned int *)0x7003
#define P_IOA_Latch (volatile unsigned int *)0x7004
#define P_IOB_Data (volatile unsigned int *)0x7005
#define P_IOB_Buffer (volatile unsigned int *)0x7006
#define P_IOB_Dir (volatile unsigned int *)0x7007
#define P_IOB_Attrib (volatile unsigned int *)0x7008
// Define for the port use by LCD Driver
#define LCD_EP 0x1000
#define LCD_RW 0x0800
#define LCD_A0 0x0400
#define LCD_RE 0x0200
#define LCD_CS 0x0100
#define LCD_CMD_Dir P_IOA_Dir
#define LCD_CMD_Attrib P_IOA_Attrib
#define LCD_CMD_Buffer P_IOA_Buffer
#define LCD_Data_BUS_Out P_IOA_Buffer
#define LCD_Data_BUS_In P_IOA_Data
#define LCD_Data_BUS_Dir P_IOA_Dir
#define LCD_Data_BUS_Attrib P_IOA_Attrib
#define LCD_Data_BUS_Byte 0//1
在代码当中,对SPCE061A 的端口的寄存器进行了定义,其实是与凌阳公司提供的头文件里是一样的,只不过这里将其单独提出来,免得再包含头文件了。
接着定义了一些常量,这些定义会在程序当中使用,在操作LCD 的控制端口时使用;可以从定义中看出LCD 的控制端口到底接在MCU 的哪个端口上;而LCD_CMD_Dir、LCD_CMD_Attrib 以及LCD_CMD_Buffer 对接在LCD 上的控制端口进行了重定义。
以上可以看出,LCD 的控制端口:CS 片选、Reset 复位端口、A0 命令数据选择端口、RW读写控制位和EP 端口分别接在了SPCE061A 的IOA8、9、10、11、12 之上。
然后又重定义了LCD_Data_BUS_Out 等四个用于LCD 数据端口的寄存器;最后还定义了一个宏:LCD_Data_BUS_Byte,该定义的值为0 或者1,分别表示LCD 的8 个数据端口接在SPCE061A 的端口中的低八位或者是高八位(SPCE061A 的端口每一组为16 个,与其CPU的字长是一样的对应);代码中的定义表示,LCD 的8 个数据端口接在SPCE061A 的IOA0~7端口之上。
5.1.2. 修改底层驱动功能函数
LCD_Driver_User.c 当中定义有LCD 的底层驱动功能函数,主要是写数据、读数据和写寄存器函数,当然也有一些函数可能被修改,下面来看看修改的代码:
LCD_DataWrite 函数:
//========================================================================
// 函数: void LCD_DataWrite(unsigned int Data)
// 描述: 写一个字节的显示数据至LCD 中的显示缓冲RAM 当中
// 参数: Data 写入的数据
// 返回: 无
// 版本:
// 2007/01/09 First version
//========================================================================
void LCD_DataWrite(unsigned int Data)
{
unsigned int uiTemp=0;
uiTemp = *LCD_CMD_Buffer;
uiTemp &= ~(LCD_EP+LCD_CS+LCD_RW); //EP CS RW to Low
uiTemp |= LCD_A0; //AO Hight
*LCD_CMD_Buffer = uiTemp;
#if LCD_Data_BUS_Byte==1
*LCD_Data_BUS_Out = (*LCD_Data_BUS_Out&0x00ff)|(Data<<8);
#else
*LCD_Data_BUS_Out = (*LCD_Data_BUS_Out&0xff00)|(Data&0x00ff);
#endif
uiTemp = *LCD_CMD_Buffer;
uiTemp |= LCD_EP; //EP to Hight
*LCD_CMD_Buffer = uiTemp;
uiTemp |= LCD_CS; //CS to Hight
*LCD_CMD_Buffer = uiTemp;
}
其实还是照着时序来写程序,只是换了种端口的操作方式罢了;先从LCD_CMD_Buffer(重定义为P_IOA_Buffer 寄存器)当中读出当前MCU 端口的输出状态,然后以读回的数据进行与、或操作,这样就可以置相对应的控制端口的位所想要的输出状态了,最后再将修改后的数据送至端口输出。
宏“#if LCD_Data_BUS_Byte==1”以及其以下的两个条件宏定义,都是在LCD_PortConfig.h当中定义的那个LCD_Data_BUS_Byte 的值来进行选择性的编译;代码中可看出,当LCD_Data_BUS_Byte 定义为1 时, 将编译代码“ *LCD_Data_BUS_Out =(*LCD_Data_BUS_Out&0x00ff)|(Data<<8);”,可知LCD 的数据端口是定义在高八位的;为0时则编译另外一条代码。
接下来的两个基本的接口函数也比较类似,如下:
//========================================================================
// 函数: unsigned int LCD_DataRead(void)
// 描述: 从LCD 中的显示缓冲RAM 当中读一个字节的显示数据
// 参数: 无
// 返回: 读出的数据,
// 版本:
// 2007/01/09 First version
// 注意:
//========================================================================
unsigned int LCD_DataRead(void)
{
unsigned int uiTemp=0;
unsigned int Bus_Dir=0;
unsigned int Read_Dat=0;
Bus_Dir = *LCD_Data_BUS_Dir; //设置数据口为输入
#if LCD_Data_BUS_Byte==1
*LCD_Data_BUS_Dir = Bus_Dir&0x00ff;
#else
*LCD_Data_BUS_Dir = Bus_Dir&0xff00;
#endif
uiTemp = *LCD_CMD_Buffer;
uiTemp &= ~(LCD_EP+LCD_CS); //EP CS to Low
uiTemp |= (LCD_A0+LCD_RW); //AO RW Hight
*LCD_CMD_Buffer = uiTemp;
uiTemp |= LCD_EP; //EP to Hight
*LCD_CMD_Buffer = uiTemp;
uiTemp &= (~LCD_EP);
*LCD_CMD_Buffer = uiTemp;
Read_Dat = *LCD_Data_BUS_In;
uiTemp |= LCD_CS; //CS to Hight
*LCD_CMD_Buffer = uiTemp;
*LCD_Data_BUS_Dir = Bus_Dir;
#if LCD_Data_BUS_Byte==1
Read_Dat = Read_Dat>>8;
#else
Read_Dat = Read_Dat&0x00ff;
#endif
return Read_Dat;
}
//========================================================================
// 函数: void LCD_RegWrite(unsigned int Command)
// 描述: 写一个字节的数据至LCD 中的控制寄存器当中
// 参数: Command 写入的数据,低八位有效(byte)
// 返回: 无
// 版本:
// 2007/01/09 First version
//========================================================================
void LCD_RegWrite(unsigned int Command)
{
unsigned int uiTemp=0;
uiTemp = *LCD_CMD_Buffer;
uiTemp &= ~(LCD_EP+LCD_CS+LCD_RW+LCD_A0); //EP CS RW A0 to Low
*LCD_CMD_Buffer = uiTemp;
#if LCD_Data_BUS_Byte==1
*LCD_Data_BUS_Out = (*LCD_Data_BUS_Out&0x00ff)|(Command<<8);
#else
*LCD_Data_BUS_Out = (*LCD_Data_BUS_Out&0xff00)|(Command&0x00ff);
#endif
uiTemp = *LCD_CMD_Buffer;
uiTemp |= LCD_EP; //EP to Hight
*LCD_CMD_Buffer = uiTemp;
uiTemp |= LCD_CS; //CS to Hight
*LCD_CMD_Buffer = uiTemp;
}
在上面的代码中,有一点要注意一下,SPCE061A 单片机的最小的数据存储单元是16 位长的“word”,而在C 语言程序当中如果定义有char 类型的数据,实际上也是占用16 位长的,在SPCE061A 的C 语言里,char 与int 没有什么区别。
另外,SPCE061A 的端口必需要使用之前对其进行初始化配置,也就是要定义好它的端口是输入还是输出之类的;为此,可以在LCD 的初始化函数当中对这些端口进行初始化设置,下面定义了一个用于端口初始化的函数如下:
//========================================================================
// 函数: void LCD_PortInit(void)
// 描述: 与LCD 连接的端口初始化代码
// 参数: 无
// 返回: 无
// 版本:
// 2007/01/09 First version
// 注意:
//========================================================================
void TimeDelay(int Time);
void LCD_PortInit(void)
{
#if LCD_Data_BUS_Byte==1
*LCD_Data_BUS_Dir = *LCD_Data_BUS_Dir|0xff00;
*LCD_Data_BUS_Attrib = *LCD_Data_BUS_Attrib|0xff00;
*LCD_Data_BUS_Out = *LCD_Data_BUS_Out|0xff00;
#else
*LCD_Data_BUS_Dir = *LCD_Data_BUS_Dir|0x00ff;
*LCD_Data_BUS_Attrib = *LCD_Data_BUS_Attrib|0x00ff;
*LCD_Data_BUS_Out = *LCD_Data_BUS_Out|0x00ff;
#endif
*LCD_CMD_Dir |= (LCD_EP+LCD_A0+LCD_RW+LCD_CS+LCD_RE);
*LCD_CMD_Attrib |= (LCD_EP+LCD_A0+LCD_RW+LCD_CS+LCD_RE);
*LCD_CMD_Buffer |= (LCD_EP+LCD_A0+LCD_RW+LCD_CS+LCD_RE);
*LCD_CMD_Buffer &= (~LCD_RE);
TimeDelay(200);
*LCD_CMD_Buffer |= LCD_RE;
TimeDelay(50);
}
初始化代码中将SPCE061A 的IOA 低八位定义为输出口,并初始其输出都为1;而控制端口IOA8、9、10、11、12 都定义为输出口,并初始化其输出为高;最后在接LCD 复位端口的LCD_RE 端口上输出一个低电平的脉冲,使LCD 模块进行复位。
而在LCD_Init 函数上也稍稍作一下修改,也就是调用一下刚才介绍的端口初始化函数而已:
void LCD_Init(void)
{
//LCD 驱动所使用到的端口的初始化(如果有必要的话)
LCD_PortInit();
LCD_RegWrite(M_LCD_ON); //LCD On
LCD_RegWrite(M_LCD_POWER_ALL); //设置上电控制模式
LCD_RegWrite(M_LCD_ELE_VOL); //电量设置模式(显示亮度)
LCD_RegWrite(0x1f); //指令数据0x0000~0x003f
LCD_RegWrite(M_LCD_VDD_SET); //V5内部电压调节电阻设置
LCD_RegWrite(M_LCD_VDD);
……
5.2. 与编译器相关的修改
SPCE061A 使用凌阳提供的unSP IDE 编译环境,在常量定义方面与Keil C51 的常量定义有些不一样,需要对代码中的一些涉及常量定义的地方修改一下,也就是将“code unsigned char”前面的“code”改成“const”即可。如在LCD_ASCII.c 中西文字库的定义改成:
const unsigned char Asii0610[] =
{
//-- MS Gothic8; 此字体下对应的点阵为:宽x 高=6x10 --
//-- 宽度不是8 的倍数,现调整为:宽度x 高度=8x10 --
0x00,0x00,0x00,0x00,0x00,0x00,0x00,……
至此,基本上代码的移植算是完成了。
不过,有一点还是建议去改的,SPCE061A 在定义char 型数据时,实际占用的是16 位长的“word”,而在LCD 驱动当中的字库都定义为8 位长度的单元,相当浪费存储空间。
所以,要借助一些工具修改一下驱动中定义的常量,将两个8 位长度的数据装在一个16 位的“word”当中。(可以利用Ultra Edit 工具的纵向选择功能,对字库中的数据进行修改)
修改后的字库如下(片断):
const unsigned char Asii0610[] =
{
/*-- MS Gothic8; 此字体下对应的点阵为:宽x 高=6x10 --*/
/*-- 宽度不是8 的倍数,现调整为:宽度x 高度=8x10 --*/
0x0000,0x0000,0x0000,0x0000,0x0000,0x0040,0x4040,0x4040,0x0060,0x0000,
0xA0A0,0xA000,0x0000,0x0000,0x0000,0x0028,0x28FC,0x50FC,0x5050,0x0000,
0x2070,0xA8A0,0x7028,0xA870,0x2000,0x00C8,0xD0D0,0x2058,0x5898,0x0000,
0x0020,0x5020,0x6098,0x9068,0x0000,……
这样修改之后,实际上字库当中每个字符的字模数据所占用的存储单元数量已经发生了变化,所以也需要对LCD_Dis.c 当中的FonSet 函数进行修改,如下:
void FontSet(unsigned char Font_NUM,unsigned char Color)
{
switch(Font_NUM)
{
case 1: Font_Wrod = 5;//10; //ASII 字符B
X_Witch = 6;
Y_Witch = 10;
Char_Color = Color;
Char_TAB = (unsigned char *)(Asii0610 - (32*5));//10));
break;
case 2: Font_Wrod = 24;//48; //汉字A
X_Witch = 17;
Y_Witch = 16;
Char_Color = Color;
Char_TAB = (unsigned char *)GB1716;
break;
default: break;
}
}
然后,在PutChar 函数当中作一些修改,以便于程序在16 位长度的数据当中提取出8 位长度的byte 数据,如下:
void PutChar(unsigned char x,unsigned char y,char a)
{
unsigned char i,j; //数据暂存
unsigned char *p_data;
unsigned char Temp;
unsigned char Index = 0;
p_data = Char_TAB + a*Font_Wrod; //要写字符的首地址
j = 0;
while((j ++) < Y_Witch)
{
if(y > Dis_Y_MAX) break;
i = 0;
while(i < X_Witch)
{
if((i&0x07)==0)
{
Temp = *(p_data + (Index>>1));
if((Index&0x01)==0)Temp = Temp>>8;
// Temp = *(p_data+Index);
Index++;
}
if((Temp & 0x80) > 0) Write_Dot_LCD/*Writ_Dot*/(x+i,y,Char_Color);
Temp = Temp << 1;
if((x+i) >= Dis_X_MAX)
{
Index += (X_Witch-i)>>3;
break;
}
i++;
}
y ++;
}
}
5.3. 液晶代码移植小结
将驱动程序移植至另外一块LCD 其实也很简单,一般只需要修改几个函数,都是在LCD_Driver_User.c 文件当中,原理上,也就是按照新LCD 的接口时序要求,重写LCD_DataWrite、LCD_RegWrite 和LCD_DataRead 函数;然后根据新的LCD 的内部控制寄存器的特点,重写相关的函数,如LCD_Init、Write_Dot_LCD、LCD_Fill 函数等。
当然,LCD 的配置文件LCD_Config.h 也是需要作一定的改动的,如果必要的话。
除此之外,如果新的LCD 在控制方式上有较大不同的话,也可以在底层适当增加一些函数,以便能够更快速的操作LCD,原则上是尽可能利用LCD 的特性。
在此,就不再详细介绍别的LCD 应用本书所述的通用版驱动程序的情况了,希望读者理解的是程序的设计思想和架构,有兴趣的朋友可以参考网站上提供的其它代码,有疑问时可以随时联系,谢谢!
当然,本书至此,已经与原来规划的目录有所出入了,其实是在写的过程当中作了一些调整,可能还会再写一本,将LCD 的驱动进行进阶的分析,界时将LCD 提升至彩色,并将原来规划的目录的内容移置下一本书当中,如图像显示的内容等。
谢谢观赏
写完了,挺累的~~~~~