ESP-IDF学习笔记-IIC与FT6236触摸屏

FT6236是一个电容屏驱动芯片,支持单点+手势或者双点触控,基于I2C协议。本篇将在ESP32C3下基于IDF开发其驱动。同时学习IIC驱动的使用。

官方文档

datasheet

I2C驱动程序

IDF的IIC驱动

ESP32-C3拥有一个IIC控制器,IDF为其提供了完整的驱动。

版本变更

在最新版的IDF中,IIC驱动分为新驱动和老驱动,他们在对用户开放的API上有所不同。老驱动引用头文件driver/i2c.h,新驱动则是i2c_master.hi2c_slave.h。本文使用老驱动。

配置驱动程序

建立IIC通信的第一步是配置驱动程序,使用i2c_config_t结构体以及i2c_param_config() 函数进行设置。

i2c_config_t:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct{
i2c_mode_t mode; /*!< I2C mode */
int sda_io_num; /*!< GPIO number for I2C sda signal */
int scl_io_num; /*!< GPIO number for I2C scl signal */
bool sda_pullup_en; /*!< Internal GPIO pull mode for I2C sda signal*/
bool scl_pullup_en; /*!< Internal GPIO pull mode for I2C scl signal*/

union {
struct {
uint32_t clk_speed; /*!< I2C clock frequency for master mode, (no higher than 1MHz for now) */
} master; /*!< I2C master config */
#if SOC_I2C_SUPPORT_SLAVE
struct {
uint8_t addr_10bit_en; /*!< I2C 10bit address mode enable for slave mode */
uint16_t slave_addr; /*!< I2C address for slave mode */
uint32_t maximum_speed; /*!< I2C expected clock speed from SCL. */
} slave; /*!< I2C slave config */
#endif // SOC_I2C_SUPPORT_SLAVE
};
uint32_t clk_flags; /*!< Bitwise of ``I2C_SCLK_SRC_FLAG_**FOR_DFS**`` for clk source choice*/
} i2c_config_t;

上述结构中,我们需要关注以下几个成员:

  • mode:选择I2C的模式,可选参数为:I2C_MODE_SLAVE,I2C_MODE_MASTER
  • sda\scl_io_num:信号线引脚
  • sda\scl_pullup_en:内部上拉使能
  • master.clk_speed:时钟频率

从模式本次暂不涉及。clk_flags用以选择时钟源,一般默认0即可。

设置完成后调用函数进行设置:

1
esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t *i2c_conf)

i2c_num标识了使用芯片的哪一个I2C控制器。该函数还设置了I2C标准的一些默认参数(高低电平时间,起始信号时序等),想要修改这些参数可以在用户自定义配置进行查看。

安装驱动程序

配置完成后,调用函数安装驱动:

1
esp_err_t i2c_driver_install(i2c_port_t i2c_num, i2c_mode_t mode, size_t slv_rx_buf_len, size_t slv_tx_buf_len,int intr_alloc_flags)

i2c_num为上面使用的I2C控制器代号,mod为模式选择。

buf_len参数为从模式下用以缓存数据,主模式不用管。intr_alloc_flags设置中断分配的标志,这里为0即可。

主机模式下的通信

这里我们只考虑主机通信这一常用模式。

I2C的驱动使用了一个命令容器的机制,用户首先创建一个命令容器,然后将要进行的各种I2C动作(包括开始时序、写地址、写数据、读数据等)依次填入命令容器,最后执行命令容器,将所有操纵传递给控制器执行,具体过程如下图所示:

主机写入数据

image-20240813133014080

主机读取数据

image-20240813133043479

  • 命令容器的创建
1
i2c_cmd_handle_t handle = i2c_cmd_link_create();

该命令会动态的分配一段内存创建命令容器。如果不想使用动态内存分配,可以使用静态创建方式:

1
2
3
uint8_t buffer[I2C_TRANS_BUF_MINIMUM_SIZE] = { 0 };

i2c_cmd_handle_t handle = i2c_cmd_link_create_static(buffer, sizeof(buffer));
  • 发送开始时序
1
err = i2c_master_start(handle);
  • 写单个字节——发送地址
1
2
3
4
//进行写操作
err = i2c_master_write_byte(handle, device_address << 1 | I2C_MASTER_WRITE, ACK_EN);
//进行读操作
err = i2c_master_write_byte(handle, device_address << 1 | I2C_MASTER_READ, ACK_EN);
  • 读写数据
1
2
3
4
5
6
//写操作
err = i2c_master_write(handle, write_buffer, write_size, ACK_EN);
//读操作
err = i2c_master_read(handle, read_buffer, read_size, I2C_MASTER_LAST_NACK);
//读单个字节
err = i2c_master_read_byte(handle, &read_buffer, I2C_MASTER_LAST_NACK);

i2c_ack_type_t规定了如何进行ACK相应,有三类:I2C_MASTER_ACK为每个直接响应ACK,I2C_MASTER_NACK为每个字节响应NACK,I2C_MASTER_LAST_NACK最后一个字节响应NACK。

  • 结束时序
1
i2c_master_stop(handle);
  • 执行命令
1
err = i2c_master_cmd_begin(i2c_num, handle, ticks_to_wait);
  • 释放命令容器内存
1
2
3
4
//释放动态
i2c_cmd_link_delete(handle);
//释放静态内存
i2c_cmd_link_delete_static(handle);

删除驱动

1
i2c_driver_delete(I2C_MASTER_NUM);

释放资源。该函数无法保证线程安全。

其他读写函数

驱动中提供了一些预制的命令容器读写函数,可以满足一些常见的读写需求,函数如下:

1
2
3
4
5
6
7
8
9
10
esp_err_t i2c_master_write_to_device(i2c_port_t i2c_num, uint8_t device_address,
const uint8_t* write_buffer, size_t write_size,
TickType_t ticks_to_wait);
esp_err_t i2c_master_read_from_device(i2c_port_t i2c_num, uint8_t device_address,
uint8_t* read_buffer, size_t read_size,
TickType_t ticks_to_wait);
esp_err_t i2c_master_write_read_device(i2c_port_t i2c_num, uint8_t device_address,
const uint8_t* write_buffer, size_t write_size,
uint8_t* read_buffer, size_t read_size,
TickType_t ticks_to_wait);

其中i2c_master_read_from_device的时序满足的是EEPROM的随机读时序(写完寄存器地址后直接发送Start信号)。

FT6236

引脚定义

FT6236触摸屏一共有6个引脚,除去3.3V供电和GND以外,还有以下几个:

  • SDA:IIC的数据线,需要上拉到VCC。
  • SCL:IIC的时钟线,需要上拉到VCC。
  • INT:中断信号线,指示是否有触摸点,低电平有效,有触摸点时可以根据设置发出低脉冲或持续拉低。需要上拉到VCC。
  • RST:复位信号线。低电平有效,需要持续低电平300ms以上,需要上拉到VCC。

注意各个信号线的上拉,这很重要。

设备结构体

设置一个数据结构体作为一个设备的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct {
int8_t int_pin;
int8_t rst_pin;
struct {
uint8_t sda_pin;
uint8_t scl_pin;
uint8_t addr;
i2c_port_t i2c_num;
}i2c;//存储bsp相关数据

touch_panel_dir_t direction;
uint16_t width;
uint16_t height;
} ft6236_dev_t;

数据读写函数

FT6236的读写时序如下:

image-20240813131948606

  • 写寄存器

开始-地址-寄存器地址-数据-结束

  • 读寄存器

开始-地址-寄存器地址-结束

开始-地址-读数据-结束

使用IDF的IIC部分相关代码实现如下:

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

static uint8_t ft6236_interface_init(ft6236_dev_t *dev)
{
//iic初始化,gpio初始化
esp_err_t err=ESP_OK;

i2c_config_t config={
.mode=I2C_MODE_MASTER,
.sda_io_num=dev->i2c.sda_pin,
.scl_io_num=dev->i2c.scl_pin,
.scl_pullup_en=GPIO_PULLUP_ENABLE,
.sda_pullup_en=GPIO_PULLUP_ENABLE,
.master.clk_speed=400000,
.clk_flags=0
};
err |= i2c_param_config(dev->i2c.i2c_num,&config);
err |= i2c_driver_install(dev->i2c.i2c_num,config.mode,0,0,0);

//int_pin must be pulled up
if(dev->int_pin != -1) {
gpio_set_direction(dev->int_pin, GPIO_MODE_INPUT);
gpio_set_pull_mode(dev->int_pin, GPIO_PULLUP_ONLY);
}
//rst_pin
if(dev->rst_pin != -1) {
gpio_set_direction(dev->rst_pin, GPIO_MODE_OUTPUT);
gpio_set_level(dev->rst_pin, 0);
vTaskDelay(400 / portTICK_PERIOD_MS);//at last 300ms
gpio_set_level(dev->rst_pin, 1);
}

return err;
}

static void ft6236_interface_deinit(ft6236_dev_t *dev)
{
i2c_driver_delete(dev->i2c.i2c_num);
if(dev->int_pin != -1)
gpio_reset_pin(dev->int_pin);
if(dev->rst_pin != -1)
gpio_reset_pin(dev->rst_pin);
}

static uint8_t ft6236_write_one_reg(ft6236_dev_t *dev,uint8_t start_addr,uint8_t write_size,uint8_t *data_buf)
{
//按照时序写寄存器
esp_err_t err=ESP_OK;
i2c_port_t i2c_num=dev->i2c.i2c_num;
uint8_t _data[2]={start_addr,data};

i2c_cmd_handle_t handle=i2c_cmd_link_create();
if(handle==NULL) return 1;
i2c_master_start(handle);
i2c_master_write_byte(handle, (dev->i2c.addr << 1) | I2C_MASTER_WRITE, true);
i2c_master_write(handle, _data, 2, true);
i2c_master_stop(handle);
err = i2c_master_cmd_begin(i2c_num, handle, 1000*portTICK_PERIOD_MS);
i2c_cmd_link_delete(handle);

return err==ESP_OK ? 0:1;
}

static uint8_t ft6236_read_reg(ft6236_dev_t *dev,uint8_t start_addr,uint8_t read_num,uint8_t *data_buf)
{
//按照时序读寄存器
esp_err_t err=ESP_OK;
i2c_port_t i2c_num=dev->i2c.i2c_num;

i2c_cmd_handle_t handle=i2c_cmd_link_create();
if(handle==NULL) return 1;
//First, set the reg address
err|=i2c_master_start(handle);
err|=i2c_master_write_byte(handle, (dev->i2c.addr << 1) | I2C_MASTER_WRITE, true);
err|=i2c_master_write_byte(handle, start_addr, true);
err|=i2c_master_stop(handle);
err|=i2c_master_cmd_begin(i2c_num, handle, 1000/portTICK_PERIOD_MS);
i2c_cmd_link_delete(handle);
if(err!=ESP_OK) return 1;//return if error

//then read data
handle=i2c_cmd_link_create();
if(handle==NULL) return 1;
err|=i2c_master_start(handle);
err|=i2c_master_write_byte(handle, (dev->i2c.addr << 1) | I2C_MASTER_READ, true);
err|=i2c_master_read(handle, data_buf, read_num,I2C_MASTER_LAST_NACK);
err|=i2c_master_stop(handle);
err|=i2c_master_cmd_begin(i2c_num, handle, 1000/portTICK_PERIOD_MS);
i2c_cmd_link_delete(handle);

return err==ESP_OK ? 0:1;
}

上面的函数是整个驱动的底层,进行移植时需要重新编写移植这些代码。

芯片寄存器

查看芯片的datasheet,可以看到芯片的寄存器

image-20240501211125962

image-20240501211137170

这里注意几个比较重要的寄存器:

  • P1_xxx: 触摸点数据寄存器。

Event Flag说明了触摸点的数据:00b-按下,01b-松开,10b-Contact,11b-No event。

Touch ID说明是第几个触摸点,当点不可用时,值为0x0F。

WEIGHT存储了触摸点的按压力度。

  • GEST_ID

存储了触摸手势的判断:

0x10——Move Up

0x14——Move Right

0x18——Move Down

0x1C——Move Left

0x48——Zoom In

0x49——Zoom Out

0x00——No Gesture

  • TD_STATUS

存储了触摸点数量,有效值为1-2。

  • TD_GROUP

触摸检测阈值设置,越小越灵敏。

  • G_MODE

中断引脚模式。

polling mode:当有触摸数据时,引脚一直为低。

trigger mode:有触摸数据时,引脚会有一个有效电平低的脉冲。

ESP-IDF触摸屏的驱动框架

ESP组件库中有一个触摸屏驱动库,但是很不幸,该库中没有FT6236的驱动,因此需要根据其驱动框架编写FT6236的驱动。

用户数据结构

驱动库需要定义一些外部的数据结构,用来给用户读取和设置驱动。

触摸板方向设置

该设置用于改变驱动输出坐标的原点位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
typedef enum {
/* @---> X
|
Y
*/
TOUCH_DIR_LRTB, /**< From left to right then from top to bottom, this consider as the original direction of the touch panel*/

/* Y
|
@---> X
*/
TOUCH_DIR_LRBT, /**< From left to right then from bottom to top */

/* X <---@
|
Y
*/
TOUCH_DIR_RLTB, /**< From right to left then from top to bottom */

/* Y
|
X <---@
*/
TOUCH_DIR_RLBT, /**< From right to left then from bottom to top */

/* @---> Y
|
X
*/
TOUCH_DIR_TBLR, /**< From top to bottom then from left to right */

/* X
|
@---> Y
*/
TOUCH_DIR_BTLR, /**< From bottom to top then from left to right */

/* Y <---@
|
X
*/
TOUCH_DIR_TBRL, /**< From top to bottom then from right to left */

/* X
|
Y <---@
*/
TOUCH_DIR_BTRL, /**< From bottom to top then from right to left */

TOUCH_DIR_MAX
} touch_panel_dir_t;

触摸事件

该数据结构表示了触摸点的触摸事件,是按下还是松开。

1
2
3
4
5
6
typedef enum {
TOUCH_EVT_PRESS = 0x0, /*!< Press event */
TOUCH_EVT_RELEASE = 0x1, /*!< Release event */
TOUCH_EVT_CONTACT = 0x2, /*!< Contact event */
TOUCH_EVT_NONE = 0x3, /*!< no event */
} touch_panel_event_t;

触摸手势

有些触摸屏可以检测触摸手势,而FT6236能够检测6种手势。驱动提供了一个数据结构用来描述。

1
2
3
4
5
6
7
8
9
typedef enum {
TOUCH_GES_NONE = 0x00, /*!< No Gesture */
TOUCH_GES_MOVE_UP = 0x10, /*!< Move up */
TOUCH_GES_MOVE_RIGHT = 0x14, /*!< Move right */
TOUCH_GES_MOVE_DOWN = 0x18, /*!< Move down */
TOUCH_GES_MOVE_LEFT = 0x1C, /*!< Move left */
TOUCH_GES_ZOOM_IN = 0x48, /*!< Zoom in */
TOUCH_GES_ZOOM_OUT = 0x49, /*!< Zoom out */
} touch_panel_gesture;

触摸屏配置结构体

用来传入配置参数的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
struct {
uint8_t addr; /*!< dev's address */
i2c_port_t i2c_num; /*!< i2c port to use */
uint8_t i2c_scl; /*!< i2c scl pin */
uint8_t i2c_sda; /*!< i2c sda pin */
}i2c; /*!< i2c bsp config */
int8_t pin_num_int; /*!< Interrupt pin of touch panel. NOTE: You can set to -1 for no connection with hardware. If PENIRQ is connected, set this to pin number. */
int8_t pin_num_rst; /*!< rst pin */
touch_panel_dir_t direction; /*!< Rotate direction */
uint16_t width; /*!< touch panel width */
uint16_t height; /*!< touch panel height */
} touch_panel_config_t;

触摸点信息

用于驱动向用户传递触摸点信息。

1
2
3
4
5
6
7
8
typedef struct {
uint8_t point_num; /*!< Touch point number */
touch_panel_gesture gesture; /*!< Gesture of touch */
touch_panel_event_t event[TOUCH_MAX_POINT_NUMBER]; /*!< Event of touch */
uint8_t weight[TOUCH_MAX_POINT_NUMBER]; /*!< weight of touch */
uint16_t curx[TOUCH_MAX_POINT_NUMBER]; /*!< Current x coordinate */
uint16_t cury[TOUCH_MAX_POINT_NUMBER]; /*!< Current y coordinate */
} touch_panel_points_t;

内部数据结构

驱动内部使用一个结构体用来代指一个设备,该结构体已经在上面给出。

用户函数

驱动需要实现以下函数:

初始化与反初始化函数

初始化内部的设备结构体,同时初始化相关外设以及相关的寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
uint8_t ft6236_init(touch_panel_config_t *config)
{
uint8_t ret=0;
uint8_t cipher=0;//must be 0x64
uint8_t firmid=0;//Firmware Version
uint8_t data=0;

g_dev.i2c.addr=config->i2c.addr;
g_dev.i2c.i2c_num=config->i2c.i2c_num;
g_dev.i2c.scl_pin=config->i2c.i2c_scl;
g_dev.i2c.sda_pin=config->i2c.i2c_sda;
g_dev.int_pin=config->pin_num_int;
g_dev.rst_pin=config->pin_num_rst;
g_dev.width=config->width;
g_dev.height=config->height;
ft6236_set_direction(config->direction);
//bsp initialize and test
ret += ft6236_interface_init(&g_dev);
ret += ft6236_read_reg(&g_dev,FT6236_REG_FIRMID,1,&firmid);
ret += ft6236_read_reg(&g_dev,FT6236_REG_CIPHER,1,&cipher);
if(ret!=0 ||cipher!=0x64)//chip id changes with chips
{
ESP_LOGI(TAG,"Initialize interface fail.cipher=%x firmware version=%x.",cipher,firmid);
return 1;
}
ESP_LOGI(TAG,"cipher=%x firmware version=%x.",cipher,firmid);
//set the interrupt mod to polling(continuous low level)
data=0;
ret += ft6236_write_one_reg(&g_dev, FT6236_REG_G_MODE, data);
//set the touch detect threshold(lower is sensitive)
data=22;
ret += ft6236_write_one_reg(&g_dev, FT6236_REG_TH_GROUP, data);

return ret;
}
1
2
3
4
void ft6236_deinit(void)
{
ft6236_interface_deinit(&g_dev);
}

坐标方向设置函数

用于改变屏幕方向。

1
2
3
4
5
6
7
8
uint8_t ft6236_set_direction(touch_panel_dir_t dir) {

if (TOUCH_DIR_MAX < dir) {
dir >>= 5;
}
g_dev.direction = dir;
return 0;
}

查询与数据获取

通过int引脚或者读取寄存器来判断是否有触摸点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* confirm if there is touch event
* @return 1:touched 0:nothing
*/
uint8_t ft6236_is_press(void)
{
/**
* @note There are two ways to determine weather the touch panel is pressed
* 1. Read the IRQ line of touch controller
* 2. Read number of points touched in the register
*/
if (-1 != g_dev.int_pin) {
return !gpio_get_level((gpio_num_t)g_dev.int_pin);
}
uint8_t num;
ft6236_read_reg(&g_dev,FT6236_REG_TD_STATUS,1,&num);
if((num&0x07) ==0)
return 0;
return 1;
}

数据点信息读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
uint8_t ft6236_get_points(touch_panel_points_t *info)
{
uint8_t data_buf[14];
uint8_t point_index=0;

ft6236_read_reg(&g_dev,FT6236_REG_GEST_ID,14,data_buf);

info->point_num =data_buf[1] &0x07;//get TD_STATUS
if (info->point_num > 0 && info->point_num <= TOUCH_MAX_POINT_NUMBER) {
info->gesture=data_buf[0];//get the gesture ID

if(data_buf[4]>>4!=0x0F)//Touch ID is valid
{
info->event[point_index] = data_buf[2] >> 6;
info->curx[point_index] = (((uint16_t)(data_buf[2]&0x0F) << 8) | data_buf[3]);
info->cury[point_index] = (((uint16_t)(data_buf[4]&0x0F) << 8) | data_buf[5]);
info->weight[point_index] = data_buf[6];
ft6236_apply_rotate(&info->curx[point_index],&info->cury[point_index]);

point_index++;
}
if(data_buf[10]>>4!=0x0F)//Touch ID is valid
{
info->event[point_index] = data_buf[8] >> 6;
info->curx[point_index] = (((uint16_t)(data_buf[8]&0x0F) << 8) | data_buf[9]);
info->cury[point_index] = (((uint16_t)(data_buf[10]&0x0F) << 8) | data_buf[11]);
info->weight[point_index] = data_buf[12];
ft6236_apply_rotate(&info->curx[point_index],&info->cury[point_index]);

point_index++;
}

if(point_index==1){//clean the unused info
info->curx[1] = 0;
info->cury[1] = 0;
}
// ESP_LOGI(TAG,"Touch ID--1:%x,2:%x",data_buf[4],data_buf[10]);
return info->point_num;
} else {
info->curx[0] = 0;
info->cury[0] = 0;
info->curx[1] = 0;
info->cury[1] = 0;
}

return 0;
}

内部函数

内部函数是驱动中需要使用的一些功能,除了上面的interface_init和reg读取,还有不同触摸屏方向下坐标的换算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static void ft6236_apply_rotate(uint16_t *x, uint16_t *y)
{
uint16_t _x = *x;
uint16_t _y = *y;

switch (g_dev.direction) {
case TOUCH_DIR_LRTB:
*x = _x;
*y = _y;
break;
case TOUCH_DIR_LRBT:
*x = _x;
*y = g_dev.height - _y;
break;
case TOUCH_DIR_RLTB:
*x = g_dev.width - _x;
*y = _y;
break;
case TOUCH_DIR_RLBT:
*x = g_dev.width - _x;
*y = g_dev.height - _y;
break;
case TOUCH_DIR_TBLR:
*x = _y;
*y = _x;
break;
case TOUCH_DIR_BTLR:
*x = _y;
*y = g_dev.width - _x;
break;
case TOUCH_DIR_TBRL:
*x = g_dev.height - _y;
*y = _x;
break;
case TOUCH_DIR_BTRL:
*x = g_dev.height - _y;
*y = g_dev.width - _x;
break;

default:
break;
}
}