ARTICLE DETAIL

资讯详情

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

FPGA与STM32通过FSMC总线实现高速编码器解码的完整方案

FPGA与STM32通过FSMC总线实现高速编码器解码的完整方案 简介面向FPGA与STM32联合开发的编码器采集应用这套资料提供一套完整可运行的FSMC交互方案核心主控为EP4CE10F17I7N与STM32F407IGT6。硬件部分针对集电极开路编码器设计了5V转3.3V隔离电路包含ADUM1300隔离芯片、1K上拉电阻和100PF滤波电容的选型并说明了外部供电驱动编码器、核心板负责隔离后供电的分工软件部分涵盖FPGA脉冲计数逻辑、ARM定时读取显示代码以及串口“reset/cr/lf”复位命令实现。压缩包共401个文件约34.45MB以C源码、Verilog/TDF逻辑文件、Quartus与Keil工程文件、PDF/HTML设计文档为主另有视频讲解、配置与编译辅助文件可按工程目录恢复开发环境代码可烧录后直接使用适合边看设计文档边对照FSMC读写时序与脉冲计数流程。已有138人学习浏览适合正在做编码器测速、FSMC总线通信或STM32FPGA异构项目的人员直接参考移植。1. FPGA与STM32之间的FSMC总线为什么是编码器应用的理想搭档增量式编码器在电机闭环、机械臂关节、转台定位里几乎是无处不在的传感器。常规做法是MCU开一个定时器做正交解码简单场景完全够用可一旦转速过高、精度要求达到每圈几万线或者需要在同一个控制周期内同时处理多路编码器反馈时单独的STM32定时器就会暴露出计数溢出、硬件资源不足、软件查询不及时等问题。FPGA擅长的是并行处理和确定性时序STM32擅长的是跑控制算法和通信协议二者之间用FSMC总线打通正好把两边优势拼成一条完整的采集-处理-反馈链路。这个设计的核心在于FPGA负责对编码器信号的边沿采样、正交解码、倍频计数和溢出管理把计数结果放到一份“寄存器表”里STM32通过FSMC接口像访问外部SRAM一样去读这份寄存器表。STM32不需要精确控制读取时序FSMC控制器会自动生成对应片选和读信号。对工程师来说这套方案既避免了纯 FPGA 方案里人机交互和算法移植的麻烦也比“STM32直连编码器”方案具备更高的计数带宽、更低的 CPU 占用率。在机器人、伺服驱动、CNC 改造和视觉定位平台上这套组合经过大量项目验证是最可靠的高精度位置反馈方案之一。对想深挖的人来说值得玩味的是FSMC读时序的建立时间设置它直接决定了高频读取时是否会出现杂散错误是真正的胜负手。2. FPGA端正交解码与FSMC从机接口的完整设计2.1 编码器信号进入FPGA后的第一级处理滤波与正交解码接编码器的A、B、Z三路信号进FPGA引脚时我一般不会直接把这些信号送进解码状态机而是先过一级数字滤波。工业现场的电机驱动线缆即使做了屏蔽也难免在上升沿附近引入毛刺常见处理方法是做一个可配置的采样窗口滤波器例如用系统时钟对输入信号做连续N次采样只有当N次采到的电平全部一致时才算有效翻转。module glitch_filter #( parameter WIDTH 4 )( input wire clk, input wire rst_n, input wire sig_in, output reg sig_out ); reg [WIDTH-1:0] shift_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin shift_reg 0; sig_out 0; end else begin shift_reg {shift_reg[WIDTH-2:0], sig_in}; if (shift_reg) sig_out 1b1; else if (~|shift_reg) sig_out 1b0; end end endmodule上面的滤波器用了“全1或全0才输出跳变”的思路WIDTH为4时意味着信号必须连续4个时钟周期保持同一电平才被接受等效于消除短于4个时钟周期的毛刺。WIDTH的具体取值要看系统时钟和编码器最高频率比如50 MHz系统时钟下WIDTH4对应约80 ns的滤波窗口编码器脉冲最高频率低于2 MHz时完全够用超过了就需要把滤波窗口缩小或做自适应旁路。要注意滤波器本身也引入了延迟会轻微增加位置信息的滞后通常在伺服环路里这个滞后远小于控制周期可以忽略。2.2 正交解码的四倍频状态机滤波之后进入核心的正交解码模块。增量编码器输出的A、B两路正交方波一个周期内有四个边沿变化采用四倍频可以把每转脉冲数放大四倍。Verilog实现上最直接的写法是边沿检测结合状态转移表这里给出状态机式写法便于后续增加反向计数和错误标志。module quad_decoder #( parameter CNT_WIDTH 32 )( input wire clk, input wire rst_n, input wire a_in, input wire b_in, output reg [CNT_WIDTH-1:0] count, output reg count_oflow, output reg count_uflow ); localparam S_00 2b00, S_01 2b01, S_10 2b10, S_11 2b11; reg [1:0] state; wire [1:0] ab {a_in, b_in}; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_00; end else begin case (state) S_00: begin case (ab) 2b01: state S_01; 2b10: state S_10; default: state S_00; endcase end S_01: begin case (ab) 2b00: state S_00; 2b11: state S_11; default: state S_01; endcase end S_10: begin case (ab) 2b00: state S_00; 2b11: state S_11; default: state S_10; endcase end S_11: begin case (ab) 2b01: state S_01; 2b10: state S_10; default: state S_11; endcase end default: state S_00; endcase end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin count {CNT_WIDTH{1b0}}; count_oflow 0; count_uflow 0; end else begin case (state) S_00: if (ab 2b01) count count 1b1; else if (ab 2b10) count count - 1b1; S_01: if (ab 2b11) count count 1b1; else if (ab 2b00) count count - 1b1; S_10: if (ab 2b00) count count 1b1; else if (ab 2b11) count count - 1b1; S_11: if (ab 2b10) count count 1b1; else if (ab 2b01) count count - 1b1; endcase end end endmodule这里状态机只用状态来判断上一个时刻的AB组合结合当前组合决定加计数还是减计数从代码逻辑上天然实现了正交信号“超前/滞后”方向判断不需要额外方向位这也是四倍频解码的标准实现方式。CNT_WIDTH取了32位在线数为2500线的编码器上四倍频后每圈10000个计数32位可以覆盖约42.9万圈对大多数运动控制场景都不会溢出。如果只做单圈测量也可以裁剪到24位或16位减小FPGA内寄存器资源占用。Z信号在这里没有参与判定通常单独接入一个锁存寄存器用于在每圈零点位置清零或锁存。2.3 FSMC从机接口地址映射与寄存器组设计FPGA在FSMC总线里扮演从机角色STM32是主机。FPGA侧需要把A、B、Z采集出的计数值、状态字、控制字映射到一组固定地址上让STM32像读外部SRAM一样进行操作。FSMC总线在NE1片选上常用地址0x60000000起始一个典型的寄存器布局如下偏移寄存器名方向位宽说明0x00CNT_L只读16 bit当前计数值低16位0x02CNT_H只读16 bit当前计数值高16位0x04STATUS只读8 bitbit0方向bit1溢出标志bit2Z信号状态0x08CTRL只写8 bitbit0清零bit1锁存使能bit2滤波器旁路0x0ALATCH_L只读16 bitZ信号到来时的锁存值低16位0x0CLATCH_H只读16 bit锁存值高16位FPGA侧用地址译码逻辑把FSMC的地址总线A[3:1]和片选信号组合起来实现寄存器读写。核心代码结构如下module fsmc_slave #( parameter CNT_WIDTH 32 )( input wire clk, input wire rst_n, // FSMC from STM32 input wire fsmc_cs_n, input wire fsmc_wr_n, input wire fsmc_rd_n, input wire [15:0] fsmc_addr, inout wire [15:0] fsmc_data, // internal interface input wire [CNT_WIDTH-1:0] cnt_value, input wire cnt_dir, input wire z_state, output reg cnt_clear, output reg latch_enable ); reg [15:0] cnt_l; reg [15:0] cnt_h; reg [7:0] status_reg; reg [7:0] ctrl_reg; reg [15:0] latch_l; reg [15:0] latch_h; reg [15:0] data_out; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_l 0; cnt_h 0; latch_l 0; latch_h 0; ctrl_reg 0; cnt_clear 0; latch_enable 0; end else begin cnt_l cnt_value[15:0]; cnt_h cnt_value[31:16]; if (z_state) begin latch_l cnt_value[15:0]; latch_h cnt_value[31:16]; end if (!fsmc_cs_n !fsmc_wr_n) begin case (fsmc_addr[15:0]) 16h0008: begin ctrl_reg fsmc_data[7:0]; cnt_clear fsmc_data[0]; latch_enable fsmc_data[1]; end default: ; endcase end end end always (*) begin case (fsmc_addr[15:0]) 16h0000: data_out cnt_l; 16h0002: data_out cnt_h; 16h0004: data_out {status_reg[15:8], cnt_dir, z_state, cnt_uflow, cnt_oflow}; default: data_out 16h0000; endcase end assign fsmc_data (!fsmc_cs_n !fsmc_rd_n) ? data_out : 16hzzzz; endmodule上面的代码实现了最基本的FSMC从机逻辑写地址0x0008时把数据总线上的低8位存入控制寄存器同时触发清零或锁存操作读地址时根据地址返回对应寄存器内容。FSMC数据总线是双向的需要三态门控制读方向否则会和STM32的数据输出冲突。关键点在于FPGA内部时钟和FSMC异步信号之间的同步问题建议对fsmc_cs_n、fsmc_wr_n、fsmc_rd_n先做两级寄存器打拍处理避免亚稳态。还有一个容易踩的坑是FSMC地址总线宽度STM32的FSMC对外地址是以16位为单位的地址偏移需要自己换算清楚写代码时保持和芯片手册一致。3. STM32端FSMC外设配置与编码器控制字读写3.1 CubeMX中的FSMC参数配置STM32端第一步是启用FSMC外设并选对片选和引脚。以STM32F407为例FSMC的NOR/PSRAM控制器支持Bank1的4个片选区域NE1对应地址0x60000000到0x63FFFFFF。编码器计数器的地址映射在FPGA内部已经固定STM32端只需要告诉FSMC控制器“我从0x60000000读数据时按怎样的时序去拉信号”。参数项推荐值说明Data bus width16 bit与FPGA内部数据总线宽度一致Memory typeSRAMFSMC按SRAM时序运行Write operationDisable只读场景可关掉写使能Address setup time0x33个HCLK周期地址建立时间Address hold time0x2地址保持时间Data setup time0x5数据建立时间Burst access modeDisable编码器读取无需突发模式在CubeMX里使能FSMC后选NE1、16位模式上面这些时序参数写在NOR/PSRAM配置界面的Timing栏目里。时钟树里HCLK如果是168 MHz一个HCLK周期约5.95 nsData setup给5个周期约29.8 ns这个值对绝大多数FPGA工程都够用。时序参数如果给得太小FPGA侧来不及把数据送上总线读回来的数据可能是前一拍的旧值给得太大又浪费带宽。初始化代码生成后在main函数里调用HAL_SRAM_Init即可完成FSMC控制器配置。3.2 通过宏定义访问FPGA寄存器STM32端访问FSMC地址空间不需要调用任何HAL库接口直接用指针解引用就是一次完整的读操作。比较推荐的做法是把地址偏移量用宏定义写清楚便于代码维护和阅读#define FPGA_BASE_ADDR 0x60000000 #define FPGA_CNT_L (*(volatile uint16_t *)(FPGA_BASE_ADDR 0x0000)) #define FPGA_CNT_H (*(volatile uint16_t *)(FPGA_BASE_ADDR 0x0002)) #define FPGA_STATUS (*(volatile uint16_t *)(FPGA_BASE_ADDR 0x0004)) #define FPGA_CTRL (*(volatile uint16_t *)(FPGA_BASE_ADDR 0x0008)) #define FPGA_LATCH_L (*(volatile uint16_t *)(FPGA_BASE_ADDR 0x000A)) #define FPGA_LATCH_H (*(volatile uint16_t *)(FPGA_BASE_ADDR 0x000C))使用这组宏时要注意volatile修饰不能去掉否则编译器在优化级别较高时可能把多次读取合并成一次导致位置值不随电机实际转动更新。读取32位计数值时需要先读高16位还是先读低16位这里有一个值得考究的次序问题电机高速旋转时如果先读低16位再读高16位两次读之间计数值可能已经跨过0xFFFF边界致使拼出来的32位值出现0x00010000量级的跳变。稳妥的办法是连续读两次高16位如果两次结果一致再用低16位拼接否则重读。uint32_t read_encoder_count(void) { uint16_t high1, high2, low; uint32_t result; do { high1 FPGA_CNT_H; low FPGA_CNT_L; high2 FPGA_CNT_H; } while (high1 ! high2); result ((uint32_t)high2 16) | low; return result; }3.3 控制字写入与状态字解析FSMC的写操作同样通过指针赋值完成。把控制寄存器定义为volatile指针后FPGA_CTRL 0x01就触发一次FPGA计数清零。实际使用中清零操作需要谨慎清零之后计数器从零重新开始如果伺服系统正依赖这个计数值做位置闭环清零瞬间会导致位置跳变。我通常会只在设备初始化或手动回零时执行清零运行过程中用Z信号的硬件锁存来记录每圈零点位置。typedef struct { uint8_t direction; uint8_t overflow; uint8_t underflow; uint8_t z_state; } encoder_status_t; encoder_status_t read_encoder_status(void) { encoder_status_t st; uint16_t raw FPGA_STATUS; st.direction (raw 1) 0x01; st.overflow (raw 2) 0x01; st.underflow (raw 3) 0x01; st.z_state (raw 0) 0x01; return st; }状态字里的溢出标志是判断系统是否异常的重要依据。如果电机单方向连续高速旋转时间过长32位计数器溢出时方向位会翻转位置值瞬间从极大值跳到极小值控制算法如果不处理这个跳变整个伺服环路就会飞出。设计做法是每次读取后检测溢出标志如果置位就累计到软件层的一个32位扩展变量里同时向FPGA写控制字清除溢出标志。这样做等于把FPGA的32位计数器软件扩展到64位对绝大多数应用来说已经是无限长计数。4. 数据通路优化中断通知、DMA读取与Z信号锁存处理4.1 位置更新率的瓶颈在哪里FSMC单次读操作本身很快16位数据在168 MHz主频下只需几百纳秒但STM32的软件架构决定了“什么时候读”比“读一次有多快”更影响位置跟踪效果。常见工程做法有两种一种是CPU循环查询FSMC寄存器代码简单但会占用大量主循环时间另一种是外部中断通知由FPGA在计数值变化或Z信号到来时拉高一个GPIOSTM32在中断里读取最新位置。第二种方式下CPU只在需要的时候才发起FSMC读事务主循环可以去处理显示、通信或算法任务。用STM32的外部中断EXTI配合FPGA的中断引脚是常见的实现方式。FPGA检测到Z信号上升沿时产生一个单时钟周期的脉冲输出到STM32的某个EXTI引脚STM32在中断回调里锁存LATCH_L和LATCH_H寄存器。相比查询Z信号的方案这个方式的好处是STM32不需要高速轮询节省的CPU时间用在电流环计算上。4.2 DMA方式的FSMC读取配置如果应用要求周期性采样位置比如20 kHz的伺服环路每次都在中断服务函数里执行FSMC读操作会占用不少中断处理时间。此时可以把FSMC挂在DMA上DMA自动从固定地址搬运数据到内存搬运完成再触发DMA中断通知CPU处理。核心配置代码如下#define BUFFER_SIZE 4 uint16_t fsmc_rx_buffer[BUFFER_SIZE]; DMA_HandleTypeDef hdma_fsmc; void FsmcDMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); hdma_fsmc.Instance DMA2_Stream0; hdma_fsmc.Init.Channel DMA_CHANNEL_0; hdma_fsmc.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_fsmc.Init.PeriphInc DMA_PINC_ENABLE; hdma_fsmc.Init.MemInc DMA_MINC_ENABLE; hdma_fsmc.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_fsmc.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_fsmc.Init.Mode DMA_NORMAL; hdma_fsmc.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_fsmc); __HAL_LINKDMA(hsram1, hdma, hdma_fsmc); }这段代码把FSMC外设和DMA流0连接起来外设地址设置为0x60000000就实现了连续4个16位寄存器的自动读取。注意PeriphInc设为使能是因为我们要依次读取CNT_L、CNT_H、STATUS、LATCH_L四个地址如果只想要单个寄存器把它关掉就行。DMA触发中断后缓冲区里的4个16位值已经同时定格不存在两次读之间数值更新的问题。4.3 多个编码器通道的并行读取扩展一套系统往往不止一个编码器FPGA侧做8路甚至16路编码器解码是很容易的引脚足够、解码模块实例化多份即可。STM32端多路读取就遇到一个实际问题FSMC只有一个Bank地址空间怎么区分多路编码器。常见做法是FPGA内部再做一层地址映射用高几位地址选择编码器通道例如0x60000000 通道号 * 0x20 寄存器偏移。STM32端把通道号做成函数参数#define CHANNEL_OFFSET(ch) (0x20 * (ch)) uint32_t read_encoder_channel(uint8_t ch) { volatile uint16_t *base (volatile uint16_t *)(FPGA_BASE_ADDR CHANNEL_OFFSET(ch)); uint16_t high1, high2, low; do { high1 *(base 1); low *(base); high2 *(base 1); } while (high1 ! high2); return ((uint32_t)high2 16) | low; }多通道设计在FPGA端有一个现实的资源考量每个解码模块的计数器位宽如果是32位16路就是512个寄存器位加上滤波器和状态机逻辑在Altera Cyclone IV或Xilinx Spartan-6级别的小型FPGA上完全够用。选型时最需要关注的是引脚总数是否够分配QEP信号、FSMC总线和若干控制IO。另一个工程实践是把所有通道的计数快照在同一个时钟沿写入一组影子寄存器STM32的DMA通道读取影子寄存器时多路位置天然是同一时刻的快照可以省去后来做软件同步的麻烦。5. 示波器验证FSMC时序时最容易忽略的几个细节5.1 用逻辑分析仪抓FSMC读周期的实际时序FSMC时序问题在仿真里很难暴露最终调试阶段强烈建议用逻辑分析仪抓真实的NE1、NOE、数据总线波形。抓取方法是把STM32的读地址模式设成单次读取在主循环里重复执行temp FPGA_CNT_L逻辑分析仪就能捕获到稳定的读波形。FSMC读操作的理想波形是NE1先拉低形成片选窗口NOE随后拉低数据总线在NOE有效的后半段保持有效数据NOE拉高后NE1跟着释放。实际调试中我看到的一种常见错误是NE1被拉低后NOE太晚拉低FPGA端已经把数据撤掉了STM32采样到的是高阻态。这时FSMC读回来的数据全是0xFFFF或随机值。解决办法是把Data setup time加大如果加大后波形正常说明FPGA侧的data_out三态门释放条件需要优化常见做法是让FSMC读信号经过同步后至少再延迟一个时钟周期再翻转输出。这类问题在仿真时看不到因为仿真里的总线延迟和引脚回滞完全理想化。5.2 数据总线的三态门冲突排查STM32的FSMC数据总线是双向口FPGA侧如果不小心把数据输出使能条件写错比如用!fsmc_rd_n而不是!fsmc_cs_n !fsmc_rd_n在读周期结束后的时间段数据总线仍被FPGA驱动这时如果STM32发起写操作两端的输出级就会打架轻则读到错误数据重则损坏引脚压焊的驱动管。排查时先断开FPGA和STM32之间物理连接单独给STM32跑一段读FSMC的测试程序测到读回的值全部是0xFFFF或0x0000说明FSMC外设本身工作正常再接上FPGA复测如果出现部分地址读值稳定、部分地址随机跳变优先怀疑FSMC地址线映射或者FPGA地址译码逻辑有冗余。FPGA侧想方便观察三态门是否存在冲突可以在数据总线并联几个串阻并抓波形正常的读周期数据线上应该有较快的边沿而冲突时波形会出现明显的平台毛刺。5.3 从“能读到数”到“读得准”的验证步骤完整的编码器应用调试建议按下面几步进行防止后面问题叠加后难以定位用信号发生器给FPGA输入一个固定频率的AB正交信号STM32串口打印计数值确认计数方向和输入方向一致。用示波器测AB相位差是否为90度如果相位差偏差较大FPGA四倍频解码逻辑会出现偶发方向抖动原因是状态机在非标准相位上会判定为反向。高速旋转时打印计数值并对比外部光栅尺读数确认FSMC连续读取没有丢拍。验证DMA模式的缓冲区数据是否与CPU循环读取一致使用CAN或以太网把位置值传到上位机画波形比较。5.4 32位计数器扩展的进一步思考当编码器线数极高或者长期累积误差要求极低时FPGA内部32位计数器可能不够用。此时有两种扩展思路一是保持32位计数在每次读操作时同时读取溢出标志软件层维护一个更高位宽的accumulator二是FPGA内部直接扩展到48位代价是FSMC需要多读两个16位寄存器。第二种方案在这类FSMC交互架构里实现成本很低因为寄存器表设计本身就是可扩展的新增两个偏移地址即可。多读两个寄存器的额外开销只有几百纳秒对于需要长期高精度计数的转台应用来说完全值得。最后提一个可以立即用在自己工程里的小技巧FPGA侧把Z信号触发锁存的那一时刻同时打一个时间戳寄存器以系统时钟周期为单位记录锁存发生的时刻。STM32读取位置和锁存时间戳配合就能推算电机的瞬时速度省掉单独的测速定时器。这个做法在很多需要精准测速的场合比“两个位置点做差值除以时间间隔”的老办法在短距离测速上要稳定得多。本文还有配套的精品资源点击获取
返回列表