【玩转APM32的DMA】手把手教你用I2C的DMA实现OLED刷屏

科技   2024-10-30 12:01   北京  
一、前言

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]的点,整个显存如下图所示:



1.2、关于IIC的DMA通道

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


二、IIC的DMA发送

2.1、IIC初始化

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

void oled_i2c_hardware_init(void){    GPIO_Config_T gpioConfigStruct;    I2C_Config_T i2cConfigStruct;    /** Enable I2C related Clock */    RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOB | RCM_APB2_PERIPH_AFIO);    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_I2C1);
/** Free I2C_SCL and I2C_SDA */ gpioConfigStruct.mode = GPIO_MODE_AF_OD; gpioConfigStruct.speed = GPIO_SPEED_50MHz; gpioConfigStruct.pin = GPIO_PIN_6; GPIO_Config(GPIOB, &gpioConfigStruct);
gpioConfigStruct.mode = GPIO_MODE_AF_OD; gpioConfigStruct.speed = GPIO_SPEED_50MHz; gpioConfigStruct.pin = GPIO_PIN_7; GPIO_Config(GPIOB, &gpioConfigStruct);
/** Config I2C1 */ I2C_Reset(I2C1); i2cConfigStruct.mode = I2C_MODE_I2C; i2cConfigStruct.dutyCycle = I2C_DUTYCYCLE_2; i2cConfigStruct.ackAddress = I2C_ACK_ADDRESS_7BIT; //i2cConfigStruct.ownAddress1 = 0XA0; i2cConfigStruct.ownAddress1 = SSD1306_ADDRESS; i2cConfigStruct.ack = I2C_ACK_ENABLE; i2cConfigStruct.clockSpeed = 400000;
I2C_Config(I2C1, &i2cConfigStruct);
/** Enable I2Cx */ I2C_Enable(I2C1);

i2c_dma_init();
}


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) ;


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

完整的IIC的DMA初始化代码如下:

void i2c_dma_init(void){    DMA_Config_T    DMA_ConfigStruct;    RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA1);
DMA_Reset(DMA1_Channel6);
DMA_ConfigStruct.peripheralBaseAddr = (uint32_t)(I2C1_BASE + 0x10) ; DMA_ConfigStruct.memoryBaseAddr = (uint32_t)NULL; DMA_ConfigStruct.dir = DMA_DIR_PERIPHERAL_DST; DMA_ConfigStruct.bufferSize = 0; DMA_ConfigStruct.peripheralInc = DMA_PERIPHERAL_INC_DISABLE; DMA_ConfigStruct.memoryInc = DMA_MEMORY_INC_ENABLE; DMA_ConfigStruct.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_BYTE; DMA_ConfigStruct.memoryDataSize = DMA_MEMORY_DATA_SIZE_BYTE; DMA_ConfigStruct.loopMode = DMA_MODE_NORMAL; DMA_ConfigStruct.priority = DMA_PRIORITY_HIGH; DMA_ConfigStruct.M2M = DMA_M2MEN_DISABLE;
DMA_Config(DMA1_Channel6, &DMA_ConfigStruct);
I2C_EnableDMA(I2C1);
}


2.3、用IIC的DMA发送

在启动DMA的传输之前,要配置源数据内存地址,传输长度,然后使能传输,使能传输之后CPU可以做其他事情,也可以等待传输完成:

void i2c_dma_transmit_buffer(unsigned char *buffer, unsigned int length){
DMA_Disable(DMA1_Channel6); DMA1_Channel6->CHMADDR = (uint32_t)buffer; DMA1_Channel6->CHNDATA = length; DMA_Enable(DMA1_Channel6); while (DMA_ReadStatusFlag(DMA1_FLAG_TC6) == RESET);
}


三、OLED的驱动

3.1、修改OLED地址模式

OLED的默认是页地址模式,每写入一行都要设置一下坐标。这样的话,传输给OLED的数据就多了很多命令和地址,非常不适合这里的DMA方式刷屏。

而理想的方式是只发一次地址,驱动芯片内部能对地址自增,这样就可以一次性发送所有显存中的数据,这里修改一下OLED的地址模式就可以实现。

官方例程默认是页地址模式,每次换行显示时需要重新发送地址,改为垂直地址模式就不用每次都发送地址,省去了额外的数据。

对于128x64的屏幕来说,只要发送128x64/8=1024字节即可。

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


这样配置驱动芯片SSD1306:

void oled_register_config(){    oled_i2c_wr_byte(0xAE, OLED_CMD);
oled_i2c_wr_byte(0xAE, OLED_CMD); //--turn off oled panel oled_i2c_wr_byte(0x00, OLED_CMD); //---set low column address oled_i2c_wr_byte(0x10, OLED_CMD); //---set high column address oled_i2c_wr_byte(0x40, OLED_CMD); //--set start line address Set Mapping RAM Display Start Line (0x00~0x3F) oled_i2c_wr_byte(0x81, OLED_CMD); //--set contrast control register oled_i2c_wr_byte(0xCF, OLED_CMD); // Set SEG Output Current Brightness oled_i2c_wr_byte(0xA0, OLED_CMD); //oled_i2c_wr_byte(0xA1,OLED_CMD);//--Set SEG/Column Mapping 0xa0左右反置 0xa1正常 oled_i2c_wr_byte(0xC0, OLED_CMD); //oled_i2c_wr_byte(0xC8,OLED_CMD);//Set COM/Row Scan Direction 0xc0上下反置 0xc8正常 oled_i2c_wr_byte(0xA6, OLED_CMD); //--set normal display oled_i2c_wr_byte(0xA8, OLED_CMD); //--set multiplex ratio(1 to 64) oled_i2c_wr_byte(0x3f, OLED_CMD); //--1/64 duty oled_i2c_wr_byte(0xD3, OLED_CMD); //-set display offset Shift Mapping RAM Counter (0x00~0x3F) oled_i2c_wr_byte(0x00, OLED_CMD); //-not offset oled_i2c_wr_byte(0xd5, OLED_CMD); //--set display clock divide ratio/oscillator frequency oled_i2c_wr_byte(0x80, OLED_CMD); //--set divide ratio, Set Clock as 100 Frames/Sec oled_i2c_wr_byte(0xD9, OLED_CMD); //--set pre-charge period oled_i2c_wr_byte(0xF1, OLED_CMD); //Set Pre-Charge as 15 Clocks & Discharge as 1 Clock oled_i2c_wr_byte(0xDA, OLED_CMD); //--set com pins hardware configuration oled_i2c_wr_byte(0x12, OLED_CMD); oled_i2c_wr_byte(0xDB, OLED_CMD); //--set vcomh oled_i2c_wr_byte(0x40, OLED_CMD); //Set VCOM Deselect Level #if 1 oled_i2c_wr_byte(0x20, OLED_CMD); //-Set Page Addressing Mode (0x00/0x01/0x02) 设置地址模式 oled_i2c_wr_byte(0x00, OLED_CMD); //00b, Horizontal Addressing Mode 水平地址模式 #else oled_i2c_wr_byte(0x20, OLED_CMD); //-Set Page Addressing Mode (0x00/0x01/0x02) oled_i2c_wr_byte(0x02, OLED_CMD); //10b, Page Addressing Mode (RESET) #endif oled_i2c_wr_byte(0x8D, OLED_CMD); //--set Charge Pump enable/disable oled_i2c_wr_byte(0x14, OLED_CMD); //--set(0x10) disable oled_i2c_wr_byte(0xA4, OLED_CMD); // Disable Entire Display On (0xa4/0xa5) oled_i2c_wr_byte(0xA6, OLED_CMD); // Disable Inverse Display On (0xa6/a7) oled_i2c_wr_byte(0xAF, OLED_CMD); //--turn on oled panel
oled_i2c_wr_byte(0xAF, OLED_CMD); /*display ON*/}


3.2、实现OLED刷全屏

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

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

void oled_i2c_write_buffer(unsigned char *buffer, unsigned int length){
I2C_EnableGenerateStart(I2C1); while (!I2C_ReadEventStatus(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); //EV5
I2C_Tx7BitAddress(I2C1, SSD1306_ADDRESS, I2C_DIRECTION_TX); while (!I2C_ReadEventStatus(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); //EV6
I2C_TxData(I2C1, 0x40); while (!I2C_ReadEventStatus(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)); //EV8
i2c_dma_transmit_buffer(buffer, length); //DMA Transmit
}


//刷新整个屏幕void oled_refresh(unsigned char mem[]){    oled_i2c_write_buffer(mem, OLED_MEM_SIZE);
oled_delay_ms(200);}


四、效果演示

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

int main(void){         oled_init();         while(1)        {                  oled_refresh(&mario1[BMP_OFFSET]);                  oled_refresh(&mario2[BMP_OFFSET]);                 oled_refresh(&mario3[BMP_OFFSET]);                  oled_refresh(&mario4[BMP_OFFSET]);                  oled_refresh(&mario5[BMP_OFFSET]);                  oled_refresh(&mario6[BMP_OFFSET]);                  oled_refresh(&mario7[BMP_OFFSET]);                  oled_refresh(&mario8[BMP_OFFSET]);                  oled_refresh(&mario9[BMP_OFFSET]);                  oled_refresh(&mario10[BMP_OFFSET]);                  oled_refresh(&mario11[BMP_OFFSET]);                  oled_refresh(&mario12[BMP_OFFSET]);           }}


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


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

while(1)        {                oled_refresh(&mario1[BMP_OFFSET]);                oled_refresh(&mario2[BMP_OFFSET]);               oled_refresh(&mario3[BMP_OFFSET]);                  oled_refresh(&mario4[BMP_OFFSET]);                  oled_refresh(&mario5[BMP_OFFSET]);                  oled_refresh(&mario6[BMP_OFFSET]);                  oled_refresh(&mario7[BMP_OFFSET]);                  oled_refresh(&mario8[BMP_OFFSET]);                  oled_refresh(&mario9[BMP_OFFSET]);                  oled_refresh(&mario10[BMP_OFFSET]);                  oled_refresh(&mario11[BMP_OFFSET]);                  oled_refresh(&mario12[BMP_OFFSET]);           }


最后是OLED显示马里奥跑步的动画效果:


以上就是今天分享的所有内容了,如果有需要查看原图、代码的小伙伴,请点击底部“阅读原文”进行下载。

END

作者:shanyuxiang
来源:21ic论坛

版权归原作者所有,如需转载,请注明出处

推荐阅读
遇到一位被国产MCU伤透了心的老板
稚晖君的机器人开源了,含全套图纸+代码
美国民兵III型核导弹制导系统和计算机内部欣赏

→点关注,不迷路←

嵌入式微处理器
关注嵌入式相关技术和资讯,你想知道的都在这里。
 最新文章