嵌入式开发中传统可靠的调试技术是在软件中从头到尾都使用printf语句,以便随时了解系统的运行状况。但总是使用printf语句并不可取,因为它可能带来不可预料的实时性能影响。文章讨论了使用printf存在的一些基本问题,并提出了使用printf语句达到好性能的一些窍门。
与使用printf有关的问题
开发人员经常忽视使用printf带来的一些问题。首先这种技术要求开发人员在软件中嵌入标准C库,这无疑会增加ROM和RAM的使用量。
第二个问题是每次使用printf语句时,系统会进入阻塞状态,直到所有字符发送完毕。这种阻塞会导致实时性能显著下降。举个例子,以9600波特通过UART打印输出诸如“Hello World!”这个简单的字符串(这是很常见的事)。我在STM32处理器上进行了简单的时序测量,如图1所示,该字符串完成格式化并打印到终端上需要花12.5ms的时间。在这段时间内,系统什么事也不能做。
图1:打印“Hello World!”
增加任何字符串格式化的工作会使情况变得更加糟糕!使用printf语句将系统状态打印到终端上(“The system state is %d”, State)会导致21ms的应用程序延时,这个时间都用在了字符串的格式化和发送上面。有人可能会说以9600波特的速率运行太荒唐了,但即使提高到115200波特,发送这两条消息仍会导致1.05ms和1.75ms的延时。也就是说,即便对于使用程度低的信息,仍要浪费很多的处理器带宽和潜在的实时性能。
现在我们来看看如何解决这些问题。
性能窍门#1——创建非阻塞printf
目前为止我遇到的每一个printf版本都是阻塞类型的。一旦开始调用printf,应用程序都会停止执行,直到成功发送完每个字符。这太没有效率了!另外一种方法是创建非阻塞版本的printf。非阻塞printf版本将能够:
格式化字符串
将格式化后的字符串填充进发送缓冲区
启动发送个字符
让中断服务例程处理发送缓冲区中剩下的字符
继续执行代码
非阻塞printf的关键之处在于建立时间,在STM32处理器上以9600波特速度执行时的建立时间在0.8至1.8ms之间。在初始建立时间后,大约每隔1ms产生一次发送中断。中断例程随即仅需35μs就能将下一个字符填充进UART的发送寄存器,然后就可以返回执行有用的任务。图2显示了周期性的中断和中断执行时间。记住,这个执行时间不包括中断开销,在这个案例中中断开销不到25个时钟周期。
图2:非阻塞型printf性能
性能窍门#2——提高波特率
令我感到奇怪的是,即使今天的串行硬件可以处理1Mbps甚至更高的波特率,仍有许多开发人员将他们的UART设置在默认的9600波特!偶尔我也会遇到胆大的开发人员将波特率设在115200。提高时钟速率可能会发生电气或硬件相关的问题,除此之外,将波特率设为1Mbps并尽可能快地输出调试消息不会有什么问题,这样可以大限度地减小实时性能问题。当波特率设为1Mbps时,用初的阻塞printf语句输出“Hello World!”只阻塞120μs,这要比12.5ms容易接受多了。
性能窍门#3——使用SWD
现代微控制器的设计者在开发芯片时都知道存在printf性能问题。例如那些想要利用ARM Cortex-M调试功能的开发人员会完全跳过UART,使用内部调试模块将printf消息通过调试器发送回集成开发环境。以这种方式跳过UART不仅节省了建立时间,其内部硬件机制也可大限度减少软件开销。内部缓冲区被填满消息,调试硬件自动处理调试探测器的消息发送,因此能够大程度地减小对应用程序实时性能的影响。
总结
很少有开发人员会完全抛弃他们喜爱而且非常有用的printf调试技术。在今天先进的微处理器硬件中,有诸多选项可以用来提高printf的性能和效率,大限度地减小对实时性能的影响。对于想要亲自体验这些改进的开发人员,我提供了一个可在STM32上运行的Keil项目,该项目演示了如何使用这些技巧。