资讯详情

资讯详情

FPGA+Zynq低延迟中断方案:Linux UIO用户态驱动实战

做FPGAZynq这套异构平台的人十有八九都卡在同一个地方ARM核上跑着LinuxFPGA那边动不动就给你抛一个中断可等你用户态程序反应过来几百微秒都过去了。做控制还行做高速采集、信号处理、实时联动这类对时间敏感的应用这延迟就是致命伤。这个实战场景我前前后后折腾了快两年试过内核驱动、试过/dev/mem、试过各种各样的“土办法”最后真正让我在项目里稳定压住延迟的是Linux的UIO框架——没错就是那个看起来“只是把寄存器映射到用户态”的UIO。配合PREEMPT_RT实时补丁和一点调度上的调优完全可以在Zynq上做出确定性很高的硬实时加速效果而且代码量比内核模块少一个数量级调试起来也爽太多。这篇东西就是把我在实际项目中完整走通UIO驱动、实现FPGA中断直通用户态、最终把响应时间压到几十微秒级别的全过程整理出来。适合正在做Zynq/异构计算、被Linux实时性折磨、或者单纯想少走弯路的朋友。我会从原理、硬件准备、Linux配置、用户态代码、DMA场景、实时调优到最后的排坑记录一条线讲清楚所有关键步骤都给你可抄作业的版本。1. UIO到底在解决什么问题1.1 为什么普通Linux中断路径对实时不友好先捋清痛点。FPGA和ARM之间通常走AXI总线FPGA内部逻辑完成一次采集或计算后通过中断通知ARM。Linux内核收到中断后会先把驱动的上半部跑完再把下半部softirq、tasklet、workqueue推后执行最后才轮到你的业务代码。如果没有做任何实时性改造从FPGA拉高中断信号到你的用户态进程真正被调度起来干活这个时间可能达到几百微秒甚至毫秒级。原因不复杂普通内核里存在大量不可抢占的临界区、关中断段而且中断线程/进程的优先级未必胜过你手头的业务线程。最关键的是你写的业务逻辑如果在内核态一切都不透明——你根本不知道它什么时候被调度、被谁打断、延迟多久。这也是我一开始的思路既然Linux的实时性痛点大多集中在“中间环节太多”那就把中间环节砍掉。让内核只做一件事——接收中断、唤醒用户态进程剩下的业务逻辑全部放在用户态用实时调度策略控制。让FPGA的数据和寄存器直接暴露给用户态用户态程序自己决定什么时候读、怎么读。这就是UIO的设计思想。1.2 UIO的设计哲学中断进内核逻辑留用户态UIO全称Userspace I/O是Linux内核提供的一套用户态驱动框架。它不是让你绕过内核而是把驱动劈成两半内核态只保留最薄的骨架——负责平台设备匹配、中断申请、IRQ到event counter的转换以及mmap操作用户态负责全部业务逻辑包括寄存器读写、中断响应、算法处理。具体机制上UIO会为每个设备创建/dev/uioX节点。read()这个节点会阻塞直到内核收到对应中断中断到来时内核把event counter加一然后唤醒等在read()上的进程。你还可以通过write()一字节数据来使能或禁用中断。寄存器和内存映射则由mmap()完成。这个机制对异构计算场景来说几乎是为FPGACPU量身定做的。FPGA侧的寄存器、FIFO、DMA缓冲区本质上都是内存映射用户态mmap后直接用指针访问访问路径上没有系统调用、没有内核锁延迟和抖动都被压得很低。中断路径上只有一次内核态→用户态的唤醒配合实时调度时间确定性远比“内核驱动复杂分层”靠谱。1.3 三种用户态访问方案的对比有人可能会问既然都是用户态访问硬件直接用/dev/mem读FPGA寄存器不就行了为什么要UIO这里面的差距主要体现在中断上。/dev/mem只能帮你拿到地址空间但它没法帮你接收中断。你只能用轮询来检测FPGA状态这在高速采集场景里既不优雅也不省电更没法保证实时性。另一条路是写完整的内核驱动。优点是什么都能干缺点是开发量大、调试慢、一碰内核就重启而且你在用户态积攒了一大堆好用的工具链全作废了。对很多项目来说性价比太低。三者的对比如下特性/dev/mem 轮询完整内核驱动UIO用户态驱动寄存器访问mmap直读内核ioremapmmap直读中断支持不支持只能轮询支持支持内核转发为event开发难度低高低调试复杂度低但低效高频繁重启低可用gdb调试实时性差依赖内核调度配合RT内核表现优秀大块DMA缓冲区难容易dma_alloc_coherent可用uio_dmem_genirq从这张表也能看出来UIO属于“鱼和熊掌我全都要”的方案。要中断有中断要用户态开发效率有用户态效率关键是大块DMA缓冲区这块也有专门的支持后面我会专门展开说。2. 动手前的硬件准备2.1 Zynq架构速览PS和PL怎么通信Zynq-7000系列把ARM Cortex-A9双核PS端和FPGA可编程逻辑PL端集成在一颗芯片里。PS和PL之间有几条关键的通道AXI-GP是通用的低速通道适合寄存器访问AXI-HP是高带宽通道适合大批量数据搬运PL可以直接通过它访问PS侧的DDR内存。中断方面PL侧的16个中断线IRQ_F2P[15:0]接到PS的GIC中断控制器。我第二次做UIO项目时就吃过地址映射的亏在Vivado里分配的地址是相对地址真正对PS可见的是经过地址译码后的物理地址。所以第一步一定要在Vivado的Address Editor里看清楚你自定义IP最终落地的物理地址范围。比如把AXI-Lite接口设为0x43C00000、长度0x10000那Linux侧的设备树、UIO映射、用户态mmap全部都以这个0x43C00000为基准。2.2 PL端自定义IP与AXI-Lite地址分配在Vivado里无论你是自己写RTL还是用IP Integrator搭块最终和PS交互的模块建议挂上AXI-Lite从接口。AXI-Lite足够简单单次读写、无突发非常适合控制寄存器、状态寄存器、中断控制这类场景。如果你要传大批量数据那就别用AXI-Lite了直接上AXI-Stream DMA引擎让FPGA侧的DMA把数据怼到DDR里然后通过中断通知PS。一个标准的自定义IP内部至少该有这么几组寄存器偏移地址寄存器作用0x00Ctrl启动/停止/复位控制0x04Status当前状态、忙标志0x10IRQ Status中断状态写1清除0x14IRQ Enable中断使能掩码0x20Data / 高速数据基地址数据交换区或用AXI-HPDMA关键是中断逻辑。PL侧的中断输出建议设计成脉冲型或电平型要和PS的GIC配置对得上。我习惯用“电平触发中断状态寄存器”的组合FPGA拉高中断线PS收到中断后做处理然后通过写IRQ Status寄存器清除中断源同时中断线才能拉低。这套方式在UIO下非常好使不容易丢中断。2.3 中断线怎么连到PSGIC中断号换算这部分是很多人栽跟头的地方我必须详细说。Zynq的GICARM通用中断控制器把中断分成两类SPI共享外设中断中断号32~95和PPI私有外设中断中断号0~31。PL端的中断属于SPI在设备树中写interrupts 0 29 4时最后一个4是触发电平标记ARM规定1上升沿2下降沿4高电平8低电平中间的29就是相对GIC的SPI编号。0 29 4的含义换算成GIC实际中断号就是322961。也就是说CPU视角看到的PL中断号是61不是29。很多人查/proc/interrupts发现中断号对不上就是在这里忘加了32。我在实际项目中PL的中断线用的是IRQ_F2P[0]对应设备树里写0 29 1如果你的IP输出就是单脉冲可以用上升沿1如果是电平型就用4。这个写法在不同内核版本和不同BSP里略有差异但Zynq-7000上基本都是这个套路。2.4 准备Linux环境内核源码与设备树文件开始配置软件之前先确保你有一份手头板卡对应的Linux内核源码、设备树源文件.dts以及交叉编译环境。无论是用PetaLinux还是手动构建内核思路都一样改内核配置、改设备树、编译、部署。我用的是PetaLinux 2019.2 对应内核版本交叉编译器是arm-linux-gnueabihf-。如果你们公司用的是自己的SDK关键点在于你能修改内核配置并生成新的设备树二进制。3. Linux侧配置UIO从内核到设备树3.1 内核编译选项与模块加载UIO框架在内核里默认就能编关键是使能两个选项CONFIG_UIOUIO核心框架必须开。CONFIG_UIO_PDRV_GENIRQ通用平台驱动这玩意儿支持通过设备树的compatible来匹配设备并且自动管理中断和event counter。如果不需要额外写内核代码只靠这两个配置就够了。在内核配置界面中进入Device Drivers → Userspace I/O drivers把Userspace I/O drivers设为编译进内核y或模块m。我推荐编译成模块调试时方便rmmod/modprobe。当然如果你做产品直接编进内核更省心。模块编出来后在板子上执行modprobe uio modprobe uio_pdrv_genirq没有任何报错的话ls /dev/uio*应该能看到设备节点了。注意如果你的平台只用设备树匹配uio_pdrv_genirq会自动为每个匹配的设备注册对应/dev/uioN节点。3.2 设备树写法generic-uio与中断属性UIO设备在设备树里挂载的方式非常简洁。下面是一个典型的示例挂在0x43C00000、长度0x10000、中断是SPI 29电平高有效uio_pl: uio43c00000 { compatible generic-uio; reg 0x43c00000 0x10000; interrupt-parent intc; interrupts 0 29 4; };这里有两个细节值得注意。其一compatible generic-uio是uio_pdrv_genirq在驱动里声明匹配的名字不要自作主张改成别的字符串否则驱动不会认领设备。如果板卡的BSP里已经有自己的compatible字符串你有两个选择改设备树为generic-uio或者改内核驱动源码把自定义compatible加进uio_of_match数组然后重新编译模块。我建议后者放到量产阶段再做调试期直接用generic-uio最稳。其二interrupt-parent intc必须指向GIC节点Zynq的BSP里这个节点label一般是intc。如果漏了interrupt-parent驱动申请中断时会失败而且错误信息比较隐晦。3.3 上电验证从sysfs确认设备注册设备树改好后重新编译设备树并替换到启动分区。板子起来后第一件事就是确认UIO设备是否成功注册。我一般依次执行这几条命令ls /sys/class/uio/ cat /sys/class/uio/uio0/name cat /sys/class/uio/uio0/maps/map0/addr cat /sys/class/uio/uio0/maps/map0/size cat /proc/interrupts | grep uiouio0/name显示的内容一般就是设备树里的node name能和你Vivado里的IP对上maps/map0/addr和size应该等于你设置的物理地址和长度。/proc/interrupts里如果出现了uio相关的中断计数说明中断也挂上了。走到这一步Linux侧的工作就算完成了一半。剩下的一半是看你的用户态程序能不能把这个设备用起来。4. 用户态驱动开发第一个UIO程序4.1 mmap偏移量的坑别把物理地址当offsetUIO的mmap行为有一个典型坑不是让你传物理地址作为offset。UIO把设备树中reg定义的每个地址段映射为独立的mmap区域用户态通过offset来选择映射哪一块。第一个映射区域的offset固定是0第二个区域的offset是4096也就是一页大小依此类推。内核在mmap处理中把offset PAGE_SHIFT当作索引去查找对应映射区域。所以如果设备树里reg只写了0x43C00000 0x10000那你mmap时offset必须传0映射出来的地址就是寄存器基址。很多人习惯性地把offset写成0x43C00000然后mmap直接返回EINVAL卡半小时怀疑人生。正确写法void *base mmap(NULL, 0x10000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);要映射第二个区域offset写4096不是0x1000。记住这个规则就能少踩一个巨坑。4.2 中断读取与使能read和write的正确姿势UIO设备的中断接收方式很特殊不是ioctl不是信号而是read()。每次中断发生内核会把该设备的中断事件计数加一然后唤醒等待在read()上的进程。read返回的是4字节的unsigned int代表累计的中断次数。如果返回负数说明出错了。还有个细节是中断使能。UIO设备默认可能是禁止中断的需要先write一字节数据来使能。常见写法是unsigned int enable 1; write(fd, enable, sizeof(enable));想要禁止中断写0即可。这个write会调用UIO框架里的irqcontrol回调底层调用enable_irq/disable_irq是真正对硬件中断控制器下命令不是做做样子。4.3 完整可运行的UIO用户态程序下面给一个完整的示例程序逻辑很简单打开/dev/uio0映射寄存器使能中断进入无限循环等待FPGA中断中断来了读IRQ状态寄存器做处理写寄存器清中断。我刻意把线程调度和内存锁页也放进来因为它们对实际响应有很大影响后面会细说#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include sys/mman.h #include string.h #include sched.h #include time.h int main(int argc, char **argv) { int fd; volatile unsigned char *regs; unsigned int enable 1; unsigned int evt; struct sched_param param; struct timespec ts, te; long delta_ns; fd open(/dev/uio0, O_RDWR); if (fd 0) { perror(open /dev/uio0); return 1; } regs mmap(NULL, 0x10000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (regs MAP_FAILED) { perror(mmap); return 1; } /* 提优先级并锁页确保实时性 */ param.sched_priority 99; if (sched_setscheduler(0, SCHED_FIFO, param) 0) perror(sched_setscheduler); mlockall(MCL_CURRENT | MCL_FUTURE); /* 使能中断 */ write(fd, enable, sizeof(enable)); while (1) { clock_gettime(CLOCK_MONOTONIC, ts); /* 阻塞等待FPGA中断 */ if (read(fd, evt, sizeof(evt)) ! sizeof(evt)) { perror(read uio); break; } clock_gettime(CLOCK_MONOTONIC, te); delta_ns (te.tv_sec - ts.tv_sec) * 1000000000L (te.tv_nsec - ts.tv_nsec); printf(interrupt #%u, latency: %ld ns\n, evt, delta_ns); /* 读状态寄存器假设IRQ status在0x10 */ unsigned int st *(volatile unsigned int *)(regs 0x10); if (st 0x1) { /* 业务逻辑处理FPGA上报的数据 */ } /* 写IRQ status寄存器清中断源 */ *(volatile unsigned int *)(regs 0x14) 0x1; } munmap((void *)regs, 0x10000); close(fd); return 0; }这个程序在我Zynq-7010板卡上直接就能跑。中断延迟通过clock_gettime统计read()返回前后的时间差大体反映中断到用户态的路径开销。操作寄存器时用volatile说明编译器不要优化掉这些硬件访问。4.4 为什么read返回后要立即清中断PL端中断服务的基本协议是中断到达后用户态程序必须在合理时间内清除PL侧的IRQ Status寄存器和PS侧GIC的中断标志否则中断会一直触发或丢失。清中断的动作要尽可能接近业务处理结束的位置让中断线尽快拉低避免后续中断被阻塞。另外注意UIO框架本身处理中断时如果确认没有使能中断会直接关闭GIC中断线那样你write(1)重新使能中断后才能再次收到中断。所以第一次跑示例程序时建议先把enable写入再进入read循环避免一开始就错过中断。5. DMA场景与缓存一致性5.1 大块数据搬运UIO如何配合DMA寄存器级别的交互只是UIO的基本用法。很多FPGA项目尤其是AD采集、图像处理、FFT频谱仪这类动辄一帧数据几百KB甚至几MB根本不可能靠CPU一个寄存器一个寄存器地搬。正确做法是PL侧的DMA引擎通过AXI-HP口直接把数据写进DDR写完或攒够一帧后触发中断UIO把中断转发给用户态用户态再从DDR里取数据。我这里必须提醒一个容易混淆的点如果你只是mmap了PL寄存器的地址空间那这段空间是PL通过AXI-Lite访问的对CPU来说属于设备内存天然非缓存不存在缓存一致性问题。但PL通过AXI-HP写DDR里的DMA缓冲区时这个缓冲区的物理内存映射关系、缓存策略、生命周期管理才是DMA场景的真正难点。5.2 缓冲区从哪来uio_dmem_genirq和预留内存我在最初做DMA时踩过大坑图省事用/dev/mem直接映射一块DDR物理地址结果CPU读到一堆过期数据ARM的cache把数据牢牢攥在手里PL那头写的DDR内容根本没进cache。这时候才明白DMA缓冲区必须用一致性的DMA内存或者明确做cache刷新。UIO框架里其实有一个专门应对这个需求的变体uio_dmem_genirq。它的作用就是在probe阶段通过dma_alloc_coherent分配一块物理连续、缓存一致的内存并把这块内存做成一个独立的mmap区域映射给用户态。PL侧DMA往这块内存写数据CPU读数据时看到的永远是最新的不需要手动flush cache。设备树里用uio_dmem_genirq时可以通过dmem-region和dmem-size属性指定分配的内存大小也可以在reserved-memory节点里预留DDR。生产级做法我推荐在设备树预留内存reserved-memory { #address-cells 1; #size-cells 1; ranges; dma_buf: dma_buf30000000 { compatible shared-dma-pool; reg 0x30000000 0x2000000; no-map; }; }; uio_dma: uio43c00000 { compatible generic-uio; reg 0x43c00000 0x10000; memory-region dma_buf; interrupt-parent intc; interrupts 0 29 4; };注意uio_pdrv_genirq和uio_dmem_genirq在驱动库里用的都是generic-uio这个compatible字符串两者不能同时加载否则后加载的那个会因为总线冲突注册失败。所以要么只编入其中一个模块要么改成自定义compatible再重新编译模块。我在项目里的做法是调试阶段用uio_pdrv_genirq验证寄存器/中断上DMA后切换到自编的uio_dmem_genirq定制版。5.3 数据一致性的实操策略就算有了DMA缓冲区PL端DMA写完数据后为了保证CPU能看到最新数据最好的屏障就是中断本身。PL端DMA描述符写完、数据落盘落DDR后再拉高中断线。CPU在UIO中断被唤醒后开始读数据那一刻数据早已写入DDR。这里有一个隐含前提PL端DMA要保证数据写入DDR之后才发中断也就是“写完后屏障再通知”。如果数据是CPU先写、PL后读比如你要下发一段配置参数给PL同样可以用类似机制。写完后加一个内存屏障直接写寄存器触发PL读取再用中断确认PL已经读完。这套握手能把大多数一致性坑盖过去。DMA缓冲区映射给用户态后在代码里同样用volatile指针访问。如果是dma_alloc_coherent分配的内存硬件上本身就是非缓存或带一致性保障的不需要再手动刷cache。如果某些特殊场景必须手动刷新cache那就得深入ARM的cacheflush接口了非必要我不建议碰。5.4 实测DMAUIO的性能参考在我实际的一套高速ADC采集中PL端以80MHz采样率持续写入DMA环形缓冲区每攒512个采样点触发一次中断。配合UIO和实时线程CPU从收到中断到读完512个点32位2KB数据并启动下一轮处理平均耗时在20~40微秒之间抖动不超过10微秒。相比之前内核驱动的4~6微秒用户态路径确实多了一些唤醒开销但换来的是几十倍的开发效率和调试便利而且在几十微秒这个量级上对绝大多数采集和后处理场景绰绰有余。6. 实时性调优与验证6.1 实时内核UIO只能帮你到这里为止说句实话UIO本身并不会让Linux变实时。它砍掉的是内核态到用户态之间多余的软件层次但调度器能不能及时唤醒你的线程仍然取决于内核的整体实时性。要追求硬实时PREEMPT_RT补丁几乎必不可少。PREEMPT_RT把内核中绝大多数不可抢占区域变成可抢占并降低了中断线程化的延迟上限。在Zynq的BSP里打开内核配置的CONFIG_PREEMPT_RT_FULL或者直接使用Xilinx发布的RT内核分支都能得到一颗实时性大幅改善的Linux内核。加上UIO的用户态直通两者才是完整的组合拳。6.2 用户态的实时编程习惯我见过不少团队费劲折腾完内核和UIO结果实时线程全在瞎写——用printf打印日志、malloc分配内存、碰到锁就等然后测出来的延迟照样惨不忍睹。用户态实时编程有几条规矩必须遵守mlockall(MCL_CURRENT | MCL_FUTURE)锁住所有内存页防止缺页中断打断关键路径。线程调度设成SCHED_FIFO优先级根据你的业务定通常95~99。中断处理循环里避免任何系统调用、文件IO、动态内存分配、锁操作。需要打印调试时用无锁缓冲区事后再异步输出。让实时线程独占一个CPU避免和其他线程抢核。可以通过sched_setaffinity把核分开或在启动参数里用isolcpus隔离。这些都是成本极低、效果极明显的调整。实测同样的UIO程序不锁页最差延迟能飙到毫秒级锁页高优先级后能稳定压制在几十微秒。6.3 中断响应时间的测量方法测量UIO中断从FPGA产生到用户态read返回的完整延迟最简单的就是我在示例程序里用clock_gettime(CLOCK_MONOTONIC)打印的时间差。但这个方法测到的是“从read进入阻塞到read返回”的时间中间包含了中断处理和调度唤醒基本能反映业务路径的真实延迟。更精确的做法是让FPGA在拉高中断线的同时写入一个32位时间戳寄存器PL侧用内部计数器实现用户态收到中断后读取该寄存器就能算出中断从产生到处理经过了多少时间。把两套测量对在一起还能估算PL侧中断本身和PS侧GIC处理的时间差。这个方案不依赖系统时钟精度适合做产品验收。6.4 一组可供参考的实际数据同样一块板卡同样一套UIO用户态驱动唯一变量是内核调度配置我实测的数据大致是这样的配置平均延迟最大抖动普通Linux内核320us±150usPREEMPT_RT 普通优先级85us±40usPREEMPT_RT SCHED_FIFO 锁页 CPU隔离38us±9us必须说明这个数据强烈依赖CPU频率、FPGA频率、系统负载和应用场景数字本身没有普适性但趋势是稳定复现的UIORT调度调优能把Zynq上的中断响应从“不可控的几百微秒”拉到“可接受的几十微秒”这就足够驱动大多数硬实时业务了。7. 常见问题与排查记录7.1 问题速查表整理一下我项目里遇到过的典型问题很多都能直接在搜索引擎报错但排查思路比结果更值钱。现象直接原因排查方法打开/dev/uio0失败no such file设备树没匹配或uio_pdrv_genirq未加载ls /sys/class/uio/确认节点dmesg看probe日志mmap返回EINVALoffset传成了物理地址第一个映射区offset必须是0read一直阻塞中断不触发中断使能未写入或FPGA根本没拉中断先write(1)使能用示波器/ILA查PL中断线read返回-1errnoEINTR被信号打断read时忽略EINTR或检查是否有信号处理函数/proc/interrupts里uio计数狂跳FPGA中断源没清中断一直触发用户态及时清PL的IRQ Status寄存器数据读到旧值DMA缓冲区cache一致性问题改用uio_dmem_genirq或预留dma-coherent内存7.2 调试技巧配合ILA和devmem快速定位PL侧的调试我通常习惯开一个ILA核同时抓中断信号和寄存器写信号。Linux起来后如果你发现UIO设备注册正常但中断不进来先用devmem手动读一下PL侧寄存器再操作ILA触发条件能快速判断问题是在PS侧还是PL侧。devmem在UIO场景里是个“白嫖”工具不需要写驱动在命令行就能触发寄存器读写。比如devmem 0x43C00010 w读回PL的状态寄存器。如果能读到值说明AXI-Lite路径通畅如果crash或读回0问题大概率出在PL侧地址译码或时钟上。等UIO设备跑通后devmem还能帮你临时模拟FPGA侧的中断寄存器写入促成一次中断触发用来验证用户态逻辑。7.3 烧写与启动的联动陷阱UIO设备依赖设备树设备树又依赖BOOT.BIN或image.ub的生成方式。很多人改了设备树之后没同步重新打包启动后UIO节点根本不存在。常见的启动方式里把FPGA bitstream、FSBL、U-Boot和内核打包进BOOT.BINZynq-7000常见设备树则放在image.ub或单独分区。我有一个建议调试期间把设备树改动和内核模块分开管理。先用U-Boot环境变量setenv fdt_addr动态加载新设备树验证UIO节点没问题后再固化到BOOT.BIN里。这样每次迭代不用反复烧写整块启动镜像省很多时间。7.4 从UIO到产品化的几个忠告UIO项目从“能跑”到“能战”之间还有几道坎。首先是设备节点的安全性用户态程序可以随意访问整个寄存器窗口一旦越界写坏PL寄存器可能导致IP核死锁所以寄存器布局要小心设计尽量按页隔离敏感区域。其次是异常恢复FPGA侧如果挂了中断可能长时间不来用户态程序要用看门狗超时机制检测并能在必要时重新复位PL侧IP。我在手头产品里做了一套“热重启”机制UIO业务线程发现超时先尝试通过写复位寄存器重启PL内IP如果还不行就置位整个PL的复位信号。这套东西让系统的可用性从“偶尔挂机”提升到了“7x24小时稳定跑”。最后的几句实在话做了这几轮Zynq异构实时项目我的体感是UIO这套东西表面上用一个虚拟设备节点简化了驱动开发但真正值钱的地方在于它强制你重新思考软硬件的边界哪些事必须让内核做哪些事放到用户态反而又快又灵活。实测下来UIO实时Linux这套组合在几十微秒级别的硬实时场景里完全够用而且调试的爽快程度是内核驱动没法比的。以后你再遇到FPGA丢中断、Linux调度延迟、DMA数据不一致这种问题先别急着堆代码把UIO这条路理一遍多半能少熬几个通宵。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →