☰
基于Nios II软核的FPGA电子钟软硬协同设计
2026/11/5 4:55:57 网站建设 项目流程

简介:面向FPGA与NIOS II软硬件协同开发的学习者,这份电子钟设计工程以Verilog实现数码管驱动,并在SOPC中借助NIOS II完成计时与交互逻辑,适合作为课程设计、实验项目或入门级综合练手案例。压缩包共602个文件、约22.66MB,包含Verilog与SystemVerilog源码、C源码及头文件、Quartus/Qsys工程与SOF配置文件等,覆盖从硬件描述、软核搭建到编译下载的完整链路。工程采用Qsys生成的1ms定时器计时,驱动8位数码管显示时间,并通过3个按键完成当前时间与闹钟时间的模式切换、加一减一调节,同时加入调整位闪烁标志、按键对应LED点亮、蜂鸣器闹钟及响铃流水灯指示。已有2296人学习,可据此理解软硬核分工、外设驱动与调时状态机设计,快速复现并扩展自己的FPGA电子钟方案。

1. 纯状态机做 FPGA 电子钟,功能一多就难以改动

拿 FPGA 做电子钟,多数人的第一版是分频器加一堆 always 块,几十行就能让数码管走起来。可一旦加上按键调时、闹钟、整点报时、掉电后重新对时,这套逻辑就会变得难以维护:改一个“调时的时候秒要不要停”的需求,得把状态编码从头捋一遍。这份基于 NIOS II 软核的 FPGA 电子钟设计走了另一条路:把走时、菜单、闹钟判断这些逻辑分支多、改动频繁的活放进软核里的 C 代码,硬逻辑只保留分频、扫描、消抖这类必须严格周期执行的部分。适合已经点亮过 LED、但还没完整做过软硬协同系统的同学,也适合想拿一个能演示、能继续加功能的工程当底座的从业者。

2. 软硬怎么切:把电子钟拆成 Avalon 总线上的七个外设

电子钟看着简单,拆开其实是四类活:时基产生、走时与闹钟判断、人机交互(按键、菜单)、显示驱动。这四类活的“性格”完全不同,混在一处写,后期改需求就是自找麻烦。这一章先把边界划清楚,再落到地址映射和顶层连线,后面几章的代码才有地方放。

2.1 硬逻辑和软核的分工边界

判断标准有两条:执行周期是否必须严格,以及改动频率有多高。

数码管动态扫描属于第一类。它要求每一位以固定频率轮流点亮,一旦扫描循环被别的事情阻塞,哪怕只停几毫秒,人眼立刻看到闪烁。我见过把扫描放在软件while里的写法,平时跑得好好的,一按按键进入设置模式、串口打印两行调试信息,屏幕就开始滚。这类活必须交给硬逻辑,用一个独立的状态机跑,软核只负责往显存里写数据,写多快、什么时候写,都不影响扫描。

按键消抖同理。机械按键的抖动窗口通常在 5~20 ms,软件消抖要靠轮询,轮询周期一旦被别的任务拉长就漏检。放在 RTL 里用一个计数器盯着,20 ms 内电平不变才认,稳定得多。

反过来,走时、闹钟比较、整点报时、调时菜单这些逻辑,一年可能要改八次。放 C 里改一行、重编译十几秒;放 Verilog 里改一次,综合加布局布线动辄几分钟,改到第五版人就烦了。软核跑在几十兆赫兹上,1 ms 中断一次的响应开销完全可以接受,这点实时性损失换来的是可维护性,很划算。

划分清楚后,系统长这样:硬逻辑负责 PLL、按键消抖、数码管扫描;软核负责 1 ms 时基中断、走时状态机、显示内容生成、蜂鸣器时序。

2.2 一张地址映射表把外设定死

动手之前先把外设清单和地址定下来,比边搭边改省事得多。下面这张表是我习惯的布局,地址按 4 KB 对齐,中断号按优先级排——定时器最靠前,因为它决定走时精度;调试串口排最后,丢了不影响功能。

外设实例名类型Avalon 接口基地址IRQ作用
cpuNios II/smaster——执行 C 代码
onchip_ram32 KB On-Chip Memoryslave0x0000_0000—代码段、堆栈
timer_1msInterval Timerslave0x0000_100001 ms 时基
seg_ctrl自定义外设slave0x0000_2000—4 字节显存 + 扫描
key_pioPIO(4 位输入)slave0x0000_3000—消抖后的按键脉冲
beep_pioPIO(1 位输出)slave0x0000_3010—蜂鸣器
jtag_uartJTAG UARTslave0x0000_40001调试打印

提示:表里的地址是手工固定的,重新 generate 之后一定要回system.h核对一遍宏定义。用自动分配地址时,加一个外设就可能让后面的偏移整体挪位,软件里写死的偏移量会全部错位。

复位向量和异常向量都指向onchip_ram,这一点别漏。有人只设了复位向量,程序跑起来一进中断就飞,查半天以为是中断控制器的问题,其实是异常向量还指向默认的空地址。

2.3 从空工程到能下载的最小系统

按下面顺序走,每一步都能单独验证,出问题好定位:

  1. 新建工程,器件型号按板子上的芯片选,别用默认值凑合。
  2. 打开 Platform Designer,加 Nios II/s、On-Chip Memory、Interval Timer、两个 PIO,再把第 4 章的自定义外设挂上。
  3. 给每个 slave 分配基地址,给 timer 和 jtag_uart 分配中断号,Nios II 的复位/异常向量指向 RAM。
  4. Generate HDL,回到顶层例化生成的系统模块。
  5. 用 Pin Planner 分配时钟、复位、按键、数码管、蜂鸣器管脚。
  6. 全编译,确认无时序违例。
  7. 打开 Nios II Software Build Tools,基于刚生成的.sopcinfo建 BSP,再建应用工程。
  8. 编译软件,先下载.sof,再下载.elf,看现象。

顶层文件我一般写得很薄,只做三件事:给时钟、等 PLL 锁定后放复位、把端口接出去。逻辑越少,出问题越好查。

// top.v —— 只做三件事:给时钟、例化 SoC、把 PIO 引到管脚 module top ( input wire clk_50m, // 板载 50 MHz 晶振 input wire rst_n, // 低有效复位,接独立按键 input wire [3:0] key_n, // 4 个按键,低有效 output wire [7:0] seg_data, // 段码 a~g + dp output wire [3:0] seg_sel, // 4 位位选,低有效 output wire beep ); wire clk_sys; wire pll_locked; // PLL:50 MHz 进 50 MHz 出,主要是拿 locked 做复位同步 pll u_pll ( .inclk0 (clk_50m), .c0 (clk_sys), .locked (pll_locked) ); // 等 PLL 锁定再放复位,否则软核可能在时钟没稳的时候就开始取指 wire sys_rst_n = rst_n & pll_locked; nios_clock_soc u_soc ( .clk_clk (clk_sys), .reset_reset_n (sys_rst_n), .key_export (key_n), // 直接进 RTL 消抖 .seg_data_export (seg_data), .seg_sel_export (seg_sel), .beep_export (beep) ); endmodule

这段里有两个参数值得说明。pll_locked参与复位是血泪经验,某些板子上电时晶振起振慢,PLL 还没锁定时时钟输出是不干净的,软核在这段时间取指会跑飞,表现就是“偶尔上电不工作、复位一下又好了”。key_n不经过软核、直接接到自定义外设,是因为消抖必须在确定的时钟域里做,走 PIO 再回 RTL 反而绕了一圈。

3. Nios II 侧的软件骨架:1 ms 中断、五状态机与显存刷新

软核里的代码不用写得多漂亮,但三件事必须做对:时基靠中断而不是靠延时、状态机要能覆盖所有设置项、IO 操作一律不放进中断服务程序。这套骨架写顺了,后面加秒表、加阴历、加温度显示都是往上叠模块的事。

3.1 时基:1 ms 定时器中断,别用 delay 死等

新手最容易踩的坑是用usleep()或空循环当秒表。这类写法的问题不是不准,而是一旦有别的事情插进来就不准了——按一下键、打一行串口,秒就慢半拍,跑一天能差出十几秒。

正确做法是让 Interval Timer 自由跑,中断里只做一件事:累加毫秒计数,凑够 1000 就置一个秒标志。所有耗时操作都退回主循环做。

/* timebase.c —— 1 ms 时基,中断里只打拍子,不做任何 IO */ #include "system.h" #include "altera_avalon_timer_regs.h" #include "sys/alt_irq.h" #include "io.h" volatile alt_u32 g_ms = 0; // 开机以来的毫秒数,主循环也用它做闪烁/长按计时 volatile alt_u8 g_tick_1s = 0; // 秒标志:中断置位,主循环清零 static void timer_isr(void *ctx) { // 先清 TO 标志,否则退出中断后会立刻重入 IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER_1MS_BASE, 0); g_ms++; if ((g_ms % 1000) == 0) { g_tick_1s = 1; // 只置标志,绝不在中断里刷数码管 } } void timebase_init(void) { // 50 MHz / 50000 = 1 kHz,即 1 ms 一次 alt_u32 period = 50000; IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER_1MS_BASE, period & 0xFFFF); IOWR_ALTERA_AVALON_TIMER_PERIODH(TIMER_1MS_BASE, (period >> 16) & 0xFFFF); IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_1MS_BASE, ALTERA_AVALON_TIMER_CONTROL_ITO_MSK | // 开中断 ALTERA_AVALON_TIMER_CONTROL_CONT_MSK | // 连续模式,到期自动重装 ALTERA_AVALON_TIMER_CONTROL_START_MSK); // 启动 alt_irq_register(TIMER_1MS_IRQ, NULL, timer_isr); }

period这个值必须跟着系统时钟走。换一块 100 MHz 的板子,period 要改成 100000,忘了改的后果是钟走得快一倍,而且显示一切正常,特别难发现。我在注释里写死“50 MHz / 50000”,就是为了下次改的时候一眼能看到依赖关系。

g_ms和g_tick_1s一定要加volatile。不加的话编译器可能把主循环里的if (g_tick_1s)优化成只读一次,秒永远不跳,现象诡异得像玄学。

3.2 走时与调时:一个五状态机加长按连加

调时功能用状态机写最省心。五个状态分别是正常运行、调时、调分、调闹钟时、调闹钟分,K1 循环切换,K2 加一,K3 减一。为了让“从 12:00 调到 12:45”不用按四十五下,K2 还要支持长按连加。

/* clock_app.c —— 走时、调时、闹钟,全在主循环 */ #include "system.h" #include "io.h" extern volatile alt_u32 g_ms; extern volatile alt_u8 g_tick_1s; typedef enum { ST_RUN = 0, ST_SET_H, ST_SET_M, ST_SET_AH, ST_SET_AM } app_state_t; static const alt_u8 SEG[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; // 共阴段码 0~9,共阳就把表整体取反 static void show_time(alt_u8 hh, alt_u8 mm, alt_u8 blink_mask) { alt_u8 d[4]; d[0] = SEG[hh / 10]; d[1] = SEG[hh % 10] | 0x80; // 用小数点当分隔符 d[2] = SEG[mm / 10]; d[3] = SEG[mm % 10]; for (int i = 0; i < 4; i++) { alt_u8 v = (blink_mask & (1 << i)) ? 0x00 : d[i]; // 正在调的那一位闪 IOWR(SEG_CTRL_BASE, i, v); // 偏移以字为单位,写第 i 个显存字节 } } int main(void) { timebase_init(); alt_u8 hh = 12, mm = 0, ss = 0; alt_u8 ah = 7, am = 30; app_state_t st = ST_RUN; alt_u32 last_key_ms = 0; alt_u32 hold_ms = 0; while (1) { // 消抖外设给出的是“按下瞬间”的脉冲,读一次就清掉了 alt_u32 keys = IORD(KEY_PIO_BASE, 0) & 0x07; if (keys & 0x01) { // K1:切状态 st = (app_state_t)((st + 1) % 5); hold_ms = 0; } // K2 短按加一,按住超过 600 ms 后每 120 ms 连加 if (keys & 0x02) { hold_ms = 0; last_key_ms = g_ms; } alt_u8 inc = (keys & 0x02) || (hold_ms > 600 && ((g_ms - last_key_ms) % 120 == 0)); if (inc) { switch (st) { case ST_SET_H: hh = (hh + 1) % 24; break; case ST_SET_M: mm = (mm + 1) % 60; break; case ST_SET_AH: ah = (ah + 1) % 24; break; case ST_SET_AM: am = (am + 1) % 60; break; default: break; } } hold_ms = (g_ms - last_key_ms); if (g_tick_1s) { g_tick_1s = 0; if (st == ST_RUN && ++ss >= 60) { ss = 0; if (++mm >= 60) { mm = 0; if (++hh >= 24) hh = 0; } } if (st == ST_RUN && hh == ah && mm == am && ss == 0) { beep_on(200); // 只在整分触发一次 } } // 设置态下让对应位闪烁,位掩码按状态给 alt_u8 mask = 0; if (st == ST_SET_H) mask = 0x03; if (st == ST_SET_M) mask = 0x0C; if (st == ST_RUN || st == ST_SET_H || st == ST_SET_M) show_time(hh, mm, (g_ms / 300 % 2) ? mask : 0); else show_time(ah, am, (g_ms / 300 % 2) ? 0x0F : 0); } }

几处细节值得单独说。inc的写法把“短按一次”和“长按连加”合并成一个变量,逻辑上等价于“这一轮循环该不该加一”,比开两个分支清爽。长按用g_ms做时基而不是计数循环,这样按键连加的快慢不受主循环跑多快影响。闹钟判断放在ss == 0那一拍,保证一分钟内只响一次,否则整分钟里每一轮循环都会触发一遍。

3.3 显示与蜂鸣:软件只写显存,IO 不进中断

show_time()里那句IOWR(SEG_CTRL_BASE, i, v)是整个显示链路唯一的软件动作。它写的是自定义外设里的 4 个字节寄存器,写完之后就不管了——扫描、位选、段码切换全在硬逻辑里跑。这意味着即使软件在某个分支里卡了 50 ms(比如串口打印一行长字符串),屏幕上一个像素都不会抖。

蜂鸣器同理,beep_on(200)只负责把时长记进变量,实际翻转 IO 的动作在主循环里按g_ms比对完成,中断服务程序里一行 IO 都没有。中断里做 IO 是另一个高频翻车点:中断上下文里调用带阻塞的打印函数,整机直接挂死,连看门狗都救不回来。

4. 硬逻辑侧:自定义 Avalon-MM 从外设、扫描与 20 ms 消抖

到这一章就是 Verilog 的部分了。两个模块:一个数码管控制器,对外表现成 Avalon-MM 从设备;一个按键消抖,输出按下脉冲。前者要挂进 Platform Designer 才能被软件访问,后者直接接在顶层。写完这两个,整机就闭环了。

4.1 自定义外设:4 字节显存加 1 kHz 扫描

这个模块的设计原则是从设备侧无等待。扫描是纯粹的内部行为,跟总线读写没有任何耦合,所以waitrequest恒为 0,主设备一拍就能完成读写,不需要插等待周期。

// seg_ctrl.v —— 4 字节显存 + 4 位动态扫描 module seg_ctrl ( input wire clk, input wire reset_n, // Avalon-MM slave input wire [1:0] avs_s0_address, input wire avs_s0_write, input wire avs_s0_read, input wire [31:0] avs_s0_writedata, output reg [31:0] avs_s0_readdata, output wire avs_s0_waitrequest, // conduit 端口 output wire [7:0] seg_data, output reg [3:0] seg_sel ); assign avs_s0_waitrequest = 1'b0; // 固定 1 拍响应,不给主设备插等待 reg [7:0] disp0, disp1, disp2, disp3; // 写侧:4 个字地址对应 4 位数字 always @(posedge clk or negedge reset_n) begin if (!reset_n) begin disp0 <= 8'h00; disp1 <= 8'h00; disp2 <= 8'h00; disp3 <= 8'h00; end else if (avs_s0_write) begin case (avs_s0_address) 2'd0: disp0 <= avs_s0_writedata[7:0]; 2'd1: disp1 <= avs_s0_writedata[7:0]; 2'd2: disp2 <= avs_s0_writedata[7:0]; 2'd3: disp3 <= avs_s0_writedata[7:0]; endcase end end // 读侧:回读显存,调试时用得上 always @(*) begin case (avs_s0_address) 2'd0: avs_s0_readdata = {24'h0, disp0}; 2'd1: avs_s0_readdata = {24'h0, disp1}; 2'd2: avs_s0_readdata = {24'h0, disp2}; default: avs_s0_readdata = {24'h0, disp3}; endcase end // 扫描节拍:50 MHz / 50000 = 1 kHz,每位点亮 1 ms reg [15:0] div_cnt; reg [1:0] scan_idx; always @(posedge clk or negedge reset_n) begin if (!reset_n) begin div_cnt <= 16'd0; scan_idx <= 2'd0; end else if (div_cnt >= 16'd49999) begin div_cnt <= 16'd0; scan_idx <= scan_idx + 1'b1; end else begin div_cnt <= div_cnt + 1'b1; end end // 先给段码,下一拍再换位选,避免换位瞬间的鬼影 reg [7:0] seg_r; always @(posedge clk or negedge reset_n) begin if (!reset_n) seg_r <= 8'h00; else case (scan_idx) 2'd0: seg_r <= disp0; 2'd1: seg_r <= disp1; 2'd2: seg_r <= disp2; default: seg_r <= disp3; endcase end assign seg_data = seg_r; always @(posedge clk or negedge reset_n) begin if (!reset_n) seg_sel <= 4'hF; // 高电平熄灭 else seg_sel <= ~(4'b0001 << scan_idx); // 低有效位选 end endmodule

关键参数三个。49999决定扫描频率,每位 1 ms,四位一轮 4 ms,帧率 250 Hz,远高于人眼闪烁阈值,即使摄像机拍摄也不会出现明显扫描线。seg_sel低有效还是高有效取决于板子上的驱动方式,共阴数码管配三极管驱动通常是低有效,直接连 IO 的可能是高有效,接反了表现是“该亮的不亮、不该亮的全亮”。段码和位选分成两拍更新,是为了消除换位瞬间上一段的残余内容,不做这个处理能看到轻微重影,尤其是数字 1 和 8 相邻的时候。

4.2 按键消抖:两级同步、20 ms 计数、边沿出脉冲

按键消抖模块比想象中要小心。按键是异步输入,直接进状态机有亚稳态风险,所以第一级必须是同步器。消抖计数的时间要足够长,20 ms 能覆盖绝大多数轻触开关,太长会让按键手感发黏。

// key_debounce.v —— 同步 + 计数消抖 + 按下沿出脉冲 module key_debounce #( parameter CLK_FREQ = 50_000_000, parameter DEBOUNCE_MS = 20 )( input wire clk, input wire reset_n, input wire [3:0] key_n, // 板上按键,低有效 output reg [3:0] key_pulse // 按下瞬间输出一个 clk 宽脉冲 ); localparam CNT_MAX = CLK_FREQ / 1000 * DEBOUNCE_MS; // 50 MHz 下是 1_000_000 // 1) 两级同步,压掉亚稳态 reg [3:0] sync0, sync1; always @(posedge clk or negedge reset_n) begin if (!reset_n) begin sync0 <= 4'hF; sync1 <= 4'hF; end else begin sync0 <= key_n; sync1 <= sync0; end end // 2) 逐位消抖:电平稳定够久才更新 reg [3:0] key_stable; reg [31:0] cnt [0:3]; genvar i; generate for (i = 0; i < 4; i = i + 1) begin : g_deb always @(posedge clk or negedge reset_n) begin if (!reset_n) begin cnt[i] <= 32'd0; key_stable[i] <= 1'b1; // 松开状态 end else if (sync1[i] != key_stable[i]) begin if (cnt[i] >= CNT_MAX - 1) begin cnt[i] <= 32'd0; key_stable[i] <= sync1[i]; end else begin cnt[i] <= cnt[i] + 1'b1; end end else begin cnt[i] <= 32'd0; // 电平回弹,计数清零重来 end end end endgenerate // 3) 下降沿(按下)出一个时钟宽的脉冲 reg [3:0] stable_d; always @(posedge clk or negedge reset_n) begin if (!reset_n) stable_d <= 4'hF; else stable_d <= key_stable; end always @(posedge clk or negedge reset_n) begin if (!reset_n) key_pulse <= 4'h0; else key_pulse <= stable_d & ~key_stable; // 1->0 即按下 end endmodule

CNT_MAX用参数推出来,换板子只改CLK_FREQ就行。输出脉冲宽度只有一个时钟周期,20 ns,软件侧如果轮询周期比这长就会漏检——所以这个模块必须配合 PIO 的中断或者足够快的轮询,我一般把主循环压在 1 ms 以内,漏检概率极低。真要保险,就在硬逻辑里加一个置位保持寄存器,软件读后写 1 清除。

4.3 挂进 Platform Designer:_hw.tcl 里要写对的几个属性

自定义模块不进组件库,Platform Designer 是看不见的。手动 New Component 走一遍图形界面也行,但重新 generate 之后容易丢配置。我习惯直接写_hw.tcl,跟着工程一起进版本管理。

# seg_ctrl_hw.tcl —— 告诉 Platform Designer 这是个 Avalon-MM 从设备 add_interface clock clock sink set_interface_property clock clockRate 50000000 add_interface reset reset sink set_interface_property reset associatedClock clock set_interface_property reset synchronousEdges DEASSERT add_interface avs_s0 avalon slave set_interface_property avs_s0 associatedClock clock set_interface_property avs_s0 associatedReset reset set_interface_property avs_s0 addressAlignment DYNAMIC set_interface_property avs_s0 readWaitTime 1 set_interface_property avs_s0 writeWaitTime 1 add_interface_port avs_s0 avs_s0_address address Input 2 add_interface_port avs_s0 avs_s0_write write Input 1 add_interface_port avs_s0 avs_s0_read read Input 1 add_interface_port avs_s0 avs_s0_writedata writedata Input 32 add_interface_port avs_s0 avs_s0_readdata readdata Output 32 add_interface_port avs_s0 avs_s0_waitrequest waitrequest Output 1 add_interface seg conduit end add_interface_port seg seg_data export Output 8 add_interface_port seg seg_sel export Output 4

readWaitTime和writeWaitTime都填 1,和 RTL 里waitrequest恒 0 对应。这两个值填大了不影响功能,但每次访问都白插等待周期,软件跑起来会莫名变慢。addressAlignment用DYNAMIC,地址对齐交给系统分配,别手填成NATIVE,否则偏移量可能和软件里的假设对不上。端口名必须和 Verilog 里的avs_s0_*严格一致,多一个下划线都报错,这个错误信息很不友好,第一次遇到会查很久。

5. 常见问题排查:钟不走、显示重影、按键连跳的五个坑

前面几章都是顺风顺水的写法,实际动手时真正耗时间的是调试。下面五条是我在这类工程上反复遇到的,按“现象—原因—解决”记下来,下次照着对。

第一条:下载完.sof数码管全亮或全灭,串口一行字都没有。

现象是所有段码常亮,按键无反应,用 SignalTap 看软核的时钟引脚也没有波形。原因八成是复位没等 PLL 锁定,软核在时钟不稳的阶段就开始取指,直接跑飞;也有可能是 Platform Designer 里 reset 的极性搞反了,模块期待低有效、外面给了高有效。解决办法就是把pll_locked与外部复位相与,并在顶层确认reset_reset_n这个名字对应的确实是低有效。改完还不行的,用示波器直接量 PLL 输出脚,先确认时钟本身有没有出来,再往上查。

第二条:钟走得快一倍或慢一半,显示完全正常。

这种最迷惑,因为看起来一切都在工作。原因基本是两个:定时器 period 的值还在按 50 MHz 算,但系统实际跑的是别的频率;或者中断服务程序里做了耗时操作,导致中断嵌套丢拍。先量一下系统时钟的实际频率,再回头核对period;确认频率没问题就去 ISR 里数语句,把能挪的都挪回主循环。我在 ISR 里只留过三个赋值,多一行都算超编。

第三条:按一下键跳好几个数,或者按了没反应。

连跳的原因通常是消抖模块输出了电平而不是脉冲,软件侧又用电平判断,主循环跑一圈就加一次。改成边沿出脉冲,同时在软件里加一条“两次有效按键之间至少间隔 200 ms”的锁。按了没反应则是反过来的问题——脉冲太窄被轮询漏掉,或者按键上拉电阻没焊,输入悬空。用万用表量按键未按下时的电平,正常应该是稳定的高电平。

第四条:数码管有重影,或者某一位明显比其他位暗。

重影来自段码和位选同一拍更新,换位瞬间上一段的残余内容还在总线上。解决办法是段码先更新、位选延后一拍,或者换位前先把段码清零做消隐。某一位偏暗则是扫描占空比不一致,检查seg_sel的移位写法有没有把某一位多算一拍,共阴共阳接反也会表现为“整体亮度正常但某位异常”。

第五条:换台电脑重新编译,软件下载进去跑飞,硬件没问题。

原因是 BSP 里的system.h地址和重新 generate 之后的硬件对不上。Platform Designer 每次 generate 都可能重新分配地址,尤其是你中途加过外设。解决办法是改完硬件之后一定重新 generate BSP,再重新编译应用工程。这条坑我踩过不止一次,后来干脆在脚本里把“generate BSP”写进编译流程,省得忘。

6. 从能跑到可信:1 Hz 心跳、24 小时漂移与脱机固化

能显示时间只是及格线,要拿去演示或者交付,还得证明它准、稳、掉电还能自己起来。我习惯用一张验证表走完最后一步,四项,每项都有明确判据,不靠感觉。

验证项方法判据
硬逻辑 1 Hz 心跳在 RTL 里做一路 1 Hz 分频输出到测试脚,示波器 10 s 门闸计频误差小于 1e-4,且与软核无关
软件走时精度记下起始时间,与 PC 对时,24 小时后比对漂

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询