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

[APM32E0] 基于APM32E030的电子墨水屏时钟

[复制链接]
 楼主| shanyuxiang 发表于 2025-6-30 23:23 | 显示全部楼层 |阅读模式
<
本帖最后由 shanyuxiang 于 2025-7-1 00:04 编辑

#申请原创#  @21小跑堂

基于APM32E030的电子墨水屏时钟

一、前言

1.1 关于APM32E030系列

APM32E030作为极具性价比的CortexM0+系列单片机,价格虽然便宜 ,功能却不少,
其中就有个带日历功能的RTC。
这个RTC可比那些只有个计时器的RTC强太多。拿来做一个电子时钟再好不过了。
其中需要显示的年、月、日、星期、时、分、秒都可以通过寄存器直接读出,不需要软件去换算。

686386860eafd57e20.png


1.2 电子墨水屏

电子墨水屏ePaper是一种采用“微胶囊电泳显示”技术的显示介质,通过不同电压吸引不同颜色的墨滴实现黑白或多个颜色的显示,由于是显示单元是墨滴,所以不需要背光,对眼睛比较友好。除了护眼,省电也它的一大特色,在断电的情况下也能保持显示,在刷屏时才费电,其他时候可以进入睡眠或者直接断电,很适合用来做电子钟或者万年历。

手上刚好有之前在海鲜市场上淘的电子标签,把上面的2.13寸墨水屏拆下作为显示屏,墨水屏的规格如下:
屏幕型号:HINK-E0213A04
分辨率:   122x250
显示颜色:黑白
支持局部刷新
2.png



更详细的资料可参考:
https://www.waveshare.net/wiki/2.13inch_e-Paper_HAT_Manual


二、电路部分

2.1 APM32E030RBT6和RTC电路

主控部分采用APM32E030R Micro-EVB开发板,该开发板板载一个 Geehy CMSIS DAP(WinUSB)调试器,
据说速度要比HID的那种更快,但是在Win7上要手动装一下驱动。
3 开发板.png

RTC相关电路比较简单,需要有个32.768KHz的晶振。
4 原理图.png


2.2 电子墨水屏驱动电路

电子墨水屏内部电压较高,需要一套驱动电路,开发板->驱动板->墨水屏,这样相连才能驱动墨水屏。
做了个驱动板,这样开发板就可以通过2.54mm-8P杜邦线与24P的墨水屏接口相连,
市面上很多墨水屏都是这样的接口,这个驱动电路不仅限于这款墨水屏,也适用于其他类似的电子墨水屏。
5 驱动电路.png


三、RTC的程序

官方发布的SDK包中有关于RTC的例程,这里根据"APM32E030_SDK_V1.0.1\Examples\BOARD_APM32E030_TINY\RTC\RTC_Calendar"
例程进行修改并封装,以便于后面的应用程序调用。

首先是RTC的初始化,主要是配置RTC的时钟,并对RTC的参数进行设置。
  1. void rtc_init(void)
  2. {
  3.     /* RTC Reset */
  4.     RTC_Init();
  5.     RTC_Reset();
  6.     RTC_Init();

  7.     /* RTC Enable Init */
  8.     RTC_EnableInit();

  9.     RTC_ConfigDateStructInit(&DateStruct);

  10.     /* RTC Disable Init */
  11.     RTC_DisableInit();
  12. }

为了显示时间和日期,需要相关的读取函数:
  1. void rtc_read_time(unsigned char *hours, unsigned char *minutes, unsigned char *seconds)
  2. {
  3.     /* Read time */
  4.     RTC_ReadTime(RTC_FORMAT_BIN, &TimeStruct);
  5.     RTC_Delay();

  6.     *hours   = TimeStruct.hours;
  7.     *minutes = TimeStruct.minutes;
  8.     *seconds = TimeStruct.seconds;
  9. }

  10. void rtc_read_data(unsigned char *year, unsigned char *month, unsigned char *day, unsigned char  *weekday)
  11. {
  12.     /* Read Date */
  13.     RTC_ReadDate(RTC_FORMAT_BIN, &DateStruct);
  14.     RTC_Delay();

  15.     *year  = DateStruct.year;
  16.     *month = DateStruct.month;
  17.     *day   = DateStruct.date;
  18.     *weekday = DateStruct.weekday;
  19. }

为了设置时间和日期,需要相关的写入函数:
  1. void rtc_write_time(unsigned char hour, unsigned char minute, unsigned char second)
  2. {
  3.     TimeStruct.H12 = 12;
  4.     TimeStruct.hours = hour;
  5.     TimeStruct.minutes = minute;
  6.     TimeStruct.seconds = second;

  7.     RTC_ConfigTime(RTC_FORMAT_BIN, &TimeStruct);
  8.     RTC_Delay();
  9. }

  10. void rtc_write_date(unsigned char year, unsigned char month, unsigned char day, unsigned char  weekday)
  11. {
  12.     DateStruct.year =  year;
  13.     DateStruct.month = month;
  14.     DateStruct.date =  day;
  15.     DateStruct.weekday = weekday;


  16.     RTC_ConfigDate(RTC_FORMAT_BIN, &DateStruct);
  17.     RTC_Delay();

  18. }


因为RTC带有日历功能,这部分的程序相对来说简单很多,这几个函数已经足够。


四、墨水屏的程序

4.1  底层硬件驱动

电子墨水屏通过SPI接口通信,需要用到以下6个信号:

MOSI  - PB5  数据
SCK    - PB3  时钟
CS      - PB4  片选
DC     - PC12 数据/命令控制
RST    - PB8  复位
BUSY  - PB9  繁忙检测

其中MOSI和SCK可以用硬件SPI的单发送模式来实现,也可以用GPIO模拟SPI来实现。
底层驱动函数主要包含SPI字节写、写命令、写数据、等繁忙。
  1. void epaper_gpio_write_cs(unsigned char level)
  2. {
  3.     if (level)
  4.         GPIO_SetBit(EPAPER_CS_GPIO, EPAPER_CS_PIN);
  5.     else
  6.         GPIO_ClearBit(EPAPER_CS_GPIO, EPAPER_CS_PIN);
  7. }

  8. void epaper_gpio_write_rst(unsigned char level)
  9. {
  10.     if (level)
  11.         GPIO_SetBit(EPAPER_RST_GPIO, EPAPER_RST_PIN);
  12.     else
  13.         GPIO_ClearBit(EPAPER_RST_GPIO, EPAPER_RST_PIN);
  14. }

  15. void epaper_gpio_write_dc(unsigned char level)
  16. {
  17.     if (level)
  18.         GPIO_SetBit(EPAPER_DC_GPIO, EPAPER_DC_PIN);
  19.     else
  20.         GPIO_ClearBit(EPAPER_DC_GPIO, EPAPER_DC_PIN);
  21. }

  22. void epaper_gpio_write_mosi(unsigned char level)
  23. {
  24.     if (level)
  25.         GPIO_SetBit(EPAPER_MOSI_GPIO, EPAPER_MOSI_PIN);
  26.     else
  27.         GPIO_ClearBit(EPAPER_MOSI_GPIO, EPAPER_MOSI_PIN);
  28. }

  29. void epaper_gpio_write_sck(unsigned char level)
  30. {
  31.     if (level)
  32.         GPIO_SetBit(EPAPER_SCK_GPIO, EPAPER_SCK_PIN);
  33.     else
  34.         GPIO_ClearBit(EPAPER_SCK_GPIO, EPAPER_SCK_PIN);
  35. }

  36. unsigned char epaper_gpio_busy_read()
  37. {
  38.     if (GPIO_ReadInputBit(EPAPER_BUSY_GPIO, EPAPER_BUSY_PIN) == BIT_RESET)
  39.         return 0;
  40.     else
  41.         return 1;
  42. }

  43. //SPI写字节
  44. void epaper_spi_wrtie(unsigned char value)
  45. {
  46.     unsigned char i;

  47.     __disable_irq();
  48.     EPAPER_SPI_DELAY;
  49.     for (i = 0; i < 8; i++)
  50.     {
  51.         epaper_gpio_write_sck(0);
  52.         EPAPER_SPI_DELAY;
  53.         if (value & 0x80)
  54.             epaper_gpio_write_mosi(1);
  55.         else
  56.             epaper_gpio_write_mosi(0);

  57.         value = (value << 1);
  58.         EPAPER_SPI_DELAY;
  59.         EPAPER_SPI_DELAY1;
  60.         epaper_gpio_write_sck(1);
  61.         EPAPER_SPI_DELAY;
  62.     }

  63.     __enable_irq();
  64. }

  65. //写命令
  66. void epaper_write_cmd(unsigned char command)
  67. {
  68.     EPAPER_SPI_DELAY;
  69.     epaper_gpio_write_cs(0);
  70.     epaper_gpio_write_dc(0);        // command write
  71.     epaper_spi_wrtie(command);
  72.     epaper_gpio_write_cs(1);
  73. }

  74. //写数据
  75. void epaper_write_data(unsigned char data)
  76. {
  77.     EPAPER_SPI_DELAY;
  78.     epaper_gpio_write_cs(0);
  79.     epaper_gpio_write_dc(1);        // command write
  80.     epaper_spi_wrtie(data);
  81.     epaper_gpio_write_cs(1);
  82. }

  83. ////等待电子纸空闲,超时后会退出
  84. unsigned char epaper_wait_busy(void)
  85. {
  86.     unsigned int i = 400;

  87.     while (i--)
  88.     {
  89.         if (epaper_is_busy() == 0)  return 0; //空闲退出

  90.         epaper_delay_xms(10);
  91.     }
  92.     return -1;  //超时退出
  93. }


4.2 全局刷图片

这款屏幕的宽度为122,高度为250,取模时需要横向取模,高位在前。
起点坐标和方向如图所示:
6 屏幕点位示意图..png


如果想要全屏显示一张图片,需要先准备一张122x250分辨率的图片,
用软件“Image2Lcd”打开这张图片,注意选择“水平扫描”,取消下方五个选项的勾,勾选“颜色翻转”,
这款屏幕1为白点、0为黑点,因此要选择颜色反转。最后点"保存" 得到一个g_Image.c文件。
7 Image2Lcd 位图取模.png

把g_Image.c中的数组全部写入到到屏幕的内存中去,
具体步骤先是设置区域大小,因为是写入全屏数据,所以调用 epaper_driver_set_window(0, 0, 122, 250);
在每一行数据写入前设置起点 epaper_driver_set_cursor(0, y),然后一次写入整行数据,重复多行,完成整幅图片的写入。
  1. //全填充 刷整个屏幕
  2. void epaper_driver_fill(unsigned char buffer[])
  3. {
  4.     unsigned short  x, y;
  5.     unsigned int i = 0;

  6.     epaper_driver_set_window(0, 0, EPAPER_WIDTH_PIXEL, EPAPER_HEIGHT_PIXEL);
  7.     for (y = 0; y < EPAPER_HEIGHT_PIXEL; y++)
  8.     {
  9.         epaper_driver_set_cursor(0, y);
  10.         epaper_write_cmd(0x24);
  11.         for (x = 0; x < EPAPER_WIDTH_BYTES; x++)
  12.         {
  13.             epaper_write_data(buffer[i++]);
  14.         }
  15.     }
  16.     epaper_driver_refresh();
  17. }

写入到屏幕的SRAM中后,屏幕并不会马上刷新,还需要发送更新命令。
  1. //刷新显示
  2. void epaper_driver_refresh(void)
  3. {

  4.     epaper_write_cmd(0x22); // DISPLAY_UPDATE_CONTROL_2
  5.     epaper_write_data(0xC4);
  6.     epaper_write_cmd(0X20); // MASTER_ACTIVATION
  7.     epaper_write_cmd(0xFF); // TERMINATE_FRAME_READ_WRITE
  8.     epaper_wait_busy();
  9. }


这样屏幕才会刷新,闪烁几次,大约3-5秒可完成全屏刷新。

涉及的驱动代码很多,更详细的代码可以参考上面链接中的微雪示例代码。


4.3 局部刷文字

  • 局部刷新的方法

电子墨水屏全刷耗时较长,如果用来显示时间,尤其是显示秒数就不太合适,这就需要改为局部刷新,局部刷新很快,不到1秒就可完成。
设为局部刷新,需要写入一个新的LUT表到屏幕:
  1. //更新LUT, 设置全刷或局刷
  2. void epaper_set_lut_table(unsigned char mode)
  3. {

  4.     epaper_write_cmd(0x32);

  5.     unsigned short i;
  6.     if (mode == EPAPER_MODE_FULL) //全刷
  7.     {
  8.         for (i = 0; i < 30; i++)
  9.         {
  10.             epaper_write_data(epaper_lut_full_update[i]);
  11.         }
  12.     }
  13.     else if (mode == EPAPER_MODE_PART)       //局刷
  14.     {
  15.         for (i = 0; i < 30; i++)
  16.             epaper_write_data(epaper_lut_partial_update[i]);
  17.     }
  18.     else;
  19. }


  • 自定义字体的制作

要想显示日期和时间,需要制作相关字库,字库就相当于多个字形图片的集合,和前面的取模和显示方法类似,只不过这里是更小的图片。
这里用“PCtoLCD”来制作所需要的字库,字幕选项设为”逐行式“
8 PCtoLCD2002 取字模.png
选择字体、设置字高、字宽,输入想要生成的文字,最后点"生成字模",
这样就得到字库数组 const unsigned char DZ_simkai24[]。
8-2 PCtoLCD2002 取字模.png

ASCII字符比较少,可以把全部ascii字符做成字库放进MCU中,使用也比较简单;
中文字符太多了全部做成字库放进MCU中不现实,所以我选择只把要用到的汉字做成字库,其他的字就显示为空格。
为了能找到某个汉字字模在这些数组中的位置,还需要做个字模和编码的映射关系表。
于是先定义这样一个新的数据类型,把每个字的GB2312编码和该字模在数组中的位置联系起来。
  1. typedef struct
  2. {
  3.     unsigned char first;      //GB2312编码
  4.     unsigned char second;     //GB2312编码
  5.     unsigned int  index;      //在字库文件中的索引

  6. } FontCode;

把所有要用的字的映射关系存放进该结构体数组中:
  1. FontCode DZ_simkai24_code[]=
  2. {
  3.   {0x20,0x00,0},  //" "   0200
  4.   {0xc4,0xea,1},  //"年"  c4ea
  5.         {0xd4,0xc2,2},  //"月"  d4c2
  6.   {0xc8,0xd5,3},  //"日"  c8d5
  7.   {0xd2,0xbb,4},  //"一"  d2bb
  8.   {0xb6,0xfe,5},  //"二"  b6fe
  9.   {0xc8,0xfd,6},  //"三"  c8fd
  10.   {0xcb,0xc4,7},  //"四"  cbc4
  11.   {0xce,0xe5,8},  //"五"  cee5
  12.   {0xc1,0xf9,9},  //"六"  c1f9
  13.   {0xcc,0xec,10}, //"天"  ccec
  14.   {0,0}
  15. };


后面显示文字时通过检索DZ_simkai24_code中编码可以找到该字在DZ_simkai24[]字模中偏移位置,
用偏移乘以该字所占大小就能得到数组中的准确位置,然后就像画图一样描进画布缓存中。
  1. //搜索汉字在数组中的索引
  2. static inline unsigned int epaper_font_search_gb2312(unsigned char code[2])
  3. {
  4.     unsigned int i = 0;

  5.     while (curFont.code[i].first > 0)
  6.     {
  7.         if ((curFont.code[i].first == code[0]) && (curFont.code[i].second == code[1]))
  8.         {
  9.             return i;
  10.         }
  11.         i++;
  12.     }
  13.     return 0;
  14. }

  15. //绘制文字
  16. void epaper_draw_text(unsigned short x0, unsigned short y0, char *text)
  17. {
  18.     unsigned short x;
  19.     unsigned int  index;
  20.     unsigned char *ptr;
  21.     unsigned short first;
  22.     x = x0;

  23.     while (*text != 0)
  24.     {
  25.         if (*text < 0x7F) //小于127(0x7F)是ASCII
  26.         {
  27.             index = epaper_font_search_ascii(*text);
  28.             ptr = &curFont.data[index * curFont.size];

  29.             epaper_clear_windows(x, y0, x + curFont.width, y0 + curFont.height);

  30.             if (curPage.area_refresh)
  31.                 epaper_draw_icon_area(x, y0, curFont.width, curFont.height, ptr);
  32.             else
  33.                 epaper_draw_icon(x, y0, curFont.width, curFont.height, ptr);

  34.             x += (curFont.width);
  35.             text++;;
  36.         }
  37.         else
  38.         {
  39.             index = epaper_font_search_gb2312(text);
  40.             ptr = &curFont.data[index * curFont.size];

  41.             epaper_clear_windows(x, y0, x + curFont.width, y0 + curFont.height);

  42.             if (curPage.area_refresh)
  43.                 epaper_draw_icon_area(x, y0, curFont.width, curFont.height, ptr);
  44.             else
  45.                 epaper_draw_icon(x, y0, curFont.width, curFont.height, ptr);

  46.             x += curFont.width;
  47.             text += 2;
  48.         }

  49.     }
  50. }


有了以上代码作为基础,显示中文就很简单了:

  1. epaper_font_set(&simkai24);
  2.   epaper_draw_text(0,0,"  年 月 日  ");
为保证能找到正确的文字编码,以上代码中的汉字须是GB2312,在Keil里面设置一下,
菜单栏View -> Configuration->Editor ,Encoding 选“Chinese GB1212” 。
9.png


五、程序整合
有了前面的RTC函数和墨水屏显示函数,后面实现日期显示和时间显示就容易很多。
每隔1秒(或小于1S)读取一次RTC的时间,如果时间和上次不同,则刷新为当前时间,
如果时间到了00:00::00 ,则刷新日期,代码如下:
  1. //显示实时时间:时、分、秒
  2. unsigned char is_refresh_date=0;
  3. void gui_page_real_time()
  4. {
  5.   static unsigned char old_hour=0xff,old_minute=0xff,old_second=0xff;
  6.         unsigned char hour,minute,second;
  7.         char temp_str[8];
  8.        
  9.         rtc_read_time(&hour,&minute,&second);
  10.        
  11.         epaper_font_set(&Digiface64);
  12.        
  13.         if(hour!=old_hour)
  14.         {
  15.           sprintf(temp_str,"%02d",hour);
  16.                 epaper_draw_text(TIME_POS_X,TIME_POS_Y,temp_str);
  17.         }
  18.        
  19.         if(minute!=old_minute)
  20.         {
  21.           sprintf(temp_str,"%02d",minute);
  22.                 epaper_draw_text(TIME_POS_X+32*3-16,TIME_POS_Y,temp_str);
  23.         }
  24.        
  25.         if(second!=old_second)
  26.         {
  27.           sprintf(temp_str,"%02d",second);
  28.                 epaper_draw_text(TIME_POS_X+32*6-16,TIME_POS_Y,temp_str);
  29.         }
  30.   if((hour!=old_hour)||(minute!=old_minute)||(second!=old_second))
  31.         {
  32.                 if(curPage.area_refresh==0)
  33.            epaper_driver_fill(curPage.buffer);
  34.                
  35.                 if(hour==0&& minute==0 && second==0)
  36.                         is_refresh_date=1;
  37.         }

  38.         old_hour   =hour;
  39.         old_minute =minute;
  40.         old_second =second;
  41. }

  42. //显示实时日期:年、月、日
  43. void gui_page_real_date()
  44. {
  45.          static unsigned char old_year=0xff,old_month=0xff,old_day=0xff,old_weekday=0xff;
  46.          unsigned char year,month,day,weekday;
  47.          char temp_str[8];
  48.                 
  49.    rtc_read_data(&year,&month,&day,&weekday);
  50.        
  51.         epaper_font_set(&Digiface24);
  52.         if(year!=old_year)
  53.         {
  54.                
  55.           sprintf(temp_str,"20%02d",year);
  56.                 epaper_draw_text(DATE_POS_X,DATE_POS_Y,temp_str);
  57.         }
  58.        
  59.         if(month!=old_month)
  60.         {
  61.           sprintf(temp_str,"%02d",month);
  62.                 epaper_draw_text(DATE_POS_X+12*6,DATE_POS_Y,temp_str);
  63.         }
  64.        
  65.         if(day!=old_day)
  66.         {
  67.           sprintf(temp_str,"%02d",day);
  68.                 epaper_draw_text(DATE_POS_X+12*10,DATE_POS_Y,temp_str);
  69.         }
  70.        
  71.         if(weekday!=old_weekday)
  72.         {
  73.                 epaper_font_set(&simkai24);
  74.                 memcpy(temp_str,&WeekdayTab[weekday*2],2);
  75.                 strcat(temp_str,"\0");
  76.                
  77.                 epaper_draw_text(DATE_POS_X+12*16,DATE_POS_Y,temp_str);
  78.         }
  79.         if((year!=old_year)||(month!=old_month)||(day!=old_day)||(weekday!=old_weekday))
  80.         {
  81.           if(curPage.area_refresh==0)
  82.                   epaper_driver_fill(curPage.buffer);
  83.         }

  84.         old_year  =year;
  85.         old_month =month;
  86.         old_day   =day;
  87.   old_weekday=weekday;        
  88. }


六、测试效果
看看最终显示效果:
10 最终效果.gif
  
  

打赏榜单

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

评论

APM32E030+HINK-E0213A04水墨屏,实现电子时钟,代码详细,值得一看。  发表于 2025-7-16 14:20
梦塑者 发表于 2025-7-1 15:58 | 显示全部楼层
电子纸真是舒服啊!
只是价格还是超过我的预算啊
chineseboyzxy 发表于 2025-7-1 16:28 | 显示全部楼层
屏幕自己带升压用的PWM输出?
xch 发表于 2025-7-1 16:41 | 显示全部楼层
梦塑者 发表于 2025-7-1 15:58
电子纸真是舒服啊!
只是价格还是超过我的预算啊

TFT 屏功耗也不大,还便宜。一秒刷一次平均功耗20~30微安
 楼主| shanyuxiang 发表于 2025-7-1 16:45 | 显示全部楼层
梦塑者 发表于 2025-7-1 15:58
电子纸真是舒服啊!
只是价格还是超过我的预算啊

新的电子墨水屏模块确实贵,我都是从二手电子标签上拆
 楼主| shanyuxiang 发表于 2025-7-1 16:47 | 显示全部楼层
chineseboyzxy 发表于 2025-7-1 16:28
屏幕自己带升压用的PWM输出?

用了个驱动板,就是那个绿色的板,用3.3V给驱动板供电。
strang 发表于 2025-7-2 08:44 | 显示全部楼层
不错不错,墨水屏价格贵 没怎么玩
strang 发表于 2025-7-2 08:44 | 显示全部楼层
不错不错,墨水屏价格贵 没怎么玩
strang 发表于 2025-7-2 08:45 | 显示全部楼层
不错不错,墨水屏价格贵 没怎么玩
xch 发表于 2025-7-2 09:14 | 显示全部楼层
xch 发表于 2025-7-2 09:15 | 显示全部楼层
梦塑者 发表于 2025-7-16 10:45 | 显示全部楼层
上大学的时候就喜欢电子纸屏,这么多年了。这个东西一没有降价,二没有普及。

评论

价格一直居高不下,可能是因为生产良率低,尤其是大尺寸的墨水屏  发表于 2025-7-16 17:35
梦塑者 发表于 2025-7-16 10:47 | 显示全部楼层
shanyuxiang 发表于 2025-7-1 16:45
新的电子墨水屏模块确实贵,我都是从二手电子标签上拆

现在越来越觉得某鱼的东西不靠谱。
现在各样奇怪的事情都可以在某鱼发生,什么到手刀,什么寄砖头。
感觉货不对版已经是见怪不怪了

评论

太贵的也不敢在上面买,这种几块钱的电子标签买来玩玩还行  发表于 2025-7-16 17:37
迷雾隐者 发表于 2025-7-17 12:01 | 显示全部楼层
非常详细的教程,从硬件到软件都有涉及,学习了!
cooldog123pp 发表于 2025-7-24 17:18 | 显示全部楼层
这种墨水瓶做成产品应该很有感觉,看上去高大上,有档次!!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

6

主题

42

帖子

1

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

6

主题

42

帖子

1

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