在调试 DSP 程序的时候,不可避免会用到 C 语言运行时提供的一些标准输入/输出函数来获取或输出一些调试信息。但是,在使用 CCS 集成开发环境时,这些调试信息往往是通过 CCS Console 窗口来输入输出的,当程序固化在 Flash 自启动时,这些调试输入输出就不能够使用了。如果,可以使这些标准输出从串口输出,这样不论调试还是固化之后都可以很方便的查看调试信息。 这里不得不要提到DSP 的 C++/C 语言运行时支持库(Runtime Support Library,即 RTS 库)。RTS 库都是类似这样的名称,rts6600_elf.lib / rts6740_elf.lib / rts6740e.lib 等等,其中 ELF 代表库二进制格式为 ELF,需要注意的是同一工程中链接的库必须是同一种格式的,COFF 或者 ELF,不能够混合使用,也不支持 COFF 格式与 ELF 格式库文件互相转换。rts6740e.lib 中的字母 e 代表大端字节序。库名中的数字代表 DSP CPU 架构,6400,6740,6600等等。 1. #include <stdio.h>; 2. 3. void main() 4. { 5. FILE *fd; 6. 7. fd= fopen("C:/test.txt", "w"); 8. fprintf(fd,"Hello, Tronlong!\n"); 9. fclose(fd); 10. 11. printf("Hello again, Tronlong!\n"); 12. }
在 CCS 下运行这段程序的效果是,在 C 盘根目录下新建 test.txt 文本文件,并在该文件中写入字符串“Hello, Tronlong!”,然后在 CCS Console 窗口输出 “Hello again, Tronlong!”。这就是 C I/O 函数的主要功能,提供一种便捷的方式与上位机交互。 但是,最常用的 C I/O 函数是类似 printf 这样可以输出调试文本的函数。
1、printf 输出到 CCS Console 1. #include <stdio.h> 2. 3. int main(void) 4. { 5. printf("Hello Tronlong!"); 6. 7. for(;;) 8. { 9. 10. } 11. } 这一段简单的 C 程序,很容易就可以知道输出结果,但是当在开发板上实际运行的时候,会发现根本不会输出“Hello Tronlong!”。为什么呢?C 语言标准输出(Standard Output,stdout)默认是缓冲的,而且是行缓冲(Line buffered)。只有当遇到行结束符('\n')的时候才会输出。当然,在缓冲区满或者程序退出的时候也会输出。 将第5行,修改为 printf("Hello Tronlong!\n"); 即可正常输出。 此外,最后的 for(;;) { } 循环是为了避免程序退出。如果程序退出会出现如下的提示,注意这个是提示,既不是警告更不是错误。
该提示是说找不到当前执行语句(图中是 /tmp/TI_MKLIBps1kJb/SRC/exit.c 这个文件,一般出现在 DSP 程序执行完成后也就是 main 函数返回后)所对应的源码(在这里,它只是一个 for 无限循环而已)。可以按下图操作找到源码。 点击定位文件...(Locate file...)按钮
在弹出的对话框定位到编译工具链的安装路径,图示路径为 D:\Project\Ti\c6000_7.4.16\lib\src。
可以看到 DSP 程序运行在 _CODE_ACCESS void abort(void) 函数中的 for (;;); /* CURRENTLY, THIS SPINS FOREVER */ 语句。
2、C I/O结构 C I/0 函数逻辑上分为三层,上层(High Level),底层(Low Level)和设备驱动层(Device-driver Level)。 上层函数是标准 C 库 I/O 流(printf, scanf, fopen, getchar等)。这些函数调用单个或者多个底层 I/O 函数来实现上层 I/O 请求。这些上层 I/O 以文件指针的方式操作,也被称为流(Stream)。使用上层 I/O 函数可以确保应用程序的可移植性,使用这些函数需要包含 stdio.h 头文件。 底层 I/O 函数由七个基本函数组成:open, read, write, close, lseek, rename 和 unlink。这些函数提供上层函数与设备驱动函数之间的接口,而设备驱动函数提供在特定硬件的的 I/O 操作。 底层函数被设计为适合所有 I/O 操作,即使操作的对象并不是实际的磁盘文件。其实,所有的 I/O 操作都可以抽象为文件操作,比如,Linux 之类的类 Unix 系统就是用设备文件来操作硬件外设的。 打开文件 I/O 语法 #include <file.h> int open(const char * path , unsigned flags , int file_descriptor);
关闭文件 I/O 语法 #include <file.h> int close (int file_descriptor ); 从文件读取字符 语法 #include <file.h> 关闭文件 I/O 语法 #include <file.h> int close (int file_descriptor );
写字符到文件 语法 #include <file.h> int write (int file_descriptor , const char * buffer , unsigned count ); 设置文件指针位置 语法 #include <file.h> off_t lseek (int file_descriptor , off_t offset , int origin ); 删除文件 语法 #include <file.h> int unlink (const char * path ); 重命名文件 语法 #include <file.h> int rename (const char * old_name , const char * new_name );
3、实现重定向 现在目标就很明确了,就是要实现串口操作的那7个底层 I/O 函数。RTS 库中提供了这样一个函数 add_device 用来添加设备,也就是把一个开发人员定义的设备注册到 I/O 设备表中。 _DECL _CODE_ACCESS int add_device( char *name, unsigned flags, int (*dopen)(const char *path, unsigned flags, int llv_fd), int (*dclose)(int dev_fd), int (*dread)(int dev_fd, char *buf, unsigned count), int (*dwrite)(int dev_fd, const char *buf, unsigned count), off_t (*dlseek)(int dev_fd, off_t offset, int origin), int (*dunlink)(const char *path), int (*drename)(const char *old_name, const char *new_name)); 这个函数,除了前两个参数,剩下的参数就是那7个函数的函数指针,现在定义一个串口设备,其中 UART 即设备名称,在后面打开设备的时候要用到, _SSA 代表只能打开一个文件实例,如果实现支持对多个串口外设操作,这里可以改成 _MSA 以便支持多实例。 1. add_device("UART", _SSA, UART_open, UART_close, UART_read, UART_write, UART_lseek, UART_unlink, UART_rename); 下一步就需要编写这7个函数的源码,在这里以 TL665x-EasyEVM 为例,包括但不仅限于该平台,理论上支持 TI 现在全部的 DSP 平台。 1. int UART_open(const char *path, unsigned flags, int fno) 2. { 3. BoardUART *uart0cfg = (BoardUART *)malloc(sizeof(BoardUART)); 4. 5. uart0cfg->ID = BoardUART0; 6. uart0cfg->BaudRate = BAUD_115200; 7. uart0cfg->config = UART_WORDL_8BITS; 8. uart0cfg->OverSampRate = UART_OVER_SAMP_RATE_16; 9. UARTInit(uart0cfg); 10. 11. return (int)uart0cfg; 12. } 13. 14. int UART_close(int fno) 15. { 16. if(fno) 17. { 18. free((void *)fno); 19. } 20. 21. return 0; 22. } 23. 24. int UART_read(int fno, char *buffer, unsigned count) 25. { 26. 27. return 0; 28. } 29. 30. int UART_write(int fno, const char *buffer, unsigned count) 31. { 32. UARTPuts((BoardUART *)fno, (char *)buffer, count); 33. 34. return 0; 35. } 36. 37. off_t UART_lseek(int fno, off_t offset, int origin) 38. { 39. 40. return 0; 41. } 42. 43. int UART_unlink(const char *path) 44. { 45. 46. return 0; 47. } 48. 49. int UART_rename(const char *old_name, const char *new_name) 50. { 51. 52. return 0; 53. }
这一层即所谓的设备驱动层,需要实现具体的硬件外设控制,方法有很多,可以直接读写寄存器,可以使用 CSL 库等等。在这里使用 C665x 平台的 Tronlong.DSP.Driver.le66 库来操作串口外设的,不过7个底层 I/O 函数中 UART_lseek / UART_unlink / UART_rename 对于抽象为文件的硬件外设是没有太大意义的这里就没有实现。 最后一步操作,就是把 C 语言标准输出重定向到我们刚才新增加的 I/O 设备,UART:/uart0 冒号(:)后面代表具体的路径,可选参数,也可以只写成 UART:。如果底层函数支持多个串口外设,可用在后面增加路径,这个路径会作为字符串传递到 open 函数。 前文提到过标准输出是行缓冲的,为了可用马上在串口看到输出结果,这里使用 setvbuf 函数禁用缓冲。
1. freopen("UART:/uart0", "w", stdout); // 重定向 stdout 到串口 2. setvbuf(stdout, NULL, _IONBF, 0); // 禁用 stdout 缓冲区 再次运行前面的例程,可用看到 printf 被输出到了串口。
如果直接通过文件 I/O 对实际硬件操作是怎么样的呢?下面以 LED 为例,虽然看起来比较奇怪,但确实是可行的方案。 1. int main(void) 2. { 3. /* 基本外设初始化 */ 4. SystemInit(); 5. 6. printf("Hello world!\r\n"); 7. 8. FILE *led1, *led2, *leds; 9. 10. led1 = fopen("LED:/led1", "w"); // 打开设备 11. led2 = fopen("LED:/led2", "w"); 12. leds = fopen("LED:/leds", "w"); 13. 14. setbuf(led1, 0); // 禁用缓冲区 15. setbuf(led2, 0); 16. setbuf(leds, 0); 17. 18. // 主循环 19. for(;;) 20. { 21. // 延时(非精确) 22. Delay(0x00FFFFFF); 23. fputc(0x00, led1); 24. fputc(0x01, led2); 25. fputc(0x18, leds); 26. 27. // 延时(非精确) 28. Delay(0x00FFFFFF); 29. fputc(0x01, led1); 30. fputc(0x00, led2); 31. fputc(0x14, leds); 32. 33. // 延时(非精确) 34. Delay(0x00FFFFFF); 35. fputc(0x0C, leds); 36. } 37. 38. // fclose(led); 39. }
1. int LED_open(const char *path, unsigned flags, int fno) 2. { 3. // 返回的文件描述符 0/1/2 系统保留 4. // 如果使用会被自动顺延 5. 6. KickUnlock(); 7. 8. if(!strcmp(path, "/led1")) 9. { 10. // 核心板 LED 11. GPIOPinMuxSet(SOC_DSC_BASE_REGS + SOC_DSC_PIN_CONTROL_0, GPIO26_UARTCTS1, GPIO_NORMAL_ENABLED); 12. GPIODirModeSet(SOC_GPIO_0_REGS, GPIO26_UARTCTS1, GPIO_DIR_OUTPUT); 13. 14. return 0x00000011; 15. } 16. else if(!strcmp(path, "/led2")) 17. { 18. // 核心板 LED 19. GPIOPinMuxSet(SOC_DSC_BASE_REGS + SOC_DSC_PIN_CONTROL_0, GPIO27_UARTRTS1, GPIO_NORMAL_ENABLED); 20. GPIODirModeSet(SOC_GPIO_0_REGS, GPIO27_UARTRTS1, GPIO_DIR_OUTPUT); 21. 22. return 0x00000012; 23. } 24. else if(!strcmp(path, "/led3")) 25. { 26. // 底板 LED 27. GPIOPinMuxSet(SOC_DSC_BASE_REGS + SOC_DSC_PIN_CONTROL_0, GPIO19_TIMO1, GPIO_NORMAL_ENABLED); 28. GPIODirModeSet(SOC_GPIO_0_REGS, GPIO19_TIMO1, GPIO_DIR_OUTPUT); 29. 30. return 0x00000013; 31. } 32. else if(!strcmp(path, "/led4")) 33. { 34. // 底板 LED 35. GPIOPinMuxSet(SOC_DSC_BASE_REGS + SOC_DSC_PIN_CONTROL_0, GPIO22_UARTCTS0, GPIO_NORMAL_ENABLED); 36. GPIODirModeSet(SOC_GPIO_0_REGS, GPIO22_UARTCTS0, GPIO_DIR_OUTPUT); 37. 38. return 0x00000014; 39. } 40. else if(!strcmp(path, "/led5")) 41. { 42. // 底板 LED 43. GPIOPinMuxSet(SOC_DSC_BASE_REGS + SOC_DSC_PIN_CONTROL_0, GPIO23_UARTRTS0, GPIO_NORMAL_ENABLED); 44. GPIODirModeSet(SOC_GPIO_0_REGS, GPIO23_UARTRTS0, GPIO_DIR_OUTPUT); 45. 46. return 0x00000015; 47. } 48. else if(!strcmp(path, "/leds")) 49. { 50. // 核心板 LED 51. GPIOPinMuxSet(SOC_DSC_BASE_REGS + SOC_DSC_PIN_CONTROL_0, GPIO26_UARTCTS1, GPIO_NORMAL_ENABLED); 52. GPIOPinMuxSet(SOC_DSC_BASE_REGS + SOC_DSC_PIN_CONTROL_0, GPIO27_UARTRTS1, GPIO_NORMAL_ENABLED); 53. 54. GPIODirModeSet(SOC_GPIO_0_REGS, GPIO26_UARTCTS1, GPIO_DIR_OUTPUT); 55. GPIODirModeSet(SOC_GPIO_0_REGS, GPIO27_UARTRTS1, GPIO_DIR_OUTPUT); 56. 57. // 底板 LED 58. GPIOPinMuxSet(SOC_DSC_BASE_REGS + SOC_DSC_PIN_CONTROL_0, GPIO19_TIMO1, GPIO_NORMAL_ENABLED); 59. GPIOPinMuxSet(SOC_DSC_BASE_REGS + SOC_DSC_PIN_CONTROL_0, GPIO22_UARTCTS0, GPIO_NORMAL_ENABLED); 60. GPIOPinMuxSet(SOC_DSC_BASE_REGS + SOC_DSC_PIN_CONTROL_0, GPIO23_UARTRTS0, GPIO_NORMAL_ENABLED); 61. 62. GPIODirModeSet(SOC_GPIO_0_REGS, GPIO19_TIMO1, GPIO_DIR_OUTPUT); 63. GPIODirModeSet(SOC_GPIO_0_REGS, GPIO22_UARTCTS0, GPIO_DIR_OUTPUT); 64. GPIODirModeSet(SOC_GPIO_0_REGS, GPIO23_UARTRTS0, GPIO_DIR_OUTPUT); 65. 66. return 0x00000020; 67. } 68. else 69. { 70. return -1; 71. } 72. } 73. 74. int LED_close(int fno) 75. { 76. 77. return 0; 78. } 79. 80. int LED_read(int fno, char *buffer, unsigned count) 81. { 82. 83. return 0; 84. } 85. 86. int LED_write(int fno, const char *buffer, unsigned count) 87. { 88. switch(fno) 89. { 90. case 0x11 : *buffer ? GPIOPinWrite(SOC_GPIO_0_REGS, GPIO26_UARTCTS1, GPIO_PIN_HIGH) : \ 91. GPIOPinWrite(SOC_GPIO_0_REGS, GPIO26_UARTCTS1, GPIO_PIN_LOW); break;
92. case 0x12 : *buffer ? GPIOPinWrite(SOC_GPIO_0_REGS, GPIO27_UARTRTS1, GPIO_PIN_HIGH) : \ 93. GPIOPinWrite(SOC_GPIO_0_REGS, GPIO27_UARTRTS1, GPIO_PIN_LOW); break; 94. 95. case 0x13 : *buffer ? GPIOPinWrite(SOC_GPIO_0_REGS, GPIO19_TIMO1, GPIO_PIN_HIGH) : \ 96. GPIOPinWrite(SOC_GPIO_0_REGS, GPIO19_TIMO1, GPIO_PIN_LOW); break; 97. 98. case 0x14 : *buffer ? GPIOPinWrite(SOC_GPIO_0_REGS, GPIO22_UARTCTS0, GPIO_PIN_HIGH) : \ 99. GPIOPinWrite(SOC_GPIO_0_REGS, GPIO22_UARTCTS0, GPIO_PIN_LOW); break; 100. 101. case 0x15 : *buffer ? GPIOPinWrite(SOC_GPIO_0_REGS, GPIO23_UARTRTS0, GPIO_PIN_HIGH) : \ 102. GPIOPinWrite(SOC_GPIO_0_REGS, GPIO23_UARTRTS0, GPIO_PIN_LOW); break; 103. 104. case 0x20 : GPIOPinWrite(SOC_GPIO_0_REGS, GPIO26_UARTCTS1, (*buffer >> 0) & 0x01); 105. GPIOPinWrite(SOC_GPIO_0_REGS, GPIO27_UARTRTS1, (*buffer >> 1) & 0x01); 106. GPIOPinWrite(SOC_GPIO_0_REGS, GPIO19_TIMO1, (*buffer >> 2) & 0x01); 107. GPIOPinWrite(SOC_GPIO_0_REGS, GPIO22_UARTCTS0, (*buffer >> 3) & 0x01); 108. GPIOPinWrite(SOC_GPIO_0_REGS, GPIO23_UARTRTS0, (*buffer >> 4) & 0x01); break; 109. } 110. 111. return 0; 112.} 113. 114.off_t LED_lseek(int fno, off_t offset, int origin) 115.{ 116. 117. return 0; 118.} 119. 120.int LED_unlink(const char *path) 121.{ 122. 123. return 0; 124.} 125. 126.int LED_rename(const char *old_name, const char *new_name) 127.{ 128. 129. return 0; 130.}
可能通过这种方式操作硬件,最合适的外设就是 EEPROM / Flash 这种存储类型的外设。
|