<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>
<title>JRNitre&#039;s Blog - 通讯协议</title>
<link>https://blog.atoery.cn/index.php/tag/message/</link>
<atom:link href="https://blog.atoery.cn/index.php/feed/tag/message/" rel="self" type="application/rss+xml" />
<language>zh-CN</language>
<description></description>
<lastBuildDate>Mon, 21 Apr 2025 22:05:00 +0800</lastBuildDate>
<pubDate>Mon, 21 Apr 2025 22:05:00 +0800</pubDate>
<item>
<title>[嵌入式] SPI 总线协议与实现</title>
<link>https://blog.atoery.cn/index.php/2025/04/21/98.html</link>
<guid>https://blog.atoery.cn/index.php/2025/04/21/98.html</guid>
<pubDate>Mon, 21 Apr 2025 22:05:00 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[1.0 SPI 简介[软件模拟 SPI 库]SPI (Serial Peripheral Interface - 串行外设接口) 是 Motorola 开发的一种由四根通信线构成的一主多从通用数...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>1.0 SPI 简介</h1><p><strong>[软件模拟 SPI 库]</strong><br></p><p>SPI (<em>Serial Peripheral Interface - 串行外设接口</em>) 是 Motorola 开发的一种由四根通信线构成的一主多从通用数据总线。</p><p>[SPI 总线具有如下特性]</p><ul><li>同步&全双工的通讯模式</li><li>支持总线挂载多设备</li></ul><h1>2.0 SPI 物理层</h1><p>SPI 总线由四根线构成：</p><ul><li><strong>SCK(Serial Clock)</strong>：时钟，用于提供时钟时序，数据的收发在时钟信号的上升沿或下降沿实现</li><li><strong>MOSI(Master Output Slave Input)</strong>：主机输出从机输入</li><li><strong>MISO(Master Input Slave Output)</strong>：主机输入从机输出</li><li><strong>SS(Slave Select)</strong> ：片选</li></ul><blockquote>值得注意的是，这里的名称并非标准，对于不同引脚名称的描述可能不同。</blockquote><p><strong>[一主多从]</strong></p><p>SPI 通讯仅支持一主机多从机的通讯方式，不支持多主机多从机的通讯方式。</p><p><strong>[SPI 片选]</strong></p><ul><li>主机的数据输出连接到从机的数据输入</li><li>从机的数据输出连接到主机的数据输入</li><li>从机的片选控制引脚连接到主机负责控制片选的引脚上</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/2414901253.png" alt="" title=""></p><p><strong>[通讯引脚 IO 配置]</strong></p><p>SPI 的输出引脚均配置为<strong>推挽输出</strong>；输入引脚配置为<strong>浮空</strong>或者<strong>上拉输入</strong>。</p><h1>3.0 SPI 协议层</h1><h2>3.1 数据传输原理</h2><p>SPI 的主机与从机中，均包含一个串行移位寄存器，该位移寄存器每来一个时钟信号移位寄存器会向左移动一位。</p><p>主机和从机中的移位寄存器的时钟由主机提供，同时该时钟也通过 SCK 引脚进行输出。</p><p>而主机移位寄存器移出的数据通过 MOSI 引脚传输至从机，而从机移位寄存器移出的数据通过 MISO 引脚传输至主机。</p><p>通过如上过程，仅需要主机时钟驱动即可实现数据在两个通信单位之间传输。</p><p>因此，SPI 通讯的核心思想是通过时钟驱动的位移寄存器将数据在主机和从机之间进行置换。</p><h2>3.2 SS 片选引脚</h2><p>SPI 通过单独的片选引脚选择不同的从机设备，需要与谁通信就将对应 SS 引脚拉低电平状态即可。</p><p>同一时间仅能选择一个从机，如果多从机被同时选择可能会产生数据冲突问题。</p><h2>3.3 SPI 时序基本单元</h2><h3>3.3.1 起始与终止信号</h3><ul><li>起始信号：SS(片选) 从高电平切换到低电平</li><li>结束信号：SS(片选) 从低电平切换到高电平</li></ul><p>因此在数据传输的过程总，通讯单元之间的 SS 引脚需要始终保持低电平。</p><h3>3.3.2 数据位的传输</h3><p>SPI 通讯为了兼容不同芯片等之间的通讯，其对于时钟驱动的数据读写方式并未做规定，因此针对不同的模式 SPI 有 4 中不同的工作模式。</p><h3>3.3.3 交换一个字节 (模式 0)</h3><ul><li>CPOL = 0：空闲状态时，SCK 为低电平</li><li>CPHA = 0：SCK 第一个边沿移入数据，第二个边沿移出数据</li></ul><p>值得注意的是在这种模式下，在 SCK 的第一个时钟边沿到来前通讯双方就要移除数据，也就是说在 SS 产生下降沿的时候就要将数据移出。</p><p>在实际使用过程中，模式 0 最为常用，后续基于代码实现中以模式 0 实现。</p><h3>3.3.4 交换一个字节 (模式 1)</h3><ul><li>CPOL = 0：空闲状态时，SCK 为低电平</li><li>CPHA = 1：SCK 第一个边沿移出数据，第二个边沿移入数据</li></ul><h3>3.3.5 交换一个字节 (模式 2&3)</h3><p>模式 2 和模式 3 仅是相对于模式 0 和模式 1 将其 SCK 信号取反，其它并无差别。</p><h1>4.0 SPI 代码实现</h1><h2>4.1 起始信号与结束信号</h2><p>SPI 的实现方式对比 I2C 简单粗暴，所谓开始&结束信号只需要拉低&高 SS 片选引脚即可：</p><pre><code>void spiStartSignal(void) {
    spIO_SS(0);
}

void spiStopSignal(void) {
    spIO_SS(1);
}</code></pre><p>值得注意的是，如果是以模式 0 和模式 2 工作的话，在起始信号发出后 MOSI 就要马上将需要发送的数据搬到 MOSI 引脚上；因此这里调用 MOSI 的函数中是没有用于控制通讯速率的延迟的：</p><pre><code>void spIO_SS(uint8_t bitValue) {    // SPI SS 片选信号
    if(bitValue != 0) {
        global_spiConfigure.IO_SS_H();
    }else{
        global_spiConfigure.IO_SS_L();
    }
}</code></pre><h3>4.2 交换一个字节数据</h3><p>根据上述对 SPI 的描述，实现交换字节的逻辑也比较简单：</p><pre><code>uint8_t spiSwapByte(uint8_t byte) {
    uint8_t recByte = 0x00;

    // 循环发送&amp;读取数据
    for(int i = 0; i &lt; 8; i++) {
        spIO_MOSI(byte &amp; (0x80 &gt;&gt; i));
        // SCK 上升沿移入(读取)数据
        spIO_SCK(1);
        if(spIO_GetBitValue() == 1) {
            recByte |= (0x80 &gt;&gt; i);
        }
        // SCK 下降沿移出(写入)数据
        spIO_SCK(0);
    }
    return recByte;
}</code></pre><ul><li>此处根据模式 0 来看，由于前边 SS 引脚已经发送了一个起始信号，因此这里 MOSI 需要马上发送第一位数据；<code>byte &amp; (0x80 &gt;&gt; i)</code> 这里通过掩码的方式逐次取出指定的数据位用于发送。</li><li>驱动 SCK 来到第一个边沿</li><li>从 MISO 引脚读取一个字节数据并存储到缓存 (<code>recByte</code>) 中</li><li>驱动 SCK 来到第二个边沿开始发送数据，一个循环结束。</li></ul><p><strong>[代码优化]</strong></p><p>针对上述代码，如果我们根据 SPI 硬件层面实现原理来看可以优化代码如下：</p><pre><code>uint8_t spiSwapByte(uint8_t byte) {
    // 循环发送&amp;读取数据
    for(int i = 0; i &lt; 8; i++) {
        // 发送最高位
        spIO_MOSI(byte &amp; 0x80);
        // SCK 上升沿移入(读取)数据
        spIO_SCK(1);
        byte &lt;&lt;= 1;
        if(spIO_GetBitValue() == 1) {
            // 将接收到的数据存至 byte 的最低位
            byte |= 0x01;
        }
        // SCK 下降沿移出(写入)数据
        spIO_SCK(0);
    }
    return byte;
}</code></pre><p>这其中与之前不同的是每次发送的都是 <code>byte</code> 的最高位，与 SPI 通讯主机中的位移寄存器工作原理相同，数据位被发送出去后，将 <code>byte</code> 整体左移一位，低位空出来的位置正好可以用于存储从机传来的数据。</p><p>因此在数据读取边沿将 <code>byte</code> 或等于 <code>0x01</code> 这样就可以将接收到的数据存储在 <code>byte</code> 的最低为，就像位移寄存器一样。</p><p>这样编程可以节省一个缓存变量，节约空间。</p><blockquote>此外，不同的模式仅有奇数边沿读取还是偶数边沿读，以及时钟的正反相的区别，因此针对模式 0 的发送函数，对代码语句的简单排序即可得到其它模式下的发送函数。</blockquote><pre><code>// Mode 1 模式
uint8_t spiSwapByte_mode1(uint8_t byte) {
    uint8_t recByte = 0x00;

    // 循环发送&amp;读取数据
    for(int i = 0; i &lt; 8; i++) {
        spIO_SCK(1);
        spIO_MOSI(byte &amp; (0x80 &gt;&gt; i));
        spIO_SCK(0);
        if(spIO_GetBitValue() == 1) {
            recByte |= (0x80 &gt;&gt; i);
        }
    }
    return recByte;
}</code></pre><h1>END 参考与声明</h1><p><strong>[参考]</strong></p><ul><li><a href="https://www.bilibili.com/video/BV1th411z7sn?spm_id_from=333.788.videopod.episodes&vd_source=ba723374214ddeba5614ef26265f47e0&p=36">STM32入门教程-2023版 细致讲解 中文字幕</a></li></ul>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/04/21/98.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/tag/message/</wfw:commentRss>
</item>
<item>
<title>[嵌入式] I2C 总线协议与实现</title>
<link>https://blog.atoery.cn/index.php/2025/04/02/71.html</link>
<guid>https://blog.atoery.cn/index.php/2025/04/02/71.html</guid>
<pubDate>Wed, 02 Apr 2025 10:18:00 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[1.0 I2C 简介本文章的参考代码已上传 Git 仓库：JRNitre/softwareI2CI2C&IIC (Inter-Integrated Circuit - 集成电路总线) 是由 NX...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>1.0 I2C 简介</h1><p>本文章的参考代码已上传 Git 仓库：<a href="http://mashirospace.cn:8418/JRNitre/softwareI2C">JRNitre/softwareI2C</a></p><p>I2C&IIC (<em>Inter-Integrated Circuit - 集成电路总线</em>) 是由 NXP (原 Phihips) 在八十年代初开发的一种多主机通用数据总线，主要用于近距离、低速的芯片之间通信；标准情况下最高传输速率为 100Kbps，快速模式下 400Kbps, 高速模式下 3.4Mbps。</p><p>其是一种两线式串行总线，顾名思义其由两根线完成数据通信；一根是数据线 SDA (Serial Data Line)  另一根是时钟线 SCL (Serial Clock Line) 主设备控制时钟频率来决定 I2C 的通信波特率。</p><p>[I2C 总线具有如下特性]</p><ul><li>传输的任意时刻仅能有一个主机。</li><li>同步通信</li><li>半双工通信</li><li>带数据应答</li><li>支持总线挂载多设备（一主多从、多主多从）</li></ul><h1>2.0 I2C 物理层</h1><p>I2C 总线有两条线构成数据传输总线：</p><ul><li>SCL：时钟线，用于主机控制数据发送的时序</li><li>SDA：数据传输线，用于传输数据</li></ul><blockquote>I2C 是多主从架构，每个设备都有唯一的通讯地址，理论上可以连接 127 个从设备。</blockquote><p>I2C 的两条总线在空闲状态时默认为高电平，因此在 I2C 总线的电路设计中，两根总线需要上拉至通讯 VCC 电平中。</p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/2207136780.png" alt="" title=""></p><p>因此，在使用单片机进行 I2C 通讯时，通讯引脚使用开漏输出对总线电平状态进行控制，引脚内部由 MOS 管控制对地导通 MOS 管关断总线上拉至 VCC; MOS 导通时总线通过 MOS 管至地，电平状态为低。</p><h1>3.0 I2C 协议层</h1><p>通过规定好的协议，按照一定规则操纵时钟线和数据线，即可实现主机与从机之间的数据交换，I2C 的大致通信过程如下：</p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/4112326226.png" alt="" title=""></p><h2>3.1 寻址方式</h2><p>主机发送起始信号开始通讯后，必须先发送一个字节的数据用于寻址；其中高七位为地址数据，最后一位为后续字节传输方向。</p><ul><li>传输方向位为 0：主机 -&gt; 从机</li><li>传输方向位为 1：从机 -&gt; 主机</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/3835967431.png" alt="" title=""></p><h2>3.2 基本时序单元</h2><h3>3.2.1 起始信号与停止信号</h3><ul><li>起始信号 (Start)：<code>当 SCL 为高电平时， SDA 从高电平向低电平跳变，代表开始传输数据</code></li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/2599483978.png" alt="" title=""></p><ul><li>结束信号 (Stop)：<code>当 SCL 为高电平时， SDA 从低电平向高电平跳变，代表数据传输结束</code></li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/2880638438.png" alt="" title=""></p><p>其中，起始与终止信号均由通讯主机发出，起始信号发出后总线处于占用状态；而停止信号发出后总线被释放处于空闲状态。</p><p>停止信号的发出有两种：</p><ul><li><strong>主机停止发送</strong>：发送停止信号</li><li><strong>从机停止接收，未向主机发送应答信号</strong>：此时主机发送停止信号结束通讯</li></ul><h3>3.2.2 应答信号</h3><p>在发送数据的过程中，所有地址或者数据都以 <code>8bit</code> 为单位进行传输，如果接收端正确的接收了 8bit 的数据，则回复一个 bit 的 <code>0</code> 作为应答信号 (ACK) 如果数据接收不正确或者接收端不再接收数据，则不回复总线状态为一个 bit <code>1</code> 的信号作为非应答信号 (NACK)。</p><p>因此 I2C 的一帧数据帧通常有 9 位。</p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/3548671479.png" alt="" title=""></p><h2>3.3 数据传输</h2><ul><li><strong>数据有效性</strong>：I2C 协议规定，在信号传输的过程中，在 SCL 为高电平时，SDA 的状态必须稳定，不允许产生电平跳变；只有在 SCL 为低电平的时候 SDA 的电平状态才可以变化。</li></ul><h3>3.3.1 I2C 发送一个字节</h3><p>基于上述基本时序单元可知，I2C 发送一个字节顺序如下：</p><ol><li>发送起始条件</li><li>发送从机设备地址</li><li>发送一位方向位</li><li>接收从机应答</li><li>发送有效数据</li><li>接收从机应答</li><li>循环 5，6 步骤，直到数据发送完毕或者无从机应答</li><li>发送结束条件</li></ol><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/891409240.png" alt="" title=""></p><h3>3.3.2 I2C 读取一个字节</h3><h2>4.0 I2C 代码实现</h2><h3>4.1 基本功能单元的封装</h3><h4>4.1.1 GPIO 工作模式配置</h4><p>由于 I2C 在工作时空闲状态为高电平，因此 SCL 和 SDA 引脚需要配置为开漏输出模式对总线进行控制。</p><pre><code>// 使能需要使用的 GPIO 所在的总线时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;

GPIO_InitStruct.GPIO_Mode        = GPIO_Mode_OUT;    // 输出模式
GPIO_InitStruct.GPIO_OType    = GPIO_OType_OD;    // 开漏
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;    

GPIO_InitStruct.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
GPIO_Init(I2C_GROUP,&amp;GPIO_InitStruct);</code></pre><h4>4.1.2 配置 TIM 定时器</h4><p>在使用 I2C 协议发送数据时，每个电平状态之间使用延时函数进行延时，从而对 I2C 的通信速度进行控制，这里使用 TIM2 定时器作为延时函数的基本实现。</p><p>在本文编写时，使用的 MCU 是 <code>STM32F401CCU6</code> 其默认时钟频率为 <code>84Mhz</code> 供给至 AB1 总线部分的时钟频率也为 <code>84Mhz</code>。</p><p>而根据上文可知，I2C 的标准通信速度为 <code>100Kbps</code> 由此可知每比特数据之间的传输间隔为：</p><p>$10us = \frac{1}{100000} $</p><p>对于 I2C 协议数据采样发生在高或低电平的中点，因此两次电平状态的时间间隔为 <code>5us</code>。</p><p>//</p><p>综上我们需要实现一个精度是 <code>us</code> 级别的延时函数，根据计数器时钟周期计算公式：</p><p>$T_{count} = \frac{(PSC + 1)  * (ARR + 1)}{f_{clock}} $</p><p>计算可知 PSC 应该等于 63 然后 <code>ARR = 延时时间 - 1</code> 得到 ARR = 4。</p><pre><code>RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

TIM_TimeBaseStructure.TIM_Period = 0xFFFFFFFF;
TIM_TimeBaseStructure.TIM_Prescaler = 83;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &amp;TIM_TimeBaseStructure);

TIM_Cmd(TIM2, ENABLE);</code></pre><h4>4.1.3 电平操纵函数</h4><p>将引脚操作函数和延时函数封装到一起，用于控制总线产生 0 或 1</p><pre><code>void i2cCon_SCL(uint8_t BitValue){
    GPIO_WriteBit(I2C_GROUP, I2C_SCL_PIN, (BitAction)(BitValue));
    i2cDelay_us(i2cSpeedDelay);
}

void i2cCon_SDA(uint8_t BitValue){
    GPIO_WriteBit(I2C_GROUP, I2C_SDA_PIN, (BitAction)(BitValue));
    i2cDelay_us(i2cSpeedDelay);
}</code></pre><h4>4.1.4 开始信号与结束信号</h4><p>基于上面封装的函数和 I2C 通信原理，组装信号时序：</p><pre><code>
void i2cSignal_Start(void){
    i2cCon_SDA(1);
    i2cCon_SCL(1);
    i2cCon_SDA(0);
    i2cCon_SCL(0);
}

void i2cSignal_Stop(void){
    i2cCon_SDA(0);
    i2cCon_SCL(1);
    i2cCon_SDA(1);
}
</code></pre><h3>4.2 数据收发</h3><h4>4.2.1 发送一个 bit 数据</h4><pre><code>void i2cSend_Byte(uint8_t byte){
    for(int i = 0; i &lt; 8; i++){
        i2cCon_SDA(!!(byte &amp; (0x80 &gt;&gt; i)));
        i2cCon_SCL(1);
        i2cCon_SCL(0);
    }
}</code></pre><h4>4.2.2 接收一个 bit 数据</h4><pre><code>uint8_t i2cReceive_Byte(void){
    uint8_t rByte = 0x00;
    i2cCon_SDA(1);
    
    for(int i = 0; i &lt; 8; i++){
        i2cCon_SCL(1);
        
        if(i2cReceive_SDA() == 1){
            rByte |= (0x80 &gt;&gt; i);
        }
        
        i2cCon_SCL(0);
    }
    
    return rByte;
}</code></pre><h3>4.3 应答信号的发送与处理</h3><h4>4.3.1 发送应答信号</h4><pre><code>void i2cSend_ACK(uint8_t ackBit){
    i2cCon_SDA(ackBit);
    i2cCon_SCL(1);
    i2cCon_SCL(0);
}</code></pre><h4>4.3.2 检查应答信号</h4><pre><code>uint8_t i2cReceive_ACK(void){
    i2cCon_SDA(1);
    i2cCon_SCL(1);
    uint8_t ackBit = i2cReceive_SDA();
    i2cCon_SCL(0);
    return ackBit;
}</code></pre>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/04/02/71.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/tag/message/</wfw:commentRss>
</item>
</channel>
</rss>