简单介绍串口、RS232、RS485在单片机与嵌入式linux系统中的使用

1 前言

  串口通信协议与spi、i2c是嵌入式基础的3个通信协议。之前仅在调试设备时使用串口功能,没深入了解过串口,以为就是个简单的协议;最近工作用到了RS484协议后,稍微深入了解了一下,发现串口涉及到的东西太多了。从硬件到linux系统的整个tty子系统框架,到应用层的协议封装发送,太多太繁杂了,深入起来毫无头绪。还好它的入门知识极其简单,应用层的接口资料很丰富,使用起来倒是没那么麻烦。基于此,我汇集前人智慧做篇总结文档。至于复杂的tty子系统还是先放一放吧,等我把基于rv1106的linux的卡片计算机做出来以后,基于这个系统专门研究一下从硬件到系统到驱动到应用层的串口全流程实现,专门针对tty子系统相关的所有东西搞出一篇博客来。

2 总线相关的概念

2.1 串口总线相关的概念

总线 :连接多个部件的信息传输线,是各部件共享的传输介质。
UART : 通用的异步接收器和发送器,串口主要是被用来实现两个设备之间的通信的。

2.2 串行总线和并行总线

串行通信 : 指的是同一时刻只能收或发一个bit位信息。因此只用1根信号线即可。
并行通信 : 指的是同一时刻可以收或发多个bit位的信息,因此需要多根信号线才行 。



  在实际的开发中具体使用的是串行还是并行根据实际的需要进行选择。

串行:UART , IIC,SPI , USB, 485, CAN
并行:硬盘,内存条,TF卡,SD卡,MIPS LCD, 摄像头

2.3 单工,全双工,半双工总线

单工 : 要么收,要么发,只能做接收设备或者发送设备。
半双工 : 可以收,可以发,但是不能同时收发。
全双工 : 可以在同一时刻既接收,又发送。



半双工:IIC, USB 2.0,485, CAN
全双工:UART, SPI,USB3.0

2.4 同步总线和异步总线

  同步通信: 一般情况下同步通信指的是通信双方根据同步信号进行通信的方式。

比如通信双 方有一个共同的时钟信号,大家根据时钟信号的变化进行通信。
 一个时钟周期收发一个bit位的数据。



  异步通信: 是指数据传输速度匹配依赖于通信双方有自己独立的系统 时钟,大家约定好通信的速度。

异步通信不需要同步信号,但是并不 是说通信的过程不同步。



2.5 串口总线的硬件连接



  UART : 通用的异步串行全双工总线。

3 串口、rs232、rs485的简单介绍

  基于上文,我们了解了基础的串口工作方式是异步串行全双工总线。如下是对串口通信协议的解释,后面学习后发现无论是是串口还是rs232或是rs485落到最底层都遵循基本的串口通信协议。

3.1 串口总线的通信协议

  通信协议:通信双方约定好通信的固定的格式,不如约定通信的速率,开始通信和结束通信的标志。
(学习任何一种总线,首先要学习的是总线的通信的协议。)



  • 起始位:发送器给接收器发送数据的开始信号;
  • 数据为:要发送的数据,先发送低位在发送高位;
  • 校验位:奇偶校验位
      奇校验:数据位和校验位1的个数之和为奇数;如果发送的数据为0x55, 校验位自动补1;如果发送的数据为0x51,校验位自动补0.
      偶校验:数据位和校验位1的个数之和为偶数;如果发送的数据为0x55, 校验位自动补0;如果发送的数据为0x51,校验位自动补1.
  • 停止位:一帧数据发送结束,校准时钟。
      为什么要校准时钟?串口采用的是异步总线,各自采用各自独立的时钟源,虽然要求时钟的频率保持一致,但是时钟也会存在一定的误差,再发送数据的过程中误差会进行累积,因此发送一帧数据结束之后需要校准时钟。
  • 波特率/比特率:单位bps, 1s中发送数据的bit位数,常用的波特率为115200bps,9600bps.
  • 8N1 : 8位数据位,没有校验位,1个停止位。


3.2 串口的简单介绍

  如下是典型的串口接线示意图。串口一般3根线,地线是基础,为信号电流提供闭合回路、消除共模干扰、提供电位基准;tx和rx是读写线,接线方式如下所示交叉连接,读写互不影响,所以它可以同时收发是全双工(所以通信线路针对某一方来说,可以只有一条通信线路,只发送不接收,或者只接收不发送;也可以有两条,既发送又接收);又因为没有时钟线,所以传输时依靠各自设备的时钟频率,是异步总线。(因此只要保证基础的配置参数没问题即可使用。)



  下图是串口数据发送示意图,实际上是一个时域示意图,就是信号随着时间变化的对应关系。比如在单片机的发送引脚上,左边的是先发生的,右边的是后发生的,数据位的切换时间就是波特率分之一秒。



  要观察到实际的串口通信,也可以用逻辑分析仪或者示波器等仪器测量单片机的串口通信,如下图是通过串口调试助手给单片机发送了一个0xE4数据,使用Kingst LA5016逻辑分析仪测量的一组波特率为9600的串口信号。从下图可以看出,左侧首先是一个起始位低电平(绿圆点标志),然后是8位数据(白圆点标志),低位在左边,高位在右边,最后一位停止位(红方点标志)。



3.3 rs232的简单介绍

  在某些工业应用场景下,经常会遇到9针的串行接口,这个串行接口叫做RS232接口。在物理结构上分为9针的和9孔的,一般称为公头和母头,如下图所示。



  RS232接口一共有9个引脚,分别定义是:1、载波检测DCD;2、接收数据RXD;3、发送数据TXD;4、数据终端准备好DTR;5、信号地线SG;6、数据准备好DSR;7、请求发送RTS;8、清除发送CTS;9、振铃提示RI。
  要让这个串口与单片机UART通信,只需要关心其中的2脚RXD、3脚TXD和5脚GND。虽然这三个引脚的名字与单片机上的串口名字一样,但是不能直接与单片机对连通信。对于RS232标准来说,它是个反逻辑,也叫做负逻辑。为何叫负逻辑?它的TXD和RXD的电压,-3V~-15V电压代表是1,+3~+15V电压代表是0。低电平代表的是1,而高电平代表的是0,所以称之为负逻辑。使用Kingst LA5016逻辑分析仪测量RS232波形如图5所示,在负逻辑的配置下,没有信号的时候为低电平,一位高位起始位(绿圆点标志),8位数据位(白圆点标志),1位停止位(红方点标志),高电平代表‘0’,低电平代表‘1’,解析的数据是0xE4。
  因为RS232电平是负逻辑,且电平值可以高达十几V,而通常单片机引脚是兼容的TTL电平标准。那么RS232接口想要与单片机相连接,就需要用一个电平转换芯片(比如MAX232)来完成,如下图所示。



  在实际项目中常会遇到板子上的串口只引出了最基本的3根线,而调试时想要使用也不能直接使用其标准的232接口,因此最常使用的就是如下所示的usb转232串口线。



3.4 rs485的简单介绍

  在工业控制、电力通讯、智能仪表等领域,通常情况下是采用串口通信的方式进行数据交换。最初采用的方式是RS232接口,由于工业现场比较复杂,各种电气设备会在环境中产生比较多的电磁干扰,会导致信号传输错误。除此之外,RS232接口只能实现点对点通信,不具备联网功能,最大传输距离也只能达到十几米,不能满足远距离通信要求。而RS485则解决了这些问题,数据信号采用差分传输方式,可以有效的解决共模干扰问题,最大距离可达1200米,并且允许多个收发设备接到同一条总线上。
  RS485采用差分信号进行通信,总线由2条线组成,分别命名为A和B,当A端电平大于B端电平时代表1,B端电平大于A端电平时代表0。所以RS485也不能直接与单片机引脚直接连接,而是需要专用的RS485转接芯片才可以与单片机UART接口连接,如下图所示。



  MAX485芯片5脚和8脚是电源引脚;6脚和7脚就是RS485通信中的A和B两个引脚;1脚和4脚分别接到单片机的RXD和TXD引脚上,使用单片机UART进行数据接收和发送;2脚和3脚是方向引脚,其中2脚是低电平使能接收器,3脚是高电平使能输出驱动器,可以把这两个引脚连到一起,平时不发送数据的时候,保持这两个引脚是低电平,让MAX485处于接收状态,当需要发送数据的时候,把这个引脚拉高,发送数据,发送完毕后再拉低这个引脚就可以了。为了提高RS485的抗干扰能力,通常会在总线的两端,在A和B之间并接一个120Ω的电阻。
  UART、RS232、RS485在通信的数据格式上是相同的,三者区别主要是逻辑电平的判定方式不同。RS232与RS485在经过专用的电平转换芯片转成TTL电平后就与标准的UART相同了,可以同样由单片机的UART外设来实现通信数据的收发。

4 基于单片机驱动串口

4.1 分析UART的电路图

  要实现串口基本功能一般会引出2根读写引脚引脚,如下是想要最终驱动串口的整体逻辑流程。
(多功能复用引脚:一个引脚具有多个功能,这样的引脚就叫做多功能复用引脚,再同一时刻只能将引脚设置为一个功能。串口实验使用的GPIO引脚就是使用的串口的复用的功能。)



  如下是pcd原理图中对应的引脚编号。






4.2 分析芯片手册

  单片机的使用流程无外乎读数据手册,查看相关的寄存器,初始化寄存器配置,如下是关于串口的寄存器的分析及参数配置。

4.2.1 分析2.5.2章节

  确定RCC,GPIOB,GPIOG,UART4外设分别接到哪根总线上,以及外设寄存器对应的基地址。






4.2.2 分析RCC章节

  使能GPIOB,GPIOG和UART4外设的时钟。






4.2.3 分析GPIO章节

  设置引脚为串口功能。



4.2.3.1 GPIOx_MODER寄存器



4.2.3.2 GPIOx_AFRL寄存器









4.2.3.3 GPIOx_AFRH寄存器









4.2.4 分析UART4章节



4.2.4.1 USART_CR1寄存器






























4.2.4.2 USART_CR2寄存器






4.2.4.3 USART_BRR寄存器






    假设usart_ker_ck_pres = 64Mhz, 波特率为115200bps,

    采样率为16倍, 则USART_BRR[15:0] = 64000000 / 115200 = 555 = 0x22B

    采样率为8倍, 则USART_BRR[15:0] = ?
        USARTDIV = 2 * 64000000 / 115200 = 1110 = 0x456
        USART_BRR[15:0] = 0x453

4.2.4.4 USART_RDR寄存器



4.2.4.5 USART_TDR寄存器



4.2.4.6 USART_PRESC寄存器



4.2.4.7 USART_ISR寄存器






TXE : 发送数据寄存器是否为空位
如果发送数据寄存器为空,则可以发送下一个字节的数据;
如果发送数据寄存器不为空,则不可以发送下一个字节的数据。

当发送寄存器中的数据被传送到发送移位寄存器中时,硬件自动将TXE位置1;
当向发送寄存器中写入数据时,硬件自动将TXE位清0.

读TXE位为0时,表示发送数据寄存器中有数据,不可以发送下一个字节的数据;
读TXE为为1时,表示发送数据寄存器中没有数据,可以发送下一个字节的数据。



    TC : 发送完成位

    当发送寄存器中的最后一个数据数据发送完成并且TXE位被置1,硬件自动将TC位置1;
    当向发送寄存器中写入数据时,硬件自动将TC位清0.

    读TC位为0时,表示最后一个数据没有发送完成,不可以发送下一个字节的数据;
    读TC为为1时,表示最后一个数据发送完成,可以发送下一个字节的数据。



    RXNE : 读数据寄存器非空位

    如果接收数据寄存器为空,则不可以从接收数据寄存器中读取数据;
    如果接收数据寄存器不为空,则可以从接收数据寄存器中读取数据。

    当接收移位寄存器中的数据被传送到接收寄存器中时,硬件自动将RXNE位置1;
    当从接收数据寄存器中读取数据之后,硬件自动将RXNE位清0.

    读RXNE位为0时,表示接收数据寄存器中没有数据,不可以从接收数据寄存器中读数据;
    读RXNE位为1时,表示接收数据寄存器中有数据,可以从接收数据寄存器中读数据。



  如上就是初始化串口所需配置的所有串口了。除了基本的gpio引脚和时钟寄存器初始化外,主要是串口波特率的初始化,和读写寄存器外加uart中断和状态寄存器(ISR)的配置(通过其中的第5位和第7位即可判断出什么时候可读可写),搞懂这些寄存器的配置,串口就可以简单拿捏了。

4.3 基于单片机寄存器的串口驱动示例

4.3.1 重定向串口输

  实际使用串口时,为了统一接口、提高效率、避免直接操作寄存器,方便串口打印信息到终端,需要把printf重定向一下(人话就是执行程序时没有统一的接口,想让串口打印信息比较麻烦,统一使用printf比较省事)。

main.c
extern void printf(const char *fmt, ...);

printf.c
void printf (const char *fmt, ...)
{
    va_list args;
    char printbuffer[100];
    va_start (args, fmt);

    /* For this to work, printbuffer must be larger than
     * anything we ever want to print.
     */
    vsprintf (printbuffer, fmt, args);
    va_end (args);
    __uart_puts (printbuffer);
}

uart.c
void __uart_putc(const char data)
{
    while (!(USART4->ISR & (0x1 << 7)));
    USART4->TDR = data;
    if (data == '\n')
        __uart_putc('\r');
}

  如果就想使用寄存器打印数据也行,如下是我实现的使用寄存器打印字符串的函数封装。

// 发送一个字节的函数
void hal_send_char(const char ch)
{
    // 1. 判断发送寄存器是否为空,如果为空发送下一个字节的数据,
    // 如果不为空等待发送数据寄存器为空。  USART4_ISR[7]
    while(!(USART4->ISR & (0x1 << 7)));
    // 2. 发送数据,向发送数据寄存器中写入数据 USART4_TDR[7:0]
    USART4->TDR = ch;
}

void hal_send_string(const char *str)
{
    // 1. 调用发送字符的函数一个一个的发送,字符串的结尾为‘\0’
     while (*str != '\0') {
        hal_send_char(*str);
        str++;
    }

}

  还有库函数的重定向方式,我这边没代码,不过殊途同归,都是一样的实现方式。(时间久远我这没有可以打印的开发板,打印效果大家就自己脑补吧)

4.3.2 串口代码实现

void hal\_uart4\_init(void)
{
// 1. 使能GPIOB,GPIOG外设的时钟 RCC\_MP\_AHB4ENSETR\[1]\[6] = 0b1
RCC->MP\_AHB4ENSETR = (0x1 << 1) | (0x1 << 6);
// 2. 设置PB2, 和 PG11引脚为复用的功能
// GPIOB\_MODER\[5:4] = 0b10  GPIOG\_MODER\[23:22] = 0b10
GPIOB->MODER &= (~(0x3 << 4));
GPIOB->MODER |= (0x2 << 4);
GPIOG->MODER &= (~(0x3 << 22));
GPIOG->MODER |= (0x2 << 22);
// 3. 设置PB2引脚为UART4\_RX功能  GPIOB\_AFRL\[11:8] --> AF8 --> 0b1000
// 设置PG11引脚为UART4\_TX功能  GPIOG\_AFRH\[15:12] --> AF6 --> 0b0110
GPIOB->AFRL &= (~(0xF << 8));
GPIOB->AFRL |= (0x8 << 8);
GPIOG->AFRH &= (~(0xF << 12));
GPIOG->AFRH |= (0x6 << 12);
// 4. 使能UART4外设的时钟  RCC\_MP\_APB1ENSETR\[16] = 0b1
RCC->MP\_APB1ENSETR = (0x1 << 16);
// 5. 判断UART4串口是否使能,如果使能则禁止串口
if (USART4->CR1 & (0x1 << 0)) {
delay\_ms(2000);  // 等待之前的串口的数据发送完成之后在禁止串口
USART4->CR1 &= (\~(0x1 << 0));  // 禁止串口的使能
}\
// 6. 设置数据位为8位的数据宽度 USART4\_CR1\[28]\[12] = 0b00
USART4->CR1 &= ~((0x1 << 28) | (0x1 << 12));
// 7. 禁止校验位,不使用校验  USART4\_CR1\[10] = 0b0
USART4->CR1 &= ~(0x1 << 10);
// 8. 设置串口的采样率为16倍或者8倍,最终会影响波特率的计算  USART4\_CR1\[15]
USART4->CR1 &= ~(0x1 << 15);
// 9. 设置停止位的个数为1位  USART4\_CR2\[13:12] = 0b00
USART4->CR2 &= ~(0x3 << 12);
// 10. 设置串口时钟的分频寄存器 USART4\_PRERC\[3:0]  最终也会影响波特率的计算
// usart\_ker\_ck 时钟源的频率等于 64MHz
// usart\_ker\_ck\_pesc = usart\_ker\_ck / USART4\_PRESC\[3:0]
USART4->PRESC &= \~(0xF << 0);
// 11. 设置串口的波特率为115200bps  USART4\_BRR\[15:0]
USART4->BRR = 0x22B;
// 12. 使能串口发送器  USART4\_CR1\[3] = 0x1
USART4->CR1 |= (0x1 << 3);
// 13. 使能串口接收器  USART4\_CR1\[2] = 0x1
USART4->CR1 |= (0x1 << 2);
// 14. 使能串口        USART4\_CR1\[0] = 0x1
USART4->CR1 |= (0x1 << 0);
}
// 发送一个字节的函数
void hal\_send\_char(const char ch)
{
// 1. 判断发送寄存器是否为空,如果为空发送下一个字节的数据,
// 如果不为空等待发送数据寄存器为空。  USART4\_ISR\[7]
while(!(USART4->ISR & (0x1 << 7)));
// 2. 发送数据,向发送数据寄存器中写入数据 USART4\_TDR\[7:0]
USART4->TDR = ch;
if (ch == '\n')
hal\_send\_char('\r');
}

// 发送字符串的函数
void hal\_send\_string(const char \*str)
{
// 1. 调用发送字符的函数一个一个的发送,字符串的结尾为‘\0’
while (\*str != '\0') {
hal\_send\_char(\*str);
str++;
}
}

// 接收一个字符的函数
char hal\_recv\_char(void)
{
char ch;
// 1. 判断接收数据寄存器中是否有有效的数据,如果有数据则读取数据
//   如果没有有效的数据,则等待有有效的数据之后在读取。 USART4\_ISR\[5]
while(!(USART4->ISR & (0x1 << 5)));
// 2. 从接收数据寄存器中读取数据到ch变量中   USART4\_RDR\[7:0]
ch = (char)USART4->RDR;
return ch;
}

// 接收一个字符串的函数

char  buffer\[LEN] = {0};
char \* hal\_recv\_string(char str\[])
{
// 1. 一个字符一个字符的接收,接收到之后将数据存到buffer缓冲区中,
// 最多接收49,接收结束之后结尾需要补'\0'.
// 接收的情况,一种是接收49个字符结束,另一种是按下enter键之后结束
// enter键是一个字符'\r'.

    unsigned int i;
    for (i = 0; i < LEN - 1;i++) {
        buffer[i] = hal_recv_char();  // 接收一个字符
        hal_send_char(buffer[i]);   // 字符再终端回显
        if (buffer[i] == '\r') {
            break;
        }
    }
    buffer[i] = '\0';   // 字符串的结尾补'\0'
    hal_send_char('\n');
    return buffer;

}

  如上就是单片机驱动串口的全部内容。总得来说就是根据数据手册的对应章节读取寄存器配置,逐个配置寄存器信息,最后即可向读写寄存器发送或读取数据实现串口功能了。

5 基于linux嵌入式系统的串口驱动方式

  使用linux系统下的串口驱动就略显复杂了,不过万变不离其宗,核心思路是一样的。在驱动和设备树都没问题的情况下直接使用官方提供的接口函数初始化串口即可直接使用了。唯一需要注意的就是调用串口读写函数前,在应用层定义自己的数据帧格式方便处理。
  在Linux中rs232和rs485的使用也不难。因为数据从应用层到cpu这段流程中,它的处理用的还是串口,最后数据由串口出来后再经过专门的rs232和rs485芯片转换成所需的电平和差分信号,即可发给机器的另一端接收。

5.1 硬件接线

  linux这里以luckfox的串口教程为基础,如下是瑞芯微rv1106芯片的开发板引脚功能图。



  从图中可以看出,作为串口复用的引脚有四组,UART2、UART3、UART4、UART5;其中UART2是调试串口。

5.1.1 核心引脚功能解析

  看上图首先就会产生第一个问题,为啥有的串口组只有俩引脚rx和tx,有的就同时又cts和rts、tx和rx,从前文可以看出,串口通信是异步全双工通信,因此,最基础的功能实现只需要tx和rx即可,那其他引脚的作用是啥呢?
  如下是对引脚功能的解释:

CTS(Clear to Send)与RTS(Request to Send)
- 作用:属于硬件流控信号,用于协调设备间数据传输的时序。
- RTS:发送方通过拉高RTS信号表示“请求发送数据”
- CTS:接收方准备好后拉高CTS信号,表示“允许发送”
- 应用场景:防止数据溢出,尤其在高速或高负载通信中。例如,当接收缓冲区满时,CTS会置低以暂停发送

5.1.2 硬件流控与ISR寄存器区别

  这又引出新的问题,从功能上看和单片机中的ISR寄存器功能类似,是在linux上的另一种表述?继续查询发现并非如此。
  瑞芯微 RV1106 的 UART4 接口支持 CTS(Clear to Send)和 RTS(Request to Send)功能,并非直接由单片机中的 ISR(中断状态寄存器)“拓展”而来,而是 UART 控制器硬件本身支持的硬件流控(Hardware Flow Control)功能。以下是具体分析:
1.硬件流控的本质
  CTS/RTS 是 UART 通信中标准的硬件流控信号,用于解决数据传输时的缓冲区溢出问题:
  RTS(输出信号):通知接收方“本设备已准备好接收数据”。
  CTS(输入信号):接收方通过该信号告知发送方“是否允许发送数据”。
  这种机制独立于数据寄存器(如发送/接收缓冲区),由 UART 控制器的专用硬件逻辑实现,无需通过软件轮询 ISR 寄存器来模拟。
2.RV1106 的 UART 控制器设计
  RV1106 的 UART 模块符合通用串行控制器规范,其流控功能通过以下方式实现:
  专用寄存器控制:如控制寄存器(UART_CR)中的 CTSE(CTS 使能位)和 RTSE(RTS 使能位),用于开启硬件流控。
  状态寄存器反馈:ISR 寄存器中的标志位(如 CTSIF)仅用于反映 CTS 引脚的电平变化状态,而非“生成”流控功能本身。
(例如:当 CTS 引脚被对方设备拉低时,CTSIF 置位触发中断,但流控的响应仍由硬件自动处理。)
3.与单片机 ISR 寄存器的区别
  单片机场景:部分低端单片机可能缺乏硬件流控模块,需通过软件模拟(如用 GPIO 和 ISR 中断手动控制电平)。但 RV1106 作为高性能 SoC,其 UART 控制器已集成完整的硬件流控电路。
  RV1106 的实现:CTS/RTS 功能由 UART 控制器的物理引脚和内部状态机直接管理,开发者只需配置相关寄存器即可启用,无需软件干预(UART4 的 CTS/RTS 引脚可通过 30Pin 扩展接口引出,)。

// 示例:使能硬件流控
uart_port->cr |= UART_CR_CTSE | UART_CR_RTSE;  // 设置控制寄存器

  RV1106 的 UART4 的 CTS/RTS 功能是芯片硬件原生支持的流控机制,通过专用寄存器和物理引脚实现,与 ISR 寄存器仅存在状态反馈关系,而非由其“拓展”而来。其设计符合主流 UART 控制器标准(如 ARM Cortex-A 系列外设),可有效提升高速通信的可靠性。
  实际使用时没太压榨uart串口的性能,对流控没有太多研究,发现它不像单片机中的ISR寄存器一样需要在软件层面频繁操作,操作都由内核来完成,使用起来更方便。

5.1.2 设备节点

  正常来说串口驱动默认集成在内核中,在4.x以上的内核无需考虑适配问题。驱动默认支持232和485协议,唯一需要考虑的是485是半双工通信的,它的DE引脚控制输出输入方向,需要额外关注一下(实际上就是在传输数据时拉低拉高电平,在应用层或驱动层都很好处理,不是难点)。



  如下是对DE引脚在应用层使用时的2种处理方式:

5.1.2.1 无需设备树配置的条件

  引脚未被复用且默认功能为GPIO若目标GPIO在硬件设计中已默认作为普通输入/输出引脚,且未被其他功能(如I2C、SPI等)复用,则可直接通过sysfs操作。

echo 227 > /sys/class/gpio/export # 导出GPIO227[8,10](@ref)
echo out > /sys/class/gpio/gpio227/direction
echo 1 > /sys/class/gpio/gpio227/value #输出高电平

  芯片厂商已预设GPIO控制器大多数SoC(如树莓派、i.MX系列)在设备树中已定义GPIO控制器的基地址和功能,用户只需按编号计算实际引脚值(例如基地址 + 引脚号),无需额外配置

5.1.2.2 必须设备树配置的场景

  引脚被其他功能复用若目标引脚默认用于其他外设(如UART、PWM等),需通过设备树解除复用并声明为GPIO。

- 在设备树中关闭I2C占用:
&i2c4 {
    status = "disabled";  // 禁用I2C4释放GPIO引脚[5](@ref)
};
- 重新配置引脚为GPIO模式:
pinctrl_my_gpio: my_gpio_grp {
    fsl,pins = <
        MX8MM_IOMUXC_I2C4_SDA_GPIO5_IO21 0x19  // 复用为GPIO5_IO21
    >;
};

  驱动需绑定GPIO资源若需要在内核驱动中动态申请GPIO(如中断处理),则必须在设备树中声明引脚编号和属性,供驱动通过of_get_named_gpio()解析。
  然后选择将de引脚的处理逻辑放在驱动中还是应用层,反正是在发送数据前将引脚置为高,发送完后置为低接收数据,考虑好通信协议和时间间隔就好了,放在哪实现都一样。

5.2 应用层串口实现

  我这里使用的是rs485的串口代码作为示例,但实际上232和485还有串口的使用方法区别不大,需要注意的是全双工和半双工以及de引脚的区别。电平转换之类的协议问题都有相应的芯片替你完成了,不需要应用层来处理(当然如果对串口性能有高要求当我没说,我目前的项目对串口的使用没那么大数据量以及远距离传输的需求)。

5.2.1 完整代码

  通过以下程序,可以实现串口通信。

#include 
#include 
#include 
#include 
#include 
#include 
int main() {
    int serial_port_num;
    char serial_port[15];

    printf("Select a serial port (3/4): ");
    scanf("%d", &serial_port_num);   sprintf(serial_port,"/dev/ttyS%d",serial_port_num);
    int serial_fd;

    serial_fd = open(serial_port, O_RDWR | O_NOCTTY);
    if (serial_fd == -1) {
        perror("Failed to open serial port");
        return 1;
    }

    struct termios tty;
    memset(&tty, 0, sizeof(tty));

    if (tcgetattr(serial_fd, &tty) != 0) {
        perror("Error from tcgetattr");
        return 1;
    }

    cfsetospeed(&tty, B9600);
    cfsetispeed(&tty, B9600);

    tty.c_cflag &= ~PARENB;
    tty.c_cflag &= ~CSTOPB;
    tty.c_cflag &= ~CSIZE;
    tty.c_cflag |= CS8;

    if (tcsetattr(serial_fd, TCSANOW, &tty) != 0) {
        perror("Error from tcsetattr");
        return 1;
    }

    char tx_buffer[] = "hello world!\n";
    ssize_t bytes_written = write(serial_fd, tx_buffer, sizeof(tx_buffer));
    if (bytes_written < 0) {
        perror("Error writing to serial port");
        close(serial_fd);
        return 1;
    }
    printf("\rtx_buffer: \n %s ", tx_buffer);

    char rx_buffer[256];
    int bytes_read = read(serial_fd, rx_buffer, sizeof(rx_buffer));
    if (bytes_read > 0) {
        rx_buffer[bytes_read] = '\0';
        printf("\rrx_buffer: \n %s ", rx_buffer);
    } else {
        printf("No data received.\n");
    }

    close(serial_fd);

    return 0;
}

5.2.2 打开串口

  这段代码首先让用户选择使用串口3或串口4进行通信,然后打开了相应的串口设备文件,将其文件描述符保存在 serial_fd 变量中。

printf("Select a serial port (3/4): ");
scanf("%d", &serial_port_num);

sprintf(serial_port,"/dev/ttyS%d",serial_port_num);
int serial_fd;

serial_fd = open(serial_port, O_RDWR | O_NOCTTY);
if (serial_fd == -1) {
    perror("Failed to open serial port");
    return 1;
}

5.2.3 配置串口

  在这部分代码中,我们定义了一个名为 tty 的 termios 结构体,用于配置串口通信的参数。首先,我们使用 memset 将其初始化为0。然后,通过 tcgetattr 函数获取当前串口的属性,并将其存储在 tty 结构体中。

struct termios tty;
memset(&tty, 0, sizeof(tty));

if (tcgetattr(serial_fd, &tty) != 0) {
    perror("Error from tcgetattr");
    return 1;
}

  在这部分代码中,我们设置了串口通信的一些参数。我们使用 cfsetospeed 和 cfsetispeed 函数将波特率设置为9600,分别用于设置输出和输入的波特率;清除 PARENB 标志,以禁用奇偶校验;通过 c_cflag 属性操作标志来清除 CSTOPB 标志,以使用一个停止位;通过清除 CSIZE 标志来清除数据位,并通过 CS8 标志来设置数据位为8位。最后,使用 tcsetattr 函数将修改后的属性设置为串口的当前属性,这里使用了 TCSANOW 标志,表示立即应用这些设置。

cfsetospeed(&tty, B9600);
cfsetispeed(&tty, B9600);

tty.c_cflag &= ~PARENB;
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;

if (tcsetattr(serial_fd, TCSANOW, &tty) != 0) {
    perror("Error from tcsetattr");
    return 1;
}

5.2.4 发送数据

  这段代码通过向 serial_fd 写入字符串数据 "hello world!\n" 以实现串口数据发送,发送成功将在终端打印数据。


char tx_buffer[] = "hello world!\n";
ssize_t bytes_written = write(serial_fd, tx_buffer, sizeof(tx_buffer));
if (bytes_written < 0) {
    perror("Error writing to serial port");
    close(serial_fd);
    return 1;
}
printf("\rtx_buffer: \n %s ", tx_buffer);

5.2.5 接收数据

  这段代码通过从 serial_fd 读取数据以实现串口数据接收,接收成功将在终端打印数据。

char rx_buffer[256];
int bytes_read = read(serial_fd, rx_buffer, sizeof(rx_buffer));
if (bytes_read > 0) {
    rx_buffer[bytes_read] = '\0';
    printf("\rrx_buffer: \n %s ", rx_buffer);
} else {
    printf("No data received.\n");
}

6 驱动层解析

  驱动层面是基于tty子系统实现的,因此使用时直接使用tty子系统的串口驱动,正常情况下不需要适配,但读读驱动代码,了解了解驱动实现还是很有用的,整个子系统远比spi和i2c子系统复杂,这块等我制作完自己的开发板后再更新。

6.1 串口上电流程的浅析

  学习串口时想到232串口为啥一上电就能输出系统开机的信息,肯定是一开始就初始化了。首先想到了开机启动流程中u-boot的作用之一就是初始化串口,这里会配置默认的波特率波特率的标准与配置;再结合Linux的启动流程:

  • Bootloader阶段:
  • 瑞芯微闭源DDR bin默认波特率为1.5Mbps,需通过ddrbin_param.txt修改为115200
  • U-Boot的CONFIG_BAUDRATE参数通常设为115200或 1500000,需与DDR bin一致
  • 内核阶段:
  • 调试串口波特率通过设备树rockchip,baudrate属性定义,支持 115200 或 1.5Mbps

  所以串口初始化的同时,对应到调试界面的波特率就固定了(u-boot与内核移植时二者使用的设备树是一样的)。
  而到这里又延申出一个问题,那初始化时是所有引脚都初始化吗?恐怕不是吧,首先用不到所有的串口,其次都初始化后存在一个问题,开机时间会延长,仅初始化串口的时间固然很短,但都这个思路来,所有的管脚都来一遍,那开机时间必然要加长了。
  又查资料外加看rk3568的数据手册发现,瑞芯微的默认调试串口引脚是uart2。如下是对默认串口引脚的确认分析:
  1.Bootloader阶段的默认串口RK3568平台在Bootloader阶段(如U-Boot)默认使用特定UART作为调试串口,具体取决于板级配置:

  • 瑞芯微官方SDK默认将 UART2 作为调试串口(通过设备树fiq-debugger节点配置)
  • 若您的硬件通过 UART0 外接RS232,需检查以下配置:
  • 确认开发板原理图中UART0是否被定义为调试串口。
  • 在U-Boot的设备树文件(如rk3568-u-boot.dtsi)中,查看fiq-debugger节点的rockchip,serial-id属性是否为<0>(即UART0)

  2.其他串口的初始化Bootloader通常仅初始化调试串口,其他UART需手动启用:

  • 非调试串口默认处于关闭状态(status = "disabled")
  • 若需使用其他UART(如UART0),需修改设备树将其status设为"okay"。

  综合上述分析确认了瑞芯微的默认初始化串口和默认波特率参数,也从头到尾弄明白了串口调试输出的全流程和全路径。

  在查询串口时了解到串口有专门的类型编号,通过编号可精准识别USB设备类型,并有效区分普通设备与串口转换设备。

设备类型 特征标识 典型VID/PID示例
普通存储设备 - bInterfaceClass=0x08 - 0781:5530(SanDisk U盘)
HID设备(键鼠) - bInterfaceClass=0x03 - 046d:c52b(罗技接收器)
串口转USB设备 - bInterfaceClass=0x02/0xFF(厂商自定义) - 0403:6001(FTDI)
网络适配器 - bInterfaceClass=0x0E(无线设备类) - 0bda:8152(Realtek网卡)

7 总结

  没啥可总结的,整篇内容是在我学习串口内容是融会贯通后整理的一篇笔记,添加了我的一点思考总结而已,比较浅显。有啥不懂的可以发我邮箱讨论。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇