[{"content":"本文介绍完整的Risc-V指令集 文章内容基于该网站教学。\n整套RV32I基础指令集仅需大约40多条指令，也是处理器必须识别的全部 “指令词汇”。这些指令可划归为下面这几大类：\n依次为算术与逻辑运算指令、立即数运算指令、加载/存储指令、分支指令、跳转指令、高位立即数指令、系统类指令。\n算术与逻辑运算（寄存器 - 寄存器型）——R-type指令（10条） 选取两个寄存器中的数据执行运算操作，并存储运算结果。这类指令是处理器最常用的基础指令：\n指令 含义 add / sub rd = rs1 + rs2 / rd = rs1 − rs2 and / or / xor 按位与 / 按位或 / 按位异或 sll / srl / sra 逻辑左移 / 逻辑右移 / 算术右移（保留符号位） slt / sltu （对应有符号/无符号）rd = （rs1 \u0026lt; rs2） ? 1 : 0 1 2 3 4 5 6 7 8 9 10 11 # 用法 add rd, rs1, rs2 表示 rd = rs1 + rs2 sub rd, rs1, rs2 表示 rd = rs1 - rs2 and rd, rs1, rs2 表示 rd = rs1 \u0026amp; rs2 or rd, rs1, rs2 表示 rd = rs1 | rs2 xor rd, rs1, rs2 表示 rd = rs1 ^ rs2 sll rd, rs1, rs2 表示 rd = rs1 \u0026lt;\u0026lt; rs2 srl rd, rs1, rs2 表示 rd = rs1 \u0026gt;\u0026gt; rs2 sra rd, rs1, rs2 表示 rd = rs1 \u0026gt;\u0026gt;\u0026gt; rs2 slt rd, rs1, rs2 表示 rd = (rs1 \u0026lt; rs2) ? 1 : 0 sltu rd, rs1, rs2 表示 rd = (rs1 \u0026lt; rs2) ? 1 : 0 算术与逻辑运算（寄存器 - 立即数型）（9条） 指令 含义 addi rd = rs1 + 立即数 (不存在 “立即数减法 subi” 指令，只需传入负数即可实现减法) andi / ori / xori 与常量按位与 / 按位或 / 按位异或 slli / srli / srai 逻辑左移 / 逻辑右移 / 算术右移常量位 slti / sltiu （对应有符号/无符号）rd = （rs1 \u0026lt; 立即数） ? 1 : 0 1 2 3 4 5 6 7 8 9 10 #用法 addi rd, rs1, imm 表示 rd = rs1 + imm andi rd, rs1, imm 表示 rd = rs1 \u0026amp; imm ori rd, rs1, imm 表示 rd = rs1 | imm xori rd, rs1, imm 表示 rd = rs1 ^ imm slli rd, rs1, imm 表示 rd = rs1 \u0026lt;\u0026lt; imm srli rd, rs1, imm 表示 rd = rs1 \u0026gt;\u0026gt; imm srai rd, rs1, imm 表示 rd = rs1 \u0026gt;\u0026gt;\u0026gt; imm slti rd, rs1, imm 表示 rd = (rs1 \u0026lt; imm) ? 1 : 0 sltiu rd, rs1, imm 表示 rd = (rs1 \u0026lt; imm) ? 1 : 0 与存储器交互（8条） 指令 含义 lw 将内存中的一个32位字加载至目标寄存器rd lh / lb 加载半字（16位）/字节（8位）到rd（带符号位扩展） lhu / lbu 加载半字/字节数据到rd，（无符号位零扩展） sw / sh / sb 将寄存器rs2中的字数据/半字数据/字节数据存入存储器 1 2 3 4 5 6 7 8 9 # 用法 lw rd, imm(rs1) 表示 rd = MEM[rs1 + imm] lh rd, imm(rs1) 表示 rd = MEM[rs1 + imm]（高位扩展符号位） lb rd, imm(rs1) 表示 rd = MEM[rs1 + imm]（高位扩展符号位） lhu rd, imm(rs1) 表示 rd = MEM[rs1 + imm]（高位扩展0） lbu rd, imm(rs1) 表示 rd = MEM[rs1 + imm]（高位扩展0） sw rs2, imm(rs1) 表示 MEM[rs1 + imm] = rs2 sh rs2, imm(rs1) 表示 MEM[rs1 + imm] = rs2的低 16 位 sb rs2, imm(rs1) 表示 MEM[rs1 + imm] = rs2的低 8 位 分支指令（6条） CPU执行“if（条件判断）”语句：分支指令会对比两个寄存器的值，仅当判定条件成立时才执行跳转操作，若条件不成立，程序则直接顺序向下执行。\n指令 表达式（成立则跳转） beq / bne rs1 == rs2 / rs1 != rs2 blt / bge rs1 \u0026lt; rs2 / rs1 ≥ rs2 (有符号数) bltu / bgeu rs1 \u0026lt; rs2 / rs1 ≥ rs2 (无符号数) 1 2 3 4 5 6 7 # 用法 beq rs1, rs2, imm 表示 if (rs1 == rs2) 跳转到 PC + 偏移地址 bne rs1, rs2, imm 表示 if (rs1 != rs2) 跳转到 PC + 偏移地址 blt rs1, rs2, imm 表示 if (rs1 \u0026lt; rs2) 跳转到 PC + 偏移地址 bge rs1, rs2, imm 表示 if (rs1 \u0026gt;= rs2) 跳转到 PC + 偏移地址 bltu rs1, rs2, imm 表示 if (rs1 \u0026lt; rs2) 跳转到 PC + 偏移地址 bgeu rs1, rs2, imm 表示 if (rs1 \u0026gt;= rs2) 跳转到 PC + 偏移地址 高位立即数指令（2条） 指令 含义 lui 把指令内的20位立即数放置到寄存器高20位，寄存器低12位清零。 auipc 20位立即数左移12位后与当前PC值相加，结果存入目标寄存器；常用于生成PC相对地址，实现位置无关寻址 1 2 3 # 用法 lui rd, imm 表示 rd = imm \u0026lt;\u0026lt; 12 auipc rd, imm 表示 rd = (imm \u0026lt;\u0026lt; 12) + PC 跳转指令（2条） 指令 含义 jal Jump and link，PC 相对跳转。跳转目标 = PC + 符号扩展后的立即数，自动把当前 PC+4（下一条指令地址）写入 rd 寄存器，主要用于函数调用 jalr 寄存器间接跳转。跳转地址由「rs1 寄存器值 + 12 位符号扩展立即数」给出，同样会将 PC+4 存入 rd；最常用格式 jalr x0,0(x1) 实现函数返回，也可用于动态计算地址跳转 1 2 3 # 用法 jal rd, imm 表示 PC = PC + imm，rd = PC_old + 4 jalr rd, imm(rs1) 表示 PC = rs1 + imm，rd = PC_old + 4 伪指令 伪指令没有对应的独立机器码，汇编器在编译阶段，会自动把一条伪指令翻译成一条或多条真实硬件指令执行。只是为了编写汇编代码更简洁、好理解。\n伪指令 翻译后指令 补充说明 li rd, 5 addi rd, x0, 5 12位范围内小立即数赋值；32位大数会编译为 lui + addi 组合 mv rd, rs addi rd, rs, 0 复制寄存器 nop addi x0, x0, 0 不做任何操作 j label jal x0, label 跳转不保存返回地址 ret jalr x0, 0(x1) 在跳转操作后，返回x1寄存器内的返回地址 特殊指令（10条） CSR（控制状态寄存器）属于独立硬件寄存器组，和通用寄存器 x0~x31 物理分离，仅能使用以上 CSR 专用指令访问，普通运算指令无法读写。\n指令 含义 ecall 环境调用，触发系统调用异常，向内核申请服务 ebreak 断点指令，触发调试异常，用于 GDB 程序调试暂停 fence 数据内存屏障，管控 Load/Store 访存乱序 fence.i 指令流水线屏障，清空指令缓存、刷新指令流 csrrw CSR 读写：读出旧值到 rd，写入 rs 数据至 CSR csrrs CSR 读置位：读出旧值到 rd，使用 rs 按位或置 1 寄存器 bit csrrc CSR 读清零：读出旧值到 rd，使用 rs 按位与清零寄存器 bit csrrwi csrrw 立即数版本，使用 5 位立即数写入 CSR csrrsi csrrs 立即数版本，使用立即数做按位置位 csrrci csrrc 立即数版本，使用立即数做按位清零 1 2 3 4 5 6 7 8 9 10 11 # 用法 ecall # 发起系统调用 ebreak # 触发调试断点异常 fence # 数据内存屏障 fence.i # 指令流水线刷新屏障 csrrw rd, csr, rs1 表示 rd = csr，csr = rs1 csrrs rd, csr, rs1 表示 rd = csr，csr = csr | rs1 csrrc rd, csr, rs1 表示 rd = csr，csr = csr \u0026amp; ~rs1 csrrwi rd, csr, imm 表示 rd = csr，csr = imm csrrsi rd, csr, imm 表示 rd = csr，csr = csr | imm csrrci rd, csr, imm 表示 rd = csr，csr = csr \u0026amp; ~imm 参考指令字段格式： opcode对照表 ","date":"2026-07-28T00:00:00Z","image":"https://blog.lumenth.me/p/rv32i-instructions/Instructions_hu_68977f752eb37336.png","permalink":"https://blog.lumenth.me/p/rv32i-instructions/","title":"RV32I指令详解"},{"content":"本文讲解 IIC 总线协议，并给出可直接参考的 Verilog 主从机代码样例。\n文章时序解析内容基于该视频。\nIIC总线介绍 IIC（Inter-Integrated Circuit，常写作 I2C）是一种同步串行总线协议，由 Philips 提出由一个主机和若干从机构成，一个主机和多个从机共享同一对 SDA/SCL 信号线。\nIIC总线仅使用两根线工作：SCL（Serial Clock，时钟线）与 SDA（Serial Data，数据线），二者均为开漏结构、靠上拉电阻拉高，主从机控制拉低。本文采用7位地址表示法。同一条总线上，每个从机地址必须唯一，主机通过地址选中特定从机进行通信。\nIIC协议 上图为 IIC 标准传输格式，图中区分了\u0026quot;主机发出\u0026quot;与\u0026quot;从机发出\u0026quot;的信号。一次完整传输由起始条件开始、停止条件结束，中间按\u0026quot;地址 → 寄存器地址 → 数据\u0026quot;的顺序交互：\n读时序：起始位 → 地址+W → ACK(从机) → 寄存器地址 → ACK(从机) → 地址+R → ACK(从机) → 数据 → ACK → … → 停止位 写时序：起始位 → 地址+W → ACK(从机) → 寄存器地址 → ACK(从机) → 写入数据 → ACK → … → 停止位 时序解析 IIC 是同步时序：SDA 上的数据在 SCL 高电平期间必须保持稳定，接收方通常在 SCL 上升沿附近采样；而 SDA 的电平翻转只能发生在 SCL 低电平期间。\n这一约束是为了使得 SCL 高电平期间的 SDA 跳变被专门保留给\u0026quot;起始/停止条件\u0026quot;。\n起始位（START, S） 总线空闲时，外接的上拉电阻将 SDA 与 SCL 都维持在高电平（IDLE 状态）。当主机准备通信时，在 SCL 为高电平期间将 SDA 拉低，即产生起始条件，标志一次传输开始。\n地址位 + W/R 起始条件之后的第一个字节用于寻址：主机在 SCL 上升沿逐位送出 8 位数据，其中前 7 位为从机地址，第 8 位为读写标志 R/W（0 = 写，1 = 读）。\n对于\u0026quot;读\u0026quot;操作，主机之所以先发\u0026quot;地址+W\u0026quot;，是为了先把要读取的寄存器地址写进从机；随后再用 repeated START 切换为\u0026quot;地址+R\u0026quot;，从机才据此把对应寄存器的数据发回来。\n应答位（ACK） 图中白色信号即应答位，ACK 由当前字节的接收方产生。每传输完 8 位数据后，接收方需在紧跟的第 9 个 SCL 周期给出应答：ACK = SDA 被拉低（低电平），NACK = SDA 释放（高电平）。\nNACK 表示发送方未收到应答。在主机读数据时，主机通常会在最后一个字节返回 NACK，通知从机停止继续发送；在其他场景下，NACK 也可能表示地址不存在、设备忙或通信异常。只有收到有效 ACK，通信才会继续。\n停止位（STOP, P） 当前一次传输结束后，主机在 SCL 为高电平期间将 SDA 由低释放为高，产生停止条件，总线回到 IDLE 状态。\nVerilog代码 本文实现的是标准模式（Standard-mode，≤100 kHz）下的 IIC 主从通信，覆盖 7 位地址、读写双方向、寄存器地址 + 连续多字节数据等最常见的外设访问场景，可作为学习与原型验证的基础 IP。\n本文实现代码范围 类别 支持内容 说明 通信模式 标准模式 Standard-mode（默认 100 kHz） 主从双机，单主机结构 寻址 7 位从机地址 主机 addr 输入、从机 SLAVE_ADDR 参数 传输方向 写（主机→从机）/ 读（主机←从机） 均支持连续多字节 寄存器访问 先写寄存器地址，再读写数据 指针自增，支持跨寄存器连续访问 起止条件 START / STOP 读操作支持 repeated START（不发 STOP 直接重启） 应答机制 每字节后 ACK 从机回 ACK；读末字节主机回 NACK 时钟延长 主机侧支持 检测到 SCL 被从机拉低即等待（clock stretching） 总线建模 SDA/SCL 开漏 逻辑 1 = 释放、逻辑 0 = 拉低，板级外接上拉 错误处理 error 标志 地址或数据被 NACK 时置位 未覆盖的部分（如需可在此基础上扩展）：10 位地址、Fast-mode / Fast-mode Plus / High-speed 等高速模式、从机主动时钟延长（本从机不拉低 SCL）、广播呼叫/设备复位等保留地址、多主机仲裁与同步。\n主机 主机驱动模块用状态机完成一次完整 IIC 传输：IDLE → START → ADDR_DEVICE(设备地址+W) → ADDR_REG(寄存器地址) → [读时 repeated START + ADDR_RD(设备地址+R)] → DATA(收发数据) → STOP。\nSDA/SCL 均为开漏建模：输出逻辑 0 表示主动拉低总线，输出逻辑 1 并不会驱动高电平来实现，而是直接释放引脚，由片外上拉电阻恢复为高电平。因此 SDA_OE 与 SCL_OE 仅控制\u0026quot;是否拉低\u0026quot;总线。\nSCL 由检测SCL边沿的计数器驱动——每检测到一次 SCL 实际边沿就重触发半周期计时，数满 T_HALF 翻转 SCL_OE。每 8 位数据后跟 1 拍 ACK：从机拉低为 ACK，释放为高即为 NACK。\n当从机把 SCL 拉低时主机检测不到上升沿，计数器不被重触发，于是停在原位等待，直到从机释放产生上升沿才继续，这便实现了时钟延长（clock stretching）。因此主机并不是\u0026quot;按固定时间翻转 SCL\u0026quot;，而是\u0026quot;等待 SCL 实际发生边沿后再开始下一段计时\u0026quot;。\nlen 表示 DATA 阶段收发的字节数；写时主机连续发送、从机每字节回 ACK，末字节后进入 STOP；读时从机发送、主机对末字节回 NACK 后进入 STOP。与外部的握手复用单个 req 位——写时表示\u0026quot;本字节已发完、请准备下一字节\u0026quot;，读时表示\u0026quot;本字节已收满、请锁存 rd_data\u0026quot;；由于读写不可能同时发生，二者共用一位即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 module IIC_Master #( parameter CLK_FREQ = 100_000_000, // 系统时钟 100MHz parameter IIC_FREQ = 100_000 // IIC 速率 100kHz )( input wire clk, // 100MHz 系统时钟 input wire rst_n, // 异步复位（低有效） // 用户接口 input wire start, // 启动一次传输 input wire rw, // 1=读, 0=写 input wire [6:0] addr, // 从机地址 input wire [7:0] reg_addr, // 寄存器地址(ADDR_REG 发送) input wire [7:0] len, // 本次传输字节数 n（len\u0026gt;=1，0 视为 1） input wire [7:0] wr_data, // 待写数据（写模式：被 req 请求时由外部更新） output reg [7:0] rd_data, // 读到的数据（读模式：被 req 通知后由外部锁存） output reg req, // 单 bit 握手：写=本字节已发完、请求下一字节；读=本字节已收满、通知可锁存 output reg busy, // 传输进行中 output reg done, // 传输完成 output reg error, // 错误标志：从机 NACK 地址或写数据时置 1，保持到下次 start 或成功完成才清零 // IIC 双线（片外总线，开漏：逻辑1=释放引脚、由板级外接上拉拉高，逻辑0=本模块拉低） inout wire sda, inout wire scl ); // 时序参数 localparam DIV = CLK_FREQ / (2 * IIC_FREQ); // 半周期 clk 数 localparam T_HALF = DIV; // 每半周期固定计数 localparam T_BUF = CLK_FREQ / 200_000; // STOP 后总线空闲时间 localparam T_STOP = CLK_FREQ / 250_000; localparam T_HDSTA = CLK_FREQ / 250_000; // (repeated) START 后 SDA 保持低的保持时间 t_HD:STA(标准模式 4.0us) reg [$clog2(DIV):0] scl_cnt; reg [$clog2(T_STOP):0] stop_cnt; //SCL和SDA的OUTPUT ENABLE //起的是下拉作用，为1就是把SCL或SDA拉到0 reg SCL_OE,SDA_OE; assign sda = SDA_OE ? 1\u0026#39;d0 : 1\u0026#39;dz; assign scl = SCL_OE ? 1\u0026#39;d0 : 1\u0026#39;dz; // 状态机 localparam IDLE = 3\u0026#39;d0, START = 3\u0026#39;d1, ADDR_DEVICE = 3\u0026#39;d2, ADDR_REG = 3\u0026#39;d3, ADDR_RD = 3\u0026#39;d4, DATA = 3\u0026#39;d5, DATA_NEXT = 3\u0026#39;d7, STOP = 3\u0026#39;d6; reg [2:0] state; // 收发/计时寄存器 reg [7:0] shift_reg; // 发送移位 / 接收收集 reg [3:0] bit_cnt; // 0~7 数据位, 8=第9拍(ACK/NACK) reg [7:0] remain; // 剩余待传字节数(含当前) reg is_last; // 当前读字节是否末字节(决定回 ACK 还是 NACK) reg ack_phase; // 读：ACK 拍已在上升沿被从机采样，待低电平释放 reg scl_run; // 1=通信中生成 SCL；0=释放 SCL(高) reg scl_run_d; wire scl_run_pos = scl_run \u0026amp;\u0026amp; !scl_run_d; // 刚进入通信，产生首拍下降沿 reg [$clog2(T_HDSTA):0] hd_cnt; // t_HD:STA 保持计时 reg first_bit; // 1=当前首字节首位需遵守 t_HD:STA(START 后保持 SDA 低) reg start_fell; // 1=已检测到 START 的 SCL 下降沿 // 同步 SCL/SDA（异步于 clk），打拍检测边沿与读取实际电平 reg [2:0] scl_sync, sda_sync; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin scl_sync \u0026lt;= 3\u0026#39;b111; sda_sync \u0026lt;= 3\u0026#39;b111; end else begin scl_sync \u0026lt;= {scl_sync[1:0], scl}; sda_sync \u0026lt;= {sda_sync[1:0], sda}; end end wire scl_in = scl_sync[1]; // 同步后的 SCL 电平 wire sda_in = sda_sync[1]; // 同步后的 SDA 电平 wire scl_rise = (scl_sync[2:1] == 2\u0026#39;b01); // SCL 上升沿 wire scl_fall = (scl_sync[2:1] == 2\u0026#39;b10); // SCL 下降沿 // SCL 生成（边沿触发 + 支持从机时钟延长） // 每检测到一次 SCL 实际边沿，重触发半周期计时；数到 T_HALF-1 翻转 SCL_OE。 // 从机把 SCL 拉低时不会检测到上升沿 → 计数器不被重触发 → 主机停在原位等待， // 直到从机释放产生上升沿才继续，即时钟延长。 // scl_run_pos 时直接拉低，产生通信的第一个下降沿(START 之后的首个 SCL 跳变)。 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin scl_cnt \u0026lt;= 0; SCL_OE \u0026lt;= 1\u0026#39;b0; scl_run_d \u0026lt;= 1\u0026#39;b0; end else begin scl_run_d \u0026lt;= scl_run; if (!scl_run) begin scl_cnt \u0026lt;= 0; SCL_OE \u0026lt;= 1\u0026#39;b0; // 空闲/START/STOP：释放 SCL，保持高 end else begin if (scl_run_pos) begin SCL_OE \u0026lt;= 1\u0026#39;b1; // 刚启动：直接拉低，产生首拍下降沿 scl_cnt \u0026lt;= 0; end else if (scl_rise || scl_fall) begin scl_cnt \u0026lt;= 0; // 边沿重触发半周期计时 end else if (scl_cnt \u0026lt; T_HALF) begin scl_cnt \u0026lt;= scl_cnt + 1\u0026#39;b1; end if (scl_cnt == (T_HALF - 1)) SCL_OE \u0026lt;= ~SCL_OE; // 半周期到，翻转驱动(释放\u0026lt;-\u0026gt;拉低) end end end // SDA/状态机：基于 SCL 边沿收发 // scl_fall（进入低电平）：发送方设置下一位 SDA；第9拍释放 SDA 让从机回 ACK // scl_rise（进入高电平）：采样 SDA（ACK / 数据位） // 每 8 位数据后跟 1 拍 ACK（从机拉低=ACK，释放=NACK） always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state \u0026lt;= IDLE; SDA_OE \u0026lt;= 1\u0026#39;b0; scl_run \u0026lt;= 1\u0026#39;b0; busy \u0026lt;= 1\u0026#39;b0; done \u0026lt;= 1\u0026#39;b0; req \u0026lt;= 1\u0026#39;b0; error \u0026lt;= 1\u0026#39;b0; shift_reg\u0026lt;= 8\u0026#39;h00; bit_cnt \u0026lt;= 4\u0026#39;d0; remain \u0026lt;= 8\u0026#39;d0; is_last \u0026lt;= 1\u0026#39;b0; ack_phase\u0026lt;= 1\u0026#39;b0; stop_cnt \u0026lt;= 0; hd_cnt \u0026lt;= 0; first_bit\u0026lt;= 1\u0026#39;b0; start_fell\u0026lt;= 1\u0026#39;b0; end else begin req \u0026lt;= 1\u0026#39;b0; // 默认不请求；字节边界处脉冲置 1 case (state) // 空闲：SDA/SCL 均释放(高)。start 拉低 SDA 形成 START IDLE: begin SDA_OE \u0026lt;= 1\u0026#39;b0; scl_run \u0026lt;= 1\u0026#39;b0; if (start) begin busy \u0026lt;= 1\u0026#39;b1; done \u0026lt;= 1\u0026#39;b0; error \u0026lt;= 1\u0026#39;b0; SDA_OE \u0026lt;= 1\u0026#39;b1; // SCL 高时拉低 SDA = START state \u0026lt;= START; end end // START：拉低 SDA(SCL 仍高)形成 START 条件，装载地址+W。 // 首字节首位需遵守 t_HD:STA：ADDR_DEVICE 会等 SCL 落下后保持 SDA 低 t_HD:STA，再驱动地址 MSB。 START: begin SDA_OE \u0026lt;= 1\u0026#39;b1; // 维持 START(SDA 低) shift_reg \u0026lt;= {addr, 1\u0026#39;b0}; // 设备地址 + W bit_cnt \u0026lt;= 4\u0026#39;d0; scl_run \u0026lt;= 1\u0026#39;b1; // 触发首拍下降沿(scl_run_pos) first_bit \u0026lt;= 1\u0026#39;b1; // 首位需 t_HD:STA 保持 hd_cnt \u0026lt;= 0; start_fell\u0026lt;= 1\u0026#39;b0; state \u0026lt;= ADDR_DEVICE; end // 设备地址 + W：主机发 8 位，第9拍采样从机 ACK ADDR_DEVICE: begin if (first_bit) begin // t_HD:STA：START 的 SCL 落下后，SDA 继续维持低 t_HD:STA，再驱动首位(地址 MSB) if (!start_fell) begin if (scl_fall) start_fell \u0026lt;= 1\u0026#39;b1; // 等 START 的 SCL 下降沿 SDA_OE \u0026lt;= 1\u0026#39;b1; // 维持 START 低 end else if (hd_cnt \u0026lt; T_HDSTA) begin hd_cnt \u0026lt;= hd_cnt + 1\u0026#39;b1; // 计 t_HD:STA SDA_OE \u0026lt;= 1\u0026#39;b1; end else begin SDA_OE \u0026lt;= ~shift_reg[7]; // t_HD:STA 满足，驱动地址 MSB first_bit\u0026lt;= 1\u0026#39;b0; start_fell\u0026lt;= 1\u0026#39;b0; end end else if (scl_fall) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) SDA_OE \u0026lt;= ~shift_reg[7]; else SDA_OE \u0026lt;= 1\u0026#39;b0; // 第9拍：释放等 ACK end else if (scl_rise) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) begin shift_reg \u0026lt;= {shift_reg[6:0], 1\u0026#39;b0}; bit_cnt \u0026lt;= bit_cnt + 1\u0026#39;b1; end else begin if (sda_in) begin // 从机 NACK 地址 error \u0026lt;= 1\u0026#39;b1; busy \u0026lt;= 1\u0026#39;b0; done \u0026lt;= 1\u0026#39;b1; scl_run \u0026lt;= 1\u0026#39;b0; state \u0026lt;= IDLE; end else begin shift_reg \u0026lt;= reg_addr; // 装载寄存器地址 bit_cnt \u0026lt;= 4\u0026#39;d0; state \u0026lt;= ADDR_REG; end end end end // 寄存器地址：主机发 8 位，第9拍采样 ACK；写则进 DATA，读则 repeated START ADDR_REG: begin if (scl_fall) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) SDA_OE \u0026lt;= ~shift_reg[7]; else SDA_OE \u0026lt;= 1\u0026#39;b0; end else if (scl_rise) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) begin shift_reg \u0026lt;= {shift_reg[6:0], 1\u0026#39;b0}; bit_cnt \u0026lt;= bit_cnt + 1\u0026#39;b1; end else begin if (sda_in) begin // 从机 NACK error \u0026lt;= 1\u0026#39;b1; busy \u0026lt;= 1\u0026#39;b0; done \u0026lt;= 1\u0026#39;b1; scl_run \u0026lt;= 1\u0026#39;b0; state \u0026lt;= IDLE; end else if (rw == 1\u0026#39;b0) begin // 写：进入 DATA 发送 len 字节（首字节 wr_data 在 start 时已备好） shift_reg \u0026lt;= wr_data; remain \u0026lt;= (len == 8\u0026#39;d0) ? 8\u0026#39;d1 : len; is_last \u0026lt;= ((len == 8\u0026#39;d0) ? 8\u0026#39;d1 : len) == 8\u0026#39;d1; bit_cnt \u0026lt;= 4\u0026#39;d0; state \u0026lt;= DATA; end else begin // 读：SCL 高时拉低 SDA = repeated START，随后发 地址+R // repeated START 与 START 一样需要保持 t_HD:STA， // 因此进入 ADDR_RD 时置位 first_bit 等标志，由其完成保持后再驱动首位。 SDA_OE \u0026lt;= 1\u0026#39;b1; shift_reg \u0026lt;= {addr, 1\u0026#39;b1}; // 设备地址 + R bit_cnt \u0026lt;= 4\u0026#39;d0; first_bit \u0026lt;= 1\u0026#39;b1; // 首位需 t_HD:STA 保持 start_fell\u0026lt;= 1\u0026#39;b0; hd_cnt \u0026lt;= 0; state \u0026lt;= ADDR_RD; end end end end // 设备地址 + R（仅读）：repeated START 后的首位同样需保持 t_HD:STA， // 然后主机发 8 位，第9拍采样 ACK 后进入 DATA 接收 ADDR_RD: begin if (first_bit) begin // t_HD:STA：repeated START 的 SCL 落下后，SDA 继续维持低 t_HD:STA，再驱动首位(地址 MSB) if (!start_fell) begin if (scl_fall) start_fell \u0026lt;= 1\u0026#39;b1; // 等 repeated START 的 SCL 下降沿 SDA_OE \u0026lt;= 1\u0026#39;b1; // 维持 START 低 end else if (hd_cnt \u0026lt; T_HDSTA) begin hd_cnt \u0026lt;= hd_cnt + 1\u0026#39;b1; // 计 t_HD:STA SDA_OE \u0026lt;= 1\u0026#39;b1; end else begin SDA_OE \u0026lt;= ~shift_reg[7]; // t_HD:STA 满足，驱动地址 MSB first_bit\u0026lt;= 1\u0026#39;b0; start_fell\u0026lt;= 1\u0026#39;b0; end end else if (scl_fall) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) SDA_OE \u0026lt;= ~shift_reg[7]; else SDA_OE \u0026lt;= 1\u0026#39;b0; end else if (scl_rise) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) begin shift_reg \u0026lt;= {shift_reg[6:0], 1\u0026#39;b0}; bit_cnt \u0026lt;= bit_cnt + 1\u0026#39;b1; end else begin if (sda_in) begin // 从机 NACK error \u0026lt;= 1\u0026#39;b1; busy \u0026lt;= 1\u0026#39;b0; done \u0026lt;= 1\u0026#39;b1; scl_run \u0026lt;= 1\u0026#39;b0; state \u0026lt;= IDLE; end else begin shift_reg \u0026lt;= 8\u0026#39;h00; remain \u0026lt;= (len == 8\u0026#39;d0) ? 8\u0026#39;d1 : len; is_last \u0026lt;= ((len == 8\u0026#39;d0) ? 8\u0026#39;d1 : len) == 8\u0026#39;d1; bit_cnt \u0026lt;= 4\u0026#39;d0; state \u0026lt;= DATA; end end end end // 数据阶段：写=主机发+从机ACK；读=从机发+主机ACK/NACK DATA: begin // —— 写 —— if (rw == 1\u0026#39;b0) begin if (scl_fall) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) SDA_OE \u0026lt;= ~shift_reg[7]; else SDA_OE \u0026lt;= 1\u0026#39;b0; end else if (scl_rise) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) begin shift_reg \u0026lt;= {shift_reg[6:0], 1\u0026#39;b0}; bit_cnt \u0026lt;= bit_cnt + 1\u0026#39;b1; end else begin if (sda_in) begin // 从机 NACK 数据 error \u0026lt;= 1\u0026#39;b1; busy \u0026lt;= 1\u0026#39;b0; done \u0026lt;= 1\u0026#39;b1; scl_run \u0026lt;= 1\u0026#39;b0; state \u0026lt;= IDLE; end else if (remain \u0026gt; 8\u0026#39;d1) begin req \u0026lt;= 1\u0026#39;b1; // 请求外部准备下一字节 remain \u0026lt;= remain - 1\u0026#39;b1; is_last \u0026lt;= (remain - 1\u0026#39;b1) == 8\u0026#39;d1; bit_cnt \u0026lt;= 4\u0026#39;d0; state \u0026lt;= DATA_NEXT; // 等 wr_data 更新后再装载 end else begin state \u0026lt;= STOP; // 全部发完 end end end end // —— 读 —— else begin if (scl_fall) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) SDA_OE \u0026lt;= 1\u0026#39;b0; // 接收期间释放 else if (!ack_phase) SDA_OE \u0026lt;= is_last ? 1\u0026#39;b0 : 1\u0026#39;b1; // ACK/NACK else begin SDA_OE \u0026lt;= 1\u0026#39;b0; if (!is_last) begin remain \u0026lt;= remain - 1\u0026#39;b1; shift_reg\u0026lt;= 8\u0026#39;h00; bit_cnt \u0026lt;= 4\u0026#39;d0; is_last \u0026lt;= (remain - 1\u0026#39;b1) == 8\u0026#39;d1; end ack_phase \u0026lt;= 1\u0026#39;b0; end end else if (scl_rise) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) begin shift_reg \u0026lt;= {shift_reg[6:0], sda_in}; if (bit_cnt == 4\u0026#39;d7) begin rd_data \u0026lt;= {shift_reg[6:0], sda_in}; // 收满 8 位 req \u0026lt;= 1\u0026#39;b1; // 通知外部锁存 bit_cnt \u0026lt;= 4\u0026#39;d8; // 进入第9拍 end else bit_cnt \u0026lt;= bit_cnt + 1\u0026#39;b1; end else begin // 第9拍上升沿：从机已采样到本字节 ACK/NACK if (is_last) state \u0026lt;= STOP; else ack_phase \u0026lt;= 1\u0026#39;b1; end end end end // 字节边界缓冲：本拍外部已在 req 后把 wr_data 更新为新字节， // 此刻再装入 shift_reg，确保下一个字节发出的是\u0026#34;新值\u0026#34;而非刚发完的旧值。 DATA_NEXT: begin shift_reg \u0026lt;= wr_data; state \u0026lt;= DATA; end // 停止位：先停 SCL(释放→高)，SCL 高时拉低 SDA，保持 T_STOP 后释放 SDA=STOP STOP: begin scl_run \u0026lt;= 1\u0026#39;b0; // 释放 SCL，等待变高 SDA_OE \u0026lt;= 1\u0026#39;b1; // 保持 SDA 低，等 SCL 高 if (scl_in) begin if (stop_cnt \u0026lt; T_STOP) stop_cnt \u0026lt;= stop_cnt + 1\u0026#39;b1; else begin SDA_OE \u0026lt;= 1\u0026#39;b0; // 释放 SDA→高：SCL 高时 SDA 上升 = STOP done \u0026lt;= 1\u0026#39;b1; error \u0026lt;= 1\u0026#39;b0; busy \u0026lt;= 1\u0026#39;b0; state \u0026lt;= IDLE; end end else stop_cnt \u0026lt;= 0; end default: state \u0026lt;= IDLE; endcase end end endmodule 从机 从机作为总线外设，通过一组片内接口与内部逻辑交互。本从机仅实现 IIC 协议，不包含具体设备功能（例如 EEPROM、RTC 等寄存器逻辑），因此可以作为通用 IIC Slave IP 复用。\n端口分为三组：片内总线侧 clk/rst_n，IIC 侧 scl/sda，以及内部交互接口 reg_addr / data_out / data_in / req / busy / done / error / rw。\nIIC 侧先把 scl/sda 打两拍同步以消除亚稳态，再据此检测边沿与起始/停止条件，用状态机完成与主机对齐的标准 IIC 从机协议：先匹配 7 位地址 → 收 1 字节寄存器地址（reg_addr）→ 进入数据收发。寄存器指针 reg_ptr 取自主机发来的寄存器地址字节（8 位），每收发 1 字节指针自增，因此天然支持跨多个寄存器的连续读写：\n写：主机先写 reg_addr，随后连发 N 字节；从机每收到 1 字节，就把 data_out 连同当前 reg_addr 通过 req 脉冲交给内部锁存，并使指针自增。 读：主机先写 reg_addr，再发 repeated START + 地址+R；从机在每个读字节开始前提前一拍用 req 请求内部把 reg_addr 处数据放到 data_in，随后从 data_in 取数连续送出，每字节后指针自增。 SDA 采用开漏建模：仅在需要拉低（回 ACK、或发送的数据位为 0）时驱动 0，其余时刻释放为高阻，由片外引脚上的上拉电阻拉高；从机只在 SCL 低电平期间更新 SDA、在高电平期间保持稳定，因此自身的跳变不会被误判为起始/停止条件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 module iic_slave #( parameter [6:0] SLAVE_ADDR = 7\u0026#39;h50 // 本从机地址（由上层指定） )( input wire clk, // 片内总线时钟 input wire rst_n, // 异步复位（低有效） input wire scl, // IIC 时钟（主机驱动，异步于 clk） inout wire sda, // IIC 数据（开漏，双向） // ===== 内部交互接口（片内逻辑侧）===== output wire [7:0] reg_addr, // 当前访问的寄存器地址（字节偏移），输出给内部 output reg [7:0] data_out, // 写方向：主机写来的数据，输出给内部锁存 input wire [7:0] data_in, // 读方向：内部给从机的数据，由内部按 reg_addr 提供 output reg req, // 单 bit 握手： // 写=收到 1 字节、请求内部把 data_out 锁存到 reg_addr； // 读=即将发送 1 字节、请求内部把 reg_addr 处数据放到 data_in output reg busy, // 从机正处于一次传输中（START 到 STOP） output reg done, // 一次传输完成（检测到 STOP） output reg error, // 错误标志（保留/可扩展：如内部 NACK、越界等，正常为 0） output reg rw // 当前传输方向：1=读（从机发，数据来自 data_in），0=写（从机收，给 data_out） ); // ============================================================ // 一、SCL/SDA 打两拍同步 + 边沿 / 起止条件检测 // ============================================================ reg [2:0] scl_sync, sda_sync; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin scl_sync \u0026lt;= 3\u0026#39;b111; sda_sync \u0026lt;= 3\u0026#39;b111; end else begin scl_sync \u0026lt;= {scl_sync[1:0], scl}; sda_sync \u0026lt;= {sda_sync[1:0], sda}; end end wire scl_rise = (scl_sync[2:1] == 2\u0026#39;b01); wire scl_fall = (scl_sync[2:1] == 2\u0026#39;b10); wire scl_high = scl_sync[1]; wire sda_in = sda_sync[1]; wire start_cond = scl_high \u0026amp;\u0026amp; (sda_sync[2:1] == 2\u0026#39;b10); wire stop_cond = scl_high \u0026amp;\u0026amp; (sda_sync[2:1] == 2\u0026#39;b01); // ============================================================ // 二、SDA 开漏输出（片外总线）：仅在需要\u0026#34;拉低\u0026#34;时驱动 0，其余释放为高阻。 // ============================================================ reg sda_oe, sda_d; assign sda = (sda_oe \u0026amp;\u0026amp; (sda_d == 1\u0026#39;b0)) ? 1\u0026#39;b0 : 1\u0026#39;bz; // ============================================================ // 三、IIC 从机状态机（与主机协议对齐：地址+W → 寄存器地址 → [repeated START+地址+R] → 数据） // 寄存器地址 reg_ptr 取自主机发来的寄存器地址字节（8 位），收发后自增； // 写：每收到 1 字节经 req 交给内部（data_out + reg_addr）； // 读：每发 1 字节前经 req 向内部要数（data_in），从 data_in 取出连续送出。 // ============================================================ localparam S_IDLE = 3\u0026#39;d0, S_START = 3\u0026#39;d1, S_ADDR = 3\u0026#39;d2, S_ACK = 3\u0026#39;d3, S_DATA = 3\u0026#39;d4, S_STOP = 3\u0026#39;d5; reg [2:0] state; reg [3:0] bit_cnt; reg [7:0] shift_rx; reg [7:0] addr_byte; reg [7:0] shift_tx; reg [7:0] reg_ptr; // 寄存器地址指针（8 位，主机发来的寄存器地址字节），收发后自增 reg mack; reg got_addr; reg reg_done; reg slave_tx; assign reg_addr = reg_ptr; // 当前寄存器地址直接给内部 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state \u0026lt;= S_IDLE; sda_oe \u0026lt;= 1\u0026#39;b0; sda_d \u0026lt;= 1\u0026#39;b1; bit_cnt \u0026lt;= 4\u0026#39;d0; shift_rx \u0026lt;= 8\u0026#39;d0; shift_tx \u0026lt;= 8\u0026#39;d0; reg_ptr \u0026lt;= 8\u0026#39;d0; rw \u0026lt;= 1\u0026#39;b0; mack \u0026lt;= 1\u0026#39;b0; got_addr \u0026lt;= 1\u0026#39;b0; reg_done \u0026lt;= 1\u0026#39;b0; slave_tx \u0026lt;= 1\u0026#39;b0; data_out \u0026lt;= 8\u0026#39;d0; req \u0026lt;= 1\u0026#39;b0; busy \u0026lt;= 1\u0026#39;b0; done \u0026lt;= 1\u0026#39;b0; error \u0026lt;= 1\u0026#39;b0; end else begin req \u0026lt;= 1\u0026#39;b0; // 默认不请求；需要时脉冲置 1 done \u0026lt;= 1\u0026#39;b0; // 默认非完成；STOP 时脉冲置 1 // 起始/停止条件最高优先级：随时重定位状态机 if (start_cond) begin state \u0026lt;= S_START; bit_cnt \u0026lt;= 4\u0026#39;d0; sda_oe \u0026lt;= 1\u0026#39;b0; got_addr \u0026lt;= 1\u0026#39;b0; reg_done \u0026lt;= 1\u0026#39;b0; slave_tx \u0026lt;= 1\u0026#39;b0; busy \u0026lt;= 1\u0026#39;b1; // reg_ptr 保留，repeated START 后读仍从已设 reg_addr 起 end else if (stop_cond) begin state \u0026lt;= S_STOP; sda_oe \u0026lt;= 1\u0026#39;b0; busy \u0026lt;= 1\u0026#39;b0; done \u0026lt;= 1\u0026#39;b1; // 传输完成脉冲 end else begin case (state) S_IDLE: begin sda_oe \u0026lt;= 1\u0026#39;b0; end S_START: begin bit_cnt \u0026lt;= 4\u0026#39;d0; sda_oe \u0026lt;= 1\u0026#39;b0; state \u0026lt;= S_ADDR; end // ADDR：收 8 位地址字节（MSB 先入）。收满后在 ACK 拍回 ACK 并判匹配/R-W。 S_ADDR: begin if (scl_rise) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) begin shift_rx \u0026lt;= {shift_rx[6:0], sda_in}; bit_cnt \u0026lt;= bit_cnt + 1\u0026#39;b1; if (bit_cnt == 4\u0026#39;d7) addr_byte \u0026lt;= {shift_rx[6:0], sda_in}; end end else if (scl_fall) begin if (bit_cnt == 4\u0026#39;d8) begin if (addr_byte[7:1] == SLAVE_ADDR) begin rw \u0026lt;= addr_byte[0]; sda_oe \u0026lt;= 1\u0026#39;b1; sda_d \u0026lt;= 1\u0026#39;b0; // 读方向：提前一拍请求内部提供 reg_ptr 指向的数据（下一字节即读首字节） if (addr_byte[0] == 1\u0026#39;b1) req \u0026lt;= 1\u0026#39;b1; state \u0026lt;= S_ACK; end else begin sda_oe \u0026lt;= 1\u0026#39;b0; state \u0026lt;= S_IDLE; end end end end // ACK：每字节后的应答拍。 // 从机发送方向(slave_tx=1，读数据)：释放 SDA，SCL 高采样主机 ACK/NACK，下降沿续传/结束； // 从机接收方向(slave_tx=0)：本低电平已驱动 ACK，下降沿释放并按子阶段决定去向。 S_ACK: begin if (slave_tx == 1\u0026#39;b1) begin if (scl_rise) begin mack \u0026lt;= sda_in; // 0=ACK 续传，1=NACK 结束 end else if (scl_fall) begin if (mack == 1\u0026#39;b0) begin // 主机 ACK：续传下一字节（数据已在上一字节末经 req 请求） bit_cnt \u0026lt;= 4\u0026#39;d0; state \u0026lt;= S_DATA; end else begin sda_oe \u0026lt;= 1\u0026#39;b0; // NACK：主机结束读 state \u0026lt;= S_IDLE; end end end else begin if (scl_fall) begin sda_oe \u0026lt;= 1\u0026#39;b0; // 释放 ACK bit_cnt \u0026lt;= 4\u0026#39;d0; if (got_addr == 1\u0026#39;b0) begin // 刚 ACK 的是\u0026#34;地址字节\u0026#34; got_addr \u0026lt;= 1\u0026#39;b1; if (rw == 1\u0026#39;b1) begin // 读：进入发送（reg_ptr 已由前面 reg_addr 字节设好，数据经 req 已请求） slave_tx \u0026lt;= 1\u0026#39;b1; reg_done \u0026lt;= 1\u0026#39;b1; bit_cnt \u0026lt;= 4\u0026#39;d0; state \u0026lt;= S_DATA; end else begin // 写：进入 DATA 收寄存器地址 slave_tx \u0026lt;= 1\u0026#39;b0; reg_done \u0026lt;= 1\u0026#39;b0; state \u0026lt;= S_DATA; end end else if (reg_done == 1\u0026#39;b0) begin // 刚 ACK 的是\u0026#34;寄存器地址字节\u0026#34;：锁存寄存器地址 reg_ptr \u0026lt;= shift_rx; reg_done \u0026lt;= 1\u0026#39;b1; slave_tx \u0026lt;= 1\u0026#39;b0; state \u0026lt;= S_DATA; end else begin // 刚 ACK 的是\u0026#34;写数据字节\u0026#34;：交给内部锁存 data_out \u0026lt;= shift_rx; // 写来的数据给内部 req \u0026lt;= 1\u0026#39;b1; // 请求内部锁存到当前 reg_addr reg_ptr \u0026lt;= reg_ptr + 1\u0026#39;b1; // 指针自增 slave_tx \u0026lt;= 1\u0026#39;b0; state \u0026lt;= S_DATA; end end end end // DATA：收发一个字节（MSB 先）。 // 发(slave_tx=1，读数据)：首拍从 data_in 装载，SCL 低电平驱动位，发满 8 位后释放供主机应答； // 收(slave_tx=0，含 reg_addr 与写数据)：SCL 上升沿移位，收满 8 位后本低电平驱动 ACK。 S_DATA: begin if (slave_tx == 1\u0026#39;b1) begin if (scl_fall) begin if (bit_cnt == 4\u0026#39;d0) begin // 首拍：装载内部提供的读数据（req 已提前一拍请求） shift_tx \u0026lt;= {data_in[6:0], 1\u0026#39;b0}; sda_oe \u0026lt;= 1\u0026#39;b1; sda_d \u0026lt;= data_in[7]; bit_cnt \u0026lt;= 4\u0026#39;d1; end else if (bit_cnt \u0026lt; 4\u0026#39;d8) begin sda_oe \u0026lt;= 1\u0026#39;b1; sda_d \u0026lt;= shift_tx[7]; shift_tx \u0026lt;= {shift_tx[6:0], 1\u0026#39;b0}; bit_cnt \u0026lt;= bit_cnt + 1\u0026#39;b1; end else begin // 8 位发完，释放供主机 ACK/NACK；自增指针并提前请求下一字节 sda_oe \u0026lt;= 1\u0026#39;b0; reg_ptr \u0026lt;= reg_ptr + 1\u0026#39;b1; req \u0026lt;= 1\u0026#39;b1; state \u0026lt;= S_ACK; end end end else begin if (scl_rise) begin if (bit_cnt \u0026lt; 4\u0026#39;d8) begin shift_rx \u0026lt;= {shift_rx[6:0], sda_in}; bit_cnt \u0026lt;= bit_cnt + 1\u0026#39;b1; end end else if (scl_fall) begin if (bit_cnt == 4\u0026#39;d8) begin sda_oe \u0026lt;= 1\u0026#39;b1; // 驱动 ACK sda_d \u0026lt;= 1\u0026#39;b0; state \u0026lt;= S_ACK; end end end end S_STOP: begin sda_oe \u0026lt;= 1\u0026#39;b0; state \u0026lt;= S_IDLE; end default: state \u0026lt;= S_IDLE; endcase end end end endmodule ","date":"2026-07-16T00:00:00Z","image":"https://blog.lumenth.me/p/iic-bus/master-slave_hu_aeb6ba63a00466af.png","permalink":"https://blog.lumenth.me/p/iic-bus/","title":"IIC总线"},{"content":"Verilog 语法教程 Verilog HDL（简称 Verilog）是一种硬件描述语言（Hardware Description Language, HDL），用于数字电路的系统设计。它能在算法级、门级、开关级等多种抽象层次上对硬件进行建模。\n阅读本教程前，你需要了解基本的数字电路知识，包括组合逻辑、时序逻辑与时钟/复位的概念。\n如果想要动手练习 Verilog，建议在 HDLBits 网站进行练习。\n一个完整例子 下面先看一个完整的计数器模块，再逐块拆解它用到的语法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 module counter10( input rstn, // 复位端，低有效 input clk, // 输入时钟 output [3:0] cnt, // 4 位计数输出 output cout // 溢出进位 ); reg [3:0] cnt_temp; // 计数寄存器 always @(posedge clk or negedge rstn) begin if (!rstn) cnt_temp \u0026lt;= 4\u0026#39;b0; // 复位时归零 else if (cnt_temp == 4\u0026#39;d9) cnt_temp \u0026lt;= 4\u0026#39;b0; // 计到 9 后归零 else cnt_temp \u0026lt;= cnt_temp + 1\u0026#39;b1; // 否则加 1 end assign cout = (cnt_temp == 4\u0026#39;d9); // 进位：计到 9 时拉高 assign cnt = cnt_temp; // 实时计数输出 endmodule 一、module 与文件结构 一个 Verilog 设计由若干 module（模块） 组成，每个模块用 module ... endmodule 包裹：\nmodule 后跟模块名与端口列表，endmodule 表示模块结束（结尾没有分号）。 模块名应使用有意义的英文，不能是 Verilog 关键字，也不能含空格。 工程中通常一个 .v 文件只放一个 module，且文件名与模块名保持一致，方便工具管理。 模块内部由**端口声明、数据类型定义、连续赋值（assign）、过程块（always）**等部分构成。 1 2 3 4 5 6 module 模块名 ( 端口列表 ); // 端口声明 // 信号/寄存器定义 // assign 连续赋值 // always 过程块 endmodule 注释 Verilog 支持两种注释，与 C 语言相同：\n行注释 //：从 // 到本行行尾。 块注释 /* ... */：可跨越多行。 1 2 3 4 // 这是一行注释 reg [3:0] cnt; // 行尾也可以写注释 /* 这是 跨行块注释 */ 注释会被综合器忽略，仅用于提升可读性。建议对关键信号含义、状态编码、时序/约束意图加上注释。\n二、端口定义 端口是模块与外部电路的接口，必须声明方向与（可选的）类型/位宽。\n1. 三种方向 方向 含义 典型用途 input 模块输入，由外部驱动 时钟、复位、数据输入 output 模块输出 数据/状态输出 inout 双向（三态） 开漏总线，如 IIC 的 sda/scl 2. 端口默认是 wire 端口声明时若只写方向（不写 reg），默认类型为 wire。因此：\ninput 永远是外部驱动的 wire，不能被 always 赋值。 output 若要在 always 块里被赋值，必须显式声明为 reg（即 output reg）；若仅由 assign 驱动则保持 wire。 3. 两种声明风格 现代 ANSI 风格（推荐）：方向与类型直接写在端口列表中，直观不易出错。\n1 2 3 4 5 6 7 8 module counter10( input rstn, input clk, output [3:0] cnt, // 默认 wire，用 assign 驱动 output cout // 默认 wire ); // ... endmodule 传统非 ANSI 风格：端口列表只列名字，类型/方向再到模块体内声明（旧代码常见，但容易遗漏）：\n1 2 3 4 5 6 7 module counter10(rstn, clk, cnt, cout); input rstn; input clk; output [3:0] cnt; output cout; // ... endmodule 三、数据类型：wire 与 reg Verilog 中最常用的两种变量类型是 线网（wire） 与 寄存器（reg）。\n1. wire（线网类型） 顾名思义表示一根\u0026quot;导线\u0026quot;，自身不能存储状态，必须由外部驱动才能呈现 0 或 1（驱动源可以是 assign、模块端口或基本门）。常用于组合逻辑的输出与模块间的连线。\n1 2 3 wire interrupt; wire flag1, flag2; wire gnd = 1\u0026#39;b0; // 常接地的线 2. reg（寄存器类型） reg 类型变量可以在过程块（always）中被赋值。从硬件角度看，它名字里的\u0026quot;寄存器\u0026quot;容易误导——它不一定综合成真正的触发器，最终变成什么电路，完全取决于它写在哪种 always 里：\n写在 always @(posedge clk) 里 → 综合成 D 触发器（Flip-Flop）：每位都是一个小存储单元，只在时钟上升沿\u0026quot;啪\u0026quot;地记住新值，平时保持不动。这是 reg 最常见的硬件形态。 写在 always @(*) 里 → 综合成组合门电路（如与门、或门、选择器），没有记忆，输出随输入即时变化。此时 reg 只是\u0026quot;过程中暂存变量\u0026quot;，并不是真正的寄存器。 若分支没写全（缺 else / default）→ 综合器为\u0026quot;保持原值\u0026quot;会悄悄生成锁存器（latch），一般要尽量避免。 一句话：reg 在时序块里 = 触发器（有记忆），在组合块里 = 组合门（无记忆），关键在于它所在的 always。\n1 2 3 reg clk_temp; reg flag1, flag2; reg [3:0] counter; 简单记忆：assign 驱动的用 wire，always 块里被赋值的用 reg。\n3. signed 有符号类型（仅介绍） 入门阶段极少用到，这里只做概念性介绍，不要求掌握，知道有这回事即可。\n默认情况下 wire / reg 是无符号的，算术与比较都按无符号处理。在类型后加 signed 关键字可声明为有符号（二进制补码），此时算术、关系运算以及 \u0026gt;\u0026gt;\u0026gt; 算术右移都会按正负来解释：\n1 2 3 4 reg signed [7:0] a; // 有符号 8 位，范围 -128 ~ +127 reg [7:0] b; // 无符号 8 位，范围 0 ~ 255 a = -8\u0026#39;sd10; // 注意：字面量也要带 s 才是\u0026#34;有符号字面量\u0026#34; 要点：有符号只改变\u0026quot;如何解释这些 bit\u0026quot;与运算规则；线网/寄存器里存的仍是普通 bit。当有符号与无符号操作数混用时，结果按无符号处理，需要特别小心。\n四、位宽与向量 1. 标量 vs 向量 从硬件角度看，Verilog 里的一个信号其实就是一根导线；指定位宽后，它就变成一捆并排的多根导线，这捆\u0026quot;导线束\u0026quot;就叫做向量（vector）。\n标量（scalar）：没有指定位宽，本质是 1 根导线，只能表示 0 或 1 一个 bit。 向量（vector）：位宽大于 1，本质是 N 根并排的导线，可一次性承载 N 个 bit，做多位运算（[3:0] 就是 4 根导线，编号 0~3）。 1 2 3 wire a; // 1 根导线（1 位标量） reg [3:0] counter; // 4 根并排导线（4 位向量），位宽 = 3-0+1 = 4 wire [31:0] data; // 32 根并排导线（32 位向量/总线） 可以想象 wire [31:0] data; 在电路里并不是一根\u0026quot;粗线\u0026quot;，而是 data[0]、data[1] … data[31] 共 32 根独立的导线捆在一起，平时统一叫 data 来批量操作。这种\u0026quot;多根导线\u0026quot;的视角，对理解后面的位选、拼接、移位都很关键。\n2. 向量声明与高低位 向量格式为 [msb : lsb]（高位在前、低位在后），位宽 = msb - lsb + 1：\n1 2 3 reg [3:0] counter; // 大端：bit3 为最高位 MSB，bit0 为最低位 LSB wire [8:2] addr; // 7 位向量，编号从 8 到 2 reg [0:31] data; // 小端：bit0 为 MSB（不推荐，易混淆，尽量统一用 [msb:lsb]） 3. 位选与域选 可以对向量做单 bit 选择或连续段选择：\n1 2 counter[0] // 取最低位 counter[3:1] // 取 bit3、bit2、bit1 共 3 位 4. 拼接与复制 拼接运算符 {}：把多个信号拼成更宽的总线。 复制 {n{ }}：把某值重复 n 次。 1 2 wire [7:0] bus = {cnt, 4\u0026#39;b0000}; // 把 4 位 cnt 拼到高 4 位，低 4 位补 0 wire [3:0] zero = {4{1\u0026#39;b0}}; // 等价于 4\u0026#39;b0000 5. 向量的数组（存储器） 前面说的 reg [3:0] counter 是一个\u0026quot;4 位宽的向量\u0026quot;。如果再加一对方括号，就声明了一组相同宽度的向量，也就是存储器（memory） —— 常用于寄存器堆、RAM、FIFO 等：\n1 2 3 reg [7:0] byte1 [3:0]; // 声明一个\u0026#34;4 个字、每字 8 位\u0026#34;的存储器 // 左侧 [7:0] = 每个字的位宽 // 右侧 [3:0] = 字的个数（地址范围 0~3） 读法要分清两个维度：\n左侧 [7:0] 是\u0026quot;每个元素的位宽\u0026quot;（数据宽度）。 右侧 [3:0] 是\u0026quot;元素的个数/地址范围\u0026quot;（深度）。 1 2 3 byte1[0] // 取第 0 个字（一个 8 位向量） byte1[2][7:0] // 取第 2 个字的全部 8 位 byte1[addr][3] // 取第 addr 个字的第 3 位 注意几个常见限制：\n不能一次性对整个存储器赋值，只能按字逐一赋值（通常写在带下标的 always 块里）： 1 2 3 4 integer i; always @(posedge clk) begin if (wen) byte1[waddr] \u0026lt;= wdata; // 只写某一个字 end 存储器元素（byte1[addr]）在传统的连续赋值 assign 中不能直接整体驱动，一般放在 always 块里用 reg 描述，或用 for 循环初始化。 wire 也可以声明成数组（如 wire [7:0] bus [3:0]），但在可综合设计中存储器几乎都用 reg。 五、数值的表示方式 1. 四种逻辑值 Verilog 用下列四种基本值表示硬件电平：\n值 含义 0 逻辑 0 或\u0026quot;假\u0026quot; 1 逻辑 1 或\u0026quot;真\u0026quot; x / X 未知（未初始化 / 多驱动冲突） z / Z 高阻（常用于 inout 三态/开漏释放） 2. 基数格式 数字语法为 \u0026lt;位宽\u0026gt;'\u0026lt;基数\u0026gt;\u0026lt;数值\u0026gt;，合法的基数有四种：\n二进制 'b / 'B 十进制 'd / 'D 十六进制 'h / 'H 八进制 'o / 'O 为严谨，实际编码中一般始终指定位宽：\n1 2 3 4\u0026#39;b1011 // 4 位二进制 32\u0026#39;h3022_c0de // 32 位十六进制，下划线仅便于阅读、无实际意义 4\u0026#39;d9 // 4 位十进制 9 3. 不定宽、负数与实数 不指定位宽时，编译器通常按宿主字长（常见 32 位）处理：counter = 'd100; 或 counter = 100;。 负数在位宽之前加减号：counter = -6'd15; 是合法的；- 放在数值之后（如 4'd-2）则非法。 Verilog 还有 real 实数类型，支持科学计数法（如 1.2e4、1E-3），但不参与综合，只用于测试/建模。 六、运算符与基础计算 Verilog 提供丰富的运算符。下面按\u0026quot;算术前先补一句：所有运算都在位上按位宽进行，结果位宽不够会截断高位、不够会补符号/零\u0026quot;来分类介绍最常用的几类。\n1. 算术运算符 运算符 含义 说明 + 加 按位宽相加，溢出则截断高位 - 减 按位宽相减 * 乘 综合为乘法器，结果位宽通常为两操作数位宽之和 / 除 综合资源开销大，尽量少用（2 的幂建议用移位代替） % 取模 同 /，综合代价高，慎用 1 2 3 4 5 6 reg [7:0] a, b; reg [8:0] sum; // 进位位宽多留 1 位 reg [15:0] prod; sum = a + b; // 8 位 + 8 位，结果用 9 位接住进位 prod = a * b; // 8×8 = 16 位 注意：Verilog 中的 + - * 是无符号按位宽计算；若操作数带 signed 声明则为有符号运算。位宽不匹配时，窄的操作数会零扩展（无符号）或符号扩展（有符号）到宽的位宽后参与运算。\n2. 移位运算符 运算符 含义 说明 \u0026lt;\u0026lt; 逻辑左移 右侧补 0，等价于 ×2ⁿ \u0026gt;\u0026gt; 逻辑右移 左侧补 0，等价于 ÷2ⁿ \u0026lt;\u0026lt;\u0026lt; 算术左移 有符号场景下同逻辑左移 \u0026gt;\u0026gt;\u0026gt; 算术右移 有符号时左侧补符号位（保持正负） 1 2 3 4 reg [7:0] x; x = x \u0026lt;\u0026lt; 1; // 左移 1 位（乘 2），低位补 0 x = x \u0026gt;\u0026gt; 2; // 右移 2 位（除 4），高位补 0 x = x \u0026lt;\u0026lt;\u0026lt; 1; // 算术左移 硬件视角：移位在综合后几乎不消耗逻辑资源——它并不是\u0026quot;计算\u0026quot;，而是把导线重新连了一下。例如 x \u0026lt;\u0026lt; 1 综合出来就是：把 x[6:0] 接到输出 [7:1]、输出最低位 [0] 接地（补 0）。正因为只是\u0026quot;改接线\u0026quot;，所以比乘除法器便宜得多。\n因此，对 2 的整数次幂的乘除，优先用移位实现，比 /、* 更省逻辑资源。例如 addr * 4 写成 addr \u0026lt;\u0026lt; 2。\n3. 位运算与归约运算符 运算符 含义 说明 ~ 按位取反 每一位取反 \u0026amp; 按位与 对应位相与 | 按位或 对应位相或 ^ 按位异或 不同为 1 ~^ / ^~ 同或 相同为 1 \u0026amp;(单目) 归约与 把所有位\u0026quot;与\u0026quot;起来，得 1 位 |(单目) 归约或 把所有位\u0026quot;或\u0026quot;起来，得 1 位 ^(单目) 归约异或 奇偶校验常用 1 2 3 4 5 6 reg [3:0] v; reg all_one, any_one, parity; all_one = \u0026amp;v; // 归约与：v 全 1 时为 1 any_one = |v; // 归约或：v 任意位为 1 时为 1 parity = ^v; // 归约异或：v 中 1 的个数为奇数时为 1 4. 关系与等式运算符 运算符 含义 \u0026gt; \u0026gt;= \u0026lt; \u0026lt;= 大小比较，结果为 1 位 == != 相等 / 不等 === !== 全等 / 不全等（会区分 x/z，仅用于仿真，不可综合） 1 if (cnt == 4\u0026#39;d9) ... // 组合/时序里常用 == 做判断 5. 拼接、复制与条件运算符 拼接 {}：已在\u0026quot;位宽与向量\u0026quot;一节介绍，可把多个信号拼成更宽总线。 复制 {n{ }}：重复某值 n 次。 条件运算符 ?:：条件 ? 真值 : 假值，常用于 assign 中做二选一。 1 2 3 assign sel_out = sel ? a : b; // 二选一多路器 assign bus = {hi, 8\u0026#39;b0, lo}; // 拼接 assign mask = {8{1\u0026#39;b1}}; // 8 位全 1 运算符优先级与 C 语言类似（乘除移位 \u0026gt; 加减 \u0026gt; 比较 \u0026gt; 位运算 \u0026gt; 条件），写复杂表达式时多用括号避免歧义。\n七、always 过程块 always 是描述硬件行为的过程块，内部可以写 if/else、case、for 等控制语句。它的行为完全由**敏感表（sensitivity list）**决定——敏感表里写什么事件，块就在什么事件发生时执行。\n实际工程中，绝大多数的 always 只有两种\u0026quot;标准写法\u0026quot;，分别对应时序逻辑和组合逻辑：\n写法 触发方式 综合出 典型用途 赋值方式 always @(posedge clk or negedge rst_n) 时钟 / 复位边沿 触发器（带复位） 打拍、计数、状态机 非阻塞 \u0026lt;= always @(*) 任意输入信号变化 组合逻辑（门电路） 译码、多选一、运算 阻塞 = 下面分别介绍这两种写法。\n1. 时序逻辑：always @(posedge clk or negedge rst_n) 敏感表里写的是边沿事件，块只在边沿到来的那一时刻执行一次。这是描述触发器（Flip-Flop）与寄存器的最标准形式，几乎总是写成\u0026quot;时钟上升沿 + 复位下降沿\u0026quot;：\n1 2 3 4 5 6 always @(posedge clk or negedge rst_n) begin if (!rst_n) q \u0026lt;= 1\u0026#39;b0; // 复位有效（低有效）时，值清零 else q \u0026lt;= d; // 否则在时钟上升沿把 d 锁存到 q end posedge clk：时钟上升沿触发——每个时钟沿把输入\u0026quot;打一拍\u0026quot;。 negedge rst_n：复位下降沿也触发，因此复位是异步的（复位一拉低立刻生效，不等待时钟）。 若要做同步复位，就把复位写进 if 判断、且不列入敏感表：always @(posedge clk) if (!rst_n) ...。 这种写法综合出来的是带复位的 D 触发器；块内使用非阻塞 \u0026lt;= 赋值（见 §八）。 2. 组合逻辑：always @(*) 组合逻辑的输出只取决于当前输入（没有时钟、没有记忆），所以只要右侧用到的任何信号变化，输出就该立刻更新。敏感表用通配符 * 表示\u0026quot;自动包含所有用到的信号\u0026quot;，既省事又不会漏写：\n1 2 3 4 5 6 7 8 always @(*) begin case (sel) 2\u0026#39;b00: y = a; 2\u0026#39;b01: y = b; 2\u0026#39;b10: y = c; default: y = 1\u0026#39;b0; endcase end * 等价于手动列出 always @(a or b or c or sel)，但不用手列，避免漏写敏感信号导致仿真与综合行为不一致。 组合逻辑块内使用阻塞 = 赋值（见 §八）。 一定要把分支写全（加 default），否则综合器会推断出锁存器（见 §3）。 硬件视角：上面这段 case 综合后，本质就是一个多路选择器（MUX）——sel 是选择信号，它控制把 a / b / c / 0 中的哪一路\u0026quot;连\u0026quot;到输出 y。所以 case、或者 ?: 条件运算符，在硬件里都是同一个东西：一个被选择信号控制的多路开关。\n3. 避免意外锁存器（latch） 组合逻辑里，如果 if 缺少 else、或 case 缺少 default，综合器会认为\u0026quot;条件不满足时输出应保持原值\u0026quot;，从而推断出锁存器。通常我们不希望出现锁存器，所以组合 always 要分支写全：\n1 2 3 4 5 6 always @(*) begin if (sel) y = a; else // 必须有 else，否则 y 在 sel=0 时被锁存 y = b; end 4. begin / end 语句块 begin ... end 把多条语句组织成一个顺序语句块，相当于 C 的 { }。在 always、if、case、for 等结构里，当分支包含多条语句时必须用 begin/end 包裹（单条语句可省略）：\n1 2 3 4 5 6 7 8 always @(posedge clk) begin if (en) begin a \u0026lt;= 1\u0026#39;b1; b \u0026lt;= 1\u0026#39;b0; // 多条语句必须包在 begin/end 中 end else a \u0026lt;= 1\u0026#39;b0; // 单条语句可省略 begin/end end 5. case 语句 case 是多分支选择，常用于译码、状态机跳转。语法：case (表达式) 分支1: 语句; 分支2: 语句; default: 语句; endcase。从硬件角度看它就是一个多路选择器（MUX）：选择信号决定把哪一路输入接到输出。注意：\n各分支按全等比较（===）匹配，且分支值之间不应重叠。 组合逻辑务必写 default，否则未覆盖的情况会被综合成锁存器。 1 2 3 4 5 6 7 8 always @(*) begin case (sel) 2\u0026#39;b00: y = a; 2\u0026#39;b01: y = b; 2\u0026#39;b10: y = c; default: y = 1\u0026#39;b0; // 防止 latch endcase end 相关变体：casez（把 ? / z 当作\u0026quot;不关心\u0026quot;通配）、casex（把 x 与 z 都当通配）。可综合设计里优先用 casez + ? 表示无关位。\n6. for 循环 for 循环用于在同一时刻展开重复操作（如批量初始化数组、按位处理）。注意它是在综合/展开（elaboration）阶段被展开成多份并行硬件，因此循环次数必须是常量，不能依赖运行时的变量：\n1 2 3 4 5 integer i; always @(posedge clk) begin for (i = 0; i \u0026lt; 4; i = i + 1) byte1[i] \u0026lt;= 8\u0026#39;b0; // 4 个字同时清零 end 因为循环会被展开，所以它常用于对存储器/向量做批量赋值、或实现移位寄存器等多位重复结构。循环变量一般用 integer 声明。\n7. initial 块 initial 与 always 类似也是过程块，但它只在上电/仿真开始时执行一次（没有敏感表）：\n1 2 3 4 reg [3:0] cnt; initial begin cnt = 4\u0026#39;b0; // 仿真初值 end 绝大多数非测试平台的 FPGA/ASIC RTL 中，不要依赖 initial 完成寄存器初始化，应使用复位逻辑。initial 主要用于**测试平台（testbench）**设置初值与激励；在可综合的 RTL 中多数综合器不支持（或忽略），变量初值应交给复位信号来完成。\n八、阻塞赋值与非阻塞赋值 always 块内的赋值有两种符号，初学者最容易混淆：\n赋值 符号 行为 阻塞赋值 = 立即生效，语句顺序执行（像软件一行行跑） 非阻塞赋值 \u0026lt;= 在块结束时统一更新，语句间并行（像硬件同时打拍） 硬件视角：电路是并行工作的——一个时钟上升沿到来，所有 D 触发器同时把各自的新值锁存进去。非阻塞 \u0026lt;= 正是这种\u0026quot;并行同时打拍\u0026quot;的建模：同一条 always 里的多条 \u0026lt;= 都基于进入本周期时的旧值计算，并在块结束时一起更新，互不干扰。而阻塞 = 是\u0026quot;算完一个再算下一个\u0026quot;的串行思路，适合描述组合逻辑里\u0026quot;前一句的结果立刻给后一句用\u0026quot;的依赖关系。记住这个\u0026quot;并行 vs 串行\u0026quot;的差别，就抓住了两种赋值的本质。\n1. 黄金规则 时序逻辑（posedge clk 等）一律用非阻塞 \u0026lt;=：保证同一时钟沿上各语句基于\u0026quot;旧值\u0026quot;同时更新。 组合逻辑（always @(*)）一律用阻塞 =：保证前后语句按顺序、即时生效。 2. 反例：时序块里用错会出 bug 意图：把输入 d 打两拍（两级流水），正确的 q2 应等于上一周期的 q1。\n1 2 3 4 5 6 7 8 9 10 11 12 // 非阻塞（正确）：两条语句都基于进入本周期时的旧值计算，q2 得到上一拍的 q1 always @(posedge clk) begin q1 \u0026lt;= d; q2 \u0026lt;= q1; end // 阻塞（错误）：q1 立即被更新，随后 q2 读到的是\u0026#34;刚更新的 q1\u0026#34;， // 结果 q1、q2 同一拍都变成 d，退化为直通，没有延迟效果 always @(posedge clk) begin q1 = d; q2 = q1; end 3. 反例：组合块里用错也会出错 1 2 3 4 5 6 7 8 9 10 11 // 阻塞（正确）：y 先算出，z 用到的是刚算出的 y always @(*) begin y = a \u0026amp; b; z = y | c; end // 非阻塞（错误）：块结束时才统一更新，z 用到的是\u0026#34;上一轮\u0026#34;的 y，并非本次结果 always @(*) begin y \u0026lt;= a \u0026amp; b; z \u0026lt;= y | c; end 牢记：\u0026ldquo;时序用 \u0026lt;=、组合用 =\u0026rdquo;，几乎能避免所有赋值类时序 bug。\n九、parameter 与 localparam 两者都用于定义常量，可在模块内作为位宽、分频系数、地址等\u0026quot;可配置参数\u0026quot;使用。\nparameter：可被上层在实例化时覆盖的常量，用来做模块参数化（如数据位宽、从机地址）。 localparam：仅模块内部使用的常量，不能被实例化覆盖（如状态机的状态编码、分频系数）。 1 2 3 4 5 6 7 module uart_tx #(parameter DATA_W = 8, parameter BAUD = 115200) ( input clk, ... ); localparam IDLE = 2\u0026#39;b00; // 状态编码，不对外暴露 localparam CNT_MAX = 100_000_000 / BAUD - 1; reg [DATA_W-1:0] shift_reg; endmodule 现代模块推荐用 #(parameter ...) 的 ANSI 风格在端口前声明参数；实例化时可按名覆盖，如 uart_tx #(.DATA_W(16)) u_tx (...)。\n十、模块实例化 Verilog 通过实例化把若干子模块\u0026quot;连起来\u0026quot;构成更大系统。可以类比成**\u0026ldquo;搭电路板 / 插元件\u0026rdquo;**：每个子模块是一颗现成的芯片，实例化就是把它焊到板子上，而 .端口(信号) 就是用导线把外部信号连到这个元件对应的引脚上。\n实例化语法：模块名 实例名 ( .端口名(连接信号), ... );，推荐用按名连接（.port(signal)），顺序可乱、可读性好：\n1 2 3 4 5 6 7 8 9 wire [3:0] cnt_w; wire cout_w; counter10 u_counter ( // u_counter 是把 counter10 这个\u0026#34;芯片\u0026#34;焊到板子上的实例 .rstn (rstn), // 用导线把外部 rstn 连到它的 rstn 脚 .clk (clk), // 把 clk 连到它的 clk 脚 .cnt (cnt_w), // 它的 cnt 输出脚，引到板上的 cnt_w 这根导线 .cout (cout_w) ); 一个模块可以实例化多次（同一颗芯片焊多颗，只要取不同实例名）。也可用按位置连接（省略 .端口名，按模块定义顺序填实参），但易错、可读性差，不推荐。参数化模块实例化时还可按名覆盖参数，如 uart_tx #(.DATA_W(16)) u_tx (...)（见 §九）。\n","date":"2026-06-22T00:00:00Z","image":"https://blog.lumenth.me/p/verilog-tutorial/Verilog_hu_8f19066ff3bea683.png","permalink":"https://blog.lumenth.me/p/verilog-tutorial/","title":"Verilog语法教程"},{"content":"本文提供了一个可用于 Hugo 内容文件的基本 Markdown 语法示例，同时还展示了 Hugo 主题中基本 HTML 元素是否被 CSS 装饰。\n标题 以下 HTML \u0026lt;h1\u0026gt; 到 \u0026lt;h6\u0026gt; 元素表示六级标题。\u0026lt;h1\u0026gt; 是最高级别的标题，而 \u0026lt;h6\u0026gt; 是最低级别的标题。\nH1 H2 H3 H4 H5 H6 段落 这里可以放一段话\n这样换行两次是新开一段\n引用块 引用块 (blockquote) 元素表示引用自其他来源的内容，可以选择性地带有引用来源，这些引用必须放在 footer 或 cite 元素中，并且可以选择性地带有行内修改，如注释和缩写。\n引用段落（未注明来源） Tiam, ad mint andaepu dandae nostion secatur sequo quae. 注意你可以在引用块中使用Markdown语法 。\n带出处的引用 Don\u0026rsquo;t communicate by sharing memory, share memory by communicating.\n— Rob Pike1\n表格 表格不是核心 Markdown 规范的一部分, 但是 Hugo 支持使用。\nName Age Bob 27 Alice 23 表格内的内联 Markdown Italics Bold Code italics bold code A B C D E F Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus ultricies, sapien non euismod aliquam, dui ligula tincidunt odio, at accumsan nulla sapien eget ex. Proin eleifend dictum ipsum, non euismod ipsum pulvinar et. Vivamus sollicitudin, quam in pulvinar aliquam, metus elit pretium purus Proin sit amet velit nec enim imperdiet vehicula. Ut bibendum vestibulum quam, eu egestas turpis gravida nec Sed scelerisque nec turpis vel viverra. Vivamus vitae pretium sapien 代码块 带反引号的代码块 1 2 3 4 5 6 7 8 9 10 \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Example HTML5 Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;Test\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 缩进四个空格的代码块 \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026quot;en\u0026quot;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026quot;utf-8\u0026quot;\u0026gt; \u0026lt;title\u0026gt;Example HTML5 Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;Test\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 使用 Hugo 内置 highlight shortcode 的代码块 1 2 3 4 5 6 7 8 9 10 \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Example HTML5 Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;Test\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Diff 代码块 1 2 3 4 5 [dependencies.bevy] git = \u0026#34;https://github.com/bevyengine/bevy\u0026#34; rev = \u0026#34;11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13\u0026#34; - features = [\u0026#34;dynamic\u0026#34;] + features = [\u0026#34;jpeg\u0026#34;, \u0026#34;dynamic\u0026#34;] 列表类型 有序列表 First item Second item Third item 无序列表 List item Another item And another item 嵌套列表 Fruit Apple Orange Banana Dairy Milk Cheese 其他元素 — abbr, sub, sup, kbd, mark GIF 是一种位图图像格式。\nH2O\nXn + Yn = Zn\n按下 CTRL + ALT + Delete 以结束会话。\n大多数 蝾螈 是夜行性的，以昆虫、蠕虫和其他小型生物为食。\n超链接图片 The above quote is excerpted from Rob Pike\u0026rsquo;s talk during Gopherfest, November 18, 2015.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-12-06T00:00:00Z","image":"https://blog.lumenth.me/p/markdown-syntax/pawel-czerwinski-8uZPynIu-rQ-unsplash_hu_e5e8cf0ddc02aac7.png","permalink":"https://blog.lumenth.me/p/markdown-syntax/","title":"Markdown语法指南"},{"content":"本文提供了Ptthon语法指南，仅供参考。（参考菜鸟教程Python3）\nPython 数据类型 Python3 的六个标准数据类型：\n不可变数据（3 个）：Number（数字）、String（字符串）、Tuple（元组）； 可变数据（3 个）：List（列表）、Dictionary（字典）、Set（集合）。 python的变量可以直接赋值： 1 2 3 4 5 6 7 counter = 100 # 整型变量 miles = 1000.0 # 浮点型变量 name = \u0026#34;runoob\u0026#34; # 字符串 print (counter) print (miles) print (name) 执行以上程序会输出如下结果：\n1 2 3 100 1000.0 runoob Number（数字） Python3 支持 int、float、bool、complex（复数）\ntype() 函数可以用来查询变量所指的对象类型 1 2 3 \u0026gt;\u0026gt;\u0026gt; a, b, c, d = 20, 5.5, True, 4+3j \u0026gt;\u0026gt;\u0026gt; print(type(a), type(b), type(c), type(d)) \u0026lt;class \u0026#39;int\u0026#39;\u0026gt; \u0026lt;class \u0026#39;float\u0026#39;\u0026gt; \u0026lt;class \u0026#39;bool\u0026#39;\u0026gt; \u0026lt;class \u0026#39;complex\u0026#39;\u0026gt; String（字符串） 字符串用单引号 \u0026rsquo; 或双引号 \u0026quot; 括起来，同时使用反斜杠转义特殊字符。\n索引值以 0 为开始值，-1 为从末尾的开始位置。\nbool（布尔类型） 即 True 或 False。\nList（列表） 列表可以完成大多数集合类的数据结构实现。列表中元素的类型可以不相同，它支持数字，字符串甚至可以包含列表（所谓嵌套）。\n列表是写在方括号 [] 之间、用逗号分隔开的元素列表。\n和字符串一样，列表同样可以被索引和截取，列表被截取后返回一个包含所需元素的新列表。\n列表截取的语法格式如下： 1 变量[头下标:尾下标] 索引值以 0 为开始值，-1 为从末尾的开始位置。\nPython 列表截取可以接收第三个参数，参数作用是截取的步长:\n1 2 3 4 5 变量[头下标:尾下标：步长] letters = [\u0026#39;a\u0026#39;,\u0026#39;e\u0026#39;,\u0026#39;i\u0026#39;,\u0026#39;o\u0026#39;,\u0026#39;u\u0026#39;] letters[1:4:2] ------------------------------- [\u0026#39;e\u0026#39;, \u0026#39;o\u0026#39;] Tuple（元组） 元组（tuple）与列表类似，不同之处在于元组的元素不能修改。元组写在小括号 () 里，元素之间用逗号隔开。\n元组中的元素类型也可以不相同： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 tuple = ( \u0026#39;abcd\u0026#39;, 786 , 2.23, \u0026#39;runoob\u0026#39;, 70.2 ) tinytuple = (123, \u0026#39;runoob\u0026#39;) print (tuple) # 输出完整元组 print (tuple[0]) # 输出元组的第一个元素 print (tuple[1:3]) # 输出从第二个元素开始到第三个元素 print (tuple[2:]) # 输出从第三个元素开始的所有元素 print (tinytuple * 2) # 输出两次元组 print (tuple + tinytuple) # 连接元组 --------------------------------------------------------- (\u0026#39;abcd\u0026#39;, 786, 2.23, \u0026#39;runoob\u0026#39;, 70.2) abcd (786, 2.23) (2.23, \u0026#39;runoob\u0026#39;, 70.2) (123, \u0026#39;runoob\u0026#39;, 123, \u0026#39;runoob\u0026#39;) (\u0026#39;abcd\u0026#39;, 786, 2.23, \u0026#39;runoob\u0026#39;, 70.2, 123, \u0026#39;runoob\u0026#39;) 虽然tuple的元素不可改变，但它可以包含可变的对象，比如list列表。\n构造包含 0 个或 1 个元素的元组较特殊，有一些额外的语法规则：\ntup1 = () # 空元组\ntup2 = (20,) # 一个元素，需要在元素后添加逗号，只有一个元素的元组需要在元素后面添加一个逗号，以区分它是一个元组而不是一个普通的值，在没有逗号的情况下，Python会将括号解释为数学运算中的括号。\nSet（集合） Python 中的集合（Set）是一种无序、可变的数据类型，用于存储唯一的元素。\n集合中的元素不会重复，并且可以进行交集、并集、差集等常见的集合操作。\n在 Python 中，集合使用大括号 {} 表示，元素之间用逗号 , 分隔。\n另外，也可以使用 set() 函数创建集合。\n注意：创建一个空集合必须用 set() 而不是 { }，因为 { } 是用来创建一个空字典。\n创建格式：\nparame = {value01,value02,\u0026hellip;}或者set(value)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 sites = {\u0026#39;Google\u0026#39;, \u0026#39;Taobao\u0026#39;, \u0026#39;Runoob\u0026#39;, \u0026#39;Facebook\u0026#39;, \u0026#39;Zhihu\u0026#39;, \u0026#39;Baidu\u0026#39;} print(sites) # 输出集合，重复的元素被自动去掉 成员测试 if \u0026#39;Runoob\u0026#39; in sites : print(\u0026#39;Runoob 在集合中\u0026#39;) else : print(\u0026#39;Runoob 不在集合中\u0026#39;) set可以进行集合运算 a = set(\u0026#39;abracadabra\u0026#39;) b = set(\u0026#39;alacazam\u0026#39;) print(a) print(a - b) # a 和 b 的差集 print(a | b) # a 和 b 的并集 print(a \u0026amp; b) # a 和 b 的交集 print(a ^ b) # a 和 b 中不同时存在的元素 Dictionary（字典） 列表是有序的对象集合，字典是无序的对象集合。两者之间的区别在于：字典当中的元素是通过键来存取的，而不是通过偏移存取。\n字典是一种映射类型，字典用 { } 标识，它是一个无序的 键(key) : 值(value) 的集合。\n键(key)必须使用不可变类型。\n在同一个字典中，键(key)必须是唯一的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 dict = {} dict[\u0026#39;one\u0026#39;] = \u0026#34;1\u0026#34; dict[2] = \u0026#34;two\u0026#34; tinydict = {\u0026#39;name\u0026#39;: \u0026#39;runoob\u0026#39;,\u0026#39;code\u0026#39;:1, \u0026#39;site\u0026#39;: \u0026#39;www.runoob.com\u0026#39;} print (dict[\u0026#39;one\u0026#39;]) # 输出键为 \u0026#39;one\u0026#39; 的值 print (dict[2]) # 输出键为 2 的值 print (tinydict) # 输出完整的字典 print (tinydict.keys()) # 输出所有键 print (tinydict.values()) # 输出所有值 ------------------------------------------------- 1 two {\u0026#39;name\u0026#39;: \u0026#39;runoob\u0026#39;, \u0026#39;code\u0026#39;: 1, \u0026#39;site\u0026#39;: \u0026#39;www.runoob.com\u0026#39;} dict_keys([\u0026#39;name\u0026#39;, \u0026#39;code\u0026#39;, \u0026#39;site\u0026#39;]) dict_values([\u0026#39;runoob\u0026#39;, 1, \u0026#39;www.runoob.com\u0026#39;]) bytes 类型 在 Python3 中，bytes 类型表示的是不可变的二进制序列（byte sequence）。\n与字符串类型不同的是，bytes 类型中的元素是整数值（0 到 255 之间的整数），而不是 Unicode 字符。\nbytes 类型通常用于处理二进制数据，比如图像文件、音频文件、视频文件等等。在网络编程中，也经常使用 bytes 类型来传输二进制数据。\n创建 bytes 对象的方式有多种，最常见的方式是使用 b 前缀：\n此外，也可以使用 bytes() 函数将其他类型的对象转换为 bytes 类型。bytes() 函数的第一个参数是要转换的对象，第二个参数是编码方式，如果省略第二个参数，则默认使用 UTF-8 编码：\nx = bytes(\u0026ldquo;hello\u0026rdquo;, encoding=\u0026ldquo;utf-8\u0026rdquo;)\n与字符串类型类似，bytes 类型也支持许多操作和方法，如切片、拼接、查找、替换等等。同时，由于 bytes 类型是不可变的，因此在进行修改操作时需要创建一个新的 bytes 对象。例如： 1 2 3 4 5 6 7 8 x = b\u0026#34;hello\u0026#34; y = x[1:3] # 切片操作，得到 b\u0026#34;el\u0026#34; z = x + b\u0026#34;world\u0026#34; # 拼接操作，得到 b\u0026#34;helloworld\u0026#34; x = b\u0026#34;hello\u0026#34; if x[0] == ord(\u0026#34;h\u0026#34;): print(\u0026#34;The first element is \u0026#39;h\u0026#39;\u0026#34;) #其中 ord() 函数用于将字符转换为相应的整数值。 条件控制 条件语句是通过一条或多条语句的执行结果（True 或者 False）来决定执行的代码块。\nif 语句 1 2 3 4 5 6 if condition_1: statement_block_1 elif condition_2: statement_block_2 else: statement_block_3 循环语句 while 循环 while 语句的一般形式： 1 2 while 判断条件(condition)： 执行语句(statements)…… while 循环使用 else 语句\n如果 while 后面的条件语句为 false 时，则执行 else 的语句块。\n语法格式如下： 1 2 3 4 while \u0026lt;expr\u0026gt;: \u0026lt;statement(s)\u0026gt; else: \u0026lt;additional_statement(s)\u0026gt; for 语句 for循环的一般格式如下：\n1 2 3 4 for \u0026lt;variable\u0026gt; in \u0026lt;sequence\u0026gt;: \u0026lt;statements\u0026gt; else: \u0026lt;statements\u0026gt; 当循环执行完毕（即遍历完 iterable 中的所有元素）后，会执行 else 子句中的代码，如果在循环过程中遇到了 break 语句，则会中断循环，此时不会执行 else 子句。\nrange() 函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 如果你需要遍历数字序列，可以使用内置 range() 函数。它会生成数列，例如: \u0026gt;\u0026gt;\u0026gt;for i in range(5): ... print(i) ... 0 1 2 3 4 使用 range() 指定区间的值： \u0026gt;\u0026gt;\u0026gt;for i in range(5,9) : print(i) 5 6 7 8 \u0026gt;\u0026gt;\u0026gt; 也可以使 range() 以指定数字开始并指定不同的增量(甚至可以是负数，有时这也叫做\u0026#39;步长\u0026#39;): \u0026gt;\u0026gt;\u0026gt;for i in range(0, 10, 3) : print(i) 0 3 6 9 \u0026gt;\u0026gt;\u0026gt; 负数： \u0026gt;\u0026gt;\u0026gt;for i in range(-10, -100, -30) : print(i) -10 -40 -70 \u0026gt;\u0026gt;\u0026gt; 结合 range() 和 len() 函数以遍历一个序列的索引,如下所示: \u0026gt;\u0026gt;\u0026gt;a = [\u0026#39;Google\u0026#39;, \u0026#39;Baidu\u0026#39;, \u0026#39;Runoob\u0026#39;, \u0026#39;Taobao\u0026#39;, \u0026#39;QQ\u0026#39;] \u0026gt;\u0026gt;\u0026gt; for i in range(len(a)): ... print(i, a[i]) ... 0 Google 1 Baidu 2 Runoob 3 Taobao 4 QQ \u0026gt;\u0026gt;\u0026gt; 还可以使用 range() 函数来创建一个列表： \u0026gt;\u0026gt;\u0026gt;list(range(5)) [0, 1, 2, 3, 4] \u0026gt;\u0026gt;\u0026gt; break 和 continue 语句及循环中的 else 子句 break 语句可以跳出 for 和 while 的循环体。如果你从 for 或 while 循环中终止，任何对应的循环 else 块将不执行。\ncontinue 语句被用来告诉 Python 跳过当前循环块中的剩余语句，然后继续进行下一轮循环。\npass 语句 pass是空语句，是为了保持程序结构的完整性。\npass 不做任何事情，一般用做占位语句\nPython 推导式 Python 推导式是一种独特的数据处理方式，可以从一个数据序列构建另一个新的数据序列的结构体。\n列表推导式 列表推导式格式为：\n1 2 3 4 5 6 7 [表达式 for 变量 in 列表] [out_exp_res for out_exp in input_list] 或者 [表达式 for 变量 in 列表 if 条件] [out_exp_res for out_exp in input_list if condition] 字典推导式 字典推导基本格式：\n1 2 3 4 5 { key_expr: value_expr for value in collection } 或 { key_expr: value_expr for value in collection if condition } 集合推导式 集合推导式基本格式：\n1 2 3 { expression for item in Sequence } 或 { expression for item in Sequence if conditional } 元组推导式 元组推导式可以利用 range 区间、元组、列表、字典和集合等数据类型，快速生成一个满足指定需求的元组。\n元组推导式基本格式：\n1 2 3 (expression for item in Sequence ) 或 (expression for item in Sequence if conditional ) 迭代器与生成器 迭代器 迭代是是访问集合元素的一种方式,可以记住遍历的位置的对象。\n迭代器对象从集合的第一个元素开始访问，直到所有的元素被访问完结束。迭代器只能往前不会后退。\n迭代器有两个基本的方法：iter() 和 next()。\n字符串，列表或元组对象都可用于创建迭代器：\n1 2 3 4 5 6 7 \u0026gt;\u0026gt;\u0026gt; list=[1,2,3,4] \u0026gt;\u0026gt;\u0026gt; it = iter(list) # 创建迭代器对象 \u0026gt;\u0026gt;\u0026gt; print (next(it)) # 输出迭代器的下一个元素 1 \u0026gt;\u0026gt;\u0026gt; print (next(it)) 2 \u0026gt;\u0026gt;\u0026gt; 迭代器对象可以使用常规for语句进行遍历：\n1 2 3 4 list=[1,2,3,4] it = iter(list) # 创建迭代器对象 for x in it: print (x, end=\u0026#34; \u0026#34;) next() 函数：\n1 2 3 4 5 6 7 8 9 10 import sys # 引入 sys 模块 list=[1,2,3,4] it = iter(list) # 创建迭代器对象 while True: try: print (next(it)) except StopIteration: sys.exit() 生成器 yield 的函数被称为生成器（generator）。\nyield 是一个关键字，用于定义生成器函数，生成器函数是一种特殊的函数，可以在迭代过程中逐步产生值，而不是一次性返回所有结果。\n生成器是一个返回迭代器的函数，只能用于迭代操作，更简单点理解生成器就是一个迭代器。\n下面是一个简单的示例，展示了生成器函数的使用： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def countdown(n): while n \u0026gt; 0: yield n n -= 1 创建生成器对象 generator = countdown(5) 通过迭代生成器获取值 print(next(generator)) # 输出: 5 print(next(generator)) # 输出: 4 print(next(generator)) # 输出: 3 使用 for 循环迭代生成器 for value in generator: print(value) # 输出: 2 1 StopIteration\nStopIteration 异常用于标识迭代的完成，防止出现无限循环的情况，在 next() 方法中我们可以设置在完成指定循环次数后触发 StopIteration 异常来结束迭代。\n在 20 次迭代后停止执行：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class MyNumbers: def __iter__(self): self.a = 1 return self def __next__(self): if self.a \u0026lt;= 20: x = self.a self.a += 1 return x else: raise StopIteration myclass = MyNumbers() myiter = iter(myclass) for x in myiter: print(x) 函数 定义一个函数 你可以定义一个由自己想要功能的函数，以下是简单的规则：\n函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。 函数内容以冒号 : 起始，并且缩进。 return [表达式] 结束函数，选择性地返回一个值给调用方，不带表达式的 return 相当于返回 None。 面向对象 简介：\n类(Class): 描述具有相同的属性和方法的对象的集合。 方法：类中定义的函数。 类变量：类变量在整个实例化的对象中是公用的，定义在类中且在函数体之外。 数据成员：类变量或者实例变量用于处理类及其实例对象的相关的数据。 方法重写：如果从父类继承的方法不能满足子类的需求，可以对其进行改写，这个过程叫方法的覆盖（override），也称为方法的重写。 局部变量：定义在方法中的变量，只作用于当前实例的类。 实例变量：在类的声明中，属性是用变量来表示的，这种变量就称为实例变量，实例变量就是一个用 self 修饰的变量。 继承：即一个派生类（derived class）继承基类（base class）的字段和方法。 实例化：创建一个类的实例，类的具体对象。 对象：通过类定义的数据结构实例。对象包括两个数据成员（类变量和实例变量）和方法。 类 类定义： 1 2 3 4 5 6 class ClassName: \u0026lt;statement-1\u0026gt; . . . \u0026lt;statement-N\u0026gt; 类对象\n类对象支持两种操作：属性引用和实例化。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class MyClass: \u0026#34;\u0026#34;\u0026#34;一个简单的类实例\u0026#34;\u0026#34;\u0026#34; i = 12345 def f(self): return \u0026#39;hello world\u0026#39; 实例化类 x = MyClass() 访问类的属性和方法 print(\u0026#34;MyClass 类的属性 i 为：\u0026#34;, x.i) print(\u0026#34;MyClass 类的方法 f 输出为：\u0026#34;, x.f()) ------------------------------------------ MyClass 类的属性 i 为： 12345 MyClass 类的方法 f 输出为： hello world 构造方法 类有一个名为 init() 的特殊方法（构造方法），该方法在类实例化时会自动调用，像下面这样：\n1 2 def __init__(self): self.data = [] ","date":"2025-12-06T00:00:00Z","image":"https://blog.lumenth.me/p/python%E6%8C%87%E5%8D%97/Python_hu_ad82554eb807664f.jpg","permalink":"https://blog.lumenth.me/p/python%E6%8C%87%E5%8D%97/","title":"Python指南"},{"content":"Hello world 正文 已经不想在经历硬盘崩溃导致的数据文本丢失了，因此我决定开一个博客来记录我的学习经历，博客使用Hugo搭建。\n博客搭建方法来自Letere-莱特雷\n主题来自Stack\n以后我会在这里不定期更新学习经验\n图片 ","date":"2025-12-06T00:00:00Z","image":"https://blog.lumenth.me/p/my_firstblog/helena-hertz-wWZzXlDpMog-unsplash_hu_2307260c751d0e0b.jpg","permalink":"https://blog.lumenth.me/p/my_firstblog/","title":"我的第一个博客"}]