首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

串口通信(1)

串口是一种串行的通信接口,通信设备可以通过它一位一位顺序地发送或者接收数据。 虽然常见的USB、以太网也是串行地进行数据收发的,但通常我们所说的串口是指类似于RS232或者它的一些变种标准的串行通信方式。 最早串口用在PC机上与打印机、猫(调制解调器)等设备进行交互,早些的机器上一般都有两个串口COM1和COM2的,现在它们已经被USB替代了。 但在因为它简单方便的特性在一些工业设备、嵌入式系统中仍然广泛使用着。基本所有的MCU芯片都会提供若干个串口, 在STM32中它们被称为USART(Universal Synchronous Asynchronous Receiver Transmitter)。

事实上USART与RS232之间没有过多的关联,后者描述了串行通信的物理接口,前者则描述了串行通信的逻辑形式。 从USART的全称中,我们也可以看出串行通信的两种形式:同步、异步。本文中,我们先简单的介绍一下RS232及其相关的变种, 然后解释一下串行通信中的同步和异步都是指什么,最后我们还是要回到STM32上介绍如何异步地发送数据。

1. 通信协议的分层描述

提到通信,我们就不得不提一下有名的OSI七层模型, 它的提出主要是为了解决不同通信协议下的不同设备之间的互联互通的问题。在七层模型中,把通信系统抽象称为7个协议层,每层都是一个相对独立的部分, 只为其上一层协议提供服务,只用其下一层协议的服务实现本层的功能,而且不同设备的之间只有相同层之间是相互认识的。

表 1 OSI七层模型

7 应用层
Application
该层直接面对用户,为用户软件提供接口,本身并不是应用软件的一部分。 例如HTTP协议为浏览器提供数据,提供邮箱服务的SMTP协议等。
6 表示层
Presentation
主要用来描述应用层数据的组织方式,例如编码形式、数据压缩、数据加解密方式等。
5 会话层
sessions
主要用来管理网络中两个通信节点之间连接的建立与撤销,只有建立了连接的两个节点之间才可以通过网络实现有意义的通信。
4 传输层
Transport
描述了网络中两个节点之间的通信方式,例如数据的分包发送和组合,数据的传送流程等。我们常说的TCP、UDP就是这一层的协议。
3 网络层
Network
定义了数据在网络中的流通方式,通过给设备一个唯一的标识符(例如IP地址),描述数据在各个设备之间的路由方式, 定义了数据经过什么设备之后能够正确的把数据传送到指定的设备上。
2 数据层
Data Link
把来自物理层的0-1逻辑信号转换为有意义的数据。它描述一帧数据从什么时候开始,到什么时候结束,数据的位定义,传输速率,校验方式等等。
1 物理层
Physical
定理了设备之间的物理连接方式,比如说是用有线还是无线的方式连接,有线连接的话具体用什么样的线缆? 逻辑0和逻辑1应该怎么表示?如果用无线连接的话还需要确定无线的频率等?

表1中简单介绍了各层的功能,事实上它只是一个参考模型,具体实现时会做很多调整,我们广泛使用的TCP/IP协议也只有4层。 七层模型详细地描述了网络通信中的各个方面,为我们理解和设计通信协议提供了很好的参考。

对于串口通信而言,一开始我们是为了调试方便,把它跟我的笔记本相连打印一些调试信息用。因此主要是点对点的通信,不涉及到数据的流通方式, 会话什么的。而我们也不会涉及到复杂的编解码、加解密问题,数据也都是以字节的形式一个个的发送和接收的,暂时也不会考虑分包发送和组合。 所以呢,目前而言我们的串口通信只会涉及到三层内容:物理层、数据层和应用层。

物理层主要是指我们先前提到的RS232、RS422、RS485、TTL等物理的电平逻辑和接口描述,数据层则就是本文中主要关注的对象USART。 而应用层具有很大的随意性,因为它与我们的需求和使用方式有关。

2. 物理层连接——电平逻辑

我们知道在计算机的世界中,所有的逻辑都是通过0-1两个数字来表示的。那么要实现两个设备之间的通信,本质上就是找到一个媒介,把要传递的内容以0-1序列的形式 传递出去。在我们的串口通信中,这个媒介就是连接两个设备之间的导线,或者是在PCB板上的一段导体。然后规定不同的电压值,表示逻辑0和逻辑1。 这里的电压值与逻辑上的0-1之间的对应关系就是所谓的电平逻辑。

在数字电路中,人们常用TTL电平或者CMOS电平来表示0和1。TTL的全称是Transistor-Transistor Logic,它是一种由三极管控制的逻辑电平。 CMOS的全称是Complementary Metal Oxide Semiconductor,它是一种由PMOS和NMOS管互补控制的逻辑电平。手册上说STM32F407的所有IO引脚都是TTL和CMOS电平兼容的,但我们 参考引脚结构,会发现它就是一种CMOS电平。 TTL和CMOS电平有5V, 3.3V, 2.5V和1.8V等不同的形式,其中5V的形式是通用的逻辑电平,3.3V及以下的都是低电压逻辑电平,称为LVTTL或者LVCMOS。

一般情况下3.3V的TTL输出高电平逻辑时最小为2.4V,低电平逻辑最大为0.4V。而输入设备检测TTL电平高于2V就认为时高电平,低于0.8V就认为是低电平。 而3.3V的CMOS则规定3.2V为输出高电平时的最小电压,0.1V为输出低电平的最大电压,输入检测以2V和1.7V作为高电平的最小电压和低电平的最大电压。 不同的芯片可能有些不一致,具体还需要查看数据手册中关于VOH(高电平输出电压)、VOL(低电平输出电压)、VIH(高电平输入电压)和VIL(低电平输入电压)的描述。

我们可以直接把MCU的串口引脚用导线的形式接出来作为通信的物理介质,通过导线上的高低电平描述逻辑0和逻辑1。但这种形式的消息传递不能实现远距离通信, 因为随着导线的延长,电平信号会逐渐衰弱,而且外界的干扰作用将会不断增强。为了提高串口通信的频率和传输距离,人们提出了RS232等一系列的串口通信协议。

RS232实际上不仅仅定义了两个通信设备之间的电平逻辑,它还描述了设备连接器的样子和引脚定义。RS232的协议标准中推荐使用25个引脚的连接器,称为DB-25。 但实际通信时并不需要这么多引脚,后来IBM-PC上对其进行了简化只用了9个引脚,称为DB-9。在这9个引脚中实际传输数据的只有2、3脚的RxD和TxD两个, 其余都是一些控制信号和地线。因此,现在人们通常只用RxD、TxD和GND这3个引脚,如果两个通信的设备之间是共地的,只用两个引脚就可以了。

从一个设备的角度去看,RxD和TxD分别负责接收数据和发送数据,两者之间可以没有什么关联同时进行,因此RS232是一个支持全双工通信的连接形式。 在这两个引脚上,用-3V~-15V作为逻辑1(又称为Mark),+3V~+15V则用作逻辑0(称为Space)。对比TTL和CMOS电平,我们可以看出RS232是与它们完全不同的电平逻辑, 这也就是为什么不能直接把MCU的引脚与电脑的串口相连的原因。通常,我们会用MAX3232等电平转换芯片,把来自MCU的TTL电平转换成为RS232电平再连接到电脑上。

虽然RS232的电平相比于TTL和CMOS而言电压范围更广了,使得我们在传输距离和稳定性上有了改善,但有时仍然不能满足我们的需求。 因而又产生了RS422, RS485这样差分形式的电平逻辑。

所谓的差分形式的电平逻辑,可以参考图1予以解释。图中的D是Driver的简称,它接收要发送的数字信号,把输入的逻辑0或者逻辑1用一对相位相反的信号A,B表示, 并输出到导线上。也就是说A为高电平时,B就是低电平。A-B信号经过导线传输到接收端形成了A'-B'信号,经过一个终端电阻\(Z_T\)传导进接收器R。 接收器根据A'-B'之间的电压差还原发送的信号。

图1 差分形式的电平逻辑结构图

差分信号的优势在于抗干扰能力强,因为如果在传输过程中信号A和信号B收到相同信号干扰的话,A,B只会产生一个相同的偏移量,而不会影响到它们的差值。 这就是所谓的共模干扰,为了保证两个信号在传输过程中的干扰源一致,或者说是主要是共模干扰信号,人们通常使用双绞线作为差分信号的导线。 这种抗干扰能力极大的提高了通信距离,RS422和RS485都采用了这种通信方式。

RS422与RS485之间的不同之处在于,RS422的接收和发送是两条独立的通道,也就是说它可以像RS232那样进行全双工通信。而RS485只有一个信号通道,接收和发送都是它, 同一时间只能发送或者接收,因而是半双工通信的。RS422可以实现一主多从的通信网络,但是从设备之间是不能够相互通信的。 RS485构建的通信网络中所有的设备之间都可以发送信息,只是同一时间只能有一个设备发送其余所有设备接收。

3. 数据层——串口逻辑

数据层的任务就是把来自物理层的电平逻辑转换成为有意义的数据,它从电平逻辑中检测数据的起始和结束信号,判定数据内容和校验方式,这里我把它称为串口逻辑。 在STM32的USART中提供了同步和异步两种串行通信机制,其中同步串行通信本质上就是一个SPI的主设备(关于这一点,以后介绍SPI通信时再详细介绍), 异步通信就是我们常见的串口通信机制,配合不同的物理层实现支持TTL、RS232、RS422、RS485等双工半双工通信形式。这里主要关注异步地通信机制。

所谓的同步串行通信是指在通信过程中,伴随着数据的传送还有一个时钟信号用来驱动通信设备接收和发送数据,SPI、I2C就是典型的同步通信方式。 而异步串行通信则是指在通信过程中没有时钟信号的通信方式,这种通信方式下需要接收方和发送方协调好,以相同的时钟频率收发数据。 这里要讨论的串口就是一种典型的异步通信方式,通信双方必须在相同的波特率下工作,否则就会产生乱码的现象。 波特率是指发送或者接收一个bit的频率。

下图2描述了USART异步通信的数据帧格式,规定没有数据发送时通信线路上应当保持逻辑1,当有数据需要发送时,要先发送一个bit的起始位。 紧跟着是一个字节的8位数据,再然后可以是一个奇偶校验位。奇偶校验是用一个bit标识发送的一帧数据中逻辑'1'的个数是奇数还是偶数, 接收方收到的一帧数据后统计逻辑'1'的个数与校验位对比如果不一致则说明数据传送错误。奇偶校验位并不是必须的,可以配置STM32不进行奇偶校验。 最后必须以停止位结束一个字节的发送,停止位的电平为逻辑'1",位长可以是1位、1.5位或者2位,具体由芯片配置决定。

图2 USART异步通信数据帧格式

在串口通信中,除了以上所说的数据帧的发送外,还有两个特殊的帧——Idle Frame和Break Frame。Idle帧的所有位都为'1',紧跟其后的是下一帧数据的起始位。 Break帧的所有位都为'0',其后会有一到两位的停止位,用以区分下一帧数据的起始位。帧格式参考图3,这两个特殊的数据帧有什么用,目前还不知道, 猜测可以用于错误处理,但我从来没有用过。此外,在STM32中还支持Smartcard协议、红外数据(IrDA)等模式,鉴于我从来没有用过,这里就省略了, 要想详细了解还需要查看参考手册

图3 Idle和Break数据帧格式

4. STM32的串口通信实现

在STM32F407系列的芯片中支持4个USART和2个UART。其中USART1和USART6挂在高速外设总线APB2上,USART2、USART3、UART4、UART5挂在了低速外设总线APB1上。 根据参考手册中关于寄存器的描述,我们定义了一个结构体用来访问USART的寄存器, 其定义形式和代码风格可以参考我们在通用IO中介绍的访问外设寄存器的方法。

        typedef struct usart_regs {
            volatile union usart_sr SR;         /* 串口状态寄存器, offset: 0x00 */
            volatile union usart_dr DR;         /* 串口数据寄存器, offset: 0x04 */
            volatile union usart_brr BRR;       /* 串口波特率寄存器, offset: 0x08 */
            volatile union usart_cr1 CR1;       /* 串口控制寄存器1, offset: 0x0C */
            volatile union usart_cr2 CR2;       /* 串口控制寄存器2, offset: 0x10 */
            volatile union usart_cr3 CR3;       /* 串口控制寄存器3, offset: 0x14 */
            volatile union usart_gtpr GTPR;     /* 串口守护时间和分频寄存器, offset: 0x18 */
        } usart_regs_t;

此外,根据各个串口寄存器的起始地址,我们还定义了如下的宏以方便的访问它们。

        /* 串口寄存器地址映射 */
        #define USART1_BASE 0x40011000
        #define USART2_BASE 0x40004400
        #define USART3_BASE 0x40004800
        #define UART4_BASE  0x40004C00
        #define UART5_BASE  0x40005000
        #define USART6_BASE 0x40011400
        /* 串口寄存器指针访问 */
        #define USART1 ((usart_regs_t *) USART1_BASE)
        #define USART2 ((usart_regs_t *) USART2_BASE)
        #define USART3 ((usart_regs_t *) USART3_BASE)
        #define UART4  ((usart_regs_t *) UART4_BASE)
        #define UART5  ((usart_regs_t *) UART5_BASE)
        #define USART6 ((usart_regs_t *) USART6_BASE)

我们的探索者开发板设计了一个调试用的串口,原理图如右图所示。它在PCB板上用了一个micro-USB的接口,直接与TTL转USB芯片CH340连接了。 这样我们将其与PC机连接的时候,既可以给板子供电,也可以进行串口调试,简单方便。 原理图中的RXD和TXD连接到了MCU芯片的PA9和PA10脚,对应的是USART1的Tx和Rx功能。

图4 探索者开发板串口原理图

我们专门定义一个函数对USART1进行初始化,首先定义了三个局部变量,其中mantissa和fraction用于计算波特率。然后调用函数usart1_init_gpio初始化相关引脚配置, 接着通过复位与时钟控制器RCC打开USART1的驱动时钟。

    void usart1_init(uint32 baudrate) {
        uint32 tmp, mantissa, fraction;
        usart1_init_gpio();
        RCC->APB2ENR.bits.usart1 = 1;// 串口时钟使能
下面配置USART1的控制寄存器,打开了接收器和发送器,使其一次通信收发8位数据,并且不进行奇偶校验,最后设置停止位长度为1位。
        USART1->CR1.bits.M = 0;      // 8数据位
        USART1->CR1.bits.PCE = 0;    // 无奇偶校验
        USART1->CR1.bits.RE = 1;     // 收使能
        USART1->CR1.bits.TE = 1;     // 发使能
        USART1->CR2.bits.STOP = USART_STOP_1bit; // 1位停止位
为了简单起见,这里不使用硬件流控制,将之关闭。所谓的流控制是指接收器和发送器必须根据发送方和接收方的指示信号进行数据传输, 涉及到请求发送(Require To Send, RTS)和清除发送(Clear To Send, CTS)两个方面。RTS是输出信号标志着接收器准备好可以接收数据,或者说是请求发送数据。 CTS是输入信号标识着可以发送数据了,因此收发的两个设备之间CTS和RTS是互联的。
        USART1->CR3.bits.CTSE = 0;   // 关闭硬件流控制
        USART1->CR3.bits.RTSE = 0;
接着我们来配置波特率,波特率计算方式如下:波特率 = f_ck / (8 * (2 - OVER8) * USARTDIV)。 OVER8 = 0时,USARTDIV = mantissa + fraction / 16;OVER8 = 1时,USARTDIV = mantissa + fraction / 8。 这里的f_ck是串口的驱动时钟,对于USART1是高速外设时钟APB2的总线时钟,频率为为84MHz,我们用宏定义FRE_APB2来表示。
        USART1->CR1.bits.OVER8 = 0;  // 对起始位进行16次重采样,并依此计算波特率
        tmp = (FRE_APB2 / 4) * 25 / baudrate;
        mantissa = tmp / 100;
        fraction = (16 * (tmp - 100 * mantissa) + 50) / 100;
        USART1->BRR.bits.mantissa = mantissa;
        USART1->BRR.bits.fraction = fraction;
至此,我们就完成了USART1基本功能的配置,下面打开串口的接收中断,并写寄存器CR1的UE位开启串口。

        USART1->CR1.bits.RXNEIE = 1 // 开启接收中断
        USART1->CR1.bits.UE = 1;     // 开启串口
    }

打开串口以后,我们就可以通过向写数据寄存器DR来发送数据了。下面是发送一个字节的简单函数,把参数写入DR寄存器中。 然后检测状态寄存器的TXE位是否置位,来判定是否发送完毕,进而退出函数。这里用了一个死循环来进行判定,这样就会造成一个问题就是,一旦系统出现了问题就会一直卡在这里。 更好的办法是判定在一段时间之内是否发送成功,如果超时则退出并报错。

        uint8 usart1_send_byte(uint8 value) {
            USART1->DR.bits.byte = value;
            while (!USART1->SR.bits.TXE);
            return 0;
        }

我们通过中断服务接收来自上位机的数据,并在中断服务函数中把接收到的数据发送给上位机。下面的函数用于配置串口的中断优先级并开启串口中断。 关于中断的详细使用方法,可以参考外部中断控制LED灯

        void config_interruts(void) {
            SCB->AIRCR = SCB_AIRCR_KEY_VALUE | NVIC_PGroup_1;

            NVIC->IPR.bits.USART1_Irq = 0x80;
            NVIC->ISER.bits.USART1_Irq = 1;
        }
我们在中断服务函数中通过RXNE确定已经接收了一个字节,并将接收到的数据通过函数usart1_send_byte()发送出去, 这样我们在上位机给开发板发送任何数据时都应当接收到相同的数据。
        void USART1_IRQHandler(void) {
            if (0 != USART1->SR.bits.RXNE) {
                uint8 data = USART1->DR.bits.byte;
                usart1_send_byte(data);
            }
        }

5. 总结

在本文中,我们先介绍了ISO的七层通信协议。然后参考七层协议,我们从物理层和数据层两个方面介绍了串口。

在物理层方面,主要是确定导线上的电平与逻辑0-1之间的对应关系。一般从芯片中直接导出的电平是TTL后者CMOS电平,它们接口简单但不适合高频远距离的传输。 因而人们提出了RS232电平。为了进一步提高传输距离和频率,又发展出了差分形式的RS422和RS485电平。

在数据称方面,主要工作是把电平信号转换为有意义的数据。对于串口通信而言主要有四个要素:

  1. 空闲时通信导线上总为逻辑1
  2. 通信以一个bit的起始位开始,对应电平逻辑为'0'
  3. 根据一帧数据中'1'出现的次数为奇数还是偶数可以进行奇偶校验
  4. 一帧数据以'1'的电平逻辑为结束位,根据不同的设置可以有1位、1.5位或者2位

最后,我们探索者开发板上实现了一个USART1的驱动,并详细解释了初始化代码。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1