说明:1、由于写函数参数浪费时间并且没有说明只有参数意义不大,所以在此函数一般只以函数名出现。
2、esp32有两个核心,编号为0和1,如果启动了wifi和蓝牙,则会默认将wifi和蓝牙运行在编号为0的核心上。
3、esp32adc2的引脚尽量不使用,因为wifi会用到。esp32引脚图和硬件资源如下所示。
硬件资源如下,其中I2C、I2S、UART等外设可以被定义到任意管脚上。
• 34 个 GPIO 口
• 12-bit SAR ADC,多达 18 个通道
• 2 个 8-bit D/A 转换器
• 10 个触摸传感器
• 4 个 SPI
• 2 个 I²S
• 2 个 I²C
• 3 个 UART
• 1 个 Host SD/eMMC/SDIO
• 1 个 Slave SDIO/SPI
• 带有专用 DMA 的以太网 MAC 接口,支持 IEEE 1588
• 双线汽车接口(TWAI®,兼容 ISO11898-1)
• IR (TX/RX)
• 电机 PWM
• LED PWM,多达 16 个通道
• 霍尔传感器
看门狗wdt
特别注意这个看门狗,在esp32中有两种看门狗,一种是中断看门狗(IWDT),一种是任务看门狗(TWDT)。下面分别介绍:
1、中断看门狗:当中断函数运行超过800ms时,中断看门狗会重启esp32;
2、esp32有两个CPU,在每一个CPU上,都有一个预先创建的任务即空闲任务,这个任务的优先级非常低(为0),并且它的作用有如下几个:
- 当esp32处于空闲状态时,自动降低功耗,增加续航时间;
- 当自己CPU下有任务被删除时,主动清理相关内存资源;
- 重置看门狗定时器
默认情况下,esp32为0号CPU上的空闲任务添加了看门狗监管逻辑,如果CPU0上的空闲任务,在5秒钟之内获取不到CPU资源,没有机会运行,它将触发看门狗重启机制,导致esp32重启。
特别注意:
1、对于任务看门狗只要任务在5秒内让出cpu资源就可以,比如使用串口打印或者延时,串口打印是io操作不占用cpu所以可以让出cpu资源。
2、cpu内核1上有空闲任务,但是这个内核上默认并没有启动任务看门狗。
3、setup和loop函数是被一个运行在内核1上的任务调用,并且它的优先级是1。如果启动内核1上的看门狗,即使运行在内核1上的函数让出cpu资源也会重启,这是因为会让出cpu后,cpu马上会去执行loop函数导致空闲任务无法被执行,可以在loop函数中加一个延时就解决了。
disableCore0WDT(); 禁用内核0上的任务看门狗,直接写在setup函数中就可以。
enableCore0WDT(); 启用内核0上的任务看门狗
disableCore1WDT();
enableCore1WDT();
esp_task_wdt_add(NULL); 申请看门狗看护当前任务。
esp_task_wdt_reset();看门狗喂狗操作即重置看门狗定时器。
延时函数
说明:在多任务场景下,调用下面的两个函数,当前任务将会让出CPU资源,其他任务可以借机获取CPU资源
delay(uint32_t ms);
delayMicroseconds(uint32_t us);
led control/pwm
特别注意只有34到39引脚不支持pwm,因为它们只支持输入模式
analogWrite(uint8_t pin, int value);用于设置占空比,位宽为8位,即value取值范围为0到255,在使用之前可能需要配置gpio输出模式
ledcWrite,用于设置占空比,一般在ledcAttach函数之后使用,比如如果ledcAttachs设置位 宽为10即电压0到3.3v对应0到1023,共1024个数,此时如果ledcWrite(1,50)则表示占空 比为50/1024
ledcRead
ledcAttach
gpio
pinMode,如果引脚只支持输入模式则不能配置成输出模式
digitalWrite(uint8_t pin, uint8_t val);在输入模式下不能使用此函数,val值有LOW和HIGH
digitalRead,无论引脚配置成输入还是输出都可以使用这个函数
中断相关函数:
attachInterrupt
attachInterruptArg
detachInterrupt
dac
dac也可以实现呼吸灯效果,它与pwm波不同,dac是直接能输出一个0v到3.3v,而pwm是方波它是通过调整占空比来实现的呼吸灯。
使用dac时,不需要再配置引脚模式,直接用dacWrite函数就可以。
dacWrite,用来设置引脚dac的值,值范围为0到255,对应的电压是0v到3.3v
adc
1、esp32上有18个12位的adc输入通道,特别注意在使用wifi时不能使用adc2的引脚。
2、使用adc时要特别注意,使用的引脚必须是支持adc转换的引脚,不支持adc的引脚使用下面函数会报错。
3、特别注意,esp32内置的adc不够准,所以在adc精度有要求的场景下很多人都会选择外置adc芯片。
analogRead
串口
硬件串口
esp32能直接使用的有两个串口,一个是串口0,另一个是串口2,它们分别对应代码中的类Serial和Serial2。还有一个串口是串口1,由于它连接的到模组中的flash,所以不能直接使用,可以重映射到其他引脚上。
Serial.begin(115200);设置串口波特率为115200
Serial.readStringUntil('\n');读取字符串,读到\n结束。
Serial.println,打印一行数据,1个参数,可以填数字包括整数和浮点数,也可以填字符串。
Serial.printf,与c语言中的printf使用一样。
重映射串口1
需要引入头文件HardwareSerial.h,创建对象HardwareSerial mySerial(1);这里参数填1表示创建一个串口1的对象。
mySerial.begin(9600,SERIAL_8N1,18,19);SERIAL_8N1表示8位数据位无校验位1位停止位,18和19表示重映射的引脚。之后就可以正常使用串口1了。
软件串口
软件串口不如硬件串口好,因为它是通过软件模拟出来的串口,不过当硬件资源不够时可以考虑使用软件串口,使用时需要引入一个第三方库,详细步骤请看b站课程。
wifi(wifi有两种模式,一种是sta模式(客户端模式),一种是ap模式(热点模式)
支持sta和ap同时打开。使用时需要引入WiFi.h头文件
sta模式:
WiFi.begin(ssid, password);
WiFi.begin,它能配置是否自动连接wifi
WiFi.reconnect
WiFi.disconnect
WiFi.isConnected,返回连接状态,如果连接成功则返回true,否则返回false
WiFi.setAutoReconnect
WiFi.getAutoReconnect
ap模式:
WiFi.softAP(ssid, password);密码可以设成空即NULL,如果设置密码则密码不能少于8位。
WiFi.softAP,它能配置最大连接数和wifi通道。
WiFi.softAPgetStationNum,返回当前连接的数目
WiFi.softAPBroadcastIP,返回ipv4地址
WiFi.softAPenableIPv6(bool enable=true);使能ipv6的支持,如果配置成功则返回真
WiFi.softAPlinkLocalIPv6,返回ipv6地址
WiFi.softAPgetHostname();返回热点名称
WiFi.softAPsetHostname(const char * hostname);设置热点名称
WiFi.softAPSSID,返回热点ssid
WiFi.softAPmacAddress(void);返回mac地址,返回值类型为String
WiFiManager(wifi库,它需要下载的)
这个库非常好用能够自动连接,当连接成功后它会自动将WiFi账号和密码保存到本地用于下次连接,如果连接不成功它会启动一个热点,手机连上这个热点就能配置联网。以后用WiFi外设可以用这个库和上面的wifi函数结合使用。-*
使用这个库时需要引入WiFiManager.h的头文件。然后在setup函数中写入以下两行代码即可。
WiFiManager manager;
manager.autoConnect("连接不成功时启动热点的名称");
如果想清除存储器中存储的wifi信息再连接wifi可以使用下面代码
WiFiManager manager;
manager.resetSettings();
manager.autoConnect("连接不成功时启动热点的名称");
esp-now
1、需要引入esp_now.h头文件。最大传输有效载荷250个字节,使用esp—now,每块开发板既可以是发送方也可以是接收方。
2、注意:在esp-now中一个设备既可以发送数据友可以接受数据,并且它又不像iic和spi一样有scl引脚发送时钟信号(产生时钟信号的设备为主机,另一个为从机),所以esp-now本身并没有从机和主机概念,但是这里为了好描述,将发送数据的一端称为主机,将接受数据的一端称为从机。
3、通信流程:如果a设备和b设备要通信。首先,需要让a向mac地址为FF.FF.FF.FF.FF.FF的设备进行广播(即向同一网段内的所有主机进行广播)直到b设备向a设备发送应答信号后才停止广播,此时b会接受到广播,然后让b记录下a的mac地址然后将a设备注册并向a设备发送应答数据,这里数据定义为了hehe,a设备接受到b设备的应答后,记录下b的mac地址并将b设备注册。完成了以上步骤就算配对完了,a设备能向b设备发送数据,a也能接受b的数据,同理b也是如此。下面代码实现了这个过程,可以直接使用。
ESP_NOW.begin(const uint8_t *pmk = NULL);初始化esp-now,在使用其他esp-now函数之前必须要调用它,如果初始化成功则返回true。特别注意使用这个函数之前应该先初始化wifi
ESP_NOW.end();结束通信。
ESP_NOW.getTotalPeerCount(); 获取配对设备数量
ESP_NOW.getEncryptedPeerCount();获取加密配对设备数量
ESP_NOW.onNewPeer(接受数据回调函数, 回调函数参数);回调函数格式为
void cb(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg);
ESP-NOW Peer,类名称,它是一个抽象类,代表了一个配对设备,要使用它中的add、remove、send成员函数必须由一个类继承它,因为它的这些成员函数在protected关键字中。
a设备代码,可能的错误:hehe数据是否为4位,代码未验证。
#include "ESP32_NOW.h"
#include "WiFi.h"
#include <esp_mac.h>
#include <vector>
// 可以修改
#define ESPNOW_WIFI_CHANNEL 6
class ESP_NOW_Broadcast_Peer : public ESP_NOW_Peer {
public:
ESP_NOW_Broadcast_Peer(uint8_t channel, wifi_interface_t iface, const uint8_t *lmk): ESP_NOW_Peer(ESP_NOW.BROADCAST_ADDR, channel, iface, lmk) {}
~ESP_NOW_Broadcast_Peer() {
remove();
}
bool begin() {
if (!ESP_NOW.begin() || !add()) {
return false;
}
return true;
}
bool send_message(const uint8_t *data, size_t len) {
if (!send(data, len)) {
return false;
}
return true;
}
};
class ESP_NOW_Peer_Class : public ESP_NOW_Peer {
public:
ESP_NOW_Peer_Class(const uint8_t *mac_addr, uint8_t channel, wifi_interface_t iface, const uint8_t *lmk) : ESP_NOW_Peer(mac_addr, channel, iface, lmk) {}
~ESP_NOW_Peer_Class() {}
bool add_peer() {
if (!add()) {
return false;
}
return true;
}
bool send_message(const uint8_t *data, size_t len) {
if (!send(data, len)) {
return false;
}
return true;
}
void onReceive(const uint8_t *data, size_t len, bool broadcast) {
// 可以修改
// 这里的通讯模式有广播和单播,MACSTR是让mac地址格式化输出的。
Serial.printf("发送端的mac地址及通讯模式" MACSTR " (%s)\n", MAC2STR(addr()), broadcast ? "broadcast" : "unicast");
Serial.printf("数据: %s\n", (char *)data);
}
};
int PEIDUI_SUCCESS=0;
ESP_NOW_Broadcast_Peer broadcast_peer(ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL);
std::vector<ESP_NOW_Peer_Class> masters;
std::vector<uint8_t> mac_addr;
void register_new_master(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) {
// 判断数据是通过广播还是单播传过来的,判断发送端mac地址之前是否发过数据
if (memcmp(info->des_addr, ESP_NOW.BROADCAST_ADDR, 6) == 0 && std::count(mac_addr.begin(),mac_addr.end(),*(info->src_addr))==0) {
// 发送端第一次广播来的数据
Serial.printf("Unknown peer " MACSTR " sent a broadcast message\n", MAC2STR(info->src_addr));
ESP_NOW_Peer_Class new_master(info->src_addr, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL);
// 将新设备添加到vector容器
masters.push_back(new_master);
mac_addr.push_back(*(info->src_addr));
if(data[0]=='h'&&data[1]=='e'&&data[2]=='h'&&data[3]=='e')
PEIDUI_SUCCESS=1;
if (!masters.back().add_peer()) {
// 可以修改
Serial.println("注册设备失败");
return;
}
} else {
// 可以修改
// 单播来的数据
log_v("Received a unicast message from " MACSTR, MAC2STR(info->src_addr));
}
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.setChannel(ESPNOW_WIFI_CHANNEL);
while (!WiFi.STA.started()) {
delay(100);
}
// Register the broadcast peer
if (!broadcast_peer.begin()) {
//注册设备失败进行重启
delay(2000);
ESP.restart();
}
int PEIDUI_SUCCESS =0;
ESP_NOW.onNewPeer(register_new_master, NULL);
while(!PEIDUI_SUCCESS)
{
char data[32]="hehe";
broadcast_peer.send_message((uint8_t *)data, sizeof(data));
}
}
void loop() {
delay(2000);
}
b设备代码
#include "ESP32_NOW.h"
#include "WiFi.h"
#include <esp_mac.h>
#include <vector>
// 可以修改
#define ESPNOW_WIFI_CHANNEL 6
class ESP_NOW_Peer_Class : public ESP_NOW_Peer {
public:
ESP_NOW_Peer_Class(const uint8_t *mac_addr, uint8_t channel, wifi_interface_t iface, const uint8_t *lmk) : ESP_NOW_Peer(mac_addr, channel, iface, lmk) {}
~ESP_NOW_Peer_Class() {}
bool add_peer() {
if (!add()) {
return false;
}
return true;
}
bool send_message(const uint8_t *data, size_t len) {
if (!send(data, len)) {
return false;
}
return true;
}
void onReceive(const uint8_t *data, size_t len, bool broadcast) {
// 可以修改
// 这里的通讯模式有广播和单播,MACSTR是让mac地址格式化输出的。
Serial.printf("发送端的mac地址及通讯模式" MACSTR " (%s)\n", MAC2STR(addr()), broadcast ? "broadcast" : "unicast");
Serial.printf("数据: %s\n", (char *)data);
}
};
std::vector<ESP_NOW_Peer_Class> masters;
std::vector<uint8_t> mac_addr;
void register_new_master(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) {
// 判断数据是通过广播还是单播传过来的,判断发送端mac地址之前是否发过数据
if (memcmp(info->des_addr, ESP_NOW.BROADCAST_ADDR, 6) == 0 && std::count(mac_addr.begin(),mac_addr.end(),*(info->src_addr))==0) {
// 发送端第一次广播来的数据
Serial.printf("Unknown peer " MACSTR " sent a broadcast message\n", MAC2STR(info->src_addr));
ESP_NOW_Peer_Class new_master(info->src_addr, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA, NULL);
// 将新设备添加到vector容器
masters.push_back(new_master);
mac_addr.push_back(*(info->src_addr));
const uint8_t* da="hehe";
new_master.send_message(da,4);
if (!masters.back().add_peer()) {
// 可以修改
Serial.println("注册设备失败");
return;
}
} else {
// 可以修改
// 单播来的数据
log_v("Received a unicast message from " MACSTR, MAC2STR(info->src_addr));
}
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.setChannel(ESPNOW_WIFI_CHANNEL);
while (!WiFi.STA.started()) {
delay(100);
}
if (!ESP_NOW.begin()) {
delay(2000);
ESP.restart();
}
ESP_NOW.onNewPeer(register_new_master, NULL);
}
void loop() {
delay(1000);
}
freertos
在esp32中有时间片的概念即cpu执行一个时间片的时间就会中断当前任务换其他任务执行,而默认一个时间片的时间是1tick,由宏定义portTICK_PERIOD_MS决定,默认1tick为1ms。
xTaskCreate,创建任务,格式为xTaskCreate(函数指针,任务名,栈空间大小,优先级,函数参数,句柄对象),说明第一个参数是函数的名字,它的返回值必须是void,参数必须是void*;栈空间大小一般设为2024或4048即2k或4k;优先级是0到24,数字越大优先级越高,其中空闲任务的优先级为0;句柄对象一般填NULL。比如xTaskCreate(print_hehe,"task1",2024,1,(void*)"hehe",NULL),虽然这种方式能创建任务,但是不够灵活,比如不能对任务进行像修改优先级这样的操作。
xTaskCreatePinnedToCore,创建任务并指定在哪个核心上运行。格式为:xTaskCreatePinnedToCore(函数指针,任务名,栈空间大小,优先级,函数参数,句柄对象,cpu内核编号0或1)
xPortGetCoreID();返回当前任务的cpu内核编号。
vTaskDelay,延迟作用,调用该函数的任务将让出CPU资源,其他任务可以借机获取CPU资源,单位是tick,其中每个tick占用的ms数,由宏定义portTICK_PERIOD_MS决定。
vTaskDelete,删除任务,一般在xTaskCreate的第一个参数的那个函数中的最后使用,vTaskDelete(NULL),表示任务结束则删除此任务
pcTaskGetName,获取任务名称,如果参数为NULL表示获得当前任务名称
uxTaskPriorityGet,获取任务优先级,如果参数为NULL表示获得当前任务优先级
代码举例:
1、动态创建任务的例子
#include <Arduino.h>
#define LED1 23 // 控制第一颗LED灯的引脚
#define LED2 22 // 控制第二颗LED灯的引脚
void handle_led1(void *ptr)
{
pinMode(LED1, OUTPUT);
char *param = (char *)ptr;
Serial.print(param);
Serial.println(" started");
while (1)
{
digitalWrite(LED1, HIGH);
vTaskDelay(1000);
digitalWrite(LED1, LOW);
vTaskDelay(1000);
}
vTaskDelete(NULL);
}
void handle_led2(void *ptr)
{
pinMode(LED2, OUTPUT);
char *param = (char *)ptr;
Serial.print(param);
Serial.println(" started");
while (1)
{
digitalWrite(LED2, HIGH);
vTaskDelay(3000);
digitalWrite(LED2, LOW);
vTaskDelay(3000);
}
vTaskDelete(NULL);
}
void setup()
{
Serial.begin(115200);
xTaskCreate(handle_led1, "task1", 2048, (void *)"led1", 1, NULL);
xTaskCreate(handle_led2, "task2", 2048, (void *)"led2", 1, NULL);
}
void loop()
{
}
2、比较灵活的动态创建任务的例子
#include <Arduino.h>
#define LED1 23 // 控制第一颗LED灯的引脚
#define LED2 22 // 控制第二颗LED灯的引脚
TaskHandle_t task1;
TaskHandle_t task2;
void handle_led1(void *ptr)
{
pinMode(LED1, OUTPUT);
char *param = (char *)ptr;
Serial.print(param);
Serial.println(" started");
while (1)
{
digitalWrite(LED1, HIGH);
vTaskDelay(1000);
digitalWrite(LED1, LOW);
vTaskDelay(1000);
}
vTaskDelete(NULL);
}
void handle_led2(void *ptr)
{
pinMode(LED2, OUTPUT);
char *param = (char *)ptr;
Serial.print(param);
Serial.println(" started");
while (1)
{
digitalWrite(LED2, HIGH);
vTaskDelay(3000);
digitalWrite(LED2, LOW);
vTaskDelay(3000);
}
vTaskDelete(NULL);
}
void setup()
{
Serial.begin(115200);
xTaskCreate(handle_led1, "task1", 2048, (void *)"led1", 1, &task1);
xTaskCreate(handle_led2, "task2", 2048, (void *)"led2", 1, &task2);
//vTaskDelete(task1);
//vTaskDelete(task2);
}
void loop()
{
}
freertos定时器
定时器有很多应用,比如每30s将数据上传一次服务器。
创建定时器:
TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction )该函数用于创建一个定时器,一共有如下几个参数:
- pcTimerName:定时器的名称,类型为一个字符串
- xTimerPeriodInTicks:定时器每隔多长时间执行一次,单位为tick,要将时间转换成tick,开始将时间除以portTICK_PERIOD_MS得到
- uxAutoReload:该定时器是否需要重复触发,为pdTRUE表示重复触发,为pdFALSE表示只触发一次
- pvTimerID:定时器的标识符,一般不用,除非在多个定时器绑定相同逻辑函数时,才会使用该参数来区分是哪一个定时器触发了逻辑函数;
- pxCallbackFunction:定时器触发后,需要执行的逻辑函数,返回值为void,只有一个参数,类型为void*
函数的返回值为定时器对象,类型为TimerHandle_t
启动定时器
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
该函数用于启动一个定时器,有两个参数:
- xTimer:第一个参数为要启动的定时器的句柄;
- xTicksToWait:第二个参数为等待时间,表示如果任务队列已满,该函数需要等待的时间,如果设置为0,表示不需要等待
- 返回值为pdPASS表示定时器启动成功,返回值为pdFAIL表示定时器启动失败
停止定时器
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
该函数用于启动一个定时器,有两个参数:
- xTimer:第一个参数为要启动的定时器的句柄;
- xTicksToWait:第二个参数为等待时间,表示如果任务队列已满,该函数需要等待的时间,如果设置为0,表示不需要等待
- 返回值为pdPASS表示定时器启动成功,返回值为pdFAIL表示定时器启动失败
freertos线程间通信、
I2C
使用时要包含Wire.h的头文件
Wire.begin();做主机,默认使用21和22引脚,频率为100KHZ
Wire.begin(sda引脚,scl引脚);做主机,频率为100KHZ
Wire.begin(sda引脚,scl引脚,200000);做主机,频率为200KHZ
Wire.begin(-1,-1,200000);做主机,使用m默认21和22引脚频率为200KHZ
Wire.begin(27);做从机,地址是27,默认使用21和22引脚,频率由主机定
Wire.begin(从机地址,sda引脚,scl引脚);做从机,频率由主机定
Wire.beginTransmission(从机地址);开始传输,值得注意的是这个函数的调用并不会产生 Start 信号 和发送 Slave Address,仅是实现通知 Arduino后面要启动 Master write to Slave 操作。beginTransmission 函数调用后,再调用 write 函数。
Wire.write(val);该函数不直接写入从器件,而是添加到 I2C 缓冲区,因此需要在此函数之后调用Wire.endTransmission(true)h函数,Wire.write(val)必须在Wire.beginTransmission和Wire.endTransmission(true)之间使用。这个函数有3个重载函数,参数可以是uint8_t类型 或字节数据类型即const uint8_t*,size_t 或字符串类型。
Wire.endTransmission(true);存储在 I2C buffer 中的数据将被传输到从机设备。参数如果为true则释放总线,false则不释放总线,参数不写表示会运行重载函数默认也是true。返回值为0表示成功,为1表示数据过长超出缓冲区,为2在地址发送时接受到NACK信号,为3在数据发送时接受到NACk信号,为4其他错误,为5超时错误。通过Wire.beginTransmission和Wire.endTransmission函数可以判断总线上是否有设备。
Wire.requestFrom(address, size);主机向从机读取数据请求并将数据放到缓冲区,第一个参数是从机地址,第二个参数是欲读取数据大小以字节为单位,但是从机不一定会发送size个数据。返回值是收到从机发来的数据量大小。根据返回值可以知道有多少数据要从缓冲区读取。
Wire.readBytes(uint8_t* buffer,size_t length);一次从缓冲区读取length个字节放到buffer数组中。
可以通过以下代码查找设备。
Wire.beginTransmission(address);
uint8_t a= Wire.endTransmission();
if(a==0)
{
总线上存在地址为address的设备
}
I2s
蓝牙
蓝牙设备使用的地址是mac地址,共6个字节。蓝牙的一些应用场景见下图。
bt classic
spp服务
esp32实现spp服务的是BluetoothSerial类,spp也叫蓝牙串口,简单来说就是让蓝牙向串口一样通信。BluetoothSerial类有两种角色,一种是主设备,一种是从设备,对于主设备,理论上一台主设备最多可以连接7台从设备,但是要注意好像esp32设置了一台主机最多连接4台从设备,对于从设备,一台从设备只能连接一台主设备,并且它也不能搜索主设备,只能被主设备搜索。esp32默认不开启ssp认证,所以默认蓝牙不需要密码就能配对。一般esp32做从机,手机或电脑做主机。
要使用spp功能,需要引入头文件BluetoothSerial.h
先要创建一个BluetoothSerial类的对象,比如BluetoothSerial BTSerial;
常用函数
BTSerial.beigin("蓝牙的名字",是否作为主机);初始化蓝牙,第2个参数如果蓝牙作为主机则填true,否则false,默认为false,即默认做从机。
BTSerial.available();判断蓝牙有多少个数据可以读,返回值为int。
BTSerial.write(const uint8_t *buffer,size_t size);最多发送size字节。
BTSerial.readBytes(char *buffer,size_t length);最多读取length个字节,如果缓冲区数据超出了这个长度则超出部分不读了,如果缓冲区中的数据不到这个长度则没影响,因为这个函数的返回值为读到的数据大小,返回值类型为size_t。
BTSerial.onData(回调函数名称);注册接收回调函数,回调函数有两个参数,比如recvData(const uint8_t *buffer,size_t size)。需要在begin之前运行。
BTSerial.disconnect();关闭当前的spp连接。
BTSerial.end();关闭蓝牙功能。
BTSerial.hasClient();判断是否有设备连接,返回bool值。
BTSerial.isClosed();判断spp连接是否已经关闭,返回bool值。
不太常用的函数
BTSerial.unpairDevice(uint8_t remoteAddress[]);解除指定地址的蓝牙设备的配对。
BTSerial.read();不带参数,返回值是int类型,返回读取的缓冲区的第一个字节,如果错误则返回-1。
BTSerial.peek();和read()类似,不同的是读完数据后不在缓存区中删除读完的数据,而read()会删除。
BTSerial.write(uint8_t c);发送一个字节。
ssp认证
下面的函数都要在BTSerial.begin运行之前运行。
BTSerial.enableSSP(); 启用SSP认证,配对的时候主机会产生一个识别码发送给从机,我们从机需要手动确认,然后主机也得确认。
BTSerial.onConfirmRequest(认证请求回调函数);注册从机将识别码发送给主机后触发的回调函数,认证请求回调函数格式为confimRequest(uint32_t val),其中参数为从机发来的识别码。
BTSerial.confirmReply(是否同意连接);参数为bool值,true表示同意,false表示不同意。常在认证回调函数中使用。
BTSerial.onAuthComplete(认证结果回调函数);注册认证结果回调函数,回调函数格式为btAuthcomplete(boolean success),如果认证成功success为true,否则为false。
经典蓝牙设备搜索
有阻塞式搜索和非阻塞式搜索之分。在使用搜索设备之前要调用BTSerial.beigin将esp32设置为主机。
BTSerial.discover(搜索时间);阻塞式搜索,搜索时间单位为ms,返回值类型为BTScanResults*,它会将搜索到的蓝牙设备保存在蓝牙列表中,所以在下次搜索之前应当清除蓝牙列表中的数据。BTScanResults中有2个常用函数,见下面。
BTScanResults中的函数:
getCout();获取搜索蓝牙的个数。
BTAdvertisedDevice* getDevice(int i);获取蓝牙列表中的第i个设备对象。这个函数返回值中也有几个函数,比如返回值的句柄名字为dev,dev->getName()表示获取设备名称。dev->getRSSI()表示蓝牙强度。dev->getCOD()获取蓝牙cod信息,COD(class of device)是蓝牙设备的类型信息,在搜寻和连接蓝牙设备时,不同的设备类型会显示不同的图标,比如蓝牙键盘、手柄等。dev->getAddress()获取蓝牙mac地址。
BTSerial.discoverClear();清空蓝牙列表的搜索结果。
ble
相对于经典蓝牙,功耗降低了90%。主机可以发起从机的连接,例如手机常作为ble的主机,从机只能等待主机的连接。同一个ble设备既可以作为主机又可以作为从机。
SDMMC
webserver
可以有空再学
分区表与ota
可以有空再学
以下代码是官方示例代码,用来ota升级用的,热点登录密码可以自己修改,默认为test123456。
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>
#include <Ticker.h>
#include "html.h"
#define SSID_FORMAT "ESP32-%06lX" // 12 chars total
#define PASSWORD "test123456" // generate if remarked
WebServer server(80);
Ticker tkSecond;
uint8_t otaDone = 0;
const char *alphanum = "0123456789!@#$%^&*abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
String generatePass(uint8_t str_len) {
String buff;
for (int i = 0; i < str_len; i++) {
buff += alphanum[random(strlen(alphanum) - 1)];
}
return buff;
}
void apMode() {
char ssid[13];
char passwd[11];
long unsigned int espmac = ESP.getEfuseMac() >> 24;
snprintf(ssid, 13, SSID_FORMAT, espmac);
#ifdef PASSWORD
snprintf(passwd, 11, PASSWORD);
#else
snprintf(passwd, 11, generatePass(10).c_str());
#endif
WiFi.mode(WIFI_AP);
WiFi.softAP(ssid, passwd); // Set up the SoftAP
MDNS.begin("esp32");
Serial.printf("AP: %s, PASS: %s\n", ssid, passwd);
}
void handleUpdateEnd() {
server.sendHeader("Connection", "close");
if (Update.hasError()) {
server.send(502, "text/plain", Update.errorString());
} else {
server.sendHeader("Refresh", "10");
server.sendHeader("Location", "/");
server.send(307);
ESP.restart();
}
}
void handleUpdate() {
size_t fsize = UPDATE_SIZE_UNKNOWN;
if (server.hasArg("size")) {
fsize = server.arg("size").toInt();
}
HTTPUpload &upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Receiving Update: %s, Size: %d\n", upload.filename.c_str(), fsize);
if (!Update.begin(fsize)) {
otaDone = 0;
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
} else {
otaDone = 100 * Update.progress() / Update.size();
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) {
Serial.printf("Update Success: %u bytes\nRebooting...\n", upload.totalSize);
} else {
Serial.printf("%s\n", Update.errorString());
otaDone = 0;
}
}
}
void webServerInit() {
server.on(
"/update", HTTP_POST,
[]() {
handleUpdateEnd();
},
[]() {
handleUpdate();
}
);
server.on("/favicon.ico", HTTP_GET, []() {
server.sendHeader("Content-Encoding", "gzip");
server.send_P(200, "image/x-icon", favicon_ico_gz, favicon_ico_gz_len);
});
server.onNotFound([]() {
server.send(200, "text/html", indexHtml);
});
server.begin();
Serial.printf("Web Server ready at http://esp32.local or http://%s\n", WiFi.softAPIP().toString().c_str());
}
void everySecond() {
if (otaDone > 1) {
Serial.printf("ota: %d%%\n", otaDone);
}
}
void setup() {
Serial.begin(115200);
apMode();
webServerInit();
tkSecond.attach(1, everySecond);
}
void loop() {
delay(150);
server.handleClient();
}
preference
能存数据且掉电不丢失,首先要创建一个命名空间,然后可以在命名空间中存储很多键值对。使用时要引入头文件Preferences.h,相应的类为Preferences,头文件中并没有提供相应的类对象,所以要自己创建对象,例如 Preferences preferences,这个对象中有很多方法,其中常用的见下面。
preferences.begin("命名空间的名字");
preferences.isKey("键的名字");如果当前命名空间中有这个键则返回true,否则返回false
preferences.putChar(const char* key, int8_t value);将键值对放到命名空间中保存起来。
preferences.getChar(const char* key, int8_t defaultValue = 0);获得命名空间中这个键对应的值,如果没有这个键则返回所设置的默认的值。
RainMaker
可以有空再学
触摸
可以有空再学