在Linux内核调试的时候,最开始因为设备驱动没有初始化,串口也不能正常的访问,而内核好像也不能通过一般的Jlink调试,这个具体原因还不清楚,只是现象上看断点停掉之后就不会继续往下运行(好像和之前的一个bug有点类似呀),先不管这些,总之我们需要有一个可以观察内核运行情况的东西,嵌入式中最为常用的当然就是printf了,虽然耗时,但是简单易用,下面就给出一个即使设备驱动没有初始化也能开始打印的方法。
内核刚启动时候运行汇编代码的时候
在内核启动的初期,代码都是汇编代码,这时候用printf好像有点麻烦,但是内核代码其实也集成了一些调试代码,稍加修改就可以实现串口的打印,比如在head-common.S中有这么一个汇编函数的定义:
arch/arm/kernel/head-common.S
192 __error_p:
193 #ifdef CONFIG_DEBUG_LL
194 adr r0, str_p1
195 bl printascii
196 mov r0, r9
197 bl printhex8
198 adr r0, str_p2
199 bl printascii
200 b __error
201 str_p1: .asciz "\nError: unrecognized/unsupported processor variant (0x"
202 str_p2: .asciz ").\n"
203 .align
204 #endif
205 ENDPROC(__error_p)
这个实际就调用了一个打印的代码,这个里面打印了一个hex8字符,还有一个字符串,我们来看看printascii是怎么实现的,这个函数是在debug.S中定义的
arch/arm/kernel/debug.S
80 ENTRY(printascii)
81 addruart_current r3, r1, r2
82 b 2f
83 1: waituart r2, r3
84 senduart r1, r3
85 busyuart r2, r3
86 teq r1, #'\n'
87 moveq r1, #'\r'
88 beq 1b
89 2: teq r0, #0
90 ldrneb r1, [r0], #1
91 teqne r1, #0
92 bne 1b
93 ret lr
94 ENDPROC(printascii)
这个函数里面调用了waituart, senduart, busyuart几个函数,这几个函数是平台相关的,再继续找一下我们看到,他们的定义是在arch/arm/include/debug/stm32.S中实现的,
arch/arm/include/debug/stm32.S
12 #if defined(CONFIG_ARCH_STM32F7)
13 # define STM32_USART_SR (0x1C) /* Interrupt&Status Register */
14 # define STM32_USART_DR (0x28) /* Transmit Data Register */
15 #else
16 # define STM32_USART_SR (0x00) /* Status Register */
17 # define STM32_USART_DR (0x04) /* Data Register */
18 #endif
19
20 #define STM32_USART_SR_TXE (1 << 7) /* Transmitter Empty */
21
22 .macro addruart, rp, rv, tmp
23 ldr \rp, =CONFIG_DEBUG_UART_PHYS @ phys address
24 ldr \rv, =CONFIG_DEBUG_UART_VIRT @ virt address
25 .endm
26
27 .macro senduart,rd,rx
28 strb \rd, [\rx, #(STM32_USART_DR)] @ Write to Data Register
29 .endm
30
31 .macro waituart,rd,rx
32 1001: ldr \rd, [\rx, #(STM32_USART_SR)] @ Read Status Register
33 tst \rd, #STM32_USART_SR_TXE @ TXE = 1 when TDR shifted
34 beq 1001b @ branch if TXE = 0
35 .endm
36
37 .macro busyuart,rd,rx
38 1001: ldr \rd, [\rx, #(STM32_USART_SR)] @ Read Status Register
39 tst \rd, #STM32_USART_SR_TXE @ TXE = 0 when TDR has data
40 beq 1001b @ branch if TXE = 0
41 .endm
这里面定义了和uart相关的几个宏,这个其实也比较简单,一个是uart的状态寄存器,用来轮询uart数据是否发送完成,一个是数据寄存器,用来向uart发送数据,比如移植到RT1050之后,我们就可以通过修改寄存器的地址和状态位的位移实现一个简单的串口。
注意,如果需要使用汇编阶段的串口需要定义CONFIG_DEBUG_LL_INCLUDE这个宏,比如:
#define CONFIG_DEBUG_LL_INCLUDE "debug/stm32.S"
这样相应的汇编文件会包含到debug.S中,从而实现调试信息的打印。
当然还有以下两个宏需要定义
2542 CONFIG_DEBUG_UART_PHYS=0x40184000
2543 CONFIG_DEBUG_UART_VIRT=0x40184000
2544 CONFIG_DEBUG_UNCOMPRESS=y
2545 CONFIG_UNCOMPRESS_INCLUDE="debug/uncompress.h"
2546 CONFIG_EARLY_PRINTK=y
完成以上修改之后就可以实现汇编阶段的打印了。
C语言阶段的打印
汇编阶段的打印是比较简单的,当程序可以运行c语言代码的时候,就应该祭出我们常用的调试利器printf了,在源代码中我们发现内核也贴心的实现了相应的函数,不过里面的Printf并不是把信息打印到串口上,而是保存在一个char buffer里面,当串口初始化完成之后再一股脑打印出来。这个对我们的调试毫无用处呀,毕竟很多错误都是在串口初始化之前出现的,难道要盲调?当然不是,我们可以将这个printf稍微修改一下,加点代码就可以实现数据到串口的打印,我们首先看看printf的具体实现。
kernel/printk/printk.c
1974 asmlinkage __visible int printk(const char *fmt, ...)
1975 {
1976 va_list args;
1977 int r;
1978
1979 va_start(args, fmt);
1980 r = vprintk_func(fmt, args);
1981 va_end(args);
1982
1983 return r;
1984 }
1985 EXPORT_SYMBOL(printk);
以上是printk的定义,原谅我没法继续往下追,因为vprintk_func = this_cpu_read(printk_func);这个一大堆的宏定义,看起来还和线程相关的,我们的目的是在初始化的时候调用这个函数,和线程没有一毛钱关系呀,看来此路不通。
偶然间想到还有一个early_printf,我们来看看这个东西是怎么一回事
1980 #ifdef CONFIG_EARLY_PRINTK
1981 struct console *early_console;
1982
1983 asmlinkage __visible void early_printk(const char *fmt, ...)
1984 {
1985 va_list ap;
1986 char buf[512];
1987 int n;
1988
1989 if (!early_console)
1990 return;
1991
1992 va_start(ap, fmt);
1993 n = vscnprintf(buf, sizeof(buf), fmt, ap);
1994 va_end(ap);
1995
2010 early_console->write(early_console, buf, n);
2011 }
2012 #endif
这个就是early_printk的实现,要使用这个函数需要定义CONFIG_EARLY_PRINTK这个宏,这个的可改造性比较强,因为里面定义了char buf,并且对各个变量进行了处理,就是这个函数在哪里调用的我还不是特别清楚。另外early_console在哪里定义的也要研究一下。
不过n = vscnprintf(buf, sizeof(buf), fmt, ap);这个我们可以利用一下,把他用来解析printk中的格式字符串,然后将结果依次通过串口发送出去,为了简单起见,我直接用固定地址访问串口的寄存器,毕竟调试完了就可以删掉了呀。
1895 asmlinkage __visible int printk(const char *fmt, ...)
1896 {
1897 printk_func_t vprintk_func;
1898 va_list args;
1899 int r;
1900 int n;
1901 char buf[512];
1902
1903 va_start(args, fmt);
1904
1905 /*
1906 * If a caller overrides the per_cpu printk_func, then it needs
1907 * to disable preemption when calling printk(). Otherwise
1908 * the printk_func should be set to the default. No need to
1909 * disable preemption here.
1910 */
1911 vprintk_func = this_cpu_read(printk_func);
1912 r = vprintk_func(fmt, args);
1913 n = vscnprintf(buf, sizeof(buf), fmt, args);
1914 va_end(args);
1915
1916 ////////// DEBUG by Major ////////////////
1917 int *uart_data;
1918 int *uart_status;
1919 int temp;
1920 int i;
1921 uart_data = (int*)0x4018401c;
1922 uart_status = (int*)0x40184014;
1923 for(i = 0; i< n; i++){
1924 while(!((*uart_status) & (1<<23)));
1925 temp = (*uart_data)&0xffffff00;
1926 *uart_data = (int)(temp|(buf[i]));
1927 }
1928 while(!((*uart_status) & (1<<23)));
1929 temp = (*uart_data)&0xffffff00;
1930 *uart_data = (int)(temp|'\r');
1931 ////////// DEBUG by Major ////////////////
1932
1933
1934 return r;
1935 }
1936 EXPORT_SYMBOL(printk);
以上就是改造之后的函数,经过改造之后就可以在串口上未初始化的时候打印一些必要的调试信息,并且支持格式化字符串的打印。