LIZARD925 发表于 2025-6-28 15:37

APM32E030 IIC的使用与OLED屏幕

本帖最后由 LIZARD925 于 2025-6-28 15:37 编辑

#技术资源#

APM32E030系列使用记录--软硬件IIC驱动OLED屏幕与菜单的移植
软硬件IIC驱动OLED屏幕显示:
此例程中,我们使用软件(硬件)IIC驱动0.96寸OLED屏幕,实现OLED屏幕显示字符、数字、中文等信息,由于网上有很多开源的OLED驱动了,所用我们只需要配置好底层的通信接口即可,我们使用上次移植好按键库的程序,找到江科大的开源OLED屏幕驱动,将下面4个文件复制到自己的工程,注意我们使用的是GB2312编码,需要选择对应的程序,否则需要进行编码的转换,加入文件夹后打开keil加入到工程中,如图所示:


加入后,我们只需要更改底层的GPIO驱动函数,实现软件模拟IIC的数据通信,即完成了软件IIC驱动OLED屏幕的移植,IIC引脚使用PB8与PB9,主要函数如下所示:
/**
* 函    数:OLED写SCL高低电平
* 参    数:要写入SCL的电平值,范围:0/1
* 返 回 值:无
* 说    明:当上层函数需要写SCL时,此函数会被调用
*         用户需要根据参数传入的值,将SCL置为高电平或者低电平
*         当参数传入0时,置SCL为低电平,当参数传入1时,置SCL为高电平
*/
void OLED_W_SCL(uint8_t BitValue)
{
      /*根据BitValue的值,将SCL置高电平或者低电平*/
      GPIO_WriteBitValue(GPIOB, GPIO_PIN_8, BitValue);
      
      /*如果单片机速度过快,可在此添加适量延时,以避免超出I2C通信的最大速度*/
      //...
}

/**
* 函    数:OLED写SDA高低电平
* 参    数:要写入SDA的电平值,范围:0/1
* 返 回 值:无
* 说    明:当上层函数需要写SDA时,此函数会被调用
*         用户需要根据参数传入的值,将SDA置为高电平或者低电平
*         当参数传入0时,置SDA为低电平,当参数传入1时,置SDA为高电平
*/
void OLED_W_SDA(uint8_t BitValue)
{
      /*根据BitValue的值,将SDA置高电平或者低电平*/
      GPIO_WriteBitValue(GPIOB, GPIO_PIN_9, BitValue);
      
      /*如果单片机速度过快,可在此添加适量延时,以避免超出I2C通信的最大速度*/
      //...
}

/**
* 函    数:OLED引脚初始化
* 参    数:无
* 返 回 值:无
* 说    明:当上层函数需要初始化时,此函数会被调用
*         用户需要将SCL和SDA引脚初始化为开漏模式,并释放引脚
*/
void OLED_GPIO_Init(void)
{
      uint32_t i, j;
      
      /*在初始化前,加入适量延时,待OLED供电稳定*/
      for (i = 0; i < 1000; i ++)
      {
                for (j = 0; j < 1000; j ++);
      }
      
      /*将SCL和SDA引脚初始化为开漏模式*/
      RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_GPIOB);      //打开GPIOB的时钟

      GPIO_Config_T GPIO_InitStructure;   
      
      GPIO_InitStructure.mode = GPIO_MODE_OUT;            // 输出模式
      GPIO_InitStructure.pin = GPIO_PIN_8|GPIO_PIN_9;      
      GPIO_InitStructure.speed = GPIO_SPEED_50MHz;      
      GPIO_InitStructure.outtype = GPIO_OUT_TYPE_OD;      //开漏输出
      GPIO_Config(GPIOB,&GPIO_InitStructure);

      
      /*释放SCL和SDA*/
      OLED_W_SCL(1);
      OLED_W_SDA(1);
}修改后,如不报错,则移植基本成功,可在主函数中添加初始化,并测试屏幕显示,可看到屏幕成功驱动,显示中文,英文,数字,图片等,主要初始化函数如下所示:
      /*OLED初始化*/
      OLED_Init();
      
      /*在(0, 0)位置显示字符'A',字体大小为8*16点阵*/
      OLED_ShowChar(0, 0, 'A', OLED_8X16);
      
      /*在(16, 0)位置显示字符串"Hello World!",字体大小为8*16点阵*/
      OLED_ShowString(16, 0, "Hello World!", OLED_8X16);
      
      /*在(0, 18)位置显示字符'A',字体大小为6*8点阵*/
      OLED_ShowChar(0, 18, 'A', OLED_6X8);
      
      /*在(16, 18)位置显示字符串"Hello World!",字体大小为6*8点阵*/
      OLED_ShowString(16, 18, "Hello World!", OLED_6X8);
      
      /*在(0, 28)位置显示数字12345,长度为5,字体大小为6*8点阵*/
      OLED_ShowNum(0, 28, 12345, 5, OLED_6X8);
      
      /*在(40, 28)位置显示有符号数字-66,长度为2,字体大小为6*8点阵*/
      OLED_ShowSignedNum(40, 28, -66, 2, OLED_6X8);
      
      /*在(70, 28)位置显示十六进制数字0xA5A5,长度为4,字体大小为6*8点阵*/
      OLED_ShowHexNum(70, 28, 0xA5A5, 4, OLED_6X8);
      
      /*在(0, 38)位置显示二进制数字0xA5,长度为8,字体大小为6*8点阵*/
      OLED_ShowBinNum(0, 38, 0xA5, 8, OLED_6X8);
      
      /*在(60, 38)位置显示浮点数字123.45,整数部分长度为3,小数部分长度为2,字体大小为6*8点阵*/
      OLED_ShowFloatNum(60, 38, 123.45, 3, 2, OLED_6X8);
      
      /*在(0, 48)位置显示汉字串"你好,世界。",字体大小为固定的16*16点阵*/
      OLED_ShowChinese(0, 48, "你好,世界。");
      
      /*在(96, 48)位置显示图像,宽16像素,高16像素,图像数据为Diode数组*/
      OLED_ShowImage(96, 48, 16, 16, Diode);
      
      /*在(96, 18)位置打印格式化字符串,字体大小为6*8点阵,格式化字符串为"[%02d]"*/
      OLED_Printf(96, 18, OLED_6X8, "[%02d]", 6);
      
      /*调用OLED_Update函数,将OLED显存数组的内容更新到OLED硬件进行显示*/
      OLED_Update();
      
      /*延时3000ms,观察现象*/
      Delay_ms(3000);
..........至此,软件IIC驱动OLED屏幕大致完成,但某些时候,可能软件IIC速度不够用,对于一些简单的屏幕显示来说可能足够,如果跑菜单等UI,软件IIC的速度很影响菜单的显示与使用体验,所以我们可以配置硬件IIC,来优化IIC驱动OLED的使用体验,我们先将简单的屏幕显示优化成硬件IIC驱动,我们需要初始化硬件IIC,并使用硬件IIC的通信接口,去适配我们的协议层,我们需要更改OLED.C 前面的引脚配置,并将软件模拟的IIC读写函数改为硬件读写命令。
/**
* 函    数:OLED引脚初始化
* 参    数:无
* 返 回 值:无
* 说    明:当上层函数需要初始化时,此函数会被调用
*         用户需要将SCL和SDA引脚初始化为开漏模式,并释放引脚
*/
void OLED_GPIO_Init(void)
{
   /*将SCL和SDA引脚初始化为开漏模式*/
    GPIO_Config_T gpio_init_structure;
    I2C_Config_T iic_init_struct;
      
    RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_GPIOB);
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_I2C1);
    RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_SYSCFG);

      /* Connect I2C to SCL */
    GPIO_ConfigPinAF(GPIOB, GPIO_PIN_SOURCE_8, GPIO_AF_PIN1);
    /* Connect I2C to SDA */
    GPIO_ConfigPinAF(GPIOB, GPIO_PIN_SOURCE_9, GPIO_AF_PIN1);
      
    gpio_init_structure.mode = GPIO_MODE_AF;
    gpio_init_structure.speed = GPIO_SPEED_50MHz;
    gpio_init_structure.outtype = GPIO_OUT_TYPE_OD;
    gpio_init_structure.pupd = GPIO_PUPD_NO;
    gpio_init_structure.pin = GPIO_PIN_8;

    GPIO_Config(GPIOB, &gpio_init_structure);

    gpio_init_structure.pin = GPIO_PIN_9;
    GPIO_Config(GPIOB, &gpio_init_structure);
      
    /* Config I2C1 */
    I2C_Reset(I2C1);
    RCM_ConfigI2CCLK(RCM_I2C1CLK_SYSCLK);
    iic_init_struct.ack = I2C_ACK_ENABLE;
    iic_init_struct.ackaddress = I2C_ACK_ADDRESS_7BIT;
    iic_init_struct.address1 = 0XA0;
    iic_init_struct.analogfilter = I2C_ANALOG_FILTER_ENABLE;
    iic_init_struct.digitalfilter = I2C_DIGITAL_FILTER_0;
    iic_init_struct.mode = I2C_MODE_I2C;
    iic_init_struct.timing = 0x80100103;      //0x00042121--766K0x1042F013--130K0x80100103--900K
    I2C_Config(I2C1, &iic_init_struct);

    I2C_Enable(I2C1);
}

/*********************引脚配置*/


/*通信协议*********************/

/**
* 函    数:I2C发送一个字节
* 参    数:Byte 要发送的一个字节数据,范围:0x00~0xFF
* 返 回 值:无
*/
void OLED_I2C_SendByte(uint8_t addr,uint8_t Byte)
{
    I2C_HandlingTransfer(I2C1, EEPROM_WRITE_ADDR, 2, I2C_RELOAD_MODE_AUTOEND, I2C_GENERATE_START_WRITE);
    I2C_TxData(I2C1, addr);
    while (I2C_ReadStatusFlag(I2C1, I2C_FLAG_TXBE) == RESET);
    I2C_TxData(I2C1, Byte);
    while (I2C_ReadStatusFlag(I2C1, I2C_FLAG_TXBE) == RESET);

    I2C_EnableGenerateStop(I2C1);

    I2C_ClearIntFlag(I2C1, I2C_INT_FLAG_STOP);
}

/**
* 函    数:OLED写命令
* 参    数:Command 要写入的命令值,范围:0x00~0xFF
* 返 回 值:无
*/
void OLED_WriteCommand(uint8_t Command)
{
      OLED_I2C_SendByte(0x00,Command);
}

/**
* 函    数:OLED写数据
* 参    数:Data 要写入数据的起始地址
* 参    数:Count 要写入数据的数量
* 返 回 值:无
*/
void OLED_WriteData(uint8_t *Data, uint8_t Count)
{
    while (Count--) {
      OLED_I2C_SendByte(0x40, *Data);
      Data++;
    }
}上面为硬件IIC的初始化函数,在硬件IIC的初始化中,其它都为通用的设置,但IIC频率的设置,我们需要查看手册进行配置,不同的配置可以使IIC的时钟工作在不同的频率,工程中,我将硬件IIC时钟配置成了1M,驱动OLED显示没问题,为后面的菜单UI刷新打下基础,由于我们OLED屏幕驱动基本为单方向的数据发送,所以主机的地址可以不用配置。

对于I2C发送一个字节的驱动,本例程OLED默认是页地址模式,所以我们发送基本有从机地址+数据地址+数据+停止位构成,所以我们的发送配置发送2字节,其余都是默认的操作。下载验证现可以正常点亮屏幕,至此硬件IIC驱动OLED我们移植完成。
菜单的移植:
经过查看数据手册,E030的IIC硬件一次只能发送256个字节,所以要用DMA+IIC,一次发送128*8个字节实现全刷屏不好实现,我们本次只使用硬件IIC去实现OLED的屏幕驱动,根据WouoUI的说明,将文件夹复制到自己的工程,并添加到工程中


在此文件下,先暂时屏蔽打印函数,并设置自己的屏幕大小,其余暂时不用更改

此时只需将原先的硬件OLED初始化函数,复制到 WouoUI_user.c中,
/**
* 函    数:OLED引脚初始化
* 参    数:无
* 返 回 值:无
* 说    明:当上层函数需要初始化时,此函数会被调用
*         用户需要将SCL和SDA引脚初始化为开漏模式,并释放引脚
*/
void OLED_GPIO_Init(void)
{
      /*将SCL和SDA引脚初始化为开漏模式*/
    GPIO_Config_T gpio_init_structure;
    I2C_Config_T iic_init_struct;
      
    RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_GPIOB);
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_I2C1);
    RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_SYSCFG);

      /* Connect I2C to SCL */
    GPIO_ConfigPinAF(GPIOB, GPIO_PIN_SOURCE_8, GPIO_AF_PIN1);
    /* Connect I2C to SDA */
    GPIO_ConfigPinAF(GPIOB, GPIO_PIN_SOURCE_9, GPIO_AF_PIN1);
      
    gpio_init_structure.mode = GPIO_MODE_AF;
    gpio_init_structure.speed = GPIO_SPEED_50MHz;
    gpio_init_structure.outtype = GPIO_OUT_TYPE_OD;
    gpio_init_structure.pupd = GPIO_PUPD_NO;
    gpio_init_structure.pin = GPIO_PIN_8;

    GPIO_Config(GPIOB, &gpio_init_structure);

    gpio_init_structure.pin = GPIO_PIN_9;
    GPIO_Config(GPIOB, &gpio_init_structure);
      
      /* Config I2C1 */
    I2C_Reset(I2C1);
    RCM_ConfigI2CCLK(RCM_I2C1CLK_SYSCLK);
    iic_init_struct.ack = I2C_ACK_ENABLE;
    iic_init_struct.ackaddress = I2C_ACK_ADDRESS_7BIT;
    iic_init_struct.address1 = 0XA0;
    iic_init_struct.analogfilter = I2C_ANALOG_FILTER_ENABLE;
    iic_init_struct.digitalfilter = I2C_DIGITAL_FILTER_0;
    iic_init_struct.mode = I2C_MODE_I2C;
    iic_init_struct.timing = 0x80100103;      //0xF--10K0x00042121--766K0x1042F013--130K0x80100103--900K
    I2C_Config(I2C1, &iic_init_struct);

    I2C_Enable(I2C1);
}
/**
* 函    数:I2C发送一个字节
* 参    数:Byte 要发送的一个字节数据,范围:0x00~0xFF
* 返 回 值:无
*/
void OLED_I2C_SendByte(uint8_t addr,uint8_t Byte)
{
    I2C_HandlingTransfer(I2C1, EEPROM_WRITE_ADDR, 2, I2C_RELOAD_MODE_AUTOEND, I2C_GENERATE_START_WRITE);
    I2C_TxData(I2C1, addr);
    while (I2C_ReadStatusFlag(I2C1, I2C_FLAG_TXBE) == RESET);
    I2C_TxData(I2C1, Byte);
    while (I2C_ReadStatusFlag(I2C1, I2C_FLAG_TXBE) == RESET);

    I2C_EnableGenerateStop(I2C1);

    I2C_ClearIntFlag(I2C1, I2C_INT_FLAG_STOP);
}
/**
* 函    数:OLED写命令
* 参    数:Command 要写入的命令值,范围:0x00~0xFF
* 返 回 值:无
*/
void OLED_WriteCommand(uint8_t Command)
{
      OLED_I2C_SendByte(0x00,Command);
}

/**
* 函    数:OLED写数据
* 参    数:Data 要写入数据的起始地址
* 参    数:Count 要写入数据的数量
* 返 回 值:无
*/
void OLED_WriteData(uint8_t *Data, uint8_t Count)
{
    while (Count--) {
      OLED_I2C_SendByte(0x40, *Data);
      Data++;
    }
}
void OLED_SendBuff(uint8_t buff)
{                  
      for(uint8_t i=0;i<8;i++)
      {
                OLED_WriteCommand (0xb0+i);    //设置页地址(0~7)(b0-b7)
                OLED_WriteCommand(0x00);//---set low column address
                OLED_WriteCommand (0x10);      //设置显示位置—列高地址   
                OLED_WriteData(buff,128); //写一页128个字符
      }
}

//--------------给主函数调用的接口函数
void TestUI_Init(void) {
    OLED_GPIO_Init();//硬件的初始化
在初始化函数中加入硬件IIC的初始化,并在main函数中调用即可
#include "apm32e030.h"                  // Device header
#include "Delay.h"
#include "LED.h"
#include "button_app.h"
#include "WouoUI.h"
#include "WouoUI_user.h"

int main (void)
{
      LED_init();
      
      /*按键初始化*/
      button_Init();
      
      WouoUI_SelectDefaultUI();
    WouoUI_AttachSendBuffFun(OLED_SendBuff);
    TestUI_Init();

      while(1)
      {
                WouoUI_Proc(30);
      }
}使用硬件IIC,并将 UI的时间尺度增大到30 ,基本可以满足UI的刷新,在按键库中,定义按键1短按为向下,按键2短按为向上,按键1长按
长按为进入,按键2长按为退出,按键1双击为向左,按键2双击为向右。
void BTN1_PRESS_DOWN_Handler(void* btn)
{
      LED1_on();
      WOUOUI_MSG_QUE_SEND(msg_down);
}
void BTN1_PRESS_REPEAT_Handler(void* btn)
{
      LED1_off();
      WOUOUI_MSG_QUE_SEND(msg_left);
}
void BTN1_LONG_RRESS_START_Handler(void* btn)
{
      LED1_turn();
      WOUOUI_MSG_QUE_SEND(msg_click);
}

void BTN2_PRESS_DOWN_Handler(void* btn)
{
      LED2_on();
      WOUOUI_MSG_QUE_SEND(msg_up);
}
void BTN2_PRESS_REPEAT_Handler(void* btn)
{
      LED2_off();
      WOUOUI_MSG_QUE_SEND(msg_right);
}
void BTN2_LONG_RRESS_START_Handler(void* btn)
{
      LED2_turn();
      WOUOUI_MSG_QUE_SEND(msg_return);
}
这样,我们的OLED屏幕菜单框架基本就搭建完成,可以通过按键进行简单的测试,如需要更快的速度,建议使用SPI接口去实现,只需要实现接口函数即可,本次IIC的OLED屏幕驱动大致完成。











永恒回声 发表于 2025-6-28 16:10

现在的I2C设备有支持1Mbps的速率的吗?
我怎么感觉支持400K的都少呢

梦之一瞥 发表于 2025-6-28 22:30

I2C的硬件模式下,数据通讯的效率还是挺高的。

SpiritSong 发表于 2025-6-29 17:56

我看楼主在I2C初始化的时候未使能中断,仍然使用的查询的方式来做的通讯。
I2C通讯的性能如何?有做过I2C中断或者DMA方式的实现吗?

霜咬回响 发表于 2025-6-30 14:53

E030的硬件资源还可以跑GUI吗?
显示字符串是不是基本就满了

星云狂想曲 发表于 2025-6-30 20:44

我去查了一下datasheet。APM32E030的I2C外设最快支持到400Kbps的速率。
这个速率下传输的吞吐率应该也能达到 40KBps吧

OceanGaze 发表于 2025-7-3 14:21

想想通过中断或者DMA的方式进行通讯的实际案例。

FrostShimmer 发表于 2025-7-3 19:34

要是驱动OLED小屏,E030的小MCU只发送的话,I2C的速率应该是很快的吧!
手里没有I2C接口的OLED屏,有机会试试
页: [1]
查看完整版本: APM32E030 IIC的使用与OLED屏幕