本次实验所用oled显示屏为黄蓝屏,即屏上1/4 部分为黄光,下3/4 为蓝;而且是固定区域显示固定颜色,颜色和显示区域均不能修改。分辨率为128*64,采用IIC 接口方式进行通讯(默认地址0x78)。
0.96 寸OLED 驱动IC
0.96’OLED(4Pin)模块采用SSD1306 为驱动芯片,模块带有稳压芯片,支持软件模拟IIC 通讯与硬件IIC 通讯,上电自动复位,功耗低,自发光自由视角。
SSD1306 有3 中寻址模式:页寻址模式、水平寻址模式、垂直寻址模式。寻址方式决定了写入数据的方式。
SCL时钟管脚 接 PD6
SDA数据管脚 接 PD7
VCC 接 3.3v电源
GND 电源地
为了实际应用,对IO口进行了进一步封装。需要改变管脚的时候,修改程序更方便。
文件oled.h
/* 若要改变管脚 直接在此配置 */
#define GPIO GPIOD
#define RCC_IO RCC_APB2Periph_GPIOD //gpio时钟
#define SCL GPIO_Pin_6 //时钟管脚
#define SDA GPIO_Pin_7 //数据管脚
文件oled.c
static void gpio_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //gpio结构体变量
RCC_APB2PeriphClockCmd(RCC_IO, ENABLE); //使能GPIOD时钟
GPIO_InitStructure.GPIO_Pin = SCL | SDA; //PD6、PD7
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_Init(GPIO, &GPIO_InitStructure); /* 初始化GPIOD */
GPIO_SetBits(GPIO, SCL | SDA); //拉高电平
}
文件oled.c
/*******************************************************************************
* 函 数 名 : OLED_Init
* 函数功能 : 0.96寸oled初始化函数
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void OLED_Init(void)
{
gpio_init(); //数据与时钟的gpio管脚初始化
delay_ms(200); //等待oled复位完成
OLED_WR_Byte(0xAE, OLED_CMD); //关闭显示
OLED_WR_Byte(0xD5, OLED_CMD); //设置时钟分频因子,震荡频率
OLED_WR_Byte(0x80, OLED_CMD); //[3:0],分频因子;[7:4],震荡频率
OLED_WR_Byte(0xA8, OLED_CMD); //设置驱动路数
OLED_WR_Byte(0X3F, OLED_CMD); //默认0X3F(1/64)
OLED_WR_Byte(0xD3, OLED_CMD); //设置显示偏移
OLED_WR_Byte(0X00, OLED_CMD); //默认为0
OLED_WR_Byte(0x00, OLED_CMD); //设置显示位置—列低地址
OLED_WR_Byte(0x10, OLED_CMD); //设置显示位置—列高地址
OLED_WR_Byte(0x40, OLED_CMD); //设置显示开始行 [5:0],行数.
OLED_WR_Byte(0x8D, OLED_CMD); //电荷泵设置
OLED_WR_Byte(0x14, OLED_CMD); //bit2,开启/关闭
OLED_WR_Byte(0x20, OLED_CMD); //设置内存地址模式
OLED_WR_Byte(0x02, OLED_CMD); //[1:0],00,列地址模式; 01,行地址模式; 10,页地址模式;默认10;
OLED_WR_Byte(0xA1, OLED_CMD); //段重定义设置,bit0: 0,0->0; 1,0->127;
OLED_WR_Byte(0xC0, OLED_CMD); //设置COM扫描方向;bit3: 0,普通模式; 1,重定义模式 COM[N-1]->COM0; N:驱动路数
//OLED_WR_Byte(0xC8,OLED_CMD); //设置COM扫描方向
OLED_WR_Byte(0xDA, OLED_CMD); //设置COM硬件引脚配置
OLED_WR_Byte(0x12, OLED_CMD); //[5:4]配置
OLED_WR_Byte(0x81, OLED_CMD); //对比度设置
OLED_WR_Byte(0xEF, OLED_CMD); //1~255;默认0X7F (亮度设置,越大越亮)
OLED_WR_Byte(0xD9, OLED_CMD); //设置预充电周期
OLED_WR_Byte(0xf1, OLED_CMD); //[3:0],PHASE 1;[7:4],PHASE 2;
OLED_WR_Byte(0xDB, OLED_CMD); //设置VCOMH 电压倍率
OLED_WR_Byte(0x30, OLED_CMD); //[6:4] 000,0.65*vcc; 001,0.77*vcc; 011,0.83*vcc;
OLED_WR_Byte(0xA4, OLED_CMD); //全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏)
OLED_WR_Byte(0xA6, OLED_CMD); //设置显示方式;bit0:1,反相显示;0,正常显示
OLED_WR_Byte(0xAF, OLED_CMD); //开启显示
OLED_Clear(); //清屏函数
以上这些初始化命令我参考了不同淘宝商家和教程给的程序,其中配置方案大致相同,个别参数配置不同:
对比度设置:
OLED_WR_Byte (0x81, OLED_CMD);
//对比度设置
OLED_WR_Byte (0xEF, OLED_CMD);
//1~255;默认0X7F (亮度设置,越大越亮)
这个很好理解,配置的参数不同只影响屏幕亮度。
而有的配置方案中多了这么两个指令:
OLED_WR_Byte (0x00, OLED_CMD);
//设置显示位置—列低地址
OLED_WR_Byte (0x10, OLED_CMD);
//设置显示位置—列高地址
经查看技术手册发现:
页地址模式设置列低半字节的开始地址(00h~0Fh)
这个命令专门为8位列地址的低半字节设置以通过页地址模式显示RAM中的数据。而每一个数据使用后列地址会自动增加。请参考表格9-1的部分以及10.1.3的部分来了解详细情况。
页地址模式设置列高半字节的开始地址(10h~1Fh)
这个命令专门为8位列地址的高半字节设置以通过页地址模式显示RAM中的数据。而每一个数据使用后列地址会自动增加。请参考表格9-1的部分以及10.1.3的部分来了解详细情况。
使用X[3:0]作为数据位,将列起始地址寄存器的低位半字节设置为页面寻址模式。复位后,初始显示行寄存器复位至0000b。
此命令仅适用于页面寻址模式。
看到这已经大概明白什么意思了,但是不知道具体有什么用,继续向下挖…
页面寻址模式(A[1:0]=10xb)。
在页寻址模式下,在读/写显示RAM之后,列地址指针自动加1。如果列地址指针到达列结束地址,则列地址指针重置为列起始地址,页地址指针不变。用户必须设置新的页面和列地址才能访问下一页RAM内容。页面寻址模式的页面和列地址点的移动顺序如图10-1所示。
在正常显示数据RAM读取或写入和页面寻址模式下,需要执行以下步骤。
定义起始RAM访问指针位置:
·通过命令B0h至B7h设置目标显示位置的页面起始地址。
·通过命令00h~0Fh设置指针的低位起始列地址。
·通过命令10h~1Fh设置指针的上起始列地址。
看到这儿,已经大概知道页寻址模式下如何向oled模块写入数据。这也是后面的清屏函数与更新显示函数的大概逻辑结构。
//设置COM扫描方向;bit3: 0普通模式; 1重定义模式COM[N-1]->COM0; N:驱动路数
oled.h
/* IIC端口定义 */
#define OLED_SCLK_Clr() GPIO_ResetBits(GPIO,SCL) //SDA IIC接口的时钟信号
#define OLED_SCLK_Set() GPIO_SetBits(GPIO,SCL)
#define OLED_SDIN_Clr() GPIO_ResetBits(GPIO,SDA) //SCL IIC接口的数据信号
#define OLED_SDIN_Set() GPIO_SetBits(GPIO,SDA)
#define OLED_CMD 0 //写命令
#define OLED_DATA 1 //写数据
oled.c
/* 模拟IIC启动信号 (时钟和数据管脚都由高到低)*/
void IIC_Start(void)
{
OLED_SCLK_Set(); //时钟脚置1
OLED_SDIN_Set(); //数据脚置1
OLED_SDIN_Clr(); //数据脚置0
OLED_SCLK_Clr(); //时钟脚置0
}
/* 模拟IIC停止信号 (时钟置高、数据管脚都由低到高)*/
void IIC_Stop(void)
{
OLED_SCLK_Set(); //时钟脚置1
OLED_SDIN_Clr(); //数据脚置0
OLED_SDIN_Set(); //数据脚置1
}
之所以把这么两个简单的函数单独列出来,是因为我在配置模拟IIC启动信号函数时,由于是第一次写模拟IIC通信协议的函数,没有严格按照时序图来写,启动函数没有起到它应有的作用,导致显示屏没有收到启动信号。
起始条件与停止条件时序图
最初写启动函数时,先是把SDA时钟线与SCL数据线拉高,然后产生下降沿时,先拉低的是时钟线,再拉低的数据线。而启动条件是通过将SCL数据线从高拉到低,同时SDA时钟线保持高来建立的;停止条件是通过将SDA时钟线从低到高拉入来建立的。刚开始配置的时候没注意这些细节,参照例程比着葫芦画瓢,想着拉高再拉低就行,直到改正过来也不知道为什么是这儿错了,知道后来参照使用手册和时序图才明白过来。(说到这不得不说数据手册真是个好东西)
每次SSD1306 收到数据后都会将SDA 信号线拉低,发送一个应答信号,单片机通过检测应答信号来判断SSD1306 是否有接受到数据。
应答时序图
时钟线拉高再拉低,不需要真的确定它接收到了本次数据再发送下个数据。
/* 模拟IIC读取从机应答信号 */
static void IIC_Wait_Ack(void)
{
OLED_SCLK_Set();
OLED_SCLK_Clr();
}
数据的发送是高位在前,也就是先发送字节的高位。数据的传输是由SDA 信号线与SCL 时钟线通过一定的规范进行传输:
在SCL 时钟线处于高电平期间SDA 的电平必须保持稳定(不允许改变电平状态);
在SCL时钟线处于低电平期间SDA 的电平允许发生改变(可以改变电平状态)。
IIC写入一个字节函数:
/* IIC写入一个字节 */
void Write_IIC_Byte(u8 IIC_Byte)
{
u8 i = 0;
u8 j;
for(i = 0; i < 8; i++)
{
OLED_SCLK_Clr(); //时钟脚置低,为数据传输做准备
if(IIC_Byte & 0x80)
j=1;
else j=0;
PDout(7) = j; //提取bit7 的数据并发送到PD7端口 (PDout(n)函数是直接操作ODR寄存器控制IO口)
IIC_Byte <<= 1; //把bit6 移到 bit7,为下一次传输数据做准备
OLED_SCLK_Set(); //时钟脚置高,发送数据
}
OLED_SCLK_Clr(); //时钟脚置低(应答)
}
根据通讯协议,在写入数据前要发送一个控制字节(Control byte)来通知模块接下来发送的一个字节是数据还是命令。
如果接受到的是命令,SSD1306 就会把接收到的下一个字节当作命令转移到命令寄存器中;
如果接受到的是数据,SSD1306 就会把接收到的下一个字节当作数据存放到图形显示数据RAM 中。
0x00:接下来发送的是命令;
0x40:接下来发送的是数据。
/* IIC写入命令 */
void Write_IIC_Command(u8 IIC_Command)
{
IIC_Start(); //开始
Write_IIC_Byte(0x78); //写入从机地址,SA0=0
IIC_Wait_Ack();
Write_IIC_Byte(0x00); //写入命令
IIC_Wait_Ack();
Write_IIC_Byte(IIC_Command); //数据
IIC_Wait_Ack();
IIC_Stop(); //停止
}
/* IIC写入数据 */
void Write_IIC_Date(u8 IIC_Date)
{
IIC_Start(); //开始
Write_IIC_Byte(0x78); //写入从机地址,SA0=0
IIC_Wait_Ack();
Write_IIC_Byte(0x40); //写入数据
IIC_Wait_Ack();
Write_IIC_Byte(IIC_Date); //数据
IIC_Wait_Ack();
IIC_Stop(); //停止
}
//向SSD1306写入一个字节。
//dat:要写入的数据/命令
//cmd:数据/命令标志 0,表示命令; 1,表示数据;
void OLED_WR_Byte(u8 date, u8 cmd)
{
if(cmd)
Write_IIC_Date(date);
else
Write_IIC_Command(date);
}
更新显示函数 & 清屏函数
u8 OLED_GRAM[128][8]; //8页 屏幕大小64*128
//更新图像到LCD
void OLED_Refresh_Gram(void)
{
u8 i, n;
for(i = 0; i < 8; i++)
{
OLED_WR_Byte(0xb0 + i, OLED_CMD); //设置页地址(0~7)
OLED_WR_Byte(0x00, OLED_CMD); //设置显示位置—列低地址
OLED_WR_Byte(0x10, OLED_CMD); //设置显示位置—列高地址
for(n = 0; n < 128; n++)
OLED_WR_Byte(OLED_GRAM[n][i], OLED_DATA);
}
}
//清屏函数
void OLED_Clear(void)
{
u8 i, n;
for(i = 0; i < 8; i++)
{
OLED_WR_Byte(0xb0 + i, OLED_CMD); //设置页地址(0~7)
OLED_WR_Byte(0x00, OLED_CMD); //设置显示位置—列低地址
OLED_WR_Byte(0x10, OLED_CMD); //设置显示位置—列高地址
for(n=0;n<128;n++)
OLED_WR_Byte(0, OLED_DATA);
} //更新显示
}
画点函数 & 画方块函数
//画点
// x: 0~127
// y: 0~63
//bit: 1填充; 0清空
void OLED_DrawPoint(u8 x, u8 y, u8 bit)
{
u8 Y, bx, date = 0;
if(x > 127 || y > 63)
return; //超出范围 结束函数
Y = 7 - y/8; //判断该点在哪一页
bx = y % 8; //定位该点所在行数
date = 1<<(7 - bx);
if(bit)
OLED_GRAM[x][Y] |= date; //填充该位
else
OLED_GRAM[x][Y] &= ~date; //清除
}
//填充(x1,y1)与(x2,y2)对角坐标的区域
//bit: 0清空; 1填充
void OLED_Fill(u8 x1, u8 y1, u8 x2, u8 y2, u8 bit)
{
u8 x, y, tra_x, tra_y;
tra_x = tra_x;
tra_y = tra_y; //避免编译器警告,并无实际用处
if(x1>127||x2>127||y1>63||y2>63)
return; //超界则退出函数
if(x1 > x2)
{
tra_x = x1;
x1 = x2;
x2 = x1;
}
if(y1 > y2)
{
tra_y = y1;
y1 = y2;
y2 = y1;
}
for(x = x1; x <= x2; x++)
for(y = y1; y <= y2; y++)
OLED_DrawPoint(x, y, bit);
OLED_Refresh_Gram();//更新显示
}
显示ASCII字符函数
//在指定位置显示一个字符,包括部分字符
//x:0~127
//y:0~63
//mode:0,反白显示;1,正常显示
//size:选择字体 12/16/24
void OLED_ShowChar(u8 x, u8 y, u8 chr, u8 size, u8 mode)
{
u8 temp, t, t1;
u8 y0 = y;
u8 csize = (size/8 + ((size%8)?1:0)) * (size/2); //得到字体一个字符对应点阵集所占的字节数
chr = chr - ' '; //把输入的字符转化为在字体数组所在位置 (字符减字符等于数字)
for(t = 0; t < csize; t++)
{
if(size == 12) temp = ascii_1206[chr][t]; //调用1206字体
else if(size == 16) temp = ascii_1608[chr][t]; //调用1608字体
else if(size == 24) temp = ascii_2412[chr][t]; //调用2412字体
else return; //没有的字库
for(t1 = 0; t1 < 8; t1++)
{
if(temp & 0x80) OLED_DrawPoint(x, y, mode);
else OLED_DrawPoint(x, y, !mode);
temp <<= 1;
y++;
if((y - y0) == size)
{
y = y0;
x++;
break;
}
}
}
}
//显示一个字符串
//x,y:起点坐标
//size:字体大小
//*p:字符串起始地址
void OLED_ShowString(u8 x, u8 y, const u8 *p, u8 size)
{
while((*p <= '~') && (*p >= ' '))//判断字符是否正确
{
if(x>(128-(size/2)))
{
x = 0;
y += size;
}
if(y > (64-size))
{
y = x = 0;
OLED_Clear();
}
OLED_ShowChar(x, y, *p, size, 1);
x += size/2;
p++;
}
}
显示数字函数
//m^n函数 为显示多位数字提供方便
static u32 oled_pow(u8 m,u8 n)
{
u32 result = 1;
while(n--)
result *= m;
return result;
}
//显示len个数字
//x,y :起点坐标
//len :数字的位数
//size:字体大小
//num :数值(0~2^32-1);
void OLED_ShowNum(u8 x, u8 y, u32 num, u8 len, u8 size)
{
u8 t, temp;
u8 enshow = 0; //判断高位是否为0 的标志
for(t = 0; t < len; t++)
{
temp = (num / oled_pow(10, len-t-1)) % 10; //从高到低 依次取出对应位上的数值
if(enshow == 0 && t < (len-1))
{
if(temp == 0) //判断最高位上的数是否有效(若最高位上的数为0 则不显示数字)
{
OLED_ShowChar(x + (size/2)*t, y,' ', size, 1);
continue; //跳出本次循环 且标志位不清除,直到最高位有效
}
else enshow = 1; //最高位有效,清除此标志位
}
OLED_ShowChar(x + (size/2)*t, y, temp+'0', size, 1); //显示数字
}
}
显示汉字函数
//显示汉字
//x,y:起点坐标
//pos:数组位置汉字显示
//size:字体大小
//mode:0,反白显示;1,正常显示
void OLED_ShowFontHZ(u8 x, u8 y, u8 pos, u8 size, u8 mode)
{
u8 temp, t, t1;
u8 y0 = y;
u8 csize = (size/8+((size%8)?1:0))*(size);//得到字体一个字符对应点阵集所占的字节数
if(size!=12 && size!=16 && size!=24)
return; //不支持的size
for(t = 0; t < csize; t++)
{
if(size == 12) temp = FontHzk[pos][t]; //调用1206字体
else if(size == 16) temp = FontHzk[pos][t]; //调用1608字体
else if(size == 24) temp = FontHzk[pos][t]; //调用2412字体
else return; //没有的字库
for(t1 = 0; t1 < 8; t1++)
{
if(temp & 0x80)
OLED_DrawPoint(x, y, mode);
else OLED_DrawPoint(x, y, !mode);
temp <<= 1;
y++;
if((y - y0) == size)
{
y = y0;
x++;
break;
}
}
}
}
取模软件生成的汉字数组
//16*16汉字字体(宋体)
const unsigned char FontHzk[][32]={
{0x00,0x00,0x00,0x00,0x1F,0xF8,0x11,0x10,0x11,0x10,0x11,0x10,0x11,0x10,0xFF,0xFE,
0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x1F,0xF9,0x00,0x01,0x00,0x0F,0x00,0x00},/*"电",0*/
{0x01,0x00,0x41,0x00,0x41,0x00,0x41,0x00,0x41,0x00,0x41,0x02,0x41,0x01,0x47,0xFE,
0x45,0x00,0x49,0x00,0x51,0x00,0x61,0x00,0x41,0x00,0x01,0x00,0x01,0x00,0x00,0x00},/*"子",1*/
{0x00,0x80,0x01,0x00,0x06,0x00,0x1F,0xFF,0xE0,0x00,0x00,0x00,0x20,0x00,0x24,0x9F,
0x24,0x92,0xA4,0x92,0x64,0x92,0x24,0x92,0x24,0x92,0x24,0x9F,0x20,0x00,0x00,0x00},/*"信",2*/
{0x00,0x02,0x00,0x0C,0x00,0x00,0x3F,0xC0,0x2A,0x9C,0x2A,0x82,0x6A,0x82,0xAA,0xA2,
0x2A,0x9A,0x2A,0x82,0x2A,0x82,0x3F,0xCE,0x00,0x00,0x00,0x10,0x00,0x0C,0x00,0x00},/*"息",3*/
};
显示效果:
对比各个显示字符函数,它们的大概思路都一样,只要把数据正确的写入OLED_GRAM数组里对应位置,再配合更新显示函数,就能准确的显示字符或汉字。
从这次配置oled模块的过程来看,学会查找技术手册、掌握外设模块与单片机通讯方法是非常必要的,这也是我有待提高的部分。
此外,我发现在程序运行的时候,原本每0.5s转换一次状态的灯变成了大约每2s转换一次。换算过后估计运行一次更新显示函数需要用时30ms。
经测试,发现更新显示函数运行用时>21ms。不知道这个时长对于64*128的显示屏来说正不正常。但大体配置已完成,已经能够正常使用,后续遇到具体问题再来改进。
//主函数程序
int main()
{
RCC_HSE_Config(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // (8/1)*9 = 72M
//外部晶振1分频 //PLL锁相环9倍频
SysTick_Init(72); //滴答定时器初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级组合方式2
USART1_Init(9600); //串口1初始化 波特率9600
LED_Init(); //LED0 LED1初始化
OLED_Init();
while(1)
{
i++;
if(i % 50 == 0)
{
LED2_green = !LED2_green;
i=0;
}
delay_ms(10);
OLED_ShowFontHZ(16*0,0,0,16,1); //电子信息
OLED_ShowFontHZ(16*1,0,1,16,1);
OLED_ShowFontHZ(16*2,0,2,16,1);
OLED_ShowFontHZ(16*3,0,3,16,1);
OLED_Refresh_Gram();//更新显示
}
}
这是个人在初次学习oled配置过程中的一些理解,如有不妥或错误之处敬请见谅并麻烦各位大佬指正,非常感谢!