基于GD32F305的多串口扩展模块设计

  作者:王维(新诺北斗航科信息技术(厦门)股份有限公司,厦门 361022) 时间:2023-01-18来源:电子产品世界

编者按:探讨船舶导航仪对多串口的需求,提出一种用GD32F305单片机扩展多串口的方案,该方案采用USB通信。以RK3128主板为例介绍该扩展方案的硬件连接,接着探讨了单片机程序的具体实现,最后介绍用libusb进行数据传输验证。

船用很多电子设备是通过RS-422 串口传输数据,比如导航仪通过RS-422 串口传输NMEA - 0183数据,这些数据包括定位信息,导航信息,船艏信息,雷达信息等。这就要求船上的显示终端需要有很多的串口来接收和发送数据。导航仪主板上的SoC 芯片原生串口数量有限,有时不能满足用户需要,这就需要外接模块来扩展串口。USB 总线连接简单,信号只需要一对差分数据线传输,全速传输模式下带宽可达12 Mbps;常见的船舶电子设备,其中RS-422最高传输需求波特率为115 200 bps,USB全速模式下传输率超过其100倍。USB总线扩展多个串口具有连线简单,传输率高的优势,适合做多串口扩展。

本文探讨的扩展模块基于单片机GD32F305设计,采用USB 总线扩展最多5 路串口。GD32F305是兆易创新公司出品的一个单片机系列,该系列单片机有一路USB总线,5 路串口,CPU 核心采用Cortex-M4,可以运行在120 MHz,功能和性能均可满足设计要求。

1   信号连接框图

本文以一款RK3128 导航仪主板为例,探讨串口扩展方案。图1 是主板信号连接图,为重点说明扩展方案,信号只保留USB 和串口部分。

1674012378293758.png

图1 主板信号连接

RK3128 是瑞芯微出品的ARM Cortex-A7 4 核处理器,RK3128有3 个原生串口,其中串口2 和SD接口复用,实际可用的原生串口只有2 个, 不够连接外部设备,因此采用本文所述方案扩展串口。如上图RK3128 有1 个USB OTG, 和1 个USB HOST 接口, 其中USB OTG用于其它通用外设( 如U 盘,鼠标) 和引导镜像烧写,USB HOST 接口连接GD32F305RB 扩展串口;GD32 的5 个串口全部引出用于连接其它船用电子设备。

GD32F305 系列单片机,CPU 核心采用Cortex-M4,最大运行频率为120 MHz,内置最少64 KB SRAM,最少128 KB FLASH,包含1 个USB OTG端口,5 个串口(3个USART 和2 个UART,5 个串口支持最高9 Mbit/s波特率) 及其他丰富的外设资源。

2   单片机程序

兆易创新提供了GD32F305 的固件库,其中包含工程模板,启动程序,丰富的外设调用程序及范例程序,并且还有用于简化USB 固件程序设计的USB 程序框架。为加速开发过程,本方案充分利用了固件库,并参照其中的USB CDC 范例代码, 以USB 程序框架为基础设计了USB 通信程序。单片机程序包括串口收发程序,USB 收发程序,数据转发及命令处理程序,图2 是整个单片机程序的概要图。

1674012606996654.png

图2 单片机程序概要

3   串口收发程序

串口收发封装为以下函数:

void uart_init();

int uart_read(int chn, void*dat, int size);

int uart_write(int chn, constvoid* dat, int size);

int uart_ioctl(int chn, intcmd, void * args);

uart_init() 为串口初始化函数,用于初始化所有用到的串口,主要包括收发缓冲初始化,串口引脚功能初始化,功能寄存器初始化,中断初始化。

uart_read() 为串口接收函数,chn 为串口编号, dat 为接收数据缓冲指针,size 为数据缓冲的字节数,返回值为实际读取到的字节数。

uart_write() 为串口发送函数,chn 为串口编号,dat为要发送的数据指针,size 为要发送的数据字节数,返回实际写入串口发送缓冲的字节数。

uart_ioctl() 用于响应控制命令,chn 为串口编号,cmd 为命令编号,args 为命令参数,返回值根据不同命令定义。uart_ioctl() 主要处理串口波特率设置,回应当前对应串口发送缓冲字节数这两个功能。

uart_read() 和 uart_write() 都是非阻塞设计,都是对相应的串口收发缓冲操作,实际数据收发是在中断函数中处理。串口的中断处理函数uart_irq_handle() 定义如下:

image.png

如上代码, 串口中断处理函数uart_irq_handle()调用了固件库串口函数usart_interrupt_flag_get() 来判断当前串口是否触发了接收和发送中断,usart_data_receive() 用于从当前串口接收寄存器读取接收到的数据,usart_data_transmit() 用于将1 个字节的数据写入当前串口的发送寄存器发送数据。bfifo_in_byte() 和bfifo_out_byte() 是一种环形缓冲bfifo 的操作函数,bfifo_in_byte() 用于将1 个字节数据写入缓冲,bfifo_out_byte() 用于从缓冲读取1 个字节数据,成功读取返回true,如果缓冲无数据则返回false。

串口一次收发字节数不固定,环形缓冲很适合这种中断处理随机字节数据流的收发。环形缓冲是一种有固定存储空间的数据结构,有读、写两个指针,读取缓冲时只操作读指针,不会修改写指针;往缓冲写入数据时只操作写指针而不会修改读指针,环形缓冲的这种指针操作机制使得操作指针时不需要对指针做中断互斥保护,因此不需要在收发数据时关闭开启中断。

环形缓冲的操作,要将指针的操作限定在环形缓冲大小之内,一般可以采用取模运算,比如以f->ptr_out为环形缓冲的读指针,f->size 为环形缓冲的字节大小,当读取完一个字节,读指针前进为例,代码如下:

image.png

本方案采用的bfifo 参照linux kernel 的kfifo, 在上述基础上优化了指针的操作,将环形缓冲的大小限定为2 的n 次方,n 为整数,将取模操作用与运算替代以加速计算过程。同上述例子一样的操作,设f->mask=f->size-1,代码如下:

image.png

由于串口接收到数据后,中断处理函数将数据保存到了环形接收缓冲中,uart_read() 函数只需要从环形接收缓冲将数据读出保存到形参;uart_write() 则将形参指向的数据写入到相应的环形发送缓冲中,并判断当前串口发送中断是否关闭,如果发送中断关闭则重新打开,单片机将触发发送中断,发送环形发送缓冲的数据。

4   USB 数据收发程序

USB 数据收发程序封装为以下函数:

image.png

usb_init() 为初始化函数,主要初始化USB 端口,USB 程序框架用到的定时器,USB 中断,各种USB 描述符等。

usb_write() 为USB 数据送函数,负责将数据通过bulk 端点发往主机,dat 为要发送的数据指针,size 为要发送的数据字节数,返回实际发送的字节数。

usb_read() 是USB 数据读取函数,负责读取从主机发送往bulk 端点的数据,dat 为数据接收缓冲指针,size 为缓冲字节数,返回值为实际读取到的字节数。

usb_set_class_callback() 用于设置USB Class 请求回调,callback 为回调函数,callback 的参数wIndex,bRequest,wValue,wLength 对应USB 标准控制传输的相应参数,dat 为数据缓冲指针,程序将bRequest 作为请求命令,当wLength > 0 时,程序根据bRequest 内容读取dat 或往dat 写数据。

USB 数据收发程序相比串口数据收发程序复杂很多,因此本方案借助兆易创新的USB 程序框架来简化设计。USB 程序框架实现了基本的USB 传输,调用固件库提供的USB 设备初始化函数,设置好相应的回调程序指针和USB 描述符,可快速实现基本的USB 数据传输。

固件库USB 设备初始化函数为 usbd_init(),其定义如下:

image.png

其中参数 udev 为 USB 驱动句柄指针,usbd_init 将初始化其数据结构,之后程序操作 USB 设备将用到该句柄。

参数 core 为 USB 设备驱动核心枚举。USB 固件库支持 USB 全速和 USB 高速设备,core 用来指示这两种类型设备的其中 1 种。(全速设备带宽为 12 Mbps,可满足设计,本方案实现的是全速设备;高速设备的带宽为480 Mbps,实现高速USB设备,需要外加ULPI芯片。)

参数desc 为USB 描述符指针,desc 定义了设备描述符、配置描述符、接口描述符等。这些描述符用来描述USB 设备的属性和用途。主机会在枚举设备时获取以确定设备是什么样的设备,需要的总线资源,通讯方式等。

参数class_core 为USB 类结构体,该结构体定义了USB 类的初始化、反初始化、类请求、数据收发等函数指针,程序在初始化时设置好这些指针,这些指针将在USB 程序框架中被调用。其定义如下:

image.png


image.png其中参数udev 为USB 驱动句柄指针,usbd_init 将初始化其数据结构,之后程序操作USB 设备将用到该句柄。参数core 为USB 设备驱动核心枚举。USB 固件库支持USB 全速和USB 高速设备,core 用来指示这两种类型设备的其中1 种。( 全速设备带宽为12 Mbps,可满足设计,本方案实现的是全速设备;高速设备的带宽为480 Mbps,实现高速USB设备,需要外加ULPI 芯片。)

image.png

其中init 为初始化函数指针,当USB 连接时该指针指向的函数被调用,程序可在初始化函数中分配端点,初始化收发缓冲等;deinit 为反初始化函数指针,USB 连接断开时被调用,程序要在这里释放资源;req_proc 为设备请求函数指针,用于处理端点0 控制传输,当主机通过端点0 请求传输时,该指针指向的函数被调用,本方案在这里响应类请求,处理串口波特率设置和串口缓冲大小获取;data_in 是处理data in 传输的函数指针,当主机向USB 设备请求数据时,该指针指向的函数被调用,程序在这里准备好要发往主机的数据;data_out是处理data out 传输的函数指针,当主机往USB 设备发送数据时,该指针指向的函数被调用,程序在这里接收主机下发的数据。

分析USB 程序框架,USB 数据传输采用DMA,1次可能传输多个字节数据;data_in 和data_out 都是在中断处理程序中被调用,因此本文案设计一种环形缓冲加双缓冲的方案来提高数据传输效率。环形缓冲用于避免变量互斥冲突,而双缓冲用于提高DMA 传输效率。

上述双缓冲,由1 个写缓冲和1 个读缓冲构成,数据结构如下:

image.png

结构体成员buffer 为内存缓冲,buf_len 为双缓冲的字节数,程序分配双缓冲时,分配buffer 空间为双倍buf_len 字节数;index 为数据索引,用于指示当前读写缓冲的地址;len 为当前写缓冲的数据字节数。

当程序往双缓冲写数据时,先获取写缓冲的地址,写缓冲的地址为buffer+index*buf_len,再将数据写入写缓冲的末尾,地址为buffer+index*buf_len+len,之后再根据数据大小累加len。

当程序要读取双缓冲数据时,程序先读取当前写缓冲的字节数,获取当前写缓冲的内存地址,再对双缓冲做一次数据缓冲翻转,将原来的读写缓冲互换。双缓冲的翻转,重点是对index 进行反运算,index = !index。当DMA 完成一次传输时,程序可以快速翻转双缓冲,将读写缓冲地址交给DMA控制器进行下一次数据传输。如此可达到减少DMA 控制器等待时间的目的,以提高数据传输效率。

关于往bulk 端点发送数据,本方案定义了一个前文所述的环形缓冲fifo_bulk_in 和双缓冲dbuf_bulk_in 来缓存数据,程序通过调用usb_write() 函数完成。usb_write() 主要负责将形参数据写入fifo_bulk_in,并检测当前USB 框架是否正在传输数据,这个状态由变量is_bulk_in_busy 表示, 如果还未启动数据传输,则取出环形缓冲fifo_bulk_in 的数据转存至dbuf_bulk_in,翻转dbuf_bulk_in,并调用固件库函数usbd_ep_send() 启动一次DMA 传输。当单片机完成一次传输,USB 框架会调用回调函数data_in(),此时根据data_in() 传入的端点号,判断端点号为bulk 端点准备bulk 数据发送。检测fifo_bulk_in 是否有数据和上次传输的字节数是否为空,函数根据以下几种情况处理:

如果fifo_bulk_in 有数据,则和上述usb_write() 检测到未启动传输时一样,取fifo_bulk_in 数据转存至dbuf_bulk_in,翻转dbuf_bulk_in,再次发起一次DMA传输。

如果fifo_bulk_in 无数据,则发起一次0 数据传输以表示当前传输完成当fifo_bulk_in 无数据,且上次是0 数据传输时,则将is_bulk_in_busy 变量设置为false,表示USB 程序框架已停止bulk 数据发送bulk 端点数据接收也采用了一个环形缓冲和一个双缓冲来缓存数据,分别用变量fifo_bulk_out 和dbuf_bulk_out 表示。

当程序调用usb_read() 时,先从fifo_bulk_out 中取数据存储到形参接收缓冲,接着检查当前bulk 端点是否正在接收数据,该状态用is_bulk_out_busy 表示,当is_bulk_out_busy 值为false 时调用固件库函数usb_ep_recv(),发起DMA 传输将数据存至dbuf_bulk_out,并将is_bulk_out_busy 值设置为true。

当bulk 端点接收到数据时,USB 程序框架调用data_out(),此时取出dbuf_bulk_out 的接收缓冲指针和接收数据字节数。先判断fifo_bulk_out 剩余空间是否大于bulk 端点最大传输量,如果空间足够则翻转dbuf_bulk_out 并调用usb_ep_recv() 发起下一次DMA 传输;否则设置is_bulk_out_busy 值为false,表示bulk 端点接收空闲。最后通过之前暂存的dbuf_bulk_out 接收缓冲指针和接收数据字节数将本次传输接收到的数据转存到fifo_bulk_out 完成本次bulk 端点数据接收处理。

当主机向单片机请求类的控制传输时,USB 程序框架将调用回调函数req_proc,请求的内容从req_proc 的参数req 获得,req 的类型usb_req 定义如下:

image.png

程序接收到类控制传输请求时,根据req->bmRequestType 判断当前数据传输方向s,如果是IN 类型的传输,则调用前文所述usb_set_class_callback() 设置的回调函数,传递req 的其它参数,如果req->wLength不为0,从回调函数读取数据到全局变量ctlbuf 准备将数据回传给主机。将ctlbuf 和req->wLength 传递给USB程序框架,USB程序框架将发送数据和状态给主机。当数据发送完成时,USB 程序框架调用前文所述data_in 通知程序,程序设置调用API 通知USB 程序框架无剩余数据,完成本次控制传输请求。

如果当前数据传输类型是OUT 传输时,则先判断req->wLength 是否为0,如果req->wLength 为0 时,直接调用前文所述usb_set_class_callback() 设置的回调函数即可。当req->wLength 不为0 时,表示此次控制传输附带数据,此时程序先用全局变量last_req 暂存req 值,然后调用API 通知USB 程序框架将把此次传输的数据保存到ctlbuf。当USB 程序框架接收完此次传输的数据,将调用前文所述的data_out 通知程序,这时程序将传递上述last_req 变量及ctlbuf 通知前文所述usb_set_class_callback() 设置的回调函数。

5   数据转发程序

数据转发程序负责将所有串口的数据通过USB 端口转发到主机,同时通过USB 端口从主机读取数据发送给指定的串口。中间的数据传输采用特定的数据格式对串口数据进行封装,标记同步头,串口编号,字节数。本方案采用的数据包格式如下:

image.png

其中sync 为同步头,固定为两个’$’字符,用于解析时找到数据包的起始位置;chn 为串口编号,对应收发数据的串口;len 为数据字节数,用于表示后面dat 的实际大小;dat 为实际收发的数据,此处定义的数组大小不作为实际数据大小。

本方案定义了函数mux_pack_data() 用于封装串口数据,其声明如下:

image.png

其中dst 为数据缓冲地址,用于存放封装好的数据包;chn 为串口编号;dat 为要传输的数据;len 为上述dat的数据字节数;返回封装后的数据字节数。

数据转发程序的串口接收部分主要操作为,逐一读取各个串口的数据,调用mux_pack_data() 将数据封装成一个个数据包存储至临时缓冲out_buf,最后调用前文所述USB 收发程序的发送函数usb_write() 将out_buf 的数据发给主机。

串口发送部分操作为,调用usb_read() 函数从主机读取数据并解析,根据解析的数据包调用uart_write()往对应的串口发送数据。解析函数为mux_parse_data(),其声明如下:

image.png

其中src 和len 为原始数据缓冲指针及数据大小;callback为回调函数;回调函数的参数chn 表示串口编号,dat为数据缓冲指针,len 为数据字节数。这里将从usb_read() 读取到的数据传入参数src 和len,当mux_parse_data() 解析到数据包,将通过callback 通知,此时程序将调用uart_write() 将数据发往指定串口。

6   命令处理程序

命令处理程序主要负责响应主机设置串口波特率,获取串口写缓冲的请求。这些请求通过USB 控制传输的类请求来处理,程序通过上文所述usb_set_class_callback() 设置类请求回调函数。类请求回调函数声明如下:

image.png

bRequest 用于表示请求的命令,wIndex 表示串口编号,wValue 根据bRequest 不同用于表示设置的值,dat和wLength 用于当前请求需要补充的数据。

用宏定义表示请求的命令,REQ_SET_BAUD,REQ_ALL_UART_WRITE_ROOM 分别表示设置串口波特率,请求所有串口的剩余写缓冲空间。

当主机请求设置串口波特率,handle_class_request将被调用,bRequest 值为REQ_SET_BAUD,wValue为波特率除以100 的值(以9600 为例,wValue 值为96),wIndex 表示串口编号,此时程序调用前文所述串口函数uart_ioctl() 设置编号为wIndex 串口的波特率为wValue×100。

当主机请求所有串口写缓冲时,bRequest 值为REQ_ALL_UART_WRITE_ROOM, 程序调用uart_ioctl() 逐一获取每个串口的剩余写缓冲空间,写至dat参数。传递至dat 的数据结构如下:

image.png

其中chn_num 为串口数量,room 为各个串口的剩余写缓冲字节数,每个串口的剩余写缓冲字节数用2 个字节的类型uint16_t 表示。

7   传输验证

本方案USB 数据传输采用libusb 编写测试程序在LINUX 系统下验证。libusb 是一个在应用层调用的跨平台USB 库,包含了USB 传输所需要的API。相比在编写内核驱动来验证本方案的数据传输,采用libusb 更快捷,更方便调试。

本方案验证传输采用了libusb 中比较方便调试的同步I/O API,声明如下:

image.png

其中libusb_control_transfer() 用于发起控制传输,参数dev_handle 为USB 设备句柄,data 为补充数据的指针,timeout 为超时毫秒数,其它参数对应控制传输USB 的标准定义。函数返回传输状态,成功传输返回枚举值LIBUSB_SUCCESS。

libusb_bulk_transfer() 用于发起bulk 传输,dev_handle 为USB 设备句柄,endpoint 为bulk 端点编号,data 为接收或发送的数据缓冲指针,length 为数据字节数,actual_length 为实际传输的字节数,timeout 为超时毫秒数。

7.1 本方案主机接收据传输验证,主要流程如下:

1)调用libusb_open_device_with_vid_pid(),根据设备vid,pid 打开USB 设备s

2)通过 libusb_control_transfer() 设置各个串口的波特率

3)通过电脑串口上发送测试文件到单片机的串口

4)电脑测试程序通过libusb_bulk_transfer() 从单片机USB 口读取数据,通过转发程序定义的协议解析数据包,根据串口号不同将数据分别存储到不同的文件

5)对比发送和接收到的文件

7.2 主机发送数据传输验证,流程如下:

1)在电脑测试程序通过 libusb_control_transfer()设置各个串口的波特率

2)电脑测试程序加载测试文件,通过转发程序定义的协议将文件数据按各个串口封装数据包,通过libusb_bulk_transfer() 往单片机USB 口发送数据

3)通过电脑串口从单片机接收数据并根据不同串口保存到不同文件

4)对比发送和接收到的文件由于USB 传输速度远高于本方案串口,为避免缓冲溢出导致数据丢失,主机发送数据时需要根据串口最大发送缓冲大小和设计的最高波特率定时通过libusb_control_transfer() 获取各个串口的剩余发送缓冲空间,测试程序根据单片机串口剩余发送缓冲空间确定当时能发送的字节数。本方案设计单片机串口最大发送缓冲大小为1 024 个字节,最高波特率为115 200,根据最高波特率串口最高发送速度为115 200/10等于11 520 字节/s,根据以上参数可知最大缓冲填满时间为1 024/11 520 ≈ 88 ms,因此本方案轮询单片机串口剩余发送缓冲时间间隔为80 ms 即可。

测试程序运行命令如下:

image.png

mux-test 为测试程序,-b 选项为波特率,这里设置为 115200;-f 选项为测试数据文件名,这里为origin.txt,origin.txt 为64kB 的文本文件;-s 为电脑的串口 tty 设备名,这里/dev/ttyUSB0 - 4 表示加载 /dev/ttyUSB0、/dev/ttyUSB1、…… /dev/ttyUSB4。

程序运行后,将从电脑串口读取到的数据存储到./tty 目录下,文件根据串口编号命名,为0.txt、1.txt、……4.txt;从USB 读取到的数据存储到./usb 目录下,文件根据单片机串口编号命名,为0.txt、1.txt、…… 4.txt。当程序运行完,运行以下命令比较文件:

image.png

以下是运行结果:

image.png

根据结果可以判断出收发的数据完全一致。

8   结束语

本文首先分析了船舶导航仪对多串口的需求,并提出USB 扩展多串口的方案,分析该方案的可行性及便利性,并提出兆易创新的GD32F305 单片机来实现这一方案。接着以RK3128 主板为例介绍该扩展方案的硬件连接,然后探讨了单片机程序的具体实现,最后介绍用libusb 进行数据传输验证。

参考文献:

[1] GD32F305xx Datasheet Rev1.3[G].

[2] GD32F30x_User_Manual_Rev2.8.pdf[G].

[3] USBFS/HS Firmware Library User Guide Revision1.0[G].

[4] Firmware Library User Guide Revison 1.0[G].

[5] USB in a Nutshell[G].

(本文来源于必威娱乐平台 杂志2023年1月期)

image.png

image.png


image.png



image.png


image.png


image.png


1 2 3 4

关键词: 串口扩展 GD32 单片机 USB 202301

加入微信
获取电子行业最新资讯
搜索微信公众号:EEPW

或用微信扫描左侧二维码

相关文章

查看电脑版