STM32学习记录(四):IIC驱动OLED
STM32中分别GPIO模拟IIC和使用IIC外设进行实验。
I²C(Inter-Integrated Circuit),是I²C总线的简称,中文名叫做集成电路总线
时序图
以下内容来自:维基百科

- Data transfer is initiated with a start condition (S) signalled by SDA being pulled low while SCL stays high.
- SCL is pulled low, and SDA sets the first data bit level while keeping SCL low (during blue bar time).
- The data is sampled (received) when SCL rises for the first bit (B1). For a bit to be valid, SDA must not change between a rising edge of SCL and the subsequent falling edge (the entire green bar time).
- This process repeats, SDA transitioning while SCL is low, and the data being read while SCL is high (B2 through Bn).
- The final bit is followed by a clock pulse, during which SDA is pulled low in preparation for the stop bit.
- A stop condition (P) is signalled when SCL rises, followed by SDA rising.
- SCL保持高电平时,SDA被master(主机)拉低,此时为开始状态(S)
- SCL被拉低,SDA在SCL保持低电平时设置第一位数据
- SDA的数据在只有在SCL保持高电平时才有效
- 重复这个过程,第2步过程
- SDA发送最后一个位后,下一个时钟脉冲SDA被拉低,为停止状态做准备
- 当SCL被拉高,SDA紧接着被拉高,会发出停止信号(P)
I²C协议大致流程
I²C协议下可以挂载多个设备,SDA,SCL都需要添加上拉电阻,
开漏输出(Open Drain)模式下MCU只有输出低电平(数据寄存器写入0)才具有驱动能力,此时I/O引脚输出VSS。

开漏输出(Open Drain)模式下MC输出数据寄存器中的逻辑"1",P-MOS和N-MOS都关闭,输出口的状态是高阻(既不是高电平也不是低电平),如果要实现高电平驱动,需要在I/O引脚处加一个上拉电阻。

在OLED模块的原理图中可以看到SCL、SDA两条线都加了4.7K的上拉电阻

I2C协议下主机控制多个外设

微控制器可以是STM32,STM32输出低电平时,SDA、SCL都被拉低;当STM32输出高电平时,STM32不能驱动SDA、SCL,这两条线是被外接的上拉电阻拉至高电平。
当多个开漏输出的引脚连接在同一根线上时,它们共同的结果相当于一个“与逻辑门”(AND gate):全1才为1,有0则0。只有当所有设备都输出高电平(实际上是不拉低,即释放总线)时,总线才为高电平。只要任何一个设备输出低电平(主动拉低总线),整条总线就被拉低为低电平。
设置页地址模式下地址

OLED显示原理
GDDRAM(Graphic Display Data RAM)与128*64像素的显示屏是一一映射关系,向GDDRAM中指定了页地址(Page Address)、列地址的位置写入数据,128*64点阵对应位置就会点亮。行方向上的64行像素点以8位为一组分为了8组,每一页(PAGE)最下面是最高有效位(MSB),最上面是最低位(LSB)。要在OLED中显示一个字符,只需向GDDRAM指定位置写入数据。如下图所示,要在页地址PAGE0,列地址为第123列的位置显示一个6*8像素的字符'A',显示数据是{0x00, 0x7C, 0x12, 0x11, 0x12, 0x7C},这个数据从取字模软件中获取,OLED使用字模库是阴码(高电平点亮)还是阳码(低电平点亮)参照商家的数据手册。

举个例子,在取字模软件获得8*16像素的字符'1',如下图所示,为什么这里字宽要输入16?这是因为这个取字模软件默认是取汉字,识别到ASCII码时,输出的字模数据才会变成8*16像素。

这个数据怎么来的呢?在ASCIIFlow绘制出字符'1'在GGDRAM中存放的样式,一列分为两个字节,先存PAGE0的数据,再取PAGE1中存放的数据。从PAGE0取1个字节(下面是最高位,上面是最低位),再从PAGE1取1个字节,就可以获得{0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x10, 0x20, 0xF8, 0x3F, 0x00, 0x20,0x00,0x20,0x00,0x00}这样的数据

软件模拟I2实现OLED字符的显示
初始化GPIO
使用单片机STM32F103C8T6的引脚PB8、PB9作为SCL、SDA。
#define SDA_WriteBit(x) GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))
#define SCL_WriteBit(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
void OLED_I2C_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
GPIO_Init(GPIOB, &GPIO_InitStructure);
SCL_WriteBit(1); //拉高SCL和SDA(本质上是上拉电阻拉高的)
SDA_WriteBit(1);
}
IIC协议开始和停止状态
参照:上面的时序图,进行编写
/* iic协议开始 */
void OLED_I2C_Start()
{
SDA_WriteBit(1);
SCL_WriteBit(1);
SDA_WriteBit(0);
SCL_WriteBit(0); //SCL为0,SCL的下一个时钟脉冲时开始发送数据
}
/* iic协议停止 */
void OLED_I2C_Stop()
{
SDA_WriteBit(0);
SCL_WriteBit(1);
SDA_WriteBit(1);
}
OLED模块初始化
void OLED_Init(void)
{
delay_s(1); //延时1s,稳定OLED上电状态
OLED_I2C_Init(); //端口初始化
/* 参照商家给的数据手册 */
OLED_WriteCmd(0xAE); //关闭显示
OLED_WriteCmd(0xD5); //设置显示时钟分频比/振荡器频率
OLED_WriteCmd(0x80);
OLED_WriteCmd(0xA8); //设置多路复用率
OLED_WriteCmd(0x3F);
OLED_WriteCmd(0xD3); //设置显示偏移
OLED_WriteCmd(0x00);
OLED_WriteCmd(0x40); //设置显示开始行
OLED_WriteCmd(0xA1); //设置左右方向,0xA1正常 0xA0左右反置
OLED_WriteCmd(0xC8); //设置上下方向,0xC8正常 0xC0上下反置
OLED_WriteCmd(0xDA); //设置COM引脚硬件配置
OLED_WriteCmd(0x12);
OLED_WriteCmd(0x81); //设置对比度控制
OLED_WriteCmd(0xCF);
OLED_WriteCmd(0xD9); //设置预充电周期
OLED_WriteCmd(0xF1);
OLED_WriteCmd(0xDB); //设置VCOMH取消选择级别
OLED_WriteCmd(0x30);
OLED_WriteCmd(0xA4); //设置整个显示打开/关闭
OLED_WriteCmd(0xA6); //设置正常/倒转显示
OLED_WriteCmd(0x8D); //设置充电泵
OLED_WriteCmd(0x14);
OLED_WriteCmd(0xAF); //开启显示
OLED_Clear();
}
OLED写数据/写指令
SSD1306 是一款单片 CMOS OLED/PLED 驱动器。《SS1306技术手册》中说明了SS1306的I2C总线数据格式。I2C总线协议中,MCU作为master(主机)要访问slave(从机):
- 首先在开始条件(S)后发送从机地址(slave address),从机地址中SA0位由D/C#引脚决定,R/W#位规定是读还是写。发送完一个字节后,由从机发出ACK信号,这里没有处理ACK。ACK确认位将在接收到每个Control byte或Data byte后产生。
- 发送的第二字节是控制字节(Control byte),Co位
- 1:说明后面发送的字节都要遵循先发送Control byte,再发送Data byte;
- 0:则后面的字节都是只包括Data byte
- D/C#位:
- 1:后续的Data byte作为指令(Command)
- 0:后续的Data byte作为数据(Data)存放在GDDRAM中
- 发送n个字节后,在停止条件(P)后停止数据的发送。

/**
* @brief MCU向从机发送一个字节的数据
* @param byte 要发送的数据
* @retval 无
*
*/
void OLED_SendByte(uint8_t byte)
{
for(uint8_t i = 0; i < 8; i++)
{
SDA_WriteBit(byte & ((0x80) >> i)); //依次发送byte中每一位二进制数据
SCL_WriteBit(1); //SCL高电平时SDA数据有效
SCL_WriteBit(0); //拉低准备下一次SCL上升沿
}
SCL_WriteBit(1); //额外的一个时钟,不处理应答信号
SCL_WriteBit(0);
}
/**
* @brief OLED写指令
* @param cmd 要写入的指令
* @retval 无
*
*/
void OLED_WriteCmd(uint8_t cmd)
{
OLED_I2C_Start();
OLED_SendByte(0x78); //发送slave(从机)地址
OLED_SendByte(0X00); //控制字节:准备写指令,Co = 0, D/C# = 0
OLED_SendByte(cmd); //写指令
OLED_I2C_Stop();
}
/**
* @brief OLED写数据
* @param data 要写入的数据
* @retval 无
*
*/
void OLED_WriteData(uint8_t data)
{
OLED_I2C_Start();
OLED_SendByte(0x78); //发送slave(从机)地址
OLED_SendByte(0X40); //控制字节:准备写数据,Co = 0, D/C# = 1
OLED_SendByte(data); //写数据
OLED_I2C_Stop();
}
OLED显示字符/字符串
这里选择8*16像素的字模数据,即宽8个像素,高16个像素。由上面可知OLED将每行的像素分成了8个页,显示字符的时候要分别在两个PAGE上写入数据。
/**
* @brief OLED设置光标位置
* @param Y 以左上角为原点,向下方向的坐标,范围:0~7
* @param X 以左上角为原点,向右方向的坐标,范围:0~127
* @retval 无
*
* **************************************************
* 128*64的点阵,从行方面来看64行的点阵分成为了8个PAGE,即PAGE0~PAGE7,这也是为什么y取0~7
* 从列方面来看,共有128列,即SEG0~SGE127,所以x取值0~127
*
*/
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
OLED_WriteCmd(0xB0 | Y); //设置PAGE起始地址
OLED_WriteCmd(0x10 | ((X & 0xF0) >> 4)); //取x的高4位并右移4位,与固定的0x10按位或,设置列地址高4位
OLED_WriteCmd(0x00 | (X & 0x0F)); //取x的低4位设置列地址低4位,与固定的0x00按位或,设置列地址低4位
}
/**
* @brief 在OLED第row行, 第col列显示一个ascii字符
* @param row 行取值范围:1~4
* @param col 列取值范围:1~16
* @param ch 要显示的ascii字符
* @retval 无
*
* 行1~4 =====> 行0~7
* x = 1, y = 0; x = 2, y = 2;
* y = kx+b 过(1, 0), (2, 2)解出y = 2x-2 = 2*(x - 1),即(row - 1) * 2
*
* 列1~16 =====> 列0~127
* x = 1, y = 0; x = 2, y = 8 ===> 得到关系式 y = 8*x - 8 = 8*(x-1),即(col-1)*8
*
*
*/
void OLED_ShowChar(uint8_t row, uint8_t col, uint8_t ch)
{
if((ch - ' ') < 0) // 如果输入的ascii字符不在字模库里面,则不在OLED上显示
return;
OLED_SetCursor((row - 1) * 2, (col - 1)* 8); //写上半部分
for (int i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[ch - ' '][i]); //ch - ' '可以得到ch在字模库中索引位置
}
OLED_SetCursor((row - 1) * 2 + 1, (col - 1)* 8); //写下半部分
for (int i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[ch - ' '][i + 8]);
}
}
/**
* @brief 在OLED第row行, 第col列显示字符串
* @param row 行取值范围:1~4
* @param col 列取值范围:1~16
* @param str 要显示的字符串
* @retval 无
*
*/
void OLED_ShowString(uint8_t row, uint8_t col, uint8_t *str)
{
for (uint8_t i = 0; str[i] != '\0'; i++)
{
OLED_ShowChar(row, col + i, str[i]);
}
}
/* OLED清屏 */
void OLED_Clear(void)
{
for (uint8_t i = 0; i < 8; i++)
{
OLED_SetCursor(i, 0); //光标设置到第i行第1列
for (uint8_t j = 0; j < 128; j++)
{
OLED_WriteData(0X00);
}
}
}
使用方法
int main()
{
OLED_Init();
OLED_ShowChar(1, 1, 'A');
OLED_ShowString(2, 1, "Hello World");
while(1)
{
}
}
使用I2C外设实现OLED字符的显示
I2C外设初始化
前面使用的是PB8、PB9来当做I2C协议的SCL、SDA线,STM32F103C8T6中提供了I2C外设,参照芯片的引脚图。这里使用芯片上的I2C外设来驱动OLED,引脚PB6、PB7的复用功能分别作为SCL、SDA。
#include "i2c.h"
#include "stm32f10x.h"
/* 从机地址,这里是OLED模块的地址 */
#define SLAVE_ADDRESS 0x78
/* 包括GPIO、I2C外设的初始化 */
void MyI2C_init(void)
{
/* 开启外设时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
/* 使用I2C外设,GPIO必须配置为复用开漏输出 */
GPIO_InitTypeDef GPIO_InitStructure;
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外设初始化 */
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_SMBusDevice;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_OwnAddress1 = SLAVE_ADDRESS;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_ClockSpeed = 400000;
I2C_Cmd(I2C1, ENABLE);
I2C_Init(I2C1, &I2C_InitStructure);
}
I2C外设发送数据
单片机作为主机向从机OLED模块发送数据,使其显示指定数据,因此这里选择参照主机发送序列图表中的数据发送格式来编写程序

发送数据流程是这样的:
- 首先使用
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发送数据
I2C_SendData(I2C1, data[i]),并且检测状态EV8,while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));发送多个数据重复执行第5步 - 产生停止条件
I2C_GenerateSTOP(I2C1, ENABLE)
/* I2C发送数据 */
void MyI2C_SendData(uint8_t *data, uint8_t len)
{
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Transmitter); //发送7位地址
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
for(int i = 0; i < len; i++)
{
I2C_SendData(I2C1, data[i]);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
I2C_GenerateSTOP(I2C1, ENABLE);
}
while循环判断状态时,最好通过加一个超时时间,超时时间到,还没有处于某个事件状态状态,就可以直接退出循环,防止程序卡死在while循环中,改进后:
/**
* @brief I2C外设发送数据
* @param data 数据数组
* @param len 数组长度
*
* ****************************
*
* I2C_CheckEvent用来检测每次发送完一个字节后的状态,参照《STM32F10x参考手册》、以及《STM32F103xx固件函数库手册》
* OK = 1, ERROR = 0
*
*/
uint8_t MyI2C_SendData(uint8_t *data, uint8_t len)
{
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
if((Timeout--) == 0) return _ERROR;
}
I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Transmitter); //发送7位地址
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if((Timeout--) == 0) return _ERROR;
}
for(int i = 0; i < len; i++)
{
I2C_SendData(I2C1, data[i]);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((Timeout--) == 0) return _ERROR;
}
}
I2C_GenerateSTOP(I2C1, ENABLE);
return _OK;
}
I2C外设接收数据

uint8_t MyI2C_ReadData(uint8_t *data)
{
/* 发送I2C开始条件*/
I2C_GenerateSTART(I2C1, ENABLE);
/* 测试EV5 */
Timeout = LONG_TIMEOUT;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
{
if((Timeout--) == 0) return _ERROR;
}
/* 发送从机地址,方向为接收 */
I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Receiver);
/* 测试EV6 */
Timeout = LONG_TIMEOUT;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
{
if((Timeout--) == 0) return _ERROR;
}
/* 失能ACK */
I2C_AcknowledgeConfig(I2C1, DISABLE);
/* 发送I2C停止条件 */
I2C_GenerateSTOP(I2C1, ENABLE);
/* 测试EV7 */
Timeout = LONG_TIMEOUT;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))
{
if((Timeout--) == 0) return _ERROR;
}
/* 接收一个字节的数据 */
*data = I2C_ReceiveData(I2C1);
/* 将应答恢复为使能,为了不影响后续可能产生的读取多字节操作 */
I2C_AcknowledgeConfig(I2C1, ENABLE);
return _OK;
}

浙公网安备 33010602011771号