news 2026/4/18 14:42:38

基于APM32E030的电子墨水屏时钟

作者头像

张小明

前端开发工程师

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




1.2 电子墨水屏
电子墨水屏ePaper是一种采用“微胶囊电泳显示”技术的显示介质,通过不同电压吸引不同颜色的墨滴实现黑白或多个颜色的显示,由于是显示单元是墨滴,所以不需要背光,对眼睛比较友好。除了护眼,省电也它的一大特色,在断电的情况下也能保持显示,在刷屏时才费电,其他时候可以进入睡眠或者直接断电,很适合用来做电子钟或者万年历。
手上刚好有之前在海鲜市场上淘的电子标签,把上面的2.13寸墨水屏拆下作为显示屏,墨水屏的规格如下:
屏幕型号:HINK-E0213A04
分辨率: 122x250
显示颜色:黑白
支持局部刷新



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


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

RTC相关电路比较简单,需要有个32.768KHz的晶振。



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

三、RTC的程序
官方发布的SDK包中有关于RTC的例程,这里根据"APM32E030_SDK_V1.0.1\Examples\BOARD_APM32E030_TINY\RTC\RTC_Calendar"
例程进行修改并封装,以便于后面的应用程序调用。
首先是RTC的初始化,主要是配置RTC的时钟,并对RTC的参数进行设置。
复制
  1. voidrtc_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. voidrtc_read_time(unsignedchar*hours,unsignedchar*minutes,unsignedchar*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. voidrtc_read_data(unsignedchar*year,unsignedchar*month,unsignedchar*day,unsignedchar*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. voidrtc_write_time(unsignedcharhour,unsignedcharminute,unsignedcharsecond)
  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. voidrtc_write_date(unsignedcharyear,unsignedcharmonth,unsignedcharday,unsignedcharweekday)
  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. voidepaper_gpio_write_cs(unsignedcharlevel)
  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. voidepaper_gpio_write_rst(unsignedcharlevel)
  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. voidepaper_gpio_write_dc(unsignedcharlevel)
  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. voidepaper_gpio_write_mosi(unsignedcharlevel)
  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. voidepaper_gpio_write_sck(unsignedcharlevel)
  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. unsignedcharepaper_gpio_busy_read()
  37. {
  38. if(GPIO_ReadInputBit(EPAPER_BUSY_GPIO, EPAPER_BUSY_PIN) == BIT_RESET)
  39. return0;
  40. else
  41. return1;
  42. }
  43. //SPI写字节
  44. voidepaper_spi_wrtie(unsignedcharvalue)
  45. {
  46. unsignedchari;
  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. voidepaper_write_cmd(unsignedcharcommand)
  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. voidepaper_write_data(unsignedchardata)
  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. unsignedcharepaper_wait_busy(void)
  85. {
  86. unsignedinti =400;
  87. while(i--)
  88. {
  89. if(epaper_is_busy() ==0)return0;//空闲退出
  90. epaper_delay_xms(10);
  91. }
  92. return-1;//超时退出
  93. }
4.2 全局刷图片
这款屏幕的宽度为122,高度为250,取模时需要横向取模,高位在前。
起点坐标和方向如图所示:


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


把g_Image.c中的数组全部写入到到屏幕的内存中去,
具体步骤先是设置区域大小,因为是写入全屏数据,所以调用 epaper_driver_set_window(0, 0, 122, 250);

在每一行数据写入前设置起点epaper_driver_set_cursor(0, y),然后一次写入整行数据,重复多行,完成整幅图片的写入。

复制
  1. //全填充 刷整个屏幕
  2. voidepaper_driver_fill(unsignedcharbuffer[])
  3. {
  4. unsignedshortx, y;
  5. unsignedinti =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. voidepaper_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. voidepaper_set_lut_table(unsignedcharmode)
  3. {
  4. epaper_write_cmd(0x32);
  5. unsignedshorti;
  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. elseif(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”来制作所需要的字库,字幕选项设为”逐行式“

选择字体、设置字高、字宽,输入想要生成的文字,最后点"生成字模",
这样就得到字库数组 const unsigned char DZ_simkai24[]。


ASCII字符比较少,可以把全部ascii字符做成字库放进MCU中,使用也比较简单;
中文字符太多了全部做成字库放进MCU中不现实,所以我选择只把要用到的汉字做成字库,其他的字就显示为空格。
为了能找到某个汉字字模在这些数组中的位置,还需要做个字模和编码的映射关系表。
于是先定义这样一个新的数据类型,把每个字的GB2312编码和该字模在数组中的位置联系起来。
复制
  1. typedefstruct
  2. {
  3. unsignedcharfirst;//GB2312编码
  4. unsignedcharsecond;//GB2312编码
  5. unsignedintindex;//在字库文件中的索引
  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. staticinlineunsignedintepaper_font_search_gb2312(unsignedcharcode[2])
  3. {
  4. unsignedinti =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. returni;
  10. }
  11. i++;
  12. }
  13. return0;
  14. }
  15. //绘制文字
  16. voidepaper_draw_text(unsignedshortx0,unsignedshorty0,char*text)
  17. {
  18. unsignedshortx;
  19. unsignedintindex;
  20. unsignedchar*ptr;
  21. unsignedshortfirst;
  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” 。


五、程序整合
有了前面的RTC函数和墨水屏显示函数,后面实现日期显示和时间显示就容易很多。
每隔1秒(或小于1S)读取一次RTC的时间,如果时间和上次不同,则刷新为当前时间,
如果时间到了00:00::00 ,则刷新日期,代码如下:
复制
  1. //显示实时时间:时、分、秒
  2. unsignedcharis_refresh_date=0;
  3. voidgui_page_real_time()
  4. {
  5. staticunsignedcharold_hour=0xff,old_minute=0xff,old_second=0xff;
  6. unsignedcharhour,minute,second;
  7. chartemp_str[8];
  8. rtc_read_time(&hour,&minute,&second);
  9. epaper_font_set(&Digiface64);
  10. if(hour!=old_hour)
  11. {
  12. sprintf(temp_str,"%02d",hour);
  13. epaper_draw_text(TIME_POS_X,TIME_POS_Y,temp_str);
  14. }
  15. if(minute!=old_minute)
  16. {
  17. sprintf(temp_str,"%02d",minute);
  18. epaper_draw_text(TIME_POS_X+32*3-16,TIME_POS_Y,temp_str);
  19. }
  20. if(second!=old_second)
  21. {
  22. sprintf(temp_str,"%02d",second);
  23. epaper_draw_text(TIME_POS_X+32*6-16,TIME_POS_Y,temp_str);
  24. }
  25. if((hour!=old_hour)||(minute!=old_minute)||(second!=old_second))
  26. {
  27. if(curPage.area_refresh==0)
  28. epaper_driver_fill(curPage.buffer);
  29. if(hour==0&& minute==0&& second==0)
  30. is_refresh_date=1;
  31. }
  32. old_hour =hour;
  33. old_minute =minute;
  34. old_second =second;
  35. }
  36. //显示实时日期:年、月、日
  37. voidgui_page_real_date()
  38. {
  39. staticunsignedcharold_year=0xff,old_month=0xff,old_day=0xff,old_weekday=0xff;
  40. unsignedcharyear,month,day,weekday;
  41. chartemp_str[8];
  42. rtc_read_data(&year,&month,&day,&weekday);
  43. epaper_font_set(&Digiface24);
  44. if(year!=old_year)
  45. {
  46. sprintf(temp_str,"20%02d",year);
  47. epaper_draw_text(DATE_POS_X,DATE_POS_Y,temp_str);
  48. }
  49. if(month!=old_month)
  50. {
  51. sprintf(temp_str,"%02d",month);
  52. epaper_draw_text(DATE_POS_X+12*6,DATE_POS_Y,temp_str);
  53. }
  54. if(day!=old_day)
  55. {
  56. sprintf(temp_str,"%02d",day);
  57. epaper_draw_text(DATE_POS_X+12*10,DATE_POS_Y,temp_str);
  58. }
  59. if(weekday!=old_weekday)
  60. {
  61. epaper_font_set(&simkai24);
  62. memcpy(temp_str,&WeekdayTab[weekday*2],2);
  63. strcat(temp_str,"\0");
  64. epaper_draw_text(DATE_POS_X+12*16,DATE_POS_Y,temp_str);
  65. }
  66. if((year!=old_year)||(month!=old_month)||(day!=old_day)||(weekday!=old_weekday))
  67. {
  68. if(curPage.area_refresh==0)
  69. epaper_driver_fill(curPage.buffer);
  70. }
  71. old_year =year;
  72. old_month =month;
  73. old_day =day;
  74. old_weekday=weekday;
  75. }
六、测试效果
看看最终显示效果:



---------------------
作者:shanyuxiang
链接:https://bbs.21ic.com/icview-3466572-1-1.html
来源:21ic.com
此文章已获得原创/原创奖标签,著作权归21ic所有,任何人未经允许禁止转载。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 6:24:32

Docker Compose 和 Docker Swarm

Docker Swarm 和 Docker Compose 都是 Docker 官方提供的容器编排工具&#xff0c;但它们的应用场景和目标有所不同&#xff0c;它们的关系可以概括为&#xff1a;分工不同、可以结合使用。 1. &#x1f3af; 用途和范围的不同特性Docker ComposeDocker Swarm应用范围单主机/单…

作者头像 李华
网站建设 2026/4/18 6:25:24

20、闪存文件系统全解析:从UBIFS到临时文件系统

闪存文件系统全解析:从UBIFS到临时文件系统 1. UBIFS文件系统 1.1 UBIFS概述 UBIFS利用UBI卷创建可靠的文件系统,它添加了子分配和垃圾回收功能,构建了完整的闪存转换层。与JFFS2和YAFFS2不同,它将索引信息存储在芯片上,因此挂载速度快,但挂载前附加UBI卷可能需要较长…

作者头像 李华
网站建设 2026/4/17 21:29:13

35、GDB调试全解析:从基础命令到内核调试

GDB调试全解析:从基础命令到内核调试 1. GDB命令文件 在每次运行GDB时,有些操作是需要重复进行的,比如设置sysroot。为了方便,可以将这些命令放在一个命令文件中,每次启动GDB时自动运行。GDB会按以下顺序读取命令: 1. $HOME/.gdbinit 2. 当前目录下的 .gdbinit …

作者头像 李华
网站建设 2026/4/18 1:57:13

D.二分查找-基础-2529. 正整数和负整数的最大计数

题目链接&#xff1a;2529. 正整数和负整数的最大计数&#xff08;简单&#xff09; 算法原理&#xff1a; 解法&#xff1a;二分查找 模板&#x1f447; 优选算法-二分&#xff1a;18.在排序数组中查找元素的第一个和最后一个位置 利用题目的按 非递减顺序 排列的条件就可以二…

作者头像 李华
网站建设 2026/4/18 7:49:38

基于EMB电子机械制动器的摩擦片磨损不均问题优化方案

目录 一、背景介绍 二、基于EMB电子机械制动器进行摩擦片磨损控制的方法 三、总结 一、背景介绍 车辆制动系统的磨损量主要受制动力分配比例及制动系统结构设计两大因素影响。在传统液压制动系统中,由于前后轮的制动力分配、摩擦面积及摩擦片材料不同,将导致前后轮制动器摩…

作者头像 李华