0、串口基础准备

嵌入式系统中,诸多设备都可以通过串口来控制,比如WiFi、蓝牙、RFID等等,因此在嵌入式设备中使用串口就显得特别重要。

首先,有几个概念简单澄清一下。

RS232、RS485和TTL指的是电气电平标准。一般而言,TTL使用0V表示低电平,+5V表示高电平。RS232使用负逻辑电平,即+3V ~ +15V表示低电平,-3V ~ -15V表示高电平。RS485和RS232一样都是基于串口的通讯接口,数据收发的操作是一致的,RS485采用差分信号负逻辑,+2V~+6V表示低电平,- 6V~-2V表示高电平。

以上三个协议(RS232、RS485和TTL)是电气特性,规定的是物理层接口要求。物理层标准的建设,就好比各种公路的建设,公路有了我们就可以在上面跑各种汽车,并制定各种汽车的不同行驶规则,这就是通信协议,UART(Universal Asynchronous Receiver/Transmitter,通常称作UART)就是其中的一种通信协议,全称是通用异步收发器,规范了在串行通信中,最常见的异步传输的通信模式,下面会有详解。

平常说的串口、COM口指的是物理接口,它们通常有9针和4针两种样式标准。4针串口标准中的四个管脚分别是:VCC、TX、RX和GND,其中TX和RX分别是发送端(Transmit)和接收端(Receive)。一般而言两个串行口互相收发信息的时候,需要让他们的这两个管脚交互相连,即A口的TX接B口的RX,反过来A口的RX接B口的TX。

在左边儿的D型9针接口中,一般而言实际用到的也是VCC、TX、RX和GND四个脚。

下面就来具体看看串口是怎么回事。

串口通常指的是串行接口,这跟并行接口相对而言。比如有一个8-bits的数据从A发往B,并行和串行的区别分别如下面所示。

可以看到,串行传输只需要一根线缆就够了,而并行的话需要8根线。他们各自的特点如下:

串行传输线缆少,因此信号衰减较慢,电磁兼容性较好,传输距离较长;并行传输线缆多,因此信号衰减较快,电磁兼容性较差,传输距离短。

串行传输速度乍看起来是并行传输的 1/N ,但实际上由于并行线缆的电磁干扰的物理特性限制,导致并行传输的最高频率(比如几十MHz)要远远低于串行传输的最高频率(高达几十GHz),因此串行传输的速度并不比并行的慢。

串行传输硬件接口简单,占用空间小,成本低;并行传输硬件接口复杂,占用空间大,成本稍高。

串行传输由于线缆少,因此数据的正常传输需要各种特定的算法,软件设计复杂;并行传输每个数据位独立,算法和软件设计相对简单。

简单来看,串行传输比并行传输有更多优势,实际上随着各种设备的主频越来越高,串行通信在现代计算机领域越来越大行其道。

那这一根线的串行通信,究竟是怎么传输数据的呢?主要有下面两种办法。

1、同步传输

所谓同步传输,最核心的地方就是用统一的时钟来控制发送方和接收方,然后规定一个同步起始字符,同时规定传输的每个字符包含的位数(一般是5-8位)就可以了。

由于同步传输必须要求收发双方时钟一致,甚至要求使用严格的同一时钟源,而这在多数场合下都是无法达到的要求,因此虽然同步传输比异步传输速度高,但应用更为广泛的还是异步传输。

2、异步传输

顾名思义,异步传输的本质是不要求收发双方时钟的同步性,这意味着他们可以各自有各自的时钟(始终不需要同步,但频率还得是一样的,不然根本就鸡同鸭讲,即波特率必须得一致),这极大降低了工程实施的复杂性。因此着重要关注的是异步传输。

既然是异步传输,即发送方随时可以发送数据过来,事先是没有跟接收方有过任何协同商量的,那接收方很自然地就会有个疑问:你的数据究竟什么时候过来?一般的做法是这样:规定一个空闲的电位状态(比如高电平),然后规定一个起始位(比如一个低电平时钟间隔),然后接着就是正常的数据位(当然也可以规定长度,比如8-bits),然后是可选的校验位(用来校验数据传输过程中是否受到电磁环境的影响发生数位翻转),然后是停止位(比如一个高电平时钟间隔)。---时序图

1、Linux串口概述

        串口参数的配置,一般包括波特率起始位比特数数据位比特数停止位比特数流控模式。在此,可以将其配置为波特率 115200、起始位 1b、数据位 8b、停止位 1b 和无流控模式。

串口的设置主要是设置 struct termios 结构体的各成员值

#include<termios.h> ----- /usr/include  跟stdio.h同一目录

typedef unsigned char   cc_t;
typedef unsigned int    speed_t;
typedef unsigned int    tcflag_t;

struct termios    //串口结构体 ------ 
{
    tcflag_t c_iflag;           /* input mode flags */     /* 输入模式标志 */
    tcflag_t c_oflag;           /* output mode flags */    /* 输出模式标志 */
    tcflag_t c_cflag;           /* control mode flags */   /* 控制模式标志  --停止位  数据位   校验位  起始位*/
    tcflag_t c_lflag;           /* local mode flags */     /* 本地模式标志 */
    cc_t c_line;                /* line discipline */      /* 线路规程 */
    cc_t c_cc[NCCS];            /* control characters */   /* 控制特性 */
    speed_t c_ispeed;           /* input speed */          /* 输入速度  */
    speed_t c_ospeed;           /* output speed */         /* 输出速度 */
    #define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
    #define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
};

设置串口中最基本的包括波特率设置,校验位和 停止位设置。

在这个结构中最为重要的是 c_cflag,通过对它的赋值,用户可以设置波 特率、字符大小、数据位、停止位、奇偶校验位和硬软流控等。

在这里,不能直接对 c_cflag 成员初始化,而要将其通过“与”、“或”操作使用 其中的某些选项。

c_iflag:输入模式标志,控制终端输入方式,具体参数如下所示。

c_iflag 参数表

键值说明

IGNBRK 忽略BREAK键输入

BRKINT 如果设置了IGNBRK,BREAK键的输入将被忽略,如果设置了BRKINT ,将产生SIGINT中断

IGNPAR 忽略奇偶校验错误

PARMRK 标识奇偶校验错误

INPCK  允许输入奇偶校验

ISTRIP 去除字符的第8个比特

INLCR  将输入的NL(换行)转换成CR(回车)

IGNCR  忽略输入的回车

ICRNL  将输入的回车转化成换行(如果IGNCR未设置的情况下)

IUCLC  将输入的大写字符转换成小写字符(非POSIX)

IXON  允许输入时对XON/XOFF流进行控制

IXANY  输入任何字符将重启停止的输出

IXOFF  允许输入时对XON/XOFF流进行控制

IMAXBEL 当输入队列满的时候开始响铃,Linux在使用该参数而是认为该参数总是已经设置

c_oflag:输出模式标志,控制终端输出方式,具体参数如下所示。

c_oflag参数

键值说明

OPOST 处理后输出

OLCUC 将输入的小写字符转换成大写字符(非POSIX)

ONLCR 将输入的NL(换行)转换成CR(回车)及NL(换行)

OCRNL 将输入的CR(回车)转换成NL(换行)

ONOCR 第一行不输出回车符

ONLRET 不输出回车符

OFILL 发送填充字符以延迟终端输出

OFDEL 以ASCII码的DEL作为填充字符,如果未设置该参数,填充字符将是NUL(‘/0’)(非POSIX)

NLDLY 换行输出延时,可以取NL0(不延迟)或NL1(延迟0.1s)

CRDLY 回车延迟,取值范围为:CR0、CR1、CR2和 CR3

TABDLY 水平制表符输出延迟,取值范围为:TAB0、TAB1、TAB2和TAB3

BSDLY 空格输出延迟,可以取BS0或BS1

VTDLY 垂直制表符输出延迟,可以取VT0或VT1

FFDLY 换页延迟,可以取FF0或FF1

c_cflag:控制模式标志,指定终端硬件控制信息,具体参数如下所示。

c_oflag参数

键值说明

CBAUD  波特率(4+1位)(非POSIX)

CBAUDEX 附加波特率(1位)(非POSIX)

CSIZE  字符长度,取值范围为CS5、CS6、CS7或CS8

CSTOPB  设置两个停止位

CREAD  使用接收器

PARENB  使用奇偶校验

PARODD  对输入使用奇偶校验,对输出使用偶校验

HUPCL  关闭设备时挂起

CLOCAL  忽略调制解调器线路状态

CRTSCTS 使用RTS/CTS流控制

c_lflag:本地模式标志,控制终端编辑功能,具体参数如下所示。

c_lflag参数

键值说明

ISIG 当输入INTR、QUIT、SUSP或DSUSP时,产生相应的信号

ICANON 使用标准输入模式   ~ICANON 不需要回车直接输入

XCASE 在ICANON和XCASE同时设置的情况下,终端只使用大写。如果只设置了XCASE,则输入字符将被转换为小写字符,除非字符使用了转义字符(非POSIX,且Linux不支持该参数)

ECHO 显示输入字符

ECHOE 如果ICANON同时设置,ERASE将删除输入的字符,WERASE将删除输入的单词

ECHOK 如果ICANON同时设置,KILL将删除当前行

ECHONL 如果ICANON同时设置,即使ECHO没有设置依然显示换行符

ECHOPRT 如果ECHO和ICANON同时设置,将删除打印出的字符(非POSIX)

TOSTOP 向后台输出发送SIGTTOU信号

tcgetattr()

1.原型

int tcgetattr(int fd, struct termois *termios_p);

2.功能

取得终端介质(fd)初始值,并把其值 赋给temios_p; 函数可以从后台进程中调用;但是,终端属性可能被后来的前台进程所改变。

tcsetattr()

1.原型

int tcsetattr(int fd, int actions, const struct termios *termios_p);

2.功能

设置与终端相关的参数 (除非需要底层支持却无法满足),使用 termios_p 引用的 termios 结构。

optional_actions (tcsetattr函数的第二个参数)指定了什么时候改变会起作用:

TCSANOW:改变立即发生

TCSADRAIN:改变在所有写入 fd 的输出都被传输后生效。这个函数应当用于修改影响输出的参数时使用。(当前输出完成时将值改变)

TCSAFLUSH :改变在所有写入 fd 引用的对象的输出都被传输后生效,所有已接受但未读入的输入都在改变发生前丢弃(同TCSADRAIN,但会舍弃当前所有值)。

设置串口中最基本的包括波特率设置,校验位和 停止位设置。在这个结构中最为重要的是 c_cflag,通过对它的赋值,用户可以设置波 特率、字符大小、数据位、停止位、奇偶校验位和硬软流控等,在这里,不能直接对 c_cflag 成员初始化,而要将其通过“与”、“或”操作使用 其中的某些选项。

CBAUD

波特率的位掩码

B0

0 波特率(放弃 DTR)

B1800

1800 波特率

B2400

2400 波特率

B4800

4800 波特率

B9600

9600 波特率

B19200

19200 波特率

B38400

38400 波特率

B57600

57600 波特率

B115200

115200 波特率

EXTA

外部时钟率

EXTB

外部时钟率

CSIZE

数据位的位掩码

CS5

5 个数据位

CS6

6 个数据位

CS7

7 个数据位

CS8

8 个数据位

CSTOPB

2 个停止位(不设则是 1 个停止位)

CREAD

接收使能

PARENB PARODD

校验位使能使用奇校验而不使用偶校验

HUPCL

最后关闭时挂线(放弃 DTR)

CLOCAL

本地连接(不改变端口所有者)

CRTSCTS

硬件流控

2、串口配置流程

1.保存原先串口配置

首先,为了安全起见和以后调试程序方便,可以先保存原先串口的配置,在这里 可以使用函数 tcgetattr(fd, &old_cfg)。该函数得到 fd 指向的终端的配置参数,并将它 们保存于 termios 结构变量 old_cfg 中。该函数还可以测试配置是否正确、该串口是否 可用等。若调用成功,函数返回值为 0,若调用失败,函数返回值为-1,其使用如下 所示:

if(tcgetattr(fd, &old_cfg) != 0)
{
    perror("tcgetattr"); 
    return -1;
}

2.激活选项

CLOCAL 和 CREAD 分别用于本地连接和接受使能,因此,首先要通过位掩码的方式激活这两个选项。

 newtio.c_cflag  |=  CLOCAL | CREAD;

调用 cfmakeraw()函数可以将终端设置为原始模式,在后面的实例中,采用原始模式进行串口数据通信。

cfmakeraw(&new_cfg);

3.设置波特率

设置波特率有专门的函数,用户不能直接通过位掩码来操作。设置波特率的主要函数有:cfsetispeed()和 cfsetospeed()。这两个函数的使用很简单,如下所示:

cfsetispeed(&new_cfg, B115200);
cfsetospeed(&new_cfg, B115200);

一般地,用户需将终端的输入和输出波特率设置成一样的。这几个函数在成功时返回 0,失败时返回-1。

4.设置字符大小

与设置波特率不同,设置字符大小并没有现成可用的函数,需要用位掩码。一般 首先去除数据位中的位掩码,再重新按要求设置。如下所示:

new_cfg.c_cflag &= ~CSIZE; /* 用数据位掩码清空数据位设置 */
new_cfg.c_cflag |= CS8;

5.设置奇偶校验位

设置奇偶校验位需要用到 termios 中的两个成员:c_cflag 和 c_iflag。首先要激活 c_cflag 中的校验位使能标志 PARENB 和是否要进行偶校验,同时还要激活 c_iflag 中 的对于输入数据的奇偶校验使能(INPCK)。无校验位,代码如下:

new_cfg.c_cflag &= ~PARENB;

如使能奇校验时,代码如下所示:

new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;

而使能偶校验时,代码如下所示:

new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD; /* 清除偶校验标志,则配置为奇校验*/
new_cfg.c_iflag |= INPCK;

6.设置停止位

设置停止位是通过激活 c_cflag 中的 CSTOPB 而实现的。若停止位为一个,则清除CSTOPB,若停止位为两个,则激活 CSTOPB。以下分别是停止位为一个和两个比特时的代码:

new_cfg.c_cflag &= ~CSTOPB; /* 将停止位设置为一个比特 */
new_cfg.c_cflag |= CSTOPB; /* 将停止位设置为两个比特 */

7.设置最少字符和等待时间

在一般的情况下,可以设置设置为阻塞等待,直read到数据才返回

termios_new.c_cc[VTIME] = 0;
termios_new.c_cc[VMIN]  = 4;

8.清除串口缓冲

由于串口在重新设置之后,需要对当前的串口设备进行适当的处理,这时就可调

用在中声明的 tcdrain()、tcflow()、tcflush()等函数来处理目前串口缓冲中的

数据,它们的格式如下所示。

int tcdrain(int fd); /* 使程序阻塞,直到输出缓冲区的数据全部发送完毕*/

int tcflow(int fd, int action) ; /* 用于暂停或重新开始输出 */int tcflush(int fd, int queue_selector); /* 用于清空输入/输出缓冲区*/

本实例中使用 tcflush()函数,对于在缓冲区中的尚未传输的数据,或者收到的

但是尚未读取的数据,其处理方法取决于 queue_selector 的值,它可能的取值有以下几种。

TCIFLUSH:对接收到而未被读取的数据进行清空处理。

TCOFLUSH:对尚未传送成功的输出数据进行清空处理。

TCIOFLUSH:包括前两种功能,即对尚未处理的输入输出数据进行清空处理。

如在本例中所采用的是第一种方法:

tcflush(fd, TCIFLUSH);

9.激活配置

在完成全部串口配置之后,要激活刚才的配置并使配置生效。这里用到的函数是tcsetattr(),

它的函数原型是:

tcsetattr(int fd, int optional_actions, const struct termios*termios_p);

其中参数 termios_p 是 termios 类型的新配置变量。

参数 optional_actions 可能的取值有以下 3 种:

TCSANOW:配置的修改立即生效。

TCSADRAIN:配置的修改在所有写入 fd 的输出都传输完毕之后生效。

TCSAFLUSH:所有已接受但未读入的输入都将在修改生效之前被丢弃。

该函数若调用成功则返回 0,若失败则返回-1,代码如下所示:

if ((tcsetattr(fd, TCSANOW, &new_cfg)) != 0)
{
    perror("tcsetattr");
    return -1;
}

10.给串口发送数据的数据加\n结束 

串口编程的流程:

  1. 打开串口---- open
  2. 初始化串口或者配置串口 ---自定义函数
  3. 访问串口---- read/write
  4. 关闭串口---- close

3、实例代码

#include <stdio.h>
#include<termios.h>     //引入Linux下的串口头文件
#include <string.h>

/*

    UART 接口一帧数据格式为1个起始位 8个数据位 无奇偶校验位 1停止位 波特率:9600
*/
int init_linux_uart(int fd)
{
//1.定义串口结构体
        struct termios old_cfg,new_cfg;
//2.得到当前串口的属性配置
//extern int tcgetattr (int __fd, struct termios *__termios_p) __THROW;
        if(tcgetattr(fd, &old_cfg) != 0)
        {
             perror("tcgetattr");
             return -1;
        }
//3.设置串口的通信模式
//extern void cfmakeraw (struct termios *__termios_p)
//将新的配置属性清零
        bzero(&new_cfg, sizeof(new_cfg));
        new_cfg = old_cfg;
        cfmakeraw(&new_cfg);
    
 //4.配置串口波特率 数据位 停止位 奇偶校验位 
        cfsetispeed(&new_cfg, B9600);
        cfsetospeed(&new_cfg, B9600);
 //激活选项:CLOCAL CREAD
    new_cfg.c_cflag  |=  CLOCAL | CREAD;
 //设置数据位:8位,需使用掩码设置
    new_cfg.c_cflag &= ~CSIZE; /* 用数据位掩码清空数据位设置 */
    new_cfg.c_cflag |= CS8;
 //设置无校验
    new_cfg.c_cflag &= ~PARENB;
    //设置停止位
    new_cfg.c_cflag &=  ~CSTOPB;
    
    //清空缓冲区
    new_cfg.c_cc[VTIME]  = 0;
        new_cfg.c_cc[VMIN] = 4;
        tcflush(fd,TCIFLUSH);     // 清空fd
    //5 将配置好的串口设置到系统中
        if ((tcsetattr(fd, TCSANOW, &new_cfg)) != 0)
        {
            perror("tcsetattr");
            return -1;
        }
        return 0;
}


int main(void)
{
    
    
        //1.打开串口  --- open 
        //2. 配置串口 --- 根据串口的实际应用来进行配置 --- 自定义一个函数来实现
        //3.读或者写串口 --- read write 
        //4.关闭串口  --- close
}

4、模块应用说明

        在上述的Linux开发板串口初始化配置完成后,只要是采用串口进行通信的模块设备,无论是什么模块,只需要根据设备的指令表,进行相应的数据发送接收处理,即可完成模块设备的初步的驱动。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐