STM32裸机编程指南-8
系列目录
- STM32裸机编程指南-1,存储和寄存器相关知识
- STM32裸机编程指南-2,更易读的外设寄存器编程
- STM32裸机编程指南-3,启动代码和向量表
- STM32裸机编程指南-4,Makefile构建自动化
- STM32裸机编程指南-5,闪烁LED
- STM32裸机编程指南-6,用SysTick中断实现闪烁
- STM32裸机编程指南-7,添加串口调试输出
- STM32裸机编程指南-8,重定向
printf()
到串口 - STM32裸机编程指南-9,用Segger Ozone进行调试
- STM32裸机编程指南-10,供应商CMSIS头文件
- STM32裸机编程指南-11,配置时钟
- STM32裸机编程指南-12,带设备仪表盘的网络服务器
重定向printf()
到串口
在这一节,我们将 uart_write_buf()
调用替换为 printf()
,它使我们能够进行格式化输出,这样可以更好的输出诊断信息,实现了“打印样式的调试”。
我们使用的GNU ARM工具链除了包含GCC编译器和一些工具外,还包含了一个被称为newlib的C库,由红帽为嵌入式系统开发。
如果我们的固件调用了一个标准C库函数,比如 strcmp()
,newlib就会被GCC链接器加到我们的固件中。
newlib实现了一些标准C函数,特别是文件输入输出操作,并且被实现的很随潮流:这些函数最终调用一组被称为 “syscalls” 的底层输入输出函数。
例如:
fopen()
最终调用_open()
fread()
最终调用_read()
fwrite()
,fprintf()
,printf()
最终调用_write()
malloc
最终调用_sbrk()
,等等
因此,通过修改 _write()
系统调用,我们可以重定向 printf()
到任何我们希望的地方,这个机制被称为 “IO retargeting”。
注意,STM32 Cube也使用ARM GCC工具链,这就是为什么Cube工程都包含 syscalls.c
文件。其它工具链,比如TI的CCS、Keil的CC,可能使用不同的C库,重定向机制会有一点区别。我们用newlib,所以修改 _write()
可以打印到串口3。
在那之前,我们先重新组织下源码结构:
- 把所有API定义放到
mcu.h
文件中 - 把启动代码放到
startup.c
文件中 - 为newlib的系统调用创建一个空文件
syscalls.c
- 修改Makefile,把
syscalls.c
和startup.c
加到build中
将所有 API 定义移动到 mcu.h
后,main.c
文件变得相当紧凑。注意我们还没提到底层寄存器,高级API函数很容易理解:
1 |
|
现在我们把 printf()
重定向到串口3,在空的 syscalls.c
文件中拷入一下内容:
1 |
|
这段代码:如果我们写入的文件描述符是 1(这是一个标准输出描述符),则将缓冲区写入串口3,否则忽视。这就是重定向的本质!
重新编译,会得到一些链接器错误:
1 | ../../arm-none-eabi/lib/thumb/v7e-m+fp/hard/libc_nano.a(lib_a-sbrkr.o): in function `_sbrk_r': |
这是因为我们使用了newlib的标准输入输出函数,那么就需要把newlib中其它的系统调用也实现。加入一些简单的什么都不做的桩函数:
1 | int _fstat(int fd, struct stat *st) { |
再重新编译,应该就不会报错了。
最后一步,将 main()
中 uart_write_buf()
替换为 printf()
,并打印一些有用的信息,比如LED状态和当前s_ticks的值:
1 | printf("LED: %d, tick: %lu\r\n", on, s_ticks); // Write message |
再重新编译,串口输出应该像这样:
1 | LED: 1, tick: 250 |
可喜可贺!我们学习了IO重定向是如何工作的,并且可以用打印输出来调试固件了。
完整工程源码可以在 step-4-printf 文件夹找到。