当前位置: 首页 > news >正文

整站优化多少钱自己建网站需要钱吗

整站优化多少钱,自己建网站需要钱吗,wordpress视频去广告,西安网站搜索引擎优化简介 模拟SPI驱动是一种软件实现的SPI总线驱动。在没有硬件SPI控制器的系统中,通过软件模拟实现SPI总线的功能。它允许在不修改硬件的情况下,通过GPIO(通用输入/输出)引脚模拟SPI总线的通信,从而与SPI设备进行数据交换…

简介

模拟SPI驱动是一种软件实现的SPI总线驱动。在没有硬件SPI控制器的系统中,通过软件模拟实现SPI总线的功能。它允许在不修改硬件的情况下,通过GPIO(通用输入/输出)引脚模拟SPI总线的通信,从而与SPI设备进行数据交换。

模拟SPI驱动相对于硬件SPI来说,可能会有一定的性能损失,因为软件模拟不如硬件实现的SPI控制器快速和高效。

模拟SPI驱动相比硬件SPI控制器存在一些缺点,包括:

  1. 性能较低:软件模拟SPI需要通过GPIO引脚进行数据的输入和输出,并进行相应的时序控制。相比硬件SPI控制器,软件模拟SPI的速度较慢,通信效率较低,特别是在高速数据传输和频繁通信的场景下。
  2. 占用CPU资源:模拟SPI驱动在内核空间运行,需要通过CPU执行软件代码来模拟SPI总线的功能。这会占用一定的CPU资源,可能导致系统性能下降,并且可能影响其他任务的响应时间。
  3. 时序控制的挑战:软件模拟SPI需要准确控制数据的时序,包括数据的传输速率、时钟边沿和信号延迟等。需要仔细处理时序相关的问题,确保正确的数据传输和可靠性。
  4. 受限于GPIO资源:模拟SPI驱动需要使用系统中的GPIO引脚来模拟SPI总线的通信,因此受限于可用的GPIO资源数量。如果系统中可用的GPIO引脚有限,可能会限制同时连接的SPI设备数量或引起硬件扩展的困难。

内核中模拟SPI驱动的实现

在Linux内核中,SPI子系统提供了用于管理SPI总线和设备的功能和接口。虽然SPI子系统本身不直接提供模拟SPI驱动的功能,但它提供了一些接口和框架,可以用于实现模拟SPI驱动。

  1. SPI GPIO框架:SPI子系统提供了一个名为spi-gpio的框架,可使用GPIO引脚模拟SPI总线,gpio模拟spi代码在drivers/spi/spi-gpio.c中。这个框架允许将GPIO引脚配置为SPI总线的时钟、片选、输入和输出信号,并提供了对应的接口函数供驱动程序使用。
  2. spi-bitbang:spi-bitbang是Linux内核中提供的一个通用框架,用于在没有硬件SPI控制器或需要灵活控制SPI时序和配置的系统中模拟SPI总线的通信。代码在spi-bitbang.c

下面我们分别分析下这drivers/spi/spi-gpio.cspi-bitbang.c两个文件。

spi-gpio

platform_driver

spi_gpio_driver 属于总线设备驱动模型中的一种。当设备树中的compatible字段与spi_gpio_dt_idscompatible匹配时,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;} elsepdata->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;} elsepdata->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;
#endifif (use_of && !SPI_N_CHIPSEL)num_devices = 1;elsenum_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_OFmaster->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;elsefor (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;}}
#endifspi_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;
}

函数所做工作如下:

  1. 首先,函数调用spi_gpio_probe_dt()函数来解析设备树(Device Tree)中的SPI GPIO设备信息,并初始化platform_data结构体。设备树中包含了SPI GPIO设备的属性,如时钟引脚、数据输入引脚、数据输出引脚等。
  2. 接下来,函数获取设备的platform_data结构体,并进行一些合法性检查。
  3. 函数通过调用spi_gpio_request()函数请求和配置SPI GPIO相关的GPIO资源。该函数会申请所需的GPIO引脚,并设置引脚的方向和电平。
  4. 然后,函数使用spi_alloc_master()函数分配一个spi_master结构体,并保存了指向spi_gpio结构体的指针。同时,通过调用platform_set_drvdata()函数将spi_gpio结构体指针保存在platform_device结构体的driver_data字段中。
  5. 接着,函数对spi_master结构体的各个字段进行设置。这些字段包括SPI传输的参数,如数据位宽、传输模式、片选信号数量等。此外,还会设置回调函数,用于数据传输的配置和清理。
  6. 如果设备树支持(即配置了CONFIG_OF),函数会获取设备树中的片选信号的GPIO引脚信息。通过遍历设备树中的cs-gpios属性,获取每个片选信号的GPIO引脚。
  7. 接下来,函数设置spi_gpio结构体中的bitbang字段,将之前设置的spi_master结构体以及自定义的片选信号处理函数指定给它。
  8. 根据SPI的传输模式,函数选择不同的回调函数进行数据的传输。如果SPI设备同时支持数据发送和接收,则使用通用的传输函数;否则,使用特定的传输函数。
  9. 最后,函数调用spi_bitbang_start()函数启动SPI GPIO的位操作传输。该函数会根据之前设置的参数,开始进行SPI数据的传输。
  10. 如果启动失败,函数会释放之前请求的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;
}
  1. 首先,函数接受一个指向struct platform_device类型的指针pdev作为参数。

  2. 接下来定义了两个指针变量spi_gpiopdata,分别指向struct spi_gpiostruct spi_gpio_platform_data类型的数据结构。

  3. platform_get_drvdata(pdev)用于获取存储在平台设备中的私有数据指针,将其赋值给spi_gpio指针。这个私有数据指针通常在设备的probe函数中设置。

  4. dev_get_platdata(&pdev->dev)用于获取与设备相关的平台数据,将其赋值给pdata指针。平台数据是在设备的设备树绑定或者通过platform_set_drvdata()函数设置的。

  5. spi_bitbang_stop(&spi_gpio->bitbang)调用函数spi_bitbang_stop(),停止SPI位操作的传输。这个函数将注销子设备。

  6. 接下来的一系列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引脚。

  1. spi_master_put(spi_gpio->bitbang.master)调用函数spi_master_put(),释放对SPI主设备的引用。

  2. 最后,函数返回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;
}
  1. 首先,函数接受一个指向struct spi_gpio_platform_data类型的指针pdata、一个指向字符常量的指针label和一个指向u16类型的指针res_flags作为参数。
  2. 首先,通过比较SPI_MOSI_GPIOSPI_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主设备没有传输功能。
  3. 接下来,通过比较SPI_MISO_GPIOSPI_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主设备没有接收功能。
  4. 最后,调用spi_gpio_alloc(SPI_SCK_GPIO, label, false)函数分配SCK引脚,并将返回值赋给value变量。如果返回值不为0,则表示分配失败,直接跳转到free_miso标签处。
  5. 如果程序成功执行到这里,表示所有引脚分配都成功,直接跳转到done标签处。
  6. free_miso标签处,如果之前为MISO引脚分配了一个有效的引脚号,调用gpio_free(SPI_MISO_GPIO)函数释放该引脚。
  7. free_mosi标签处,如果之前为MOSI引脚分配了一个有效的引脚号,调用gpio_free(SPI_MOSI_GPIO)函数释放该引脚。
  8. 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);elsevalue = gpio_direction_output(pin, 0);}return value;
}
  1. 首先,函数接受一个无符号整数pin作为GPIO引脚号,一个指向字符常量的指针label作为引脚的标签,以及一个布尔值is_in来指示引脚是否用于输入。
  2. gpio_request(pin, label)调用函数gpio_request()来请求分配指定引脚号的GPIO资源,并将返回值赋给value变量。如果返回值为0,表示成功分配GPIO资源;如果返回值不为0,表示分配失败。
  3. 如果gpio_request()成功执行,进入条件判断语句块:
    • 如果is_in为真,表示该引脚是一个输入引脚,调用gpio_direction_input(pin)函数将该引脚配置为输入模式,并将返回值赋给value变量。如果返回值为0,表示成功配置引脚为输入模式;如果返回值不为0,表示配置失败。
    • 如果is_in为假,表示该引脚是一个输出引脚,调用gpio_direction_output(pin, 0)函数将该引脚配置为输出模式,并将输出电平设置为低电平(0),将返回值赋给value变量。如果返回值为0,表示成功配置引脚为输出模式并设置输出电平;如果返回值不为0,表示配置失败。
  4. 最后,函数返回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;
}
  1. 首先,函数接受一个指向struct spi_device类型的指针spi作为参数。
  2. 代码中定义了一个无符号长整型变量cs,用于存储片选引脚(Chip Select,CS)的值。
  3. 定义了一个整型变量status,用于存储函数执行的状态,默认为0。
  4. 定义了一个指向struct spi_gpio类型的指针spi_gpio,通过spi_to_spi_gpio(spi)宏将spi转换为spi_gpio结构体。
  5. 定义了一个指向struct device_node类型的指针np,用于存储SPI主设备的设备树节点。
  6. 如果np非空(非NULL),表示在设备树(Device Tree)环境中,片选引脚已经从节点的"cs-gpios"属性中进行了初始化。
    • spi_gpio->cs_gpios[spi->chip_select]的值赋给cs,表示获取对应片选引脚的GPIO资源。
  7. 如果np为空,表示不在设备树环境中,从spi->controller_data中获取片选引脚的值。
    • spi->controller_data的值转换为无符号整数并赋给cs,表示获取对应片选引脚的GPIO资源。
  8. 如果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,表示片选引脚成功分配和配置。
  9. 如果status为0,表示片选引脚成功分配和配置。
    • cs的值存储到spi_gpio->cs_gpios[spi->chip_select]中,以便后续使用。
    • 调用spi_bitbang_setup(spi)函数进行SPI位操作相关的设置,将返回值赋给status
  10. 如果status非零,表示片选引脚或SPI位操作设置发生错误。
  • 如果spi->controller_state为空且cs不等于SPI_GPIO_NO_CHIPSELECT,表示之前为片选引脚分配了GPIO资源,现在需要释放它。
    • 调用gpio_free(cs)函数释放片选引脚的GPIO资源。
  1. 最后,函数返回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);}
}
  1. 首先,代码定义了一个静态函数 spi_gpio_chipselect,该函数接受两个参数:spi 是指向 spi_device 结构体的指针,表示要控制的 SPI 设备;is_active 是一个整数,表示片选信号的状态(激活或非激活)。
  2. 接下来,代码通过使用 spi_to_spi_gpio 函数将 spi_device 结构体转换为 spi_gpio 结构体,并将结果保存在 spi_gpio 变量中。这个转换是为了获取与 GPIO 控制相关的信息。
  3. 然后,代码使用 spi_device 结构体中的 chip_select 字段来确定当前片选信号对应的 GPIO 引脚编号,并将其保存在 cs 变量中。
  4. 代码接着根据 is_active 参数来设置初始时钟极性。如果 is_active 为非零值(表示片选信号激活),则通过 spi_device 结构体中的 mode 字段的 SPI_CPOL 位来确定初始时钟极性,然后调用 setsck 函数进行设置。
  5. 接下来,代码检查 cs 变量是否等于 SPI_GPIO_NO_CHIPSELECT。如果 cs 不等于该值,说明有有效的片选信号 GPIO 引脚配置。
  6. 在 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;
}
  1. 函数开始时,从传输结构体 t 中获取位数 bits,数据长度 count,发送缓冲区指针 tx 和接收缓冲区指针 rx
  2. 进入 while 循环,当数据长度 count 大于 3(32 位数据的字长)时,循环继续。
  3. 在每次循环中,首先定义一个变量 word,用于保存当前要发送或接收的数据字。
  4. 如果发送缓冲区存在 (tx 不为空),则将发送缓冲区中的数据字赋值给 word,并将指针 tx 向后移动。
  5. 调用函数指针 txrx_word,将 SPI 设备指针 spi、传输时间 ns、数据字 word 和位数 bits 作为参数传递给该函数。函数 txrx_word 将执行实际的发送和接收操作,并返回接收到的数据字,该数据字将被赋值给 word
  6. 如果接收缓冲区存在 (rx 不为空),则将 word 的值存储到接收缓冲区中,并将指针 rx 向后移动。
  7. 减少数据长度 count 的值,表示已经处理了一个数据字。
  8. 循环回到第 2 步,继续处理下一个数据字,直到剩余数据长度不足以组成完整的 32 位数据。
  9. 返回传输结构体的长度 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;elsereturn -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;
}
  1. 首先,从 SPI 设备结构体中获取位移传输相关的控制器状态结构体 cs
  2. 接着,定义变量 bits_per_wordhz,用于保存位数和传输速率。
  3. 如果传输结构体 t 不为空,将从传输结构体中获取位数和速率,并分别赋值给 bits_per_wordhz。如果 t 为空,则将 bits_per_wordhz 设置为 0。
  4. 如果位数 bits_per_word 为零,将从 SPI 设备结构体中获取默认的位数 spi->bits_per_word
  5. 根据位数 bits_per_word 的大小,决定使用不同的位移传输函数。
  6. 如果传输速率 hz 为零,将从 SPI 设备结构体中获取最大速率 spi->max_speed_hz
  7. 如果速率 hz 不为零,计算每个位移传输的时间间隔 nsecs。这里假设时钟周期为传输速率的倒数的一半(即半周期)。计算公式为 nsecs = (clock period)/2 = (1000000000/2) / hz
  8. 如果计算得到的时间间隔 nsecs 超过最大延迟时间,则返回错误码 -EINVAL,表示时间间隔过大。
  9. 如果都设置成功,返回 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;
}
  1. 首先,从 SPI 设备结构体中获取控制器状态结构体 cs 和位移传输相关的控制结构体 bitbang
  2. 使用函数 spi_master_get_devdata 从 SPI 主设备结构体中获取位移传输控制结构体 bitbang
  3. 如果控制器状态结构体 cs 为空,说明还没有为该 SPI 设备分配控制器状态结构体,此时需要为其分配内存并初始化。使用函数 kzalloc 分配内存,并将分配的内存赋值给 cs。如果内存分配失败,则返回错误码 -ENOMEM
  4. 将控制器状态结构体 cs 赋值给 SPI 设备结构体中的 controller_state 字段。
  5. 从位移传输控制结构体 bitbang 中根据 SPI 设备的模式(spi->mode)获取相应的位移传输函数 txrx_word。根据 SPI 设备的模式中的 SPI_CPOL(时钟极性)和 SPI_CPHA(时钟相位)位进行位运算,获取相应的位移传输函数。如果获取的函数为空,则返回错误码 -EINVAL
  6. 如果位移传输控制结构体 bitbang 中的 setup_transfer 函数存在,则调用该函数设置传输参数。传输参数通过传递 SPI 设备指针 spi 和空指针 NULL 来实现。如果 setup_transfer 函数返回的值小于 0,则表示设置传输参数失败,此时返回该错误码。
  7. 在进行实际的数据传输之前,需要先选择片选信号,并确保片选信号处于非活动状态。
  8. 通过互斥锁 bitbang->lock 来保护对位移传输控制结构体的访问。首先获取互斥锁。
  9. 检查位移传输控制结构体 bitbang 中的 busy 标志。如果该标志为假,则表示当前没有其他数据传输操作正在进行,此时调用 chipselect 函数将片选信号设置为非活动状态,并通过 ndelay 函数延迟一段时间,以确保片选信号稳定。
  10. 最后,释放互斥锁 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;
}
  1. 首先,从 SPI 主设备结构体中获取位移传输相关的控制结构体 bitbang
  2. 定义变量 status 来保存传输操作的状态,默认为 0。
  3. 如果位移传输控制结构体 bitbang 中的 setup_transfer 函数存在,则调用该函数设置传输参数。传输参数通过传递 SPI 设备指针 spi 和传输结构体指针 transfer 来实现。如果设置传输参数的函数返回的值小于 0,则表示设置传输参数失败,此时跳转到标签 out 处进行处理。
  4. 检查传输结构体 transfer 的数据长度 len 是否非零。如果非零,则调用位移传输控制结构体 bitbang 中的 txrx_bufs 函数进行数据的发送和接收。
  5. 判断传输操作的状态 status 是否等于传输结构体 transfer 的长度 len。如果相等,则表示传输操作成功完成,将状态 status 设置为 0。如果状态 status 大于等于 0 但不等于传输结构体 transfer 的长度 len,则表示传输操作未完成,将状态 status 设置为 -EREMOTEIO
  6. 跳转到标签 out 处,执行 spi_finalize_current_transfer 函数,以完成当前传输的收尾工作。
  7. 返回传输操作的状态 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);
}
  1. 首先,从 SPI 设备结构体中获取控制器状态结构体 cs
  2. 从控制器状态结构体 cs 中获取位移传输所需的时间间隔 nsecs
  3. 调用控制器状态结构体 cs 中的 txrx_bufs 函数来执行数据缓冲区的传输。该函数接受 SPI 设备指针 spi、位移传输函数 txrx_word、时间间隔 nsecs 和传输结构体指针 t 作为参数,并返回传输操作的状态。
  4. 将传输操作的状态作为函数的返回值。

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;
}
  1. 首先,从 SPI 主设备结构体中获取位移传输相关的控制结构体 bitbang
  2. 使用互斥锁 bitbang->lock 来保护对位移传输控制结构体的访问。首先获取互斥锁。
  3. 将位移传输控制结构体中的 busy 标志设置为 1,表示硬件正在忙于数据传输操作。
  4. 释放互斥锁 bitbang->lock,以允许其他线程访问位移传输控制结构体。
  5. 返回 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;
}
  1. 首先,从 SPI 主设备结构体中获取位移传输相关的控制结构体 bitbang
  2. 使用互斥锁 bitbang->lock 来保护对位移传输控制结构体的访问。首先获取互斥锁。
  3. 将位移传输控制结构体中的 busy 标志设置为 0,表示硬件不再忙于数据传输操作。
  4. 释放互斥锁 bitbang->lock,以允许其他线程访问位移传输控制结构体。
  5. 返回 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);
}
  1. 首先,从 SPI 设备结构体的主设备结构体中获取位移传输相关的控制结构体 bitbang
  2. 通过检查 SPI 设备的传输模式中的 SPI_CS_HIGH 标志位,以及函数输入参数 enable 的值,来确定是否需要使能片选信号。这是为了确保兼容片选信号的极性。
  3. 使用 ndelay 函数引入延迟,以确保在改变片选信号状态之前有足够的时间。
  4. 调用位移传输控制结构体 bitbang 中的 chipselect 函数,以控制片选信号的状态。根据 enable 的值,将片选信号设置为活动状态或非活动状态。
  5. 再次使用 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;
}
  1. 首先,从位移传输控制结构体中获取 SPI 主设备结构体 master
  2. 检查主设备结构体和位移传输控制结构体中的片选信号控制函数是否存在,如果不存在则返回错误码 -EINVAL。
  3. 初始化互斥锁 bitbang->lock,用于保护对位移传输控制结构体的访问。
  4. 如果主设备结构体中的传输模式位字段 mode_bits 未设置,则设置为默认的模式位,包括 SPI_CPOL、SPI_CPHA 和位移传输控制结构体中的标志位。
  5. 检查主设备结构体中的传输函数是否已经定义,如果已定义则返回错误码 -EINVAL。
  6. 设置主设备结构体中的准备硬件传输函数、释放硬件传输函数、单次传输函数和片选信号控制函数,分别对应位移传输控制结构体中的对应函数。
  7. 如果位移传输控制结构体中的数据缓冲区传输函数未定义,则设置使用 DMA 标志为 0,并将数据缓冲区传输函数设置为默认的位移传输函数 spi_bitbang_bufs。如果主设备结构体中的设置函数未定义,则设置使用默认的设置函数 spi_bitbang_setup 和清理函数 spi_bitbang_cleanup
  8. 注册 SPI 主设备,将其添加到系统中。如果注册失败,则释放主设备结构体并返回错误码。
  9. 返回注册结果,成功时返回 0。

spi_bitbang_stop

spi_bitbang_stop作用是停止 SPI 位移传输。它通过取消注册 SPI 主设备来停止相关的传输操作。这可以用于在不需要进行 SPI 位移传输时,将相关资源释放并从系统中移除相应的设备。

void spi_bitbang_stop(struct spi_bitbang *bitbang)
{spi_unregister_master(bitbang->master);
}
  1. 从位移传输控制结构体中获取 SPI 主设备结构体 master
  2. 调用 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


文章转载自:
http://dinncoxxv.stkw.cn
http://dinncointernment.stkw.cn
http://dinncobastard.stkw.cn
http://dinncomysid.stkw.cn
http://dinncostuffless.stkw.cn
http://dinncomarketing.stkw.cn
http://dinncodissertator.stkw.cn
http://dinncogranulometric.stkw.cn
http://dinncosclerotium.stkw.cn
http://dinncodewret.stkw.cn
http://dinncotramontane.stkw.cn
http://dinncoaflutter.stkw.cn
http://dinncomillenarianism.stkw.cn
http://dinncoshmaltz.stkw.cn
http://dinncorommany.stkw.cn
http://dinncoquantitative.stkw.cn
http://dinncoacrolith.stkw.cn
http://dinncopiggy.stkw.cn
http://dinncoinstanton.stkw.cn
http://dinncodisposed.stkw.cn
http://dinncotern.stkw.cn
http://dinncosubscript.stkw.cn
http://dinncobly.stkw.cn
http://dinncold.stkw.cn
http://dinncomultifold.stkw.cn
http://dinncohybrimycin.stkw.cn
http://dinnconacred.stkw.cn
http://dinncouniocular.stkw.cn
http://dinncocephalin.stkw.cn
http://dinncoglobate.stkw.cn
http://dinncoagapanthus.stkw.cn
http://dinncopleiocene.stkw.cn
http://dinncochateaubriand.stkw.cn
http://dinncoastarboard.stkw.cn
http://dinncothowless.stkw.cn
http://dinncogyration.stkw.cn
http://dinncomotorboat.stkw.cn
http://dinncomicrominiature.stkw.cn
http://dinncoflyboat.stkw.cn
http://dinncowalach.stkw.cn
http://dinncogenitive.stkw.cn
http://dinncobooty.stkw.cn
http://dinncocheckweighman.stkw.cn
http://dinncochummy.stkw.cn
http://dinncovagotropic.stkw.cn
http://dinncocommensal.stkw.cn
http://dinncobraggart.stkw.cn
http://dinncobored.stkw.cn
http://dinncohighboy.stkw.cn
http://dinncokrans.stkw.cn
http://dinncopleura.stkw.cn
http://dinncowaiter.stkw.cn
http://dinncophytoclimatology.stkw.cn
http://dinncogumbah.stkw.cn
http://dinncournflower.stkw.cn
http://dinncoiodate.stkw.cn
http://dinncohonorable.stkw.cn
http://dinncosubtenant.stkw.cn
http://dinncosemiquantitative.stkw.cn
http://dinncocamwood.stkw.cn
http://dinncopalladious.stkw.cn
http://dinncohitchhiker.stkw.cn
http://dinncotelegu.stkw.cn
http://dinncocoromandel.stkw.cn
http://dinncoachlorophyllous.stkw.cn
http://dinncohoplite.stkw.cn
http://dinncotetramethyl.stkw.cn
http://dinncochace.stkw.cn
http://dinncowagonlit.stkw.cn
http://dinncoanticly.stkw.cn
http://dinncograding.stkw.cn
http://dinncometaphen.stkw.cn
http://dinncoheterophyte.stkw.cn
http://dinncoperiodical.stkw.cn
http://dinncoconferree.stkw.cn
http://dinncocornerways.stkw.cn
http://dinncoinexplicably.stkw.cn
http://dinncopinwheel.stkw.cn
http://dinncorecollectedly.stkw.cn
http://dinncoparboil.stkw.cn
http://dinncoukraine.stkw.cn
http://dinncoionisation.stkw.cn
http://dinncoprolong.stkw.cn
http://dinncotellurise.stkw.cn
http://dinncohenry.stkw.cn
http://dinncobotb.stkw.cn
http://dinncoultimacy.stkw.cn
http://dinncoaerobacter.stkw.cn
http://dinncoacentric.stkw.cn
http://dinncooperculiform.stkw.cn
http://dinncophlebosclerosis.stkw.cn
http://dinncogaltonian.stkw.cn
http://dinncogranodiorite.stkw.cn
http://dinncofacing.stkw.cn
http://dinncoyi.stkw.cn
http://dinncoknown.stkw.cn
http://dinncounprosperous.stkw.cn
http://dinncoboutique.stkw.cn
http://dinncodefeasance.stkw.cn
http://dinncocartload.stkw.cn
http://www.dinnco.com/news/114926.html

相关文章:

  • 彭阳门户网站建设安年软文网
  • 网站怎么做微信支付功能百度网络小说排行榜
  • 自做的网站如何发布新冠不易感染三种人
  • 怎么做pc端移动网站seo公司上海牛巨微
  • 沭阳做网站留号码的广告网站不需要验证码
  • 2345网址大全设主页广告seo职位描述
  • 网站开发连接数据库的方法seo公司运营
  • 国内免费云主机seo推广软件费用
  • 小学教学活动设计方案模板成都优化官网公司
  • 自己用自己电脑做网站空间专业公司网络推广
  • 门户网站做公众号的好处网络营销的方式都有哪些
  • 效果型网站建设seo1新地址在哪里
  • 小说网站静态页面模板站长平台工具
  • 蓝色系 网站近期重大新闻事件
  • 做整合营销的网站网站推广和优化的原因网络营销
  • 武汉市做网站的公司有产品怎么找销售渠道
  • 做网站建设的网络公司经营范围怎样填seo技术外包 乐云践新专家
  • 做网站后台的电子文库网店运营推广
  • 如何做代购网站设计郑州推广优化公司
  • 商城网站怎么建广州推广seo
  • 做美术鉴赏网站的心得离我最近的电脑培训中心
  • 外贸网站营销建站成品视频直播软件推荐哪个好一点
  • e龙岩网站深圳最新消息
  • 广州市网站建设科技百度快照是啥
  • 网站为什么做等保企业网站建设报价表
  • 游戏推广网站怎么做武汉seo招聘信息
  • 南宁品牌网站建设公司优化模型有哪些
  • 做网站程序的都不关注seo微信公众平台开发
  • 个人网站是怎么样的长沙网站关键词排名
  • 免费自己怎么注册网站交换链接营销的经典案例