学无止境--linux串口编程(RS485)

发布时间 2023-11-14 12:53:32作者: xMofang

备注:学习记录所用,若有高手不吝赐教,万分感谢!

一、概括

  linux将串口都映射成了TTY终端,所以在串口编程时,找到并使能平台的TTY,然后操作TTY终端即可。

  例如对于Nuclei平台的轩辕91030M芯片设备树:

	uart0: serial@10013000 {
		compatible = "sifive,uart0";
		reg = <0x0 0x10013000 0x0 0x1000>;
		interrupt-parent = <&plic0>;
		interrupts = <2>;
		clocks = <&hfclk2>;
		status = "okay";
	};

	uart1: serial@10012000 {
		compatible = "sifive,uart0";
		reg = <0x0 0x10012000 0x0 0x1000>;
		interrupt-parent = <&plic0>;
		interrupts = <3>;
		clocks = <&hfclk2>;
		status = "okay";
	};

  将status都设置为"okay",然后在“/dev/”目录下就会看到ttySIF0ttySIF1两个串口终端设备。其中ttySIF0是tty终端设备,也就是我们调试时连接到串口助手所用的串口,另一个ttySIF1就可以用来作为RS485串口。

二、收发控制

  RS485是半双工,需要一个gpio控制收发(如果硬件可以自动收发控制,则不需要)。

  gpio控制收发的写法我了解的主要有三种:

1、修改tty驱动

    网上能查到的基本都是这种,可以自行查找。

    本人认为移植和维护比较麻烦,还要修改内核的tty驱动。

 2、用户态驱动

    以gpio5为例,就是在用户态实现以下过程:

    echo 485 > /sys/class/gpio/export

    echo out > /sys/class/gpio/gpio485/direction

    echo 0 > /sys/class/gpio/gpio485/value 

    echo 1 > /sys/class/gpio/gpio485/value 

 3、内核态驱动

    3.1、设备树配置

/*
	 * GPIO_ACTIVE_HIGH  0
	 * GPIO_ACTIVE_LOW   1
	 */
	tl485ctl {
		compatible = "tl,rs485_ctl";
		de-gpios = <&gpio 8 1>;
	};

    此配置下,若是gpio有修改,只需要修改设备树即可。

    3.2、驱动代码

      注册一个杂项设备。

查看代码
//#define TLMOD_DEBUG
#ifdef TLMOD_DEBUG
#define DPRINTK(x...) printk("tl485_ctl DEBUG:" x)
#else
#define DPRINTK(x...)
#endif

#define DRIVER_NAME "tl485_ctl"

struct tl485_ctl{
	int de_gpio;
	struct device_node	*nd;  /*设备节点--设备树中的 tl485ctl {...};*/
};

struct tl485_ctl tl485dev;

int tl485_ctl_open(struct inode *inode,struct file *filp)
{
	DPRINTK("Device Opened Success!\n");
	return nonseekable_open(inode, filp);
}

int tl485_ctl_release(struct inode *inode,struct file *filp)
{
	DPRINTK("Device Closed Success!\n");return 0;
}

int tl485_ctl_pm(bool enable)
{
	int ret = 0;
	DPRINTK("firecxx debug: GPS PM return %d\r\n" , ret);
	return ret;
};

long tl485_ctl_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	DPRINTK("firecxx debug: tl485_ctl_ioctl cmd is %d\n" , cmd);
	switch(cmd)
	{       
		case 1:
				gpio_set_value(tl485dev.de_gpio, 1);
				udelay(10); /*可忽略*/
				DPRINTK("tl485_ctl Set High!\n");
			break;
		case 0:           
				gpio_set_value(tl485dev.de_gpio, 0);
				udelay(10); /*可忽略*/
				DPRINTK("tl485_ctl Set Low!\n");
			break;
		default:
			DPRINTK("tl485_ctl COMMAND ERROR!\n");
			return -ENOTTY;
	}
	return 0;
}

static struct file_operations tl485_ctl_ops = {
	.owner  = THIS_MODULE,
	.open   = tl485_ctl_open,
	.release= tl485_ctl_release,
	.unlocked_ioctl     = tl485_ctl_ioctl,
};

static struct miscdevice tl485_ctl_dev = {
	.minor  = MISC_DYNAMIC_MINOR,
	.fops   = &tl485_ctl_ops,
	.name   = "tl485_ctl_pin", /*真正需要操作的设备名*/
};

static int tl485_ctl_probe(struct platform_device *pdev)
{
    int err = 0;
    int ret;
    
    DPRINTK("tl485_ctl Initialize\n");

	tl485dev.nd = pdev->dev.of_node;
	if(tl485dev.nd == NULL)
	{
		printk(KERN_ERR "Can't get node tl485_ctl!\n");
		return err;
	}

	tl485dev.de_gpio = of_get_named_gpio(tl485dev.nd, "de-gpios", 0);
    if(tl485dev.de_gpio < 0)
	{
		printk(KERN_ERR "Can't get de_gpio!\n");
		return err;
	}

	DPRINTK("tl485_ctl Initialize : %d\n", tl485dev.de_gpio);

	err = gpio_request(tl485dev.de_gpio, "tl485_ctl");
    if (err) 
    {
        printk(KERN_ERR "failed to request GPIO_%d for ""tl485_ctl control\n", tl485dev.de_gpio);
        return err;
    }
    gpio_direction_output(tl485dev.de_gpio, 0); /*真正设置gpio方向的地方*/
    //gpio_free(tl485dev.de_gpio);

    ret = misc_register(&tl485_ctl_dev); /*里面实现了分配设备号、设备注册、class创建、dev创建等过程*/
    if(ret<0)
    {
        printk(KERN_ERR "tl485_ctl:register device failed!\n");
        goto exit;
    }
    return 0;

    exit:
    misc_deregister(&tl485_ctl_dev);

    return ret;
}

static int tl485_ctl_remove (struct platform_device *pdev)
{
	gpio_free(tl485dev.de_gpio);
	misc_deregister(&tl485_ctl_dev);  
	return 0;
}

static int tl485_ctl_suspend (struct platform_device *pdev, pm_message_t state)
{
	DPRINTK("tl485_ctl suspend:power off!\n");
	return 0;
}

static int tl485_ctl_resume (struct platform_device *pdev)
{
	DPRINTK("tl485_ctl resume:power on!\n");
	return 0;
}

/* 设备树匹配列表 */
static const struct of_device_id tl485_ctl_of_match[] = {
	{.compatible = "tl,rs485_ctl"},
	{}
};

static struct platform_driver tl485_ctl_driver = {
	.probe = tl485_ctl_probe,
	.remove = tl485_ctl_remove,
	.suspend = tl485_ctl_suspend,
	.resume = tl485_ctl_resume,
	.driver = {
		.name = "tl485_ctl",
		.owner = THIS_MODULE,
		.of_match_table = tl485_ctl_of_match,
	},
};

static int __init tl485_ctl_init(void)
{
	return platform_driver_register(&tl485_ctl_driver);
}

static void __exit tl485_ctl_exit(void)
{
	platform_driver_unregister(&tl485_ctl_driver);
}

module_init(tl485_ctl_init);
module_exit(tl485_ctl_exit);
MODULE_LICENSE("GPL");

  然后在需要时控制/dev/tl485_ctl_pin即可。

三、用户态代码

  1、串口配置

查看代码
 static int io_uart_set_opt(int ttyfd,int nSpeed, int nBits, char nEvent, int nStop)
{
    struct termios newtio, oldtio;
	
    if (tcgetattr(ttyfd, &oldtio) != 0) 
    { 
        perror("SetupSerial 1");
		return -1;
    }
	
    bzero(&newtio, sizeof(newtio));
 	
    newtio.c_cflag |= CLOCAL | CREAD;

	/* 设置字符大小 */
    newtio.c_cflag &= ~CSIZE;
    switch(nBits)
    {
        case 7:
			newtio.c_cflag |= CS7;
			break;
        case 8:
			newtio.c_cflag |= CS8;
			break;
    }

    switch(nEvent)
    {
        case 'O':
            newtio.c_cflag |= PARENB;
			newtio.c_cflag |= PARODD;
			newtio.c_iflag |= (INPCK | ISTRIP);
            break;
        case 'E': 
            newtio.c_iflag |= (INPCK | ISTRIP);
			newtio.c_cflag |= PARENB;
			newtio.c_cflag &= ~PARODD;
            break;
        case 'N':  
            newtio.c_cflag &= ~PARENB;
            break;
    }

    switch(nSpeed)
    {
        case 2400:
            cfsetispeed(&newtio, B2400);
            cfsetospeed(&newtio, B2400);
            break;
        case 4800:
            cfsetispeed(&newtio, B4800);
            cfsetospeed(&newtio, B4800);
            break;
        case 9600:
            cfsetispeed(&newtio, B9600);
            cfsetospeed(&newtio, B9600);
            break;
        case 115200:
            cfsetispeed(&newtio, B115200);
            cfsetospeed(&newtio, B115200);
            break;
        case 460800:
            cfsetispeed(&newtio, B460800);
            cfsetospeed(&newtio, B460800);
            break;
        case 921600:
            cfsetispeed(&newtio, B921600);
            cfsetospeed(&newtio, B921600);
            break;
        default:
            cfsetispeed(&newtio, B9600);
			cfsetospeed(&newtio, B9600);
            break;
    }
	
    if (nStop == 1)
    	newtio.c_cflag &= ~CSTOPB;
    else if (nStop == 2)
    	newtio.c_cflag |= CSTOPB;
	
    newtio.c_cc[VTIME] = 0;
	newtio.c_cc[VMIN] = 0;
	
    tcflush(ttyfd, TCIOFLUSH);
	
    if((tcsetattr(ttyfd, TCSANOW, &newtio))!=0)
    {
        perror("com set error");
        return -1;
    }

	return 0;
}

  需要的有不少,这些都可以在网上找到。

  2串口收发控制

查看代码
 static int io_rs485_to_send(void)
{
    int ret;
#if KERNEL_RS485_CTRL
	int fd;
	char *tl485_ctl = "/dev/tl485_ctl_pin";
	
	fd = open(tl485_ctl, O_RDWR);
	if(fd < 0)
    {
        printf("Open %s failed\n", tl485_ctl);
        close(fd);
		return -1;
    }
	
    ret = ioctl(fd, 1, 0);
    if(ret<0)
    {
        printf("tl485 set ctl to high failed!\r\n");
		close(fd);
        return -1;
    }

	close(fd);
    return 0;
#else
	if(devGpioSet(8, 1) < 0)
		return -1;
	
	return 0;
#endif
}

static int io_rs485_to_recv(void)
{
    int ret;
#if KERNEL_RS485_CTRL
	int fd;
	char *tl485_ctl = "/dev/tl485_ctl_pin";
	
	fd=open(tl485_ctl, O_RDWR);
	if(fd < 0)
    {
        printf("Open %s failed\n", tl485_ctl);
        close(fd);
		exit(1);
    }
	
    ret = ioctl(fd, 0, 0);
    if(ret<0)
    {
    	close(fd);
        printf("tl485 set ctl to low failed!\r\n");
        return -1;
    }
	
    close(fd);
    return 0;
#else
	if(devGpioSet(8, 0) < 0)
		return -1;

	return 0;
#endif
}

 3、串口初始化

查看代码
 int devRS485InitPort(int com)
{
	int fd;
	int rv;
	int flags = 0;
	
	fd = open(USART_RS485_DEV, O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd < 0) {
        perror("Open error:\n");
		exit(1);
    }

	/* 恢复串口为阻塞状态 */
	if (fcntl(fd, F_SETFL, 0) < 0) {
		printf("fcntl failed.\n");
		return -1;
	}

    /* 测试该设备是否为tty设备 */
	if (isatty(fd) == 0) {
		printf("not tty device.\n");
		return -1;
	}

    rv = io_uart_set_opt(fd, 9600, 8, 'N', 1);
    if (rv < 0) {
        printf("Set uart faild\n");
		return -1;
    }

	return fd;
}

  4、串口发送函数

查看代码
STATUS devRS485SendDatas(int ttyfd, const char *buf, int len)
{
	int rv = 0;
	ssize_t wlen = 0;
	int sendflag = 0;
	
	rv = io_rs485_to_send();
	if(rv < 0)
	{
		printf("set 485 to send failed\n");
		return -1;
	}

	wlen = write(ttyfd, buf, len);
	if (wlen != len) {
        tcflush(ttyfd, TCOFLUSH);
		printf("write 485 failed\n");
        return -1;
    }
    
    /* write只是将数据从文件写到了发送缓存区,
     * tcdrain是等待发送缓存区发送完成,完成之前阻塞。
     * 从很多教程都说这个函数会在这里阻塞,但是我测试波形时发现:
       收发控制管脚总是在RS485数据线刚出波形就置为接收状态了
       !!!!!!!这里没搞懂,所以在后面加了延时。
     */
	rv = tcdrain(ttyfd); 
	if(rv < 0)
	{
		printf("Wite 485 send failed\n");
		return -1;
	}

	usleep(len * 1000); /*9600发送1BYTE数据大约1ms*/

	rv = io_rs485_to_recv();
	if(rv < 0)
	{
		printf("Set 485 recv failed\n");
		return -1;
	}

	return 0;
}

  5、接收函数

查看代码
int g485fd;

int open485(void)
{
    int retval;
    fd_set rfds;
    struct timeval tv;
    int nread;
    char gRcvBuf[256];
    int gRcvLen = 0;
    
    g485fd = devRS485InitPort(0);
    if (g485fd < 0) {
        printf("error: open console error.\r\n");
        close(g485fd);
        return ERROR;
    }
    
    // wait 2.5s ,select最后一个参数
    tv.tv_sec = 2;      //阻塞时间(秒)
    tv.tv_usec = 500;   //阻塞时间(毫秒)
	
    while (1)
    {	
        FD_ZERO(&rfds);
        FD_SET(g485fd, &rfds);

        retval = select(g485fd + 1 , &rfds, NULL, NULL, NULL); /*最后一个参数为NULL表示有数据前一直阻塞*/
        if (retval == -1) {
            perror("select()");
            break;
        }
        else if (retval) { // pan duan shi fou hai you shu ju
            if(!FD_ISSET(g485fd,&rfds)) /*判断是不是这个串口触发的*/
                break;
            
            /*测试时发现这里每次调用read()只收到一个byte*/
            nread = read(g485fd, gRcvBuf + gRcvLen, 256); 
            gRcvLen += nread;

            /*缓存区越界处理,这里只是随便写的,需要修改*/
            if (gRcvLen >= sizeof(gRcvBuf))
                gRcvLen = 0; 
			
            //printf("gRcvLen = %d ", gRcvLen);
	
            if (gRcvBuf[gRcvLen - 2] == '\r' && gRcvBuf[gRcvLen-1] == '\n') {
                FD_ZERO(&rfds);
                FD_SET(g485fd, &rfds);
	
                retval = select(g485fd + 1 , &rfds, NULL, NULL, NULL);
                if (!retval) continue;// no datas, break
			}
            
            /*
             *包解析流程
             ......
             */

            //for (int i = 0; i < 19; i++) {
            //	printf("%02x ", gRcvBuf[i]); 
            //}

            //printf("gRcvLen=%d\r\n", gRcvLen);
        }
        else {
			continue;
        };
    }

  创建一个任务处理串口的接收即可。