<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://purl.org/rss/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel rdf:about="https://blog.atoery.cn/index.php/feed/rss/tag/cpp/">
<title>JRNitre&#039;s Blog - C/C++</title>
<link>https://blog.atoery.cn/index.php/tag/cpp/</link>
<description></description>
<items>
<rdf:Seq>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/07/28/161.html"/>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/06/29/156.html"/>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/06/10/155.html"/>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/06/05/153.html"/>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/05/25/149.html"/>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/05/04/102.html"/>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/04/21/98.html"/>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/04/19/100.html"/>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/04/16/92.html"/>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/04/16/91.html"/>
</rdf:Seq>
</items>
</channel>
<item rdf:about="https://blog.atoery.cn/index.php/2025/07/28/161.html">
<title>[STM32] EXTI 外部中断的介绍与使用</title>
<link>https://blog.atoery.cn/index.php/2025/07/28/161.html</link>
<dc:date>2025-07-28T11:58:42+08:00</dc:date>
<description>1.0 简介EXTI（Extern Interrupt）外部中断 EXTI 可以监测指定 GPIO 口的电平信号，当其指定的 GPIO 口产生电平变化的时候，EXTI 立即向 NVIC 发出中断申请，经过 NVIC 裁决后即可中断 CPU 主程序，使得 CPU 执行 EXTI 对应的中断程序。其中 EXTI 支持四种不同的触发方式上升沿 数字电平从低电平 0 变化到高电平 1 的一瞬间叫做上升沿。下降沿 数字电平从高电平 1 变化到低电平 0 的一瞬间叫做下降沿。双边沿 上升沿或者下降沿。软件触发 不需要硬件触发的外部中断，而由软件触发的中断。1.1 EXTI 特性EXTI 支持所有 GPIO 口，但是相同 Pin 的 IO 口不可以同时触发中断（比如PA0，PB0不能同时触发中断）。EXTI 支持的通道数：16个GPIO_Pin、PVD输出、RTC时钟、USB唤醒、以太网唤醒（合计20个中断线路）。触发相应方式：中断响应、事件相应（事件响应不会触发中断，而是触发别的外设操作）。2.0 EXIT 外部中断2.1 开启RCC时钟由于NVIC和EXTI是默认开启时钟的外设，因此不需要开启这两个外设的时钟；需要开启的有GPIO时钟以及AFIO时钟。RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOx, ENABLE);   // GPIO
RCC_APB2PeriphClockCmd (RCC_APB2Periph_AFIO, ENABLE);    // AFIO2.2 配置GPIO口工作模式由于使用EXTI外部中断，因此需要使目标 GPIO 口工作在上拉输入 (GPIO_Mode_IPU) 的模式下。GPIO_InitTypeDef GOIO_InitStrucuture;

GOIO_InitStrucuture.GPIO_Mode = GPIO_Mode_IPU;      // GPIO 上拉输入
GOIO_InitStrucuture.GPIO_Pin = GPIO_Pin_x;          // 配置需要使用的 Pin 口
GOIO_InitStrucuture.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_Init(GPIOB,&amp;GOIO_InitStrucuture);2.3 配置 AFIO通过 AFIO 将需要使用的 GPIO 口连接到 EXTI。GPIO_EXTILineConfig (GPIO_PortSourceGPIOx, GPIO_PinSourcex);
// 将 GPIOx 连接至 AFIO 的 x 中断线路上通过这个函数可以将指定的 GPIO 端口与指定的 EXTI 线路相连2.4 配置EXIT选择中断触发方式。EXTI_InitTypeDef EXTI_InitStructure;                        // EXTI初始化结构体

EXTI_InitStructure.EXTI_Line = EXTI_Linex;                  // 选择需要配置的中断线，这个与配置AFIO时所配置的中断线相同
EXTI_InitStructure.EXTI_LineCmd = ENABLE;                   // 选择指定中断线的状态
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;         // 指定模式(EXTI_Mode_Interrupt : 中断模式)
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;     // 指定触发信号的有效边沿

EXTI_Init(&amp;EXTI_InitStructure);EXTI 的几种触发模式 .EXTI_TriggerEXTI_Trigger_Rising —— 上升沿触发EXTI_Trigger_Falling —— 下降沿触发EXTI_Trigger_Rsing_Falling —— 双边沿触发2.5 配置 NVIC给中断分配一个合适的优先级。NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 
// 配置NCIV优先级分组（抢占优先级、响应优先级）
// 该函数在每一个芯片中只能使用一次

NVIC_TypeDef NVIC_InitStructure;                            // NCIV初始化结构体

NVIC_InitStructure.NCIV_IRQChannel = EXTI15_10_IRQn;        // 指定中断通道开启或者关闭
NVIC_InitStructure.NCIV_IRQChannelCmd = ENABLE;             // 指定通道使能还是失能（ENABLE,DISABLE）
NVIC_InitStructure.NCIV_IRQChannelPreemptionPriority =  1;  // 指定抢占优先级的优先级
NVIC_InitStructure.NCIV_IRQChannelSubPriority = 1;          // 指定响应优先级的优先级

NVIC_Init(&amp;NVIC_InitStructure);NVIC优先级的几种分组NVIC_PriorityGroup抢占优先级相应优先级NVIC_PriorityGroup_000-15NVIC_PriorityGroup_10-10-7NVIC_PriorityGroup_20-30-3NVIC_PriorityGroup_30-70-1NVIC_PriorityGroup_40-1502.5 EXTI中断函数在 EXTI 配置完毕后，当达到中断触发条件后就会自动执行中断函数中的代码。//中断函数
void EXTI15_10_IRQHandler(void){
    if (EXTI_GetITStatus(EXTI_Linex) == SET ) {      // 判断中断标志位，判断是否为想要触发中断的通道

        /* 
         * 需要执行的函数 
        */

        EXTI_ClearITPendingBit(EXTI_Linex);         // 清除中断标志位，防止程序重复申请中断造成死循环
    }
}</description>
</item>
<item rdf:about="https://blog.atoery.cn/index.php/2025/06/29/156.html">
<title>[Qt] Qt Quick QML 入门 / QML 与 C++ 进行交互 0x01</title>
<link>https://blog.atoery.cn/index.php/2025/06/29/156.html</link>
<dc:date>2025-06-29T15:42:00+08:00</dc:date>
<description>1.0 概述QML 是一种声明式脚本语言 (Qt Meta Language) 作为与 C++ 并列的 Qt 开发语言。示例：一个具有按钮的页面import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.5

Window {
    id: window
    visible: true
    width: 640
    height: 480
    title: qsTr(&quot;Button_Demo&quot;)

    Button{
        id: getBtn
        text: &quot;Button&quot;
        width: 120
        height: 40
        onClicked: {
            // TODO 点击回调
        }
    }

}2.0 QML 与 C++QML 与 C++ 的交互可以分为四种形式：注册 C++ 对象到 QML ，在 QML 中访问对象将 QML 暴露给 C++ 进行操作C++ 创建 QML 对象并进行操作C++ 对象与 QML 通过信号与槽进行交互本文章进对第一种方式进行介绍 (后续会完善别的方法并在上面进行引用)3.0 注册 C++ 对象到 QML 并使用首先创建一个类给 QML 调用：#include &lt;QObject&gt;

class qmlClass : public QObject
{
    Q_OBJECT
public:
    explicit qmlClass(QObject *parent = nullptr);

    Q_INVOKABLE void setValue(int value);
    Q_INVOKABLE int getValue(void);

signals:

private:
    int Value;
};创建一个类继承自 QObject 这里简单实现一个获取和设置 value 的方法。这里需要注意的是两个方法均用 Q_INVOKABLE 关键字进行修饰，这是类中的子方法能被 qml 访问的关键点，不能忽略。实现前者声明好的方法：qmlClass::qmlClass(QObject *parent) : QObject(parent){
    this-&gt;Value = 100;
}


int qmlClass::getValue(){
    return this-&gt;Value;
}

void qmlClass::setValue(int value){
    this-&gt;Value = value;
}现在来到 main.cpp 文件中，使用 qml 引擎将前者编写的类放在其中声明：qmlClass qclass;
engine.rootContext()-&gt;setContextProperty(&quot;qmlclass&quot;, &amp;qclass);#include &lt;QGuiApplication&gt;
#include &lt;QQmlApplicationEngine&gt;
#include &lt;QQmlContext&gt;

#include &quot;qmlclass.h&quot;

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;

    qmlClass qclass;
    engine.rootContext()-&gt;setContextProperty(&quot;qmlclass&quot;, &amp;qclass);


    const QUrl url(QStringLiteral(&quot;qrc:/main.qml&quot;));
    QObject::connect(&amp;engine, &amp;QQmlApplicationEngine::objectCreated,
                     &amp;app, [url](QObject *obj, const QUrl &amp;objUrl) {
        if (!obj &amp;&amp; url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}现在就可以在 qml 中调用前面编写好的代码了：声明一个 label 用于显示 valueLabel{
    id: label
    x: 298
    y: 118
    text: &quot;Value: &quot;
    anchors.horizontalCenter: parent.horizontalCenter
    anchors.bottom: getBtn.top
    anchors.bottomMargin: 5
    anchors.topMargin: 10
}在按钮的 onClicked 方法中调用我们编写的方法来更新 label 中的文本：    Button{
        id: getBtn
        text: &quot;getValue&quot;
        anchors.verticalCenterOffset: 0
        anchors.horizontalCenterOffset: 0
        width: 120
        height: 40
        anchors.centerIn: parent

        onClicked: {
            label.text = &quot;Value &quot; + qmlclass.getValue()
        }
    }实现效果：</description>
</item>
<item rdf:about="https://blog.atoery.cn/index.php/2025/06/10/155.html">
<title>[C++] 迭代器(iterator)介绍与使用</title>
<link>https://blog.atoery.cn/index.php/2025/06/10/155.html</link>
<dc:date>2025-06-10T14:21:30+08:00</dc:date>
<description>1.0 什么是迭代器？摘要迭代器是一种检查容器内元素并遍历元素的数据类型，通常用于对 C++ 中各种容器内元素的访问。指向某个位置的对象可能指向可读内存地址/对象用于以与数据布局无关的方式迭代容器元素还用于指定容器中的位置和范围 （用于插入、删除等）2.0 从标准容器中获取元素 begin() 和 end()可以从标准容器中获取一些特定位置的数据 (第一个元素、最后一个元素) 获取方式有两种：成员函数container.begin()container.end()独立函数std::begin(container)std::end(container)2.1 通过 * 来访问指定位置的元素基于 2.0 中给出的两种通过迭代器访问特定位置元素的方法，编写示例代码：std::vector&lt;int&gt; vint;

// init container
for (int i = 0; i &lt; 10; ++i){
    vint.push_back(i);
}

auto vint_begin = vint.begin();
auto vint_end = vint.end();

//auto vint_begin = std::begin(vint);
//auto vint_end = std::end(vint);

std::cout &lt;&lt; &quot;vector begin - &quot; &lt;&lt; *vint_begin &lt;&lt; std::endl;

return 0;通过 *vint_begin 可以访问指定位置的元素：*vint_begin*(vint_begin + 1)值得注意的是 *vint_end 迭代器仅用于位置指示器不能用于访问元素。vector -&gt;   |1|2|3|4|5|6|7| |
            |i|           |e|还有一些操作++i 向终点移动 1 (--i 向起点移动 1)vector -&gt;   |1|2|3|4|5|6|7| |
              |i|         |e|i += x 前进 x (i -= x 倒退 x)i += 4 / 前进 4
vector -&gt;   |1|2|3|4|5|6|7| |
                    |i|   |e|3.0 基于迭代器的循环通过上面对迭代器的介绍显而易见的简单用法：for (auto i = vint.begin(); i != vint.end(); ++i){
    std::cout &lt;&lt; *i &lt;&lt; std::endl;
}4.0 迭代范围4.1 从容器中擦除 erase()vint.erase(std::begin(vint) + 3, std::begin(vint) + 6);END 参考与引用[参考文章]C++ 迭代器简介 - hacking C++C++ 迭代器(iterator)超详解+实例演练</description>
</item>
<item rdf:about="https://blog.atoery.cn/index.php/2025/06/05/153.html">
<title>[C++] cmdline 轻量的命令行解析库</title>
<link>https://blog.atoery.cn/index.php/2025/06/05/153.html</link>
<dc:date>2025-06-05T17:44:00+08:00</dc:date>
<description>1.0 简述在使用 C++ 写命令行工具的时候，需要解析输入的参数，该库就是为这一解析工作提供一个方便快捷的方案。2.0 使用该项目仅包含一个头文件 include &quot;cmdline.h&quot;以下是一个简单示例#include &lt;iostream&gt;
#include &quot;cmdline/cmdline.h&quot;

int main(int argc, char *argv[]){
    /* 创建一个命令解析器 */
    cmdline::parser options;

    /*  1op -&gt; 长名称
     *  2op -&gt; 短名称 / 如果没有短名写 &#039;\0&#039;
     *  3op -&gt; 解释
     *  4op -&gt; 是否必填
     *  5op -&gt; 默认值，仅在 4op 为 false 的时候生效
     */
    options.add&lt;std::string&gt;(
        &quot;host&quot;,
        &#039;h&#039;,
        &quot;host name&quot;,
        true,
        &quot;&quot;);

    // range -&gt; 可以用于限制输入值范围
    options.add&lt;int&gt;(
        &quot;port&quot;,
        &#039;p&#039;,
        &quot;port number&quot;,
        false,
        80,
        cmdline::range(1, 65535));

    // oneof 限制参数的可选值
    options.add&lt;std::string&gt;(
        &quot;type&quot;,
        &#039;t&#039;,
        &quot;protocol type&quot;,
        false,
        &quot;http&quot;,
        cmdline::oneof&lt;std::string&gt;(&quot;http&quot;, &quot;https&quot;, &quot;ssh&quot;, &quot;ftp&quot;));

    // 可以定义 bool 值
    options.add(&quot;gzip&quot;, &#039;\0&#039;, &quot;gzip when transfer&quot;);

    /* 执行解析器 */
    options.parse_check(argc, argv);

    std::cout &lt;&lt; &quot;input type -&gt; &quot; &lt;&lt; options.get&lt;std::string&gt;(&quot;type&quot;) &lt;&lt; std::endl;
    std::cout &lt;&lt; &quot;input host -&gt; &quot; &lt;&lt; options.get&lt;std::string&gt;(&quot;host&quot;) &lt;&lt; std::endl;
    std::cout &lt;&lt; &quot;input port -&gt; &quot; &lt;&lt; options.get&lt;int&gt;(&quot;port&quot;) &lt;&lt; std::endl;

    // bool 值仅能通过 exist() 方法判断
    if (options.exist(&quot;gzip&quot;)){
        std::cout &lt;&lt; &quot;gzip enable&quot; &lt;&lt; std::endl;
    }else{
        std::cout &lt;&lt; &quot;gzip disable&quot; &lt;&lt; std::endl;
    }


    return 0;
}传入定义好的参数即可看到效果可以输出帮助消息END 参考与引用[参考文章]【C++】cmdline —— 轻量级的C++命令行解析库 </description>
</item>
<item rdf:about="https://blog.atoery.cn/index.php/2025/05/25/149.html">
<title>[C++] 函数高级用法</title>
<link>https://blog.atoery.cn/index.php/2025/05/25/149.html</link>
<dc:date>2025-05-25T18:56:03+08:00</dc:date>
<description>1.0 函数的默认参数函数的型参列表中，型参可以有默认值：返回值类型 函数名 (型参 = 默认值)int summation(int a = 10, int b = 20){
  return a + b;
}在使用函数的默认参数时有以下几点需要注意：如果函数中的某一个位置已经有了默认参数，则此后所有型参都必须有默认参数：int summation (int a, int b = 10, int c) 这样是不合法的。如果函数声明中有默认参数，那么函数实现中必须保持一致。2.0 函数的占位参数为函数提供占位参数，占位参数只有参数类型声明而没有参数名声明。int func(int a, int b, int){
  return a + b;
}一般情况下函数体内部无法使用占位函数，在 C++ 中使用占位参数可以方便后续程序拓展&兼容 C 中可能出现的不规范写法。3.0 函数重载函数重载使函数名可以相同，提高复用性。函数重载需要满足以下几个条件：同一个作用域下函数名相同函数参数类型、个数不同、顺序不同函数的返回值不可以成为函数重载的满足条件void func(int a){}

void func(const int a){}根据传入参数的数据类型的不同调用到的参数不同。</description>
</item>
<item rdf:about="https://blog.atoery.cn/index.php/2025/05/04/102.html">
<title>[屏幕驱动] SPI 驱动 ST7735s 应用 &amp;amp; 寄存器初始化详解</title>
<link>https://blog.atoery.cn/index.php/2025/05/04/102.html</link>
<dc:date>2025-05-04T22:29:00+08:00</dc:date>
<description>1.0 ST7735s 芯片简介st7735s 132RGB x 162 点 262k 色，带帧存储器的单芯片 TFT 控制器/驱动器ST7735s 使用 SPI 通讯接口可以将显示数据存储到芯片内部的显示 RAM 中，其容量位 132  162  18 位。[分辨率]其支持的分辨率为 132Bits  162Bits  RGB[显示模式]全彩色 (Full Color)：262K，RGB=(666)真彩色 (Color Reduce)：8色，RGB=(111)[显示特性]软件可编程色彩深度模式局部窗口移动和数据滚动功能2.0 电气连接&STM32引脚配置我这里使用的是一块集成了编码器的 1.8inc 屏幕：引脚功能BLK背光信号，文本例程不包含背光控制CSSPI 片选DC数据模式选择信号，1 -&gt; 写命令/ 0 -&gt; 写数据RST复位信号SDASPI 数据线，ST7735s 作从机，接主机的 MOSISCLSPI 时钟线VDD供电引脚，兼容 5V&3.3VGND接地本文基于 STM32401CCU6 HAL 库，采用硬件 SPI + DMA 的方式发送数据。APB 总线时钟频率配置为 84MHz。使用 SPI1_CH1，PA5-&gt;SCK/PA6-&gt;SPI1_MOSI/PA7-&gt;SPI1_MISO，这里我没有实现硬件片选，IO 配置为推挽输出手动操纵片选信号。配置三个推挽输出引脚用于 ST7735s 的 DC/CS/RST 引脚，PC13 用于操控指示灯。开启 DMA 连接到 SPI1_TX，这里 ST7735s 作为从机不会向主机发送信号，这里的 DMA 仅需要 TX 不需要 RX。使用外部时钟，开启 SWD 调试接口，开启 USART1 用于串口调试。上述配置完毕后 GENERATE CODE 生成代码，关于生成平台和 CubeMX 生成配置此处不赘述。3.0 ST7735s 控制流程flowchart TD
    A[启动] --&gt; B
    B[复位] --&gt; C
    C[初始化、配置参数] --&gt; D
    D[控制现存]3.0.5 SPI 基本操作功能封装根据前表可知，ST7735s 通过 DC 引脚控制数据&命令模式，结合软件控制 CS 引脚将其封装成指令&数据发送函数。发送指令void st7735s_sendCommand(uint8_t command) {
    st7735s_setNSS(GPIO_PIN_RESET);    // 拉低 CS 引脚，SPI 开始通讯
    HAL_GPIO_WritePin(ST7735S_DC_GRP, ST7735S_DC_PIN, GPIO_PIN_RESET);    // 将 DC 引脚置低代表传输的是指令
    HAL_SPI_Transmit(&amp;hspi1, &amp;command, 1, 0xFFFF);    // 调用硬件 SPI 发送数据
    st7735s_setNSS(GPIO_PIN_SET);    // 释放 CS 引脚，结束 SPI 通讯
}发送数据void st7735s_sendData(uint8_t data) {
    st7735s_setNSS(GPIO_PIN_RESET);    // 拉低 CS 引脚，SPI 开始通讯
    HAL_GPIO_WritePin(ST7735S_DC_GRP, ST7735S_DC_PIN, GPIO_PIN_SET);    // 将 DC 引脚置高代表传输的是数据
    HAL_SPI_Transmit(&amp;hspi1, &amp;data, 1, 0xFFFF);    // 调用硬件 SPI 发送数据
    st7735s_setNSS(GPIO_PIN_SET);    // 释放 CS 引脚，结束 SPI 通讯
}3.1 复位信号上表可知 Reset Pulse Duration(复位脉冲持续时间) 最小为 10us，Rest Cancel(复位取消) 最大时间为 5ms，并且复位后最好等待 120ms因此这里拉低 RST 引脚 1ms 后等待 120ms 再进行后续操作即可/* *.h */
#define ST7735S_RES_H       HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET)
#define ST7735S_RES_L       HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET)

/* *.c */
void st7735s_sendResetSignal(void) {
    ST7735S_RES_H;
    HAL_Delay(1);
    ST7735S_RES_L;
    HAL_Delay(120);
}3.2 参数配置3.2.1 0x11 退出睡眠模式首先执行指令 0x11 退出睡眠模式，方便后续配置，值得注意的是当退出睡眠模式后，此指令不会再发生作用，如果想要使用则需要再次进入睡眠模式 0x10 实现。当发送了指令退出睡眠模式后，需要等待 120ms 待电源电压和时钟电路稳定后再发送下一个命令&数据。st7735s_sendCommand(0x11);
HAL_Delay(120);3.2.2 帧率控制这里的 0xB1 到 0xB3 寄存器用于配置不同屏幕模式下的帧率，这里的三个寄存器配置方式一致，因此这里只给出 0xB1 的图片和配置参考，后两个寄存器参数与第一个一致：0xB1：正常模式/全彩色彩0xB2：空闲模式/8 色色彩0xB3：部分模式/全彩色彩这个配置项由三个 parameter 组成，每个 parameter 配置一个参数，不过这里不太一样，下面会解释：RTNA 这个参数占该寄存器的低四位，在配置这个参数前我们先看手册中给出的信息可知，这三个参数作为一个公式的变量存在，最后计算得到当前模式下的刷新率：$$
\frac{fosc}{(RTNA \times 2 + 40) \times (LINE + FPA + BPA + 2)}= FrameRate
$$FrameRate 最终得到的刷新率fosc 这是一个固定值 850kHzFPA 和 BPA 的值大于 0RTNA、FPA、BPA 这三个值由我们配置寄存器控制，LINE 是每行像素数量，我的屏幕是 160 (横向分辨率)得到了上面的条件后在我们配置寄存器前先计算一下参数：我们想将屏幕配置为 60Hz 根据公式可得：$$
\frac{850000}{(RTNA \times 2 + 40) \times (160 + FPA + BPA + 2)}= 60
$$整理可得$$
14166.66 = (2 \times RTNA + 40) \times (162 \times FPA \times BPA)
$$下面我们只需要找到三个参数满足上述上述式子并且 FPA 和 BPA 大于 0 即可，我们先假设 RTNA = 5$$
14166.66 = 50(162 \times FPA \times BPA)
$$$$
FPA + BPA = 121.33
$$现在的式子显而易见 FPA 取 60 BPA 取 61 即可。此时再回到寄存器表中 RTNA 取 5 -&gt; 0101 -&gt; 0x05，FPA 取 60 -&gt; 111100 -&gt; 0x3C，BPA 取 61 -&gt; 111101 -&gt; 0x3D0xB2 和 0xB3 使用同样的参数进行配置。/* Frame Rate Control - 帧率控制 */
st7735s_sendCommand(0xB1);  // Frame Rate Control - In normal Mode/Full colors
st7735s_sendData(0x05);
st7735s_sendData(0x3C);
st7735s_sendData(0x3C);

st7735s_sendCommand(0xB2);  // Frame Rate Control - In Idle mode/8 - colors
st7735s_sendData(0x05);
st7735s_sendData(0x3C);
st7735s_sendData(0x3C);

st7735s_sendCommand(0xB3);  // Frame Rate Control - In Partial mdoe/full colors
st7735s_sendData(0x05);
st7735s_sendData(0x3C);
st7735s_sendData(0x3C);3.2.3 0xB4 显示反转控制这个寄存器用于控制在各个模式下的像素反转方式，可以减少闪烁和提高显示质量；其有三个参数每个占一位：NLA：正常模式/全彩色彩NLB：空闲模式/8 色色彩NLC：部分模式/全彩色彩每一位置 0 代表点反转，1 代表列反转。其中点反转是用于大部分情况，这里全部置 0 -&gt; 0x00/* Display Inversion Control - 显示反转控制 */
st7735s_sendCommand(0xB4);
st7735s_sendData(0x00);3.2.4 电源控制[0xC0 Power Control 1 - 电源控制 1]0xC0 的寄存器手册如图，可见需要配置的参数比较多，该寄存器主要用于配置显示器的工作电压和模式，并且显而易见 C0H 列有五行，有三行参数可以配置，下面按照顺序介绍每行参数和每位的作用。[1st Parameter]AVDD 首先是该寄存器的高三位配置参数：AVDD 这是一个三位字段，用于配置模拟电源电压，用为液晶显示器提供驱动电压，根据表可得其有 8 种组合，对应的电压值为 4.5v 到 5.2v(步长 0.1v)至于如何配置该位需要取决于你的硬件设计和显示需求，如果需要更高的亮度和对比度可以选较高的值：111 -&gt; 5.2v。相反如果需要较低的功耗等则可以选择较低的电压。不过此处我们采取一个中间值：100 -&gt; 4.9v 作为我们的配置。VRHP 占用了剩下的五位，其作用是正参考电压高电平，它影响到了显示器内部的电压调节器的工作范围，正确的设置该参数有助于确保色彩和对比度的正确。首先根据手册可得，该位的配置分为了两个部分，首先 VRHP 的最高位用于配置 GVDD 的两种可选范围，我们也能看到当 VRHP 最高位为 1 时 GVDD 仅有六位有效位，其在范围上远不如为 0 时的范围（32 位）这种分段设计允许用户在较宽的范围内灵活调整 GVDD，同时保留了一个小范围的高电压的选项。这里我们选用 GVDD 范围较宽的配置，VRHP 最高位置 0如上，VRHP 最高位为 0 后，低四位就有了 32 种组合，对应 GVDD 的范围为：4.7v 到 3.15v，步长为 0.05v，这里的配置与上一步配置 AVDD 时相同，如果需要更高的对比度或亮度等则可以配置更高的 GVDD 值反之亦然。因此与 AVDD 配置时相同，我们选用一个中间值：01000 -&gt; 4.3v 作为我们的配置。结合上述文字，VRHP 部分的配置就为：001000。到此 针对 1 parameter 的配置解析就完成了，不过在我们将数据写入到寄存器之前还有一个细节需要注意，前文我们提到 AVDD 需要三位用于配置，而在介绍 VRHP 的时候提到了通过 VRHP[5] 配置电压范围，这里的 5 很关键，这代表了这一个参数配置需要使用 6 位的空间？这明显不对，与 AVDD 的配置数据加起来明显超了！再次查表发现 VRHP[5] 被放在了 3rd parameter 的最低位中，同样后文要讲的 VRHN[5] 也在这配置，所以需要要将刚刚配置好的 VRHP 001000 中的最高位摘取出 1 位，即 01000 与 AVDD 的配置位合并写入到第一个数据包中：10001000 -&gt; 0x88[2nd Parameter]VRHN 第二个数据包就比较简单了，仅有低 5 位用于配置 VRHN 与刚才讲到的 VRHP 相反，用于配置负参考电压高电平，该参数的影响前文也提到了，若想拥有用户需要的显示效果则需要正确配置这两个参数。VRHN 的配置方式与 VRHP 的配置方式完全相同，这里就不再赘述了，不过值得注意的是它的 VRHN[5] 同样在第三个数据包中 (在第三个数据包中还会提的)同样的我们采用中间值作为我们的配置：01000 -&gt; 4.3v 作为我们的配置。到此 第二个数据包就配置完成了，根据手册高三位写 0 低五位写 01000 即为 00001000 -&gt; 0x08[3rd Parameter]MODE 这是一个两位字段，占用寄存器高两位进行配置，用于控制电源模式，其有四种合。其中表中标注了 01和11 该两位作为保留位并不建议使用Don't use this setting!。那么此时仅剩两个配置项：00 -&gt; 2X 模式和 10 -&gt; AUTO 模式00 可以将倍压电路设置工作在 2 倍增压模式。10 可以自动调整倍压电路的工作状态，根据负载动态优化功耗和性能。可见既然提供了 AUTO 模式，无脑配置为 AUTO 也可以适应绝大多数应用场景。到此 我们配置好了 0xC0 的所有参数，并且将我们上述提到的 VRHP 和 VRHN 的范围控制位和 MDOE 配置位打包写入即可完成配置，根据手册可得 MODE 写入到高两位，另外两个参数 (均为 0) 写入低两位，其余位根据手册要求 [D5 - D2] -&gt; 0001。最终得到 10000100 -&gt; 0x84，结合上面的配置，针对 0xC0 的配置数据包即为 0x88、0x08、0x84[0xC1 Power Control 2 - 电源控制 2]0xC1 同样是电源配置寄存器，不过它需要发送的数据包还是比较少的 (发送一个就行)，首先介绍一些该寄存器的功能，它主要用于控制 VGH 和 VGL 的供电电平，这些电压直接影响到显示器的驱动能力和功耗。VGH：正高压，用于驱动液晶单元的开启状态。VGL：负低压，用于驱动液晶单元的关闭状态。下面我们优先介绍位于 D3 和 D2 位的 VGHBT 这个参数直接影响 VGH 的输出，VGH 的值通常会带来更高的对比度和更快的相应速度，但是可能会增加功耗。这是一个两位的参数，共有四种配置选项：00 -&gt; $2 \times AVDD + VGH25 - 0.5$01 -&gt; $3 \times AVDD - 0.5$10 -&gt; $3 \times AVDD + VGH25 - 0.5$11 -&gt; 不推荐使用，保留用于测试可以看到关于 VGH 的值有三种可用的配置公式，其中涉及到 AVDD 和本寄存器中可以配置 VGH25。在我们的配置中，还是选择一个平衡的方案进行配置，这里选用计算较为简单的 $3 \times AVDD - 0.5$ 方案 -&gt; 01 根据前文的配置可以知道 AVDD 我们配置为了 4.9v 我们已知了所有的参数，但是现在并不要着急计算该式子的值，我们继续往下看寄存器的配置。VGLSEL 该参数同样有四种配置选项，其不用计算直接控制 VGL 的输出：00 -&gt; -7.501 -&gt; -1010 -&gt; -12.511 -&gt; -13一样的理念，采用 -10v -&gt; 01 作为一个平衡的选择。现在我们已经有了计划好的 VGH 和 VGL 我们还需要注意一件事情，手册中提到：$VGH - VGL \le 32V$ 这个条件限制了 VGH 和 VGL 之间的差值不能超过 32V 这一限制可以保护液晶显示器中的电子元件。根据上述的限制我们计算一下刚才选择的配置是否合理。$$ 
VGH = 3 \times 4.9 - 0.5 = 14.2V
$$$$
VGL = -10V
$$将二者相减可得 24.2V 满足限制条件。现在我们配置好了两个参数，还有一个参数 VGH25 并未配置，虽然它会在 VGHBT 中参与运算，但是本次我们并未选用，不过该位置也要配置一个参数防止未来切换配置或者硬件中存在以来该值的位置。对照手册将其设置位一个平衡值 2.2 -&gt; 01 即可。综上所述根据手册的要求将 VGH25 置于高两位，VGHB 和 VGLSEL 按照顺序置于最低位，其余位置 0 即可 01000101 -&gt; 0x69[0xC2 Power Control 3 - 电源控制 3]与 Power Control 1 寄存器类似，它由两个数据包配置，一共控制三个参数。[1st Parameter]DCA 首先，我们先看第一个 parameter 的高两位，是 DCA9 和 DCA8 如果此时关注一下第二个 parameter 就能发现还有 DCA7 到 DCA6 很显然这个参数的配置有一点不一样！不过在配置这个参数之前需要了解这一位是做什么的：DCA 用于将升压电路的升压周期设置为正常模式或全彩模式，具体来说它通过调整升压时钟 (BCLK) 的分频系数来控制升压电路的工作频率，关于这个参数配置我们需要重点关注一下手册中提供的表格。首先从位于第一个 parameter 高两位的 DCA[9:8] 看起，这一位用于配置整体的分频系数的范围，它决定了升压电路的基本工作频率范围，在该参数选定后后续的参数用于调节每一个部分的分频比例。DCA[9:8] 有四种选择，对应了四种范围，如何判断范围呢？这张表需要以行为单位看，比如 DCA[9:8] = 00 时 DCA[7:0] 的可选项就已经配置好了 BLCK/3、BLCK/1、BLCK/1、BLCK/1，根据表可得：BLCK/1 -&gt; 00BLCK/3 -&gt; 01BLCK/2 -&gt; 10BLCK/4 -&gt; 11BLCK/3、BLCK/1、BLCK/1、BLCK/1 -&gt; 01_00_00_00在确定好了后续可以选择的范围之后我们着重关心的就是 DCA[9:8] 的选择了，此时众多的分频频率到底是如何影响最终的显示效果的？前面我们在配置 VGH 和 VGL 的时候看到这两个参数配置好后往往需要 4/5V 以上的电压，这就依赖升压电路来提供所需电压：较高的分频值就意味着更长的升压周期，通常会降低功耗但是可能会导致升压效率的下降。较低的分频值就意味着更快的升压周期，通常能提供更高的升压效率，但是可能会增加功耗。了解了上面的信息后，对于参数的配置就豁然开朗了，我们还是选择平衡的配置选项，首先将分频范围限制在 BCLK/1 到 BLCK/3 之间，对应 DCA[9:8] -&gt; 00在这种范围限制下表格给出了如下分配分频配置：DCA[7:6] -&gt; BCLK/3 -&gt; 01DCA[5:4] -&gt; BCLK/1 -&gt; 00DCA[3:2] -&gt; BCLK/1 -&gt; 00DCA[1:0] -&gt; BCLK/1 -&gt; 00综上所述针对 DCA[9:0] 的配置应该为 0001000000 由于我们先构建第一个 parameter 因此只抽取 DCA[9:8] -&gt; 00AP 这个参数占用了寄存器的低三位，共有 8 种组合，用于配置运算放大器中的电流量。运算放大器在液晶显示器中用于信号放大和电压调节，通过调整运算放大器的电流量可以调整显示效果、功耗和响应速度。000 -&gt; 运算放大器停止工作001 -&gt; 小电流010 -&gt; 中低电流011 -&gt; 中电流100 -&gt; 中高电流101 -&gt; 大电流110 -&gt; 保留/不使用111 -&gt; 保留/不使用根据上述的数据显而易见，最后两种组合为保留组合并且如果关闭了运算放大器可能会导致显示器无法正常工作，根据文字叙述我们采用中等电流 011 即可。SPA 手册中同样提供了这个参数的表格，可见他跟 AP 的配置完全一样，它的作用是控制源极驱动电路中的运算放大器电流量，虽然与 AP 名字上有所不同，但是功能上大抵相同这里不再赘述直接使用 AP 的配置 011至此第一个数据包就构建完毕了由两位 DAC 参数 00 加三位 SPA 和 两位 AP 参数构成 00011011 -&gt; 0x1B[2nd Parameter]根据一个数据包构建时的分析来看，这第二个数据包就很简单了，将剩余的分频配置写入即可 01000000 -&gt; 0x40[0xC3 Power Control 4 - 电源控制 4]这个部分的配置整体上与 0xC2 的配置相同 0x1B 和 0x40[0xC4 Power Control 5 - 电源控制 5]同样的这里的配置比较无趣，与上面相同 0x1B 和 0x40[0xC5 VCOM Control 1]VCOM 控制，该寄存器仅配置一个参数，VCOM 电压，VCOM 是显示器中公共极电压，其对显示效果有重要影响具体会影响闪烁、对比度和色彩均匀性。闪烁：过高的 VCOM 电压会导致屏幕闪烁，过低会导致图像暗淡或出现色彩偏差。对比度：适当的 VCOM 电压会增强图像对比度，使颜色更加鲜明。手册提供了详细的配置项和电压的对照表，这里选用位于中间的 -1.2v -&gt; 011111 -&gt; 0x1F由于该配置项占低位因此空余位置全部写 0 该寄存器配置为 0x1F 即可。[0xC5 VCOM Offset Control]这是电源配置部分的最后一个寄存器，用于配置 VCOM 偏移量，微调 VCOM 电压，功能上与上一个寄存器相同，或者说这两个寄存器配置的都是同一个参数，这个参数分为两个部分；此外手册中提到如果需要使用这个参数需要将位于 0xD9 的 VMF_EN 位置 1VMF[4]：用于控制偏移方向 0 正向偏移，1 负向偏移。VMF[3:0]：配置具体的偏移量，从 +16d 到 -15d由于这里是用于微调 VCOM 参数的，这里根据需要开启和配置参数进行调整，本例中就不开启这个参数了。[电源配置 - 代码]到这里电源部分就配置完毕了，代码部分如下：/* Power Contrl - 电源配置 */
st7735s_sendCommand(0xC0);  // Power Control 1
st7735s_sendData(0x88);
st7735s_sendData(0x08);
st7735s_sendData(0x84);

st7735s_sendCommand(0xC1);  // Power Control 2
st7735s_sendData(0x69);

st7735s_sendCommand(0xC2);  // Power Control 3
st7735s_sendData(0x1B);
st7735s_sendData(0x40);

st7735s_sendCommand(0xC3);  // Power Control 4
st7735s_sendData(0x1B);
st7735s_sendData(0x40);

st7735s_sendCommand(0xC4);  // Power Control 5
st7735s_sendData(0x1B);
st7735s_sendData(0x40);

st7735s_sendCommand(0xC5);  // VCOM Voltage
st7735s_sendData(0x1F);3.2.5 0x36 配置显存数据访问方式[地址顺序配置 - 高三位]MY：行地址顺序 - 如果希望图像上下反转此位置高用于控制行地址顺序是否反转 MY 置高则行地址顺序将被反转（即从最后一行开始到第一行）MX：列地址顺序 - 如果希望图像左右反转此位置高这一位用于控制列地址是否反转其功能与 MY 位功能一致不再赘述。MV：行列地址顺序交换 - 如果希望图像旋转 90°/270° 此位置高当此位被置高时，原本的行地址将作为列地址使用，列地址将作为行地址使用；通常的表现时屏幕内容向左或者向右旋转了 90°这三位搭配使用可以用于配置屏幕内容的方向，以便适应不同的硬件布局等；本例中不需要特殊布局全部置 0[ML 垂直刷新方向配置]此位用于配置屏幕的垂直刷新方向，默认（0）位从上到下刷新屏幕；配置为 1 后变为从下到上刷新屏幕；此处使用默认刷新方向配置为 0[RGB&amp;BGR 颜色选择开关控制]此位配置位低使用 RGB 模式，配置为高则为 BGR 模式，此处使用 RGB 模式配置为 0 ；注意，屏幕色彩如果出现问题则可以优先检查程序中使用的 RGB 模式与配置的模式相同。[MH 水平刷新方向配置]此位作用与 ML 位相同，配置水平刷新方向，配置为 0 则为从左向右刷新，配置为 1 则为从右向左刷新，此处使用默认配置为 0由于我打算竖着用这块屏幕所以将其上下左右翻转 0xC0/* Memory Data Access Control - 显存数据访问方式 */
st7735s_sendCommand(0x36);
st7735s_sendData(0xC0);3.2.6 Gamma 矫正[正 Gamma 矫正]这个配置这个指令所需要的 parameter 比较多，足足有 16 个，但是仔细看一看给出的寄存器表格就可以发现实际上一共配置了三个参数:VRF0P[5:0] - 6 位二进制值，用于调整高电平可变电阻 VRHP，较大的值可以提高最高灰度级的亮度，但可能会导致过曝。PK0P[5:0] ~ PK9P[5:0] - 这些参数用于配置不同灰度级别上的表现，可以精细调整这里的参数用于获得更好的显示效果。SELVOP[5:0] ~ SELV63P[5:0] - 6 位二进制值，用于选择不同灰度级的电压，影响颜色显示的准确性和均匀性，这里可以参照标准 Gamma 曲线进行设置。VOS0P[5:0] - 6 位二进制值，用于调整低电平可变电阻 VRLP，较大的值可以提高最低灰度级的亮度，但可能导致黑场不纯。[负 Gamma 矫正]由于负 Gamma 矫正配置的是当像素电平较低时的参数，因此与正 Gamma 矫正使用同样的参数。综上 Gamma 配置部分我直接采用了别人验证好的一套参数，代码如下：/* Gamma Correction Characteristics Setting - Gamma 矫正设置 */
st7735s_sendCommand(0xE0);  // 正极性 Gamma 校正
st7735s_sendData(0x20);     // VRF0P = 0x20
st7735s_sendData(0x00);     // SELV0P = 0x00
st7735s_sendData(0x02);     // SELV1P = 0x02
st7735s_sendData(0x04);     // SELV2P = 0x04
st7735s_sendData(0x08);     // SELV3P = 0x08
st7735s_sendData(0x0E);     // SELV4P = 0x0E
st7735s_sendData(0x12);     // SELV5P = 0x12
st7735s_sendData(0x1A);     // SELV6P = 0x1A
st7735s_sendData(0x24);     // SELV7P = 0x24
st7735s_sendData(0x2A);     // SELV8P = 0x2A
st7735s_sendData(0x30);     // SELV9P = 0x30
st7735s_sendData(0x36);     // SELV10P = 0x36
st7735s_sendData(0x3C);     // SELV11P = 0x3C
st7735s_sendData(0x3F);     // SELV12P = 0x3F
st7735s_sendData(0x00);     // SELV13P = 0x00
st7735s_sendData(0x00);     // SELV14P = 0x00

st7735s_sendCommand(0xE1);  // 负极性 Gamma 校正
st7735s_sendData(0x04);     // VRF0N
st7735s_sendData(0x16);     // SELV0N
st7735s_sendData(0x06);     // SELV1N
st7735s_sendData(0x0D);     // SELV2N
st7735s_sendData(0x2D);     // SELV3N
st7735s_sendData(0x26);     // SELV4N
st7735s_sendData(0x23);     // SELV5N
st7735s_sendData(0x27);     // SELV6N
st7735s_sendData(0x27);     // SELV7N
st7735s_sendData(0x25);     // SELV8N
st7735s_sendData(0x2D);     // SELV9N
st7735s_sendData(0x3B);     // SELV10N
st7735s_sendData(0x02);     // SELV11N
st7735s_sendData(0x03);     // SELV12N
st7735s_sendData(0x06);     // SELV13N
st7735s_sendData(0x13);     // SELV14N3.2.7 0x3A 接口像素格式这个寄存器用于配置每位像素使用的颜色深度，这会影响显示颜色的质量和数据传输效率，这个寄存器只有一个参数 IFPF 根据表可得：011 -&gt; 12-bit101 -&gt; 16-bit110 -&gt; 18-bit111 -&gt; 未使用这里使用最常用的 16bit 色彩深度 101 -&gt; 0x05/* Interface Pixel Format - 接口像素格式 */
st7735s_sendCommand(0x3A);
st7735s_sendData(0x05);3.2.8 0x29 开启显示到了这里 ST7735s 的初始化配置流程就结束了，最后发送 0x29 命令即可开启显示。3.3 屏幕坐标位置控制由于我使用的屏幕分辨率为 128 * 160 因此在这个屏幕中表示坐标仅需要两个八位数据即可，同理如果需要表示一个范围直接使用四个八位数据即可。在 ST7735s 中使用 0x2A 和 0x2B 分别表示 x 范围和 y 范围。由于我们只用到了每个寄存器的低八位进行传输，因此两个寄存器不使用的部分填充 0 即可。st7735s_sendCommand(0x2A);  // Cloumn Address Set
st7735s_sendData(0x00);
st7735s_sendData(xStart);
st7735s_sendData(0x00);
st7735s_sendData(xEnd);

st7735s_sendCommand(0x2B);  // Row Address Set
st7735s_sendData(0x00);
st7735s_sendData(yStart);
st7735s_sendData(0x00);
st7735s_sendData(yEnd);坐标范围配置好后通过发送 0x2C 指令后即可开始发送颜色数据，对坐标范围内的颜色进行配置。void st7735s_write_address(uint8_t xStart, uint8_t xEnd, uint8_t yStart, uint8_t yEnd) {
    st7735s_sendCommand(0x2A);  // Cloumn Address Set
    st7735s_sendData(0x00);
    st7735s_sendData(xStart);
    st7735s_sendData(0x00);
    st7735s_sendData(xEnd);

    st7735s_sendCommand(0x2B);  // Row Address Set
    st7735s_sendData(0x00);
    st7735s_sendData(yStart);
    st7735s_sendData(0x00);
    st7735s_sendData(yEnd);

    st7735s_sendCommand(0x2C);
}3.4 全屏颜色填充从这里开始初始化的工作完成了，下面有一些简单的图形绘制示例展示，展示一些绘制方面的基本原理。简单实现全屏颜色填充的逻辑比较简单，先发送需要填充颜色的区域后再循环向区域写入需要的颜色数据即可。前面配置时提到了，我在这里将颜色深度配置为了 16bit 因此我们需要向其中传入一个 16bit 的颜色数据；这就需要一个通过 SPI 总线发送 16bit 数据的函数：void st7735s_sendu16Data(uint16_t data) {
    // 将 16bit 数据拆分为两个 8bit 数据用于发送
    uint8_t dataH = (uint8_t)(data &gt;&gt; 8);
    uint8_t dataL = (uint8_t)(data &amp; 0xFF);


    ST7735S_NSS_L;  // 拉低片选 - 开始通讯
    ST7735S_DC_H;   // 拉高 DC 代表的是数据
    // 先发送高字节，再发送低字节
    HAL_SPI_Transmit(&amp;hspi1, &amp;dataH, 1, 0xFFFF);
    HAL_SPI_Transmit(&amp;hspi1, &amp;dataL, 1, 0xFFFF);
    ST7735S_NSS_H;  // 拉高片选 - 结束通讯
}然后根据上面描述的流程，先设置范围，再循环填充即可：// 我选用了一个淡绿色作为测试 -&gt; 0x8426

void st7735s_RefreshAll(uint16_t rgb) {
    st7735s_write_address(0, 127, 0, 159);      // 设置显示范围
    for(uint16_t j = 0; j &lt; 160; j++) {         // 两个 for 遍历这个范围发送颜色数据
        for(uint16_t i = 0; i &lt; 128; i++) {
            st7735s_sendu16Data(rgb);
        }
    }
}[显示效果]3.5 区域颜色填充区域填充的原理与全屏一样，只不过是设置计算好的范围后再填充。void st7735s_drawBlock(uint8_t x, uint8_t y, uint8_t len, uint16_t color) {
    st7735s_write_address(x, x + len - 1, y, y + len - 1);
    for (int i = 0; i &lt; len; i++) {
        for (int j = 0; j &lt; len; j++) {
            st7735s_sendu16Data(color);
        }
    }
}[显示效果]3.7 画点画点函数非常简单，简单视作绘制一个边长为 1 像素的正方形即可。void st7735s_drawPoint(uint8_t x, uint8_t y, uint16_t color) {
    st7735s_write_address(x, x, y, y );
    st7735s_sendu16Data(color);
}3.8 显示图片待编写3.9 LVGL 显示测试待编写END 参考与引用[参考地址]CSDN - ST7735S应用笔记ST7735SDataSheet.PDF本文部分内容由 Ai 辅助生成</description>
</item>
<item rdf:about="https://blog.atoery.cn/index.php/2025/04/21/98.html">
<title>[嵌入式] SPI 总线协议与实现</title>
<link>https://blog.atoery.cn/index.php/2025/04/21/98.html</link>
<dc:date>2025-04-21T22:05:00+08:00</dc:date>
<description>1.0 SPI 简介[软件模拟 SPI 库]SPI (Serial Peripheral Interface - 串行外设接口) 是 Motorola 开发的一种由四根通信线构成的一主多从通用数据总线。[SPI 总线具有如下特性]同步&全双工的通讯模式支持总线挂载多设备2.0 SPI 物理层SPI 总线由四根线构成：SCK(Serial Clock)：时钟，用于提供时钟时序，数据的收发在时钟信号的上升沿或下降沿实现MOSI(Master Output Slave Input)：主机输出从机输入MISO(Master Input Slave Output)：主机输入从机输出SS(Slave Select) ：片选值得注意的是，这里的名称并非标准，对于不同引脚名称的描述可能不同。[一主多从]SPI 通讯仅支持一主机多从机的通讯方式，不支持多主机多从机的通讯方式。[SPI 片选]主机的数据输出连接到从机的数据输入从机的数据输出连接到主机的数据输入从机的片选控制引脚连接到主机负责控制片选的引脚上[通讯引脚 IO 配置]SPI 的输出引脚均配置为推挽输出；输入引脚配置为浮空或者上拉输入。3.0 SPI 协议层3.1 数据传输原理SPI 的主机与从机中，均包含一个串行移位寄存器，该位移寄存器每来一个时钟信号移位寄存器会向左移动一位。主机和从机中的移位寄存器的时钟由主机提供，同时该时钟也通过 SCK 引脚进行输出。而主机移位寄存器移出的数据通过 MOSI 引脚传输至从机，而从机移位寄存器移出的数据通过 MISO 引脚传输至主机。通过如上过程，仅需要主机时钟驱动即可实现数据在两个通信单位之间传输。因此，SPI 通讯的核心思想是通过时钟驱动的位移寄存器将数据在主机和从机之间进行置换。3.2 SS 片选引脚SPI 通过单独的片选引脚选择不同的从机设备，需要与谁通信就将对应 SS 引脚拉低电平状态即可。同一时间仅能选择一个从机，如果多从机被同时选择可能会产生数据冲突问题。3.3 SPI 时序基本单元3.3.1 起始与终止信号起始信号：SS(片选) 从高电平切换到低电平结束信号：SS(片选) 从低电平切换到高电平因此在数据传输的过程总，通讯单元之间的 SS 引脚需要始终保持低电平。3.3.2 数据位的传输SPI 通讯为了兼容不同芯片等之间的通讯，其对于时钟驱动的数据读写方式并未做规定，因此针对不同的模式 SPI 有 4 中不同的工作模式。3.3.3 交换一个字节 (模式 0)CPOL = 0：空闲状态时，SCK 为低电平CPHA = 0：SCK 第一个边沿移入数据，第二个边沿移出数据值得注意的是在这种模式下，在 SCK 的第一个时钟边沿到来前通讯双方就要移除数据，也就是说在 SS 产生下降沿的时候就要将数据移出。在实际使用过程中，模式 0 最为常用，后续基于代码实现中以模式 0 实现。3.3.4 交换一个字节 (模式 1)CPOL = 0：空闲状态时，SCK 为低电平CPHA = 1：SCK 第一个边沿移出数据，第二个边沿移入数据3.3.5 交换一个字节 (模式 2&3)模式 2 和模式 3 仅是相对于模式 0 和模式 1 将其 SCK 信号取反，其它并无差别。4.0 SPI 代码实现4.1 起始信号与结束信号SPI 的实现方式对比 I2C 简单粗暴，所谓开始&结束信号只需要拉低&高 SS 片选引脚即可：void spiStartSignal(void) {
    spIO_SS(0);
}

void spiStopSignal(void) {
    spIO_SS(1);
}值得注意的是，如果是以模式 0 和模式 2 工作的话，在起始信号发出后 MOSI 就要马上将需要发送的数据搬到 MOSI 引脚上；因此这里调用 MOSI 的函数中是没有用于控制通讯速率的延迟的：void spIO_SS(uint8_t bitValue) {    // SPI SS 片选信号
    if(bitValue != 0) {
        global_spiConfigure.IO_SS_H();
    }else{
        global_spiConfigure.IO_SS_L();
    }
}4.2 交换一个字节数据根据上述对 SPI 的描述，实现交换字节的逻辑也比较简单：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;
}此处根据模式 0 来看，由于前边 SS 引脚已经发送了一个起始信号，因此这里 MOSI 需要马上发送第一位数据；byte &amp; (0x80 &gt;&gt; i) 这里通过掩码的方式逐次取出指定的数据位用于发送。驱动 SCK 来到第一个边沿从 MISO 引脚读取一个字节数据并存储到缓存 (recByte) 中驱动 SCK 来到第二个边沿开始发送数据，一个循环结束。[代码优化]针对上述代码，如果我们根据 SPI 硬件层面实现原理来看可以优化代码如下：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;
}这其中与之前不同的是每次发送的都是 byte 的最高位，与 SPI 通讯主机中的位移寄存器工作原理相同，数据位被发送出去后，将 byte 整体左移一位，低位空出来的位置正好可以用于存储从机传来的数据。因此在数据读取边沿将 byte 或等于 0x01 这样就可以将接收到的数据存储在 byte 的最低为，就像位移寄存器一样。这样编程可以节省一个缓存变量，节约空间。此外，不同的模式仅有奇数边沿读取还是偶数边沿读，以及时钟的正反相的区别，因此针对模式 0 的发送函数，对代码语句的简单排序即可得到其它模式下的发送函数。// 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;
}END 参考与声明[参考]STM32入门教程-2023版 细致讲解 中文字幕</description>
</item>
<item rdf:about="https://blog.atoery.cn/index.php/2025/04/19/100.html">
<title>[嵌入式] STM32 在 FreeRTOS 下实现 us 级别延时</title>
<link>https://blog.atoery.cn/index.php/2025/04/19/100.html</link>
<dc:date>2025-04-19T18:57:48+08:00</dc:date>
<description>1.0 前言与概述最近在编写软件实现的 I2C 和 SPI 协议库，需要稳定的 1us 延时用作心跳函数。在 FreeRTOS 下编写 us 延时逻辑不太一样，故在文章中作记录。与裸机实现 us 延时方法相近，使用滴答定时器通过比较数值即可实现延时。在 RTOS 环境下实现思路如下：计算指定延时时长所需要的计数器的值关闭系统任务调度 - 防止延时器件被任务调度打断影响计数精度获取当前重装载值获取开始延时寄存器里面的计数值不断获取当前计数器的值如果当前计数器值大于设定的计数器的值，退出延时释放系统任务调度2.0 实现代码void delay_us(uint32_t us) {
  volatile uint32_t tcnt = 0;
  uint32_t reload = SysTick-&gt;LOAD;
  uint32_t ticks = us * (SystemCoreClock / 1000000);

  vTaskSuspendAll();  // 阻止 OS 调度
  volatile uint32_t told = SysTick-&gt;VAL;
  while(1) {
    volatile uint32_t tnow = SysTick-&gt;VAL;
    if(tnow != told) {
      if(tnow&lt;told) {
        tcnt += told - tnow;
      }else {
        tcnt += reload - tnow + told;
      }
      told = tnow;
      if(tcnt &gt;= ticks) {
        break;
      }
    }
  }
  xTaskResumeAll(); // 恢复 OS 调度
}3.0 实现过程分析3.1 初始化部分uint32_t tcnt = 0;：用于累计已经经过的时间uint32_t reload = SysTick-&gt;LOAD;：获取 SysTick 的重装载值，这是定时器的最大值uint32_t ticks = us * (SystemCoreClock / 1000000);：计算需要延时的 SysTick 的值3.2 暂停任务调度通过 FreeRTOS 提供的 vTaskSupendAll(); 函数，暂停操作系统的任务调度，防止在延时期间发生任务切换，影响延时精度。3.3 获取初始计数器值uint32_t told = SysTick-&gt;VAL;：获取 SysTick 定时器当前计数值3.4 主循环主循环中会一直循环，直到时间累计到目标值 ticks3.4.1 获取当前计数器值uint32_t tnow = SysTick-&gt;VAL;：获取当前 SysTick 计数器的值3.4.2 判断数值是否发生变化if(tnow != told)：如果 tnow 和 told 的值不等，则 SysTick 的值发生了变化，更新累计时间 tcnt3.4.3 计算时间差if(tnow &lt; told)：如果 tnow 小于 told，则说明计数器并未溢出，累加差值 tcnt = told - tnow；否则说明计数器发生了溢出，reload - tnow 得到 tnow 到 0 的剩余时间，再减去 told 计算出 reload 到 told 的时间。3.4.4 更新旧计数值told = tnow 更新 told 的值，便于下次比较3.4.5 判断是否达到目标延时如果累计时间 tcnt 大于或等于 ticks 则达到了目标延时时长，退出循环。3.4.6 恢复任务调度FreeRTOS 提供的 xTaskResumeAll(); 函数可以恢复操作系统的任务调度4.0 上述代码存在的问题上述的代码中，可以轻松的实现 us 级别的延时功能，但是存在一些问题；程序中为了保证延时的精度，在延时函数的核心功能代码中关闭了系统的任务调度。这会对其它线程的任务运行产生干扰。因为在系统的任务调度被暂停后，所有的任务切换都会被禁止，相当于回到了裸机状态，直到调度器被恢复。这种行为可能会对其它线程中要求实时性的任务产生负面影响，尤其是多任务系统。4.1 另一种实现 us 延时的方法通过 STM32 的硬件外设 TIM 定时器可以实现无需关闭任务调度实现 us 延时。4.2 实现代码void tim_delay_us (uint32_t us) {
  uint32_t start = __HAL_TIM_GET_COUNTER(&amp;htim2);  // 获取当前计数值
  uint32_t elapsed = 0;

  while (elapsed &lt; us) {
    uint32_t now = __HAL_TIM_GET_COUNTER(&amp;htim2);  // 获取当前计数值
    if (now &gt;= start) {
      elapsed = now - start;  // 正常情况
    } else {
      elapsed = 0xFFFFFFFF - start + now;  // 处理溢出
    }
  }
}通过硬件定时器的实现可在不占用调度器的情况下实现同样的延时效果。END 参考与声明[参考文章]STM32在FreeRTOS下的us延时本文部分内容和代码由 Ai 辅助生成</description>
</item>
<item rdf:about="https://blog.atoery.cn/index.php/2025/04/16/92.html">
<title>[FreeRTOS] 堆和栈的基本概念&amp;amp;在FreeRTOS中的使用</title>
<link>https://blog.atoery.cn/index.php/2025/04/16/92.html</link>
<dc:date>2025-04-16T21:41:29+08:00</dc:date>
<description>1.0 堆1.1 基本概念堆的本质是在 RAM 中的一段连续的内存区域，开发者可以主动申请一些内存块，其中与栈不同的是，堆的声明周期是完全由程序员进行控制的。1.2 堆的管理机制动态性碎片化风险手动管理1.3 例子在 C/C++ 中，定义一个变量时，其存储位置取决于其定义位置上下文：如果在函数内定义，则其存储在栈中。若在全局或静态作用域定义，则存储在静态存储区。真正的堆内存必须通过动态分配函数进行显示申请。char heap_buf[1024];    // 若在函数内进行申请，则存储在栈中，函数结束后自动释放-char* heap_buf = (char*)malloc(1024);   // 显式申请堆内存
2.0 栈2.1 基本概念栈是一种先进后出的数据结构，其基本操作通常包括：压栈和弹栈。2.2 栈的特点后进先出 (LIFO)高效性有限容量 (固定大小栈)自动管理：在嵌入式系统中，函数调用时使用的栈由编译器和处理器自动管理，无需程序员手动干预。3.0 RTOS 如何使用栈END 参考 & 声明[参考]FreeRTOS入门与工程实践 --由浅入深带你学习FreeRTOS（FreeRTOS教程 基于STM32，以实际项目为导向） -&gt; BV1Jw411i7Fz本文部分内容由 Ai 辅助生成</description>
</item>
<item rdf:about="https://blog.atoery.cn/index.php/2025/04/16/91.html">
<title>[ARM 架构] 浅入 - 简单汇编指令</title>
<link>https://blog.atoery.cn/index.php/2025/04/16/91.html</link>
<dc:date>2025-04-16T20:32:00+08:00</dc:date>
<description>ARM 架构是一种精简指令集计算机，其一些基本的汇编指令如下：读内存 LOADLDR R0, [R1, #4 ;    读地址 “R1 + 4&quot;，将从 R1 读取的 4 个字节存入 R0写内存 STORESTR R0, [R1, #4] ;    将 R0 的数据 4 个字节写入地址 R1加减 ADD&amp;SUBADD R0, R1, R2 ;    R0 = R1 + R2
ADD R0, R1, #1 ;    R0 = R1 + 1
SUB R0, R1, R2 ;    R0 = R1 - R2
SUB R0, R1, #1 ;    R0 = R1 - 1比较 CMPCMP R0, R1 ;    比较 R0 与 R1 的值，将结果保存到 PSR[程序状态寄存去] 中跳转 B&amp;BLB main ;    直接跳转
BL main ;    先把返回地址保存到 LR 寄存器后再跳转未来我深入学习 ARM 架构或者汇编等后，这部分可能会继续更新~</description>
</item>
</rdf:RDF>