单片机:STM32F103C8T6
I2C设备:OLED_SSD1306 四针I2C通讯
| **指令名称** | **指令代码 (Hex)** | **描述** | |-----------------------------------------|--------------------|---------------------------------------------------------------| | **SET_CONTRAST** | 0x81 | 设置对比度。后续字节为对比度值(0x00 - 0xFF)。 | | **DISPLAY_ON** | 0xAF | 打开显示器。 | | **DISPLAY_OFF** | 0xAE | 关闭显示器。 | | **SET_DISPLAY_CLOCK_DIVIDE_RATIO** | 0xD5 | 设置显示时钟分频比,后续字节为分频值。 | | **SET_MUX_RATIO** | 0xA8 | 设置多路复用比率(行数)。后续字节为行数(0x0F - 0x3F)。 | | **SET_DISPLAY_OFFSET** | 0xD3 | 设置显示偏移量。后续字节为偏移量(0 - 63)。 | | **SET_START_LINE** | 0x40 | 设置起始行(0x00 - 0x3F)。 | | **CHARGEPUMP** | 0x8D | 启用或禁用充电泵。后续字节:`0x10` - 禁用,`0x14` - 启用。 | | **SEG_REMAP** | 0xA0 or 0xA1 | 设置段重映射。`0xA0` - 默认,`0xA1` - 反向。 | | **COM_SCAN_MODE** | 0xC0 or 0xC8 | 设置扫描方向。`0xC0` - 正向,`0xC8` - 反向。 | | **SET_COM_PINS** | 0xDA | 设置COM引脚硬件配置。后续字节为设置(例如,0x12 - 选择高电平配置)。| | **SET_CONTRAST_CONTROL** | 0x81 | 设置对比度。后续字节为对比度值(0x00 - 0xFF)。 | | **PRECHARGE_PERIOD** | 0xD9 | 设置预充电周期。后续字节为设置值。 | | **VCOMH_DESELECT_LEVEL** | 0xDB | 设置VCOMH电压参考值。后续字节为电压水平(0x00 - 0x3F)。 | | **SET_COLUMN_ADDRESS** | 0x21 | 设置列地址范围,后续字节为起始列和结束列地址(0x00-0x7F)。| | **SET_PAGE_ADDRESS** | 0x22 | 设置页地址范围,后续字节为起始页和结束页地址(0x00-0x07)。| | **WRITE_DATA** | 0x40 | 写数据到显示屏。此指令后可以跟随数据进行图像显示。 | | **NOP (No Operation)** | 0xE3 | 无操作指令。 | | **TURN_ON_SCROLL** | 0x2F | 开启滚动功能。 | | **TURN_OFF_SCROLL** | 0x2E | 关闭滚动功能。 | | **SET_SCROLL_VERTICAL_AND_HORIZONTAL** | 0x29 | 启动垂直和水平滚动。后续字节为滚动配置参数。 | | **SET_VERTICAL_SCROLL_AREA** | 0xA3 | 设置垂直滚动区域。后续字节为起始行和行数。 | | **DEACTIVATE_SCROLL** | 0x2E | 结束滚动。 | | **ACTIVATE_SCROLL** | 0x2F | 开始滚动。 | | **SET_HORIZONTAL_SCROLL** | 0x26 | 设置水平滚动。 | | **SET_VERTICAL_SCROLL** | 0x27 | 设置垂直滚动。 |

首先我们选用两个IO口将其配置为开漏输出模式并且配置上拉电阻。
#ifndef __OLED_H__ #define __OLED_H__ #include "main.h" #include "OLED.h" /* I2C 引脚宏定义 */ #define SCL_Port GPIOB #define SDA_Port GPIOB #define SCL_Pin GPIO_PIN_10 #define SDA_Pin GPIO_PIN_11 /* I2C 引脚电平设置 */ #define SCL_Low HAL_GPIO_WritePin(SCL_Port,SCL_Pin,0); #define SDA_Low HAL_GPIO_WritePin(SDA_Port,SDA_Pin,0); #define SCL_High HAL_GPIO_WritePin(SCL_Port,SCL_Pin,1); #define SDA_High HAL_GPIO_WritePin(SDA_Port,SDA_Pin,1); void MY_IIC_START(); void MY_IIC_STOP(); void MY_IIC_Write(uint8_t Data); uint8_t MY_IIC_WaitACK(); uint8_t MY_IIC_WriteCommand(uint8_t Address,uint8_t Command,uint8_t Data); #endif
首先我们根据I2C的时序利用软件模拟这几个部分。START信号是当SDA为高电平时,SCL拉低。
void MY_IIC_START()
{
SDA_High;
SCL_High;//同时拉高两条线
SDA_Low; // START信号是在SCL高电平期间,SDA从高变低
SCL_Low;
}
I2C结束信号的要求是,在SCL保持高电平的时候,拉高SDA。
/* IIC结束信号 */
void MY_IIC_STOP()
{
SDA_Low; // STOP信号是在SCL高电平期间,SDA从低变高
SCL_High;
SDA_High;
}
ACK信号是当SCL拉低时,从机的SDA会发送一个高电平信号给主机。这里我们利用推挽输出可以读取电平的特性直接利用HAL_GPIO_Read函数来阅读。
uint8_t MY_IIC_WaitACK()
{
int ack = 0;
SCL_Low;//拉低SDA线
if (HAL_GPIO_ReadPin(SDA_Port,SDA_Pin) == 0) // 如果 SDA 线拉低,表示收到 ACK
{
ack = 1;
}
else
{
ack = 0;
}
// 拉高 SCL,准备结束这一周期
SCL_High;
return ack; // 返回 ACK 状态,1表示收到ACK,0表示没有ACK
}
数据发送时,要求SCL在低电平的时候准备好数据,当SCL在高电平的时候要求数据稳定,从最高位开始我们逐位比较然后拉高拉低SDA线。

我们在OLED.H文件中添加相对应的指令宏定义,接下来我们来介绍一下OLED的工作原理。

于 128x64 分辨率的 OLED 屏幕,屏幕上有 128 列 和 64 行 像素。为了简化管理,SSD1306 将显示分为 8 页,每页对应 8 行像素。因此,总共有 8 页(每页 128 列),每页占用 128 字节,表示 128 列 × 8 行像素的数据。
1. 设置页地址
设置页地址的命令是 0xB0 + 页地址。例如,想设置为页地址 3,可以发送命令 0xB3。
2. 设置列地址
列地址是通过两条命令来设置的:
0x00 + 列地址低 4 位
0x10 + 列地址高 4 位
例如,若要设置列地址为 50:
列地址低 4 位:50 & 0x0F = 0x02
列地址高 4 位:(50 >> 4) & 0x0F = 0x03
因此,列地址 50 的设置命令为:
0x00 + 0x02 = 0x02 (低 4 位)
0x10 + 0x03 = 0x13 (高 4 位)
3. 发送数据
每个字节对应一列的 8 个像素,每位表示一个像素的状态。通常来说:
0x00 表示该列的所有像素关闭。
0xFF 表示该列的所有像素点亮。(就像流水灯)
例如,若要点亮第 1 列的所有像素,可以发送字节 0xFF。
void OLED_Init(void);
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t dot);
void OLED_Init(void)
{
HAL_Delay(100); // 等待OLED上电稳定
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_DISPLAY_OFF); // 关闭显示
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_CLOCK_DIV); // 设置时钟分频
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, 0x80); // 分频系数
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_MULTIPLEX); // 设置多路复用
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, 0x3F); // 复用率 1/64
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_DISPLAY_OFFSET); // 设置显示偏移
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, 0x00); // 无偏移
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, 0x40); // 设置显示起始行
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_CHARGE_PUMP); // 设置电荷泵
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_CHARGE_PUMP_ON); // 启用电荷泵
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_ADDR_MODE); // 设置寻址模式
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_ADDR_MODE_PAGE); // 页寻址模式
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_COM_SCAN_DIR); // 设置COM扫描方向
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_COM_PIN_CFG);// 设置COM引脚配置
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, 0x12); // COM引脚配置
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_CONTRAST); // 设置对比度
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, 0xCF); // 对比度值
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_PRECHARGE); // 设置预充电周期
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, 0xF1); // 预充电周期
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_VCOM_LEVEL); // 设置VCOMH
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, 0x40); // VCOMH值
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_NORMAL_DISPLAY); // 正常显示(不反色)
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, 0xA1); // 设置段重映射
// 清屏
for(uint8_t page = 0; page < 8; page++) {
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_PAGE_ADDR | page);
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_COL_LOW);
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_COL_HIGH);
for(uint8_t col = 0; col < 128; col++) {
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_DATA, 0x00);
}
}
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_DISPLAY_ON); // 开启显示
}
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t dot)
{
uint8_t page, bit, data;
// 检查坐标是否有效
if(x > 127 || y > 63) return;
// 计算页地址(y/8)和位位置(y%8)
page = y / 8;
bit = y % 8;
// 设置要绘制点的页地址和列地址
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_PAGE_ADDR | page);
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_COL_LOW | (x & 0x0F));
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_CMD, OLED_SET_COL_HIGH | (x >> 4));
// 读取当前数据(这里需要先写入,因为OLED不支持读操作)
data = 0x00; // 假设当前数据为0
// 设置或清除对应的位
if(dot) {
data |= 1 << bit; // 设置点
} else {
data &= ~(1 << bit); // 清除点
}
// 写回数据
MY_IIC_WriteCommand(OLED_ADDRESS, OLED_DATA, data);
}
我们添加OLED的初始化函数和画点函数。并且我们添加一个测试函数来看看能不能点亮。
void OLED_DrawHeart_Int(uint8_t center_x, uint8_t center_y, uint8_t size)
{
int16_t x, y;
// 扫描可能的区域
for(y = -16; y < 16; y++) {
for(x = -16; x < 16; x++) {
// 心形方程:(x²+y²-1)³ - x²y³ ≤ 0
int32_t x2 = x * x;
int32_t y2 = y * y;
int32_t eq = (x2 + y2 - 100) * (x2 + y2 - 100) * (x2 + y2 - 100) - x2 * y2 * y;
if(eq <= 0) {
int8_t draw_x = center_x + (x * size) / 16;
int8_t draw_y = center_y + (y * size) / 16;
if(draw_x >= 0 && draw_x < 128 && draw_y >= 0 && draw_y < 64) {
OLED_DrawPoint(draw_x, draw_y, 1);
}
}
}
}
}

需要注意的是,软件I2C由于是通过GPIO翻转来模拟时序的,因此如果芯片的主频过快会导致两个语句的时间不够满足I2C通讯时序的要求,在适当条件下我们可以通过加一些延时函数(微秒级/纳秒级)来调节时序,当然使用硬件I2C大部分情况下不会遇到这个问题,下一期为大家介绍如何使用硬件I2C替代软件I2C,非常方便且高效,并且我们利用取模软件实现字符串的显示。


登录 或 注册 后才可以进行评论哦!
还没有评论,抢个沙发!