首页
chatGPT
关于
友链
其它
统计
更多
壁纸
留言
Search
1
cgroup--(4)cgroup v1和cgroup v2的详细介绍
6,535 阅读
2
修改Linux Kernel defconfig的标准方法
6,447 阅读
3
Android系统之VINTF(1)manifests&compatibility matrices
6,054 阅读
4
使用git生成patch和应用patch
3,570 阅读
5
c语言的__attribute__
3,183 阅读
默认分类
文章收集
学习总结
算法
环境配置
知识点
入门系列
vim
shell
Git
Make
Android
Linux
Linux命令
内存管理
Linux驱动
Language
C++
C
Rust
工具
软件工具
Bug
COMPANY
登录
Search
标签搜索
Rust
shell
Linux
c
uboot
Vim
vintf
Linux驱动
Android
device_tree
git
DEBUG
arm64
链表
数据结构
IDR
内核
ELF
gcc
ARM
adtxl
累计撰写
380
篇文章
累计收到
16
条评论
首页
栏目
默认分类
文章收集
学习总结
算法
环境配置
知识点
入门系列
vim
shell
Git
Make
Android
Linux
Linux命令
内存管理
Linux驱动
Language
C++
C
Rust
工具
软件工具
Bug
COMPANY
页面
chatGPT
关于
友链
其它
统计
壁纸
留言
搜索到
23
篇与
的结果
2022-10-22
[正点原子]Linux驱动学习笔记--33.Regmap API实验
暂无简介
2022年10月22日
371 阅读
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日
469 阅读
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日
583 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--19.Linux RTC驱动实验
暂无简介
2022年10月22日
448 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--20.IIC驱动实验
暂无简介
2022年10月22日
490 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--17.Linux input子系统驱动实验
暂无简介
2022年10月22日
484 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--15.Linux自带LED驱动实验
暂无简介
2022年10月22日
568 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--16.Linux misc 驱动实验
暂无简介
2022年10月22日
343 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--14.platform设备驱动实验
暂无简介
2022年10月22日
652 阅读
0 评论
0 点赞
2022-10-22
[正点原子]Linux驱动学习笔记--13.Linux异步通知实验
暂无简介
2022年10月22日
404 阅读
0 评论
0 点赞
1
2
3