/* ----------------------------------------------------------------------------- * saolei.c —— 纯 C 扫雷(RV32IM 子集:只用这颗核实现了的指令) * * 显示:把每个格子写成显存里的 1 个字节(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,40 颗雷(16 是 2 的幂 => 索引全用移位,颜色/字形查表也不用除法) * 网格:40 列 x 30 行,棋盘居中放在 (行 7..22, 列 12..27) * ---------------------------------------------------------------------------*/ #include "mmio.h" #define BW 16u /* 棋盘宽(格) */ #define BH 16u /* 棋盘高(格) */ #define NCELLS (BW * BH) /* 256 */ #define NMINES 40u #define BM_WORDS (NCELLS / 32u) /* 8 */ #define BOARD_C0 12u /* 棋盘左上角在网格里的列 */ #define BOARD_R0 7u /* 棋盘左上角在网格里的行 */ /* 游戏状态 */ #define ST_PLAY 0u #define ST_WIN 1u #define ST_LOSE 2u /* ---- 棋盘状态:3 个位图,每个 8 个字(256 位) ---- */ static unsigned mine_bm[BM_WORDS]; static unsigned open_bm[BM_WORDS]; static unsigned flag_bm[BM_WORDS]; /* 其它全局状态(全部进 .bss,靠 start.c 清零) */ static unsigned opened_cnt; static unsigned flags_cnt; static unsigned g_state; static unsigned g_first_done; static unsigned rnd_state; static unsigned cur_r, cur_c; /* 光标所在的网格格 */ static int acc_x, acc_y; /* 指针累积量(1/8 格,有符号) */ static unsigned bfs_q[NCELLS]; /* 揭开用的队列 */ /* ============================ 小工具 ============================ */ /* 位图取位/置位/清位: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(gr * GRID_COLS + gc, CELL(glyph, color)); } /* 把棋盘格 (r,c) 画出来 */ static void draw_tile(unsigned r, unsigned c, unsigned glyph, unsigned color) { draw_cell(BOARD_R0 + r, 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 < BOARD_R0) return 0u; if (cur_r >= (BOARD_R0 + BH)) return 0u; if (cur_c < BOARD_C0) return 0u; if (cur_c >= (BOARD_C0 + BW)) return 0u; return 1u; } static unsigned cur_index(void) { return ((cur_r - BOARD_R0) << 4) + (cur_c - BOARD_C0); /* *BW(=16) 用移位 */ } /* LCG 随机数:只用 mul/add/移位 */ static unsigned rnd_next(void) { rnd_state = rnd_state * 1103515245u + 12345u; return rnd_state >> 16; } /* ============================ 画面 ============================ */ static void clear_screen(void) { unsigned i; for (i = 0u; i < (GRID_COLS * GRID_ROWS); i = i + 1u) { fb_write(i, 0u); } } static void draw_frame(void) { unsigned gc, gr; for (gc = (BOARD_C0 - 1u); gc <= (BOARD_C0 + BW); gc = gc + 1u) { draw_cell(BOARD_R0 - 1u, gc, G_SOLID, C_CYAN); draw_cell(BOARD_R0 + BH, gc, G_SOLID, C_CYAN); } for (gr = (BOARD_R0 - 1u); gr <= (BOARD_R0 + BH); gr = gr + 1u) { draw_cell(gr, BOARD_C0 - 1u, G_SOLID, C_CYAN); draw_cell(gr, BOARD_C0 + BW, G_SOLID, C_CYAN); } } static void draw_all_covered(void) { unsigned r, c; for (r = 0u; r < BH; r = r + 1u) { for (c = 0u; c < 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 = i >> 4, c = i & 15u, n = 0u, rr, cc; unsigned r0 = (r == 0u) ? 0u : (r - 1u); unsigned r1 = (r + 1u >= BH) ? (BH - 1u) : (r + 1u); unsigned c0 = (c == 0u) ? 0u : (c - 1u); unsigned c1 = (c + 1u >= BW) ? (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 << 4) + cc); } } return n; } static void lose(unsigned hit) { unsigned k; for (k = 0u; k < NCELLS; k = k + 1u) { if (bget(mine_bm, k)) { if (k == hit) { draw_tile(k >> 4, k & 15u, G_BOOM, C_RED); } else if (!bget(flag_bm, k)) { draw_tile(k >> 4, k & 15u, G_MINE, C_RED); } } } REG_W(LED_ADDR) = 0xDEADu; g_state = ST_LOSE; } static void win(void) { unsigned k; for (k = 0u; k < NCELLS; k = k + 1u) { if (bget(mine_bm, k)) { draw_tile(k >> 4, k & 15u, 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; if (bget(open_bm, i) || bget(flag_bm, i)) return; if (bget(mine_bm, i)) { lose(i); return; } bfs_q[qt] = i; qt = qt + 1u; 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(cur >> 4, cur & 15u, n, num_color(n)); if (n != 0u) continue; /* 空白格:把周围 8 格入队(队列有界,满了就丢,不会越界) */ r = cur >> 4; c = cur & 15u; r0 = (r == 0u) ? 0u : (r - 1u); r1 = (r + 1u >= BH) ? (BH - 1u) : (r + 1u); c0 = (c == 0u) ? 0u : (c - 1u); c1 = (c + 1u >= BW) ? (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 << 4) + cc; if (qt < NCELLS) { bfs_q[qt] = ni; qt = qt + 1u; } } } } if (opened_cnt == (NCELLS - NMINES)) win(); } static void place_mines(unsigned safe) { unsigned placed = 0u, i, r, c; unsigned sr = safe >> 4; unsigned sc = safe & 15u; while (placed < NMINES) { i = rnd_next() & 255u; /* NCELLS = 256,正好 2 的幂,不需要取模 */ r = i >> 4; c = i & 15u; /* 首点及其 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 - BOARD_R0; c = cur_c - 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) = NMINES - 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)(BOARD_R0 - 1u)) nr = (int)(BOARD_R0 - 1u); if (nr > (int)(BOARD_R0 + BH)) nr = (int)(BOARD_R0 + BH); if (nc < (int)(BOARD_C0 - 1u)) nc = (int)(BOARD_C0 - 1u); if (nc > (int)(BOARD_C0 + BW)) nc = (int)(BOARD_C0 + 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 /* 背景用:光标可以在整个画面上移动 */ 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) { /* 背景:点到"开始"按钮 -> 拉起软件中断,跳到 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 == (BOARD_R0 - 1u)) && (cur_c == (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; clear_screen(); /* 标题区:两排雷图案,纯图形(字模里没有字母;要显示文字就得扩字模,见 README) */ for (i = 0u; i < 6u; i = i + 1u) { draw_cell(6u, 15u + i * 2u, G_MINE, C_RED); draw_cell(8u, 15u + i * 2u, G_MINE, C_RED); } /* "开始"按钮:绿色实心条,上下各一条黄线框住它 */ 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); } /* 下方提示条 */ 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 * -> 回到背景程序被打断的那条指令,继续等下一次"开始"。 * ========================================================================== */ #define RET_R (BOARD_R0 - 1u) #define RET_C (BOARD_C0 - 1u) void level_main(void) { unsigned r; /* ---- 关卡初始化 ---- */ for (r = 0u; r < BM_WORDS; r = r + 1u) { mine_bm[r] = 0u; open_bm[r] = 0u; flag_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 = BOARD_R0 + 8u; cur_c = BOARD_C0 + 8u; clear_screen(); draw_frame(); draw_all_covered(); draw_cell(RET_R, RET_C, G_BAR, C_YELLOW); /* 左上角黄格 = 返回 */ set_bg(C_BLUE); update_cursor_reg(); REG_W(LED_ADDR) = NMINES; REG_W(REG_SEG) = NMINES; /* 数码管显示剩余雷数 */ /* ---- 关卡主循环 ---- */ 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_BLUE); } } } } 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; /* 用自由计数器当随机种子 */ 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); } } }