QCA9531开机过程中受到串口RX数据的影响导致启动过程死机【已解决】

当wifi板配合新飞控板的时候,由于飞控板的TX在不停的向我的wifi板发送数据,导致我wifi板在上电启动过程中受到太多数据导致串口的环形缓冲区存放数据太多,导致数据溢出,进而导致系统死机。如下图:

排查思路:

1.确认卡死的地方是处于哪个启动阶段

由于开发板启动需要经历三个阶段,uboot->kernel->rootfs,那么要先确定是在哪个阶段导致的卡死,才能对症下药,从上图死机日志可以看出,从freeing unused kernel memory : 172k freed,和init started :busybox v1.01 muti-cal binary这两句话中间开始收到了乱码数据。也就是要先确认这个时间节点的控制权是属于内核还是rootfs控制的。

由经验可知,busyxox multi-call binary是多个命令的合集,我们在make busyboxconfig里面选中的所有的命令,最终在make编译完之后都会被链接到这个二进制文件,它相当于一个仓库,里面存放了我们的所有命令。在bootargs里我传入的init 进程为/sbin/init,所以在系统启动时候执行的第一个进程应该是init,但是init也是被链接到busybox这个二进制文件,所以init 进程是通过 BusyBox 来执行的,这就解释了为什么上图中会打印init started:BusyBox v1.01 multi-call binary这句话、从日志可以看出:串口乱码数据到来的时机是在系统的init进程运行之前,由此可以判断收到乱码数据时候,他的控制权还在内核,我在内核源码里面搜了“freeing unused kernel memory :”这一串字符,然后大概看了一下内核启动逻辑,发现这句话执行的时候已经是内核启动即将结束,已经在收尾的阶段去释放内核未使用的空间了。但即使这样,我们还是可以得到结论:在收到串口垃圾数据的那一刻,控制权还在内核,还没有交给用户空间。所以如果在linux系统的用户空间对串口进行操作,是没办法解决这个问题的,我们必须修改内核驱动代码才能屏蔽掉这些垃圾数据

2.禁用内核启动过程中串口的接收功能

由上面第一步分析,我们知道要是要修改串口驱动代码,把他的rx功能从默认打开的状态改为关闭,才能禁止收到那些垃圾数据。

我在内核启动打印日志里看到了我的串口驱动器的类型,属于8250,型号的串口设备是16550A,并且通过mmap映射到内存中的0xb8020000地址处,中断号为19

现在我们有两种方式来修改串口的配置:

第一种:直接用8250串口驱动代码里为我们封装好的那些数据结构,也就是直接调用内核驱动源码里面已经写好的那些接口。这样的优点是不容易出错,但缺点是需要阅读串口驱动代码的逻辑,包括tty子系统框架,以及它对串口驱动的管理以及他们之间的关系,这样才知道从哪里入手修改串口设置

第二种:我们已经知道了ttyS0(也就是我的串口)被映射到了0xb8020000这个内存地址,它直接对应到串口硬件的寄存器的基地址。我们在用户空间对该地址+xx偏移地址进行读写操作,实际上就相当于直接操作串口硬件的寄存器,可以看着qca9531这款芯片的datasheet上的说明来配置串口。通过直接参照芯片手册里串口寄存器的配置,来对该映射地址进行操作,就像玩stm32单片机一样直接修改寄存器的值,简单粗暴,无需考虑那么多驱动的抽象层。因为有mmap映射,所以直接在应用层就配置寄存器

从难易程度来比较,第二种方式相对简单。目前我实现了第一种方式,也就是修改内核驱动代码,后面有时间再试一下第二种

3,内核源码之8250串口驱动代码分析

由于内核源码过于复杂,在这里无法详细阐述串口驱动实现的详细逻辑,只能简单做个概括。

基于2.6.31版本内核源码,8250驱动在/kernel.2.6.31/drivers/serial目录下,该驱动是基于platfrom框架写的,这个驱动主要对strct uart_ops这个结构体里的各个函数指针成员进行了详细的实现。uart_ops 是一个接口抽象层,定义了操作 UART 设备的所有标准方法。任何特定的 UART 驱动(比如 8250 驱动)都可以根据自己的需求来实现这些方法。它与硬件无关,更多是关于如何对 UART 设备进行操作的一系列方法定义

由上图可以看到uart_ops已经定义了一个名为stop_rx的函数指针,他指向serial8250_stop_rx,理论上,内核已经帮我写好stoprx的函数,我们只需要直接调用这个函数就行

该函数在驱动中的原具体实现如下:

static void serial8250_stop_rx(struct uart_port *port)
{
	struct uart_8250_port *up = (struct uart_8250_port *)port;

	up->ier &= ~UART_IER_RLSI;
	up->port.read_status_mask &= ~UART_LSR_DR;
	serial_out(up, UART_IER, up->ier);
}

该函数主要是为了停止 8250 UART 控制器的接收功能。通过清除RX线路状态中断和RX数据就绪”标志位",该函数禁用了接收数据的中断触发,从而不再处理接收到的数据

把这个函数在serial8250_startup函数的末尾处调用,这个函数将在每次应用层open打开串口的时候都会被调用一次,如果吧serial8250_stop_rx函数放在这里面调用,就可以实现在应用层去调用它,实现rx禁止的功能。但是通过我的实验,再开机的过程中仍然会收到垃圾数据,于是我对该数进行了改进,下面是改进后的代码

static void serial8250_stop_rx(struct uart_port *port)
{
    struct uart_8250_port *up = (struct uart_8250_port *)port;
    unsigned long flags;
    int timeout = 10000; // 超时计数器
    printk(KERN_INFO "serial8250_stop_rx function\n");

    spin_lock_irqsave(&up->port.lock, flags);
    
    // 禁用接收中断
    up->ier &= ~UART_IER_RDI;  // 禁用接收数据中断
    up->ier &= ~UART_IER_RLSI; // 禁用接收线路状态中断
    up->port.read_status_mask &= ~UART_LSR_DR;
    serial_out(up, UART_IER, up->ier);

    spin_unlock_irqrestore(&up->port.lock, flags);
    
    // 清空接收缓冲区,增加超时防止陷入无限循环
    while ((serial_in(up, UART_LSR) & UART_LSR_DR) && timeout--) {
        (void)serial_in(up, UART_RX);
    }
    
    if (timeout <= 0) {
        printk(KERN_WARNING "serial8250_stop_rx: RX buffer clear timed out\n");
    }

    printk(KERN_INFO "IER after stop RX: 0x%x\n", serial_in(up, UART_IER));
}

改进前后区别:1.相比原始版本只禁用接收线路状态的中断,改进后的版本加上了禁用接收数据中断 UART_IER_RDI,相当于完全禁用了数据接收相关的所有中断。

2.加上了自旋锁机制,保证了对中断使能寄存器和缓冲区的修改在锁的保护下完成

3.在禁用中断后,循环清空了 UART 的接收缓冲区,防止缓冲区中遗留数据

4.为了防止在清空缓冲区时陷入无限循环,增加了一个超时计数器 (timeout)。如果在预定的时间内无法清空缓冲区,将发出一个警告

5.增加了printk打印,可以在内核日志中输出调试信息(比如 IER 的值),以此来判断rx到底是否被禁用

同样把他放到serial8250_startup函数的末尾处调用它,然后重新编译内核,烧到开发板,上电,观察启动日志:

上面可以看到这里UART_IER寄存器的值经过我们的设置之后变成了0,表示所有的接收中断都被禁止了。而且在橙色区域内的系统启动过程中也没有出现以前的收到rx影响导致系统启动死机的问题,也没有提示串口缓冲区overruning的问题。为了保险起见,我在Linux系统的rcs启动脚本里又加上了一些处理逻辑,如下图红框中所示:其中closeuart可执行文件是我写在应用层写的一个程序用来关闭串口rx功能的,而exec < /dev/null这一句目的是重定向标准输入到 /dev/null,确保不会从用户或其他输入设备接收到任何数据这样相当于有了双重保险。

好了,到现在,通过修改内核串口驱动代码和修改rcS启动脚本,已经可以避免了一开始的rx数据导致启动失败无法正常进入到系统的问题,测试了一百多次也都没有复现。那么接下来要做的是在进入到系统后重新打开串口rx功能,因为我的wifiapp要用到串口和飞控进行双向通信,所以在开机能正常进入系统后,在rcS脚本启动wifiapp之前,我们要重新打开串口rx功能,才能保证wifiapp功能运行正常。已知在进入系统后,用户的操作都属于用户空间,但是内核驱动代码运行在内核空间。我试过在wifiapp中用termions结构体那些参数设置,来打开串口,可是都没有效果,看来这个结构体里的那些成员可以控制的权利有限,他只能设置一些浅层次的串口属性。因为我们在内核中相当于直接修改寄存器的状态,关闭了所有接收中断,如果想打开的话,还是要通过内核代码来控制,所谓解铃还须系铃人。那么这就涉及到用户空间和内和空间如何交互的问题,常见的交互方式包括系统调用、ioctlprocsysfs文件系统、mmap。除此之外用stty -F命令也可以配置串口,但很可惜由于我的文件系统体积较小,而且指令集是用很老版本的busybox制作,里面没有对stty命令的支持,所以这个方式不行。我们上面已经说过,mmap这种方式比较简单粗暴,可以直接控制硬件寄存器,可以参考datasheet,把寄存器修改为对应的打开状态就可以。但是我现在不使用这种方法,我想换一种方式。综合评估下来,还是用ioctl实现比较简单,因为我上面在内核代码中写了一个serial8250_stop_rx函数,那么我现在可以再写一个serial8250_start_rx函数,把之前禁用的那些跟rx接收有关的控制位再全部打开,相当于实现和serial8250_stop_rx完全相反的功能,也就可以使能rx。

内容如下:

static void serial8250_start_rx(struct uart_port *port)
{
    struct uart_8250_port *up = (struct uart_8250_port *)port;
    unsigned long flags;

    printk(KERN_INFO "serial8250_start_rx function\n");

    // 锁定以防止并发访问
    spin_lock_irqsave(&up->port.lock, flags);
    
    // 启用接收数据中断和接收线路状态中断
    up->ier |= UART_IER_RDI;  // 启用接收数据中断
    up->ier |= UART_IER_RLSI; // 启用接收线路状态中断
    up->port.read_status_mask |= UART_LSR_DR;
    serial_out(up, UART_IER, up->ier);

    // 解锁
    spin_unlock_irqrestore(&up->port.lock, flags);

    printk(KERN_INFO "IER after start RX: 0x%x\n", serial_in(up, UART_IER));
}

rx使能函数写好了,我们还是把它放到serial8250_startup函数末尾处调用,因为在应用层每次用open函数打开串口的时候,内核都会重新执行一遍serial8250_startup函数,也就相当于调用了我的serial8250_start_rx函数。这时我在应用程序发送一个ioctrl指令给它,比如给他发送1的时候让他调用start_rx函数,发送2的时候让他调用stop_rx,如国内和没收到应用空间的控制字,默认让他执行stop_rx,这段时间也是在内核启动的时候,还没尽到用户空间,正好这段时间我们是希望他屏蔽rx的。

我在应用层ioctrl写法如下:

#define TIOCSRXENABLE   _IO('T', 0x01)  // 自定义命令,启用 RX
#define TIOCSRXDISABLE  _IO('T', 0x02)  // 自定义命令,禁用 RX
void open_uart_rx_functions(int cmd)
{
	int fd = open(SERIAL_PORT0, O_RDWR);
	if (fd < 0) {
		perror("open");
		return -1;
	}
	if(cmd == 1)
	{
		// 启用 RX
		if (ioctl(fd, TIOCSRXENABLE) < 0) {
			perror("ioctl RX enable");
		}else
		{
			zww_log("send RX enable cmd to kernel ok!\n\n");
		}
	}
	else if(cmd == 2)
	{
		// 禁用 RX
		if (ioctl(fd, TIOCSRXDISABLE) < 0) {
			perror("ioctl RX disable");
		}
		else
		{
			zww_log("send RX disabled cmd to kernel ok!\n\n");
		}
	}
	else{
		zww_log("invaild ioctrl command\n");
	}
	usleep(10000);
	close(fd);
}

接下来写驱动层的ioctrl接收的时候,我发现了一些问题,我在8250.c驱动代码里根本没找到file_operation操作合集,这就让我很疑惑,既然没有系统调用借口,那用户空间和内核是怎么交互的呢?然后我顺着代码逻辑,一直往前看,在serial_core.c里找到一个名为static const struct tty_operations uart_ops的结构体,这是属于tty子系统的驱动部分,看一下他们的关系:

前面我在内核代码里调用serial8250_stop_rx那个函数,是直接改的8250串口控制器的驱动,但是我现在要在应用层发指令给驱动层来控制8250驱动,这中间还要经过一个tty抽象层,因为在用户空间打开/dev/ttyS*的时候其实是在和tty层交互,而不是直接和8250串口驱动交互,所以要想控制串口驱动,就要用到tty层提供的系统调用接口,也就是需要调用tty_operations uart_ops这个操作合集里的ioctrl,才能最终控制到串口驱动。

所以内核里的驱动代码应该这么写(serial_core.c文件里):

#define TIOCSRXENABLE   _IO('T', 0x01)  // 自定义命令,启用 RX
#define TIOCSRXDISABLE  _IO('T', 0x02)  // 自定义命令,禁用 RX
int open_uart_rx_flag = 0;
static int
uart_ioctl(struct tty_struct *tty, struct file *filp, unsigned int cmd,
	   unsigned long arg)
{
	/*printk(KERN_INFO "uart_ioctl function()\n");*/
	struct uart_state *state = tty->driver_data;
	void __user *uarg = (void __user *)arg;
	int ret = -ENOIOCTLCMD;
	/*
	 * These ioctls don't rely on the hardware to be present.
	 */
	switch (cmd)
	{
	case TIOCSRXENABLE:
		// 在这里实现你的自定义控制逻辑
		open_uart_rx_flag = 1 ;//enable rx
		printk(KERN_INFO "kernel recv inctrl cmd:open_uart_rx_flag == 1 enable rx\n");
		ret = 0;
		break;
	case TIOCSRXDISABLE:
		open_uart_rx_flag = 2 ;//disabled rx
		printk(KERN_INFO "kernel recv inctrl cmd:open_uart_rx_flag == 2 disabled rx\n");
		ret = 0;
		break;
	}
}

内核已经实现了uart_ioctl这个函数,我们只是用他这个接口,并在这个函数里添加自己的控制逻辑,但是不能删掉这个函数原本里的内容(我测试过)。当收到用户空间发来控制命令为TIOCSRXENABLE的时候,这时候设置open_uart_rx_flag=1;当收到TIOCSRXDISABLE时,设置open_uart_rx_flag=2;然后把open_uart_rx_flag 变量extern到8250.c文件里。

在8250.c里的serial8250_startup函数末尾处,根据open_uart_rx_flag的值来选择执行serial8250_stop_rx还是serial8250_start_rx。

好了,让我们现在实验一下,看一下效果:下面这张截图,是我开发板在启动的最后时刻打印的信息,可以看到最终我的应用程序执行的时候通过ioctol发送了TIOCSRXENABLE给内核,然后内核打印出来了kernel recv inctrl cmd:open_uart_rx_flag == 1 enable rx这句话,后面也成功执行了serial8250_start_rx函数,成功重新打开了串口。

用户空间调用8250串口驱动的流程如下:

总结:本篇博客介绍了如何通过修改内核源码,实现以下功能:从内核启动到系统初始化完成,这一段时间,完全屏蔽串口的接收功能,从而避免了rx数据导致的开发板启动死机的问题。在屏蔽rx并顺利进入到系统之后,这时候在用户空间通过ioctrl调用内核中相关代码,重新打开rx功能,以便保证应用程序的功能运行正常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值