本文系原创,学习笔记而已,错误难免,仅供参考交流,可以注明出处后转载。
将spi适配器配置为从设备时,当从设备片选引脚没有连接主设备当片选引脚时,发现轮询从设备的设备状态寄存时,设备繁忙的那个状态位一直被置1
今天发现一个新的症状:spi控制器在发送帧的时候会缺失部分时钟信号,比如设置的每个帧是16bit,就应该有16个时钟信号。但是我看到的少于16个。忙活了几个小时发现是因为我的代码在每次传输过程中会关闭spi端口(寄存器SSPCR1的bit 1 )。而却没有检查是否数据传输完毕。如果数据未传输完毕就关闭spi端口,就会导致传输信号中断!因此关闭端口前一定要检查数据是否传输结束!或者,一定要在合适的时候去关闭spi端口.由此得到的启发就是我们可以在传输过程中改变数据帧的大小!
两个spi控制器一个做master,一个做slaver进行交互的重要配置:
主机:配置为第二次时钟跳变沿捕获数据。
从机,配置为第一次时钟跳变沿捕获数据。
必须的哦!
原因:主机SSP设置为:前沿输出后沿采样。从机设置为:前沿采样后沿输出。在主从机之间进行数据传输时,正好可以做到主机输出时从机开始采样,从机输出时主机开始采样。
关于spi的片选信号:今天发现,当持续发送多个数据帧的时候片选会在该发送期间被拉低,而不是在每个帧发送期间都被拉低!
原因:这是Motorola SPI协议定义的!(参考文档:DDI0194E_ssp_pl022_r1p3_trm.pdf Page:p2-15 & p2-18 原文:for continuous back-to-back word transfer, the SSPFSSOUT pin is held LOW between successive data words and termination is the same as that of the single word transfer.)而且是当且仅当SPH=1(即前沿输出,后沿采样模式)时才有效的!所谓连续发送的意思就是你不断向TX-fifo写入数据保持TX-fifo非空,这一过程就叫连续,如果此过程中SPI控制器被配置为SPH=1,那么你就会发现此“连续”期间的CS一直被拉低!直到TX-fifo为空,这被认为是一次“连续传输”的结束,此时CS会被拉高。而当我们将SPI控制器设置为SPH=0(前沿采样,后沿输出模式)时,则无论连续与否,每个data-size被发出后CS都会被拉高,也就是说哪怕不停的写TX-fifo保持其非空,也会看到在每个data被发出后CS有一个被拉高的脉冲!
当然无论何种情况(无论SPH=1 或者SPH=0),你可能都有一种需求,期望每个数据帧被发出后CS都会被拉高,那就每次向fifo写入一个数据吧,然后当fifo空了以后在写入下一个数据,这样无论如何你都能看到CS在一个data完成后被拉高!
spi传输过程中从机总是会丢失一些数据,是因为从机频率设置太低了,来不及处理数据。这说的是所配置的从机的clk频率必须大于主机的clk时钟频率的12倍!只有这样从机才能检测到主机输入时钟的变化沿!换句话说,当我们配置主机时钟的时候我们知道这就是将来主机的输出clk时钟,而同样的寄存器配置到了从机这边就被认为是在配置从机的采样频率。(从机不发出clk,从机采样来自主机clk)必须知道的是这个采样频率不同于针对高低电平转换的采样频率,那个只要采样频率大于被采样的2倍即可!而现在从机要检测时钟的上升沿和下降沿,因此采样频率要远远大于被采样的频率,文档中要求是至少12倍,我现在还不是太明白这个数字是怎么算出来的!
顺便说下主从通讯过程(这部分是我猜的,如果不对请指正):主机发出时钟信号,从机没有时钟发出。从机以高频检测来自主机的时钟信号的变化沿并在变化沿将数据位送出去(拉高或拉低SDOUT)或采样SDIN上的数据(到底是采样还是发出要看SPH的设置)。主机在发出一个data(驱动自己的SCK和SDOUT)的时候会同时采样自己的SDIN,该采样的数据被认为是来自从设备的数据。从设备在此期间会根据来自主机的SCK驱动自己的SDOUT从而把自己的TX-fifo中的数据送出去,因此你有必要事先或者说适时地向从设备的TX-fifo写入你想要送到主设备的数据。(BTW:从设备其实也能驱动自己的SCK,但是一边我们都配置为不驱动)。也就是说:对于slaver而言,如果要给master一个响应或者给master一个消息,可以把这个数据放到自己的TX——FIFO中,这样当mster向slaver发送数据的时候便会同步的取走这个数据!(貌似如果你不向slaver的TX-fifo写入数据的话,master取走的总是残留在TX-fifo的最后一个数据,如果不向fifo写入新数据,那么这个fifo顶端的数据总是不会被覆盖掉!)
再说DMA:开始搞SPI的DAM模式后,终于可以吧DMA操作搞清楚一些了!
DMA的真正的物理通道可能只有一个,其他的都是逻辑通道,所有的逻辑通道分时复用一个物理通道。有硬件DMA请求和软件DMA请求,硬件请求指的是设备有硬件的DMA请求线直接连接到DMA控制器用来触发一个传输请求,而软件请求指的是如果你想让某个DMA通道动作的时候只要去设置一个对应的寄存器位就可以触发该DAM通道发生动作!另外,你可以设置为一个DMA请求发送一个burst,也可以设置为一个DMA请求发送所有的burst,burst是DMA传输的最小单位,也就是DMA的一次动作至少是一个burst。当然和burst对应的还有一种传输模式叫做single模式,这种模式下可以一次只传输一个data size大小的数据。而不是整个burst。这要看DMA控制器是否支持single模式啦!(burst由1到多个data size组成)
每次DMA传输前都会读取本次传输的配置(在DMA的控制寄存器中),所以每次传输前你都需要从新配置本次传输的参数,如burst size ,data size ,burst interval,data interval等等。不要想着这次传输和上一次的参数一样而不去配置DMA!(当然,一般情况下DMA驱动的dma_request相关函数会把这些工作都做好,你只要一次次的调用dma_request就可以完成一次次传输了)
几个必须设置的参数:
source_data_size, destination_data_size, source_data_interval, destination_data_interval, source_burst_interval, destination_burst_interval, source_data_change_sel, destination_data_change_sel, source_address,destination_address, source_burst_size, source_total_data_size,
所有参数的描述单位都是BYTE !
source_data_size:源数据大小,欲被DMA控制器搬运的最小数据元,可以设置为三种情况: 1 BYTE,半个字(2 BYTE),一个字(4 BYTE)
destination_data_size 目的数据大小,同上
source_data_interval 源数据间隔,或者说步长。含义:每次搬完一个source_data_size描述的数据块以后,下次搬运的起始位置相对于上次搬运起点的间隔。本次搬运的起始地址=上次搬运的起始地址(source_address)+或- source_data_interval
destination_data_interval 目的数据间隔(步长)。含义:每次搬来一个souce_data_size描述的数据块后,放到哪里呢?这就是本次放置位置和上次放置位置的间隔,本次应该放置的位置首地址=上次放置的位置(destination_address)+或- destination_data_interval
source_burst_interval 源burst间隔(步长),当一个burst发送完毕后,下一个burst的起始位置地址= 当前source_address +或- source_burst_interval (我猜这里的加或者减也受source_data_change_sel控制,没测试过减,但是加是没有问题的)
destination_burst_interval 目的端burst间隔(步长),当一个burst存储后,下一个burst的存储起始位置地址= 当前destination_address +或- destination_burst_interval (我猜这里的加或者减也受destination_data_change_sel控制,没测试过减,但是加是没有问题的)
source_data_change_sel:源端待处理数据是按照地址递增还是按照地址递减?这个标志位会影响到source_address的值影响它的加减,见下方描述)
destination_data_change_sel:目的端收到数据后,是递增存储还是递减式存储?这个标志位会影响到destination_address的值(影响它的加减,见下方描述)
source_address:源端数据传输起始地址,这里放的永远是下一个将要处理的数据的起始地址,当一个source_data_size传输完毕后,这里的值便被更新为:当前值 +或- source_data_interval
destination_address:目的端存放起始地址。这里存放的是收到下一个destination_data_size大小的数据后要被存放的起始位置地址。存完后这里值更新为:当前值 +或- destination_data_interval
source_burst_size:源数据的每个burst的大小。
source_total_data_size:这里记录的是总的待搬运的数据个数,每次搬运后这个值会递减
两种需要关心的中断:一个burst完成后产生的中断,所有数据(所有burst)发送完毕后产生的中断。
实际应用中的问题:SSP控制器支持DMA模式和FIFO模式的数据传输,在DMA模式下会由DMA控制器把数据从内存搬运到FIFO中。这个过程中要求destination addr是固定不变的。因为我们对fifo的操作总是表征为写或者读一个固定的寄存器。这就是说我必须在DMA传输时固定住destination addr 让每一个被DMA搬运过来的数据都写入FIFO中。怎么实现呢?只要将destination端的data_interval, burst_interval设置为0,并且将data_change_sel?设置为递增就可以了!这样目的端的address就被锁定不动了。反过来如果你期望让DMA控制器从FIFO搬运数据到内存中去,那你就需要吧source端的地址固定住,设置方法和上面说的类似哦!
顺便说下,DMA的这两个interval以及date_size的设置是很有意思的,最简单的,如果你需要让数据按照顺序一个不落的被搬运,那务必将data_interval和burst_interval设置为同样的值!
举个例子:假设从内存单元0x00地址开始按照顺序在每个字节中存放一个十六进制数 0 1 2 3 4 5 6 ……
针对source端我们做如下设置: data_size = 1 BYTE;data_interval = 4 BYTE;burst_interval = 4 BYTE; data_change_sel = 增加;address = 0x00;
那么实际的数据搬运过程如下:0x03,0x07,0x0b,0x0f,0x13,0x17,0x1b,0x1f ……
如果 data_size = 4 BYTE, 其他配置不变,结果将是:0x03020100,0x07060504,0x0b0a0908,0x0f0e0d0c,0x13121110,……
如果 data_size = 2 BYTE, 其他配置不变,结果将是:0x0302, 0x0706, 0x 0b0a, 0x0f0e, 0x1312 ……
关于中断:
前两天测试一个ite的电容TP出了点儿问题,困扰我很久,昨天终于搞明白了。对中断的了解又深入了一点点。
问题描述:该电容屏以I2C接口和CPU通讯,对该电容屏以轮询的方式进行报点操作完全正常。该电容屏有一个pin脚用做INT,当手指接触电容屏后该pin脚会被拉低,从而触发cpu的中断用以告知驱动去读取报点信息,当手指离开后该pin脚会从新恢复到高电平。在轮询模式中我用示波器测量该引脚,发现该引脚确实可以在触摸时拉低,手指离开后从新拉高。由此可见该CTP的中断功能是完全正常而且可用的!考虑到目前测试的环境是单线程的,我便理所当然的认为只要把读取报点信息的代码放到中断处理函数中去就可以了,虽然也知道中断处理函数要尽可能的短暂,要尽快的返回,比如在linux 的 kernel中我们一般会把中断处理分为上半部和底半部,上半部是真正的中断处理函数,一般就是做个简单的工作,如屏蔽一下中断,触发一下底半部等。这样就可以让中断处理函数尽快的返回了,而更多的复杂和耗时的工作都是交由底半部去完成的,底半部一般都是一个工作队列。但是当时我却想,反正是单线程的,中断什么时候返回还不是都一样么。那就这么整了!(也可能想偷个懒,因为前面的工作也碰到了类似的问题,当时我都会老老实实的按照上述两个半部分的思想去coding,在单线程环境下为了模拟出一个底半部,模拟出一个schedule动作,我会执行一个死循环,再设置一个全局的flag,在中断的上半部去更改这个flag,死循环中检查这个flag并决定是否执行底半部,如果flag被标记,我就清除flag并执行底半部,否则死循环继续,。。。。) 然而,偷懒的结果是简单的把所有报点工作都放到中断处理函数当中后根本不行!发现进到中断以后根本就出不来,也就是中断处理函数一直在运行,而且从log信息来判断发现竟然是I2c操作出现了问题!他奶奶的,这让我惊出一身冷汗,因为这个I2c的驱动也是我写的,我这个人最大的缺点就是遇到问题总爱怀疑自己!这我就不得不想,难道前阵子搞得这个I2C驱动有缺陷????可是当时我测试很多次呀。。。都挺好的么。。。。怀着忐忑的心情,我决定用trace单步调试下看看,到底死到哪里了。结果发现当TP中断发生后,开始通过I2C读取TP的报点信息,I2C控制器首先发送TP的设备地址,此时收到了TP的ACK ,这一小步是正常的。然后I2C控制器会再发送一个字节到TP(你知道,这个一般是用来设置seek指针的),这里就出现了问题,发现这个数据一直无法发出,log显示timeout!奇怪,怎么发不出去呢?分析下我的I2C驱动吧,当第一个数据(地址+读写位)发出后,后续的信息发送会由I2c的中断处理函数完成,也即是说第一个信息发出后i2c驱动会打开自己的中断,当TX-fifo空了后i2c会产生中断,在中断处理函数中会发送下一个数据,现在看来这个数据没有被发出去,而且还超时了,就说明I2c根本就没有产生中断!可是为什么i2c不产生中断了呢?
第二天给一个同事描述了一下我的问题,终于得到了答案:原来是因为我在一个中断处理函数中等待另一个中断,这不就死锁了吗?靠!结果就是这么简单!我却没有想到,还好,这下可以记住了,那顺便我把中断的这个东西也总结下吧!
首先,系统中有一个中断控制器,可以处理32种不同的中断,每种中断都有一个使能位,比如有一种中断叫GPIO中断,先把这一类中断的使能位设置了!另外还有一个寄存器的每一位会用来表示当前是否发生了这一类中断,比如现在发生了GPIO中断,那这个对应位就会置1,如果前面你也使能了这一类中断,那么好了,现在就会保存现场,(此后不响应任何中断),然后去查找中断向量表,找到对应的中断处理函数去执行,完了再恢复现场,(此后便可以相应中断),再继续!我猜有一个寄存器位或者别的什么东西作为标志,当进入中断后设置一下这个标志就不会再相应任何中断了,当中断处理结束的时候在清除一下这个位,就可以继续相应后来的中断了。
这里我们看的出,那就是中断不能嵌套,但这不是绝对的,有些实时性要求高的场合,会用一些允许中断嵌套的CPU。
说到这里我们就清楚了,我前面碰到的问题就是在一个中断处理程序中等待另一个中断的发生。。。。。这是要死锁的。。。 解决的办法当然还是用底半部和顶半部的思想啦。好了就写到这里了。。。。很累呀