fputc是什么,为什么要重写
fputc是C标准库里"输出一个字符"的底层函数。printf内部就是循环调用fputc把字符一个个写出去的。
在电脑上,fputc默认输出到屏幕,但在STM32上不一定有屏幕,只有串口。所以要重写fputc,让它把字符写到UART发送寄存器。
fputc实现
1
2
3
4
5
6
7
8
9
10
| int fputc(int ch, FILE *f)
{
// 等待发送寄存器为空
while(!(USART1->SR & USART_SR_TXE));
// 写入发送寄存器,硬件自动发送
USART1->DR = ch;
return ch;
}
|
为什么用查询方式?
先来对比三种UART发送方式:
查询:CPU轮询标志位,等待就绪。优点是简单可靠,但需要CPU持续等待。适合单字符或调试打印。
中断:TXE为空时触发中断,然后在中断里发下一个字符。好处是CPU可以同时处理其他事情,不用傻等。但是中断很频繁,因为TDR只能存1Byte,所以一次发送只能发1Byte。不选择中断的原因:fputc是同步函数,中断是异步机制,两者天然冲突。
DMA:硬件自动搬运,CPU完全解放。但有启动开销,而且DMA通道有限。不选择DMA的原因:启动开销太大,发送1Byte的数据,非常不划算。
fgetc和__backspace
fgetc
fgetc是scanf的底层调用,每次读取一个字符,有两种实现方式:
这里假设接收到的数据会被放入环形队列,可以根据情况选择中断或者DMA。但是要注意裸机不能用查询方式,因为如果用阻塞方式实现fgetc,它会占着CPU,其他函数不会有执行权。
1. 非阻塞
1
2
3
4
5
6
7
| int fgetc(FILE *f) {
uint8_t ch;
if (circle_buffer_read(&uart1_rx_buffer, &ch) != 0) {
return EOF; // 缓冲区空,返回EOF
}
return ch;
}
|
2. 阻塞
1
2
3
4
5
| int fgetc(FILE *f) {
uint8_t ch;
while(circle_buffer_read(&uart1_rx_buffer, &ch) != 0); // 死等数据
return ch;
}
|
阻塞方式适合scanf这种"必须读到数据才能继续"的场景。
__backspace
为什么需要__backspace?
scanf 是格式化输入,比如 scanf("%d", &x) 只接受数字。如果用户输入 12abc,scanf 会读取 12,遇到 a 时发现不符合格式,需要把 a “退回"给输入流。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // __backspace用到的static变量 -> 其实这个功能很简单,就是临时保存一个退回的字符
static uint8_t last_char;
static int backspace_flag = 0;
int __backspace(int ch) {
last_char = ch; // 记住要退回的字符
backspace_flag = 1; // 标记有退回字符
return 0;
}
int fgetc(FILE *f) {
// 优先返回被退回的字符
if (backspace_flag) {
backspace_flag = 0;
return last_char;
}
// 否则从环形缓冲区读
uint8_t ch;
while(circle_buffer_read(&uart1_rx_buffer, &ch) != 0);
return ch;
}
|
其实有一种异常情况fgetc并没有正确处理,比如如果我一直要格式化读取数字,但是用户输入了字母,假设是a:
- 读取到
a,退回 - 再次试图读取数字
- fgetc不会再次检查正确性,而是会直接返回
- 用户期望得到数字,却得到了字母
a
但这并不是BUG,而是C的设计哲学:机制在底层,责任在上层。库不替你做决定——也许你想丢弃非法输入,也许想重试,也许想报错退出。由你决定。
换句话说就是:库没做的,都是程序员要负责的。C选择了信任程序员。
常见的处理模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
| int x;
int result = scanf("%d", &x);
if (result == EOF) {
// 输入结束(比如串口断开)
} else if (result == 0) {
// 格式不匹配,有非法字符
while (getchar() != '\n'); // 清空到换行
printf("输入错误,请重新输入\n");
} else {
// result == 1,成功读取
printf("你输入了: %d\n", x);
}
|