目录
前言
维特智能的6轴IMU模组可以自动输出角加速度、陀螺仪、角度等信息,并且可以通过串口或者I2C读取,因此我们可以直接将读取到的IMU姿态角映射到PWM波的脉宽,从而实现2维舵机云台的简单控制。本文使用ESP32S3通过串口读取IMU数据并生成对应的PWM波控制2维舵机云台。
硬件准备
维特智能6轴IMU:型号JY62
这款imu模组在维特的官方淘宝店就有卖,模组内部集成了TDK家的高精度imu芯片以及自研的高精度算法,输出速率快且稳定,唯一缺点就是小贵(开玩笑啦 其实是主播穷~)。
官网资料:JY62产品资料
购买链接:维特智能六轴加速度电子陀螺仪传感器姿态倾角温补传感器模块JY62-淘宝网
ESP32S3开发板
就是普通的esp32s3小系统板。
二维舵机云台
常规的2DoF舵机云台,一般由结构件和2个舵机(水平和垂直)组成,很容易买到。
杜邦线若干
软件环境
esp-idf+vscode开发环境
这里不涉及配环境教程,请读者参看其他教程。
运行维特官方的ESP32例程
打开维特官方的JY62产品资料文档,可以看到SDK一栏,点进去就能看到各种版本的SDK。
然后再点击下图所示的ESP32_SDK-UART快速上手。
点进去之后下载对应的ESP32工程压缩包,如下图:
之后将工程在vscode中打开。在main.c文件中可以看到如下引脚分配:
#define UART_RX_PIN 5
#define UART_TX_PIN 4
即imu模组的TX连ESP32的引脚5,RX连ESP32的引脚4。之后将imu模组的GND连上ESP32的GND,模组VCC连ESP32的3v3或5v。接着就是编译并烧录代码,并打开esp-idf的串口监视窗口,应该能看到如下imu模组不断输出的信息,如果晃动imu模组,相应的数据会发生改变。
ESP32的PWM功能介绍
以上已经通过ESP32的串口功能成功读取到imu模组输出的信息,但是要驱动舵机的话,需要PWM波,这里就不具体介绍PWM的相关理论了,而是简单介绍一下ESP32的PWM资源。本文使用的是ESP32S3,其PWM资源如下。本文使用的是MCPWM外设来生成PWM波驱动舵机。
LED控制PWM(LEDC)
-
通道数量:LEDC支持8个独立通道。
-
特性:
-
可配置周期和占空比,占空比分辨率可达14位。
-
支持多种时钟源,包括APB时钟和外部主晶振时钟。
-
在CPU处于轻睡眠模式时仍可运行。
-
支持占空比的渐进增加或减少,适用于LED RGB颜色渐变生成器。
-
-
引脚配置:LEDC的信号可以通过GPIO矩阵连接到任意GPIO引脚。
电机控制PWM(MCPWM)
-
通道数量:MCPWM包含两个MCPWM外设,每个外设支持3个PWM运算符。
-
特性:
-
每个MCPWM外设包含一个时钟分频器(预分频器)、三个PWM定时器、三个PWM运算符和一个捕获模块。
-
PWM定时器用于生成定时参考,PWM运算符根据这些参考生成所需的波形。
-
任何PWM运算符都可以配置为使用任何PWM定时器的定时参考,不同的PWM运算符可以使用同一个PWM定时器的定时参考来产生相关的PWM信号。
-
不同的PWM定时器也可以同步在一起。
-
-
引脚配置:MCPWM的信号可以通过GPIO矩阵连接到任意GPIO引脚。
MCPWM外设相关API
mcpwm_gpio_init
功能:将指定的GPIO初始化为MCPWM信号输出引脚。
函数原型:
esp_err_t mcpwm_gpio_init(mcpwm_unit_t mcpwm_num, mcpwm_io_signals_t io_signal, int gpio_num);
参数解读:
-
mcpwm_num:MCPWM单元编号(如
MCPWM_UNIT_0
或MCPWM_UNIT_1
)。 -
io_signal
:指定的MCPWM信号(如MCPWM0A
、MCPWM0B
等,具体信号见mcpwm_io_signals_t
)。 -
gpio_num
:要绑定的GPIO编号。
mcpwm_init
功能:初始化MCPWM单元和定时器。
函数原型:
esp_err_t mcpwm_init(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, const mcpwm_config_t *mcpwm_conf);
参数解读:
-
mcpwm_num:MCPWM单元编号。
-
timer_num
:MCPWM定时器编号(如MCPWM_TIMER_0
、MCPWM_TIMER_1
等)。 -
mcpwm_config
:指向mcpwm_config_t
结构体的指针,包含定时器的配置信息(如频率、占空比等)。
mcpwm_set_duty_in_us
功能:设置MCPWM的占空比,以微秒为单位。
函数原型:
esp_err_t mcpwm_set_duty_in_us(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, mcpwm_generator_t gen, uint32_t duty_in_us);
参数解读:
-
mcpwm_num:MCPWM单元编号。
-
timer_num
:MCPWM定时器编号。 -
gen
:PWM信号生成器编号(如MCPWM_GEN_A
或MCPWM_GEN_B
)。 -
duty_in_us
:高电平的持续时间(单位为微秒),其实就是脉宽。
核心代码解读
将imu输出的角度信息映射到PWM波的脉宽
博主所使用的舵机是270度舵机,即正负可以达到135度,而驱动舵机一般是用50HZ即周期为20ms的PWM波,脉宽范围即高电平的持续时间为500us到2500us。由于PWM控制舵机是线性的,因此可以用一个简单的公式将任意角度映射到其对应的脉宽,代码如下,用手写一下就能理解啦~
#define SERVO_MIN_PULSEWIDTH_US 500 // 最小脉冲宽度(微秒)
#define SERVO_MAX_PULSEWIDTH_US 2500 // 最大脉冲宽度(微秒)
#define SERVO_MIN_DEGREE -135.0f // 最小角度(270度范围)
#define SERVO_MAX_DEGREE 135.0f // 最大角度(270度范围)
// 角度映射到脉宽
static inline uint32_t angle_2_pulsewidth(float angle)
{
return (uint32_t)((angle - SERVO_MIN_DEGREE) * (SERVO_MAX_PULSEWIDTH_US - SERVO_MIN_PULSEWIDTH_US) / (SERVO_MAX_DEGREE - SERVO_MIN_DEGREE) + SERVO_MIN_PULSEWIDTH_US);
}
初始化两个舵机的PWM配置
其次就是初始化水平舵机和垂直舵机的PWM配置,相关函数上面已经介绍过了。本文ESP32输出用于驱动水平和垂直舵机PWM信号的引脚分别为引脚18和17。
#include "driver/mcpwm.h"
#define SERVO_HORIZONTAL_GPIO 18 // 水平舵机连接的 GPIO 引脚
#define SERVO_VERTICAL_GPIO 17 // 垂直舵机连接的 GPIO 引脚
/**
* @brief PWM初始化
*/
void Servo_Init(void){
// 初始化水平舵机 MCPWM
mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A,SERVO_HORIZONTAL_GPIO);
mcpwm_config_t servo_config = {
.frequency = 50, // 舵机的频率为 50Hz
.cmpr_a = 0, // 初始占空比为 0
.duty_mode = MCPWM_DUTY_MODE_0,
.counter_mode = MCPWM_UP_COUNTER,
};
ESP_ERROR_CHECK(mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &servo_config));
ESP_LOGI(TAG, "水平舵机 MCPWM 初始化成功");
// 初始化垂直舵机 MCPWM
mcpwm_gpio_init(MCPWM_UNIT_1, MCPWM1A, SERVO_VERTICAL_GPIO);
ESP_ERROR_CHECK(mcpwm_init(MCPWM_UNIT_1, MCPWM_TIMER_1, &servo_config));
ESP_LOGI(TAG, "垂直舵机 MCPWM 初始化成功");
// 设置舵机到初始位置(0 度)
ESP_ERROR_CHECK(mcpwm_set_duty_in_us(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, angle_2_pulsewidth(0.0f)));
ESP_ERROR_CHECK(mcpwm_set_duty_in_us(MCPWM_UNIT_1, MCPWM_TIMER_1, MCPWM_OPR_A, angle_2_pulsewidth(0.0f)));
ESP_LOGI(TAG, "舵机设置到 0 度");
}
main函数
基于维特官方的工程,在while(1)循环里,ESP32一旦获得imu模组输出的angle信息,就将其转换成脉宽,然后通过mcpwm_set_duty_in_us函数设置占空比,就能实时用imu模组来控制2维舵机云台啦~此外还添加了打印日志,实时输出2个舵机的PWM脉宽和角度,不过这里偏航角是有累计误差的,因为6轴IMU没有地磁来修正其积分累计误差。另外,由于舵机云台是2DoF的,而imu模组会输出3个角度,因此其中有一个用不上(滚转角)。
void app_main(void)
{
float fAcc[3], fGyro[3], fAngle[3];
int i;
xTaskCreate(Usart0_task, "Usart0_task", 4096, NULL, 5, NULL);
xTaskCreate(Usart1_task, "Usart1_task", 4096, NULL, 5, NULL);
WitInit(WIT_PROTOCOL_NORMAL, 0x50);
WitSerialWriteRegister(SensorUartSend);
WitRegisterCallBack(SensorDataUpdata);
WitDelayMsRegister(Delayms);
printf("\r\n********************** wit-motion normal example ************************\r\n");
AutoScanSensor();
/******************************舵机********************************* */
Servo_Init();
vTaskDelay(pdMS_TO_TICKS(1000)); // 等待舵机就位
while (1)
{
if(s_cDataUpdate)
{
for(i = 0; i < 3; i++)
{
fAcc[i] = sReg[AX+i] / 32768.0f * 16.0f;
fGyro[i] = sReg[GX+i] / 32768.0f * 2000.0f;
fAngle[i] = sReg[Roll+i] / 32768.0f * 180.0f;
}
if(s_cDataUpdate & ACC_UPDATE)
{
printf("acc:%.3f %.3f %.3f\r\n", fAcc[0], fAcc[1], fAcc[2]);
s_cDataUpdate &= ~ACC_UPDATE;
}
if(s_cDataUpdate & GYRO_UPDATE)
{
printf("gyro:%.3f %.3f %.3f\r\n", fGyro[0], fGyro[1], fGyro[2]);
s_cDataUpdate &= ~GYRO_UPDATE;
}
if(s_cDataUpdate & ANGLE_UPDATE)
{
printf("angle:%.3f %.3f %.3f\r\n", fAngle[0], fAngle[1], fAngle[2]);
/******************************************************添加的控制代码*******************************************************************/
// 设置舵机角度
uint32_t horizontal_pulsewidth = angle_2_pulsewidth(fAngle[2]);
uint32_t vertical_pulsewidth = angle_2_pulsewidth(fAngle[0]);
ESP_ERROR_CHECK(mcpwm_set_duty_in_us(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, horizontal_pulsewidth));//水平舵机,对应云台偏航角
ESP_ERROR_CHECK(mcpwm_set_duty_in_us(MCPWM_UNIT_1, MCPWM_TIMER_1, MCPWM_OPR_A, vertical_pulsewidth));//垂直舵机,对应云台俯仰角
// 打印日志
ESP_LOGI(TAG, "水平舵机脉冲宽度: %lu us", horizontal_pulsewidth);
ESP_LOGI(TAG, "垂直舵机脉冲宽度: %lu us", vertical_pulsewidth);
ESP_LOGI(TAG, "偏航角: %.2f 度,俯仰角: %.2f 度", fAngle[2], fAngle[0]);
/*************************************************************************************************************************/
s_cDataUpdate &= ~ANGLE_UPDATE;
}
if(s_cDataUpdate & MAG_UPDATE)
{
printf("mag:%d %d %d\r\n", sReg[HX], sReg[HY], sReg[HZ]);
s_cDataUpdate &= ~MAG_UPDATE;
}
if(s_cDataUpdate & TEMP_UPDATE)
{
printf("temp:%f\r\n", sReg[TEMP]/100.0);
s_cDataUpdate &= ~TEMP_UPDATE;
}
}
}
}
最终效果
连线示意图如下所示。这里主播踩了一个坑,就是2个舵机的VCC跟GND最好用另一个板子来供,然后ESP32跟那个板子共地,因为ESP32数据线是连的电脑,舵机断电和上电瞬间可能会产生反电动势通过ESP32的数据线打在电脑上,触发电脑的过流保护而导致电脑关机,这个其实可能也跟舵机有关,可能是主播买的舵机的工作电流比较大的缘故。
结果演示
下面是esp-idf的串口监视窗口,可以看到除了原先的imu信息,还多了新加的打印日志。当然博主为了方便演示,代码中直接用的绝对量来控制,正常工程应用的话应该用增量控制。
PS:完整工程还请读者移步至我的gitee仓库:esp_imuServo: 基于维特6轴imu模组和ESP32控制2维舵机云台