单片机: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,非常方便且高效,并且我们利用取模软件实现字符串的显示。

#ifndef__OLED_H__#
#includemain.h#
#defineSCL_PortGPIOB#
#defineSCL_PinGPIO_PIN_10#
嘉立创PCB

还没有评论,抢个沙发!