msm8909_wk2124_SPI转串口485

发布时间 2023-08-05 12:26:57作者: 可乐klelee

项目使用的是高通的msm8909平台,采用广和通SC806开发板,开发环境采用Ubuntu18.04。SC806默认有两路串口,对项目来说不够使用,需要进行转接,所以采用了wk2124将一路SPI转换为4路串口,然后再加485芯片,转换为4路485接口。接下来详细看看整个配置过程。

概述

说明:本文档会将为开提供的官方文档的信息摘录过来,并在其基础上对驱动文件进行详细说明!

WK2124芯片能够实现将1路SPI转换成4路串口,WK 系列扩展的子通道的 UART 具备如下功能特点:
每个子通道 UART 的波特率、字长、校验格式可以独立设置,最高可以提供2Mbps 的通信速率。
每个子通道具备收/发独立的 256 级 FIFO,FIFO 的中断可按用户需求进行编程触发点且具备超时中断功能。

简单的示意图如下:

image-20230629152006553

  1. WK 芯片作 SPI 从设备和 CPU 端的主 SPI 需要连接的信号有 CS 信号(此信号不能一直拉低,需要用主 SPI 的 CS 信号控制)、CLK 信号、MOSI 信号、MISO 信号,具体连接方式如上图。

  2. IRQ 信号为 WK 芯片的中断输出信号,需要连接到 CPU 具有外部中断功能的GPIO 上。IRQ 引脚外部需要加上拉电阻。

  3. RST 作为复位引脚,在 SPI 拓展 4 串口的时候,可以不用连接到 CPU.直接使用阻容复位电路。

WK2124驱动工作框架

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

    image-20230629152707566

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

    image-20230805091918020

WK2124驱动简介

为开厂商提供了芯片的驱动程序,我们只需要配置设备树,把驱动放到相应的目录下即可。为开的驱动程序我已经放到了附件中,可以进行下载使用,也可以联系我直接发你。

关于wk2xxx_spi.c的具体分析,我将在《wk2124驱动详解》一文中深入说明,这里就不过多说明了。

调试记录

把一个驱动添加到自己的内核中大概要做的事情包括:

  1. 看原理图了解引脚配置
  2. 根据原理图进行设备树配置
  3. 修改驱动程序,解析设备树
  4. 修改驱动程序,添加自定义功能

大概如此吧,接下来详细看看。

原理图

关于WK2124芯片的接口原理图,点击下载WK2124_SPI接口原理图,其实项目开发中可以用不到这个,我只是找到了,就做个分享。

然后来看项目中的原理图,了解一下相关GPIO的配置吧。

image-20230805101437298

这部分展示了WK2124芯片的一个连接情况。从上面的原理图中,我做出如下分析:

  1. wk2124连接的spi总线是SPI3(并且是使用四路GPIO复用的,分别是GPIO 0,1,2,3)
  2. wk2124的reset脚接了gpio89
  3. wk2124的irq脚接了gpio92
  4. gpio97和gpio69分别做了485_1和485_2的收发控制引脚

目前来说,能读到的信息就是这些。

设备树

根据上面看原理图,得到了spi总线、reset和irq引脚配置、485控制引脚的gpio。那么接下来我们就开始配置设备树吧。

SPI3总线配置

我们需要把wk2124的驱动挂载在spi3总线上就需要确保spi3总线是存在的,可是事实上去设备树文件中查看的时候却只有spi0总线是存在的。对于这种情况,我们应该及时想到这一路spi是可复用的gpio复用出来的。于是就来看看gpio复用表。

image-20230805102852633

SPI3是通过4路GPIO复用得到的pin-func是1. 然后根据这些信息,我们去配置一个SPI3总线,具体配置方法可以参照SPI0

设备树配置路径: kernel/arch/arm/boot/dts/qcom/sc806-evk/msm8909.dtsi

首先为spi_3起个别名

image-20230805110841079

这一步不是必须的,这里只是为了和SPI0保持一致,实际上后期对spi3的引用依旧使用的是spi_3。然后配置spi_3节点:

/* like: spi3 dts Configure */
	spi_3: spi@78b7000 { /* BLSP1 QUP2 */
                compatible = "qcom,spi-qup-v2";
                #address-cells = <1>;
                #size-cells = <0>;
                reg-names = "spi_physical", "spi_bam_physical";
                reg = <0x78b7000 0x600>,
                      <0x7884000 0x23000>;
                interrupt-names = "spi_irq", "spi_bam_irq";
                interrupts = <0 97 0>, <0 238 0>;
                spi-max-frequency = <19200000>;
                pinctrl-names = "spi_default", "spi_sleep";
                pinctrl-0 = <&spi3_default &spi3_cs0_active>;
                pinctrl-1 = <&spi3_sleep &spi3_cs0_sleep>;
                clocks = <&clock_gcc clk_gcc_blsp1_ahb_clk>,
                         <&clock_gcc clk_gcc_blsp1_qup3_spi_apps_clk>;
                clock-names = "iface_clk", "core_clk";
                qcom,infinite-mode = <0>;
                qcom,use-bam;
                qcom,use-pinctrl;
                qcom,ver-reg-exists;
                qcom,bam-consumer-pipe-index = <8>;
                qcom,bam-producer-pipe-index = <9>;
                qcom,master-id = <86>;
	};

以上是我参考spi0,为spi3做的配置。其中spi_3: spi@78b7000 中的地址,需要联系主控厂商获取:

blsp_mapping

如图所示,需要配置的有Physical address, IRQ以及Consumer-producer pipes。不管是哪条spi总线,都由同一个驱动加载,其compatible没有区分,不同spi差异在于地址,中断号,gpio配置上。

通过上面的配置,编译boot并重新刷机之后,就dev下就可以找到spi3设备了。

挂载wk2124到SPI3

上面的SPI3 配置完成之后,接下来把wk2xxx这个设备挂载在spi3总线上:

设备树配置路径: kernel/arch/arm/boot/dts/qcom/sc806-evk/msm8909.dtsi

/* like: wk2xxx dts Configure */
&spi_3 {
	status = "okay";
	max-freq = <48000000>;
	wk2xxx_spi@00 {
		status = "okay";
		compatible = "qcom,wk2xxx_spi";
		reg = <0>;
		spi-max-frequency = <19200000>;
		type = <0>;
		reset_gpio = <&msm_gpio 89 1>;
		irq_gpio = <&msm_gpio 92 0>;
		cs-gpios = <&msm_gpio 2 0>;
		rs485ctl1-gpio = <&msm_gpio 97 1>;
		rs485ctl2-gpio = <&msm_gpio 69 1>;
		rs485ctl3-gpio = <&msm_gpio 88 1>;
		rs485ctl4-gpio = <&msm_gpio 31 1>;
	};
};
  1. status:如果要启用 SPI,那么设置为 okay,如不启用,设置为 disable
  2. wk2xxx_spi@00:由于硬件使用的是 SPI3 的 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 中没有单独设置。
  9. 485控制gpio。用于控制485的收发,需要在驱动中解析并使用。

驱动程序移植

为开提供的驱动程序中已经对reset脚、irq脚以及cs脚进行了解析是使用。所以对于这三个gpio我们不需要过多的关心。我们需要做的就是在wk2xxx的驱动中解析我们加入的4路485控制gpio。

probe的修改

我们需要知道的是,驱动程序注册完成以后,会回到当前驱动中执行驱动的probe函数,可以这么说,probe就相当于是驱动程序的main函数。

所以根据我们上面的思路,我们需要在probe中调用485控制引脚的解析函数。

修改路径:kernel/drivers/spi/wk2xxx_spi.c (由于wk2124于spi总线相连,属于一个spi设备,所以将其放在spi目录下)

static int wk2xxx_probe(struct spi_device *spi)
{
    //const struct sched_param sched_param = { .sched_priority = MAX_RT_PRIO / 2 };
    const struct sched_param sched_param = { .sched_priority = 100 / 2 };
    uint8_t i;
    int ret, irq;
    uint8_t dat[1];
	static struct wk2xxx_port *s;
    #ifdef _DEBUG_WK_FUNCTION
        printk(KERN_ALERT "%s!!--in--\n", __func__);
    #endif
...
    //Obtain the GPIO number of CS signal
    ret=wk2xxx_spi_csgpio_parse_dt(&spi->dev,&cs_gpio_num);
    if(ret!=0){
        printk(KERN_ALERT "wk2xxx_probe(cs_gpio)  cs_gpio_num = 0x%d\n",cs_gpio_num);
        ret=cs_gpio_num;
        goto out_gpio;

    }

/* like: rs485ctl parse run in probe */
    //Obtain the GPIO number of rs485ctl1 signal
    ret=wk2xxx_spi_rs485ctl1_parse_dt(&spi->dev,&rs485ctl1_gpio_num);
    if(ret!=0){
        printk(KERN_ALERT "wk2xxx_probe(rs485ctl1_gpio)  rs485ctl1_gpio_num = 0x%d\n",rs485ctl1_gpio_num);
        ret=rs485ctl1_gpio_num;
        goto out_gpio;

    }
...

   /* Setup interrupt */
	ret = devm_request_irq(&spi->dev, irq, wk2xxx_irq,IRQF_TRIGGER_FALLING, dev_name(&spi->dev), s);
  
	if (!ret){
        printk(KERN_ALERT "devm_request_irq success. ret=%d.\n",ret);
        return 0;
    }

out_port:
	for (i=0; i<NR_PORTS; i++) {
        printk(KERN_ALERT "uart_remove_one_port:%ld. status= 0x%d\n",s->p[i].port.iobase,ret);
		uart_remove_one_port(&wk2xxx_uart_driver, &s->p[i].port);
	}
out_clk:
    kthread_stop(s->kworker_task); 
out_gpio:
    if(rs485ctl1_gpio_num>0){
        printk(KERN_ALERT "gpio_free(rs485ctl1_gpio_num)= 0x%d,ret=0x%d\n",rs485ctl1_gpio_num,ret);
        gpio_free(rs485ctl1_gpio_num);  
        rs485ctl1_gpio_num=0;
    }
	return ret;
}

probe函数很长,我做了简单的删除,在probe函数中我们首先添加几个gpio的解析函数的调用:

    //Obtain the GPIO number of rs485ctl1 signal
    ret=wk2xxx_spi_rs485ctl1_parse_dt(&spi->dev,&rs485ctl1_gpio_num);
    if(ret!=0){
        printk(KERN_ALERT "wk2xxx_probe(rs485ctl1_gpio)  rs485ctl1_gpio_num = 0x%d\n",rs485ctl1_gpio_num);
        ret=rs485ctl1_gpio_num;
        goto out_gpio;

    }

然后添加其解析失败处理逻辑:

    if(rs485ctl1_gpio_num>0){
        printk(KERN_ALERT "gpio_free(rs485ctl1_gpio_num)= 0x%d,ret=0x%d\n",rs485ctl1_gpio_num,ret);
        gpio_free(rs485ctl1_gpio_num);  
        rs485ctl1_gpio_num=0;
    }

我们紧接着来看wk2xxx_spi_rs485ctl1_parse_dt函数.

解析设备树

wk2xxx_spi_rs485ctl1_parse_dt 函数参考了cs脚gpio解析函数书写的,它接收两个参数:devrs485ctl1_gpio_num,事实上传入的rs485ctl1_gpio_num 是一个全局变量:

/* like: rs485ctl gpio number */
int rs485ctl1_gpio_num;
int rs485ctl2_gpio_num;
int rs485ctl3_gpio_num;
int rs485ctl4_gpio_num;

完全可以在函数内部直接赋值,不知道为开为什么要这样传址处理。不过无所谓了。我们继续来看这个解析函数:

其实这个函数不是很满足内核编码规范的,这个函数中实现了:解析设备树,申请gpio使用权,初始化gpio状态,三个功能。这在内核的编码规范中是不允许的,所以希望参考本文章的朋友能将这些功能分开写。

通过这个函数我们可以得到一个gpio号,也就是那个全局变量,之后的操作中,我们控制485的收发,就可以直接操作这个gpio了。

/* like: wk2xxx_spi_rs485ctl1_parse_dt */
static int wk2xxx_spi_rs485ctl1_parse_dt(struct device *dev,int *rs485ctl1_gpio)
{

	int rs485ctl1_flags; 
	#ifdef _DEBUG_WK_FUNCTION
        printk(KERN_ALERT "%s!!--in--\n", __func__);
	#endif
	*rs485ctl1_gpio = of_get_named_gpio_flags(dev->of_node, "rs485ctl1-gpio", 0,(enum of_gpio_flags *)&rs485ctl1_flags);
    if (!gpio_is_valid(*rs485ctl1_gpio)){
		printk(KERN_ERR"invalid wk2xxx_rs485ctl2_gpio: %d\n", *rs485ctl1_gpio);
		return -1;
    }
   
    if(	*rs485ctl1_gpio){
		if (gpio_request(*rs485ctl1_gpio , "rs485ctl1-gpio")){
            printk(KERN_ERR"gpio_request failed!! rs485ctl1_gpio : %d!\n",*rs485ctl1_gpio);
		    gpio_free(*rs485ctl1_gpio);
		    return  -100;
        }
    }
	printk(KERN_ERR"wk2xxx_rs485ctl1_gpio: %d", *rs485ctl1_gpio);
    gpio_direction_output(*rs485ctl1_gpio,1);// output high
	#ifdef _DEBUG_WK_FUNCTION
        printk(KERN_ALERT "%s!!--exit--\n", __func__);
	#endif
	return 0;
}

这里只写了ctl485_1的配置,其他的类推。

添加自定义功能

放在一起说的话,其实上面解析485控制引脚就是在配置自定义功能了。接下来我们真正去控制这个485串口的收发。对于485串口的原理,这里不过多的赘述了,后面有机会在单独写文章说说。简单来说,485的收信和发信都在一个控制引脚的控制下进行,当这个引脚拉高,此时485就进入发送态,引脚拉低,485进入接受态。所以对于我们这个需求来说,无非就是在485要发送消息之前将对应的控制引脚拉高,发完之后马上拉低。

对于上面这个需求,重点就在于开始发送发送完成的两个节点。

这里简单提一下上层调用串口发送数据的一个过程:

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

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

流程如下:

image-20230805114932430

那我们的目标就很明确了,我们只需要在wk2xxx_tx_chars()函数中添加控制语句,即可控制485的收发了。我们来看这个函数:

先了解一下这个还是的被调过程,这个函数是在中断处理函数wk2xxx_port_irq()中被调用的,也就是出发了发送中断,到这个函数里面就是为了发送数据的。程序会判断,串口的循环队列里面是否有数据,如果有数据就发送,最后判断循环队列是否为空,如果为空就发送完成,调用stop停止发送,退出中断处理。对于我的需求,下面是我的程序:

static void wk2xxx_tx_chars(struct uart_port *port)
{   
    struct wk2xxx_port *s = dev_get_drvdata(port->dev);
    struct wk2xxx_one *one = to_wk2xxx_one(port, port);
    uint8_t fsr,tfcnt,dat[1],txbuf[256]={0};
    int count,tx_count,i;
	int len_tfcnt,len_limit,len_p=0;
	len_limit=SPI_LEN_LIMIT;

    if (one->port.x_char) {   
        wk2xxx_write_slave_reg(s->spi_wk,one->port.iobase,WK2XXX_FDAT_REG,one->port.x_char);
        one->port.icount.tx++;
        one->port.x_char = 0;
        goto out;
    }

    if(uart_circ_empty(&one->port.state->xmit) || uart_tx_stopped(&one->port)){
        goto out;
    }

    wk2xxx_read_slave_reg(s->spi_wk,one->port.iobase,WK2XXX_FSR_REG,&fsr);
    wk2xxx_read_slave_reg(s->spi_wk,one->port.iobase,WK2XXX_TFCNT_REG,&tfcnt); 
    if(tfcnt==0){
	    tx_count=(fsr & WK2XXX_FSR_TFULL_BIT)?0:256;
		#endif
    }else{
		tx_count=256-tfcnt;
    } 
    if(tx_count>200){
        tx_count=200;
    } 
	count = tx_count;
	i=0;
	while(count){
  		if(uart_circ_empty(&one->port.state->xmit))
     		break;
	   	txbuf[i]=one->port.state->xmit.buf[one->port.state->xmit.tail];
	   	one->port.state->xmit.tail = (one->port.state->xmit.tail + 1) & (UART_XMIT_SIZE - 1);
	   	one->port.icount.tx++;
	   	i++;
		count=count-1;
        #ifdef _DEBUG_WK_TX
            printk(KERN_ALERT "tx_chars:0x%x--\n",txbuf[i-1]);
        #endif
    };

    #ifdef WK_FIFO_FUNCTION 
	len_tfcnt=i;    
	while(len_tfcnt){
    // 如果串口循环队列非空,就发送数据,这个时候拉高相应的控制引脚。
		if(len_tfcnt>len_limit){
            /* like: start to transfer data, the gpio of rs485ctl turn to high */
            wk2xxx_spi_rs485ioctl_high(one->port.iobase);
            wk2xxx_write_fifo(s->spi_wk,one->port.iobase,len_limit,txbuf+len_p);    
			len_p=len_p+len_limit;
			len_tfcnt=len_tfcnt-len_limit;
        }else{
            /* like: start to transfer data, the gpio of rs485ctl turn to high */
            wk2xxx_spi_rs485ioctl_high(one->port.iobase);
            wk2xxx_write_fifo(s->spi_wk,one->port.iobase,len_tfcnt,txbuf+len_p);
			len_p=len_p+len_tfcnt;
			len_tfcnt=0;
		}
	}
    #else
	    for(count=0;count<i;count++){
            wk2xxx_write_slave_reg(s->spi_wk,one->port.iobase,WK2XXX_FDAT,txbuf[count]);	
        }
    #endif
out:
    // 这个位置相对于原版程序改动比较大,原版程序这里需要读两次寄存器,浪费时间会导致接收数据丢失。我的程序中将两个寄存器值合并判断,降低时间要求。
    while(1) {
        wk2xxx_read_slave_reg(s->spi_wk,one->port.iobase,WK2XXX_FSR_REG,dat);
        printk("FSR: %08X\n", dat[0]);
        if ((dat[0]&0x05) == 0) { 
            wk2xxx_spi_rs485ioctl_low(one->port.iobase);
            break;
        }
    }

    if (uart_circ_chars_pending(&one->port.state->xmit) < WAKEUP_CHARS){
        uart_write_wakeup(&one->port); 
    }
    if (uart_circ_empty(&one->port.state->xmit)){
        wk2xxx_stop_tx(&one->port);
    }
}

在以上的修改代码中,涉及到两个gpio控制函数,如下:

这两个函数逻辑很简单,就不赘述了。

static void wk2xxx_spi_rs485ioctl_high(uint8_t port) {
    switch (port){
        case 1:
            gpio_set_value(rs485ctl3_gpio_num, RS485CTL_GPIO_HIGH);
            break;
        case 2:
            gpio_set_value(rs485ctl4_gpio_num, RS485CTL_GPIO_HIGH);
            break;
        case 3:
            gpio_set_value(rs485ctl1_gpio_num, RS485CTL_GPIO_HIGH);
            break;
        case 4:
            gpio_set_value(rs485ctl2_gpio_num, RS485CTL_GPIO_HIGH);
            break;
        default:
            break;
    }

}

static void wk2xxx_spi_rs485ioctl_low(uint8_t port) {
    printk("like: set ttysWK port %d to receive mode!", port);
    switch (port){
        case 1:
            gpio_set_value(rs485ctl3_gpio_num, RS485CTL_GPIO_LOW);
            break;
        case 2:
            gpio_set_value(rs485ctl4_gpio_num, RS485CTL_GPIO_LOW);
            break;
        case 3:
            gpio_set_value(rs485ctl1_gpio_num, RS485CTL_GPIO_LOW);
            break;
        case 4:
            gpio_set_value(rs485ctl2_gpio_num, RS485CTL_GPIO_LOW);
            break;
        default:
            break;
    }

}

说明

本文章系项目总结时编写,如果疑问可以联系博主解答。

附件

原版wk2xxx_spi.c <--- 点击下载

原版wk2xxx.h <--- 点击下载

修改wk2xxx_spi_klelee.c <--- 点击下载

wk2124数据手册 <--- 点击下载