GrandLine 发表于 2022-7-26 08:59

【N32G430开发板试用】智能手表功能演示样机

@安小芯    @21小跑堂


概述基于N32G430C8L7-STB开发板结合圆形TFT、Wi-Fi模块、RTC、心率模块、按键模块做一个简单功能的智能手表,具有显示时钟、心率、实时天气的功能。

软件功能
[*]通过板载的按键实现智能手表不同功能界面的切换:待机界面、时间界面、日期界面、天气界面、心率监测界面;
[*]通过SPI接口驱动SPI接口的圆形TFT,显示相应的功能界面
[*]通过SPI接口驱动SPI FLASH;SPI FLASH中存放着字库、图片等信息(天气图片、心率波形图等);
[*]通过USART2与ESP8266 Wi-Fi模块进行通讯,连接远程的知心天气,获取本地数据,更新显示到显示屏上;
[*]通过I2C驱动心率监测模块,读取当前的心率信息,结合官方的算法,在屏幕上进行显示;


系统框图天气数据获取我们使用ESP8266 Wi-Fi模块与知心天气网进行连接,注册用户可以创建一个免费版的数据通道(访问频次有限制,只能固定获取一个地址的天气数据),我们向知心天气发送一个带有特定参数的GET请求,然后根据知心天气反馈的数据通过cJSON解析得到天气代码和温度信息,然后对应相应的天气图标显示的屏幕上。在使用cJSON解析数据时,是动态开辟的内存空间,在解析完成后切记一定要将刚刚开辟的内存空间释放掉,否则会产生内存溢出,导致程序程序运行异常的情况发生!!!心率监测我们使用官方开放的算法库,这个算法库存在不稳定性,是不能商用的,但作为演示测试是可以的,官方或者第三方提供收费的算法库,可用于商用监测。

硬件准备
[*]N32G430C8L7-STB开发板
[*]N32G430C8L7-STB_EBK功能扩展板(ESP8266 Wi-Fi模块、心率监测模块、SPI FLASH模块、TFT显示屏)
[*]相关数据连接线


N32G430C8L7-STB_EBK功能扩展板原理图
PCB-2D正面图

代码实现代码部分仅摘录了部分,完整的功能代码可以下载文末的软件工程源代码附件。
ESP8266 Wi-Fi模块相关部分void ATK_ESP01_StartProcess(void)
{
    uint8_t Result = 0;

    do
    {
      SysTick_DelayMS(10);

      Result = ATK_ESP01_AT_SendCommand("AT", "OK", 100);

    } while(Result == 0);

    /* 开回显 */
    ATK_ESP01_AT_SendCommand("ATE1",   "OK", 100);

    /* 查询版本信息 */
    ATK_ESP01_AT_SendCommand("AT+GMR", "OK", 100);

    /* 设置当前Wi-Fi模式: Station模式 */
    ATK_ESP01_AT_SendCommand("AT+CWMODE=1", "OK", 100);

    /* 连接WiFi */
    if(ATK_ESP01_AT_SendCommand("AT+CWJAP=\"********\",\"********\"", "OK", 8000) == 0)
    {
      if(ATK_ESP01_AT_SendCommand(NULL, "OK", 8000) == 0)
      {
            printf("\r\nWi-Fi Connect Error!");
            return;
      }
    }

    ESP01S_WiFiFlag = 1;

    /* 查询IP地址 */
    ATK_ESP01_AT_SendCommand("AT+CIFSR", "OK", 100);

    /* 设置单链接 */
    ATK_ESP01_AT_SendCommand("AT+CIPMUX=0", "OK", 100);


    /* 连接服务器 */
    if(ATK_ESP01_AT_SendCommand("AT+CIPSTART=\"TCP\",\"api.seniverse.com\",80", "OK", 8000) == 0)
    {
      printf("\r\nServer Connect Error!");
      return;
    }

    ESP01S_ServerFlag = 1;

    /* 透明传输 */
    ATK_ESP01_AT_SendCommand("AT+CIPMODE=1", "OK", 100);

    /* 开始传输 */
    if(ATK_ESP01_AT_SendCommand("AT+CIPSEND", ">", 1000) == 0)
    {
      printf("\r\nTransfer Not Ready...");
      return;
    }

    ESP01S_InitFlag = 1;
}

void ATK_ESP01_Init(void)
{
    ATK_ESP01_InitRESET();

    ATK_ESP01_InitUSART();

    ATK_ESP01_StartProcess();

    TASK_Append(TASK_ID_ATK, ATK_ESP01_Handler, 5000);
}

void ATK_ESP01_Handler(void)
{
    if(ESP01S_InitFlag)
    {
      /* 每间隔5秒获取一次天气信息, 服务器限制每分钟最多20次!!! */
      ATK_ESP01_SendString("GET https://api.seniverse.com/v3/weather/now.json?key=*****************&location=shanghai&language=zh-Hans&unit=c\r\n");
    }
}

界面显示相关部分void SmartWatch_CreatePageIDLE(void)
{
    static uint8_t StartX = 120, StartY = 120;
    static uint8_t StepX= 10,StepY= 10;
    static uint8_t DirX   = 0,   DirY   = 0;

    char *cText = "   ";
    char *dText = "你好!";

    uint8_t Height = 16;
    uint8_t Width= strlen(dText) * 8;

    srand(SysTick_Tick);

    LCD_ShowLOG(StartX, StartY, cText);

    if(DirX == 0)
    {
      StartX -= StepX;

      if(StartX <= 36)
      {
            StartX = 36;
            DirX   = 1;

            StepX = rand() % 10 + 5;
            StepY = rand() % 10 + 5;
      }
    }
    else
    {
      StartX += StepX;

      if((StartX + Width) >= 204)
      {
            StartX = 204 - Width;
            DirX   = 0;

            StepX = rand() % 10 + 5;
            StepY = rand() % 10 + 5;
      }
    }

    if(DirY == 0)
    {
      StartY -= StepY;

      if(StartY <= 36)
      {
            StartY = 36;
            DirY   = 1;

            StepX = rand() % 10 + 5;
            StepY = rand() % 10 + 5;
      }
    }
    else
    {
      StartY += StepY;

      if((StartY + Height) >= 204)
      {
            StartY = 204 - Height;
            DirY   = 0;

            StepX = rand() % 10 + 5;
            StepY = rand() % 10 + 5;
      }
    }

    LCD_ShowLOG(StartX, StartY, dText);
}

void SmartWatch_CreatePageRTC(void)
{
    uint8_t YearNumber = {0x02, 0x00, 0x00, 0x00};
    uint8_t DateNumber = {0x00, 0x00, 0x0E, 0x00, 0x00};
    uint8_t TimeNumber = {0x00, 0x00, 0x0B, 0x00, 0x00};
    uint8_t MiaoNumber = {0x00, 0x00};

    if(SmartWatch_PageState == 0)
    {
      TimeNumber = (RTC_Calendar.hour   / 10) % 10;
      TimeNumber = (RTC_Calendar.hour   % 10);
      TimeNumber = (RTC_Calendar.minute / 10) % 10;
      TimeNumber = (RTC_Calendar.minute % 10);

      for(uint8_t i = 0; i < sizeof(TimeNumber); i++)
      {
            LCD_DrawFontLED(40+i*32,80, TimeNumber);
      }

      MiaoNumber = (RTC_Calendar.second / 10) % 10;
      MiaoNumber = (RTC_Calendar.second % 10);

      for(uint8_t i = 0; i < sizeof(MiaoNumber); i++)
      {
            LCD_DrawFontLED(88+i*32, 140, MiaoNumber);
      }
    }
    else
    {
      YearNumber = (RTC_Calendar.year / 10) % 10;
      YearNumber = (RTC_Calendar.year % 10);

      for(uint8_t i = 0; i < sizeof(YearNumber); i++)
      {
            LCD_DrawFontLED(56+i*32,80, YearNumber);
      }

      DateNumber = (RTC_Calendar.month / 10) % 10;
      DateNumber = (RTC_Calendar.month % 10);
      DateNumber = (RTC_Calendar.day   / 10) % 10;
      DateNumber = (RTC_Calendar.day   % 10);

      for(uint8_t i = 0; i < sizeof(DateNumber); i++)
      {
            LCD_DrawFontLED(40+i*32, 140, DateNumber);
      }
    }
}

void SmartWatch_CreatePageWeather(void)
{
    uint8_t TempNumber = {0x00, 0x00, 0x0D};

    for(uint8_t i = 0; i < 40; i++)
    {
      if(Weather.Value == atoi(Weather_Code))
      {
            uint8_t StartX = (240 - strlen(Weather.Chinese) * 8) / 2;

            LCD_ShowLOG(StartX, 115, Weather.Chinese);

            LCD_DrawImage(0x100000, Weather.Offset, Weather.Length, 0);

            break;
      }
    }

    TempNumber = (atoi(Weather_Temp) / 10) % 10;
    TempNumber = (atoi(Weather_Temp) % 10);

    for(uint8_t i = 0; i < sizeof(TempNumber); i++)
    {
      LCD_DrawFontLED(72+i*32, 150, TempNumber);
    }
}

extern int32_tn_sp02;
extern int32_tn_heart_rate;

void SmartWatch_CreatePageHeartRate(void)
{
    static uint8_t Index = 0, Heartrate = 0;
    static uint8_t OldWeiShu = 0;

    uint8_t HR_Number = {0x00, 0x00, 0x00};
    uint8_t HR_WeiShu    = 0, StartX = 0;

    LCD_DrawImage(0x200000, HR_IMAGE, HR_IMAGE, 1);

    Index = (Index + 1) % 46;

    Heartrate = n_heart_rate;

    if(Heartrate < 10)
    {
      HR_WeiShu = 1;
      HR_Number = Heartrate % 10;

      StartX = (240 - 32 * 1) / 2;
    }
    else if(Heartrate < 99)
    {
      HR_WeiShu = 2;
      HR_Number = (Heartrate / 10) % 10;
      HR_Number = (Heartrate % 10);

      StartX = (240 - 32 * 2) / 2;
    }
    else
    {
      HR_WeiShu = 3;
      HR_Number = (Heartrate / 100);
      HR_Number = (Heartrate / 10) % 10;
      HR_Number = (Heartrate % 10);

      StartX = (240 - 32 * 3) / 2;
    }

    if(OldWeiShu > HR_WeiShu)
    {
      LCD_ShowLOG(72, 150, "            ");
      LCD_ShowLOG(72, 166, "            ");
    }

    OldWeiShu = HR_WeiShu;

    for(uint8_t i = 0; i < HR_WeiShu; i++)
    {
      LCD_DrawFontLED(StartX+i*32, 150, HR_Number);
    }

    Heartrate++;

    LCD_ShowLOG(108, 190, "BPM");
}


心率监测相关部分void maxim_max30102_startup(void)
{
    uint8_t dummy = 0;

    /* resets the MAX30102 */
    maxim_max30102_reset();

    /* read and clear status register */
    maxim_max30102_read_reg(0, &dummy);

    /* initializes the MAX30102 */
    maxim_max30102_init();

    /* read the first 500 samples, and determine the signal range */
    for(uint16_t i = 0; i < 500; i++)
    {
      /* wait until the interrupt pin asserts */
      while(GPIO_Input_Pin_Data_Get(GPIOB, GPIO_PIN_2) == PIN_SET);

      /* read from MAX30102 FIFO */
      maxim_max30102_read_fifo((aun_red_buffer + i),(aun_ir_buffer + i));

      printf("\r\nred = %i, ir = %i", aun_red_buffer, aun_ir_buffer);
    }

    /* calculate heart rate and SpO2 after first 500 samples (first 5 seconds of samples) */
    maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, 500, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
}

void MAX30102_Handler(void)
{
    static uint8_tmaxim_max30102_staus = 0;
    static uint16_t maxim_max30102_index = 0;

    switch(maxim_max30102_staus)
    {
      case 0:
            for(uint16_t i = 100; i < 500; i++)
            {
                aun_red_buffer = aun_red_buffer;
                aun_ir_buffer = aun_ir_buffer ;
            }

            maxim_max30102_staus = 1;
            maxim_max30102_index = 400;
            break;

      case 1:
            if(GPIO_Input_Pin_Data_Get(GPIOB, GPIO_PIN_2) != PIN_SET)
            {
                maxim_max30102_read_fifo((aun_red_buffer + maxim_max30102_index),
                                       (aun_ir_buffer+ maxim_max30102_index));

                if(maxim_max30102_index++ >= 500)
                {
                  maxim_max30102_staus = 2;
                }
            }
            break;

      case 2:
            maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, 500, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid);
            maxim_max30102_staus = 0;

            if(ch_hr_valid   == 1 && n_heart_rate < 120)
            {
                printf("\r\nvalid heart rate : %d", n_heart_rate);
            }

            if(ch_spo2_valid == 1 && n_sp02      <= 100)
            {
                printf("\r\nvalid heart rate : %d", n_heart_rate);                  
                printf("\r\nvalid spO2       : %d", n_sp02);
            }
            break;

      default:
            break;
    }
}
KEY相关部分void KEY_Handler(char *Name, uint8_t State)
{
    if(strcmp(Name, "WAKEUP") == 0)
    {
      if(State == 0)
      {
            LCD_ClearScreen(BACKCOLOR);
            SmartWatch_PageIndex=0;
      }
    }

    if(strcmp(Name, "KEY1") == 0)
    {
      if(State == 0)
      {
            if(SmartWatch_PageIndex != 1)
            {
                SmartWatch_PageState = 0;
            }
            else
            {
                if(SmartWatch_PageState == 0)
                {
                  SmartWatch_PageState = 1;
                }
                else
                {
                  SmartWatch_PageState = 0;
                }
            }

            LCD_ClearScreen(BACKCOLOR);
            SmartWatch_PageIndex=1;
      }
    }

    if(strcmp(Name, "KEY2") == 0)
    {
      if(State == 0)
      {
            LCD_ClearScreen(BACKCOLOR);
            SmartWatch_PageIndex=2;
      }
    }

    if(strcmp(Name, "KEY3") == 0)
    {
      if(State == 0)
      {
            LCD_ClearScreen(BACKCOLOR);
            SmartWatch_PageIndex=3;
      }
    }
}


运行界面 & 演示视频
https://www.bilibili.com/video/BV1Ka411T7iG/

附件软件工程源代码:功能扩展板原理图:

其它当前智能手表的随着显示内容越来越丰富,功能越来越强大,显示分辨率也越来越高;之前240*240的分辨率使用SPI就可以满足刷屏需求;现在使用在智能手表上的高分辨率的320*320、480*480等等液晶显示屏,如果还使用SPI已经有明显的卡顿/迟钝的现象,所以现在很多这样高分辨率的显示屏使用了MIPI或者QSPI的接口来进行通讯控制,国民技术在N32G45X系列MCU上配备了QSPI接口,后面有机会可以申请一块板子来验证一下QSPI接口驱动显示屏的效果方案(模块已到位就差开发板来验证了)。

www5911839 发表于 2022-7-26 12:28

感谢分享好文章,大佬强无敌

xld0932 发表于 2022-7-26 12:56

www5911839 发表于 2022-7-26 12:28
感谢分享好文章,大佬强无敌

这都被发现了……

WoodData 发表于 2022-7-26 15:12

真不错

www5911839 发表于 2022-7-26 17:45

xld0932 发表于 2022-7-26 12:56
这都被发现了……

每天用爬虫抓得21ic的精华帖,大佬文章篇篇精华,优秀的藏也藏不住啊

gyh974 发表于 2022-7-27 18:59

www5911839 发表于 2022-7-26 17:45
每天用爬虫抓得21ic的精华帖,大佬文章篇篇精华,优秀的藏也藏不住啊 ...

怎么爬取?

GrandLine 发表于 2022-8-24 15:15

https://www.bilibili.com/video/BV1Ka411T7iG/

niceguy 发表于 2022-11-3 17:39

这个QSPI屏的资料可以分享下吗

zhouchen605768 发表于 2023-2-1 19:05

学习了

abotomson 发表于 2023-3-9 12:03

网上有资料可以参考的吗?            

ulystronglll 发表于 2023-3-9 12:48

心率监测使用的是什么模块?            

everyrobin 发表于 2023-3-9 13:14

这个esp8266是at指令操作的吗

backlugin 发表于 2023-3-10 11:42

flash是用来保存数据数据的?

macpherson 发表于 2023-3-10 12:55

图片是怎么保存进去的 ?            

loutin 发表于 2023-3-10 13:29

这个界面做的非常好看呢。            

burgessmaggie 发表于 2023-3-10 13:40

GET请求是必须要参数的吗?            
页: [1]
查看完整版本: 【N32G430开发板试用】智能手表功能演示样机