常规打印方法 在STM32的应用中,我们常常对printf进行重定向的方式来把打印信息printf到我们的串口助手。在MDK环境中,我们常常使用MicroLIB+fputc的方式实现串口打印功能,即: 要实现fputc函数的原因是:printf函数依赖于fputc函数,重新实现fputc内部从串口发送数据即可间接地实现printf打印输出数据到串口。 不知道大家有没有看过正点原子裸机串口相关的例程,他们的串口例程里不使用MicroLIB,而是使用标准库+fputc的方式。相关代码如: - #if 1
3 `; I& K( G/ ^; `: X% a9 S" p& e* ] - #pragma import(__use_no_semihosting)2 |1 e3 J- F' X
- //标准库需要的支持函数
1 U# b) u. j. O6 \+ _3 c - struct __FILE
' D, S! A' Y# l' Q - {; N5 K% a0 b! u! L' u
- int handle;
6 v$ y; b( z# e$ F: D9 X" d - };! |8 f& e, K4 x1 o. Q# J
% S$ H6 y8 Z, s' n, y& {9 \4 O- FILE __stdout; y/ w$ {9 F$ _) z$ L! p; o
- /**, |' Q) V+ G7 d" n4 r
- * @brief 定义_sys_exit()以避免使用半主机模式
* G8 `1 p9 f# Q8 Z4 | P$ X# K. _ - * @param void
% I4 C4 ~8 ^& c, L9 I3 R$ P - * @return void
; x6 P) v/ g9 l' e, o& Z* s - */6 G- S2 {& i+ [; S" j
- void _sys_exit(int x)+ C8 p; Q9 Q: ^ w
- {
) V; W N; `4 w1 ]1 Q0 u - x = x;% g3 Z$ M) F5 t0 e0 k) h. y5 N! A
- }
) Q2 @6 j8 G9 u+ m - ; w1 \/ I9 Q U8 q2 j
- int fputc(int ch, FILE *f)
% t- \ P7 ^" [, Y" r - {
* s, ~5 {8 y) D1 Z3 @; ` - while((USART1->ISR & 0X40) == 0); //循环发送,直到发送完毕" Z/ D" g- o9 h/ g( V! o2 i4 }
$ q, f" \/ a# o, K- USART1->TDR = (u8) ch;
. H2 h# M1 E: b- q [3 d4 j - return ch;
2 O: S k& z$ h+ C8 g( U - }/ C# d4 ^6 y6 V8 h
- #endif
$ @4 Q% y3 i* `! q6 O7 ?
复制代码
% y- z `$ v2 o8 o自己实现一个打印函数以上的几种方法基本上是改造C库的printf函数来实现串口打印的功能。其实我们也可以自己实现一个串口打印的功能。 printf本身就是一个变参函数,其原型为: - int printf (const char *__format, ...);
" c) d/ P1 N# P1 B; c) y
复制代码 - y; g! c; {7 {# T3 v3 f) U
所以,我们要重新封装的一个串口打印函数自然也应该是一个变参函数。具体实现如下: 1、基于STM32的HAL库- #define TX_BUF_LEN 256 /* 发送缓冲区容量,根据需要进行调整 */5 T& d. V8 B' L4 e- ^) p
- uint8_t TxBuf[TX_BUF_LEN]; /* 发送缓冲区 */
3 t; U- c7 Z8 M5 ]5 Q - void MyPrintf(const char *__format, ...)9 ~% a) ~: f+ ]' O* _& k# E& l' Z2 c
- {8 N8 X% O- W9 H. U" b
- va_list ap;
& r4 B2 c+ P# D6 L& V7 c. i - va_start(ap, __format);1 L9 Y4 a6 M: R
- 3 \% |' f$ b& M, D' a' p
- /* 清空发送缓冲区 */* j0 t& `" ? b9 {7 Z
- memset(TxBuf, 0x0, TX_BUF_LEN);# X0 z; F. ?" {% P. ~) m& I E! ~; e
-
$ g! N4 O5 s0 k# D5 r# g - /* 填充发送缓冲区 */* }4 c% L0 A2 |* \* N) ?9 E9 F
- vsnprintf((char*)TxBuf, TX_BUF_LEN, (const char *)__format, ap); G3 [4 n) z/ m3 C& f3 l# n4 i3 w
- va_end(ap);( P: f2 `( A! s& O+ E1 Z. d
- int len = strlen((const char*)TxBuf);
* I6 r; R$ D2 V. P- I8 X/ R l9 G - % f* t, H0 n2 x3 N
- /* 往串口发送数据 */: F$ p, S8 w# E8 J3 y
- HAL_UART_Transmit(&huart1, (uint8_t*)&TxBuf, len, 0xFFFF);
9 C: x* t! e/ l4 ?1 P - }
1 b6 T5 f5 y" Z& w* W
复制代码
8 k8 z5 y6 |8 [$ F: n/ {因为我们使用printf函数基本不使用其返回值,所以这里直接用void类型了。自定义变参函数需要用到va_start、va_end等宏,需要包含头文件stdarg.h。 这里我们使用的是STM32的HAL库,其给我们提供HAL_UART_Transmit接口可以直接把整个发送缓冲区的内容给一次性发出去。 2、基于STM32标准库若是基于STM32的标准库,就需要一字节一字节的循环发送出去,具体代码如: - #define TX_BUF_LEN 256 /* 发送缓冲区容量,根据需要进行调整 */
. ^/ n6 m% y% O* J, h: D - uint8_t TxBuf[TX_BUF_LEN]; /* 发送缓冲区 */
) b( s, X- X5 X) v4 m C$ k7 P; u - void MyPrintf(const char *__format, ...)( |; O O, G' {$ s
- {
8 |& K* O9 A, K - va_list ap;
9 T% B @# T+ s/ Z; k5 r - va_start(ap, __format);
7 @( C7 r/ Z+ r- H7 X0 }; n - ( q& A. |2 O% ^ P. N' O
- /* 清空发送缓冲区 */
+ R( E8 `! ^/ o5 d5 F& P3 M - memset(TxBuf, 0x0, TX_BUF_LEN);. n. i5 B/ U t+ g) v
- 8 R- q9 v H+ |3 r, w7 a
- /* 填充发送缓冲区 */6 l5 R* f% _! Q2 E1 K
- vsnprintf((char*)TxBuf, TX_BUF_LEN, (const char *)__format, ap);
. W. l( P7 s9 z( Q - va_end(ap);
# q+ c% A7 Y2 \2 v - int len = strlen((const char*)TxBuf);
0 E. o, ~3 m& q# m - 8 b* k2 c9 k0 k
- /* 往串口发送数据 */4 y. g6 @4 r, g" b' P2 o
- for (int i = 0; i < len; i++)9 @ m- w/ W/ h1 {, x2 H
- {: q5 H7 _! w& e a* b& D, w
- while(USART_GetFlagStatus(USART1, USART_FLAG_TC)==RESET); - R& j" N+ R9 n) ~3 M) V6 w% Q
- USART_SendData(USART1, TxBuf[i]);
' ?1 z. V( Q) K. |8 V - }
+ F4 h* U" @9 M9 n) H! b' j - }5 q7 D% P5 Z% e5 Z$ o! }! d
复制代码 1 s8 X8 H: j9 k1 w4 B# Y8 K l
测试结果: 我们也可以使用我们的MyPrintf函数按照上一篇文章:======的方式封装一个宏打印函数:
% g n& @ C5 U& M2 f / s/ u9 y' j& R0 U9 c1 G. l
 以上就是我们自定义方式实现的一种串口打印函数。 但是,我想说:对于串口打印的使用,我们没必要自己创建一个打印函数。看到这,是不是有人想要打我了。。。。看了半天,你却跟我说没必要用。。。 哈哈,别急,我们不应用在串口打印调试方面,那可以用在其它方面呀。 (1)应用一:比如最近我在实际应用中:我们的MCU跑的是我们老大自己写的一个小的操作系统+我们公司自己开发的上位机。我们MCU端与上位机使用的是串口通讯,MCU往上位机发送的数据有两种类型,一种是HEX格式数据,一种是字符串数据。 但是我们下位机的这两种数据,在通过串口发送之前都得统一把数据封包交给那个系统通信任务,然后再由通信任务发出去。在这里,就不能用printf了。老大也针对他的这个系统实现了一个deb_printf函数用于打印调试。 但是,那个函数既复杂又很鸡肋,稍微复杂一点的数据就打印不出来了。因此我利用上面的思路给它新封装了一个打印调试函数,很好用,完美地兼容了老大的那个系统。具体代码就不分享了,大体代码、思路如上。 (2)应用二:我们在使用串口与ESP8266模块通讯时,可利用类似这样的方式封装一个发送数据的函数,这个函数的使用可以像printf一样简单。可以以很简单的方式把数据透传至服务端,比如我以前的毕设中就有这么应用:
6 [/ ^1 N+ y" q \ : D& t% V5 a' M+ B3 N; j! C% l
% @7 I4 X+ [3 U: l% T; I/ A. T |