这是我第一次写文章,可能有很多错误或者描述不到位的地方,希望大家多多包涵。如果有前辈发现我的文章中有什么问题,欢迎指出,谢谢。同时,这篇文章也是用作我自己以后查询一些资料之用,所以我会分析很多的源码、协议等。不需要的朋友可以自行省略。程序上参考了野火和中景园的部分例程。
昨天,做了个小实验,使用STM32配合OLED12864实现对多个LED发光二极管的亮度管理。具体的要求如下:能够在OLED上显示出的LED的种类,如红灯、绿灯、蓝灯等。并在后面显示当前板子上对应的LED的实际亮度。利用两个按键进行调节。其中第一个按键用于选择LED,第二个按键实现对当前选中的LED的亮度进行调节。调节LED亮度的手段为PWM,调节的亮度等级为0-9。
我使用了野火霸道V-2开发板,板上的芯片为STM32F103ZET6。板上外设搭载了一个RGB灯,可以分别控制红色、绿色、蓝色光的亮度。红、绿、蓝分别接在板子的PB5、PB0、PB1引脚。原理图如下:
12864采用了OLED的版本,支持SPI通讯协议。
SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。最早由摩托罗拉公司提出,是一种高速的,全双工,同步的通信总线。现广泛的用于ADC、LCD等设备。
优点:1.传输速度快
2.用4条线完成数据传输,节约芯片管脚
缺点:1没有指定的流控制,没有应答机制确认是否接收到数据
SPI由四条数据线组成:
1.SCK:时钟线,由主机控制
2.MISO: M代表主机,I代表INPUT,S代表从机,O代表OUTPUT,即主收从发数据线
3.MOSI: M代表主机,O代表OUTPUT,S代表从机,I代表INPUT,即主发从收数据线
4.CS:片选信号线。0代表被选中,1代表没被选中
关于SPI协议本文只做基本的介绍,更多详细的了解大家可以在CSDN上查找其他的笔记,也可以在b 站搜索SPI协议,查找相关视频进行学习。
12864显示屏是一种统称,并非指专门的某一型号的显示屏。12864代表的含义是横向有128个像素,纵向有64个像素,即128*64大小的液晶屏。类似的命明规则包括1602(16代表一行可以显示16个字符,02表示有两行),1601(16代表一行可以显示16个字符,01表示有一行)等。不同的厂商生产的12864具体的使用方法可能不同,所以需要大家注意。
我选用的12864是OLED12864,没有带字库,所以需要我们利用取模软件对汉字进行取模后,才可以显示出汉字。12864支持SPI协议。可以利用SPI协议进行写入。与标准的SPI协议不同,我们只需要主机发送数据到OLED12864上即可。而且在SPI总线上只挂载了一个外设,所以我们将CS线硬件接地,保持选中状态。所以,我们实际上只使用了SPI协议中的MOSI线和SCLK线。由于我的PCB板上的丝印分别对应的接口为SDA和SCL,所以我在程序中使用了SDA和SCL来表示。OLED12864上还有两个接口,分别为RES和D/C,RES用于OLED的重置,D/C线用于控制向OLED12864发送的是命令还是数据。
要想控制好12864,首先需要查阅其数据手册。根据数据手册可以得知,在每次使用OLED12864之前,首先需要对屏幕进行初始化。而对屏幕初始化则需要使用SPI协议进行数据传输,所以,我首先分析如何使用SPI进行数据传输。需要说明的是,我采用了ARM软件模拟的方法实现SPI传输,没有使用STM32的片上外设。
void OLED_WR_Byte(uint8_t dat,uint8_t cmd)
{
u8 i;
if(cmd)
OLED_DC_GPIO_DATA;
else
OLED_DC_GPIO_COM;
for(i=0;i<8;i++)
{
OLED_SCLK_Clr();
if(dat&0x80)
OLED_SDA_HIGH;
else
OLED_SDA_LOW;
OLED_SCLK_Set();
dat<<=1;
}
OLED_DC_GPIO_DATA;
e}
这个自定义函数模拟了SPI协议,用于向 OLED12864发送数据。其中第一个形参用于填写发送数据的具体值。第二个形参用与选择发送的是数据,还是指令。所谓数据,大家可以理解为需要写入的十六进制数,而指令,则可以理解为当把数据写入oled的显存中后,让其显示出来。接下来,我们简单分析一下这个函数的具体实现原理。根据OLED12864的数据手册可知。当D/C线接为高电平时,代表向OLED12864写入数据,D/C线为低电平时写入指令。当调用该函数的时候,程序首先读取第二个形参com的数据。根据com形参的值,选择D/C线的值。OLED_DC_GPIO_DATA; 和 OLED_DC_GPIO_COM;两句我在头文件中定义。
选择好发送是数据还是指令。接下来就要发送形参1data中储存的具体的值,输输入的值一般为8bit。根据SPI协议,OLED12864在时钟线上升沿时接收数据。所以,我们首先调用OLED_SCLK_Clr();拉低时钟线。接下来是SPI发送的核心。0x80转化为二进制数为10000000(b),将我们要发送的数据和1000000(b)做与运算之后,就可以需要发送数据的最高位写的是0还是1.如果最高位为0,则SDA线发送低电平,代表逻辑0,反之,发送高电平,代表逻辑1。发送完数据后,拉高时钟线,产生一个上升沿。使OLED12864读取数据。这一个过程,完成了一个bit的发送。发送完一个bit的数据。将bata中的值左移一位。在重复上面的过程。一次需要发送8个bit即要重复8次上述过程。所以,我们将该部分程序放在for循环中。实现一个字节(8bit)数据的发送。
利用该函数,我们就可以向OLED12864写入数据或指令,实现OLED12864的控制。
在程序的最开始,定义了一个二维数据,用于存放OLED12864每一个点上的内容。可以理解为OLED12864的显存。
uint8_t OLED_GRAM[128][8];
这个二维数组的定义和其采用的主控有关。下面引用一下数据手册的内容。
/********************************************************************************/
本屏所用的驱动 IC 为 SSD1306;其具有内部升压功能;所以在设计的时候不需要再专一设计 升压电路;当然了本屏也可以选用外部升压,具体的请详查数据手册。SSD1306 的每页包含了 128 个字节,总共 8 页,这样刚好是 12864 的点阵大小。
/******************************************************************************/
驱动IC内部把OLED12864横向的64个像素点,以8个为一组,分成8组。每一组中纵向有128列。在讲解OLED12864控制时讲过,我们每次发送数据是8bit为一个单位进行传输。之所以这样设计,和内部IC驱动将其分为8组,每一组中有8个bit是相关的。也就是说,我们想要在OLED上显示内容,只需要把我们想显示的内容写入在OLED_GRAM[128][8]数组。在将数组中的值一次发送到驱动IC即可。接下来介绍将数组的数据发送到驱动IC的函数。
void OLED_Refresh(void)
{
u8 i,n;
for(i=0;i<8;i++)
{
OLED_WR_Byte(0xb0+i,OLED_CMD); //设置行起始地址
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);
}
}
这一个函数采用for循环嵌套的方法实现。变量i控制当前写入的是第几组,变量n控制当前写的是第几列。0xb0、0x00、0x10等属于OLED12864芯片定义好的指令,想要了解的朋友可以自行查阅数据手册。
了解了OLED12864如何显示内容。我们就可以在OLED12864上显示出我们想要的内容了。但是直接修改OLED_GRAM[][]数组中的值非常麻烦。所以我们可以利用一些专门的函数来帮助我们。接下来介绍用于实现汉字显示的函数。
void OLED_ShowChinese(u8 x,u8 y,u8 num,u8 size1)
{
u8 i,m,n=0,temp,chr1;
u8 x0=x,y0=y;
u8 size3=size1/8;
while(size3--)//行数控制
{
chr1=num*size1/8+n;
n++;
for(i=0;i<size1;i++)//列数控制
{
if(size1==16)
{temp=Hzk1[chr1][i];}//调用16*16字体
else if(size1==24)
{temp=Hzk2[chr1][i];}//调用24*24字体
else if(size1==32)
{temp=Hzk3[chr1][i];}//调用32*32字体
else if(size1==64)
{temp=Hzk4[chr1][i];}//调用64*64字体
else return;
for(m=0;m<8;m++)//把temp变量的值赋值给
{
if(temp&0x01)OLED_DrawPoint(x,y);
else OLED_ClearPoint(x,y);
temp>>=1;
y++;
}
x++;
if((x-x0)==size1)
{x=x0;y0=y0+8;}
y=y0;
}
}
}
由于 OLED12864内部没有中文字库,所以实现需要利用取模软件对汉字进行取模。再将取模的结果存放于一个二维数组中。本例程以中文字模存放于Hzk?数组中为例。
这个函数有四个形参,前两个代表了需要写入汉字的起始坐标(x,y),第三个形参num代表储存在Hzk?[][]数组中的下标。第四个形参sizel代表显示汉字的大小,可输入的值有:16、24、32、64,数字代表汉字需要占据的长宽数据。如输入16,则表示汉字的占据了横向16个,纵向16个,即16*16个像素的位置。
函数内部变量,x0、y0分别记录了汉字起始的X、Y坐标,size3由size1/8得来。用于控制汉字的纵向长度。这一点有些朋友可能不太理解,我做个简单的说明。在前面的讲解中我提到,OLED12864在纵向的64个点被分成了8组。每次写入时都是8bit的数据进行写入。所以我们将其除以8,实现控制纵向上需要写入多少个组。num用于控制需要HZK?[][]数组的下标。
该程序的实现同样使用了循环的嵌套,最外层的while循环的判断条件中写入了size3–,实现纵向控制。内部嵌套了以个for循环。用于写入每一祖中的每一列的值。汉字的大小由size1控制。所以for循环运行的条件为 i
void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size1)
{
u8 i,m,temp,size2,chr1;
u8 y0=y;
size2=(size1/8+((size1%8)?1:0))*(size1/2); //得到字体一个字符对应点阵集所占的字节数
chr1=chr-' '; //计算偏移后的值
for(i=0;i<size2;i++)
{
if(size1==12)
{temp=asc2_1206[chr1][i];} //调用1206字体
else if(size1==16)
{temp=asc2_1608[chr1][i];} //调用1608字体
else if(size1==24)
{temp=asc2_2412[chr1][i];} //调用2412字体
else return;
for(m=0;m<8;m++) //写入数据
{
if(temp&0x80)OLED_DrawPoint(x,y);
else OLED_ClearPoint(x,y);
temp<<=1;
y++;
if((y-y0)==size1)
{
y=y0;
x++;
break;
}
}
}
}
PWM是英文“ Pulse Width Modulation” 的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽度的控制。可以简单的理解为占空比。即一段限定的时间内,低电平占据的百分比是多少。其作用主要用于电机调速与LED的亮度控制。
STM32中共有三类定时器,包括基本定时器——TIM6和TIM7,通用定时器TIM2、TIM3、TIM4、TIM5,高级定时器TIM1、TIM8。除了基本定时器以外都可以用于PWM输出。其中通用定时器可以输出4路,高级定时器可以输出8路。
我们实验中采用的是TIM3。对应的引脚为PB0、PB1、PB5。其中因为PB5属于复用功能。所以在使用程序时需要先进行重映射。
STM32的固件库中有四个和定时器相关的库函数。其中使用PWM需要配置其中的两个即可: 时 基 初 始 化 结 构 体 TIM_TimeBaseInitTypeDef 、 输 出 比 较 初 始 化 结 构 体 TIM_OCInitTypeDef。具体如何配置不做介绍,感兴趣的朋友可以直接看我最后放上来的程序。
在配置完成TIM的配置之后,我们只需要直接将需要写入的PWM值写入到对应的CCR寄存器中即可。为了方便使用,采用函数的方式进行写入。
void SetColorValue(uint8_t r,uint8_t g,uint8_t b)
{
//根据颜色值修改定时器的比较寄存器值
COLOR_TIMx->COLOR_RED_CCRx = r;
COLOR_TIMx->COLOR_GREEN_CCRx = g;
COLOR_TIMx->COLOR_BLUE_CCRx = b;
}
设计菜单首先需要分清楚三件事:1.可用于交互的手段 2.需要用于调节的内容 3.显示选中的方式 。在野火的霸道V2开发板上可用于交互的有两个实体按键和一个电容按键。但是经过测试电容按键的灵敏度简直令人感动到落泪。所以使用两个按键作为交互手段。在该实验中我们的任务设定是需要能够分别调节RGB灯的亮度。所以需要调节的内容就是亮度和LED灯的颜色。显示选中方式采用了在当前选中颜色的亮度后面画圆的方法。板子上的按键一用于选择不同颜色的LED灯。按键二用于调节当前选中的LED灯的亮度。
菜单的内容分为三部分:灯的颜色、灯的亮度以及选中后显示的圆圈。因此,我写了一个初始话函数,让我们要内容显示在OLED12864上。
void caidan_display(uint8_t red,uint8_t green,uint8_t blue)
{
OLED_Str_Chinese(2,0,0);
OLED_Str_Chinese(3,0,3);
OLED_Str_Chinese(2,1,1);
OLED_Str_Chinese(3,1,3);
OLED_Str_Chinese(2,2,2);
OLED_Str_Chinese(3,2,3);
OLED_ShowNum(68,0,red,2,12);
OLED_ShowNum(68,18,green,2,12);
OLED_ShowNum(68,34,blue,2,12);
OLED_Refresh();
}
void OLED_Str_Chinese(u8 x,u8 y,u8 num)
{
x = x*16;
y = y*16;
u8 size1 = 16;
u8 i,m,n=0,temp,chr1;
u8 x0=x,y0=y;
u8 size3=size1/8;
while(size3--)//行数控制
{
chr1=num*size1/8+n;
n++;
for(i=0;i<size1;i++)//列数控制
{
temp=Chidan[chr1][i];
for(m=0;m<8;m++)//把temp变量的值赋值给
{
if(temp&0x01)OLED_DrawPoint(x,y);
else OLED_ClearPoint(x,y);
temp>>=1;
y++;
}
x++;
if((x-x0)==size1)
{x=x0;y0=y0+8;}
y=y0;
}
}
}
第一个函数 caidan_display();用于显示出菜单上的红灯、绿灯、蓝灯。以及对应的亮度。其中有三个形参。分别对应在红灯、绿灯、蓝灯后面现实的内容。第二个函数OLED_Str_Chinese,是 OLED_ShowChinese(u8 x,u8 y,u8 num,u8 size1)函数做了一些修改后得到的,原理不变,变得地方有两个。1.由于在该函数中使用的字体都为16*16的格式。所以我在函数内部做了一些计算。在输入时只需要写入第几行第几列显示汉字即可,不需要在写入横向和纵向的像素点位置。2.由于OLED12864内部没有带汉字字库。所以需要将要使用的汉字的取模,并放入数组中。Chidan[chr1][i]数组,就是我用来存放汉字取模结果的数组。
##总程序
int main(void)
{
uint8_t red = 0,green = 0,bull = 0;
uint8_t value1 = 0,value2 = 0,value3 = 0;
uint8_t key = -1;//0表示选中颜色,1表示选中PWM
uint8_t i = 10;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);
KEY_GPIO_Config();
delay_init();
OLED_Cofing();
caidan_display(00,00,00);
COLOR_TIMx_LED_Init();
while(1)
{
if(Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN))//按下按键1,用于改变选中目标
{
OLED_Clear();
key++;
key = key % 3;
key_RGB_chang(key);
}
if(Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN))//按下按键2,用于改变值
{
OLED_Clear();
if(key==0)
{
red = red + 25;
red = red % 250;
value1 = (++value1) %10;
}
else if(key==1)
{
green = green + 25;
green = green % 250;
value2 = (++value2) %10;
}
else if(key==2)
{
bull = bull + 25;
bull = bull % 250;
value3 = (++value3) %10;
}
//key_RGB_chang(key);
}
caidan_display(value1,value2,value3);
SetColorValue(red,green,bull);
OLED_Refresh();
}
}
在main函数里,定义了一个变量key用来代表当前选中的颜色定义了red、bull 、green三个变量用于亮度的控制,value1—value3用于存放OLED上显示的颜色对应的亮度。当按键1被按下时,通过key++改变当前选中的颜色。并利用key_RGB_chang(key);函数用于在当前选中的函数后面画圈,代表当前被选中。当按键二被按下后,会进入一个if语句进行分类处理。分类的依据是key的值,即当前选中的颜色。按钮二被按下以后用于存放亮度的变量加25(因为PWM分为了255级。程序中需要实现的是10级调节亮度,255/10后得25余5).用于显示在OLED上的value变量也进行加1。实现OLED显示和实际亮度的同一。两个if语句处理完之后。把if语句里面修改的变量值写入函数中。实现菜单显示的改变和RGB灯亮度的调节。
源程序下载地址:https://download.csdn.net/download/qq_41286749/12110273
本来想最开始的地方放个实验效果视频,但由于我东西放在学校的实验室里,而人已经回家了。所以就没发通过视频的方式演示给大家看了。有需要的话我可以回学校以后简单的录个视频给大家看看。第一次写文章,可能有很多的错误以及表达上不到位的地方,欢迎大家积极评论,共同交流。