<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Spi on Klin's Notebook 🍉</title><link>https://klinlike.github.io/tags/spi/</link><description>Recent content in Spi on Klin's Notebook 🍉</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><copyright>Klin</copyright><lastBuildDate>Thu, 07 May 2026 20:11:00 +0800</lastBuildDate><atom:link href="https://klinlike.github.io/tags/spi/index.xml" rel="self" type="application/rss+xml"/><item><title>通信协议里的“高位在前”到底在说什么</title><link>https://klinlike.github.io/posts/2026-05-07-protocol-high-bit-meaning/</link><pubDate>Thu, 07 May 2026 20:11:00 +0800</pubDate><guid>https://klinlike.github.io/posts/2026-05-07-protocol-high-bit-meaning/</guid><description>&lt;p&gt;关键点在于：这里其实有两个层次的“高位”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字节层次&lt;/strong&gt;：多字节数据里先发高字节（更高权重的字节）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;位层次&lt;/strong&gt;：单字节内部先发高位（bit7 到 bit0）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个层次彼此独立，不要混为一谈。&lt;/p&gt;
&lt;h2 id="例子"&gt;例子
&lt;/h2&gt;&lt;p&gt;用 &lt;code&gt;12345 = 0x3039&lt;/code&gt; 作为例子，它由两个字节组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高字节：&lt;code&gt;0x30 = 0011 0000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;低字节：&lt;code&gt;0x39 = 0011 1001&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以 SPI 的 &lt;code&gt;MSB first&lt;/code&gt; 为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;strong&gt;字节层次&lt;/strong&gt;：先发高字节，再发低字节&lt;/li&gt;
&lt;li&gt;在&lt;strong&gt;位层次&lt;/strong&gt;：每个字节都按高位到低位发送&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;字节层次发送顺序：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;第1个：0x30（高字节）
第2个：0x39（低字节）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;位顺序：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;发送 0x30 时：
位顺序：0 -&amp;gt; 0 -&amp;gt; 1 -&amp;gt; 1 -&amp;gt; 0 -&amp;gt; 0 -&amp;gt; 0 -&amp;gt; 0
↑高位 ↑低位
发送 0x39 时：
位顺序：0 -&amp;gt; 0 -&amp;gt; 1 -&amp;gt; 1 -&amp;gt; 1 -&amp;gt; 0 -&amp;gt; 0 -&amp;gt; 1
↑高位 ↑低位
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="再看一个反例lsb-first"&gt;再看一个反例：LSB first
&lt;/h2&gt;&lt;p&gt;还是用 &lt;code&gt;0x3039&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字节层次（不变）：先发 &lt;code&gt;0x30&lt;/code&gt;，再发 &lt;code&gt;0x39&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;位层次（变化）：每个字节按低位到高位发送（bit0 到 bit7）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;位顺序：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;发送 0x30 时：
位顺序：0 -&amp;gt; 0 -&amp;gt; 0 -&amp;gt; 0 -&amp;gt; 1 -&amp;gt; 1 -&amp;gt; 0 -&amp;gt; 0
↑低位 ↑高位
发送 0x39 时：
位顺序：1 -&amp;gt; 0 -&amp;gt; 0 -&amp;gt; 1 -&amp;gt; 1 -&amp;gt; 1 -&amp;gt; 0 -&amp;gt; 0
↑低位 ↑高位
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>SPI NOR Flash硬件和指令</title><link>https://klinlike.github.io/posts/2026-05-06-spi-nor-flash/</link><pubDate>Wed, 06 May 2026 20:26:23 +0800</pubDate><guid>https://klinlike.github.io/posts/2026-05-06-spi-nor-flash/</guid><description>&lt;h2 id="nor-flash硬件"&gt;NOR Flash硬件
&lt;/h2&gt;&lt;p&gt;前提说明：本笔记以 SPI NOR Flash（Winbond W25Q64）为例，适用于 NOR 常见指令与擦写模型（如页编程、扇区擦除）。
若实际项目使用的是 SPI NAND，其页/块结构、读写流程、状态位含义及坏块/ECC 处理与 NOR 不同，不能直接套用本文细节。&lt;/p&gt;
&lt;h2 id="物理特性"&gt;物理特性
&lt;/h2&gt;&lt;p&gt;NOR Flash 的核心约束是“先擦后写”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;覆写数据前必须先擦除&lt;/li&gt;
&lt;li&gt;擦除后的比特位恢复为 &lt;code&gt;0xFF&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;擦除耗时通常明显高于写入&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="存储层次结构"&gt;存储层次结构
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页（Page）&lt;/strong&gt;：256 字节，最小写入单位&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扇区（Sector）&lt;/strong&gt;：通常 4KB（16 页），最小擦除单位&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;块（Block）&lt;/strong&gt;：32KB 或 64KB，可擦除单位&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;整片（Chip）&lt;/strong&gt;：支持全片擦除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大多数 SPI NOR Flash 都是这种层次。不同型号的差异通常体现在页大小、扇区/块组织和命令集细节；“256 字节”通常对应的是页而不是扇区。&lt;/p&gt;
&lt;h2 id="状态寄存器"&gt;状态寄存器
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BUSY = 1&lt;/strong&gt;：正在执行擦除/写入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BUSY = 0&lt;/strong&gt;：操作完成，可进行下一步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用法：每次擦除、写入后，都要轮询状态寄存器，等待 &lt;code&gt;BUSY = 0&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WEL = 1&lt;/strong&gt;：写使能已设置，可写入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WEL = 0&lt;/strong&gt;：写使能未设置，不可写入&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用法：每次擦除/写操作前都必须先让 &lt;code&gt;WEL = 1&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="常用命令"&gt;常用命令
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;命令/流程&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;读数据&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x03 + 地址 -&amp;gt; 读数据&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;读状态寄存器&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x05 -&amp;gt; 状态&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写使能&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x06&lt;/code&gt;（每次擦除/写后 WEL 自动清零，下次前需重新写使能）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;扇区擦除（4KB）&lt;/td&gt;
&lt;td&gt;写使能 + &lt;code&gt;0x20 + 地址&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;块擦除（32KB）&lt;/td&gt;
&lt;td&gt;写使能 + &lt;code&gt;0x52 + 地址&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;块擦除（64KB）&lt;/td&gt;
&lt;td&gt;写使能 + &lt;code&gt;0xD8 + 地址&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全片擦除&lt;/td&gt;
&lt;td&gt;写使能 + &lt;code&gt;0xC7&lt;/code&gt;（或 &lt;code&gt;0x60&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;写入流程略微复杂：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;写使能&lt;/li&gt;
&lt;li&gt;扇区擦除&lt;/li&gt;
&lt;li&gt;等 &lt;code&gt;BUSY=0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;写使能&lt;/li&gt;
&lt;li&gt;页编程 &lt;code&gt;0x02 + 地址 + 数据（1~256 字节）&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;等 &lt;code&gt;BUSY=0&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;页编程最多写入 256 字节。若写到页末尾后继续送数据，地址会在当前页内回绕到页起始位置，覆盖已有数据；单次页编程不能跨页。&lt;/p&gt;
&lt;h2 id="为什么叫页编程"&gt;为什么叫“页编程”
&lt;/h2&gt;&lt;p&gt;这是翻译带来的直觉偏差。英文是 &lt;code&gt;Page Program&lt;/code&gt;，其中 &lt;code&gt;Program&lt;/code&gt; 在这里是“写入/烧写”含义，不是“写程序”。
所以“页写入”更直观，但行业里约定俗成叫“页编程”。&lt;/p&gt;
&lt;h2 id="关于擦除"&gt;关于擦除
&lt;/h2&gt;&lt;p&gt;如果某个页自上次擦除后还没被写过，可以不重复擦除。比如刚完成全片擦除，或已擦除扇区但仅写入了该扇区中的其他页。&lt;/p&gt;
&lt;p&gt;要点是：&lt;strong&gt;擦除最小单位是扇区，写入最小单位是页&lt;/strong&gt;。擦除扇区会同时清掉这个扇区内其他页的数据，必要时应先备份再回写。&lt;/p&gt;
&lt;h2 id="地址回绕"&gt;地址回绕
&lt;/h2&gt;&lt;p&gt;例子：从地址 &lt;code&gt;0x10F0&lt;/code&gt; 写入 20 字节。&lt;/p&gt;
&lt;p&gt;页边界（每 256 字节一页）：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;页0：0x0000 ~ 0x00FF
页1：0x0100 ~ 0x01FF
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;按地址直觉，你可能以为会这样：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;0x10F0 ~ 0x10FF -&amp;gt; 写16字节（正常）
0x1100 ~ 0x1103 -&amp;gt; 写4字节（下一页开头）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但页编程实际会在当前页内回绕：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;0x10F0 ~ 0x10FF -&amp;gt; 写16字节
地址回绕 -&amp;gt; 回到0x1000（本页开头）
0x1000 ~ 0x1003 -&amp;gt; 写4字节（覆盖页开头原数据）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但这只是现象，为什么会有这种回绕机制呢？
因为 &lt;code&gt;Page Program&lt;/code&gt; 这条命令的本质是“在当前页连续写入”，内部地址自增只在页内有效（低 8 位递增）。
当超过该页末尾（256 字节边界）时，地址低 8 位会从 &lt;code&gt;0xFF -&amp;gt; 0x00&lt;/code&gt;，高位不变，于是回到同一页开头继续写，造成覆盖。&lt;/p&gt;
&lt;h2 id="需要注意的超时"&gt;需要注意的超时
&lt;/h2&gt;&lt;p&gt;虽然 Flash 硬件操作理论上一定会完成，但在实际工程里，软件可能永远等不到这个信号。
可能原因包括 &lt;code&gt;SPI&lt;/code&gt; 配置错误、中断未使能等。因此驱动开发一定要增加超时时间，作为系统兜底，避免永久卡死并方便异常处理。&lt;/p&gt;</description></item><item><title>SPI：与 I2C/UART 对比及四种模式</title><link>https://klinlike.github.io/posts/2026-05-04-spi-protocol-hal/</link><pubDate>Mon, 04 May 2026 19:55:22 +0800</pubDate><guid>https://klinlike.github.io/posts/2026-05-04-spi-protocol-hal/</guid><description>&lt;h2 id="四线接口clkdodics"&gt;四线接口：CLK、DO、DI、CS
&lt;/h2&gt;&lt;p&gt;四条线：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CLK&lt;/strong&gt; 时钟线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DO&lt;/strong&gt; 数据输出（主从视角下也常对应 &lt;strong&gt;MOSI&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DI&lt;/strong&gt; 数据输入（主从视角下也常对应 &lt;strong&gt;MISO&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CS&lt;/strong&gt; 片选（每个设备一条，独享）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="与-i2cuart-的对比"&gt;与 I2C、UART 的对比
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;SPI&lt;/th&gt;
&lt;th&gt;I2C&lt;/th&gt;
&lt;th&gt;UART&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;同步/异步&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;同步（有时钟线）&lt;/td&gt;
&lt;td&gt;同步（有时钟线）&lt;/td&gt;
&lt;td&gt;异步（无时钟线）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;通信线数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4线（CS/CLK/DO/DI）&lt;/td&gt;
&lt;td&gt;2线（SCL/SDA）&lt;/td&gt;
&lt;td&gt;2线（TX/RX）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;多设备&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CS 独享，CLK/DO/DI 共用&lt;/td&gt;
&lt;td&gt;地址选择&lt;/td&gt;
&lt;td&gt;点对点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;传输特点&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;发送 N 字节必接收 N 字节&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;主控控制读写&lt;/td&gt;
&lt;td&gt;全双工独立&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;速度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;高速（MHz 级）&lt;/td&gt;
&lt;td&gt;中速（100k–400k–1M）&lt;/td&gt;
&lt;td&gt;低速（波特率限制）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;适用场景&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flash、高速传感器&lt;/td&gt;
&lt;td&gt;EEPROM、传感器&lt;/td&gt;
&lt;td&gt;调试、通信模块&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;同步/异步&lt;/strong&gt;：有时钟线时，可以精准地知道何时读取数据，不需要约定波特率、误差也不会累积，所以速度快，这就是同步通信。否则只能异步通信，速度会慢。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CS 独享&lt;/strong&gt;：每个设备都有单独的一根 CS 线。和 I2C 不同，I2C 通过软件协议尽量复用连接线（地址广播），SPI 则是通过硬件解决，减少了协议开销，可以满足更高的速度要求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;发送 N 字节必接收 N 字节&lt;/strong&gt;：这是硬件上的限制，软件上可以读取但忽略、发送假数据，但是硬件上 SPI 被设计成了时钟由发送动作产生——不发送就没有时钟，也就不能接收。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="cpol--cpha-与四种模式"&gt;CPOL / CPHA 与四种模式
&lt;/h2&gt;&lt;p&gt;四种模式通过两个开关组合而来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPOL&lt;/strong&gt;（Clock Polarity，时钟极性）：空闲时时钟电平。CPOL=0 平时为低电平，CPOL=1 平时为高电平。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPHA&lt;/strong&gt;（Clock Phase，时钟相位）：在第几个边沿采样。CPHA=0 为第一个边沿，CPHA=1 为第二个边沿。&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;CPOL&lt;/th&gt;
&lt;th&gt;CPHA&lt;/th&gt;
&lt;th&gt;时钟空闲&lt;/th&gt;
&lt;th&gt;采样边沿&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mode 0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;低电平&lt;/td&gt;
&lt;td&gt;上升沿（第 1 个）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mode 1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;低电平&lt;/td&gt;
&lt;td&gt;下降沿（第 2 个）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mode 2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;高电平&lt;/td&gt;
&lt;td&gt;下降沿（第 1 个）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mode 3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;高电平&lt;/td&gt;
&lt;td&gt;上升沿（第 2 个）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;为什么需要多种模式？&lt;/strong&gt; 不同的设备有不同的数据输出时序要求：如果数据在时钟边沿前就准备好，就用 CPHA=0；如果数据在时钟边沿后才稳定，就用 CPHA=1。&lt;/p&gt;
&lt;p&gt;若数据在上升沿前已稳定，常用 Mode 0 或 Mode 3；若在下降沿前已稳定，常用 Mode 1 或 Mode 2。&lt;/p&gt;
&lt;h2 id="状态位txe-与-rxne"&gt;状态位：TXE 与 RXNE
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TXE&lt;/strong&gt;（Transmit Buffer Empty）：TX buffer 空，可以写数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RXNE&lt;/strong&gt;（Receive Buffer Not Empty）：RX buffer 非空，可读&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三种传输方式"&gt;三种传输方式
&lt;/h2&gt;&lt;p&gt;和 I2C、UART 一样，HAL 库也提供了&lt;strong&gt;查询、中断、DMA&lt;/strong&gt; 三种方式。&lt;/p&gt;
&lt;h3 id="核心函数"&gt;核心函数
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HAL_SPI_TransmitReceive&lt;/code&gt; — 同时发送和接收&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAL_SPI_Transmit&lt;/code&gt; — 仅发送&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAL_SPI_Receive&lt;/code&gt; — 仅接收&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>