msm8909_wk2124_SPI转串口485
项目使用的是高通的msm8909平台,采用广和通SC806开发板,开发环境采用Ubuntu18.04。SC806默认有两路串口,对项目来说不够使用,需要进行转接,所以采用了wk2124将一路SPI转换为4路串口,然后再加485芯片,转换为4路485接口。接下来详细看看整个配置过程。
概述
说明:本文档会将为开提供的官方文档的信息摘录过来,并在其基础上对驱动文件进行详细说明!
WK2124芯片能够实现将1路SPI转换成4路串口,WK 系列扩展的子通道的 UART 具备如下功能特点:
每个子通道 UART 的波特率、字长、校验格式可以独立设置,最高可以提供2Mbps 的通信速率。
每个子通道具备收/发独立的 256 级 FIFO,FIFO 的中断可按用户需求进行编程触发点且具备超时中断功能。
简单的示意图如下:
-
WK 芯片作 SPI 从设备和 CPU 端的主 SPI 需要连接的信号有 CS 信号(此信号不能一直拉低,需要用主 SPI 的 CS 信号控制)、CLK 信号、MOSI 信号、MISO 信号,具体连接方式如上图。
-
IRQ 信号为 WK 芯片的中断输出信号,需要连接到 CPU 具有外部中断功能的GPIO 上。IRQ 引脚外部需要加上拉电阻。
-
RST 作为复位引脚,在 SPI 拓展 4 串口的时候,可以不用连接到 CPU.直接使用阻容复位电路。
WK2124驱动工作框架
-
WK 驱动工作在 linux 内核层,向上提供 4 个串口设备节点供应用层用户调用。也就是说 WK 驱动注册成功以后,在/dev/ 目录下会生成 ttysWK0、ttysWK1、ttysWK2、ttysWK3 共 4 个串口设备节点,应用层就可以按照操作普通串口节点的方式操作。
-
WK 驱动需要和 WK 芯片进行数据交互,数据交互是通过 SPI 总线进行的,所以 WK 驱动会调用 SPI 总线驱动接口进行数据收发。
WK2124驱动简介
为开厂商提供了芯片的驱动程序,我们只需要配置设备树,把驱动放到相应的目录下即可。为开的驱动程序我已经放到了附件中,可以进行下载使用,也可以联系我直接发你。
关于wk2xxx_spi.c的具体分析,我将在《wk2124驱动详解》一文中深入说明,这里就不过多说明了。
调试记录
把一个驱动添加到自己的内核中大概要做的事情包括:
- 看原理图了解引脚配置
- 根据原理图进行设备树配置
- 修改驱动程序,解析设备树
- 修改驱动程序,添加自定义功能
大概如此吧,接下来详细看看。
原理图
关于WK2124芯片的接口原理图,点击下载WK2124_SPI接口原理图,其实项目开发中可以用不到这个,我只是找到了,就做个分享。
然后来看项目中的原理图,了解一下相关GPIO的配置吧。
这部分展示了WK2124芯片的一个连接情况。从上面的原理图中,我做出如下分析:
- wk2124连接的spi总线是SPI3(并且是使用四路GPIO复用的,分别是GPIO 0,1,2,3)
- wk2124的reset脚接了gpio89
- wk2124的irq脚接了gpio92
- gpio97和gpio69分别做了485_1和485_2的收发控制引脚
目前来说,能读到的信息就是这些。
设备树
根据上面看原理图,得到了spi总线、reset和irq引脚配置、485控制引脚的gpio。那么接下来我们就开始配置设备树吧。
SPI3总线配置
我们需要把wk2124的驱动挂载在spi3总线上就需要确保spi3总线是存在的,可是事实上去设备树文件中查看的时候却只有spi0总线是存在的。对于这种情况,我们应该及时想到这一路spi是可复用的gpio复用出来的。于是就来看看gpio复用表。
SPI3是通过4路GPIO复用得到的pin-func是1. 然后根据这些信息,我们去配置一个SPI3总线,具体配置方法可以参照SPI0
设备树配置路径: kernel/arch/arm/boot/dts/qcom/sc806-evk/msm8909.dtsi
首先为spi_3
起个别名
这一步不是必须的,这里只是为了和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
中的地址,需要联系主控厂商获取:
如图所示,需要配置的有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>; }; };
- status:如果要启用 SPI,那么设置为 okay,如不启用,设置为 disable
- wk2xxx_spi@00:由于硬件使用的是 SPI3 的 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 中没有单独设置。
- 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解析函数书写的,它接收两个参数:dev
和rs485ctl1_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()函数读到接收的数据。
流程如下:
那我们的目标就很明确了,我们只需要在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数据手册 <--- 点击下载
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库