前言
(1)讲良心话,如果是想真心了解句柄是什么的,就直接看代码。国内由太多关于句柄的一些专业术语了。反正互相抄,这些垃圾文,我也懒得喷,因为大家都在骂,我就不凑热闹了。
(2)不过,唯一让我感到诧异的是,这些垃圾文的收藏量居然惊人的高,以至于明明国内一大批讲解句柄是什么的。却在交流群中常常能够看到很多人问,句柄到底是什么。
(3)我也不想自夸,反正呢,我只能保证这篇博客和网上哪些动不动一堆术语无脑堆的不同。我直接上代码。
(4)事先叠甲,本文需要一定的C语言指针基础。零基础铁定看不懂
实例代码讲解
(1)国内网站已有大量这种堆术语的,我就不再赘述,直接上实际代码。
(2)以我最近移植的一款AHT20温湿度传感器为例子,AHT20
是I2C
设备。当我每次和AHT20
进行交互的时候,都需要告诉记得AHT20
挂载在哪条I2C上,这无疑非常的麻烦。
(3)于是我们可以使用句柄。我们只有在初始化的时候,需要记得,AHT20
挂载在哪条总线上,之后只需要操作aht20_create()
函数返回的句柄即可。
(4)我个人认为,学习编程术语最好的办法就是直接看代码,因为人的语言容易产生歧义。所以我也不想多废话了,直接上代码。建议不理解的人多看几遍如下代码。
<1>如下代码中的i2c_cmd_link_create()
,i2c_master_start()
,i2c_master_write_byte()
,i2c_master_cmd_begin()
都是ESP32的库函数。其实我们不需要了解这四个库函数有什么用,只需要知道,这些函数里面,一旦使用到了一些特定的参数,例如sens->bus
(AHT20
挂载的I2C
端口),或者sens->dev_addr
(AHT20
的地址)的时候,我其他外部文件调用aht20_read_data()
只需要传入一个句柄aht20_handle_t
,就可以无脑操作了。
<2>但是,需要注意的一点是,aht20_create()
创建句柄的时候,还是要记住AHT20
的一些特定信息的,例如挂载的I2C
端口和地址信息。
/*---------------------*/
/*------ aht20.h ------*/
/*---------------------*/
typedef void *aht20_handle_t; //这个就是句柄,对外可见
/**
* @brief 创建一个AHT20的句柄
*
* @param port 挂载在哪个I2C上
* -ATH20_SLAVE_ADDRESS 传感器AHT20的地址
*
* @return AHT20的句柄
*/
aht20_handle_t aht20_create(i2c_port_t port, uint8_t ATH20_SLAVE_ADDRESS);
/**
* @brief 读取AHT20的温湿度数据
*
* @param sensor 句柄
* -humidity 读取到的湿度值
* -temperature 读取到的温度值
*
* @return 无
*/
esp_err_t aht20_read_data(aht20_handle_t sensor, float* humidity, float* temperature);
/*---------------------*/
/*------ aht20.c ------*/
/*---------------------*/
typedef struct
{
i2c_port_t bus;
uint8_t dev_addr;
// ...
}aht20_dev_t; //这个就是TCB,只有aht20.c文件可以使用,对外不可见
aht20_handle_t aht20_create(i2c_port_t port, uint8_t ATH20_SLAVE_ADDRESS )
{
aht20_dev_t *sens = (aht20_dev_t *) calloc(1, sizeof(aht20_dev_t));
/*=== AHT20命令初始化 ===*/
sens->bus = port;
sens->dev_addr = ATH20_SLAVE_ADDRESS << 1;
// ...
}
esp_err_t aht20_read_data(aht20_handle_t sensor, float* humidity, float* temperature)
{
aht20_dev_t *sens = (aht20_dev_t *) sensor;
esp_err_t ret;
// ...
cmd = i2c_cmd_link_create();
ret = i2c_master_start(cmd);
ret = i2c_master_write_byte(cmd, sens->dev_addr | I2C_MASTER_READ, true);
// ...
ret = i2c_master_cmd_begin(sens->bus, cmd, 10 / portTICK_PERIOD_MS);
return ESP_OK;
}
/*---------------------*/
/*------ main.h ------*/
/*---------------------*/
void main()
{
float humidity, temperature;
aht20_handle_t aht20;
//创建AHT20句柄
aht20 = aht20_create(I2C_NUM_0);
while(1)
{
//读取AHT20温湿度数据
ret = aht20_read_data(aht20, &humidity, &temperature);
//判断数据是否读取成功
TEST_ASSERT_EQUAL(ESP_OK, ret);
//打印温湿度数据
ESP_LOGI(TAG,"humidity is %f , temperature is %f C",humidity,temperature);
}
}
(5)看完上面的代码,可能就会有有同学会说了,这样做会不会需要多浪费一个4字节的空间存放
void *
类型的句柄(假设32位机器)?既然有了TCB了,我们不可以让TCB结构体全局可见吗(被extern
修饰的结构体变量)?这样不就节省创建一个void *
类型变量。这也很明显是开发经验缺乏的导致的疑惑,如果是大型项目,一定是要避免全局可见的变量的,因为很容易产生各种奇奇怪怪的问题。毕竟, 你要知道一个大型项目是几十人甚至上百人开发。开发周期有很长,你这种全局可见的变量,可能别人的变量名字冲突了最后他修改了你的值,产生bug调试很托开发周期,很容易出现一颗老鼠屎打烂一锅汤的情况。
结论
(1)所谓的句柄很好理解,说白了就是一个
void *
类型的指针。我们利用这个指针,通过强制类型转换,可很好的实现高内聚低耦合的特性。
(2)TCB就是句柄通过强制类型转换之后的结构类型指针,TCB和句柄指向的是同一个区域。他们两者的区别如下:
<1>TCB是不对外可见,只有当前文件可以使用。而句柄是对外可见的。
<2>句柄是一个void *
类型的指针,不能直接操作。TCB是一个具象的结构体指针,可以直接操作。