ESP32芯片-LEDC外设(另附Arduino代码)
在文档内,我们就看这里就好
在大量的说明舵机之前,不妨让我们先来了解一些关于芯片本身的能力:
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html
有个神奇的地方,ESP的芯片有专门控制LED的外设,又因为控制LED就是控制的PWM,舵机的控制也是PWM,所以很自然的就会想到用LED的外设去控制舵机。
可以参考官方的文档
LED 控制 (LEDC) 外设主要用于控制 LED 的强度,但也可用于生成 PWM 信号以用于其他目的。它有 16 个通道,可以生成独立的波形,例如用于驱动 RGB LED 设备。
我们完全可以做出这样的设备
LEDC 通道分为两组,每组 8 个通道,一共16个通道。
一组 LEDC 通道以高速模式运行。该模式在硬件中实现,并提供 PWM 占空比的自动和无干扰更改。
一组通道工作在低速模式,PWM 占空比必须由驱动程序在软件中改变。每组通道也可以使用不同的时钟源。
高速模式可实现计时器设置的无故障切换。这意味着如果定时器设置被修改,更改将自动应用于定时器的下一个溢出中断。相比之下,更新低速定时器时,设置的更改应由软件明确触发。LEDC 驱动程序在后台处理它。
例如,当ledc_timer_config()
或ledc_timer_set()
被调用时。
https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf#ledpwm
在设计手册里面有着更加详细的论述,我们之后再研读
一共是2组的通道
PWM 控制器可以自动逐渐增加或减少占空比,允许在没有任何处理器干扰的情况下进行衰减。
LED PWM 控制器主要设计用于驱动 LED。它提供了很大的 PWM 占空比设置灵活性。例如,5 kHz 的 PWM 频率可以具有 13 位的最大占空比分辨率。这意味着可以将占空比设置为 0 到 100% 之间的任意值,分辨率为 ~0.012%(2 ** 13 = 8192 个离散级别的 LED 强度)。这个参数已经满足很多的需求了~所以ESP32完全可以拿来做成品的开发。
LEDC 可用于生成足以为其他设备(例如数码相机模块)计时的更高频率的信号。在这种情况下,最大可用频率为 40 MHz,占空比分辨率为 1 位。这意味着占空比固定为 50% 且无法调整。
如何使用
控制器的API设置流程
定时器配置
通过调用函数ledc_timer_config()
并传递ledc_timer_config_t
包含以下配置设置的数据结构来设置计时器:
速度模式
ledc_mode_t
定时器编号
ledc_timer_t
PWM信号频率
PWM 占空比的分辨率
频率和占空比分辨率是相互依赖的。PWM 频率越高,可用的占空比分辨率越低,反之亦然。
通道配置
设置定时器后,配置所需的通道(其中之一ledc_channel_t
)。这是通过调用函数来完成的ledc_channel_config()
。
与定时器配置类似,通道设置函数应该传递一个ledc_channel_config_t
包含通道配置参数的结构。
此时,通道应开始运行并在选定的 GPIO 上生成 PWM 信号,如 中所配置ledc_channel_config_t
,具有定时器设置中指定的频率和给定的占空比。可以通过调用函数随时暂停通道操作(信号生成)ledc_stop()
。
改变 PWM 信号
一旦通道开始运行并产生具有恒定占空比和频率的 PWM 信号,有几种方法可以改变该信号。驱动 LED 时,主要是改变占空比以改变光强度。
更改 PWM 频率
LEDC API 提供了几种“动态”更改 PWM 频率的方法:
通过调用设置频率
ledc_set_freq()
。有相应的功能ledc_get_freq()
可以查看当前频率。通过调用
ledc_bind_channel_timer()
将其他一些计时器绑定到通道来更改频率和占空比分辨率。通过调用更改频道的计时器
ledc_channel_config()
。
使用软件更改 PWM 占空比
要设置占空比,请使用专用功能ledc_set_duty()
。之后,调用ledc_update_duty()
以激活更改。要检查当前设置的值,请使用相应的_get_
功能ledc_get_duty()
。
使用硬件更改 PWM 占空比
LEDC 硬件提供了从一个占空比值逐渐过渡到另一个值的方法。要使用此功能,请启用淡入淡出,ledc_fade_func_install()
然后通过调用可用的淡入淡出函数之一对其进行配置:
ledc_set_fade_with_time()
ledc_set_fade_with_step()
ledc_set_fade()
最后开始淡入淡出ledc_fade_start()
。
如果不再需要,可以使用 禁用衰落和相关中断ledc_fade_func_uninstall()
。
接下来我们看专业的电机控制器,ESP32 有两个 MCPWM 单元,可用于控制不同类型的电机。每个单元具有三对 PWM 输出:
MCPWM 单元的更详细框图如上所示。每个 A/B 对可由三个定时器定时器 0、1 和 2 中的任何一个提供时钟。同一个定时器可用于为一对以上的 PWM 输出提供时钟。每个单元还能够收集输入,例如检测电机过流或过压,以及获得反馈,例如转子位置。SYNC SIGNALS
FAULT SIGNALS
CAPTURE SIGNALS
我这里先打乱一下叙述的方向,先不说电机,再说一下舵机。舵机组件使用 LEDC 外设产生 PWM 信号,可以实现对最多 16 路舵机的独立控制(ESP32 支持 16 路通道,ESP32-S2 支持 8 路通道),控制频率可选择为 50 ~ 400 Hz。使用该层 API,用户只需要指定舵机所在组、通道和目标角度,即可实现对舵机的角度操作。
这里出现了ESP32-S2.ESP32-S2是在ESP32的基础上进行了一些裁剪和添加。最重要的部分是移除了蓝牙,增加了USB OTG。下面可以看看和以往的芯片有什么区别,我一直以为是S2是升级产品来着。
芯片框图
这是乐鑫首款内置 USB 支持的产品。
S2更快的单核 LX7 和更强大的协处理器以某种方式平衡了双核处理器的缺乏。它还具有较少的 SRAM 和 ROM,但同样支持更大的外部存储器。
但这款新型微控制器的最佳特性是基于 RISC-V 架构的 ULP 协处理器。与 ESP32 中的相比,这应该意味着非常低的功耗和更高的处理能力。ESP32-S2 还能够在不使用时动态关闭 Wi-Fi 收发器以节省电量。即使在 Wi-Fi “开启” ping 路由器的情况下,这也允许低功耗。
数据表讨论了在 1% 占空比时仅为 5uA (!!) 和 24uA 的低功耗模式。这意味着你终于可以拥有一台支持电池供电的 Wi-Fi 设备——这在以前的版本中不太可行(你不想每周更换电池,不是吗?)。
通过新的 ESP32-S2,乐鑫试图填补 ESP8266 和 ESP32 在功能和价格上的差距。ESP32-S2 不是 ESP32 的杀手。相反,它更像是一个 ESP8266 杀手——我希望杀的成功。毕竟8266也有段时间没有被更新了。
https://maker.pro/esp8266/tutorial/a-comparison-of-the-new-esp32-s2-to-the-esp32
在现在使用的舵机内部一般存在一个产生固定周期和脉宽的基准信号,通过与输入 PWM 信号进行比较,获得电压差输出,进而控制电机的转动方向和转动角度。常见的 180 度角旋转舵机一般以 20 ms (50 Hz) 为时钟周期,通过 0.5 ~ 2.5 ms 高电平脉冲,对应控制舵机在 0 ~ 180 度之间转动。
LEDC可用于对控制精度要求较低的场景,例如玩具小车、遥控机器人、家庭自动化等。
使用方法
舵机初始化:使用
servo_init()
对一组通道进行初始化,ESP32 包含LEDC_LOW_SPEED_MODE
和LEDC_HIGH_SPEED_MODE
两组通道,有些芯片可能只支持一组。初始化配置项主要包括最大角度、信号频率、最小输入脉宽和最大输入脉宽,用于计算角度和占空比的对应关系;引脚和通道用于分别指定芯片引脚和 LEDC 通道的对应关系;设置目标角度:使用
servo_write_angle()
通过指定舵机所在组、所在通道和目标角度,对舵机角度进行控制;读取当前角度:可使用
servo_read_angle()
获取舵机当前角度,需要注意的是该角度是根据输入信号进行推算的理论角度;舵机去初始化:当一组多个舵机都不再使用时,可使用
servo_deinit()
对一组通道进行去初始化。
servo_config_t servo_cfg = {
.max_angle = 180,
.min_width_us = 500,
.max_width_us = 2500,
.freq = 50,
.timer_number = LEDC_TIMER_0,
.channels = {
.servo_pin = {
SERVO_CH0_PIN,
SERVO_CH1_PIN,
SERVO_CH2_PIN,
SERVO_CH3_PIN,
SERVO_CH4_PIN,
SERVO_CH5_PIN,
SERVO_CH6_PIN,
SERVO_CH7_PIN,
},
.ch = {
LEDC_CHANNEL_0,
LEDC_CHANNEL_1,
LEDC_CHANNEL_2,
LEDC_CHANNEL_3,
LEDC_CHANNEL_4,
LEDC_CHANNEL_5,
LEDC_CHANNEL_6,
LEDC_CHANNEL_7,
},
},
.channel_number = 8,
} ;
iot_servo_init(LEDC_LOW_SPEED_MODE, &servo_cfg);
float angle = 100.0f;
// Set angle to 100 degree
iot_servo_write_angle(LEDC_LOW_SPEED_MODE, 0, angle);
// Get current angle of servo
iot_servo_read_angle(LEDC_LOW_SPEED_MODE, 0, &angle);
//deinit servo
iot_servo_deinit(LEDC_LOW_SPEED_MODE);
https://github.com/espressif/esp-iot-solution/tree/e4672a5208f97a0e77b4c91568b7fa46e7a73ab5
ESP-IoT-Solution 包含用于开发物联网系统的设备驱动程序和代码框架,作为 ESP-IDF 的额外组件,更容易启动。
ESP-IoT-Solution 包含以下内容:
传感器、显示器、音频、输入、执行器等的设备驱动程序。
低功耗、安全、存储等的框架和文档。
espressif 开源解决方案的实际应用指南。
就是在这里,是乐鑫新的使用框架。
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html#setting-up-development-environment
搭建使用环境在上面的链接里面尝试在Win10系统搭建esp32编译系统.上
我们这次使用到舵机控制代码在这里
头文件
初始化
取消初始化
设置角度
读取角度
/**
* @brief 电机配置
*
*/
typedef struct
{
uint16_t max_angle; /**< Servo最大角度 */
uint16_t min_width_us; /**< 最小角度对应的脉冲宽度,一般为500us */
uint16_t max_width_us; /**< 最大角度对应的脉冲宽度,一般为2500us */
uint32_t freq; /**< PWM 频率 */
ledc_timer_t timer_number; /**< ledc的定时器编号 */
servo_channel_t channels; /**< 要使用的频道 */
uint8_t channel_number; /**< 总通道数 */
} servo_config_t;
/**
* @brief 电机的通道设置
*
*/
typedef struct
{
gpio_num_t servo_pin[LEDC_CHANNEL_MAX]; /**< PWM输出引脚编号 */
ledc_channel_t ch[LEDC_CHANNEL_MAX]; /**< 使用的ledc通道 */
} servo_channel_t;
都是对应的,其实有点索然无味的感觉。。。后面看看实现吧
esp_err_t iot_servo_read_angle(ledc_mode_t speed_mode, uint8_t channel, float *angle)
{
SERVO_CHECK(speed_mode < LEDC_SPEED_MODE_MAX, "LEDC speed mode invalid", ESP_ERR_INVALID_ARG);
SERVO_CHECK(channel < LEDC_CHANNEL_MAX, "LEDC channel number too large", ESP_ERR_INVALID_ARG);
uint32_t duty = ledc_get_duty(speed_mode, channel);
float a = calculate_angle(speed_mode, duty);
*angle = a;
return ESP_OK;
}
自己看吧,我这里先不读代码
static void _set_angle(ledc_mode_t speed_mode, float angle)
{
for (size_t i = 0; i < 8; i++)
{
iot_servo_write_angle(speed_mode, i, angle);
}
}
写个测试的代码来控制一组舵机,突然感觉写到这里就不想写了,下面写点简单的,先来个esp8266的控制
找个可以输出PWM的引脚
#include <Arduino.h>
#include <Servo.h>
Servo servo;
void setup ()
{
servo.attach(14);//PWM引脚设置,与GPIO引脚号对应.
}
void loop ()
{
// To 0°
servo.write(0);
delay(1000);
// To 90°
servo.write(90);
delay(1000);
// To 180°
servo.write(180);
delay(1000);
}
这不白给
ESP32用网页控制会高大上一些
#include <WiFi.h>
#include "Servo.h"
Servo myservo;
static const int servoPin = 13;
const char *ssid = "";
const char *password = "";
// 端口80
WiFiServer server(80);
// 存储HTTP请求的变量
String header;
// 解码 HTTP GET 值
String valueString = String(5);
int pos1 = 0;
int pos2 = 0;
void setup()
{
Serial.begin(9600);
delay(1000);
myservo.attach(servoPin);
Serial.print("连接到 ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
// Print local IP address and start web server
Serial.println("");
Serial.println("WiFi 已连接.");
Serial.println("IP 地址: ");
Serial.println(WiFi.localIP());
server.begin();
}
void loop()
{
WiFiClient client = server.available(); // 监听客户端
if (client)
{ // 如果有新客户端连接
Serial.println("新Client."); // 打印新连接
String currentLine = ""; // 这个串来保存客户端传入的数据
while (client.connected())
{
if (client.available())
{ // 如果有字节从客户端读取
char c = client.read(); // 读取一个字节,然后
Serial.write(c); // 打印一下
header += c;
if (c == '\n')
{ // 如果字节是换行符
// 如果当前行为空,则一行中有两个换行符
// 这是客户端HTTP请求的借宿,因此发送响应
if (currentLine.length() == 0)
{
// HTTP标头总是以响应代码开头 (如 HTTP/1.1 200 OK)
// 和一个内容的类型,以便客户端知道接下来要发生啥事,+一个空行
client.println("HTTP/1.1 200 OK");
client.println("内容-类型:text/html");
client.println("连接: 关闭");
client.println();
// 显示网页
client.println("<!DOCTYPE html><html>");
client.println("<head><meta name=" viewport " content=" width = device - width, initial - scale = 1 ">");
client.println("<link rel=" icon " href=" data:, ">");
// CSS 来设置开/关按钮的样式
// 随意更改背景颜色的字体大小什么的,自己瞎鸡儿搞搞
client.println("<style>body { text-align: center; font-family: " Trebuchet MS ", Arial; margin-left:auto; margin-right:auto;}");
client.println(".slider { width: 300px; }</style>");
client.println("<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>");
// Web Page
client.println("</head><body><h1>ESP32 with Servo</h1>");
client.println("<p>Position: <span id="servoPos"></span></p>");
client.println("<input type="range" min="0" max="180" class="slider" id="servoSlider" onchange="servo(this.value)" value="" + valueString + ""/>");
client.println("<script>var slider = document.getElementById("servoSlider");");
client.println("var servoP = document.getElementById("servoPos"); servoP.innerHTML = slider.value;");
client.println("slider.oninput = function() { slider.value = this.value; servoP.innerHTML = this.value; }");
client.println("$.ajaxSetup({timeout:1000}); function servo(pos) { ");
client.println("$.get("/?value=" + pos + "&"); {Connection: close};}</script>");
client.println("</body></html>");
//GET /?value=180& HTTP/1.1
if (header.indexOf("?value=") >= 0) {
pos1 = header.indexOf('=');
pos2 = header.indexOf('&');
valueString = header.substring(pos1 + 1, pos2);
//旋转servo
myservo.write(valueString.toInt());
Serial.println(valueString);
}
// HTTP响应以另一个空行结束
client.println();
// 跳出循环
break;
}
else
{ // 如果有换行符,就清除当前行
currentLine = "";
}
}
else if (c != '\r')
{ // 除回车外还有别的东西
currentLine += c; //加到当前行的尾部
}
}
}
// 清除头变量
header = "";
// 关闭连接
client.stop();
Serial.println("客户端断开连接.");
Serial.println("");
}
}
主要逻辑就是这些,不会网页的自己去看看
然会就是这个舵机的库,常规的库使用不了,你得使用esp32的专用库
另外TT如果驱动舵机也是得换库才可以,因为底层实现不一样
https://blog.csdn.net/m0_46139849/article/details/107295765?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-9.baidujs&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-9.baidujs
https://oshwhub.com/eedadada/esp32-bakery
https://maker.pro/esp8266/projects/joystick-esp8266-mpu6050
https://www.cnblogs.com/kerwincui/tag/esp32/
https://blog.csdn.net/qq_40500005/article/details/113966067
另外有很多优秀的资料去参考,附上链接
另外使用IDF进行舵机控制的测试代码也附上:
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "iot_servo.h"
#include "unity.h"
#include "sdkconfig.h"
#ifdef CONFIG_IDF_TARGET_ESP32
#define SERVO_CH0_PIN 32
#define SERVO_CH1_PIN 25
#define SERVO_CH2_PIN 26
#define SERVO_CH3_PIN 27
#define SERVO_CH4_PIN 14
#define SERVO_CH5_PIN 12
#define SERVO_CH6_PIN 13
#define SERVO_CH7_PIN 15
#define SERVO_CH8_PIN 2
#define SERVO_CH9_PIN 0
#define SERVO_CH10_PIN 4
#define SERVO_CH11_PIN 5
#define SERVO_CH12_PIN 18
#define SERVO_CH13_PIN 19
#define SERVO_CH14_PIN 21
#define SERVO_CH15_PIN 22
#elif CONFIG_IDF_TARGET_ESP32S2
#define SERVO_CH0_PIN 1
#define SERVO_CH1_PIN 2
#define SERVO_CH2_PIN 3
#define SERVO_CH3_PIN 4
#define SERVO_CH4_PIN 5
#define SERVO_CH5_PIN 6
#define SERVO_CH6_PIN 7
#define SERVO_CH7_PIN 8
#endif
static void _set_angle(ledc_mode_t speed_mode, float angle)
{
for (size_t i = 0; i < 8; i++)
{
iot_servo_write_angle(speed_mode, i, angle);
}
}
TEST_CASE("Servo_motor test", "[servo][iot]")
{
servo_config_t servo_cfg_ls = {
.max_angle = 180,
.min_width_us = 500,
.max_width_us = 2500,
.freq = 50,
.timer_number = LEDC_TIMER_0,
.channels = {
.servo_pin = {
SERVO_CH0_PIN,
SERVO_CH1_PIN,
SERVO_CH2_PIN,
SERVO_CH3_PIN,
SERVO_CH4_PIN,
SERVO_CH5_PIN,
SERVO_CH6_PIN,
SERVO_CH7_PIN,
},
.ch = {
LEDC_CHANNEL_0,
LEDC_CHANNEL_1,
LEDC_CHANNEL_2,
LEDC_CHANNEL_3,
LEDC_CHANNEL_4,
LEDC_CHANNEL_5,
LEDC_CHANNEL_6,
LEDC_CHANNEL_7,
},
},
.channel_number = 8,
};
TEST_ASSERT(ESP_OK == iot_servo_init(LEDC_LOW_SPEED_MODE, &servo_cfg_ls));
/**
* Only ESP32 has the high speed mode
*/
#ifdef CONFIG_IDF_TARGET_ESP32
servo_config_t servo_cfg_hs = {
.max_angle = 180,
.min_width_us = 500,
.max_width_us = 2500,
.freq = 100,
.timer_number = LEDC_TIMER_0,
.channels = {
.servo_pin = {
SERVO_CH8_PIN,
SERVO_CH9_PIN,
SERVO_CH10_PIN,
SERVO_CH11_PIN,
SERVO_CH12_PIN,
SERVO_CH13_PIN,
SERVO_CH14_PIN,
SERVO_CH15_PIN,
},
.ch = {
LEDC_CHANNEL_0,
LEDC_CHANNEL_1,
LEDC_CHANNEL_2,
LEDC_CHANNEL_3,
LEDC_CHANNEL_4,
LEDC_CHANNEL_5,
LEDC_CHANNEL_6,
LEDC_CHANNEL_7,
},
},
.channel_number = 8,
};
TEST_ASSERT(ESP_OK == iot_servo_init(LEDC_HIGH_SPEED_MODE, &servo_cfg_hs));
#endif
size_t i;
float angle_ls, angle_hs;
for (i = 0; i <= 180; i++)
{
_set_angle(LEDC_LOW_SPEED_MODE, i);
#ifdef CONFIG_IDF_TARGET_ESP32
_set_angle(LEDC_HIGH_SPEED_MODE, (180 - i));
#endif
vTaskDelay(50 / portTICK_PERIOD_MS);
iot_servo_read_angle(LEDC_LOW_SPEED_MODE, 0, &angle_ls);
#ifdef CONFIG_IDF_TARGET_ESP32
iot_servo_read_angle(LEDC_HIGH_SPEED_MODE, 0, &angle_hs);
#endif
#ifdef CONFIG_IDF_TARGET_ESP32
ESP_LOGI("servo", "[%d|%.2f], [%d|%.2f]", i, angle_ls, (180 - i), angle_hs);
#else
ESP_LOGI("servo", "[%d|%.2f]", i, angle_ls);
(void)angle_hs;
#endif
}
iot_servo_deinit(LEDC_LOW_SPEED_MODE);
#ifdef CONFIG_IDF_TARGET_ESP32
iot_servo_deinit(LEDC_HIGH_SPEED_MODE);
#endif
}