WK2124下linux驱动分析与移植

WK系列 SPI拓展4串口驱动移植参考文档 V2.4

1、概述

本文档主要适用于SPI拓展UART 驱动移植的参考。本文档基于V2.4版本的驱动来进行说明的,其它版本的驱动也可以参考。

1.1 WK系列串口扩展芯片简介      

      目前WK系列能实现SPI扩展UART的芯片包括 WK2124、WK2204、WK2168、WK2132。目前WK2124、WK2204、WK2168能实现SPI扩展4路UART,WK2132能实现扩展2路UART。目前这几款芯片使用的都是相同的linux驱动。

WK系列扩展的子通道的UART具备如下功能特点:

每个子通道UART的波特率、字长、校验格式可以独立设置,最高可以提供2Mbps的通信速率。

每个子通道具备收/发独立的256 级FIFO,FIFO的中断可按用户需求进行编程触发点且具备超时中断功能。

2、SPI拓展串口驱动简介

2.1  硬件连接示意图

           

 

  1. WK芯片作SPI从设备和CPU端的主SPI需要连接的信号有CS信号(此信号不能一直拉低,需要用主SPI的CS信号控制)、CLK信号、MOSI信号、MISO信号,具体连接方式如上图。
  2. IRQ信号为WK芯片的中断输出信号,需要连接到CPU具有外部中断功能的GPIO上。IRQ引脚外部需要加上拉电阻。
  3. RST作为复位引脚,在SPI拓展4串口的时候,可以不用连接到CPU.直接使用阻容复位电路。

2.2 linux   串口驱动基本框架简介

 

1、WK驱动工作在linux 内核层,向上提供4个串口设备节点供应用层用户调用。也就是说WK驱动注册成功以后,在/dev/ 目录下会生成 ttysWK0、ttysWK1、ttysWK2、ttysWK3  共4个串口设备节点,应用层就可以按照操作普通串口节点的方式操作。

2、WK驱动需要和WK芯片进行数据交互,数据交互是通过SPI总线进行的,所以WK驱动会调用SPI总线驱动接口进行数据收发。

3. WK SPI拓展UART驱动简介

3.1 开平台简介

3.1.1 硬件平台

本驱动在开发的时候使用了Firefly-RK3399和IMX8这款开发板。该开发板接口丰富,开发驱动很方便。

3.1.2 软件平台简介

该驱动是在Ubuntu16.04系统上开发。内核版本4.4.具体见下图:

3.2 驱动介绍

  

驱动源文件wk2xxx_spi.c

下面对驱动作一些基本介绍

3.2.1 ​串口驱动信息描述和数据结构

   3.2.1.1 串口驱动描述

鉴于芯片的相关特性和驱动编写的需要,定义了结构体 wk2xxx_port用于对WK的SPI转串口驱动进行描述。程序清单入下所示。

struct wk2xxx_port 
{
    const struct wk2xxx_devtype *devtype;
    struct uart_driver      uart;
    struct spi_device *spi_wk;
    struct workqueue_struct *workqueue;
    struct work_struct work;
    unsigned char           buf[256];
    struct kthread_worker   kworker;
    struct task_struct      *kworker_task;
    struct kthread_work     irq_work;
    int irq_gpio_num;                         /*中断IO的GPIO编号*/
    int rst_gpio_num;                         /*复位引脚的GPIO编号*/
    int irq_gpio;                            /*中断编号*/
    int minor;      /* minor number */
    int tx_empty; 
    struct wk2xxx_one       p[NR_PORTS];
};

3.2.1.2 串口端口描述

定义一个结构体wk2xxx_one来描述WK2xxx芯片的串口端口进行描述,实际上是对uart_port的进一步封装,增加了两个内核队列和芯片子串口一些寄存器。程序如下:

struct wk2xxx_one 
{

    struct uart_port port;//[NR_PORTS];
    struct kthread_work     start_tx_work;
    struct kthread_work     stop_rx_work;
    uint8_t line;
    uint8_t new_lcr_reg;
    uint8_t new_fwcr_reg;
    uint8_t new_scr_reg; 
    /*baud register*/
    uint8_t new_baud1_reg;
    uint8_t new_baud0_reg;
    uint8_t new_pres_reg;
};

3.2.2 串口驱动的底层基本操作

3.2.2.1 读全局寄存器描述

该函数调用SPI接口,实现读WK2XXX芯片的全局寄存器,全局寄存器通常包括GENA 、GRST、GIER、GIFR、GMUT、GPDIR、GPDAT等

static int wk2xxx_read_global_reg(struct spi_device *spi,uint8_t reg,uint8_t *dat)
{
    struct spi_message msg;
    uint8_t buf_wdat[2];
    uint8_t buf_rdat[2];
    int status;
    struct spi_transfer index_xfer = {
                .len            = 2,
                .speed_hz   = wk2xxx_spi_speed,
    };
    mutex_lock(&wk2xxxs_reg_lock);
    status =0;
    spi_message_init(&msg);
    buf_wdat[0] = 0x40|reg;
    buf_wdat[1] = 0x00;
    buf_rdat[0] = 0x00;
    buf_rdat[1] = 0x00;
    index_xfer.tx_buf = buf_wdat;
    index_xfer.rx_buf =(void *) buf_rdat;
    spi_message_add_tail(&index_xfer, &msg);
    status = spi_sync(spi, &msg);
    mutex_unlock(&wk2xxxs_reg_lock);
    if(status){
        return status;
    }
    *dat = buf_rdat[1];
    return 0;
}

3.2.2.2 写全局寄存器

该函数调用SPI接口,实现对WK2XXX芯片的全局寄存器写操作,全局寄存器通常包括GENA 、GRST、GIER、GIFR、GMUT、GPDIR、GPDAT等

/*
* This function write wk2xxx of Global register:
*/
static int wk2xxx_write_global_reg(struct spi_device *spi,uint8_t reg,uint8_t dat)
{
    struct spi_message msg;
    uint8_t buf_reg[2];
    int status;
    struct spi_transfer index_xfer = {
            .len            = 2,
            .speed_hz   = wk2xxx_spi_speed,
    };
    mutex_lock(&wk2xxxs_reg_lock);
    spi_message_init(&msg);
    /* register index */
    buf_reg[0] = 0x00|reg;
    buf_reg[1] = dat;
    index_xfer.tx_buf = buf_reg;
    spi_message_add_tail(&index_xfer, &msg);
    status = spi_sync(spi, &msg);
    mutex_unlock(&wk2xxxs_reg_lock);
    return status;
}

3.2.2.3 读子串口寄存器函数描述

该函数调用SPI接口实现读子串口的相关寄存器。Uint8_t port 这个参数表示对应的子串口编号。

/*
* This function read wk2xxx of slave register:
*/
static int wk2xxx_read_slave_reg(struct spi_device *spi,uint8_t port,uint8_t reg,uint8_t *dat)
{
    struct spi_message msg;
    uint8_t buf_wdat[2];
    uint8_t buf_rdat[2];
    int status;
    struct spi_transfer index_xfer = {
            .len            = 2,
            .speed_hz   = wk2xxx_spi_speed,
    };
    mutex_lock(&wk2xxxs_reg_lock);
    status =0;
    spi_message_init(&msg);
    buf_wdat[0] = 0x40|(((port-1)<<4)|reg);
    buf_wdat[1] = 0x00;
    buf_rdat[0] = 0x00;
    buf_rdat[1] = 0x00;
    index_xfer.tx_buf = buf_wdat;
    index_xfer.rx_buf =(void *) buf_rdat;
    spi_message_add_tail(&index_xfer, &msg);
    status = spi_sync(spi, &msg);
    mutex_unlock(&wk2xxxs_reg_lock);
    if(status){
        return status;
    }
    *dat = buf_rdat[1];
    return 0;

}

3.2.2.4 写子串口寄存器函数描述

  该函数调用SPI接口,实现写子串口的寄存器。

/*
* This function write wk2xxx of Slave register:
*/
static int wk2xxx_write_slave_reg(struct spi_device *spi,uint8_t port,uint8_t reg,uint8_t dat)
{
    struct spi_message msg;
    uint8_t buf_reg[2];
    int status;
    struct spi_transfer index_xfer = {
        .len            = 2,
        .speed_hz   = wk2xxx_spi_speed,
    };
    mutex_lock(&wk2xxxs_reg_lock);
    spi_message_init(&msg);
    /* register index */
    buf_reg[0] = ((port-1)<<4)|reg;
    buf_reg[1] = dat;
    index_xfer.tx_buf = buf_reg;
    spi_message_add_tail(&index_xfer, &msg);
    status = spi_sync(spi, &msg);
    mutex_unlock(&wk2xxxs_reg_lock);
    return status;
}

3.2.2.5 读fifo函数描述 

该函数通过调用SPI接口实现读子串口的FIFO(也就是子串口的接收缓存区)。具体函数如下:

static int wk2xxx_read_fifo(struct spi_device *spi,uint8_t port,uint8_t fifolen,uint8_t *dat)
{
    struct spi_message msg;
    int status,i;
    uint8_t recive_fifo_data[MAX_RFCOUNT_SIZE+1]={0};
    uint8_t transmit_fifo_data[MAX_RFCOUNT_SIZE+1]={0};
    struct spi_transfer index_xfer = {
            .len            = fifolen+1,
            .speed_hz   = wk2xxx_spi_speed,
    }; 
    if(!(fifolen>0)){
        printk(KERN_ERR "%s,fifolen error!!\n", __func__);
        return 1;
    }
    mutex_lock(&wk2xxxs_reg_lock);
    spi_message_init(&msg);
    /* register index */
    transmit_fifo_data[0] = ((port-1)<<4)|0xc0;
    index_xfer.tx_buf = transmit_fifo_data;
    index_xfer.rx_buf =(void *) recive_fifo_data;
    spi_message_add_tail(&index_xfer, &msg); 
    status = spi_sync(spi, &msg);
    for(i=0;i<fifolen;i++)
        *(dat+i)=recive_fifo_data[i+1];
    mutex_unlock(&wk2xxxs_reg_lock);
    return status;
        
}

3.2.2.6 写FIFO函数描述

该函数通过调用SPI接口实现写子串口FIFO(也就是子串口发送缓存区)。具体函数如下:

static int wk2xxx_write_fifo(struct spi_device *spi,uint8_t port,uint8_t fifolen,uint8_t *dat)
{
    struct spi_message msg;
    int status,i;
    uint8_t recive_fifo_data[MAX_RFCOUNT_SIZE+1]={0};
    uint8_t transmit_fifo_data[MAX_RFCOUNT_SIZE+1]={0};
    struct spi_transfer index_xfer = {
            .len            = fifolen+1,
            .speed_hz   = wk2xxx_spi_speed,
    }; 
    if(!(fifolen>0)){
        printk(KERN_ERR "%s,fifolen error,fifolen:%d!!\n", __func__,fifolen);
        return 1;
    }
    mutex_lock(&wk2xxxs_reg_lock);
    spi_message_init(&msg);
    /* register index */
    transmit_fifo_data[0] = ((port-1)<<4)|0x80;
    for(i=0;i<fifolen;i++){
        transmit_fifo_data[i+1]=*(dat+i);
    }
    index_xfer.tx_buf = transmit_fifo_data;
    index_xfer.rx_buf =(void *) recive_fifo_data;
    spi_message_add_tail(&index_xfer, &msg);
    status = spi_sync(spi, &msg);
    mutex_unlock(&wk2xxxs_reg_lock);
    return status;        
}

3.2.3 驱动架构与应用层(用户空间)之间的分析

本驱动遵循标准的tty驱动的架构。tty 架构如下图所示:

 

一般来说tty架构可以分成两层:一层是下层我们的串口驱动层,直接操作WK2XXX芯片,同时向上提供一组标准的接口,这组接口通过结构体struct uart_ops来实现,该结构体涵盖了驱动对串口的所有操作。还有一层是上层tty层,包括tty_coreline_discipline.他们各自实现实现一个ops结构,用户空间通过tty注册的字符设备节点来访问驱动。

Wk2xxx 驱动的struct uart_ops结构体如下:

static struct uart_ops wk2xxx_pops = {
    tx_empty:       wk2xxx_tx_empty,
    set_mctrl:      wk2xxx_set_mctrl,
    get_mctrl:      wk2xxx_get_mctrl,
    stop_tx:        wk2xxx_stop_tx,
    start_tx:       wk2xxx_start_tx,
    stop_rx:        wk2xxx_stop_rx,
    enable_ms:      wk2xxx_enable_ms,
    break_ctl:      wk2xxx_break_ctl,
    startup:        wk2xxx_startup,
    shutdown:       wk2xxx_shutdown,
    set_termios:    wk2xxx_termios,
    type:           wk2xxx_type,
    release_port:   wk2xxx_release_port,
    request_port:   wk2xxx_request_port,
    config_port:    wk2xxx_config_port,
    verify_port:    wk2xxx_verify_port,

};

3.2.3.1 驱动注册

在驱动编译完成以后。驱动加载成功以后,驱动会向系统注册4个串口设备节点,我们可以在/dev/ 目录下找到ttysWK0 、ttysWK1、ttysWK2、ttysWK3这4个节点。

用户空间可以通过这个4个节点访问4个不同的串口。通常用户空间通过

3.2.3.2   用户空间Open()\close()串口设备节点

用户空间通常通过open()\close()函数打开或者关闭设备节点。

1、如下用户空间打开串口设备节点:

 

注意:Dev为设备节点指针(设备节点的路径如下)

当用户空间打开串口的时候,驱动层会调用如下函数:

static int wk2xxx_startup(struct uart_port *port)

该函数主要是来初始化wk2xxx芯片当前子串口的寄存器和设置子串口的初始波特率(115200)。示意图入下:

​​​​​​​

 2. 用户空间关闭串口设备节点

       用户关闭设备节点如下:    close(fd);

       注意:fd 是open串口时获得的。

     当用户空间调用close()串口的时候。驱动主要是调用static void wk2xxx_shutdown(struct uart_port *port) 函数来实现关闭串口。

     该函数主要实现关闭子串口的时钟、中断等操作。

3.2.3.3 设置子串口波特率和数据格式

 

应用层设置波特率和数据格式有专

驱动层设置波特率是通过如下的函数来实现的:

static void wk2xxx_termiosstruct uart_port *portstruct ktermios *termios,struct ktermios *old)

3.2.3.4 通过串口读写数据

应用层通过write() /read()函数来实现子串口的收发。那么驱动层是怎么来实现的:

  1. 用户空间和驱动层之间的数据是怎么交互的。

​​​​​​​

 用户空间和驱动层之间在数据传递上并不是直接传递的。当write()写数据时,用户空间仅仅是把数据传递给tty缓冲区,然后驱动程序收到发送数据的指令,然后按照一定的流程去发送数据;当接收数据的时候,驱动层首先把接收的数据放入tty缓冲区,用户空间read()去读数据,那么就能从tty缓冲区读出子串口接收的数据。

2.驱动层接收和发送数据的实现

驱动层接收和发送数据都依赖于中断。具体的示意图如下:

 

发送数据:用户空间需要发送数据,首先调用write(),并把需要发送的数据传递到tty缓存区.驱动层调用wk2xxx_start_tx()告诉驱动有数据需要发送,WK2xxx芯片产生中断,中断函数通过wk2xxx_tx_chars()函数把tty缓存区的数据取出来,并把数据写入wk2xxx芯片的发送fifo,芯片再自动发送发送fifo中的数据。

接收数据:当WK2xxx芯片接收的数据都是暂时存在子串口的接收fifo,当接收fifo中数据个数到达设置的接收中断触点,芯片产生接收中断,中断函数通过wk2xxx_rx_chars()函数,从接收fifo中读出接收的数据,然后传递给tty缓存区。那么用户空间就可以通过read()函数读到接收的数据。

4.驱动的移植

驱动的移植一般过程就是修改内核端的DTS配置,然后编译驱动,加载驱动,最后就是测试。

4.1 ​​​​​​​配置DTS节点

在DTS文件当中添加SPI驱动节点描述。如下图所示:

 

本驱动使用的是SPI1,

  1. status:如果要启用SPI,那么设置为okay,如不启用,设置为disable
  2. wk2xxx_spi@00:由于硬件使用的是SPI1的cs0引脚,所以设置为00.如果使用cs1,则设置为01
  3. compatible:这里的属性必须与驱动中的结构体:of_device_id 中的成员 compatible 保持一致。这个是SPI驱动匹配的关键。
  4. reg:此处与wk2xxx_spi@00:保持一致。此处设置为:00
  5. spi-max-frequency:此处设置 spi 使用的最高频率。wk2xxx芯片spi最高支持10000000
  6. reset_gpio:该选项在SPI驱动当中不是必须的。该gpio和WK2xxx芯片的复位引脚相连,用于控制芯片的复位。根据实际使用的gpio去修改。
  7. irq_gpio:   该gpio和wk2xxx芯片的IRQ引脚相连,用于接收wk2xxx芯片传递来的中断信号。估计具体使用的GPIO去修改。
  8. SPI的工作模式设置,默认工作在0模式,所以在dts中没有单独设置。

4.2 ​​​​​​​驱动修改

驱动当中有些差异配置,是需要根据具体的硬件使用情况去修改的。

4.2.1 晶振频率值修改

如下中WK_CRASTAL_CLK是芯片外部的实际晶振值,目前我们在测试中使用的是24Mhz的晶振,所以晶振值是24000000.如果用的是12Mhz晶振就修改为12000000.

4.2.2 调试接口

开启如下的宏,可以在驱动运行的时候增加打印信息。方便调试

 4.2.3 功能接口

通常下面的这些功能,按照默认设置就可以,除非有相应需求才开启

 

 上面的宏定义是一些功能接口:

#define WK_FIFO_FUNCTION   //用读写fifo的方式读写uart数据,默认开启
//#define WK_FlowControl_FUNCTION  //硬件流控功能开启,默认不开启
//#define WK_WORK_KTHREAD
//#define WK_RS485_FUNCTION        //RS485自动收发功能开启,根据实际需求开启,
//#define WK_RSTGPIO_FUNCTION     //复位引脚开启。如果硬件设计了复位引脚控制,可以开启

4.2.4 芯片型号修改

WK2XXX系列芯片在主接口相同的情况下,驱动是可以兼容的,但是还有还是存在一些差异,比如扩展子串口的数量,我们可以通过修改芯片类型结构体去实现,入下图红色框中的

可以按照下面的方式去修改

s->devtype=&wk2124_devtype;表示芯片是wk2124.
s->devtype=&wk2168_devtype;表示芯片是wk2168.
s->devtype=&wk2204_devtype;表示芯片是wk2204.
s->devtype=&wk2132_devtype;表示芯片是wk2132.
s->devtype=&wk2212_devtype;表示芯片是wk2212.

 4.3 驱动的编译

驱动可以和内核编译到一起,也可以单独编译成模块加载。我们就按照编译成模块的方式分享一遍驱动的编译过程。

4.3.1编译前的准备工作

编译驱动以前需要搭建好交叉编译环境。其次就是要准备好编译工具(编译器),最后就是先要编译好开发平台内核。以上这些网上都有详细的资料,这里再在介绍。

其次准备驱动源文件和makefile文件。

驱动配套的Makefile文件如下,请参考:

ARCH:= arm64
MVTOOL_PREFIX = /home/xxw/firefly_pro/Firefly_Linux_SDK_v1.0/prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gun-
CROSS_COMPILE= $(MVTOOL_PREFIX)
KDIR := /home/xxw/firefly_pro/Firefly_Linux_SDK_v1.0/aio3399-kernel
TARGET              =wk2xxx_spi
EXEC = $(TARGET)
obj-m :=$(TARGET).o
PWD :=$(shell pwd)
all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
    rm -rf *.o *~core.depend.*.cmd *.ko *.mod.c .tmp_versions $(TARGET)

注意下面通常是需要修改的:编译器路径和内核文件路径

MVTOOL_PREFIX :指向编译器的路径

KDIR:内核文件路径

4.3.2 编译驱动

正常情况下,把驱动和Makefile文件放交叉编译环境中,执行make指令,就会成功编译出对应的驱动模块文件wk2xxx_spi.ko文件。

 但是编译过程当中通常都会遇见一些问题,这些问题主要是和平台差异相关的问题。下面就把我们遇见过的一些问题分享一下。

4.3.2.1 ​​​​​​​Kthread_work相关定义问题

 在头文件#include <linux/kthread.h> 下,定义了相关的函数和结构体。入下图

 

在实际调试中,我们发现不同平台下,对于这些函数定义存在差异。所以我们在头文件中定义了宏定义

#define WK_WORK_KTHREAD

通过条件编译的方式来切换。

如果在编译的时候,出现如下的一些编译错误,就可以通过宏定义

#define WK_WORK_KTHREAD来调整。

4.3.2.2 ​​​​​​​ Port.flags 赋值问题

 

struct uart_port

在函数static int wk2xxx_probe(struct spi_device *spi)中给struct  uart_port初始化的时候。可能会出现如下代码编译不过的情况。

如果出现port.flags这个标志位编译不过,那么可以如下操作:

1.替换。如下图

用红色框中的标志代替上面的。

2.用UPF_BOOT_AUTOCONF 替换 ASYNC_BOOT_AUTOCONF,如下所示

            s->p[i].port.flags    = UPF_BOOT_AUTOCONF;

    //s->p[i].port.flags    = ASYNC_BOOT_AUTOCONF;

3.方法1不行,那么只有参考平台串口驱动中该标志位的赋值。

说明:struct uart_port该结构体定义在include/linux/serial_core.h

 4.3.2.3  ​​​​​​MAX_RT_PRIO无定义问题

 

const struct sched_param sched_param = { .sched_priority = MAX_RT_PRIO / 2 };

MAX_RT_PRIO这个参数有可能定义的头文件找不到,导致编译不过,可以直接用100代替该参数。如下图所示:

 4.4 驱动调试

在驱动编译完成以后,会生成wk2xxx_spi.ko文件。

4.4.1 加载驱动

把wk2xxx_spi.ko 文件push到开发板。执行如下命令加载驱动模块

insmod wk2xx_spi.ko如下:

 

驱动加载成功,会在/dev/目录下出现对应的串口节点,如图:

ttysWK* 这些节点就可以当做标准串口设备节点编程使用。

4.4.2 测试驱动

我们下面介绍一些简单的测试方法.简单的测试数据发送和数据接收。

我们在linux开发板端采用命令的方式操作串口设备节点。然后用USB转串口工具,一端连接WK2XXX芯片的UART1,一端连接到PC端,PC端用串口助手接收和发送数据。硬件连接示意图如下

  1. 串口发送字符串

在开发板端使用命令echo “123456abcdefg”>/dev/ttysWK1

该命令的默认波特率是9600。演示结果如下。

2、串口接收字符和发送字符

首先是cat设备节点,如下图。Cat 设备节点入下图。

 

cat  设备节点有如下功能。首先是会打开对应的串口设备节点(波特率9600),准备接收字符数据。其次是把接收的字符数据然后通过串口设备节点发送出来,这一点是需要注意的。所以cat命令不光有数据的接收,也有数据的发送。

演示操作截图如下:

 

 如上图所示:该测试过程的数据流向与过程如下:PC上的USB串口通过TX发送出数据------>开发板上的WK2xxx接收到数据,并传给系统------》开发板系统把收到的数据,然后通过WK2xxx芯片发送出来-------》PC接收到返回的数据。示意图如下:

 

 

 

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值