本文以STM32F103C8T6最小系统板为例 OLED屏为128*64像素 驱动芯片SSD1306
本文讲解标准库 硬件IIC和软件IIC 驱动OLED
硬件IIC
硬件IIC要了解的第一个东西那便是 IIC结构体:
//设置SCL时钟频率,此值要低于400000
//指定工作模式,可选IIC模式及SMBUS模式
//时钟占空比,可选low/high=2:0或16:9
//自身的IIC设备地址
//使能或者关闭响应,一般是使能
//指定地址长度,可为7或10
从结构体中就会发出一些疑问 为啥SCL时钟频率要小于等于400000
时钟占空比是啥 为啥是 2:0 或者16:9
那么下面便为你们介绍
从STM32F103中文参考手册中我们可以看到:
IIC的CCR寄存器:
初学者看上去 是不是一脸懵逼 下面的算式是干啥 不知道
计算时钟频率:
标准模式:
Thigh=CCR * TPCLK1 Tlow=CCR*TPCLK1
快速模式中Tlow/Thigh=2时:
Thigh=CCR* TPCLK1 Tlow=2*CCR*TPCLK1
快速模式中: Tlow/Thigh=16/9时:
Thigh=9*CCR*TPCLK1
Tlow=16*CCR*TPCLK1
例子:以快速模式中Tlow/Thigh=2时:
PCLK1=36MHZ,想要配置400Kbit/s 方法:
PCLK1时钟周期:TPCLK1=1/36000000
目标SCL时钟周期:TSCL=1/400000
SCL时钟周期内的高电平时间:Thigh=TSCL/3
SCL时钟周期内的低电平时间: Tlow=2*TSCL/3
计算CCR的值:CCR=Thigh/TPCLK1=1/400000 *1/3 *36000000=30
计算出来的值写入到寄存器即可
这就是时钟占空比的意义。用来确定IIC速度
后面就要考虑硬件IIC的通信过程了
其主要的几个寄存器是:
主要寄存器CR1 CR2 SR1 SR2寄存器
SR1 SR2寄存器 主要用到 用于判断通讯状态.
SR1寄存器
主要是以上4个状态寄存器进行状态判断.
硬件IIC写数据图:
这个是最重要的,这便是硬件IIC和别的芯片之间通讯的时序
如果这里看不懂 往下看代码 看完就知道了
再来看OLED屏参数:
驱动芯片:SSD1306
电源电压:3.3------5.5V
SSD1306 128*64bit 分8页 每页128字节
以下命令就是操控OLED的 这些命令在下面代码也会有体现
命令0x81:设置对比度。包含两个字节,第一个0x81为命令,随后是一个字节设置对比度
命令0xAE/0xAF:关闭显示和打开显示
命令0x8D:包含两个字节,第一个字节是命令,第二个是设置值,电荷泵必须要开,不然看不到屏幕显示
命令0xB0-0xB7设置页地址
命令0x00-0x0F设置列低位4位地址
命令0x10-0x1F设置列高位4位地址
下面开始将代码:
硬件IIC我提供以下几个函数
void IICInit(void); //IIC初始化函数
void IIC_WriteByte(uint8_t addr,uint8_t data); //IIC写一个字节给其他芯片函数
void WriteCmd(uint8_t cmd); //IIC写命令函数
void WriteData(uint8_t data); //IIC写数据函数
void OLEDInit(void); //OLED屏初始化函数
void OLED_SetPos(uint8_t x,uint8_t y); //OLED屏设置坐标x,y
void OLED_Fill(uint8_t Fill_Data); //填充OLED屏
void OLED_CLS(void); //清楚OLED屏
void OLED_ON(void); //打开OLED屏
void OLED_OFF(void); //关闭OLED屏
void OLED_ShowStr(uint8_t x,uint8_t y,uint8_t *q,uint8_t TextSize);
//显示字符串
void OLED_ShowChinese(uint8_t x,uint8_t y,uint8_t *data,uint8_t length);
//显示中文
void OLED_ShowBMP(uint8_t x,uint8_t y,uint8_t *data);
//显示图片
下面将以上函数一一讲解
首先来看void IICInit(void) IIC初始化函数
void IICInit(void)
{
I2C_InitTypeDef I2C_InitTypeDefStruct;
GPIO_InitTypeDef GPIO_InitTypeDefStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
GPIO_InitTypeDefStruct.GPIO_Mode=GPIO_Mode_AF_OD;
GPIO_InitTypeDefStruct.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitTypeDefStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitTypeDefStruct);
I2C_DeInit(I2C1);
I2C_InitTypeDefStruct.I2C_Ack=I2C_Ack_Enable ;
I2C_InitTypeDefStruct.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;
I2C_InitTypeDefStruct.I2C_ClockSpeed=400000;
I2C_InitTypeDefStruct.I2C_DutyCycle=I2C_DutyCycle_2;
I2C_InitTypeDefStruct.I2C_Mode=I2C_Mode_I2C ;
I2C_InitTypeDefStruct.I2C_OwnAddress1=0x30;
I2C_Init(I2C1,&I2C_InitTypeDefStruct);
I2C_Cmd(I2C1,ENABLE);
}
此初始化函数先打开GPIOB,IIC1的时钟
在初始化对应的结构体
设置时钟速度400000HZ 时钟占空比2:1 自己的地址随便写的0x30
使能IIC
为什么要用开漏输出呢 硬件IIC 就是把普通IO复用成功能IO
在中文参考手册8.1.11可以看到:
再来看void IIC_WriteByte(uint8_t addr,uint8_t data)函数 此函数最重要
void IIC_WriteByte(uint8_t addr,uint8_t data)//最重要 硬件IIC通信
{
while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY));
//首先判断IIC1是否忙碌 用状态判断
I2C_GenerateSTART(I2C1,ENABLE);
//开始IIC1
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT));
//通过事件判断IIC是否开始 SR1寄存器位0 对应EV5
I2C_Send7bitAddress(I2C1,OLED_ADDR,I2C_Direction_Transmitter);
//IIC发送7位器件地址
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
//通过事件判断IIC地址是否匹配 SR1寄存器位1 对应EV6
I2C_SendData(I2C1,addr);
//IIC发送器件的内部地址,内部地址当作数据发送
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTING));
//通过事件判断数据是否发送成功 对应EV8
I2C_SendData(I2C1,data);
//IIC发送真正的数据
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTING));
//通过事件判断数据是否发送成功 对应EV8
I2C_GenerateSTOP(I2C1,ENABLE);
//IIC停止
}
以上便是通过硬件IIC与元器件之间通信的一次全过程
对应那个可能你看不懂的图
再来看void WriteCmd(uint8_t cmd);
void WriteCmd(uint8_t cmd)
{
IIC_WriteByte(0x00,cmd); //0.96OLED屏规定的 0x00写命令
}
有了那个重要函数 就可以真正与OLED屏通信了
在OLED屏中规定了 0x00表示写命令
void WriteData(uint8_t data)同理可得写数据
void WriteData(uint8_t data)
{
IIC_WriteByte(0x40,data); //0.96OLED屏规定的0x40写数据
}
OLED屏规定了0x40是写地址
接下来是void OLEDInit(void) //厂商提供的初始化代码
void OLEDInit(void) //厂商提供的初始化代码
{
// ms_delay(100);
WriteCmd(0xAE); //display off
WriteCmd(0x20); //Set Memory Addressing Mode
WriteCmd(0x10); //00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
WriteCmd(0xb0); //Set Page Start Address for Page Addressing Mode,0-7
WriteCmd(0xc8); //Set COM Output Scan Direction
WriteCmd(0x00); //---set low column address
WriteCmd(0x10); //---set high column address
WriteCmd(0x40); //--set start line address
WriteCmd(0x81); //--set contrast control register
WriteCmd(0xff); //ÁÁ¶Èµ÷½Ú 0x00~0xff
WriteCmd(0xa1); //--set segment re-map 0 to 127
WriteCmd(0xa6); //--set normal display
WriteCmd(0xa8); //--set multiplex ratio(1 to 64)
WriteCmd(0x3F); //
WriteCmd(0xa4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content
WriteCmd(0xd3); //-set display offset
WriteCmd(0x00); //-not offset
WriteCmd(0xd5); //--set display clock divide ratio/oscillator frequency
WriteCmd(0xf0); //--set divide ratio
WriteCmd(0xd9); //--set pre-charge period
WriteCmd(0x22); //
WriteCmd(0xda); //--set com pins hardware configuration
WriteCmd(0x12);
WriteCmd(0xdb); //--set vcomh
WriteCmd(0x20); //0x20,0.77xVcc
WriteCmd(0x8d); //--set DC-DC enable
WriteCmd(0x14); //
WriteCmd(0xaf); //--turn on oled panel
}
这段代码可以不用管 厂商会给 最后三行代码 则对应着OLED屏打开电荷帮代码 后面会讲
又来了这个表 上面就是OLED屏对应的命令
上面函数最后三个就是 对应 显示开 电荷泵开 序号1、2
在介绍设置坐标函数之前 讲一下OLED屏的布局
128*64 指的是有128列 64行 共128*8个字节
OLED屏中 将128*8字节分成了8页 每页有128*8个比特
128*64中 对应128*64个点 每个点就是一个坐标 这个坐标写0就不亮 写1就亮
如果想点亮整个屏幕 那就需要写128*64个1 熄灭就是128*64个0
OLED屏给的命令3、4、5便可以找到对应的坐标,然后将其写0和1就行
接下来就是void OLED_SetPos(uint8_t x,uint8_t y) //设置OLED屏坐标代码
void OLED_SetPos(uint8_t x,uint8_t y) //设置OLED屏坐标代码 根据上图来的
{
WriteCmd(0xB0+y);
WriteCmd(((x>>4)&0x0F) |0x10);
//WriteCmd((x&0x0F)|0x01);
WriteCmd(x&0x0F);
}
设置坐标
第一行设置y的值 设置第几页 0xB0+y对应序号3
第二行设置列的高地址 x>>4 x左移4位 然后&0x0F 算出高四位 在| 0x10 对应序号5
第三行和第四行都是序号4 设置列的低地址 但是用第4行更好
这边找到了128*64中的坐标 接下来便是写0还是1的事了
void OLED_Fill(uint8_t Fill_Data) //字节填充函数 在OLED屏启动后 先填充一下 防止有脏数据
void OLED_Fill(uint8_t Fill_Data) //字节填充函数 在OLED屏启动后 先填充一下 防止有脏数据
{
uint8_t m,n;
for(m=0;m<8;m++) //总共8页
{
WriteCmd(0xB0+m);//设置页地址
WriteCmd(0x00); //设置列的低地址
WriteCmd(0x10); //设置列的高地址
for(n=0;n<128;n++)
{
WriteData(Fill_Data); //写入数据
}
}
按页写的 每一页的每一列都是从上往下写 所以列的小地址在上
}
一次写一页中的一列地址 一页占8位 故一列写8位 一页有128列 故写128次 然后在写下一列
void OLED_CLS(void) //清屏
void OLED_CLS(void) //清屏
{
OLED_Fill(0x00);
}
清屏函数 直接全部给0 故熄灭
void OLED_ON(void) //打开电荷泵和显示开 和上面厂家给的最后三行一样
void OLED_ON(void) //打开电荷泵等 由上图命令得来
{
WriteCmd(0x8D);
WriteCmd(0x14);
WriteCmd(0xAF);
}
void OLED_OFF(void) //关闭电荷泵和显示
void OLED_OFF(void) //关闭电荷泵等
{
WriteCmd(0x8D);
WriteCmd(0x10);
WriteCmd(0xAE);
}
oid OLED_ShowStr(uint8_t x,uint8_t y,uint8_t *q,uint8_t TextSize)//显示字符串 注意取字模的方式和大小 本文是按列刷 6x8或者8x16
6x8 6指x轴 8指y轴 在y轴上面每8位为一页 一个字符就是从左向右扫
X 0 1 2...6 按列扫 如下图 取模的时候一定要注意低位在前 列行式
扫描6列后一个字符就显示成功 纵坐标为第一页8位
以下几个函数 一定要注意 自己的取模 不然就是错的
void OLED_ShowStr(uint8_t x,uint8_t y,uint8_t *q,uint8_t TextSize)//显示字符串 注意取字模//的方式和大小 本文是按列刷 6x8或者8x16
//6x8 6指x轴 8指y轴 在y轴上面每8位为一页 一个字符就是从左向右扫
//X 0 1 2...6 按列扫 如下图 取模的时候一定要注意低位在前 列行式
{
uint8_t i,c;
switch(TextSize)
{
case 1:
while(*q!='\0')
{
c=*q-32;
if(x>127)
{
x=0;
y++;
}
OLED_SetPos(x,y);
for(i=0;i<6;i++)
{
WriteData(F6x8[c][i]);
}
x+=6;
q++;
}
break;
case 2:
while(*q!='\0')
{
c=*q-32;
if(x>127)
{
x=0;
y++;
}
OLED_SetPos(x,y);
for(i=0;i<8;i++)
{
WriteData(F8X16[c*16+i]);
}
OLED_SetPos(x,y+1);
for(i=0;i<8;i++)
{
WriteData(F8X16[c*16+i+8]);
}
x+=8;
q++;
}
break;
}
}
void OLED_ShowChinese(uint8_t x,uint8_t y,uint8_t *data,uint8_t length)//显示中文
{
int i,j;
for(i=0;i<length;i++)
{
if(x>127)
{
x=0;
y+=2;
}
OLED_SetPos(x,y);
for(j=0;j<16;j++)
{
WriteData(data[i*32+j]);
}
OLED_SetPos(x,y+1);
for(j=0;j<16;j++)
{
WriteData(data[i*32+j+16]);
}
x+=16;
}
}
中文的取模方式是16 x16 显示两个中文 每个中文是由2*16个字节组成。
void OLED_ShowBMP(uint8_t x,uint8_t y,uint8_t *data)//显示一个BMP图片 只支持黑白图
{
int i,j;
for(i=0;i<4;i++)
{
OLED_SetPos(x,y+i);
for(j=0;j<64;j++)
{
WriteData(data[i*64+j]);
}
}
}
//像素为64*32 占4页 每页写64列
以下是完整代码:
main.c
#include "main.h"
#include "led/led.h"
#include "exit/exit.h"
#include "uart/uart.h"
#include "tim/tim.h"
#include "systick/systick.h"
#include "iicoled/iicoled.h"
extern unsigned char F16X16_Chinese[];
extern unsigned char F64x32[];
int main()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
InitLED();
InitExit();
InitUart();
InitTim4General();
InitTim3GeneralPWM();
IICInit();
OLEDInit();
OLED_Fill(0x00);
//OLED_ShowStr(0,0,"Hello",1);
//OLED_ShowStr(0,4,"Hello",2);
//OLED_ShowChinese(0,2,F16X16_Chinese,2);
OLED_ShowBMP(0,0,F64x32);
while(1)
{
ms_delay(500);
GPIO_SetBits(GPIOC,GPIO_Pin_13);
ms_delay(500);
GPIO_ResetBits(GPIOC,GPIO_Pin_13);
}
}
iicoled.c
#include "iicoled/iicoled.h"
#include "iicoled/codetab.h"
void IICInit(void)
{
I2C_InitTypeDef I2C_InitTypeDefStruct;
GPIO_InitTypeDef GPIO_InitTypeDefStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
GPIO_InitTypeDefStruct.GPIO_Mode=GPIO_Mode_AF_OD;
GPIO_InitTypeDefStruct.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitTypeDefStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitTypeDefStruct);
I2C_DeInit(I2C1);
I2C_InitTypeDefStruct.I2C_Ack=I2C_Ack_Enable ;
I2C_InitTypeDefStruct.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;
I2C_InitTypeDefStruct.I2C_ClockSpeed=400000;
I2C_InitTypeDefStruct.I2C_DutyCycle=I2C_DutyCycle_2;
I2C_InitTypeDefStruct.I2C_Mode=I2C_Mode_I2C ;
I2C_InitTypeDefStruct.I2C_OwnAddress1=0x30;
I2C_Init(I2C1,&I2C_InitTypeDefStruct);
I2C_Cmd(I2C1,ENABLE);
}
void IIC_WriteByte(uint8_t addr,uint8_t data)
{
while(I2C_GetFlagStatus(I2C1,I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C1,ENABLE);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1,OLED_ADDR,I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1,addr);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTING));
I2C_SendData(I2C1,data);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTING));
I2C_GenerateSTOP(I2C1,ENABLE);
}
void WriteCmd(uint8_t cmd)
{
IIC_WriteByte(0x00,cmd);
}
void WriteData(uint8_t data)
{
IIC_WriteByte(0x40,data);
}
void OLEDInit(void)
{
// ms_delay(100);
WriteCmd(0xAE); //display off
WriteCmd(0x20); //Set Memory Addressing Mode
WriteCmd(0x10); //00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
WriteCmd(0xb0); //Set Page Start Address for Page Addressing Mode,0-7
WriteCmd(0xc8); //Set COM Output Scan Direction
WriteCmd(0x00); //---set low column address
WriteCmd(0x10); //---set high column address
WriteCmd(0x40); //--set start line address
WriteCmd(0x81); //--set contrast control register
WriteCmd(0xff); //ÁÁ¶Èµ÷½Ú 0x00~0xff
WriteCmd(0xa1); //--set segment re-map 0 to 127
WriteCmd(0xa6); //--set normal display
WriteCmd(0xa8); //--set multiplex ratio(1 to 64)
WriteCmd(0x3F); //
WriteCmd(0xa4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content
WriteCmd(0xd3); //-set display offset
WriteCmd(0x00); //-not offset
WriteCmd(0xd5); //--set display clock divide ratio/oscillator frequency
WriteCmd(0xf0); //--set divide ratio
WriteCmd(0xd9); //--set pre-charge period
WriteCmd(0x22); //
WriteCmd(0xda); //--set com pins hardware configuration
WriteCmd(0x12);
WriteCmd(0xdb); //--set vcomh
WriteCmd(0x20); //0x20,0.77xVcc
WriteCmd(0x8d); //--set DC-DC enable
WriteCmd(0x14); //
WriteCmd(0xaf); //--turn on oled panel
}
void OLED_SetPos(uint8_t x,uint8_t y)
{
WriteCmd(0xB0+y);
WriteCmd(((x>>4)&0x0F) |0x10);
//WriteCmd((x&0x0F)|0x01);
WriteCmd(x&0x0F);
}
void OLED_Fill(uint8_t Fill_Data)
{
uint8_t m,n;
for(m=0;m<8;m++)
{
WriteCmd(0xB0+m);
WriteCmd(0x00);
WriteCmd(0x10);
for(n=0;n<128;n++)
{
WriteData(Fill_Data);
}
}
}
void OLED_CLS(void)
{
OLED_Fill(0x00);
}
void OLED_ON(void)
{
WriteCmd(0x8D);
WriteCmd(0x14);
WriteCmd(0xAF);
}
void OLED_OFF(void)
{
WriteCmd(0x8D);
WriteCmd(0x10);
WriteCmd(0xAE);
}
void OLED_ShowStr(uint8_t x,uint8_t y,uint8_t *q,uint8_t TextSize)
{
uint8_t i,c;
switch(TextSize)
{
case 1:
while(*q!='\0')
{
c=*q-32;
if(x>127)
{
x=0;
y++;
}
OLED_SetPos(x,y);
for(i=0;i<6;i++)
{
WriteData(F6x8[c][i]);
}
x+=6;
q++;
}
break;
case 2:
while(*q!='\0')
{
c=*q-32;
if(x>127)
{
x=0;
y++;
}
OLED_SetPos(x,y);
for(i=0;i<8;i++)
{
WriteData(F8X16[c*16+i]);
}
OLED_SetPos(x,y+1);
for(i=0;i<8;i++)
{
WriteData(F8X16[c*16+i+8]);
}
x+=8;
q++;
}
break;
}
}
void OLED_ShowChinese(uint8_t x,uint8_t y,uint8_t *data,uint8_t length)
{
int i,j;
for(i=0;i<length;i++)
{
if(x>127)
{
x=0;
y+=2;
}
OLED_SetPos(x,y);
for(j=0;j<16;j++)
{
WriteData(data[i*32+j]);
}
OLED_SetPos(x,y+1);
for(j=0;j<16;j++)
{
WriteData(data[i*32+j+16]);
}
x+=16;
}
}
void OLED_ShowBMP(uint8_t x,uint8_t y,uint8_t *data)
{
int i,j;
for(i=0;i<4;i++)
{
OLED_SetPos(x,y+i);
for(j=0;j<64;j++)
{
WriteData(data[i*64+j]);
}
}
}
iicoled.h
#include "main.h"
#include "systick/systick.h"
#define OLED_ADDR 0x78
void IICInit(void);
void IIC_WriteByte(uint8_t addr,uint8_t data);
void WriteCmd(uint8_t cmd);
void WriteData(uint8_t data);
void OLEDInit(void);
void OLED_SetPos(uint8_t x,uint8_t y);
void OLED_Fill(uint8_t Fill_Data);
void OLED_CLS(void);
void OLED_ON(void);
void OLED_OFF(void);
void OLED_ShowStr(uint8_t x,uint8_t y,uint8_t *q,uint8_t TextSize);
void OLED_ShowChinese(uint8_t x,uint8_t y,uint8_t *data,uint8_t length);
void OLED_ShowBMP(uint8_t x,uint8_t y,uint8_t *data);
codetab.h部分代码 这个是字模代码
运行截图
软件IIC明天更新
有问题欢迎讨论 QAQ。