首页 > 代码库 > PCI-CAN卡驱动与数据通信调试小记
PCI-CAN卡驱动与数据通信调试小记
以前做项目,不注意记录调试过程中遇到的问题,以后应该注意这一点。今天抽空总结一下PCI-CAN卡驱动与数据通信调试过程中遇到的问题,方便以后回忆和思考。
1. 中断服务之字节流报文组包状态机
这是一个典型的适合采用状态机来思考和处理数据的场合。报文一般分为这几个字段:报文头,长度,命令,数据,校验和。在报文接收端,能看到的只是一连串的字节,这需要状态机的控制。
状态机分这几个状态:(1)接收报文头;(2)接收报文长度字段;(3)接收剩余部分,以报文长度作为跳出判断状态条件;(4)校验报文;(5)数据加工存储,组包完成。
图1 状态机之报文组包
在最初的代码中,没有用状态机的思想,比较混乱。在使用状态机的过程中,注意状态转移条件和计数器。
2. 双端口RAM数据读写过程分析
为了保证数据的完备性,双端口RAM要求PCI和MCU不能在同一时刻读写同一个memory uint,为此双端口RAM提供了独占使用的Sepahores,特殊的电路设计保证了Sempahores被一端写入0后,另一端就会写入0失败。通过Sepahores协调MCU和PCI对RAM的同时Read/Write操作。
将双端口RAM作为循环队列,在RAM的某个指定地址放置读写指针。以PCI向MCU下行数据为例说明:循环队列缓冲区从0~0x0FFF,0x3FE0放置MCU读指针,0x3FE4放置PCI写指针,指针占用4个字节。PCI写指针缩写pWR, MCU读指针缩写pRD.
如图2所示,当pRD、pWR递增到0x1000时自动复位到0x0000(队列缓冲区不包含0x1000),pRD和pWR在队列的位置分图2所示的两种情况。循环队列的深度是0x1000个字节,当写入的字节总数 - 读出的字节总数 > =0x1000时,则发生队列溢出,造成数据丢失。
在情况1时,pWR >= pRD,pRD~pWR之间灰色RAM区域,是等待被读出的有效数据。读取数据的字节数 = pWR-pRD;
在情况2时,pWR < pRD,pRD~0x1000, 0~pWR之间灰色RAM区域,是等待被读出的有效数据。读取数据的字节数 = pWR+0x1000-pRD。
图2 循环队列读写过程分析
如果将循环队列的头尾假想的连续起来,可以把队列想象成无限长度,那么永远满足:pRD < pWR,换句话说,读操作发生在小于pWR的区域,写操作发生在大于等于pWR的区域,所以我们可以不用担心读写数据区域的重叠(?),只需关心pWR指针本身读写的完备性。所以,PCI写数据和MCU读数据的过程是这样的:
PCI写数据过程:
(1) 从RAM中读pWR;
(2) 向 >= pWR区域写入数据,此时pWR保持不变,维护1个局部的字节计数WRCnt;
(3) 获取锁/Sepahore;
(4) 在RAM中更新pWR, pWR = pWR+WRCnt;
(5) 释放锁/Sepahore;
(6) 向MCU发中断。
MCU读数据过程:
(1) 获取锁/Sepahore;
(2) 从RAM读出pWR;
(3) 释放锁/Sepahore;
(4) 从RAM读出pRD,计算应该读出数据的字节数;
(5) 循环读数据,此时pRD保持不变,维护1个局部的字节计数RDCnt;
(6) 在RAM中更新pRD, pRD = pRD+RDCnt (或 pRD = pWR)。
细心的你可能会发现,我在上文中标注了1个红色的问号。这里存在了一种假设,FIFO深度足够大,读写数据的速度足够匹配。只有在满足该假设的情况下,我们才不用担心数据区域的重叠。当此假设不满足时,很可能存在这样一种情况,pWR马上就要追上pRD(追上!!不是pRD<pWR么?不过这里说的没错,如果你不赞同,尝试理解一下我的意思),此时读写数据就会重叠。这种情况下,怎么办?这需要进行写操作是清楚的知道pRD在哪里,如果必要的话,增加pRD(快点快点,我都快追上你了,踢你一脚,别挡我的路!哈哈)。在写入数据前,判断pWR==pRD?满足该条件时,递增pRD。这样会降低效率!!!
很幸运,我们的系统满足这个假设,所以就不用进行这种复杂的操作啦。很多时候,一个系统设计的并不完美,存在逻辑上的漏洞,可是如果简单简洁本身就是一种美妙的事情,那么我们用这个漏洞去换取这份美妙,也很棒。
3. PCI本地总线写RAM失败现象
像你说话的时候,牙齿竟然咬到自己的舌头。ou, my god!不过这确实发生了。
引用PCI local bus spec. “This bridge provides a low latency path through which the processor may directly access PCI devices mapped anywhere in the memory or I/O address spaces”.是的,我们的双端口RAM映射在CPU的memory空间,理论上说,CPU读写双端口RAM与CPU读写内存之间不存在任何区别,我以前从来没有怀疑过CPU竟然会读写内存失败,所以这个问题一直隐藏的很深,让我很费解很费解,为什么竟然丢数据丢的这么莫名其妙!
让我们分析一下,双端口RAM的读写时序,如图3所示。
写数据:EN#是RAM使能信号,此后RAM在不需要任何驱动的情况下,其内部逻辑自动定位到ADDR指示的memory uint;WR#是写RAM选通信号,触发RAM内部电路对DATA进行锁存,并将数据写入ADDR指示的memory unit。在WR#选通时,需要保证DATA和ADDR的信号时稳定的,也就是需要在ADDR/DATA有效与WR#选通之间有latency,可能是1或几个clk.
读数据:与写数据逻辑相反,DATA是输出,EN#选通后,RAM将数据放置到DATA线上,确保DATA和ADDR是稳定时,选通RD#。
图3 双端口RAM的写时序和读时序
我们在看看9030本地总线的读写时序,为了准确,直接截图9030 datasheet.
图4 9030本地写时序
图5 9030本地读时序
已经很清楚了, 注意Write Strobe Delay和Read Strobe Delay,这两个值是9030的EEPROM设置的。
图6 9030 EEPROM space descriptor典型值
参考9030 data sheet的典型值,设置EEPROM,复位9030芯片。从此之后,再也没有碰到CPU读写双端口RAM失败。难道牙齿再也不会咬到舌头了么?我不是很确定,但很久没有咬到了。
4. 谨慎对待计数器的位宽
问题:函数RecvFunc从缓冲区读取数据,返回读取数据的个数。函数内部设置1个计数器,返回此计数器的值。很不小心,这个内部计数器被设计为8位宽,而我读取数据个数有可能超过0xFF,导致经常发现数据丢失?
这个问题很简单,也很隐藏。当一个系统存在多个原因导致数据通信丢失时,这个不起眼的问题最初会掩盖掉重要的问题所在,致使问题复杂化。单独列出来,是为了警告自己,不要犯类似的错误。
5. 谨慎对待数组下标
问题:从报文中获取某个字段,并将该字段作为数组下标使用。起初,我考虑到数据通信失败时,数组下标越界的可能性,但是想到经过报文校验,应该不会发生吧。不过确实发生了,导致非法的内存访问,程序崩溃退出。
问题虽然很简单,很幼稚,缺经常发生。
6. 使用try-catch块避免程序自动崩溃结束
在遇到问题5这样的情况,程序会直接被OS结束掉。这给会造成极其恶劣的用户体验,一定要避免。设计的程序要具备一项性能,不关用户怎么操作,可以向用户输出失败,但决不允许莫名其妙的跑飞,崩溃。
可是貌似不管你怎么细心的设计,程序还是可能在不起眼的地方出现意外。C++中有一项重要的机制,try-catch块,windows也实现了结构化异常机制。请恰当的使用这些机制!
7. 接收数据缓冲区/FIFO留足够空间
数据通信丢失报文的一个重要原因,可能是FIFO深度不够。测试FIFO深度够不够,可以设置一个FIFO深度使用的最大计数。
8. windows系统偶发性的时延,数据挤压而在某个小的时间段喷涌数据
数据通信丢失报文的另一个重要原因,可能是FIFO深度已经很深的情况下,数据突发性的向总线喷涌,而总线带宽有限,这时就需要更大的FIFO深度。我们的测试程序运行在xp环境下,VC编写。windows应用程序基于消息框架,每个进程和线程都有自己的消息队列。我们的测试程序使用多媒体timer,每隔1ms产生一次TimerHandler调用。这一过程可以这么理解,windows每隔1ms向TimerHandler消息队列发1个timer消息,如果消息没有及时处理(windows很复杂,干着很多让你摸不着头脑的事情,所以不要奢望每个消息发出后,繁忙的windows都会把宝贵的CPU及时交给TimerHandler),那么消息队列就会变得很长。例如某个100ms时间段,TimerHandler一直没有得到调用,其消息队列堆积了100个timer消息,然后CPU使用权终于交给TimerHandler了,TimerHandler迫不及待的的把堆积的100个消息,恨不得在1ms都给处理了。所以数据从windows向洪水泄闸一样涌向RAM,又从RAM涌向MCU,MCU不停的往内部FIFO灌报文数据,而CAN总线最高只有1Mbps的数据带宽,即使CAN控制器以最快的速度发,MCU内部FIFO也会很可能堆满数据,造成FIFO溢出,报文丢失啦,就像长江洪水一样。
这只是我们的假想,能不能实际看看呢?通过逻辑分析仪采集中断信号,验证了我们的结果。