[嵌入式]统一嵌入式设备串口输入输出方式的愿望

文章目录

  • 1. 现状
  • 2. 愿望
    • 2.1 经典
    • 2.2 封装
    • 2.3 安全
    • 2.4 缓冲
    • 2.5 C51
  • 3. 后记

1. 现状

  在嵌入式设备上做输入输出还是相当不统一的, 特别是运行不起Linux, 所有资源都非常紧缺的设备, 或是那些走不了Arduino的单片机, 比如51单片机. 但也不仅仅是这些被特别强调的单片机, 比如在一些常用于大学生做比赛或测试等的项目中的stm32单片机的输入输出也是不怎么统一的.
  我的愿望是统一所有设备的输入输出方式.

2. 愿望

  C语言stdio.h本身提供了还算良好的统一的输入输出接口, 包括一些魔数如EOF, '\0', NULL, 一些数据类型如fpos_t, FILE, size_t, 默认的输入输出流stdout, stdin, stderr, 以及一批输入输出函数等1.

2.1 经典

  在这批接口上对串口做一个简单的封装其实是很有利于程序的移植的. 如将stdout默认指向串口1, 然后以下这个经典程序:

#include 
int main() {
	printf("Hello world!\n");
	return 0;
}

甚至还能在对串口做了封装的单片机上运行, 并且能在串口输出结果.

2.2 封装

  不过毕竟是嵌入式设备, 不能要求这些设备在实现接口后每当设备启动时都要开启串口, 让stdout指向串口, 让printf能工作.
  但是可以设想这样子, 让串口的启动转移到fopenfreopen中去. 像下面:

#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. 最后还可以在fopenfreopen的第二个字符串参数里指定串口的打开方式等.
  如果要改变串口的波特率等, 可以这样:

#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.3 安全

  然而事实上, 这些在上世纪七八十年代设计的接口, 用今天先进得多的视角来看其实还是充满了各类漏洞. 如此一想, 微软一族的"绑架函数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;
}

2.4 缓冲

  为流设置缓冲区在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模式的设定. 如此可以加快数据的处理和发送, 也能方便对方以行为单位处理数据.

2.5 C51

  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

3. 后记

  fopenfreopen以字符串为流标识来打开流的方式其实是相当灵活的, 用设计模式的话说叫工厂模式.
  日后串口扩张了, 或是流模式增加了诸如I2C等接口, 再或是真正地添加了文件系统了之后, 可以类似地对这些外设做封装, 为fopen提供打开该流的标识.
  值得一提, 在Windows上是用COMx作为标识用于打开各种流. 在Linux上用/dev/*表示各种可打开的设备文件.


  1. stdio.h的41个输入输出函数的功能关系图 ↩︎

  2. 有人认为这些所谓的安全函数, 是将用户的程序更紧密地与VS耦合, 故称"绑架". ↩︎

你可能感兴趣的:([嵌入式]统一嵌入式设备串口输入输出方式的愿望)