目录
项目介绍
本项目以美信的MAX32660为主控芯片,配有两种传感器,均通过I2C与MCU通信,并将传感器信息及MCU内部RTC计算的万年历信息显示在OLED屏上,可以作为智能手表的原型。
使用器件
MAX32660-EVSYS
MAX32660详细介绍可参见:
MAX32660,低功耗Arm Cortex-M4 FPU处理器,带基于FPU的微控制器(MCU),256KB Flash和96KB SRAM。其主板上已安装基于MAX32625PICO的调试适配器;完成编程后,可将其直接拆卸。其外设资源包含:
- 最大14路 GPIO
- 最大两路 SPI
- 一路 I2S
- 最大两路 UART
- 最大两路 I2C
- 四通道标准DMA控制器
- 3路32位计时器
- 看门狗计时器
- RTC 32.768kHz
ADXL345数字加速度计
是一款小而薄的超低功耗3轴加速度计,分辨率高(13位),测量范围达±16g。数字输出数据为16位二进制补码格式,可通过SPI(3线或4线)或I2C数字接口访问。可以在倾斜检测应用中测量静态重力加速度,还可以测量运动或冲击导致的动态加速度。其高分辨率(3.9mg/LSB),能够测量不到1.0°的倾斜角度变化。
该器件提供多种特殊检测功能:
- 活动和非活动检测功能通过比较任意轴上的加速度与用户设置的阈值来检测有无运动发生;
- 敲击检测功能可以检测任意方向的单振和双振动作;
- 自由落体检测功能可以检测器件是否正在掉落等。
详细功能描述可参见网站及说明文档:
👉 得捷电子
Temp&Hum 15 Click
Mikroe的即插即用的传感器,内部含有SHT40以测量温湿度信息,I2C通信,原用于LPCS55S69-EVK上的,在此满足智能手表基本的传感器功能。
详细功能描述可参见网站及说明文档:
👉 得捷电子
SS1306驱动的4线SPI 12864 OLED屏
经典OLED屏,虽然商家说SPI/I2C均可,但是得改电阻,在此使用4线SPI通信模式,原本想上u8g2的,但是移植不是很顺利。
系统引脚连接
- Temp & Hum 15 Click(SHT40):
外设引脚 | 芯片引脚 |
---|---|
SCL | P0_8 |
SDA | P0_9 |
- ADXL345:
外设引脚 | 芯片引脚 |
---|---|
SCL | P0_8 |
SDA | P0_9 |
SDO | GND |
CS | VCC |
INT1 | NC |
INT2 | NC |
- OLED:
外设引脚 | 芯片引脚 |
---|---|
CS | P0_7 |
DC | P0_3 |
RES | P0_2 |
D1 | P0_5 |
D0 | P0_6 |
关键性代码及说明
SHT40获取温湿度信息
Click插件上所使用的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");
}
ADXL获取倾角信息
关于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);
RTC万年历
使用MCU内部的RTC以实现两种功能:
- 万年历,初始化当前时间(年月日星期时分秒),将时间显示在OLED屏上并每秒刷新时间;
- 自动设置每分钟更新一次倾角、温湿度信息,将这些信息显示在OLED屏上。
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;
}
SS1306 12864 4-SPI OLED显示
对于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屏上。
按键即刻更新
按下按键,OLED变化如下两图:
可以发现,按下时LED灯亮,温湿度、倾角信息有变化。
每分钟自动更新
由于设置初始时间为00秒,每分钟自动更新一次数值,如下两图:
由于程序存在延迟,且读取数据时的I2C读写需要时间,会出现OLED跳秒显示的情况,但RTC时间仍是准的。
项目总结
该项目可实现功能如下:
- OLED屏幕显示年月日星期时分秒、温湿度信息、倾角信息;
- 可采集温湿度、倾角数据,自动每分钟更新(获取)一次,每次更新LED亮1秒;
- 用芯片内部RTC计算万年历,每秒更新时间至屏幕,及触发每分钟更新的功能(中断处理);
- 配有按键,按下提示灯亮1秒,并立刻更新温湿度、倾角数据至屏幕;
- 初始时间只能通过烧写时写入,重新上电则时间重置;
- 获取的温湿度、三轴加速度、倾角数据会通过串口打印。