一、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(ClockPolarity):是用来配置SCLK在哪种状态下,电平有效。
CPOL=0:高电平有效,低电平空闲。
CPOL=1:低电平有效,高电平空闲。
时钟相位CPHA(ClockPhase):用于配置数据采样是在第几个边缘,0表示第一个边缘(前沿)Leading edge),1表示第二个边缘(后边)Trailing edge)。
CPHA=0:数据采样在第一边,数据发送在第二边。
CPHA=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 = f_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著