DesktopScreen

跟着b站小智学长的视频学习桌面屏幕硬件开发的一个小项目。

GitHub:https://github.com/The-Itach1/DesktopScreen

觉得md文件上传图片麻烦,可以用我开发的一个小工具smfucker,针对于smms图床开发的图床小工具。

硬件

直接用的现成的,电路图可以看看。

软件

环境搭建

这里我采用的Linux的环境,毕竟还是懂一些Linux的相关东西的,整体就是Ubuntu+vscode+Remote - SSH插件,方便直接ssh连上Ubuntu的终端,和进行代码管理等操作。

Ubuntu系统中配置SSH

1
sudo apt-get install openssh-server 

可能报一些错误,网上都能找到原因。

乐鑫ESP32 SDK环境搭建

类似于安装windows的sdk那些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
依赖
sudo apt-get update
sudo apt-get install git wget flex bison gperf python3 python3-pip python3-setuptools
解压
tar -xzvf esp-idf.tar.gz -C ~/esp/
安装
cd ~/esp/esp-idf
export IDF_GITHUB_ASSETS="dl.espressif.com/github_assets"
./install.sh
设置环境
安装完成工具链后,把esp-idf路径设为本地路径,在终端执行(yourpath为你本机路径):
. yourpath/esp/esp-idf/export.sh

如果您需要经常运行 ESP-IDF,您可以为执行 export.sh 创建一个别名,具体步骤如下,
1、在终端中使用vim打开~/.bashrc,然后把以下路径添加到本地环境变量中
vim ~/.bashrc
2、按键盘i按键进行编辑,写入以下命令
alias get_idf='. yourpath/esp/esp-idf/export.sh'
3、按esc按键退出编辑,写入:wq保存退出
4、执行以下命令同步环境变量
source ~/.bashrc
5、直接执行get_idf即可

安装依赖可能会遇到版本问题,有些包可能需要降一下版本,可跟着报错搜索,因为我的Ubuntu是20.04的。

实际上官方文档中都有,官方文档:快速入门 - ESP32 - — ESP-IDF 编程指南 release-v4.4 文档 (espressif.com)

git命令

初始化

1
git init

hello_world

先用项目中example中的hello_world项目直接拷贝下,其源码如下。

1
2
3
4
5
6
7
cd hello_world

//设置目标板卡相关
idf.py set-target esp32

//可配置工程属性
idf.py menuconfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* Hello World Example

This example code is in the Public Domain (or CC0 licensed, at your option.)

Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"

void app_main(void)
{
printf("Hello world!\n");

/* Print chip information */
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
printf("This is %s chip with %d CPU core(s), WiFi%s%s, ",
CONFIG_IDF_TARGET,
chip_info.cores,
(chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "");

printf("silicon revision %d, ", chip_info.revision);

printf("%dMB %s flash\n", spi_flash_get_chip_size() / (1024 * 1024),
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");

printf("Minimum free heap size: %d bytes\n", esp_get_minimum_free_heap_size());

for (int i = 10; i >= 0; i--) {
printf("Restarting in %d seconds...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
printf("Restarting now.\n");
fflush(stdout);
esp_restart();
}

功能就是打印hello world,和打印芯片信息,然后等待10秒后重启。

我们进行编译操作。

1
2
idf.py fullclean
idf.py build

编译完成后,其输出信息会出现下面的话。

1
2
3
Project build complete. To flash, run this command:
/home/itach1/.espressif/python_env/idf4.4_py3.8_env/bin/python ../esp-idf/components/esptool_py/esptool/esptool.py -p (PORT) -b 460800 --before default_reset --after hard_reset --chip esp32 write_flash --flash_mode dio --flash_size detect --flash_freq 40m 0x1000 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0x10000 build/hello_world.bin
or run 'idf.py -p (PORT) flash'

其实就是编译完成了,其提供了两种命令,意思是如何将编译好的bin固件烧录到flash中,我们选择第二种。
三个固件,bootloader.bin,partition-table.bin,hello_world.bin。

1.png

  • partition table是 ESP-IDF 框架中实现的一种分配 flash 的方式,分区表将一块完整的 flash 根据模块进行划分,规定了每个模块使用的 flash 区域和大小。分区表本身也占用存储在 flash 中(默认为地址 0x8000)。
  • bootloader.bin,硬件启动的引导程序,0x1000。
  • hello_world.bin,用户固件。

接下来就是将固件刷进去。将usb接口连上去,linux查看名称ls /dev/ttyU*,一般是/dev/ttyUSB0。
然后3个命令,刷入的时候要打开下载模式,按下io键,然后再按下rst键,然后同时松开,就进入了下载模式,会有蜂鸣声。

1
2
3
4
5
6
//下载
idf.py -p /dev/ttyUSB0 flash
//监视器
idf.py -p /dev/ttyUSB0 monitor
//构建、下载、监视
idf.py -p /dev/ttyUSB0 flash monitor

这里遇到个报错 [Errno 13] could not open port /dev/ttyUSB0,没权限,解决方法给权限sudo chmod -R 777 /dev/ttyUSB0

然后就能看到工作的一些细节了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
I (13) boot: ESP-IDF v4.4.2-296-g4b8915d7af-dirty 2nd stage bootloader
I (13) boot: compile time 20:55:48
I (13) boot: chip revision: 3
I (17) boot_comm: chip revision: 3, min. bootloader chip revision: 0
I (24) boot.esp32: SPI Speed : 40MHz
I (28) boot.esp32: SPI Mode : DIO
I (33) boot.esp32: SPI Flash Size : 2MB
I (37) boot: Enabling RNG early entropy source...
I (43) boot: Partition Table:
I (46) boot: ## Label Usage Type ST Offset Length
I (54) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (61) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (69) boot: 2 factory factory app 00 00 00010000 00100000
I (76) boot: End of partition table
I (80) boot_comm: chip revision: 3, min. application chip revision: 0
I (87) esp_image: segment 0: paddr=00010020 vaddr=3f400020 size=078f4h ( 30964) map
I (107) esp_image: segment 1: paddr=0001791c vaddr=3ffb0000 size=02308h ( 8968) load
I (111) esp_image: segment 2: paddr=00019c2c vaddr=40080000 size=063ech ( 25580) load
I (125) esp_image: segment 3: paddr=00020020 vaddr=400d0020 size=14a30h ( 84528) map
I (156) esp_image: segment 4: paddr=00034a58 vaddr=400863ec size=04f28h ( 20264) load
I (165) esp_image: segment 5: paddr=00039988 vaddr=50000000 size=00010h ( 16) load
I (171) boot: Loaded app from partition at offset 0x10000
I (171) boot: Disabling RNG early entropy source...
I (187) cpu_start: Pro cpu up.
I (187) cpu_start: Starting app cpu, entry point is 0x40081000
0x40081000: call_start_cpu1 at /home/itach1/esp/esp-idf/components/esp_system/port/cpu_start.c:148

I (0) cpu_start: App cpu up.
I (201) cpu_start: Pro cpu start user code
I (202) cpu_start: cpu freq: 160000000
I (202) cpu_start: Application information:
I (206) cpu_start: Project name: hello_world
I (211) cpu_start: App version: 1
I (216) cpu_start: Compile time: Feb 27 2023 20:55:42
I (222) cpu_start: ELF file SHA256: ea5506dd6fe21a17...
I (228) cpu_start: ESP-IDF: v4.4.2-296-g4b8915d7af-dirty
I (235) heap_init: Initializing. RAM available for dynamic allocation:
I (242) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
I (248) heap_init: At 3FFB2BF0 len 0002D410 (181 KiB): DRAM
I (254) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM
I (260) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
I (267) heap_init: At 4008B314 len 00014CEC (83 KiB): IRAM
I (274) spi_flash: detected chip: generic
I (278) spi_flash: flash io: dio
W (282) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (296) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
Hello world!
This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, silicon revision 3, 2MB external flash
Minimum free heap size: 295104 bytes
Restarting in 10 seconds...
Restarting in 9 seconds...
Restarting in 8 seconds...
Restarting in 7 seconds...
Restarting in 6 seconds...
Restarting in 5 seconds...
Restarting in 4 seconds...
Restarting in 3 seconds...
Restarting in 2 seconds...

Done

可以看到boot的一些日志,一些堆栈信息,和main函数执行的结果。

日志打印

开发过程难免会输出一些日志,打印一些信息,其使用到的就是ESP_LOGI()这个宏,其定义在#include “esp_log.h”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*
* SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* This is a STUB FILE HEADER used when compiling ESP-IDF to run tests on the host system.
* The header file used normally for ESP-IDF has the same name but is located elsewhere.
*/
#pragma once

#include <stdint.h>
#include <stdio.h>

#include "sdkconfig.h"

#ifdef __cplusplus
extern "C" {
#endif

#define strlcpy(a, b, c)
#define strlcat(a, b, c)

#define heap_caps_malloc(a, b) NULL
#define MALLOC_CAP_INTERNAL 0
#define MALLOC_CAP_8BIT 0

#define LOG_LOCAL_LEVEL CONFIG_LOG_DEFAULT_LEVEL

typedef enum {
ESP_LOG_NONE, /*!< No log output */
ESP_LOG_ERROR, /*!< Critical errors, software module can not recover on its own */
ESP_LOG_WARN, /*!< Error conditions from which recovery measures have been taken */
ESP_LOG_INFO, /*!< Information messages which describe normal flow of events */
ESP_LOG_DEBUG, /*!< Extra information which is not necessary for normal use (values, pointers, sizes, etc). */
ESP_LOG_VERBOSE /*!< Bigger chunks of debugging information, or frequent messages which can potentially flood the output. */
} esp_log_level_t;

#define LOG_COLOR_E
#define LOG_COLOR_W
#define LOG_COLOR_I
#define LOG_COLOR_D
#define LOG_COLOR_V
#define LOG_RESET_COLOR

#undef _Static_assert
#define _Static_assert(cond, message)

uint32_t esp_log_timestamp(void);
void esp_log_write(esp_log_level_t level, const char* tag, const char* format, ...) __attribute__ ((format (printf, 3, 4)));

#define LOG_FORMAT(letter, format) LOG_COLOR_ ## letter #letter " (%d) %s: " format LOG_RESET_COLOR "\n"

#define ESP_LOGE( tag, format, ... ) if (LOG_LOCAL_LEVEL >= ESP_LOG_ERROR) { esp_log_write(ESP_LOG_ERROR, tag, LOG_FORMAT(E, format), esp_log_timestamp(), tag, ##__VA_ARGS__); }

#define ESP_LOGW( tag, format, ... ) if (LOG_LOCAL_LEVEL >= ESP_LOG_WARN) { esp_log_write(ESP_LOG_WARN, tag, LOG_FORMAT(W, format), esp_log_timestamp(), tag, ##__VA_ARGS__); }

#define ESP_LOGI( tag, format, ... ) if (LOG_LOCAL_LEVEL >= ESP_LOG_INFO) { esp_log_write(ESP_LOG_INFO, tag, LOG_FORMAT(I, format), esp_log_timestamp(), tag, ##__VA_ARGS__); }

#define ESP_LOGD( tag, format, ... ) if (LOG_LOCAL_LEVEL >= ESP_LOG_DEBUG) { esp_log_write(ESP_LOG_DEBUG, tag, LOG_FORMAT(D, format), esp_log_timestamp(), tag, ##__VA_ARGS__); }

#define ESP_LOGV( tag, format, ... ) if (LOG_LOCAL_LEVEL >= ESP_LOG_VERBOSE) { esp_log_write(ESP_LOG_VERBOSE, tag, LOG_FORMAT(V, format), esp_log_timestamp(), tag, ##__VA_ARGS__); }

// Assume that flash encryption is not enabled. Put here since in partition.c
// esp_log.h is included later than esp_flash_encrypt.h.
#define esp_flash_encryption_enabled() false

#ifdef __cplusplus
}
#endif

日志等级也分几种,ERROR,WARN,INFO,DEBUG,VERBOSE,可以以打印不同等级的日志。

简单修改下main函数,使其打印几条日志。

1
2
3
4
5
6
#include "esp_log.h"

static const char *TAG = "MAIN APP";

ESP_LOGI(TAG, "this is info log");
ESP_LOGE(TAG, "this is error log");

然后编译,烧录,重启会发现日志正在打印,不同的颜色代表不同等级的日志。

2.png

最后将这个项目push到dev1。

这里我在linux下配ssh的公钥时遇到点问题,我直接用我windows中ssh中的文件,我GitHub和gitee都用的一个ssh,但是linux会报错,经过搜索,这篇文章挺有用,(54条消息) github 配置了公钥依旧提示git@github.com‘s password: Permission denied, please try again. 的解决办法_XeonYu的博客-CSDN博客

首先将rsa_pub的权限修改为0600,然后ssh目录下创建一个config文件。

1
2
3
4
5
6
7
itach1@itach1:~/.ssh$ sudo chmod 0600 id_rsa

config文件内容如下
Host github.com
Hostname ssh.github.com
Port 443
User git

然后自己github创一个仓库,将代码上传到dev1分支,当然,也可以先直接上传到本地,就像视频中的那样。

1
2
3
4
5
git init
git add
git commit -m ""
git push origin 远程仓库链接
git push -u origin dev1

多任务创建

实际工程中,会遇到多任务的情况,类似于windows下的线程。

xTaskCreate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static BaseType_t xTaskCreate(TaskFunction_t pvTaskCode, 
const char *const pcName,
const uint32_t usStackDepth,
void *const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *const pvCreatedTask)
创建一个新任务,并将其添加到准备运行的任务列表中。
在内部,在FreeRTOS实现中,任务使用两个内存块。第一个块用于保存任务的数据结构。第二个块被任务用作其堆栈。如果使用xTaskCreate()创建任务,则在xTaskCreate()函数中自动动态分配两个内存块。 (see http://www.freertos.org/a00111.html).如果使用xTaskCreateStatic()创建任务,则应用程序编写器必须提供所需的内存。因此,xTaskCreateStatic()允许在不使用任何动态内存分配的情况下创建任务。
有关不使用任何动态内存分配的版本,请参见xTaskCreateStatic()。
xTaskCreate()只能用于创建能够无限制地访问整个微控制器内存映射的任务。包含MPU支持的系统也可以使用xTaskCreateRestricted()创建MPU受限的任务。

Return pdPASS 如果任务成功创建并添加到就绪列表,否则在文件projdefs.h中定义错误代码
Note 如果程序使用线程局部变量(用“__thread”关键字指定的局部变量),则将在任务的堆栈上为它们分配存储空间。
Parameters
* pvTaskCode:指向任务输入函数的指针。任务必须被实现为永不Return(如:死循环),或者应该使用vtTaskDelete函数终止。
* pcName:该任务的描述性名称。这主要用于方便调试。通过设置 **MAX_TASK_NAME_LEN** 定义最大长度-默认值为16
* usStackDepth:指定为字节数的任务堆栈的大小。请注意,这不同于vanilla FreeRTOS。
* pvParameters:将用作所创建的任务的参数的指针。
* uxPriority:任务运行的优先级。包含MPU支持的系统可以通过设置优先级参数的位portPRIVILEGE_BIT来选择在特权(系统)模式下创建任务。
例如,要在优先级2处创建特权任务,应将uxPriority参数设置为(2|portPRIVILEGE_BIT)。
* pvCreatedTask:用于传递回可引用所创建任务的句柄。

示例用法:
// 要创建的任务
void vTaskCode( void * pvParameters )
{
for( ;; )
{
//任务代码就在这里了。
}
}

// 创建任务的函数。
void vOtherFunction( void )
{
static uint8_t ucParameterToPass;
TaskHandle_t xHandle = NULL;
// 创建任务,保存句柄。
// 注意被传递的参数ucParameterToPass,在任务的生命周期内必须存在,所以这里声明static。
// 如果它只是一个自动堆栈变量,那么当新任务试图访问它时,它可能已经不复存在了,或者至少已经被损坏了。
xTaskCreate( vTaskCode,
"NAME",
STACK_SIZE,
&ucParameterToPass,
tskIDLE_PRIORITY,
&xHandle );

configASSERT( xHandle );

// 使用该句柄可以删除该任务。
if( xHandle != NULL )
{
vTaskDelete( xHandle );
}
}

我们直接照猫画虎,将其简单修改后写入到main中。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/* Hello World Example

This example code is in the Public Domain (or CC0 licensed, at your option.)

Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"
#include "esp_log.h"

static const char *TAG = "MAIN APP";

// 要创建的任务
void vTaskCode( void * pvParameters )
{
for( ;; )
{
printf("Task run ...\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}

void app_main(void)
{
printf("Hello world!\n");

//打印几条日志
ESP_LOGI(TAG, "this is info log");
ESP_LOGE(TAG, "this is error log");
static uint8_t ucParameterToPass;
TaskHandle_t xHandle = NULL;
xTaskCreate(vTaskCode,
"Task",
2048,
&ucParameterToPass,
10,
&xHandle );

/*打印芯片信息*/
/* Print chip information */
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
printf("This is %s chip with %d CPU core(s), WiFi%s%s, ",
CONFIG_IDF_TARGET,
chip_info.cores,
(chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "");

printf("silicon revision %d, ", chip_info.revision);

printf("%dMB %s flash\n", spi_flash_get_chip_size() / (1024 * 1024),
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");

printf("Minimum free heap size: %d bytes\n", esp_get_minimum_free_heap_size());


while(1){
printf("system run ...\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}

}

执行结果如下。
3.png

定时器

开发过程中,可能会遇到需要定期执行某一任务的要求,就需要用到定时器这个东西,可以定期执行某些任务,也就是间隔多少时间,会发送警报,然后将时间数据发送出去。

可以先看官方文档:定时器 - - — ESP-IDF 编程指南 release-v4.1 文档 (espressif.com)

还有GitHub的example:https://github.com/espressif/esp-idf/blob/349385cb645432b17a4f2148bdd0e863136324d1/examples/peripherals/timer_group/main/timer_group_example_main.c

需要了解的几个api如下,一个是创建队列实例,用来保存项目,一个是等待接收到项目,有点像windows的waitforsingleobject函数。

xQueueCreate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
xQueueCreate(uxQueueLength, uxItemSize)

创建一个新的队列实例。这将分配新队列所需的存储空间,并返回该队列的句柄。

Return 如果成功创建队列,则返回新创建队列的句柄。
如果无法创建队列,则返回0
Parameters
• uxQueueLength: 队列可包含的最大项目数。
• uxItemSize: 队列中的每个项所需要的字节数。
项目按副本排队,而不是引用,因此这是每个发布的项目复制的字节数。
队列上的每个项的大小必须相同。

示例用法:
struct AMessage
{
char ucMessageID;
char ucData[ 20 ];
};

void vATask( void *pvParameters )
{
QueueHandle_t xQueue1, xQueue2;

// 创建一个能够包含10个uint32_t值的队列。
xQueue1 = xQueueCreate( 10, sizeof( uint32_t ) );

if( xQueue1 == 0 )
{
// 未创建队列,也不能使用该队列。
}

// 创建一个能够包含10个指向AMessage结构的指针的队列。
// 这些数据应该通过指针传递,因为它们包含了大量的数据。
xQueue2 = xQueueCreate( 10, sizeof( struct AMessage * ) );
if( xQueue2 == 0 )
{
// 未创建队列,也不能使用该队列。
}

// ... 其余的任务代码。
}

xQueueReceive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
BaseType_t xQueueReceive(QueueHandle_t xQueue, 
void *const pvBuffer,
TickType_t xTicksToWait)

从队列中接收一个项目。该项目通过副本接收,因此必须提供足够大小的缓冲区。
在创建队列时定义了复制到缓冲区中的字节数。
成功接收到的项目会在队列中删除。
此函数不能在中断服务例行程序中使用。请参见xQueueReceiveFromISR。

Return 如果已从队列成功接收到项目,则为pdTRUE,否则为pdFALSE。
Parameters
• xQueue: 要接收项目的队列的句柄。
• pvBuffer: 指向接收项目复制到的缓冲区的指针。
• xTicksToWait: 如果调用时队列为空,任务应阻塞等待项目接收的最长时间量。
如果xTicksToWait为0,xQueueReceive()将立即返回。
时间在滴答周期中定义,因此如果需要,应使用常数portTICK_PERIOD_MS转换为实时。

示例用法:
struct AMessage
{
char ucMessageID;
char ucData[ 20 ];
} xMessage;

QueueHandle_t xQueue;

// 创建队列并发布值的任务。
void vATask( void *pvParameters )
{
struct AMessage *pxMessage;

// 创建一个能够包含10个指向AMessage结构的指针的队列。这些数据应该通过指针传递,因为它们包含了大量的数据。
xQueue = xQueueCreate( 10, sizeof( struct AMessage * ) );

if( xQueue == 0 )
{
// 无法创建该队列。
}

// ...

// 发送指向结构AMessage对象的指针。如果队列已满,则不要阻止。
pxMessage = & xMessage;

xQueueSend( xQueue, ( void * ) &pxMessage, ( TickType_t ) 0 );

// ... 其余的任务代码。
}

// 要从队列中接收的任务。
void vADifferentTask( void *pvParameters )
{
struct AMessage *pxRxedMessage;
if( xQueue != 0 )
{
// 接收已创建的队列上的消息。如果有一条消息不能立即可用,则阻止10个ticks。
if( xQueueReceive( xQueue, &( pxRxedMessage ), ( TickType_t ) 10 ) )
{
// pcRxedMessage现在指向vatask发布的结构AMessage变量。
}
}

// ... 其余的任务代码。
}

GitHub中的例子稍微复杂一点,视频中的例子简单一点,过程如下。

  • xQueueCreate初始化队列,用来保存传递事件的示例结构。

  • 然后初始定时器,example_tg0_timer_init,这个函数好像是固定代码,其中比较重要的是第三个参数,代表时间间隔为多少,也就是多少时间一个警报。

  • example_tg0_timer_init中有一步比较重要,配置报警值和报警中断,timer_isr_register(),这个函数会注册定时器组 0 ISR 处理程序timer_group0_isr()

  • timer_group0_isr()中会处理下传递事件的示例结构,然后将其发送给主程序的任务,xQueueSendFromISR()。

  • timer_example_evt_task()任务函数中的xQueueReceive(),会一直等待项目的到来,因为其第三个参数设置为了portMAX_DELAY,timer_event_t结构体到了,才会继续向下执行。

ds_timer.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/* Timer group-hardware timer example

This example code is in the Public Domain (or CC0 licensed, at your option.)

Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include "esp_types.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/periph_ctrl.h"
#include "driver/timer.h"
#include "esp_log.h"

#include "ds_timer.h"

static const char *TAG = "TIMER APP";

#define TIMER_DIVIDER 16 // 定时器时钟分频器
#define TIMER_SCALE (TIMER_BASE_CLK / TIMER_DIVIDER/1000) //将计数器值转换为秒
#define TIMER_INTERVAL0_SEC (10) // //第一个定时器的采样测试间隔 10毫秒
#define TEST_WITH_RELOAD 1 // 测试将在没有自动重新加载的情况下完成

/*
* 传递事件的示例结构
* 从定时器中断处理程序到主程序。
*/
typedef struct {
uint64_t timer_minute_count;
uint64_t timer_second_count;
} timer_event_t;

timer_event_t g_timer_event;

xQueueHandle timer_queue;

/*
* 定时器组 0 ISR 处理程序
*
* 笔记:
* 我们不在这里调用计时器 API,因为它们没有用 IRAM_ATTR 声明。
* 如果我们同意在禁用 SPI 闪存缓存时不为定时器 irq 提供服务,
* 我们可以在没有 ESP_INTR_FLAG_IRAM 标志的情况下分配此中断并使用普通 API。
*/
void IRAM_ATTR timer_group0_isr(void *para)
{
timer_spinlock_take(TIMER_GROUP_0);
int timer_idx = (int) para;

/*准备基本的事件数据
然后将被发送回主程序任务*/
timer_event_t evt;

/*清除中断
并在不重新加载的情况下更新定时器的闹钟时间*/
timer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0);

/*警报被触发后
我们需要再次启用它,以便下次触发*/
timer_group_enable_alarm_in_isr(TIMER_GROUP_0, timer_idx);

/*现在只需将事件数据发送回主程序任务*/
xQueueSendFromISR(timer_queue, &evt, NULL);
timer_spinlock_give(TIMER_GROUP_0);
}

/*
* 初始化定时器组 0 的选定定时器
*
* timer_idx - 要初始化的定时器编号
* auto_reload - 计时器是否应该在报警时自动重新加载?
* timer_interval_sec - 要设置的警报间隔
*/
static void example_tg0_timer_init(int timer_idx,
bool auto_reload, double timer_interval_sec)
{
/* Select and initialize basic parameters of the timer */
timer_config_t config = {
.divider = TIMER_DIVIDER,
.counter_dir = TIMER_COUNT_UP,
.counter_en = TIMER_PAUSE,
.alarm_en = TIMER_ALARM_EN,
.auto_reload = auto_reload,
}; // default clock source is APB
timer_init(TIMER_GROUP_0, timer_idx, &config);

/* Timer's counter will initially start from value below.
Also, if auto_reload is set, this value will be automatically reload on alarm */
timer_set_counter_value(TIMER_GROUP_0, timer_idx, 0x00000000ULL);

/*配置报警值和报警中断。*/
timer_set_alarm_value(TIMER_GROUP_0, timer_idx, timer_interval_sec * TIMER_SCALE);
timer_enable_intr(TIMER_GROUP_0, timer_idx);
timer_isr_register(TIMER_GROUP_0, timer_idx, timer_group0_isr,
(void *) timer_idx, ESP_INTR_FLAG_IRAM, NULL);

timer_start(TIMER_GROUP_0, timer_idx);
}

/*
* 本示例程序的主要任务
*/
static void timer_example_evt_task(void *arg)
{
while (1) {
timer_event_t evt;
xQueueReceive(timer_queue, &evt, portMAX_DELAY);
g_timer_event.timer_minute_count ++;
//60s 计算一次 刷新时间
if(g_timer_event.timer_minute_count >= 6000){
g_timer_event.timer_minute_count = 0;
ESP_LOGI(TAG, "1 minute run ");
}
g_timer_event.timer_second_count ++;
//1s计算一次
if(g_timer_event.timer_second_count >= 100){
g_timer_event.timer_second_count = 0;
ESP_LOGI(TAG, "1s run ");
}
}
}

/*
* In this example, we will test hardware timer0 and timer1 of timer group0.
*/
void ds_timer_init(void)
{
//初始化
g_timer_event.timer_minute_count = 0;
g_timer_event.timer_second_count = 0;
//创建一个新的队列实例,实际上就是分配了空间。
timer_queue = xQueueCreate(10, sizeof(timer_event_t));

//初始化定时器
example_tg0_timer_init(TIMER_0, TEST_WITH_RELOAD, TIMER_INTERVAL0_SEC);
//创建一个任务,用来接收时间数据
xTaskCreate(timer_example_evt_task, "timer_evt_task", 2048, NULL, 5, NULL);
}


然后将其添加到对应文件夹,写好ds_timer.h,在app_main中调用一下,最后修改下main目录下的CMakeLists.txt。

编译,查看结果。

4.png

然后上传到dev3。

文件系统

文件管理,对应例子在https://github.com/espressif/esp-idf/tree/349385cb645432b17a4f2148bdd0e863136324d1/examples/storage/spiffs。

官方文档:SPIFFS 文件系统 - - — ESP-IDF 编程指南 release-v4.1 文档 (espressif.com)

视频中的例子相比和example里面的代码相比较,实际上就是将初始化过程,样例过程,和卸载分区并禁用SPIFFS写成了3个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/* HTTP File Server Example

This example code is in the Public Domain (or CC0 licensed, at your option.)

Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/

#include <sys/param.h>
#include <stdio.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>

#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_spiffs.h"
#include "esp_err.h"

#include "ds_spiffs.h"


/* This example demonstrates how to create file server
* using esp_http_server. This file has only startup code.
* Look in file_server.c for the implementation */

static const char *TAG="spiffs";

esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs",
.partition_label = NULL,
.max_files = 5, // This decides the maximum number of files that can be created on the storage
.format_if_mount_failed = true
};

/* Function to initialize SPIFFS */
esp_err_t init_spiffs(void)
{
ESP_LOGI(TAG, "Initializing SPIFFS");

esp_err_t ret = esp_vfs_spiffs_register(&conf);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount or format filesystem");
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "Failed to find SPIFFS partition");
} else {
ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
}
return ESP_FAIL;
}

size_t total = 0, used = 0;
ret = esp_spiffs_info(NULL, &total, &used);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));
return ESP_FAIL;
}

ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
return ESP_OK;
}

void ds_spiffs_deinit(){
// All done, unmount partition and disable SPIFFS
esp_vfs_spiffs_unregister(conf.partition_label);
ESP_LOGI(TAG, "SPIFFS unmounted");
}

void ds_spiffs_test(){
// Use POSIX and C standard library functions to work with files.
// First create a file.
ESP_LOGI(TAG, "Opening file");
FILE* f = fopen("/spiffs/hello.txt", "w");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for writing");
return;
}
fprintf(f, "Hello World!\n");
fclose(f);
ESP_LOGI(TAG, "File written");

// Check if destination file exists before renaming
struct stat st;
if (stat("/spiffs/foo.txt", &st) == 0) {
// Delete it if it exists
unlink("/spiffs/foo.txt");
}

// Rename original file
ESP_LOGI(TAG, "Renaming file");
if (rename("/spiffs/hello.txt", "/spiffs/foo.txt") != 0) {
ESP_LOGE(TAG, "Rename failed");
return;
}

// Open renamed file for reading
ESP_LOGI(TAG, "Reading file");
f = fopen("/spiffs/foo.txt", "r");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for reading");
return;
}
char line[64];
fgets(line, sizeof(line), f);
fclose(f);
// strip newline
char* pos = strchr(line, '\n');
if (pos) {
*pos = '\0';
}
ESP_LOGI(TAG, "Read from file: '%s'", line);
}


比较重要的是我们需要划分分区来存放文件系统,所以要用到之前提到的partitions table来自己划分下分区,直采用example中的。

1
2
3
4
5
6
# Name,   Type, SubType, Offset,  Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
storage, data, spiffs, , 0xF0000,

由于我们是自定义分区表,所以需要配置工程属性。

1
2
3
4
idf.py menuconfig
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"

5.png

6.png

最后保存即可。

编译,烧录,查看结果。
7.png

非易失性存储器NVS-WiFi密码

这也是一种存储方式,也在example的storage的例子中,代码在:https://github.com/espressif/esp-idf/blob/349385cb645432b17a4f2148bdd0e863136324d1/examples/storage/nvs_rw_value/main/nvs_value_example_main.c

NVS 通过调用 spi_flash_{read|write|erase} API 对主 flash 的部分空间进行读、写、擦除操作,包括 data 类型和 nvs 子类型的所有分区。NVS 最适合存储一些较小的数据。

NVS 的操作对象为键值对,其中键是 ASCII 字符串,当前支持最大键长为 15 个字符,值可以为以下几种类型:

  • 整数型:uint8_tint8_tuint16_tint16_tuint32_tint32_tuint64_tint64_t
  • \0 结尾的字符串;
  • 可变长度的二进制数据 (BLOB)

所以nvs是可以理解为Python中的字典,然后特点是重启后,数值是不会消失的,所以可以用这个存储wifi密码。

GitHub中example中的例子的过程如下,官方教程中有一些api的解释:非易失性存储库 - - — ESP-IDF 编程指南 release-v4.1 文档 (espressif.com)

  • nvs_flash_init()初始化。
  • nvs_open () API 选择使用带有 nvs 标签的分区。
  • nvs_set_xx(),nvs_get_xx(),读写键和值。
  • nvs_commit(nvs_handle);将任何挂起的更改写入非易失性存储。
  • nvs_close(nvs_handle);关闭存储句柄

视频中的代码更完整一点,创建了一个ds_system_data.c来保存wifi密码的一些处理,设置WiFi密码,打印WiFi密码,可能后面还会保存一些其他的数据吧。

ds_system_data.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include "sdkconfig.h"
#include "esp_system.h"
#include "esp_log.h"

#include "ds_system_data.h"


static const char *TAG = "ds_system_data";

//定义wifi结构体变量
SYSTRM_DATA_T g_system_data;

//初始化数据
void ds_system_data_init(){
memset(&g_system_data,0,sizeof(SYSTRM_DATA_T));
}

//获取wifi结构体变量
SYSTRM_DATA_T get_system_data(){
return g_system_data;
}

//给wifi结构体变量赋值
void set_system_data_wifi_info(char * p_ssid,uint8_t p_ssidlen,char *p_psw ,uint8_t p_pswlen){
if(p_pswlen >= MAX_SETTING_SSID_LEN || p_ssidlen >= MAX_SETTING_PSW_LEN){
ESP_LOGE(TAG, "MAX_SETTING_SSID_PWD_LEN ERROR");
}
g_system_data.setting_ssid_len = p_ssidlen;
g_system_data.setting_psw_len = p_pswlen;
memcpy(g_system_data.setting_ssid,p_ssid,p_ssidlen);
memcpy(g_system_data.setting_psw,p_psw,p_pswlen);
g_system_data.setting_ssid[p_ssidlen] = '\0';
g_system_data.setting_psw[p_pswlen] = '\0';
}

//打印
void print_system_data_wifi_info(){
printf("\r\nwifi_ssid:");
for(int i = 0;i<g_system_data.setting_ssid_len;i++){
printf("%c",g_system_data.setting_ssid[i]);
}

printf("\r\nwifi_password:");
for(int i = 0;i<g_system_data.setting_psw_len;i++){
printf("%c",g_system_data.setting_psw[i]);
}
printf("\r\n");
}

//检测wifi密码是否设置
SETTING_DATA_E check_system_data_wifi_info(){
if(g_system_data.setting_ssid_len != 0){
return SETTING_DATA_HAS_WIFI_INFO;
}
return SETTING_DATA_INIT;
}

然后是nvs的初始化和设置wifi和启动时的从nvs读取wifi密码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/* Non-Volatile Storage (NVS) Read and Write a Value - Example

For other examples please check:
https://github.com/espressif/esp-idf/tree/master/examples

This example code is in the Public Domain (or CC0 licensed, at your option.)

Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"

#include "ds_nvs.h"
#include "ds_system_data.h"

static const char *TAG = "ds_nvs";

NVS_WIFI_INFO_E wifi_config_flag = NVS_WIFI_INFO_NULL;

//设置wifi信息。
void ds_nvs_save_wifi_info(){
esp_err_t err;
nvs_handle_t nvs_handle;
err = nvs_open("wificonfig", NVS_READWRITE, &nvs_handle);
if (err != ESP_OK) {
ESP_LOGI(TAG,"Error (%s) opening NVS handle!\n", esp_err_to_name(err));
return ;
}
wifi_config_flag = NVS_WIFI_INFO_SAVE;
ESP_ERROR_CHECK(nvs_set_u8(nvs_handle, "wifi_flag", wifi_config_flag));
ESP_ERROR_CHECK(nvs_set_str(nvs_handle, "ssid", get_system_data().setting_ssid));
ESP_ERROR_CHECK(nvs_set_str(nvs_handle, "psw", get_system_data().setting_psw));
ESP_ERROR_CHECK(nvs_commit(nvs_handle));
nvs_close(nvs_handle);
}

//从nvs读取wifi信息。
NVS_WIFI_INFO_E ds_nvs_read_wifi_info(){
esp_err_t err;
nvs_handle_t nvs_handle;
err = nvs_open("wificonfig", NVS_READWRITE, &nvs_handle);
if (err != ESP_OK) {
ESP_LOGI(TAG,"Error (%s) opening NVS handle!\n", esp_err_to_name(err));
return NVS_WIFI_INFO_ERROR;
}
uint8_t wifi_config = 0;
ESP_ERROR_CHECK(nvs_get_u8(nvs_handle, "wifi_flag", &wifi_config));
wifi_config_flag = wifi_config;
if(wifi_config_flag == NVS_WIFI_INFO_SAVE){
ESP_LOGI(TAG,"has wifi config info");
char ssid[32];
char psw[64];
size_t ssid_len = sizeof(ssid);
size_t psw_len = sizeof(psw);
ESP_ERROR_CHECK(nvs_get_str(nvs_handle, "ssid", ssid, &ssid_len));
ESP_ERROR_CHECK(nvs_get_str(nvs_handle, "psw", psw, &psw_len));
set_system_data_wifi_info(ssid,ssid_len,psw,psw_len);
print_system_data_wifi_info();
}else{
ESP_LOGI(TAG,"wifi config info null");
}
nvs_close(nvs_handle);
return wifi_config_flag;
}
//nvs初始化
void ds_nvs_init(){
// Initialize NVS
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK( err );
}

放置到对应目录后,在main函数中调用下,修改下CmakeLists.txt,编译 ,烧录。

8.png

然后添加到dev4。

GPIO

GPIO(英语:General-purpose input/output),通用型之输入输出的简称,用于电信号在电路中的输入输出,以方便控制电路部件。

在这个项目中,与其相关的就是TP触摸屏和墨水屏。

9.png

10.png

通过电路图可以找到,主控通过IO4和IO5来传输TP的电信号,我的理解是每当触碰时,就会产生输入电信号,然后未触碰了,就进行复位。

然后代码层面,可以仿照GitHub的例子进行编写,开始同样是gpio的初始化。

首先定义好主控的io口。

1
2
3
4
5
6
7
8
9
10
11
/**
* GPIO status:
* GPIO5: output
* GPIO4: input, pulled up, interrupt from rising edge
*/

#define GPIO_OUTPUT_IO_0 5
#define GPIO_OUTPUT_PIN_SEL ((1ULL<<GPIO_OUTPUT_IO_0))
#define GPIO_INPUT_IO_0 4
#define GPIO_INPUT_PIN_SEL ((1ULL<<GPIO_INPUT_IO_0))
#define ESP_INTR_FLAG_DEFAULT 0

初始化和安装gpio的中断服务,也就每当触碰,产生输入时,会触发中断,然后向main函数中的任务发送对应的电平信息。

ds_gpio.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
static xQueueHandle gpio_evt_queue = NULL;

static void IRAM_ATTR gpio_isr_handler(void* arg)
{
uint32_t gpio_num = (uint32_t) arg;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

static void gpio_task_example(void* arg)
{
uint32_t io_num;
for(;;) {
if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
printf("GPIO[%d] intr, val: %d\n", io_num, gpio_get_level(io_num));

}
}
}

void ds_touch_gpio_init(){
gpio_config_t io_conf;
//禁用中断
io_conf.intr_type = GPIO_PIN_INTR_DISABLE;
//设置为输出模式
io_conf.mode = GPIO_MODE_OUTPUT;
//要设置的引脚的位掩码
io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
//禁用下拉模式
io_conf.pull_down_en = 0;
//禁用上拉模式
io_conf.pull_up_en = 0;
//使用给定的设置配置 GPIO
gpio_config(&io_conf);

//GPIO interrupt type : both rising and falling edge
io_conf.intr_type = GPIO_INTR_ANYEDGE;
//bit mask of the pins, use GPIO4/5 here
io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;
//set as input mode
io_conf.mode = GPIO_MODE_INPUT;
//enable pull-up mode
io_conf.pull_up_en = 1;
gpio_config(&io_conf);

//change gpio intrrupt type for one pin
// gpio_set_intr_type(GPIO_INPUT_IO_0, GPIO_INTR_NEGEDGE);

//创建一个队列来处理来自 isr 的 gpio 事件
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
//启动 gpio 任务
xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);

//安装 gpio isr 服务
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
//为特定的 gpio pin 挂钩 isr 处理程序
gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);

}

void ds_gpio_init(){
ds_touch_gpio_init();
}

//初始化复位引脚
void ds_gpio_set_touch_rst(uint32_t level){
gpio_set_level(GPIO_OUTPUT_IO_0, level);
}

视频中没有给出测试的后续,这里我在main函数中调用一下,然后接上屏幕进行烧录测试。

接上屏幕。

11.png

12.png

上传到dev5。

I2C驱动触摸屏

什么是I2C

I2C总线是由Philips公司在80年代开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。

IIC一共有只有两个总线: 一条是双向的串行数据线SDA,一条是串行时钟线SCL

  • SDA(Serial data)是数据线,D代表Data也就是数据,Send Data 也就是用来传输数据的
  • SCL(Serial clock line)是时钟线,C代表Clock 也就是时钟 也就是控制数据发送的时序的

13.png

所以我们还需要配置工程属性,将IIC的io接口配置一下。

14.png

在阅读源码后,这个驱动实际上要做的事就是通过i2c来对tp触摸屏这个设备的一些寄存器的值进行读取,这些寄存器代表了其的一些状态,比如说触碰状态,触碰个数等,然后转换成我想要的信息。

首先需要写对应的读写函数,这个部分在ds_i2c.c,里面的函数功能主要是同i2c来读取tp设备的一些寄存器的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/* i2c - Example

For other examples please check:
https://github.com/espressif/esp-idf/tree/master/examples

See README.md file to get detailed usage of this example.

This example code is in the Public Domain (or CC0 licensed, at your option.)

Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include "driver/i2c.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include "ds_i2c.h"

//设置读取地址
static esp_err_t i2c_master_set_addr(uint8_t u8Cmd){
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (ESP_SLAVE_ADDR << 1) | WRITE_BIT, ACK_CHECK_EN);
i2c_master_write_byte(cmd, u8Cmd, ACK_CHECK_EN);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd);
if (ret != ESP_OK) {
printf("i2c_master_set_addr error\n");
}
return ret;
}

//读取数据
esp_err_t i2c_master_read_slave(uint8_t u8Cmd, uint8_t *data_rd, size_t size)
{
if (size == 0) {
return ESP_OK;
}
i2c_master_set_addr(u8Cmd);

i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (ESP_SLAVE_ADDR << 1) | READ_BIT, ACK_CHECK_EN);
for(int index=0;index<(size-1);index++)
{
i2c_master_read_byte(cmd, data_rd+index, ACK_VAL);
}
i2c_master_read_byte(cmd, data_rd+size-1, NACK_VAL);

i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd);
if (ret != ESP_OK) {
printf("i2c_master_read_slave error\n");
}
return ret;
}

//写入数据
esp_err_t i2c_master_write_slave(uint8_t u8Cmd, uint8_t *data_wr, size_t size)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();

i2c_master_start(cmd);
i2c_master_write_byte(cmd, (ESP_SLAVE_ADDR << 1) | WRITE_BIT, ACK_CHECK_EN);
i2c_master_write_byte(cmd, u8Cmd, ACK_CHECK_EN);
i2c_master_write(cmd, data_wr, size, ACK_CHECK_EN);
i2c_master_stop(cmd);

esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd);
if (ret != ESP_OK) {
printf("i2c_master_write_slave error\n");
}
return ret;
}

/**
* @brief i2c master initialization
*/
esp_err_t i2c_master_init(void)
{
int i2c_master_port = I2C_MASTER_NUM;
i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = I2C_MASTER_SDA_IO;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf.scl_io_num = I2C_MASTER_SCL_IO;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf.master.clk_speed = I2C_MASTER_FREQ_HZ;
i2c_param_config(i2c_master_port, &conf);
return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
}

接下来写tp触摸屏的驱动,这个需要根据厂家提供的源码和驱动手册进行编写,但是没搞过,所以,直接看代码吧。

首先是初始化,gpio的初始化,i2c的初始化,用i2c的读写对tp触摸屏的一些属性进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//初始化
void init_ft6336(){
uint8_t w_data,r_data = 0;
memset(&gTPS,0,sizeof(TouchPoint_T));

//GPIO初始化 INT中断和复位引脚
ds_touch_gpio_init();
//复位初始化
ds_gpio_set_touch_rst(GPIO_RST_LOW);
vTaskDelay(50 / portTICK_PERIOD_MS);
ds_gpio_set_touch_rst(GPIO_RST_HIGHT);
vTaskDelay(100 / portTICK_PERIOD_MS);
//I2C初始化
i2c_master_init();
vTaskDelay(100 / portTICK_PERIOD_MS);

w_data=0;
//设置为正常操作模式
i2c_master_write_slave(FT_DEVIDE_MODE,&w_data,1);
w_data=22;
//设置触摸有效值22 越小越灵敏
i2c_master_write_slave(FT_ID_G_THGROUP,&w_data,1);
i2c_master_read_slave(FT_ID_G_THGROUP,&r_data,1);
printf("init THGROUP = %d \n",r_data);
w_data=14;
//设置激活周期 不能小于12 最大14
i2c_master_write_slave(FT_ID_G_PERIODACTIVE,&w_data,1);
i2c_master_read_slave(FT_ID_G_PERIODACTIVE,&r_data,1);
printf("init PERIODACTIVE = %d \n",r_data);
w_data = 0;
//中断产生方式 持续电平
i2c_master_write_slave(FT_ID_G_MODE,&w_data,1);
i2c_master_read_slave(FT_ID_G_MODE,&r_data,1);
printf("init G_MODE = %d \n",r_data);
}

接下来是处理触摸的中断,只不过这个为什么产生这个中断的原因还是不太清楚,因为这个函数的调用是在while循环,给我的感觉更像是,一个随时需要运行的代码,用来更新当时的状态,而不是触摸进行触发,或许后面会加入到gpio的那个中断处理程序中。

1
2
3
4
//触摸中断处理
void get_ft6336_touch_sta(TP_POSITION_T *position){
scan_ft6336();
count_position_ft6336(position);

**scan_ft6336()**,这个函数用来扫描触摸屏寄存器状态、数据,FT6335最大支持双触,所以其会读取两个点的信息,步骤如下。

  • 首先判断触摸点状态,也就是是否有触摸点,有几个触摸点。
  • 当有触摸点,就读取两个触摸点的坐标信息。
  • 当状态是刚从触摸转为未触摸,就设置下松开标记。
  • 当状态是一直未触摸,将坐标清0,更新状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//扫描触摸屏寄存器状态、数据
static void scan_ft6336()
{
uint8_t i=0;
uint8_t sta = 0;
uint8_t buf[4] = {0};
uint8_t gestid=0;
i2c_master_read_slave(0x02,&sta,1);//读取触摸点的状态
gTPS.touch_count=sta;
i2c_master_read_slave(0x01,&gestid,1);//读取触摸点的状态
if(sta & 0x0f) //判断是否有触摸点按下,0x02寄存器的低4位表示有效触点个数
{
gTPS.touch_sta = ~(0xFF << (sta & 0x0F)); //~(0xFF << (sta & 0x0F))将点的个数转换为触摸点按下有效标志
for(i=0;i<2;i++) //分别判断触摸点1-5是否被按下
{
if(gTPS.touch_sta & (1<<i)) //读取触摸点坐标
{ //被按下则读取对应触摸点坐标数据
i2c_master_read_slave(FT6236_TPX_TBL[i],buf,4); //读取XY坐标值
gTPS.x[i]=((uint16_t)(buf[0]&0X0F)<<8)+buf[1];
gTPS.y[i]=((uint16_t)(buf[2]&0X0F)<<8)+buf[3];
// printf("%x %x %x %x x=%d y=%d\n",buf[0],buf[1],buf[2],buf[3],gTPS.x[i],gTPS.y[i]);
// if((buf[0]&0XC0)!=0X80)
// {
// gTPS.x[i]=gTPS.y[i]=0;//必须是contact事件,才认为有效
// gTPS.touch_sta &=0xe0; //清除触摸点有效标记
// return;
// }
}
}
gTPS.touch_sta |= TP_PRES_DOWN; //触摸按下标记
}
else
{
if(gTPS.touch_sta &TP_PRES_DOWN) //之前是被按下的
gTPS.touch_sta &= ~0x80; //触摸松开标记
else
{
gTPS.x[0] = 0;
gTPS.y[0] = 0;
gTPS.touch_sta &=0xe0; //清除触摸点有效标记
}
}
}

接下来将数据转为实际位置,主要是两种情况,一个触摸点,两个触摸点,然后输出下坐标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//转换为实际位置
static void count_position_ft6336(TP_POSITION_T *position){
// printf("------count_position_ft6336 %d------\n",gTPS.touch_count);
switch(gTPS.touch_count)
{
case 1:
// printf("x=%d y=%d\n",gTPS.x[0],gTPS.y[0]);
if((gTPS.x[0]!=0)&&(gTPS.y[0]!=0)
&&(gTPS.x[0]<200)&&(gTPS.y[0]<200))//软件滤掉无效操作
{
//To 152x152
gTPS.x[0]=gTPS.x[0]*152/200;
gTPS.y[0]=gTPS.y[0]*152/200;
position->status = 1;
position->x = gTPS.x[0];
position->y = gTPS.y[0];
/******调试使用****/
printf("触摸点个数=%d\r\n",gTPS.touch_count); //FT6336U最多支持两点触控
printf("x0:%d,y0:%d\r\n",gTPS.x[0],gTPS.y[0]);
return;
}
break;
case 2:
if((gTPS.x[0]!=0)&&(gTPS.y[0]!=0)
&&(gTPS.x[1]!=0)&&(gTPS.y[1]!=0)
&&(gTPS.x[0]<200)&&(gTPS.y[0]<200)
&&(gTPS.x[1]<200)&&(gTPS.y[1]<200))//软件滤掉无效操作
{
//To 152x152
gTPS.x[0]=gTPS.x[0]*152/200;
gTPS.y[0]=gTPS.y[0]*152/200;
gTPS.x[1]=gTPS.x[1]*152/200;
gTPS.y[1]=gTPS.y[1]*152/200;
/******调试使用****/
printf("触摸点个数::%d\r\n",gTPS.touch_count); //FT6336U最多支持两点触控
printf("x0:%d,y0:%d\r\n",gTPS.x[0],gTPS.y[0]);
printf("x1:%d,y1:%d\r\n",gTPS.x[1],gTPS.y[1]);
}
break;
default:
break;
}
for(int i=0;i<2;i++)
{
gTPS.x[i]=0;
gTPS.y[i]=0;
}
position->status = 0;
position->x = gTPS.x[0];
position->y = gTPS.y[0];
}

最后main函数中调用下,编译烧录,触摸屏幕进行测试。

15.png

SPI 水墨屏

**SPI **

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上占用四根线。

屏幕接口

SCLK IO25 SPI 串口通信时钟信号线。

SDI IO26 SPI 串口通信数据信号线。

CS IO27 片选,低电平有效。

D/C IO14 数据/命令 读写选择,高电平为数据,低电平为命令。

RES IO12 电子纸复位信号,低电平有效。

BUSY IO13 电子纸刷新时,BUSY 引脚发出忙信号给主MCU,此时 MCU 无法对电子纸驱动 IC 进行读写操作;电子纸刷新完成后,BUSY 引脚发出闲置状态信号,此时 MCU 可以对 电子纸驱动 IC 进行读写操作。GDEW 系列电子纸 BUSY 引脚忙状态为高电平(GDEH 系列为低电平),BUSY 引脚空闲状态反之。

对我来说,知道如何显示图像就够了,不太想知道其中的工作原理,了解下就行了。

同样先是gpio的初始化,spi通信的代码编写,可参考https://github.com/espressif/esp-idf/tree/349385cb645432b17a4f2148bdd0e863136324d1/examples/peripherals/spi_master

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/* SPI Master example

This example code is in the Public Domain (or CC0 licensed, at your option.)

Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"

#include "ds_gpio.h"


#define DMA_CHAN 2

// #define PIN_NUM_MISO 0
// #define PIN_NUM_MOSI 5
// #define PIN_NUM_CLK 25
// #define PIN_NUM_CS 27

#define PIN_NUM_MISO 23
#define PIN_NUM_MOSI 26
#define PIN_NUM_CLK 25
#define PIN_NUM_CS 27

//To speed up transfers, every SPI transfer sends a bunch of lines. This define specifies how many. More means more memory use,
//but less overhead for setting up / finishing transfers. Make sure 240 is dividable by this.
#define PARALLEL_LINES 16

spi_device_handle_t spi;

void spi_send_cmd(const uint8_t cmd)
{
esp_err_t ret;
spi_transaction_t t;
ds_gpio_set_screen_dc(0);
ds_gpio_set_screen_cs(0);
memset(&t, 0, sizeof(t)); //Zero out the transaction
// t.flags=SPI_TRANS_USE_TXDATA;
t.length=8; //Command is 8 bits
t.tx_buffer=&cmd; //The data is the cmd itself
t.user=(void*)0; //D/C needs to be set to 0
ret=spi_device_polling_transmit(spi, &t); //Transmit!
ds_gpio_set_screen_cs(1);
assert(ret==ESP_OK); //Should have had no issues.
}

void spi_send_data(const uint8_t data)
{
esp_err_t ret;
spi_transaction_t t;
ds_gpio_set_screen_dc(1);
ds_gpio_set_screen_cs(0);
memset(&t, 0, sizeof(t)); //Zero out the transaction
t.length=8; //Len is in bytes, transaction length is in bits.
t.tx_buffer=&data; //Data
t.user=(void*)1; //D/C needs to be set to 1
ret=spi_device_polling_transmit(spi, &t); //Transmit!
ds_gpio_set_screen_cs(1);
assert(ret==ESP_OK); //Should have had no issues.
}

//This function is called (in irq context!) just before a transmission starts. It will
//set the D/C line to the value indicated in the user field.
void spi_pre_transfer_callback(spi_transaction_t *t)
{
int dc=(int)t->user;
printf("dc callback\n");
ds_gpio_set_screen_dc(dc);
}

void screen_spi_init(void)
{
esp_err_t ret;
spi_bus_config_t buscfg={
.miso_io_num = PIN_NUM_MISO, // MISO信号线
.mosi_io_num = PIN_NUM_MOSI, // MOSI信号线
.sclk_io_num = PIN_NUM_CLK, // SCLK信号线
.quadwp_io_num = -1, // WP信号线,专用于QSPI的D2
.quadhd_io_num = -1, // HD信号线,专用于QSPI的D3
.max_transfer_sz = 64*8, // 最大传输数据大小

};
spi_device_interface_config_t devcfg={
.clock_speed_hz=15*1000*1000, //Clock out at 26 MHz
.mode=0, //SPI mode 0
.queue_size=7, //We want to be able to queue 7 transactions at a time
// .pre_cb=spi_pre_transfer_callback, //Specify pre-transfer callback to handle D/C line
};
//Initialize the SPI bus
ret=spi_bus_initialize(HSPI_HOST, &buscfg, 0);
ESP_ERROR_CHECK(ret);
//Attach the LCD to the SPI bus
ret=spi_bus_add_device(HSPI_HOST, &devcfg, &spi);
ESP_ERROR_CHECK(ret);

}

void screen_spi_test(){
spi_send_cmd(0x55);
vTaskDelay(10 / portTICK_PERIOD_MS);
spi_send_data(0x55);
// ds_gpio_set_screen_rst(0); // Module reset
// vTaskDelay(10 / portTICK_PERIOD_MS);
// ds_gpio_set_screen_rst(1);
// vTaskDelay(10 / portTICK_PERIOD_MS);

// ds_gpio_set_screen_dc(0); // Module reset
// vTaskDelay(10 / portTICK_PERIOD_MS);
// ds_gpio_set_screen_dc(1);
// vTaskDelay(10 / portTICK_PERIOD_MS);
// printf("ds_gpio_get_screen_busy() %d\n",ds_gpio_get_screen_busy());
}

最后就是我想知道的如何控制显示内容,其实就是给对应的像素点赋值,类似于下面的代码。

1
2
3
4
5
6
7
8
9
10
11
//图片全刷-数据函数
void ds_screen_full_display_data(const uint8_t *data){

unsigned int i;
spi_send_cmd(0x24); //write RAM for black(0)/white (1)
for(i=0;i<5000;i++)
{
spi_send_data(*data);
data++;
}
}

其中的data就是数据,实际上就是bit位,1代表白,0代表黑。

视频中是刷的全白,看不出来,这里我搞了个表情包。

16.png

WiFi AP

AP模式:指的无线接入点,创建一个无线网络的模式,家里的路由器就是最好的例子,通俗易懂的理解AP模式就是创造一个wifi然后我们用手机之类的设备去连接wifi。

可参考:https://github.com/espressif/esp-idf/tree/349385cb645432b17a4f2148bdd0e863136324d1/examples/wifi/getting_started/softAP

修改下文件配置。

17.png

编译,烧录,手机连接。

18.png

WiFi STA

STA模式:每一个连接到热点上的手机都可以称为STA站点,也就是我们的ESP32在STA模式下工作就可以连接路由器发出的wifi。

简而言之就是去连接热点,然后自己就有网了。

代码github也有,主要的变化在于,其连接的密码,是从之前nvs获取的,针对于这个问题,这个项目采用的联网模式是,ap + html 修改密码 和WiFi名称的界面 + sta去连接wifi从而实现联网的。

先自己设置下一个固定的wifi名称和密码,然后调用函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/* WiFi station Example

This example code is in the Public Domain (or CC0 licensed, at your option.)

Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"

#include "lwip/err.h"
#include "lwip/sys.h"

#include "ds_wifi_sta.h"
#include "ds_system_data.h"

/* The examples use WiFi configuration that you can set via project configuration menu

If you'd rather not, just change the below entries to strings with
the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid"
*/
#define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID
#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
#define EXAMPLE_ESP_MAXIMUM_RETRY CONFIG_ESP_MAXIMUM_RETRY

/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;

/* The event group allows multiple bits for each event, but we only care about two events:
* - we are connected to the AP with an IP
* - we failed to connect after the maximum amount of retries */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1

static const char *TAG = "wifi station";

static int s_retry_num = 0;

static uint8_t start_status = 0;

static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
ESP_LOGI(TAG, "-----------%s event %d",event_base,event_id);
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
esp_wifi_connect();
ESP_LOGI(TAG, "retry to connect to the AP");
// if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
// esp_wifi_connect();
// s_retry_num++;
// ESP_LOGI(TAG, "retry to connect to the AP");
// } else {
// xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
// }
// ESP_LOGI(TAG,"connect to the AP fail");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}

void wifi_init_sta(void)
{
// 创建事件组
s_wifi_event_group = xEventGroupCreate();

// ESP_ERROR_CHECK(esp_netif_init());
// ESP_ERROR_CHECK(esp_event_loop_create_default());

esp_netif_create_default_wifi_sta();

wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// 注册WIFI事件
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
// 注册IP事件
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));

wifi_config_t wifi_config = {
.sta = {
// .ssid = EXAMPLE_ESP_WIFI_SSID,
// .password = EXAMPLE_ESP_WIFI_PASS,
/* Setting a password implies station will connect to all security modes including WEP/WPA.
* However these modes are deprecated and not advisable to be used. Incase your Access point
* doesn't support WPA2, these mode can be enabled by commenting below line */
.threshold.authmode = WIFI_AUTH_WPA2_PSK,

.pmf_cfg = {
.capable = true,
.required = false
},
},
};

memcpy(wifi_config.sta.ssid,get_system_data().setting_ssid,32);
memcpy(wifi_config.sta.password,get_system_data().setting_psw,64);
printf("\r\nwifi_ssid:");
for(int i = 0;i<get_system_data().setting_ssid_len;i++){
printf("%c",wifi_config.sta.ssid[i]);
}

printf("\r\nwifi_password:");
for(int i = 0;i<get_system_data().setting_psw_len;i++){
printf("%c",wifi_config.sta.password[i]);
}
printf("\r\n");

ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
ESP_ERROR_CHECK(esp_wifi_start() );

ESP_LOGI(TAG, "wifi_init_sta finished.");

// 创建事件组
/* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
* number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);

// 连接事件组
/* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
* happened. */
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
} else {
ESP_LOGE(TAG, "UNEXPECTED EVENT");
}


}

void ds_wifi_sta_start(void)
{
if(start_status == 0){
start_status = 1;
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA START");
wifi_init_sta();
}else{
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA IS STARTING");
}
}

void ds_wifi_sta_stop(){
if(start_status == 1){
start_status = 0;
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA STOP");
ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler));
ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler));
vEventGroupDelete(s_wifi_event_group);
ESP_ERROR_CHECK(esp_wifi_stop() );
ESP_ERROR_CHECK(esp_wifi_deinit() );
}else{
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA IS STOPING");
}

}

其中最主要的是那个事件处理函数,如果一直未连接成功,则会一直尝试重复连接,如果有对应的wifi出现,连接成功后,就调用xEventGroupSetBits(),发送消息,xEventGroupWaitBits()接受消息,根据返回的bits变量,判断是否连接成功,打印wifi名称和密码,只不过这里他打印的是固定设置的字符串。

19.png

HTTP SERVER

为了解决不每次都要设置sta密码,视频中就搞了个ap+html输入界面,用来设置要连接的wifi信息 。

https://github.com/espressif/esp-idf/tree/349385cb645432b17a4f2148bdd0e863136324d1/examples/protocols/http_server/file_serving

看了GitHub源码后,感觉c语言来写html的前后端是真的麻烦呀。

先看html关于密码的代码,有点html和javascript的应该能看懂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<form action="">
<h2>WiFi 密码配置</h2>
<input id="wifi" type="text" placeholder="请输入WiFi账号">
<input id="code" type="text" placeholder="请输入WiFi密码">
<button id="set_wifi" type="button" onclick="send_wifi()">提交</button>
</form>

function send_wifi() {
var input_ssid = document.getElementById("wifi").value;
var input_code = document.getElementById("code").value;
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "/wifi_data", true);
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4) {
if (xhttp.status == 200) {
alert("WiFi设置成功!")
console.log(xhttp.responseText);
location.reload()
} else if (xhttp.status == 0) {
alert("设置失败,请检查网络连接!");
location.reload()
return
} else {
alert(xhttp.status + " Error!\n" + xhttp.responseText);
location.reload()
return
}
}
};
var data = {
"wifi_name":input_ssid,
"wifi_code":input_code
}
xhttp.send(JSON.stringify(data));
}

form表单,两个text,代码ssid和密码,一个button按钮,点击就触发js函数send_wifi(),这个函数负责一JSON的格式采用post向/wifi_data接口提交。

所以服务端,我们需要编写对应的接口处理代码,这时候我们还需要之前的spiffs。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static esp_err_t send_wifi_handler(httpd_req_t *req)
{
int total_len = req->content_len;
int cur_len = 0;
char *buf = ((struct file_server_data *)(req->user_ctx))->scratch;
int received = 0;
if (total_len >= SCRATCH_BUFSIZE) {
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "content too long");
return ESP_FAIL;
}
while (cur_len < total_len) {
received = httpd_req_recv(req, buf + cur_len, total_len);
if (received <= 0) {
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to post control value");
return ESP_FAIL;
}
cur_len += received;
}

buf[total_len] = '\0';
printf("recived data length is :%d\n",total_len);
for (int i = 0; i <total_len ; i++){
putchar(buf[i]);
}
printf("\r\nwifi data recived!\r\n");
cJSON *root = cJSON_Parse(buf);

char *ssid = cJSON_GetObjectItem(root, "wifi_name")->valuestring;
char *psw = cJSON_GetObjectItem(root, "wifi_code")->valuestring;
int ssid_len = strlen(ssid);
int psw_len = strlen(psw);
set_system_data_wifi_info(ssid,ssid_len,psw ,psw_len);
print_system_data_wifi_info();
cJSON_Delete(root);
httpd_resp_sendstr(req, "Post control value successfully");
return ESP_OK;
}

大概过程就是接收前端传过来的两个参数,然后调用set_system_data_wifi_info(),保存到wifi信息中。

这里我遇到个问题,就是之前写SPIFFS测试时,是创建了一个foo.txt的,这个东西会影响前端页面的展示,所以需要将flash全清一下,idf.py -p PORT erase_flash

20.png

AP&STA

最终的wifi模式是AP&STA的,也就是两个同时打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#include "freertos/FreeRTOS.h"
#include "esp_wifi.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "driver/gpio.h"

#include <string.h>
#include "ds_wifi_ap_sta.h"
#include "ds_system_data.h"
#include "ds_http_server.h"
#include "ds_dns_server.h"
#include "ds_pwm.h"
#include "ds_ui_page_manage.h"

#define TAG "app_sta_ap"

esp_netif_t *sta;
esp_netif_t *ap;

xQueueHandle wifi_event_queue;

static void wifi_event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
static int s_retry_num = 0;
ESP_LOGI(TAG, "event_base = %s event_id = %d",event_base,event_id);
//STA event
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
esp_wifi_connect();
set_wifi_sta_status(WIFI_STA_MODE_INIT);
ESP_LOGI(TAG, "retry to connect to the AP");
// s_retry_num++;
// if(s_retry_num >= 200){
// set_wifi_sta_status(WIFI_STA_MODE_CONNECT_TIMEOUT);
// s_retry_num = 0;
// ESP_LOGI(TAG, "connect to the AP timeout");
// }else{
// esp_wifi_connect();
// set_wifi_sta_status(WIFI_STA_MODE_INIT);
// ESP_LOGI(TAG, "retry to connect to the AP");
// }
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "connect success ! got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
send_beep_event(BEEP_SHORT_500MS);
ds_ui_page_manage_send_action(PAGE_TYPE_MEMU);
set_wifi_sta_status(WIFI_STA_MODE_CONNECT_SUCCESS);
}

//AP event
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) {
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
ESP_LOGI(TAG, "station "MACSTR" join, AID=%d", MAC2STR(event->mac), event->aid);
set_wifi_ap_status(WIFI_AP_MODE_CONNECT);
send_beep_event(BEEP_SHORT_500MS);
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d", MAC2STR(event->mac), event->aid);
set_wifi_ap_status(WIFI_AP_MODE_DISCONNECT);
}
}

static void ds_wifi_ap_sta_start(void)
{
sta = esp_netif_create_default_wifi_sta();
ap = esp_netif_create_default_wifi_ap();

wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL));
ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_APSTA) );
wifi_config_t sta_config = {
.sta = {
.ssid = CONFIG_ESP_WIFI_SSID,
.password = CONFIG_ESP_WIFI_PASSWORD,
.bssid_set = false,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg = {
.capable = true,
.required = false
},
}
};

if(has_wifi_info()){
memcpy(sta_config.sta.ssid,get_system_data().setting_ssid,32);
memcpy(sta_config.sta.password,get_system_data().setting_psw,64);
}

wifi_config_t ap_config = {
.ap = {
.ssid = CONFIG_ESP_AP_WIFI_SSID,
.ssid_len = strlen(CONFIG_ESP_AP_WIFI_SSID),
.channel = 11,
.password = CONFIG_ESP_AP_WIFI_PASSWORD,
.max_connection = CONFIG_ESP_AP_MAX_STA_CONN,
.authmode = WIFI_AUTH_OPEN
},
};
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_config));
ESP_ERROR_CHECK(esp_wifi_start());
}

static void ds_wifi_ap_sta_update_info(){
wifi_config_t sta_config = {
.sta = {
.ssid = CONFIG_ESP_WIFI_SSID,
.password = CONFIG_ESP_WIFI_PASSWORD,
.bssid_set = false,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg = {
.capable = true,
.required = false
},
}
};
if(has_wifi_info()){
memcpy(sta_config.sta.ssid,get_system_data().setting_ssid,32);
memcpy(sta_config.sta.password,get_system_data().setting_psw,64);
}
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config));
}

static void ds_wifi_ap_sta_stop(void){
ESP_LOGI(TAG, "ESP_WIFI_MODE_AP STOP");
ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler));
ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler));
esp_netif_destroy_default_wifi(sta);
esp_netif_destroy_default_wifi(ap);
ESP_ERROR_CHECK(esp_wifi_stop() );
ESP_ERROR_CHECK(esp_wifi_deinit() );
}

static void wifi_net_task(void* arg)
{
//http服务器初始化
// dns_server_start();
// web_server_start();
ds_http_server_init();
for(;;) {
WIFI_SET_EVENT_E evt;
xQueueReceive(wifi_event_queue, &evt, portMAX_DELAY);
printf("get wifi set event %d\n",evt);
switch (evt)
{
case AP_STA_START:
set_is_ap_sta_open(true);
set_wifi_sta_status(WIFI_STA_MODE_INIT);
set_wifi_ap_status(WIFI_AP_MODE_DISCONNECT);
ds_wifi_ap_sta_start();
break;
case AP_STA_UPDATE:
ds_wifi_ap_sta_update_info();
break;
case AP_STA_STOP:
set_is_ap_sta_open(false);
ds_wifi_ap_sta_stop();
break;
default:
break;
}
}
}

void ds_wifi_send_event(WIFI_SET_EVENT_E event){
WIFI_SET_EVENT_E evt;
evt = event;
xQueueSend(wifi_event_queue, &evt, 0);
}

void ds_wifi_ap_sta_init(){
wifi_event_queue = xQueueCreate(10, sizeof(WIFI_SET_EVENT_E));
xTaskCreate(wifi_net_task, "wifi_net_task", 4096, NULL, 10, NULL);
}

只不过这是最后的代码,将其设置为了一个任务,只不过要单独拎出来测试需要将一些代码删掉,比如说蜂鸣器的代码,包括一些system_data的东西,因为这些东西都还没写。

到这里wifi的配置实际上就完成了,上传到dev8。

final

实际上视频的最后并没有讲解如何一步步编写各个对应的功能,从而构建整个项目,应该是如果要讲的话太费时间了。

个人感觉主要部分如下。

  • ds_timer.c中的处理各种动作的,包括当前处于什么页面,执行动作是滑动还是点触,从而转到不同的业务进行处理,包括他是如何识别是滑动还是短触的。
  • 还有就是网络部分,首先是采用了ap&sta模式,然后ap模式设置的无密码,我把他改成有密码了,要不然谁都可以进后台,然后就是天气api请求,这里他提供的城市没有成都,看完他整个流程后,发现其实只用修改前端html中的城市拼音即可,这些请求实际上也不是很复杂,相比于Python的写法确实要复杂一些,多了一些初始化,以及数据处理上要麻烦一些。
  • 最后就是其页面展示这一块,就是控制像素点,并且实际上其字体的显示,都可以直接当api来用,也比较方便,只需要确定显示的位置以及显示字体内容。
  • 最后就是定时器器的控制。

最后我在其基础上小添加了一点其他小东西。

最后代码上传到了main。

成品展示

21.png

22.png

23.png