linux驱动系列学习之spi子系统(五)

一、spi子系统简介

1.spi总线        

        串行外设接口(Serial Peripheral Interface,SPI)是微控制器和外围IC(如传感器、ADC、DAC、移位寄存器、SRAM等)之间使用最广泛的接口之一。 SPI是一种同步、全双工、主从式接口。来自主机或从机的数据在时钟上升沿或下降沿同步。主机和从机可以同时传输数据。SPI接口可以是3线式或4线式。本质上SPI是一个移位寄存器。

SPI总线一般四根线:

SCK:串行时钟,由SPI总线的主机提供,用于同步通信。

CS:一个spi总线上cs可以有多个,主机通过cs引脚控制不同的从机,cs为低电平的时候表示从机被选中,从而主机和被选中的从机通信。同一时刻一般只有从机的cs引脚被置低。

MOSI:主机输出、从机输入。数据由主机向从机发送。

MISO:主机输入、从机输出。数据由从机向主机发送。

时钟极性和时钟相位:

在SPI中,主机可以选择时钟极性和时钟相位。

时钟极性CPOL(Clock Polarity):是用来配置SCLK的电平处于哪种状态时有效。

CPOL=0:表示高电平有效,低电平处于空闲态。

CPOL=1:表示低电平有效,高电平处于空闲态。

时钟相位CPHA(Clock Phase):是用来配置数据采样是在第几个边沿,0表示第一个边沿(前沿Leading edge),1表示第二个边沿(后沿Trailing edge)。

CPHA=0:表示数据采样是在第1个边沿,数据发送在第2个边沿。

CPHA=1:表示数据采样是在第2个边沿,数据发送在第1个边沿。

        主机需要根据从机的要求选择时钟极性和时钟相位,也即从机的传输模式决定了主机的传输模式。故先要了解从机的SPI是何种模式,然后再将主机的SPI的模式设置成同样的模式,即可正常通讯。根据CPOL和CPHA位的选择,有四种SPI模式可用。

        传统的SPI协议是全双工、一根数据线的总线协议,现在的SPI协议可以多跟数据线同时用于读写,由2、4、8根线。SPI是一种传输速度快,但没有数据传输校验的短距离通信方式(i2c通信会有ack应答等保证数据传输到)。

2. spi子系统

        linux下的spi驱动不支持热插拔(i2c同样不支持,usb、hdmi支持热插拔)。因此需要驱动提供板级信息。使用的时候需要将板级信息填充到结构体里面、之后进行注册即可。

二、spi子系统使用(以icm20608为例)

        1.入口、退出函数

//驱动入口处函数
static int __init icm20608_init(void)
{
    //注册一个spi驱动。
	return spi_register_driver(&icm20608_spi_driver);
}
//驱动退出处函数
static void __exit icm20608_exit(void)
{
	return spi_unregister_driver(&icm20608_spi_driver);
}

 2. icm20608的spi驱动注册、卸载              

         icm20608_spi_driver是一个struct spi_driver类型的结构体。用来存放spi驱动相关的参数,包括probe、remove函数。  icm20608_spi_driver具体内如如下:

static const struct spi_device_id icm20608_id_table[]={
	{"alientek,icm20608", 0},
	{},
};

static const struct of_device_id icm20608_of_match[]={
	{.compatible = "alientek,icm20608"},
	{},
};


static struct spi_driver icm20608_spi_driver = {
	.probe = icm20608_probe,
	.remove = icm20608_remove,
	.driver = {
			.owner = THIS_MODULE,
			.name = "icm20608",
			.of_match_table = icm20608_of_match,
	},
	.id_table = icm20608_id_table,

};

alientek,icm20608字符串是用于在设备树里面匹配到对应的节点。

3. probe、remove函数

probe函数里面需要做一下事情:


1. 构建设备号
    alloc_chrdev_region(&icm20608_device.dev_id, 0, 1, ICM20608_NAME);
    icm20608_device.major = MAJOR(icm20608_device.dev_id);
2. 初始化字符设备
    cdev_init(&icm20608_device.cdev, &icm20608_device_fops);
    cdev_add(&icm20608_device.cdev,icm20608_device.dev_id,1);
3. 创建类
    icm20608_device.class = class_create(THIS_MODULE,ICM20608_NAME);
4. 创建设备
    icm20608_device.device = device_create(icm20608_device.class, NULL, icm20608_device.dev_id, NULL, ICM20608_NAME);
5. 获取设备节点
    icm20608_device.device_node = of_find_node_by_path("/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02010000");
6. 设置片选引脚
    icm20608_device.cs_gpio = of_get_named_gpio(icm20608_device.device_node, "cs-gpio", 0);
    gpio_direction_output(icm20608_device.cs_gpio, 1);
7. 设置spi
    spi_setup(spi);
8. icm20608寄存器初始化
    icm20608_reg_init();

具体代码如下:

static int icm20608_probe(struct spi_device *spi)
{
	int ret = 0;
	printk("icm20608_probe start\n");
	//1. 构建设备号
	if(icm20608_device.major){
		icm20608_device.dev_id = MKDEV(icm20608_device.major, 0);
		register_chrdev_region(icm20608_device.dev_id, 1, ICM20608_NAME);
	} else{
		alloc_chrdev_region(&icm20608_device.dev_id, 0, 1, ICM20608_NAME);
		icm20608_device.major = MAJOR(icm20608_device.dev_id);
	}

	//2. 初始化字符设备
	cdev_init(&icm20608_device.cdev, &icm20608_device_fops);
	cdev_add(&icm20608_device.cdev,icm20608_device.dev_id,1);
	
	//3. 创建类

	icm20608_device.class = class_create(THIS_MODULE,ICM20608_NAME);
	if(IS_ERR(icm20608_device.class)){
		return PTR_ERR(icm20608_device.class);
	}
	
	
	//4. 创建设备
	icm20608_device.device = device_create(icm20608_device.class, NULL, icm20608_device.dev_id, NULL, ICM20608_NAME);
	if(IS_ERR(icm20608_device.device)){
		return PTR_ERR(icm20608_device.device);
	}

	//5. 获取设备节点
	icm20608_device.device_node = of_find_node_by_path("/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02010000");
	if(icm20608_device.device_node == NULL){
		printk("icm20608 device node not find\n");
		return -EINVAL;
	}
		
	//6. 获取片选信号,设置片选信号引脚属性

	icm20608_device.cs_gpio = of_get_named_gpio(icm20608_device.device_node, "cs-gpio", 0);
	if(icm20608_device.cs_gpio < 0){
		printk("icm20608 cs gpio not find\n");
		return -EINVAL;
	}
	
	ret = gpio_direction_output(icm20608_device.cs_gpio, 1);
	if(ret < 0){
		printk("icm20608 cs gpio set fail\n");
		return -EINVAL;
	}


    //7. 设置spi
	spi->mode = SPI_MODE_0;
	spi_setup(spi);
	icm20608_device.privative_data = spi;
	
    //8. 寄存器初始化
	icm20608_reg_init();
	printk("icm20608_probe ok\n");
	return 0;
	
}

4. icm20608结构体及驱动文件操作        

        icm20608的文件操作则存放在类型为struct file_operations的icm20608_device_fops结构体中,这个对应linux下的设备文件,包括open、release、read等函数,与平常使用的驱动fops函数类似,在open里面获取当前获取驱动自定义的结构体。

static const struct file_operations  icm20608_device_fops = 
{
	.owner = THIS_MODULE,
	.open  = icm20608_device_open,
	.read  = icm20608_device_read,
	.release = icm20608_device_release,
};

        每个应用打开该设备(/dev/icm20608),均会获得一个icm20608_device结构体,用于后面的read、write、ioctl等操作。icm20608_device的类型是struct icm20608_device,自定义的一个icm操作资源描述结构体,用于控制icm硬件,定义如下。

struct icm20608_device{
    dev_t                dev_id;             //设备号
    int                  major;              //主设备号
    int                  minor;              //次设备号
    struct cdev          cdev;               //字符设备
    struct class         *class;             //类
    struct device        *device;            //设备
    struct device_node   *device_node;       //设备节点
    void                 *privative_data;    //私有数据
    int                  cs_gpio;            //片选引脚
};

5. spi驱动的读写函数

        spi总线本质上是两个移位寄存器进行交换数据。主机选择cs引脚,并进行读写。linux的spi驱动框架提供了响应的API函数,只要调用响应的API即可完成spi总线的读写过程。

主机读过程:

unsigned char tx_data[len];
struct spi_message m;
struct spi_transfer *t;
struct spi_device *spi = (struct spi_device *)icm20608_device->privative_data; //获取spi

t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
if(t == NULL){
	return -1;
}

gpio_set_value(icm20608_device->cs_gpio,0);  //选择cs
tx_data[0] = reg | 0x80;  //写数据的时候寄存器地址bit8要置1
t->tx_buf = tx_data;       //要发送的数据
t->len = 1;  

spi_message_init(&m);        //初始化spi消息
spi_message_add_tail(t,&m);   //将要发送的数据添加到message消息队列
ret = spi_sync(spi,&m);       //发送数据

kzfree(t);
gpio_set_value(icm20608_device->cs_gpio,1);  //取消选择cs

主机写过程:

unsigned char tx_data[len];
struct spi_message m;
struct spi_transfer *t;
struct spi_device *spi = (struct spi_device *)icm20608_device->privative_data; //获取spi

t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
if(t == NULL){
	return -1;
}

gpio_set_value(icm20608_device->cs_gpio,0);  //选择cs
tx_data[0] = 0xff;
t->rx_buf = buf;	   //要读取的数据
t->len = len;  

spi_message_init(&m);        //初始化spi消息
spi_message_add_tail(t,&m);   //将要发送的数据添加到message消息队列
ret = spi_sync(spi,&m);       //发送数据

kzfree(t);
gpio_set_value(icm20608_device->cs_gpio,1);  //取消选择cs

6. icm20608的读写函数

        根据spi驱动框架的api可以写出icm20608的读写操作函数。

static int icm20608_read_regs(struct icm20608_device *icm20608_device,uint8_t reg,uint8_t *buf,uint8_t len)
{
	int ret = 0;
	unsigned char tx_data[len];
	
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)icm20608_device->privative_data;

	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
	if(t == NULL){
		return -1;
	}
	
	gpio_set_value(icm20608_device->cs_gpio,0);  //选择cs

	//先发送寄存器地址,后读取数据
	tx_data[0] = reg | 0x80;  //写数据的时候寄存器地址bit8要置1
	t->tx_buf = tx_data;       //要发送的数据
	t->len = 1;  

	spi_message_init(&m);        //初始化spi消息
	spi_message_add_tail(t,&m);   //将要发送的数据添加到message消息队列
	ret = spi_sync(spi,&m);       //发送数据

	//读取数据
	tx_data[0] = 0xff;
	t->rx_buf = buf;	   //要读取的数据
	t->len = len;  

	spi_message_init(&m);		 //初始化spi消息
	spi_message_add_tail(t,&m);	 //将要发送的数据添加到message消息队列
	ret = spi_sync(spi,&m);		 //发送数据
		
	kzfree(t);
	gpio_set_value(icm20608_device->cs_gpio,1);   //拉高
	
	return ret;
	
}
static int icm20608_write_regs(struct icm20608_device *icm20608_device,uint8_t reg,uint8_t *buf,uint8_t len)
{
	int ret = 0;
	unsigned char tx_data[len];
	
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)icm20608_device->privative_data;

	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
	if(t == NULL){
		return -1;
	}
	
	gpio_set_value(icm20608_device->cs_gpio,0);  //选择cs

	//先发送寄存器地址,后发送数据
	tx_data[0] = reg & ~0x80;  //写数据的时候寄存器地址bit8要清零
	t->tx_buf = tx_data;       //要发送的数据
	t->len = 1;  

	spi_message_init(&m);        //初始化spi消息
	spi_message_add_tail(t,&m);   //将要发送的数据添加到message消息队列
	ret = spi_sync(spi,&m);       //发送数据

	//发送数据
	t->tx_buf = buf;	   //要发送的数据
	t->len = len;  

	spi_message_init(&m);		 //初始化spi消息
	spi_message_add_tail(t,&m);	 //将要发送的数据添加到message消息队列
	ret = spi_sync(spi,&m);		 //发送数据
		
	kzfree(t);
	gpio_set_value(icm20608_device->cs_gpio,1);   //拉高
	
	return ret;
	
}

剩下的写一个寄存器、读一个寄存器、寄存器初始化。


static uint8_t icm20608_read_one_reg(struct icm20608_device *icm20608_device,uint8_t reg)
{
	uint8_t buf = 0;
	icm20608_read_regs(icm20608_device,reg,&buf,1);
	return buf;

}

static void icm20608_write_one_reg(struct icm20608_device *icm20608_device,uint8_t reg,uint8_t value)
{
	uint8_t buf = value;
	icm20608_write_regs(icm20608_device,reg,&buf,1);
}

static void icm20608_reg_init(void)
{
	u8 value = 0;
	
	icm20608_write_one_reg(&icm20608_device, ICM20_PWR_MGMT_1, 0x80);
	mdelay(50);
	icm20608_write_one_reg(&icm20608_device, ICM20_PWR_MGMT_1, 0x01);
	mdelay(50);

	value = icm20608_read_one_reg(&icm20608_device, ICM20_WHO_AM_I);
	printk("HTQ_ICM20608 ID = %#X\r\n", value);	

	icm20608_write_one_reg(&icm20608_device, ICM20_SMPLRT_DIV, 0x00); 	/* 输出速率是内部采样率					*/
	icm20608_write_one_reg(&icm20608_device, ICM20_GYRO_CONFIG, 0x18); 	/* 陀螺仪±2000dps量程 				*/
	icm20608_write_one_reg(&icm20608_device, ICM20_ACCEL_CONFIG, 0x18); 	/* 加速度计±16G量程 					*/
	icm20608_write_one_reg(&icm20608_device, ICM20_CONFIG, 0x04); 		/* 陀螺仪低通滤波BW=20Hz 				*/
	icm20608_write_one_reg(&icm20608_device, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz 			*/
	icm20608_write_one_reg(&icm20608_device, ICM20_PWR_MGMT_2, 0x00); 	/* 打开加速度计和陀螺仪所有轴 				*/
	icm20608_write_one_reg(&icm20608_device, ICM20_LP_MODE_CFG, 0x00); 	/* 关闭低功耗 						*/
	icm20608_write_one_reg(&icm20608_device, ICM20_FIFO_EN, 0x00);		/* 关闭FIFO						*/
}

7. 应用测试代码

        应用只需要打开对应的设备节点,进行read、write操作即可得到icm20608的数据。

代码如下(借用原子哥的代码,仅用于测试):

int main(int argc, char *argv[])
{
	int fd;
	char *filename;
	signed int databuf[7];
	unsigned char data[14];
	signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
	signed int accel_x_adc, accel_y_adc, accel_z_adc;
	signed int temp_adc;

	float gyro_x_act, gyro_y_act, gyro_z_act;
	float accel_x_act, accel_y_act, accel_z_act;
	float temp_act;

	int ret = 0;

	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	fd = open(filename, O_RDWR);
	if(fd < 0) {
		printf("can't open file %s\r\n", filename);
		return -1;
	}

	while (1) {
		ret = read(fd, databuf, sizeof(databuf));
		if(ret == 0) { 			/* 数据读取成功 */
			gyro_x_adc = databuf[0];
			gyro_y_adc = databuf[1];
			gyro_z_adc = databuf[2];
			accel_x_adc = databuf[3];
			accel_y_adc = databuf[4];
			accel_z_adc = databuf[5];
			temp_adc = databuf[6];

			/* 计算实际值 */
			gyro_x_act = (float)(gyro_x_adc)  / 16.4;
			gyro_y_act = (float)(gyro_y_adc)  / 16.4;
			gyro_z_act = (float)(gyro_z_adc)  / 16.4;
			accel_x_act = (float)(accel_x_adc) / 2048;
			accel_y_act = (float)(accel_y_adc) / 2048;
			accel_z_act = (float)(accel_z_adc) / 2048;
			temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;


			printf("\r\n原始值:\r\n");
			printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);
			printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);
			printf("temp = %d\r\n", temp_adc);
			printf("实际值:");
			printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
			printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);
			printf("act temp = %.2f°C\r\n", temp_act);
		}
		usleep(100000); /*100ms */
	}
	close(fd);	/* 关闭文件 */	
	return 0;
}

三、总结

        linux系统中有厂家写好的spi相关驱动,在使用中只是使用最下面注册spi设备相关的操作,将spi设备相关的操作注册到上层厂家写好的spi框架中(spi_register_driver(&icm20608_spi_driver)函数注册),在这个结构体中需要提供probe、remove、设备名(对应设备数中名字)等,probe根据设备名用于匹配设备树相关信息(insmod icm.ko)既是这个过程。当在应用中使用open则对应与fops相关操作,与hello world驱动并无大的不同。

环境:服务器ubuntu16,正点原子imx6ull开发板emmc版本。

参考书:Linux设备驱动开发详解(基于最新的Linux4.0内核) 宋宝华著

              Linux设备驱动程序   J & G著  

猜你喜欢

转载自blog.csdn.net/zichuanning520/article/details/124633528