硬件环境
单片机: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线。
void MY_IIC_Write(uint8_t Data){ uint8_t bit_idx;//定义一个变量用来八次循环 // 发送数据字节 for (bit_idx = 0; bit_idx < 8; bit_idx++) { // 设置 SDA 线(数据位) if (Data & 0x80) {//比较每个最高位 SDA_High; // 最高位是1就拉高SDA } else { SDA_Low; // 最高位是0就拉低SDA } SCL_High;// 拉高 SCL 线,表示开始时钟周期 SCL_Low;// 拉低 SCL 线,准备下一个数据位 Data <<= 1;// 数据左移一位,准备发送下一个数据位 }}
这样子我们的I2C基础函数就写好了,接下来我们需要写一个函数,用这些基础函数给器件发送"设备地址","命令(寄存器地址)","数据"。
uint8_t MY_IIC_WriteCommand(uint8_t Address,uint8_t Command,uint8_t Data){ MY_IIC_START();//I2C起始信号 MY_IIC_Write(Address);//设备地址 if(!MY_IIC_WaitACK())//如果没有响应 { return 0;//返回失败 } MY_IIC_Write(Command);//发送命令 if(!MY_IIC_WaitACK())//如果没有响应 { return 0;//返回失败 } MY_IIC_Write(Data);//发送数据 if(!MY_IIC_WaitACK())//如果没有响应 { return 0;//返回失败 } MY_IIC_STOP();//结束通讯}
1
深度封装
接着我们按照命令的形式进行深度封装,发送命令时I2C第二个参数是0x00,显示数据的时候是0x40,我们按照指令大全对其深度封装。
// OLED IIC地址#define OLED_ADDRESS 0x78 // 0x3C << 1
// 控制字节#define OLED_CMD 0x00 // 写命令#define OLED_DATA 0x40 // 写数据
// 基础控制指令#define OLED_DISPLAY_ON 0xAF // 开启显示#define OLED_DISPLAY_OFF 0xAE // 关闭显示#define OLED_NORMAL_DISPLAY 0xA6 // 正常显示#define OLED_INVERSE_DISPLAY 0xA7 // 反色显示
// 寻址设置指令#define OLED_ADDR_MODE 0x20 // 设置寻址模式#define OLED_ADDR_MODE_HOR 0x00 // 水平寻址#define OLED_ADDR_MODE_VER 0x01 // 垂直寻址#define OLED_ADDR_MODE_PAGE 0x02 // 页寻址
// 硬件配置指令#define OLED_SET_CONTRAST 0x81 // 对比度设置#define OLED_SET_MULTIPLEX 0xA8 // 多路复用设置#define OLED_COM_SCAN_DIR 0xC8 // COM扫描方向#define OLED_DISPLAY_OFFSET 0xD3 // 显示偏移#define OLED_COM_PIN_CFG 0xDA // COM引脚配置
// 时序控制指令#define OLED_SET_CLOCK_DIV 0xD5 // 显示时钟分频#define OLED_SET_PRECHARGE 0xD9 // 预充电周期#define OLED_SET_VCOM_LEVEL 0xDB // VCOMH取消选择级别
// 电荷泵指令#define OLED_CHARGE_PUMP 0x8D // 充电泵设置#define OLED_CHARGE_PUMP_ON 0x14 // 启用充电泵#define OLED_CHARGE_PUMP_OFF 0x10 // 禁用充电泵
// 寻址指令#define OLED_SET_PAGE_ADDR 0xB0 // 页地址设置(0xB0~0xB7)#define OLED_SET_COL_LOW 0x00 // 列地址低4位设置#define OLED_SET_COL_HIGH 0x10 // 列地址高4位设置
我们在OLED.H文件中添加相对应的指令宏定义,接下来我们来介绍一下OLED的工作原理。
对于 128×64 分辨率的 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,非常方便且高效,并且我们利用取模软件实现字符串的显示。