@安小芯 @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[4] = {0x02, 0x00, 0x00, 0x00};
- uint8_t DateNumber[5] = {0x00, 0x00, 0x0E, 0x00, 0x00};
- uint8_t TimeNumber[5] = {0x00, 0x00, 0x0B, 0x00, 0x00};
- uint8_t MiaoNumber[2] = {0x00, 0x00};
- if(SmartWatch_PageState == 0)
- {
- TimeNumber[0] = (RTC_Calendar.hour / 10) % 10;
- TimeNumber[1] = (RTC_Calendar.hour % 10);
- TimeNumber[3] = (RTC_Calendar.minute / 10) % 10;
- TimeNumber[4] = (RTC_Calendar.minute % 10);
- for(uint8_t i = 0; i < sizeof(TimeNumber); i++)
- {
- LCD_DrawFontLED(40+i*32, 80, TimeNumber[i]);
- }
- MiaoNumber[0] = (RTC_Calendar.second / 10) % 10;
- MiaoNumber[1] = (RTC_Calendar.second % 10);
- for(uint8_t i = 0; i < sizeof(MiaoNumber); i++)
- {
- LCD_DrawFontLED(88+i*32, 140, MiaoNumber[i]);
- }
- }
- else
- {
- YearNumber[2] = (RTC_Calendar.year / 10) % 10;
- YearNumber[3] = (RTC_Calendar.year % 10);
- for(uint8_t i = 0; i < sizeof(YearNumber); i++)
- {
- LCD_DrawFontLED(56+i*32, 80, YearNumber[i]);
- }
- DateNumber[0] = (RTC_Calendar.month / 10) % 10;
- DateNumber[1] = (RTC_Calendar.month % 10);
- DateNumber[3] = (RTC_Calendar.day / 10) % 10;
- DateNumber[4] = (RTC_Calendar.day % 10);
- for(uint8_t i = 0; i < sizeof(DateNumber); i++)
- {
- LCD_DrawFontLED(40+i*32, 140, DateNumber[i]);
- }
- }
- }
- void SmartWatch_CreatePageWeather(void)
- {
- uint8_t TempNumber[3] = {0x00, 0x00, 0x0D};
- for(uint8_t i = 0; i < 40; i++)
- {
- if(Weather[i].Value == atoi(Weather_Code))
- {
- uint8_t StartX = (240 - strlen(Weather[i].Chinese) * 8) / 2;
- LCD_ShowLOG(StartX, 115, Weather[i].Chinese);
- LCD_DrawImage(0x100000, Weather[i].Offset, Weather[i].Length, 0);
- break;
- }
- }
- TempNumber[0] = (atoi(Weather_Temp) / 10) % 10;
- TempNumber[1] = (atoi(Weather_Temp) % 10);
- for(uint8_t i = 0; i < sizeof(TempNumber); i++)
- {
- LCD_DrawFontLED(72+i*32, 150, TempNumber[i]);
- }
- }
- extern int32_t n_sp02;
- extern int32_t n_heart_rate;
- void SmartWatch_CreatePageHeartRate(void)
- {
- static uint8_t Index = 0, Heartrate = 0;
- static uint8_t OldWeiShu = 0;
- uint8_t HR_Number[3] = {0x00, 0x00, 0x00};
- uint8_t HR_WeiShu = 0, StartX = 0;
- LCD_DrawImage(0x200000, HR_IMAGE[Index][0], HR_IMAGE[Index][1], 1);
- Index = (Index + 1) % 46;
- Heartrate = n_heart_rate;
- if(Heartrate < 10)
- {
- HR_WeiShu = 1;
- HR_Number[0] = Heartrate % 10;
- StartX = (240 - 32 * 1) / 2;
- }
- else if(Heartrate < 99)
- {
- HR_WeiShu = 2;
- HR_Number[0] = (Heartrate / 10) % 10;
- HR_Number[1] = (Heartrate % 10);
- StartX = (240 - 32 * 2) / 2;
- }
- else
- {
- HR_WeiShu = 3;
- HR_Number[0] = (Heartrate / 100);
- HR_Number[1] = (Heartrate / 10) % 10;
- HR_Number[2] = (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[i]);
- }
- 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[i], aun_ir_buffer[i]);
- }
- /* 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_t maxim_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[i - 100] = aun_red_buffer[i];
- aun_ir_buffer [i - 100] = aun_ir_buffer [i];
- }
- 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接口驱动显示屏的效果方案(模块已到位就差开发板来验证了)。
|