STM32——软件IIC显示字符

一、整体说明

这是一套 STM32 软件模拟 I2C 驱动 0.96 寸 OLED(SSD1306) 的完整工程代码。特点:

  • 纯软件 I2C,不占用硬件 I2C 外设
  • 引脚:SCL=PB0SDA=PB1
  • 驱动芯片:SSD1306
  • 支持:清屏、显示字符、显示汉字、显示图片

二、文件总览

整个工程由 3 个核心文件 组成:

  1. oledr.c ------ 驱动核心(最重要,本文重点讲解)
  2. oledr.h ------ 宏定义、函数声明
  3. codetab.h ------ 字库(ASCII、汉字、图片取模数据)

三、oledr.c 超详细逐模块讲解(博客核心)

模块 1:延时函数(软件 I2C 必须靠延时控制时序)

复制代码
// 微秒级延时(粗略延时,满足I2C时序即可)
static void delay_us(unsigned char num)
{
	unsigned char i;
	while(num--)
	{
		i = 10;
		while(i--);
	}
}

// 毫秒级延时(用于初始化等待、上电稳定)
static void delay_ms(unsigned int ms)
{
	unsigned int x,y;
	for(x = ms;x > 0;x--)
		for(y=12000;y>0;y--);
}

作用

  • delay_us:控制 I2C 通信时序,保证 SDA/SCL 变化稳定
  • delay_ms:OLED 上电需要稳定时间,初始化前必须等待

模块 2:GPIO 初始化(软件 I2C 最重要一步)

复制代码
static void OLED_GPIO_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	
	// 开 GPIOB + AFIO 时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
	
	// 模式:通用开漏输出(软件I2C必须用这个!)
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
	// 引脚:PB0(SCL) + PB1(SDA)
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
	// 速度:50MHz
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	
	GPIO_Init(GPIOB, &GPIO_InitStruct);
	
	// 初始状态:SCL=1,SDA=1(I2C 总线空闲状态)
	OLED_SCLK_Set();
	OLED_SDIN_Set();
}

关键知识点

  1. 必须开 AFIO 时钟
  2. 模式必须是 Out_OD 开漏输出
    • 硬件 I2C 用 AF_OD
    • 软件 I2C 必须用通用开漏
  3. I2C 空闲状态 = SCL=1,SDA=1

模块 3:I2C 起始信号(通信的 "开场白")

复制代码
static void OLED_IIC_Start(void)
{
	OLED_SCLK_Set();   // SCL=1
	OLED_SDIN_Set();   // SDA=1
	delay_us(1);       // 稳定
	OLED_SDIN_CLR();   // SDA 从 1→0(产生起始信号)
	delay_us(1);
        OLED_SCLK_CLR();    // 拉低时钟,准备传输数据
}

I2C 起始信号规则(标准)

SCL 保持高电平时,SDA 由高变低 → 起始信号OLED 检测到这个信号,才知道 "要开始通信了"。

模块 4:I2C 停止信号(通信的 "结束语")

复制代码
static void OLED_IIC_Stop(void)
{
	OLED_SDIN_CLR();
	delay_us(1);
	OLED_SCLK_Set();
	delay_us(1);
	OLED_SDIN_Set();   // SDA 从 0→1(产生停止信号)
	delay_us(1);
}

规则

SCL 高电平时,SDA 由低变高 → 停止信号

模块 5:I2C 等待 ACK 应答(确认 OLED 收到数据)

复制代码
static unsigned char IIC_Wait_Ack(void)
{
	unsigned char ack;
	
	OLED_SCLK_CLR();
	delay_us(1);
	OLED_SDIN_Set();
	delay_us(1);
	OLED_SCLK_Set();
	delay_us(1);
	
	// 读取 SDA 电平
	if(OLED_READ_SDIN())
		ack = IIC_NO_ACK; // 无应答
	else
		ack = IIC_ACK;    // 有应答
	
	OLED_SCLK_CLR();
	delay_us(1);
	
	return ack;
}

作用

每发 1 个字节,OLED 必须拉低 SDA 表示 "我收到了"。如果无应答 → 通信失败。

模块 6:I2C 发送一个字节(核心收发函数)

复制代码
static void Write_IIC_Byte(unsigned char IIC_Byte)
{
	unsigned char i;
	for(i=0;i<8;i++)  // 一个字节8位
	{
		OLED_SCLK_CLR();    // 拉低时钟,准备放数据
		delay_us(1);
		
		// 从最高位开始发送
		if(IIC_Byte & 0x80)
			OLED_SDIN_Set();
		else
			OLED_SDIN_CLR();
		
		IIC_Byte <<= 1;     // 左移,准备下一位
		delay_us(1);
		OLED_SCLK_Set();    // 上升沿发送数据
		delay_us(1);
	}
	OLED_SCLK_CLR();
	delay_us(1);	
	IIC_Wait_Ack();       // 等待应答
}

原理

I2C 发送数据规则:

  • SCL 低 → 放数据
  • SCL 高 → 数据被读取
  • 高位先发

模块 7:发送命令 / 发送数据(OLED 指令规则)

发送命令(设置 OLED:亮度、地址、模式等)

复制代码
static void Write_IIC_Command(unsigned char IIC_Command)
{
	OLED_IIC_Start();
	Write_IIC_Byte(0x78);    // OLED 地址
	Write_IIC_Byte(0x00);    // 0x00 = 写命令
	Write_IIC_Byte(IIC_Command);
	OLED_IIC_Stop();
}

发送数据(显示内容:点亮像素)

复制代码
static void Write_IIC_Data(unsigned char IIC_Data)
{
	OLED_IIC_Start();
	Write_IIC_Byte(0x78);    // OLED 地址
	Write_IIC_Byte(0x40);    // 0x40 = 写数据
	Write_IIC_Byte(IIC_Data);
	OLED_IIC_Stop();
}

超级重点

  • 0x00 = 写命令
  • 0x40 = 写数据
  • 0x78 = OLED 设备地址这是 SSD1306 严格规定的通信格式!

模块 8:写命令 / 写数据 封装函数

复制代码
void OLED_WR_Byte(unsigned char data, unsigned char cmd)
{
	if(cmd)
		Write_IIC_Data(data);   // cmd=1 → 数据
	else
		Write_IIC_Command(data);// cmd=0 → 命令
}

模块 9:设置光标坐标(决定在哪里显示)

复制代码
void OLED_Set_Pos(unsigned char x,unsigned char y)
{
	OLED_WR_Byte(0xb0+y,OLED_CMD);           // 设置页地址(Y)
	OLED_WR_Byte((x&0x0f),OLED_CMD);          // 列地址低4位
	OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD); // 列地址高4位
}

OLED 128×64 分为 8 页 × 128 列

  • 一页 = 8 行
  • 所以 Y 方向 0~7
  • X 方向 0~127

模块 10:开显示 / 关显示

复制代码
void OLED_Display_On(void)
{
	OLED_WR_Byte(0x8D,OLED_CMD);
	OLED_WR_Byte(0x14,OLED_CMD); // 开启电荷泵
	OLED_WR_Byte(0xAF,OLED_CMD); // 开显示
}

void OLED_Display_Off(void)
{
	OLED_WR_Byte(0x8D,OLED_CMD);
	OLED_WR_Byte(0x10,OLED_CMD); // 关闭电荷泵
	OLED_WR_Byte(0xAE,OLED_CMD); // 关显示
}

模块 11:清屏函数

复制代码
void OLED_Clear(void)
{
	unsigned char i,n;
	for(i=0;i<8;i++) // 8页
	{
		OLED_WR_Byte(0xb0+i,OLED_CMD);
		OLED_WR_Byte(0x00,OLED_CMD);
		OLED_WR_Byte(0x10,OLED_CMD);
		for(n=0;n<128;n++) // 128列
			OLED_WR_Byte(0,OLED_DATA); // 全部写0
	}
}

模块 12:显示一个 ASCII 字符

复制代码
void OLED_ShowChar(unsigned char x,unsigned char y,unsigned char chr)
{
	unsigned char c = 0,i = 0;
	c = chr - ' '; // 偏移量计算
	
	if(SIZE == 16) // 16×8大小字符
	{
		OLED_Set_Pos(x,y);
		for(i=0;i<8;i++)
			OLED_WR_Byte(F8X16[c*16+i],OLED_DATA);
		
		OLED_Set_Pos(x,y+1);
		for(i=0;i<8;i++)
			OLED_WR_Byte(F8X16[c*16+8+i],OLED_DATA);
	}
	else // 6×8大小
	{
		OLED_Set_Pos(x,y);
		for(i=0;i<6;i++)
			OLED_WR_Byte(F6x8[c][i],OLED_DATA);
	}
}

原理

  • 一个 16×8 字符占 2 页
  • 先写上 8 行,再写下 8 行
  • 数据来自 codetab.h 字库

模块 13:OLED 初始化(最关键!必须按顺序发)

复制代码
void OLED_Init(void)
{
	OLED_GPIO_Init();	
	delay_ms(200);	

	OLED_WR_Byte(0xAE,OLED_CMD);	// 关显示
	OLED_WR_Byte(0x00,OLED_CMD);	// 列低地址
	OLED_WR_Byte(0x10,OLED_CMD);	// 列高地址
	OLED_WR_Byte(0x40,OLED_CMD);	// 起始行
	OLED_WR_Byte(0xB0,OLED_CMD);	// 页地址
	
	OLED_WR_Byte(0x81,OLED_CMD); 	
	OLED_WR_Byte(0xFF,OLED_CMD);	// 亮度
	
	OLED_WR_Byte(0xA1,OLED_CMD);	// 左右反转
	OLED_WR_Byte(0xA6,OLED_CMD);	// 正常显示
	
	OLED_WR_Byte(0xA8,OLED_CMD);	
	OLED_WR_Byte(0x3F,OLED_CMD);	// 64行
	
	OLED_WR_Byte(0xC8,OLED_CMD);	// 上下反转

	OLED_WR_Byte(0xD3,OLED_CMD);	
	OLED_WR_Byte(0x00,OLED_CMD);	
	
	OLED_WR_Byte(0xD5,OLED_CMD);	
	OLED_WR_Byte(0x80,OLED_CMD);	
	
	OLED_WR_Byte(0xD9,OLED_CMD);	
	OLED_WR_Byte(0xF1,OLED_CMD);	
	
	OLED_WR_Byte(0xDA,OLED_CMD);	
	OLED_WR_Byte(0x12,OLED_CMD);	
	
	OLED_WR_Byte(0xDB,OLED_CMD);	
	OLED_WR_Byte(0x40,OLED_CMD);	
	
	OLED_WR_Byte(0x8D,OLED_CMD);	
	OLED_WR_Byte(0x14,OLED_CMD);	
	
	OLED_WR_Byte(0xAF,OLED_CMD);	// 开显示
	OLED_Clear();        
	OLED_Set_Pos(0,0); 	
}

作用

按 SSD1306 官方要求初始化:

  • 时钟
  • 驱动路数
  • 内存模式
  • 扫描方向
  • 对比度
  • 开启电荷泵(必须开,否则不亮

四、oledr.h 头文件

复制代码
#ifndef _OLEDR_H
#define _OLEDR_H

#include "stm32f10x.h"

//==================== 1. I2C 引脚操作宏定义 ====================
#define OLED_SCLK_Set()    GPIO_SetBits(GPIOB, GPIO_Pin_0)   // SCL=1
#define OLED_SCLK_CLR()    GPIO_ResetBits(GPIOB, GPIO_Pin_0) // SCL=0
#define OLED_SDIN_Set()    GPIO_SetBits(GPIOB, GPIO_Pin_1)   // SDA=1
#define OLED_SDIN_CLR()    GPIO_ResetBits(GPIOB, GPIO_Pin_1) // SDA=0
#define OLED_READ_SDIN()   GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)  // 读SDA电平

//==================== 2. 命令/数据标志位 ====================
#define IIC_ACK      0     // 应答信号
#define IIC_NO_ACK   1     // 非应答信号

#define OLED_CMD     0     // 写命令
#define OLED_DATA    1     // 写数据

//==================== 3. 字库选择 ====================
#define SIZE        16     // 字体大小:16=16*8, 其他=6*8

//==================== 4. 函数声明 ====================
void OLED_WR_Byte(unsigned char data, unsigned char cmd);
void OLED_Set_Pos(unsigned char x,unsigned char y);
void OLED_Display_On(void);
void OLED_Display_Off(void);
void OLED_Clear(void);
void OLED_ShowChar(unsigned char x,unsigned char y,unsigned char chr);
void OLED_Init(void);

#endif

详细解释

1. 防止头文件重复包含

复制代码
#ifndef _OLEDR_H
#define _OLEDR_H
...
#endif
  • 作用:防止一个 .c 文件多次包含同一个头文件,导致重复定义报错
  • 所有正式的 C 语言头文件都必须这么写。

2. 包含 STM32 库文件

复制代码
#include "stm32f10x.h"
  • 作用:引入 STM32 的标准库,让编译器认识 GPIO_SetBitsGPIO_InitTypeDef 等函数和结构体。
  • 没有这一行,所有 GPIO 操作都无法编译。

3. I2C 引脚操作宏(核心!)

复制代码
#define OLED_SCLK_Set()    GPIO_SetBits(GPIOB, GPIO_Pin_0)
#define OLED_SCLK_CLR()    GPIO_ResetBits(GPIOB, GPIO_Pin_0)
#define OLED_SDIN_Set()    GPIO_SetBits(GPIOB, GPIO_Pin_1)
#define OLED_SDIN_CLR()    GPIO_ResetBits(GPIOB, GPIO_Pin_1)
#define OLED_READ_SDIN()   GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)
作用:

把 STM32 复杂的库函数,简化成一句话

  • SCLK_Set() → 让 PB0 输出高电平
  • SCLK_CLR() → 让 PB0 输出低电平
  • SDIN_Set() → 让 PB1 输出高电平
  • SDIN_CLR() → 让 PB1 输出低电平
  • READ_SDIN() → 读取 PB1 现在是高电平还是低电平
为什么要写宏?

原来代码要写:

复制代码
GPIO_ResetBits(GPIOB, GPIO_Pin_0);

现在只需要写:

复制代码
OLED_SCLK_CLR();

代码更简洁、可读性更强、移植更方便

硬件对应:
  • PB0 = I2C_SCL(时钟线)
  • PB1 = I2C_SDA(数据线)

4. 命令 / 数据标志

复制代码
#define OLED_CMD     0     // 写命令
#define OLED_DATA    1     // 写数据
  • OLED 通信规则:
    • 0 → 发送命令(设置屏幕、亮度、坐标)
    • 1 → 发送数据(显示内容、点亮像素点)

5. 字体大小定义

复制代码
#define SIZE        16
  • SIZE=16 → 使用 16×8 大小的 ASCII 字符(两行高度)
  • 不是 16 → 使用 6×8 大小的 ASCII 字符(一行高度)

6. 函数声明

复制代码
void OLED_Init(void);
void OLED_Clear(void);
void OLED_ShowChar(...);
...
  • 作用:告诉编译器,这些函数在 oledr.c 里实现,
  • 这样 main.c 包含头文件后,就可以直接调用这些函数

五、main.c 主函数详细解释

复制代码
#include "stm32f10x.h"
#include "main.h"
#include "stdio.h"
#include "oledr.h"

// 毫秒级延时函数
void delay(uint16_t time)
{
	uint16_t i = 0;
	while(time--)
	{
		i = 12000;
		while(i--);
	}
}

extern const unsigned char BMP2[];

int main(void)
{
    // 中断分组(本工程暂时没用)
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

    // 1. 初始化 OLED
    OLED_Init();

    // 2. 等待 2 秒
    delay(2000);

    // 3. 清屏(全部熄灭)
    OLED_Clear();

    // 4. 显示字符
    OLED_ShowChar(30,2,'O');
    OLED_ShowChar(38,2,'K');

    // 死循环,程序停在这里
    while(1)
    {

    }
}

1. 头文件包含

复制代码
#include "stm32f10x.h"
#include "main.h"
#include "oledr.h"
  • stm32f10x.h:STM32 核心库
  • oledr.h:OLED 驱动头文件(必须包含,否则不能用 OLED 函数)

2. 延时函数

复制代码
void delay(uint16_t time)
{
	uint16_t i = 0;
	while(time--)
	{
		i = 12000;
		while(i--);
	}
}
  • 作用:毫秒级粗略延时
  • 用于:初始化后等待屏幕稳定、方便观察效果

3. 主函数入口

复制代码
int main(void)
{
  • 程序从这里开始执行

4. OLED 初始化(最重要)

复制代码
OLED_Init();
  • 内部执行:
    1. 初始化 PB0、PB1 为开漏输出
    2. 发送一系列 SSD1306 初始化命令
    3. 打开屏幕电荷泵(必须开,否则不亮)
    4. 默认清屏

5. 延时 2 秒

复制代码
delay(2000);
  • 等待屏幕稳定,防止刚上电就操作导致异常

6. 清屏

复制代码
OLED_Clear();
  • 把屏幕所有点都写 0 → 全部熄灭

7. 显示字符

复制代码
OLED_ShowChar(30,2,'O');
OLED_ShowChar(38,2,'K');
  • 格式:OLED_ShowChar(x, y, 字符)
  • x=30 → 水平第 30 列
  • y=2 → 垂直第 2 页(一页 8 行)
  • 功能:在屏幕上显示 OK
相关推荐
百万老师5 小时前
自然语言编程时代,如何零基础学习掌握嵌入式编程
c语言·单片机·嵌入式硬件·学习·ai全流程闭环开发
efangfd5 小时前
TXS0104 和 TXB0104 的 IO 驱动电流对比
单片机·嵌入式硬件
gihigo19986 小时前
STM32F407 Modbus RTU主站例程
stm32·单片机·嵌入式硬件
程序员差不多先生7 小时前
HisparkStudio有哪些开发功能?
嵌入式硬件
都在酒里7 小时前
STM32驱动AT24C系列I2C EEPROM详解(标准库版):零死角,直接可用
stm32·单片机·嵌入式硬件
一枝小雨7 小时前
RISC-V架构的中断与异常处理机制学习笔记
单片机·架构·嵌入式·risc-v·内核原理·中断与异常
国产芯片设计8 小时前
小家电单段码屏项目实战|YL1621 LCD驱动开发与调试心得
驱动开发·stm32·单片机·mcu·51单片机
czhaii8 小时前
STM32G系列单片机产品说明
stm32·单片机·嵌入式硬件
都在酒里8 小时前
STM32标准库驱动TB6612FNG双H桥电机驱动模块(PWM调速/正反转/制动/多模式实战,附完整工程代码)
stm32·单片机·嵌入式硬件