前几天收到了8962的开发板,按照个人习惯先在上面跑了一些里面的演示程序,谁知这一跑就跑了三天。知道是哪个程序有这么大的吸引力吗?就是那个Hello程序。在OLED显示器上显示“Hello world!”的程序。于是我就决定从它入手来学习8962。
这个Hello主程序很简单,如下:
int
main(void)
{
//
// Set the clocking to run directly from the crystal.
//
SysCtlClockSet(SYSCTL_SYSDIV_1 | SYSCTL_USE_OSC | SYSCTL_OSC_MAIN |
SYSCTL_XTAL_8MHZ);
//
// Initialize the OLED display.
//
RIT128x96x4Init(1000000);
//
// Hello!
//
RIT128x96x4StringDraw("Hello World!", 30, 24, 15);
//
// Finished.
//
while(1)
{
}
}
这个程序只有四行有效语句,设置时钟、初始化OLED显示器、显示文字、再给他个死循环跑去。这有什么可学习的吗?别急,在RIT128x96x4Init上点击右键,选择:Go To Definition Of ‘RIT128x96x4Init’,看看,现在我们就有好多事情可做了。
当然,有的朋友可能会问,为什么不在 ‘SysCtlClockSet’上点右键,该不会是老太太挑柿子,拣软的捏吧。呵呵,非常遗憾的告诉你,我喜欢吃柿饼。所以我已经把 ‘SysCtlClockSet’这个硬柿子给做成了柿饼。制作过程参见:https://bbs.eeworld.com.cn/thread-238076-1-5.html
好了,我们现在看看这个RIT128x96x4Init中有些什么东东吧。
void
RIT128x96x4Init(unsigned long ulFrequency)
{
unsigned long ulIdx;
//
// Enable the SSI0 and GPIO port blocks as they are needed by this driver.
//
SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0);
SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA);
SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIO_OLEDDC);
//
// Configure the SSI0CLK and SSIOTX pins for SSI operation.
//
GPIOPinTypeSSI(GPIO_PORTA_BASE, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_5);
GPIOPadConfigSet(GPIO_PORTA_BASE, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_5,
GPIO_STRENGTH_8MA, GPIO_PIN_TYPE_STD_WPU);
//
// Configure the GPIO port pin used as a D/Cn signal for OLED device,
// and the port pin used to enable power to the OLED panel.
//
GPIOPinTypeGPIOOutput(GPIO_OLEDDC_BASE, GPIO_OLEDDC_PIN | GPIO_OLEDEN_PIN);
GPIOPadConfigSet(GPIO_OLEDDC_BASE, GPIO_OLEDDC_PIN | GPIO_OLEDEN_PIN,
GPIO_STRENGTH_8MA, GPIO_PIN_TYPE_STD);
GPIOPinWrite(GPIO_OLEDDC_BASE, GPIO_OLEDDC_PIN | GPIO_OLEDEN_PIN,
GPIO_OLEDDC_PIN | GPIO_OLEDEN_PIN);
//
// Configure and enable the SSI0 port for master mode.
//
RIT128x96x4Enable(ulFrequency);
//
// Clear the frame buffer.
//
RIT128x96x4Clear();
//
// Initialize the SSD1329 controller. Loop through the initialization
// sequence array, sending each command "string" to the controller.
//
for(ulIdx = 0; ulIdx < sizeof(g_pucRIT128x96x4Init);
ulIdx += g_pucRIT128x96x4Init[ulIdx] + 1)
{
//
// Send this command.
//
RITWriteCommand(g_pucRIT128x96x4Init + ulIdx + 1,
g_pucRIT128x96x4Init[ulIdx] - 1);
}
}
这就是这个函数的完整代码,下面我们来仔细分析一下它。
SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0);
SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA);
SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIO_OLEDDC);
这三行是用来使能所用的外设的,在这里,函数的编写者犯了一个小小的失误。
在刚开始的宏定义中,已经将SYSCTL_PERIPH_GPIO_OLEDDC定义成了SYSCTL_PERIPH_GPIOA,所以这两句的功能是完全一样的,属于重复操作,应该将其中一句去掉。
至于第一句,虽然SSI0和所用的端口为GPIOA端口中的引脚,但是SSI接口还包含SSI的功能模块,所以还需要独立给其设置使能。
接下来就是对SSI管脚特性的设置了,如下:
GPIOPinTypeSSI(GPIO_PORTA_BASE, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_5);
GPIOPadConfigSet(GPIO_PORTA_BASE, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_5,
GPIO_STRENGTH_8MA, GPIO_PIN_TYPE_STD_WPU);
在这里又犯了重复设置的毛病,原因何在?在GPIOPinTypeSSI上点右键~~~
在GPIOPinTypeSSI中对管脚进行了如下操作。
GPIODirModeSet(ulPort, ucPins, GPIO_DIR_MODE_HW);
GPIOPadConfigSet(ulPort, ucPins, GPIO_STRENGTH_2MA, GPIO_PIN_TYPE_STD);
我们看到,在GPIOPinTypeSSI函数里已经对管脚类型做出了设置,只不过是驱动能力2mA而且不带弱上拉的。而OLED接口芯片是一个CMOS芯片,不会需要太多的输入电流的。可能程序的作者希望提供尽可能大的驱动能力以防不测,这样做也合情合理,不过,这样的话,这个芯片所提供的2mA驱动电流选项还有什么用呢?
另外一个就是推挽输出带弱上拉(这个功能中没有使用输入功能)。对于上下拉电阻,我的理解是这样的:在输入电路中,用来提供一个确定的电平,防止在输入悬空状态下受到干扰。或者在OD输出中。不过在OD输出中这个电阻一般外置。因此这里设置的上拉电阻也没有道理,下面这一句应该去掉。
同理还有这里的GPIOPadConfigSet,同样应该去掉。
GPIOPinTypeGPIOOutput(GPIO_OLEDDC_BASE, GPIO_OLEDDC_PIN | GPIO_OLEDEN_PIN);
GPIOPadConfigSet(GPIO_OLEDDC_BASE, GPIO_OLEDDC_PIN | GPIO_OLEDEN_PIN,
GPIO_STRENGTH_8MA, GPIO_PIN_TYPE_STD);
GPIOPinWrite(GPIO_OLEDDC_BASE, GPIO_OLEDDC_PIN | GPIO_OLEDEN_PIN,
GPIO_OLEDDC_PIN | GPIO_OLEDEN_PIN);
后面的GPIOPinWrite的作用是打开OLED电源,并将D/C引脚置高,即设置在数据输入状态,防止因为单片机误操作写入命令带来不确定的后果。当然,后者可能是我自己想象出来的。因为如果这是作者的初衷的话,后面的RITWriteCommand函数就应该是这个样子的:
static void
RITWriteCommand(const unsigned char *pucBuffer, unsigned long ulCount)
{
……
//
// Clear the command/control bit to enable command mode.
//
GPIOPinWrite(GPIO_OLEDDC_BASE, GPIO_OLEDDC_PIN, 0);
……
//
// Set the command/control bit to Disable command mode.
//
GPIOPinWrite(GPIO_OLEDDC_BASE, GPIO_OLEDDC_PIN, GPIO_OLEDDC_PIN);
}
但事实上,最后那一段是我添加上去的~~
OLED所需要的管脚都已经分配好了,下一步就是对其进行功能配置了,RIT128x96x4Enable函数从字面上也可以看出来,这是用来使能OLED显示器的。由于OLED显示器采用的是SSI端口来传输数据,所以,对其配置也就是对SSI端口进行配置。
这里主要调用了SSIConfigSetExpClk函数来初始化SSI端口,虽然看起来有些复杂,但都是中规中矩的寄存器设置,对照寄存器表不难读懂。值得一提的是这里分频器的设置是先确定需要的比特率,然后在函数中计算分频系数。这样做的好处是用户不需要去关心比特率公式,但是需要不少的运算量,所以我个人还是比较喜欢直接写入计算好的数值的。
前面所说得设置全部都是对8962芯片内部资源的设置,下面将要进入最激动人心的时刻,对OLED进行操作了。这块OLED显示器采用的驱动芯片是SSD1329U2。我没有找到它的资料,不过找到了SSD1329U1的资料
SSD1329_datasheet.pdf
(803.72 KB, 下载次数: 33)
,想来二者的命令应该是一样的,至少目前我所用过的指令都是一样的。
以前DOS时代写批处理时第一句一般总是CLS,也就是清屏命令。一般遇到需要显示的时候,特别是需要格式化显示的时候,总是要先清理一下屏幕,防止原来屏幕上的信息干扰,造成误读。所以我们的OLED在开始显示之前也应该清一下屏,创造一个干净的显示环境。清屏函数如下:
void
RIT128x96x4Clear(void)
{
static const unsigned char pucCommand1[] = { 0x15, 0, 63 };
static const unsigned char pucCommand2[] = { 0x75, 0, 127 };
unsigned long ulRow, ulColumn;
//
// Clear out the buffer used for sending bytes to the display.
//
*(unsigned long *)&g_pucBuffer[0] = 0;
*(unsigned long *)&g_pucBuffer[4] = 0;
//
// Set the window to fill the entire display.
//
RITWriteCommand(pucCommand1, sizeof(pucCommand1));
RITWriteCommand(pucCommand2, sizeof(pucCommand2));
RITWriteCommand(g_pucRIT128x96x4HorizontalInc,
sizeof(g_pucRIT128x96x4HorizontalInc));
//
// Loop through the rows
//
for(ulRow = 0; ulRow < 96; ulRow++)
{
//
// Loop through the columns. Each byte is two pixels,
// and the buffer hold 8 bytes, so 16 pixels are cleared
// at a time.
//
for(ulColumn = 0; ulColumn < 128; ulColumn += 8 * 2)
{
//
// Write 8 clearing bytes to the display, which will
// clear 16 pixels across.
//
RITWriteData(g_pucBuffer, sizeof(g_pucBuffer));
}
}
}
这个函数刚开始就设置了两个数组,内容分别为:0x15, 0, 63和0x75, 0, 127。这里,0x15是用来设置GDDRAM行地址范围的命令,而0x75则用来设置列地址范围(参见下面的SSD1329数据手册),不过仅设置可以操作的内存地址范围,对显示范围没有影响,应用此命令可以向内部任意矩形区域连续写入数据而不用关心该区域内所有空间地址是否连续。该芯片可以驱动128X128点阵显示器,也就是其内部存储器可以存储128*128个显示单元的数据。同时,由于其显示采用四级灰度控制,因此,一个显示单元占用4bit数据,也就是半个字节,横向两个相邻单元共用一个字节显示。所以,其内部存储器阵列为64*128字节。这里初始化将使用所有的存储字节,包括不在显示范围内的字节,即96~~127行。当然,这个显示范围也是相对的,后面将介绍。
后面还写入了一个名为g_pucRIT128x96x4HorizontalInc的命令,其内容是:0xA0,0x52。
0xA0这个命令定义了很多设置。它定义了GDDRAM存储单元的排列方式,地址自增选项及扫描控制。具体可以参考数据手册,我这里仅针对程序中所用的模式来介绍。
0x52的二进制形式为:0101,0010。
最低位(记作A[0]),设置行扫描方向,0表示从左向右扫描、1表示从右向左扫描。
A[1],GDDRAM中每个字节可以控制两个相邻单元的显示,那么这个字节中数据的分配便有个先后问题。当A[1]=0时,显示低位在前(左),高位在后;当A[1]=1时,显示高位在前,低位在后。
A[2],地址自增模式。当A[2]=0时,数据为一行一行写入GDDRAM中的。每写完一个字节,行地址自动加一。当A[2]=1时则按列写入,两列同时写入!为什么?可以参考前面内容思考一下。
A[4],设置列扫描方向,A[4]=0时扫描方向为从上到下,而A[4]=1时,扫描方向为从下到上。
A[6],设置是否隔行扫描。
跟g_pucRIT128x96x4HorizontalInc在一起定义的还有另外一个命令,g_pucRIT128x96x4VerticalInc从字面意思来看,这个是采用纵向地址增量方式来写入数据的。在OLED初始化时采用了横向地址增量来清屏,而在显示中,则采用纵向地址增量来写入显示数据,参见RIT128x96x4StringDraw函数。我们要玩转OLED,就要在这个命令上做文章,来让显示数据转起来!
g_pucRIT128x96x4VerticalInc在程序中定义为:0xA0,0x56。我们先给它来个水平镜像,就是让显示的文字在水平方向上反着显示。那应该是在A[0]上做文章了吧?把g_pucRIT128x96x4VerticalInc改为:0xA0,0x57试试。成功了吗?No,非但没有镜像,显示字符已经乱七八糟不成样子了。这是因为一个数据可以控制水平方向上相邻的两个像素显示,我们将扫描方向反向后,原来123456排列的像素现在变成了563412,很明显不是我们想要的654321排列。该怎么办呢?别忘了A[1]。好了,把它改为:0xA0,0x55试试,是不是成功反过来了。
然后再让他纵向翻转一下,这次没有半字节问题存在,应该比较容易成功吧。把A[4]反过来,原来的0xA0,0x55就变成了0xA0,0x45。编译,运行!
别吃惊,学习的道路总是坎坷的,知道字符为什么消失了吗?看一下显示坐标,RIT128x96x4StringDraw("Hello World!", 30, 24, 15)。纵坐标从第24行开始显示,共显示八行,即第24~~31行。那么当扫描方向反向后这些文字的纵坐标到哪儿了呢?总共128行,底部空出24(正常坐标下的0~~23行)个空行。就变成第116~~123行了。这个显示器最多才96行,我们将数据写到显示范围之外了。所以,将这里的24改成60(也可以是其它值,保证不会被翻出屏幕就是了)后,就可以看到文字又被我们“玩转”了一下~~
好了,今天就让OLED“转”到这儿吧,等想出新的招数后再转它!最后,我发现这个“Hello World!”静态显示时傻呆傻呆的,就给了它设定了一个新的活动,淡入淡出。好了,我要休息了,让它自个儿淡去吧~~
int
main(void)
{
//
// Set the clocking to run directly from the crystal.
//
long i;
char m,a;
SysCtlClockSet(SYSCTL_SYSDIV_1 | SYSCTL_USE_OSC | SYSCTL_OSC_MAIN |
SYSCTL_XTAL_8MHZ);
//
// Initialize the OLED display.
//
RIT128x96x4Init(1000000);
m=0;
a=1;
while(1)
{
RIT128x96x4StringDraw("Hello World!", 30, 24, m);
m+=a;
for (i=0;i++<100000;);
if ((m==15)|(m==0)) a=-a;
}
}