小识堂 | 重新定向 printf 输出到串口 ,你知多少???
重新定向 printf 输出到串口
概述
在调试 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等等。
而 RTS 库中,很大一部分函数是 C I/O 函数。在 CCS 集成开发环境下 TI 指定了一套协议通过 C I/O 函数与运行着 CCS 的电脑进行通信。(请参阅 http://processors.wiki.ti.com/index.php/CIO_System_Call_Protocol 获取 C I/O 的详细协议说明)比如,可以通过 C 语言文件操作函数 fopen / fread / fwrite 等函数直接读取电脑上的文件进行处理或者使用 clock() 函数来测量时间等等。
1. #include;
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
1、printf 输出到 CCS Console
1. #include
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
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
int open(const char * path , unsigned flags , int file_descriptor);
关闭文件 I/O
语法
#include
int close (int file_descriptor );
从文件读取字符
语法
#include
关闭文件 I/O
语法
#include
int close (int file_descriptor );
写字符到文件
语法
#include
int write (int file_descriptor , const char * buffer , unsigned count );
设置文件指针位置
语法
#include
off_t lseek (int file_descriptor , off_t offset , int origin );
删除文件
语法
#include
int unlink (const char * path );
重命名文件
语法
#include
int rename (const char * old_name , const char * new_name );
3
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 这种存储类型的外设。
广州创龙,你身边的主板定制专家!
每周一堂课,动动手指头