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 硬件连接示意图
- WK芯片作SPI从设备和CPU端的主SPI需要连接的信号有CS信号(此信号不能一直拉低,需要用主SPI的CS信号控制)、CLK信号、MOSI信号、MISO信号,具体连接方式如上图。
- IRQ信号为WK芯片的中断输出信号,需要连接到CPU具有外部中断功能的GPIO上。IRQ引脚外部需要加上拉电阻。
-
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_core、line_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_termios( struct uart_port *port, struct ktermios *termios,struct ktermios *old)
3.2.3.4 通过串口读写数据
应用层通过write() /read()函数来实现子串口的收发。那么驱动层是怎么来实现的:
- 用户空间和驱动层之间的数据是怎么交互的。
用户空间和驱动层之间在数据传递上并不是直接传递的。当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,
- status:如果要启用SPI,那么设置为okay,如不启用,设置为disable
- wk2xxx_spi@00:由于硬件使用的是SPI1的cs0引脚,所以设置为00.如果使用cs1,则设置为01
- compatible:这里的属性必须与驱动中的结构体:of_device_id 中的成员 compatible 保持一致。这个是SPI驱动匹配的关键。
- reg:此处与wk2xxx_spi@00:保持一致。此处设置为:00
- spi-max-frequency:此处设置 spi 使用的最高频率。wk2xxx芯片spi最高支持10000000。
- reset_gpio:该选项在SPI驱动当中不是必须的。该gpio和WK2xxx芯片的复位引脚相连,用于控制芯片的复位。根据实际使用的gpio去修改。
- irq_gpio: 该gpio和wk2xxx芯片的IRQ引脚相连,用于接收wk2xxx芯片传递来的中断信号。估计具体使用的GPIO去修改。
- 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端用串口助手接收和发送数据。硬件连接示意图如下
- 串口发送字符串
在开发板端使用命令echo “123456abcdefg”>/dev/ttysWK1
该命令的默认波特率是9600。演示结果如下。
2、串口接收字符和发送字符
首先是cat设备节点,如下图。Cat 设备节点入下图。
cat 设备节点有如下功能。首先是会打开对应的串口设备节点(波特率9600),准备接收字符数据。其次是把接收的字符数据然后通过串口设备节点发送出来,这一点是需要注意的。所以cat命令不光有数据的接收,也有数据的发送。
演示操作截图如下:
如上图所示:该测试过程的数据流向与过程如下:PC上的USB串口通过TX发送出数据------>开发板上的WK2xxx接收到数据,并传给系统------》开发板系统把收到的数据,然后通过WK2xxx芯片发送出来-------》PC接收到返回的数据。示意图如下: