这一部分,我们首先通过Hello World例程来熟悉一下VSCode开发ESP32的步骤流程(因此Hello World例程是必看的),然后逐一讲解开发板各个硬件外设的使用。
乐鑫官方提供了很多例程,它的例程文件位于D:\Espressif\frameworks\esp-idf-v5.1.3\examples,其中D:\Espressif是我的ESP-IDF安装路径,如果你的安装路径和我的不一样,你可以通过自己的安装路径找到它。
我们再在自己的硬盘上新建一个文件夹,来作为我们的实验文件夹,注意不要包含中文路径,我在D盘新建了一个名称为esp32c3的文件夹,路径是D:\esp32c3,以后我们会把需要的官方例程复制到这里来做实验。
第3章 Hello World例程
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:01-hello_world.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
3.1 准备例程
复制官方hello_world例程文件夹到我们的实验文件夹。官方hello_world例程文件夹位于esp-idf-v5.1.3\examples\get-started\,复制到我的实验文件夹以后的路径是D:\esp32c3\hello_world。
打开VSCode软件,使用菜单“文件”->“打开文件夹”,选择D:\esp32c3\hello_world文件夹打开它,打开后我们在VSCode左侧的项目面板中,点击main前面的“>”符号,展开main里面的文件,如下图所示:
我们点击hello_world_main.c文件,这个文件就在右侧打开了。这个文件里面的程序,就是ESP32要运行的代码,app_main是主函数。
工程中包含两个CMakeLists.txt文件,hello_world目录下有一个,main目录下有一个,这两个文件的用途不一样。
我们点击hello_world目录下的CMakeList.txt文件,右侧可以看到里面的内容,如下代码所示。
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(hello_world)
2
3
4
5
6
这个里面是用来做CMake的编译配置,在最后一行的project(hello_wrold),括号里面的名称,我们叫做工程名称,一般和工程文件夹名称一致,也可以不一样,这里面的名称,决定了编译后的bin文件的名称,比如现在编译后的bin文件名称就是hello_world.bin文件,如果你改成了project(xxx),编译后的名称就是xxx.bin。
我们点击main目录下的CMakeList.txt文件,右侧可以看到里面的内容,如下代码所示。
idf_component_register(SRCS "hello_world_main.c"
INCLUDE_DIRS "")
2
这个里面是用来添加源文件路径的,本工程里面只有一个c文件,所以只有一个hello_world_main.c。如果工程中需要再添加一个c文件,就可以在这里添加路径。一般情况下,当你在工程中新建一个c文件后,这里会自动把你新建的c文件添加进入,如果没有自动添加进入,手动添加即可,这里我们先了解一下就可以,我们在后面学习其它例程的时候会用到。
本工程的现象是在串口终端输出hello world,然后经过10秒钟后重启,如此往复。程序不用做修改,直接编译下载就可以。接下来看看编译下载的配置流程。
3.2 编译和下载
在VSCode左下角,有一些选项需要配置,如下图所示。
第1个配置图标是串口号的选择。点击图标,在VSCode软件中间最上方会出现可选的串口号。我们选择开发板上对应的串口号,除了COM1之外的另外一个串口号,一般就是开发板上的串口号。注意,使用TYPE-C数据线把电脑和开发板连接,并且安装好驱动后,才可以看到开发板的串口号。如果你这里有多个串口号,可以去电脑的设备管理器里面看看开发板上的串口号具体是哪个,开发板上使用的串口芯片型号是CH343。
这里,我点击COM31,之后,又出现工程路径,再点击一下工程路径就可以了。 第2个配置图标是目标芯片的选择。点击图标,在VSCode软件中间最上方,首先会出现工程路径,点击选择,然后就会出现可选的芯片型号,我们选择esp32c3。 点击esp32c3之后,又会弹出下载方式的选择,这里出现三种方式,第一种是使用ESP-PROG,第二种是使用内置USB-JTAG,第三种是选择USB转串口,这里我们选择第三种:via ESP USB Bridge。 需要注意的是,选择目标芯片的过程需要10秒左右,等右下角的进度条状态完成之后再进行下一步。第3个配置图标是工程文件夹路径,默认会自动配置好,不用修改。把鼠标放上去就可以看到路径。如果你把工程又复制到别的地方了,这里可能就需要修改一下。
第4个配置图标是menuconfig图标,点击图标会打开menuconfig,如下图所示。
点击左侧的Serial flasher config,在右侧出现的配置选项里面,Flash size默认是2MB,我们改成8MB,因为我们开发板上的FLASH芯片是8MB。其它的不用修改,点击“保存”,保存刚才的配置,然后点击“X”关闭menuconfig。第5个图标是一个垃圾桶形象,用来删除编译过程中形成的文件,这里我们还没有编译,用不着处理。
第6个图标是编译按钮,点击这个按钮,就可以开始编译。
第7个图标是下载方式的选择,我们选择UART。
第8个图标是下载按钮,点击这个按钮,可以下载编译好的程序到ESP32开发板。第9个图标是终端观察窗口,下载完程序以后,我们可以点击这个按钮,观察单片机的运行状态,这个其实就是一个串口终端,程序运行过程中,会使用串口发送一些信息,在终端就可以看到。
第10个图标是编译、下载、终端的综合按钮,相当于是依次点击了上面讲的3个按钮,点击一下,会按照顺序完成编译、下载、打开终端。
现在,我们可以点击这个按钮,执行编译下载和打开终端。第一次编译,需要的时间比较长,之后如果稍微修改一下程序再编译,编译时间就没有第一次那么长了。在编译的过程中,会在终端窗口看到编译进度信息。等到程序下载完毕,然后自动打开终端,我们会在终端中看到如下信息:
Hello world!
This is esp32c3 chip with 1 CPU core(s), WiFi/BLE, silicon revision v0.4, 8MB external flash
Minimum free heap size: 330660 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...
2
3
4
5
6
7
8
9
3.3 程序源码分析
接下来,我们分析一下源程序代码。
在app_main函数中,使用了好几个printf输出数据,结合刚才的输出内容,再对照这个程序代码,你就可以看懂了。
几个printf输出语句之后,跟了一个fflush(stdout),fflush是C语言库函数,使用这个库函数,需要头文件包含<stdio.h>文件。fflush(stdin)的作用是清空输入缓冲区,fflush(stdout)的作用是清空输出缓冲区,这里我们使用的是fflush(stdout)。
printf输出的内容,是先放在缓冲区的,遇到换行符\n才会立即输出,如果没有换行符,它就不会及时输出,在没有换行符的情况下,使用fflush(stdout)就会立即输出,不过,虽然会立即输出,它不会自动换行,要想自动换行,还得加换行符。
在这个例程中,我们去掉fflush(stdout),依然会正常执行程序,是因为前面已经有换行符了,这里起到一个保险作用。如果你想试试我们刚才说的内容,可以修改程序试一下。
void app_main(void)
{
while (1)
{
printf("Hello world!");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
// 这里注释掉不用的代码
}
2
3
4
5
6
7
8
9
void app_main(void)
{
while (1)
{
printf("Hello world!");
fflush(stdout);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
// 这里注释掉不用的代码
}
2
3
4
5
6
7
8
9
10
void app_main(void)
{
while (1)
{
printf("Hello world!\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
// 这里注释掉不用的代码
}
2
3
4
5
6
7
8
9
上面有3个代码片段,第1个是printf中没有加\n换行符,编译下载后,你会看到printf没有及时输出内容,而是间隔一段时间输出一堆。第2个加了fflush函数,就可以间隔1秒输出了,不过没有换行。第3个加了换行符,在没有fflush函数的情况下,也可以正常及时输出。
注释代码,可以使用快捷键Crtl和/按键。先选中要注释的代码,然后按住Crtl键不要放,然后按一下/键,代码就会被注释,注意输入法要在英文状态下。如果想取消注释,也是Ctrl和/键。
第4章 BOOT_KEY按键
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:02-gpio_key.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
4.1 GPIO简介
ESP32-C3是QFN32封装,GPIO引脚一共有22个,从GPIO0到GPIO21。理论上,所有的IO都可以复用为任何外设功能,但有些引脚用作连接芯片内部FLASH或者外部FLASH功能时,官方不建议用作其它用途。
通过开发板的原理图,可以看到开发板上的ESP32引脚连接情况。这里我们使用BOOT按键,来学习一下GPIO功能。
ESP32的GPIO,可以用作输入、输出,可以配置内部上拉、下拉,可以配置为中断引脚。
这里我们把连接BOOT按键的IO9引脚,设置为GPIO中断,接收BOOT按键请求。
4.2 编写代码
我们复制 esp-idf-v5.1.3\examples\get-started\sample_project 这个工程到我们的实验文件夹,然后把这个文件夹的名称修改为 gpio_key,方便日后搞清楚这个工程的作用。修改后的工程路径为 D:\esp32c3\gpio_key。
使用 VSCode 打开 gpio_key 这个文件夹。单击打开工程一级目录下的 CMakeLists.txt 文件(注意不是 main 目录下的),然后我们把工程名字修改为 gpio_key,保存后关闭此文件。
project(gpio_key)
点击打开main.c文件,发现里面只写了这么几行代码:
#include <stdio.h>
void app_main(void)
{
}
2
3
4
5
6
从这个工程原来的文件夹名字就可以知道,这是一个示例工程,我们现在需要实现按键中断,比较简单,所以在这个工程上写就可以了。
现在再打开一个VSCode软件,然后打开esp-idf-v5.1.3整个工程文件夹,然后我们依次找到examples\peripherals\gpio\generic_gpio这个工程作为参考,注意不要修改这个工程中的内容和配置,只是作为参考。
我们单击gpio_example_main.c打开这个文件,找到app_main函数。
复制它的前几行语句(第80~93行)到我们自己的gpio_key工程中,如下所示: #include <stdio.h>
#include <stdio.h>
void app_main(void)
{
//zero-initialize the config structure.
gpio_config_t io_conf = {};
//disable interrupt
io_conf.intr_type = GPIO_INTR_DISABLE;
//set as output mode
io_conf.mode = GPIO_MODE_OUTPUT;
//bit mask of the pins that you want to set,e.g.GPIO18/19
io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
//disable pull-down mode
io_conf.pull_down_en = 0;
//disable pull-up mode
io_conf.pull_up_en = 0;
//configure GPIO with the given settings
gpio_config(&io_conf);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
然后按照开发板上BOOT按键连接的是GPIO9进行修改。 第1条语句,定义了一个gpio_config_t结构体变量。
第2条语句,定义引脚中断类型。开发板上的按键没有按下的时候是高电平,按下去以后是低电平,我们定义成下降沿中断。这里原来是GPIO_INTR_DISABLE,表示中断关闭,这里我们修改为GPIO_INTR_NEGEDGE,即下降沿中断。这些宏定义在gpio_types.h文件中被定义,我们在gpio_example_main.c文件中的GPIO_INTR_DISABLE上单击右键,然后选择“转到定义”,就可以找到这几个宏定义,如下所示:
typedef enum {
GPIO*INTR_DISABLE = 0, /*!< Disable GPIO interrupt _/
GPIO_INTR_POSEDGE = 1, /_!< GPIO interrupt type : rising edge _/
GPIO_INTR_NEGEDGE = 2, /_!< GPIO interrupt type : falling edge _/
GPIO_INTR_ANYEDGE = 3, /_!< GPIO interrupt type : both rising and falling edge _/
GPIO_INTR_LOW_LEVEL = 4, /_!< GPIO interrupt type : input low level trigger _/
GPIO_INTR_HIGH_LEVEL = 5, /_!< GPIO interrupt type : input high level trigger \_/
GPIO_INTR_MAX,
} gpio_int_type_t;
2
3
4
5
6
7
8
9
第3条语句是配置模式,这里的模式是GPIO_MODE_OUTPUT,我们修改为GPIO_MODE_INPUT输入模式。 第4条语句是配置选择哪个引脚,这里我们把GPIO_OUTPUT_PIN_SEL修改为1<<GPIO_NUM_9,因为BOOT按键连接到了GPIO9。 第5、6条语句配置是否打开上下拉电阻,0是关闭,1是打开,我们把上拉打开。 前面都是给结构体成员变量赋值,最后一句使用gpio_config函数进行配置。 改完以后的代码如下,我顺便把注释也对应的修改了一下。
void app_main(void)
{
//zero-initialize the config structure.
gpio_config_t io_conf = {};
//falling edge interrupt
io_conf.intr_type = GPIO_INTR_NEGEDGE;
//set as input mode
io_conf.mode = GPIO_MODE_INPUT;
//bit mask of the pins GPIO9
io_conf.pin_bit_mask = 1<<GPIO_NUM_9;
//disable pull-down mode
io_conf.pull_down_en = 0;
//enable pull-up mode
io_conf.pull_up_en = 1;
//configure GPIO with the given settings
gpio_config(&io_conf);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
上面的代码,总结来说一下,就是先定义一个GPIO结构体,然后给GPIO结构体成员变量赋值,然后使用GPIO配置函数配置GPIO。给结构体成员变量赋值,也可以在定义的时候直接赋值,如下代码所示:
void app_main(void)
{
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_NEGEDGE, //falling edge interrupt
.mode = GPIO_MODE_INPUT, //set as input mode
.pin_bit_mask = 1<<GPIO_NUM_9, //bit mask of the pins GPIO9
.pull_down_en = 0, //disable pull-down mode
.pull_up_en = 1 //enable pull-up mode
};
//configure GPIO with the given settings
gpio_config(&io_conf);
}
2
3
4
5
6
7
8
9
10
11
12
接下来,我们再复制gpio_example_main.c文件中的第108~116行代码到我们的main.c文件中。
//create a queue to handle gpio event from isr
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
//start gpio task
xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);
//install gpio isr service
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
//hook isr handler for specific gpio pin
gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);
2
3
4
5
6
7
8
9
开始,定义了一个队列句柄,用来处理gpio队列消息。 然后,定义了两个函数。 第一个函数是gpio中断服务函数,当GPIO产生中断的时候呢,会进入这个函数,xQueueSendFromISR函数将参数GPIO_NUM_9添加到队列消息。 第二个函数是gpio的任务函数,在任务函数中,接收队列消息,当接收到一个队列消息时,打印字符串。PRIu32 是C语言中用于格式化输出的宏,用于打印32位无符号整数。它是由C99标准引入的,位于inttypes.h头文件中。在使用该宏时,需要包含inttypes.h头文件。gpio_get_level函数用于获取引脚的电平。 这些内容,不需要做修改。 接下来,我们再把需要的头文件添加到我们的main.c文件就可以了。 我们复制gpio_example_main.c中的第9~16行到我们的main.c文件中,放到main.c文件的最上方。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
2
3
4
5
6
7
8
使用printf函数,需要添加stdio.h头文件。string.h和stdlib.h我们这里用不着,可以去掉。接下来是3个freeRTOS的头文件,最后一个头文件是用于gpio的配置。
4.3 编译和下载
接下来,依次配置VSCode左下角的配置选项,串口号、目标芯片、下载方式、menuconfig里面,把FLASH大小修改为8MB,其它不做修改。详细的配置方法,在Hello World例程里面已经讲过了,这里就不再赘述了。 然后编译下载,并打开终端查看,当按一次BOOT按键,就会在终端看见下面的输出:
GPIO[9] intr, val: 0
以上就是GPIO作为中断输入的例子。中断输入是项目中常用的方式,如果你想试试查询法,可以不开中断,使用gpio_get_level函数查询引脚电平。 引脚设置为输出,可以使用gpio_set_level来控制引脚的电平。具体使用方法,可以查看我们刚才参考的gpio例程。 ESP32还有一个特殊的引脚,就是GPIO11,它默认是VDD_SPI引脚,VDD_SPI默认是一个3.3V输出的电源引脚,可以用来给外部的FLASH芯片供电,这个引脚也可以修改为GPIO11,作为通用引脚使用。需要注意的是,这个修改是不可逆的,修改成GPIO11以后,就不能再修改为3.3V输出电源引脚了。我们在设计电路的时候,可以根据需求,决定是否用这个引脚给外部FLASH供电。我们的开发板没有用这个引脚给外部FLASH供电,而是作为GPIO引脚。这个引脚设置为GPIO的方法,我们会在I2S音频接口章节介绍。
第5章 温湿度传感器
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:03-humi_temp.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
5.1 传感器介绍
电路板上温湿度传感器型号是GXHTC3,是北京中科银河芯科技有限公司研发的一款芯片。采用I2C接口与ESP32-C3通信,I2C地址是0x70。
5.2 编写i2c驱动程序
我们复制esp-idf-v5.1.3\examples\get-started\sample_project这个工程到我们的实验文件夹,然后把这个文件夹的名称修改为humi_temp,方便日后搞清楚这个工程的作用,humi是湿度的前4个首字母,temp是温度的前4个首字母。修改后我的工程路径是D:\esp32c3\humi_temp。
打开VSCode软件,然后使用软件打开humi_temp文件夹,准备对其进行修改。
再打开一个VSCode软件,然后使用软件打开esp-idf-v5.1.3整个工程文件夹,然后我们依次找到examples\peripherals\i2c\i2c_simple这个工程作为参考,注意不要修改这个工程中的内容和配置,只是作为参考。
我们先点击打开humi_temp工程目录下的CMakeList.txt文件,修改工程的名称为humi_temp,然后保存关闭此文件。
project(humi_temp)
我们最终的目标是,先初始化I2C接口,然后使用I2C通信,读取温湿度传感器中的数据,然后使用printf打印到串口。为了方便管理工程,我们需要在main中新建myi2c.c、myi2c.h、gxhtc3.c、gxhtc3.h四个文件。 在main上面单击右键,然后选择“新建文件”,然后写入文件名称,分别命名为myi2c.c、myi2c.h、gxhtc3.c和gxhtc3.h。
现在我们单击打开main下的CMakeList.txt文件,可以看到gxhtc3.c和myi2c.c文件已经被添加到路径里面了,如下代码所示,如果你的没有自动添加,你需要手动添加。
idf_component_register(SRCS "gxhtc3.c" "myi2c.c" "main.c"
INCLUDE_DIRS ".")
2
我们给myi2c.h和gxhtc3.h文件的最开始处,分别加入代码#pragma once。这是一条预处理指令,告诉编译器这个头文件只可以编译一次。
#pragma once
然后在myi2c.c文件的最开始,添加头文件myi2c.h。
#include "myi2c.h"
然后在gxhtc3.c文件的最开始,添加头文件gxhtc3.h。
#include "gxhtc3.h"
然后在main.c文件的最开始,添加头文件myi2c.h和gxhtc3.h。
#include "myi2c.h"
#include "gxhtc3.h"
2
我们复制i2c_simple工程下i2c_simple_main.c文件中的60~79行代码到humi_temp工程下myi2c.c文件。
/**
* @brief i2c master initialization
*/
static esp_err_t i2c_master_init(void)
{
int i2c_master_port = I2C_MASTER_NUM;
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这个函数用于初始化I2C接口为主机模式,ESP32为I2C主机,温湿度传感器为I2C从机。因为这个函数会被其它文件调用,所以需要把static关键字去掉。
这里面用到了几个宏定义,复制i2c_simple_main.c文件中第25~31行代码,放到myi2c.h文件中。
#define I2C_MASTER_SCL_IO CONFIG_I2C_MASTER_SCL /*!< GPIO number used for I2C master clock */
#define I2C_MASTER_SDA_IO CONFIG_I2C_MASTER_SDA /*!< GPIO number used for I2C master data */
#define I2C_MASTER_NUM 0 /*!< I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip */
#define I2C_MASTER_FREQ_HZ 400000 /*!< I2C master clock frequency */
#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
#define I2C_MASTER_TIMEOUT_MS 1000
2
3
4
5
6
7
第1、2行代码定义SCL和SDA的引脚需要,我们需要按照开发板原理图修改。
第3行定义I2C的序号,ESP32-C3芯片内部只有一个I2C外设,这里定义成0就可以了。
第4行定义I2C通信速率,一般情况下,I2C器件的通信速率有3种,100k、400k、1M,速度越大,通信越快,I2C传感器芯片手册上会提到支持的最大速率,温湿度传感器GXHTC3的I2C通信速率可以达到1M,这里写的是400k,不需要修改。
第5、6行定义发送缓存和接收缓存大小,主机模式下,这两个值设置为0,从机模式下才需要用到这个值。
修改后的代码如下:
#define I2C_MASTER_SCL_IO GPIO_NUM_1 /*!< GPIO number used for I2C master clock */
#define I2C_MASTER_SDA_IO GPIO_NUM_0 /*!< GPIO number used for I2C master data */
#define I2C_MASTER_NUM 0 /*!< I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip */
#define I2C_MASTER_FREQ_HZ 400000 /*!< I2C master clock frequency */
#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
#define I2C_MASTER_TIMEOUT_MS 1000
2
3
4
5
6
7
现在我们在myi2c.h文件中,声明一下刚才在myi2c.c文件中定义的函数,放到define宏定义的下面。
extern esp_err_t i2c_master_init(void);
这里用到了esp_err_t,所以需要在前面包含esp_err.h文件。
#include "esp_err.h"
直到现在,myi2c.h文件就全部修改好了,完整的代码如下。
#pragma once
#include "esp_err.h"
#define I2C_MASTER_SCL_IO GPIO_NUM_1 /*!< GPIO number used for I2C master clock */
#define I2C_MASTER_SDA_IO GPIO_NUM_0 /*!< GPIO number used for I2C master data */
#define I2C_MASTER_NUM 0 /*!< I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip */
#define I2C_MASTER_FREQ_HZ 400000 /*!< I2C master clock frequency */
#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */
#define I2C_MASTER_TIMEOUT_MS 1000
extern esp_err_t i2c_master_init(void);
2
3
4
5
6
7
8
9
10
11
12
13
现在我们点击打开myi2c.c文件,这个文件中用到了i2c函数,需要在文件中添加头文件i2c.h。
#include "driver/i2c.h"
直到现在,myi2c.c文件就全部修改好了,完整的代码如下。
#include "myi2c.h"
#include "driver/i2c.h"
/**
* @brief i2c master initialization
*/
esp_err_t i2c_master_init(void)
{
int i2c_master_port = I2C_MASTER_NUM;
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
接下来我们在app_main函数中,调用i2c_master_init初始化函数。复制i2c_simple_main.c文件中第85~86行代码,放到main.c文件中的app_main函数中。如下代码所示:
void app_main(void)
{
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
}
2
3
4
5
第1条语句,调用初始化函数,并且检测是否初始化成功。
第2条语句,相当于是一个printf函数,第1个参数TAG是一个字符串指针变量,到时候会输出到串口,这个TAG需要在前面定义。复制i2c_simple_main.c文件中第23行代码,放到include代码下面。
static const char *TAG = "i2c-simple-example";
然后我们把TAG的内容改成main,以后在终端窗口看到main,就表示这个输出来自于main.c文件,改完后如下所示:
static const char *TAG = "main";
接下来我们给main.c添加头文件,文件中用到了ESP_ERROR_CHECK和ESP_LOGI,需要添加esp_log.h头文件。
#include "esp_log.h"
到现在,main.c文件的全部代码如下所示:
#include <stdio.h>
#include "myi2c.h"
#include "gxhtc3.h"
#include "esp_log.h"
static const char *TAG = "main";
void app_main(void)
{
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
}
2
3
4
5
6
7
8
9
10
11
12
5.3 编写GXHTC3驱动程序
接下来,开始给gxhtc3.c文件添加代码。
我们一般会先读取gxhtc3的ID号,来判断gxhtc3芯片是否存在和正常。根据gxhtc3的数据手册,读取命令为0xEFC8,发送命令后,可以读出16位的ID号和1个CRC字节。CRC字节用来校验判断读取的数据是否正确。
我们先写一个CRC校验函数。
#define POLYNOMIAL 0x31 // P(x) = x^8 + x^5 + x^4 + 1 = 00110001
//CRC校验
uint8_t gxhtc3_calc_crc(uint8_t *crcdata, uint8_t len)
{
uint8_t crc = 0xFF;
for(uint8_t i = 0; i < len; i++)
{
crc ^= (crcdata[i]);
for(uint8_t j = 8; j > 0; --j)
{
if(crc & 0x80) crc = (crc << 1) ^ POLYNOMIAL;
else crc = (crc << 1);
}
}
return crc;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这个CRC校验函数有两个参数,一个返回值。两个参数分别是需要校验的数据和数据长度。返回值是计算好的CRC字节。这个计算好的CRC字节,将来需要和读出的CRC字节做对比,如果一致,就说明读出数据正确。
函数中的POLYNOMIAL是多项式因子,这个值也是在gxhtc3的数据手册上给出的。^符号是异或运算符,按位异或,位相同则为0,位不同则为1。
我们先看crc ^= (crcdata[i])这条语句,crc初始值是0xFF,crcdata与crc按位与或运算后,把计算后的值赋值给crc,crcdata中位是0的位置会变成1,位是1的位置会变成0。这条语句的作用就是把crcdata的值,1变成0,0变成1,然后赋值给crc。
之后的for循环,按位计算,如果位是0,直接左移1位,如果位是1,左移1位后再与多项式按位与或运算。 接下来,写读取ID的函数。
// 读取ID
esp_err_t gxhtc3_read_id(void)
{
esp_err_t ret;
uint8_t data[3];
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, 0x70 << 1 | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, 0xEF, true);
i2c_master_write_byte(cmd, 0xC8, true);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS);
if (ret != ESP_OK) {
goto end;
}
cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, 0x70 << 1 | I2C_MASTER_READ, true);
i2c_master_read(cmd, data, 3, I2C_MASTER_LAST_NACK);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS);
if(data[2]!=gxhtc3_calc_crc(data,2)){
ret = ESP_FAIL;
}
end:
i2c_cmd_link_delete(cmd);
return ret;
}
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
读取ID的具体过程为,先发送写字节头,然后发送读取ID命令,然后发送读字节头,然后读取3个字节,分别是2个字节的ID号和1个字节的CRC字节。读出来以后,把ID号两个字节带入到了CRC校验函数,计算号的CRC值与读出的值作比较。函数中的0x70是gxhtc3的地址,0xEFC8读ID命令。如果读到的ID号通过了CRC校验,那就说明读取ID号成功。
因为函数中用到了i2c的函数,所以给这个文件也添加一个i2c.h头文件。上面的函数中用到了I2C_MASTER_NUM这个宏定义,这个宏定义是在myi2c.h文件中定义的,所以也需要添加myi2c.h头文件。
#include "myi2c.h"
#include "driver/i2c.h"
2
接下来给gxhtc3.h文件中添加读取ID函数的声明。
extern esp_err_t gxhtc3_read_id(void);
这里用到了esp_err_t类型,所以也需要调用一下对应的头文件。
#include "esp_err.h"
然后我们在main.c文件中调用这个函数看看能正确读取ID号。app_main的代码如下所示:
void app_main(void)
{
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
esp_err_t ret = gxhtc3_read_id();
while(ret != ESP_OK)
{
ret = gxhtc3_read_id();
ESP_LOGI(TAG,"GXHTC3 READ ID");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
ESP_LOGI(TAG,"GXHTC3 OK");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
读取ID号,如果读取成功,返回ESP_OK,如果不是,就隔一秒钟读一下,如果一直读不到ESP_OK,那就不会跳出这个循环。
这里使用了vTaskDelay函数,需要在main.c文件中添加freeRTOS相关头文件。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
2
接下来,就可以编译下载看一下结果了。
依次配置VSCode左下角的配置选项,串口号、目标芯片、下载方式、menuconfig里面,把FLASH大小修改为8MB,其它不做修改。详细的配置方法,在Hello World例程里面已经讲过了,这里就不再赘述了。
然后编译下载,并打开终端查看。
I (276) main: I2C initialized successfully
I (286) main: GXHTC3 OK
I (286) main_task: Returned from app_main()
2
3
出现上面的显示,说明温湿度传感器现在可以正常工作。
5.4 编写读取温湿度数据程序
接下来,我们再给gxhtc3.c文件中添加读取温湿度的相关函数。根据gxhtc3的数据手册上介绍,每一次读取数据,都需要经过四组命令,按照执行顺序,分别是唤醒、测量、读出、休眠,我们分别写这四个命令的函数。
//唤醒
esp_err_t gxhtc3_wake_up(void)
{
int ret;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, 0x70 << 1 | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, 0x35, true);
i2c_master_write_byte(cmd, 0x17, true);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
以上是唤醒函数,唤醒命令是0x3517。
// 测量
esp_err_t gxhtc3_measure(void)
{
int ret;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, 0x70 << 1 | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, 0x7c, true);
i2c_master_write_byte(cmd, 0xa2, true);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
以上是测量命令,根据gxhtc3数据手册,测量命令一共有8种,详情大家可以去看数据手册,这里我使用的是其中一种,0x7ca2。
uint8_t tah_data[6];
// 读出温湿度数据
esp_err_t gxhtc3_read_tah(void)
{
int ret;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, 0x70 << 1 | I2C_MASTER_READ, true);
i2c_master_read(cmd, tah_data, 6, I2C_MASTER_LAST_NACK);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
以上是读出温湿度数据的函数。读到的数据字节放到tah_data这个数组里面,需要在gxhtc3.c文件的include下面定义tah_data数组。读出函数需要跟在测量函数后使用,一次读取6个字节,分别是2个温度数据+1个温度CRC字节+2个湿度数据+1个湿度CRC字节。
// 休眠
esp_err_t gxhtc3_sleep(void)
{
int ret;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, 0x70 << 1 | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, 0xB0, true);
i2c_master_write_byte(cmd, 0x98, true);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
上面是休眠函数,休眠命令是0xB098。 接下来,我们再写一个函数,把上面的4个命令函数使用上,获取温湿度数据,并且计算出最后的结果。
uint16_t rawValueTemp, rawValueHumi;
float temp=0, humi=0;
uint8_t temp_int, humi_int;
2
3
// 获取并计算温湿度数据
esp_err_t gxhtc3_get_tah(void)
{
int ret;
gxhtc3_wake_up();
gxhtc3_measure();
vTaskDelay(20 / portTICK_PERIOD_MS);
gxhtc3_read_tah();
gxhtc3_sleep();
if((tah_data[2]!=gxhtc3_calc_crc(tah_data,2)||(tah_data[5]!=gxhtc3_calc_crc(&tah_data[3],2)))){
temp = 0;
humi = 0;
temp_int = 0;
humi_int = 0;
ret = ESP_FAIL;
}
else{
rawValueTemp = (tah_data[0]<<8) | tah_data[1];
rawValueHumi = (tah_data[3]<<8) | tah_data[4];
temp = (175.0 * (float)rawValueTemp) / 65535.0 - 45.0;
humi = (100.0 * (float)rawValueHumi) / 65535.0;
temp_int = round(temp);
humi_int = round(humi);
ret = ESP_OK;
}
return ret;
}
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
上面定义了3种类型的变量。第1种是原始温湿度值,分别是2字节的温湿度数据。第2种是浮点数温湿度数据,这个是根据原始数据和数据手册上的公式计算出的结果。第3种是整形温湿度数据,四舍五入取整数作为结果。这几个定义放到gxhtc3.c文件的开始处,或者直接放到这个函数的上方,都可以。
函数里面的if语句用来判断CRC校验结果。转换整形数据的时候,用到了round函数,需要添加math.h头文件。
#include <math.h>
然后我们在gxhtc3.h文件中,添加gxhtc3_get_tah函数声明,因为接下来要在main.c文件中调用。
extern esp_err_t gxhtc3_get_tah(void);
因为用到了esp_err_t类型,所以还需要添加esp_err.h头文件。
#include "esp_err.h"
现在打开main.c文件,在app_main函数中的while循环读取ID的下面,创建一个gxhtc3_task任务。
xTaskCreate(gxhtc3_task, "gxhtc3_task", 4096, NULL, 6, NULL);
然后编写这个任务函数。
static void gxhtc3_task(void *args)
{
esp_err_t ret;
while(1)
{
ret = gxhtc3_get_tah();
if (ret!=ESP_OK) {
ESP_LOGE(TAG,"GXHTC3 READ TAH ERROR.");
}
else{
ESP_LOGI(TAG, "TEMP:%.1f HUMI:%.1f", temp, humi);
ESP_LOGI(TAG, "TEMP:%d HUMI:%d", temp_int, humi_int);
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在任务函数中,间隔1秒钟,读取一次温湿度数据,并且打印温湿度数据到终端。打印函数这里注意一下,在没有读到正确数据的时候,使用的是ESP_LOGE,读到正确数据使用的是ESP_LOGI,这两个区别是,I是打印信息,打印出来的是绿色,E是打印错误,打印出来的内容是红色。 这其中用到了temp humi temp_int humi_int这几个变量,所以需要在函数前面声明一下来自外部文件。
extern float temp,humi;
extern uint8_t temp_int,humi_int;
2
直到这里,驱动代码就写完了,接下来,我们在主函数中调用这个任务函数。
void app_main(void)
{
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
esp_err_t ret = gxhtc3_read_id();
while(ret != ESP_OK)
{
ret = gxhtc3_read_id();
ESP_LOGI(TAG,"GXHTC3 READ ID");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
ESP_LOGI(TAG,"GXHTC3 OK");
xTaskCreate(gxhtc3_task, "gxhtc3_task", 4096, NULL, 6, NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
最后,我们编译下载并打开终端查看,正常情况下,会看到如下输出结果:
I (276) main: I2C initialized successfully
I (286) main: GXHTC3 OK
I (286) main_task: Returned from app_main()
I (306) main: TEMP:21.8 HUMI:35.1
I (306) main: TEMP:22 HUMI:35
2
3
4
5
第6章 姿态传感器
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:04-attitude.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
6.1 传感器介绍
开发板上的姿态传感器型号是QMI8658C,内部集成3轴加速度传感器和3轴陀螺仪传感器,支持SPI和I2C通信,在我们的开发板上使用的是I2C通信,ESP32-C3只有1个I2C外设,我们开发板上的所有I2C设备,都使用一个I2C通信接口,通过I2C设备的地址,来决定和谁通信,QMI8658C的I2C地址是0x6A。
本例程,我们将最终完成测量XYZ三个轴的角度,把角度数据通过串口传输到终端。
6.2 编写QMI8658C驱动程序
姿态传感器例程,我们还是使用sample project作为模板,我们复制esp-idf-v5.1.3\examples\get-started\sample_project这个工程到我们的实验文件夹,然后把这个文件夹的名称修改为attitude,attitude是姿态的意思。修改后我的工程路径为D:\esp32c3\attitude。
打开VSCode软件,然后使用软件打开attitude文件夹,准备对其进行修改。
我们先点击打开attitude工程目录下的CMakeList.txt文件,修改工程的名称为attitude,然后保存关闭此文件。
project(attitude)
本例程需要用到I2C通信,现在我们把温湿度例程里面的myi2c.h和myi2c.c文件复制到attitude工程中的main目录下,这个是在电脑硬盘上完成复制和粘贴。
VSCode软件上的显示为: 我们点开main目录下的CMakeLists.txt文件,可以看到myi2c.c文件已经添加到编译路径。idf_component_register(SRCS "qmi8658c.c" "myi2c.c" "main.c"
INCLUDE_DIRS ".")
2
点击打开qmi8658c.h文件,在最上面添加#pragma once
#pragma once
点击打开qmi8658c.c文件,在最上面添加包含qmi8658c.h文件。
#include "qmi8658c.h"
点击打开main.c文件,添加头文件。
#include "myi2c.h"
#include "qmi8658c.h"
2
在app_main函数中,先调用I2C初始化函数。
void app_main(void)
{
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
}
2
3
4
5
函数里使用到了ESP_LOGI,需要包含esp_log.h头文件。
#include "esp_log.h"
还需要给ESP_LOGI里面的TAG定义一下。
static const char *TAG = "MAIN";
接下来,开始写qmi8658c的驱动函数。
我们先写两个读取qmi8658c寄存器的函数和写入qmi8658c寄存器的函数。写入函数用于配置传感器的参数,读取函数用于读取传感器的寄存器数据,例如ID号,状态等。这两个函数放入qmi8658c.c文件中。
esp_err_t qmi8658c_register_read(uint8_t reg_addr, uint8_t *data, size_t len)
{
return i2c_master_write_read_device(I2C_MASTER_NUM, QMI8658C_SENSOR_ADDR, ®_addr, 1, data, len, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
}
esp_err_t qmi8658c_register_write_byte(uint8_t reg_addr, uint8_t data)
{
uint8_t write_buf[2] = {reg_addr, data};
return i2c_master_write_to_device(I2C_MASTER_NUM, QMI8658C_SENSOR_ADDR, write_buf, sizeof(write_buf), I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
}
2
3
4
5
6
7
8
9
10
11
然后我们在qmi8658c.c文件中添加这两个函数需要的头文件。 #include "driver/i2c.h" #include "myi2c.h" 函数里面用到了QMI8658C_SENSOR_ADD,我们在qmi8658c.h文件中定义一下。 #define QMI8658C_SENSOR_ADDR 0x6A 接下来,我们需要写一个qmi8658c初始化函数,用于读取ID号,配置加速度、陀螺仪范围等参数。这个函数涉及到了qmi8658c的寄存器,所以我们先用枚举类型定义寄存器,放到qmi8658c.h文件中。
enum qmi8658c_reg
{
QMI8658C_WHO_AM_I,
QMI8658C_REVISION_ID,
QMI8658C_CTRL1,
QMI8658C_CTRL2,
QMI8658C_CTRL3,
QMI8658C_CTRL4,
QMI8658C_CTRL5,
QMI8658C_CTRL6,
QMI8658C_CTRL7,
QMI8658C_CTRL8,
QMI8658C_CTRL9,
QMI8658C_CATL1_L,
QMI8658C_CATL1_H,
QMI8658C_CATL2_L,
QMI8658C_CATL2_H,
QMI8658C_CATL3_L,
QMI8658C_CATL3_H,
QMI8658C_CATL4_L,
QMI8658C_CATL4_H,
QMI8658C_FIFO_WTM_TH,
QMI8658C_FIFO_CTRL,
QMI8658C_FIFO_SMPL_CNT,
QMI8658C_FIFO_STATUS,
QMI8658C_FIFO_DATA,
QMI8658C_I2CM_STATUS = 44,
QMI8658C_STATUSINT,
QMI8658C_STATUS0,
QMI8658C_STATUS1,
QMI8658C_TIMESTAMP_LOW,
QMI8658C_TIMESTAMP_MID,
QMI8658C_TIMESTAMP_HIGH,
QMI8658C_TEMP_L,
QMI8658C_TEMP_H,
QMI8658C_AX_L,
QMI8658C_AX_H,
QMI8658C_AY_L,
QMI8658C_AY_H,
QMI8658C_AZ_L,
QMI8658C_AZ_H,
QMI8658C_GX_L,
QMI8658C_GX_H,
QMI8658C_GY_L,
QMI8658C_GY_H,
QMI8658C_GZ_L,
QMI8658C_GZ_H,
QMI8658C_MX_L,
QMI8658C_MX_H,
QMI8658C_MY_L,
QMI8658C_MY_H,
QMI8658C_MZ_L,
QMI8658C_MZ_H,
QMI8658C_dQW_L = 73,
QMI8658C_dQW_H,
QMI8658C_dQX_L,
QMI8658C_dQX_H,
QMI8658C_dQY_L,
QMI8658C_dQY_H,
QMI8658C_dQZ_L,
QMI8658C_dQZ_H,
QMI8658C_dVX_L,
QMI8658C_dVX_H,
QMI8658C_dVY_L,
QMI8658C_dVY_H,
QMI8658C_dVZ_L,
QMI8658C_dVZ_H,
QMI8658C_AE_REG1,
QMI8658C_AE_REG2,
QMI8658C_RESET = 96
};
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
结合QMI8658C的数据手册中的寄存器定义表格,写出这个枚举定义。枚举类型的第一个值默认是0,和寄存器WHO_AM_I的地址一样,所以不用标出,然后依次递增,遇到地址不连续的寄存器地址时,单独标出,最后的结果如上代码所示。 接下来写qmi8658c初始化函数到qmi8658c.c文件。
void qmi8658c_init(void)
{
uint8_t id = 0;
qmi8658c_register_read(QMI8658C_WHO_AM_I, &id ,1);
while (id != 0x05)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
qmi8658c_register_read(QMI8658C_WHO_AM_I, &id ,1);
}
ESP_LOGI(TAG, "QMI8658C OK!");
qmi8658c_register_write_byte(QMI8658C_RESET, 0xb0); // 复位
vTaskDelay(10 / portTICK_PERIOD_MS);
qmi8658c_register_write_byte(QMI8658C_CTRL1, 0x40); // CTRL1 设置地址自动增加
qmi8658c_register_write_byte(QMI8658C_CTRL7, 0x03); // CTRL7 允许加速度和陀螺仪
qmi8658c_register_write_byte(QMI8658C_CTRL2, 0x95); // CTRL2 设置ACC 4g 250Hz
qmi8658c_register_write_byte(QMI8658C_CTRL3, 0xd5); // CTRL3 设置GRY 512dps 250Hz
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
初始化函数里面,首先读取qmi8658c的ID号,如果不正确,就继续读,如果正确,往下执行。确定qmi8658c没有问题,先复位芯片,然后进行配置。CTRL1,配置地址自动增加后,我们读取一连串的加速度和陀螺仪数据,只写个首地址就可以连续读了。CTRL2配置加速度的量程和输出速率,CTRL3配置陀螺仪的量程和输出速率,CTRL7配置允许加速度和陀螺仪。
函数里面用到了ESP_LOGI,用来输出信息,这里的TAG,需要定义。我们把这个TAG定义,放到qmi8658c.c文件中的包含头文件的下面。
static const char *TAG = "QMI8658C";
函数里面使用了freeRTOS的延时函数,所以需要包含freeRTOS头文件。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
2
函数中也用到了ESP_LOGI宏,所以需要再添加它的头文件。
#include "esp_log.h"
现在我们把这个函数的声明写到qmi8658c.h文件。
extern void qmi8658c_init(void);
接下来我们在main.c文件中的app_main函数中调用这个初始化函数。
void app_main(void)
{
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
qmi8658c_init();
}
2
3
4
5
6
7
接下来,就可以编译下载看一下结果了。
依次配置VSCode左下角的配置选项,串口号、目标芯片、下载方式、menuconfig里面,把FLASH大小修改为8MB,其它不做修改。详细的配置方法,在Hello World例程里面已经讲过了,这里就不再赘述了。
然后编译下载,并打开终端查看。
I (275) MAIN: I2C initialized successfully
I (1285) QMI8658C: QMI8658C OK!
I (1295) main_task: Returned from app_main()
2
3
上面终端显示,我截图了倒数3条。
MAIN: I2C initialized successfully,这个输出,是main.c文件中主函数中的ESP_LOGI输出的。
QMI8658C: QMI8658C OK!,这个输出,是qmi8658c.c文件中的初始化函数中的ESP_LOGI输出的。
6.3 编写读取姿态数据程序
配置好传感器以后,我们就可以读取加速度值和陀螺仪值了,我们先定义一个结构体类型,用来存放加速度值、陀螺仪值以及姿态值。这个结构体,放到qmi8658c.h文件中。
typedef struct{
int16_t acc_y;
int16_t acc_x;
int16_t acc_z;
int16_t gyr_y;
int16_t gyr_x;
int16_t gyr_z;
float AngleX;
float AngleY;
float AngleZ;
}t_sQMI8658C;
2
3
4
5
6
7
8
9
10
11
结构体成员,前3个,放xyz方向的加速度值,再接下来3个,放xyz方向陀螺仪值,这6个值都是从传感器读出来的原始值,最后3个,放XYZ的角度值,这3个值,需要我们通过计算得到。 这个结构体中用到了int16_t,需要包含stdint.h头文件。
#include <stdint.h>
接下来,写读取加速度值和陀螺仪值的函数,放到qmi8658c.c文件中。
void qmi8658c_Read_AccAndGry(t_sQMI8658C *p)
{
uint8_t status, data_ready=0;
int16_t buf[6];
qmi8658c_register_read(QMI8658C_STATUS0, &status, 1); // 读状态寄存器
if (status & 0x03) // 判断加速度和陀螺仪数据是否可读
{
data_ready = 1;
}
if (data_ready == 1)
{
data_ready = 0;
qmi8658c_register_read(QMI8658C_AX_L, (uint8_t *)buf, 12); // 读加速度值
p->acc_x = buf[0];
p->acc_y = buf[1];
p->acc_z = buf[2];
p->gyr_x = buf[3];
p->gyr_y = buf[4];
p->gyr_z = buf[5];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
函数中,先读取状态寄存器,看看加速度值和陀螺仪值是否可读,然后再读。读到后的值,最终传入t_sQMI8658C定义的结构体。 注意一下这里面的buf数据变量,定义的时候是16位的6个元素,在读寄存器的时候,强制为8位指针变量,读12个字节。这里,大家可以看一下寄存器定义,加速度寄存器有6个,陀螺仪寄存器有6个,每个值都是由低字节寄存器和高字节寄存器组成。 然后我们再写一个计算姿态的函数,计算姿态,可以单独使用加速度值,可以单独使用陀螺仪值,也可以融合使用,它们各自有优缺点,下面,我们写一个使用加速度值计算姿态的函数。
void qmi8658c_fetch_angleFromAcc(t_sQMI8658C *p)
{
float temp;
qmi8658c_Read_AccAndGry(p);
temp = (float)p->acc_x / sqrt( ((float)p->acc_y * (float)p->acc_y + (float)p->acc_z * (float)p->acc_z) );
p->AngleX = atan(temp)*57.3f; // 180/3.14=57.3
temp = (float)p->acc_y / sqrt( ((float)p->acc_x * (float)p->acc_x + (float)p->acc_z * (float)p->acc_z) );
p->AngleY = atan(temp)*57.3f; // 180/3.14=57.3
temp = (float)p->acc_z / sqrt( ((float)p->acc_x * (float)p->acc_x + (float)p->acc_y * (float)p->acc_y) );
p->AngleZ = atan(temp)*57.3f; // 180/3.14=57.3
}
2
3
4
5
6
7
8
9
10
11
12
13
通过上面函数的计算,XYZ每个方向的角度值的范围都是-90°到90°。 这个函数中用到了atan函数,需要在文件中包含头文件math.h。
#include <math.h>
我们把这个计算角度的函数在qmi8658c文件中进行声明。
extern void qmi8658c_fetch_angleFromAcc(t_sQMI8658C *p);
然后我们在app_main函数中调用它。
void app_main(void)
{
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
qmi8658c_init();
while (1)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
qmi8658c_fetch_angleFromAcc(&QMI8658C);
ESP_LOGI(TAG, "angle_x = %.1f angle_y = %.1f angle_y = %.1f",QMI8658C.AngleX, QMI8658C.AngleY, QMI8658C.AngleZ);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在主函数中,qmi8658c初始化以后,每间隔1秒钟计算1次角度值,然后通过串口发送到终端。 这里面把读取到的值给了QMI8658C这个变量,需要在主函数前面定义一下。
t_sQMI8658C QMI8658C;
函数里面用到了freeRTOS的延时函数,需要在main.c文件的最前面包含相关头文件。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
2
现在我们就可以编译下载看结果了。
I (276) MAIN: I2C initialized successfully
I (1286) QMI8658C: QMI8658C OK!
I (2296) MAIN: angle_x = -3.2 angle_y = -0.3 angle_y = 86.8
I (3296) MAIN: angle_x = -3.1 angle_y = -0.3 angle_y = 86.9
I (4296) MAIN: angle_x = -3.1 angle_y = -0.4 angle_y = 86.9
I (5296) MAIN: angle_x = -3.0 angle_y = -0.3 angle_y = 87.0
I (6296) MAIN: angle_x = -3.1 angle_y = -0.4 angle_y = 86.8
2
3
4
5
6
7
转动开发板,可以看到数值的变化。每个轴的数值变化范围,都是从-89~89。现在的数值,是只使用加速度值根据理论原理计算出来的结果,由于高频噪声等因素,结果不是最精确的。如果想要精确的结果,还需要结合陀螺仪数值以及其它各种算法才行。
第7章 地磁传感器
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:05-azimuth.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
7.1 传感器介绍
开发板上的地磁传感器型号是QMC5883L,它也是使用I2C与ESP32通信,I2C地址为0X0D。 本例程,我们使用地磁传感器QMC5883L计算方位角,最终,把开发板放平到桌子上,旋转开发板一周,输出0~359°的数值到串口终端。
7.2 编写QMC5883L驱动程序
我们还是使用sample project作为模板,我们复制esp-idf-v5.1.3\examples\get-started\sample_project这个工程到我们的实验文件夹,然后把这个文件夹的名称修改为azimuth,azimuth是方位角的意思。 打开VSCode软件,然后使用软件打开azimuth文件夹,准备对其进行修改。 我们先点击打开azimuth工程目录下的CMakeList.txt文件,修改工程的名称为azimuth,然后保存关闭此文件。
project(azimuth)
本例程需要用到I2C通信,现在我们把温湿度例程里面的myi2c.h和myi2c.c文件复制到Attitude工程中的main目录下,这个是在电脑硬盘上完成复制和粘贴。
VSCode软件上的显示为: 我们点开main目录下的CMakeLists.txt文件,可以看到myi2c.c文件已经添加到编译路径。idf_component_register(SRCS "myi2c.c" "main.c"
INCLUDE_DIRS ".")
2
然后我们在main目录下新建2个文件,分别是qmc5883l.c和qmc5883l.h文件。
然后我们再点开main目录下的CMakeLists.txt文件,确认一下qmc5883l.c文件有没有被添加到路径。idf_component_register(SRCS "qmc5883l.c" "myi2c.c" "main.c"
INCLUDE_DIRS ".")
2
点击打开qmc5883l.h文件,在最上面添加#pragma once
#pragma once
点击打开qmc5883l.c文件,在最上面添加包含qmc5883l.h文件。
#include "qmc5883l.h"
点击打开main.c文件,添加头文件。
#include "myi2c.h"
#include "qmc5883l.h"
2
在app_main函数中,先调用I2C初始化函数。
void app_main(void)
{
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
}
2
3
4
5
函数里使用到了ESP_LOGI,需要包含esp_log.h头文件。
#include "esp_log.h"
还需要给ESP_LOGI里面的TAG定义一下。
static const char *TAG = "MAIN";
接下来,开始写qmc5883l的驱动函数。 我们先写两个读取qmc5883l寄存器的函数和写入qmc5883l寄存器的函数。写入函数用于配置传感器的参数,读取函数用于读取传感器的寄存器数据,例如ID号,状态等。把这两个函数放到qmc5883l.c文件中。
esp_err_t qmc5883L_register_read(uint8_t reg_addr, uint8_t *data, size_t len)
{
return i2c_master_write_read_device(I2C_MASTER_NUM, QMC5883L_SENSOR_ADDR, ®_addr, 1, data, len, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
}
esp_err_t qmc5883L_register_write_byte(uint8_t reg_addr, uint8_t data)
{
uint8_t write_buf[2] = {reg_addr, data};
return i2c_master_write_to_device(I2C_MASTER_NUM, QMC5883L_SENSOR_ADDR, write_buf, sizeof(write_buf), I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
}
2
3
4
5
6
7
8
9
10
11
然后我们在qmc5883l.c文件中添加这两个函数需要的头文件。
#include "driver/i2c.h"
#include "myi2c.h"
2
函数里面用到了QMC5883L_SENSOR_ADDR,我们在qmc5883l.h文件中定义一下。
#define QMC5883L_SENSOR_ADDR 0x0D
接下来,我们需要写一个qmc5883l初始化函数,用于读取ID号,配置加速度、陀螺仪范围等参数。这个函数涉及到了qmc5883l的寄存器,所以我们先用枚举类型定义寄存器,放到qmc5883l.h文件中。
enum qmc5883l_reg
{
QMC5883L_XOUT_L,
QMC5883L_XOUT_H,
QMC5883L_YOUT_L,
QMC5883L_YOUT_H,
QMC5883L_ZOUT_L,
QMC5883L_ZOUT_H,
QMC5883L_STATUS,
QMC5883L_TOUT_L,
QMC5883L_TOUT_H,
QMC5883L_CTRL1,
QMC5883L_CTRL2,
QMC5883L_FBR,
QMC5883L_CHIPID = 13
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
结合QMC5883L的数据手册中的寄存器定义表格,写出这个枚举定义。枚举类型的第一个值默认是0,和寄存器XOUT_L的地址一样,所以不用标出,然后依次递增,遇到地址不连续的寄存器地址时,单独标出,最后的结果如上代码所示。 接下来写qmc5883l初始化函数到qmc5883l.c文件。
void qmc5883l_init(void)
{
uint8_t id = 0;
qmc5883L_register_read(QMC5883L_CHIPID, &id ,1);
while (id != 0xff) // 确定ID号是否正确
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
qmc5883L_register_read(QMC5883L_CHIPID, &id ,1);
}
ESP_LOGI(TAG, "QMC5883L OK!");
qmc5883L_register_write_byte(QMC5883L_CTRL2, 0x80); // 复位芯片
vTaskDelay(10 / portTICK_PERIOD_MS);
qmc5883L_register_write_byte(QMC5883L_CTRL1, 0x05); //Continuous模式 50Hz
qmc5883L_register_write_byte(QMC5883L_CTRL2, 0x00);
qmc5883L_register_write_byte(QMC5883L_FBR, 0x01);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
初始化函数里面,首先读取qmc5883l的ID号,如果不正确,就继续读,如果正确,往下执行。确定qmc5883l没有问题,先复位芯片,然后进行配置。CTRL1,配置成了连续采集模式,输出速率50Hz。CTRL2,可以用来配置是否复位以及数据读取方式。FBR寄存器,数据手册推荐写入0x01。 函数里面用到了ESP_LOGI,用来输出信息,这里的TAG,需要定义。我们把这个TAG定义,放到qmc5883l.c文件中的包含头文件的下面。
static const char *TAG = "QMC5883L";
函数里面使用了freeRTOS的延时函数,所以需要包含freeRTOS头文件,放到qmc5883l.c文件中。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
2
函数里面也用到了ESP_LOGI,所以还需要添加log头文件。
#include "esp_log.h"
现在我们把这个函数的声明写到qmc5883l.h文件。
extern void qmc5883l_init(void);
接下来我们在main.c文件中的app_main函数中调用这个初始化函数。
void app_main(void)
{
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
qmc5883l_init();
}
2
3
4
5
6
7
接下来,就可以编译下载看一下结果了。 依次配置VSCode左下角的配置选项,串口号、目标芯片、下载方式、menuconfig里面,把FLASH大小修改为8MB,其它不做修改。详细的配置方法,在Hello World例程里面已经讲过了,这里就不再赘述了。 然后编译下载,并打开终端查看。
I (275) MAIN: I2C initialized successfully
I (5285) QMC5883L: QMC5883L OK!
I (5295) main_task: Returned from app_main()
2
3
上面终端显示,我截图了倒数3条。 MAIN: I2C initialized successfully,这个输出,是main.c文件中主函数中的ESP_LOGI输出的。 QMC5883L: QMC5883L OK!,这个输出,是qmc5883l.c文件中的初始化函数中的ESP_LOGI输出的。
7.3 编写读取方位角程序
配置好传感器以后,我们就可以读取磁力值了,我们先定义一个结构体类型,用来存放磁力值以及方位角值。这个结构体,放到qmc5883l.h文件中。
typedef struct{
int16_t mag_x;
int16_t mag_y;
int16_t mag_z;
float azimuth;
}t_sQMC5883L;
2
3
4
5
6
结构体成员,前3个,放xyz方向的磁力值,最后的azimuth方位角,需要我们通过计算得到。 这个结构体中用到了int16_t,需要包含stdint.h头文件。
#include <stdint.h>
接下来,写读取地磁传感器值的函数,放到qmc5883l.c文件中。
void qmc5883l_read_xyz(t_sQMC5883L *p)
{
uint8_t status, data_ready=0;
int16_t mag_reg[3];
qmc5883L_register_read(QMC5883L_STATUS, &status, 1); // 读状态寄存器
if (status & 0x01)
{
data_ready = 1;
}
if (data_ready == 1)
{
data_ready = 0;
qmc5883L_register_read(QMC5883L_XOUT_L, (uint8_t *)mag_reg, 6);
p->mag_x = mag_reg[0];
p->mag_y = mag_reg[1];
p->mag_z = mag_reg[2];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
函数中,先读取状态寄存器,看看磁力值是否可读,然后再读。读到后的值,最终传入t_sQMC5883L定义的结构体。 注意一下这里面的mag_reg数据变量,定义的时候是16位的3个元素,在读寄存器的时候,强制为8位指针变量,读6个字节。这里,大家可以看一下寄存器定义,磁力值寄存器有6个,每个值都是由低字节寄存器和高字节寄存器组成。 然后我们再写一个计算方位角的函数,使用磁力值计算方位角,最简单的方式,只需要一个公式。我们把这个函数放到qmc5883l.c文件中。
void qmc5883l_fetch_azimuth(t_sQMC5883L *p)
{
qmc5883l_read_xyz(p);
p->azimuth = (float)atan2(p->mag_y, p->mag_x) * 180.0 / 3.1415926 + 180.0;
}
2
3
4
5
6
这个函数里面用到了atan2函数,需要包含头文件math.h文件。
#include <math.h>
我们把这个计算方位角的函数在qmc5883l.h文件中进行声明。
extern void qmc5883l_fetch_azimuth(t_sQMC5883L *p);
然后我们在app_main函数中调用它。
void app_main(void)
{
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
qmc5883l_init();
while (1)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
qmc5883l_fetch_azimuth(&QMC5883L);
ESP_LOGI(TAG, "azimuth = %.1f", QMC5883L.azimuth);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在主函数中,qmc5883l初始化以后,每间隔1秒钟计算1次方位角值,然后通过串口发送到终端。 这里面把读取到的值给了QMC5883这个结构体变量,需要在主函数前面定义一下。
t_sQMC5883L QMC5883L;
函数里面用到了freeRTOS的延时函数,需要在main.c文件的最前面包含相关头文件。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
现在我们就可以编译下载看结果了。
I (276) main_task: Calling app_main()
I (276) MAIN: I2C initialized successfully
I (2286) QMC5883L: QMC5883L OK!
I (3296) MAIN: azimuth = 260.1
I (4296) MAIN: azimuth = 260.2
I (5296) MAIN: azimuth = 259.3
2
3
4
5
6
7
8
9
把开发板放到桌子上放平,然后转动开发板,可以看到数值的变化,变化范围是0~359。现在输出的数值,是按照理论原理计算得出的结果,如果作为指南针,会有误差,代码还需要优化。
第8章 音频-扬声器和麦克风
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:06-i2s_es8311.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
8.1 音频芯片介绍
开发板上带有一个麦克风,一个扬声器,音频编解码芯片使用ES8311。麦克风直接连接到了ES8311芯片上,ES8311和扬声器之间,还有一个音频驱动放大器。ES8311通过I2S接口与ESP32-C3连接。
8.2 播放音乐
本例程,我们直接在官方提供的例程上修改,就可以完成。
复制esp-idf-v5.1.3\examples\peripherals\i2s\i2s_codec\i2s_es8311这个例程,到我们自己的实验文件夹。不需要改名字,我的路径是D:\esp32c3\i2s_es8311。
打开软件VSCode,然后使用VSCode打开i2s_es8311工程文件夹。
现在我们需要针对开发板上的引脚连接,先把例程中的引脚相关代码修改一下。
ES8311这个芯片不仅使用I2S接口与ESP32连接,还有I2C接口与ESP32连接,I2C接口用于配置,I2S接口用于音频传输。
点击打开example_config.h文件,I2C引脚相关代码在它24~35行之间,这里使用了条件编译,根据使用的ESP32不同型号,定义I2C引脚。针对我们开发板上ESP32-C3,我们应该修改它的33、34行,我们修改为开发板上使用的GPIO0和GPIO1引脚。
#define I2C_SCL_IO (GPIO_NUM_1)
#define I2C_SDA_IO (GPIO_NUM_0)
2
I2S引脚相关代码在它的37~48之间。 先修改39~41行。
#define I2S_MCK_IO (GPIO_NUM_10)
#define I2S_BCK_IO (GPIO_NUM_8)
#define I2S_WS_IO (GPIO_NUM_12)
2
3
然后修改46、47行。
#define I2S_DO_IO (GPIO_NUM_11)
#define I2S_DI_IO (GPIO_NUM_7)
2
I2S的5个引脚就修改好了。
特别要注意I2S引脚当中的GPIO11,这个引脚目前还是VDD_SPI引脚,默认是一个电源引脚,输出3.3V,我们需要把它变成GPIO11才可以使用。这个变化是不可逆的,变成GPIO11以后,就不能再变成VDD_SPI引脚了。你们自己设计产品的时候注意,如果这个引脚用作VDD_SPI给外部FLASH供电,千万不要把它搞成GPIO11,否则就得换芯片了。我们的开发板上只把它用做GPIO11,I2S_DO引脚。把它变成GPIO11,需要调用一个函数即可。
printf("ESP_EFUSE_VDD_SPI_AS_GPIO start\n-----------------------------\n");
esp_efuse_write_field_bit(ESP_EFUSE_VDD_SPI_AS_GPIO);
2
我们把上面两行代码放到app_main函数的最开始处。第一行语句是提示,第二行代码是把VDD_SPI引脚变成GPIO引脚的函数。使用这个函数,需要调用esp_efuse_table.h头文件。
#include "esp_efuse_table.h"
接下来,还有一个引脚需要控制,就是音频放大器芯片NS4150B的EN引脚,这个引脚连接到了ESP32-C3的GPIO13,这个引脚通过下拉电阻接地,低电平关闭音频输出,高电平打开音频输出,这里我们需要把这个引脚变成高电平才可以。
/* 初始化PA芯片NS4150B控制引脚 低电平关闭音频输出 高电平允许音频输出 */
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE, //disable interrupt
.mode = GPIO_MODE_OUTPUT, //set as output mode
.pin_bit_mask = 1<<13, //bit mask of the pins
.pull_down_en = 0, //disable pull-down mode
.pull_up_en = 1, //enable pull-up mode
};
//configure GPIO with the given settings
gpio_config(&io_conf);
gpio_set_level(GPIO_NUM_13, 1); // 输出高电平
2
3
4
5
6
7
8
9
10
11
12
把上面的代码,放到app_main函数中,就放到刚才添加的两行控制VDD_SPI引脚代码后面就可以。
直到这里,代码就修改完毕了,接下来我们配置好左下角的串口号、芯片型号、menuconfig等。
注意,配置menuconfig之前,一定要先选好芯片型号,否则menuconfig的内容会被复位,还得在配置一下。所以我们先把芯片配置成esp32-c3,然后打开menuconfig配置。这里需要配置两个地方,一个是FLASH容量改成8MB,另外一个是Example Configuration。
I (10340) i2s_es8311: [music] i2s music played, 634240 bytes are written.
关于如何替换成自己定义的声音,可以参考这个例程的README.MD文件中的Customize your own music这一部分。另外,也可以看第17章的例程,第17章的例程有开机音乐,教程中有介绍如何制作。
8.3 回声模式
以上是音乐播放,接下来我们测试麦克风输入,这个只需要在menuconfig中把模式修改为echo回声模式就可以了。
把Example mode修改为echo之后,在Set MIC gain这里修改麦克风的增益为0dB,再大的话,可能会引起啸叫,最后把Voice volume修改为80,就是扬声器的声音。然后保存,关闭。 编译下载后,我们对着开发板说话,会在扬声器里面听到自己的声音。麦克风位于USB接口的左边。 注意:把VDD_SPI引脚修改为GPIO11的那条代码,只需要执行一次就可以,执行一次以后,就把它注释掉。第9章 LCD显示
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:07-spi_lcd.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
9.1 LCD介绍
开发板上的液晶屏是2.0寸的IPS高清液晶屏,分辨率240*320,显示非常清晰。液晶屏驱动芯片ST7789,采用SPI通信方式与ESP32-C3连接。
9.2 编写程序
本章节的例程,我们在官方的spi_lcd_touch这个例程基础上修改。spi_lcd_touch官方例程的路径是esp-idf-v5.1.3\examples\peripherals\lcd\spi_lcd_touch。复制到我们自己的实验文件夹后,把名称修改为spi_lcd,就是把touch去掉,因为本章节只完成lcd显示,下一个章节再完成触摸功能。修改后的路径为D:\esp32c3\spi_lcd。
这个例程的最终效果是在LCD上显示了一个lvgl的meter小部件,接下来我们开始修改。
使用VSCode打开这个工程,我们先把芯片型号修改为esp32-c3,这一步很重要,因为之后就要修改menuconfig了。如果没有先修改芯片型号为esp32-c3,之后再修改芯片型号的话,会把menuconfig内容复位。
我们点击打开CMakeLists.txt文件,修改工程名称。
project(spi_lcd)
在左边工程目录展开main文件夹,可以看到在工程中有两个c文件。lvgl_demo_ui.c,和spi_lcd_touch_example_main.c。
点击打开文件spi_lcd_touch_example_main.c,在头文件之后,我们看到如下代码:#if CONFIG_EXAMPLE_LCD_CONTROLLER_ILI9341
#include "esp_lcd_ili9341.h"
#elif CONFIG_EXAMPLE_LCD_CONTROLLER_GC9A01
#include "esp_lcd_gc9a01.h"
#endif
#if CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_STMPE610
#include "esp_lcd_touch_stmpe610.h"
#endif
2
3
4
5
6
7
8
9
这个工程,默认用到的LCD驱动芯片是ILI9341或者GC9A01,用到的触摸屏驱动芯片是STMPE610。我们的开发板LCD驱动是ST7789,触摸屏驱动是FT6336,和它给的不一样,所以需要做一些修改。上面的代码使用了#if来加载对应的头文件,配置是在menuconfig中,我们先设置目标芯片为esp32c3,然后点击图标打menuconfig。
左侧点击Example Configuration,右侧出现配置选项。LCD controller model中可以下拉选择ILI9341或者GC9A01。如果勾选Enable LCD touch,就可以打开触摸屏功能,触摸屏驱动,我们下一节再使用,本小节不打开。 现在我们只把flash大小修改为8M就可以了,关闭menuconfig。关闭后,我们给menuconfig增加一个LCD驱动选项,添加ST7789选项。 点击工程左侧Kconfig.projbuild文件打开,然后添加st7789,最后如下代码所示: choice EXAMPLE_LCD_CONTROLLER
prompt "LCD controller model"
default EXAMPLE_LCD_CONTROLLER_ILI9341
help
Select LCD controller model
config EXAMPLE_LCD_CONTROLLER_ILI9341
bool "ILI9341"
config EXAMPLE_LCD_CONTROLLER_GC9A01
bool "GC9A01"
config EXAMPLE_LCD_CONTROLLER_ST7789
bool "ST7789"
endchoice
2
3
4
5
6
7
8
9
10
11
12
13
14
15
保存文件后,先点击VSCode软件中左下角的文件夹图标,更新一下当前工程,目的是让menuconfig中添加刚才添加的st7789选项。
打开menuconfig,就可以看到LCD驱动选项中多了ST7789,我们把LCD驱动选择为ST7789,然后点击“保存”,并关闭menuconfig。 spi_lcd_touch_example_main.c文件中的第20行到第30行,在用#if选择包含对应头文件的这里,我们不需要做任何修改,因为ST7789已经存在于IDF的components中,不需要再另外写头文件了。触摸屏没有允许,驱动程序,暂时也不用管。 第38行到第48行,背光是低电平打开,所以把1改成0。然后对照开发板原理图,修改好引脚的序号。 修改好以后的代码如下所示:
#define EXAMPLE_LCD_PIXEL_CLOCK_HZ (20 * 1000 * 1000)
#define EXAMPLE_LCD_BK_LIGHT_ON_LEVEL 0
#define EXAMPLE_LCD_BK_LIGHT_OFF_LEVEL !EXAMPLE_LCD_BK_LIGHT_ON_LEVEL
#define EXAMPLE_PIN_NUM_SCLK 3
#define EXAMPLE_PIN_NUM_MOSI 5
#define EXAMPLE_PIN_NUM_MISO -1
#define EXAMPLE_PIN_NUM_LCD_DC 6
#define EXAMPLE_PIN_NUM_LCD_RST -1
#define EXAMPLE_PIN_NUM_LCD_CS 4
#define EXAMPLE_PIN_NUM_BK_LIGHT 2
#define EXAMPLE_PIN_NUM_TOUCH_CS -1
2
3
4
5
6
7
8
9
10
11
其中,-1表示没有用到此引脚。
第51到57行定义LCD的分辨率,原来只有ILI9391和GC9A01,我们增加ST7789的定义,我们开发板上的液晶屏分辨率是240*320,添加后的代码如下所示:
// The pixel number in horizontal and vertical
#if CONFIG_EXAMPLE_LCD_CONTROLLER_ILI9341
#define EXAMPLE_LCD_H_RES 240
#define EXAMPLE_LCD_V_RES 320
#elif CONFIG_EXAMPLE_LCD_CONTROLLER_GC9A01
#define EXAMPLE_LCD_H_RES 240
#define EXAMPLE_LCD_V_RES 240
#elif CONFIG_EXAMPLE_LCD_CONTROLLER_ST7789
#define EXAMPLE_LCD_H_RES 240
#define EXAMPLE_LCD_V_RES 320
#endif
2
3
4
5
6
7
8
9
10
11
现在我们在CONFIG_EXAMPLE_LCD_CONTROLLER_ILI9341代码上双击,这一串代码会被全部选中,按Crtl+F键,调出“寻找”小工具,位于右上角的位置,并且可以看到此时要寻找的是CONFIG_EXAMPLE_LCD_CONTROLLER_ILI9341,如果你这里显示的不是,就复制这一串字符到寻找的输入框里面就可以。然后这里显示一共有3项,点击上下箭头,调整到第3项,位于215行,因为刚才增加了3行,你搜索到的可能是在215行附近,如下所示:
#if CONFIG_EXAMPLE_LCD_CONTROLLER_ILI9341
ESP_LOGI(TAG, "Install ILI9341 panel driver");
ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(io_handle, &panel_config, &panel_handle));
#elif CONFIG_EXAMPLE_LCD_CONTROLLER_GC9A01
ESP_LOGI(TAG, "Install GC9A01 panel driver");
ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle));
#endif
2
3
4
5
6
7
这里我们再增加ST7789的选项,修改后如下:
#if CONFIG_EXAMPLE_LCD_CONTROLLER_ILI9341
ESP_LOGI(TAG, "Install ILI9341 panel driver");
ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(io_handle, &panel_config, &panel_handle));
#elif CONFIG_EXAMPLE_LCD_CONTROLLER_GC9A01
ESP_LOGI(TAG, "Install GC9A01 panel driver");
ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle));
#elif CONFIG_EXAMPLE_LCD_CONTROLLER_ST7789
ESP_LOGI(TAG, "Install ST7789 panel driver");
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
#endif
2
3
4
5
6
7
8
9
10
现在,我们再修改一下idf_component.yml这个文件里面的内容。这个文件可以指定你要下载的官方组件代码。点击打开后,代码如下:
dependencies:
idf: ">=4.4"
lvgl/lvgl: "~8.3.0"
esp_lcd_ili9341: "^1.0"
esp_lcd_gc9a01: "^1.0"
esp_lcd_touch_stmpe610: "^1.0"
2
3
4
5
6
现在我们不需要ILI9341、GC9A01、STMPE610的组件代码,所以这里把他们删掉或者注释掉,我是注释了,代码如下,前面加了#号,就表示把它注释掉了。
dependencies:
idf: ">=4.4"
lvgl/lvgl: "~8.3.0"
# esp_lcd_ili9341: "^1.0"
# esp_lcd_gc9a01: "^1.0"
# esp_lcd_touch_stmpe610: "^1.0"
2
3
4
5
6
然后编译下载到开发板上以后,显示出了界面,但是是镜像的,所以我们还需要修改一下镜像。
我们找到位于app_main函数中的镜像函数esp_lcd_panel_mirror,可以利用Ctrl+F搜索一下,注意是app_main中的镜像函数,原代码是这样写的:
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, true, false));
函数中有3个参数,其中第2个参数表示是否关于X轴镜像,第3个参数表示是否关于Y轴镜像。我们修改代码如下:
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, false, false));
再次编译下载,就可以看到画面不再镜像。不过,此时的颜色,其实是反色的,并不是原色。这个看lvgl_demo_ui.c文件中绘制图形用的颜色就可以看出来。
我们找到位于app_main函数中的esp_lcd_panel_invert_color函数,这个函数就在esp_lcd_panel_mirror函数的上面,源代码如下:
#if CONFIG_EXAMPLE_LCD_CONTROLLER_GC9A01
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true));
#endif
2
3
如果型号是GC9A01,才会调用,所以这条语句并没有调用,我们可以把ST7789的条件判断也加进去,如下所示:
#if CONFIG_EXAMPLE_LCD_CONTROLLER_GC9A01
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true));
#elif CONFIG_EXAMPLE_LCD_CONTROLLER_ST7789
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true));
#endif
2
3
4
5
我们再次编译下载后看到,颜色进行了反色处理,但是对照lvgl_demo_ui.c中的绘制颜色,还是不对,这是因为还有一个地方需要修改。在打开209行到213行的位置,可以配置RGB格式,现在的模式是BGR,我们改成RGB。改好后如下所示:
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = EXAMPLE_PIN_NUM_LCD_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = 16,
};
2
3
4
5
6
我们修改的是上面代码的第4行,把LCD_RGB_ELEMENT_ORDER_BGR改成了LCD_RGB_ELEMENT_ORDER_RGB。
然后再编译下载,显示界面就正常了。
第10章 LCD触摸
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:08-spi_lcd_touch.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
10.1 触摸屏介绍
开发板上的触摸屏是电容触摸屏,用手指就可以触摸,支持双指触摸。触摸芯片型号是FT6336,使用I2C接口与ESP32-C3连接,I2C地址为0x38。
10.2 添加触摸屏组件
上一个例程,我们实现了LCD的显示,本例程,我们实现LCD触摸。 上一个例程显示的画面中,有一个ROTATE按钮,实现触摸后,这个按钮就可以按了,按一下,里面的meter就会旋转90度。 我们把上一个例程,就在实验文件夹里面复制粘贴一下,然后修改名称为spi_lcd_touch。修改后的工程路径为D:\esp32c3\spi_lcd_touch。 使用VSCode打开spi_lcd_touch工程文件夹。 点击打开工程目录下的CMakeLists.txt文件,修改工程名称为spi_lcd_touch。
project(spi_lcd_touch)
乐鑫官方有个在线的组件管理工具,网址是:https://components.espressif.com/ 这个工具提供了很多常用的组件代码,有很多常用芯片的驱动函数。我们开发板上使用的电容触摸屏控制芯片是FT6336,我们在官网的搜索框中输入FT6336搜索,发现有一个组件,经过我的验证,不太好用。但是又发现一个好用的,你可以搜索ft5x06,这个型号和我们的FT6336代码是通用的,如下图所示。
点击进去,在网页的右侧有使用方法,如下图所示。 我们点击复制按钮,复制阴影里面的内容,然后点击VSCode左下角的“终端”图标打开终端,出现一个你的工程路径,不管有没有连接开发板都可以。 然后把刚才复制的内容粘贴到这里,只需要在输入的这里点击右键就可以粘贴,粘贴完以后点击回车。PS D:\esp32c3\spi_lcd_touch> idf.py add-dependency "espressif/esp_lcd_touch_ft5x06^1.0.6"
回车后出现如下提示,表示成功添加组件。
PS D:\esp32c3\spi_lcd_touch> idf.py add-dependency "espressif/esp_lcd_touch_ft5x06^1.0.6"
Executing action: add-dependency
Successfully added dependency "espressif/esp_lcd_touch_ft5x06^1.0.6" to component "main"
PS D:\esp32c3\spi_lcd_touch>
2
3
4
然后我们点击打开idf_component.yml文件,会发现这里面多了一行添加组件的代码,就是下面代码中的第2行。
dependencies:
espressif/esp_lcd_touch_ft5x06: "^1.0.6"
idf: ">=4.4"
lvgl/lvgl: "~8.3.0"
# esp_lcd_ili9341: "^1.0"
# esp_lcd_gc9a01: "^1.0"
# esp_lcd_touch_stmpe610: "^1.0"
2
3
4
5
6
7
点击软件左下角的垃圾桶图标,删除之前编译过的所有文件。删除后,会发现build文件夹不在了。 现在我们点击编译按钮,编译一下工程。编译完成后看工程左侧的managed_components组件,原来只有lvgl,现在多了两个关于触摸屏的组件。
10.3 编写程序
接下来,我们点击打开main目录下的spi_lcd_touch_example_main.c文件,修改这里面关于触摸屏的代码部分。 ESP32-C3与FT6336通过I2C通信(地址是0x38),我们需要给代码中添加I2C初始化的代码。在电脑硬盘上,复制之前用过的(例如地磁传感器例程中)myi2c代码到本工程的main文件夹。
然后在VSCode下点击打开main下的CMakeLists.txt文件,看看是否添加好了myi2c.c的编译路径,如果没有自动添加,你就自己把myi2c.c写进入,最后如下所示:idf_component_register(SRCS "myi2c.c" "spi_lcd_touch_example_main.c" "lvgl_demo_ui.c"
INCLUDE_DIRS ".")
2
在spi_lcd_touch_example_main.c文件的最开始处添加包含头文件myi2c.h
#include "myi2c.h"
在app_main函数的最开始处添加调用I2C初始化代码。
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
2
至此,I2C初始化工作完成。 上一个章节,我们知道,LCD驱动芯片型号默认的是ILI9341和GC9A01,然后我们在menuconfig中添加自定义选项ST7789来配置LCD驱动芯片型号。现在,我们打开menuconfig,点击Example Configuration后,再点击勾选Enable LCD touch,又出现一个选项LCD touch controller model,里面只有一个型号STMPE610,我们需要给它添加FT6336,我们先关闭menuconfig。
点击打开Kconfig.projbuild文件,修改触摸屏部分,增加FT6336的选型,如下代码所示。这里面的help是提示作用。 choice EXAMPLE_LCD_TOUCH_CONTROLLER
prompt "LCD touch controller model"
depends on EXAMPLE_LCD_TOUCH_ENABLED
default EXAMPLE_LCD_TOUCH_CONTROLLER_STMPE610
help
Select LCD touch controller model
config EXAMPLE_LCD_TOUCH_CONTROLLER_STMPE610
bool "STMPE610"
config EXAMPLE_LCD_TOUCH_CONTROLLER_FT6336
bool "FT6336"
endchoice
2
3
4
5
6
7
8
9
10
11
12
13
保存关闭这个文件后,先点击VSCode软件中左下角的文件夹图标,更新一下当前工程,目的是让menuconfig中添加刚才添加的ft6336选项。
然后我们再打开menuconfig,选择LCD驱动型号为ST7789,然后勾选LCD touch,然后选择触摸驱动为FT6336,如下图所示。在include包含完头文件后面一些,有一个条件编译CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_STMPE610,这里我们增加FT6336的条件编译,增加FT6336的头文件,代码修改后如下:
#if CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_STMPE610
#include "esp_lcd_touch_stmpe610.h"
#elif CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_FT6336
#include "esp_lcd_touch_ft5x06.h"
#endif
2
3
4
5
接下来,修改app_main中的代码。大概从243行开始的这里,有个CONFIG_EXAMPLE_LCD_TOUCH_ENABLED的条件编译。这里有一行代码是这样的:
esp_lcd_panel_io_spi_config_t tp_io_config = ESP_LCD_TOUCH_IO_SPI_STMEP610_CONFIG(EXAMPLE_PIN_NUM_TOUCH_CS);
这里需要修改四处地方,第一个是把类型定义(esp_lcd_panel_io_spi_config_t )里面的spi修改为i2c,第二个是把配置函数名称(ESP_LCD_TOUCH_IO_SPI_STMEP610_CONFIG)里面的SPI改成I2C,第三个是把配置函数名称中的STMPE610改成了FT5x06,注意这里的5x06的x是小写。最后一个是把配置函数括号里面的EXAMPLE_PIN_NUM_TOUCH_CS删除,最后代码如下所示:
esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_FT5x06_CONFIG();
改完后,我们把鼠标放到ESP_LCD_TOUCH_IO_I2C_FT5x06_CONFIG()代码上面单击右键弹出菜单,选择第一个“转到定义”,会发现这是一个宏定义,在esp_lcd_touch_ft5x06.h文件中定义。
回到spi_lcd_touch_example_main.c文件,把app_main函数中的下面这两行代码删掉。因为我们的触摸屏是I2C接口,这里不需要Attach附着。// Attach the TOUCH to the SPI bus
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &tp_io_config, &tp_io_handle));
2
接下来是tp_cfg配置,不需要修改。
再接下来,我们找到下面这几行代码,这几行代码是添加驱动的。
#if CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_STMPE610
ESP_LOGI(TAG, "Initialize touch controller STMPE610");
ESP_ERROR_CHECK(esp_lcd_touch_new_spi_stmpe610(tp_io_handle, &tp_cfg, &tp));
#endif // CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_STMPE610
2
3
4
把这几行代码复制粘贴到它的后面,添加后,再修改为相应的驱动名称,最后代码如下所示:
#if CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_STMPE610
ESP_LOGI(TAG, "Initialize touch controller STMPE610");
ESP_ERROR_CHECK(esp_lcd_touch_new_spi_stmpe610(tp_io_handle, &tp_cfg, &tp));
#endif // CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_STMPE610
#if CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_FT6336
ESP_LOGI(TAG, "Initialize touch controller FT6336");
ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_ft5x06(tp_io_handle, &tp_cfg, &tp));
#endif // CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_FT6336
2
3
4
5
6
7
8
上面代码中,第2个if编译,是我们修改的。
把鼠标放到esp_lcd_touch_new_i2c_ft5x06上面单击右键选择“转到定义”,会发现这个函数是在esp_lcd_touch_ft5x06.c文件中定义的。
粘贴完以后,我们再找到esp_lcd_touch_ft5x06.c文件点击打开,拖到最后面,有两个函数,分别是I2C读写函数。代码如下所示:
static esp_err_t touch_ft5x06_i2c_write(esp_lcd_touch_handle_t tp, uint8_t reg, uint8_t data)
{
assert(tp != NULL);
// *INDENT-OFF*
/* Write data */
return esp_lcd_panel_io_tx_param(tp->io, reg, (uint8_t[]){data}, 1);
// *INDENT-ON*
}
static esp_err_t touch_ft5x06_i2c_read(esp_lcd_touch_handle_t tp, uint8_t reg, uint8_t *data, uint8_t len)
{
assert(tp != NULL);
assert(data != NULL);
/* Read data */
return esp_lcd_panel_io_rx_param(tp->io, reg, data, len);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我们需要修改的就是这两个函数,改完后如下所示:
static esp_err_t touch_ft5x06_i2c_write(esp_lcd_touch_handle_t tp, uint8_t reg, uint8_t data)
{
assert(tp != NULL);
// *INDENT-OFF*
/* Write data */
// return esp_lcd_panel_io_tx_param(tp->io, reg, (uint8_t[]){data}, 1);
// *INDENT-ON*
uint8_t write_buf[2] = {reg, data};
return i2c_master_write_to_device(0, 0x38, write_buf, sizeof(write_buf), 1000 / portTICK_PERIOD_MS);
}
static esp_err_t touch_ft5x06_i2c_read(esp_lcd_touch_handle_t tp, uint8_t reg, uint8_t *data, uint8_t len)
{
assert(tp != NULL);
assert(data != NULL);
/* Read data */
// return esp_lcd_panel_io_rx_param(tp->io, reg, data, len);
return i2c_master_write_read_device(0, 0x38, ®, 1, data, len, 1000 / portTICK_PERIOD_MS);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这其中的0x38是FT6336的I2C地址。 现在,我们编译下载到开发板,然后用手指按ROTATE按钮,就可以看到屏幕旋转了90度,但是显示不正常。还需要再修改代码。 点击按钮后的处理函数是example_lvgl_port_update_callback,这个函数位于app_main函数上面一点,我们找到这个函数,发现这个函数是一个switch case结构,有4种case,分别对应旋转的4个方向。默认是LV_DISP_ROT_NONE,点击一次按钮后,变成LV_DISP_ROT_90,再点击一次变成LV_DISP_ROT_180,再点击一次变成LV_DISP_ROT_270,再点击一次回到LV_DISP_ROT_NONE。 刚才我们做实验,点击了一次,说明函数进LV_DISP_ROT_90进行了处理。
case LV_DISP_ROT_90:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, true);
esp_lcd_panel_mirror(panel_handle, true, true);
2
3
4
我们刚才点击后看到,xy坐标是转换过来了,就是x方向有镜像,所以我们改成下面的代码。
case LV_DISP_ROT_90:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, true);
esp_lcd_panel_mirror(panel_handle, false, true);
2
3
4
然后再编辑下载到开发板,点击ROTATE按钮后,发现旋转了90度后可以正常显示了,然后我们再点击ROTATE按钮,发现旋转后的图像在x方向镜像,所以我们把180的代码修改一下,修改后如下所示:
case LV_DISP_ROT_180:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, false);
esp_lcd_panel_mirror(panel_handle, true, true);
2
3
4
修改好以后,再次编辑下载到开发板看结果,发现270还是在x方向镜像,修改后如下所示:
case LV_DISP_ROT_270:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, true);
esp_lcd_panel_mirror(panel_handle, true, false);
2
3
4
修改好以后,再次编辑下载到开发板看结果,发现最后回到NONE的时候,x方向镜像了,所以我们再修改一下NONE的代码,修改后如下:
case LV_DISP_ROT_NONE:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, false);
esp_lcd_panel_mirror(panel_handle, false, false);
2
3
4
修改好以后,再次编辑下载到开发板看结果,这次就发现都正常了。总结一下,修改每个case里面mirror函数的x轴参数就可以,把原来的true改成false,把false改成true。
第11章 LVGL Demo
代码下载
链接在资料下载中心
章节百度网盘中的例程代码(IDF)!!
文件名称:09-lvgl_demo.zip
上面是已经做好的例程。
下面是本例程制作的全部过程。
11.1 LVGL Demo介绍
上面两个例程,LCD显示使用的是LVGL的Meter小部件。LVGL官方还给我们提供了几个比较完整的Demo,位于lvgl__lvgl\demos文件夹中。我们可以在开发板上跑这些Demo,看看效果。
Benchmark demo演示了矩形、边框、阴影、文本、图像混合等各种情况下的性能测试。
keypad_encoder演示了按钮、下拉列表、滚轮、滑块、开关和文本输入的应用。
music演示了一个音乐播放界面和音乐列表。
stress演示了一个压力测试程序,其中包含了大量的对象创建、删除、动画、样式使用等。
widgets演示了一个电商页面,比较接近于实际应用,其中文本输入框还可以调出26键键盘输入。
11.2 编写程序
把上一章节编写好的例程spi_lcd_touch复制粘贴到同一个文件夹,也就是我们的实验文件夹,然后重命名为lvgl_demo,修改后的工程路径为D:\esp32c3\lvgl_demo。
使用VSCode打开lvgl_demo工程文件夹。打开以后,先点击垃圾桶图标,删除之前的编译过程文件,然后点击打开CMakeList.txt文件给工程命名为lvgl_demo,然后保存关闭此文件。
project(lvgl_demo)
现在我们在VSCode左侧面板依次点击打开managed_components\espressifesp_lcd_touch\lvgllvgl\demos,就可以看到这里提供的几个demo名称。
点击打开benchmark这个例程,看看它下面都有哪些代码。其中,最后两张png图片,就是这个示例的效果演示,可以点击查看。 如上图所示,我们发现这个例程是横屏的。而现在我们的例程默认是竖屏的,所以我们先把屏改成默认横屏的。需要修改几处地方。 点击打开spi_lcd_touch_example_main.c文件,在前面一点找到如下配置分辨率的地方,把320改成240,把240改成320,下面是我改好后的代码。#elif CONFIG_EXAMPLE_LCD_CONTROLLER_ST7789
#define EXAMPLE_LCD_H_RES 320 // 水平方向
#define EXAMPLE_LCD_V_RES 240 // 垂直方向
#endif
2
3
4
然后找到example_lvgl_port_update_callback函数,现在的case分别是NONE、90、180、270,默认是NONE,是竖屏,我们需要把默认改成横屏,就是默认是90的样子,所以,我们现在把90改成NONE,然后把180改成90,把270改成180,把NONE改成270。接下来需要改一下触摸屏的touch mirror,我们看原来的NONE和现在的NONE中panel mirror的区别,就是把y进行了mirror,所以现在也需要把touch的y进行mirror。 修改后的代码如下所示。
/* Rotate display and touch, when rotated screen in LVGL. Called when driver parameters are updated. */
static void example_lvgl_port_update_callback(lv_disp_drv_t *drv)
{
esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) drv->user_data;
switch (drv->rotated) {
case LV_DISP_ROT_90:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, false);
esp_lcd_panel_mirror(panel_handle, false, false);
#if CONFIG_EXAMPLE_LCD_TOUCH_ENABLED
// Rotate LCD touch
esp_lcd_touch_set_mirror_y(tp, false);
esp_lcd_touch_set_mirror_x(tp, true);
#endif
break;
case LV_DISP_ROT_180:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, true);
esp_lcd_panel_mirror(panel_handle, false, true);
#if CONFIG_EXAMPLE_LCD_TOUCH_ENABLED
// Rotate LCD touch
esp_lcd_touch_set_mirror_y(tp, false);
esp_lcd_touch_set_mirror_x(tp, true);
#endif
break;
case LV_DISP_ROT_270:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, false);
esp_lcd_panel_mirror(panel_handle, true, true);
#if CONFIG_EXAMPLE_LCD_TOUCH_ENABLED
// Rotate LCD touch
esp_lcd_touch_set_mirror_y(tp, false);
esp_lcd_touch_set_mirror_x(tp, true);
#endif
break;
case LV_DISP_ROT_NONE:
// Rotate LCD display
esp_lcd_panel_swap_xy(panel_handle, true);
esp_lcd_panel_mirror(panel_handle, true, false);
#if CONFIG_EXAMPLE_LCD_TOUCH_ENABLED
// Rotate LCD touch
esp_lcd_touch_set_mirror_y(tp, false);
esp_lcd_touch_set_mirror_x(tp, true);
#endif
break;
}
}
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
接下来修改app_main函数。在app_main函数中,找到esp_lcd_panel_mirror函数,原来的mirror,xy都是false不翻转,从上一个函数中我们知道,y需要翻转,所以我们把y参数位置变成true。然后我们需要在这个函数语句前面增加swap_xy函数,因为竖屏变成横屏,XY左边也需要翻转。所以最后的代码修改为:
ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel_handle, true));
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, false, true));
2
就在上面两个函数下面有一个CONFIG_EXAMPLE_LCD_TOUCH_ENABLED条件编译,这里面修改触摸屏的初始化值,需要修改x_max、y_max、swap_xy和mirror_y。其中,x_max和y_max对调,因为原来竖屏时候的x最大值是240,y轴的最大值是320,现在变成横屏,x的最大值变成了320,y轴的最大值变成了240。然后是swap_xy和mirror_y,原来是0,现在修改为1。修改后的代码如下所示:
esp_lcd_touch_config_t tp_cfg = {
.x_max = EXAMPLE_LCD_V_RES,
.y_max = EXAMPLE_LCD_H_RES,
.rst_gpio_num = -1,
.int_gpio_num = -1,
.flags = {
.swap_xy = 1,
.mirror_x = 1,
.mirror_y = 0,
},
};
2
3
4
5
6
7
8
9
10
11
到这里为止,竖屏改横屏的工作就做完了,现在就可以编译下载看结果了,不过,需要先修改一下menuconfig,因为我们在最开始点了垃圾桶图标,删除了一些配置,现在需要重新打开配置一下FLASH大小,默认是2MB,修改为8MB。
编译下载看结果,再点击按钮试试,如果正常,就代表代码修改成功,否则翻回去和我的代码对比一样看看哪里出问题了。
接下来我们修改代码跑benchmark demo。
打开menuconfig,左边下拉到最后面,点击最后的Demos菜单,然后在右面找到Benchmark your system,在前面勾选。然后保存关闭。
//example_lvgl_demo_ui(disp);
lv_demo_benchmark();
2
调用lv_demo_benchmark函数,需要添加头文件lv_demos.h,我们把这个头文件添加到文件最开始包含头文件的地方就可以了。
#include "lv_demos.h"
到这里,benchmark程序就完成了,我们再编译下载看结果。液晶屏上开始跑demo,还会提示帧率,跑完以后,会出现一张表,可以用手指滑动看每个项目的帧率统计值。
接下来看keypad_encoder例程。
打开menuconfig,左边下拉到最后面,点击最后的Demos菜单,然后在右面的Demos下面,只勾选Demonstrate the usage of encoder and keyboard,然后保存关闭。
//example_lvgl_demo_ui(disp);
//lv_demo_benchmark();
lv_demo_keypad_encoder();
2
3
编译下载看结果。这个实验中有各种交互式选项。
接下来看stress例程。
打开menuconfig,左边下拉到最后面,点击最后的Demos菜单,然后在右面的Demos下面,只勾选Stress test for LVGL,然后保存关闭。
在app_main函数里面,把lv_demo_keypad_encoder注释掉,添加lv_demo_stress函数。
//example_lvgl_demo_ui(disp);
//lv_demo_benchmark();
//lv_demo_keypad_encoder();
lv_demo_stress();
2
3
4
编译下载看结果。液晶屏最后显示的画面,就是一堆乱七八糟的项目在更替,就和这个例程文件夹中的gif动态图片显示的效果一样。
接下来看widgets例程。
打开menuconfig,左边下拉到最后面,点击最后的Demos菜单,然后在右面的Demos下面,勾选Show some widget和Enable slide show,然后保存关闭。
//example_lvgl_demo_ui(disp);
//lv_demo_benchmark();
//lv_demo_keypad_encoder();
//lv_demo_stress();
lv_demo_widgets();
2
3
4
5
这时候如果你编译,就会有如下错误提示:
#error Insufficient memory for lv_demo_widgets. Please set LV_MEM_SIZE to at least 38KB (38ul * 1024ul). 48KB
is recommended.
2
意思是内存不够,提示:至少要38KB,推荐48KB。
我们可以在menuconfig中修改lvgl内存大小。打开menuconfig,左侧拉到最后,点击Memory settings,右侧的Size of the memory used by 'lv_mem_alloc' in kilobytes中,原来是32,我们改成48。
接下来看music例程。
从music例程的README.md文件中可以看到,这个例程的最佳分辨率是480x272,我们的LCD分辨率是320x240,所以最后的显示效果不是很好。
打开menuconfig,左边下拉到最后面,点击最后的Demos菜单,然后在右面的Demos下面,只勾选Music player demo,然后保存关闭。
//example_lvgl_demo_ui(disp);
//lv_demo_benchmark();
//lv_demo_keypad_encoder();
//lv_demo_stress();
//lv_demo_widgets();
lv_demo_music();
2
3
4
5
6
现在编译的话,会报错,提示lv_font_montserrat_12和lv_font_montserrat_16没有定义。这是因为没有把宏定义打开,我们再次打开menuconfig,左边下拉到最后面,点击Font usage菜单,右侧勾选Enable Montserrat 12和Enable Montserrat 16,14是默认勾选的,然后保存,关闭。
再次编译就不会报错了。下载看结果。因为我们的屏幕的分辨率比它推荐的小,所以画面显示不全。这个例程只是画面演示,也没有添加真正的音乐进去。到此,LVGL的全部自带Demo,就都演示完毕了。