APP下载

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

2023-05-30王维

电子产品世界 2023年1期
关键词:单片机

王维

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

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

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

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

1信号连接框图

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

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是整个单片机程序的概要图。

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() 定义如下:

如上代码,串口中断处理函数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 为环形缓冲的字节大小,当读取完一个字节,读指针前进为例,代码如下:

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

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

4 USB 数据收发程序

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

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(),其定义如下:

其中参数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 程序框架中被调用。其定义如下:

其中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个读缓冲构成,数据结构如下:

结构体成员 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定义如下:

程序接收到类控制传输请求时,根据 req->bm  RequestType判断当前数据传输方向 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 端口从主机读取数据发送给指定的串口。中间的数据传输采用特定的数据格式对串口数据进行封装,标记同步头,串口编号,字节数。本方案采用的数据包格式如下:

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

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

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

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

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

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

6命令处理程序

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

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的数据结构如下:

typedef struct _all_room{

uint8_t chn_num;

uint16_t room[5];

}all_room_t;

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

7传输验证

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

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

int libusb_control_transfer(libusb_device_handle *dev_handle,

uint8_t request_type, uint8_t bRequest, uint16_t wValue, uint16_t wIndex,

unsigned char *data, uint16_t wLength, unsigned int timeout);

int libusb_bulk_transfer(libusb_device_handle *dev_handle,

unsigned char endpoint, unsigned char *data, int length,

int *actual_length, unsigned int timeout);

其中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()获取各个串口的剩余发送缓冲空间,测试程序根据单片机串口剩余发送缓冲空间确定当时能发送的字节数。本方案设计单片机串口最大发送缓冲大小为1024个字节,最高波特率为115200,根据最高波特率串口最高发送速度为 115 200/10等于 11 520 字节 /s,根据以上参数可知最大缓冲填满时间为 1 024/11 520 ≈ 88 ms,因此本方案轮询单片机串口剩余发送缓冲时间间隔为 80 ms即可。

测试程序运行命令如下 :

./mux-test -b 115200 -f ./origin.txt -s /dev/ttyUSB0-4

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。当程序运行完,运行以下命令比较文件:

for i in $(seq 0 4); do diff -s ./origin.txt ./tty/$i.txt ; done; \

for i in $(seq 0 4); do diff -s ./origin.txt ./usb/$i.txt ; done

以下是运行结果 :

Files ./origin.txt and ./tty/0.txt are identical

Files ./origin.txt and ./tty/1.txt are identical

Files ./origin.txt and ./tty/2.txt are identical

Files ./origin.txt and ./tty/3.txt are identical

Files ./origin.txt and ./tty/4.txt are identical

Files ./origin.txt and ./usb/0.txt are identical

Files ./origin.txt and ./usb/1.txt are identical

Files ./origin.txt and ./usb/2.txt are identical

Files ./origin.txt and ./usb/3.txt are identical

Files ./origin.txt and ./usb/4.txt are identical

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

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 Revision 1.0[G].

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

[5] USB in a Nutshell[G].

猜你喜欢

单片机
基于单片机的SPWM控制逆变器的设计与实现
基于单片机的层次渐变暖灯的研究
基于单片机的多功能智能插排
基于单片机的便捷式LCF测量仪
小议PLC与单片机之间的串行通信及应用
MSP430单片机在仿真中要注意的几点问题
基于单片机的平衡控制系统设计
基于单片机的三维LED点阵设计
Microchip推出两个全新PIC单片机系列
基于Proteus的单片机控制系统的仿真设计