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数据手册 <--- 点击下载