Edit online

设计说明

3 Dec 2024
Read time: 10 minute(s)

串口芯片

在驱动代码说明开始前先介绍几种通用的串口芯片。
  • 8250:IBM PC 及兼容机使用的第一种串口芯片。这是一种相对来说很慢的芯片,有时候装载到它的寄存器速度太快,它来不及处理,就会出现数据丢失现象。8250 有 7 个寄存器,支持的最大波特率为 56kb。
  • 16450:8250A 的快速版。加快了处理器存取它的速度,但最大速度还是 56kb。有些人实际用得比这高也可以。
  • 16550/16550A:第一种带先进先出(FIFO)功能的 8250 系列串口芯片。

    AIC UART 模块是一 16550A UART,属于标准的 8250 系,而 Linux 对 8250 系串口有通用的驱动,我们采取在该驱动上添加 AIC 私有的代码的方式实现我们的驱动。

驱动配置宏

因为 8250 驱动兼容很多后续开发的串口芯片,因此有非常多的编译功能配置项。

menuconfig 中位置:device drivers/Character devices/8250 16550 and compatible serial support。关键配置项说明如下:

1. 关键配置项
配置项 说明
SERIAL_8250_DEPRECATED_OPTIONS: Support 8250_core.* kernel options (DEPRECATED) 3.7 版本的错误,不应该打开
SERIAL_8250_PNP: 8250/16550 PNP device support 不打开
SERIAL_8250_16550A_VARIANTS: 16550A serial port 打开
SERIAL_8250_FINTEK: Fintek F81216A LPC to 4 UART RS485 API 不打开
SERIAL_8250_CONSOLE: 8250/16550 and compatible serial port 打开
SERIAL_8250_DMA: DMA support for 16550 compatible UART controllers 打开
SERIAL_8250_NR_UARTS: Maximum number of 8250/16550 serial ports 设置为 8
SERIAL_8250_RUNTIME_UARTS: Number of 8250/16550 serial ports to register at runtime 设置为 8
SERIAL_8250_EXTENDED: Extended 8250/16550 serial driver options 没有使用,无关系
SERIAL_8250_MANY_PORTS 没有使用,无关系
SERIAL_8250_SHARE_IRQ: Support for sharing serial interrupts 不打开
SERIAL_8250_RSA: Support RSA serial ports 不打开,没有使用
SERIAL_8250_DW: Support for Synopsys DesignWare 8250 quirks 不打开,不需要兼容
SERIAL_OF_PLATFORM: Device tree based probing for 8250 ports 不打开,使用 AIC 定义的 OF

寄存器

8250 驱动使用索引来定义寄存器,地址的计算方式为索引 * 位宽。 AIC UART 的寄存器大致可分为两种:
  • 标准寄存器:0x7C 之前,为 8250 标准寄存器,在 include/uapi/linux/serial_reg.h 中定义,AIC 对如下 3 个寄存器值有修改
    #define UART_IER   1    /*Interrupt Enable: Shifter_Reg_Empty_EN*/
    #define UART_IIR   2    /*Interrupt Identity:Shifter_Reg_Empty_INT*/
    #define UART_MCR   4    /*Modem Control:UART_FUNCTION*/
  • 扩展寄存器:0x7C 之后,为 AIC 新扩展,需要额外的逻辑代码,譬如 RS485,在 8250_artinchip.h 中定义
    #define AIC_REG_UART_USR   0x1f        /* UART Status Register */
    #define AIC_REG_UART_TFL   0x20        /* transmit FIFO level */
    #define AIC_REG_UART_RFL   0x21        /* Receive FIFO Level */
    #define AIC_REG_UART_HSK   0x22        /* DMA Handshake Configuration */
    #define AIC_REG_UART_HALT  0x29        /* Halt tx register */
    #define AIC_REG_UART_DLL   0x2C        /* DBG DLL */
    #define AIC_REG_UART_DLH   0x2D        /* DBG DLH */
    #define AIC_REG_UART_RS485 0x30        /* RS485 control status register */

程序入口

  • 8250_core:8250 标准寄存器/逻辑的初始化入口,三方驱动大都借助 8250_core 完成其工作,文件:8250_core.c。
    module_init(serial8250_init);
    module_exit(serial8250_exit);
  • aic-uart:aic8250 是 artinchip 的 uart 驱动的入口,声明驱动和初始化私有寄存器/逻辑,文件:8250_artinchip.c
    static const struct of_device_id aic8250_of_match[] = {
       { .compatible = "artinchip,aic-uart-v1.0" },
       { /* Sentinel */ }
    };
    MODULE_DEVICE_TABLE(of, aic8250_of_match);
    
    static struct platform_driver aic8250_platform_driver = {
       .driver = {
          .name          = AICUART_DRIVER_NAME,
          .pm            = &aic8250_pm_ops,
          .of_match_table        = aic8250_of_match,
       },
       .probe                    = aic8250_probe,
       .remove           = aic8250_remove,
    };
    module_platform_driver(aic8250_platform_driver);

数据结构

  • uart_8250_port
    声明在 include/linux/serial_8250.h 中,驱动中以变量名:up 变量存在,artinchip 驱动中未进行任何设置,只 overlay 了 AIC 平台的 rs485 设置函数, 其他完全使用 8250_core.c 中参数。uart_8250_port 的初始化在 8250_core.c:serial8250_register_8250_port 中完成。
    struct uart_8250_port {
       struct uart_port          port;
       …
       struct uart_8250_dma      *dma;
       const struct uart_8250_ops        *ops;
       /* 8250 specific callbacks */
       int                       (*dl_read)(struct uart_8250_port *);
       void                      (*dl_write)(struct uart_8250_port *, int);
       struct uart_8250_em485 *em485;
       void                      (*rs485_start_tx)(struct uart_8250_port *);
       void                      (*rs485_stop_tx)(struct uart_8250_port *);
    
       .dl_read          = default_serial_dl_read
       .dl_write         = default_serial_dl_write
       .rs485_config = aic8250_rs485_config;
       .rs485            = NULL;
       .rs485_start_tx = NULL;
       .rs485_stop_tx = NULL;
    
  • uart_port:UART 驱动的关键数据结构,驱动中以变量名:p 存在,声明 include/linux/serial_core.h,包含变量和关键操作函数的 overlay 入口。
    struct uart_port {
       spinlock_t     lock;                      /* port lock */
       unsigned long  iobase;            /* in/out */
       unsigned char __iomem     *membase;               /* read/write */
       unsigned int   (*serial_in)(struct uart_port *, int);
       void           (*serial_out)(struct uart_port *, int, int);
       ... ...
  • aic8250_data
    struct aic8250_data {
            struct aic8250_port_data     data;
    
            int                  msr_mask_on;
            int                  msr_mask_off;
            struct clk           *clk;
            struct reset_control *rst;
    
            unsigned int         uart_16550_compatible:1;
            unsigned int         tx_empty;
            unsigned int         rs485simple;      //compact-io mode
    };
  • Operations:uart_port 各操作接口设置和操作的寄存器。

    蓝色接口为可以在用户的驱动中定制,否则使用 core 中标准接口。

    .handle_irq  = aic8250_handle_irq;
    .pm          = aic8250_do_pm;
    .serial_in   = aic8250_serial_in32;
    .serial_out  = aic8250_serial_out32;
    .set_ldisc   = aic8250_set_ldisc;
    .set_termios = aic8250_set_termios;
    .set_mctrl   = serial8250_set_mctrl, //set modem control, 包括 termios  和 modem 的转换,Modem Control Reg: MCR
    .get_mctrl   = serial8250_get_mctrl, //get modem control
    .startup     = serial8250_startup,   //寄存器初始化
    .shutdown    = serial8250_shutdown,  //
    .get_divisor   = serial8250_do_get_divisor
    .set_divisor    = serial8250_do_set_divisor
    .handle_break =
    .tx_empty       = serial8250_tx_empty    //Line Status: Transmitter Empty & TX Holding Register Empty
    .stop_tx     = serial8250_stop_tx,   //serial8250_clear_THRI,Interrupt Enable Reg,Enable Transmitter:10,Enable Receive:01
    .start_tx    = serial8250_start_tx,  //serial8250_set_THRI,Interrupt Enable Reg,Enable Transmitter:10,Enable Receive:01
    .throttle    = serial8250_throttle,  //synclink_gt.c,
    .unthrottle  = serial8250_unthrottle,
    .stop_rx     = serial8250_stop_rx,   //Interrupt Enable Reg,Enable Receive:01,Enable receiver line status interrupt: 04
    .enable_ms   = serial8250_enable_ms, //Interrupt Enable Reg,Enable Modem status interrupt:08
    .break_ctl   = serial8250_break_ctl, //Line Control Reg,Break Control Bit:
    .type                = serial8250_type,              //8250 or 16550 or TI16750
    .release_port        = serial8250_release_port,      //release_mem_region
    .request_port= serial8250_request_port,//request_mem_region
    .config_port = serial8250_config_port,       //
    .verify_port = serial8250_verify_port,
    

关键流程

  • serial8250_init
    serial8250_init
       |-->serial8250_isa_init_ports
         |-->UART_NR                        //8 个
         |-->serial8250_init_port
             |-->port->ops = &serial8250_pops;    //标准 ops
         |-->serial8250_set_defaults     //io,dma,fifosize,标准 DMA 接口
         |-->serial8250_isa_config
       |-->uart_register_driver
       |-->serial8250_pnp_init
       |-->platform_device_add
       |-->serial8250_register_ports
       |-->platform_driver_register
    
  • aic8250_probe
    aic8250_probe
       |-->regs = platform_get_resource
       |-->platform_get_irq
       |-->p->handle_irq = aic8250_handle_irq;
       |-->p->pm         = aic8250_do_pm;
       |-->p->type               = PORT_16550
       |-->p->serial_in  = aic8250_serial_in32;
       |-->p->serial_out         = aic8250_serial_out32;
       |-->p->set_ldisc            = aic8250_set_ldisc;
       |-->p->set_termios        = aic8250_set_termios;
       |-->p->regshift            = dts:reg-shift, reg 32bit
       |-->Prepare clk
       |-->prepare reset
       |-->aic8250_apply_quirks  : 处理 aic  私有逻辑,dcd-override,dsr-override,cts-override,ri-override
          |-->aic8250_init_special_reg
       |-->data->data.line = serial8250_register_8250_port: return port number
       |-->platform_set_drvdata
    
  • serial8250_register_8250_port
    serial8250_register_8250_port
       |-->return port number
       |-->serial8250_find_match_or_unused
       |-->copy aic uart_8250_port to local uart_8250_port
       |-->uart_get_rs485_mode:
         |-->rs485-rts-delay,rs485-rx-during-tx,linux,rs485-enabled-at-boot-time,rs485-rts-active-low,rs485-term
       |-->mctrl_gpio_init:    //null
       |-->serial8250_set_defaults //io,dma,fifosize setting
       |-->serial8250_apply_quirks
         |-->uart_add_one_port
    

RS485

ArtInChip 的 RS485 有两种工作模式:

  • 标准模式:标准 RS485 接口,有 B+/B-两根数据线和发送使能,共三线

    通过在 board.dts 中添加 linux,rs485-enabled-at-boot-time 配置为 RS485 模式,代码流为:
    static void aic8250_apply_quirks(struct device *dev, struct uart_port *p,
          struct aic8250_data *data)
    {
            struct device_node *np = p->dev->of_node;
            int id;
    
            uart_get_rs485_mode(p);
    }
    
    drivers/tty/serial/serial_core.c
    
    int uart_get_rs485_mode(struct uart_port *port)
    {
       if (device_property_read_bool(dev, "linux,rs485-enabled-at-boot-time"))
          rs485conf->flags |= SER_RS485_ENABLED;
    }
  • 精简模式:为 ArtInChip 定制版,使用单数据线进行数据传输,加发送使能共二线

    精简模式 RS485 首先是 RS485,因此需要在开启 RS485 的基础上进行额外的配置,通过在 board.dts 中添加 aic,rs485-compact-io-mode 完成配置,代码流为:
    drivers/tty/serial/serial_artinchip.c
    aic8250_apply_quirks:
    if (device_property_read_bool(dev, "aic,rs485-compact-io-mode"))
       data->rs485simple = 1;
    
    static int aic8250_rs485_config(struct uart_port *p,
          struct serial_rs485 *prs485)
    {
       struct aic8250_data *d = to_aic8250_data(p->private_data);
         unsigned int mcr = p->serial_in(p, UART_MCR);
         unsigned int rs485 = p->serial_in(p, AIC_REG_UART_RS485);
    
         mcr &= AIC_UART_MCR_FUNC_MASK;
         if (prs485->flags & SER_RS485_ENABLED) {
                 if (d->rs485simple)
                         mcr |= AIC_UART_MCR_RS485S;
                 else
                         mcr |= AIC_UART_MCR_RS485;
    
                 rs485 |= AIC_UART_RS485_RXBFA;
                 rs485 &= ~AIC_UART_RS485_CTL_MODE;
         } else {
                 mcr = AIC_UART_MCR_UART;
                 rs485 &= ~AIC_UART_RS485_RXBFA;
         }
    
         p->serial_out(p, UART_MCR, mcr);
         p->serial_out(p, AIC_REG_UART_RS485, rs485);
    
         return 0;
    }

DMA

AIC 的 DMA 和标准的 DMA 有 一点使用不同,限制了我们只能使用自己私有的 DMA 接口,主要差别点在于我们必须设置 UART FIFO 中 的数据长度进行 DMA 搬运才不会出错,测试的逻辑为:

  • 如果设置了 FIFO 的中断触发为 1/2,则在 FIFO 中数据达到 128byte 后会收到 data ready 中断

  • 如果这个时候 UAR T 在继续接收,则 FIFO 中的数据会按 4bytes/次 的速度增加

  • 如果设置了 DMA 接收长度为 64,则 128-64 = 64 会丢失

  • 如果设置了 DM A 接收长度为 1024,则因为 fifo 中的数据最多为 256,则永远收不满

因此 AIC 的 DMA 处理逻辑为:

  • 收到 data ready 中断后,先停掉 UART 的接收,防止 FIFO 中数据增加

  • 读取当时 FIFO 中数据的长度,作为 DMA 的参数进行数据搬移

  • DMA 搬移成功后重启 UART 数据接收

int aic8250_dma_rx(struct uart_8250_port *p)
{
   struct uart_8250_dma              *dma = p->dma;
   struct dma_async_tx_descriptor    *desc;

   if (dma->rx_running)
      return 0;

   aic8250_set_ier(p, false);
   dma->rx_size = p->port.serial_in(&p->port, AIC_REG_UART_RFL);

   desc = dmaengine_prep_slave_single(dma->rxchan, dma->rx_addr,
                  dma->rx_size, DMA_DEV_TO_MEM,
                  DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
   if (!desc)
      return -EBUSY;

   dma->rx_running = 1;
   desc->callback = aic8250_dma_rx_complete;
   desc->callback_param = p;

   dma->rx_cookie = dmaengine_submit(desc);

   dma_async_issue_pending(dma->rxchan);

   return 0;
}

DMA 寄存器

DMA 的所有寄存器不建议进行参数调整,使用默认值即可,但 DMA 需要工作在 HSK 模式。
p->serial_out(p, AIC_REG_UART_HSK, AIC_HSK_HAND_SHAKE);

修改总结

AIC 的 UART 驱动附着于 8250 标准驱动,但又有不一样的地方,总结一下修改列表
  • 驱动接口
    • 添加 AIC 私有接口,8250_artinchip.c,8250_artinchip.h

    • 从 board.dst 中读取并配置 uartclk

    • 配合 SOC 功能简化代码

    • 移除 autoconfig 功能,因为有些 try 会导致输出乱码

    • DMA 策略添加和私有逻辑接口

RS485 支持

RS485 的支持需要设置 Modem_Control 和 RS485 Control and Status 两个寄存器,添加了 aic8250_rs485_config 处理接口