管目前工业以太网已经相当普及,但在工控领域仍然存在大量使用UART通过RS485和RS422组网的设备和控制器,导致含有多UART的嵌入式系统仍有较大市场需求。意法半导体和兆易创新等主流微控制器(MCU)厂商都有10个以上UART的器件,但在很多场景下仍然无法覆盖所有应用场景。另外,对于主控单元是微处理器(MPU,能运行Linux)的嵌入式系统,UART口一般较少,就不得不使用16C550/16C554这类扩展芯片来实现多UART。
用FPGA来实现可定制的多UART口扩展是一种不错的解决方案。其中,在Zynq上通过AXI总线扩展多个UART难度不高,但限制了主控单元的使用,且成本较高。我趁着这个国庆长假在低成本的国产FPGA/CPLD上实现了一种基于FSMC接口(GD32上称为EXMC接口)的多串口控制器,适用于所有兼容ISA总线接口的嵌入式MCU/MPU系统。可以根据应用场景任意裁剪UART的个数、FIFO缓冲的深度。
以下原创内容欢迎网友转载,但请注明出处: https://www.cnblogs.com/helesheng
欢迎大家在本人的github仓库下载验证:https://github.com/helesheng/uart_fsmc_fifo
回到顶部
一、整体结构
我设计的多UART控制器采用外设地址映射模式思路,FPGA模拟成一个8位的FSMC设备和处理器(如STM32)通信。其中,每个UART口占用数据接收寄存器地址(UARTx_RX_Reg_Addr,小写x可以代表任意串口编号的阿拉伯数字,下同)、接收缓冲长度寄存器地址(UART1_RX_LEN_Reg_Addr)、数据发送寄存器地址(UARTx_TX_Reg_Addr)以及发送缓冲长度寄存器地址(UARTx_TX_LEN_Reg_Addr)共四个字节的地址;另外,每个UART口还占用了发送状态寄存器(STAT_TX_Addr)和接收状态寄存器(STAT_RX_Addr)中的各一个位,来表示该UART当前是否处于发送或接收状态。
其中,每个UART的数据发送和接收寄存器在逻辑概念上虽然只占用的一个字节的地址,但在物理上却映射到一组接收FIFO和发送FIFO的一个端口。以接收FIFO为例:每当UART口收到一个字节的数据A时,FPGA就会将该数据压入接收FIFO中,而处理器也不需要立即将刚收到的数据读走,因为即使处理器没有及时地在下次收到数据B之前读走该数据,FIFO还是能够缓存这个新数据。而当处理器腾出时间处理该UART口的接收数据时,还是能从同一个地址(UARTx_RX_Reg_Addr)按照收到这些数据的顺序A->B->C....依次读取他们。这种方式的优势有二:
1、相对于嵌入式处理器,UART属于低速设备,对于接收而言,增加了FIFO缓冲的UART控制器无需处理器过于频繁的查询,只需要间隔相当一段时间查看一下FIFO中是否缓存了数据即可;对于发送而言,增加了FIFO缓冲的控制器也可以一次缓冲多个需要发送的字节,降低处理器关注的频率。
2、发送和接收缓冲分别都只占用一个字节的地址空间,即可实现任意长度数据包的收发。当然,代价是处理器只能顺序访问缓冲区,而无法实现随机访问,但这对于需要严格按顺序读写的UART口而言,这并不是问题。
下面的代码是我在作为处理器的STM32代码中定义的UART相关寄存器地址:
UART寄存器地址(STM32侧)
FPGA代码中定义的UART相关寄存器地址:
UART寄存器地址(FPGA侧)
回到顶部
二、FPGA中八位并行接口(FSMC)设备端的实现
1、读写时钟控制信号的产生
下图是我在STM32手册中截取的配置成SRAM的读写模式时FSMC接口的读写时序。
image
图1 FSMC(SRAM模式)读时序
image
图2 FSMC(SRAM模式)写时序
对于100pin的STM32而言,并不拥有全部26根地址线,而只有A16-A23共8根,以及一根自动产生的片选线NCE1/NCE2(控制地址范围达到全部4G寻址空间的四分之一)。
FPGA侧为了实现FSMC的设备端接口,核心要求是能够在写使能NWE和读使能NOE控制下在FPGA中实现数据的锁存和输出。有两种合理的技术路线:其一,使用NWE或NOE作为数据寄存器的锁存时钟;其二,用FPGA系统时钟作为数据寄存器的锁存时钟,NWE或NOE作锁存器读写的使能信号。
第一种思路最直接,但问题是缓冲FIFO的存储时钟只能由STM32的读、写动作产生,但一般FIFO IP在真正进行读写之前也需要时钟信号来完成初始化。如果采用第一种技术路线,势必需要STM32作几组无意义的“空读写”,以产生必要的初始化时钟。另外,也可能由于STM32到FPGA之间的PCB走线造成时序约束难度的增加,并提升外部高频信号干扰的可能,因此我没有选择这条技术路线。第二种思路的难点在于NWE或NOE信号低电平期间,可能产生多个FPGA的系统时钟,从而造成对同字节数据被缓冲FIFO看做多个数据,进而被多次读或写。
为实现第一种技术路线,我用如下代码将长度不确定的NWE或NOE信号转换为一个长度为1个系统时钟的正脉冲作为缓冲FIFO的读写使能控制信号。完美的解决了单次STM32读写造成多次缓冲FIFO读写的问题。
读端口使能控制信号rd_uart_rx_fifo_1ck
上述代码的核心思路是检测NWE或NOE信号上出现的下降沿(此处被语句:assign en_fsmc_oe = (fsmc_a[21] == 1'b0) && (rd_n == 1'b0) && (cs_n == 1'b0);变成了en_fsmc_oe和rd_uart1_rx_fifo信号的上升沿),方法是用移位寄存器缓冲两个FPGA系统时钟有效边沿时刻的信号,并用异或判断二者是否相反,最后用与运算判断是上升沿还是下降沿。这种方法有效的避免了NWE和NOE持续时间超过一个FPGA系统时钟周期,以及上升、下降沿混淆的问题。但应注意:由于STM32产生的NWE或NOE信号与FPGA系统时钟不同步,wr_data_1ck信号上持续的1个时钟周期的高电平最早可能开始于NWE或NOE下降沿出现的瞬间,最迟可能开始于NWE或NOE下降沿出现一个时钟周期之后——因此由图1可知,读数据建立DATAST应该大于1或2(@72MHz HCLK)。
2、双向数据线数据输出及高阻态控制
其核心思想是设计一个组合逻辑数据多路器,将不同地址输出的数据连接到out_data[7:0],并用输出使能信号en_fsmc_oe实现高阻态控制。
读端口输出电路设计
3、收、发缓冲FIFO的实现
我使用了安路低成本的小精灵2(EF2系列)价格最低的一款CPLD/FPGA来实现我的设计,下图是TD(Tang Dynasty)开发环境中FIFO的图形化配置界面,每扩展一个深度为256字节的UART需要2个这样的FIFO各(分别作发送和接收缓冲)。选择用片上的9K块RAM资源来实现,而没有选择用LE中的DRAM(离散RAM),原因是为了降低应用对宝贵LE资源的占用,但由于258*8的结构其实只使用了每个EMB9K中RAM资源的四分之一,后续复杂应用还有较大的缓冲深度升级余地。对资源最少的EF2L15而言,由于只有6个EMB9K,最多只能实现3个UART口。若需更多UART,需要选择更大的可编程芯片。
image
图3 256字节的缓冲FIFO
下面是发送和接收FIFO的例化代码。
接收和发送缓冲FIFO例化
4、UART模块的实现
TD自带UART的IP,配置界面如下图所示。其中模块时钟为50MHz,不建议大家为了省电使用更低频率:实验显示使用低频时钟后,UART连续接收多个字节数据包时可能出现少量字节识别错误。
image
图4 UART模块配置
下面是UART的例化代码。
例化UART IP代码
该模块收、发时序如下图所示。
image
图5 UART模块接收数据时序
image
图6 UART模块发送数据时序
正确完成一个字节数据的接收后,模块将在rx_vld上产生1个时钟周期的高电平,正好用于接收缓冲FIFO的写使能信号,嵌入式处理器可以在合适的时间从接收FIFO中读取之前收到的数据。发送控制信号tx_en的生成逻辑比较麻烦,它需要在发送FIFO非空的情况下不断产生长度为1个时钟周期的正脉冲。我使用一个有限状态机FSM来产生所需的读取发送缓冲FIFO的信号和使能UART发送的tx_en信号。
读取发送缓冲和UART发送使能控制状态机
缺省状态为空闲状态WAIT_STATE,只有当UART发送完成(uart_tx_rdy_wire为1)且发送缓冲FIFO中有数据时(uart_tx_fifo_empty为0),状态机才进入读取发送缓冲FIFO状态(READ_FIFO_STATE),以及启动UART发送状态(WRITE_UART_REG_STATE),而后两个状态都只固定的停留1个时钟周期。对应的标志uart_tx_fifo_rd_en_1ck和uart_tx_en_1ck则被分别连接到了前文提到的发送缓冲FIFO模块和UART模块。
5、状态寄存器内容的更新
状态寄存器中可以存放任何UART控制器想与嵌入式处理器沟通的内容。为防止不同状态位延迟不同造成的竞争和冒险,我采用一组寄存器来锁存状态,锁存时钟就是系统时钟。
UART状态寄存器的实现
其中PERI_UART_RX_STATE[7:0] 等寄存器地址,请参考本文开始阶段的定义。
6、FPGA时序约束
本文所述电路系统频率仅为50MHz,且作为UART模块外部IO接口频率在1MHz以下,基本不需要太严格的时序约束。我只对输入10MHz时钟和PLL派生出的50MHz时钟进行了简单约束如下:
时钟约束
回到顶部
三、嵌入式控制器代码的实现
如果选择STM32作为嵌入式控制器,则需选择100pin以上的型号,方具备FSMC接口,起初始化代码如下。
FSMC初始化代码
我在STM32中移植了RTOS,每个任务负责管理一个UART,定时查询接收状态寄存器STAT_RX_Addr在有数据时不断读取接收FIFO,随后将收到的数据再发给同一个串口。这样用PC和串口调试助手就可以观察每个串口的收发功能是否正确了。
管理单个UART口的任务代码举例
回到顶部
四、总结
为了验证功能的正确性,我在低成本的EF2L15上,用上述方法实现做了两个UART(每个UART约占用400-500个LE之外,还占用2个EMB9K),分别不间断的用两个UART口分别运行modbus协议栈通信,和读取传感器数据测试,目前为止未发现问题。