@安小芯 @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接口驱动显示屏的效果方案(模块已到位就差开发板来验证了)。
|