一.GPIO输入输出配置
想要配置GPIO为简单的高低电平输出有2种方法
方式一:基本方式
1:将GPIO设置成普通IO口 | gpio_pad_select_gpio(需要设置的IO口) |
2:设置GPIO模式 | gpio_set_direction(需要设置的IO口,IO模式) |
3:设置默认电平(输入模式不需要) | gpio_set_level(需要设置的IO口,0/1) |
代码如下:
void LED_GPIO_Init(void)
{
gpio_pad_select_gpio(GPIO_LED); // 选择GPIO口
gpio_set_direction(GPIO_LED, GPIO_MODE_OUTPUT); // GPIO作为输出,GPIO模式如下
//GPIO_MODE_DISABLE 禁用输入和输出;
//GPIO_MODE_INPUT 仅输入;
//GPIO_MODE_OUTPUT 仅输出模式;
//GPIO_MODE_OUTPUT_OD 只输出开漏模式;
//GPIO_MODE_INPUT_OUTPUT_OD 输出和输入采用开漏模式;
//GPIO_MODE_INPUT_OUTPUT 输出和输入模式
gpio_set_level(GPIO_LED, 0); // 默认低电平
}
方式二:结构体方式
通过配置gpio_config_t结构体来配置GPIO,再通过gpio_config(配置好的结构体)函数使能.
代码如下:
#define GPIO_SET 22 // 配置GPIO口
#define GPIO_SET_IO (1ULL<<GPIO_Set) // 配置GPIO_Set位寄存器
void gpio_init(void)
{
gpio_config_t io_conf; // 定义一个gpio_config类型的结构体,下面的都算对其进行的配置
io_conf.intr_type = GPIO_INTR_DISABLE; // 禁止中断,中断模式如下
//GPIO_INTR_POSEDGE 上升沿中断
//GPIO_INTR_NEGEDGE 下降沿中断
//GPIO_INTR_ANYEDGE 上升沿和下降沿中断
//GPIO_INTR_LOW_LEVEL 输入低电平触发中断
//GPIO_INTR_HIGH_LEVEL 输入高电平触发中断
//GPIO_INTR_DISABLE 禁止中断
io_conf.mode = GPIO_MODE_OUTPUT; // 选择输出模式
//GPIO_MODE_DISABLE 禁用输入和输出;
//GPIO_MODE_INPUT 仅输入;
//GPIO_MODE_OUTPUT 仅输出模式;
//GPIO_MODE_OUTPUT_OD 只输出开漏模式;
//GPIO_MODE_INPUT_OUTPUT_OD 输出和输入采用开漏模式;
//GPIO_MODE_INPUT_OUTPUT 输出和输入模式
io_conf.pin_bit_mask = GPIO_SET_IO; // 配置GPIO_OUT寄存器
io_conf.pull_down_en = 0; // 禁止下拉
io_conf.pull_up_en = 0; // 禁止上拉
gpio_config(&io_conf); // 最后配置使能
}
gpio_set_level(GPIO_SET, 0); // 把这个GPIO输出低电平
gpio_set_level(GPIO_SET, 1); // 把这个GPIO输出高电平
二.GPIO中断配置
- 通过配置gpio_config_t结构体将GPIO配置成需要的中断模式并注册结构体;
- 通过函数gpio_install_isr_service(优先级参数)安装GPIO全局中断服务,即所有GPIO中断共享一个优先级!如需单独设置GPIO优先级参考后续学习笔记!
- 最后是通过函数gpio_isr_handler_add(GPIO,相应 ISR 处理函数,ISR 处理程序的参数);注册中断回调函数开启中断;
具体代码如下
#define GPIO_SET 22 // 配置GPIO口
#define GPIO_SET_IO (1ULL<<GPIO_Set) // 配置GPIO_Set位寄存器
void gpio_init(void)
{
gpio_config_t io_conf; // 定义一个gpio_config类型的结构体,下面的都算对其进行的配置
io_conf.intr_type = GPIO_INTR_NEGEDGE; // 禁止中断,中断模式如下
//GPIO_INTR_POSEDGE 上升沿中断
//GPIO_INTR_NEGEDGE 下降沿中断
//GPIO_INTR_ANYEDGE 上升沿和下降沿中断
//GPIO_INTR_LOW_LEVEL 输入低电平触发中断
//GPIO_INTR_HIGH_LEVEL 输入高电平触发中断
//GPIO_INTR_DISABLE 禁止中断
io_conf.mode = GPIO_MODE_INPUT; // 选择输出模
io_conf.pin_bit_mask = GPIO_SET_IO; // 配置GPIO_OUT寄存器
io_conf.pull_down_en = 0; // 禁止下拉
io_conf.pull_up_en = 1; // 引脚电平上拉
gpio_config(&io_conf); // 最后配置使能
gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1); //注册GPIO全局中断服务,参数为优先级如下
/*具体参阅 esp_intr_alloc.h
ESP_INTR_FLAG_LEVEL1 //< Accept a Level 1 interrupt vector (lowest priority)
ESP_INTR_FLAG_LEVEL2 //< Accept a Level 2 interrupt vector
ESP_INTR_FLAG_LEVEL3 //< Accept a Level 3 interrupt vector
ESP_INTR_FLAG_LEVEL4 //< Accept a Level 4 interrupt vector
ESP_INTR_FLAG_LEVEL5 //< Accept a Level 5 interrupt vector
ESP_INTR_FLAG_LEVEL6 //< Accept a Level 6 interrupt vector
ESP_INTR_FLAG_NMI //< Accept a Level 7 interrupt vector (highest priority)
ESP_INTR_FLAG_SHARED //< Interrupt can be shared between ISRs
ESP_INTR_FLAG_EDGE //< Edge-triggered interrupt
ESP_INTR_FLAG_IRAM //< ISR can be called if cache is disabled
ESP_INTR_FLAG_INTRDISABLED //< Return with this interrupt disabled
ESP_INTR_FLAG_LOWMED //< Low and medium prio interrupts.These can be handled in C.
ESP_INTR_FLAG_HIGH //< High level interrupts. Need to be handled in assembly.
ESP_INTR_FLAG_LEVELMASK //< Mask for all level flags
*/
gpio_isr_handler_add(GPIO_SET, gpio_isr_handler,NULL);//添加中断处理函数
}
void IRAM_ATTR gpio_isr_handler()//中断回调函数
{
;//中断内容
}
拓展:ESP32 芯片具有 34 个物理 GPIO 焊盘,对应功能如下表
通用输入输出口 | 模拟功能 | 实时时钟通用输入输出 | 注释 |
---|---|---|---|
GPIO0 | ADC2_CH1 | RTC_GPIO11 | Strapping pin;电容式触摸GPIO |
GPIO1 | TXD | ||
GPIO2 | ADC2_CH2 | RTC_GPIO12 | Strapping pin;电容式触摸GPIO |
GPIO3 | RXD | ||
GPIO4 | ADC2_CH0 | RTC_GPIO10 | 电容式触摸GPIO |
GPIO5 | Strapping pin | ||
GPIO6 | SPI0/1 | ||
GPIO7 | SPI0/1 | ||
GPIO8 | SPI0/1 | ||
GPIO9 | SPI0/1 | ||
GPIO10 | SPI0/1 | ||
GPIO11 | SPI0/1 | ||
GPIO12 | ADC2_CH5 | RTC_GPIO15 | Strapping pin;JTAG;电容式触摸GPIO |
GPIO13 | ADC2_CH4 | RTC_GPIO14 | JTAG;电容式触摸GPIO |
GPIO14 | ADC2_CH6 | RTC_GPIO16 | JTAG;电容式触摸GPIO |
GPIO15 | ADC2_CH3 | RTC_GPIO13 | Strapping pin;JTAG;电容式触摸GPIO |
GPIO16 | SPI0/1 | ||
GPIO17 | SPI0/1 | ||
GPIO18 | |||
GPIO19 | |||
GPIO20 | This pin is only available on ESP32-PICO-V3 chip package | ||
GPIO21 | |||
GPIO22 | |||
GPIO23 | |||
GPIO25 | ADC2_CH8 | RTC_GPIO6 | |
GPIO26 | ADC2_CH9 | RTC_GPIO7 | |
GPIO27 | ADC2_CH7 | RTC_GPIO17 | 电容式触摸GPIO |
GPIO32 | ADC1_CH4 | RTC_GPIO9 | 电容式触摸GPIO |
GPIO33 | ADC1_CH5 | RTC_GPIO8 | 电容式触摸GPIO |
GPIO34 | ADC1_CH6 | RTC_GPIO4 | GPI |
GPIO35 | ADC1_CH7 | RTC_GPIO5 | GPI |
GPIO36 | ADC1_CH0 | RTC_GPIO0 | GPI |
GPIO37 | ADC1_CH1 | RTC_GPIO1 | GPI |
GPIO38 | ADC1_CH2 | RTC_GPIO2 | GPI |
GPIO39 | ADC1_CH3 | RTC_GPIO3 | GPI |
需要注意:
- GPI:GPIO34-39只能设置为输入模式,没有软件使能的上拉或下拉功能
- TXD & RXD 通常用于刷机和调试
- ADC2:使用 Wi-Fi 时不能使用 ADC2 引脚。因此,如果您使用 Wi-Fi 并且无法从 ADC2 GPIO 获取值,您可以考虑改用 ADC1 GPIO
- 使用 ADC 或睡眠模式下使用 Wi-Fi 和蓝牙时,请不要使用 GPIO36 和 GPIO39 的中断。有关问题的详细描述,请参考 ESP32 ECO 和 Bug 解决方法 > 中的第 3.11 节。
- Strapping 引脚:GPIO0、GPIO2、GPIO5(开机时必须为高电平)、GPIO12 (MTDI)(开机时必须为高电平) 和 GPIO15 (MTDO)(开机时必须为高电平) 是 Strapping 引脚。更多信息,请参考ESP32 datasheet
- SPI0/1:GPIO6-11 和 GPIO16-17 通常连接到模块集成的 SPI flash 和 PSRAM,因此不应用作其他用途。
- JTAG:GPIO12-15 通常用于在线调试。
-
ESP32有两个I2C通道,任何引脚都可以设置为SDA或SCL.默认的I2C引脚是GPIO 21 (SDA);GPIO 22 (SCL)
-
默认情况下,SPI的引脚映射是:
-
每个GPIO的绝对最大电流为40mA
-
有些GPIO在启动或复位时,会将其状态变为HIGH或输出PWM信号.这意味着,如果您的输出连接到这些GPIO上,当ESP32复位或启动时,您可能会得到意想不到的结果
GPIO 1
GPIO 3
GPIO 5
GPIO 6-11(连接到ESP32集成的SPI闪存——不建议使用)
GPIO 14
GPIO 15 -
EN是3.3V调节器的使能引脚。它是被拉起的,因此连接到GND以禁用3.3V调节器。例如,这意味着您可以使用这个连接到按钮的引脚来重启您的ESP32
三.通用定时器
- 16-bit 时钟预分频器,分频系数为 2-65536
- 64-bit 时基计数器
- 可配置的向上/向下时基计数器:增加或减少
- 暂停和恢复时基计数器
- 报警时自动重新加载
- 当报警值溢出/低于保护值时报警
- 软件控制的即时重新加载
- 电平触发中断和边沿触发中断
16-bit 预分频器是以 APB 时钟(缩写 APB_CLK,频率通常为 80 MHz)作为基础时钟,在使用寄存 器 TIMGn_Tx_DIVIDER 配置分频器除数前,必须关闭定时器.否则会导致不可预知的结果.
64-bit 通用定时器可自动重新加载向上/向下计数器且支持自动重新加载和软件即时重新加载,计数器达到软件设定值时会触发报警事件
想要通过idf创建硬件定时器需要以下步骤
- 包含#include "driver/timer.h"头文件
- 提前提供配置结构体 timer_config_t
- 将结构传递给 timer_init(定时器分组,定时器型号,结构体地址) 函数创建定时器
- 设置定时器预装值 timer_set_counter_value(定时器分组,定时器型号,0x00000000ULL)
- 设置报警阈值 timer_set_alarm_value(定时器分组,定时器型号,3000*(TIMER_BASE_CLK/8/1000))
- 定时器中断使能timer_enable_intr(定时器分组,定时器型号)
-
注册定时器中断函数timer_isr_register(定时器分组,定时器型号,定时器中断回调函数,传递给定时器回调函数的参数,优先级参数,返回的句柄);
-
启动定时器 timer_start(定时器分组,定时器型号);
#include "driver/timer.h"
void Time_Init()
{
timer_config_t config =
{
.divider = 8, //分频系数可以为 2-65536 基础频率为80 MHz
.counter_dir = TIMER_COUNT_UP,
//TIMER_COUNT_UP计数方式为向上计数
//TIMER_COUNT_DOWN为向下计数
.counter_en = TIMER_PAUSE,
//TIMER_PAUSE调用timer_init函数以后不启动计数,调用timer_start时才开始计数
//TIMER_START为调用timer_init函数以后直接开始计数
.alarm_en = TIMER_ALARM_EN,
//TIMER_ALARM_EN到达计数值启动报警对应标志位置1
//TIMER_ALARM_DIS为关闭报警
.auto_reload = 1,
//1 自动重新装载预装值
//0 不重装载预装值
};//设置定时器初始化参数
timer_init(TIMER_GROUP_0,TIMER_0,&config);//初始化定时器
//TIMER_GROUP_0 定时器分组0
//TIMER_GROUP_1 定时器分组0
//TIMER_0 定时器0
//TIMER_1 定时器1
timer_set_counter_value(TIMER_GROUP_0,TIMER_0,0x00000000ULL);//设置定时器预装值
timer_set_alarm_value(TIMER_GROUP_0,TIMER_0,(TIMER_BASE_CLK/8/1000));
//设置报警阈值 TIMER_BASE_CLK 为80M
//例子:1000[=定时1000ms=1s] (TIMER_BASE_CLK[定时器时钟/8[分频系数]/1000[延时为ms级别,因此除以1000])
timer_enable_intr(TIMER_GROUP_0,TIMER_0);//定时器中断使能
timer_isr_register
(
TIMER_GROUP_0,TIMER_0,
timer_group0_isr, //定时器中断回调函数
(void*)TIMER_0, //传递给定时器回调函数的参数
ESP_INTR_FLAG_IRAM, //把中断放到 IRAM 中
NULL //调用成功以后返回中断函数的地址,一般用不到
);//注册定时器中断函数
timer_start(TIMER_GROUP_0,TIMER_0); //启动定时器
}
void IRAM_ATTR timer_group0_isr(void *para)//定时器中断回调函数
{
uint32_t timer_intr = timer_group_get_intr_status_in_isr(TIMER_GROUP_0); //获取中断状态
if (timer_intr & TIMER_INTR_T0)//判断定时器0分组的0号定时器是否产生中断
{
timer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0);//清除中断状态
timer_group_enable_alarm_in_isr(TIMER_GROUP_0, TIMER_0);//重新使能定时器中断
}
}
四.软件定时器
- 引用头文件 #include "esp_timer.h"
- 创建定时器接收句柄 esp_timer_handle_t timer1;
- 提前提供配置结构体 esp_timer_create_args_t
- 创建定时器函数:esp_timer_create(定时器的参数结构体,返回创建的定时器句柄);
- 启动周期定时器函数: esp_timer_start_periodic( 定时器句柄, 定时器定时周期单位us ); 启动单次定时器函数: esp_timer_start_once( 定时器句柄, 定时器定时周期单位us );
- esp_timer_stop(esp_timer_handle_t timer) 停止定时器
- esp_timer_delete(esp_timer_handle_t timer) 删除定时器
#include "esp_timer.h"
#include "driver/timer.h"
esp_timer_handle_t esp_timer_0_handle = 0; /创建/定时器句柄
void Time2_Init()
{
esp_timer_create_args_t esp_timer =
{
.callback = &esp_timer_0, //定时器回调函数
.arg = NULL, //传递给回调函数的参数
.name = "esp_timer0", //定时器名称
};//定时器结构体初始化
esp_timer_create(&esp_timer,&esp_timer_0_handle);//创建定时器
esp_timer_start_periodic(esp_timer_0_handle,1000*1000);//周期定时单位为us 1000*1000=1s
void esp_timer_0()//定时器回调函数
{
;
}
五.硬件I2C
- 支持多设备的总线。“总线”指多个设备共用的信号线,在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
- 一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步
- 同步半双工:数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输
- 每个连接到总线的设备都有一个唯一的地址,主机利用这个地址在不同设备之间的访问。
- 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
- 常用的速率:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s。
I2C通讯一般具有两种方式,分别为软件模拟的方式即控制IO口输出高低电平或硬件 I2C 的方式
ESP32中,已经带了2条通道的硬件的I2C,通过GPIO 交换矩阵实现io口连接到I2C通道,因此只需要调用相关的API接口,就可以实现I2C通讯,步骤如下
- 引用头文件#include "driver/i2c.h"
- 提前提供配置结构体 i2c_config_t i2c_conf
- 配置I2C: i2c_param_config(I2C_OLED_MASTER_NUM, &conf)
- 使能I2C: i2c_driver_install(I2C_OLED_MASTER_NUM, conf.mode,0, 0, 0)
- 配置通信格式
具体详下列代码
一. 配置I2C:
/**
* @brief 配置I2Cx-主机模式,(I2C端口、总线速率、SCL引脚,SDA引脚)
* - 例:i2c_master_init(I2C_NUM_0, 100000, GPIO_NUM_18, GPIO_NUM_19);
*
* @param i2c_num I2C端口号。I2C_NUM_0 / I2C_NUM_1
* @param clk_speed I2C总线速率。单位Hz,多使用 100000 400000
* @param scl_io_num SCL端口号。除仅能做输入 和 6、7、8、9、10、11之外的任意端口。
* @param sda_io_num SDA端口号
*
* @return
* - none
*/
esp_err_t i2c_master_init(i2c_port_t i2c_num, uint32_t clk_speed, gpio_num_t scl_io_num, gpio_num_t sda_io_num)
{
i2c_config_t conf_master;
conf_master.mode = I2C_MODE_MASTER;
conf_master.sda_io_num = sda_io_num;
conf_master.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf_master.scl_io_num = scl_io_num;
conf_master.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf_master.master.clk_speed = clk_speed;
conf_master.clk_flags = 0;
i2c_param_config(i2c_num, &conf_master);
return i2c_driver_install(i2c_num, conf_master.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
}
二. 配置I2C通信协议:
/**
* @brief I2Cx-写从设备的值
* - 不带有写器件寄存器的方式,适用于 BH1750、ADS1115/1118等少数I2C设备,这类设备通常内部寄存器很少
* - 例:i2c_master_write_slave(I2C_NUM_0, 0x68, &test, 1, 100 / portTICK_RATE_MS);
*
* ___________________________________________________________________
* | start | slave_addr + wr_bit + ack | write n bytes + ack | stop |
* --------|---------------------------|----------------------|------|
* @param i2c_num I2C端口号。I2C_NUM_0 / I2C_NUM_1
* @param slave_addr I2C写从机的器件地址
* @param data_wr 写入的值的指针,存放写入进的数据
* @param size 写入的寄存器数目
* @param ticks_to_wait 超时等待时间
*
*/
esp_err_t i2c_master_write_slave(i2c_port_t i2c_num, uint8_t slave_addr, uint8_t *data_wr, size_t size, TickType_t ticks_to_wait)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_WRITE, 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_num, cmd, ticks_to_wait);
i2c_cmd_link_delete(cmd);
return ret;
}
/**
* @brief I2Cx-写从设备的寄存器值
* - 带有写器件寄存器的方式,适用于 MPU6050、ADXL345、HMC5983、MS5611、BMP280等绝大多数I2C设备
* - 例:i2c_master_write_slave_reg(I2C_NUM_0, 0x68, 0x75, &test, 1, 100 / portTICK_RATE_MS);
*
* ____________________________________________________________________________________
* | start | slave_addr + wr_bit + ack | reg_addr + ack | write n bytes + ack | stop |
* --------|---------------------------|----------------|----------------------|------|
*
* @param i2c_num I2C端口号。I2C_NUM_0 / I2C_NUM_1
* @param slave_addr I2C写从机的器件地址
* @param reg_addr I2C写从机的寄存器地址
* @param data_wr 写入的值的指针,存放写入进的数据
* @param size 写入的寄存器数目
* @param ticks_to_wait 超时等待时间
*
* @return
* - esp_err_t
*/
esp_err_t i2c_master_write_slave_reg(i2c_port_t i2c_num, uint8_t slave_addr, uint8_t reg_addr, uint8_t *data_wr, size_t size, TickType_t ticks_to_wait)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN);
i2c_master_write_byte(cmd, reg_addr, 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_num, cmd, ticks_to_wait);
i2c_cmd_link_delete(cmd);
return ret;
}
/**
* @brief I2Cx-读从设备的值
* - 不带有读器件寄存器的方式,适用于 BH1750、ADS1115/1118等少数I2C设备,这类设备通常内部寄存器很少
* - 例:i2c_master_read_slave(I2C_NUM_0, 0x68, &test, 1, 100 / portTICK_RATE_MS);
*
* ________________________________________________________________________________________
* | start | slave_addr + rd_bit + ack | read n-1 bytes + ack | read 1 byte + nack | stop |
* --------|---------------------------|----------------------|--------------------|------|
* @param i2c_num I2C端口号。I2C_NUM_0 / I2C_NUM_1
* @param slave_addr I2C读从机的器件地址
* @param data_rd 读出的值的指针,存放读取出的数据
* @param size 读取的寄存器数目
* @param ticks_to_wait 超时等待时间
*
* @return
* - esp_err_t
*/
esp_err_t i2c_master_read_slave(i2c_port_t i2c_num, uint8_t slave_addr, uint8_t *data_rd, size_t size, TickType_t ticks_to_wait)
{
if (size == 0) {
return ESP_OK;
}
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_READ, ACK_CHECK_EN);
if (size > 1) {
i2c_master_read(cmd, data_rd, size - 1, 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_num, cmd, ticks_to_wait);
i2c_cmd_link_delete(cmd);
return ret;
}
/**
* @brief I2Cx-读从设备的寄存器值
* - 带有读器件寄存器的方式,适用于 MPU6050、ADXL345、HMC5983、MS5611、BMP280等绝大多数I2C设备
* - 例:i2c_master_read_slave_reg(I2C_NUM_0, 0x68, 0x75, &test, 1, 100 / portTICK_RATE_MS);
*
* _____________________________________________________________________________________________________________________________________________
* | start | slave_addr + rd_bit + ack | reg_addr + ack | start | slave_addr + wr_bit + ack | read n-1 bytes + ack | read 1 byte + nack | stop |
* --------|---------------------------|------------------------|---------------------------|----------------------|--------------------|------|
*
* @param i2c_num I2C端口号。I2C_NUM_0 / I2C_NUM_1
* @param slave_addr I2C读从机的器件地址
* @param reg_addr I2C读从机的寄存器地址
* @param data_rd 读出的值的指针,存放读取出的数据
* @param size 读取的寄存器数目
* @param ticks_to_wait 超时等待时间
*
* @return
* - esp_err_t
*/
esp_err_t i2c_master_read_slave_reg(i2c_port_t i2c_num, uint8_t slave_addr, uint8_t reg_addr, uint8_t *data_rd, size_t size, TickType_t ticks_to_wait)
{
if (size == 0) {
return ESP_OK;
}
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN);
i2c_master_write_byte(cmd, reg_addr, ACK_CHECK_EN);
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_READ, ACK_CHECK_EN);
if (size > 1) {
i2c_master_read(cmd, data_rd, size - 1, 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_num, cmd, ticks_to_wait);
i2c_cmd_link_delete(cmd);
return ret;
}
/**
* @brief I2Cx-读从设备的寄存器值(寄存器地址 或 命令 为2字节的器件)
* - 带有读器件寄存器的方式,适用于 SHT20、GT911 这种寄存器地址为16位的I2C设备
* - 例:i2c_master_read_slave_reg_16bit(I2C_NUM_0, 0x44, 0xE000, &test, 6, 100 / portTICK_RATE_MS);
*
* ____________________________________________________________________________________________________________________________________________________
* | start | slave_addr + rd_bit + ack | reg_addr(2byte) + ack | start | slave_addr + wr_bit + ack | read n-1 bytes + ack | read 1 byte + nack | stop |
* --------|---------------------------|-------------------------------|---------------------------|----------------------|--------------------|------|
*
* @param i2c_num I2C端口号。I2C_NUM_0 / I2C_NUM_1
* @param slave_addr I2C读从机的器件地址
* @param reg_addr I2C读从机的寄存器地址(2byte)
* @param data_rd 读出的值的指针,存放读取出的数据
* @param size 读取的寄存器数目
* @param ticks_to_wait 超时等待时间
*
* @return
* - esp_err_t
*/
esp_err_t i2c_master_read_slave_reg_16bit(i2c_port_t i2c_num, uint8_t slave_addr, uint16_t reg_addr, uint8_t *data_rd, size_t size, TickType_t ticks_to_wait)
{
if (size == 0) {
return ESP_OK;
}
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN);
i2c_master_write_byte(cmd, reg_addr>>8, ACK_CHECK_EN);
i2c_master_write_byte(cmd, reg_addr, ACK_CHECK_EN);
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_READ, ACK_CHECK_EN);
if (size > 1) {
i2c_master_read(cmd, data_rd, size - 1, 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_num, cmd, ticks_to_wait);
i2c_cmd_link_delete(cmd);
return ret;
}
六.UART串口通信
使用UART的步骤如下
以下概述描述了如何使用 UART 驱动程序的功能和数据类型在 ESP32 和其他 UART 设备之间建立通信。概述反映了典型的编程工作流程,并分为以下部分:
-
设置通讯参数——设置波特率、数据位、停止位等。
-
设置通信引脚- 分配用于连接到设备的引脚。
-
驱动程序安装- 为 UART 驱动程序分配 ESP32 的资源。
-
运行 UART 通信- 发送/接收数据
-
使用中断- 在特定通信事件上触发中断
-
删除驱动程序- 如果不再需要 UART 通信,释放分配的资源
步骤 1 到 3 包括配置阶段。第 4 步是 UART 开始运行的地方。步骤 5 和 6 是可选的。
一.首先设置UART通信参数,有两种方法
① 单步法(结构体法):配置uart_config_t结构体,再调用函数uart_param_config(UART_NUM_2, 结构体地址)并将一个结构传(指针)递给它
#include "driver/uart.h"
const uart_port_t uart_num = UART_NUM_0;//设置UART端口号UART_NUM_0,UART_NUM_1,UART_NUM_2
uart_config_t uart_config =
{
.baud_rate = 115200,//设置波特率
.data_bits = UART_DATA_8_BITS,//设置传输位数
.parity = UART_PARITY_DISABLE,//设置奇偶校验位
.stop_bits = UART_STOP_BITS_1,//设置停止位
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,//设置硬件流控方式
.rx_flow_ctrl_thresh = 122,//设置硬件RTS阈值
.source_clk = UART_SCLK_APB//设置时钟源
};
// Configure UART parameters
ESP_ERROR_CHECK(uart_param_config(uart_num, &uart_config));
② 分步法:通过调用下表中的专用函数来单独配置特定参数。如果重新配置单个参数,这些功能也很有用。
上述每个函数都有一个_get_对应项来检查当前设置的值。例如要检查当前的波特率值,调用uart_get_baudrate().
二.设置通信引脚
分配引脚的方法是调用函数uart_set_pin(UART编号,TXD引脚,RXD引脚,RTS引脚,CTS引脚)如果没有启用硬件流控制RTS和CTS引脚参数为UART_PIN_NO_CHANGE即不使用该引脚.
uart_set_pin(UART_NUM_0, 4, 5, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
//@param UART编号
//@param TXD引脚
//@param RXD引脚
//@param RTS引脚
//@param CTS引脚
//参数UART_PIN_NO_CHANGE可以将引脚设置为空
三.驱动安装
安装驱动的方法是调用函数uart_driver_install(UART编号,接收(RX)缓冲区大小, 发送(TX)缓冲区大小 (可为零), 想要的 UART 事件队列大小, UART 事件队列句柄(输出参数), 分配中断的标志);
QueueHandle_t eventQueue;
const int uart_buffer_size = (1024 * 2);
uart_driver_install
(
UART_NUM_0, //UART 编号
uart_buffer_size, //Rx 缓冲区大小
uart_buffer_size, //Tx 缓冲区大小
16, //事件队列长度单位是int(可以不要,此参数填0,然后下一个参数填NULL)
&eventQueue, //UART 事件队列句柄(输出参数)类型为FreeRTOS的队列,如果设置为NULL,驱动程序将不会使用事件队列。
0 //中断分配标志,这里写 0 表示不想分配中断
);
四.收发数据
串行通信由每个 UART 控制器的有限状态机 (FSM) 控制。
发送数据的过程包括以下步骤:
-
将数据写入 Tx FIFO 缓冲区
-
FSM 序列化数据
-
FSM 将数据发送出去
接收数据的过程类似,只是步骤相反:
-
FSM 处理传入的串行流并将其并行化
-
FSM 将数据写入 Rx FIFO 缓冲区
-
从 Rx FIFO 缓冲区读取数据
发送函数uart_write_bytes(UART编号, 发送内容首地址, 长度);发送成功后返回发送内容的长度.
uart_write_bytes_with_break (UART编号, 发送内容首地址, 长度, 中断信号持续时间) (单位:当前波特率发送一位所需的时间)与uart_write_bytes功能类似只是在发送结束时增加了一个串口中断信号.
static char DATA = "Hello World!";
void TaskSend(void *arg){
while (1)
{
int count = uart_write_bytes(UART_NUM_0, &DATA, 12);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main()
{
xTaskCreate(TaskSend, "TaskSend", 2048, NULL, 1, NULL);
}
接收函数uart_read_bytes(UART编号, 发送内容首地址, 长度, 阻塞时间(单位:RTOS 滴答计数));
在读取数据之前,您可以调用uart_get_buffered_data_len(UART编号, 长度地址)来检查Rx FIFO缓冲区中可用的字节数,然后再读取相应的内容,这样就不会造成不必要的阻塞.
const int uart_num = UART_NUM_0;
uint8_t data[128];
int length = 0;
uart_get_buffered_data_len(uart_num, (size_t*)&length);
length = uart_read_bytes(uart_num, data, length, 100);
如果不再需要 Rx FIFO 缓冲区中的数据,您可以通过调用清除缓冲区uart_flush().
如果uart_driver_install()不再需要与建立的通信,则可以通过调用删除驱动程序以释放分配的资源uart_driver_delete().
七.SPI总线通信
SPI资源介绍:
- 1.ESP32 集成了 4 个 SPI 外设。
- 2.SPI0 和 SPI1 用于内部访问 ESP32 的附加闪存。两个控制器共享相同的 SPI 总线信号,并且有一个仲裁器来确定哪个可以访问总线。在 SPI1 总线上使用 SPI Master 驱动程序时有很多限制.
- 3.SPI2 和 SPI3 是通用 SPI 控制器,有时分别称为 HSPI 和 VSPI。它们对用户开放。SPI2 和 SPI3 具有各自名称相同的独立总线信号。每条总线有 3 条 CS 线来驱动最多相同数量的 SPI 从机。
- 4.如果 SPI 需要高速运行,请使用专用的 IO_MUX 引脚。GPIO 矩阵允许时钟频率最高为 40 MHz 的信号,如果使用 IO_MUX 引脚则为 80 MHz。
-
Pin Name
SPI2
SPI3
GPIO Number
CS0*
15
5
SCLK
14
18
MISO
12
19
MOSI
13
23
QUADWP
2
22
QUADHD
4
21
-
SPI模式介绍:
- 一.四线标准SPI
四线标准SPI由SCK、MOSI、MISO、CS四根线组成。四线标准SPI是全双工的通讯
总线名称 | 总线功能 |
---|---|
SCK | 时钟线,决定着通讯的速度 |
MOSI | 主输出从输入。主机输入,从机输入 |
MISO | 主输入从输出。主机输入,从机输出 |
CS | 片选线。当片选线被拉低的时候,总线有效,可以开始通讯 |
二.三线SPI
三线SPI就是把MISO和MOSI总线进行了合并。同一时间只能进行单方向的读或者写。是半双工的通讯。
三.Dual SPI
Dual SPI是四线半双工的SPI通讯。Dual SPI就是让MISO和MOSI同时进行发送或者接收的工作。因此通讯速度会得到极大的提高。这个时候MISO和MOSI总线名称就变成了IO0和IO1
四.Quad SPI
Quad SPI是六线半双工的SPI通讯。除了SCK和CS总线以外,增加了IO0、IO1、IO2、IO3四条总线,这四条总线能同时进行并行的读写,比Dual SPI通讯速度相比,又得到了极大的提高。有时候IO2和IO3引脚与WP和HD引脚共用。WD是写保护,HD是状态保持。
使用SPI主机模式的步骤如下:
1.初始化总线
/**
* @brief 配置SPIx主机模式,配置DMA通道、DMA字节大小,及 MISO、MOSI、CLK的引脚。
* - (注意:普通GPIO最大只能30MHz,而IOMUX默认的SPI-IO,CLK最大可以设置到80MHz)
* - 例:spi_master_init(SPI2_HOST, LCD_DEF_DMA_CHAN, LCD_DMA_MAX_SIZE, SPI2_DEF_PIN_NUM_MISO, SPI2_DEF_PIN_NUM_MOSI, SPI2_DEF_PIN_NUM_CLK);
*
* @param host_id SPI端口号。SPI1_HOST / SPI2_HOST / SPI3_HOST
* @param dma_chan 使用的DMA通道 :SPI_DMA_DISABLED不使用DMA / SPI_DMA_CH1 通道1 / SPI_DMA_CH2通道2 / SPI_DMA_CH_AUTO启用 DMA,通道由驱动程序自动选择
* @param max_tran_size DMA最大的传输字节数(会根据此值给DMA分配内存,值越大分配给DMA的内存就越大,单次可用DMA传输的内容就越多)不使用DMA传输,SPI一次能够传输的数据长度只有64byte
* @param miso_io_num MISO端口号。除仅能做输入 和 6、7、8、9、10、11之外的任意端口,但仅IOMUX默认的SPI-IO才能达到最高80MHz上限。
* @param mosi_io_num MOSI端口号
* @param clk_io_num CLK端口号
*
* @return
* - none
*/
void spi_master_init(spi_host_device_t host_id, int dma_chan, uint32_t max_tran_size,gpio_num_t miso_io_num, gpio_num_t mosi_io_num, gpio_num_t clk_io_num)
{
esp_err_t ret;
// 配置 MISO、MOSI、CLK、CS 的引脚,和DMA最大传输字节数
spi_bus_config_t buscfg={
.miso_io_num=miso_io_num, // MISO引脚定义
.mosi_io_num=mosi_io_num, // MOSI引脚定义
.sclk_io_num=clk_io_num, // CLK引脚定义
.quadwp_io_num=-1, // HD引脚不设置,这个引脚配置Quad SPI的时候才有用
.quadhd_io_num=-1, // WP引脚不设置,这个引脚配置Quad SPI的时候才有用
.max_transfer_sz=max_tran_size, // 最大传输字节数,非DMA最大64bytes,DMA最大4096bytes
};
ret=spi_bus_initialize(host_id, &buscfg, dma_chan);// 初始化SPI总线
ESP_ERROR_CHECK(ret);//判断是否创建成功
}
2.初始化设备
spi_device_handle_t Slave_SPI = NULL;创建从机spi句柄
/**
* @brief 设备初始化,初始化SPI总线,配置为 SPI mode 1.(CPOL=0, CPHA=1),CS引脚使用软件控制(ESP32的硬件CS流控会导致AS5047P通信不正常)
* -从机的初始化除了设置SPI总线,没有其他过程,不用配置寄存器。电后至少延时等待tpon=10ms。进行SPI通信即可。
* - 例:spi_as5047p_init(SPI3_HOST, 100*1000, AS5047P_SOFT_CS0);
*
* @param host_id SPI端口号。SPI1_HOST / SPI2_HOST / SPI3_HOST
* @param clk_speed 设备的SPI速度(注意:普通GPIO最大只能30MHz,需要根据设备对应的速度调整)
* @param cs_io_num CS端口号,使用软件控制(ESP32的硬件CS流控由于传输速度过快会导致设备通信不正常可调整为软件流控)
*
* @return
* - none
*/
void spi_init(spi_host_device_t host_id, uint32_t clk_speed, gpio_num_t cs_io_num)
{
// 设备初始化
spi_device_interface_config_t devcfg={
.clock_speed_hz=clk_speed,
//配置通讯的时钟频率。
//这个频率受到io_mux和input_delay_ns限制。
//如果是io直连的,时钟上限是80MHZ,如果是gpio交换矩阵连接进来的,时钟上限是40MHZ。
//如果是全双工,时钟上限是26MHZ。并且还要考虑输入延时。在相同输入延时的条件下,使用gpio交换矩阵会比使用io mux最大允许的时钟频率小。可以通过spi_get_freq_limit()来计算能够允许的最大时钟频率是多少
//有关SPI通讯时钟极限和配置的问题,后面会详细说一下。
#define SPI_MASTER_FREQ_8M (APB_CLK_FREQ/10) ///< 8MHz
#define SPI_MASTER_FREQ_9M (APB_CLK_FREQ/9) ///< 8.89MHz
#define SPI_MASTER_FREQ_10M (APB_CLK_FREQ/8) ///< 10MHz
#define SPI_MASTER_FREQ_11M (APB_CLK_FREQ/7) ///< 11.43MHz
#define SPI_MASTER_FREQ_13M (APB_CLK_FREQ/6) ///< 13.33MHz
#define SPI_MASTER_FREQ_16M (APB_CLK_FREQ/5) ///< 16MHz
#define SPI_MASTER_FREQ_20M (APB_CLK_FREQ/4) ///< 20MHz
#define SPI_MASTER_FREQ_26M (APB_CLK_FREQ/3) ///< 26.67MHz
#define SPI_MASTER_FREQ_40M (APB_CLK_FREQ/2) ///< 40MHz
#define SPI_MASTER_FREQ_80M (APB_CLK_FREQ/1) ///< 80MHz
.mode=1,
//spi模式 参数为 0:SPI mode 0.(CPOL=0, CPHA=0) / 1:SPI mode 1.(CPOL=0, CPHA=1) / 2:SPI mode 2.(CPOL=1, CPHA=0) / 3:SPI mode 3.(CPOL=1, CPHA=1)
.spics_io_num=-1,
// CS引脚定义-1为不启用,可调整为软件流控
.queue_size=7,
//传输队列的长度,表示可以在通讯的时候挂起多少个spi通讯。在中断通讯模式的时候会把当前spi通讯进程挂起到队列中
//---------------------------------------------------------------------------------------
//以下不常用看需求配置
.pre_cb=xxx_callback,
//在开始传输之前调用的回调函数
.post_cb=xxx_callback,
//传输完成后调用的回调函数
.address_bits = 32;
//1.如果设置为0,在通讯的时候就不会发送地址位。
//2.如果设置了非零值,就会在spi通讯的地址发送阶段发送指定长度的address数据。
//如果设置了非零值并且在后面数据发送结构体中没有定义addr的值,会默认发送指定长度0值
//3.我们后面发送数据会使用到spi_transaction_t结构体,这个结构体会使用spi_device_interface_config_t中定义好address、command和dummy的长度
//如果想使用非固定长度,就要使用spi_transaction_ext_t结构体了。这个结构体包括了四个部分,包含了一个spi_transaction_t和address、command、dummy的长度。
//我们要做的就是在spi_transaction_ext_t.base.flags中设置SPI_TRANS_VARIABLE_ADDR/CMD/DUMMY
//然后定义好这三部分数据的长度,然后用spi_transaction_ext_t.base的指针代替spi_transaction_t的指针即可
.command_bits = 8;
//与address_bits是一样的
.dummy_bits = 3*8;
//这里的配置方法与address_bits是一样的。但是要着重说一下这个配置的意义,后面会再说一遍
//1.dummy_bits是用来用来补偿输入延迟。
//在read phase开始阶段之前被插入进去。在dummy_bits的时钟下,并不进行数据读取的工作
//相当于这段时间发送的clock都是虚拟的时钟,并没有功能。在输入延迟最大允许时间不够的时候,可以通过这种方法进行配置,从而
//能够使得系统工作在更高的时钟频率下。
//3.如果主机设备只进行write操作,可以在flags中设置SPI_DEVICE_NO_DUMMY,关闭dummy bits的发送。只有写操作的话,即使使用了gpio交换矩阵,时钟周期也可以工作在80MHZ
.duty_cycle_pos = 0,
//正时钟的占空比,以 1/256 为增量(128 = 50%/50% 占空比)。将其设置为 0(=不设置)等同于将其设置为 128。
.cs_ena_pretrans = xxx,
//在传输之前,cs应该保持激活状态多少个时钟,只有半双工的时候才需要配置
.cs_ena_posttrans = xxx,
//在传输之后,片选线应该保持激活状态多少个时钟.
.input_delay_ns = 0,
//时钟发出信号到miso进行输入直接会有延迟,这个参数就是配置这个允许的最大延迟时间。
//如果主机接收到从机时钟,但是超过这个时间没有收到miso发来的输入信号,就会返回通讯失败。
//这个时间即使设置为0,也能正常工作,但是最好通过手册或逻辑分析仪进行估算。能够实现更好的通讯。
//超过8M的通讯都应该认真设置这个数字
.flags
//配置与从机有关的一些参数,比如MSB还是LSB,使不使用三线SPI
#define SPI_DEVICE_TXBIT_LSBFIRST (1<<0) //发送命令/地址/数据LSB优先,而不是默认的MSB优先
#define SPI_DEVICE_RXBIT_LSBFIRST (1<<1) //首先接收数据LSB,而不是默认的MSB
#define SPI_DEVICE_BIT_LSBFIRST (SPI_DEVICE_TXBIT_LSBFIRST|SPI_DEVICE_RXBIT_LSBFIRST) //首先发送和接收LSB
#define SPI_DEVICE_3WIRE (1<<2) //使用MOSI (=spid)发送和接收数据
#define SPI_DEVICE_POSITIVE_CS (1<<3) //在事务期间使CS为正而不是负
#define SPI_DEVICE_HALFDUPLEX (1<<4) //在接收数据之前发送数据,而不是同时接收数据
#define SPI_DEVICE_CLK_AS_CS (1<<5) //如果CS是激活的,则在CS线上输出时钟
#define SPI_DEVICE_NO_DUMMY (1<<6) //读取高频时存在定时问题(频率与是否使用iomux引脚有关,slave看到时钟后有效时间)。
//* -在半双工模式下,驱动程序在读取相位前自动插入虚拟位以修复计时问题。设置此标志以禁用此功能。
//* -在全双工模式下,硬件不能使用虚位,因此没有办法防止正在读取的数据被损坏。设置此标志以确认您将只使用输出,或不使用虚拟位进行读取,风险自负。
#define SPI_DEVICE_DDRCLK (1<<7)
}
esp_err_t ret;
// 将外设与SPI总线关联
ret=spi_bus_add_device(host_id, &devcfg, &Slave_SPI);
ESP_ERROR_CHECK(ret);
//---------------------------------------------------------------------------------
// 配置软件cs引脚
gpio_pad_select_gpio(cs_io_num);
/* Set the GPIO as a push/pull output */
gpio_set_direction(cs_io_num, GPIO_MODE_OUTPUT);
gpio_set_level(cs_io_num, 1);
// 上电后至少延时等待tpon=10ms。才可以进行SPI通信。
vTaskDelay(200 / portTICK_PERIOD_MS);
}
3.写入命令,地址和读取数据
固定长度结构体:
/**
* @brief spi总线发送并接收一帧uint16的数据,用以AS5047P通信
* - 例:data = as5047p_spi_send_and_recv_uint16(spi, addr, cs_io_num);
*
* @param SPI关联的句柄,通过此来调用SPI总线上的设备
* @param senddata spi发送的uint16数据
* @param cs_io_num CS端口号,使用软件控制(ESP32的硬件CS流控会导致AS5047P通信不正常)
*
* @return
* - spi接收到的uint16数据(重新经过大小端排序后的数据)
*/
static uint16_t SPI_send_and_recv_uint16(spi_device_handle_t spi, uint16_t senddata, gpio_num_t cs_io_num)
{
uint8_t temp = 0;
spi_transaction_t transaction_config; //定义数据结构体
memset(&transaction_config, 0, sizeof(transaction_config)); //初始化结构体
transaction_config.cmd = 0x9F;
//看设备是否需要进行配置
transaction_config.length = 2 * 8;
//16Bit。2个字节。要发送或者接收的数据的长度,不算前面的cmd/address/dummy的长度
transaction_config.tx_buffer =senddata;
//.flags设置为SPI_TRANS_USE_TXDATA则使用结构体内部空间tx_data
//如果未设置则使用tx_buffer需要指向外部指针
transaction_config.rx_buffer = NULL;
//.flags设置为SPI_TRANS_USE_RXDATA则使用结构体内部空间rx_data
//如果未设置则使用rx_buffer需要指向外部指针
transaction_config.flags = SPI_TRANS_USE_RXDATA;
//配置与从机有关的一些参数,比如MSB还是LSB,使不使用三线SPI
#define SPI_TRANS_MODE_DIO (1<<0) //以2位模式传输/接收数据
#define SPI_TRANS_MODE_QIO (1<<1) //以4位模式传输/接收数据
#define SPI_TRANS_USE_RXDATA (1<<2) //接收到spi_transaction_t的rx_data成员,而不是接收到rx_buffer的内存。
#define SPI_TRANS_USE_TXDATA (1<<3) //传输spi_transaction_t的tx_data成员,而不是tx_buffer中的数据。使用此选项时不要设置tx_buffer。
#define SPI_TRANS_MODE_DIOQIO_ADDR (1<<4) //也可以通过SPI_MODE_DIO/SPI_MODE_QIO选择的模式传输地址
#define SPI_TRANS_VARIABLE_CMD (1<<5) //使用``spi_transaction_ext_t``中的``command_bits``而不是``spi_device_interface_config_t``中的默认值。
#define SPI_TRANS_VARIABLE_ADDR (1<<6) //使用“spi_transaction_ext_t”中的“address_bits”,而不是“spi_device_interface_config_t”中的默认值。
#define SPI_TRANS_VARIABLE_DUMMY (1<<7) //使用' ' spi_transaction_ext_t '中的' ' dummy_bits ' ',而不是' ' spi_device_interface_config_t ' '中的默认值。
#define SPI_TRANS_CS_KEEP_ACTIVE (1<<8) //数据传输后保持CS活动
#define SPI_TRANS_MULTILINE_CMD (1<<9) //命令阶段使用的数据线与数据阶段相同(否则,命令阶段只使用一条数据线)。
#define SPI_TRANS_MODE_OCT (1<<10) //以8位模式传输/接收数据
#define SPI_TRANS_MULTILINE_ADDR SPI_TRANS_MODE_DIOQIO_ADDR //在地址阶段使用的数据线与数据阶段相同(否则,在地址阶段只使用一条数据线)
gpio_set_level(cs_io_num, 0);// 软件CSn
esp_err_t ret;
ret=spi_device_polling_transmit(spi, &transaction_config); // 开始传输
ESP_ERROR_CHECK(ret);
gpio_set_level(cs_io_num, 1);// 软件CSn
temp = *transaction_config.rx_data;
//.flags设置为SPI_TRANS_USE_RXDATA则使用结构体内部空间rx_data
return temp; // 返回接收的数据
非固定长度结构体:
/**
* @brief spi总线发送并接收一帧uint16的数据,用以AS5047P通信
* - 例:data = as5047p_spi_send_and_recv_uint16(spi, addr, cs_io_num);
*
* @param SPI关联的句柄,通过此来调用SPI总线上的设备
* @param senddata spi发送的uint16数据
* @param cs_io_num CS端口号,使用软件控制(ESP32的硬件CS流控会导致AS5047P通信不正常)
*
* @return
* - spi接收到的uint16数据(重新经过大小端排序后的数据)
*/
static uint16_t SPI_send_and_recv_uint16(spi_device_handle_t spi, uint16_t senddata, gpio_num_t cs_io_num)
{
typedef struct {
struct spi_transaction_t base; ///< Transaction data, so that pointer to spi_transaction_t can be converted into spi_transaction_ext_t
uint8_t command_bits; ///< The command length in this transaction, in bits.
uint8_t address_bits; ///< The address length in this transaction, in bits.
uint8_t dummy_bits; ///< The dummy length in this transaction, in bits.
} spi_transaction_ext_t ;//定义spi_transaction_ext_t类型的结构体
spi_transaction_ext_t ext; //定义数据结构体
memset(&ext, 0, sizeof(ext)); //初始化结构体
ext.command_bits = 8; //command长度是可变的,本次发送command长度为8bits
ext.address_bits = 0; //address长度是可变的,本次发送长度为0bits
ext.dummy_bits = 0; //.flags设置为SPI_TRANS_VARIABLE_DUMMY不使用总线初始化的dummy_bits,改为使用当前配置
ext.base.cmd = 0x00;
//看设备是否需要进行配置,
//.flags设置为SPI_TRANS_VARIABLE_CMD不使用总线初始化的command_bits,改为使用当前配置
ext.base.addr = 0x00;
//看设备是否需要进行配置,
//.flags设置为SPI_TRANS_VARIABLE_ADDR不使用总线初始化的address_bits,改为使用当前配置
ext.base.length = 2 * 8;
//16Bit。2个字节。要发送或者接收的数据的长度,不算前面的cmd/address/dummy的长度
ext.base.tx_buffer = senddata;
//.flags设置为SPI_TRANS_USE_TXDATA则使用结构体内部空间tx_data
//如果未设置则使用tx_buffer需要指向外部指针
ext.base.rx_buffer = NULL;
//.flags设置为SPI_TRANS_USE_RXDATA则使用结构体内部空间rx_data
//如果未设置则使用rx_buffer需要指向外部指针
ext.base.flags = SPI_TRANS_USE_RXDATA | SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR;
//配置与从机有关的一些参数,比如MSB还是LSB,使不使用三线SPI
#define SPI_TRANS_MODE_DIO (1<<0) //以2位模式传输/接收数据
#define SPI_TRANS_MODE_QIO (1<<1) //以4位模式传输/接收数据
#define SPI_TRANS_USE_RXDATA (1<<2) //接收到spi_transaction_t的rx_data成员,而不是接收到rx_buffer的内存。
#define SPI_TRANS_USE_TXDATA (1<<3) //传输spi_transaction_t的tx_data成员,而不是tx_buffer中的数据。使用此选项时不要设置tx_buffer。
#define SPI_TRANS_MODE_DIOQIO_ADDR (1<<4) //也可以通过SPI_MODE_DIO/SPI_MODE_QIO选择的模式传输地址
#define SPI_TRANS_VARIABLE_CMD (1<<5) //使用``spi_transaction_ext_t``中的``command_bits``而不是``spi_device_interface_config_t``中的默认值。
#define SPI_TRANS_VARIABLE_ADDR (1<<6) //使用“spi_transaction_ext_t”中的“address_bits”,而不是“spi_device_interface_config_t”中的默认值。
#define SPI_TRANS_VARIABLE_DUMMY (1<<7) //使用' ' spi_transaction_ext_t '中的' ' dummy_bits ' ',而不是' ' spi_device_interface_config_t ' '中的默认值。
#define SPI_TRANS_CS_KEEP_ACTIVE (1<<8) //数据传输后保持CS活动
#define SPI_TRANS_MULTILINE_CMD (1<<9) //命令阶段使用的数据线与数据阶段相同(否则,命令阶段只使用一条数据线)。
#define SPI_TRANS_MODE_OCT (1<<10) //以8位模式传输/接收数据
#define SPI_TRANS_MULTILINE_ADDR SPI_TRANS_MODE_DIOQIO_ADDR //在地址阶段使用的数据线与数据阶段相同(否则,在地址阶段只使用一条数据线)
gpio_set_level(cs_io_num, 0);// 软件CSn
esp_err_t ret;
ret=spi_device_polling_transmit(spi, &ext); // 开始传输
ESP_ERROR_CHECK(ret);
gpio_set_level(cs_io_num, 1);// 软件CSn
temp = *transaction_config.rx_data;
//.flags设置为SPI_TRANS_USE_RXDATA则使用结构体内部空间rx_data
return temp; // 返回接收的数据
发送可以以中断方式发送,也可以以轮询方式发送。
以中断方式发送
中断事务将阻塞事务例程,直到事务完成,从而允许 CPU 运行其他任务。
一个应用程序任务可以将多个事务排队,驱动程序会自动在中断服务例程(ISR)中逐一处理它们。它允许任务切换到其他程序,直到所有事务完成。
esp_err_t spi_device_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);
以轮询方式发送
轮询事务不使用中断。该例程不断轮询 SPI 主机的状态位,直到事务完成。
所有使用中断事务的任务都可以被队列阻塞。此时,他们将需要等待 ISR 运行两次才能完成事务。轮询事务可以节省花在队列处理和上下文切换上的时间,从而缩短事务持续时间。缺点是在处理这些事务时 CPU 很忙。
esp_err_t spi_device_polling_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);
八.ADC模数转换器
ESP32 集成了 2 个 SAR(逐次逼近寄存器)ADC,总共支持 18 个测量通道(模拟使能引脚)。
支持这些渠道:
ADC1:
-
8 个通道:GPIO32 - GPIO39
ADC2:
-
10 个通道:GPIO0、GPIO2、GPIO4、GPIO12 - GPIO15、GOIO25 - GPIO27
Vref 是 ESP32 ADC 内部用于测量输入电压的参考电压。ESP32 ADC 可以测量从 0 V 到 Vref 的模拟电压。在不同的芯片中,Vref 不同,中位数为 1.1 V。为了转换大于 Vref 的电压,输入电压可以在输入到 ADC 之前进行衰减。有 4 种衰减选项可供选择,衰减越高,可测量的输入电压就越高。
衰减 | 可测量输入电压范围 |
---|---|
| 100毫伏~950毫伏 |
| 100毫伏~1250毫伏 |
| 150 毫伏 ~ 1750 毫伏 |
| 150毫伏~2450毫伏 |
ADC 转换是将输入模拟电压转换为数字值。ADC 驱动程序 API 提供的 ADC 转换结果是原始数据。Single Read 模式下 ESP32 ADC 原始结果的分辨率为 12 位。
要根据 ADC 原始结果计算电压,可以使用以下公式:Vout = Dout * Vmax / Dmax (1)
Vout | 数字输出结果,代表电压。 |
Dout | ADC 原始数字读数结果。 |
Vmax | 最大可测量输入模拟电压,请参阅ADC 衰减。 |
Dmax | 输出ADC原始数字读数结果的最大值,在Single Read模式下为4095,在Continuous Read模式下为4095。 |
对于带有 eFuse ADC 校准位的电路板,esp_adc_cal_raw_to_voltage()可用于获取校准后的转换结果。这些结果代表实际电压(以 mV 为单位)。这些数据无需通过式(1)进行变换。如果在没有 eFuse ADC 校准位的板上使用 ADC 校准 API,则会生成警告。请参阅ADC 校准。
一些 ADC2 引脚用作 strapping 引脚(GPIO 0、2、15),因此不能自由使用。以下官方开发包就是这种情况:
ESP32 DevKitC:由于外部自动编程电路,GPIO 0 无法使用。
ESP-WROVER-KIT:GPIO 0、2、4 和 15 由于不同用途的外部连接而无法使用。
由于 ADC2 模块也被 Wi-Fi 使用,所以一起使用时只有一个可以抢占,这意味着它们adc2_get_raw()可能会被阻塞直到 Wi-Fi 停止,反之亦然。
#include <string.h>
#include <stdio.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_adc/adc_continuous.h"
#include "esp_adc/adc_cali_scheme.h"
#define EXAMPLE_READ_LEN 256 //设置ADC转换帧的大小,以字节为单位
static void continuous_adc_init(adc_channel_t channel, adc_continuous_handle_t *out_handle)
{
adc_continuous_handle_t handle = NULL;//创建adc返回句柄
adc_continuous_handle_cfg_t adc_config = {
.max_store_buf_size = 1024,//设置驱动程序将ADC转换结果保存到的池的最大大小(以字节为单位)
.conv_frame_size = EXAMPLE_READ_LEN,//设置ADC转换帧的大小,以字节为单位
};//配置adc连续驱动程序结构体
adc_continuous_new_handle(&adc_config, &handle);//将结构体写入初始化函数
adc_digi_pattern_config_t adc_pattern = {
.atten = ADC_ATTEN_DB_11,//ADC衰减
.channel = channel,//io对应的channel通道号
.unit = ADC_UNIT_1,//io所属的ADC1
.bit_width = SOC_ADC_DIGI_MAX_BITWIDTH,//原始转换结构的位宽
};//配置每个adc通道配置列表
adc_continuous_config_t dig_cfg = {
.sample_freq_hz = 20 * 1000,//采样频率hz为单位
.conv_mode = ADC_CONV_SINGLE_UNIT_1,//连续转换模式ESP32 only supports ADC1 DMA mode
.format = ADC_DIGI_OUTPUT_FORMAT_TYPE1,//转换输出格式ESP32 only supports ADC1 DMA mode
.pattern_num = 1,//使用的channel通道数量
.adc_pattern = &adc_pattern,//配置每个adc通道配置列表
};//创建adc连续器结构体配置
adc_continuous_config(handle, &dig_cfg);//配置adc连续器结构体
*out_handle = handle;//返回adc返回句柄
}
static void adc_cali_raw_to_voltage_init(adc_cali_handle_t *out_handle)
{
adc_cali_handle_t handle = NULL;//创建adc数据转换矫正句柄
adc_cali_line_fitting_config_t cali_config = {
.unit_id = ADC_UNIT_1,//io所属的ADC1
.atten = ADC_ATTEN_DB_11,//ADC衰减
.bitwidth = SOC_ADC_DIGI_MAX_BITWIDTH,//原始转换结构的位宽
};//需要与adc输出的设置匹配
adc_cali_create_scheme_line_fitting(&cali_config, &handle);
*out_handle = handle;//返回数据矫正装换返回句柄
}
void app_main(void)
{
uint32_t ret_num = 0;
uint8_t result[EXAMPLE_READ_LEN] = {0};
memset(result, 0xcc, EXAMPLE_READ_LEN);
adc_continuous_handle_t handle = NULL;//创建adc初始化句柄
adc_cali_handle_t chackhandle = NULL;//创建adc数据转换矫正句柄
continuous_adc_init(6, &handle);//初始化6号通道
adc_continuous_start(handle);//开启adc连续度模式
adc_cali_raw_to_voltage_init(&chackhandle);//初始化adc数据矫正
while (1) {
adc_continuous_read(handle, result, EXAMPLE_READ_LEN, &ret_num, 0);//读出adc数据
for (int i = 0; i < ret_num; i += SOC_ADC_DIGI_RESULT_BYTES)
{
int mV = 0;
adc_digi_output_data_t *p = (void*)&result[i];//数据转存
adc_cali_raw_to_voltage(chackhandle, p->type1.data, &mV);//adc数据矫正
printf("Cali Voltage: %d mV\n", mV);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
ESP_ERROR_CHECK(adc_continuous_stop(handle));//停止adc
ESP_ERROR_CHECK(adc_continuous_deinit(handle));//取消adc初始化
}
单次读adc代码设置如下(基于idf v5.0版本旧版本不适用)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "soc/soc_caps.h"
#include "esp_log.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
const static char *TAG = "EXAMPLE";
/*---------------------------------------------------------------
ADC General Macros
---------------------------------------------------------------*/
//ADC1 Channels
#define Motor_1_ADC1_A ADC_CHANNEL_4//定义1号电机A相电流通道
#define Motor_1_ADC1_B ADC_CHANNEL_5//定义1号电机B相电流通道
#define Motor_2_ADC1_A ADC_CHANNEL_6//定义2号电机A相电流通道
#define Motor_2_ADC1_B ADC_CHANNEL_7//定义2号电机A相电流通道
static int adc_raw[4];
static int Motor_1_ADC1_A_voltage;
static int Motor_1_ADC1_B_voltage;
static int Motor_2_ADC1_A_voltage;
static int Motor_2_ADC1_B_voltage;
static bool example_adc_calibration_init(adc_unit_t unit, adc_atten_t atten, adc_cali_handle_t *out_handle);
static void example_adc_calibration_deinit(adc_cali_handle_t handle);
void app_main(void)
{
//-------------ADC1 Init---------------//
adc_oneshot_unit_handle_t adc1_handle;//创建adc1初始化返回句柄
adc_oneshot_unit_init_cfg_t init_config1 = {
.unit_id = ADC_UNIT_1,
.ulp_mode = ADC_ULP_MODE_DISABLE,
};//创建adc初始化结构体
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle));//配置adc1初始化
//-------------ADC1 Config---------------//
adc_oneshot_chan_cfg_t config = {
.bitwidth = ADC_BITWIDTH_DEFAULT,//12位位宽
.atten = ADC_ATTEN_DB_11,//adc衰减
};//创建adc模式
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, Motor_1_ADC1_A, &config));//配置adc模式
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, Motor_1_ADC1_B, &config));//配置adc模式
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, Motor_2_ADC1_A, &config));//配置adc模式
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, Motor_2_ADC1_B, &config));//配置adc模式
//-------------ADC1 Calibration Init---------------//
adc_cali_handle_t adc1_cali_handle = NULL;//创建adc矫正返回句柄
bool do_calibration1 = example_adc_calibration_init(ADC_UNIT_1, ADC_ATTEN_DB_11, &adc1_cali_handle);
while (1) {
ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, Motor_1_ADC1_A, &adc_raw[1]));
ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, Motor_1_ADC1_B, &adc_raw[2]));
ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, Motor_2_ADC1_A, &adc_raw[3]));
ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, Motor_2_ADC1_B, &adc_raw[4]));
if (do_calibration1) {
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_handle, adc_raw[1], Motor_1_ADC1_A_voltage));
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_handle, adc_raw[2], Motor_1_ADC1_B_voltage));
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_handle, adc_raw[3], Motor_2_ADC1_A_voltage));
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_handle, adc_raw[4], Motor_2_ADC1_B_voltage));
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
//Tear Down
ESP_ERROR_CHECK(adc_oneshot_del_unit(adc1_handle));
if (do_calibration1) {
example_adc_calibration_deinit(adc1_cali_handle);
}
}
/*---------------------------------------------------------------
ADC Calibration
---------------------------------------------------------------*/
static bool example_adc_calibration_init(adc_unit_t unit, adc_atten_t atten, adc_cali_handle_t *out_handle)
{
adc_cali_handle_t handle = NULL;
esp_err_t ret = ESP_FAIL;
bool calibrated = false;
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED//线路拟合方案
if (!calibrated) {
ESP_LOGI(TAG, "calibration scheme version is %s", "Curve Fitting");
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = unit,
.atten = atten,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
ret = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
if (ret == ESP_OK) {
calibrated = true;
}
}
#endif
#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED//曲线拟合方案
if (!calibrated) {
ESP_LOGI(TAG, "calibration scheme version is %s", "Line Fitting");
adc_cali_line_fitting_config_t cali_config = {
.unit_id = unit,
.atten = atten,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
ret = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
if (ret == ESP_OK) {
calibrated = true;
}
}
#endif
*out_handle = handle;
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Calibration Success");
} else if (ret == ESP_ERR_NOT_SUPPORTED || !calibrated) {
ESP_LOGW(TAG, "eFuse not burnt, skip software calibration");
} else {
ESP_LOGE(TAG, "Invalid arg or no memory");
}
return calibrated;
}
static void example_adc_calibration_deinit(adc_cali_handle_t handle)
{
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED//线路拟合方案
ESP_LOGI(TAG, "deregister %s calibration scheme", "Curve Fitting");
ESP_ERROR_CHECK(adc_cali_delete_scheme_curve_fitting(handle));
#elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED//曲线拟合方案
ESP_LOGI(TAG, "deregister %s calibration scheme", "Line Fitting");
ESP_ERROR_CHECK(adc_cali_delete_scheme_line_fitting(handle));
#endif
}
十.MCPWM电机控制器
ESP32有两个MCPWM单元,可用于控制不同类型的电机。每个单元有三对PWM输出。(如下图,每对输出标记为A、B。共六对PWM输出)
MCPWM 外设是一种多功能 PWM 发生器,它包含各种子模块,使其成为电机控制、数字电源等电力电子应用中的关键元件。通常,MCPWM 外设可用于以下场景:
-
数字电机控制,例如有刷/无刷直流电机、RC 伺服电机
-
基于开关模式的数字电源转换
-
电源 DAC,其中占空比等于 DAC 模拟值
-
计算外部脉冲宽度,并将其转换为速度、距离等其他模拟值
-
为磁场定向控制 (FOC) 生成空间矢量 PWM (SVPWM) 信号
九.蓝牙应用
1:传统蓝牙(Basic Rate/Enhanced Data Rate (BR/EDR))
2:低功耗蓝牙(Low Energy (LE)低功耗即所谓的新型的低功耗蓝牙技术)
注意:经典蓝牙和低功耗蓝牙两者物理层调制解调方式是不一样的,所以低功耗蓝牙设备和经典蓝牙设备两者之间是不能相互通信的,选型的时候千万不要搞混,如果主设备是低功耗蓝牙设备,从设备也必须是低功耗蓝牙设备;同样,经典蓝牙的从设备也只能和经典蓝牙的主设备进行通信。不过市场上还有一种双模蓝牙设备,即同时支持低功耗蓝牙和经典蓝牙,比如我们天天用到的手机,手机可以和经典蓝牙设备通信,也可以和低功耗蓝牙设备通信,如前所述,这不代表低功耗蓝牙设备可以和经典蓝牙设备通信,其实手机使用了分时机制来达到同时和低功耗蓝牙设备以及经典蓝牙设备通信的目的,即手机让双模蓝牙芯片不断地在低功耗蓝牙模式和经典蓝牙模式之间进行切换,以同时支持低功耗蓝牙设备和经典蓝牙设备。低功耗蓝牙方案,经典蓝牙方案,还是双模蓝牙方案,大家选型的时候一定要弄明白他们之间的区别,以选择适合自己的蓝牙方案。
补充:最新的蓝牙smart ready可以兼容低功耗和传统蓝牙,手机中的蓝牙普遍用的这种
连接方式 | 网络拓扑结构 | 经典应用 |
点对点连接 | 点对点(1:1) | 手机与蓝牙音响 |
广播连接 | 点对多(1:N) | 机场广播 |
mesh连接 | 多点对多点(N:N) | 智能家居生态 |
一.低功耗蓝牙架构介绍:
Host(蓝牙主机)中包含:
1:主机/控制器接口 (Host Controller Interface,HCl)
用于通过软件API或者硬件接口比如UART,SPI,USB实现主机和控制器进行数据交互.
2:逻辑链路控制与适配协议 (Logical Link Control and Adaptation Protocol,L2CAP)
为上一层提供数据封装服务,允许合理的端对端的数据交互
3:属性协议 (Attribute Protocol,ATT)
允许一个设备明文发送确定的数据片到另一个设备.
4:安全管理器 (Security Manager,SM)
安全管理层定义校验和密匙分发的方法,为协议栈其他层的安全连接和交换数据提供方法.
5:通用属性规范 (Generic Attribute Profile,GATT)
定义了对端设备的数据规则,数据存储在属性服务器的“属性”里,供属性客户端执行读写操作
6:通用访问规范 (Generic Access Profile,GAP)
面向应用或者规范,解决设备发现,为设备连接相关设备
市面上有许多开源的蓝牙协议栈例如:
蓝牙协议栈 | 使用的平台 |
bluez | linus系统官方 |
bluedroid | Android系统 |
Zephyr | 物联网操作系统 |
AilOS-Things | 阿里物联网操作系统 |
nimble | RT-Thread物联网操作系统 |
Esp32使用的是bluedroid蓝牙协议栈