极客秀
搜索

基于STM32F1的软件I2C点亮OLED全流程教学

硬件环境

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

1.转载请保留原文链接谢谢!
2.本站所有资源文章出自互联网收集整理,本站不参与制作,如果侵犯了您的合法权益,请联系本站我们会及时删除。
3.本站发布资源来源于互联网,可能存在水印或者引流等信息,请用户擦亮眼睛自行鉴别,做一个有主见和判断力的用户。
4.本站资源仅供研究、学习交流之用,若使用商业用途,请购买正版授权,否则产生的一切后果将由下载用户自行承担。
5.联系方式(#替换成@):pm#vimge.com

  相关内容