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

[产品应用] CW32L052 体验丝滑数字OLED小时钟

[复制链接]
 楼主| lulugl 发表于 2023-7-21 14:07 | 显示全部楼层 |阅读模式
<
#有奖活动# #申请原创#[url=home.php?mod=space&uid=760190]@21小跑堂 [/url]
CW32L052 内部集成 2 个 I2C 控制器,能按照设定的传输速率(标准,快速,高速)将需要发送的数据按照 I2C 规范串行发送到 I2C 总线上,并对通信过程中的状态进行检测,另外还支持多主机通信中的总线冲突和仲裁处理。
其实实现对I2C外设的控制,目前有两种驱动方式,一种是纯硬件的驱动,另一种是用GPIO摸拟出时序。两种驱动方式各有所长,硬件驱动可以实现高速的通信速度,占用CPU时间少,但是其也有缺点,一是有些MCU的硬件I2C的驱动有BUG,与莫些外设不兼容等。模拟I2C驱动,具有不受IO的限制,移植到不同的MCU,只需要改动GPIO的驱动就行,移植比较方便,但是因为它也有缺点,由于需要延时函数,占用CPU时间多,通信速度受GPIO速度的影响等。就我目前接触的大多数MCU来说,程序员更加会选择模拟来实现I2C的驱动,因为移植起来方便、快速。
经过几天的学习CW32L052的用户手册,我发现其硬件的I2C的驱动的掌握难点在于,对其过 I2C 状态寄存器 I2Cx_STAT的掌握是一个难点,在其用户手册中,他的状态达28个之多,其中的26个为正常接收或发送状态,2个特殊状态(0xF8:I2C总线无可用信息;0x00: 总线错误)。其I2C状态码如下表所示:
81b394f5449370118f5dc19ab03adf64
40ee14cd4fd5f8b0e7f12b19eabcbdc8
经过学习官方的cw32l052_i2c.c中的函数,结合我以住驱动SSD1306的经验,成细的驱动了OLED屏,现在驱动方法分享如下:
1、选取合适的硬件I2C驱动管脚, 由于我原来在L083开发板上面使用了与LCD段码屏的管脚导致不起时序,所以这次我避免用到有可能起冲突的管脚。经查看原理图,开发板上的PB8,PB9是接到的开发板的EEPROM上的,原理图如下:
50ea702104c2964aa693afdebb5d9738
于是,我选取PB8为SCL,PB9为SDA,经查看用户手册,这两个管脚为I2C1,复用管脚代码如下:
  1. PB08_AFx_I2C1SCL();

  2. PB09_AFx_I2C1SDA();

初始化的次序为:使能GPIOB的时钟——使能I2C1时钟——复用GPIO为I2C1——配置GPIO为GPIO_MODE_OUTPUT_OD模式——配置I2C的波特率——配置I2C1总线——使用能I2C1,具体代码如下:
  1. void OLED_I2C_Init(void)

  2. {

  3. GPIO_InitTypeDef GPIO_InitStructure = {0};

  4. I2C_InitTypeDef I2C_InitStruct = {0};

  5. //__RCC_GPIOB_CLK_ENABLE();

  6. //__RCC_I2C1_CLK_ENABLE();

  7. CW_SYSCTRL->AHBEN_f.GPIOB = 1;

  8. CW_SYSCTRL->APBEN1_f.I2C1 = 1U; //

  9. PB08_AFx_I2C1SCL();

  10. PB09_AFx_I2C1SDA();

  11. GPIO_InitStructure.Pins = I2C1_SCL_GPIO_PIN | I2C1_SDA_GPIO_PIN;

  12. GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;

  13. GPIO_Init(I2C1_SCL_GPIO_PORT, &GPIO_InitStructure);

  14. I2C_InitStruct.I2C_Baud = 0x1; // 48000 000/(8*(1+11) = 500k

  15. I2C_InitStruct.I2C_BaudEn = ENABLE;

  16. I2C_InitStruct.I2C_FLT = DISABLE;

  17. I2C_InitStruct.I2C_AA = DISABLE;

  18. I2C1_DeInit();

  19. I2C_Master_Init(CW_I2C1,&I2C_InitStruct);//初始化模块

  20. I2C_Cmd(CW_OLED_I2C, ENABLE);

  21. }

  22. void OLED_I2C_Init(void)

  23. {

  24. GPIO_InitTypeDef GPIO_InitStructure = {0};

  25. I2C_InitTypeDef I2C_InitStruct = {0};

  26. //__RCC_GPIOB_CLK_ENABLE();

  27. //__RCC_I2C1_CLK_ENABLE();

  28. CW_SYSCTRL->AHBEN_f.GPIOB = 1;

  29. CW_SYSCTRL->APBEN1_f.I2C1 = 1U; //

  30. PB08_AFx_I2C1SCL();

  31. PB09_AFx_I2C1SDA();

  32. GPIO_InitStructure.Pins = I2C1_SCL_GPIO_PIN | I2C1_SDA_GPIO_PIN;

  33. GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;

  34. GPIO_Init(I2C1_SCL_GPIO_PORT, &GPIO_InitStructure);

  35. I2C_InitStruct.I2C_Baud = 0x1; // 48000 000/(8*(1+11) = 500k

  36. I2C_InitStruct.I2C_BaudEn = ENABLE;

  37. I2C_InitStruct.I2C_FLT = DISABLE;

  38. I2C_InitStruct.I2C_AA = DISABLE;

  39. I2C1_DeInit();

  40. I2C_Master_Init(CW_I2C1,&I2C_InitStruct);//初始化模块

  41. I2C_Cmd(CW_OLED_I2C, ENABLE);

  42. }

此次I2C不起用中断,采取循环获取I2C状态来决定下一步数据写入的方法。
需要驱动OLED,首先发出起始信号,然后判断STA状态寄存器的状态来做下一步的动作。而驱动SSD1306最基本的函数为向其写入一个byte的数据,其他的都是可以通用的,具体实现的代码如下:
  1. //向OLED寄存器地址写一个byte的数据

  2. int I2C_WriteByte(uint8_t addr,uint8_t data)

  3. {

  4. uint8_t u8i = 0, u8State;

  5. uint16_t timeout = 0xffff;

  6. I2C_GenerateSTART(CW_OLED_I2C, ENABLE);

  7. //获取状态

  8. while(1)

  9. {

  10. while((0 == I2C_GetIrq(CW_OLED_I2C)) && timeout--);

  11. if(timeout == 0) return 1;

  12. u8State = I2C_GetState(CW_OLED_I2C);

  13. switch(u8State)

  14. {

  15. case 0x08: //发送完START信号

  16. I2C_GenerateSTART(CW_OLED_I2C, DISABLE);

  17. I2C_Send7bitAddress(CW_OLED_I2C, OLED_ADDR, 0x00);

  18. break;

  19. case 0x18: //发送完SLA+W信号,ACK已收到

  20. I2C_SendData(CW_OLED_I2C, addr);

  21. break;

  22. case 0x28:

  23. I2C_SendData(CW_OLED_I2C, data);

  24. u8i ++;

  25. break;

  26. case 0x20: //发送完SLA+W后从机返回NACK

  27. break;

  28. case 0x38: //主机在发送 SLA+W 阶段或者发送数据阶段丢失仲载 或者 主机在发送 SLA+R 阶段或者回应 NACK 阶段丢失仲裁

  29. I2C_GenerateSTART(CW_OLED_I2C, ENABLE);

  30. break;

  31. case 0x30:

  32. I2C_GenerateSTOP(CW_OLED_I2C, ENABLE);

  33. break;

  34. default:

  35. break;

  36. }

  37. if(u8i>1)

  38. {

  39. I2C_GenerateSTOP(CW_OLED_I2C, ENABLE);

  40. I2C_ClearIrq(CW_OLED_I2C);

  41. break;

  42. }

  43. I2C_ClearIrq(CW_OLED_I2C);

  44. }

  45. return 0;

  46. }

实现好这个函数后,我们就可以往ssd1306中写数据了,经过测试显示图片如下:
49b556ae5e8f57fe377fbe40f9b52d89
到此图形的显示就好了,但是要显示出优雅的OLED时钟,还需要处理很多东西,比如图片的制作,上翻等动画,就需要很多时间来制作,于是我在B站上面找到了大佬“@量子卫星的索莱内姆”的开源作品,他在github上提供了源码。我经学习他的作品实现了漂亮时钟的制作,现将移植过程分享如下:
要实现时钟的显示,需要用到三个函数,一个是OLED.c显示,他用于OLED的初始化、显存的读写、实现画点画园、画图片的功能。在函数中先初定义GDDRAM缓存区
static uint8_t OLED_RAM[8][128];用于预先对缓存的数据填写,然后做一次性的写入OLED中。在制作时钟翻滚显示中有两个重要的函数,现在对函数作解读如下:
1、设置像素点的偏移,其中X、Y为起始坐标,x、y为坐标的偏移,如果set_pexl为1则点亮,如果是0测关闭。
  1. /***************************************************************

  2. Prototype : void SetPixel_for_ScrollDigit(int16_t X, int16_t Y, int16_t x, int16_t y, uint8_t set_pixel)

  3. Parameters : X

  4. Parameters : Y

  5. Parameters : x

  6. Parameters : y

  7. Parameters : set_pixel

  8. return: none

  9. Description : 设置坐标像素点数据(可以为滚动动画服务)

  10. ***************************************************************/

  11. void SetPixel_For_Scroll(int16_t X, int16_t Y, int16_t x, int16_t y, uint8_t set_pixel)

  12. {

  13. if(set_pixel)

  14. {

  15. OLED_RAM[(Y+y)/8][X+x] |= (0x01 << ((Y+y)%8));

  16. }

  17. else

  18. {

  19. OLED_RAM[(Y+y)/8][X+x] &= ~(0x01 << ((Y+y)%8));

  20. }

  21. }

从bmp大图片中获取小图片作为滚动动画的一帧图片,函数输入参数为图片显示的x、y坐标,显示的图片,所选的图片在素材中的纵、横坐标,以及一帧图片的高度,获取素材的最一行的显示数据。
函数从坐标开始一行一行开始提取数据数据,把数据更新到相对的像素点上,以此来实现动画的效果。
代码如下:
  1. /****************************************************************************************************************************************************

  2. Prototype : void Draw_Digit_BMP(uint16_t x1, uint16_t y1, const uint8_t BMP[], uint16_t Y,uint8_t W, uint8_t H, uint16_t end_line)

  3. Parameters : x1 确定图片显示位置(左上角像素点横坐标)

  4. Parameters : y1 确定图片显示位置(左上角像素点纵坐标)

  5. Parameters : BMP[] 素材图片

  6. Parameters : Y 所选的一帧图片在素材图片中的纵坐标

  7. Parameters : W 素材图片宽度(也是一帧图片的宽度)

  8. Parameters : H 一帧图片的高度

  9. Parameters : end_line 在素材图片中划出最后一行(用于滚动循环,首尾相接)

  10. return: none

  11. Description : 从bmp大图片中获取小图片作为滚动动画的一帧图片

  12. *****************************************************************************************************************************************************/

  13. void Draw_BMP_For_Scroll(uint16_t x1, uint16_t y1, const uint8_t BMP[], uint16_t Y, uint8_t W, uint8_t H, uint16_t end_line)

  14. {

  15. uint16_t x0,y0,y,Temp;

  16. for(y = Y , y0 = 0 ; y0 < H ; y++ , y0++)

  17. {

  18. if(y > end_line) y -= (end_line+1);

  19. for(x0 = 0; x0 < W ; x0++)

  20. {

  21. Temp = GetPixel_For_Scroll(x0, y, BMP, W);

  22. SetPixel_For_Scroll(x1,y1,x0,y0,Temp);

  23. }

  24. }

  25. }

在draw_rolling_colck.c文件中,其就一个函数Draw_Rollin_clock,他的主要功能就是实现不断的对整个画面进行数据刷新,在个位向10位进位时提供进位翻页的截图图显示,如果没有翻页测按原来动画进行显示。在数据组装完成后执行函数OLED_RefreshPartRAM(2,4,0,127);对整个显存进行更新,代码如下:
  1. void Draw_Rolling_Clock()

  2. {

  3. switch(H1)

  4. {

  5. case 0:if(Y1 < 24*2+1) Y1 = 24*2;if(Y1 < 24*2+24) Y1++;break;

  6. case 1:

  7. case 2:if(Y1 < H1*24-23 || Y1 > H1*24) Y1 = H1*24-24;if(Y1 < H1*24) Y1++;

  8. }

  9. Draw_BMP_For_Scroll(0, 16, Scroll_Digit_BMP[0], Y1, 20, 24, 2*24+23);//end_line=2*24+23,Scroll_Digit_Small_BMP划到2*24+23行,即0~2

  10. switch(H2)

  11. {

  12. case 0:

  13. {

  14. if(Hour == 0) {TEMP = 3;if(Y2 < 24*3+1 || Y2 > 4*24) Y2 = 24*3;if(Y2 < 24*3+24) Y2++;break;}

  15. if(Hour == 10 || Hour == 20){TEMP = 9;if(Y2 < 24*9+1) Y2 = 24*9;if(Y2 < 24*9+24) Y2++;break;}

  16. }

  17. case 1:

  18. case 2:

  19. case 3:

  20. case 4:

  21. case 5:

  22. case 6:

  23. case 7:

  24. case 8:

  25. case 9:if(Y2 < H2*24-23 || Y2 > H2*24) Y2 = H2*24-24;if(Y2 < H2*24) Y2++;TEMP = 9;//if(Hour == 23) TEMP = 3;else TEMP = 9;

  26. }

  27. Draw_BMP_For_Scroll(22, 16, Scroll_Digit_BMP[0], Y2, 20, 24, TEMP*24+23);//end_line=Temp*24+23,Scroll_Digit_Small_BMP划到Temp*24+23行,即0~Temp

  28. switch(M1)

  29. {

  30. case 0:if(Y3 < 24*5+1) Y3 = 24*5;if(Y3 < 24*5+24) Y3++;break;

  31. case 1:

  32. case 2:

  33. case 3:

  34. case 4:

  35. case 5:if(Y3 < M1*24-23 || Y3 > M1*24) Y3 = M1*24-24;if(Y3 < M1*24) Y3++;

  36. }

  37. Draw_BMP_For_Scroll(50, 16, Scroll_Digit_BMP[0], Y3, 20, 24, 5*24+23);//end_line=5*24+23,Scroll_Digit_Small_BMP划到5*24+23行,即0~5

  38. switch(M2)

  39. {

  40. case 0:if(Y4 < 24*9+1) Y4 = 24*9;if(Y4 < 24*9+24) Y4++;break;

  41. case 1:

  42. case 2:

  43. case 3:

  44. case 4:

  45. case 5:

  46. case 6:

  47. case 7:

  48. case 8:

  49. case 9:if(Y4 < M2*24-23 || Y4 > M2*24) Y4 = M2*24-24;if(Y4 < M2*24) Y4++;

  50. }

  51. Draw_BMP_For_Scroll(72, 16, Scroll_Digit_BMP[0], Y4, 20, 24, 9*24+23);//end_line=9*24+23,Scroll_Digit_Small_BMP划到9*24+23行,即0~9

  52. switch(S1)

  53. {

  54. case 0:if(Y5 < 16*5+1) Y5 = 16*5;if(Y5 < 16*5+16) Y5++;break;

  55. case 1:

  56. case 2:

  57. case 3:

  58. case 4:

  59. case 5:if(Y5 < S1*16-15 || Y5 > S1*16) Y5 = S1*16-16;if(Y5 < S1*16) Y5++;

  60. }

  61. Draw_BMP_For_Scroll(94, 24, Scroll_Digit_Small_BMP[0], Y5, 14, 16, 5*16+15);//end_line=6*16+15,Scroll_Digit_Small_BMP划到6*16+15行,即0~6

  62. switch(S2)

  63. {

  64. case 0:if(Y6 < 16*9+1) Y6 = 16*9;if(Y6 < 16*9+16) Y6++;break;

  65. case 1:

  66. case 2:

  67. case 3:

  68. case 4:

  69. case 5:

  70. case 6:

  71. case 7:

  72. case 8:

  73. case 9:if(Y6 < S2*16-15 || Y6 > S2*16) Y6 = S2*16-16;if(Y6 < S2*16) Y6++;

  74. }

  75. Draw_BMP_For_Scroll(111, 24, Scroll_Digit_Small_BMP[0], Y6, 14, 16, 9*16+15);//end_line=9*16+15,Scroll_Digit_Small_BMP划到9*16+15行,即0~9

  76. if(Second % 2 == 1) OLED_DrawBMP(44,16,4,24,Colon_BMP[0]); //绘制冒号

  77. else OLED_AreaClear(44,16,4,24); //清除冒号

  78. OLED_RefreshPartRAM(2,4,0,127);

  79. }

例程的时钟产生是基于systick的,我这里使用RTC的定时中断来产生,定时闹钟每一秒钟进入中断,然后更新时、分、秒的数据,代码如下:
  1. void ShowTime(void)

  2. {

  3. static uint8_t show_state = 0;

  4. show_state =show_state%2;

  5. lcd_clear();

  6. RTC_TimeTypeDef RTC_TimeStruct = {0};

  7. RTC_DateTypeDef RTC_DateStruct = {0};

  8. RTC_GetDate(&RTC_DateStruct);

  9. RTC_GetTime(&RTC_TimeStruct);

  10. //显示-

  11. lcd_show_string(5,18);

  12. lcd_show_string(2,18);

  13. if(show_state == 0)

  14. {

  15. lcd_show_string(7,RTC_DateStruct.Year>>4);

  16. lcd_show_string(6,RTC_DateStruct.Year&0x0F);

  17. lcd_show_string(4,RTC_DateStruct.Month>>4);

  18. lcd_show_string(3,RTC_DateStruct.Month&0x0F);

  19. lcd_show_string(1,RTC_DateStruct.Day>>4);

  20. lcd_show_string(0,RTC_DateStruct.Day&0x0F);

  21. }else {

  22. lcd_show_string(7,RTC_TimeStruct.Hour>>4);

  23. lcd_show_string(6,RTC_TimeStruct.Hour&0x0F);

  24. lcd_show_string(4,RTC_TimeStruct.Minute>>4);

  25. lcd_show_string(3,RTC_TimeStruct.Minute&0x0F);

  26. lcd_show_string(1,RTC_TimeStruct.Second>>4);

  27. lcd_show_string(0,RTC_TimeStruct.Second&0x0F);

  28. }

  29. show_state++;

  30. Hour = (RTC_DateStruct.Year>>4) + (RTC_TimeStruct.Hour&0x0F) ;

  31. Minute = (RTC_TimeStruct.Minute>>4) + (RTC_TimeStruct.Minute&0x0F);

  32. Second = (RTC_TimeStruct.Second>>4) + (RTC_TimeStruct.Second&0x0F);

  33. H1 =RTC_TimeStruct.Hour>>4;

  34. H2 = RTC_TimeStruct.Hour&0x0F;

  35. M1 = RTC_TimeStruct.Minute>>4;

  36. M2 = RTC_TimeStruct.Minute&0x0F;

  37. S1 = RTC_TimeStruct.Second>>4;

  38. S2 = RTC_TimeStruct.Second&0x0F;

  39. // u1_printf(".Date is 20%02x/%02x/%02x(%s).Time is %02x%s:%02x:%02x\r\n", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Day, WeekdayStr[RTC_DateStruct.Week], RTC_TimeStruct.Hour, H12AMPMStr[RTC_TimeStruct.H24][RTC_TimeStruct.AMPM],RTC_TimeStruct.Minute, RTC_TimeStruct.Second);

  40. }

最后,在主函数中,调用Draw_Rolling_Clock,实现了效果:
  1. int32_t main(void)

  2. {

  3. RCC_Configuration();

  4. InitTick( 48000000 );

  5. uart1_init();

  6. my_rtc_init();

  7. lcd_init();

  8. lcd_clear();

  9. OLED_Init();

  10. UART_SendString(CW_UART1, "start\r\n");

  11. while(1)

  12. {

  13. if(uart1_rx_state == 1)

  14. {

  15. uart1_rx_state = 0;

  16. uart1_rx_cnt = 0;

  17. u1_printf("rcv:%s\r\n",uart1_rx_buff);

  18. ShowTime();

  19. memset(uart1_rx_buff, 0, UART1_RX_MAXLEN);

  20. }

  21. SysTickDelay(20);

  22. Draw_Rolling_Clock();

  23. }

  24. }
【总结】CW32L052的硬件I2C设计非常科学,提供了28个STA状态码,可以给用户提供精准的状态,来实现下一个状态的转换,为OLED时钟的显示提供了强大的显示驱动,时钟的动画显示非常优雅!效果见视频:


打赏榜单

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

昨天 发表于 2023-8-2 14:04 | 显示全部楼层
源代码没打包啊??。
elephant00 发表于 2023-8-3 17:24 | 显示全部楼层
丝滑数字OLED小时钟
 楼主| lulugl 发表于 2023-8-3 21:06 | 显示全部楼层
elephant00 发表于 2023-8-3 17:24
丝滑数字OLED小时钟

感谢大注的关注!
bogejiayou 发表于 2025-3-26 09:34 | 显示全部楼层
很好的学习心得,过程和思路都细说了,有空复刻一个学习一下。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

180

主题

830

帖子

12

粉丝

180

主题

830

帖子

12

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