/* ----------------------------------------------------------------------------- * saolei.c —— 纯 C 扫雷(RV32IM 子集:只用这颗核实现了的指令) * * 显示:把每个格子写成显存里的 2 个字节(MMIO 0x2000 起) * 输入:鼠标事件(中断驱动)——中断里搬到软件队列,主循环在下面处理 * * 针对这颗 CPU 的硬约束(很重要,改代码时别踩): * 1) 存储只有 lw / sw / sb,**没有 lb/lh/lbu/lhu** —— * 所以只按字节"写"显存,从不按字节"读";所有状态都用 32 位字保存。 * 2) **没有 auipc** —— 编译时必须 -fno-pic -mcmodel=medlow,并且不能出现 * 只读数据(.rodata),所以本文件没有任何字符串/常量表(字模在 RTL 里)。 * 3) 乘除只有 mul/mulh/mulhsu/mulhu 和**无符号**的 divu/remu —— * 所以本文件所有索引运算都用"2 的幂 + 移位/掩码",一次除法都不用。 * 4) 没有 fence/ebreak 等;中断返回是 uret(在 isr.c 里)。 * * 棋盘:16 x 16,雷数可选 20/40/60(16 是 2 的幂 => 索引全用移位) * 网格:40 列 x 30 行,棋盘居中放在 (行 7..22, 列 12..27) * ---------------------------------------------------------------------------*/ #include "mmio.h" /* 三档难度完全照微软扫雷:初级 9x9/10 雷、中级 16x16/40 雷、高级 30x16/99 雷。 * 棋盘尺寸是运行期变量(三档各不相同),所以位图与队列都按最大尺寸分配。 */ #define BW_MAX 30u /* 最大棋盘宽(高级) */ #define BH_MAX 16u /* 最大棋盘高 */ #define NCELLS_MAX (BW_MAX * BH_MAX) /* 480 */ #define BM_WORDS_MAX ((NCELLS_MAX + 31u) / 32u) /* 15 */ /* 当前难度下的棋盘参数(由 set_difficulty() 设定) */ static unsigned g_bw, g_bh; /* 棋盘宽、高(格) */ static unsigned g_ncells; /* g_bw * g_bh */ static unsigned g_bm_words; /* 位图字数 */ static unsigned g_board_r0; /* 棋盘左上角在网格里的行(居中) */ static unsigned g_board_c0; /* 棋盘左上角在网格里的列(居中) */ /* 格索引 <-> (行, 列)。这颗核有无符号除法 divu/remu,直接用;虽然多周期, * 但一局游戏这点开销无所谓。 */ #define IDX_R(i) ((i) / g_bw) #define IDX_C(i) ((i) % g_bw) /* 游戏状态 */ #define ST_PLAY 0u #define ST_WIN 1u #define ST_LOSE 2u /* ---- 棋盘状态:3 个位图,每个 8 个字(256 位) ---- */ static unsigned mine_bm[BM_WORDS_MAX]; static unsigned open_bm[BM_WORDS_MAX]; static unsigned flag_bm[BM_WORDS_MAX]; static unsigned seen_bm[BM_WORDS_MAX]; /* BFS:已经入过队的格子,避免重复入队把队列撑满 */ /* 其它全局状态(全部进 .bss,靠 start.c 清零) */ static unsigned opened_cnt; static unsigned flags_cnt; static unsigned g_state; static unsigned g_first_done; static unsigned g_mines; static unsigned g_difficulty; static unsigned rnd_state; static unsigned cur_r, cur_c; /* 光标所在的网格格 */ static int acc_x, acc_y; /* 指针累积量(1/8 格,有符号) */ static unsigned bfs_q[NCELLS_MAX]; /* 揭开用的队列(按最大棋盘分配) */ /* ============================ 小工具 ============================ */ /* 位图取位/置位/清位:i = r*16 + c,全部用移位和掩码 */ static unsigned bget(const unsigned *bm, unsigned i) { return (bm[i >> 5] >> (i & 31u)) & 1u; } static void bset(unsigned *bm, unsigned i) { bm[i >> 5] |= (1u << (i & 31u)); } static void bclr(unsigned *bm, unsigned i) { bm[i >> 5] &= ~(1u << (i & 31u)); } static void draw_cell(unsigned gr, unsigned gc, unsigned glyph, unsigned color) { fb_write_cell(gr * GRID_COLS + gc, glyph, color); } /* 把棋盘格 (r,c) 画出来 */ static void draw_tile(unsigned r, unsigned c, unsigned glyph, unsigned color) { draw_cell(g_board_r0 + r, g_board_c0 + c, glyph, color); } static unsigned num_color(unsigned n) { if (n == 1u) return C_BLUE; if (n == 2u) return C_GREEN; if (n == 3u) return C_RED; if (n == 4u) return C_MAGENTA; if (n == 5u) return C_YELLOW; if (n == 6u) return C_CYAN; return C_GRAY; } static unsigned in_board(void) { if (cur_r < g_board_r0) return 0u; if (cur_r >= (g_board_r0 + g_bh)) return 0u; if (cur_c < g_board_c0) return 0u; if (cur_c >= (g_board_c0 + g_bw)) return 0u; return 1u; } static unsigned cur_index(void) { return ((cur_r - g_board_r0) * g_bw) + (cur_c - g_board_c0); } /* LCG 随机数:只用 mul/add/移位 */ static unsigned rnd_next(void) { rnd_state = rnd_state * 1103515245u + 12345u; return rnd_state >> 16; } /* 设定难度:棋盘尺寸 + 雷数 + 居中位置,三档完全照微软扫雷。 */ static void set_difficulty(unsigned difficulty) { g_difficulty = difficulty; if (difficulty == 0u) /* 初级:9x9,10 雷 */ { g_bw = 9u; g_bh = 9u; g_mines = 10u; } else if (difficulty == 2u) /* 高级:30x16,99 雷 */ { g_bw = 30u; g_bh = 16u; g_mines = 99u; } else /* 中级:16x16,40 雷 */ { g_bw = 16u; g_bh = 16u; g_mines = 40u; } g_ncells = g_bw * g_bh; g_bm_words = (g_ncells + 31u) / 32u; g_board_c0 = (GRID_COLS - g_bw) / 2u; /* 棋盘居中 */ g_board_r0 = (GRID_ROWS - g_bh) / 2u; } /* ============================ 画面 ============================ */ static void clear_screen(void) { unsigned i; for (i = 0u; i < (GRID_COLS * GRID_ROWS); i = i + 1u) { fb_write_cell(i, G_BLANK, C_BLACK); } } static void draw_frame(void) { unsigned gc, gr; for (gc = (g_board_c0 - 1u); gc <= (g_board_c0 + g_bw); gc = gc + 1u) { draw_cell(g_board_r0 - 1u, gc, G_SOLID, C_CYAN); draw_cell(g_board_r0 + g_bh, gc, G_SOLID, C_CYAN); } for (gr = (g_board_r0 - 1u); gr <= (g_board_r0 + g_bh); gr = gr + 1u) { draw_cell(gr, g_board_c0 - 1u, G_SOLID, C_CYAN); draw_cell(gr, g_board_c0 + g_bw, G_SOLID, C_CYAN); } } static void draw_all_covered(void) { unsigned r, c; for (r = 0u; r < g_bh; r = r + 1u) { for (c = 0u; c < g_bw; c = c + 1u) { draw_tile(r, c, G_COVER, C_GRAY); } } } static void update_cursor_reg(void) { REG_W(REG_CURSOR) = (cur_c & 31u) | ((cur_r & 31u) << 8); } static void set_bg(unsigned color) { REG_W(REG_CTRL) = (color << 4) | 1u; /* bit0 = 光标显示 */ } /* ============================ 游戏逻辑 ============================ */ static unsigned count_adj(unsigned i) { unsigned r = IDX_R(i), c = IDX_C(i), n = 0u, rr, cc; unsigned r0 = (r == 0u) ? 0u : (r - 1u); unsigned r1 = (r + 1u >= g_bh) ? (g_bh - 1u) : (r + 1u); unsigned c0 = (c == 0u) ? 0u : (c - 1u); unsigned c1 = (c + 1u >= g_bw) ? (g_bw - 1u) : (c + 1u); for (rr = r0; rr <= r1; rr = rr + 1u) { for (cc = c0; cc <= c1; cc = cc + 1u) { if ((rr == r) && (cc == c)) continue; n = n + bget(mine_bm, (rr * g_bw) + cc); } } return n; } static void lose(unsigned hit) { unsigned k; for (k = 0u; k < g_ncells; k = k + 1u) { if (bget(mine_bm, k)) { if (k == hit) { draw_tile(IDX_R(k), IDX_C(k), G_BOOM, C_RED); } else if (!bget(flag_bm, k)) { draw_tile(IDX_R(k), IDX_C(k), G_MINE, C_RED); } } } REG_W(LED_ADDR) = 0xDEADu; g_state = ST_LOSE; } static void win(void) { unsigned k; for (k = 0u; k < g_ncells; k = k + 1u) { if (bget(mine_bm, k)) { draw_tile(IDX_R(k), IDX_C(k), G_FLAG, C_YELLOW); } } REG_W(LED_ADDR) = 0xF00Du; g_state = ST_WIN; } static void reveal_at(unsigned i) { unsigned qh = 0u, qt = 0u, cur, n, r, c, rr, cc, ni; unsigned r0, r1, c0, c1, k; if (bget(open_bm, i) || bget(flag_bm, i)) return; if (bget(mine_bm, i)) { lose(i); return; } for (k = 0u; k < g_bm_words; k = k + 1u) seen_bm[k] = 0u; bfs_q[qt] = i; qt = qt + 1u; bset(seen_bm, i); while (qh < qt) { cur = bfs_q[qh]; qh = qh + 1u; if (bget(open_bm, cur)) continue; bset(open_bm, cur); opened_cnt = opened_cnt + 1u; n = count_adj(cur); draw_tile(IDX_R(cur), IDX_C(cur), n, num_color(n)); if (n != 0u) continue; /* 空白格:把周围 8 格入队(已入过队的跳过,队列不会溢出) */ r = IDX_R(cur); c = IDX_C(cur); r0 = (r == 0u) ? 0u : (r - 1u); r1 = (r + 1u >= g_bh) ? (g_bh - 1u) : (r + 1u); c0 = (c == 0u) ? 0u : (c - 1u); c1 = (c + 1u >= g_bw) ? (g_bw - 1u) : (c + 1u); for (rr = r0; rr <= r1; rr = rr + 1u) { for (cc = c0; cc <= c1; cc = cc + 1u) { if ((rr == r) && (cc == c)) continue; ni = (rr * g_bw) + cc; if (bget(seen_bm, ni) || bget(open_bm, ni) || bget(flag_bm, ni)) continue; if (qt < NCELLS_MAX) { bset(seen_bm, ni); bfs_q[qt] = ni; qt = qt + 1u; } } } } if (opened_cnt == (g_ncells - g_mines)) win(); } static void place_mines(unsigned safe) { unsigned placed = 0u, i, r, c; unsigned sr = IDX_R(safe); unsigned sc = IDX_C(safe); while (placed < g_mines) { i = rnd_next() % g_ncells; /* 尺寸不再是 2 的幂,用 remu 取模 */ r = IDX_R(i); c = IDX_C(i); /* 首点及其 8 邻居不放雷,保证第一次点击一定能展开 */ if ((r + 1u >= sr) && (r <= (sr + 1u)) && (c + 1u >= sc) && (c <= (sc + 1u))) continue; if (bget(mine_bm, i)) continue; bset(mine_bm, i); placed = placed + 1u; } } static void click_here(void) { unsigned i; if (!in_board()) return; i = cur_index(); if (bget(flag_bm, i)) return; if (!g_first_done) { place_mines(i); g_first_done = 1u; } reveal_at(i); } static void toggle_flag(void) { unsigned i, r, c; if (!in_board()) return; i = cur_index(); if (bget(open_bm, i)) return; r = cur_r - g_board_r0; c = cur_c - g_board_c0; if (bget(flag_bm, i)) { bclr(flag_bm, i); flags_cnt = flags_cnt - 1u; draw_tile(r, c, G_COVER, C_GRAY); } else { bset(flag_bm, i); flags_cnt = flags_cnt + 1u; draw_tile(r, c, G_FLAG, C_YELLOW); } REG_W(LED_ADDR) = g_mines - flags_cnt; } /* ============================ 鼠标事件 ============================ */ /* 关卡内:光标允许落在盘面外那一圈边框上(左上角那格是"返回") */ static void move_cursor(int dr, int dc) { int nr = (int)cur_r + dr; int nc = (int)cur_c + dc; if (nr < (int)(g_board_r0 - 1u)) nr = (int)(g_board_r0 - 1u); if (nr > (int)(g_board_r0 + g_bh)) nr = (int)(g_board_r0 + g_bh); if (nc < (int)(g_board_c0 - 1u)) nc = (int)(g_board_c0 - 1u); if (nc > (int)(g_board_c0 + g_bw)) nc = (int)(g_board_c0 + g_bw); cur_r = (unsigned)nr; cur_c = (unsigned)nc; update_cursor_reg(); } /* ======================= 运行环境状态(背景 / 关卡) ======================= * 程序分成两层,正好对应"伪操作系统 + 关卡"的设想: * 背景程序 main() 从头跑到尾:画开始界面、等鼠标; * 关卡程序 level_main() 跑在 IRQ1(0x100) 的中断服务程序里:点"开始"进入, * 点"返回"就 uret 回到背景被打断的那条指令。 * ========================================================================= */ static unsigned g_in_level; /* 0 = 背景界面,1 = 关卡内 */ static unsigned g_exit; /* 关卡内请求返回 */ /* 背景界面上"开始"按钮的位置(行 20,列 17..22) */ #define START_R 20u #define START_C0 17u #define START_C1 22u #define DIFF_EASY_R 14u #define DIFF_NORMAL_R 16u #define DIFF_HARD_R 18u static void draw_start_screen(void); /* 背景用:光标可以在整个画面上移动 */ static void move_cursor_screen(int dr, int dc) { int nr = (int)cur_r + dr; int nc = (int)cur_c + dc; if (nr < 0) nr = 0; if (nr > (int)(GRID_ROWS - 1u)) nr = (int)(GRID_ROWS - 1u); if (nc < 0) nc = 0; if (nc > (int)(GRID_COLS - 1u)) nc = (int)(GRID_COLS - 1u); cur_r = (unsigned)nr; cur_c = (unsigned)nc; update_cursor_reg(); } /* 处理一个鼠标事件:ev = {btn[20:16], dy[15:8], dx[7:0]} */ static void handle_event(unsigned ev) { unsigned ux = ev & 0xFFu; unsigned uy = (ev >> 8) & 0xFFu; unsigned btn = (ev >> 16) & 31u; int dx = (ux & 0x80u) ? ((int)ux - 256) : (int)ux; int dy = (uy & 0x80u) ? ((int)uy - 256) : (int)uy; unsigned want_flag = (REG_R(REG_INPUT) & 9u) != 0u; /* SW0 或 SW3 = 插旗模式 */ if ((btn & 1u) != 0u) { /* 左键 */ if (g_in_level == 0u) { if (cur_r == DIFF_EASY_R) { set_difficulty(0u); draw_start_screen(); return; } if (cur_r == DIFF_NORMAL_R) { set_difficulty(1u); draw_start_screen(); return; } if (cur_r == DIFF_HARD_R) { set_difficulty(2u); draw_start_screen(); return; } /* 背景:点到"开始"按钮 -> 拉起软件中断,跳到 IRQ1 里的关卡程序 */ if ((cur_r == START_R) && (cur_c >= START_C0) && (cur_c <= START_C1)) { g_in_level = 1u; /* 关卡程序返回时自己清 0 */ REG_W(REG_LEVEL) = 1u; } } else if ((cur_r == (g_board_r0 - 1u)) && (cur_c == (g_board_c0 - 1u))) { g_exit = 1u; /* 关卡:点左上角黄格 = 返回上一层 */ } else if (want_flag != 0u) { toggle_flag(); } else { click_here(); } return; } if ((btn & 2u) != 0u) { /* 右键(PS/2 鼠标有;按键没有) */ if (g_in_level != 0u) toggle_flag(); return; } /* 移动:单位是 1/8 格,累计到 8 就动一格(纯加减比较,没有除法) */ acc_x += dx; acc_y += dy; while (acc_x >= 8) { if (g_in_level != 0u) move_cursor(0, 1); else move_cursor_screen(0, 1); acc_x -= 8; } while (acc_x <= -8) { if (g_in_level != 0u) move_cursor(0, -1); else move_cursor_screen(0, -1); acc_x += 8; } while (acc_y >= 8) { if (g_in_level != 0u) move_cursor(1, 0); else move_cursor_screen(1, 0); acc_y -= 8; } while (acc_y <= -8) { if (g_in_level != 0u) move_cursor(-1, 0); else move_cursor_screen(-1, 0); acc_y += 8; } } /* ============================ 背景程序(伪操作系统)============================ * 从头跑到尾:画开始界面 -> 等鼠标 -> 点到"开始"按钮就拉起软件中断。 * ========================================================================== */ static void draw_start_screen(void) { unsigned i, selected_color; clear_screen(); /* 标题和难度菜单使用 RTL 字模中的大写字母。 */ draw_cell(6u, 18u, G_M, C_RED); draw_cell(6u, 19u, G_I, C_RED); draw_cell(6u, 20u, G_N, C_RED); draw_cell(6u, 21u, G_E, C_RED); draw_cell(6u, 22u, G_S, C_RED); selected_color = C_YELLOW; if (g_difficulty != 0u) selected_color = C_GRAY; draw_cell(DIFF_EASY_R, 18u, G_E, selected_color); draw_cell(DIFF_EASY_R, 19u, G_A, selected_color); draw_cell(DIFF_EASY_R, 20u, G_S, selected_color); draw_cell(DIFF_EASY_R, 21u, G_Y, selected_color); selected_color = C_YELLOW; if (g_difficulty != 1u) selected_color = C_GRAY; draw_cell(DIFF_NORMAL_R, 16u, G_N, selected_color); draw_cell(DIFF_NORMAL_R, 17u, G_O, selected_color); draw_cell(DIFF_NORMAL_R, 18u, G_R, selected_color); draw_cell(DIFF_NORMAL_R, 19u, G_M, selected_color); draw_cell(DIFF_NORMAL_R, 20u, G_A, selected_color); draw_cell(DIFF_NORMAL_R, 21u, G_L, selected_color); selected_color = C_YELLOW; if (g_difficulty != 2u) selected_color = C_GRAY; draw_cell(DIFF_HARD_R, 18u, G_H, selected_color); draw_cell(DIFF_HARD_R, 19u, G_A, selected_color); draw_cell(DIFF_HARD_R, 20u, G_R, selected_color); draw_cell(DIFF_HARD_R, 21u, G_D, selected_color); /* "START" 按钮:绿色实心条,上下各一条黄线框住它。 */ for (i = START_C0; i <= START_C1; i = i + 1u) { draw_cell(START_R - 1u, i, G_BAR, C_YELLOW); draw_cell(START_R, i, G_SOLID, C_GREEN); draw_cell(START_R + 1u, i, G_BAR, C_YELLOW); } /* 按钮上的字母不能用 C_BLACK:VGA 里像素色 = 字模点 ? 字符色 : 背景色, * 而背景是黑的,所以“黑字”的字形点和空白处全黑 —— 整格完全看不见。 * 这里用黄色:黑底黄字,和上下的黄线框也更呼应。 */ draw_cell(START_R, 17u, G_S, C_YELLOW); draw_cell(START_R, 18u, G_T, C_YELLOW); draw_cell(START_R, 19u, G_A, C_YELLOW); draw_cell(START_R, 20u, G_R, C_YELLOW); draw_cell(START_R, 21u, G_T, C_YELLOW); /* 下方提示条 */ for (i = 12u; i <= 27u; i = i + 1u) { draw_cell(24u, i, G_BAR, C_CYAN); } set_bg(C_BLACK); cur_r = START_R; cur_c = (START_C0 + START_C1) / 2u; update_cursor_reg(); REG_W(LED_ADDR) = 0x0001u; /* LED = 背景状态码 */ REG_W(REG_SEG) = 0u; } /* ============================ 关卡(游戏)程序 ============================ * **运行在 IRQ1 的中断服务程序里**(入口 isr1_entry,固定地址 0x100): * 背景 main() 点"开始" -> 写 LEVEL 寄存器 -> 硬件拉起 irq_i[1] * -> CPU 跳到 0x100 -> isr1_entry 保存现场、打开 MIE、调用本函数 * -> 这里跑完整一局(鼠标中断 0x200 优先级更高,会嵌套进来把事件放进队列, * 所以本循环只要轮询队列) * -> 点左上角黄格 = 返回:清 LEVEL、return -> isr1_entry 恢复现场并 uret * -> 回到背景程序被打断的那条指令,继续等下一次"开始"。 * ========================================================================== */ /* 关卡里“返回上一层”的黄格 = 棋盘左上角外面一格(随难度尺寸移动) */ void level_main(void) { unsigned r; /* ---- 关卡初始化 ---- */ for (r = 0u; r < g_bm_words; r = r + 1u) { mine_bm[r] = 0u; open_bm[r] = 0u; flag_bm[r] = 0u; seen_bm[r] = 0u; } opened_cnt = 0u; flags_cnt = 0u; g_first_done = 0u; g_state = ST_PLAY; g_exit = 0u; acc_x = 0; acc_y = 0; cur_r = g_board_r0 + (g_bh / 2u); /* 光标从棋盘中心开始 */ cur_c = g_board_c0 + (g_bw / 2u); clear_screen(); draw_frame(); draw_all_covered(); draw_cell(g_board_r0 - 1u, g_board_c0 - 1u, G_BAR, C_YELLOW); /* 左上角黄格 = 返回 */ set_bg(C_BLACK); update_cursor_reg(); REG_W(LED_ADDR) = g_mines; REG_W(REG_SEG) = g_mines; /* 数码管显示剩余雷数 */ /* ---- 关卡主循环 ---- */ while (g_exit == 0u) { if (g_evq_head != g_evq_tail) { unsigned ev = g_evq[g_evq_head]; g_evq_head = (g_evq_head + 1u) & EVQ_MASK; if (g_state == ST_PLAY) { handle_event(ev); } else if ((ev >> 16) & 1u) { g_exit = 1u; /* 分出胜负后任意点击 = 返回 */ } } else if (g_state != ST_PLAY) { /* 分出胜负后让背景闪动(用 VGA 帧计数做时间基准,不需要定时器) */ static unsigned phase; if ((REG_R(REG_FRAME) & 31u) == 0u) { phase = phase ^ 1u; if (phase != 0u) { set_bg((g_state == ST_WIN) ? C_GREEN : C_RED); } else { set_bg(C_BLACK); } } } } REG_W(REG_LEVEL) = 0u; /* 清软件中断:否则下次点"开始"没有新的上升沿 */ g_in_level = 0u; /* 回到背景状态 */ draw_start_screen(); /* 恢复开始界面 */ } /* ============================ 背景主程序 ============================ */ extern void crt_zero_bss(void); int main(void) { crt_zero_bss(); /* 清 .bss(上电 RAM 本来就是 0,保险起见) */ rnd_state = REG_R(REG_TIME) | 1u; /* 用自由计数器当随机种子 */ set_difficulty(1u); /* 默认中级:16x16,40 颗雷 */ REG_W(REG_MCTRL) = 1u; /* 打开鼠标中断(IRQ2,入口 0x200) */ REG_W(REG_LEVEL) = 0u; /* 清关卡软中断 */ g_in_level = 0u; draw_start_screen(); /* 背景循环:只服务鼠标事件;点"开始"后会被 IRQ1 里的关卡程序"接管", * 关卡返回时又回到这个循环里继续。 */ for (;;) { if (g_evq_head != g_evq_tail) { unsigned ev = g_evq[g_evq_head]; g_evq_head = (g_evq_head + 1u) & EVQ_MASK; handle_event(ev); } } }