目录
前言
一、项目内容
实验简介
二、IIC模块
1、IIC协议简介
2、物理层
3、协议层
4、硬件IIC代码配置
5、软件模拟IIC配置
1、起始信号与停止信号
2、从机应答信号
3、数据的有效性
4、数据传输
三、OLED模块
1、软件配置
2、OLED原理
1、OLED初始化函数
2、写入起始坐标
3、清屏函数
4、显示字符串
5、显示文字
6、显示图片(bmp)
4、总结
本篇文章对IIC通信协议的原理做了总结,并在硬件IIC配置和软件模拟IIC上做了代码输出,由此来进行一个OLED屏幕的操作总结。
本项实验的硬件组成有STM32F103C8T6芯片的开发板、OLED模块(0.96寸4针IIC接口OLED显示屏),时间用系统滴答定时器SysTick,主要模块配置是硬件IIC配置和软件模拟IIC配置。
根据IIC通讯原理,来进行数据传输,进行一个OLED屏幕的显示,文字,图像。
IIC通讯协议(Inter---Integrted Circuit)是由Phiips飞利浦公司开发的,由于他引脚少,硬件实现简单,可拓展性强,不需要UASRT,CAN通讯协议的外部收发设备,现在被广泛使用在系统内多个集成电路IC(芯片)间的通讯。
IIC模块接收和发送数据,并将数据从串行转换成并行,或并行转换成串行,可以开启或禁止中 断。同步串行也就是半双工的通讯方式,接口通过数据引脚(SDA)和时钟引脚(SCL)连接到IIC总线。允许连接到标准(高达100kHz)或 快速(高达400kHz)的IIC总线。
接口可以下述4种模式中的一种运行:
1、从发送器模式
2、从接收器模式
3、主发送器模式(本文学习)
4、主接收器模式
该模块默认地工作于从模式。接口在生成起始条件后自动地从从模式切换到主模式;当仲裁丢失或产生停止信号时,则从主模式切换到从模式。允许多主机功能。
图片来源:零死角玩转 STM32F103—霸道>>>第24章 I2C—读写EEPROM>>>常见的 I2C 通讯系统
它的物理层有如下特点:
1、它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线 中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
2、 一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线 (SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
3、总线通过上拉电阻接到电源,当IIC设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态,由上拉电阻把总线拉成高电平。
4、多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定哪个设备占用总线。
具有三种传输模式:标准模式传输速率为100kbit/s,快速模式为400kbit/s,高速模式下可达3.4M/s,但目前大多1IC设备尚不支持高速模式。
当然,本次要分两个本分来写,硬件IIC和软件IIC。
硬件IIC:对应芯片上的IIC外设,有相对应的IIC驱动电路,其所使用的IIC脚位也是专用的。速度快并可用于DMA。
软件IIC:一般是用GPIO脚位,用软件控制管脚状态以及模拟IIC通信波形,可以在任何脚位上来模拟。
区别:硬件IIC的效率要远高于软件的,而软件IIC不受引脚限制,接口比较灵活。
软件IIC是通过GPIO,软件模拟寄存器的工作方式,而硬件IIC是直接调用内部寄存器进行配
置。如果要从具体硬件上来看,可以去看下芯片手册。因为固件IIC的端口是固定的,所以会有所区别。
IIC 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
图片来源:零死角玩转 STM32F103—霸道>>>第24章 I2C—读写EEPROM>>>图 24-4 I2C 通讯复合格式
IIC通讯概括
1、其中 S 表示由主机的 I2C 接口产生的传输起始信号(S),这时连接到 I2C 总线上的所有从机都会接收到这个信号。 起始信号产生后,所有从机就开始等待主机紧接下来广播的从机地址信号 (SLAVE_ADDRESS)。
2、在 I2C 总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,根据 I2C协议,这个从机地址可以是 7 位或 10 位。 在地址位之后,是传输方向的选择位,该位为 0即主机向从机写数据。该位为1时,则相反,即主机由从机读数据。在此传输后选择数据寄存器和命令寄存器两种,传输到对应寄存器表示要传输数据/命令。
3、从机接收到匹配的地址后,主机或从机会返回一个应答(ACK)或非应答(NACK)信号, 只有接收到应答信号后,主机才能继续发送或接收数据。重复这个过程,可以向从机传输N个数据, 这个N没有大小限制
4、主要有以下4个部分,①起始信号与停止信号,②应答信号,③数据的有效性,④数据传输。在后面软件模拟IIC中进行一个详细概括。
STM32f10x的IIC片上外设专门负责实现IIC通讯协议,它们的IIC 通讯信号引出到不同 GPIO引脚上,使用时必须配置到这些指定的引脚自动根据协议的要求产生通讯信号,收发数据并缓存起来,CPU只要检测该外设的状态和访问数据寄存器就能完成数据收发。
图片来源:STM32F1xx中文参考手册 >>> 8.3.9 I2C1 复用功能重映射>>>表48
为此我们一般用的PB6和PB7的引脚。在此之中需要对GPIO的引脚进行配置和IIC功能进行配置。其中配置的内容在代码中有注释。
#include "stm32f10x.h"
#include "oled.h"
#include "SysTick.h"
#include "codetab.h"
void I2C_Configuration(void)
{
I2C_InitTypeDef I2C_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1 , ENABLE );
I2C_DeInit( I2C1);
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//使能应答位
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//指定地址长度,可为7或者10
I2C_InitStructure.I2C_ClockSpeed = 400000;//时钟速度400kHZ,参数值不高于400KHZ
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//时钟占空比,可选low/high(低电平比高电平)= 2:0或16:9
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;//模式
I2C_InitStructure.I2C_OwnAddress1 = 0x30;//主机地址,可为7或者10位,但只有IIC1能配置10位的
I2C_Init( I2C1, &I2C_InitStructure); //初始化I2C外设配置
I2C_Cmd(I2C1,ENABLE); //使能I2C外设
//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);
}
使用 I2C 外设通讯时,在通讯的不同阶段它会对“状态寄存器(SR1 及 SR2)”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。
图片来源:STM32F1xx中文参考手册>>>24 I2C接口>>>24.3.1 模式选择>>>图245 主发送器传送序列图
主发送器的流程和事件说明在图中有说明,所以IIC写入字节参数函数需要流程如下,都是在IIC.h文件中查找相应的结构体配置。
//I2C写入字节参数
void I2C_WriteByte(uint8_t addr,uint8_t data)
{
while ( I2C_GetFlagStatus( I2C1, I2C_FLAG_BUSY)); //检查I2C总线是否繁忙
I2C_GenerateSTART(I2C1, ENABLE ); //开启I2C1
while ( !I2C_CheckEvent( I2C1, I2C_EVENT_MASTER_MODE_SELECT)); //检查状态EV5,主模式
I2C_Send7bitAddress( I2C1, OLED_ADDRESS , I2C_Direction_Transmitter); //发送器件(OLED)地址
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 ); //关闭I2C总线
}
软件IIC是通过GPIO管脚,软件模拟寄存器的工作方式,用软件来控制管脚的状态以及模拟IIC通讯的波形,从而达到一个IIC通讯过程。在下面就详细的来编写IIC软件模拟通讯的4个过程,①起始信号与停止信号②应答信号③数据的有效性④数据传输
其中的定时器是系统滴答定时器SysTick,详细可以看STM32的SysTick系统定时器_努力学习的多云的博客-CSDN博客
起始信号:当SCL为高电平期间,SDA有高到低的跳变;启动信号是一种电平跳变时序信号,而不是一个电平信号。
停止信号:当SCL为高电平期间,SDA由低到高的跳变;停止信号也是一种高电平跳变时序信号,而不是一个电平信号。
起始信号和停止信号一般由主机产生。
数据和地址按8位/字节进行传输,高位在前。跟在起始条件后的1或2个字节是地址(7位模式为1 个字节,10位模式为2个字节)。地址只在主模式发送。
图片来源:STM32F1xx中文参考手册>>>24 I2C接口>>>24.3 I2C功能描述>>>24.3.1 模式选择
以下为模拟起始信号和停止信号代码,软件模拟工作波形完成
#include "stm32f10x.h" // Device header
#include "oled_soft.h"
#include "SysTick.h"
#include "codetab.h"
static void OLED_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );
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 );
//IIC总线的SCL和SDA空闲状态下两条信号线处于高电平
OLED_SCLK_Set(); //设PB0(SCL)为高电平
OLED_SDAT_Set(); //设PB1(SDA)为高电平
}
//模拟IIC的起始信号
static void OLED_IIC_Start(void)
{
OLED_SCLK_Set(); //时钟总线高电平
OLED_SDAT_Set(); //数据总线高电平
us_delay(1);
OLED_SDAT_Clr();
us_delay(1);
OLED_SCLK_Clr();
us_delay(1);
}
//模拟IIC的停止信号
static void OLED_IIC_Stop(void)
{
OLED_SDAT_Clr();
us_delay(1);
OLED_SCLK_Set(); //时钟总线高电平
us_delay(1);
OLED_SDAT_Set(); //数据总线高电平
us_delay(1);
}
为了更加直观方便的编写,我们可以在.h文件中进行宏定义
#ifndef _OLED_SOFT_H_
#define _OLED_SOFT_H_
#include "stm32f10x.h"
#define OLED_SCLK_Set() GPIO_SetBits( GPIOB, GPIO_Pin_0) // PB0(SCL)输出高
#define OLED_SCLK_Clr() GPIO_ResetBits( GPIOB, GPIO_Pin_0) // PB0(SCL)输出低
#define OLED_SDAT_Set() GPIO_SetBits( GPIOB, GPIO_Pin_1) // PB1(SDA)输出高
#define OLED_SDAT_Clr() GPIO_ResetBits( GPIOB, GPIO_Pin_1) // PB1(SDA)输出低
#define OLED_Read_SDAT() GPIO_ReadInputDataBit( GPIOB, GPIO_Pin_1) //读取PB1(SDA)电平
//应答信号为低电平时,为应答位(ACK)
//应答信号为高电平时,为非应答位(NACK)
#define IIC_ACK 0 //应答
#define IIC_NACK 1 //不应答
#define OLED_CMD 0 //写命令
#define OLED_DATA 1 //写数据
#define SIZE 16 //显示字符大小
#define Max_Column 128 //最大列数
#endif
发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位)表示接收器已经成功地接收了该字节:应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
对于反馈有效应答位ACK的要求是,接收器在第九个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。
在图中红圈可以看到,SCL为低电平后SDA再升为高电平,再SCL变为高电平,由此我们可以进行一个软件模拟IIC波形。
代码如下:返回一个应答信号
//模拟IIC读取从机应答信号
static unsigned char IIC_Wait_Ack(void)
{
unsigned char ack;
OLED_SCLK_Clr(); //时钟线置低
us_delay(1);
OLED_SDAT_Set(); //数据总线高电平
us_delay(1);
OLED_SCLK_Set(); //时钟总线高电平
us_delay(1);
if ( OLED_Read_SDAT() ) //读取PB1(SDA)电平
{
ack = IIC_NACK; //1,不应答
}
else
{
ack = IIC_ACK; //0,应答
}
OLED_SCLK_Clr(); //时钟线置低
us_delay(1);
return ack;
}
IIC总线进行数据传输时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。SDA数据线在 SCL的每个时钟周期传输一位数据。
即:数据在SCL的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
看3、数据的有效性中,数据位的传输是边沿触发的。在IIC总线上数据传输,传送的每一位数据都有一个时钟脉冲相对应,SCL 为高电平时SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“ 1”,为低电平时表示数据“0”。即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。
传输数据的时候,将SCL置低,然后设置SDA总线对应的引脚电平为高/低,SDA电平确定后再讲SCL置高,将8个位由高到低依次发送出去。
//IIC写入一个字节Byte
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) //读取最高位 与上1000 0000
{
OLED_SDAT_Set(); //最高位为1
}
else
{
OLED_SDAT_Clr(); //最高位为0
}
us_delay(1);
OLED_SCLK_Set(); //时钟总线置高电平,产生上升沿,把数据发送出去
us_delay(1);
IIC_Byte <<= 1; //数据左移一位
}
OLED_SCLK_Clr(); //时钟线置低
us_delay(1);
while (IIC_Wait_Ack()); //等待从机应答信号
}
//IIC写命令
static void Write_IIC_Command(unsigned char IIC_Command)
{
OLED_IIC_Start();
Write_IIC_Byte(0x78); //写入从机地址
Write_IIC_Byte(0x00); //写入命令
Write_IIC_Byte(IIC_Command); //写命令
OLED_IIC_Stop(); //发送停止信号
}
//IIC写数据
static void Write_IIC_Data(unsigned char IIC_Data)
{
OLED_IIC_Start();
Write_IIC_Byte(0x78); //写入从机地址
Write_IIC_Byte(0x40); //写入命令
Write_IIC_Byte(IIC_Data); //写数据
OLED_IIC_Stop(); //发送停止信号
}
//对OLED写入一个字节Byte
void OLED_Write_Byte(unsigned char dat, unsigned char cmd)
{
if(cmd)
{
Write_IIC_Data(dat); //写入数据
}
else
{
Write_IIC_Command(dat); //写入命令
}
}
在这里我们所需要用到的是文字取模软件是PCtoLCD2002(字符模式),其中要取模的话选择C51格式,图片显示的话是需要转换成BMP格式,然后在Lmage2Lcd图片取模软件中进行取模。
本次实验用的是OLED模块(0.96寸4针IIC接口OLED显示屏)SSD1306,电源电压3.3-5.5V,总共四个接口,VCC,GND,SCL(IIC总线时钟信号),SDA(IIC总线数据信号)。
SSD1306是一个为映射静态 RAM 保存位模式来显示。该 RAM 的为 128 * 64 bit大小,RAM 分为 8 页,从 PAFE0 到 PAGE7,用于单色 128 * 64 点阵显示,如下图所示
当一个数据字节写到 GDDRAM 中,所有当前列的同一页的行图像数据都会被被填充(比如, 被列地址指针指向的整列(8 位)都会被填充)。数据位 D0 写到顶行,而数据位 D7 写到底行,如下图所示
下面这幅图会更简单懂
一般OLED出厂厂家会给一份初始化函数,简单来说就是对OLED写入各种指令,每个都会不太一样,这里就粘贴本次实验的
//OLED屏幕初始化
void OLED_Init(void)
{
OLED_GPIO_Init(); //GPIO口初始化
ms_delay(200); //延迟,由于单片机上电初始化比OLED快,所以必须加上延迟,等待OLED上复位完成
OLED_Write_Byte(0xAE,OLED_CMD); //关闭显示
OLED_Write_Byte(0x00,OLED_CMD); //设置低列地址
OLED_Write_Byte(0x10,OLED_CMD); //设置高列地址
OLED_Write_Byte(0x40,OLED_CMD); //设置起始行地址
OLED_Write_Byte(0xB0,OLED_CMD); //设置页地址
OLED_Write_Byte(0x81,OLED_CMD); // 对比度设置,可设置亮度
OLED_Write_Byte(0xFF,OLED_CMD); // 265
OLED_Write_Byte(0xA1,OLED_CMD); //设置段(SEG)的起始映射地址;column的127地址是SEG0的地址
OLED_Write_Byte(0xA6,OLED_CMD); //正常显示;0xa7逆显示
OLED_Write_Byte(0xA8,OLED_CMD); //设置驱动路数(16~64)
OLED_Write_Byte(0x3F,OLED_CMD); //64duty
OLED_Write_Byte(0xC8,OLED_CMD); //重映射模式,COM[N-1]~COM0扫描
OLED_Write_Byte(0xD3,OLED_CMD); //设置显示偏移
OLED_Write_Byte(0x00,OLED_CMD); //无偏移
OLED_Write_Byte(0xD5,OLED_CMD); //设置震荡器分频
OLED_Write_Byte(0x80,OLED_CMD); //使用默认值
OLED_Write_Byte(0xD9,OLED_CMD); //设置 Pre-Charge Period
OLED_Write_Byte(0xF1,OLED_CMD); //使用官方推荐值
OLED_Write_Byte(0xDA,OLED_CMD); //设置 com pin configuartion
OLED_Write_Byte(0x12,OLED_CMD); //使用默认值
OLED_Write_Byte(0xDB,OLED_CMD); //设置 Vcomh,可调节亮度(默认)
OLED_Write_Byte(0x40,OLED_CMD); 使用官方推荐值
OLED_Write_Byte(0x8D,OLED_CMD); //设置OLED电荷泵
OLED_Write_Byte(0x14,OLED_CMD); //开显示
OLED_Write_Byte(0xAF,OLED_CMD); //开启OLED面板显示
OLED_Clear(); //清屏
OLED_Set_Pos(0,0); //设置数据写入的起始行、列
}
比如说,如果页地址设置为 B2h,低列地址是 03h 高列地址为 00h,那么就意味着开始列是
PAGE2 的 SEG3.RAM 。输出数据字节将写到 RAM 列 3 的位置。简单的OLED屏幕指令表如下
由此我们进行一个各个函数的编写
//设置数据写入的起始坐标(行和列)
//x:列的起始低地址与起始高地址
//y:页的起始页的地址 0-7
void OLED_Set_Pos(unsigned char x,unsigned char y)
{
OLED_Write_Byte(0xb0+y,OLED_CMD); //写入页地址
OLED_Write_Byte((x&0x0f),OLED_CMD); //写入列的地址,低半个字节
OLED_Write_Byte((x&0xf0)>>4 | 0x10,OLED_CMD); //写入列地址,高半个字节
}
//全屏填充
//SSD1306显存总共为128*64bit大小,分为8页,每页128个字节
void OLED_Fill(unsigned char Fill_Data)
{
unsigned char m,n;
for (m=0;m<8;m++)
{
OLED_Write_Byte(0xb0 +m,OLED_CMD); //从0-7页依次写入
OLED_Write_Byte(0x00,OLED_CMD); //列低地址
OLED_Write_Byte(0x10,OLED_CMD); //列高地址
for (n=0;n<128;n++)
{
OLED_Write_Byte(Fill_Data,OLED_DATA);
}
}
}
//清屏函数
void OLED_Clear(void)
{
OLED_Fill(0x00);
}
//显示字符
void OLED_ShowChar(unsigned char x,unsigned char y,unsigned char chr)
{
unsigned char c = 0,i = 0;
c = chr - ' '; //获取字符的偏移量
if(x > Max_Column)
{
x = 0; //如果列超出范围(128),就从下两页的第0列开始
y = y+2; //
}
if(SIZE == 16) //字符大小如果为16*8
{
OLED_Set_Pos(x , y); //从x,y开始画点
for(i=0;i<8;i++)
{
OLED_Write_Byte(F8X16[c*16+i],OLED_DATA); //找出一个字符C的数组位数,先把第一列画完
}
OLED_Set_Pos(x , y+1); //开始换页
for(i=0;i<8;i++)
{
OLED_Write_Byte(F8X16[c*16+i+8],OLED_DATA); //找出一个字符C的数组位数,先把第一列画完
}
}
else if(SIZE == 6) //如果为6*8
{
OLED_Set_Pos(x , y); //从x,y开始画点
for(i=0;i<6;i++)
OLED_Write_Byte(F6x8[c*8][i],OLED_DATA); //找出一个字符C的数组位数,先把第一列画完
}
}
//显示字符串
void OLED_ShowString(unsigned char x, unsigned char y, unsigned char *str)
{
unsigned char j=0;
while(str[j] != '\0') //判断是否为最后一个字符
{
OLED_ShowChar(x,y,str[j]); //显示字符
x+=8; //列数+8,一个字符占8
if(x >= Max_Column)
{
x = 0;
y +=2;
}
j++;
}
}
//显示文字,想显示哪个直接给数字即可
void OLED_ShowChinese(unsigned char x,unsigned char y,unsigned char num)
{
unsigned char t,addr=0;
OLED_Set_Pos(x,y);
for(t=0;t<16;t++)
{
OLED_Write_Byte(Hzk[2*num][t],OLED_DATA); //画num在数组位置的第1页16列的点
addr+=1; //数组地址+1
}
OLED_Set_Pos(x,y+1);
for(t=0;t<16;t++)
{
OLED_Write_Byte(Hzk[2*num+1][t],OLED_DATA); //画num在数组位置的第2页16列的点
addr+=1; //数组地址+1
}
}
//显示图片
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
本篇文章主要是对IIC通讯协议的原理,和OLED模块的使用做了一个总结,虽然看起来多,但涉及的内容全面,一个资料一个资料总结的,认真看完肯定大有收获,如有错误和不对的地方,望及时联系改正,谢谢大家。在此放个图片给大家看看,是不是很神奇。