FastBond智能可穿戴之智能手表原型
CSDN:工程源文件
本项目以美信的MAX32660为主控芯片,配有两种传感器,均通过I2C与MCU通信,并将传感器信息及MCU内部RTC计算的万年历信息显示在OLED屏上,可以作为智能手表的原型。
MAX32660详细介绍可参见:
Funpack 第六期
MAX32660,低功耗Arm Cortex-M4 FPU处理器,带基于FPU的微控制器(MCU),256KB Flash和96KB SRAM。其主板上已安装基于MAX32625PICO的调试适配器;完成编程后,可将其直接拆卸。其外设资源包含:
是一款小而薄的超低功耗3轴加速度计,分辨率高(13位),测量范围达±16g。数字输出数据为16位二进制补码格式,可通过SPI(3线或4线)或I2C数字接口访问。可以在倾斜检测应用中测量静态重力加速度,还可以测量运动或冲击导致的动态加速度。其高分辨率(3.9mg/LSB),能够测量不到1.0°的倾斜角度变化。
该器件提供多种特殊检测功能:
得捷电子
Mikroe的即插即用的传感器,内部含有SHT40以测量温湿度信息,I2C通信,原用于LPCS55S69-EVK上的,在此满足智能手表基本的传感器功能。
详细功能描述可参见网站及说明文档:
得捷电子
经典OLED屏,虽然商家说SPI/I2C均可,但是得改电阻,在此使用4线SPI通信模式,原本想上u8g2的,但是移植不是很顺利。
外设引脚 | 芯片引脚 |
---|---|
SCL | P0_8 |
SDA | P0_9 |
外设引脚 | 芯片引脚 |
---|---|
SCL | P0_8 |
SDA | P0_9 |
SDO | GND |
CS | VCC |
INT1 | NC |
INT2 | NC |
外设引脚 | 芯片引脚 |
---|---|
CS | P0_7 |
DC | P0_3 |
RES | P0_2 |
D1 | P0_5 |
D0 | P0_6 |
Click插件上所使用的SHT40详细说明可参见:
SHT40说明文档
商家已提供了对应SHTx的驱动代码,参见:
GitHub
在工程中,只需要按照对应的MCU,更改I2C读写部分的代码即可。在此使用MAX32660,根据帮助文档,对sensirion_hw_i2c_implementation.c
内关于I2C初始化、读写的函数进行定制化的修改即可(头文件等省略):
// SHT40 and ADXL345 interrupt handler
void I2C0_IRQHandler(void)
{
I2C_Handler(MXC_I2C0);
return;
}
int16_t sensirion_i2c_select_bus(uint8_t bus_idx)
{
// IMPLEMENT or leave empty if all sensors are located on one single bus
return STATUS_FAIL;
}
void sensirion_i2c_init(void)
{
//Setup the I2C0
int error = 0;
const sys_cfg_i2c_t sys_i2c_cfg = NULL;
//I2C_Shutdown(SHT40_I2C);
if((error = I2C_Init(SHT40_I2C, I2C_FAST_MODE, &sys_i2c_cfg)) != E_NO_ERROR)
{
printf("Error initializing I2C0.(Error code = %d)\n", error);
while(1);
}
NVIC_EnableIRQ(I2C0_IRQn);
}
void sensirion_i2c_release(void)
{
// IMPLEMENT or leave empty if no resources need to be freed
}
int8_t sensirion_i2c_read(uint8_t address, uint8_t* data, uint16_t count)
{
int error = 0;
if((error = I2C_MasterRead(SHT40_I2C, (address << 1)|1, data, count, 0)) != count)
{
printf("Error reading%d\n", error);
return error;
}
return 0;
}
int8_t sensirion_i2c_write(uint8_t address, const uint8_t* data, uint16_t count)
{
int error = 0;
if((error = I2C_MasterWrite(SHT40_I2C, (address << 1)|0, data, count, 0)) != count)
{
printf("Error writing %d\n", error);
return error;
}
return 0;
}
void sensirion_sleep_usec(uint32_t useconds)
{
mxc_delay(useconds);
}
在主程序中,通过如下语句即可得出温湿度测量值(测量值已经过处理):
int32_t temperature, humidity;
int8_t ret = 0;
ret = sht4x_measure_blocking_read(&temperature, &humidity);
if (ret == STATUS_OK)
{
printf("measured temperature: %0.2f degreeCelsius, "
"measured humidity: %0.2f percentRH\n", temperature / 1000.0f, humidity / 1000.0f);
}
else
{
printf("error reading measurement\n");
}
关于ADXL345的资料网上挺多的,根据STM32等MCU的使用历程修改即可。修改I2C初始化、读写等即可:
#define ADXL345_I2C MXC_I2C0
int ADXL345_Init(void)
{
uint8_t error;
//I2C_Shutdown(ADXL345_I2C);
if((error = I2C_Init(ADXL345_I2C, I2C_FAST_MODE, NULL)) != E_NO_ERROR)
{
printf("Error initializing I2C0.(Error code = %d)\n", error);
while(1);
}
NVIC_EnableIRQ(I2C0_IRQn);
if(ADXL345_RD_Reg(DEVICE_ID) == 0xE5) //读取器件ID
{
ADXL345_WR_Reg(INT_ENABLE, 0x00);
ADXL345_WR_Reg(DATA_FORMAT, 0x0B); //低电平中断输出,13位全分辨率,输出数据右对齐,16g量程
ADXL345_WR_Reg(BW_RATE, 0x0C); //数据输出速度为400Hz
ADXL345_WR_Reg(POWER_CTL, 0x38); //链接使能,自动睡眠,测量模式
ADXL345_WR_Reg(OFSX, 0x00);
ADXL345_WR_Reg(OFSY, 0x00);
ADXL345_WR_Reg(OFSZ, 0x00);
return E_NO_ERROR;
}
return 1;
}
uint8_t ADXL345_Write_Len(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf)
{
uint8_t txdata[16], error;
uint8_t i;
txdata[0] = reg;
for(i = 1; i < len+1; i++){
txdata[i] = buf[i-1];
}
if((error = I2C_MasterWrite(ADXL345_I2C, (addr << 1)|0, txdata, len+1, 0)) != len+1)
{
printf("Error writing %d in ADXL345_Write_Len!\n", error);
return error;
}
return 0;
}
uint8_t ADXL345_Read_Len(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *buf)
{
uint8_t res;
uint8_t error = 0;
if((error = I2C_MasterWrite(ADXL345_I2C, (addr << 1)|0, ®, 1, 0)) != 1)
{
printf("Error writing %d in ADXL345_Read_Len!\n", error);
return error;
}
if((error = I2C_MasterRead(ADXL345_I2C, (addr << 1)|1, buf, len, 0)) != len)
{
printf("Error reading %d in ADXL345_Read_Len!\n", error);
return error;
}
return 0;
}
uint8_t ADXL345_WR_Reg(uint8_t reg,uint8_t data)
{
uint8_t error;
uint8_t txdata[2];
txdata[0] = reg;
txdata[1] = data;
if((error = I2C_MasterWrite(ADXL345_I2C, ADXL345_WRITE, txdata, 2, 0)) != 2)
{
printf("Error writing %d in ADXL345_WR_Reg!\n", error);
return error;
}
return 0;
}
uint8_t ADXL345_RD_Reg(uint8_t reg)
{
uint8_t error;
uint8_t data;
if((error = I2C_MasterWrite(ADXL345_I2C, ADXL345_WRITE, ®, 1, 0)) != 1)
{
printf("Error writing %d in ADXL345_RD_Reg!\n", error);
return error;
}
if((error = I2C_MasterRead(ADXL345_I2C, ADXL345_READ, &data, 1, 0)) != 1)
{
printf("Error reading %d in ADXL345_RD_Reg!\n", error);
return error;
}
return data;
}
需要注意的是,初始化的设计需根据ADXL345手册对相应功能进行修改。接着,写测量倾角的功能函数:
void ADXL345_PROC(float *angle_x, float *angle_y, float *angle_z)
{
short x, y, z;
float x_acc_ad, y_acc_ad, z_acc_ad;
float x_angle, y_angle, z_angle;
ADXL345_Read_Average(&x, &y, &z, 10); //读取x,y,z 3个方向的加速度值 总共10次
printf("Acc of X-axis: %.1f m/s2\n", x*1.0/256*9.8);
printf("Acc of Y-axis: %.1f m/s2\n", y*1.0/256*9.8);
printf("Acc of Z-axis: %.1f m/s2\n", z*1.0/256*9.8);
x_acc_ad = x*1.0/32;
y_acc_ad = y*1.0/32;
z_acc_ad = z*1.0/32;
x_angle = ADXL345_Get_Angle(x_acc_ad, y_acc_ad, z_acc_ad, 1);
y_angle = ADXL345_Get_Angle(x_acc_ad, y_acc_ad, z_acc_ad, 2);
z_angle = ADXL345_Get_Angle(x_acc_ad, y_acc_ad, z_acc_ad, 0);
printf("Angle of X-axis: %.1f degree\n", x_angle);
printf("Angle of Y-axis: %.1f degree\n", y_angle);
printf("Angle of Z-axis: %.1f degree\n", z_angle);
*angle_x = x_angle;
*angle_y = y_angle;
*angle_z = z_angle;
}
首先计算X、Y、Z轴加速度,再计算倾角,将两者信息均打印至串口。感觉历程给出的算法得出的数值不对,上述额外的校准计算部分参考如下:
B站:ADXL345加速度计教程[HowToMechatronics]
再次需要注意的是,主程序中,初始化后需要自动校准(Auto Adjust)一次,此时需要持平ADXL345芯片以作为基准,如:
uint8_t xval = 0, yval = 0, zval= 0;
while(ADXL345_Init() != E_NO_ERROR)
{
printf("ADXL345 initialization failed\n");
mxc_delay(MXC_DELAY_SEC(1));
}
ADXL345_AUTO_Adjust(&xval, &yval, &zval);
通过加入下代码即可获取倾角值:
float angle_x, angle_y, angle_z;
ADXL345_PROC(&angle_x, &angle_y, &angle_z);
使用MCU内部的RTC以实现两种功能:
RTC初始化及中断服务函数代码参考MAXIM提供的RTC例程,计算当前时间的函数(GetNowTime()
,有修改),参考见:
Funpack第六期–使用美信半导体MAX32660-EVSYS开发板制作的具有通知提醒和体温测量功能的手表原型-by叶开
在RTC初始化函数中,记录开始时的秒数;开启报警,每60秒触发报警中断,调用RTC中断服务函数。
/* in .h define the struct time_t*/
typedef struct
{
uint16_t year;
uint8_t month;
uint16_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
uint8_t weekday;
bool leap;
} time_t, *time_t_ptr;
/* code below in .c file */
#define TIME_OF_DAY_SEC 60
time_t nowTime;
uint32_t start_sec = 0;
sys_cfg_rtc_t sys_cfg =
{
.tmr = MXC_TMR0
};
static const uint32_t SECS_PER_MIN = 60;
static const uint32_t SECS_PER_HR = 60 * SECS_PER_MIN;
static const uint32_t SECS_PER_DAY = 24 * SECS_PER_HR;
/* RTC Initialization */
int Clock_Init(void)
{
NVIC_EnableIRQ(RTC_IRQn);
/* Set the time: 2021-11-27 23:56:00, not a leap year */
nowTime.year = 2021;
nowTime.month = 11;
nowTime.day = 27;
nowTime.hour = 23;
nowTime.minute = 56;
nowTime.second = 0;
nowTime.leap = false;
start_sec = nowTime.hour * SECS_PER_HR + nowTime.minute * SECS_PER_MIN + nowTime.second;
if(RTC_Init(MXC_RTC, 0, 0, &sys_cfg) != E_NO_ERROR)
{
printf("Failed RTC_Setup().\n");
return -1;
}
if(RTC_SetTimeofdayAlarm(MXC_RTC, TIME_OF_DAY_SEC) != E_NO_ERROR)
{
printf("Failed RTC_SetTimeofdayAlarm().\n");
return -1;
}
if(RTC_EnableTimeofdayInterrupt(MXC_RTC) != E_NO_ERROR)
{
printf("Failed RTC_EnableTimeofdayInterrupt().\n");
return -1;
}
if(RTC_EnableRTCE(MXC_RTC) != E_NO_ERROR)
{
printf("Failed RTC_EnableRTCE().\n");
return -1;
}
return E_NO_ERROR;
}
RTC中断服务函数如下:
void RTC_IRQHandler(void)
{
int time;
int flags = RTC_GetFlags();
/* Check time-of-day alarm flag. */
if(flags & MXC_F_RTC_CTRL_ALDF)
{
RTC_ClearFlags(MXC_F_RTC_CTRL_ALDF);
// printTime();
DataUpdate();
/* Set a new alarm 10 seconds from current time. */
time = RTC_GetSecond();
if(RTC_SetTimeofdayAlarm(MXC_RTC, time + TIME_OF_DAY_SEC) != E_NO_ERROR)
{
/* Handle Error */
}
}
}
中断发生,调用DataUpdate()
,在该函数内进行温湿度、倾角信息的读取更新;并设置下一次60秒的报警。
参考的GetNowTime()
函数存在bug,原本每次产生日翻转后会将设置的RTC初值减去246060,但这样会改变期望实现的每一分钟自动报警的计数值,从而无法实现这一功能。另外,原函数的年翻转存在bug(即设置为2021-12-31 23:59:59,原因是未考虑12月31日进位一天)。
要想不影响自动报警功能,则不能在每次获取当前时间时改变RTC值,因此引入两个全局变量作为储存,每次获取时间做数值上的修正即可:
uint32_t new_start_sec = 0;
uint32_t start_sec = 0;
uint8_t dayRedress_leap[12] =
{
0, 3, 4, 0, 2, 5, 0, 3, 6, 1, 4, 6
};
uint8_t dayRedress_common[12] =
{
0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5
};
void GetNowTime(time_t *nowTime)
{
uint32_t day = 0, hr = 0, min = 0, sec = 0;
sec = RTC_GetSecond() + start_sec;
if(new_start_sec > 0)
sec -= 24 * 60 * 60;
day = sec / SECS_PER_DAY;
sec -= day * SECS_PER_DAY;
hr = sec / SECS_PER_HR;
sec -= hr * SECS_PER_HR;
min = sec / SECS_PER_MIN;
sec -= min * SECS_PER_MIN;
nowTime -> hour = hr;
nowTime -> minute = min;
nowTime -> second = sec;
if (day >= 1)
{
nowTime -> day++;
new_start_sec = sec - 24 * 60 * 60;
}
if ((nowTime -> year % 400 == 0) || ((nowTime -> year % 100 != 0) && (nowTime -> year % 4 == 0)))
{
nowTime -> leap = true;
}
else
{
nowTime -> leap = false;
}
switch (nowTime -> month)
{
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
if (nowTime -> day >= 32)
{
nowTime -> month++;
nowTime -> day -= 31;
}
break;
case 12: // 年翻转bug修正
if (nowTime -> day >= 32)
{
nowTime -> month = 1;
nowTime -> year++;
nowTime -> day -= 31;
}
break;
case 4:
case 6:
case 9:
case 11:
if (nowTime -> day >= 31)
{
nowTime -> month++;
nowTime -> day -= 30;
}
break;
case 2:
if (nowTime -> leap)
{
if (nowTime -> day >= 30)
{
nowTime -> month++;
nowTime -> day -= 29;
}
}
else
{
if (nowTime -> day >= 29)
{
nowTime -> month++;
nowTime -> day -= 28;
}
}
break;
default:
break;
}
nowTime -> weekday = (nowTime -> year + nowTime -> year / 4 + nowTime -> year / 400 - nowTime -> year / 100
+ (nowTime -> leap ? dayRedress_leap[nowTime -> month - 1] :
dayRedress_common[nowTime -> month - 1]) + nowTime -> day - 1) % 7;
}
对于OLED的4线SPI GPIO口初始化,需要先配置SCK/MOSI/RST/DC/CS IO口,再调用SPI初始化:
/* in .h define the GPIO ports and pins */
#define SPI_SCK_PORT PORT_0
#define SPI_SCK_PIN PIN_6
#define SPI_MOSI_PORT PORT_0
#define SPI_MOSI_PIN PIN_5
#define SPI_CS_PORT PORT_0
#define SPI_CS_PIN PIN_7
#define SPI_RST_PORT PORT_0
#define SPI_RST_PIN PIN_2
#define SPI_DC_PORT PORT_0
#define SPI_DC_PIN PIN_3
/* code below in .c file */
void gpio_init(void)
{
gpio_cfg_t gpio_SCK =
{
.port = SPI_SCK_PORT,
.mask = SPI_SCK_PIN,
.pad = GPIO_PAD_NONE,
.func = GPIO_FUNC_OUT,
};
GPIO_Config(&gpio_SCK);
gpio_cfg_t gpio_RST =
{
.port = SPI_RST_PORT,
.mask = SPI_RST_PIN,
.pad = GPIO_PAD_PULL_UP,
.func = GPIO_FUNC_OUT,
};
GPIO_Config(&gpio_RST);
gpio_cfg_t gpio_DC =
{
.port = SPI_DC_PORT,
.mask = SPI_DC_PIN,
.pad = GPIO_PAD_NONE,
.func = GPIO_FUNC_OUT,
};
GPIO_Config(&gpio_DC);
gpio_cfg_t gpio_CS =
{
.port = SPI_CS_PORT,
.mask = SPI_CS_PIN,
.pad = GPIO_PAD_NONE,
.func = GPIO_FUNC_OUT,
};
GPIO_Config(&gpio_CS);
gpio_cfg_t gpio_MOSI =
{
.port = SPI_MOSI_PORT,
.mask = SPI_MOSI_PIN,
.pad = GPIO_PAD_NONE,
.func = GPIO_FUNC_OUT,
};
GPIO_Config(&gpio_MOSI);
GPIO_OutSet(&gpio_SCK);
GPIO_OutSet(&gpio_MOSI);
GPIO_OutSet(&gpio_RST);
GPIO_OutSet(&gpio_DC);
GPIO_OutSet(&gpio_CS);
}
void OLED_Init(void)
{
SPI_Shutdown(OLED_SPI);
if (SPI_Init(OLED_SPI, 0, SPI_SPEED) != E_NO_ERROR)
{
printf("Error configuring SPI!\n");
mxc_delay(MXC_DELAY_SEC(1));
}
OLED_RST_CLR();
mxc_delay(MXC_DELAY_MSEC(100));
OLED_RST_SET();
/* 以下为历程中提供的初始化命令,略 */
}
修改向OLED写入数据的函数:
spi_req_t oledreq =
{
.tx_data = NULL,
.rx_data = NULL,
.ssel_pol = SPI_POL_LOW,
.len = 1,
.bits = 8,
.width = SPI17Y_WIDTH_1, // NOT applicable to SPI1A and SPI1B, value ignored
.ssel = 0, // NOT applicable to SPI1A and SPI1B, value ignored
.deass = 1, // NOT applicable to SPI1A and SPI1B, value ignored
.tx_num = 0,
.rx_num = 0,
.callback = NULL,
};
void OLED_WR_Byte(uint8_t data,uint8_t cmd)
{
if(cmd)
OLED_DC_SET();
else
OLED_DC_CLR();
oledreq.tx_data = &data;
SPI_MasterTrans(OLED_SPI, &oledreq);
OLED_DC_SET();
}
在主程序,可以通过如下代码完成OLED屏幕显示:
while (1)
{
OLED_Clear();
/* 显示时间信息 */
Oled_ShowTime();
/* 显示温湿度信息 */
sprintf(string_todisplay, "T:%.2f H:%.2f", temperature / 1000.0f, humidity / 1000.0f);
OLED_ShowString(20, 30, (uint8_t*)string_todisplay, 12);
/* 显示倾角信息 */
sprintf(string_todisplay, "X:%.1f Y:%.1f Z:%.1f", angle_x, angle_y, angle_z);
OLED_ShowString(0, 45, (uint8_t*)string_todisplay, 12);
OLED_Refresh();
mxc_delay(MXC_DELAY_SEC(1));
}
时间信息的显示Oled_ShowTime()
,借助上一节中GetNowTime()
即可:
const char *weekStr[7] =
{
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
void Oled_ShowTime()
{
char ch[30];
GetNowTime(&nowTime);
/* YYYY-MM-DD WEEK*/
sprintf(ch, "%04d-%02d-%02d %s", nowTime.year, nowTime.month, nowTime.day, weekStr[nowTime.weekday]);
OLED_ShowString(20, 0, (uint8_t*)ch, 12);
/* HH-MM-SS */
sprintf(ch, "%02d:%02d:%02d", nowTime.hour, nowTime.minute, nowTime.second);
OLED_ShowString(40, 15, (uint8_t*)ch, 12);
}
自此,完成所有信息的显示,时间每秒刷新,温湿度、倾角信息每分钟刷新的功能。最后添加一个按键,以即刻刷新温湿度、倾角信息,并在按键按下后,LED灯亮1秒。
对LED、PB(push button)初始化:
extern int buttonPressed;
void pbHandler(void *pb)
{
buttonPressed = 1;
}
void gpio_init(void)
{
LED_Init();
PB_Init();
PB_RegisterCallback(0, pbHandler);
PB_IntEnable(0);
}
当每次更新温湿度、倾角信息时(调用DataUpdate()
),调用LED_On(0)
即可;在主程序while(1)
中,每次均关灯,并判断按键是否被按下:
while(1)
{
LED_Off(0);
if(buttonPressed)
{
DataUpdate();
buttonPressed = 0;
}
}
写成如此判断的缺陷是,并非每次按下均能立即判断是否被按下,因为主循环内存在1秒延时函数;写成按键中断的形式可以立即响应,但不知为何SHT40的I2C读数据会出错。另,写成如下形式也可判断,或许由于按键未消抖,导致未及时更新的次数更多。
if(PB_Get(0))
{
DataUpdate();
}
设定初始时间2021-11-27 23:56:00,OLED显示如下:
按下按键,OLED变化如下两图:
由于设置初始时间为00秒,每分钟自动更新一次数值,如下两图:
由于程序存在延迟,且读取数据时的I2C读写需要时间,会出现OLED跳秒显示的情况,但RTC时间仍是准的。
该项目可实现功能如下:
FastBond智能可穿戴之智能手表原型
CSDN:工程源文件