编写SPI_Master驱动程序
文章目录
- 编写SPI_Master驱动程序
- 参考资料:
- 一、 SPI驱动框架
- 1.1 总体框架
- 1.2 怎么编写SPI_Master驱动
- 1.2.1 编写设备树
- 1.2.2 编写驱动程序
- 二、 编写程序
- 2.1 数据传输流程
- 2.2 写代码
- 致谢
参考资料:
- 内核头文件:
include\linux\spi\spi.h
- 内核文档:
Documentation\devicetree\bindings\spi\spi-bus.txt
- 内核源码:
drivers\spi\spi.c
、drivers\spi\spi-sh.c
- 内核源码:
一、 SPI驱动框架
1.1 总体框架
1.2 怎么编写SPI_Master驱动
1.2.1 编写设备树
在设备树中,对于SPI Master,必须的属性如下:
- #address-cells:这个SPI Master下的SPI设备,需要多少个cell来表述它的片选引脚
- #size-cells:必须设置为0
- compatible:根据它找到SPI Master驱动
可选的属性如下:
- cs-gpios:SPI Master可以使用多个GPIO当做片选,可以在这个属性列出那些GPIO
- num-cs:片选引脚总数
其他属性都是驱动程序相关的,不同的SPI Master驱动程序要求的属性可能不一样。
在SPI Master对应的设备树节点下,每一个子节点都对应一个SPI设备,这个SPI设备连接在该SPI Master下面。
这些子节点中,必选的属性如下:
- compatible:根据它找到SPI Device驱动
- reg:用来表示它使用哪个片选引脚
- spi-max-frequency:必选,该SPI设备支持的最大SPI时钟
可选的属性如下:
- spi-cpol:这是一个空属性(没有值),表示CPOL为1,即平时SPI时钟为低电平
- spi-cpha:这是一个空属性(没有值),表示CPHA为1),即在时钟的第2个边沿采样数据
- spi-cs-high:这是一个空属性(没有值),表示片选引脚高电平有效
- spi-3wire:这是一个空属性(没有值),表示使用SPI 三线模式
- spi-lsb-first:这是一个空属性(没有值),表示使用SPI传输数据时先传输最低位(LSB)
- spi-tx-bus-width:表示有几条MOSI引脚;没有这个属性时默认只有1条MOSI引脚
- spi-rx-bus-width:表示有几条MISO引脚;没有这个属性时默认只有1条MISO引脚
- spi-rx-delay-us:单位是毫秒,表示每次读传输后要延时多久
- spi-tx-delay-us:单位是毫秒,表示每次写传输后要延时多久
1.2.2 编写驱动程序
- 核心为:分配/设置/注册spi_master结构体
- 对于老方法,spi_master结构体的核心是transfer函数
二、 编写程序
2.1 数据传输流程
2.2 写代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <linux/timer.h>
#include <linux/delay.h>
#include <linux/list.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/spi/spi.h>
static struct spi_master *g_virtual_master;
static struct work_struct g_virtual_ws;
static const struct of_device_id spi_virtual_dt_ids[] = {
{ .compatible = "100ask,virtual_spi_master", },
{ /* sentinel */ }
};
static void spi_virtual_work(struct work_struct *work)
{
struct spi_message *mesg;
while (!list_empty(&g_virtual_master->queue)) {
mesg = list_entry(g_virtual_master->queue.next, struct spi_message, queue);
list_del_init(&mesg->queue);
/* 假装硬件传输已经完成 */
mesg->status = 0;
if (mesg->complete)
mesg->complete(mesg->context);
}
}
static int spi_virtual_transfer(struct spi_device *spi, struct spi_message *mesg)
{
#if 0
/* 方法1: 直接实现spi传输 */
/* 假装传输完成, 直接唤醒 */
mesg->status = 0;
mesg->complete(mesg->context);
return 0;
#else
/* 方法2: 使用工作队列启动SPI传输、等待完成 */
/* 把消息放入队列 */
mesg->actual_length = 0;
mesg->status = -EINPROGRESS;
list_add_tail(&mesg->queue, &spi->master->queue);
/* 启动工作队列 */
schedule_work(&g_virtual_ws);
/* 直接返回 */
return 0;
#endif
}
static int spi_virtual_probe(struct platform_device *pdev)
{
struct spi_master *master;
int ret;
/* 分配/设置/注册spi_master */
g_virtual_master = master = spi_alloc_master(&pdev->dev, 0);
if (master == NULL) {
dev_err(&pdev->dev, "spi_alloc_master error.\n");
return -ENOMEM;
}
master->transfer = spi_virtual_transfer;
INIT_WORK(&g_virtual_ws, spi_virtual_work);
master->dev.of_node = pdev->dev.of_node;
ret = spi_register_master(master);
if (ret < 0) {
printk(KERN_ERR "spi_register_master error.\n");
spi_master_put(master);
return ret;
}
return 0;
}
static int spi_virtual_remove(struct platform_device *pdev)
{
/* 反注册spi_master */
spi_unregister_master(g_virtual_master);
return 0;
}
static struct platform_driver spi_virtual_driver = {
.probe = spi_virtual_probe,
.remove = spi_virtual_remove,
.driver = {
.name = "virtual_spi",
.of_match_table = spi_virtual_dt_ids,
},
};
static int virtual_master_init(void)
{
return platform_driver_register(&spi_virtual_driver);
}
static void virtual_master_exit(void)
{
platform_driver_unregister(&spi_virtual_driver);
}
module_init(virtual_master_init);
module_exit(virtual_master_exit);
MODULE_DESCRIPTION("Virtual SPI bus driver");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Hilbert");
测试程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include <errno.h>
/* dac_test /dev/spidevB.D <val> */
int main(int argc, char **argv)
{
int fd;
unsigned int val;
struct spi_ioc_transfer xfer[1];
int status;
unsigned char tx_buf[2];
unsigned char rx_buf[2];
if (argc != 3)
{
printf("Usage: %s /dev/spidevB.D <val>\n", argv[0]);
return 0;
}
fd = open(argv[1], O_RDWR);
if (fd < 0) {
printf("can not open %s\n", argv[1]);
return 1;
}
val = strtoul(argv[2], NULL, 0);
val <<= 2; /* bit0,bit1 = 0b00 */
val &= 0xFFC; /* 只保留10bit */
tx_buf[1] = val & 0xff;
tx_buf[0] = (val>>8) & 0xff;
memset(xfer, 0, sizeof xfer);
xfer[0].tx_buf = tx_buf;
xfer[0].rx_buf = rx_buf;
xfer[0].len = 2;
status = ioctl(fd, SPI_IOC_MESSAGE(1), xfer);
if (status < 0) {
printf("SPI_IOC_MESSAGE %d\n", errno);
return -1;
}
/* 打印 */
val = (rx_buf[0] << 8) | (rx_buf[1]);
val >>= 2;
printf("Pre val = %d\n", val);
return 0;
}
设备树:
vitural_spi_master {
compatible = "hilbert,virtual_spi_master";
status = "okay";
cs-gpios = <&gpio4 27 GPIO_ACTIVE_LOW>;
num-chipselects = <1>;
#address-cells = <1>;
#size-cells = <0>;
virtual_spi_dev: virtual_spi_dev@0 {
compatible = "spidev";
reg = <0>;
spi-max-frequency = <100000>;
};
};
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/work/imx-6ull/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /work/imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o spi_test spi_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order spi_test
obj-m += virtual_spi_master.o
致谢
以上笔记源自
韦东山
老师的视频课程,感谢韦老师,韦老师是嵌入式培训界一股清流,为嵌入式linux开发点起的星星之火,也愿韦老师桃李满园。聚是一团火,散是满天星!
在这样一个速食的时代,坚持做自己,慢下来,潜心琢磨,心怀敬畏,领悟知识,才能向下扎到根,向上捅破天,背着世界往前行!
仅此向嵌入行业里的每一个认真做技术的从业者致敬!