Gadget应用实例之serial


文章目录

  • Gadget应用实例之serial
  • 参考资料:
    • 一、硬件体验
    • 二、 Serial分析
      • 2.1 软件框架
      • 2.2 数据传输
        • 2.2.1 APP访问
        • 2.2.2 printk
    • 3. 编程
    • 4. 上机实验
  • 致谢

参考资料:

  • 简单记录一下Linux gadget serial的工作
  • Linux文档:Documentation\usb\gadget_serial.txt
  • UART
    • 回顾TTY的概念
    • 回顾console的概念

一、硬件体验

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

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

# modprobe g_serialg_serial gadget: Gadget Serial v2.4g_serial gadget: g_serial readyg_serial gadget: high-speed config #2: CDC ACM config# ls /dev/ttyGS0 -lcrw-rw----1 root dialout 246, 0 Jan1 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 adaptersHilbert@ubuntu22.04:~$ ls /dev/ttyACM0 -lcrw-rw---- 1 root dialout 166, 0 Mar5 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.cstatus= 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.cret = gserial_alloc_line(&opts->port_num); // drivers\usb\gadget\function\u_serial.c// 注册TTYtty_dev = tty_port_register_device(&ports[port_num].port->port,gs_tty_driver, port_num, NULL);// 注册consolegserial_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_opengs_start_io(port);// 取出out端点(对应Host来说是out, 对于板子来说就是输入)struct usb_ep*ep = port->port_usb->out;// 给out端点分配usb_requeststatus = 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_writegs_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// 把要打印的数据放入环形buffergs_buf_put(&info->con_buf, buf, count);// 唤醒内核线程wake_up_process(info->console_thread);// 内核线程gs_console_thread// 被唤醒后// 取出输入端点和它的usb_requestreq = info->console_req;ep = port->port_usb->in;// 从环形buffer得到数据、设置usb_requestxfer = 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 #include #include #include #include #include #include #include #include #include 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个数据才返回, * 但是没有数据总不能一直等吧" />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  */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 \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 -lpthreadarm-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开发点起的星星之火,也愿韦老师桃李满园。聚是一团火,散是满天星!

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