首页
关于
友链
其它
统计
壁纸
Search
1
修改Linux Kernel defconfig的标准方法
4,786 阅读
2
Android系统之VINTF(1)manifests&compatibility matrices
4,455 阅读
3
cgroup--(4)cgroup v1和cgroup v2的详细介绍
4,113 阅读
4
c语言的__attribute__
2,812 阅读
5
Android系统之VINTF(3)Matching Rules
2,220 阅读
默认分类
文章收集
学习总结
每天一刷
环境配置
知识点
入门系列
vim
shell
Git
Make
Android
Linux
Linux命令
内存管理
Linux驱动
Language
C++
C
工具
软件工具
Bug
登录
Search
标签搜索
shell
Linux
c
uboot
Vim
vintf
Linux驱动
Android
device_tree
git
DEBUG
链表
数据结构
IDR
内核
ELF
gcc
网址
内存管理
进程管理
adtxl
累计撰写
332
篇文章
累计收到
8
条评论
首页
栏目
默认分类
文章收集
学习总结
每天一刷
环境配置
知识点
入门系列
vim
shell
Git
Make
Android
Linux
Linux命令
内存管理
Linux驱动
Language
C++
C
工具
软件工具
Bug
页面
关于
友链
其它
统计
壁纸
搜索到
23
篇与
Linux驱动
的结果
2022-10-22
[正点原子]Linux驱动学习笔记--33.Regmap API实验
暂无简介
2022年10月22日
323 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--21.Linux SPI驱动实验
1. SPI&ICM-20608简介1.1 SPI简介同 I2C 一样, SPI 是很常用的通信接口,也可以通过 SPI 来连接众多的传感器。相比 I2C 接口, SPI 接口的通信速度很快, I2C 最多 400KHz,但是 SPI 可以到达几十 MHz。 I.MX6U 也有4 个 SPI 接口,可以通过这 4 个 SPI 接口来连接一些 SPI 外设。 I.MX6U-ALPHA 使用 SPI3 接口连接了一个六轴传感器 ICM-20608,本章我们就来学习如何使用 I.MX6U 的 SPI 接口来驱动ICM-20608,读取 ICM-20608 的六轴数据。上一章我们讲解了 I2C, I2C 是串行通信的一种,只需要两根线就可以完成主机和从机之间的通信,但是 I2C 的速度最高只能到 400KHz,如果对于访问速度要求比价高的话 I2C 就不适合了。本章我们就来学习一下另外一个和 I2C 一样广泛使用的串行通信: SPI, SPI 全称是 Serial Perripheral Interface,也就是串行外围设备接口。 SPI 是 Motorola 公司推出的一种同步串行接口技术,是一种高速、全双工的同步通信总线, SPI 时钟频率相比 I2C 要高很多,最高可以工作在上百 MHz。 SPI 以主从方式工作,通常是有一个主设备和一个或多个从设备,一般 SPI 需要4 根线,但是也可以使用三根线(单向传输),本章我们讲解标准的 4 线 SPI,这四根线如下:①、 CS/SS, Slave Select/Chip Select,这个是片选信号线,用于选择需要进行通信的从设备。I2C 主机是通过发送从机设备地址来选择需要进行通信的从机设备的, SPI 主机不需要发送从机设备,直接将相应的从机设备片选信号拉低即可。②、 SCK, Serial Clock,串行时钟,和 I2C 的 SCL 一样,为 SPI 通信提供时钟。③、 MOSI/SDO, Master Out Slave In/Serial Data Output,简称主出从入信号线,这根数据线只能用于主机向从机发送数据,也就是主机输出,从机输入。④、 MISO/SDI, Master In Slave Out/Serial Data Input,简称主入从出信号线,这根数据线只能用户从机向主机发送数据,也就是主机输入,从机输出。SPI 通信都是由主机发起的,主机需要提供通信的时钟信号。主机通过 SPI 线连接多个从设备的结构如图 27.1.1.1 所示:SPI 有四种工作模式,通过串行时钟极性(CPOL)和相位(CPHA)的搭配来得到四种工作模式:①、 CPOL=0,串行时钟空闲状态为低电平。②、 CPOL=1,串行时钟空闲状态为高电平,此时可以通过配置时钟相位(CPHA)来选择具体的传输协议。③、 CPHA=0,串行时钟的第一个跳变沿(上升沿或下降沿)采集数据。④、 CPHA=1,串行时钟的第二个跳变沿(上升沿或下降沿)采集数据。这四种工作模式如图 27.1.1.2 所示:跟 I2C 一样, SPI 也是有时序图的,以 CPOL=0, CPHA=0 这个工作模式为例, SPI 进行全双工通信的时序如图 27.1.1.3 所示:从图 27.1.1.3 可以看出, SPI 的时序图很简单,不像 I2C 那样还要分为读时序和写时序,因为 SPI 是全双工的,所以读写时序可以一起完成。图 27.1.1.3 中, CS 片选信号先拉低,选中要通信的从设备,然后通过 MOSI 和 MISO 这两根数据线进行收发数据, MOSI 数据线发出了0XD2 这个数据给从设备,同时从设备也通过 MISO 线给主设备返回了 0X66 这个数据。这个就是 SPI 时序图。关于 SPI 就讲解到这里,接下来我们看一下 I.MX6U 自带的 SPI 外设: ECSPI。1.2 I.MX6U ECSPI 简介I.MX6U 自带的 SPI 外设叫做 ECSPI,全称是 Enhanced Configurable Serial Peripheral Interface,别看前面加了个“EC”就以为和标准 SPI 有啥不同的, 其实就是 SPI。 ECSPI 有 64*32 个接收FIFO(RXFIFO)和 64*32 个发送 FIFO(TXFIFO), ECSPI 特性如下:①、全双工同步串行接口。②、可配置的主/从模式。③、四个片选信号,支持多从机。④、发送和接收都有一个 32x64 的 FIFO。⑤、片选信号 SS/CS,时钟信号 SCLK 极性可配置。⑥、支持 DMA。I.MX6U 的 ECSPI 可以工作在主模式或从模式,本章我们使用主模式, I.MX6U 有 4 个ECSPI,每个 ECSPI 支持四个片选信号,也就说,如果你要使用 ECSPI 的硬件片选信号的话,一个 ECSPI 可以支持 4 个外设。如果不使用硬件的片选信号就可以支持无数个外设,本章实验我们不使用硬件片选信号,因为硬件片选信号只能使用指定的片选 IO,软件片选的话可以使用任意的 IO。我们接下来看一下 ECSPI 的几个重要的寄存器,首先看一下 ECSPIx_CONREG(x=1~4)寄存器,这是 ECSPI 的控制寄存器,此寄存器结构如图 27.1.2.1 所示:寄存器 ECSPIx_CONREG 各位含义如下:BURST_LENGTH(bit31:24): 突发长度,设置 SPI 的突发传输数据长度,在一次 SPI 发送中最大可以发送 2^12bit 数据。可以设置 0X000~0XFFF,分别对应 1~2^12bit。我们一般设置突发长度为一个字节,也就是 8bit, BURST_LENGTH=7。CHANNEL_SELECT(bit19:18): SPI 通道选择,一个 ECSPI 有四个硬件片选信号,每个片选信号是一个硬件通道,虽然我们本章实验使用的软件片选,但是 SPI 通道还是要选择的。可设置为 0~3,分别对应通道 0~3。 I.MX6U-ALPHA 开发板上的 ICM-20608 的片选信号接的是ECSPI3_SS0,也就是 ECSPI3 的通道 0,所以本章实验设置为 0。DRCTL(bit17:16): SPI 的 SPI_RDY 信号控制位,用于设置 SPI_RDY 信号,为 0 的话不关心 SPI_RDY 信号;为 1 的话 SPI_RDY 信号为边沿触发;为 2 的话 SPI_DRY 是电平触发。PRE_DIVIDER(bit15:12): SPI 预分频, ECSPI 时钟频率使用两步来完成分频,此位设置的是第一步,可设置 0~15,分别对应 1~16 分频。POST_DIVIDER(bit11:8): SPI 分频值, ECSPI 时钟频率的第二步分频设置,分频值为2^POST_DIVIDER。CHANNEL_MODE(bit7:4): SPI 通道主/从模式设置, CHANNEL_MODE[3:0]分别对应 SPI通道 3~0, 为 0 的话就是设置为从模式,如果为 1 的话就是主模式。比如设置为 0X01 的话就是设置通道 0 为主模式。SMC(bit3):开始模式控制,此位只能在主模式下起作用,为 0 的话通过 XCH 位来开启 SPI突发访问,为 1 的话只要向 TXFIFO 写入数据就开启 SPI 突发访问。XCH(bit2): 此位只在主模式下起作用,当 SMC 为 0 的话此位用来控制 SPI 突发访问的开启。HT(bit1): HT 模式使能位, I.MX6ULL 不支持。EN(bit0): SPI 使能位,为 0 的话关闭 SPI,为 1 的话使能 SPI。接下来看一下寄存器 ECSPIx_CONFIGREG,这个也是 ECSPI 的配置寄存器,此寄存器结构如图 27.1.2.2 所示:寄存器 ECSPIx_CONFIGREG 用到的重要位如下:HT_LENGTH(bit28:24): HT 模式下的消息长度设置, I.MX6ULL 不支持。SCLK_CTL(bit23:20):设置 SCLK 信号线空闲状态电平, SCLK_CTL[3:0]分别对应通道3~0,为 0 的话 SCLK 空闲状态为低电平,为 1 的话 SCLK 空闲状态为高电平。DATA_CTL(bit19:16):设置 DATA 信号线空闲状态电平, DATA_CTL[3:0]分别对应通道3~0,为 0 的话 DATA 空闲状态为高电平,为 1 的话 DATA 空闲状态为低电平。SS_POL(bit15:12): 设置 SPI 片选信号极性设置, SS_POL[3:0]分别对应通道 3~0,为 0 的话片选信号低电平有效,为 1 的话片选信号高电平有效。SCLK_POL(bit7:4): SPI 时钟信号极性设置,也就是 CPOL, SCLK_POL[3:0]分别对应通道 3~0,为 0 的话 SCLK 高电平有效(空闲的时候为低电平),为 1 的话 SCLK 低电平有效(空闲的时候为高电平)。SCLK_PHA(bit3:0): SPI时钟相位设置,也就是CPHA, SCLK_PHA[3:0]分别对应通道3~0,为 0 的话串行时钟的第一个跳变沿(上升沿或下降沿)采集数据,为 1 的话串行时钟的第二个跳变沿(上升沿或下降沿)采集数据。通过 SCLK_POL 和 SCLK_PHA 可以设置 SPI 的工作模式。接下来看一下寄存器 ECSPIx_PERIODREG,这个是 ECSPI 的采样周期寄存器,此寄存器结构如图 27.1.2.3 所示:寄存器 ECSPIx_PERIODREG 用到的重要位如下:CSD_CTL(bit21:16): 片选信号延时控制位,用于设置片选信号和第一个 SPI 时钟信号之间的时间间隔,范围为 0~63。CSRC(bit15): SPI 时钟源选择,为 0 的话选择 SPI CLK 为 SPI 的时钟源,为 1 的话选择32.768KHz 的晶振为 SPI 时钟源。我们一般选择 SPI CLK 作为 SPI 时钟源, SPI CLK 时钟来源如图 27.1.2.4 所示:图 27.1.2.4 中各部分含义如下:①、这是一个选择器,用于选择根时钟源,由寄存器 CSCDR2 的位 ECSPI_CLK_SEL 来控制,为 0 的话选择 pll3_60m 作为 ECSPI 根时钟源。为 1 的话选择 osc_clk 作为 ECSPI 时钟源。本章我们选择 pll3_60m 作为 ECSPI 根时钟源。②、 ECSPI 时钟分频值,由寄存器 CSCDR2 的位 ECSPI_CLK_PODF 来控制,分频值为2^ECSPI_CLK_PODF。本章我们设置为 0,也就是 1 分频。③、最终进入 ECSPI 的时钟,也就是 SPI CLK=60MHz。SAMPLE_PERIO: 采样周期寄存器,可设置为 0~0X7FFF 分别对应 0~32767 个周期。接下来看一下寄存器 ECSPIx_STATREG,这个是 ECSPI 的状态寄存器,此寄存器结构如图27.1.2.5 所示:寄存器 ECSPIx_STATREG 用到的重要位如下:TC(bit7):传输完成标志位,为 0 表示正在传输,为 1 表示传输完成。RO(bit6): RXFIFO 溢出标志位,为 0 表示 RXFIFO 无溢出,为 1 表示 RXFIFO 溢出。RF(bit5): RXFIFO 空标志位,为 0 表示 RXFIFO 不为空,为 1 表示 RXFIFO 为空。RDR(bit4): RXFIFO 数据请求标志位,此位为 0 表示 RXFIFO 里面的数据不大于RX_THRESHOLD,此位为 1 的话表示 RXFIFO 里面的数据大于 RX_THRESHOLD。RR(bit3): RXFIFO 就绪标志位,为 0 的话 RXFIFO 没有数据,为 1 的话表示 RXFIFO 中至少有一个字的数据。TF(bit2): TXFIFO 满标志位,为 0 的话表示 TXFIFO 不为满,为 1 的话表示 TXFIFO 为满。TDR(bit1): TXFIFO 数据请求标志位,为 0 表示 TXFIFO 中的数据大于 TX_THRESHOLD,为 1 表示 TXFIFO 中的数据不大于 TX_THRESHOLD。TE(bit0): TXFIFO 空标志位,为 0 表示 TXFIFO 中至少有一个字的数据,为 1 表示 TXFIFO为空。最后就是两个数据寄存器, ECSPIx_TXDATA 和 ECSPIx_RXDATA,这两个寄存器都是 32位的,如果要发送数据就向寄存器 ECSPIx_TXDATA 写入数据,读取及存取 ECSPIx_RXDATA里面的数据就可以得到刚刚接收到的数据。1.3 ICM-20608 简介ICM-20608 是 InvenSense 出品的一款 6 轴 MEMS 传感器,包括 3 轴加速度和 3 轴陀螺仪。ICM-20608 尺寸非常小,只有 3x3x0.75mm,采用 16P 的 LGA 封装。 ICM-20608 内部有一个 512字节的 FIFO。陀螺仪的量程范围可以编程设置,可选择± 250,±500,±1000 和±2000° /s,加速度的量程范围也可以编程设置,可选择± 2g,±4g, ±8g 和±16g。陀螺仪和加速度计都是 16 位的 ADC,并且支持 I2C 和 SPI 两种协议,使用 I2C 接口的话通信速度最高可以达到400KHz,使用 SPI 接口的话通信速度最高可达到 8MHz。 I.MX6U-ALPHA 开发板上的 ICM-20608 通过 SPI 接口和 I.MX6U 连接在一起。 ICM-20608 特性如下:①、陀螺仪支持 X,Y 和 Z 三轴输出,内部集成 16 位 ADC,测量范围可设置:±250,±500,±1000 和±2000° /s。②、加速度计支持 X,Y 和 Z 轴输出,内部集成 16 位 ADC,测量范围可设置:±2g,±4g,±4g,±8g 和±16g。③、用户可编程中断。④、内部包含 512 字节的 FIFO。⑤、内部包含一个数字温度传感器。⑥、耐 10000g 的冲击。⑦、支持快速 I2C,速度可达 400KHz。⑧、支持 SPI,速度可达 8MHz。ICM-20608 的 3 轴方向如图 27.1.3.1 所示:ICM-20608 的结构框图如图 27.1.3.2 所示:如果使用 IIC 接口的话 ICM-20608 的 AD0 引脚决定 I2C 设备从地址的最后一位,如果 AD0为 0 的话 ICM-20608 从设备地址是 0X68,如果 AD0 为 1 的话 ICM-20608 从设备地址为 0X69。本章我们使用 SPI 接口,跟上一章使用 AP3216C 一样, ICM-20608 也是通过读写寄存器来配置和读取传感器数据,使用 SPI 接口读写寄存器需要 16 个时钟或者更多(如果读写操作包括多个字节的话),第一个字节包含要读写的寄存器地址,寄存器地址最高位是读写标志位,如果是读的话寄存器地址最高位要为 1,如果是写的话寄存器地址最高位要为 0,剩下的 7 位才是实际的寄存器地址,寄存器地址后面跟着的就是读写的数据。表 27.1.3.1 列出了本章实验用到的一些寄存器和位,关于 ICM-20608 的详细寄存器和位的介绍请参考 ICM-20608 的寄存器手册:续:ICM-20608 是在 I.MX6U-ALPHA 开发板底板上,原理图如图 27.2.1 所示:2. Linux 下 SPI 驱动框架简介SPI 驱动框架和 I2C 很类似,都分为主机控制器驱动和设备驱动,主机控制器也就是 SOC的 SPI 控制器接口。比如在裸机篇中的《第二十七章 SPI 实验》,我们编写了 bsp_spi.c 和 bsp_spi.h这两个文件,这两个文件是 I.MX6U 的 SPI 控制器驱动,我们编写好 SPI 控制器驱动以后就可以直接使用了,不管是什么 SPI 设备, SPI 控制器部分的驱动都是一样,我们的重点就落在了种类繁多的 SPI 设备驱动。2.1 SPI 主机驱动SPI 主机驱动就是 SOC 的 SPI 控制器驱动,类似 I2C 驱动里面的适配器驱动。 Linux 内核使用 spi_master 表示 SPI 主机驱动, spi_master 是个结构体,定义在 include/linux/spi/spi.h 文件中,内容如下(有缩减):// 示例代码 62.1.1.1 spi_master 结构体 315 struct spi_master { 316 struct device dev; 317 318 struct list_head list; ...... 326 s16 bus_num; 327 328 /* chipselects will be integral to many controllers; some others 329 * might use board-specific GPIOs. 330 */ 331 u16 num_chipselect; 332 333 /* some SPI controllers pose alignment requirements on DMAable 334 * buffers; let protocol drivers know about these requirements. 335 */ 336 u16 dma_alignment; 337 338 /* spi_device.mode flags understood by this controller driver */ 339 u16 mode_bits; 340 341 /* bitmask of supported bits_per_word for transfers */ 342 u32 bits_per_word_mask; ...... 347 /* limits on transfer speed */ 348 u32 min_speed_hz; 349 u32 max_speed_hz; 350 351 /* other constraints relevant to this driver */ 352 u16 flags; ...... 359 /* lock and mutex for SPI bus locking */ 360 spinlock_t bus_lock_spinlock; 361 struct mutex bus_lock_mutex; 362 363 /* flag indicating that the SPI bus is locked for exclusive use */ 364 bool bus_lock_flag; ...... 372 int (*setup)(struct spi_device *spi); 373 ...... 393 int (*transfer)(struct spi_device *spi, 394 struct spi_message *mesg); ...... 434 int (*transfer_one_message)(struct spi_master *master, 435 struct spi_message *mesg); ...... 462 };第 393 行, transfer 函数,和 i2c_algorithm 中的 master_xfer 函数一样,控制器数据传输函数。第 434 行, transfer_one_message 函数,也用于 SPI 数据发送,用于发送一个 spi_message,SPI 的数据会打包成 spi_message,然后以队列方式发送出去。也就是 SPI 主机端最终会通过 transfer 函数与 SPI 设备进行通信,因此对于 SPI 主机控制器的驱动编写者而言 transfer 函数是需要实现的,因为不同的 SOC 其 SPI 控制器不同,寄存器都不一样。和 I2C 适配器驱动一样, SPI 主机驱动一般都是 SOC 厂商去编写的,所以我们作为 SOC 的使用者,这一部分的驱动就不用操心了,除非你是在 SOC 原厂工作,内容就是写 SPI 主机驱动。SPI 主机驱动的核心就是申请 spi_master,然后初始化 spi_master,最后向 Linux 内核注册spi_master。1、 spi_master 申请与释放spi_alloc_master 函数用于申请 spi_master,函数原型如下:struct spi_master *spi_alloc_master(struct device *dev, unsigned size)函数参数和返回值含义如下:dev:设备,一般是 platform_device 中的 dev 成员变量。size: 私有数据大小,可以通过 spi_master_get_devdata 函数获取到这些私有数据。返回值: 申请到的 spi_master。spi_master 的释放通过 spi_master_put 函数来完成,当我们删除一个 SPI 主机驱动的时候就需要释放掉前面申请的 spi_master, spi_master_put 函数原型如下:void spi_master_put(struct spi_master *master)函数参数和返回值含义如下:master:要释放的 spi_master。返回值: 无。2、 spi_master 的注册与注销当 spi_master 初始化完成以后就需要将其注册到 Linux 内核, spi_master 注册函数为spi_register_master,函数原型如下:int spi_register_master(struct spi_master *master)函数参数和返回值含义如下:master:要注册的 spi_master。返回值: 0,成功;负值,失败。I.MX6U 的 SPI 主机驱动会采用 spi_bitbang_start 这个 API 函数来完成 spi_master 的注册,spi_bitbang_start 函数内部其实也是通过调用 spi_register_master 函数来完成 spi_master 的注册。如果要注销 spi_master 的话可以使用 spi_unregister_master 函数,此函数原型为:void spi_unregister_master(struct spi_master *master)函数参数和返回值含义如下:master:要注销的 spi_master。返回值: 无。如果使用 spi_bitbang_start 注册 spi_master 的话就要使用 spi_bitbang_stop 来注销掉spi_master。2.2 SPI 设备驱动spi 设备驱动和 i2c 设备驱动也很类似, Linux 内核使用 spi_driver 结构体来表示 spi 设备驱动,我们在编写 SPI 设备驱动的时候需要实 现 spi_driver 。 spi_driver 结构体定义在include/linux/spi/spi.h 文件中,结构体内容如下:// 示例代码 62.1.1.2 spi_driver 结构体 180 struct spi_driver { 181 const struct spi_device_id *id_table; 182 int (*probe)(struct spi_device *spi); 183 int (*remove)(struct spi_device *spi); 184 void (*shutdown)(struct spi_device *spi); 185 struct device_driver driver; 186 };可以看出, spi_driver 和 i2c_driver、 platform_driver 基本一样,当 SPI 设备和驱动匹配成功以后 probe 函数就会执行。同样的, spi_driver 初始化完成以后需要向 Linux 内核注册, spi_driver 注册函数为spi_register_driver,函数原型如下:int spi_register_driver(struct spi_driver *sdrv)函数参数和返回值含义如下:sdrv: 要注册的 spi_driver。返回值: 0,注册成功;赋值,注册失败。注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver,使用 spi_unregister_driver 函数完成 spi_driver 的注销,函数原型如下:void spi_unregister_driver(struct spi_driver *sdrv)函数参数和返回值含义如下:sdrv: 要注销的 spi_driver。返回值: 无。spi_driver 注册示例程序如下:// 示例代码 62.1.1.3 spi_driver 注册示例程序 1 /* probe 函数 */ 2 static int xxx_probe(struct spi_device *spi) 3 { 4 /* 具体函数内容 */ 5 return 0; 6 } 7 8 /* remove 函数 */ 9 static int xxx_remove(struct spi_device *spi) 10 { 11 /* 具体函数内容 */ 12 return 0; 13 } 14 /* 传统匹配方式 ID 列表 */ 15 static const struct spi_device_id xxx_id[] = { 16 {"xxx", 0}, 17 {} 18 }; 19 20 /* 设备树匹配列表 */ 21 static const struct of_device_id xxx_of_match[] = { 22 { .compatible = "xxx" }, 23 { /* Sentinel */ } 24 }; 25 26 /* SPI 驱动结构体 */ 27 static struct spi_driver xxx_driver = { 28 .probe = xxx_probe, 29 .remove = xxx_remove, 30 .driver = { 31 .owner = THIS_MODULE, 32 .name = "xxx", 33 .of_match_table = xxx_of_match, 34 }, 35 .id_table = xxx_id, 36 }; 37 38 /* 驱动入口函数 */ 39 static int __init xxx_init(void) 40 { 41 return spi_register_driver(&xxx_driver); 42 } 43 44 /* 驱动出口函数 */ 45 static void __exit xxx_exit(void) 46 { 47 spi_unregister_driver(&xxx_driver); 48 } 49 50 module_init(xxx_init); 51 module_exit(xxx_exit);第 1~36 行, spi_driver 结构体,需要 SPI 设备驱动人员编写,包括匹配表、 probe 函数等。和 i2c_driver、 platform_driver 一样,就不详细讲解了。第 39~42 行,在驱动入口函数中调用 spi_register_driver 来注册 spi_driver。第 45~48 行,在驱动出口函数中调用 spi_unregister_driver 来注销 spi_driver。2.3 SPI 设备和驱动匹配过程SPI 设备和驱动的匹配过程是由 SPI 总线来完成的,这点和 platform、 I2C 等驱动一样, SPI总线为 spi_bus_type,定义在 drivers/spi/spi.c 文件中,内容如下:示例代码 62.1.3.1 spi_bus_type 结构体 131 struct bus_type spi_bus_type = { 132 .name = "spi", 133 .dev_groups = spi_dev_groups, 134 .match = spi_match_device, 135 .uevent = spi_uevent, 136 };可以看出, SPI 设备和驱动的匹配函数为 spi_match_device,函数内容如下:// 示例代码 62.1.3.2 spi_match_device 函数 99 static int spi_match_device(struct device *dev, struct device_driver *drv) 100 { 101 const struct spi_device *spi = to_spi_device(dev); 102 const struct spi_driver *sdrv = to_spi_driver(drv); 103 104 /* Attempt an OF style match */ 105 if (of_driver_match_device(dev, drv)) 106 return 1; 107 108 /* Then try ACPI */ 109 if (acpi_driver_match_device(dev, drv)) 110 return 1; 111 112 if (sdrv->id_table) 113 return !!spi_match_id(sdrv->id_table, spi); 114 115 return strcmp(spi->modalias, drv->name) == 0; 116 }spi_match_device 函数和 i2c_match_device 函数对于设备和驱动的匹配过程基本一样。第 105 行, of_driver_match_device 函数用于完成设备树设备和驱动匹配。比较 SPI 设备节点的 compatible 属性和 of_device_id 中的 compatible 属性是否相等,如果相当的话就表示 SPI 设备和驱动匹配。第 109 行, acpi_driver_match_device 函数用于 ACPI 形式的匹配。第 113 行, spi_match_id 函数用于传统的、无设备树的 SPI 设备和驱动匹配过程。比较 SPI设备名字和 spi_device_id 的 name 字段是否相等,相等的话就说明 SPI 设备和驱动匹配。第 115 行,比较 spi_device 中 modalias 成员变量和 device_driver 中的 name 成员变量是否相等。3. I.MX6U SPI 主机驱动分析和 I2C 的适配器驱动一样, SPI 主机驱动一般都由 SOC 厂商编写好了,打开 imx6ull.dtsi文件,找到如下所示内容:// 示例代码 62.2.1 imx6ull.dtsi 文件中的 ecspi3 节点内容 1 ecspi3: ecspi@02010000 { 2 #address-cells = <1>; 3 #size-cells = <0>; 4 compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi"; 5 reg = <0x02010000 0x4000>; 6 interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>; 7 clocks = <&clks IMX6UL_CLK_ECSPI3>, 8 <&clks IMX6UL_CLK_ECSPI3>; 9 clock-names = "ipg", "per"; 10 dmas = <&sdma 7 7 1>, <&sdma 8 7 2>; 11 dma-names = "rx", "tx"; 12 status = "disabled"; 13 };重点来看一下第 4 行的 compatible 属性值, compatible 属性有两个值“fsl,imx6ul-ecspi”和“fsl,imx51-ecspi”,在 Linux 内核源码中搜素这两个属性值即可找到 I.MX6U 对应的 ECSPI(SPI)主机驱动。 I.MX6U 的 ECSPI 主机驱动文件为 drivers/spi/spi-imx.c,在此文件中找到如下内容:// 示例代码 62.2.2 spi_imx_driver 结构体 694 static struct platform_device_id spi_imx_devtype[] = { 695 { 696 .name = "imx1-cspi", 697 .driver_data = (kernel_ulong_t) &imx1_cspi_devtype_data, 698 }, { 699 .name = "imx21-cspi", 700 .driver_data = (kernel_ulong_t) &imx21_cspi_devtype_data, ...... 713 }, { 714 .name = "imx6ul-ecspi", 715 .driver_data = (kernel_ulong_t) &imx6ul_ecspi_devtype_data, 716 }, { 717 /* sentinel */ 718 } 719 }; 720 721 static const struct of_device_id spi_imx_dt_ids[] = { 722 { .compatible = "fsl,imx1-cspi", .data = &imx1_cspi_devtype_data, }, ...... 728 { .compatible = "fsl,imx6ul-ecspi", .data = &imx6ul_ecspi_devtype_data, }, 729 { /* sentinel */ } 730 }; 731 MODULE_DEVICE_TABLE(of, spi_imx_dt_ids); ...... 1338 static struct platform_driver spi_imx_driver = { 1339 .driver = { 1340 .name = DRIVER_NAME, 1341 .of_match_table = spi_imx_dt_ids, 1342 .pm = IMX_SPI_PM, 1343 }, 1344 .id_table = spi_imx_devtype, 1345 .probe = spi_imx_probe, 1346 .remove = spi_imx_remove, 1347 }; 1348 module_platform_driver(spi_imx_driver);第 714 行, spi_imx_devtype 为 SPI 无设备树匹配表。第 721 行, spi_imx_dt_ids 为 SPI 设备树匹配表。第 728 行,“fsl,imx6ul-ecspi”匹配项,因此可知 I.MX6U 的 ECSPI 驱动就是 spi-imx.c 这个文件。第 1338~1347 行, platform_driver 驱动框架,和 I2C 的适配器驱动一样, SPI 主机驱动器采用了 platfom 驱动框架。当设备和驱动匹配成功以后 spi_imx_probe 函数就会执行。spi_imx_probe 函数会从设备树中读取相应的节点属性值,申请并初始化 spi_master,最后调用 spi_bitbang_start 函数(spi_bitbang_start 会调用 spi_register_master 函数)向 Linux 内核注册spi_master。对于 I.MX6U 来讲, SPI 主机的最终数据收发函数为 spi_imx_transfer,此函数通过如下层层调用最终实现 SPI 数据发送:spi_imx_transfer-> spi_imx_pio_transfer-> spi_imx_push-> spi_imx->txspi_imx 是个 spi_imx_data 类型的机构指针变量,其中 tx 和 rx 这两个成员变量分别为 SPI数据发送和接收函数。 I.MX6U SPI 主机驱动会维护一个 spi_imx_data 类型的变量 spi_imx,并且使用 spi_imx_setupxfer 函数来设置 spi_imx 的 tx 和 rx 函数。根据要发送的数据数据位宽的不同,分别有 8 位、 16 位和 32 位的发送函数,如下所示:spi_imx_buf_tx_u8 spi_imx_buf_tx_u16 spi_imx_buf_tx_u32同理,也有 8 位、 16 位和 32 位的数据接收函数,如下所示:spi_imx_buf_rx_u8 spi_imx_buf_rx_u16 spi_imx_buf_rx_u32我们就以 spi_imx_buf_tx_u8 这个函数为例,看看,一个自己的数据发送是怎么完成的,在spi-imx.c 文件中找到如下所示内容:// 示例代码 62.2.3 spi_imx_buf_tx_u8 函数 152 #define MXC_SPI_BUF_TX(type) \ 153 static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx) \ 154 { \ 155 type val = 0; \ 156 \ 157 if (spi_imx->tx_buf) { \ 158 val = *(type *)spi_imx->tx_buf; \ 159 spi_imx->tx_buf += sizeof(type); \ 160 } \ 161 \ 162 spi_imx->count -= sizeof(type); \ 163 \ 164 writel(val, spi_imx->base + MXC_CSPITXDATA); \ 165 } 166 167 MXC_SPI_BUF_RX(u8) 168 MXC_SPI_BUF_TX(u8)从示例代码 62.2.3 可以看出, spi_imx_buf_tx_u8 函数是通过 MXC_SPI_BUF_TX 宏来实现的。第 164 行就是将要发送的数据值写入到 ECSPI 的 TXDATA 寄存器里面去,这和我们 SPI 裸机实验的方法一样。将第 168 行的 MXC_SPI_BUF_TX(u8)展开就是 spi_imx_buf_tx_u8 函数。其他的 tx 和 rx 函数都是这样实现的,这里就不做介绍了。关于 I.MX6U 的主机驱动程序就讲解到这里,基本套路和 I2C 的适配器驱动程序类似。4. SPI 设备驱动编写流程4.1 SPI 设备信息描述1、 IO 的 pinctrl 子节点创建与修改首先肯定是根据所使用的 IO 来创建或修改 pinctrl 子节点,这个没什么好说的,唯独要注意的就是检查相应的 IO 有没有被其他的设备所使用,如果有的话需要将其删除掉!2、 SPI 设备节点的创建与修改采用设备树的情况下, SPI 设备信息描述就通过创建相应的设备子节点来完成,我们可以打开 imx6qdl-sabresd.dtsi 这个设备树头文件,在此文件里面找到如下所示内容:示例代码 62.3.1.1 m25p80 设备节点 308 &ecspi1 { 309 fsl,spi-num-chipselects = <1>; 310 cs-gpios = <&gpio4 9 0>; 311 pinctrl-names = "default"; 312 pinctrl-0 = <&pinctrl_ecspi1>; 313 status = "okay"; 314 315 flash: m25p80@0 { 316 #address-cells = <1>; 317 #size-cells = <1>; 318 compatible = "st,m25p32"; 319 spi-max-frequency = <20000000>; 320 reg = <0>; 321 }; 322 };示例代码 62.3.1.1 是 I.MX6Q 的一款板子上的一个 SPI 设备节点,在这个板子的 ECSPI 接口上接了一个 m25p80,这是一个 SPI 接口的设备。第 309 行,设置“fsl,spi-num-chipselects”属性为 1,表示只有一个设备。第 310 行,设置“cs-gpios”属性,也就是片选信号为 GPIO4_IO09。第 311 行,设置“pinctrl-names”属性,也就是 SPI 设备所使用的 IO 名字。第 312 行,设置“pinctrl-0”属性,也就是所使用的 IO 对应的 pinctrl 节点。第 313 行,将 ecspi1 节点的“status”属性改为“okay”。第 315~320 行, ecspi1 下的 m25p80 设备信息,每一个 SPI 设备都采用一个子节点来描述其设备信息。第 315 行的“m25p80@0”后面的“0”表示 m25p80 的接到了 ECSPI 的通道 0上。这个要根据自己的具体硬件来设置。第 318 行, SPI 设备的 compatible 属性值,用于匹配设备驱动。第 319 行,“spi-max-frequency”属性设置 SPI 控制器的最高频率,这个要根据所使用的SPI 设备来设置,比如在这里将 SPI 控制器最高频率设置为 20MHz。第 320 行, reg 属性设置 m25p80 这个设备所使用的 ECSPI 通道,和“m25p80@0”后面的“0”一样。我们一会在编写 ICM20608 的设备树节点信息的时候就参考示例代码 62.3.1.1 中的内容即可。62.3.2 SPI 设备数据收发处理流程SPI 设备驱动的核心是 spi_driver,这个我们已经在 62.1.2 小节讲过了。当我们向 Linux 内核注册成功 spi_driver 以后就可以使用 SPI 核心层提供的 API 函数来对设备进行读写操作了。首先是 spi_transfer 结构体,此结构体用于描述 SPI 传输信息,结构体内容如下:示例代码 62.3.2.1 spi_transfer 结构体 603 struct spi_transfer { 604 /* it's ok if tx_buf == rx_buf (right?) 605 * for MicroWire, one buffer must be null 606 * buffers must work with dma_*map_single() calls, unless 607 * spi_message.is_dma_mapped reports a pre-existing mapping 608 */ 609 const void *tx_buf; 610 void *rx_buf; 611 unsigned len; 612 613 dma_addr_t tx_dma; 614 dma_addr_t rx_dma; 615 struct sg_table tx_sg; 616 struct sg_table rx_sg; 617 618 unsigned cs_change:1; 619 unsigned tx_nbits:3; 620 unsigned rx_nbits:3; 621 #define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */ 622 #define SPI_NBITS_DUAL 0x02 /* 2bits transfer */ 623 #define SPI_NBITS_QUAD 0x04 /* 4bits transfer */ 624 u8 bits_per_word; 625 u16 delay_usecs; 626 u32 speed_hz; 627 628 struct list_head transfer_list; 629 };第 609 行, tx_buf 保存着要发送的数据。第 610 行, rx_buf 用于保存接收到的数据。第 611 行, len 是要进行传输的数据长度, SPI 是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以 spi_transfer 中也就没有发送长度和接收长度之分。spi_transfer 需要组织成 spi_message, spi_message 也是一个结构体,内容如下:示例代码 62.3.2.2 spi_message 结构体 660 struct spi_message { 661 struct list_head transfers; 662 663 struct spi_device *spi; 664 665 unsigned is_dma_mapped:1; ...... 678 /* completion is reported through a callback */ 679 void (*complete)(void *context); 680 void *context; 681 unsigned frame_length; 682 unsigned actual_length; 683 int status; 684 685 /* for optional use by whatever driver currently owns the 686 * spi_message ... between calls to spi_async and then later 687 * complete(), that's the spi_master controller driver. 688 */ 689 struct list_head queue; 690 void *state; 691 };在使用spi_message之前需要对其进行初始化, spi_message初始化函数为spi_message_init,函数原型如下:void spi_message_init(struct spi_message *m)函数参数和返回值含义如下:m: 要初始化的 spi_message。返回值: 无。spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中,这里我们要用到 spi_message_add_tail 函数,此函数原型如下:void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)函数参数和返回值含义如下:t: 要添加到队列中的 spi_transfer。m: spi_transfer 要加入的 spi_message。返回值: 无。spi_message 准备好以后就可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync,函数原型如下:int spi_sync(struct spi_device *spi, struct spi_message *message)函数参数和返回值含义如下:spi: 要进行数据传输的 spi_device。message:要传输的 spi_message。返回值: 无。异步传输不会阻塞的等到 SPI 数据传输完成,异步传输需要设置 spi_message 中的 complete成员变量, complete 是一个回调函数,当 SPI 异步传输完成以后此函数就会被调用。 SPI 异步传输函数为 spi_async,函数原型如下:int spi_async(struct spi_device *spi, struct spi_message *message)函数参数和返回值含义如下:spi: 要进行数据传输的 spi_device。message:要传输的 spi_message。返回值: 无。在本章实验中,我们采用同步传输方式来完成 SPI 数据的传输工作,也就是 spi_sync 函数。综上所述, SPI 数据传输步骤如下:①、申请并初始化 spi_transfer,设置 spi_transfer 的 tx_buf 成员变量, tx_buf 为要发送的数据。然后设置 rx_buf 成员变量, rx_buf 保存着接收到的数据。最后设置 len 成员变量,也就是要进行数据通信的长度。②、使用 spi_message_init 函数初始化 spi_message。③、使用spi_message_add_tail函数将前面设置好的spi_transfer添加到spi_message 队列中。④、使用 spi_sync 函数完成 SPI 数据同步传输。通过 SPI 进行 n 个字节的数据发送和接收的示例代码如下所示:// 示例代码 62.3.2.3 SPI 数据读写操作 /* SPI 多字节发送 */ static int spi_send(struct spi_device *spi, u8 *buf, int len) { int ret; struct spi_message m; struct spi_transfer t = { .tx_buf = buf, .len = len, }; spi_message_init(&m); /* 初始化 spi_message */ spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */ ret = spi_sync(spi, &m); /* 同步传输 */ return ret; } /* SPI 多字节接收 */ static int spi_receive(struct spi_device *spi, u8 *buf, int len) { int ret; struct spi_message m; struct spi_transfer t = { .rx_buf = buf, .len = len, }; spi_message_init(&m); /* 初始化 spi_message */ spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */ ret = spi_sync(spi, &m); /* 同步传输 */ return ret; }
2022年10月22日
287 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--22.串口实验
串口是很常用的一个外设,在 Linux 下通常通过串口和其他设备或传感器进行通信,根据电平的不同,串口分为 TTL 和 RS232。不管是什么样的接口电平,其驱动程序都是一样的,通过外接 RS485 这样的芯片就可以将串口转换为 RS485 信号,正点原子的 I.MX6U-ALPHA 开发板就是这么做的。对于正点原子的 I.MX6U-ALPHA 开发板而言, RS232、 RS485 以及 GPS 模块接口通通连接到了 I.MX6U 的 UART3 接口上,因此这些外设最终都归结为 UART3 的串口驱动。本章我们就来学习一下如何驱动 I.MX6U-ALPHA 开发板上的 UART3 串口,进而实现 RS232、RS485 以及 GPS 驱动。1. Linux 下 UART 驱动框架1.1 uart_driver 注册与注销同 I2C、 SPI 一样, Linux 也提供了串口驱动框架,我们只需要按照相应的串口框架编写驱动程序即可。串口驱动没有什么主机端和设备端之分,就只有一个串口驱动,而且这个驱动也已经由 NXP 官方已经编写好了,我们真正要做的就是在设备树中添加所要使用的串口节点信息。当系统启动以后串口驱动和设备匹配成功,相应的串口就会被驱动起来,生成/dev/ttymxcX(X=0….n)文件。虽然串口驱动不需要我们去写,但是串口驱动框架我们还是需要了解的, uart_driver 结构体表示 UART 驱动, uart_driver 定义在 include/linux/serial_core.h 文件中,内容如下:// 示例代码 63.1.1 uart_driver 结构体 295 struct uart_driver { 296 struct module *owner; /* 模块所属者 */ 297 const char *driver_name; /* 驱动名字 */ 298 const char *dev_name; /* 设备名字 */ 299 int major; /* 主设备号 */ 300 int minor; /* 次设备号 */ 301 int nr; /* 设备数 */ 302 struct console *cons; /* 控制台 */ 303 304 /* 305 * these are private; the low level driver should not 306 * touch these; they should be initialised to NULL 307 */ 308 struct uart_state *state; 309 struct tty_driver *tty_driver; 310 };每个串口驱动都需要定义一个 uart_driver,加载驱动的时候通过 uart_register_driver 函数向系统注册这个 uart_driver,此函数原型如下:int uart_register_driver(struct uart_driver *drv)函数参数和返回值含义如下:drv: 要注册的 uart_driver。返回值: 0,成功;负值,失败。注销驱动的时候也需要注销掉前面注册的 uart_driver,需要用到 uart_unregister_driver 函数,函数原型如下:void uart_unregister_driver(struct uart_driver *drv)函数参数和返回值含义如下:drv: 要注销的 uart_driver。返回值: 无。1.2 uart_port 的添加与移除uart_port 表示一个具体的 port, uart_port 定义在 include/linux/serial_core.h 文件,内容如下(有省略):// 示例代码 63.1.2 uart_port 结构体 117 struct uart_port { 118 spinlock_t lock; /* port lock */ 119 unsigned long iobase; /* in/out[bwl] */ 120 unsigned char __iomem *membase; /* read/write[bwl] */ ...... 235 const struct uart_ops *ops; 236 unsigned int custom_divisor; 237 unsigned int line; /* port index */ 238 unsigned int minor; 239 resource_size_t mapbase; /* for ioremap */ 240 resource_size_t mapsize; 241 struct device *dev; /* parent device */ ...... 250 };uart_port 中最主要的就是第 235 行的 ops, ops 包含了串口的具体驱动函数,这个我们稍后再看。每个 UART 都有一个 uart_port,那么 uart_port 是怎么和 uart_driver 结合起来的呢?这里要用到 uart_add_one_port 函数,函数原型如下:int uart_add_one_port(struct uart_driver *drv,struct uart_port *uport)函数参数和返回值含义如下:drv:此 port 对应的 uart_driver。uport: 要添加到 uart_driver 中的 port。返回值: 0,成功;负值,失败。卸载 UART 驱动的时候也需要将 uart_port 从相应的 uart_driver 中移除,需要用到uart_remove_one_port 函数,函数原型如下:int uart_remove_one_port(struct uart_driver *drv, struct uart_port *uport)函数参数和返回值含义如下:drv:要卸载的 port 所对应的 uart_driver。uport: 要卸载的 uart_port。返回值: 0,成功;负值,失败。3. uart_ops 实现在上面讲解 uart_port 的时候说过, uart_port 中的 ops 成员变量很重要,因为 ops 包含了针对 UART 具体的驱动函数, Linux 系统收发数据最终调用的都是 ops 中的函数。 ops 是 uart_ops类型的结构体指针变量, uart_ops 定义在 include/linux/serial_core.h 文件中,内容如下:// 示例代码 63.1.3 uart_ops 结构体 49 struct uart_ops { 50 unsigned int (*tx_empty)(struct uart_port *); 51 void (*set_mctrl)(struct uart_port *, unsigned int mctrl); 52 unsigned int (*get_mctrl)(struct uart_port *); 53 void (*stop_tx)(struct uart_port *); 54 void (*start_tx)(struct uart_port *); 55 void (*throttle)(struct uart_port *); 56 void (*unthrottle)(struct uart_port *); 57 void (*send_xchar)(struct uart_port *, char ch); 58 void (*stop_rx)(struct uart_port *); 59 void (*enable_ms)(struct uart_port *); 60 void (*break_ctl)(struct uart_port *, int ctl); 61 int (*startup)(struct uart_port *); 62 void (*shutdown)(struct uart_port *); 63 void (*flush_buffer)(struct uart_port *); 64 void (*set_termios)(struct uart_port *, struct ktermios *new, 65 struct ktermios *old); 66 void (*set_ldisc)(struct uart_port *, struct ktermios *); 67 void (*pm)(struct uart_port *, unsigned int state, 68 unsigned int oldstate); 69 70 /* 71 * Return a string describing the type of the port 72 */ 73 const char *(*type)(struct uart_port *); 74 75 /* 76 * Release IO and memory resources used by the port. 77 * This includes iounmap if necessary. 78 */ 79 void (*release_port)(struct uart_port *); 80 81 /* 82 * Request IO and memory resources used by the port. 83 * This includes iomapping the port if necessary. 84 */ 85 int (*request_port)(struct uart_port *); 86 void (*config_port)(struct uart_port *, int); 87 int (*verify_port)(struct uart_port *, struct serial_struct *); 88 int (*ioctl)(struct uart_port *, unsigned int, unsigned long); 89 #ifdef CONFIG_CONSOLE_POLL 90 int (*poll_init)(struct uart_port *); 91 void (*poll_put_char)(struct uart_port *, unsigned char); 92 int (*poll_get_char)(struct uart_port *); 93 #endif 94 };UART 驱动编写人员需要实现 uart_ops,因为 uart_ops 是最底层的 UART 驱动接口,是实实在在的和 UART 寄存器打交道的。关于 uart_ops 结构体中的这些函数的具体含义请参考Documentation/serial/driver 这个文档。UART 驱动框架大概就是这些,接下来我们理论联系实际,看一下 NXP 官方的 UART 驱动文件是如何编写的。2. I.MX6U UART 驱动分析2.1 UART 的 platform 驱动框架打开 imx6ull.dtsi 文件,找到 UART3 对应的子节点,子节点内容如下所示:// 示例代码 63.2.1 uart3 设备节点 1 uart3: serial@021ec000 { 2 compatible = "fsl,imx6ul-uart", 3 "fsl,imx6q-uart", "fsl,imx21-uart"; 4 reg = <0x021ec000 0x4000>; 5 interrupts = <GIC_SPI 28 IRQ_TYPE_LEVEL_HIGH>; 6 clocks = <&clks IMX6UL_CLK_UART3_IPG>, 7 <&clks IMX6UL_CLK_UART3_SERIAL>; 8 clock-names = "ipg", "per"; 9 dmas = <&sdma 29 4 0>, <&sdma 30 4 0>; 10 dma-names = "rx", "tx"; 11 status = "disabled"; 12 };重点看一下第 2, 3 行的 compatible 属性,这里一共有三个值:“fsl,imx6ul-uart”、“fsl,imx6quar”和“fsl,imx21-uart”。在 linux 源码中搜索这三个值即可找到对应的 UART 驱动文件,此文件为 drivers/tty/serial/imx.c,在此文件中可以找到如下内容:// 示例代码 63.2.2 UART platform 驱动框架 267 static struct platform_device_id imx_uart_devtype[] = { 268 { 269 .name = "imx1-uart", 270 .driver_data = (kernel_ulong_t)&imx_uart_devdata[IMX1_UART], 271 }, { 272 .name = "imx21-uart", 273 .driver_data = (kernel_ulong_t)&imx_uart_devdata[IMX21_UART], 274 }, { 275 .name = "imx6q-uart", 276 .driver_data = (kernel_ulong_t)&imx_uart_devdata[IMX6Q_UART], 277 }, { 278 /* sentinel */ 279 } 280 }; 281 MODULE_DEVICE_TABLE(platform, imx_uart_devtype); 282 283 static const struct of_device_id imx_uart_dt_ids[] = { 284 { .compatible = "fsl,imx6q-uart", .data = &imx_uart_devdata[IMX6Q_UART], }, 285 { .compatible = "fsl,imx1-uart", .data = &imx_uart_devdata[IMX1_UART], }, 286 { .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], }, 287 { /* sentinel */ } 288 }; ...... 2071 static struct platform_driver serial_imx_driver = { 2072 .probe = serial_imx_probe, 2073 .remove = serial_imx_remove, 2074 2075 .suspend = serial_imx_suspend, 2076 .resume = serial_imx_resume, 2077 .id_table = imx_uart_devtype, 2078 .driver = { 2079 .name = "imx-uart", 2080 .of_match_table = imx_uart_dt_ids, 2081 }, 2082 }; 2083 2084 static int __init imx_serial_init(void) 2085 { 2086 int ret = uart_register_driver(&imx_reg); 2087 2088 if (ret) 2089 return ret; 2090 2091 ret = platform_driver_register(&serial_imx_driver); 2092 if (ret != 0) 2093 uart_unregister_driver(&imx_reg); 2094 2095 return ret; 2096 } 2097 2098 static void __exit imx_serial_exit(void) 2099 { 2100 platform_driver_unregister(&serial_imx_driver); 2101 uart_unregister_driver(&imx_reg); 2102 } 2103 2104 module_init(imx_serial_init); 2105 module_exit(imx_serial_exit);可以看出 I.MX6U 的 UART 本质上是一个 platform 驱动,第 267~280 行, imx_uart_devtype为传统匹配表。第 283~288 行,设备树所使用的匹配表,第 284 行的 compatible 属性值为“fsl,imx6q-uart”。第 2071~2082 行, platform 驱动框架结构体 serial_imx_driver。第 2084~2096 行,驱动入口函数,第 2086 行调用 uart_register_driver 函数向 Linux 内核注册 uart_driver,在这里就是 imx_reg。第 2098~2102 行,驱动出口函数,第 2101 行调用 uart_unregister_driver 函数注销掉前面注册的 uart_driver,也就是 imx_reg。2.2 uart_driver 初始化在 imx_serial_init 函数中向 Linux 内核注册了 imx_reg, imx_reg 就是 uart_driver 类型的结构体变量, imx_reg 定义如下:// 示例代码 63.2.3 imx_reg 结构体变量 1836 static struct uart_driver imx_reg = { 1837 .owner = THIS_MODULE, 1838 .driver_name = DRIVER_NAME, 1839 .dev_name = DEV_NAME, 1840 .major = SERIAL_IMX_MAJOR, 1841 .minor = MINOR_START, 1842 .nr = ARRAY_SIZE(imx_ports), 1843 .cons = IMX_CONSOLE, 1844 };2.3 uart_port 初始化与添加当 UART 设备和驱动匹配成功以后 serial_imx_probe 函数就会执行,此函数的重点工作就是初始化 uart_port,然后将其添加到对应的 uart_driver 中。在看 serial_imx_probe 函数之前先来看一下 imx_port 结构体, imx_port 是 NXP 为 I.MX 系列 SOC 定义的一个设备结构体,此结构体内部就包含了 uart_port 成员变量, imx_port 结构体内容如下所示(有缩减):// 示例代码 63.2.4 imx_port 结构体 216 struct imx_port { 217 struct uart_port port; 218 struct timer_list timer; 219 unsigned int old_status; 220 unsigned int have_rtscts:1; 221 unsigned int dte_mode:1; 222 unsigned int irda_inv_rx:1; 223 unsigned int irda_inv_tx:1; 224 unsigned short trcv_delay; /* transceiver delay */ ...... 243 unsigned long flags; 245 };第 217 行, uart_port 成员变量 port。接下来看一下 serial_imx_probe 函数,函数内容如下:// 示例代码 63.2.5 serial_imx_probe 函数 1969 static int serial_imx_probe(struct platform_device *pdev) 1970 { 1971 struct imx_port *sport; 1972 void __iomem *base; 1973 int ret = 0; 1974 struct resource *res; 1975 int txirq, rxirq, rtsirq; 1976 1977 sport = devm_kzalloc(&pdev->dev, sizeof(*sport), GFP_KERNEL); 1978 if (!sport) 1979 return -ENOMEM; 1980 1981 ret = serial_imx_probe_dt(sport, pdev); 1982 if (ret > 0) 1983 serial_imx_probe_pdata(sport, pdev); 1984 else if (ret < 0) 1985 return ret; 1986 1987 res = platform_get_resource(pdev, IORESOURCE_MEM, 0); 1988 base = devm_ioremap_resource(&pdev->dev, res); 1989 if (IS_ERR(base)) 1990 return PTR_ERR(base); 1991 1992 rxirq = platform_get_irq(pdev, 0); 1993 txirq = platform_get_irq(pdev, 1); 1994 rtsirq = platform_get_irq(pdev, 2); 1995 1996 sport->port.dev = &pdev->dev; 1997 sport->port.mapbase = res->start; 1998 sport->port.membase = base; 1999 sport->port.type = PORT_IMX, 2000 sport->port.iotype = UPIO_MEM; 2001 sport->port.irq = rxirq; 2002 sport->port.fifosize = 32; 2003 sport->port.ops = &imx_pops; 2004 sport->port.rs485_config = imx_rs485_config; 2005 sport->port.rs485.flags = 2006 SER_RS485_RTS_ON_SEND | SER_RS485_RX_DURING_TX; 2007 sport->port.flags = UPF_BOOT_AUTOCONF; 2008 init_timer(&sport->timer); 2009 sport->timer.function = imx_timeout; 2010 sport->timer.data = (unsigned long)sport; 2011 2012 sport->clk_ipg = devm_clk_get(&pdev->dev, "ipg"); 2013 if (IS_ERR(sport->clk_ipg)) { 2014 ret = PTR_ERR(sport->clk_ipg); 2015 dev_err(&pdev->dev, "failed to get ipg clk: %d\n", ret); 2016 return ret; 2017 } 2018 2019 sport->clk_per = devm_clk_get(&pdev->dev, "per"); 2020 if (IS_ERR(sport->clk_per)) { 2021 ret = PTR_ERR(sport->clk_per); 2022 dev_err(&pdev->dev, "failed to get per clk: %d\n", ret); 2023 return ret; 2024 } 2025 2026 sport->port.uartclk = clk_get_rate(sport->clk_per); 2027 if (sport->port.uartclk > IMX_MODULE_MAX_CLK_RATE) { 2028 ret = clk_set_rate(sport->clk_per, IMX_MODULE_MAX_CLK_RATE); 2029 if (ret < 0) { 2030 dev_err(&pdev->dev, "clk_set_rate() failed\n"); 2031 return ret; 2032 } 2033 } 2034 sport->port.uartclk = clk_get_rate(sport->clk_per); 2035 2036 /* 2037 * Allocate the IRQ(s) i.MX1 has three interrupts whereas later 2038 * chips only have one interrupt. 2039 */ 2040 if (txirq > 0) { 2041 ret = devm_request_irq(&pdev->dev, rxirq, imx_rxint, 0, 2042 dev_name(&pdev->dev), sport); 2043 if (ret) 2044 return ret; 2045 2046 ret = devm_request_irq(&pdev->dev, txirq, imx_txint, 0, 2047 dev_name(&pdev->dev), sport); 2048 if (ret) 2049 return ret; 2050 } else { 2051 ret = devm_request_irq(&pdev->dev, rxirq, imx_int, 0, 2052 dev_name(&pdev->dev), sport); 2053 if (ret) 2054 return ret; 2055 } 2056 2057 imx_ports[sport->port.line] = sport; 2058 2059 platform_set_drvdata(pdev, sport); 2060 2061 return uart_add_one_port(&imx_reg, &sport->port); 2062 }第 1971 行,定义一个 imx_port 类型的结构体指针变量 sport。第 1977 行,为 sport 申请内存。第 1987~1988 行,从设备树中获取 I.MX 系列 SOC UART 外设寄存器首地址,对于I.MX6ULL 的 UART3 来说就是 0X021EC000。得到寄存器首地址以后对其进行内存映射,得到对应的虚拟地址。第 1992~1994 行,获取中断信息。第 1996~2034 行,初始化 sport,我们重点关注的就是第 2003 行初始化 sport 的 port 成员变量,也就是设置 uart_ops 为 imx_pops, imx_pops 就是 I.MX6ULL 最底层的驱动函数集合,稍后再来看。第 2040~2055 行,申请中断。第 2061 行,使用 uart_add_one_port 向 uart_driver 添加 uart_port,在这里就是向 imx_reg 添加 sport->port。2.4 imx_pops 结构体变量imx_pops 就是 uart_ops 类型的结构体变量,保存了 I.MX6ULL 串口最底层的操作函数,imx_pops 定义如下:// 示例代码 63.2.6 imx_pops 结构体 1611 static struct uart_ops imx_pops = { 1612 .tx_empty = imx_tx_empty, 1613 .set_mctrl = imx_set_mctrl, 1614 .get_mctrl = imx_get_mctrl, 1615 .stop_tx = imx_stop_tx, 1616 .start_tx = imx_start_tx, 1617 .stop_rx = imx_stop_rx, 1618 .enable_ms = imx_enable_ms, 1619 .break_ctl = imx_break_ctl, 1620 .startup = imx_startup, 1621 .shutdown = imx_shutdown, 1622 .flush_buffer = imx_flush_buffer, 1623 .set_termios = imx_set_termios, 1624 .type = imx_type, 1625 .config_port = imx_config_port, 1626 .verify_port = imx_verify_port, 1627 #if defined(CONFIG_CONSOLE_POLL) 1628 .poll_init = imx_poll_init, 1629 .poll_get_char = imx_poll_get_char, 1630 .poll_put_char = imx_poll_put_char, 1631 #endif 1632 };imx_pops 中的函数基本都是和 I.MX6ULL 的 UART 寄存器打交道的,这里就不去详细的分析了。简单的了解了 I.MX6U 的 UART 驱动以后我们再来学习一下,如何驱动正点原子I.MX6U-ALPHA 开发板上的 UART3 接口。
2022年10月22日
323 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--19.Linux RTC驱动实验
暂无简介
2022年10月22日
325 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--20.IIC驱动实验
暂无简介
2022年10月22日
319 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--17.Linux input子系统驱动实验
暂无简介
2022年10月22日
325 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--15.Linux自带LED驱动实验
暂无简介
2022年10月22日
358 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--16.Linux misc 驱动实验
暂无简介
2022年10月22日
306 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--14.platform设备驱动实验
暂无简介
2022年10月22日
415 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--13.Linux异步通知实验
暂无简介
2022年10月22日
295 阅读
0 评论
0 点赞
1
2
3