正点原子驱动开发BUG(一)--SPI无法正常通信

目录

    • 一、问题描述
    • 二、讲该问题的解决方案
    • 三、imx6ull的spi适配器驱动程序控制片选分析
        • 3.1 设备icm20608的驱动程序分析
        • 3.2 imx的spi适配器的驱动程序分析
    • 四、BUG修复测试
    • 五、其他问题

一、问题描述

使用正点的im6ull开发板进行spi通信驱动开发实验的时候,主机无法与从机进行正常通信。就算使用正点的例程,也无法正常通信。读不到从机寄存器中的值。以读取从机ID为例,例子为正点原子的例程基础上添加了几行printk用来打印信息:

void icm20608_reginit(void)
{
	u8 value = 1;
	u8 test = 1;
	
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x80);		/* 复位 */
	mdelay(50);
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x01);		/* 关闭睡眠 */
	mdelay(50);

	printk("ICM20608 ID = %#X\r\n", value);	
	value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);	 /* 这里读出来的ID不对 */
	printk("ICM20608 ID = %#X\r\n", value);	
	...
}
...
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{
	u8 data = 0;
	icm20608_read_regs(dev, reg, &data, 1);
	return data;
}
...
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{

	int ret = -1;
	unsigned char txdata[1];
	unsigned char * rxdata;
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)dev->private_data;
    
	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
	if(!t) {
		return -ENOMEM;
	}

	rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL);	/* 申请内存 */
	if(!rxdata) {
		goto out1;
	}

	/* 一共发送len+1个字节的数据,第一个字节为
	寄存器首地址,一共要读取len个字节长度的数据,*/
	txdata[0] = reg | 0x80;		/* 写数据的时候首寄存器地址bit8要置1 */			
	t->tx_buf = txdata;			/* 要发送的数据 */
    t->rx_buf = rxdata;			/* 要读取的数据 */
	t->len = len+1;				/* t->len=发送的长度+读取的长度 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */
	if(ret) {
		goto out2;
	}
	
    memcpy(buf , rxdata+1, len);  /* 只需要读取的数据 */

out2:
	kfree(rxdata);					/* 释放内存 */
out1:	
	kfree(t);						/* 释放内存 */
	
	return ret;
}

读ID失败,读出来ID是0,单纯是因为调用icm20608_read_onereg函数时会把返回值初始化为0,也就是根本没读到寄存器中内容

在这里插入图片描述

二、讲该问题的解决方案

首先直接说明问题所在:如果使用正点的spi驱动开发例程则设备树中spi适配器设备节点下的cs-gpios属性不能写成cs-gpio。当使用的属性名位cs-gpios的时候是由该spi适配器匹配的驱动程序来控制片选。

其实正点原子也在开发指南中说了:

2 行,设置当前片选数量为 1,因为就只接了一个 ICM20608。
第 3 行,一定要使用 “cs-gpios”属性来描述片选引脚,SPI 主机驱动就会控制片选引脚。
第 5 行,设置 IO 要使用的 pinctrl 子节点,也就是我们在示例代码 62.5.1.1 中新建的
pinctrl_ecspi3。

正点提供的linux源码中的documentation中也提到怎么编写对应设备树了,该文件位于linux源码位置/Documentation/devicetree/bindings/spi中:

* Freescale (Enhanced) Configurable Serial Peripheral Interface
  (CSPI/eCSPI) for i.MX

Required properties:
- compatible :
  - "fsl,imx1-cspi" for SPI compatible with the one integrated on i.MX1
  - "fsl,imx21-cspi" for SPI compatible with the one integrated on i.MX21
  - "fsl,imx27-cspi" for SPI compatible with the one integrated on i.MX27
  - "fsl,imx31-cspi" for SPI compatible with the one integrated on i.MX31
  - "fsl,imx35-cspi" for SPI compatible with the one integrated on i.MX35
  - "fsl,imx51-ecspi" for SPI compatible with the one integrated on i.MX51
- reg : Offset and length of the register set for the device
- interrupts : Should contain CSPI/eCSPI interrupt
- fsl,spi-num-chipselects : Contains the number of the chipselect
- cs-gpios : Specifies the gpio pins to be used for chipselects.
- clocks : Clock specifiers for both ipg and per clocks.
- clock-names : Clock names should include both "ipg" and "per"
See the clock consumer binding,
	Documentation/devicetree/bindings/clock/clock-bindings.txt
- dmas: DMA specifiers for tx and rx dma. See the DMA client binding,
		Documentation/devicetree/bindings/dma/dma.txt
- dma-names: DMA request names should include "tx" and "rx" if present.

Example:

ecspi@70010000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx51-ecspi";
	reg = <0x70010000 0x4000>;
	interrupts = <36>;
	fsl,spi-num-chipselects = <2>;
	cs-gpios = <&gpio3 24 0>, /* GPIO3_24 */
		   <&gpio3 25 0>; /* GPIO3_25 */
	dmas = <&sdma 3 7 1>, <&sdma 4 7 2>;
	dma-names = "rx", "tx";
};

但是我太信任正点提供的linux源码了,直接用了正点提供的出厂linux源码,这个出厂linux源码中的READ_ME解释如下:

说明:
1、使用此uboot和linux源码可以编译得到正点原子I.MX6U出货时的uboot和Linux固件。兼容正点原子所有RGB屏,板子上所有功能也相应调试好。直接按【正点原子】I.MX6U用户快速体验Vx.x.pdf文档编译出来使用!
2、驱动指南也提及过编译这里的源码,只是教学编译体验。

问题答疑:
1、看到1、例程源码\10、开发板教程对应的uboot和linux源码这里还有一份正点原子的uboot和linux源码,为什么会有两份源码?
答:1、例程源码\10、开发板教程对应的uboot和linux源码是正点原子驱动指南做驱动实验、移植uboot和linux所用的源码。而出厂源码则是用于出货所使用,客户无需再调试。直接编译使用!

2、为什么要和出厂源码分开?
答:由于多种原因,出厂使用的源码不能与驱动指南所使用的源码同时进行。出厂源码会随时修复bug或者添加新的驱动以兼容正点原子的其他模块。

3、那我可不可以这么理解:出厂源码是给客户直接用在产品上使用,而教程源码则是用于初学者用于学习驱动和移植uboot和Linux上使用?
答:恩,可以这么理解。因为出厂源码是几乎无需再调试了,适用正点原子的ALPHA和Mini开发板,给一些快速上手的客户使用!而教程源码则是用于学习或者自己开发使用!

他说的是出厂源码是几乎无需再调试了,适用于正点原子的ALPHA开发板可以直接上手用,我就直接用了。但其实他的设备树是这么写的:

&ecspi3 {
        fsl,spi-num-chipselects = <1>;
        cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>;		/* 必须使用cs-gpio属性来描述片选引脚(如果你想让spi适配器驱动程序来设置片选的话) */
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_ecspi3>;	/* 难不成是因为这里是pinctrl-0,这个0正好对应这第0通道,即icm20608 */
        status = "okay";

	spidev: icm20608@0 {
	compatible = "alientek,icm20608";
        spi-max-frequency = <8000000>;
        reg = <0>;
    };
};

打眼一看内容跟正点的驱动指南中提到的都一样,所以我就觉得没啥问题,但是实际上可以看到,有一个属性名写成了cs-gpio而不是cs-gpios,这是与正点的spi驱动开发例程不匹配的。为什么不匹配?这得分析分析适配器的驱动程序

三、imx6ull的spi适配器驱动程序控制片选分析

3.1 设备icm20608的驱动程序分析

首先说明问题:问题出现在厂商编写的spi适配器驱动程序上。但在此之前,我们要先来看看我们的设备驱动,也就是正点使用的设备icm20608的驱动程序中的一个函数:spi_setup()

static int icm20608_probe(struct spi_device *spi)
{
	...
	/*初始化spi_device */
	spi->mode = SPI_MODE_0;	/*MODE0,CPOL=0,CPHA=0*/
	spi_setup(spi);
	icm20608dev.private_data = spi; /* 设置私有数据 */

	/* 初始化ICM20608内部寄存器 */
	icm20608_reginit();		
	return 0;
}

该函数传入一个struct spi_device类型的变量。正点驱动对这个函数一带而过,只是说这个函数必须得有。其实就是这个函数中出了问题。该函数位于drivers/spi/spi.c中,我只列出源码中与错误相关的部分:

int spi_setup(struct spi_device *spi)
{
	unsigned	bad_bits, ugly_bits;
	int		status = 0;
	...
	spi_set_cs(spi, false);

	if (spi->master->setup)
		status = spi->master->setup(spi);
	...
	return status;
}
...
...
static void spi_set_cs(struct spi_device *spi, bool enable)
{
	if (spi->mode & SPI_CS_HIGH)
		enable = !enable;

	if (spi->cs_gpio >= 0)
		gpio_set_value(spi->cs_gpio, !enable);
	else if (spi->master->set_cs)
		spi->master->set_cs(spi, !enable);
}

spi_setup调用的spi_set_cs函数中可以看到,如果spi->cs_gpio >= 0,函数调用gpio_set_value(spi->cs_gpio, !enable)来设置某个goio口的电平,比如在正点的教程中如果要选中icm20608外设,那么就要设置gpio1_20这个gpio口的电平,所以应该出现的情况是spi->cs_gpio = 20。如果没满足第一个if,就会判断第二个else if,这个master->set_cs根本没设置,所以也用不到。

那么spi->cs_gpio是在哪里设置的呢?在spi.c文件中有这么一个函数spi_add_device(),截取出相关的内容:

int spi_add_device(struct spi_device *spi)
{
	static DEFINE_MUTEX(spi_add_lock);
	struct spi_master *master = spi->master;
	struct device *dev = master->dev.parent;
	int status;

	/* Chipselects are numbered 0..max; validate. */
	if (spi->chip_select >= master->num_chipselect) {
		dev_err(dev, "cs%d >= max %d\n",
			spi->chip_select,
			master->num_chipselect);
		return -EINVAL;
	}
	...
	if (master->cs_gpios)
		spi->cs_gpio = master->cs_gpios[spi->chip_select];
	...
}

可以看到在最后一个if中,如果master->cs_gpios不为0,那么就会设置spi->cs_gpio = master->cs_gpios[spi->chip_select]。这个spi->chip_select相当于master->cs_gpios这个数组的索引(master->cs_gpioscs_gpios的是struct spi_master结构体下的一个成员变量,为一个int类型的指针,其实就是一个数组),这个会在下一部分提到。

spi->chip_select又是在哪设置的呢?看一下spi.c文件中的of_register_spi_device()函数:

#if defined(CONFIG_OF)
static struct spi_device *
of_register_spi_device(struct spi_master *master, struct device_node *nc)
{
	struct spi_device *spi;
	int rc;
	u32 value;

	/* Alloc an spi_device */
	spi = spi_alloc_device(master);
	if (!spi) {
		dev_err(&master->dev, "spi_device alloc error for %s\n",
			nc->full_name);
		rc = -ENOMEM;
		goto err_out;
	}

	/* Select device driver */
	rc = of_modalias_node(nc, spi->modalias,
				sizeof(spi->modalias));
	if (rc < 0) {
		dev_err(&master->dev, "cannot find modalias for %s\n",
			nc->full_name);
		goto err_out;
	}

	/* Device address */
	rc = of_property_read_u32(nc, "reg", &value);
	if (rc) {
		dev_err(&master->dev, "%s has no valid 'reg' property (%d)\n",
			nc->full_name, rc);
		goto err_out;
	}
	spi->chip_select = value;
	...
	/* Register the new device */
	rc = spi_add_device(spi);
	if (rc) {
		dev_err(&master->dev, "spi_device register error %s\n",
			nc->full_name);
		goto err_out;
	}

	return spi;
}

该函数会调用of_property_read_u32()函数解析设备树中的spi设备的reg属性值,并赋值给变量value而在设备树中这个值中填入的正是片选值,然后设置spi->chip_select = value,并调用刚刚提到的spi_add_device()函数。该函数会在of_register_spi_devices()函数中调用:

static void of_register_spi_devices(struct spi_master *master)
{
	struct spi_device *spi;
	struct device_node *nc;

	if (!master->dev.of_node)
		return;

	for_each_available_child_of_node(master->dev.of_node, nc) {
		spi = of_register_spi_device(master, nc);
		if (IS_ERR(spi))
			dev_warn(&master->dev, "Failed to create SPI device for %s\n",
				nc->full_name);
	}
}

就相当于可能会注册很多个spi设备,所以用个for循环来一个一个注册。这个注册很多个spi设备的函数会在spi_register_master()函数中被调用,而这个函数是跟spi适配器有关。

可以看到,最终的最终,spi->cs_gpio其实是跟master->cs_gpios有关系,并且上述函数的调用的源头也是spi_register_master()函数。所以必须得去看spi适配器的驱动函数了。但在此之前先来看一下当设备树中写的属性为cs-gpio时出现的情况,我们在驱动程序中编写如下测试代码:

static int icm20608_probe(struct spi_device *spi)
{
	...
	/*初始化spi_device */
	spi->mode = SPI_MODE_0;	/*MODE0,CPOL=0,CPHA=0*/
	spi_setup(spi);
	printk("chip select:%d\r\n", spi->chip_select);
	printk("cs gpio:%d\r\n", spi->cs_gpio);
	icm20608dev.private_data = spi; /* 设置私有数据 */
	...
	return 0;
}

注册编译出来的ko模块的时候结果如下:

在这里插入图片描述
可以看到,spi->chip_select正常获取到了设备树中spi设备reg属性的值,但是spi->cs_gpio却没有获取到我们想要获取的gpio号20吗,而是-2。至于为什么,需要看下一部分。

3.2 imx的spi适配器的驱动程序分析

在第二部分中列出的设备树代码中只是列出了引用ecspi3时的一些修改,该设备真正定义在imx6ull.dtsi下:

ecspi3: ecspi@02010000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
	reg = <0x02010000 0x4000>;
	interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_ECSPI3>,
			<&clks IMX6UL_CLK_ECSPI3>;
	clock-names = "ipg", "per";
	dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
	dma-names = "rx", "tx";
	status = "disabled";
};

其中的属性compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";,根据这个去找对应的驱动程序,找到的驱动程序为drivers/spi/spi-imx.c,找到其.probe函数,并列出其与问题相关的部分:

static int spi_imx_probe(struct platform_device *pdev)
{
	struct device_node *np = pdev->dev.of_node;
	const struct of_device_id *of_id =
			of_match_device(spi_imx_dt_ids, &pdev->dev);
	struct spi_imx_master *mxc_platform_info =
			dev_get_platdata(&pdev->dev);
	struct spi_master *master;
	struct spi_imx_data *spi_imx;
	struct resource *res;
	int i, ret, num_cs, irq;

	if (!np && !mxc_platform_info) {
		dev_err(&pdev->dev, "can't get the platform data\n");
		return -EINVAL;
	}

	ret = of_property_read_u32(np, "fsl,spi-num-chipselects", &num_cs);	/* 获取到num_cs为1 */
	if (ret < 0) {
		if (mxc_platform_info)
			num_cs = mxc_platform_info->num_chipselect;
		else
			return ret;
	}

	master = spi_alloc_master(&pdev->dev,
			sizeof(struct spi_imx_data) + sizeof(int) * num_cs);	/* 这个函数会把num_chipselect初始化为1 */
	if (!master)
		return -ENOMEM;

	...
	spi_imx->bitbang.master = master;

	for (i = 0; i < master->num_chipselect; i++) {
		int cs_gpio = of_get_named_gpio(np, "cs-gpios", i);
		if (!gpio_is_valid(cs_gpio) && mxc_platform_info)	/* gpio_is_valid: asm-generic/gpio.h return 1 or 0*/
			cs_gpio = mxc_platform_info->chipselect[i];

		spi_imx->chipselect[i] = cs_gpio;
		if (!gpio_is_valid(cs_gpio))
			continue;

		ret = devm_gpio_request(&pdev->dev, spi_imx->chipselect[i],
					DRIVER_NAME);
		if (ret) {
			dev_err(&pdev->dev, "can't get cs gpios\n");
			goto out_master_put;
		}
	}

	spi_imx->bitbang.chipselect = spi_imx_chipselect;
	spi_imx->bitbang.setup_transfer = spi_imx_setupxfer;
	spi_imx->bitbang.txrx_bufs = spi_imx_transfer;
	spi_imx->bitbang.master->setup = spi_imx_setup;
	spi_imx->bitbang.master->cleanup = spi_imx_cleanup;
	spi_imx->bitbang.master->prepare_message = spi_imx_prepare_message;
	spi_imx->bitbang.master->unprepare_message = spi_imx_unprepare_message;
	spi_imx->bitbang.master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;
	...
	master->dev.of_node = pdev->dev.of_node;
	ret = spi_bitbang_start(&spi_imx->bitbang);
	...
	return ret;
}

并会在spi_bitbang_start函数中调用了spi_register_master()函数,也就是上一部分提到的很多函数的源头,向内核注册spi_master设备,该函数位于drivers/spi/spi-bitbang.c中,列出相关部分:

int spi_bitbang_start(struct spi_bitbang *bitbang)
{
	struct spi_master *master = bitbang->master;
	int ret;

	if (!master || !bitbang->chipselect)
		return -EINVAL;

	spin_lock_init(&bitbang->lock);
	...
	ret = spi_register_master(spi_master_get(master));
	if (ret)
		spi_master_put(master);

	return 0;
}

接下来我们就可以看一下spi_register_master()这个函数了。上一部分讲的函数主要作用是帮助注册spi设备,而这个函数的作用是向内核注册spi适配器设备,该函数同样位于spi.c中,只列出与我们的bug相关的代码:

int spi_register_master(struct spi_master *master)
{
	static atomic_t		dyn_bus_id = ATOMIC_INIT((1<<15) - 1);
	struct device		*dev = master->dev.parent;
	struct boardinfo	*bi;
	int			status = -ENODEV;
	int			dynamic = 0;

	if (!dev)
		return -ENODEV;

	status = of_spi_register_master(master);
	...
	/* Register devices from the device tree and ACPI */
	of_register_spi_devices(master);		/* 这里会调用of_register_spi_device */
	acpi_register_spi_devices(master);
done:
	return status;
}

关键函数就是of_spi_register_master()这个函数,该函数同样定义在spi.c中,其源码如下:

#ifdef CONFIG_OF
static int of_spi_register_master(struct spi_master *master)
{
	int nb, i, *cs;
	struct device_node *np = master->dev.of_node;
	printk("in of_spi_register_master?\r\n");  /* 我自己加的test,判断一下CONFIG_OF这个宏定义是否开启了 */
	if (!np)
		return 0;

	nb = of_gpio_named_count(np, "cs-gpios");
	master->num_chipselect = max_t(int, nb, master->num_chipselect);

	/* Return error only for an incorrectly formed cs-gpios property */
	if (nb == 0 || nb == -ENOENT)
		return 0;
	else if (nb < 0)
		return nb;

	cs = devm_kzalloc(&master->dev,
			  sizeof(int) * master->num_chipselect,
			  GFP_KERNEL);
	master->cs_gpios = cs;

	if (!master->cs_gpios)
		return -ENOMEM;

	for (i = 0; i < master->num_chipselect; i++)
		cs[i] = -ENOENT;

	for (i = 0; i < nb; i++)
		cs[i] = of_get_named_gpio(np, "cs-gpios", i);

	return 0;
}

从这个函数中可以看出,该函数首先调用of_gpio_named_count来统计cs-gpios这个属性中设置的gpio的个数。很不幸,我们在设备树中把属性写成了cs-gpio,所以该函数找不到节点,就会返回负值。然后该函数将master->num_chipselect设定为了master->num_chipselectnb这两个变量中的最大值,master->num_chipselect这个变量会在.probe函数中通过读取设备树中spi适配器设备节点中的"fsl,spi-num-chipselects"属性来初始化。根据设备树来看,该属性的值被设为了1。

前文提到,cs_gpios是一个int类型的指针,所以在该函数中将他指向了一段使用devm_kzalloc()函数分配的内存,可以理解为现在cs_gpios就是一个数组了。

然后该函数调用两个for循环,来对master->cs_gpios这个数组进行初始化,因为master->num_chipselect为1,所以第一个for循环正常执行,csp[i] = -ENOENT而这个宏定义ENOENT恰恰就是2。并且第二个for不会执行,因为他根本找不到"cs-gpios"这个属性(我们写的设备树烧了一个s)。

所以综上,master->cs_gpios全被初始化为了-2,导致第一部分提到的spi设备结构体中的变量(即spi->cs_gpio)也为-2,从而导致在调用gpio_set_value()函数来对spi->cs_gpio这个gpio号的时候没有正确设置。

四、BUG修复测试

前面已经说明了,把设备树中的属性改为"cs-gpios"应该就行了:

&ecspi3 {
        fsl,spi-num-chipselects = <1>;
        cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;		/* 必须使用cs-gpio属性来描述片选引脚,可能spi适配器驱动里用的就是这个名称"cs-gpio" */
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_ecspi3>;	/* 难不成是因为这里是pinctrl-0,这个0正好对应这第0通道,即icm20608 */
        status = "okay";

	spidev: icm20608@0 {
	compatible = "alientek,icm20608";
        spi-max-frequency = <8000000>;
        reg = <0>;
    };
};

然后重新编译设备树,测试结果如下:
在这里插入图片描述
gpio口获取也对了,也能正确读到icm20608的设备id了。

五、其他问题

其实有个问题不知道有没有注意到,就是在分析驱动程序的时候出现了一个#ifdef CONFIG_OF,上述很多函数都依赖于这个宏必须得被定义了才能起作用。这个宏我根本没找到在哪里定义的,如果没定义这个宏,那之前的分析就白费了,所以找了一个依赖于这个宏的函数进行测试:

#ifdef CONFIG_OF
static int of_spi_register_master(struct spi_master *master)
{
	int nb, i, *cs;
	struct device_node *np = master->dev.of_node;
	printk("in of_spi_register_master?\r\n");  /* 我自己加的test,判断一下CONFIG_OF这个宏定义是否开启了 */
	...
	return 0;
}

然后重新编译内核,再次启动内核,启动过程中打印结果如下:

在这里插入图片描述
看来这个宏确实是定义了,但是定义在哪个位置我真没找到,有知道的老哥可以教一下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/253822.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Redis Set类型

集合类型也是保存多个字符串类型的元素的&#xff0c;但和列表类型不同的是&#xff0c;集合中 1&#xff09;元素之间是无序的 2&#xff09;元素不允许重复 一个集合中最多可以存储2的32次方个元素。Redis 除了支持集合内的增删查改操作&#xff0c;同时还支持多个集合取交…

【ZYNQ学习】PL第一课

这节课讲什么&#xff1f; 这节课的名字本来是想写为LED&#xff0c;但这一课里除了LED也有按键&#xff0c;又想换为GPIO控制&#xff0c;但关于PL的GPIO控制&#xff0c;不应该这么草率和简单&#xff0c;而且这一课有很多和ZYNQ或者PL关联性不强的东西要说。 所以我写了删删…

基于AT89C52单片机的计算器设计与仿真

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/88637995?spm1001.2014.3001.5503 源码获取 B 源码仿真图课程设计51 摘 要 计算器一般是指“电子计算器”,能进行数学运算的手持机器&#xff0c;拥有集成电路芯…

UE4 去除重复纹理

如果直接连的话&#xff0c;效果如下&#xff1a; 就存在很多重复的纹理&#xff0c;如何解决这个问题呢&#xff1f; 将同一个纹理&#xff0c;用不同的Tilling&#xff0c;将Noise进行Lerp两者之间&#xff0c;为什么要这么做呢&#xff1f;因为用一个做清晰纹理&#xff0c;…

适用于 Windows 和 Mac 的 10 款最佳照片恢复软件(免费和付费)

丢失照片很容易。这里点击错误&#xff0c;那里贴错标签的 SD 卡&#xff0c;然后噗的一声&#xff0c;一切都消失了。值得庆幸的是&#xff0c;在技术领域&#xff0c;你可以纠正一些错误。其中包括删除数据或格式化错误的存储设备。 那么&#xff0c;让我们看看可用于从 SD …

人工智能与自动驾驶:智能出行时代的未来之路

一、前言 首先&#xff0c;我们先来说下什么是人工智能&#xff0c;人工智能&#xff08;Artificial Intelligence&#xff0c;简称AI&#xff09;是一门研究如何使计算机系统能够模拟、仿真人类智能的技术和科学领域。它涉及构建智能代理&#xff0c;使其能够感知环境、理解和…

Unity中URP下的菲涅尔效果实现(个性化修改)

文章目录 前言一、我们修正一下上篇文章中&#xff0c;可能遗留的Bug1、N向量 变为 单位向量2、使颜色范围在合理区间 二、实现菲涅尔效果强弱可自定义调节三、修改菲涅尔效果颜色1、在属性面板定义颜色属性2、在常量缓冲区申明该参数3、在片元着色器中&#xff0c;用颜色和菲涅…

【日积月累】Mysql性能优化

目录 【日积月累】Mysql性能优化 1.前言2.MySql性能优化的主要方面2.1硬件和操作系统成面优化2.2架构设计层优化&#xff08;表结构&#xff09;2.3程序配置优化2.5表结构及索引优化2.4sql执行优化 3.总结4.参考 文章所属专区 积月累 1.前言 MySQL是一种常用的开源关系型数据…

【PHP入门】1.1-PHP初步语法

-PHP语法初步- PHP是一种运行在服务器端的脚本语言&#xff0c;可以嵌入到HTML中。 1.1.1PHP代码标记 在PHP历史发展中&#xff0c;可以使用多种标记来区分PHP脚本 ASP标记&#xff1a; <% php代码 %>短标记&#xff1a; <? Php代码 ?>&#xff0c;以上两种…

【TB作品】51单片机 实物+仿真-电子拔河游戏_亚博 BST-M51

代码工程。 http://dt4.8tupian.net/2/28880a66b12880.pg3这段代码是用于一个数字拔河游戏的嵌入式系统&#xff0c;采用了基于8051架构的单片机&#xff0c;使用Keil C51编译器。 主要功能包括&#xff1a; 数码管显示&#xff1a;使用了四个数码管&#xff08;通过P2的控制…

从零开始在Linux服务器配置并运行YOLO8+Web项目

✅作者简介&#xff1a;大家好&#xff0c;我是 Meteors., 向往着更加简洁高效的代码写法与编程方式&#xff0c;持续分享Java技术内容。 &#x1f34e;个人主页&#xff1a;Meteors.的博客 &#x1f49e;当前专栏&#xff1a; 神经网络&#xff08;随缘更新&#xff09; ✨特色…

js基础入门

先来一点js基础&#xff0c;其实js大部分的时候都在处理对象或者数组。 对象四个基本操作&#xff1a;增删改查 掌握元素的增删改查&#xff0c;了解如何拷贝&#xff0c;深拷贝和浅拷贝的区别。详情见代码 <script>//创建对象一共有三种赋值声明的语法let obj{} //赋值…

Ps:动作 - 基础篇

Ps 中的动作 Action可以让你自动、连续地完成多个操作&#xff0c;包括基础的调色、复杂的合成等。对于经常要执行的任务&#xff0c;使用动作&#xff0c;可大大提高工作效率。 Ps菜单&#xff1a;窗口/动作 Window/Action 快捷键&#xff1a;Alt F9 ◆ ◆ ◆ 认识动作面板…

如何用Python向图像中加入噪声

我们在做机器视觉项目的过程中&#xff0c;有的时候需要向图像中加入噪声。Pytorch本身不支持类似的功能&#xff0c;如果自己写的话&#xff0c;不但麻烦&#xff0c;而且容易出错。好在skimage支持这个功能。代码如下&#xff1a; import skimage import matplotlib.pyplot …

web之CSS悬停效果页面设计

参考小米商城的“手机”页面&#xff0c;做出一个“手机”的标签&#xff0c;实现当鼠标悬停在“手机”上时&#xff0c;出现手机系列菜单&#xff1b;当鼠标移走时&#xff0c;菜单页面消失的效果&#xff01; 参考图&#xff1a; 实现代码&#xff1a; <!DOCTYPE html&g…

机器学习笔记 - 时间序列分析基础概念解释

一、简述 时间序列分析是一种统计方法,可检查定期收集的数据点以揭示潜在的模式。该技术与各个行业高度相关,因为它可以根据历史数据做出决策和预测。通过了解过去并预测未来,时间序列分析在金融、医疗保健、能源、供应链管理、天气预报、营销等领域发挥着至关重要的作用。 …

How to helm install prometheus 【 helm 安装 prometheus 】

文章目录 1. 简介2. 简单部署3. 数据持久化部署3.1 设置必要的环境变量3.2 运行安装脚本3.3 查看 1. 简介 kube-prometheus-stack是一个基于Prometheus和Grafana的开源软件套件&#xff0c;用于在Kubernetes集群中进行监控和可视化。它提供了一套完整的工具和组件&#xff0c;…

神经网络学习小记录76——Tensorflow2设置随机种子Seed来保证训练结果唯一

神经网络学习小记录76——Tensorflow2设置随机种子Seed来保证训练结果唯一 学习前言为什么每次训练结果不同什么是随机种子训练中设置随机种子 学习前言 好多同学每次训练结果不同&#xff0c;最大的指标可能会差到3-4%这样&#xff0c;这是因为随机种子没有设定导致的&#x…

ArcMap自定义脚本工具箱迁移至ArcGIS pro

本文记录了将ArcMap10.7创建的自定义脚本工具箱&#xff08;.tbx&#xff09;迁移至ArcGIS pro的过程 ArcGIS Pro使用的是python版本与ArcMap不同&#xff0c;前者为python3&#xff0c;后者为python2。由于python3 和 python2 的部分语法不兼容&#xff0c;以及一些地理处理工…

Redis设计与实现之事务

一、事务 Redis 通过 MULTI 、DISCARD 、EXEC 和 WATCH 四个命令来实现事务功能&#xff0c;本章首先讨 论使用 MULTI 、DISCARD 和 EXEC 三个命令实现的一般事务&#xff0c;然后再来讨论带有 WATCH 的事务的实现。 因为事务的安全性也非常重要&#xff0c;所以本章最后通过…