在嵌入式设备上做输入输出还是相当不统一的, 特别是运行不起Linux, 所有资源都非常紧缺的设备, 或是那些走不了Arduino的单片机, 比如51单片机. 但也不仅仅是这些被特别强调的单片机, 比如在一些常用于大学生做比赛或测试等的项目中的stm32单片机的输入输出也是不怎么统一的.
我的愿望是统一所有设备的输入输出方式.
C语言stdio.h
本身提供了还算良好的统一的输入输出接口, 包括一些魔数如EOF
, '\0'
, NULL
, 一些数据类型如fpos_t
, FILE
, size_t
, 默认的输入输出流stdout
, stdin
, stderr
, 以及一批输入输出函数等1.
在这批接口上对串口做一个简单的封装其实是很有利于程序的移植的. 如将stdout
默认指向串口1, 然后以下这个经典程序:
#include
int main() {
printf("Hello world!\n");
return 0;
}
甚至还能在对串口做了封装的单片机上运行, 并且能在串口输出结果.
不过毕竟是嵌入式设备, 不能要求这些设备在实现接口后每当设备启动时都要开启串口, 让stdout
指向串口, 让printf
能工作.
但是可以设想这样子, 让串口的启动转移到fopen
和freopen
中去. 像下面:
#include
int main() {
FILE* uart2 = fopen("uart2", "9600");
fprintf(uart2, "this is uart2\n");
printf("test\n");
freopen("uart1", "9600", stdout);
printf("this is uart1\n");
return 0;
}
fopen
开启串口2, 然后fprintf
让字符串输出到串口2. 第一个printf
没有效果, 不过可以添加条件编译, 让这个printf
在仿真时能输出在仿真控制台上.下面freopen
正式启动串口1, 让stdout
指向串口1, 随后的printf
真正输出字符串到串口1. 最后还可以在fopen
和freopen
的第二个字符串参数里指定串口的打开方式等.
如果要改变串口的波特率等, 可以这样:
#include
int main() {
FILE* uart1 = fopen("uart1", "9600")
if(uart1 == NULL) {
printf("串口1打开失败\n");
return 1;
}
{
FILE* newstream = freopen("uart1", "115200", uart1);
if(newstream == NULL) {
printf("改变串口1波特率失败\n");
return 1;
}
}
return 0;
}
若是不希望有全局变量的话, 可以取消掉三个标准流stdout
, stdin
, stderr
, 以及基于标准流的一系列函数printf
等.同时不使用errno
.
然而事实上, 这些在上世纪七八十年代设计的接口, 用今天先进得多的视角来看其实还是充满了各类漏洞. 如此一想, 微软一族的"绑架函数2"其实也还是挺不错的输入输出接口. 例如:
#include
int main() {
FILE* uart1;
errno_t result;
const char str[] = "Hello world!";
result = fopen_s(&uart1, "uart1", "9600");
if(result != 0) {
printf("串口1打开失败, 错误码: %d (%s)\n", result, strerror(result));
return 1;
}
fprintf("uart1: %s\n", str, sizeof(str) / sizeof(char));
fclose(uart1);
return 0;
}
为流设置缓冲区在stdio.h
也有专门的接口:
#include
int main() {
FILE* uart1 = fopen("uart1", "9600");
char buf[256];
setvbuf(uart1, buf, _IOLBF, sizeof(buf));
fprintf(uart1, "Hello");
fprintf(uart1, " world!\n");
return 0;
}
setvbuf
设置缓冲区之后, 通过fprintf
输出的数据不会马上输出到串口, 除非输出的数据里包含了换行符或缓冲区满了再或者调用了fflush
强制发送数据, 刷新缓冲区. 这是_IOLBF
模式的设定. 如此可以加快数据的处理和发送, 也能方便对方以行为单位处理数据.
C51并没有为用户提供FILE
等, 因此可以在这里实验一下, 下面是效果.
#include
#include
char putchar (char c) {
return fputc(c, stdout);
}
void main() {
FILE* pf;
char str[16];
pf = fopen("uart1", NULL);
fputs("openfile", pf);
fputs("Hello world!", stdout);
fgets(str, 16, stdin); /* 在这里会停下来等待串口数据直到有换行符或缓冲区满 */
printf("input: %s\r\n", str);
printf("hello world\r\n");
while(1);
}
编译结果: Program Size: data=59.1 xdata=0 const=0 code=2533
fopen
和freopen
以字符串为流标识来打开流的方式其实是相当灵活的, 用设计模式的话说叫工厂模式.
日后串口扩张了, 或是流模式增加了诸如I2C等接口, 再或是真正地添加了文件系统了之后, 可以类似地对这些外设做封装, 为fopen
提供打开该流的标识.
值得一提, 在Windows
上是用COMx
作为标识用于打开各种流. 在Linux
上用/dev/*
表示各种可打开的设备文件.
stdio.h
的41个输入输出函数的功能关系图 ↩︎
有人认为这些所谓的安全函数, 是将用户的程序更紧密地与VS耦合, 故称"绑架". ↩︎