
简介这份寄存器实验报告面向数字电路、FPGA与Verilog HDL课程的在校学生及自学者用于完成移位寄存器设计类实验与课程报告撰写。文档围绕寄存器分类与工作原理展开梳理了串行输入串行输出、串行输入并行输出、并行输入串行输出、并行输入并行输出四种移位寄存器的差异与适用场景并给出八位并行输入串行输出右移寄存器与八位串行输入并行输出寄存器的完整Verilog源码。实验流程覆盖Quartus II建工程、选择Cyclone系列EP1C6Q240C8芯片、编译与功能及序仿真、引脚分配表、生成SOF文件再到借助TD-EDA实验箱、SOPC开发板与ByteBlaster II下载电缆完成硬件验证读者可据此复现接线、配置与结果核对全过程也可直接参考其报告结构与仿真结论整理成文。资源包为单个doc文档共1个文件约108KB体积轻便便于打印与批注目前已有1715人学习下载适合作为实验预习、上机对照与报告模板使用。1. 寄存器实验报告.doc 为什么总是写不厚做嵌入式实验交付物常常是一个叫「寄存器实验报告.doc」的文件。多数人交上去的是现象流水账贴一张 LED 亮了的照片写一句配置成功现象符合预期。换一块板、换一颗芯片这份报告就一点用没有了。寄存器是所有裸机代码的入口——89C52RC 的 SFR、STM32 的外设映射、PCIe 桥接器挂在配置空间里的那一堆字、Modbus 从站的保持寄存器表本质都是同一件事往一个地址写特定比特再读回验证。报告要记的就是这条链路地址是多少、位域怎么切、写序为什么是这个顺序、读回值对不对、不对时怎么定位。适合三类人看单片机课程的学生、从业务代码转到裸机的工程师、需要出具板级验证记录的测试人员。反直觉的地方在于一份能被人反复翻的报告最厚的那一节往往不是成功而是第一次为什么失败、后来改了什么。2. 寄存器实验的底座地址映射、位域与最小可复现环境寄存器实验写不出深度九成是底座没搭清楚地址从哪来、位域怎么算、用什么工具确认写进去了。这三件事定了后面无论做 GPIO、定时器还是 ADC记录方式都是同一套模板。2.1 从 89C52RC 的 SFR 到 STM32 的 MMIO寄存器到底是什么89C52RC 这类 8051 内核芯片把 P0、TMOD、SCON、TCON 放在独立的特殊功能寄存器空间地址落在 0x80 到 0xFF部分还能按位寻址写起来就是P1 0xFE;一行。编译器认识sfr、sbit关键字地址由头文件给出人不需要算偏移。到了 ARM Cortex-M 这一代外设寄存器被拉进了统一的内存地址空间从 0x40000000 起按总线分段写寄存器和写内存没有语法差别靠指针解引用完成。再往外扩PCIe 桥接器的配置空间是 Bus/Device/Function 加偏移的三段式寻址Modbus 从站则是协议层定义的一张 0x0000 到 0xFFFF 的寄存器表。形态不同读-改-写的心智模型是同一个。平台访问方式典型地址一次写入长什么样89C52RCSFR 直接寻址0x80~0xFFP1 0xFE;STM32F1AHB/APB 内存映射0x40000000 起*(volatile uint32_t*)0x40020018 1u5;PCIe 桥接器PCI 配置空间Bus/Dev/Fn offset主机用 CFG 读写事务Modbus 从站协议寄存器表0x0000~0xFFFF06 功能码写单个保持寄存器UVM 寄存器模型镜像值 / 期望值不占物理地址predict()更新镜像最后一行值得单独说验证侧的寄存器模型里那个镜像值只是软件认为硬件现在是什么和硬件真实值完全可以分叉。报告里如果引用镜像值就得同时写明该值来自模型预测未经读回校验否则后面的人拿着这份报告对不上硬件会怀疑整份实验。2.2 地址和位域怎么写死宏、结构体与对齐 CMSIS 头文件同一条寄存器写法有好几种报告附录里最好把用到的宏全贴出来不然别人复现时算错位移。/* 方式一基址宏 强转指针最贴近数据手册方便和手册逐行对照 */ #define GPIOA_BASE 0x40020000UL #define GPIOA_MODER (*(volatile uint32_t *)(GPIOA_BASE 0x00U)) #define GPIOA_ODR (*(volatile uint32_t *)(GPIOA_BASE 0x14U)) #define GPIOA_BSRR (*(volatile uint32_t *)(GPIOA_BASE 0x18U)) #define MODER_PIN5_SHIFT 10U /* MODER5 占第 10、11 位 */ #define MODER_PIN5_OUT (1U MODER_PIN5_SHIFT) /* 方式二结构体映射。字段顺序必须和手册偏移逐字节对齐否则整体错位 */ typedef struct { volatile uint32_t MODER; /* 0x00 模式 */ volatile uint32_t OTYPER; /* 0x04 输出类型 */ volatile uint32_t OSPEEDR; /* 0x08 输出速度 */ volatile uint32_t PUPDR; /* 0x0C 上下拉 */ volatile uint32_t IDR; /* 0x10 输入数据 */ volatile uint32_t ODR; /* 0x14 输出数据 */ volatile uint32_t BSRR; /* 0x18 置位复位 */ } GPIO_TypeDef; #define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)volatile不是为了防止编译器优化这句口号它真正拦的是三件事把连续两次读合并成一次、把写暂存到寄存器里延后下发、以及把写了但没人读的语句整条删掉。在调试版本里把volatile去掉往往会看到写寄存器无效的假象。结构体写法可读性好但手册一旦改动偏移整个结构体会连锁错位宏写法虽然丑grep起来快排错时更省事。2.3 最小可复现环境编译、下载与寄存器快照一份能被复现的报告至少要留三条命令怎么编、怎么下载、怎么把寄存器读出来。# 1) 编译出带调试信息的 elf-O0 保证寄存器读写不被重排 arm-none-eabi-gcc -mcpucortex-m3 -mthumb -O0 -g3 \ -T stm32f103.ld -o reg_lab.elf main.c # 2) 起调试服务通过 ST-Link 的 SWD 挂上目标板 openocd -f interface/stlink.cfg -f target/stm32f1x.cfg # 3) 另开终端连调试端口读 GPIOA 基址起连续 7 个字 telnet localhost 4444 halt mdw 0x40020000 7 # 依次读出 MODER..BSRR 共 7 个 32 位寄存器 resume-O0 -g3是关键优化等级一上去编译器会把相邻的寄存器写合并、把只写一次的中间值消掉实验现象就变得不可解释。mdw按字32 位连续读读回的 7 个值正好对应结构体里的 7 个成员把它们抄进报告就是一份寄存器快照。习惯用printf的人也可以把快照格式化后从串口打出来好处是能和波形、日志放在同一份文件里坏处是串口一慢就会掩盖时序问题两种都要留一手。3. 三类典型寄存器实验的记录方式GPIO、定时器中断与 ADC/DAC寄存器实验报告写不厚通常是选错了记录对象花了大量篇幅描述波形却漏掉了写入顺序和读回值。下面按三种最常布置的实验说清每一步该记什么。3.1 GPIOBSRR 与 ODR 的差别决定报告里贴哪段代码/* 第一步别忘了开时钟时钟没开时写进去的值会静默丢失 */ RCC_APB2ENR | (1U 2U); /* IOPAEN使能 GPIOA */ /* 把 PA5 配成通用推挽输出先清零两位模式再写入 01 */ GPIOA_MODER ~(3U MODER_PIN5_SHIFT); GPIOA_MODER | MODER_PIN5_OUT; /* 置位用 BSRR 的低 16 位复位用高 16 位一次写原子完成 */ GPIOA_BSRR (1U 5U); /* PA5 输出高 */ GPIOA_BSRR (1U (5U 16U)); /* PA5 输出低 */这里有个报告里必须写明的坑BSRR 是只写寄存器读回来的值恒为 0。所以写完 BSRR 再读 BSRR 校验这个动作不但无效还会让人误判成写失败。要校验输出电平必须去读 ODR 的对应位。另一个坑是MODER的读-改-写如果直接用|而不先清零第二次配置同一个引脚时两位会叠成非法组合比如00 | 01变成01没问题但10 | 01就变成11模拟模式引脚就不输出了。寄存器偏移读写属性报告里该记什么MODER0x00R/W每个引脚 2 位写出实际二进制值IDR0x10R输入电平与外部实测电压对照ODR0x14R/W当前输出电平可作为写后读回校验点BSRR0x18只写只记写入值不记读回值3.2 中断与异常NVIC ISER、STIR 与向量表偏移 VTOR中断类实验最容易写成能进中断了但报告真正该记的是使能到触发之间的每个寄存器动作。/* 1) 向量表搬到 SRAM 起始处偏移 0x200低 9 位必须为 0对齐要求 */ SCB_VTOR 0x20000000U | 0x200U; __DSB(); /* 数据同步屏障确保写入对取指单元可见 */ /* 2) 使能 EXTI0 中断也就是编号 6 的异常ISER0 第 6 位置 1 */ NVIC_ISER0 (1U 6U); /* 3) 不依赖外部引脚直接软件触发同一个异常方便反复复现 */ NVIC_STIR 6U; /* 4) 需要挂起时写 ISPR清挂起写 ICPR优先级写 IPR[6] 的高 4 位 */STIR 同样是只写寄存器读回无意义但它的价值极高写一次触发一次不需要真的去按按键或者抖动引脚中断服务程序的入口逻辑可以被反复验证。VTOR 的低 9 位是保留位写非零值属于未定义行为报告里要把算出来的对齐偏移量写清楚。改完 VTOR 之后一定要跟一条数据同步屏障否则会出现改了向量表却还跳旧地址的现象这种问题在仿真器里未必复现上板才暴露。3.3 ADC/DACDAC DHR 的双缓冲与 ADC 校准位的生效时机DAC 这一侧数据寄存器 DHR 和输出寄存器 DOR 之间隔着一级双缓冲写进去的值不会立刻出现在引脚上。/* 12 位右对齐数据寄存器先写 DHR硬件在触发事件后搬到 DOR */ DAC_DHR12R1 2048U; /* 使能通道 1然后读回确认使能位真的立起来了 */ DAC_CR | (1U 0U); /* EN1 */ while ((DAC_CR (1U 0U)) 0U) { /* 等使能位读回为 1读不回来说明参考电压或时钟没就绪 */ }ADC 这一侧的坑更隐蔽带偏移校准OFFTRIM 一类的转换器校准位没生效之前读到的结果寄存器里装的是未补偿的原始码值。写了校准寄存器不等于校准已经作用到数据通路中间往往差几个转换周期或一次内部更新事件。报告里必须写一行明确的话——本次读到的码值是校准前还是校准后——否则后面的人拿两条曲线对比会以为是自己算错了缩放系数。放大器、外部 ADC 芯片同理配置寄存器写完要读回转换结果要标注采样时刻。器件侧关键寄存器写序要点读回校验点DAC 通道 1DHR12R1 / DOR先写 DHR再靠触发搬移DAC_CR 的 EN1 位ADC 通用CR2 的 ADON / DR使能后等稳定再启动转换DR 高 16 位与校准标志外部 ADC 芯片配置寄存器组按手册顺序写含 CRC/模式位读回配置寄存器本身3.4 Modbus 03 读保持寄存器地址映射表比代码更重要上位机侧读寄存器代码只有几行出错几乎全出在地址和字节序上。from pymodbus.client import ModbusSerialClient cli ModbusSerialClient(port/dev/ttyUSB0, baudrate9600, parityN, stopbits1, bytesize8, timeout1) cli.connect() # 03 功能码从站 1起始地址 0x0000连续读 2 个保持寄存器 rr cli.read_holding_registers(address0x0000, count2, slave1) if not rr.isError(): raw rr.registers # 列表元素是 16 位无符号整数 temp raw[0] / 10.0 # 缩放系数来自设备手册不能凭感觉 print(f0x0000 {raw[0]}, 折算后 {temp})最常见的错是把手册里的 1 基地址直接当协议地址用手册写40001协议里要发的是 0x0000差 1 会让整段读取整体偏移。第二个错是字序32 位量拆成两个 16 位寄存器时高低字谁在前由设备决定报告里必须把这一条写进映射表而不是留给读者猜。手册地址协议地址名称数据类型缩放字序400010x0000温度uint16÷10不涉及400020x0001累计流量高字uint32 高÷100高字在前400030x0002累计流量低字uint32 低÷100高字在前4. 寄存器写不进去怎么办把排错过程写进报告写不进寄存器这件事几乎每个做裸机的人都遇到过。与其在报告里把它藏起来不如单独列一节因为这一节最能体现对器件的理解深度。4.1 先分辨只读、只写、写 1 清除与影子寄存器同样是读回值不对原因完全不同先归类再动手。现象常见原因验证手段读回恒为 0外设时钟没开或该寄存器只写查 RCC 使能位换一个可读寄存器对照写 1 后读回 0写 1 清除W1C语义按中断标志位的方式对待别当失败值延迟一个周期才变影子寄存器 双缓冲等更新事件后再读只有部分位改变位域被写保护或锁定查锁定寄存器与解锁序列读回值与写入值差整段地址算错或结构体错位用绝对地址直写一次做对照4.2 使能顺序与写保护时钟、解锁序列、锁定寄存器绝大部分写不进去其实是顺序问题。可靠的顺序是开时钟 → 外设复位 → 解锁如果器件要求→ 配置功能寄存器 → 使能输出或转换 → 读回校验 → 加锁。中间任何一步提前或推后都可能让后面的写静默失败。带锁位的器件要特别注意比如某些频率合成器需要按地址 数据成对写入任何一次字节错位都会让后续配置全部落到错误的寄存器上而带 SPI 接口的 ADC写入失败经常是因为模式位或 CRC 校验没按手册打开芯片直接丢弃了整个帧。下面这个读回校验函数可以直接抄进实验代码关键在mask。#include stdbool.h /* 写后读回校验mask 只圈出可写且本次要校验的位避免被只读位干扰 */ static bool reg_write_check(volatile uint32_t *reg, uint32_t val, uint32_t mask) { *reg val; /* 执行写入读-改-写已在调用方完成 */ __DSB(); /* 数据同步屏障等写真正到达外设总线 */ return ((*reg mask) (val mask)); }__DSB()不能省尤其在外设时钟远低于内核时紧接着的一次读很可能走在写的前面导致读回旧值。mask必须排除只读位、保留位和 W1C 位如果图省事传0xFFFFFFFF遇到任何一个只读位为 1 就会永远校验失败白折腾半天。4.3 交叉验证SWD 在线读回与总线抓包单靠 CPU 视角读回还不够因为读回值可能被缓存或时序污染。用调试器直接读物理地址是第二条独立路径# 板上在线读绕过 CPU直接从总线视角看寄存器当前值 halt mdw 0x40020000 7 resume # 主机侧看 PCI 配置空间前 64 字节桥接器类器件常用这种方式核对 BAR lspci -s 01:00.0 -xxx | head -8如果 CPU 读回和调试器读回不一致问题基本落在总线时序或时钟域上而不是软件逻辑。SPI、I2C 这类串行外设再补一条路径用逻辑分析仪抓一次写操作把地址字节、数据字节和高低电平对齐着看能直接确认是地址发错还是从机没应答。三路信号对得上这一节就可以收进报告作为已排除项。4.4 失败记录该写多细每条失败记录至少留五项时间、操作内容、写前寄存器值、写后读回值、当时的判断。不用写成长文08:32 写 DAC_CR 的 EN1 位写前 0x00000000写后读回 0x00000000判断为参考电压未就绪这样一行就够。真正让报告有复现价值的是这行记录后面跟的那句后来改了什么。5. 用 Python 自动生成寄存器实验报告位域表、快照与波形一起落进 Word手工把 7 个快照值逐位拆成二进制再填表是寄存器实验里最容易出错也最没价值的环节。把快照存成 JSON用脚本生成位域表报告的可信度立刻上一个台阶。5.1 从寄存器快照生成位域解析表import json from docx import Document # 位域名 - (高位, 低位, 含义)数值全部来自数据手册不要凭记忆填 BITS { MODER5: (11, 10, 00 输入 / 01 输出 / 10 复用 / 11 模拟), OSPEED5: (11, 10, 输出速度档位), OT5: (5, 5, 0 推挽 / 1 开漏), PUPD5: (11, 10, 00 浮空 / 01 上拉 / 10 下拉), } def parse(val, hi, lo): 从 32 位值里抠出 [hi:lo] 这一段返回无符号整数 return (val lo) ((1 (hi - lo 1)) - 1) snap json.load(open(snapshot.json))[GPIOA_MODER] doc Document() doc.add_heading(寄存器位域解析, level2) t doc.add_table(rows1, cols4) t.style Table Grid for i, h in enumerate([位域, 位区间, 提取值, 含义]): t.rows[0].cells[i].text h for name, (hi, lo, desc) in BITS.items(): v parse(snap, hi, lo) cells t.add_row().cells cells[0].text name cells[1].text f[{hi}:{lo}] cells[2].text f0b{v:0{hi - lo 1}b} # 补齐位宽避免 0b1 看错位 cells[3].text desc doc.save(寄存器实验报告.docx)parse的移位和掩码顺序不能反先右移再与掩码反过来会把高位一起带进来。格式化时补足位宽同样重要同一段位域一会儿显示0b1一会儿显示0b01读表的人会误判成不同引脚。这份脚本的输入只有snapshot.json所以只要第 2 章那三条命令重跑一遍报告里的数字就能整体刷新。5.2 快照 JSON 该带哪些字段快照文件本身要能自证来源否则生成出来的表格只是好看的假数据。字段作用示例timestamp采样时刻用于对齐串口日志2025-03-11T09:14:22commit固件版本标识这份快照对应哪次编译a7f3c21addr寄存器绝对地址0x40020000name寄存器名GPIOA_MODERvalue32 位读回值0x00000400source读数途径SWD / 串口 / 模型预测swd把source单独列出来的意义在于调试器读回、串口打印、验证模型镜像值这三者混在一张表里时不加标注就没人知道该信哪个。凡是source为模型预测的行在文档里加一句说明表示这不是硬件实测值。5.3 让报告可复核的三个技巧第一个是把位域定义和手册页码绑在一起BITS字典里再加一列页码审核的人顺着页码就能对照原文档比写见手册有用得多。第二个是每张波形图配一句这张图证明什么比如图中第 3 个时钟下降沿后 MISO 拉高说明从机已应答地址字节而不是只放图不写结论。第三个是让脚本在快照校验失败时直接抛异常终止生成而不是把错误值写进 Word——报告一旦生成出来带上了错误数字后面传阅的人很难逐行核对宁可不生成。本文还有配套的精品资源点击获取