基于ESP32的智能运动装置
1. 项目的背景
1.1 项目的意义
随着科技的进度,人们对于健康的要求越来越高,对于体育运动的训练也越来越重视。对于运动装备的智能化已经成为大家的考虑范围。因此基于ESP32的智能运动装置便成为一个引人注目的课题。
•提升运动体验:智能运动装置结合了传感器技术、数据分析和语音播报等先进技术,可以根据具体的运动去做相应的调整,实现运动动作正确性检测、播报、运动数据上云等等。
•促进科技与体育的结合:智能运动装置推动科技与体育的融合。通过先进的传感器技术和数据处理以及云技术,不仅可以提升棒球运动的水平和质量,还有助于推动体育科技的发展和创新。这对于激发年轻人对科技和体育的兴趣,促进体育产业的发展具有重要意义。
1.2 需求分析
1.2.1硬件需求
1. MPU6050模块
MPU6050模块可以获得加速度值、饶X、Y、Z三个轴的角速度值,分别为ACC_X、ACC_Y、ACC_Z和GYR_X、GYR_Y、GYR_Z,还可以通过四元数的格式给出。其中MPU6050通过IIC与ESP32通信。
Tips :四元数主要应用于机器人的姿态解算。
2. ESP32主控板
ESP32具有很强的功能,主控芯片具体的配置如下:
- 处理器:Tensilica LX6 双核处理器(一核处理高速连接;一核独立应用开发)
- 主频:32 位双核处理器,CPU 正常工作速度为 80 MHz,最高可达 240 MHz
- SRAM:520KB,最大支持 8 MB 片外 SPI SRAM
- Flash:最大支持 16 MB 片外 SPI Flash
- WiFi 协议:支持 802.11 b/g/n/d/e/i/k/r 等协议,速度高达150 Mbps
- 蓝牙协议:支持蓝牙 v4.2 完整标准,包含传统蓝牙 (BR/EDR) 和低功耗蓝牙 (BLE)
- 同时他还具备丰富的外设接口:比如 GPIO、ADC、DAC、SPI、I²C、I²S、UART 等常用接口。
其中,本发明的ESP32用于读取MPU6050的IIC数据和压力传感器ADC数值,经过数据处理之后,可以给ASRPRO语音模块发送串口信号,实现语音播报;同时也向blinker云服务器发送数据,实现数据上云。
3. 薄膜压力传感器
本项目使用的是薄膜压力应变片,需要配合相应的电路使用,这里接了33k上拉电阻,通过ESP32的ADC通道读取读取传感器的分压,将压力的变化转换成电信号的变化传递给ESP32主控板。
4. 具有蓝牙和wifi的手机
手机的功能主要是下载blinker客户端,用于实现ESP32的wifi和蓝牙接入,从而实现数据在手机端端实时查看。
2. 技术路线
电子部分有ESP32主控板、ASR PRO语音模块、降压模块、薄膜压力传感器和传感器电路、MPU6050等模块。通过ESP32主控板,可以完成击球旋转的检测、击球位置的检测、发送相关的语音控制信号给ASR PRO语音模块。通过MPU6050可以根据加速度和角度信息完成击球旋转的检测。通过薄膜压力传感器和相关的电阻上拉电路完成ADC模拟量的读取,以实现击球位置的检测,并将击球位置的信号发给语音模块ASR PRO。
3. 软件和硬件设计
3.1 薄膜压力传感器ADC取样电路
本文采用薄膜压力传感器获取压力,而压力应变片本身是一个可变的电阻,加之本发明对于压力只需要知道变化情况即可,为了方便起见,采用ESP32等ADC通道读取压力传感器的分压,从而知道压力的大小变化情况。
为此,本文设计了采样电路,主要是将薄膜压力应变片接上拉电阻。
3.2 代码讲解
这里是ESP32的代码,下面会对代码进行详细讲解。
#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif
#ifndef LED_BUILTIN
#define LED_BUILTIN 13
#endif
#include <MPU6050_tockn.h>
#include <Button.h>
#define BLINKER_BLE
#include <Blinker.h>
#include <Wire.h>
#define SDA 25
#define SCL 26
Button button1(27);
MPU6050 mpu6050(Wire);
// char auth[] = "a93660bb7639";
// char ssid[] = "maegae";
// char pswd[] = "awhkd8nfje2b7";
BlinkerNumber Number1("Total");
BlinkerNumber Number2("Success");
BlinkerNumber Number3("ContactPoint");
//int value[]={};
int flag1=1;
int flag2=1;
int done=0;
// unsigned long time1=0;
// unsigned long time2=0;
float total=0;
float fail=0;
int contactpoint=0; //6.15
int i=0;
void dataRead(const String & data)
{
BLINKER_LOG("Blinker readString: ", data);
}
// define two tasks for Blink & AnalogRead
void TaskBlink( void *pvParameters );
void TaskAnalogReadA3( void *pvParameters );
// the setup function runs once when you press reset or power the board
void setup() {
// initialize serial communication at 115200 bits per second:
BLINKER_DEBUG.stream(Serial);
Blinker.begin();
button1.begin();
Serial.begin(115200);
Serial2.begin(9600);
Wire.begin(SDA,SCL);
mpu6050.begin();
mpu6050.calcGyroOffsets(true);
// Now set up two tasks to run independently.
xTaskCreatePinnedToCore(
TaskBlink
, "TaskBlink" // A name just for humans
, 2048 // This stack size can be checked & adjusted by reading the Stack Highwater
, NULL
, 2 // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
, NULL
, ARDUINO_RUNNING_CORE);
xTaskCreatePinnedToCore(
TaskAnalogReadA3
, "AnalogReadA3"
, 1024 // Stack size
, NULL
, 1 // Priority
, NULL
, ARDUINO_RUNNING_CORE);
}
void loop()
{
Blinker.run();
}
void TaskBlink(void *pvParameters) // This is a task.
{
(void) pvParameters;
for (;;) // A Task shall never return or exit.
{
int adc1=analogRead(32);
int adc2=analogRead(35);
int adc3=analogRead(34);
Serial.println(adc1);
Serial.println(adc2);
Serial.println(adc3);
if(adc1!=4095||adc2!=4095||adc3!=4095)
{
if(flag2==0&&adc1<=adc2&&adc2<=adc3)
{
Serial.print("a");
Serial2.print("a");
flag2=1;
done=1;
contactpoint=1;
Number3.print(contactpoint);
delay(5);
}
else if(flag2==0&&adc1<=adc3&&adc3<=adc2)
{
Serial.print("a");
Serial2.print("a");
flag2=1;
done=1;
contactpoint=1;
Number3.print(contactpoint);
delay(5);
}
else if(flag2==0&&adc2<=adc1&&adc1<=adc3)
{
Serial.print("b");
Serial2.print("b");
flag2=1;
done=1;
contactpoint=2;
Number3.print(contactpoint);
delay(5);
}
else if(flag2==0&&adc2<=adc3&&adc3<=adc1)
{
Serial.print("b");
Serial2.print("b");
flag2=1;
done=1;
contactpoint=2;
Number3.print(contactpoint);
delay(5);
}
else if(flag2==0&&adc3<=adc1&&adc1<=adc2)
{
Serial.print("c");
Serial2.print("c");
flag2=1;
done=1;
contactpoint=3;
Number3.print(contactpoint);
delay(5);
}
else if(flag2==0&&adc3<=adc2&&adc2<=adc1)
{
Serial.print("c");
Serial2.print("c");
flag2=1;
done=1;
contactpoint=3;
Number3.print(contactpoint);
delay(5);
}
}
if (button1.released())
{
if(total<5)
{
total+=1;
flag1=0;
flag2=0;
done=0;
Serial2.print("begin");
Number1.print(total);
delay(5);
}
}
if (flag1==0 && mpu6050.getAngleY()<=-65 && done==0)
{
Serial2.print("false");
Serial.println("false");
flag1=1;
fail+=1;
}
if(total>=5 && done==1)
{
Serial.println(total);
Serial.println(fail);
float suc=(total-fail)/total;
Serial.println(int(suc*100));
Serial2.print("w");
Serial2.print(int(((total-fail)/(total))*100));
Number2.print(int(((total-fail)/(total))*100));
delay(10);
total=0;
fail=0;
Number1.print(total);
delay(5);
}
delay(100);
}
}
void TaskAnalogReadA3(void *pvParameters) // This is a task.
{
(void) pvParameters;
for (;;)
{
mpu6050.update();
vTaskDelay(10); // one tick delay (15ms) in between reads for stability
}
}
由于本项目需要完成串口和IIC通信,还需要完成数据处理的功能以及数据上云的功能,涉及的任务量比较多,因此需要考虑ESP32的多核任务调度的问题,以实现运算资源的合理分配。为此,本项目采用FreeRTOS实时操作系统去分配任务。定义了两个TASK,一个是完成MPU6050模块的更新;另一个TASK是完成ADC采样以及分析旋转和击打位置,并完成Blinker上云和语音模块的播报。
Tips: ESP32的FreeRTOS可以进行任务的分配,可以定义堆栈的大小以及任务的优先级。!!! 注意:堆栈大小不能随意调整,以防出现堆栈溢出的情况。每个任务都要配有延时,以确保任务调度的正常进行。
这里是ASR PRO语音模块的代码。
#include "asr.h"
extern "C"{ void * __dso_handle = 0 ;}
#include "setup.h"
#include "HardwareSerial.h"
#include "myLib/asr_event.h"
uint32_t snid;
String Rec;
void hardware_init();
void UART_RX();
void UART1_RX();
void ASR_CODE();
//{ID:250,keyword:"命令词",ASR:"最大音量",ASRTO:"音量调整到最大"}
//{ID:251,keyword:"命令词",ASR:"中等音量",ASRTO:"音量调整到中等"}
//{ID:252,keyword:"命令词",ASR:"最小音量",ASRTO:"音量调整到最小"}
void hardware_init(){
Rec = "";
xTaskCreate(UART_RX,"UART_RX",128,NULL,4,NULL);
xTaskCreate(UART1_RX,"UART1_RX",256,NULL,4,NULL);
vTaskDelete(NULL);
}
void UART_RX(){
while (1) {
if(Serial.available() > 0){
Rec = Serial.readString();
if(Rec == "hello"){
Serial.print(Rec);
delay(200);
enter_wakeup(5000);
delay(200);
//{ID:500,keyword:"命令词",ASR:"耍接官",ASRTO:"收到ESP32信息"}
play_audio(500);
}
}
delay(20);
}
vTaskDelete(NULL);
}
void UART1_RX(){
while (1) {
if(Serial1.available() > 0){
Rec = Serial1.readString();
Serial.println(Rec);
if(Rec == "false"){
Serial.print(Rec);
delay(200);
enter_wakeup(100000);
delay(200);
//{ID:501,keyword:"命令词",ASR:"接耍官",ASRTO:"击球发生旋转,请改正姿势"}
play_audio(501);
}
else if(Rec == "begin"){
delay(200);
enter_wakeup(100000);
delay(200);
//{ID:502,keyword:"命令词",ASR:"官耍接",ASRTO:"开始击球"}
play_audio(502);
}
else if(Rec == "a"){
Serial.print(Rec);
delay(200);
enter_wakeup(100000);
delay(200);
//{ID:503,keyword:"命令词",ASR:"丝耍接",ASRTO:"击球位置偏高"}
play_audio(503);
//play_num((int64_t)((uint8_t)Rec[2] * 100), 1);
}
else if(Rec == "b"){
Serial.print(Rec);
delay(200);
enter_wakeup(100000);
delay(200);
//{ID:504,keyword:"命令词",ASR:"粮耍接",ASRTO:"好球"}
play_audio(504);
}
else if(Rec == "c"){
Serial.print(Rec);
delay(200);
enter_wakeup(100000);
delay(200);
//{ID:505,keyword:"命令词",ASR:"菌耍接",ASRTO:"击球位置偏低"}
play_audio(505);
}
if(isdigit(Rec[2]))
{
if(Rec[0]=="a")
{
Serial.print(Rec);
delay(200);
enter_wakeup(100000);
delay(200);
play_audio(503);
//play_num((int64_t)((uint8_t)Rec[2] * 100), 1);
}
else if(Rec[0]=="b")
{
Serial.print(Rec);
delay(200);
enter_wakeup(100000);
delay(200);
play_audio(504);
}
else if(Rec[0]=="c")
{
Serial.print(Rec);
delay(200);
enter_wakeup(100000);
delay(200);
play_audio(505);
}
delay(10);
int len = sizeof(Rec);
int n = len-2;
String broadcast=Rec.substring(2,n);
char cbroadcast[10];
strcpy(cbroadcast,broadcast.c_str());
Serial.println(atoi(cbroadcast));
//{ID:506,keyword:"命令词",ASR:"耍接官",ASRTO:"击球成功率"}
play_audio(506);
delay(20);
play_num((int64_t)((uint8_t)(atoi(cbroadcast)) * 100), 1);
// char broadcast[n+1];
// strncpy(broadcast, Rec+2, n);
// //memcpy(broadcast,Rec+3,n);
// broadcast[n] = '\0';
//Serial.print(atoi(broadcast));
//delay(200);
}
}
delay(20);
}
vTaskDelete(NULL);
}
/*描述该功能...
*/
void ASR_CODE(){
switch (snid) {
case 1:
digitalWrite(4,0);
break;
case 2:
digitalWrite(4,1);
break;
}
}
void setup()
{
//{speak:小蝶-清新女声,vol:10,speed:10,platform:haohaodada}
//{playid:10001,voice:欢迎使用智能棒球棒}
//{playid:10002,voice:我退下了,请用小棒唤醒我}
//{ID:0,keyword:"唤醒词",ASR:"天问五幺",ASRTO:"我在"}
//{ID:1,keyword:"命令词",ASR:"打开灯光",ASRTO:"好的,马上打开灯光"}
setPinFun(13,SECOND_FUNCTION);
setPinFun(14,SECOND_FUNCTION);
Serial.begin(9600);
setPinFun(2,FORTH_FUNCTION);
setPinFun(3,FORTH_FUNCTION);
Serial1.begin(9600);
xTaskCreate(hardware_init,"hardware_init",256,NULL,100,NULL);
//{ID:84,keyword:"命令词",ASR:"条耍改",ASRTO:"零"}
//{ID:85,keyword:"命令词",ASR:"官接思",ASRTO:"一"}
//{ID:86,keyword:"命令词",ASR:"痛官松",ASRTO:"二"}
//{ID:87,keyword:"命令词",ASR:"削丝误",ASRTO:"三"}
//{ID:88,keyword:"命令词",ASR:"景粮载",ASRTO:"四"}
//{ID:89,keyword:"命令词",ASR:"博菌避",ASRTO:"五"}
//{ID:90,keyword:"命令词",ASR:"裁纯碉",ASRTO:"六"}
//{ID:91,keyword:"命令词",ASR:"插趣悟",ASRTO:"七"}
//{ID:92,keyword:"命令词",ASR:"辞暖慌",ASRTO:"八"}
//{ID:93,keyword:"命令词",ASR:"纵猛淡",ASRTO:"九"}
//{ID:94,keyword:"命令词",ASR:"锦耗暂",ASRTO:"十"}
//{ID:95,keyword:"命令词",ASR:"燃智截",ASRTO:"百"}
//{ID:96,keyword:"命令词",ASR:"佛驻延",ASRTO:"千"}
//{ID:97,keyword:"命令词",ASR:"隔枪绍",ASRTO:"万"}
//{ID:98,keyword:"命令词",ASR:"惨愤昂",ASRTO:"亿"}
//{ID:99,keyword:"命令词",ASR:"丙迈扯",ASRTO:"负"}
//{ID:100,keyword:"命令词",ASR:"铸猜隆",ASRTO:"点"}
}
4. 排坑笔记
4.1 ADC通道与Wi-Fi的冲突问题:
!!!请注意,ADC2通道在ESP32开启WIFI后,功能会受到限制,据说是WIFI需要ADC2通道的IO口去驱动,建议使用ADC1通道!上图展示了ADC1和ADC2通道。
4.2字符串的处理
ESP32串口给ASRPRO发送串口数据时,ASRPRO是一个字节读取的,并且存储的类型是String,为了对字符串进行便捷的处理操作,需要借助C语言库函数,但是 C语言库函数只支持char类型的处理,因此需要考虑String和char类型的转换。
具体的实现流程如下:
int len = sizeof(Rec);
int n = len-2;
String broadcast=Rec.substring(2,n);
char cbroadcast[10];
strcpy(cbroadcast,broadcast.c_str());
Serial.println(atoi(cbroadcast));
TIPS: ASR PRO语音模块接到的是Rec(是一个String型),需要用到substring()
去取某些位,然后使用strcpy()
函数做字符串的copy,其中要使用到c_str()
做String型转char类型