【驱动】SPI驱动分析(五)-模拟SPI驱动
简介
模拟SPI驱动是一种软件实现的SPI总线驱动。在没有硬件SPI控制器的系统中,通过软件模拟实现SPI总线的功能。它允许在不修改硬件的情况下,通过GPIO(通用输入/输出)引脚模拟SPI总线的通信,从而与SPI设备进行数据交换。
模拟SPI驱动相对于硬件SPI来说,可能会有一定的性能损失,因为软件模拟不如硬件实现的SPI控制器快速和高效。
模拟SPI驱动相比硬件SPI控制器存在一些缺点,包括:
- 性能较低:软件模拟SPI需要通过GPIO引脚进行数据的输入和输出,并进行相应的时序控制。相比硬件SPI控制器,软件模拟SPI的速度较慢,通信效率较低,特别是在高速数据传输和频繁通信的场景下。
- 占用CPU资源:模拟SPI驱动在内核空间运行,需要通过CPU执行软件代码来模拟SPI总线的功能。这会占用一定的CPU资源,可能导致系统性能下降,并且可能影响其他任务的响应时间。
- 时序控制的挑战:软件模拟SPI需要准确控制数据的时序,包括数据的传输速率、时钟边沿和信号延迟等。需要仔细处理时序相关的问题,确保正确的数据传输和可靠性。
- 受限于GPIO资源:模拟SPI驱动需要使用系统中的GPIO引脚来模拟SPI总线的通信,因此受限于可用的GPIO资源数量。如果系统中可用的GPIO引脚有限,可能会限制同时连接的SPI设备数量或引起硬件扩展的困难。
内核中模拟SPI驱动的实现
在Linux内核中,SPI子系统提供了用于管理SPI总线和设备的功能和接口。虽然SPI子系统本身不直接提供模拟SPI驱动的功能,但它提供了一些接口和框架,可以用于实现模拟SPI驱动。
- SPI GPIO框架:SPI子系统提供了一个名为
spi-gpio的框架,可使用GPIO引脚模拟SPI总线,gpio模拟spi代码在drivers/spi/spi-gpio.c中。这个框架允许将GPIO引脚配置为SPI总线的时钟、片选、输入和输出信号,并提供了对应的接口函数供驱动程序使用。 - spi-bitbang:spi-bitbang是Linux内核中提供的一个通用框架,用于在没有硬件SPI控制器或需要灵活控制SPI时序和配置的系统中模拟SPI总线的通信。代码在
spi-bitbang.c中
下面我们分别分析下这drivers/spi/spi-gpio.c 和 spi-bitbang.c两个文件。
spi-gpio
platform_driver
spi_gpio_driver 属于总线设备驱动模型中的一种。当设备树中的compatible字段与spi_gpio_dt_ids的compatible匹配时,spi_gpio_probe将被调用,在probe函数中初始化并注册设备。
static struct platform_driver spi_gpio_driver = {
.driver = {
.name = DRIVER_NAME,
.of_match_table = of_match_ptr(spi_gpio_dt_ids),
},
.probe = spi_gpio_probe,
.remove = spi_gpio_remove,
};
module_platform_driver(spi_gpio_driver);
spi_gpio_dt_ids
设备树需要添加 spi-gpio节点,这样才能probe成功。
static const struct of_device_id spi_gpio_dt_ids[] = {
{ .compatible = "spi-gpio" },
{}
};
spi_gpio_probe_dt
spi_gpio_probe_dt主要作用是解析设备树中的SPI GPIO设备信息,并将其存储在platform_data结构体中。这样,在SPI GPIO驱动的探测函数中,可以通过pdev设备的platform_data字段获取这些信息,并根据需要进行相应的配置和操作。
static int spi_gpio_probe_dt(struct platform_device *pdev)
{
int ret;
u32 tmp;
struct spi_gpio_platform_data *pdata;
struct device_node *np = pdev->dev.of_node;
const struct of_device_id *of_id =
of_match_device(spi_gpio_dt_ids, &pdev->dev);
if (!of_id)
return 0;
pdata = devm_kzalloc(&pdev->dev, sizeof(*pdata), GFP_KERNEL);
if (!pdata)
return -ENOMEM;
ret = of_get_named_gpio(np, "gpio-sck", 0);
if (ret < 0) {
dev_err(&pdev->dev, "gpio-sck property not found\n");
goto error_free;
}
pdata->sck = ret;
ret = of_get_named_gpio(np, "gpio-miso", 0);
if (ret < 0) {
dev_info(&pdev->dev, "gpio-miso property not found, switching to no-rx mode\n");
pdata->miso = SPI_GPIO_NO_MISO;
} else
pdata->miso = ret;
ret = of_get_named_gpio(np, "gpio-mosi", 0);
if (ret < 0) {
dev_info(&pdev->dev, "gpio-mosi property not found, switching to no-tx mode\n");
pdata->mosi = SPI_GPIO_NO_MOSI;
} else
pdata->mosi = ret;
ret = of_property_read_u32(np, "num-chipselects", &tmp);
if (ret < 0) {
dev_err(&pdev->dev, "num-chipselects property not found\n");
goto error_free;
}
pdata->num_chipselect = tmp;
pdev->dev.platform_data = pdata;
return 1;
error_free:
devm_kfree(&pdev->dev, pdata);
return ret;
}
spi_gpio_probe
spi_gpio_probe使用了bitbang模式实现SPI协议的位操作传输,在Linux内核的SPI子系统中注册并初始化一个SPI GPIO设备。
static int spi_gpio_probe(struct platform_device *pdev)
{
int status;
struct spi_master *master;
struct spi_gpio *spi_gpio;
struct spi_gpio_platform_data *pdata;
u16 master_flags = 0;
bool use_of = 0;
int num_devices;
// 解析设备树中的SPI GPIO设备信息并初始化platform_data结构体
status = spi_gpio_probe_dt(pdev);
if (status < 0)
return status;
if (status > 0)
use_of = 1;
// 获取设备的platform_data结构体
pdata = dev_get_platdata(&pdev->dev);
#ifdef GENERIC_BITBANG
// 如果没有platform_data或者设备树中没有定义num_chipselect属性,返回错误码
if (!pdata || (!use_of && !pdata->num_chipselect))
return -ENODEV;
#endif
if (use_of && !SPI_N_CHIPSEL)
num_devices = 1;
else
num_devices = SPI_N_CHIPSEL;
// 请求和配置SPI GPIO相关的GPIO资源
status = spi_gpio_request(pdata, dev_name(&pdev->dev), &master_flags);
if (status < 0)
return status;
// 分配spi_master结构体,并保存spi_gpio结构体指针
master = spi_alloc_master(&pdev->dev, sizeof(*spi_gpio) +
(sizeof(unsigned long) * num_devices));
if (!master) {
status = -ENOMEM;
goto gpio_free;
}
spi_gpio = spi_master_get_devdata(master);
platform_set_drvdata(pdev, spi_gpio);
spi_gpio->pdev = pdev;
if (pdata)
spi_gpio->pdata = *pdata;
// 设置spi_master结构体的一些字段
master->bits_per_word_mask = SPI_BPW_RANGE_MASK(1, 32);
master->flags = master_flags;
master->bus_num = pdev->id;
master->num_chipselect = num_devices;
master->setup = spi_gpio_setup;
master->cleanup = spi_gpio_cleanup;
#ifdef CONFIG_OF
master->dev.of_node = pdev->dev.of_node;
if (use_of) {
int i;
struct device_node *np = pdev->dev.of_node;
/*
* In DT environments, take the CS GPIO from the "cs-gpios"
* property of the node.
*/
if (!SPI_N_CHIPSEL)
spi_gpio->cs_gpios[0] = SPI_GPIO_NO_CHIPSELECT;
else
for (i = 0; i < SPI_N_CHIPSEL; i++) {
status = of_get_named_gpio(np, "cs-gpios", i);
if (status < 0) {
dev_err(&pdev->dev,
"invalid cs-gpios property\n");
goto gpio_free;
}
spi_gpio->cs_gpios[i] = status;
}
}
#endif
spi_gpio->bitbang.master = master;
spi_gpio->bitbang.chipselect = spi_gpio_chipselect;
// 设置SPI传输相关的回调函数
if ((master_flags & (SPI_MASTER_NO_TX | SPI_MASTER_NO_RX)) == 0) {
spi_gpio->bitbang.txrx_word[SPI_MODE_0] = spi_gpio_txrx_word_mode0;
spi_gpio->bitbang.txrx_word[SPI_MODE_1] = spi_gpio_txrx_word_mode1;
spi_gpio->bitbang.txrx_word[SPI_MODE_2] = spi_gpio_txrx_word_mode2;
spi_gpio->bitbang.txrx_word[SPI_MODE_3] = spi_gpio_txrx_word_mode3;
} else {
spi_gpio->bitbang.txrx_word[SPI_MODE_0] = spi_gpio_spec_txrx_word_mode0;
spi_gpio->bitbang.txrx_word[SPI_MODE_1] = spi_gpio_spec_txrx_word_mode1;
spi_gpio->bitbang.txrx_word[SPI_MODE_2] = spi_gpio_spec_txrx_word_mode2;
spi_gpio->bitbang.txrx_word[SPI_MODE_3] = spi_gpio_spec_txrx_word_mode3;
}
spi_gpio->bitbang.setup_transfer = spi_bitbang_setup_transfer;
spi_gpio->bitbang.flags = SPI_CS_HIGH;
// 启动SPI GPIO位操作传输
status = spi_bitbang_start(&spi_gpio->bitbang);
if (status < 0) {
gpio_free:
if (SPI_MISO_GPIO != SPI_GPIO_NO_MISO)
gpio_free(SPI_MISO_GPIO);
if (SPI_MOSI_GPIO != SPI_GPIO_NO_MOSI)
gpio_free(SPI_MOSI_GPIO);
gpio_free(SPI_SCK_GPIO);
spi_master_put(master);
}
return status;
}
函数所做工作如下:
- 首先,函数调用
spi_gpio_probe_dt()函数来解析设备树(Device Tree)中的SPI GPIO设备信息,并初始化platform_data结构体。设备树中包含了SPI GPIO设备的属性,如时钟引脚、数据输入引脚、数据输出引脚等。 - 接下来,函数获取设备的
platform_data结构体,并进行一些合法性检查。 - 函数通过调用
spi_gpio_request()函数请求和配置SPI GPIO相关的GPIO资源。该函数会申请所需的GPIO引脚,并设置引脚的方向和电平。 - 然后,函数使用
spi_alloc_master()函数分配一个spi_master结构体,并保存了指向spi_gpio结构体的指针。同时,通过调用platform_set_drvdata()函数将spi_gpio结构体指针保存在platform_device结构体的driver_data字段中。 - 接着,函数对
spi_master结构体的各个字段进行设置。这些字段包括SPI传输的参数,如数据位宽、传输模式、片选信号数量等。此外,还会设置回调函数,用于数据传输的配置和清理。 - 如果设备树支持(即配置了
CONFIG_OF),函数会获取设备树中的片选信号的GPIO引脚信息。通过遍历设备树中的cs-gpios属性,获取每个片选信号的GPIO引脚。 - 接下来,函数设置
spi_gpio结构体中的bitbang字段,将之前设置的spi_master结构体以及自定义的片选信号处理函数指定给它。 - 根据SPI的传输模式,函数选择不同的回调函数进行数据的传输。如果SPI设备同时支持数据发送和接收,则使用通用的传输函数;否则,使用特定的传输函数。
- 最后,函数调用
spi_bitbang_start()函数启动SPI GPIO的位操作传输。该函数会根据之前设置的参数,开始进行SPI数据的传输。 - 如果启动失败,函数会释放之前请求的GPIO资源,并释放分配的
spi_master结构体的内存空间。
spi_gpio_remove
spi_gpio_remove函数,它的目的是从SPI GPIO平台设备中移除驱动程序并释放相关的资源,如GPIO引脚和SPI主设备。
static int spi_gpio_remove(struct platform_device *pdev)
{
struct spi_gpio *spi_gpio;
struct spi_gpio_platform_data *pdata;
spi_gpio = platform_get_drvdata(pdev);
pdata = dev_get_platdata(&pdev->dev);
/* stop() unregisters child devices too */
spi_bitbang_stop(&spi_gpio->bitbang);
if (SPI_MISO_GPIO != SPI_GPIO_NO_MISO)
gpio_free(SPI_MISO_GPIO);
if (SPI_MOSI_GPIO != SPI_GPIO_NO_MOSI)
gpio_free(SPI_MOSI_GPIO);
gpio_free(SPI_SCK_GPIO);
spi_master_put(spi_gpio->bitbang.master);
return 0;
}
首先,函数接受一个指向
struct platform_device类型的指针pdev作为参数。接下来定义了两个指针变量
spi_gpio和pdata,分别指向struct spi_gpio和struct spi_gpio_platform_data类型的数据结构。platform_get_drvdata(pdev)用于获取存储在平台设备中的私有数据指针,将其赋值给spi_gpio指针。这个私有数据指针通常在设备的probe函数中设置。dev_get_platdata(&pdev->dev)用于获取与设备相关的平台数据,将其赋值给pdata指针。平台数据是在设备的设备树绑定或者通过platform_set_drvdata()函数设置的。spi_bitbang_stop(&spi_gpio->bitbang)调用函数spi_bitbang_stop(),停止SPI位操作的传输。这个函数将注销子设备。接下来的一系列if语句用于检查是否为每个GPIO引脚分配了一个有效的引脚号,并释放这些引脚。
if (SPI_MISO_GPIO != SPI_GPIO_NO_MISO)检查是否为MISO引脚分配了一个非零的引脚号,如果是,则调用gpio_free(SPI_MISO_GPIO)释放该引脚。if (SPI_MOSI_GPIO != SPI_GPIO_NO_MOSI)检查是否为MOSI引脚分配了一个非零的引脚号,如果是,则调用gpio_free(SPI_MOSI_GPIO)释放该引脚。最后,调用gpio_free(SPI_SCK_GPIO)释放SCK引脚。
spi_master_put(spi_gpio->bitbang.master)调用函数spi_master_put(),释放对SPI主设备的引用。最后,函数返回0,表示成功执行函数。
spi_gpio_request
spi_gpio_request的函数,它用于请求并分配SPI GPIO引脚的资源,并根据硬件配置设置SPI主设备的传输和接收功能标志。如果引脚分配成功,函数返回0,否则返回一个非零值表示分配失败。
static int spi_gpio_request(struct spi_gpio_platform_data *pdata,
const char *label, u16 *res_flags)
{
int value;
/* NOTE: SPI_*_GPIO symbols may reference "pdata" */
if (SPI_MOSI_GPIO != SPI_GPIO_NO_MOSI) {
value = spi_gpio_alloc(SPI_MOSI_GPIO, label, false);
if (value)
goto done;
} else {
/* HW configuration without MOSI pin */
*res_flags |= SPI_MASTER_NO_TX;
}
if (SPI_MISO_GPIO != SPI_GPIO_NO_MISO) {
value = spi_gpio_alloc(SPI_MISO_GPIO, label, true);
if (value)
goto free_mosi;
} else {
/* HW configuration without MISO pin */
*res_flags |= SPI_MASTER_NO_RX;
}
value = spi_gpio_alloc(SPI_SCK_GPIO, label, false);
if (value)
goto free_miso;
goto done;
free_miso:
if (SPI_MISO_GPIO != SPI_GPIO_NO_MISO)
gpio_free(SPI_MISO_GPIO);
free_mosi:
if (SPI_MOSI_GPIO != SPI_GPIO_NO_MOSI)
gpio_free(SPI_MOSI_GPIO);
done:
return value;
}
- 首先,函数接受一个指向
struct spi_gpio_platform_data类型的指针pdata、一个指向字符常量的指针label和一个指向u16类型的指针res_flags作为参数。 - 首先,通过比较
SPI_MOSI_GPIO和SPI_GPIO_NO_MOSI的值来判断是否为MOSI引脚分配了一个有效的引脚号。- 如果
SPI_MOSI_GPIO不等于SPI_GPIO_NO_MOSI,表示为MOSI引脚分配了一个有效的引脚号,接下来调用spi_gpio_alloc(SPI_MOSI_GPIO, label, false)函数分配该引脚,并将返回值赋给value变量。如果返回值不为0,则表示分配失败,直接跳转到done标签处。 - 如果
SPI_MOSI_GPIO等于SPI_GPIO_NO_MOSI,表示硬件配置中没有使用MOSI引脚,这时将设置*res_flags中的SPI_MASTER_NO_TX标志,表示SPI主设备没有传输功能。
- 如果
- 接下来,通过比较
SPI_MISO_GPIO和SPI_GPIO_NO_MISO的值来判断是否为MISO引脚分配了一个有效的引脚号。- 如果
SPI_MISO_GPIO不等于SPI_GPIO_NO_MISO,表示为MISO引脚分配了一个有效的引脚号,接下来调用spi_gpio_alloc(SPI_MISO_GPIO, label, true)函数分配该引脚,并将返回值赋给value变量。如果返回值不为0,则表示分配失败,直接跳转到free_mosi标签处。 - 如果
SPI_MISO_GPIO等于SPI_GPIO_NO_MISO,表示硬件配置中没有使用MISO引脚,这时将设置*res_flags中的SPI_MASTER_NO_RX标志,表示SPI主设备没有接收功能。
- 如果
- 最后,调用
spi_gpio_alloc(SPI_SCK_GPIO, label, false)函数分配SCK引脚,并将返回值赋给value变量。如果返回值不为0,则表示分配失败,直接跳转到free_miso标签处。 - 如果程序成功执行到这里,表示所有引脚分配都成功,直接跳转到
done标签处。 free_miso标签处,如果之前为MISO引脚分配了一个有效的引脚号,调用gpio_free(SPI_MISO_GPIO)函数释放该引脚。free_mosi标签处,如果之前为MOSI引脚分配了一个有效的引脚号,调用gpio_free(SPI_MOSI_GPIO)函数释放该引脚。done标签处,函数返回value变量的值,表示引脚分配的结果。如果返回值为0,表示成功执行函数,否则表示分配引脚失败。
spi_gpio_alloc
spi_gpio_alloc函数用于分配和配置一个GPIO引脚的资源。它首先通过gpio_request()函数请求分配GPIO资源,并根据is_in参数来配置引脚的输入或输出模式。如果分配和配置成功,函数返回0,否则返回一个非零值表示分配和配置失败。
static int spi_gpio_alloc(unsigned pin, const char *label, bool is_in)
{
int value;
value = gpio_request(pin, label);
if (value == 0) {
if (is_in)
value = gpio_direction_input(pin);
else
value = gpio_direction_output(pin, 0);
}
return value;
}
- 首先,函数接受一个无符号整数
pin作为GPIO引脚号,一个指向字符常量的指针label作为引脚的标签,以及一个布尔值is_in来指示引脚是否用于输入。 gpio_request(pin, label)调用函数gpio_request()来请求分配指定引脚号的GPIO资源,并将返回值赋给value变量。如果返回值为0,表示成功分配GPIO资源;如果返回值不为0,表示分配失败。- 如果
gpio_request()成功执行,进入条件判断语句块:- 如果
is_in为真,表示该引脚是一个输入引脚,调用gpio_direction_input(pin)函数将该引脚配置为输入模式,并将返回值赋给value变量。如果返回值为0,表示成功配置引脚为输入模式;如果返回值不为0,表示配置失败。 - 如果
is_in为假,表示该引脚是一个输出引脚,调用gpio_direction_output(pin, 0)函数将该引脚配置为输出模式,并将输出电平设置为低电平(0),将返回值赋给value变量。如果返回值为0,表示成功配置引脚为输出模式并设置输出电平;如果返回值不为0,表示配置失败。
- 如果
- 最后,函数返回
value变量的值,表示引脚分配和配置的结果。如果返回值为0,表示成功执行函数;如果返回值不为0,表示分配和配置引脚失败。
spi_gpio_cleanup
spi_gpio_cleanup的函数,用于清理和释放SPI GPIO相关资源。它首先检查特定的SPI片选引脚是否分配了有效的GPIO资源,如果是,则释放该GPIO资源。然后,调用spi_bitbang_cleanup(spi)函数清理和释放与SPI位操作相关的资源。
static void spi_gpio_cleanup(struct spi_device *spi)
{
struct spi_gpio *spi_gpio = spi_to_spi_gpio(spi);
unsigned long cs = spi_gpio->cs_gpios[spi->chip_select];
if (cs != SPI_GPIO_NO_CHIPSELECT)
gpio_free(cs);
spi_bitbang_cleanup(spi);
}
spi_gpio_setup
pi_gpio_setup的函数,用于设置SPI GPIO相关的配置。它首先根据设备树环境或SPI控制器数据获取片选引脚的值,然后根据情况请求分配并配置片选引脚的GPIO资源。如果片选引脚成功分配和配置,将进行SPI位操作的设置。如果设置失败,将释放之前分配的GPIO资源。函数最终返回设置的状态,0表示成功,非零表示失败。
static int spi_gpio_setup(struct spi_device *spi)
{
unsigned long cs;
int status = 0;
struct spi_gpio *spi_gpio = spi_to_spi_gpio(spi);
struct device_node *np = spi->master->dev.of_node;
if (np) {
/*
* In DT environments, the CS GPIOs have already been
* initialized from the "cs-gpios" property of the node.
*/
cs = spi_gpio->cs_gpios[spi->chip_select];
} else {
/*
* ... otherwise, take it from spi->controller_data
*/
cs = (uintptr_t) spi->controller_data;
}
if (!spi->controller_state) {
if (cs != SPI_GPIO_NO_CHIPSELECT) {
status = gpio_request(cs, dev_name(&spi->dev));
if (status)
return status;
status = gpio_direction_output(cs,
!(spi->mode & SPI_CS_HIGH));
}
}
if (!status) {
/* in case it was initialized from static board data */
spi_gpio->cs_gpios[spi->chip_select] = cs;
status = spi_bitbang_setup(spi);
}
if (status) {
if (!spi->controller_state && cs != SPI_GPIO_NO_CHIPSELECT)
gpio_free(cs);
}
return status;
}
- 首先,函数接受一个指向
struct spi_device类型的指针spi作为参数。 - 代码中定义了一个无符号长整型变量
cs,用于存储片选引脚(Chip Select,CS)的值。 - 定义了一个整型变量
status,用于存储函数执行的状态,默认为0。 - 定义了一个指向
struct spi_gpio类型的指针spi_gpio,通过spi_to_spi_gpio(spi)宏将spi转换为spi_gpio结构体。 - 定义了一个指向
struct device_node类型的指针np,用于存储SPI主设备的设备树节点。 - 如果
np非空(非NULL),表示在设备树(Device Tree)环境中,片选引脚已经从节点的"cs-gpios"属性中进行了初始化。- 将
spi_gpio->cs_gpios[spi->chip_select]的值赋给cs,表示获取对应片选引脚的GPIO资源。
- 将
- 如果
np为空,表示不在设备树环境中,从spi->controller_data中获取片选引脚的值。- 将
spi->controller_data的值转换为无符号整数并赋给cs,表示获取对应片选引脚的GPIO资源。
- 将
- 如果
spi->controller_state为空,表示SPI控制器的状态未初始化。- 如果
cs不等于SPI_GPIO_NO_CHIPSELECT,表示为该片选引脚分配了有效的GPIO资源。- 调用
gpio_request(cs, dev_name(&spi->dev))函数请求分配片选引脚的GPIO资源,并将返回值赋给status。 - 如果
gpio_request()执行失败(返回值非零),直接返回status表示设置失败。 - 调用
gpio_direction_output(cs, !(spi->mode & SPI_CS_HIGH))函数将片选引脚配置为输出模式,并根据SPI模式中的SPI_CS_HIGH标志设置输出电平。
- 调用
- 如果
spi->controller_state为空且status仍然为0,表示片选引脚成功分配和配置。
- 如果
- 如果
status为0,表示片选引脚成功分配和配置。- 将
cs的值存储到spi_gpio->cs_gpios[spi->chip_select]中,以便后续使用。 - 调用
spi_bitbang_setup(spi)函数进行SPI位操作相关的设置,将返回值赋给status。
- 将
- 如果
status非零,表示片选引脚或SPI位操作设置发生错误。
- 如果
spi->controller_state为空且cs不等于SPI_GPIO_NO_CHIPSELECT,表示之前为片选引脚分配了GPIO资源,现在需要释放它。- 调用
gpio_free(cs)函数释放片选引脚的GPIO资源。
- 调用
- 最后,函数返回
status,表示设置的结果。如果返回值为0,表示成功执行函数;如果返回值非零,表示设置失败。
spi_gpio_chipselect
spi_gpio_chipselect作用是根据传入的参数控制 SPI 设备的片选信号的 GPIO 引脚状态,以实现 SPI 通信中的片选功能。同时,根据 SPI 协议的要求,可以设置初始时钟极性。
static void spi_gpio_chipselect(struct spi_device *spi, int is_active)
{
struct spi_gpio *spi_gpio = spi_to_spi_gpio(spi);
unsigned long cs = spi_gpio->cs_gpios[spi->chip_select];
/* set initial clock polarity */
if (is_active)
setsck(spi, spi->mode & SPI_CPOL);
if (cs != SPI_GPIO_NO_CHIPSELECT) {
/* SPI is normally active-low */
gpio_set_value_cansleep(cs, (spi->mode & SPI_CS_HIGH) ? is_active : !is_active);
}
}
- 首先,代码定义了一个静态函数
spi_gpio_chipselect,该函数接受两个参数:spi是指向spi_device结构体的指针,表示要控制的 SPI 设备;is_active是一个整数,表示片选信号的状态(激活或非激活)。 - 接下来,代码通过使用
spi_to_spi_gpio函数将spi_device结构体转换为spi_gpio结构体,并将结果保存在spi_gpio变量中。这个转换是为了获取与 GPIO 控制相关的信息。 - 然后,代码使用
spi_device结构体中的chip_select字段来确定当前片选信号对应的 GPIO 引脚编号,并将其保存在cs变量中。 - 代码接着根据
is_active参数来设置初始时钟极性。如果is_active为非零值(表示片选信号激活),则通过spi_device结构体中的mode字段的SPI_CPOL位来确定初始时钟极性,然后调用setsck函数进行设置。 - 接下来,代码检查
cs变量是否等于SPI_GPIO_NO_CHIPSELECT。如果cs不等于该值,说明有有效的片选信号 GPIO 引脚配置。 - 在 SPI 协议中,通常片选信号是低电平有效的。因此,代码使用
gpio_set_value_cansleep函数来设置片选信号 GPIO 引脚的电平。根据spi_device结构体中的mode字段的SPI_CS_HIGH位,如果该位为真,则表示片选信号是高电平有效,那么根据is_active参数的值直接传递给gpio_set_value_cansleep函数;如果该位为假,则表示片选信号是低电平有效,那么取is_active参数的反值作为电平状态传递给gpio_set_value_cansleep函数。
spi-bitbang
bitbang_txrx_32
bitbang_txrx_32用于进行 SPI 位移传输。通过调用传入的函数指针 txrx_word 实现实际的发送和接收操作。
static unsigned bitbang_txrx_32(
struct spi_device *spi,
u32 (*txrx_word)(struct spi_device *spi,
unsigned nsecs,
u32 word, u8 bits),
unsigned ns,
struct spi_transfer *t
) {
unsigned bits = t->bits_per_word;
unsigned count = t->len;
const u32 *tx = t->tx_buf;
u32 *rx = t->rx_buf;
while (likely(count > 3)) {
u32 word = 0;
if (tx)
word = *tx++;
word = txrx_word(spi, ns, word, bits);
if (rx)
*rx++ = word;
count -= 4;
}
return t->len - count;
}
- 函数开始时,从传输结构体
t中获取位数bits,数据长度count,发送缓冲区指针tx和接收缓冲区指针rx。 - 进入
while循环,当数据长度count大于 3(32 位数据的字长)时,循环继续。 - 在每次循环中,首先定义一个变量
word,用于保存当前要发送或接收的数据字。 - 如果发送缓冲区存在 (
tx不为空),则将发送缓冲区中的数据字赋值给word,并将指针tx向后移动。 - 调用函数指针
txrx_word,将 SPI 设备指针spi、传输时间ns、数据字word和位数bits作为参数传递给该函数。函数txrx_word将执行实际的发送和接收操作,并返回接收到的数据字,该数据字将被赋值给word。 - 如果接收缓冲区存在 (
rx不为空),则将word的值存储到接收缓冲区中,并将指针rx向后移动。 - 减少数据长度
count的值,表示已经处理了一个数据字。 - 循环回到第 2 步,继续处理下一个数据字,直到剩余数据长度不足以组成完整的 32 位数据。
- 返回传输结构体的长度
t->len减去剩余的未处理数据长度count,表示成功发送和接收的数据长度。
spi_bitbang_setup_transfer
spi_bitbang_setup_transfer用于设置 SPI 位移传输的参数的函数。根据传入的传输结构体 t 中的参数设置位移传输的位数和速率,并选择相应的位移传输函数进行数据的发送和接收。
int spi_bitbang_setup_transfer(struct spi_device *spi, struct spi_transfer *t)
{
struct spi_bitbang_cs *cs = spi->controller_state;
u8 bits_per_word;
u32 hz;
if (t) {
bits_per_word = t->bits_per_word;
hz = t->speed_hz;
} else {
bits_per_word = 0;
hz = 0;
}
/* spi_transfer level calls that work per-word */
if (!bits_per_word)
bits_per_word = spi->bits_per_word;
if (bits_per_word <= 8)
cs->txrx_bufs = bitbang_txrx_8;
else if (bits_per_word <= 16)
cs->txrx_bufs = bitbang_txrx_16;
else if (bits_per_word <= 32)
cs->txrx_bufs = bitbang_txrx_32;
else
return -EINVAL;
/* nsecs = (clock period)/2 */
if (!hz)
hz = spi->max_speed_hz;
if (hz) {
cs->nsecs = (1000000000/2) / hz;
if (cs->nsecs > (MAX_UDELAY_MS * 1000 * 1000))
return -EINVAL;
}
return 0;
}
- 首先,从 SPI 设备结构体中获取位移传输相关的控制器状态结构体
cs。 - 接着,定义变量
bits_per_word和hz,用于保存位数和传输速率。 - 如果传输结构体
t不为空,将从传输结构体中获取位数和速率,并分别赋值给bits_per_word和hz。如果t为空,则将bits_per_word和hz设置为 0。 - 如果位数
bits_per_word为零,将从 SPI 设备结构体中获取默认的位数spi->bits_per_word。 - 根据位数
bits_per_word的大小,决定使用不同的位移传输函数。 - 如果传输速率
hz为零,将从 SPI 设备结构体中获取最大速率spi->max_speed_hz。 - 如果速率
hz不为零,计算每个位移传输的时间间隔nsecs。这里假设时钟周期为传输速率的倒数的一半(即半周期)。计算公式为nsecs = (clock period)/2 = (1000000000/2) / hz。 - 如果计算得到的时间间隔
nsecs超过最大延迟时间,则返回错误码-EINVAL,表示时间间隔过大。 - 如果都设置成功,返回 0,表示设置传输参数的函数执行成功。
spi_bitbang_setup
spi_bitbang_setup在进行实际的数据传输之前,设置 SPI 设备的位移传输相关参数。它通过获取位移传输函数、调用传输参数设置函数、设置片选信号状态等步骤来完成设置。
int spi_bitbang_setup(struct spi_device *spi)
{
struct spi_bitbang_cs *cs = spi->controller_state;
struct spi_bitbang *bitbang;
bitbang = spi_master_get_devdata(spi->master);
if (!cs) {
cs = kzalloc(sizeof(*cs), GFP_KERNEL);
if (!cs)
return -ENOMEM;
spi->controller_state = cs;
}
/* per-word shift register access, in hardware or bitbanging */
cs->txrx_word = bitbang->txrx_word[spi->mode & (SPI_CPOL|SPI_CPHA)];
if (!cs->txrx_word)
return -EINVAL;
if (bitbang->setup_transfer) {
int retval = bitbang->setup_transfer(spi, NULL);
if (retval < 0)
return retval;
}
dev_dbg(&spi->dev, "%s, %u nsec/bit\n", __func__, 2 * cs->nsecs);
/* NOTE we _need_ to call chipselect() early, ideally with adapter
* setup, unless the hardware defaults cooperate to avoid confusion
* between normal (active low) and inverted chipselects.
*/
/* deselect chip (low or high) */
mutex_lock(&bitbang->lock);
if (!bitbang->busy) {
bitbang->chipselect(spi, BITBANG_CS_INACTIVE);
ndelay(cs->nsecs);
}
mutex_unlock(&bitbang->lock);
return 0;
}
- 首先,从 SPI 设备结构体中获取控制器状态结构体
cs和位移传输相关的控制结构体bitbang。 - 使用函数
spi_master_get_devdata从 SPI 主设备结构体中获取位移传输控制结构体bitbang。 - 如果控制器状态结构体
cs为空,说明还没有为该 SPI 设备分配控制器状态结构体,此时需要为其分配内存并初始化。使用函数kzalloc分配内存,并将分配的内存赋值给cs。如果内存分配失败,则返回错误码-ENOMEM。 - 将控制器状态结构体
cs赋值给 SPI 设备结构体中的controller_state字段。 - 从位移传输控制结构体
bitbang中根据 SPI 设备的模式(spi->mode)获取相应的位移传输函数txrx_word。根据 SPI 设备的模式中的SPI_CPOL(时钟极性)和SPI_CPHA(时钟相位)位进行位运算,获取相应的位移传输函数。如果获取的函数为空,则返回错误码-EINVAL。 - 如果位移传输控制结构体
bitbang中的setup_transfer函数存在,则调用该函数设置传输参数。传输参数通过传递 SPI 设备指针spi和空指针NULL来实现。如果setup_transfer函数返回的值小于 0,则表示设置传输参数失败,此时返回该错误码。 - 在进行实际的数据传输之前,需要先选择片选信号,并确保片选信号处于非活动状态。
- 通过互斥锁
bitbang->lock来保护对位移传输控制结构体的访问。首先获取互斥锁。 - 检查位移传输控制结构体
bitbang中的busy标志。如果该标志为假,则表示当前没有其他数据传输操作正在进行,此时调用chipselect函数将片选信号设置为非活动状态,并通过ndelay函数延迟一段时间,以确保片选信号稳定。 - 最后,释放互斥锁
bitbang->lock。
spi_bitbang_transfer_one
spi_bitbang_transfer_one作用是执行单个传输操作,包括设置传输参数和进行数据的发送和接收。它通过调用位移传输控制结构体中的函数来完成传输操作,并根据传输的结果来设置传输操作的状态。最后,执行收尾工作并返回传输操作的状态。
static int spi_bitbang_transfer_one(struct spi_master *master,
struct spi_device *spi,
struct spi_transfer *transfer)
{
struct spi_bitbang *bitbang = spi_master_get_devdata(master);
int status = 0;
if (bitbang->setup_transfer) {
status = bitbang->setup_transfer(spi, transfer);
if (status < 0)
goto out;
}
if (transfer->len)
status = bitbang->txrx_bufs(spi, transfer);
if (status == transfer->len)
status = 0;
else if (status >= 0)
status = -EREMOTEIO;
out:
spi_finalize_current_transfer(master);
return status;
}
- 首先,从 SPI 主设备结构体中获取位移传输相关的控制结构体
bitbang。 - 定义变量
status来保存传输操作的状态,默认为 0。 - 如果位移传输控制结构体
bitbang中的setup_transfer函数存在,则调用该函数设置传输参数。传输参数通过传递 SPI 设备指针spi和传输结构体指针transfer来实现。如果设置传输参数的函数返回的值小于 0,则表示设置传输参数失败,此时跳转到标签out处进行处理。 - 检查传输结构体
transfer的数据长度len是否非零。如果非零,则调用位移传输控制结构体bitbang中的txrx_bufs函数进行数据的发送和接收。 - 判断传输操作的状态
status是否等于传输结构体transfer的长度len。如果相等,则表示传输操作成功完成,将状态status设置为 0。如果状态status大于等于 0 但不等于传输结构体transfer的长度len,则表示传输操作未完成,将状态status设置为-EREMOTEIO。 - 跳转到标签
out处,执行spi_finalize_current_transfer函数,以完成当前传输的收尾工作。 - 返回传输操作的状态
status。
spi_bitbang_bufs
spi_bitbang_bufs封装了对控制器状态结构体中的位移传输函数的调用,以执行数据缓冲区的传输。它通过获取位移传输所需的时间间隔和调用位移传输函数来完成传输操作,并返回传输操作的状态。
static int spi_bitbang_bufs(struct spi_device *spi, struct spi_transfer *t)
{
struct spi_bitbang_cs *cs = spi->controller_state;
unsigned nsecs = cs->nsecs;
return cs->txrx_bufs(spi, cs->txrx_word, nsecs, t);
}
- 首先,从 SPI 设备结构体中获取控制器状态结构体
cs。 - 从控制器状态结构体
cs中获取位移传输所需的时间间隔nsecs。 - 调用控制器状态结构体
cs中的txrx_bufs函数来执行数据缓冲区的传输。该函数接受 SPI 设备指针spi、位移传输函数txrx_word、时间间隔nsecs和传输结构体指针t作为参数,并返回传输操作的状态。 - 将传输操作的状态作为函数的返回值。
spi_bitbang_prepare_hardware
spi_bitbang_prepare_hardware作用是在进行数据传输之前,准备 SPI 硬件。它通过设置位移传输控制结构体中的 busy 标志来指示硬件正在忙于数据传输操作。这样做可以确保在进行数据传输之前,其他线程不会干扰硬件的正常操作。
static int spi_bitbang_prepare_hardware(struct spi_master *spi)
{
struct spi_bitbang *bitbang;
bitbang = spi_master_get_devdata(spi);
mutex_lock(&bitbang->lock);
bitbang->busy = 1;
mutex_unlock(&bitbang->lock);
return 0;
}
- 首先,从 SPI 主设备结构体中获取位移传输相关的控制结构体
bitbang。 - 使用互斥锁
bitbang->lock来保护对位移传输控制结构体的访问。首先获取互斥锁。 - 将位移传输控制结构体中的
busy标志设置为 1,表示硬件正在忙于数据传输操作。 - 释放互斥锁
bitbang->lock,以允许其他线程访问位移传输控制结构体。 - 返回 0,表示硬件准备操作执行成功。
spi_bitbang_unprepare_hardware
spi_bitbang_unprepare_hardware在完成数据传输后释放 SPI 硬件资源。通过将位移传输控制结构体中的 busy 标志设置为 0,表示硬件不再忙于数据传输操作。这样做可以确保在释放硬件资源之前,其他线程可以正常访问硬件。
static int spi_bitbang_unprepare_hardware(struct spi_master *spi)
{
struct spi_bitbang *bitbang;
bitbang = spi_master_get_devdata(spi);
mutex_lock(&bitbang->lock);
bitbang->busy = 0;
mutex_unlock(&bitbang->lock);
return 0;
}
- 首先,从 SPI 主设备结构体中获取位移传输相关的控制结构体
bitbang。 - 使用互斥锁
bitbang->lock来保护对位移传输控制结构体的访问。首先获取互斥锁。 - 将位移传输控制结构体中的
busy标志设置为 0,表示硬件不再忙于数据传输操作。 - 释放互斥锁
bitbang->lock,以允许其他线程访问位移传输控制结构体。 - 返回 0,表示硬件释放操作执行成功。
spi_bitbang_set_cs
spi_bitbang_set_cs作用是根据输入参数的值来控制 SPI 设备的片选信号。它通过检查 SPI 设备的传输模式和输入参数的值,判断是否需要使能片选信号,并调用位移传输控制结构体中的函数来设置片选信号的状态。同时,使用延迟函数确保在改变片选信号状态前后有适当的延迟时间。
static void spi_bitbang_set_cs(struct spi_device *spi, bool enable)
{
struct spi_bitbang *bitbang = spi_master_get_devdata(spi->master);
/* SPI core provides CS high / low, but bitbang driver
* expects CS active
* spi device driver takes care of handling SPI_CS_HIGH
*/
enable = (!!(spi->mode & SPI_CS_HIGH) == enable);
ndelay(SPI_BITBANG_CS_DELAY);
bitbang->chipselect(spi, enable ? BITBANG_CS_ACTIVE :
BITBANG_CS_INACTIVE);
ndelay(SPI_BITBANG_CS_DELAY);
}
- 首先,从 SPI 设备结构体的主设备结构体中获取位移传输相关的控制结构体
bitbang。 - 通过检查 SPI 设备的传输模式中的
SPI_CS_HIGH标志位,以及函数输入参数enable的值,来确定是否需要使能片选信号。这是为了确保兼容片选信号的极性。 - 使用
ndelay函数引入延迟,以确保在改变片选信号状态之前有足够的时间。 - 调用位移传输控制结构体
bitbang中的chipselect函数,以控制片选信号的状态。根据enable的值,将片选信号设置为活动状态或非活动状态。 - 再次使用
ndelay函数引入延迟,以确保在改变片选信号状态后有足够的时间。
spi_bitbang_start
spi_bitbang_start进行了一系列的设置和配置,包括初始化互斥锁、设置主设备结构体中的函数指针、注册 SPI 主设备等。通过这些操作,可以使得 SPI 位移传输准备就绪,并可以进行数据传输操作。
int spi_bitbang_start(struct spi_bitbang *bitbang)
{
struct spi_master *master = bitbang->master;
int ret;
if (!master || !bitbang->chipselect)
return -EINVAL;
mutex_init(&bitbang->lock);
if (!master->mode_bits)
master->mode_bits = SPI_CPOL | SPI_CPHA | bitbang->flags;
if (master->transfer || master->transfer_one_message)
return -EINVAL;
master->prepare_transfer_hardware = spi_bitbang_prepare_hardware;
master->unprepare_transfer_hardware = spi_bitbang_unprepare_hardware;
master->transfer_one = spi_bitbang_transfer_one;
master->set_cs = spi_bitbang_set_cs;
if (!bitbang->txrx_bufs) {
bitbang->use_dma = 0;
bitbang->txrx_bufs = spi_bitbang_bufs;
if (!master->setup) {
if (!bitbang->setup_transfer)
bitbang->setup_transfer =
spi_bitbang_setup_transfer;
master->setup = spi_bitbang_setup;
master->cleanup = spi_bitbang_cleanup;
}
}
/* driver may get busy before register() returns, especially
* if someone registered boardinfo for devices
*/
ret = spi_register_master(spi_master_get(master));
if (ret)
spi_master_put(master);
return ret;
}
- 首先,从位移传输控制结构体中获取 SPI 主设备结构体
master。 - 检查主设备结构体和位移传输控制结构体中的片选信号控制函数是否存在,如果不存在则返回错误码 -EINVAL。
- 初始化互斥锁
bitbang->lock,用于保护对位移传输控制结构体的访问。 - 如果主设备结构体中的传输模式位字段
mode_bits未设置,则设置为默认的模式位,包括 SPI_CPOL、SPI_CPHA 和位移传输控制结构体中的标志位。 - 检查主设备结构体中的传输函数是否已经定义,如果已定义则返回错误码 -EINVAL。
- 设置主设备结构体中的准备硬件传输函数、释放硬件传输函数、单次传输函数和片选信号控制函数,分别对应位移传输控制结构体中的对应函数。
- 如果位移传输控制结构体中的数据缓冲区传输函数未定义,则设置使用 DMA 标志为 0,并将数据缓冲区传输函数设置为默认的位移传输函数
spi_bitbang_bufs。如果主设备结构体中的设置函数未定义,则设置使用默认的设置函数spi_bitbang_setup和清理函数spi_bitbang_cleanup。 - 注册 SPI 主设备,将其添加到系统中。如果注册失败,则释放主设备结构体并返回错误码。
- 返回注册结果,成功时返回 0。
spi_bitbang_stop
spi_bitbang_stop作用是停止 SPI 位移传输。它通过取消注册 SPI 主设备来停止相关的传输操作。这可以用于在不需要进行 SPI 位移传输时,将相关资源释放并从系统中移除相应的设备。
void spi_bitbang_stop(struct spi_bitbang *bitbang)
{
spi_unregister_master(bitbang->master);
}
- 从位移传输控制结构体中获取 SPI 主设备结构体
master。 - 调用
spi_unregister_master函数来取消注册 SPI 主设备。这将从系统中移除该 SPI 主设备。
本文参考
https://blog.csdn.net/qq_16054639/article/details/106733956
https://www.cnblogs.com/TWL123/p/9516269.html
https://whycan.com/t_5012.html
https://www.imooc.com/article/33911
https://blog.csdn.net/Creator_Ly/article/details/109640572
【驱动】SPI驱动分析(五)-模拟SPI驱动的更多相关文章
- spi驱动框架全面分析,从master驱动到设备驱动
内核版本:linux2.6.32.2 硬件资源:s3c2440 参考: 韦东山SPI视频教程 内容概括: 1.I2C 驱动框架回顾 2.SPI 框架简单介绍 3.maste ...
- 【Linux高级驱动】如何分析并移植网卡驱动
dm9000的驱动分析 m9000_init platform_driver_register(); db); db); ); ; id_val ; id_val ; /* 获取芯片型号 */ id ...
- linux SPI驱动——gpio模拟spi驱动(三)
一:首先在我的平台注册platform_device,保证能让spi-gpio.c能执行到probe函数. 1: struct spi_gpio_platform_data { 2: unsigned ...
- linux enc28j60网卡驱动移植(硬件spi和模拟spi)
本来想移植DM9000网卡的驱动,无奈硬件出了点问题,通过杜邦线链接开发板和DM9000网卡模块,系统上电,还没加载网卡驱动就直接崩溃了,找不到原因...刚好手上有一个enc28j60的网卡模块,于是 ...
- am335x gpio 模拟 spi 驱动添加
kernel 内 make menuconfig // make menuconfig Device Drivers ---> [*] SPI support ---> <*> ...
- STM32-24位AD7799驱动之手册代码详解,支持模拟SPI和硬件SPI
1.AD7799介绍 AD7799结构图如下所示: 其中REFIN参考电压建议为2.5V, REFIN电压低于0.1V时,则差分输入ad值就无法检测了,如下图所示: 注意: 如果REG_CONFIG的 ...
- linux SPI驱动——简单的gpio模拟SPI驱动测试 (二)
1: /* 2: * Add by xuyonghong for duotin car radio fm 3: * Copyright (C) 2016-5-24 xuyonghong@duotin. ...
- linux spi驱动开发学习-----spidev.c和spi test app
一.spidev.c文件 看一个设备驱动的方法: module_init标识的入口初始化函数spidev_init,(module_exit标识的出口函数) 设备与设备驱动匹配时候调用的probe方法 ...
- DAVINCI DM6446 开发攻略——V4L2视频驱动和应用分析
针对DAVINCI DM6446平台,网络上也有很多网友写了V4L2的驱动,但只是解析Montavista linux-2.6.10 V4L2的原理.结构和函数,深度不够.本文决定把Montavis ...
- linux设备驱动归纳总结(五):3.操作硬件——IO静态映射【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-83299.html linux设备驱动归纳总结(五):3.操作硬件——IO静态映射 xxxxxxxxx ...
随机推荐
- 0x04.信息收集
探针 被动:借助网上的一些接口查询或者网上已经获取到的,查看历史信息. 主动:使用工具,从本地流量出发,探测目标信息,会发送大量流量到对方服务器上. 谷歌语法 懒人语法:https://pentest ...
- 轻量级SpringBoot配置中心 - Minimal-Config
介绍 minimal-config-spring-boot-starter,是基于Spring-Boot原生配置注入实现原理的基础上,拓展的轻量级配置中心,项目体积只有24KB,设计理念为服务中小型项 ...
- linux笔记一(基础命令)
总结: 1.ls – List ls会列举出当前工作目录的内容(文件或文件夹),就跟你在GUI中打开一个文件夹去看里面的内容一样. 2.mkdir – Make Directory mkdir 用 ...
- Codeforces Round 906 (Div. 2)A-E1
A. Doremy's Paint 3 记数组中数的种类数为\(k\),当\(k=1\)时,答案为\(yes\):当\(k=2\)时,记两个种类的数的个数差为\(d\),当\(d≤1\)时,答案为\( ...
- [ABC281F] Xor Minimization
div class="part"> Problem Statement You are given a sequence of non-negative integers $ ...
- Git使用(GitEE)
Git分布式版本控制工具 1. Git概述 1.1 Git历史 Git 诞生于一个极富纷争大举创新的年代.Linux 内核开源项目有着为数众多的参与者. 绝大多数的 Linux 内核维护工作都花在了提 ...
- .net 温故知新【16】:Asp.Net Core WebAPI 筛选器
一.筛选器 通过使用筛选器可在请求处理管道中的特定阶段之前或之后运行代码. 这即是我们经常听到的面向切面编程AOP(Aspect Oriented Programming)技术,AOP通过预编译方式和 ...
- 经典卷积神经网络LeNet&AlexNet&VGG
LeNet LeNet-5是一种经典的卷积神经网络结构,于1998年投入实际使用中.该网络最早应用于手写体字符识别应用中.普遍认为,卷积神经网络的出现开始于LeCun等提出的LeNet网络,可以说Le ...
- 正则表达式之grep与sed用法
一.grep和egrep的用法 (一)grep用法 grep是根据给出的条件查找特定的字符.用单引号查找指定的单词,图1.1.grep后面可选项用**-n显示查找的行数:-i不区分大小写查找图1.2 ...
- Redis 使用的 10 个小技巧
Redis 在当前的技术社区里是非常热门的.从来自 Antirez 一个小小的个人项目到成为内存数据存储行业的标准,Redis已经走过了很长的一段路. 随之而来的一系列最佳实践,使得大多数人可以正确地 ...