一、I2C协议介绍
(一)概述
I2C(Inter-Integrated Circuit)是一种由飞利浦公司(现为恩智浦半导体)开发的串行通信协议,广泛用于连接低速外围设备到处理器或微控制器。I2C协议使用两根线进行通信:SDA(串行数据线)和SCL(串行时钟线)。
- 多主多从:支持多个主设备和从设备在同一总线上通信。同一时刻只有一个主机。
- I双线制:2C总线只需要一根数据线和一根时钟线两根(SDA、SCL)。
- 地址寻址:每个从设备都有一个唯一的7位或10位地址,主设备通过地址选择通信对象。
- 半双工通讯:数据在同一根线上双向传输,但不能同时进行。
- 模式:标准模式(100 kbps)、快速模式(400 kbps)、高速模式(3.4 Mbps)等。
(二)基本电路
(三)通讯协议
1、起始信号和终止信号
SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号;
SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号。
2、数据状态
I2C的SDA线和SCL线在空闲时为高电平状态。在进行数据传输时,SCL为高电平期间SDA必须保持稳定(可改变电平状态);只有在SCL为低电平期间,SDA才允许改变电平状态。在SCL为高电平期间,SDA线的任何改变都会被视为总线发出的起始信号或终止信号。
3、应答/非应答信号
每当主机向从机发送完一个字节的数据,主机总是需要等待从机给出一个应答信号,以确认从机是否成功接收到了数据。
- 应答信号为低电平时,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节;
- 应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
4、I2C传输协议
主设备和从设备进行数据传输时遵循以下协议格式。数据通过一条SDA数据线在主设备和从设备之间传输0和1的串行数据。串行数据序列的结构可以分为:
二、STM32的软件I2C实现基本代码
oled.h文件:(只展示一些宏定义)
#ifndef __OLED_H
#define __OLED_H
#include "main.h"
#define OLED_I2C_SDA GPIO_PIN_6 // SDA<----->PB6
#define OLED_I2C_SCL GPIO_PIN_7 // SCL<----->PB7
#define OLED_I2C_PORT GPIOB
#define OLED_CLK_ENABLE __HAL_RCC_GPIOB_CLK_ENABLE() // 使能I2C的GPIO时钟
#define SCL_PIN_SET HAL_GPIO_WritePin(OLED_I2C_PORT, OLED_I2C_SCL, GPIO_PIN_SET)
#define SCL_PIN_RESET \
HAL_GPIO_WritePin(OLED_I2C_PORT, OLED_I2C_SCL, GPIO_PIN_RESET)
#define SDA_PIN_SET HAL_GPIO_WritePin(OLED_I2C_PORT, OLED_I2C_SDA, GPIO_PIN_SET)
#define SDA_PIN_RESET \
HAL_GPIO_WritePin(OLED_I2C_PORT, OLED_I2C_SDA, GPIO_PIN_RESET)
#endif //__OLED_H
(一)初始化GPIO
void OLED_I2C_Init(void) {
// 1、使能GPIO时钟
OLED_CLK_ENABLE;
// 2、初始化I2C使用到的GPIO口
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = OLED_I2C_SCL | OLED_I2C_SDA; // SCL,SDA
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推完输出模式
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速模式
HAL_GPIO_Init(OLED_I2C_PORT, &GPIO_InitStruct);
// 空闲时SCL和SDA都是高电平
HAL_GPIO_WritePin(OLED_I2C_PORT, OLED_I2C_SCL, GPIO_PIN_SET);
HAL_GPIO_WritePin(OLED_I2C_PORT, OLED_I2C_SDA, GPIO_PIN_SET);
}
(二)起始终止信号
// 发送I2C起始信号
void I2C_Start(void) {
SDA_PIN_SET;
delay_us(2); // 将SDA拉高
SCL_PIN_SET;
delay_us(2); // 将SCL拉高
SDA_PIN_RESET;
delay_us(2); // 将SDA拉低
SCL_PIN_RESET;
delay_us(2); // 将SCL拉低
}
// 发送I2C终止信号
void I2C_Stop(void) {
SDA_PIN_RESET;
delay_us(2); // 将SDA拉低
SCL_PIN_SET;
delay_us(2); // 将SCL拉高
SDA_PIN_SET;
delay_us(2); // 将SDA拉高
}
(三)发送一个byte数据
void I2C_SendByte(u8 data) {
for (u8 i = 0; i < 8; i++) { // 将data的8位从最高位依次写入
SCL_PIN_RESET;
delay_us(2);
if (data & 0x80) {
SDA_PIN_SET;
} else {
SDA_PIN_RESET;
}
delay_us(2);
SCL_PIN_SET;
delay_us(2);
SCL_PIN_RESET;
delay_us(2);
data <<= 1; // 左移一位
}
}
(四)接收一个byte数据
在OLED显示中用不到:
//读取一个字节的数据
u8 I2C_ReadByte(u8 ack){
SCL_PIN_RESET;
SDA_IN(); //设置SDA为输入模式
u8 ret = 0x00;
for(u8 index = 0; index < 8; index++){
//接收顺序 : 高位 ---> 低位
SCL_PIN_RESET;
delay_us(2);
SCL_PIN_SET;
ret <<= 1;
if(READ_I2C_SDA() == GPIO_PIN_SET) //读取SDA电平值
ret++;
}
if(ack){
I2C_PIN_ACK();
} else {
I2C_PIN_NACK();
}
SCL_PIN_RESET;
return ret;
}
(五)应答/非应答信号
//等待从机发送应答码
u8 I2C_WaitAck(void) {
SCL_PIN_SET;
delay_us(2);
SCL_PIN_RESET;
delay_us(2);
return 1;
}
//发送应答码ACK
void MY_I2C_ACK(void){
/*
*/
SCL_PIN_RESET;
I2C_SDA_OUT(); //SDA输出模式
SDA_PIN_RESET;
delay_us(2);
SCL_PIN_SET;
delay_us(2);
SCL_PIN_RESET;
}
//发送非应答码NACK
void MY_I2C_NACK(void){
SCL_PIN_RESET;
SDA_OUT(); //SDA输出模式
SDA_PIN_SET;
delay_us(2);
SCL_PIN_SET;
delay_us(2);
SCL_PIN_RESET;
}
三、SSD1306 0.96寸OLED显示屏
(一)上电流程
(二)写数据格式及流程
1、写数据格式
2、写数据流程

3、代码实现写一个byte数据
void OLED_SendByte(u8 data, u8 cmd) {
I2C_Start(); // 发送一个起始信号
I2C_SendByte(OLED_ADDR); // 发送 OLED地址
I2C_WaitAck(); // 等待ack
if (cmd) { //写命令 or 写数据
I2C_SendByte(0x40); //写命令
} else {
I2C_SendByte(0x00); //协数据
}
I2C_WaitAck(); // 等待ack
I2C_SendByte(data); //写入的数据or命令
I2C_WaitAck();
I2C_Stop(); //发送停止信号
}
4、SSD1306 OLED初始化代码
OLED_SendByte(0xAE,OLED_CMD); //关闭显示
OLED_SendByte(0xD5,OLED_CMD); //设置显示时钟分频比/振荡器频率
OLED_SendByte(0x80,OLED_CMD);
OLED_SendByte(0xA8,OLED_CMD); //设置多路复用率
OLED_SendByte(0x3F,OLED_CMD);
OLED_SendByte(0xD3,OLED_CMD); //设置显示偏移
OLED_SendByte(0x00,OLED_CMD);
OLED_SendByte(0x40,OLED_CMD); //设置显示开始行
OLED_SendByte(0xA1,OLED_CMD); //设置左右方向,0xA1正常 0xA0左右反置
OLED_SendByte(0xC8,OLED_CMD); //设置上下方向,0xC8正常 0xC0上下反置
OLED_SendByte(0xDA,OLED_CMD); //设置COM引脚硬件配置
OLED_SendByte(0x12,OLED_CMD);
OLED_SendByte(0x81,OLED_CMD); //设置对比度控制
OLED_SendByte(0xCF,OLED_CMD);
OLED_SendByte(0xD9,OLED_CMD); //设置预充电周期
OLED_SendByte(0xF1,OLED_CMD);
OLED_SendByte(0xDB,OLED_CMD); //设置VCOMH取消选择级别
OLED_SendByte(0x30,OLED_CMD);
OLED_SendByte(0xA4,OLED_CMD); //设置整个显示打开/关闭
OLED_SendByte(0xA6,OLED_CMD); //设置正常/倒转显示
OLED_SendByte(0x8D,OLED_CMD); //设置充电泵
OLED_SendByte(0x14,OLED_CMD);
OLED_SendByte(0xAF,OLED_CMD); //开启显示
(三)OLED操作
//清屏
void OLED_Clear(void) {
for (u8 row = 0; row < 128; row++) {
for (u8 column = 0; column < 8; column++)
OLED_RAM[row][column] = 0x00;
}
OLED_Flush();
}
//将OLED_RAM数据写入屏幕,即屏幕刷新
void OLED_Flush(void) {
u8 i, n;
for (i = 0; i < 8; i++) {
OLED_SendByte(0xb0 + i, OLED_CMD); // 设置行起始地址
OLED_SendByte(0x00, OLED_CMD); // 设置低列起始地址
OLED_SendByte(0x10, OLED_CMD); // 设置高列起始地址
for (n = 0; n < 128; n++)
OLED_SendByte(OLED_RAM[n][i], OLED_DATA);
}
}
void OLED_DisplayOn(void) {
OLED_SendByte(0x8D, OLED_CMD); // 电荷泵使能
OLED_SendByte(0x14, OLED_CMD); // 开启电荷泵
OLED_SendByte(0xAF, OLED_CMD); // 点亮屏幕
}
void OLED_displayOff(void) {
OLED_SendByte(0x8D, OLED_CMD); // 电荷泵使能
OLED_SendByte(0x10, OLED_CMD); // 关闭电荷泵
OLED_SendByte(0xAE, OLED_CMD); // 关闭屏幕
}
/*
* 在屏幕的(x, y)位置画一个点
* x:x坐标(0~127)
* y:y坐标(0~63)
* */
void OLED_DrawPoint(u8 x, u8 y) {
u8 i, m, n;
if (x < 0 || x > 127 || y < 0 || y > 63) {
printf("Function OLED_DrawPoint parameter error\r\n");
printf("0 <= x <= 127, 0 <= y <= 63\r\n");
return;
}
i = y / 8;
m = y % 8;
n = 1 << m;
OLED_RAM[x][i] |= n;
}
void OLED_ClearPoint(u8 x, u8 y) {
u8 i, m, n;
if (x < 0 || x > 127 || y < 0 || y > 63) {
printf("Function OLED_DrawPoint parameter error\r\n");
printf("0 <= x <= 127, 0 <= y <= 63\r\n");
return;
}
i = y / 8;
m = y % 8;
n = 1 << m;
OLED_RAM[x][i] = ~OLED_RAM[x][i];
OLED_RAM[x][i] |= n;
OLED_RAM[x][i] = ~OLED_RAM[x][i];
}
/*
* 在屏幕的上的(x1, y1)、(x2,y2)连接画一条直线
* x:x坐标(0~127)
* y:y坐标(0~63)
* */
void OLED_DrawLine(u8 x1, u8 y1, u8 x2, u8 y2) {
if (x1 > 127 || x2 < 0 || y1 > 63 || y2 < 0 || x1 > x2 || y1 > y2) {
printf("Function OLED_DrawLine parameter error:\r\n");
printf(" 0 <= x <= 127, 0 <= y <= 63;\r\n");
printf(" x1 < x2, y1 < y2;\r\n");
printf("x1=%d,y1=%d,x2=%d,y2=%d\r\n", x1, y1, x2, y2);
return;
}
x1 = x1 > 0 ? x1 : 0;
x2 = x2 > 127 ? 127 : x2;
y1 = y1 < 0 ? 0 : y1;
y2 = y2 > 63 ? 63 : y2;
if (x1 == x2) { // 画竖线
for (u8 i = 0; i < (y2 - y1); i++)
OLED_DrawPoint(x1, y1 + i);
} else if (y1 == y2) { // 画横线
for (u8 i = 0; i < (x2 - x1); i++)
OLED_DrawPoint(x1 + i, y1);
} else {
u8 k1 = y2 - y1;
u8 k2 = x2 - x1;
u8 k = k1 * 10 / k2;
for (u8 i = 0; i < (x2 - x1); i++) {
OLED_DrawPoint(x1 + i, y1 + i * k / 10);
}
}
}
//画一个矩形
void OLED_DrawRectangle(u8 x1, u8 y1, u8 x2, u8 y2, u8 fill) {
if (x1 > 127 || x2 < 0 || y1 > 63 || y2 < 0 || x1 > x2 || y1 > y2) {
printf("Function OLED_DrawRectangle parameter error\r\n");
printf("0 <= x <= 127, 0 <= y <= 63\r\n");
return;
}
x1 = x1 > 0 ? x1 : 0;
x2 = x2 > 127 ? 127 : x2;
y1 = y1 < 0 ? 0 : y1;
y2 = y2 > 63 ? 63 : y2;
if (fill) { // 填充
for (u8 i = x1; i < x2; i++)
OLED_DrawLine(i, y1, i, y2);
} else { // 空心
OLED_DrawLine(x1, y1, x1, y2);
OLED_DrawLine(x1, y1, x2, y1);
OLED_DrawLine(x1, y2, x2, y2);
OLED_DrawLine(x2, y1, x2, y2);
}
}
//画圆
void OLED_DrawCircle(u8 x, u8 y, u8 r, u8 fill) {
if (x < 0 || x > 127 || y < 0 || y > 63 || r < 0) {
printf("Function OLED_DrawPoint parameter error\r\n");
printf(" 0 <= x <= 127, 0 <= y <= 63, r > 0\r\n");
return;
}
int a = 0;
int b = r;
int d = 3 - 2 * r;
if (fill) {
u8 x_Start = x - r < 0 ? 0 : x - r;
u8 x_End = x + r > 127 ? 127 : x + r;
u8 y_Start = y - r < 0 ? 0 : y - r;
u8 y_End = y + r > 127 ? 127 : y + r;
for (u8 i = x_Start; i <= x_End; i++)
for (u8 j = y_Start; j <= y_End; j++)
if ((x - i) * (x - i) + (y - j) * (y - j) <= r * r)
OLED_DrawPoint(i, j);
} else {
// 绘制圆的八分之一部分,然后利用对称性绘制整个圆
while (a <= b) {
// 绘制八个对称的点
OLED_DrawPoint(x + a, y + b); // 第一象限
OLED_DrawPoint(x - a, y + b); // 第二象限
OLED_DrawPoint(x + a, y - b); // 第四象限
OLED_DrawPoint(x - a, y - b); // 第三象限
OLED_DrawPoint(x + b, y + a); // 第一象限(45度对称)
OLED_DrawPoint(x - b, y + a); // 第二象限(45度对称)
OLED_DrawPoint(x + b, y - a); // 第四象限(45度对称)
OLED_DrawPoint(x - b, y - a); // 第三象限(45度对称)
// 更新x和y的值,以及决策变量d
if (d < 0) {
d += 4 * a + 6;
a++;
} else {
d += 4 * (a - b) + 10;
a++;
b--;
}
}
}
}
//显示单个字符
void OLED_ShowChar(u8 x, u8 y, char ch, u8 size) {
u8 i, m, temp, size2, chr1;
u8 y0 = y;
size2 = (size / 8 + ((size % 8) ? 1 : 0)) *
(size / 2); // 得到字体一个字符对应点阵集所占的字节数
chr1 = ch - ' '; // 计算偏移后的值
for (i = 0; i < size2; i++) {
if (size == 12)
temp = asc2_1206[chr1][i]; // 调用1206字体
else if (size == 16)
temp = asc2_1608[chr1][i]; // 调用1608字体
else if (size == 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) == size) {
y = y0;
x++;
break;
}
}
}
}
// 显示字符串
// x,y:起点坐标
// size1:字体大小
//*chr:字符串起始地址
void OLED_ShowString(u8 x, u8 y, u8 *chr, u8 size1) {
while ((*chr >= ' ') && (*chr <= '~')) { // 判断是不是非法字符!
OLED_ShowChar(x, y, *chr, size1);
x += size1 / 2;
if (x > 128 - size1) { // 换行
x = 0;
y += 2;
}
chr++;
}
}
//滚动显示
void OLED_ScrollDisplay() {
OLED_SendByte(0x2E, OLED_CMD); // 先禁用滚动
OLED_SendByte(0x00, OLED_CMD); // 发送一个虚拟字节
OLED_SendByte(0x27, OLED_CMD); // 连续水平向右滚动显示
OLED_SendByte(0x00, OLED_CMD); // 发送一个虚拟字节
OLED_SendByte(0x00, OLED_CMD); // 开始页地址
OLED_SendByte(0x00, OLED_CMD); // 每个滚动步骤之间的时间间隔为5帧
OLED_SendByte(0x07, OLED_CMD); // 结束页地址
OLED_SendByte(0x00, OLED_CMD); // 发送一个虚拟字节
OLED_SendByte(0xff, OLED_CMD); // 发送一个虚拟字节
OLED_SendByte(0x2F, OLED_CMD); // 开启滚动显示
}