1 | /* |
---|
2 | * Data processing function. |
---|
3 | * |
---|
4 | */ |
---|
5 | |
---|
6 | #include <linux/init.h> |
---|
7 | #include <linux/wait.h> |
---|
8 | #include <linux/module.h> |
---|
9 | |
---|
10 | #include <linux/version.h> |
---|
11 | |
---|
12 | #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,32) |
---|
13 | #include <linux/semaphore.h> |
---|
14 | #else |
---|
15 | #include <asm/semaphore.h> |
---|
16 | #endif |
---|
17 | |
---|
18 | #include <asm/io.h> |
---|
19 | #include <asm/cacheflush.h> |
---|
20 | |
---|
21 | #include <dmxdev.h> |
---|
22 | #include <dvb_demux.h> |
---|
23 | #include <linux/dvb/dmx.h> |
---|
24 | |
---|
25 | #include "pti.h" |
---|
26 | #include "pti_main.h" |
---|
27 | #include "pti_hal.h" |
---|
28 | #include "ts_makros.h" |
---|
29 | |
---|
30 | #define U32 u32 |
---|
31 | #define U16 u16 |
---|
32 | #define U8 u8 |
---|
33 | |
---|
34 | #ifdef WITH_CAMROUTING |
---|
35 | extern int camRouting; |
---|
36 | #endif |
---|
37 | extern int waitMS; |
---|
38 | #ifdef CONFIG_PRINTK |
---|
39 | extern int enableStatistic; |
---|
40 | #else |
---|
41 | extern int enableSysStatistic; |
---|
42 | #endif |
---|
43 | extern int max_pti_dma; |
---|
44 | extern struct TCDMAConfigExt_s *TCDMAConfigExt_t; |
---|
45 | |
---|
46 | static struct stpti *pti = NULL; |
---|
47 | extern void (*demultiplexDvbPackets)(struct dvb_demux *demux, const u8 *buf, int count); |
---|
48 | |
---|
49 | void setDmaThreshold(int a, int b) |
---|
50 | { |
---|
51 | } |
---|
52 | |
---|
53 | #ifdef CONFIG_PRINTK |
---|
54 | int pktCount; |
---|
55 | int prevPktCount; |
---|
56 | int maxDelta; |
---|
57 | int minDelta = 999999999; |
---|
58 | int loopCount = 0; |
---|
59 | int lastPktCount; |
---|
60 | #endif |
---|
61 | |
---|
62 | static wait_queue_head_t bufferHalfFull; |
---|
63 | |
---|
64 | void paceSwtsByPti(void) |
---|
65 | { |
---|
66 | if (wait_event_interruptible(bufferHalfFull, 1)) |
---|
67 | { |
---|
68 | printk("wait_event_interruptible failed\n"); |
---|
69 | return; |
---|
70 | } |
---|
71 | } |
---|
72 | |
---|
73 | EXPORT_SYMBOL(paceSwtsByPti); |
---|
74 | |
---|
75 | #ifdef CONFIG_PRINTK |
---|
76 | struct pidStatistic_s |
---|
77 | { |
---|
78 | int number; |
---|
79 | unsigned long time; |
---|
80 | |
---|
81 | unsigned long complete; |
---|
82 | unsigned int rate; |
---|
83 | |
---|
84 | unsigned int min_rate; |
---|
85 | unsigned int max_rate; |
---|
86 | |
---|
87 | u8 lastCC; |
---|
88 | u8 ccError; |
---|
89 | }; |
---|
90 | |
---|
91 | struct pidStatistic_s pidS[0xffff]; |
---|
92 | |
---|
93 | void ptiStatistic(int num, u8 *pBuf) |
---|
94 | { |
---|
95 | int i; |
---|
96 | u8 cc; |
---|
97 | /* walk over the packets, count each pid and remeber the current time */ |
---|
98 | for (i = 0; i < num; i++) |
---|
99 | { |
---|
100 | u16 pid = ts_pid(pBuf + (i * 188)); |
---|
101 | pidS[pid].number++; |
---|
102 | pidS[pid].complete++; |
---|
103 | if ((pidS[pid].time == 0) || (time_after(jiffies, pidS[pid].time + HZ /* 1 sekunde vorbei ? */))) |
---|
104 | { |
---|
105 | pidS[pid].time = jiffies; |
---|
106 | pidS[pid].rate = pidS[pid].number; |
---|
107 | pidS[pid].number = 0; |
---|
108 | if ((pidS[pid].rate > pidS[pid].max_rate) || (pidS[pid].max_rate == 0)) |
---|
109 | pidS[pid].max_rate = pidS[pid].rate; |
---|
110 | if ((pidS[pid].rate < pidS[pid].min_rate) || (pidS[pid].min_rate == 0)) |
---|
111 | pidS[pid].min_rate = pidS[pid].rate; |
---|
112 | } |
---|
113 | cc = ts_cc(pBuf + (i * 188)); |
---|
114 | if (pidS[pid].lastCC != 255) |
---|
115 | { |
---|
116 | if ((pidS[pid].lastCC + 1) % 0x10 != cc) |
---|
117 | { |
---|
118 | pidS[pid].ccError++; |
---|
119 | printk("cc error last %d exp %d got %d\n", pidS[pid].lastCC, (pidS[pid].lastCC + 1) % 0x10, cc); |
---|
120 | } |
---|
121 | } |
---|
122 | pidS[pid].lastCC = cc; |
---|
123 | } |
---|
124 | } |
---|
125 | |
---|
126 | void ptiStatisticClean(void) |
---|
127 | { |
---|
128 | int i; |
---|
129 | for (i = 0; i < 0xffff; i++) |
---|
130 | { |
---|
131 | if ((time_after(jiffies, pidS[i].time + (HZ * 5) /* 5 sekunde vorbei ? */))) |
---|
132 | { |
---|
133 | pidS[i].rate = 0; |
---|
134 | pidS[i].min_rate = 0; |
---|
135 | pidS[i].max_rate = 0; |
---|
136 | pidS[i].complete = 0; |
---|
137 | pidS[i].number = 0; |
---|
138 | pidS[i].time = 0; |
---|
139 | pidS[i].lastCC = 255; |
---|
140 | pidS[i].ccError = 0; |
---|
141 | } |
---|
142 | } |
---|
143 | } |
---|
144 | |
---|
145 | void ptiStatisticOut(void) |
---|
146 | { |
---|
147 | int i; |
---|
148 | for (i = 0; i < 0xffff; i++) |
---|
149 | { |
---|
150 | if (pidS[i].rate != 0) |
---|
151 | printk("pidstatistic: pid 0x%x, rate = %d packets/s (complete %lu) %d Mbps/s, min %d/s - max %d/s, CC error %d\n", i, |
---|
152 | pidS[i].rate, pidS[i].complete, (pidS[i].rate * 188 * 8) / (1024 * 1024), pidS[i].min_rate, pidS[i].max_rate, pidS[i].ccError); |
---|
153 | } |
---|
154 | } |
---|
155 | #else |
---|
156 | extern unsigned long pti_last_time; |
---|
157 | extern unsigned long pti_count; |
---|
158 | |
---|
159 | void ptiStatistic(int num) |
---|
160 | { |
---|
161 | pti_last_time = jiffies; |
---|
162 | pti_count += num; |
---|
163 | } |
---|
164 | #endif |
---|
165 | |
---|
166 | #define TSM_STREAM_CONF(n) (n * 0x20) |
---|
167 | #define TSM_SERIAL_NOT_PARALLEL (1 << 0) |
---|
168 | #define TSM_SYNC_NOT_ASYNC (1 << 1) |
---|
169 | #define TSM_ALIGN_BYTE_SOP (1 << 2) |
---|
170 | #define TSM_ASYNC_SOP_TOKEN (1 << 3) |
---|
171 | #define TSM_INVERT_BYTECLK (1 << 4) |
---|
172 | #define TSM_ADD_TAG_BYTES (1 << 5) |
---|
173 | #define TSM_REPLACE_ID_TAG (1 << 6) |
---|
174 | #define TSM_STREAM_ON (1 << 7) |
---|
175 | #define TSM_RAM_ALLOC_START(size) ((size & 0xff) << 8) |
---|
176 | #define TSM_PRIORITY(priority) ((priority & 0xf) << 16) |
---|
177 | |
---|
178 | #define TSM_STREAM_STATUS(n) ((n * 0x20) + 0x10) |
---|
179 | #define TSM_STREAM_LOCK (1 << 0) |
---|
180 | #define TSM_INPUTFIFO_OVERFLOW (1 << 1) |
---|
181 | #define TSM_RAM_OVERFLOW (1 << 2) |
---|
182 | #define TSM_ERRONEOUS_PACKETS(value) ((value >> 3) & 0x1f) |
---|
183 | #define TSM_STRAM_COUNTER_VALUE(value) ((value >> 8) & 0xffffff) |
---|
184 | |
---|
185 | #define TSM_STREAM_CONF2(n) ((n * 0x20) + 0x18) |
---|
186 | #define TSM_CHANNEL_RESET (1 << 0) |
---|
187 | #define TSM_DISABLE_MID_PACKET_ERROR (1 << 1) |
---|
188 | #define TSM_DISABLE_START_PACKET_ERROR (1 << 2) |
---|
189 | #define TSM_SHORT_PACKET_COUNT (value) ((value >> 27) & 0x3f) |
---|
190 | |
---|
191 | unsigned long reg_tsm_config = 0; |
---|
192 | |
---|
193 | void stm_tsm_interrupt(void) |
---|
194 | { |
---|
195 | int n; |
---|
196 | for (n = 0; n < 5; n++) |
---|
197 | { |
---|
198 | int status = readl(reg_tsm_config + TSM_STREAM_STATUS(n)); |
---|
199 | int on = readl(reg_tsm_config + TSM_STREAM_CONF(n)) & TSM_STREAM_ON; |
---|
200 | if (!on) |
---|
201 | continue; |
---|
202 | if (status & TSM_RAM_OVERFLOW) |
---|
203 | { |
---|
204 | /* ack the overflow */ |
---|
205 | writel(status & ~TSM_RAM_OVERFLOW, reg_tsm_config + TSM_STREAM_STATUS(n)); |
---|
206 | /* printk("%s: TSMerger RAM Overflow on channel %d(%x), deploying work around\n",__FUNCTION__,n,status); */ |
---|
207 | printk("OFLOW(%d) ", n); |
---|
208 | } |
---|
209 | else if (status & TSM_INPUTFIFO_OVERFLOW) |
---|
210 | { |
---|
211 | /* ack the overflow */ |
---|
212 | writel(status & ~TSM_INPUTFIFO_OVERFLOW, reg_tsm_config + TSM_STREAM_STATUS(n)); |
---|
213 | printk("I-OFLOW(%d) ", n); |
---|
214 | } |
---|
215 | } |
---|
216 | } |
---|
217 | |
---|
218 | inline static void processDmaChannel(int tc_dma_index, |
---|
219 | U32 ReadPtr_physical, |
---|
220 | U32 WritePtr_physical, |
---|
221 | TCDMAConfig_t *DMAConfig_p) |
---|
222 | { |
---|
223 | struct dvb_demux *demux = pti_hal_get_demux_from_dma_index(tc_dma_index); |
---|
224 | /* calculate the write index in the buffer */ |
---|
225 | U32 WriteInd = (WritePtr_physical - TCDMAConfigExt_t[tc_dma_index].BasePtr_physical); |
---|
226 | /* calculate the read index in the buffer */ |
---|
227 | U32 ReadInd = (ReadPtr_physical - TCDMAConfigExt_t[tc_dma_index].BasePtr_physical); |
---|
228 | /* calculate the read pointer */ |
---|
229 | U8 *pBuf_add_ReadInd = (TCDMAConfigExt_t[tc_dma_index].pBuf + ReadInd); |
---|
230 | /* decrease the index by one packet because it might be rolled back or |
---|
231 | * be incomplete |
---|
232 | |
---|
233 | * ***: laut meinen debugs sind nie unvollstaendige Pakete gekommen. |
---|
234 | * Ausserdem wuerde dem demuxer nicht vollstaendige Pakete uebergeben werden, |
---|
235 | * was dieser dann verwirft, daher auf 188 ausrichten. |
---|
236 | */ |
---|
237 | if (WriteInd >= 188) |
---|
238 | WriteInd -= 188; // normal case |
---|
239 | else |
---|
240 | WriteInd = TCDMAConfigExt_t[tc_dma_index].bufSize_sub_188; //after a wraparound (e.g. WriteInd = 0) |
---|
241 | // align to 188 |
---|
242 | WriteInd = (WriteInd / 188) * 188; |
---|
243 | #ifdef CONFIG_PRINTK |
---|
244 | prevPktCount = pktCount; |
---|
245 | #endif |
---|
246 | /* validate pointers, even if they are invalid we want to process |
---|
247 | status blocks */ |
---|
248 | if ((TCDMAConfigExt_t[tc_dma_index].pBuf != NULL) && (demux != NULL)) |
---|
249 | { |
---|
250 | struct DeviceContext_s *pContext = (struct DeviceContext_s *)demux->priv; |
---|
251 | int num = 0; |
---|
252 | if (WriteInd < ReadInd) |
---|
253 | { |
---|
254 | int bytenum = (TCDMAConfigExt_t[tc_dma_index].bufSize - ReadInd); |
---|
255 | int num1 = bytenum / 188; |
---|
256 | int num2 = WriteInd / 188; |
---|
257 | num = num1 + num2; |
---|
258 | #ifdef CONFIG_PRINTK |
---|
259 | pktCount += num; |
---|
260 | #endif |
---|
261 | if (demultiplexDvbPackets != NULL) |
---|
262 | { |
---|
263 | #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,32) |
---|
264 | // __flush_invalidate_region(pBuf_add_ReadInd, bytenum); |
---|
265 | invalidate_ioremap_region(0, pBuf_add_ReadInd, 0, bytenum); |
---|
266 | #else |
---|
267 | dma_cache_inv(pBuf_add_ReadInd, bytenum); |
---|
268 | #endif |
---|
269 | demultiplexDvbPackets(demux, pBuf_add_ReadInd, num1); // process packets before wraparound |
---|
270 | #ifdef CONFIG_PRINTK |
---|
271 | if (enableStatistic) |
---|
272 | ptiStatistic(num1, pBuf_add_ReadInd); |
---|
273 | #else |
---|
274 | if (enableSysStatistic) |
---|
275 | ptiStatistic(num1); |
---|
276 | #endif |
---|
277 | #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,32) |
---|
278 | // __flush_invalidate_region(TCDMAConfigExt_t[tc_dma_index].pBuf, WriteInd); |
---|
279 | invalidate_ioremap_region(0, TCDMAConfigExt_t[tc_dma_index].pBuf, 0, WriteInd); |
---|
280 | #else |
---|
281 | dma_cache_inv(TCDMAConfigExt_t[tc_dma_index].pBuf, WriteInd); |
---|
282 | #endif |
---|
283 | demultiplexDvbPackets(demux, TCDMAConfigExt_t[tc_dma_index].pBuf, num2); // process packets after wraparound |
---|
284 | #ifdef CONFIG_PRINTK |
---|
285 | if (enableStatistic) |
---|
286 | ptiStatistic(num2, TCDMAConfigExt_t[tc_dma_index].pBuf); |
---|
287 | #else |
---|
288 | if (enableSysStatistic) |
---|
289 | ptiStatistic(num2); |
---|
290 | #endif |
---|
291 | } |
---|
292 | //printk("+"); |
---|
293 | } |
---|
294 | else |
---|
295 | { |
---|
296 | int bytenum = (WriteInd - ReadInd); |
---|
297 | num = bytenum / 188; |
---|
298 | #ifdef CONFIG_PRINTK |
---|
299 | pktCount += num; |
---|
300 | #endif |
---|
301 | if (demultiplexDvbPackets != NULL) |
---|
302 | { |
---|
303 | #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,32) |
---|
304 | // __flush_invalidate_region(pBuf_add_ReadInd, bytenum); |
---|
305 | invalidate_ioremap_region(0, pBuf_add_ReadInd, 0, bytenum); |
---|
306 | #else |
---|
307 | dma_cache_inv(pBuf_add_ReadInd, bytenum); |
---|
308 | #endif |
---|
309 | demultiplexDvbPackets(demux, pBuf_add_ReadInd, num); |
---|
310 | #ifdef CONFIG_PRINTK |
---|
311 | if (enableStatistic) |
---|
312 | ptiStatistic(num, pBuf_add_ReadInd); |
---|
313 | #else |
---|
314 | if (enableSysStatistic) |
---|
315 | ptiStatistic(num); |
---|
316 | #endif |
---|
317 | } |
---|
318 | //printk("."); |
---|
319 | } |
---|
320 | /* if the input is the DVR device the data is played |
---|
321 | back via SWTS to enable passing the playback data |
---|
322 | though the hardware descrambler */ |
---|
323 | if (pContext->pPtiSession->source == DMX_SOURCE_DVR0) |
---|
324 | { |
---|
325 | if (num < TCDMAConfigExt_t[tc_dma_index].bufSize_div_188_div_2) |
---|
326 | { |
---|
327 | /* Buffer is less than half-empty, enable the semaphore. |
---|
328 | Enforce binary semaphore behavior, otherwise the caller |
---|
329 | wouldn't block when necessary. */ |
---|
330 | wake_up_interruptible(&bufferHalfFull); |
---|
331 | } |
---|
332 | //printk("pti_task: %d\n", num); |
---|
333 | } |
---|
334 | } |
---|
335 | //printk("pti_task: RI %d, BA %x\n", ReadInd, DMAConfig_p->BasePtr_physical); |
---|
336 | #ifdef CONFIG_PRINTK |
---|
337 | if ((pktCount - prevPktCount) > maxDelta) |
---|
338 | { |
---|
339 | maxDelta = pktCount - prevPktCount; |
---|
340 | } |
---|
341 | if ((pktCount - prevPktCount) < minDelta) |
---|
342 | { |
---|
343 | minDelta = pktCount - prevPktCount; |
---|
344 | } |
---|
345 | loopCount++; |
---|
346 | if (!(loopCount % 50) && (enableStatistic)) |
---|
347 | { |
---|
348 | printk("statistic: pktCount %d last packets %d minDelta %d maxDelta %d\n", pktCount, pktCount - lastPktCount, minDelta, maxDelta); |
---|
349 | ptiStatisticOut(); |
---|
350 | ptiStatisticClean(); |
---|
351 | lastPktCount = pktCount; |
---|
352 | minDelta = 999999999; |
---|
353 | maxDelta = 0; |
---|
354 | } |
---|
355 | #endif |
---|
356 | /* |
---|
357 | * acknowledge all packets in the block |
---|
358 | |
---|
359 | * ***: wird das abschneiden und ausrichten des WriteInd deaktiviert, |
---|
360 | * so kann WritePtr_physical verwendet werden. |
---|
361 | */ |
---|
362 | writel(TCDMAConfigExt_t[tc_dma_index].BasePtr_physical + WriteInd, (void *) &DMAConfig_p->DMARead_p); |
---|
363 | } |
---|
364 | |
---|
365 | #define SysConfigBaseAddress 0x19001000 |
---|
366 | #define SYS_CFG0 0x100 |
---|
367 | #define SYS_CFG1 0x104 |
---|
368 | |
---|
369 | #if defined(CONFIG_CPU_SUBTYPE_STX7111) || defined(CONFIG_CPU_SUBTYPE_STX7105) |
---|
370 | #define TSMergerBaseAddress 0xFE242000 |
---|
371 | #else |
---|
372 | #define TSMergerBaseAddress 0x19242000 |
---|
373 | #endif |
---|
374 | |
---|
375 | #define TSM_1394_CFG 0x0810 |
---|
376 | #define TSM_PTI_SEL 0x0200 |
---|
377 | #define TSM_1394_DEST 0x0210 |
---|
378 | |
---|
379 | unsigned long reg_sys_config = 0; |
---|
380 | |
---|
381 | int pti_task(void *data) |
---|
382 | { |
---|
383 | TCDMAConfig_t *DMAConfig_p; |
---|
384 | int vLoopDMAs; |
---|
385 | U32 WritePtr_physical; |
---|
386 | U32 ReadPtr_physical; |
---|
387 | int time_to_wait = msecs_to_jiffies(waitMS); |
---|
388 | #ifdef CONFIG_PRINTK |
---|
389 | int count = 0; |
---|
390 | #endif |
---|
391 | pti = (struct stpti *) data; |
---|
392 | daemonize("pti_task"); |
---|
393 | //set highest prio |
---|
394 | set_user_nice(current, -20); |
---|
395 | printk("pti_task: using %d dma channel\n", max_pti_dma); |
---|
396 | init_waitqueue_head(&pti->queue); |
---|
397 | init_waitqueue_head(&bufferHalfFull); |
---|
398 | while (1) |
---|
399 | { |
---|
400 | wait_event_timeout(pti->queue, 0, time_to_wait); |
---|
401 | #ifndef CONFIG_PRINTK |
---|
402 | //update pti time |
---|
403 | if (enableSysStatistic) |
---|
404 | ptiStatistic(0); |
---|
405 | #endif |
---|
406 | #ifdef CONFIG_PRINTK |
---|
407 | count++; |
---|
408 | if (count > 500) |
---|
409 | { |
---|
410 | printk("\n"); |
---|
411 | count = 0; |
---|
412 | pti_hal_output_slot_state(); |
---|
413 | } |
---|
414 | #endif |
---|
415 | /* pti_hal_output_slot_state(); */ |
---|
416 | /* Dagobert: configure the stream routing in accordance to |
---|
417 | * the scrambled state. route stream only through cimax |
---|
418 | * if screem is scrambled, otherwise directly to pti |
---|
419 | */ |
---|
420 | reg_tsm_config = (unsigned long) ioremap(TSMergerBaseAddress, 0x0900); |
---|
421 | #ifdef CONFIG_PRINTK |
---|
422 | stm_tsm_interrupt(); |
---|
423 | #endif |
---|
424 | #ifdef WITH_CAMROUTING |
---|
425 | if (camRouting == 1) |
---|
426 | { |
---|
427 | int state; |
---|
428 | if ((state = pti_hal_get_scrambled()) != -1) |
---|
429 | { |
---|
430 | u32 reg; |
---|
431 | if (reg_sys_config == 0) |
---|
432 | { |
---|
433 | reg_sys_config = (unsigned long) ioremap(SysConfigBaseAddress, 0x0900); |
---|
434 | } |
---|
435 | reg = ctrl_inl(reg_tsm_config + TSM_1394_CFG); |
---|
436 | if (state == 1) |
---|
437 | { |
---|
438 | ctrl_outl(0x6, reg_sys_config + SYS_CFG0); |
---|
439 | ctrl_outl(reg | 0x20000 , reg_tsm_config + TSM_1394_CFG); |
---|
440 | ctrl_outl(0xe , reg_tsm_config + TSM_PTI_SEL); |
---|
441 | ctrl_outl(0x1 , reg_tsm_config + TSM_1394_DEST); |
---|
442 | } |
---|
443 | else |
---|
444 | { |
---|
445 | ctrl_outl(0x2, reg_sys_config + SYS_CFG0); |
---|
446 | ctrl_outl(reg & ~ 0x20000 , reg_tsm_config + TSM_1394_CFG); |
---|
447 | ctrl_outl(0xf , reg_tsm_config + TSM_PTI_SEL); |
---|
448 | ctrl_outl(0x0 , reg_tsm_config + TSM_1394_DEST); |
---|
449 | } |
---|
450 | } |
---|
451 | } |
---|
452 | #endif |
---|
453 | for (vLoopDMAs = 0; vLoopDMAs < /* TC_Params_p->TC_NumberDMAs*/ max_pti_dma; vLoopDMAs++) |
---|
454 | { |
---|
455 | DMAConfig_p = &((TCDMAConfig_t *)tc_params.TC_DMAConfigStart)[vLoopDMAs]; |
---|
456 | ReadPtr_physical = readl((void *) &DMAConfig_p->DMARead_p); |
---|
457 | WritePtr_physical = readl((void *) &DMAConfig_p->DMAWrite_p); |
---|
458 | #if 0 |
---|
459 | if (vLoopDMAs == 1) |
---|
460 | printk("r%x w%x ", ReadPtr_physical, WritePtr_physical); |
---|
461 | #endif |
---|
462 | if (ReadPtr_physical != WritePtr_physical) |
---|
463 | { |
---|
464 | processDmaChannel(vLoopDMAs, ReadPtr_physical, WritePtr_physical, DMAConfig_p); |
---|
465 | } |
---|
466 | } |
---|
467 | } /* while true */ |
---|
468 | printk("######## PTI task terminated\n"); |
---|
469 | // TODO: stop dma |
---|
470 | } |
---|
471 | |
---|