Linux: SPI 驱动
文章目录
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. SPI 总线驱动
2.1 SPI 总线拓扑
SPI 总线是Master设备和Slave从设通信的接口,是一种高速、全双工
的同步串行通信总线。简单看一下 SPI 总线的拓扑结构:
其中:
SCLK: SPI 总线时钟,4MHz-20MHz;
MOSI: 数据线,从Master传送数据到Slave;
MISO: 数据线,从Slave传送数据到Master;
SSx: Slave片选信号,可能标记成CS更常见。
2.2 SPI 总线工作模式
SPI 有4种工作模式,通过串行时钟极性(CPOL)
和相位(CPHA)
的搭配来得到4种工作模式。先看下 CPOL
和 CPHA
的作用:
1. CPOL=0,串行时钟空闲状态为低电平。
2. CPOL=1,串行时钟空闲状态为高电平,此时可以通过配置时钟相位(CPHA)来选择具体的传输协议。
3. CPHA=0,串行时钟的第1个跳变沿(上升沿或下降沿)采集数据。
4. CPHA=1,串行时钟的第2个跳变沿(上升沿或下降沿)采集数据。
再看由 CPOL
和 CPHA
搭配的模式:
SCL空闲时电平 采样 CPHA Mode
低电平 上升沿 CPOL = 0, CPHA = 0 Mode0
低电平 下降沿 CPOL = 0, CPHA = 1 Mode1
高电平 下降沿 CPOL = 1, CPHA = 0 Mode2
高电平 上升沿 CPOL = 1, CPHA = 1 Mode3
通过配置 SPI Master控制器,可以配置 SPI 的工作模式,而从设的模式一般是固定的,可以参数从设的数据手册。
SPI 的读写不同于 I2C,不需要显示标记,因为 SPI 是全双工的,读写可以同时进行。
2.3 SPI 总线驱动编写
编写 SPI 总线驱动相关的内核接口:
extern struct spi_controller *__spi_alloc_controller(struct device *host,unsigned int size, bool slave);
extern int spi_register_controller(struct spi_controller *ctlr);
看一个 SPI 总线驱动框架示例:
spi@01c68000 {compatible = "allwinner,sun8i-h3-spi";...spi@0 {compatible = "nanopi,spidev";...};
};
static int sun6i_spi_probe(struct platform_device *pdev)
{struct spi_master *master;/* 创建SPI总线控制器对象 */master = spi_alloc_master(&pdev->dev, sizeof(struct sun6i_spi));.../* SPI控制器中断处理: 数据传输完成、传输FIFO等的处理 */ret = devm_request_irq(&pdev->dev, irq, sun6i_spi_handler,0, "sun6i-spi", sspi);...master->max_speed_hz = 100 * 1000 * 1000; /* 支持的最小时钟 */master->min_speed_hz = 3 * 1000; /* 支持的最小时钟 */master->set_cs = sun6i_spi_set_cs; /* 片选接口 */master->transfer_one = sun6i_spi_transfer_one; /* 设置SPI总线数据传输接口 */...master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH | SPI_LSB_FIRST; /* 设置工作模式 */.../* 注册SPI总线控制器对象到系统 */ret = devm_spi_register_master(&pdev->dev, master);return 0;
}static const struct of_device_id sun6i_spi_match[] = {...{ .compatible = "allwinner,sun8i-h3-spi", .data = (void *)SUN8I_FIFO_DEPTH },{}
};static struct platform_driver sun6i_spi_driver = {.probe = sun6i_spi_probe,....driver = {....of_match_table = sun6i_spi_match,...};
};
3. SPI 从设驱动
以 ads7846
触摸输入设备驱动为例来分析。
spi@01c68000 {compatible = "allwinner,sun8i-h3-spi";.../* SPI 从设 ads7846,挂接在 SPI 总线 spi@01c68000 上 */pitft-ts@1 {compatible = "ti,ads7846";...spi-max-frequency = <2000000>;interrupt-parent = <&pio>;interrupts = <6 9 IRQ_TYPE_EDGE_FALLING>; /* PG9 / EINT9 */...};
};
static const struct of_device_id ads7846_dt_ids[] = {...{ .compatible = "ti,ads7846", .data = (void *) 7846 },...{ }
};/* SPI 从设驱动入口 */
static int ads7846_probe(struct spi_device *spi)
{struct input_dev *input_dev;...spi->bits_per_word = 8;spi->mode = SPI_MODE_0;err = spi_setup(spi); /* 设置 SPI 工作模式和时钟频率 *//* 创建输入设备对象 */input_dev = input_allocate_device();.../* 配置输入设备 */input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_ABS);input_dev->keybit[BIT_WORD(BTN_TOUCH)] = BIT_MASK(BTN_TOUCH);input_set_abs_params(input_dev, ABS_X,pdata->x_min ? : 0,pdata->x_max ? : MAX_12BIT,0, 0);.../* 上电 */ts->reg = regulator_get(&spi->dev, "vcc");err = regulator_enable(ts->reg);/* 注册输入中断处理接口:上报按键事件 */err = request_threaded_irq(spi->irq, ads7846_hard_irq, ads7846_irq,irq_flags, spi->dev.driver->name, ts);.../* 注册输入设备 */err = input_register_device(input_dev);...return 0;
}static struct spi_driver ads7846_driver = {.driver = {.name = "ads7846",....of_match_table = of_match_ptr(ads7846_dt_ids),},.probe = ads7846_probe,...
};
一个 SPI 输入设备的驱动框架已经出来了,现在还剩一点需要说明:系统是何时创建 spi_device
来触发驱动的 ads7846_probe()
接口的?答案是 SPI 控制器驱动对象注册的时候:
#define devm_spi_register_master(_dev, _ctlr) \\devm_spi_register_controller(_dev, _ctlr)int devm_spi_register_controller(struct device *dev,struct spi_controller *ctlr)
{int ret;...ret = spi_register_controller(ctlr);...return ret;
}int spi_register_controller(struct spi_controller *ctlr)
{...dev_set_name(&ctlr->dev, "spi%u", ctlr->bus_num);status = device_add(&ctlr->dev);...list_add_tail(&ctlr->list, &spi_controller_list);list_for_each_entry(bi, &board_list, list)spi_match_controller_to_boardinfo(ctlr, &bi->board_info); /* 旧的 spi_register_board_info() 方式创建 spi_device */of_register_spi_devices(ctlr); /* DTS 方式创建 spi_device */acpi_register_spi_devices(ctlr); /* ACPI 方式创建 spi_device */
done:return status;
}/* 旧的 spi_register_board_info() 方式创建 spi_device */
static void spi_match_controller_to_boardinfo(struct spi_controller *ctlr,struct spi_board_info *bi)
{struct spi_device *dev;dev = spi_new_device(ctlr, bi);...
}struct spi_device *spi_new_device(struct spi_controller *ctlr,struct spi_board_info *chip)
{struct spi_device *proxy;proxy = spi_alloc_device(ctlr); /* 创建 spi_device 对象 */...status = spi_add_device(proxy); * 注册 spi_device 到 driver core,触发驱动 probe 接口 */...return proxy;
}/* DTS 方式创建 spi_device */
static void of_register_spi_devices(struct spi_controller *ctlr)
{struct spi_device *spi;/ 扫描 DTS 中 SPI 总线上挂接的从设节点,为它们创建 spi_device。 * 如前面 DTS 代码片段中的 "ti,ads7846" 。*/for_each_available_child_of_node(ctlr->dev.of_node, nc) {...spi = of_register_spi_device(ctlr, nc);...}
}static struct spi_device *
of_register_spi_device(struct spi_controller *ctlr, struct device_node *nc)
{struct spi_device *spi;int rc;spi = spi_alloc_device(ctlr); /* 创建 spi_device 对象 */...rc = of_spi_parse_dt(ctlr, spi, nc); /* 解析 SPI 从设 DTS 配置 */rc = spi_add_device(spi); /* 注册 spi_device 到 driver core,触发驱动 probe 接口 */return spi;
}
4. SPI 用户空间接口
我们可以通过 SPI 子系统,提供的用户空间接口来操控 SPI 从设。
4.1 创建 SPI 总线用户空间字符设备节点
spi@01c68000 {compatible = "allwinner,sun8i-h3-spi";.../* SPI 总线用户空间设备 DTS */spi@0 {compatible = "nanopi,spidev";...};
};
/* drivers/spi/spidev.c */static const struct of_device_id spidev_dt_ids[] = {...{ .compatible = "nanopi,spidev" },...{},
};
MODULE_DEVICE_TABLE(of, spidev_dt_ids);static int spidev_probe(struct spi_device *spi)
{struct spidev_data *spidev;...minor = find_first_zero_bit(minors, N_SPI_MINORS);if (minor < N_SPI_MINORS) struct device *dev;spidev->devt = MKDEV(SPIDEV_MAJOR, minor);/* 创建 SPI 总线的字符设备节点:供用户空间访问 */dev = device_create(spidev_class, &spi->dev, spidev->devt,spidev, "spidev%d.%d",spi->master->bus_num, spi->chip_select);} else {...}...
}s
tatic struct spi_driver spidev_spi_driver = {.driver = {.name = "spidev",.of_match_table = of_match_ptr(spidev_dt_ids),...},.probe = spidev_probe,...
};static int __init spidev_init(void)
{int status;/* SPI 总线字符设备 */status = register_chrdev(SPIDEV_MAJOR, "spi", &spidev_fops);...spidev_class = class_create(THIS_MODULE, "spidev");...status = spi_register_driver(&spidev_spi_driver);...return status;
}
module_init(spidev_init);
4.2 操作 SPI 总线用户字符设备节点
int fd, mode = SPI_MODE_0;fd = open("/dev/spidev1.0", O_RDWR);
ret = ioctl(fd, SPI_IOC_WR_MODE, &mode); /* 设置工作模式 */
ret = ioctl(fd_spi, SPI_IOC_WR_BITS_PER_WORD, &bits); /* 设置SPI的数据位 */
ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed); /* 设置速度 */
ret = ioctl(fd, SPI_IOC_MESSAGE(n), &tr); /* 数据发送 */
...
close(fd);
更多细节参考 drivers/spi/spidev.c
中的 spidev_ioctl()
接口。