37 Linux 485收发切换延时导致接收数据错乱问题解决

37.1 前言

这几天在对接Linux上位机与stm32开发板,通过485进行对接。Linux上位机端485是通过串口+485转换芯片+一个IO控制方向组成的。原本以为485这东西简简单单(之前有做过两块单片机的485驱动),没想到因为Linux系统调用延时导致485收发切换延时,导致Linux上位机始终不能正确接收stm32下位机回复的数据。

期间以为是硬件问题,然后一直找硬件的人帮忙查看485阻容匹配是否正确,后面硬件的确发现一些问题,并作了一些改进,但仍然未能解决收发数据错乱问题,没办法,拿示波器看了又看,最后发现原来Linux上位机这边485发送数据后没能快速切换为接收状态,而下位机却在切换之前回复了上位机,因此导致Linux 485接收数据乱码。

37.2 切换延时探析

从波形找出了问题所在,回归串口编程,如下图示。在发送数据前将485芯片控为发送状态,发送后等待串口数据发送完成,然后在将485芯片控为接收状态。如果发送完不调用tcdrain而直接控IO可能导致数据没有发送完成就中断了,因此必须要找个方式等待数据发送完成。直接sleep肯定不行,因为sleep函数本来就是不精准的,有时sleep少,有时sleep多,难搞。

 

         串口硬件发送数据应该很快不用那么25毫秒才把几个字节的数据发送出去啊!!什么问题呢?查阅资料是发现一个合理的解释是:Linux对硬件实时性高,对于用户请求的实时性较低。

这个得说说Linux工作队列相关机制,对于硬件操作Linux处理的很及时,但是对于数据Linux可能将其交给系统的下半部的内核线程去处理,这就可能导致用户的系统调用存在一定的延时。为此,如果想要解决这种影响,那么就换一种方式去等待数据发送完成而切换485的收发状态。

37.3 Linux 485收发切换延时解决方案选择

关于Linux485切换延时问题,成熟的解决方案有以下几种:

第一、硬件自控收发,要重新设计硬件不太现实,特别是对于已经发货很多的产品;

第二、更改串口驱动,使其增加对485 的支持,工作量太大,需要对tty子系统比较熟悉,难度有点大;

第三、编写Linux驱动模块,监控串口状态寄存器,有用户层触发监控再由内核线程自行判断控制方向,该方式实测可行,较第二更为简单。

本篇亦是基于第三种方式,解决Linux485切换延时的问题的。

 

37.4 Linux 485切换方案实现

目标:编写一个内核模块,模块内开一个内核线程(平时处于休眠状态),结合高精度定时器及条件变量查询串口状态寄存器的发送位标记,直到寄存器发送状态为置为空闲,此时将485切换为接收状态。该内核模块主要实现了iotcl函数,提供接口给用户层触发查询串口状态标志位情况,还有两个高精度内核定时器,用以精准延时或查询。

实现代码:

readStatus.h:

#ifndef _READSTATUS_H_
#define _READSTATUS_H_

struct stDataInfo
{
	int nOption;
};

#define READSTATUS_IOC_MAGIC 0XF157
#define READSTATUS_IOCSET   _IOW(READSTATUS_IOC_MAGIC, 0, struct stDataInfo)
#define READSTATUS_IOCGET	_IOR(READSTATUS_IOC_MAGIC, 1, struct stDataInfo)

#endif

readStatus.c:

/*****************************************************************
* 包含头文件
******************************************************************/
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/hrtimer.h>
#include <linux/jiffies.h>
#include <linux/device.h>	//class_createnikey
#include <linux/sched.h>	//wake_up_process()
#include <linux/kthread.h>	//kthread_create()、kthread_run()
#include <asm/uaccess.h>
#include <linux/err.h> 
#include <linux/time.h>
#include <linux/delay.h> //for msleep

#include "readStatus.h"

/*****************************************************************
* 宏定义(仅在当前C文件使用的宏定义写在当前C文件中,否则需写在H文件中)
******************************************************************/
#define I_DEVICE_NAME			"iTestDev"

#define I_UART0_REGFR			0x120a0018 //Uart状态寄存器地址

#define I_GPIO0_CTLREG			0x112F0034 //控制寄存器地址
#define I_GPIO0_DIRREG			0x120D0400 //方向寄存器地址
#define I_GPIO0_DATAREG			0x120D03FC //数据寄存器地址

/*****************************************************************
* 结构定义(仅在当前C文件使用的结构体写在当前C文件中,否则需写在H文件中)
******************************************************************/
struct _stMngrInfo{
	int major;
	struct class *iTest_class;
	struct device *iTest_device;

	unsigned int *pUartFR;				/*串口状态寄存器指针*/
	unsigned int *p485CtlReg;			/*GPIO0_6控制寄存器指针*/
	unsigned int *p485DirReg;			/*GPIO0_6方向寄存器指针*/
	unsigned int *p485DataReg;			/*GPIO0_6数据寄存器指针*/

	int nKPthreadWorkFlag;				/*内核线程运行标记*/
	struct task_struct *pkThreadTask; /*内核线程指针*/
	
	int nQueWaiting;					/*等待队列条件*/	
	wait_queue_head_t iwaitQue; 		/*等待队列*/  
	struct hrtimer ihrtimer;			/*高精度定时器*/	

	int SSwitchDelayFlag;				/*发送后接收延时*/
	struct hrtimer Sihrtimer;			/*高精度定时器*/	
};

/*****************************************************************
* 全局变量定义
******************************************************************/
struct _stMngrInfo gstMngrInfo;
static DEFINE_MUTEX(gReadStatusMutex); /*互斥锁*/

/*****************************************************************
* 静态变量定义
******************************************************************/

/*****************************************************************
* 外部变量声明(如果全局变量没有在其它的H文件声明,引用时需在此处声明,
*如果已在其它H文件声明,则只需包含此H文件即可)
******************************************************************/


static int iTest_HwInit(struct _stMngrInfo *pgstMngrInfo)
{
	pgstMngrInfo->pUartFR = ioremap(I_UART0_REGFR, 4);
	if(pgstMngrInfo->pUartFR==NULL){
		printk(KERN_EMERG "ioremap %x faild\n",I_UART0_REGFR);
		return -1;
	}

	pgstMngrInfo->p485CtlReg = ioremap(I_GPIO0_CTLREG, 4);
	if(pgstMngrInfo->p485CtlReg==NULL){
		printk(KERN_EMERG "ioremap %x faild\n",I_GPIO0_CTLREG);
		goto iExit1;
	}
	
	pgstMngrInfo->p485DirReg = ioremap(I_GPIO0_DIRREG, 4);
	if(pgstMngrInfo->p485DirReg==NULL){
		printk(KERN_EMERG "ioremap %x faild\n",I_GPIO0_DIRREG);
		goto iExit2;
	}
	
	pgstMngrInfo->p485DataReg = ioremap(I_GPIO0_DATAREG, 4);
	if(pgstMngrInfo->p485DataReg==NULL){
		printk(KERN_EMERG "ioremap %x faild\n",I_GPIO0_DATAREG);
		goto iExit3;
	}
	
	printk(KERN_EMERG "iTest_HwInit OK.\n");
	return 0;

iExit3:
	iounmap(pgstMngrInfo->p485DataReg);
iExit2:
	iounmap(pgstMngrInfo->p485CtlReg);
iExit1:
	iounmap(pgstMngrInfo->pUartFR);
	
	printk(KERN_EMERG "iTest_HwInit Fail.\n");
	return -1;
}


static unsigned int iTest_Set485Dir(int nOption)
{
	struct _stMngrInfo *pgstMngrInfo = &gstMngrInfo;
	unsigned int nReg485Value = 0;

	//选为GPIO6模式
	nReg485Value = readl(pgstMngrInfo->p485CtlReg);
	nReg485Value &= ~0xF;
	writel(nReg485Value,pgstMngrInfo->p485CtlReg);

	//设为输出
	nReg485Value = readb(pgstMngrInfo->p485DirReg);
	nReg485Value |= (1<<6);
	writeb((char)nReg485Value,pgstMngrInfo->p485DirReg);

	//设置输出电平
	if(nOption==1)
	{
		nReg485Value = readb(pgstMngrInfo->p485DataReg);
		nReg485Value |= (1<<6);
		writeb((char)nReg485Value,pgstMngrInfo->p485DataReg);
	}
	else
	{
		nReg485Value = readb(pgstMngrInfo->p485DataReg);
		nReg485Value &= ~(1<<6);
		writeb((char)nReg485Value,pgstMngrInfo->p485DataReg);
	}
	
	return 0;
}

static int iTest_wakeUpKThread(void)
{
	struct _stMngrInfo *pgstMngrInfo = &gstMngrInfo;

	mutex_lock(&gReadStatusMutex);
	pgstMngrInfo->nQueWaiting = 1;
	mutex_unlock(&gReadStatusMutex);

	wake_up(&pgstMngrInfo->iwaitQue);
	return 0;
}

static unsigned short iTest_scanUARTFRBusyBit(struct _stMngrInfo *pgstMngrInfo)
{
	unsigned short sRegUartValue = 0;
	sRegUartValue = readw(pgstMngrInfo->pUartFR);

	sRegUartValue = (sRegUartValue &(1<<3));

	return sRegUartValue;
}

static enum hrtimer_restart iTest_ShrtimerHander(struct hrtimer *timer)  
{
	iTest_Set485Dir(0);//设为接收状态
	return HRTIMER_NORESTART;
}

static enum hrtimer_restart iTest_hrtimerHander(struct hrtimer *timer)  
{
	mutex_lock(&gReadStatusMutex);
	gstMngrInfo.nQueWaiting = 1;
	mutex_unlock(&gReadStatusMutex);
	
	wake_up(&gstMngrInfo.iwaitQue);
	return HRTIMER_NORESTART;
}

static int iTest_kthread(void *arg)
{
	int nRet = 0;
	struct _stMngrInfo *pgstMngrInfo = (struct _stMngrInfo *)arg;
	
	while(!kthread_should_stop()) 
	{
		/*添加这个是为了能退出线程*/
		if(pgstMngrInfo->nKPthreadWorkFlag == 0)
		{
			msleep(1000);
			continue;
		}

		/*等待唤醒,然后查看串口发送位状态*/
		wait_event(pgstMngrInfo->iwaitQue,pgstMngrInfo->nQueWaiting);		
		mutex_lock(&gReadStatusMutex);
		pgstMngrInfo->nQueWaiting = 0;
		mutex_unlock(&gReadStatusMutex);
		
		nRet = iTest_scanUARTFRBusyBit(pgstMngrInfo);
		if(nRet==0)
		{
			hrtimer_start(&pgstMngrInfo->Sihrtimer,ktime_set(0,5000*1000),HRTIMER_MODE_REL);  
		}
		else
		{
			/*启动高精度定时器,1ms后唤醒线程*/
			hrtimer_start(&pgstMngrInfo->ihrtimer,ktime_set(0,1000*1000),HRTIMER_MODE_REL);  
		}
	}
	
	printk(KERN_EMERG "iTest_kthread exit OK!\n");
	return 0;
}

static long iTest_unioctl(struct file *pfilp, unsigned int cmd, unsigned long arg)
{
	int nRet = 0;
	struct stDataInfo iDataInfo;

	if (copy_from_user(&iDataInfo, (const void *)arg, sizeof(struct stDataInfo))) {
		nRet = -EFAULT;
		goto iExit;
	}

	switch (cmd) {
		case READSTATUS_IOCGET: //get 内核态数据
			{
				if (copy_to_user((void *)arg, &iDataInfo, sizeof(struct stDataInfo))) {
					nRet = -EFAULT;
				}
			}
			break;
		case READSTATUS_IOCSET:
			{
				if(iDataInfo.nOption == 1)
				{
					//设为发送
					iTest_Set485Dir(1);
				}
				else if(iDataInfo.nOption == 0)
				{
					//唤醒线程
					iTest_wakeUpKThread();
				}
			}
			break;
		default:
			{
				pr_err("wrong ioctl cmd[%d]\n", cmd);
				nRet = -EINVAL;
			}
		break;
	}

iExit:

	return nRet;
}

static const struct file_operations iTest_fops = {
	.owner	= THIS_MODULE,
	.unlocked_ioctl = iTest_unioctl,
};

static int __init iTest_Init(void)
{
	int nRet = -1;
	memset(&gstMngrInfo,0,sizeof(gstMngrInfo));
	
	/* 主设备号设置为0表示由系统自动分配主设备号 */
	gstMngrInfo.major = register_chrdev(0, I_DEVICE_NAME, &iTest_fops);

	/* 创建iTest_class类 */
	gstMngrInfo.iTest_class = class_create(THIS_MODULE, "iTestClass");

	/* 在iTest_class类下创建设备,并在/dev/目录下创建iTestDevice节点*/
	gstMngrInfo.iTest_device = device_create(gstMngrInfo.iTest_class, NULL, MKDEV(gstMngrInfo.major, 0), NULL, "iTestDevice");

	nRet = iTest_HwInit(&gstMngrInfo);
	if(nRet<0)
		return -1;

	/*高精度定时器初始化*/
	 /*初始化等待队列*/  
	gstMngrInfo.nQueWaiting = 0;  
	init_waitqueue_head(&gstMngrInfo.iwaitQue);  

	/*初始化高精度定时器*/  
	hrtimer_init(&gstMngrInfo.ihrtimer,CLOCK_MONOTONIC,HRTIMER_MODE_REL);
	gstMngrInfo.ihrtimer.function = iTest_hrtimerHander;    /*唤醒查询线程回调函数*/

	hrtimer_init(&gstMngrInfo.Sihrtimer,CLOCK_MONOTONIC,HRTIMER_MODE_REL);
	gstMngrInfo.Sihrtimer.function = iTest_ShrtimerHander;    /*切换接收回调*/

	/*创建内核线程*/
	gstMngrInfo.nKPthreadWorkFlag = 1;
	gstMngrInfo.pkThreadTask = kthread_create(iTest_kthread,&gstMngrInfo,"iTest_task");
	if (IS_ERR(gstMngrInfo.pkThreadTask))
	{
		nRet = PTR_ERR(gstMngrInfo.pkThreadTask);
		printk(KERN_EMERG "kthread_create fail.\n");
		return nRet;
	}
	//开始线程
	wake_up_process(gstMngrInfo.pkThreadTask);

	printk(KERN_EMERG "iTest_Init OK.\n");
	return nRet;
}


static void __exit iTest_Exit(void)
{

	if (gstMngrInfo.pkThreadTask!=NULL)
	{
		gstMngrInfo.nKPthreadWorkFlag = 0;
		iTest_wakeUpKThread();
		kthread_stop(gstMngrInfo.pkThreadTask);
		gstMngrInfo.pkThreadTask = NULL;
	}
	
	hrtimer_cancel(&gstMngrInfo.ihrtimer);
	hrtimer_cancel(&gstMngrInfo.Sihrtimer);

	iounmap(gstMngrInfo.p485DataReg);
	iounmap(gstMngrInfo.p485CtlReg);
	iounmap(gstMngrInfo.pUartFR);
	
	unregister_chrdev(gstMngrInfo.major, I_DEVICE_NAME);
	device_unregister(gstMngrInfo.iTest_device);
	class_destroy(gstMngrInfo.iTest_class);	
	
	printk(KERN_EMERG "iTest_Exit OK.\n");
}
 
module_init(iTest_Init);
module_exit(iTest_Exit);

MODULE_AUTHOR("ljc");
MODULE_LICENSE("GPL");
 

  • 2
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值