Gadget应用实例之serial

文章详细介绍了如何使用LinuxGadget实现USB串口通信,包括硬件连接、设备节点的创建、软件框架分析、数据传输过程(APP访问与printk)以及编程示例,展示了在PC和嵌入式板子间进行串口通信的方法。
摘要由CSDN通过智能技术生成

Gadget应用实例之serial



参考资料:

一、硬件体验

使用USB线连接板子的OTG口和PC的USB口。

然后在板子加载驱动程序后,可以看到新的设备节点/dev/ttyGS0:

# modprobe g_serial
g_serial gadget: Gadget Serial v2.4
g_serial gadget: g_serial ready
g_serial gadget: high-speed config #2: CDC ACM config

# ls /dev/ttyGS0 -l
crw-rw----    1 root     dialout   246,   0 Jan  1 00:30 /dev/ttyGS0

在PC上,如果是Windows系统,可以在设备管理器里看到新的USB串口:
在这里插入图片描述

在PC上,如果是VMware上的Linux系统,按下图操作,先把USB串口连接到VMware:
在这里插入图片描述

然后在PC Linux中可以看到新的设备节点:

Hilbert@ubuntu22.04:~$ dmesg
[  286.903239] usb 1-1: new high-speed USB device number 2 using ehci-pci
[  287.254549] usb 1-1: New USB device found, idVendor=0525, idProduct=a4a7, bcdDevice= 4.09
[  287.254550] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[  287.254551] usb 1-1: Product: Gadget Serial v2.4
[  287.254552] usb 1-1: Manufacturer: Linux 4.9.88 with 2184000.usb
[  287.342786] cdc_acm 1-1:2.0: ttyACM0: USB ACM device
[  287.343202] usbcore: registered new interface driver cdc_acm
[  287.343202] cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters
Hilbert@ubuntu22.04:~$ ls /dev/ttyACM0 -l
crw-rw---- 1 root dialout 166, 0 Mar  5 22:38 /dev/ttyACM0

二、 Serial分析

2.1 软件框架

Gadget串口的框架如下:

在这里插入图片描述

u_serial提供了有2种方法来使用Gadget串口:

在这里插入图片描述

  • u_serial.c里注册console结构体gserial_cons。启动Linux内核时传入commandline参数"console=ttyGS0"后,内核的printk的信息通过Gadget串口打印出来(Host要打开USB串口):

    在这里插入图片描述

注册TTY和console的过程:

gs_bind // drivers\usb\gadget\legacy\serial.c
    status  = serial_register_ports(cdev, &serial_config_driver,
		    "acm");
		    
		    	fi_serial[i] = usb_get_function_instance(f_name);
	
	
acm_alloc_instance // drivers\usb\gadget\function\f_acm.c
	ret = gserial_alloc_line(&opts->port_num); // drivers\usb\gadget\function\u_serial.c
	
			// 注册TTY
            tty_dev = tty_port_register_device(&ports[port_num].port->port,
                    gs_tty_driver, port_num, NULL);

			// 注册console
			gserial_console_init();
            	register_console(&gserial_cons);
	

2.2 数据传输

2.2.1 APP访问

注意,在USB中数据传输总是由Host发起,所以:

  • 板子要事先准备好空间(设置好out方向的usb_request并放入队列),以便接收Host发来的数据;
  • 板子有数据想发送给Host时需要设置in方向的usb_request,以便Host读取。

板子上的APP访问/dev/ttyGS0时,就会导致gs_tty_ops结构体的对应函数被调用:

在这里插入图片描述

APP调用open函数时,会导致如下调用:

gs_open
	gs_start_io(port);
		// 取出out端点(对应Host来说是out, 对于板子来说就是输入)
		struct usb_ep		*ep = port->port_usb->out;
		
		// 给out端点分配usb_request
        status = gs_alloc_requests(ep, head, gs_read_complete,
            &port->read_allocated);

		// 给in端点分配usb_request, 但是在open时并没有把in方向的usb_request放入队列
        status = gs_alloc_requests(port->port_usb->in, &port->write_pool,
                gs_write_complete, &port->write_allocated);

        // 把usb_request放入队列, 如果Host发来数据, 这个usb_request的complete函数被调用
		started = gs_start_rx(port);
					status = usb_ep_queue(out, req, GFP_ATOMIC);

APP调用write函数时,会导致如下调用:

gs_write
	gs_start_tx(port);
		// 把usb_request放入队列, Host读取数据时就可以从中得到数据
		status = usb_ep_queue(in, req, GFP_ATOMIC);
2.2.2 printk

启动Linux内核时传入commandline参数"console=ttyGS0"后,内核的printk的信息通过Gadget串口打印出来(Host要打开USB串口)。

内核的printk函数会导致gserial_cons结构体中的write指针即gs_console_write函数被调用:

在这里插入图片描述

gs_console_write函数的调用关系如下:

gs_console_write
	// 把要打印的数据放入环形buffer
	gs_buf_put(&info->con_buf, buf, count);
	
	// 唤醒内核线程
	wake_up_process(info->console_thread);
	
// 内核线程
gs_console_thread
	// 被唤醒后
	
	// 取出输入端点和它的usb_request
	req = info->console_req;
	ep = port->port_usb->in;
	
	// 从环形buffer得到数据、设置usb_request
	xfer = gs_buf_get(&info->con_buf, req->buf, size);
	req->length = xfer;
	
	// 把usb_request放入队列,以便Host读取
	ret = usb_ep_queue(ep, req, GFP_ATOMIC);

3. 编程

PC: open/read/write /dev/ttyACM0

板子: open/read/write /dev/ttyGS0

参考资料:https://stackoverflow.com/questions/7469139/what-is-the-equivalent-to-getch-getche-in-linux

源码:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>
#include <pthread.h>


static struct termios old, current;

/* Initialize new terminal i/o settings */
void initTermios(int echo) 
{
  tcgetattr(0, &old); /* grab old terminal i/o settings */
  current = old; /* make new settings same as old settings */
  current.c_lflag &= ~ICANON; /* disable buffered i/o */
  if (echo) {
      current.c_lflag |= ECHO; /* set echo mode */
  } else {
      current.c_lflag &= ~ECHO; /* set no echo mode */
  }
  tcsetattr(0, TCSANOW, &current); /* use these new terminal i/o settings now */
}

/* Restore old terminal i/o settings */
void resetTermios(void) 
{
  tcsetattr(0, TCSANOW, &old);
}


/* set_opt(fd,115200,8,'N',1) */
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
	struct termios newtio,oldtio;
	
	if ( tcgetattr( fd,&oldtio) != 0) { 
		perror("SetupSerial 1");
		return -1;
	}
	
	bzero( &newtio, sizeof( newtio ) );
	newtio.c_cflag |= CLOCAL | CREAD; 
	newtio.c_cflag &= ~CSIZE; 

	newtio.c_lflag  &= ~(ICANON | ECHO | ECHOE | ISIG);  /*Input*/
	newtio.c_oflag  &= ~OPOST;   /*Output*/

	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;
	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[VMIN]  = 1;  /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
	newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间: 
	                         * 比如VMIN设为10表示至少读到10个数据才返回,
	                         * 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)
	                         * 假设VTIME=1,表示: 
	                         *    10秒内一个数据都没有的话就返回
	                         *    如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
	                         */

	tcflush(fd,TCIFLUSH);
	
	if((tcsetattr(fd,TCSANOW,&newtio))!=0)
	{
		perror("com set error");
		return -1;
	}
	//printf("set done!\n");
	return 0;
}

int open_port(char *com)
{
	int fd;
	//fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);
	fd = open(com, O_RDWR|O_NOCTTY);
    if (-1 == fd){
		return(-1);
    }
	
	  if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/
	  {
			printf("fcntl failed!\n");
			return -1;
	  }
  
	  return fd;
}

static void *my_read_thread_func (void *data)
{
    int fd = (int)data;
    int iRet;
    char c;
    
	while (1)
	{
		iRet = read(fd, &c, 1);
        printf("%c", c);
        fflush(stdout);
	}
}


/*
 * ./serial_send_recv <dev>
 */
int main(int argc, char **argv)
{
	int fd;
	int iRet;
	char c;
	pthread_t tid;

	/* 1. open */

	/* 2. setup 
	 * 115200,8N1
	 * RAW mode
	 * return data immediately
	 */

	/* 3. write and read */
	
	if (argc != 2)
	{
		printf("Usage: \n");
		printf("%s </dev/ttySAC1 or other>\n", argv[0]);
		return -1;
	}

	fd = open_port(argv[1]);
	if (fd < 0)
	{
		printf("open %s err!\n", argv[1]);
		return -1;
	}

	iRet = set_opt(fd, 115200, 8, 'N', 1);
	if (iRet)
	{
		printf("set port err!\n");
		return -1;
	}

    /* 创建一个读线程 */
	iRet = pthread_create(&tid, NULL, my_read_thread_func, (void *)fd);
	if (iRet)
	{
		printf("pthread_create err!\n");
		return -1;
	}
    
    

	printf("Enter a char: ");
    initTermios(1);
    
    // 写线程
	while (1)
	{		
        c = getchar();
		iRet = write(fd, &c, 1);
		if (iRet != 1)
			printf("can not write data\n");
	}

    resetTermios();

	return 0;
}


4. 上机实验

编译2个版本:PC、ARM

gcc -o serial_send_recv_pc serial_send_recv.c -lpthread
arm-buildroot-linux-gnueabihf-gcc -o serial_send_recv_arm serial_send_recv.c -lpthread

使用USB线连接板子的OTG口、PC的USB口,PC上监测到USB串口后把它连接到VMWare,确定:

  • 开发板上有设备节点:/dev/ttyGS0
  • Ubuntu上有设备节点:/dev/ttyACM0

测试:

  • 在Ubuntu上执行:sudo ./serial_send_recv_pc /dev/ttyACM0
  • 在板子上执行:sudo ./serial_send_recv_arm /dev/ttyGS0
  • 双方即可互发数据

PC上监测到USB串口后把它连接到VMWare,确定:

  • 开发板上有设备节点:/dev/ttyGS0
  • Ubuntu上有设备节点:/dev/ttyACM0

测试:

  • 在Ubuntu上执行:sudo ./serial_send_recv_pc /dev/ttyACM0
  • 在板子上执行:sudo ./serial_send_recv_arm /dev/ttyGS0
  • 双方即可互发数据

致谢



以上笔记源自韦东山老师的视频课程,感谢韦老师,韦老师是嵌入式培训界一股清流,为嵌入式linux开发点起的星星之火,也愿韦老师桃李满园。聚是一团火,散是满天星!

在这样一个速食的时代,坚持做自己,慢下来,潜心琢磨,心怀敬畏,领悟知识,才能向下扎到根,向上捅破天,背着世界往前行!
仅此向嵌入行业里的每一个认真做技术的从业者致敬!



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值