ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ZYNQ EMIO调试UART完整实验:从Vivado配置到Linux设备树

ZYNQ EMIO调试UART完整实验:从Vivado配置到Linux设备树 把ZYNQ的UART1从固定的MIO引脚挪到EMIO再通过PL逻辑自定义引脚引出——这个操作我在一个多路串口项目里折腾了整整一天才完全跑通。很多资料只讲了MIO和EMIO的区别却没有说清楚在Vivado里到底怎么配、约束怎么下、Linux下怎么变成ttyS。这篇文章就把我这次“基于ZYNQ的EMIO调试UART实验”的完整过程写出来包括硬件设计、裸机验证、Linux设备树、以及调试中踩过的坑。适合正在做ZYNQ板级调试、打算把PS串口引出到任意PL引脚、或者MIO不够用来扩展串口的同学参考。1. 为什么UART调试要绕到EMIO上MIO与EMIO的选型逻辑1.1 MIO与EMIO到底差在哪ZYNQ的PS端UART控制器物理上挂在APB总线上但它的引脚引出有两种方式通过MIO直接连到PS封装的固定引脚或者通过EMIO接口送到PL内部再由PL引脚连接到板卡外部。很多第一次接触ZYNQ的人会误以为EMIO也是一种物理引脚实际上EMIO只是PS和PL之间的一组内部接口信号它本身不直接出到芯片外部必须通过PL的IO资源做二次约束才能最终到达封装引脚。MIO和EMIO的差别可以从几个维度看对比项MIOEMIO物理路径PS直接到封装引脚PS到PL再经PL引脚出芯片引脚位置固定不可更改任意PL引脚可自由布线可用数量54个固定bank电压受PL bank和封装限制数量充裕时序灵活性固定驱动强度有限可由PL逻辑控制搭配约束灵活调整适用场景标准外设优先复用MIO被占用、需要自定义管脚位置UART控制器本身的数据通路、寄存器、FIFO、波特率发生器等逻辑都在PS内部EMIO改变的不是控制器只是物理信号的出口。这意味着软件侧的驱动、中断、寄存器操作完全不用改动你只需要在硬件设计阶段把信号引到PL引脚上就行了。这也是EMIO能成为调试串口常用方案的根本原因。1.2 哪些情况下你不得不选EMIO实际项目中选EMIO通常不是因为你喜欢绕路而是因为MIO实在不够用。MIO只有54个SD卡要占一部分QSPI Flash要占一部分GEM网口还要占一部分分给UART的固定引脚就只剩下那几个而且位置固定板级布局经常被逼着绕线。这时候把UART1通过EMIO引到PL侧一个顺手的引脚就能避开MIO的资源竞争。还有一种情况是板子要支持多种启动模式或外设可选MIO引脚被复用开关切换来切换去为了不互相干扰直接把调试串口做成EMIO在PL侧指定一个不参与外设复用的普通IO。这样无论是换启动配置还是改外设调试串口都不受影响。另外如果你在做FPGA原型验证或者需要把UART信号送进PL逻辑内部做协议分析比如抓包、转发、环路测试那就必须走EMIO。因为只有EMIO才能让PS的UART信号出现在PL的逻辑资源里你可以用逻辑分析仪IP、ILA、或者自定义模块监听这个信号。1.3 EMIO UART实验的目标我这次实验的目标很直接在Vivado里把ZYNQ PS的UART1配置成EMIO方式把tx和rx两个信号引出到PL侧的两个引脚上然后外接一个USB转串口模块让开发板通过这个“另类的串口”打印出调试信息并在Linux系统下让它作为标准串口设备使用。实验硬件并不复杂一块ZYNQ-7000系列开发板一个USB转串口小板几根杜邦线就够了。重点在于整个链路Block Design里的EMIO信号怎么设置、顶层端口怎么创建、XDC约束怎么下、裸机程序怎么跑通、Linux设备树怎么改。这一套流程搞明白以后任何需要把PS外设挪到PL引脚上的设计都能举一反三。2. 实验硬件设计从Block Design到FPGA引脚约束的完整链路2.1 在Vivado里配置PS端UART走EMIO在Vivado里创建工程以后第一步是添加ZYNQ7 Processing System IP。双击打开Re-customize IP进入PS配置界面。左侧Peripheral I/O标签页里能直接看到UART0、UART1等外设。UART1默认可能没有勾选需要打开。关键的一步是右侧的引脚模式选择不要选择MIO要把UART1的TX和RX都改成“EMIO”也就是让这两个信号通过EMIO接口进入PL。配置完以后Close并Run Block Automation生成顶层wrapper。这时候你点开Block Design里的连接图会发现ZYNQ IP右侧多出来一组名为UART1的接口信号这就是需要接出去的EMIO信号。图形界面上默认可能显示成uart1_tx、uart1_rx具体信号名取决于Vivado版本它们是PS输出/输入的复位、时钟之外的附加接口。一个容易忽略的细节是UART1的EMIO接口里除了tx和rx往往还有cts、rtn、dtr、dsr这些流量控制信号。如果你的设计只用两线串口不接流控就不需要把这些信号连出去但最好在Block Design中把它们在IP内部disable掉避免vivado综合时报未连接端口的警告。2.2 理解EMIO接口的信号与位宽ZYNQ的EMIO接口并不是一个单一的总线而是分成了很多组每组对应一类外设。UART1对应的一组位宽和信号定义在ZYNQ TRM里有明确说明。就本次实验而言重要的是tx和rx这一对信号。不过这里有一个比较绕的点在Vivado的Block Design里你看到的接口往往是usb0_port_indicator这样的宽信号或者uart1信号组内部又拆成了规范的接口名。在某些版本里uart1的tx和rx可能被封装成UART1_rtl这样的接口需要你手动右键“Make External”把整个接口引到顶层。如果你点开之后看到的是UART1_rtl_0_tx和UART1_rtl_0_rx这样的独立端口也没问题直接在顶层保留即可。从功能上看EMIO的UART信号位宽是固定的tx为PS输出rx为PS输入。其他的如cts、rts在使能硬件流控时会用到本次实验不需要保持不连接即可。2.3 顶层连接与XDC引脚约束生成块设计后右键Block Design选择Generate Output Products然后Create HDL Wrapper。这时顶层文件里会看到类似下面的端口module zynq_emio_uart_wrapper ( ... inout var_vp, ... output UART1_rtl_0_tx, input UART1_rtl_0_rx, ... );如果看不到tx/rx出现在顶层回到Block Design确认是否手动创建了外部端口。Vivado有时会把没有连接的端口自动优化掉。确保这些端口已经在顶层声明后接下来就是编写XDC约束文件。我用的两个PL引脚位置和电平标准是这样约束的set_property PACKAGE_PIN N16 [get_ports {UART1_rtl_0_tx}] set_property IOSTANDARD LVCMOS33 [get_ports {UART1_rtl_0_tx}] set_property PACKAGE_PIN N17 [get_ports {UART1_rtl_0_rx}] set_property IOSTANDARD LVCMOS33 [get_ports {UART1_rtl_0_rx}]注意端口名必须和顶层文件里的一致大小写敏感。引脚位置要根据你板卡实际连接USB转串口模块的排针来选我这里只是示例。IOSTANDARD一样要仔细看如果所在PL bank的VCCO接了1.8V就不能直接用LVCMOS33否则综合阶段可能不报错但生成比特流之后板级电路无法正常工作甚至可能损伤引脚。2.4 生成比特流前要注意的检查点在点Generate Bitstream之前我建议做三件检查第一打开Address Editor确认ZYNQ的UART1控制器地址已经映射到处理器CPU访问的地址空间里。UART1的固定物理地址是0xE0001000这一般是自动的但如果你手动编辑过地址空间容易把它搞乱。裸机工程里使用XPAR_XUARTPS_1_BASEADDR时会依赖这个地址。第二确认PS侧的UART参考时钟是否配置正确。在PS配置界面勾选UART1时会有一个输入时钟选项通常可以用IOPLL或者某个FCLK_CLK0分频后的时钟。我这里选了50MHz因为这个频率在常见的波特率分频计算下误差最小。后面Linux设备树里也要保持一致。第三也是EMIO特有的检查确认PL侧有没有被其他约束占用你要用的引脚。同一个引脚不能同时约束给两个端口Vivado会在DRC阶段直接报错。我当时就是因为某个测试点残留了旧约束导致UART引脚布不上线找了半天才在XDC里翻到问题。3. 裸机回环和Linux串口两套软件栈下的EMIO UART验证3.1 裸机工程一个最简单的字符回环硬件综合完成后我选择Export Hardware并启动SDK/Vitis。新建一个Hello World模板工程时需要指定一个BSPBSP里会包含串口驱动的支持。如果你用的是Hello World模板默认会输出到STDOUT配置的串口。因为ZYNQ平台默认的stdout通常是UART0而我们把UART1引出去了所以第一步就是把BSP设置里的stdout和stdin改成uart1。在SDK 2017.4之后的版本可以在platform.spr文件里的Board Support Package Settings里找到stdout和stdin下拉框选择uart1。当然也可以不依赖stdio直接在代码里用UART驱动函数发送。我写了一个简单的回环测试代码让开发板收到什么字符就原样发回去#include xparameters.h #include xuartps.h #include xil_printf.h static XUartPs UartInstance; int main(void) { XUartPs_Config *Config; u8 RecvBuf; int Status; Config XUartPs_LookupConfig(XPAR_XUARTPS_1_DEVICE_ID); Status XUartPs_CfgInitialize(UartInstance, Config, Config-BaseAddress); if (Status ! XST_SUCCESS) { xil_printf(UART init failed!\r\n); return -1; } XUartPs_SetBaudRate(UartInstance, 115200); XUartPs_SetOperMode(UartInstance, XUARTPS_OPER_MODE_NORMAL); xil_printf(EMIO UART1 Loopback Test\r\n); while (1) { if (XUartPs_Recv(UartInstance, RecvBuf, 1) 1) { XUartPs_Send(UartInstance, RecvBuf, 1); } } }这段代码的思路很简单先找到UART1的配置信息并初始化然后设置波特率最后在循环里收一个字节、发一个字节。如果你在串口助手里发送A能收到A那么PS端的UART控制器和EMIO链路基本没问题。这一步能验证的核心点有两个一是EMIO信号确实把UART数据通路引到了PL引脚二是引脚约束和电平标准没有问题。如果回环失败问题大概率出在硬件连接或XDC上而不是软件。3.2 Linux下的设备树配置让EMIO UART变成标准tty裸机跑通之后我继续在Linux下验证。如果你使用的是PetaLinux需要重新生成设备树和内核镜像如果手动编译则需要确认设备树中UART1节点的状态。ZYNQ Linux的设备树中UART1节点通常位于axi节点下地址是e0001000兼容字符串在新内核里一般是cdns,uart-r1p8或者xlnx,xuartps。一个典型的节点类似这样uart1: seriale0001000 { compatible cdns,uart-r1p8; reg 0xe0001000 0x1000; interrupts 0 50 4; clocks clkc 23, clkc 39; clock-names uart_clk, pclk; status okay; };对于EMIO方式UART1的引脚不在MIO上所以设备树里不需要声明pinctrl。这一点和直接用MIO时的配置有区别。有些参考设计在MIO引脚上挂了pinctrl-0节点用于引脚功能选择而EMIO方式下引脚功能已经在PL里通过XDC确定了PS软件侧看不到也不需要管MIO pinmux。设备树里另一个关键字段是clocks。uart_clk对应的是PS给UART外设的时钟如果你的Vivado工程里设置的是50MHz设备树里clkc 23也应该是50MHz。如果设备树里的时钟频率和实际配置不一致Linux内核里的serial驱动会按错误的时钟源计算波特率表现出来就是串口乱码。修改完设备树后编译生成对应的dtb替换SD卡里的设备树文件。如果你用PetaLinux可以直接用petalinux-build -c device-tree等命令重新生成。如果只是做实验也可以把DTS编译成DTB后单独替换启动分区里的文件。ZYNQ启动时还会用到BOOT.BIN、boot.scr、image.ub这些文件这里不再展开但设备树是否正确会直接影响到系统启动后是否出现ttyPS0或ttyS0这个节点。3.3 通过串口工具实测通信Linux启动后我用命令确认UART1被识别成哪个设备节点dmesg | grep tty如果看到类似cdns-uart e0001000.serial: ttyPS0 at MMIO 0xe0001000说明设备树和驱动都正常。在老版本驱动下可能显示ttyS0名字不同功能一样。然后用stty设置波特率并读取数据stty -F /dev/ttyPS0 115200 raw cat /dev/ttyPS0在另一个终端向串口发送数据echo EMIO UART test /dev/ttyPS0如果你想做一个快速的回环测试可以在设备侧用硬件短接tx和rx然后这样操作stty -F /dev/ttyPS0 115200 raw -echo echo loopback /dev/ttyPS0 head -n1 /dev/ttyPS0这里需要提醒的是不同USB转串口模块的行为不太一样有的模块有内部默认回环有的没有。如果你发现发送之后立即收到数据先确认是不是硬件上tx和rx被短接了不要急着判定是软件问题。4. 调试EMIO UART时最容易翻车的四个细节接反、约束、波特率与复位4.1 TX和RX接反最经典的串口故障所有串口调试里概率最高的坑就是TX和RX方向接反。如果你用USB转串口模块连开发板模块的TX应该接开发板的RX模块的RX应该接开发板的TX。这是交叉连接不是直连。很多新手按“同名相连”的直觉把TX接TX、RX接RX结果什么都收不到。判断方法其实很简单用万用表量电平。串口空闲状态下发送引脚应该是高电平大约3.3V或更高。如果一根排针是0V、另一根是3.3V一般高电平的是TX但也不能完全依赖这个规律因为如果板卡上的UART控制器被配置成高位驱动拉低可能不是这个状态。更可靠的办法是查原理图确认板上丝印到底是以板卡为主还是以模块为主。如果回环测试失败先不要看代码直接用杜邦线短接板卡上的tx和rx引脚。短接后自发自收如果通了说明发送和接收路径都正常问题一定出在外部连接上。如果短接后还是不通那就要往XDC约束和硬件设计方向查了。4.2 引脚约束明明写了却布不上线Vivado编译的时候如果PL引脚没有约束通常会报错说IO placement失败。但有一种情况很隐蔽约束文件里写的端口名和顶层端口名不一致Vivado会忽略这条约束然后又因为端口没有位置约束综合时默认把它放在不可用的虚拟引脚上导致实现阶段报错。排查时我会在综合后打开Report I/O确认目标端口是不是已经被分配到了想要的bank和引脚位置。如果发现端口没出现或者约束无效优先检查端口名大小写和get_ports的引号。另一个常见问题是PI引脚被其他用途占用了。ZYNQ的PL引脚中一部分是配置专用引脚比如CCLK、DONE、PROGRAM_B等这些引脚不能当普通IO。如果你在XDC里约束到某个专用引脚DRC会直接报冲突。还有的板卡把某些PL引脚接到了LED、按键、DDR、PCIe等固定位置你在XDC里强行复用轻则影响原有功能重则短路。实验前最好对照板卡原理图选一组空闲排针避免踩雷。4.3 波特率漂移导致乱码EMIO方式本身不会引起波特率漂移真正影响波特率的是UART的时钟源。ZYNQ的UART控制器支持广泛的时钟输入但具体分频精度取决于实际时钟频率。如果Vivado里配置的参考时钟和设备树里上报的时钟频率不一致Linux驱动计算分频系数时就会算错表现出来就是能收到字节但全是乱码。ZYNQ UART波特率计算可以简化成baud clock / (bdiv * (oversample 1))其中oversample通常取6bdiv是可配置的分频值。如果时钟频率是50MHz115200波特率对应的bdiv大约是49.6取整后会有少量误差。这个误差在容忍范围内但如果你用的时钟是全桥总线频率比如100MHz分频系数会更大误差可能影响通信。解决乱码问题我一般先降到9600波特率试试。如果9600正常而115200乱码基本可以断定是波特率分频精度问题而不是外部时序问题。这时候去检查Vivado里的UART参考时钟和设备树里的clock-frequency是否一致。4.4 上电后第一帧丢字复位与状态机问题有一种现象很难排查程序跑起来后前面几个字符丢失后面数据都正常。这个问题在裸机环境中经常被忽略因为xil_printf会等待FIFO为空才返回如果复位后PS UART状态机还没有完全准备好第一次写入可能被吞掉。我的解决办法是在主程序初始化之后加一个短暂延时例如把软件延时函数调用几百毫秒或者循环等待UART控制器的TX FIFO清零。在Linux下这个问题通常被内核串口框架消化掉了因为内核会做延时和总线访问约定。但如果你的PL引脚旁边有严重的信号完整性问题比如线太长、走线阻抗不匹配也可能出现偶尔第一个字符丢失。这种情况优先改善外部连线质量比如缩短杜邦线长度、使用带屏蔽的串口线。如果丢失的是Linux下earlycon的前几个字符那可能和earlycon使用UART0还是UART1有关要在内核启动参数里指定串口地址和设备树匹配。这种问题比较深不展开太多但至少要知道它存在。5. 从EMIO UART实验延伸出去把它变成日常调试工具的小建议5.1 我建议你在开发板上预置一个EMIO UART经过这次实验我后来在所有项目里都养成了一个习惯只要FPGA引脚资源有富余就固定预留一组PL引脚作为EMIO调试串口而不是依赖MIO上的默认UART。原因是ZYNQ的MIO在外设被移作他用时重新分配非常痛苦。比如你已经把MIO的某组引脚给了SD卡想抢回来给UART就要改硬件、改设备树、改启动配置牵一发动全身。而EMIO调试串口放在PL侧不仅引脚位置可以任意调整而且即使PS端MIO资源再紧张只要FCLK和UART控制器还在调试通道就不受影响。哪怕遇到没网络、没JTAG、只剩一个串口的环境也能借此看到Linux启动信息定位问题。5.2 如果要多路UARTEMIO能做什么PS端只有两个UART控制器所以哪怕EMIO能引出很多引脚也不能凭空变出更多串口。不过EMIO可以让这两个UART控制器的物理引脚分别落在PL的不同位置比如一路放在扩展排针一路接到PL内部状态机做调试监控。如果你真的需要三四路UART通常做法是在PL里例化多个UART IP核或者使用单路UART转多路UART的可编程逻辑方案。EMIO此时更多是作为PS与PL之间数据桥接的通道而不是简单的引脚复用。如果你刚做完本实验下一步可以试着把EMIO UART的信号接进ILA集成逻辑分析仪通过JTAG实时抓串口波形。这比拿示波器对排针方便很多而且能直接看到PS发出的字节是否完整到达PL引脚。只要在XDC里把uart1_tx信号同时约束到一个探针引脚上或者直接在Block Design里添加ILA IP并连接uart1_tx接口即可。这个技巧在实际调试中是加分项。5.3 记录一份调试清果避免重复踩坑我把这次实验常踩的问题做成了一份简短的检查清单后面每次做EMIO相关调试都会先过一遍检查顶层端口名和XDC里的get_ports是否完全一致。检查PL bank的VCCO是否和IOSTANDARD匹配。检查Block Design里UART参考时钟频率与设备树是否一致。检查TX和RX是否交叉连接外部模块的GND是否共地。检查UART控制器地址是否映射正确裸机工程中使用的是UART1的Device ID。回环测试失败时先短接板卡自身TX/RX区分是软件还是硬件问题。这些条目每一条都是当时实际踩过的。按照这个顺序排查大多数EMIO UART问题都能在半小时内定位而不是耗费一整天。最后再分享一个小技巧调试UART时不要只依赖printf输出试着在代码里加入一个自动回显功能。这样一来你就能用同一个串口工具同时判断“发送链路”和“接收链路”是否都正常。配合ELOOP测试命令很多看似玄学的乱码问题其实都会快速收敛到某一个具体的硬件节点上。
返回列表