spi节点一般表示spi控制器, 它会被转换为platform_device, 全志H3的总线设备驱动位于busses/spi-sun6i.c,该文件有对应的platform_driver;platform_driver的probe函数中会调用i2c_add_numbered_adapter,来增加一个spi_master,调用过程如下:
sun6i_spi_probe
devm_spi_register_master
devm_spi_register_controller
spi_register_controller
of_register_spi_devices(ctlr); /* Register devices from the device tree */
for_each_available_child_of_node(ctlr->dev.of_node, nc)
spi = of_register_spi_device(ctlr, nc);
由上面的调用流程可以得知,spi节点遍历其下的子节点,转换成一个个spi_device 在spi总线驱动程序中,才会处理SPI的设备树节点,找到处理代码, 通过of_register_spi_device创建SPI设备
在未使用设备树描述spi设备时,spi设备的平台信息由spi_register_board_info来注册 相关函数调用如下:
drivers/spi/spi.c:
spi_register_board_info
/* 对于每一个spi_master,调用spi_match_master_to_boardinfo */
list_for_each_entry(master, &spi_master_list, list)
spi_match_master_to_boardinfo
/* board_info里含有bus_num, 如果某个spi_master的bus_num跟它一样
* 则创建一个新的spi_device
*/
if (master->bus_num == bi->busnum)
spi_new_device
spi_alloc_device
/* 记录bi信息, 比如片选,MODE,MAX HZ */
spi_add_device /* 根据名字找到spi_driver, 调用它的probe函数 */
spi_setup(spi);
device_add /* 会绑定到一个spi_driver */
在全志H3平台上,SPI总线和SPI设备的平台信息都是由设备树来描述。通过反编译sun8i-h3-nanopi-m1.dtb可以得出设备树中的spi节点
spi@01c68000 {
compatible = "allwinner,sun8i-h3-spi";
reg = <0x1c68000 0x1000>;
interrupts = <0x0 0x41 0x4>;
clocks = <0x4 0x1e 0x4 0x52>;
clock-names = "ahb", "mod";
dmas = <0x7 0x17 0x7 0x17>;
dma-names = "rx", "tx";
pinctrl-names = "default";
pinctrl-0 = <0x1e 0x1f>;
resets = <0x4 0xf>;
status = "okay";
#address-cells = <0x1>;
#size-cells = <0x0>;
cs-gpios = <0x14 0x2 0x3 0x0 0x14 0x0 0x6 0x0>;
linux,phandle = <0x61>;
phandle = <0x61>;
spi@0 {
compatible = "nanopi,spidev";
/* 此属性值用于与spi设备驱动匹配 */
reg = <0x0>;
//第0个片选
status = "okay";
spi-max-frequency = <0x989680>;
//最大频率10M
linux,phandle = <0x62>;
phandle = <0x62>;
};
spiflash@0 {
#address-cells = <0x1>;
#size-cells = <0x1>;
compatible = "mxicy,mx25l12805d";
reg = <0x0>;
status = "disabled";
spi-max-frequency = <0x2faf080>;
mode = <0x0>;
linux,phandle = <0x63>;
phandle = <0x63>;
partition@0 {
reg = <0x0 0x1000000>;
label = "spi-flash";
};
};
pitft@0 {
compatible = "sitronix,st7789v";
reg = <0x0>;
status = "disabled";
spi-max-frequency = <0x2faf080>;
rotate = <0x5a>;
fps = <0x21>;
buswidth = <0x8>;
dc-gpios = <0x14 0x0 0x1 0x0>;
reset-gpios = <0x14 0x6 0xb 0x0>;
debug = <0x0>;
linux,phandle = <0x64>;
phandle = <0x64>;
};
pitft-ts@1 {
compatible = "ti,ads7846";
reg = <0x1>;
status = "disabled";
spi-max-frequency = <0x1e8480>;
interrupt-parent = <0x14>;
interrupts = <0x6 0x9 0x2>;
pendown-gpio = <0x14 0x6 0x9 0x1>;
ti,swap-xy;
ti,vref-delay-usecs = <0x3e8>;
ti,x-min = [00 64];
ti,x-max = [0f ff];
ti,y-min = [00 64];
ti,y-max = [0f ff];
ti,vref-mv = <0xce4>;
ti,x-plate-ohms = [01 00];
ti,penirq-recheck-delay-usecs = <0xa>;
ti,settle-delay-usec = [00 64];
ti,keep-vref-on = <0x1>;
ti,pressure-max = [0f ff];
ti,debounce-max = <0xa>;
ti,debounce-tol = <0x1e>;
ti,debounce-rep = <0x1>;
linux,phandle = <0x65>;
phandle = <0x65>;
};
};
硬件信息说明(linux4.14,soc是nanopi-m1)
共阳数码管使用的是 3641BS ,数码管模块的原理图如下所示,通过两片74HC595芯片分别控制数码管的位选和段选,位选高电平有效 段选低电平有效,这样就可以点亮数码管。
流程 :
发送8位位选数据
发送8位段选数据
全志H3两个spi控制器,相关数码管模块中的MOSI,SCK,CS接在PC0,PC2,PC3。由于数码管不会返回数据给soc。因此不需要使用MISO。
SPI0:
MOSI======>>> PC0
MISO======>>> PC1
SCK ======>>> PC2
CS ======>>> PC3
使用spi0通过控制74HC595锁存器来达到控制八段数码管的目的
在sun8i-h3-nanopi.dtsi中引用了spi0节点,并在其下构造了spidev0子节点,首先我们把它的状态disable掉,spi0外接74HC595的相关信息通过编写驱动来注册spi_register_board_info
359 spidev0: spi@0 {
360 compatible = "nanopi,spidev";
361 reg = <0>;
362 status = "disable";
363
364 spi-max-frequency = <10000000>;
365 };
编译设备树文件,把dtb文件拷贝到启动卡的第一个分区,重启开发板。进入文件系统
查看sysfs没有spi 设备节点
root@wu:~# ls /sys/bus/spi/devices/
root@wu:~#
root@wu:~# ls /dev/spidev0.0
/dev/spidev0.0
可以通过/dev/spidev0.0从用户空间访问
使用总线设备驱动模型来编写spi设备驱动
- 编写spi_info_595.c,设置spi_board_info结构体,通过spi_board_info注册到系统
-
static struct spi_board_info spi_info_smg[] = { { .modalias = "595_smg", /* 对应的spi_driver名字也是"595_smg" */ .max_speed_hz = 10000, /* max spi clock (SCK) speed in HZ */ .bus_num = 0, /* nanopi里smg接在SPI CONTROLLER 0 */ .mode = SPI_MODE_0, .chip_select = 0, /* 片选编号 */ //.platform_data = (const void *)S3C2410_GPG(4) , /* oled_dc, 它在spi_driver里使用 */ }, }; static int spi_info_smg_init(void) { return spi_register_board_info(spi_info_smg, ARRAY_SIZE(spi_info_smg)); }
-
编写spi_drv_595.c
- 设置注册一个spi_driver结构体,当spi_driver里的name和spi_board_info里的modalias字段相匹配时,则会调用spi_driver结构体里的probe函数
static struct spi_driver spi_smg_drv = {
.driver = {
.name = "595_smg",
.owner = THIS_MODULE,
},
.probe = spi_smg_probe,
.remove = spi_smg_remove,
};
static int spi_smg_init(void)
{
return spi_register_driver(&spi_smg_drv);
}
-
- 在probe函数中获得得到在总线驱动程序中解析得到的spi_device,并为该设备注册一个字符设备
static struct file_operations smg_ops = {
.owner = THIS_MODULE,
.open = smg_open,
.write = smg_write,
};
static int spi_smg_probe(struct spi_device *spi)
{
spi_smg_dev = spi;
printk("595_msg probed\n");
/* 注册一个 file_operations */
major = register_chrdev(0, "595_smg", &smg_ops);
class = class_create(THIS_MODULE, "595_smg");
/* 为了让mdev根据这些信息来创建设备节点 */
device_create(class, NULL, MKDEV(major, 0), NULL, "595_smg"); /* /dev/595_smg */
return 0;
}
- 填充字符设备中的file_operations结构体 ,在驱动的write函数中,通过spi写入数码管对应的位码和段码
static ssize_t smg_write(struct file *file,
const char __user *buf,
size_t count, loff_t *ppos)
{
int ret;
unsigned char data[2];
if (count > 2)
return -EINVAL;
ret = copy_from_user(data, buf, count);
write_test(data, 2);
return 1;
}
int smg_open (struct inode *inode, struct file *file)
{
printk("drv open\n");
return 0;
}
- 构造spi_message,通过异步传输给spi总线
static int write_test(unsigned char *buffer, int len)
{
int status;
struct spi_transfer t = {
.tx_buf = buffer,
.len = len,
};
struct spi_message m;
spi_message_init(&m);
spi_message_add_tail(&t, &m);
DECLARE_COMPLETION_ONSTACK(done);
m.complete = complete;
m.context = &done;
printk("spi_async send begin!\n");
status = spi_async(spi_smg_dev,&m);
if(status == 0){
wait_for_completion(&done);
status = m.status;
if (status == 0)
status = m.actual_length;
}
return status;
}
编写驱动程序的Makefile
KERN_DIR =/samba_share/nanopi_4.14/linux
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += spi_info_595.o
obj-m += spi_drv_595.o
编译成功后,挂载nfs,加载spi_info_595.ko这一模块,出现Unknown symbol spi_register_board_info,这是因为内核中没有导出该符号 drivers/spi/spi.c文件里找到spi_register_board_info函数实现的地方,在后面加上一句:EXPORT_SYMBOL_GPL(spi_register_board_info);
重新编译内核即可。
加载spi_info_595.ko这一模块,成功后在sysfs文件下出现spi设备
root@wu:/mnt/spi_test/spidrv_nodt# ls /sys/bus/spi/devices/
spi0.0
加载spi_drv_595.ko这一模块,就会和注册上的设备相互匹配
root@wu:/mnt/spi_test/spidrv_nodt# insmod spi_drv_595.ko
[ 288.557768] 595_msg probed
编写测试程序
#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/types.h>
uint8_t led_value_table[] = {0xC0, 0xF9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90};
uint8_t led_num_table[]={0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01};
void show_led_num(int fd,int value,int num)
{
int ret;
uint8_t tx[]={led_value_table[value],led_num_table[num],};
write(fd,tx,2);
}
int main(int argc, char **argv)
{
int fd;
char val;
unsigned char buf[3];
int led_value,led_num;
if(argc!=3)
{
printf("cmd : ./spi_usrspace led_value led_num \n ");
return -1;
}
led_value=atoi(argv[1]);
if ((led_value) < 0 || (led_value > 9)) { /* 该值必须在 0 ~ 9 之间 */
printf("led num just in 0 ~ 9 \n");
return -1;
}
led_num = atol(argv[2]); /* 获取程序输入参数的数字选择值 */
if ((led_num < 0) || (led_num > 7)) { /* 该值必须在 0 ~ 9 之间 */
printf("led number just in 0 ~ 7");
return -1;
}
fd = open("/dev/595_smg", O_RDWR);
sleep(1);
show_led_num(fd,led_value,led_num);
return 0;
}
交叉编译后,数码管成功显示数字
编写基于设备树下spi设备驱动程序,平台信息有设备树来指定
在sun8i-h3-nanopi.dtsi中引用了spi0节点,现在sun8i-h3-nanopi-m1.dts在增加一个节点
&spi0{
status="okay";
595@0 {
compatible = "hc,595";
reg = <0>;
status = "okay";
spi-max-frequency = <100000>;
};
};
编译设备树,把dtb文件拷贝到启动卡的第一个分区,重启开发板。进入文件系统
root@wu:~# ls /sys/bus/spi/devices/
spi0.0
root@wu:~# cat /sys/bus/spi/devices/spi0.0/of_node/compatible
hc,595
编写驱动程序
- 构造一个platform_driver,其中的of_match_table字段需要与 595@0节点的compatible属性值一致,当匹配时则调用platform_driver的probe函数
static const struct of_device_id ids[]=
{
{.compatible="hc,595"},
{}
};
static struct spi_driver spi_smg_drv = {
.driver = {
.name = "wu",
.owner = THIS_MODULE,
.of_match_table=ids,
},
.probe = spi_smg_probe,
.remove = spi_smg_remove,
};
static int spi_smg_init(void)
{
return spi_register_driver(&spi_smg_drv);
}
- 在probe函数中获得得到在总线驱动程序中解析得到的spi_device,并为该设备注册一个字符设备
static struct file_operations smg_ops = {
.owner = THIS_MODULE,
.open = smg_open,
.write = smg_write,
};
static int spi_smg_probe(struct spi_device *spi)
{
spi_smg_dev = spi;
printk("595_msg probed\n");
/* 注册一个 file_operations */
major = register_chrdev(0, "595_smg", &smg_ops);
class = class_create(THIS_MODULE, "595_smg");
/* 为了让mdev根据这些信息来创建设备节点 */
device_create(class, NULL, MKDEV(major, 0), NULL, "595_smg"); /* /dev/595_smg */
- 填充字符设备中的file_operations结构体 ,在驱动的write函数中,通过spi写入数码管对应的位码和段码
static ssize_t smg_write(struct file *file,
const char __user *buf,
size_t count, loff_t *ppos)
{
int ret;
unsigned char data[2];
if (count > 2)
return -EINVAL;
ret = copy_from_user(data, buf, count);
write_test(data, 2);
return 1;
}
int smg_open (struct inode *inode, struct file *file)
{
printk("drv open\n");
return 0;
}
- 在platform_driver的remove函数中,注销该字符设备
static int spi_smg_remove(struct spi_device *spi)
{
device_destroy(class, MKDEV(major, 0));
class_destroy(class);
unregister_chrdev(major, "595_smg");
return 0;
}
使用上面的测试程序,进行测试,显示正常