Video
视频
讲
解
1
项目介绍
本项目以美信的MAX32660为主控芯片,配有两种传感器,均通过I2C与MCU通信,并将传感器信息及MCU内部RTC计算的万年历信息显示在OLED屏上,可以作为智能手表的原型。
2
使用器件
· MAX32660-EVSYS
MAX32660,低功耗Arm Cortex-M4 FPU处理器,带基于FPU的微控制器(MCU),256KB Flash和96KB SRAM。其主板上已安装基于MAX32625PICO的调试适配器;完成编程后,可将其直接拆卸。
其外设资源包含:
1. 最大14路 GPIO
2. 最大两路 SPI
3. 一路 I2S
4. 最大两路 UART
5. 最大两路 I2C
6. 四通道标准DMA控制器
7. 3路32位计时器
8. 看门狗计时器
9. RTC 32.768kHz
· ADXL345数字加速度计
是一款小而薄的超低功耗3轴加速度计,分辨率高(13位),测量范围达±16g。数字输出数据为16位二进制补码格式,可通过SPI(3线或4线)或I2C数字接口访问。可以在倾斜检测应用中测量静态重力加速度,还可以测量运动或冲击导致的动态加速度。其高分辨率(3.9mg/LSB),能够测量不到1.0°的倾斜角度变化。
该器件提供多种特殊检测功能:
1.活动和非活动检测功能通过比较任意轴上的加速度与用户设置的阈值来检测有无运动发生;
2.敲击检测功能可以检测任意方向的单振和双振动作;
3.自由落体检测功能可以检测器件是否正在掉落等。
· Temp&Hum 15 Click
Mikroe的即插即用的传感器,内部含有SHT40以测量温湿度信息,I2C通信,原用于LPCS55S69-EVK上的,在此满足智能手表基本的传感器功能。
· SS1306驱动的4线SPI 12864 OLED屏
经典OLED屏,虽然商家说SPI/I2C均可,但是得改电阻,在此使用4线SPI通信模式,原本想上u8g2的,但是移植不是很顺利。
系统引脚连接:
1.Temp & Hum 15 Click(SHT40):
2.ADXL345:
3.OLED:
3
关键性代码及说明
SHT40获取温湿度信息(点击“阅读原文”获取)
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;
}
(由于代码较长,请点击“阅读原文”获取)
在主程序中,通过如下语句即可得出温湿度测量值(测量值已经过处理):
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初始化、读写等即可:
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;
}
(由于代码较长,请点击“阅读原文”获取)
需要注意的是,初始化的设计需根据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以实现两种功能:
1、万年历,初始化当前时间(年月日星期时分秒),将时间显示在OLED屏上并每秒刷新时间;
2、自动设置每分钟更新一次倾角、温湿度信息,将这些信息显示在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 */
time_t nowTime;
uint32_t start_sec = 0;
sys_cfg_rtc_t sys_cfg =
{
.tmr = MXC_TMR0
};
(由于代码较长,请点击“阅读原文”获取)
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初值减去24*60*60,但这样会改变期望实现的每一分钟自动报警的计数值,从而无法实现这一功能。另外,原函数的年翻转存在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] =
{
3, 4, 0, 2, 5, 0, 3, 6, 1, 4, 6
};
uint8_t dayRedress_common[12] =
{
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;
> 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;
}
(由于代码较长,请点击“阅读原文”获取)
SS1306 12864 4-SPI OLED显示
对于OLED的4线SPI GPIO口初始化,需要先配置SCK/MOSI/RST/DC/CS IO口,再调用SPI初始化:
/* in .h define the GPIO ports and pins */
/* 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);
(由于代码较长,请点击“阅读原文”获取)
修改向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();
}
4
功能演示结果及说明
初始化
设定初始时间2021-11-27 23:56:00,OLED显示如下:
此时已经读取了温湿度及倾角值并显示在OLED屏上。
按键即刻更新
按下按键,OLED变化如下两图:
可以发现,按下时LED灯亮,温湿度、倾角信息有变化。
每分钟自动更新
由于设置初始时间为00秒,每分钟自动更新一次数值,如下两图:
由于程序存在延迟,且读取数据时的I2C读写需要时间,会出现OLED跳秒显示的情况,但RTC时间仍是准的。
5
项目总结 & 心得体会
该项目可实现功能如下:
1.OLED屏幕显示年月日星期时分秒、温湿度信息、倾角信息;
2.可采集温湿度、倾角数据,自动每分钟更新(获取)一次,每次更新LED亮1秒;
3.用芯片内部RTC计算万年历,每秒更新时间至屏幕,及触发每分钟更新的功能(中断处理);
4.配有按键,按下提示灯亮1秒,并立刻更新温湿度、倾角数据至屏幕;
5.初始时间只能通过烧写时写入,重新上电则时间重置;
6.获取的温湿度、三轴加速度、倾角数据会通过串口打印。
心得体会:
1.此次使用的MAX32660麻雀虽小,五脏俱全。遗憾是GPIO口太少,最后想加一个串口以更新时间初始值的功能但没有多余的UART可以使用;
2.ADXL345本想使用中断以检测自由落体的,但是可能设置有问题,中断出不来,只能实现测量倾角;
3.后续希望将屏幕换成I2C通信的,这样可以多一个串口资源,并希望利用RTC实现一些闹钟、低功耗、自动唤醒等场景。
6
代码获取
代码都整理在原文页面,记得点击获取哦~
END
硬禾学堂
硬禾团队一直致力于给电子工程师和相关专业的同学,带来规范的核心技能课程,帮助大家在学习和工作的各个阶段,都能有效地提升自己的职业能力。
硬禾学堂
我们一起在电子领域探索前进
关注硬禾服务号,随时直达课堂
点击阅读原文,获取完整代码~