shanyuxiang 发表于 2025-6-30 23:23

基于APM32E030的电子墨水屏时钟

本帖最后由 shanyuxiang 于 2025-7-1 00:04 编辑

#申请原创#@21小跑堂

基于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的参数进行设置。void rtc_init(void)
{
    /* RTC Reset */
    RTC_Init();
    RTC_Reset();
    RTC_Init();

    /* RTC Enable Init */
    RTC_EnableInit();

    RTC_ConfigDateStructInit(&DateStruct);

    /* RTC Disable Init */
    RTC_DisableInit();
}
为了显示时间和日期,需要相关的读取函数:void rtc_read_time(unsigned char *hours, unsigned char *minutes, unsigned char *seconds)
{
    /* Read time */
    RTC_ReadTime(RTC_FORMAT_BIN, &TimeStruct);
    RTC_Delay();

    *hours   = TimeStruct.hours;
    *minutes = TimeStruct.minutes;
    *seconds = TimeStruct.seconds;
}

void rtc_read_data(unsigned char *year, unsigned char *month, unsigned char *day, unsigned char*weekday)
{
    /* Read Date */
    RTC_ReadDate(RTC_FORMAT_BIN, &DateStruct);
    RTC_Delay();

    *year= DateStruct.year;
    *month = DateStruct.month;
    *day   = DateStruct.date;
    *weekday = DateStruct.weekday;
}

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

    RTC_ConfigTime(RTC_FORMAT_BIN, &TimeStruct);
    RTC_Delay();
}

void rtc_write_date(unsigned char year, unsigned char month, unsigned char day, unsigned charweekday)
{
    DateStruct.year =year;
    DateStruct.month = month;
    DateStruct.date =day;
    DateStruct.weekday = weekday;


    RTC_ConfigDate(RTC_FORMAT_BIN, &DateStruct);
    RTC_Delay();

}


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

四、墨水屏的程序
4.1底层硬件驱动
电子墨水屏通过SPI接口通信,需要用到以下6个信号:
MOSI- PB5数据
SCK    - PB3时钟
CS      - PB4片选
DC   - PC12 数据/命令控制
RST    - PB8复位
BUSY- PB9繁忙检测

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

void epaper_gpio_write_rst(unsigned char level)
{
    if (level)
      GPIO_SetBit(EPAPER_RST_GPIO, EPAPER_RST_PIN);
    else
      GPIO_ClearBit(EPAPER_RST_GPIO, EPAPER_RST_PIN);
}

void epaper_gpio_write_dc(unsigned char level)
{
    if (level)
      GPIO_SetBit(EPAPER_DC_GPIO, EPAPER_DC_PIN);
    else
      GPIO_ClearBit(EPAPER_DC_GPIO, EPAPER_DC_PIN);
}

void epaper_gpio_write_mosi(unsigned char level)
{
    if (level)
      GPIO_SetBit(EPAPER_MOSI_GPIO, EPAPER_MOSI_PIN);
    else
      GPIO_ClearBit(EPAPER_MOSI_GPIO, EPAPER_MOSI_PIN);
}

void epaper_gpio_write_sck(unsigned char level)
{
    if (level)
      GPIO_SetBit(EPAPER_SCK_GPIO, EPAPER_SCK_PIN);
    else
      GPIO_ClearBit(EPAPER_SCK_GPIO, EPAPER_SCK_PIN);
}

unsigned char epaper_gpio_busy_read()
{
    if (GPIO_ReadInputBit(EPAPER_BUSY_GPIO, EPAPER_BUSY_PIN) == BIT_RESET)
      return 0;
    else
      return 1;
}

//SPI写字节
void epaper_spi_wrtie(unsigned char value)
{
    unsigned char i;

    __disable_irq();
    EPAPER_SPI_DELAY;
    for (i = 0; i < 8; i++)
    {
      epaper_gpio_write_sck(0);
      EPAPER_SPI_DELAY;
      if (value & 0x80)
            epaper_gpio_write_mosi(1);
      else
            epaper_gpio_write_mosi(0);

      value = (value << 1);
      EPAPER_SPI_DELAY;
      EPAPER_SPI_DELAY1;
      epaper_gpio_write_sck(1);
      EPAPER_SPI_DELAY;
    }

    __enable_irq();
}

//写命令
void epaper_write_cmd(unsigned char command)
{
    EPAPER_SPI_DELAY;
    epaper_gpio_write_cs(0);
    epaper_gpio_write_dc(0);      // command write
    epaper_spi_wrtie(command);
    epaper_gpio_write_cs(1);
}

//写数据
void epaper_write_data(unsigned char data)
{
    EPAPER_SPI_DELAY;
    epaper_gpio_write_cs(0);
    epaper_gpio_write_dc(1);      // command write
    epaper_spi_wrtie(data);
    epaper_gpio_write_cs(1);
}

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

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

      epaper_delay_xms(10);
    }
    return -1;//超时退出
}


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),然后一次写入整行数据,重复多行,完成整幅图片的写入。
//全填充 刷整个屏幕
void epaper_driver_fill(unsigned char buffer[])
{
    unsigned shortx, y;
    unsigned int i = 0;

    epaper_driver_set_window(0, 0, EPAPER_WIDTH_PIXEL, EPAPER_HEIGHT_PIXEL);
    for (y = 0; y < EPAPER_HEIGHT_PIXEL; y++)
    {
      epaper_driver_set_cursor(0, y);
      epaper_write_cmd(0x24);
      for (x = 0; x < EPAPER_WIDTH_BYTES; x++)
      {
            epaper_write_data(buffer);
      }
    }
    epaper_driver_refresh();
}
写入到屏幕的SRAM中后,屏幕并不会马上刷新,还需要发送更新命令。//刷新显示
void epaper_driver_refresh(void)
{

    epaper_write_cmd(0x22); // DISPLAY_UPDATE_CONTROL_2
    epaper_write_data(0xC4);
    epaper_write_cmd(0X20); // MASTER_ACTIVATION
    epaper_write_cmd(0xFF); // TERMINATE_FRAME_READ_WRITE
    epaper_wait_busy();
}


这样屏幕才会刷新,闪烁几次,大约3-5秒可完成全屏刷新。
涉及的驱动代码很多,更详细的代码可以参考上面链接中的微雪示例代码。

4.3 局部刷文字

[*]局部刷新的方法

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

    epaper_write_cmd(0x32);

    unsigned short i;
    if (mode == EPAPER_MODE_FULL) //全刷
    {
      for (i = 0; i < 30; i++)
      {
            epaper_write_data(epaper_lut_full_update);
      }
    }
    else if (mode == EPAPER_MODE_PART)       //局刷
    {
      for (i = 0; i < 30; i++)
            epaper_write_data(epaper_lut_partial_update);
    }
    else;
}


[*]自定义字体的制作

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

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

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


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

    while (curFont.code.first > 0)
    {
      if ((curFont.code.first == code) && (curFont.code.second == code))
      {
            return i;
      }
      i++;
    }
    return 0;
}

//绘制文字
void epaper_draw_text(unsigned short x0, unsigned short y0, char *text)
{
    unsigned short x;
    unsigned intindex;
    unsigned char *ptr;
    unsigned short first;
    x = x0;

    while (*text != 0)
    {
      if (*text < 0x7F) //小于127(0x7F)是ASCII
      {
            index = epaper_font_search_ascii(*text);
            ptr = &curFont.data;

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

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

            x += (curFont.width);
            text++;;
      }
      else
      {
            index = epaper_font_search_gb2312(text);
            ptr = &curFont.data;

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

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

            x += curFont.width;
            text += 2;
      }

    }
}

有了以上代码作为基础,显示中文就很简单了:
epaper_font_set(&simkai24);
epaper_draw_text(0,0,"年 月 日");为保证能找到正确的文字编码,以上代码中的汉字须是GB2312,在Keil里面设置一下,菜单栏View -> Configuration->Editor ,Encoding 选“Chinese GB1212” 。

五、程序整合有了前面的RTC函数和墨水屏显示函数,后面实现日期显示和时间显示就容易很多。每隔1秒(或小于1S)读取一次RTC的时间,如果时间和上次不同,则刷新为当前时间,如果时间到了00:00::00 ,则刷新日期,代码如下://显示实时时间:时、分、秒
unsigned char is_refresh_date=0;
void gui_page_real_time()
{
static unsigned char old_hour=0xff,old_minute=0xff,old_second=0xff;
        unsigned char hour,minute,second;
        char temp_str;
       
        rtc_read_time(&hour,&minute,&second);
       
        epaper_font_set(&Digiface64);
       
        if(hour!=old_hour)
        {
          sprintf(temp_str,"%02d",hour);
                epaper_draw_text(TIME_POS_X,TIME_POS_Y,temp_str);
        }
       
        if(minute!=old_minute)
        {
          sprintf(temp_str,"%02d",minute);
                epaper_draw_text(TIME_POS_X+32*3-16,TIME_POS_Y,temp_str);
        }
       
        if(second!=old_second)
        {
          sprintf(temp_str,"%02d",second);
                epaper_draw_text(TIME_POS_X+32*6-16,TIME_POS_Y,temp_str);
        }
if((hour!=old_hour)||(minute!=old_minute)||(second!=old_second))
        {
                if(curPage.area_refresh==0)
           epaper_driver_fill(curPage.buffer);
               
                if(hour==0&& minute==0 && second==0)
                        is_refresh_date=1;
        }

        old_hour   =hour;
        old_minute =minute;
        old_second =second;
}

//显示实时日期:年、月、日
void gui_page_real_date()
{
       static unsigned char old_year=0xff,old_month=0xff,old_day=0xff,old_weekday=0xff;
       unsigned char year,month,day,weekday;
       char temp_str;
              
   rtc_read_data(&year,&month,&day,&weekday);
       
        epaper_font_set(&Digiface24);
        if(year!=old_year)
        {
               
          sprintf(temp_str,"20%02d",year);
                epaper_draw_text(DATE_POS_X,DATE_POS_Y,temp_str);
        }
       
        if(month!=old_month)
        {
          sprintf(temp_str,"%02d",month);
                epaper_draw_text(DATE_POS_X+12*6,DATE_POS_Y,temp_str);
        }
       
        if(day!=old_day)
        {
          sprintf(temp_str,"%02d",day);
                epaper_draw_text(DATE_POS_X+12*10,DATE_POS_Y,temp_str);
        }
       
        if(weekday!=old_weekday)
        {
                epaper_font_set(&simkai24);
                memcpy(temp_str,&WeekdayTab,2);
                strcat(temp_str,"\0");
               
                epaper_draw_text(DATE_POS_X+12*16,DATE_POS_Y,temp_str);
        }
        if((year!=old_year)||(month!=old_month)||(day!=old_day)||(weekday!=old_weekday))
        {
          if(curPage.area_refresh==0)
                  epaper_driver_fill(curPage.buffer);
        }

        old_year=year;
        old_month =month;
        old_day   =day;
old_weekday=weekday;        
}


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

梦塑者 发表于 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


页: [1]
查看完整版本: 基于APM32E030的电子墨水屏时钟