返回列表 发新帖我要提问本帖赏金: 50.00元(功能说明)

[APM32E1] 玩转APM32的DMA-用I2C的DMA实现OLED刷屏

[复制链接]
18518|12
 楼主| shanyuxiang 发表于 2024-5-3 18:06 | 显示全部楼层 |阅读模式
本帖最后由 shanyuxiang 于 2024-6-4 22:40 编辑

#申请原创#  @21小跑堂
玩转APM32的DMA-用I2C的DMA实现OLED刷屏


一、前言

1.1、关于OLED

OLED屏是一种常见的的显示屏,下面以0.96寸OLED模块为例来实现用IIC的DMA来实现OLED屏幕的刷新,
用DMA方式不需要程序一个个字节发送,通过启动DMA自动完成整个屏幕的刷新,可以节约大量的CPU时间。
该屏幕分辨率为128x64,每个点占用1bit,于是整个显存占用128x64/8=1024Byte,驱动芯片为SSD1306,支持SPI和IIC接口,
这里采用IIC接口,只需要接三根线SCL、SDA、RES,这里IIC接到I2C1上,
RES是复位可以用硬件复位,也可以通过IO控制复位,这里随便接一个IO即可。

时钟 SCL  -- PB6
数据 SDA -- PB7
复位 RES -- PB5

显存中的第一个字节表示第1列的第1到8行这8个点,也就是坐标为X[0],Y[0-7]的点,整个显存如下图所示:
OLED12864显存.drawio.png

10-3.jpg


1.2、关于IIC的DMA通道

APM32E103的IIC是支持DMA的收发的,通过芯片的用户手册可知I2C1_TX的对应的是 DMA1的通道6。

1.png


二、IIC的DMA发送

2.1 IIC初始化

这里参考SDK中的“I2C\I2C_TwoBoards\I2C_TwoBoards_Master”例程,配置为主机模式,修改一下地址即可:

  1. void oled_i2c_hardware_init(void)
  2. {
  3.     GPIO_Config_T gpioConfigStruct;
  4.     I2C_Config_T i2cConfigStruct;
  5.     /** Enable I2C related Clock */
  6.     RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOB | RCM_APB2_PERIPH_AFIO);
  7.     RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_I2C1);

  8.     /** Free I2C_SCL and I2C_SDA */
  9.     gpioConfigStruct.mode = GPIO_MODE_AF_OD;
  10.     gpioConfigStruct.speed = GPIO_SPEED_50MHz;
  11.     gpioConfigStruct.pin = GPIO_PIN_6;
  12.     GPIO_Config(GPIOB, &gpioConfigStruct);

  13.     gpioConfigStruct.mode = GPIO_MODE_AF_OD;
  14.     gpioConfigStruct.speed = GPIO_SPEED_50MHz;
  15.     gpioConfigStruct.pin = GPIO_PIN_7;
  16.     GPIO_Config(GPIOB, &gpioConfigStruct);

  17.     /**  Config I2C1 */
  18.     I2C_Reset(I2C1);
  19.     i2cConfigStruct.mode = I2C_MODE_I2C;
  20.     i2cConfigStruct.dutyCycle = I2C_DUTYCYCLE_2;
  21.     i2cConfigStruct.ackAddress = I2C_ACK_ADDRESS_7BIT;
  22.     //i2cConfigStruct.ownAddress1 = 0XA0;
  23.     i2cConfigStruct.ownAddress1 = SSD1306_ADDRESS;
  24.     i2cConfigStruct.ack = I2C_ACK_ENABLE;
  25.     i2cConfigStruct.clockSpeed = 400000;

  26.     I2C_Config(I2C1, &i2cConfigStruct);

  27.     /** Enable I2Cx */
  28.     I2C_Enable(I2C1);


  29.     i2c_dma_init();

  30. }


2.2 DMA初始化

在SDK中“DMA_MemoryToMemory”的基础上进行修改,官方例程中是内存到内存,而这里是从内存到IIC外设,所以需要根据实际情况作修改,

修改传输方向,以外设作为目的地址:
DMA_ConfigStruct.dir  = DMA_DIR_PERIPHERAL_DST;


外设的地址填 I2C1_DATA 寄存器的地址,查看用户手册可知DATA寄存器的偏移地址是0x10:
DMA_ConfigStruct.peripheralBaseAddr = (uint32_t)(I2C1_BASE + 0x10) ;

3.png

数据大小改为按字节传输:
DMA_ConfigStruct.memoryDataSize     = DMA_MEMORY_DATA_SIZE_BYTE;

完整的IIC的DMA初始化代码如下:
  1. void i2c_dma_init(void)
  2. {
  3.     DMA_Config_T    DMA_ConfigStruct;
  4.     RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA1);

  5.     DMA_Reset(DMA1_Channel6);

  6.     DMA_ConfigStruct.peripheralBaseAddr = (uint32_t)(I2C1_BASE + 0x10) ;
  7.     DMA_ConfigStruct.memoryBaseAddr     = (uint32_t)NULL;
  8.     DMA_ConfigStruct.dir                = DMA_DIR_PERIPHERAL_DST;
  9.     DMA_ConfigStruct.bufferSize         = 0;
  10.     DMA_ConfigStruct.peripheralInc      = DMA_PERIPHERAL_INC_DISABLE;
  11.     DMA_ConfigStruct.memoryInc          = DMA_MEMORY_INC_ENABLE;
  12.     DMA_ConfigStruct.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_BYTE;
  13.     DMA_ConfigStruct.memoryDataSize     = DMA_MEMORY_DATA_SIZE_BYTE;
  14.     DMA_ConfigStruct.loopMode = DMA_MODE_NORMAL;
  15.     DMA_ConfigStruct.priority = DMA_PRIORITY_HIGH;
  16.     DMA_ConfigStruct.M2M      = DMA_M2MEN_DISABLE;

  17.     DMA_Config(DMA1_Channel6, &DMA_ConfigStruct);

  18.     I2C_EnableDMA(I2C1);

  19. }


2.3 用IIC的DMA发送

在启动DMA的传输之前,要配置源数据内存地址,传输长度,然后使能传输,使能传输之后CPU可以做其他事情,也可以等待传输完成:
  1. void i2c_dma_transmit_buffer(unsigned char *buffer, unsigned int length)
  2. {

  3.     DMA_Disable(DMA1_Channel6);
  4.     DMA1_Channel6->CHMADDR = (uint32_t)buffer;
  5.     DMA1_Channel6->CHNDATA = length;
  6.     DMA_Enable(DMA1_Channel6);
  7.     while (DMA_ReadStatusFlag(DMA1_FLAG_TC6) == RESET);

  8. }


三、OLED的驱动

3.1修改OLED地址模式

OLED的默认是页地址模式,每写入一行都要设置一下坐标,这样的话传输给OLED的数据就多了很多命令和地址,非常不适合这里的DMA方式刷屏,
理想的方式是只发一次地址,驱动芯片内部能对地址自增,这样就可以一次性发送所有显存中的数据,这里修改一下OLED的地址模式就可以实现,
官方例程默认是页地址模式,每次换行显示时需要重新发送地址,改为垂直地址模式就不用每次都发送地址,省去了额外的数据,
对于128x64的屏幕来说只要发送128x64/8=1024字节即可。

查看SSD1306的的数据手册可知,地址模式默认是10b,把地址为0x20的寄存器值写成00b就是水平地址模式。
2.png


这样配置驱动芯片SSD1306:
  1. void oled_register_config()
  2. {
  3.     oled_i2c_wr_byte(0xAE, OLED_CMD);

  4.     oled_i2c_wr_byte(0xAE, OLED_CMD); //--turn off oled panel
  5.     oled_i2c_wr_byte(0x00, OLED_CMD); //---set low column address
  6.     oled_i2c_wr_byte(0x10, OLED_CMD); //---set high column address
  7.     oled_i2c_wr_byte(0x40, OLED_CMD); //--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
  8.     oled_i2c_wr_byte(0x81, OLED_CMD); //--set contrast control register
  9.     oled_i2c_wr_byte(0xCF, OLED_CMD); // Set SEG Output Current Brightness
  10.     oled_i2c_wr_byte(0xA0, OLED_CMD); //oled_i2c_wr_byte(0xA1,OLED_CMD);//--Set SEG/Column Mapping     0xa0左右反置 0xa1正常
  11.     oled_i2c_wr_byte(0xC0, OLED_CMD); //oled_i2c_wr_byte(0xC8,OLED_CMD);//Set COM/Row Scan Direction   0xc0上下反置 0xc8正常
  12.     oled_i2c_wr_byte(0xA6, OLED_CMD); //--set normal display
  13.     oled_i2c_wr_byte(0xA8, OLED_CMD); //--set multiplex ratio(1 to 64)
  14.     oled_i2c_wr_byte(0x3f, OLED_CMD); //--1/64 duty
  15.     oled_i2c_wr_byte(0xD3, OLED_CMD); //-set display offset   Shift Mapping RAM Counter (0x00~0x3F)
  16.     oled_i2c_wr_byte(0x00, OLED_CMD); //-not offset
  17.     oled_i2c_wr_byte(0xd5, OLED_CMD); //--set display clock divide ratio/oscillator frequency
  18.     oled_i2c_wr_byte(0x80, OLED_CMD); //--set divide ratio, Set Clock as 100 Frames/Sec
  19.     oled_i2c_wr_byte(0xD9, OLED_CMD); //--set pre-charge period
  20.     oled_i2c_wr_byte(0xF1, OLED_CMD); //Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
  21.     oled_i2c_wr_byte(0xDA, OLED_CMD); //--set com pins hardware configuration
  22.     oled_i2c_wr_byte(0x12, OLED_CMD);
  23.     oled_i2c_wr_byte(0xDB, OLED_CMD); //--set vcomh
  24.     oled_i2c_wr_byte(0x40, OLED_CMD); //Set VCOM Deselect Level
  25.                
  26.                 #if 1
  27.                 oled_i2c_wr_byte(0x20, OLED_CMD); //-Set Page Addressing Mode (0x00/0x01/0x02) 设置地址模式
  28.     oled_i2c_wr_byte(0x00, OLED_CMD); //00b, Horizontal Addressing Mode  水平地址模式
  29.                 #else
  30.     oled_i2c_wr_byte(0x20, OLED_CMD); //-Set Page Addressing Mode (0x00/0x01/0x02)
  31.     oled_i2c_wr_byte(0x02, OLED_CMD); //10b, Page Addressing Mode (RESET)
  32.                 #endif
  33.                
  34.     oled_i2c_wr_byte(0x8D, OLED_CMD); //--set Charge Pump enable/disable
  35.     oled_i2c_wr_byte(0x14, OLED_CMD); //--set(0x10) disable
  36.     oled_i2c_wr_byte(0xA4, OLED_CMD); // Disable Entire Display On (0xa4/0xa5)
  37.     oled_i2c_wr_byte(0xA6, OLED_CMD); // Disable Inverse Display On (0xa6/a7)
  38.     oled_i2c_wr_byte(0xAF, OLED_CMD); //--turn on oled panel

  39.     oled_i2c_wr_byte(0xAF, OLED_CMD); /*display ON*/
  40. }


3.2 实现OLED刷全屏

在发送显示数据之前还是先发一个从机设备地址0x78,再发一个写数据指令0x40,

接着以DMA方式发送1024字节的显示数据,这样就完成了整个屏幕的刷新,刷新过程中不需要CPU的干预:

  1. void oled_i2c_write_buffer(unsigned char *buffer, unsigned int length)
  2. {

  3.     I2C_EnableGenerateStart(I2C1);
  4.     while (!I2C_ReadEventStatus(I2C1, I2C_EVENT_MASTER_MODE_SELECT));  //EV5

  5.     I2C_Tx7BitAddress(I2C1, SSD1306_ADDRESS, I2C_DIRECTION_TX);
  6.     while (!I2C_ReadEventStatus(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); //EV6

  7.     I2C_TxData(I2C1, 0x40);
  8.     while (!I2C_ReadEventStatus(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  //EV8

  9.     i2c_dma_transmit_buffer(buffer, length);  //DMA Transmit

  10. }

  1. //刷新整个屏幕
  2. void oled_refresh(unsigned char mem[])
  3. {
  4.     oled_i2c_write_buffer(mem, OLED_MEM_SIZE);

  5.     oled_delay_ms(200);
  6. }



四、效果演示

为了测试IIC的DMA刷屏,用GIF转了个12帧的位图,在循环中依次调用全屏刷新函数,可以看到动画效果。
  1. int main(void)
  2. {

  3.         oled_init();

  4.         while(1)
  5.         {

  6.                  oled_refresh(&mario1[BMP_OFFSET]);
  7.                  oled_refresh(&mario2[BMP_OFFSET]);
  8.                  oled_refresh(&mario3[BMP_OFFSET]);
  9.                   oled_refresh(&mario4[BMP_OFFSET]);
  10.                   oled_refresh(&mario5[BMP_OFFSET]);
  11.                   oled_refresh(&mario6[BMP_OFFSET]);
  12.                   oled_refresh(&mario7[BMP_OFFSET]);
  13.                   oled_refresh(&mario8[BMP_OFFSET]);
  14.                   oled_refresh(&mario9[BMP_OFFSET]);
  15.                   oled_refresh(&mario10[BMP_OFFSET]);
  16.                   oled_refresh(&mario11[BMP_OFFSET]);
  17.                   oled_refresh(&mario12[BMP_OFFSET]);
  18.    
  19.         }
  20. }


位图的取模可以用工具 PCtoLCD2003,取模方式按如下设置,主要要注意选择行列式,低位在前,输出格式调成数组即可:

5.png


在while(1)循环中依次将每张图片作为显存发送给OLED,每张图片之间配合一定的延时,这样就形成了动画效果:
  1. while(1)
  2.         {

  3.                oled_refresh(&mario1[BMP_OFFSET]);
  4.                oled_refresh(&mario2[BMP_OFFSET]);
  5.                oled_refresh(&mario3[BMP_OFFSET]);
  6.                   oled_refresh(&mario4[BMP_OFFSET]);
  7.                   oled_refresh(&mario5[BMP_OFFSET]);
  8.                   oled_refresh(&mario6[BMP_OFFSET]);
  9.                   oled_refresh(&mario7[BMP_OFFSET]);
  10.                   oled_refresh(&mario8[BMP_OFFSET]);
  11.                   oled_refresh(&mario9[BMP_OFFSET]);
  12.                   oled_refresh(&mario10[BMP_OFFSET]);
  13.                   oled_refresh(&mario11[BMP_OFFSET]);
  14.                   oled_refresh(&mario12[BMP_OFFSET]);
  15.    
  16.         }


最后是OLED显示马里奥跑步的动画效果(加载的图片较大,需要稍等一会...):

11.gif

  

APM32E10x_SDK_V1.1_oled_i2c_dma.rar

251.89 KB, 下载次数: 39

本次实验的源码

打赏榜单

21小跑堂 打赏了 50.00 元 2024-07-29
理由:恭喜通过原创审核!期待您更多的原创作品~

评论

你好,APM32项目合作【兼职项目,不耽误上班】,感兴趣可以私聊,项目薪资1-1.2万  发表于 2025-5-21 13:23
基于APM32单片机的I2C通信,使用DMA释放CPU压力,完成OLED屏幕的显示。  发表于 2024-7-29 16:52
caigang13 发表于 2024-5-4 08:43 来自手机 | 显示全部楼层
用DMA可以提高CPU利用效率,本来IIC通信效率就低了。
 楼主| shanyuxiang 发表于 2024-5-4 15:19 | 显示全部楼层
caigang13 发表于 2024-5-4 08:43
用DMA可以提高CPU利用效率,本来IIC通信效率就低了。

IIC速度确实不高 用DMA的话在传输过程中CPU可以做其他事
chenjun89 发表于 2024-5-5 20:11 来自手机 | 显示全部楼层
用DMA可以弥补一下IIC的劣势
21小跑堂 发表于 2024-5-13 14:28 | 显示全部楼层
halo 大佬,根据我们的审核标准,您目前的文章还没满800字哦~您可以补充内容后再次@21小跑堂进行审核~
 楼主| shanyuxiang 发表于 2024-5-15 09:19 | 显示全部楼层
21小跑堂 发表于 2024-5-13 14:28
halo 大佬,根据我们的审核标准,您目前的文章还没满800字哦~您可以补充内容后再次@21小跑堂进行审核~ ...

可以自己查看字数吗?
WoodData 发表于 2024-5-16 15:40 | 显示全部楼层
学习学习
szt1993 发表于 2024-5-23 17:49 | 显示全部楼层
IIC数据通信非常简单实用的通信方式
夜幕叙事曲 发表于 2025-5-30 23:00 | 显示全部楼层
我一直觉得DMA的应用一定要上RTOS才可以
涡流远见者 发表于 2025-5-31 08:44 | 显示全部楼层
DMA发送过程中要是发生错误了怎么处理啊?
还走I2C错误中断吗?
 楼主| shanyuxiang 发表于 2025-6-14 11:30 | 显示全部楼层
涡流远见者 发表于 2025-5-31 08:44
DMA发送过程中要是发生错误了怎么处理啊?
还走I2C错误中断吗?

I2C和DMA都有自己的错误标志,要看具体的错误类型
您需要登录后才可以回帖 登录 | 注册

本版积分规则

8

主题

50

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部