Skip to content

实验部分

课程的实验基于一个物理嵌入式系统,核心硬件是 Espressif ESP32-C3 开发板(搭载 RISC-V 微处理器),配合一块含 6 个按钮和 8 个开关的定制 PCB,以及一个 8×32 的 LED 点阵显示屏。LED 显示屏被用来可视化内存中的数据:一个包含 8 个 unsigned 32-bit int 元素的数组,每个元素对应显示屏的一行,每个 bit 对应一行中的一个像素,最左边的 LED 是 MSB,最右边是 LSB。

每周有一个 Lab(课内 2.5 小时完成)和一个 Postlab(课后独立完成)。

Lab 1 入门——GPIO 与位运算

核心概念:位运算、指针

学生实现三个操作 GPIO 引脚的 C 函数:pinSetup(配置引脚为输入或输出)、pinWrite(向输出引脚写入数字值)、pinRead(从输入引脚读取数字值)。这三个函数的功能等同于 Arduino 的 pinModedigitalWritedigitalRead,但本课程要求学生从底层实现它们,直接操作微控制器特定内存地址中的 bit。

pinRead 为例:ESP32-C3 的地址 0x6000403C 保存了一个 32 位值,每一位对应一个 GPIO 输入引脚的状态。要读取某个引脚,需要先用指针访问该地址,再用位移和位掩码提取对应的那一位。

验证方式:把两个 LED 接到两个输出引脚上观察效果,用按钮输入和串口监视器验证输入引脚功能,最后用两个按钮的逻辑运算(如异或 XOR)控制两个 LED 输出。

Postlab 1 打位游戏——移位与位运算

核心概念:位移、位运算、回绕

基于经典游戏"Kill the Bit"。8 个相邻 LED 中有一个被点亮,持续向右移动并在最右端回绕到最左端。8 个开关分别对应 8 个 LED。每一步,将开关状态与 LED 状态逐位比较:开关"上"匹配 LED"亮",开关"下"匹配 LED"灭";不匹配的位置 LED 在下一步亮起,匹配的则灭掉。玩家目标是通过翻转开关使所有 LED 熄灭。

学生需要实现的三个关键部分:用 unsigned 8-bit int 表示 LED 状态并实现右移回绕行为;用 pinRead 和位移把 8 个开关状态存入另一个 unsigned 8-bit int;写一行位运算代码比较 LED 状态和开关状态。

Lab 2 ASCII 字符显示——数组与字符串

核心概念:ASCII 编码、空终止字符串、数组、屏幕缓冲区

学生用 7 个开关输入一个 8 位值(MSB 固定为 0),解释为 ASCII 字符并在串口打印。加入按钮后,每次按下按钮采样一次开关值,追加到字符数组中形成字符串;输入空字符 '\0' 时打印整个字符串并重置数组。

然后学生在 LED 显示屏上渲染字符。课程提供了一个 ascii.h 头文件,包含每个 ASCII 字符的 8×8 位图(bitmap),存为二维数组。学生写一个函数,把字符串的前 4 个字符的位图放入屏幕缓冲区数组,让数据传输到 LED 显示屏后正确显示文字。这要求操作数组元素整体以及元素内部的单个 bit。

Postlab 2 滚动文字——数组与位操作进阶

核心概念:数组操作、位操作、帧(frame)渲染

在 Lab 2 的基础上实现文字在 LED 显示屏上的滚动效果:每隔约 100 毫秒把内容整体左移一列。

学生先实现一个类似 strlen 的函数计算字符串长度,据此算出总共需要渲染的帧数(即消息的总列数)。核心函数需要根据当前循环次数,确定应该显示消息的哪一段、每个字符显示完整还是只显示一部分,再把对应的位图数据正确地写入屏幕缓冲区。与 Lab 2 不同,滚动时首尾字符可能只显示一部分,需要处理对齐问题。

Lab 3 贪吃蛇之结构体与编码设计

核心概念:结构体、位编码设计、位操作、指针

实现贪吃蛇游戏的基础框架。游戏棋盘就是 8×32 的 LED 显示屏。

位置编码设计:每个位置用一个 unsigned 8-bit int 表示,高 5 位编码 x 坐标(列,0–31),低 3 位编码 y 坐标(行,0–7)。这种设计只用一半内存(相比分别用两个 8-bit int 存 x 和 y),而且因为没有多余的未使用 bit,取模行为是天然内建的——坐标溢出时自动回绕,不需要额外代码实现。

学生实现的函数:getXgetYsetXsetY(用位移和掩码提取/修改坐标);setPixel(根据位置设置屏幕缓冲区中对应的 bit);drawBoard(用 setPixel 渲染蛇和食物);updateSnake(通过指向 Snake 结构体的指针,移动蛇一步);随机生成食物的函数。

蛇用一个 C 结构体表示,成员包括:body 数组(每个元素是一个位置编码)、方向、长度。body 数组的第一个元素是蛇头。

Postlab 3 贪吃蛇之游戏逻辑

核心概念:条件判断、结构体指针操作

完成游戏逻辑。实现 snakeAteFood(判断蛇头是否和食物位于同一位置)和 snakeCollisionCheck(判断蛇头是否撞到自己的身体)。加入按下重置按钮后重新开始的功能。

最后学生自选一项增强功能独立实现(不提供任何指导),选项包括:蛇撞到边界时游戏结束;暂停按钮;吃食物加速、不吃减速直到太慢则游戏结束;计分系统;蛇死亡时可视化衰减效果。

Lab 4 汇编入门——C 到 RISC-V 翻译

核心概念:RISC-V 汇编、C 到汇编的手动翻译、寄存器使用

学生把 Lab 1 中写过的 C 函数(pinReadpinWritepinSetup)手动翻译成 RISC-V 汇编。先从 pinRead 开始,因为它的实现不需要控制流指令,学生可以先熟悉计算类指令;然后实现需要条件分支的 pinWritepinSetup。另外还要把 Lab 3 的 setPixel 也用汇编重写。

关键约束:本周学生尚未学习调用约定和栈,因此只允许使用 x0 和 a0–a7 这些寄存器;所有 procedure 都不调用其他 procedure;每个 procedure 末尾的返回指令已在 starter code 中提供;函数参数放在哪个寄存器、返回值放在哪个寄存器,由实验指导书直接告知。2023 年春季第二学期的版本还禁止使用伪指令(pseudo-instructions),要求学生只用处理器真正能执行的指令。

Postlab 4 冒泡排序——汇编中的数组操作

核心概念:排序算法的汇编实现、指令编码

学生把冒泡排序的 C 实现翻译成 RISC-V 汇编。每次交换(swap)发生时调用一个 C 函数在 LED 显示屏上可视化 8 个元素的数组状态。调用该函数所需的 procedure call 代码和 calling convention 相关指令由课程提供(因为学生此时还没学过)。

额外练习:探索一段汇编 procedure 的反汇编(disassembly),重点关注 RISC-V 指令如何被编码为二进制并存储在内存中。寄存器使用约束和伪指令限制与 Lab 4 相同。

Lab 5 生命游戏——调用约定与栈

核心概念:RISC-V procedure call、调用约定(calling convention)、栈(stack)、寄存器分类

用 RISC-V 汇编实现 Conway's Game of Life。这是第一个要求学生完整遵守调用约定的实验——可以使用全部 32 个寄存器,但必须自己负责 callee-saved 寄存器的保存和恢复。

游戏规则简化为三条:活细胞有 2 或 3 个活邻居则存活;死细胞恰好有 3 个活邻居则复活;其他活细胞死亡,其他死细胞保持死亡。游戏棋盘是 8×32 LED 显示屏,边界回绕。

学生需要翻译的核心函数:checkNeighbors(给定棋盘指针和坐标,返回某个细胞有多少活邻居)。这个函数内部会调用 tallyNeighbors(实际计数活邻居数量的辅助函数),因此学生必须正确处理嵌套 procedure call 中的调用约定——在调用 tallyNeighbors 前保存需要的寄存器到栈上,调用返回后再恢复。

对于 tallyNeighbors,课程提供了完整的汇编实现,但故意没有加入调用约定相关的指令。学生需要在合适的位置添加 sp 的增减、sw(压栈)和 lw(弹栈)指令,使其符合 calling convention。这样学生可以专注于理解调用约定本身,而不用同时操心算法逻辑。

最后学生翻译整个棋盘更新函数(board update function)到汇编,完成后系统就能运行 Game of Life。

Postlab 5 快速排序——综合运用

核心概念:递归 procedure 的调用约定、栈的策略性使用

把快速排序翻译成 RISC-V 汇编。与 Postlab 4 不同的是:学生需要分别实现 partition procedure 和调用它的 quicksort procedure;快速排序是递归的,所以必须策略性地使用栈来保存和恢复寄存器;学生需要自己编写调用可视化函数的代码(包括 procedure call 本身和 calling convention 所需的所有指令),不再由课程提供。这是对第 5 周所学内容(调用约定、栈)的综合考察。