前言
一、项目涉及的内容
项目简介
二、模块实操
1. IIC模块
1.1 IIC协议格式
1.2 开始信号与停止信号
1.3 写数据
1.3.1 硬件IIC代码编写
1.3.2 软件模拟IIC代码编写
2. OLED板块
前言
本篇文章对使用IIC协议操作OLED屏幕进行了总结。
一、项目涉及的内容
本项实验的硬件组成有STM32F103C8T6开发板、OLED模块(0.96寸4针IIC接口OLED显示屏),设计到的软件模块有GPIO、IIC、系统定时器SysTick,接下来对这些主要模块进行讲解,回顾项目的重点,希望大家也能有所收获。
项目简介
通过IIC进行数据传输,操控OLED屏幕显示文字、图像。
二、模块实操
1. IIC模块
IIC(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行总线,用于连接微控制器及其外围设备。一个 I2C 总线只需要使用两条总线线路就可以进行通讯,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线SDA即用来表示数据,时钟线SCL用于数据收发同步。SCL由主机产生,SDA由主机或者从机产生。
I2C是同步串行通信,同时它属于半双工,也就是说同一时间SDA只能由一个设备发送信号。IIC是以字节的方式进行传输的,每次传输8位(一个字节)。
接口是通过SCL与SDA连接到IIC总线上的,接口可以下述4种模式中的一种运行:
● 从发送器模式
● 从接收器模式
● 主发送器模式
● 主接收器模式
模块默认地工作于从模式。接口在生成起始条件后自动地从从模式切换到主模式;当仲裁丢
失(两个及以上的主设备要同时通信时的优先级判定)或产生停止信号时,则从主模式切换到从模式。主模式时, I2C接口启动数据传输并产生时钟信号。串行数据传输总是以起始条件开始并以停止条件结束。
1.1 IIC协议格式
IIC的通讯方式简单概括一下就是:
- 主机产生开始信号,跟在起始条件后的1或2个字节是地址(7位模式为1个字节, 10位模式为2个字节),字节的最后一位确定读/写。从机I2C接口能识别它自己的地址(7位或10位)和广播呼叫地址,I2C中与从机地址相同的从机开始与主机进行通讯。
- 第二个字节传送从机寄存器地址,也就是我们要往哪个地址传输内容,分为数据寄存器和命令寄存器两种,跟名字一样传输到对应寄存器表示要传输数据/命令。
- 第三个字节开始才是传送给从机数据看命令是多少或者数据是多少,主机每发送完一
个字节数据,都要等待从机的应答信号(ACK),重复这个过程,可以向从机传输 N 个数据,
这个 N 没有大小限制。
图片来源:零死角玩转 STM32F103—霸道>>>第24章 I2C—读写EEPROM>>>图 24-4 I2C 通讯复合格式
1.2 开始信号与停止信号
I2C 总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答
信号。
开始信号: SCL 为高电平时, SDA 由高电平向低电平跳变,开始传送数据。
结束信号: SCL 为高电平时, SDA 由低电平向高电平跳变,结束传送数据。
应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,
表示已收到数据。 CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号, CPU 接
收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为
受控单元出现故障。
这些信号中,起始信号是必需的,结束信号和应答信号可以不要。
图片来源:STM32F1xx中文参考手册>>>24 I2C接口>>>24.3 I2C功能描述>>>24.3.1 模式选择
IIC通讯中,数据和地址都是按8位/字节进行传输,高位在前。跟在起始条件后的1或2个字节是地址(7位模式为1个字节, 10位模式为2个字节)。地址只在主模式发送。我们选择的是7位地址模式,可以看到我们发送起始信号之后,在一个字节传输的8个时钟后的第9个时钟期间,接收器必须回送一个应答位(ACK)给发送器。
1.3 写数据
IIC通讯有软件的方式和硬件的方式两种,软件方式引脚定义会更灵活,硬件方式引脚是固定的,但通讯的效率会更高一些。
1.3.1 硬件IIC代码编写
STM32的IIC片上外设专门负责实现IIC通讯协议,只要配置好这个外设,它就会自动根据协议的要求产生通讯信号,收发数据并缓存起来,CPU只要检测该外设的状态和访问数据寄存器就能完成数据收发。
stm32F1系列的硬件I2C外设默认情况下使用PB6和PB7两个引脚。
图片来源:STM32F1xx中文参考手册 >>> 8.3.9 I2C1 复用功能重映射
我们分别配置GPIO口与I2C外设的结构体成员,然后调用库函数GPIO_Init()与 I2C_Init() 把结构体的配置写入到寄存器中。
//硬件IIC初始化配置
void I2C_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd( RCC_APB1Periph_I2C1, ENABLE);
//PB6 --SCL; PB7 --SDA
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOB, &GPIO_InitStructure); //初始化GPIO配置
I2C_DeInit( I2C1); //复位初始化IIC的配置
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //使能响应
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //指定地址长度,可为7或10
I2C_InitStructure.I2C_ClockSpeed = 400000; //设置SCL时钟频率,400kHz
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //时钟占空比,可选low/high = 2:0或16:9
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //指定工作模式为IIC,可选IIC模式及SMBUS模式
I2C_InitStructure.I2C_OwnAddress1 = 0X10; //自身的IIC设备地址
I2C_Init( I2C1, &I2C_InitStructure); //初始化IIC外设配置
I2C_Cmd( I2C1, ENABLE); //使能IIC外设
}
IIC主机发送符合协议内容的数据出去之后,IIC外设会产生相应的时间EVx,也就是IIC外设的相关寄存器会自动置0或置1,我们只要去读取对应的寄存器就可以确定事件是否发生。
图片来源:STM32F1xx中文参考手册>>>24 I2C接口>>>24.3.1 模式选择>>>图245 主发送器传送序列图
图片来源:STM32F1xx中文参考手册>>>24 I2C接口>>>24.3.3 I2C主模式>>>图242 I2C的功能框图
我们要发送数据,要做的有以下几件事情:
- 先检查IIC总线是否有被其他的主机使用
- 如果IIC总线处于空闲状态,我们就使用库函数I2C_GenerateSTART()产生IIC起始信号
- 发送从机地址
- 发送寄存器地址,确定是要输入命令还是数据内容
- 往命令/数据寄存器发送数据内容
- 关闭IIC总线,让IIC总线重新进入空闲状态等待被主机激活
其中除了一开始检查IIC总线是否处于繁忙状态外,每一件事情我们都要去检测对应的EVx事件是否发生,等待每一个事情触发事件后我们继续进行下一步,检测的函数以及对应的事件都已经在文件stm32f10x_i2c.h与stm32f10x_i2c.c中定义好了。
//硬件IIC写字节
void I2C_WriteByte(uint8_t addr, uint8_t data)
{
//先检查I2C总线是否繁忙
while( I2C_GetFlagStatus( I2C1, I2C_FLAG_BUSY));
//开启I2C1
I2C_GenerateSTART( I2C1, ENABLE);
while( !I2C_CheckEvent( I2C1, I2C_EVENT_MASTER_MODE_SELECT)); //检查是否应答,等待触发EV5
//发送器件地址
I2C_Send7bitAddress( I2C1, Slave_Address, I2C_Direction_Transmitter);
while( !I2C_CheckEvent( I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); //等待触发EV6
//发送寄存器地址
I2C_SendData( I2C1, addr);
while( !I2C_CheckEvent( I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)); //等待触发EV8
//发送数据
I2C_SendData( I2C1, data);
while( !I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED) ); //等待触发EV8_2
//关闭I2C1总线
I2C_GenerateSTOP( I2C1, ENABLE);
}
1.3.2 软件模拟IIC代码编写
我们也可以直接控制STM32的两个GPIO引脚分别用作SCL及SDA,然后按照上述信号的时序要求,遵循通讯协议,控制引脚的输出(如果是接收数据则是读取SDA电平),就可以实现IIC通讯。直接控制 GPIO 引脚电平产生通讯时序,由 CPU 控制每个时刻的引脚状态,这种方式称之为“软件模拟协议”方式。
我们进行初始化配置的时候,就不需要配置IIC外设了,只需要对GPIO口进行配置就可以了。
//软件模拟IIC协议初始化
void OLED_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);
//PB0 --SCL; PB1 --SDA
//I2C支持多个主设备与多个从设备连接在同一根总线上,如果不开漏输出,会出现短路现象
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init( GPIOB, &GPIO_InitStructure);
OLED_SCLK_Set(); //初始化时钟总线高电平
OLED_SDIN_Set(); 初始化总线高电平
}
软件模拟IIC时序的话,就要按照IIC协议规定的方式严格执行控制引脚的输出。
图片来源:STM32F1xx中文参考手册>>>24 I2C接口>>>24.3 I2C功能描述>>>24.3.1 模式选择
//模拟I2C起始信号
static void OLED_IIC_Start(void)
{
OLED_SCLK_Set(); //时钟总线高电平
OLED_SDIN_Set(); //数据总线高电平
us_delay(1);
OLED_SDIN_Clr();
us_delay(1);
OLED_SCLK_Clr();
us_delay(1);
}
//模拟I2C结束信号
static void OLED_IIC_Stop(void)
{
OLED_SDIN_Clr(); //数据总线低电平
us_delay(1);
OLED_SCLK_Set(); //时钟总线高电平
us_delay(1);
OLED_SDIN_Set();
us_delay(1);
}
//模拟I2C读取从机应答信号
static unsigned char IIC_Wait_Ack(void)
{
unsigned char ack;
OLED_SCLK_Clr(); //时钟总线低电平
us_delay(1);
OLED_SDIN_Set(); //数据总线高电平
us_delay(1);
OLED_SCLK_Set(); //时钟总线高电平
us_delay(1);
if(OLED_READ_SDIN()) //无应答
{
ack = IIC_NO_ACK;
}
else
{
ack = IIC_ACK;
}
OLED_SCLK_Clr(); //时钟线置低
us_delay(1);
return ack;
}
传输时, SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“ 1”,为低电平时表示数据“0”。当 SCL为低电平时, SDA 的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备。传输数据的时候,将SCL置低,然后设置SDA总线对应的引脚电平为高/低,SDA电平确定后再讲SCL置高,将8个位由高到低依次发送出去。
//IIC传输一个字节,传输时先从高位开始
static void Write_IIC_Byte(unsigned char IIC_Byte)
{
unsigned char i; //定义变量
for(i=0;i<8;i++)
{
OLED_SCLK_Clr(); //时钟线置低
us_delay(1);
if(IIC_Byte & 0x80) //读取最高位
OLED_SDIN_Set(); //最高位为1
else
OLED_SDIN_Clr(); //最高位为0
us_delay(1);
OLED_SCLK_Set(); //时钟线置高,产生上升沿把数据送出去
us_delay(1);
IIC_Byte <<= 1; //数据左移一位
}
OLED_SCLK_Clr(); //时钟线置低
us_delay(1);
IIC_Wait_Ack(); //从机等待(这边应该是要监测返回值是否正确,当不应答时重新进行连接或者暂停传输数据)
}
2. OLED板块
我们用的0.96寸OLED显示屏芯片是SSD1306,这里我们将OLED屏幕作为从机,数据和应答都是通过SDA发送的,SSD1306的IIC通讯接口的数据总线是由SDAout和SDAin输入组成的,SDAin和SDAout绑定到了一起作为SDA,SDAout引脚可以不连接,当不连接时,应答信号将会被IIC总线忽略。
当主机通过开始条件初始化IIC通讯,并且广播的从机地址与我们要接收数据的从机地址相同时,发起开始信号的主机与从机双方将建立通讯,控制字节或数据字节开始通过SDA传输。
数据位的解读是基于D/C#位引脚的输入,如果 D/C#引脚是高,D[7:0]就被解读为写到图像显示数据 RAM( GDDRAM)中的显示数据,GDDRAM 列地址指针将会在每次数据写之后自动加 1。
如果是低, D[7:0]的输入就被解读为一个命令。然后数据输入就会被解码并写到相关的命令
寄存器中。
GDDRAM 是一个为映射静态 RAM 保存位模式来显示。该 RAM 的大小为 128 * 64 为, RAM
分为 8 页,从 PAFE0 到 PAGE7,用于单色 128 * 64 点阵显示,如下图所示
图片来源:SSD1306-OLED驱动芯片中文手册
SSD1306 中有三种不同的内存地址模式:页地址模式,水平地址模式,垂直地址模式。我们使用页地址模式,在页地址模式下,在显示 RAM 读写之后,列地址指针自动加一。如果列地址指针达到了列的结束地址,列地址指针重置为列开始地址并且页地址指针不会改变。用户需要设置新的页
和列地址来访问下一页 RAM 内容。页地址模式下 PAGE 和列地址指针的移动模式参考下图
使用下面的步骤来定义开始 RAM 访问的位置:
1. 通过命令 B0h 到 B7h 来设置目标显示位置的页开始地址
2. 通过 00h~0Fh 来设置低开始列地址的指针
3. 通过命令 10h~1Fh 来设置高开始列地址
比如说,如果页地址设置为 B2h,低列地址是 03h 高列地址为 00h,那么就意味着开始列是
PAGE2 的 SEG3.RAM 访问指针的位置如下图所示。输出数据字节将写到 RAM 列 3 的位置。
OLED屏幕的初始化代码用到的指令都是芯片手册上有固定的格式,厂家一般会提供初始化代码,格式略有不同,这部分就不贴出来了。
下边代码分别是设置屏幕显示的位置、显示字符、显示字符串、显示中文、显示图片
//设置数据写入的起始行,列
//x:列的起始低地址与起始高地址
//y:起始页地址 0-7
void OLED_Set_Pos(unsigned char x, unsigned char y)
{
OLED_WR_Byte( 0xb0+y, OLED_CMD); //写入页地址
OLED_WR_Byte( x&0x0f, OLED_CMD); //写入列的地址,低半个字节
OLED_WR_Byte( (x&0xf0)>>4 | 0x10, OLED_CMD); //写入列地址,高半个字节
}
- 设置OLED屏幕显示ASCII码字符,有8*16与6*8两种字符大小
//显示字符
void OLED_ShowChar(unsigned char x, unsigned char y, unsigned char chr)
{
unsigned char j = 0;
chr = chr - ' '; //获取字符的偏移量
if( x >= Max_Column)//让x限制在128列内
x=0;
if( SIZE == 16) //字符8*16(高度是16,所以要分为两个上下两部分来发送)
{
OLED_Set_Pos(x,y); //上半段
for(j=0; j<8; j++)
{
OLED_WR_Byte( F8X16[chr*16+j],OLED_DAT);
}
OLED_Set_Pos(x,y+1); //下半段
for(j=0; j<8; j++)
{
OLED_WR_Byte( F8X16[chr*16+j+8],OLED_DAT);
}
}
else if( SIZE == 6) //字符6*8
{
OLED_Set_Pos(x,y);
for(j=0; j<6; j++)
OLED_WR_Byte( F6x8[chr*8][j],OLED_DAT);
}
}
//显示字符串
void OLED_ShowString(unsigned char x, unsigned char y, unsigned char *chr)
{
unsigned char j=0;
while(chr[j] != '\0') //判断是不是最后一个字符
{
if(SIZE == 16)
{
if( x >= Max_Column)
{
x=0;
y=y+2;
}
OLED_ShowChar(x,y,chr[j]); //显示字符
x=x+8; //一个字符占8列
}
else if(SIZE == 6)
{
if( x >= Max_Column)
{
x=0;
y=y+1;
}
OLED_ShowChar(x,y,chr[j]); //显示字符
x=x+6; //一个字符占6列
}
j++; //下一个字符
}
}
//显示文字
void OLED_ShowChinese(unsigned char x, unsigned char y, unsigned char list)
{
unsigned char i=0;
OLED_Set_Pos(x, y); //中文字体上半部分
for(i=0;i<16;i++)
{
OLED_WR_Byte( F16x16[list*32+i], OLED_DAT); //一个字体占用了16+16个字节,所以是list乘以32,下半部分就再加上16
}
OLED_Set_Pos(x,y+1); //中文字体下半部分
for(i=0;i<16;i++)
{
OLED_WR_Byte( F16x16[list*32+16+i], OLED_DAT);
}
}
//显示图片
void OLED_DrawBMP(unsigned char x0, unsigned char y0, unsigned char x1, unsigned char y1, unsigned char BMP[])
{
unsigned int j =0;
unsigned char x,y;
if(y1%8 == 0)
y = y1/8;
else
y = y1/8 +1;
for(y=y0;y<y1;y++)
{
OLED_SetPos(x0,y);
for(x=x0;x<x1;x++)
{
WriteDat(BMP[j++]);
}
}
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)