NAND Flash的读写之前已经写过一篇NAND Flash驱动相关的文章了,处理器用的是TI的8核DSP TMS320C6678,为了后续用它做大批量的数据处理,而现在有苦于暂时没有数据源,所以想先在Flash里存好待处理的数据,后面用起来方便一点。
  为了准备尽可能多的数据,128MB容量的NAND Flash,我准备往里面写125MB。
  一开始我特别激动,我就准备把这些数据写成常量,然后放到工程里一起编译,最后用仿真器下载进去。这一个数据文件就得几百兆。

  后来试了一下发现,这文件太大了,CCS也扛不住,CCS还提示了“JVM heap low detected”,后来我还不死心,想办法提高CCS的heap size,但都没有很好的效果。125M数据确实有点多。
  为了解决这个问题,我就想用串口吧。因为现在板子上6678能跟上位机直接通信的除了JTAG,也就只有串口了。但串口的速度又快不上去,虽然能够做,但是做出来还是让人哭笑不得。
  最后我用了3M的波特率(USB转422用的FT232,最高只支持3M波特率),然后实际传输的过程中受到NAND Flash写入速度的限制,大概只做到了9kB/s,我花了4个小时把125MB的数据写到了NAND Flash里。
  现在回想了一下,自己在写Flash 的时候一直用的是单个的写Page,没有用到Flash里的Cache。如果每次用串口收到一个Block的数据之后再用写Cache的方法写入Flash应该可以更快。

整体方案

  整体方案如上图所示,大批量的数据传输,很容易出现发送的数据量和接收的数据量对不上的情况。所以这个整体方案有了一个握手的机制,每次“发送请求-相应”之后只能发送一定量的数据。
  具体情况是这样的,上位机发送图像数据,图像是640×512字节/帧的图像数据,而NAND Flash一个Block是2048×64字节。简单计算可知,两帧图像正好可以放在5个Block里。所以我就一次发两帧图的数据。
  每次上位机发送请求的时候,可以包含目的Flash的Block首地址,之后发送的数据就写到以这个Block首地址开始的5个Block里。
  6678的UART每收到16个字节的数据可以产生中断,因此发送请求的长度为16字节,然后6678的响应为4字节,因为UART的数据寄存器就是4字节位宽的深度为32字节的FIFO。

上位机实现

  上位机用python实现是最方便的,我一开始用的win10的串口调试助手,win10的应用商店里就有,用起来也非常方便。但是后来发现它发文件的时候,是把文件中的每个字符单独发送,而大于127的ASCII码都是不能打印的字符,也就不能写在文本文档里,所以这个不能用来以文件发送图像数据。

  用opencv-python读取图像中每个像素的数据,然后用pySerial逐个字节发送,实现简单,灵活性高。具体实现的功能如下:

  • 共发送“./image/”目录下的400帧图像,每组发两帧图,共发送200组
  • 串口波特率3000000Baud,无奇偶校验,一位停止位
  • 发送请求包含四个word,依次是“0xAAAAAAAA, blockBaseAddr, blockBaseAddr, 0xAAAAAAAA”
  • DSP响应“0x55555555”
  • 每发送2048字节,等待0.2秒,确保Flash烧录完成,不会落下EMDA完成中断
  • DSP完成后接收发送响应“0xAAAAAAAA”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import serial
from serial.serialutil import EIGHTBITS, PARITY_NONE, STOPBITS_ONE
import cv2 as cv
import time

if __name__ == '__main__':
## serial setup
ser = serial.Serial()
ser.port = 'COM5'
ser.baudrate = 3000000
ser.bytesize = EIGHTBITS
ser.parity = PARITY_NONE
ser.stopbits = STOPBITS_ONE
ser.timeout = None
ser.xonxoff = False
ser.rtscts = False
ser.write_timeout = None
ser.dsrdtr = False
ser.open()

h = 512
w = 640
for p in range(200):
## send request
btArr = bytearray(16)
blockAddrLo = (p*5)%256
blockAddrHi = int((p*5)/256)
btArr[0:4] = [0xAA, 0xAA, 0xAA, 0xAA]
btArr[4:8] = [blockAddrLo, blockAddrHi, 0, 0]
btArr[8:12] = [blockAddrLo, blockAddrHi, 0, 0]
btArr[12:16] = [0xAA, 0xAA, 0xAA, 0xAA]
ser.write(btArr)

## waiting for response
while True:
rdData = ser.read(4)
if rdData == bytes([0x55, 0x55, 0x55, 0x55]):
break

#send data
sendTimes = 0
for q in range(2):
img = cv.imread('./images/'+str(2*p+q)+'.bmp')
btArr = bytearray()
for i in range(h):
for j in range(w):
btArr.append(img.item(i,j,0))
## send group, there are 16 bytes in a group
if len(btArr) == 16:
ser.write(btArr)
sendTimes = sendTimes + 1

if sendTimes%128 == 0:
sendTimes = 0
## waiting for flash program finish, don't send too fast
time.sleep(0.2)
btArr = bytearray()
print('Frame '+str(2*p+q)+' Finish')

## waiting for the finishing response
while True:
rdData = ser.read(4)
if rdData == bytes([0xAA, 0xAA, 0xAA, 0xAA]):
print('Finish Group '+str(p+1))
break

DSP具体实现细节

  6678上具体实现的功能大致如上图所示,还有一些初始化、中断、发送响应等没有在上图中画出。UART在接收到16字节数据后可以产生中断或者DMA事件,合理地配置DMA的PaRAM,就可以让DMA自动完成从数据寄存器到ping-pong缓冲区的数据搬运。在每次DMA传完一个缓冲区后,产生DMA完成中断,告知CPU把缓冲区的数据写到Flash里。

UART

  UART是一次发送或者接收一个字节,但数据寄存器是4字节的。所以会有这样的现象:在UART发数据的时候,往寄存器写一次数据,TX信号上就会有4个字节被发送。接收也是一样,是4个字节4个字节接收。
  UART有一个状态寄存器,从里面可以看到当前接收FIFO内的数据个数,当设置了接收中断使能和接收FIFO半满使能时,数据达到16字节和32字节时各会产生一次中断,但如果不把数据读走,之后就不会再产生接收中断了。
  UART的中断和EDMA事件的关系:在要用EDMA时间的时候一定要开UART的中断使能。我的代码一开始是这样写的:我想在DSP等上位机发请求的时候使能UART的接收中断,在收到中断后关闭中断使能(这样后面接收数据都会由DMA去处理)。结果这样DMA也没法收到这个事件了。所以这个中断使能不能关,而是应该关系统中断的使能。
  在开DMA通道使能之前一定要先清DMA事件!!开UART接收的系统中断使能之前也需要清挂起的系统中断标识!!
关UART接收中断,打开EDMA通道使能:

1
2
3
4
5
// Enable Edma channel
CSL_edma3ClearDMAChannelEvent(hEdma3, CSL_EDMA3_REGION_GLOBAL, UART_RX_CHANNEL);
CSL_edma3DMAChannelEnable(hEdma3, CSL_EDMA3_REGION_GLOBAL, UART_RX_CHANNEL);
// Disable receive interrupt
CSL_cpintcDisableSysInterrupt(hCpintc0, UART_RXINT_NUM);

关EDMA通道使能,打开UART接收中断:

1
2
3
4
5
6
// Disable Edma channel
CSL_edma3DMAChannelDisable(hEdma3, CSL_EDMA3_REGION_GLOBAL, UART_RX_CHANNEL);

// Enable receive interrupt
CSL_cpintcClearSysInterrupt(hCpintc0, UART_RXINT_NUM);
CSL_cpintcEnableSysInterrupt(hCpintc0, UART_RXINT_NUM);

EDMA

  EDMA在使用的时候要特别注意:当数据源或者数据目的在L2空间的时候,一定要用L2的全局地址,不然不会有任何报错,只会发现DMA传输正常完成,然而数据又不在想要的地方,就非常诡异。

  DMA的PaRAM有一个link的功能,就是在一个PaRAM中的数据搬运完成之后,这个PaRAM中的设置可以更新为另一个预先设置好的PaRAM的设置。link字段是一个16bit的相对于EDMA基址寄存器的偏移地址,第一个PaRAM的偏移地址是“0x4000”,然后第二个是“0x4020”,……,因为一个PaRAM的大小是32字节。这个link的功能就可以实现PaRAM的自动更新,非常有用!!
  现在仔细想想,EMDA每次完成一个PaRAM,这个PaRAM可以变得无效(link=“0xFFFF”),或者保持不变(OPT中的static字段为1),或者用link来变成任意其他设置,可以说是非常全面了。
  设置PaRAM实现ping-pong传输。这个在6678的DMA文档里有例子,要实现一个最简单的ping-pong传输,至少要设置3个PaRAM,其中一个用作和对应事件绑定的PaRAM,并把它的link字段链接到另外两个PaRAM的其中一个;剩下两个分别设置成从相同的源数据地址搬运数据到不同的缓冲区(ping-pong缓冲区),然后把里面的link字段设置成对方的偏移地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
for(i = 1; i<257; i++){
if(i == 128 || i == 256){
paramSetup.option = CSL_FMK(TPCC_PARAM_OPT_PRIV, (Uint32)1) | //PRIV read only 1
CSL_FMK(TPCC_PARAM_OPT_PRIVID, 0) | //PRIVID read only 0
CSL_FMK(TPCC_PARAM_OPT_ITCCHEN, 0) | //Intermediate transfer completion chaining enable
CSL_FMK(TPCC_PARAM_OPT_TCCHEN, 0) | //Transfer complete chaining enable
CSL_FMK(TPCC_PARAM_OPT_ITCINTEN, 0) | //Intermediate transfer completion interrupt enable
CSL_FMK(TPCC_PARAM_OPT_TCINTEN, 1) | //Transfer complete interrupt enable
CSL_FMK(TPCC_PARAM_OPT_TCC, UART_RX_CHANNEL) | //Transfer complete code
CSL_FMK(TPCC_PARAM_OPT_TCCMOD, 0) | //Transfer complete code mode; 0 - Normal, 1 - Early completion
CSL_FMKT(TPCC_PARAM_OPT_FWID, 32) | //FIFO width in CONST mode; 8/16/32/64/128/256
CSL_FMK(TPCC_PARAM_OPT_STATIC, 0) | //Static set; 0 - Set is not static, 1 - Set is static
CSL_FMK(TPCC_PARAM_OPT_SYNCDIM, 1) | //Transfer synchronization dimension; 0 - A mode; 1 - AB mode
CSL_FMK(TPCC_PARAM_OPT_DAM, 0) | //Destination address mode 0-INCR; 1-CONST
CSL_FMK(TPCC_PARAM_OPT_SAM, 1); //Source address mode 0-INCR; 1-CONST
}
else{
paramSetup.option = CSL_FMK(TPCC_PARAM_OPT_PRIV, (Uint32)1) | //PRIV read only 1
CSL_FMK(TPCC_PARAM_OPT_PRIVID, 0) | //PRIVID read only 0
CSL_FMK(TPCC_PARAM_OPT_ITCCHEN, 0) | //Intermediate transfer completion chaining enable
CSL_FMK(TPCC_PARAM_OPT_TCCHEN, 0) | //Transfer complete chaining enable
CSL_FMK(TPCC_PARAM_OPT_ITCINTEN, 0) | //Intermediate transfer completion interrupt enable
CSL_FMK(TPCC_PARAM_OPT_TCINTEN, 0) | //Transfer complete interrupt enable
CSL_FMK(TPCC_PARAM_OPT_TCC, 0) | //Transfer complete code
CSL_FMK(TPCC_PARAM_OPT_TCCMOD, 0) | //Transfer complete code mode; 0 - Normal, 1 - Early completion
CSL_FMKT(TPCC_PARAM_OPT_FWID, 32) | //FIFO width in CONST mode; 8/16/32/64/128/256
CSL_FMK(TPCC_PARAM_OPT_STATIC, 0) | //Static set; 0 - Set is not static, 1 - Set is static
CSL_FMK(TPCC_PARAM_OPT_SYNCDIM, 1) | //Transfer synchronization dimension; 0 - A mode; 1 - AB mode
CSL_FMK(TPCC_PARAM_OPT_DAM, 0) | //Destination address mode 0-INCR; 1-CONST
CSL_FMK(TPCC_PARAM_OPT_SAM, 1); //Source address mode 0-INCR; 1-CONST
}
paramSetup.srcAddr = (Uint32)&(hUart->DATA);
paramSetup.aCntbCnt = 0x00010010;//aCnt = 16, bCnt = 1
paramSetup.dstAddr = (Uint32)(buffer+((i-1)<<4));
paramSetup.srcDstBidx = 0x00100000;
if(i == 256){
paramSetup.linkBcntrld = 0x00044020;
}
else{
paramSetup.linkBcntrld = 0x00044000+((i+1)<<5);
}
paramSetup.srcDstCidx = 0x00000000;
paramSetup.cCnt = 0x00000001;
if(i == 1){
hParam = CSL_edma3GetParamHandle(hEdma3, 0, &status);
CSL_edma3ParamSetup(hParam, &paramSetup);
}
hParam = CSL_edma3GetParamHandle(hEdma3, i, &status);
CSL_edma3ParamSetup(hParam, &paramSetup);
}

  在上面的代码中,我设置了257个PaRAM,第0个PaRAM用来和通道绑定,剩下256个PaRAM用来被link,并且是循环link。DMA每次传16个字节,需要传128次才能从UART数据寄存器读到2048个字节。

EDMA中断

  PaRAM的OPT中有一个TCC字段,用来指定完成中断号,或者chained EDMA事件。
  总共有64个EDMA中断号,本来应该是对应64个EDMA通道的,但实际上它可以任意指定,就比如我42号通道产生一个0号的EDMA中断,但是一定要把对应的中断使能才行,多个不同的通道也可以产生同一个中断,这样方便用同一个中断服务函数去处理不同的事件。

  EDMA中断还分为内部完成中断和最终完成中断,看上面的表格就很清楚。每个PaRAM的传输任务实际上是分几次完成的。A-Synchronized模式,传输控制器TC总共要以ACNT的大小,发出BCNT×CCNT次传输请求;AB-Synchronized模式,传输控制器TC总共发CCNT次传输请求。TC的每次传输请求都可以产生中断,TCINTEN可以使能最后一次传输完成中断,ITCINTEN可以使能除了最后一次意外的前几次传输完成中断。
  所以我在上面的代码中对第128个和256个PaRAM设置了传输完成中断,CPU根据这个DMA中断开始写Flash。

静态地址

  EDMA要访问FIFO有一个要求,就是要地址是256bit对齐的,就是地址线的低5bit都是0。还要求BIDX也是32字节(256bit)的整数倍,这个要求很好理解,因为地址线已经要求256bit对齐了,如果BIDX不是32字节对齐的,那么每次地址线变化BIDX,那地址线就不能满足要求了。
  但是它为什么会有这个要求呢?看到OPT里的FWID大概就能理解了,6678支持FIFO宽度最大为256字节。从FIFO读数据,读一次之后FIFO端口就出现下一个数据,所以如果真的遇到一个32字节位宽的FIFO,6678要能够一次性读写32字节数据,可能就是因为这个所以把地址线限制在256bit对齐。
  所以这个地址CONST模式,我理解的最关键的就是它能够让6678将FWID长度的数据作为一个整体进行读写。
  比如现在的UART数据寄存器,就是一个数据位宽32bit,深度为8的一个FIFO。要从里面读16字节数据有很多种方法。

  • 首先这个数据寄存器的地址低5bit是0,满足256bit对齐
  • 可以把这个地址设置为CONST,FWID为32bit。ACNT = 0x0010,BCNT=0x0001,一次读16个字节。因为已经设置了CONST地址,所以TC会自动地每次读4字节,然后读4次。
  • 也可以不设置成CONST,然后ACNT=0x0004,BCNT=0x0004,这样也能实现同样的效果。
  • 把地址设置成CONST后,还有2种设置方法,比如ACNT=0x0002,BCNT=0x0008;ACNT=0x0004,BCNT=0x0004实现的也都是相同的功能,但是没必要这样设置。

最终结果

  用四小时从串口发了125M的图像数据,写入到DSP外接的NAND Flash 中。