0
0
0

鸿蒙造物者之路 05 · 五脏六腑

文章摘要
|

续写章节|沿用「造物者」系列的武侠讲解体,面向电子信息 / 嵌入式方向的新手

=================前情提要===============

灯亮了。

老陈甩给我一颗 SHT30 温湿度传感器,说下一章讲总线——I2C、SPI、UART、PWM、ADC。

他说这是嵌入式的"五脏六腑"。我当时不懂为什么用这个词。后来我明白了:一盏灯只有开和关,而总线,是让一个设备真正能"感知世界"和"表达自己"的东西。

========================================


这一章你会得到什么

🎯 学完这一章,你应该能:

  • ☐ 用 I2C 读出 SHT30 的温湿度(本书第一个真正「有用」的成果)
  • ☐ 分清 UART / I2C / SPI 各自的适用场景和经典大坑
  • ☐ 会用 PWM 控制舵机、用 ADC 读模拟量
  • ☐ 懂中断的规矩(什么能做什么不能做)
  • ☐ 会用互斥锁保护共享的外设总线

**🧰 需要准备:**第 04 章环境 + SHT30 模块(买模块别买裸片)+ 4.7kΩ 电阻两个 + 万用表。

**⏱ 预计用时:**约 6 小时

📊 难度:★★★★☆ 外设多,但每个都有正反馈


一、为什么需要总线

"一个 GPIO 能表达多少信息?"老陈开场就问。

“一个 bit。”

“那我要读一个温度值,比如 26.5 度,需要多少根线?”

我想了想:“26.5 如果用 0.1 度分辨率,就是 265,需要 9 个 bit……九根线?”

"九根线。"老陈笑了,“那我要接三个传感器呢?二十七根?芯片引脚不要钱吗?PCB 不要钱吗?”

“所以……”

"所以有了总线。"老陈在白板上画了两根线。

“总线的核心思想,是用’协议’换’引脚’。 用两根线、加一套约定好的时序规则,就能传无数数据。代价是——你要懂时序。”

1.1 五种总线,各管一摊

总线 线数 速率 典型用途 一句话特点
UART 2(TX/RX)+ GND 常见 9600~921600 bps 调试日志、GPS、蓝牙模组、AT 指令 最简单,最古老,调试命脉
I2C 2(SDA/SCL) 100k / 400k / 3.4M 传感器、EEPROM、触摸屏、RTC 省线,但慢
SPI 4(SCLK/MOSI/MISO/CS) 可达几十 MHz 屏幕、Flash、SD 卡、高速 ADC 最快,全双工
PWM 1 — 舵机、调光、电机调速、蜂鸣器 不是通信,是"调比例"
ADC 1 — 光敏、电池电压、电位器、模拟量 把"连续"变成"数字"

“再加上一个 I2S,专门传音频的,做语音项目才用得上,先混个眼熟。”

“学这么多,从哪个开始?”

"从 UART 开始——不是因为它最重要,而是因为它最好调。"老陈说,“它是你调试任何东西的命根子。”

二、UART:最老也最离不开

2.1 它简单到只有三根线

设备 A                     设备 B
  TX  ───────────────────►  RX
  RX  ◄───────────────────  TX
 GND  ────────────────────  GND

"TX 接 RX,RX 接 TX,GND 共地。"老陈说,“接反了是最经典的低级错误,我见过研究生都接反过。”

2.2 异步:没有时钟线怎么同步?

“UART 叫异步串口,它没有时钟线。那接收方怎么知道一位数据从哪开始到哪结束?”

“……靠约定?”

"靠约定,而且靠起始位。"老陈画了个时序:

空闲:高电平 ──────┐
                   │
         ┌───┐ ┌──┴┐   ┌───┬───┬───┬───┬───┬───┬───┬───┬─────┐
         │起始│ │ D0│   │D1 │D2 │D3 │D4 │D5 │D6 │D7 │校验│  停止 │
         │ 0  │ │   │   │   │   │   │   │   │   │   │ 位 │   1   │
         └───┘ └────┘   └───┴───┴───┴───┴───┴───┴───┴───┴─────┘
              ↑
          下降沿 = 起始位 = "我要开始发了"

“**双方事先约定好波特率(每秒传多少位),然后靠起始位的下降沿对齐。**发送方以约定速率逐位发,接收方以同样的速率逐位采样。”

“波特率不对会怎样?”

"乱码。"老陈说,“这是串口问题里排名第一的原因。记住几个常用值:”

波特率 用途
9600 老设备、GPS 模块默认
115200 最常用,OpenHarmony 日志默认
460800 / 921600 高速日志、烧录

“格式怎么约定?”

“8N1——8 个数据位、无校验(None)、1 个停止位。这是默认约定,九成场合都是它。”

常见配置写法:115200, 8N1
              ↑波特率  ↑8数据位 N无校验 1停止位

2.3 在鸿蒙里用 UART

“轻量系统上的接口:”

函数 作用
IoTUartInit(port, &attr) 初始化串口,配置波特率等
IoTUartWrite(port, buf, len) 发送
IoTUartRead(port, buf, len) 接收(注意超时)
IoTUartSetFlowCtrl(port, mode) 硬件流控
/* 初始化 UART1,115200 8N1 */
IotUartAttribute attr = {
    .baudRate   = 115200,
    .dataBits   = UART_ATTR_DATABIT_8,
    .stopBits   = UART_ATTR_STOPBIT_1,
    .parity     = UART_ATTR_PARITY_NONE,
    .rxBlock    = UART_ATTR_RX_BLOCK,      /* 阻塞读 */
    .txBlock    = UART_ATTR_TX_BLOCK,
};
IoTUartInit(HI_UART_IDX_1, &attr);

/* 发数据 */
const char *msg = "hello\r\n";
IoTUartWrite(HI_UART_IDX_1, (unsigned char *)msg, strlen(msg));

/* 收数据(阻塞,最多读 100 字节) */
unsigned char buf[100] = {0};
uint32_t len = 100;
IoTUartRead(HI_UART_IDX_1, buf, &len);

“注意 IoTUartRead 的 len 参数既是输入也是输出——你传进去想要读多少,它返回实际读了多少。这是个容易搞混的设计。”

2.4 串口的真正价值

"UART 最重要的用途,其实不是通信——"老陈说。

“是调试?”

"是调试。"他强调,“在没有屏幕、没有键盘的嵌入式设备上,串口日志是你唯一的眼睛。”

他给我列了三条铁律:

1. 做任何项目,第一件事是把串口调通
2. 日志要打在关键路径上:初始化后、收到数据后、出错时
3. 日志里带上"标签"和"值",别只打一句 ok

“什么叫带上标签和值?”

/* ❌ 不好的日志 */
printf("ok\r\n");
printf("error\r\n");

/* ✅ 好的日志 */
printf("[SHT30] read: temp=%.1f humi=%.1f\r\n", temp, humi);
printf("[SHT30] ERR: i2c write failed, ret=%d, retry=%d\r\n", ret, retry);

“三个月后你回头看代码,‘ok’ 什么都告诉不了你。”

三、I2C:两根线撑起半个传感器世界

"现在讲最重要的那个。"老陈把 SHT30 插到面包板上。

3.1 物理层:为什么必须上拉

        VCC
         │
    ┌────┴────┐
   4.7k     4.7k          ← 上拉电阻,必须有
    │         │
    ├─────────┼───── SDA(数据)
    │         │
    └────┬────┴───── SCL(时钟)
         │
    ┌────┴────┐
  设备1      设备2        ← 所有设备并联,靠地址区分

"I2C 是开漏输出,器件只能把线拉低,不能拉高。"老陈说,“所以必须靠外部上拉电阻把线’拉’回高电平。”

“忘了上拉会怎样?”

"线永远在低电平,总线瘫痪,怎么读都是 0。"老陈说,“这是 I2C 最经典的硬件坑。”

“阻值多大?”

"一般 4.7kΩ。"老陈说,“总线设备多、速率高的时候可以小一点(2.2k),设备少可以大一点(10k)。但你新手就用 4.7k,不会错。”

3.2 协议层:地址 + 寄存器

"每个 I2C 设备有一个 7 位地址。"老陈说,“主机想跟谁说话,就先发谁的地址。”

一次完整的"读寄存器"操作:

主机                                   从机(地址 0x44)
  │
  ├─ START ─────────────────────────►
  ├─ 地址 0x44 + 写位(0) ────────────►   ← "我要写你"
  ├─ ACK ◄─────────────────────────────   ← 从机应答
  ├─ 寄存器地址 0x2C ────────────────►   ← "我要读这个寄存器"
  ├─ ACK ◄─────────────────────────────
  ├─ RESTART ────────────────────────►   ← 重新起始(不释放总线)
  ├─ 地址 0x44 + 读位(1) ────────────►   ← "我要读你"
  ├─ ACK ◄─────────────────────────────
  ├─ 数据字节 1 ◄──────────────────────
  ├─ 主机发 ACK ─────────────────────►   ← "继续发"
  ├─ 数据字节 2 ◄──────────────────────
  ├─ 主机发 NACK ────────────────────►   ← "够了"
  └─ STOP ───────────────────────────►

“记住这个流程,它是 I2C 的骨架。 所有 I2C 读操作基本都是这个套路:先写寄存器地址,再重启读数据。”

“为什么中间要 Restart 不直接读?”

"因为 I2C 没有’方向反转’的概念。"老陈说,“一次传输要么一直写要么一直读,要换方向只能重新起始。”

3.3 地址怎么算

"设备手册上写的地址,经常让人困惑。"老陈举了个例子:

SHT30 数据手册写着:I2C address 0x44

实际发送的字节:
    7位地址 = 100 0100 (0x44)
    读操作:(0x44 << 1) | 1 = 0x89
    写操作:(0x44 << 1) | 0 = 0x88

"手册给的是 7 位地址,实际发送时要左移一位、最低位放读写标志。"老陈说,“但很多代码库(包括鸿蒙的)会让你直接填 7 位地址,它内部帮你移位。所以两边别重复移位——这是新手最容易犯的错。”

“怎么判断我该填哪个?”

"看函数文档,或者——直接用逻辑分析仪抓一下波形。"老陈说,“后面我会跟你讲,逻辑分析仪是嵌入式第二好的投资。”

“第一好的是什么?”

"一块好的万用表。"老陈毫不犹豫。

3.4 代码:读 SHT30

“现在写代码,读温湿度。”

/* sht30.c —— 读 SHT30 温湿度 */
#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "iot_i2c.h"
#include "iot_gpio.h"
#include "iot_errno.h"

#define I2C_BUS        0          /* I2C 总线号 */
#define SHT30_ADDR     0x44       /* 7 位地址 */
#define CMD_MEAS_HIGH  0x2C       /* 高精度测量命令 */
#define CMD_MEAS_PARAM 0x06

/* ---------- 写命令 ---------- */
static int Sht30SendCmd(uint8_t cmdHigh, uint8_t cmdLow)
{
    uint8_t buf[2] = { cmdHigh, cmdLow };

    /* IoTI2cWrite(bus, 器件7位地址, 数据, 长度) */
    int ret = IoTI2cWrite(I2C_BUS, (SHT30_ADDR << 1) | 0x00, buf, sizeof(buf));
    if (ret != IOT_SUCCESS) {
        printf("[SHT30] ERR write cmd failed, ret=%d\r\n", ret);
        return -1;
    }
    return 0;
}

/* ---------- 读数据 ---------- */
static int Sht30ReadData(uint8_t *buf, uint32_t len)
{
    /* IoTI2cRead(bus, 器件7位地址|读位, 缓冲区, 长度) */
    int ret = IoTI2cRead(I2C_BUS, (SHT30_ADDR << 1) | 0x01, buf, len);
    if (ret != IOT_SUCCESS) {
        printf("[SHT30] ERR read data failed, ret=%d\r\n", ret);
        return -1;
    }
    return 0;
}

/* ---------- 解析:SHT30 返回的 6 字节 ---------- */
static int Sht30Parse(uint8_t *raw, float *temp, float *humi)
{
    /* 数据格式:T高 T低 T校验 H高 H低 H校验 */
    uint16_t tRaw = (raw[0] << 8) | raw[1];
    uint16_t hRaw = (raw[3] << 8) | raw[4];

    /* 转换公式(来自数据手册) */
    *temp = -45.0f + 175.0f * (float)tRaw / 65535.0f;
    *humi = 100.0f * (float)hRaw / 65535.0f;
    return 0;
}

/* ---------- 任务 ---------- */
static void Sht30Task(void *arg)
{
    (void)arg;
    uint8_t raw[6] = {0};
    float temp = 0, humi = 0;

    /* I2C 初始化:波特率 400k */
    IoTI2cInit(I2C_BUS, 400000);
    printf("[SHT30] i2c init, 400kHz\r\n");

    while (1) {
        /* 1. 发测量命令 */
        if (Sht30SendCmd(CMD_MEAS_HIGH, CMD_MEAS_PARAM) != 0) {
            osDelay(100);
            continue;
        }

        /* 2. 等测量完成(高精度模式最多 15ms) */
        osDelay(2);    /* 20ms */

        /* 3. 读 6 字节 */
        if (Sht30ReadData(raw, sizeof(raw)) == 0) {
            Sht30Parse(raw, &temp, &humi);
            printf("[SHT30] temp=%.1f C, humi=%.1f %%\r\n", temp, humi);
        }

        osDelay(200);   /* 2 秒读一次 */
    }
}

static void Sht30Demo(void)
{
    osThreadAttr_t attr = {0};
    attr.name = "Sht30Task";
    attr.stack_size = 4096;
    attr.priority = osPriorityNormal;

    if (osThreadNew((osThreadFunc_t)Sht30Task, NULL, &attr) == NULL) {
        printf("[SHT30] create task FAILED\r\n");
    }
}
APP_FEATURE_INIT(Sht30Demo);

"几个要注意的地方。"老陈指着屏幕:

第一,地址移位要看接口。“上面我写的 (SHT30_ADDR << 1) | 0x00,是因为这个接口要求你自己拼读写位。有些版本/平台的接口会自动移位,你填 0x44 就行。以你手上的 SDK 头文件为准。”

第二,I2C 初始化别忘。“IoTI2cInit 不调用,后面全是失败。”

第三,测量要等。“传感器转换数据需要时间,发完命令立刻读,读到的是上一次的或者垃圾值。”

3.5 I2C 排错清单

现象 原因
全部读到 0x00 没接上拉电阻 / SDA 被拉死
全部读到 0xFF 线路断开 / 器件没上电 / 地址错
写成功读失败 时序不对 / 没等转换完成
偶尔失败 速率太高(降到 100k 试)/ 线太长干扰
找不到设备 地址错(7位 vs 8位)/ 模块坏了

"最后一招:写个扫描程序,把 0x01~0x7F 全部试一遍,看哪个地址有回应。"老陈说,“这是 I2C 调试的万能钥匙。”

/* I2C 地址扫描:找出总线上所有设备 */
void I2cScan(uint8_t bus)
{
    printf("[SCAN] scanning i2c bus %d:\r\n", bus);
    for (uint8_t addr = 0x01; addr < 0x80; addr++) {
        uint8_t dummy = 0;
        if (IoTI2cWrite(bus, (addr << 1) | 0x00, &dummy, 0) == IOT_SUCCESS) {
            printf("[SCAN]   found device at 0x%02X\r\n", addr);
        }
    }
    printf("[SCAN] done\r\n");
}

四、SPI:快,但要数线

“如果说 I2C 是省线省到极致,SPI 就是用线换速度。”

4.1 四根线

主机                          从机
 SCLK  ─────────────────────►  SCLK      时钟
 MOSI  ─────────────────────►  MOSI      主机发,从机收
 MISO  ◄───────────────────── MISO       从机发,主机收
 CS    ─────────────────────►  CS        片选(每个从机一根)

“CS(片选)是关键。“老陈说,”**SPI 没有地址概念,主机靠拉低某个从机的 CS 来选中它。**所以挂 N 个从机,就要 N 根 CS 线。”

“所以挂十个设备要十根 CS?”

"所以 SPI 不适合挂很多设备。"老陈说,“它的主场是点对点高速传输——屏幕、Flash、SD 卡。”

4.2 四种模式:CPOL 和 CPHA

“SPI 有个烦人的地方:它有四种模式。”

模式 CPOL(空闲时钟电平) CPHA(采样边沿) 说明
Mode 0 0(空闲低) 0(第一个边沿采样) 最常用
Mode 1 0 1(第二个边沿采样)
Mode 2 1(空闲高) 0
Mode 3 1 1 也常用

“模式不对会怎样?”

"数据错位——你读到的字节每一位都偏了一格。"老陈说,“现象很典型:明明该读到 0x5A,读出来是 0xB4 或者 0x2D(左右移了一位)。”

“怎么知道设备用哪种?”

"查数据手册。"老陈说,“或者——Mode 0 和 Mode 3 挨个试,两种能覆盖 90% 的器件。”

4.3 全双工的含义

“SPI 是全双工的——发送的同时也在接收。”

/* SPI 传输:发的同时也在收 */
uint8_t txBuf[4] = {0x9F, 0x00, 0x00, 0x00};   /* 读 Flash 的 JEDEC ID 命令 */
uint8_t rxBuf[4] = {0};

IoTSPiTransfer(spiBus, txBuf, rxBuf, 4);

/* 注意:rxBuf[0] 是发送 0x9F 那会儿收到的(无意义)
         rxBuf[1..3] 才是真正的返回数据 */

“新手常犯的错:以为第一个收到的字节就是答案。“老陈说,”**SPI 没有’先发命令再读结果’这回事,你发第一个字节的时候,从机还没来得及回应。**所以要多发几个空字节(dummy byte)把数据’顶’出来。”

五、PWM 和 ADC:模拟量的两条路

5.1 PWM:用数字信号模拟"中间值"

“PWM 最简单,也最实用。”

一个周期内,高电平占的比例叫"占空比"

占空比 0%     ▁▁▁▁▁▁▁▁▁▁▁▁   全灭
占空比 25%    █▁▁▁█▁▁▁█▁▁▁   微亮
占空比 50%    ██▁▁██▁▁██▁▁   半亮
占空比 75%    ███▁███▁███▁   较亮
占空比 100%   ████████████   全亮

"灯其实一直在快速开关,只是人眼看不见(视觉暂留)。"老陈说,“你调的不是电压,是’亮的时间占比’。”

两个关键参数:

参数 含义 影响
频率 一秒多少个周期 太低会闪烁(灯)/ 有噪音(电机);舵机常用 50Hz
占空比 高电平占的比例 LED 调光、电机转速

"舵机是 PWM 最经典的应用。"老陈拿起一个小舵机,“50Hz,一个周期 20ms,靠高电平的时长决定角度:”

舵机控制(50Hz,周期 20ms)
  0.5ms 高电平  →  0°
  1.0ms 高电平  →  45°
  1.5ms 高电平  →  90°   ← 中位
  2.0ms 高电平  →  135°
  2.5ms 高电平  →  180°
/* 轻量系统 PWM 接口 */
IoTPwmInit(PWM_PORT);              /* 初始化 */
IoTPwmStart(PWM_PORT, duty, freq);  /* duty=占空比(0-100), freq=频率 */
IoTPwmStop(PWM_PORT);

5.2 ADC:把连续变成离散

“ADC 是反过来的——把连续变化的电压,变成离散的数字。”

三个关键参数:

参数 含义 常见值
分辨率 用多少位表示一个值 8/10/12/16 bit
参考电压 满量程对应多少伏 常见 3.3V / 1.8V
采样率 每秒采多少次 看需求
12 位 ADC,参考电压 3.3V:
    量化台阶 = 3.3V / (2^12 - 1) = 3.3 / 4095 ≈ 0.806 mV

    读到 0     →  0.000 V
    读到 2048  →  1.651 V
    读到 4095  →  3.300 V

"最常见的应用:电池电量检测。"老陈说,“但要注意——”

“注意什么?”

"分压。"老陈画了个图,“电池电压可能高于 ADC 的量程,直接接上去会烧片。所以要分压:”

电池 4.2V ──┬── R1 (100k) ──┬──► ADC 引脚
            │               │
            └───────────────┘
                            │
                         R2 (100k)
                            │
                           GND

ADC 读到 = 4.2V × R2/(R1+R2) = 4.2 × 0.5 = 2.1V  ← 安全
软件里再乘回去:真实电压 = ADC值 × 2

"还有一点——"老陈补充,“ADC 读到的值经常抖动。 要做软件滤波,最简单的就是多次采样取平均:”

/* 多次采样取平均,抗抖动 */
uint16_t AdcReadAvg(int channel, int times)
{
    uint32_t sum = 0;
    for (int i = 0; i < times; i++) {
        uint16_t val = 0;
        IoTAdcRead(channel, &val, IOT_ADC_CUR_BAIS_DEFAULT);
        sum += val;
        osDelay(1);      /* 采样间隔 */
    }
    return (uint16_t)(sum / times);
}

六、中断与并发:多任务下的坑

"最后一块,也是最容易被忽视的。"老陈的语气严肃了。

“现在你的程序里有多个线程——一个读传感器、一个闪灯、一个处理网络。它们可能同时要访问同一个资源。”

6.1 中断里不能干什么

"中断服务程序(ISR)是整个系统里优先级最高的东西。"老陈说,“所以它有铁律:”

中断里 ❌ 绝对不能做的:
  · 调用可能阻塞的函数(osDelay、等待信号量、malloc)
  · 做耗时操作(打印大量日志、复杂计算)
  · 调用非可重入函数

中断里 ✅ 应该做的:
  · 记个标记 / 置个事件
  · 往队列里丢个消息
  · 尽快退出,把活交给线程去做

“这叫’上半部/下半部’——上半部(中断)只做最紧急的事,下半部(线程)做剩下的活。”

6.2 LiteOS-M 的同步工具

工具 用途 典型场景
信号量 Semaphore 计数 / 通知 中断通知线程"数据来了"
互斥锁 Mutex 独占访问 保护共享变量、保护外设总线
事件 Event 多条件等待 等"按键按下 且 网络就绪"
消息队列 Queue 传数据 传感器数据传给处理线程
/* 互斥锁保护共享的 I2C 总线 */
static OsalMutex g_i2cLock;

void SafeI2cRead(uint8_t addr, uint8_t *buf, uint32_t len)
{
    OsalMutexLock(&g_i2cLock);      /* 上锁 */
    IoTI2cRead(I2C_BUS, addr, buf, len);
    OsalMutexUnlock(&g_i2cLock);    /* 解锁 */
}

“为什么要锁 I2C 总线?”

"因为 I2C 是共享的。"老陈说,“两个线程同时发 I2C 命令,波形会绞在一起,两边都读到垃圾。加锁保证同一时刻只有一个线程在用总线。”

6.3 一个真实的坑

"给你讲个我踩过的坑。"老陈难得讲了故事。

“早些年做一款工业采集器,采集线程每 100ms 读一次传感器,网络线程每 5 秒发一次数据。上线之后,偶尔会读到离谱的温度——比如 200 度。”

“查了两天,最后发现:网络线程发数据的时候,会去读一下状态寄存器确认传感器在线——它没加锁,跟采集线程撞上了。”

“两个线程同时在 I2C 总线上发数据,波形叠加,读出来的字节是两帧数据混在一起的结果。”

“怎么解决的?”

"加互斥锁,把所有 I2C 访问包起来。"老陈说,“从那以后我养成了一个习惯——任何一个被多个线程访问的硬件资源,从第一天起就加保护。”

=================小剧场模式=================

我:老师,这么多总线,我要是全记不住怎么办?

老陈:不用全记住。 你只需要记住三件事。

我:哪三件?

老陈:第一,知道什么时候该用哪个。 传感器用 I2C,屏幕/存储用 SPI,调试用 UART,调光调速用 PWM,测电压用 ADC。

我:第二?

老陈:第二,知道出问题先查什么。 I2C 查上拉和地址,SPI 查模式和片选,UART 查波特率和接线。

我:第三?

老陈:第三,会看数据手册。 任何一颗新芯片,你要的信息都在手册里——地址、时序、寄存器、转换公式。

我:手册好厚……

老陈:不用全读。 你会越来越知道该翻哪几页。新手读全文,老手只查表。

我:对了,那个"逻辑分析仪"是什么?

老陈:能抓波形的小设备。 I2C、SPI 的时序,肉眼看不见,但逻辑分析仪能把它画出来——哪一根线在什么时候变高变低,一目了然。

我:贵吗?

老陈:几十块钱的就能用。 买一个,你会回来谢我的。

========================================

七、本章速查

总线 关键参数 最常见的坑 一句话口诀
UART 波特率、8N1 TX/RX 接反、波特率不对 TX 接 RX,115200 是默认
I2C 7位地址、上拉电阻 没上拉 / 地址 7位8位混 要上拉,先扫地址
SPI CPOL/CPHA 模式、CS 模式不对数据错位 Mode 0/3 挨个试
PWM 频率、占空比 频率太低会闪 舵机 50Hz,1.5ms 中位
ADC 分辨率、参考电压 超量程烧片、读数抖 超量程要分压,读数要滤波
并发 互斥锁、信号量 中断里阻塞、共享资源没保护 中断快进快出,共享必加锁

"这一章结束,"老陈合上本子,“南向的地基就打完了。”

“打完了?我总觉得还差点什么。”

“差什么?”

我想了想:“我现在能让板子读数、能闪灯、能打日志。但是……它有屏幕吗?有界面吗?用户怎么用它?”

老陈眼睛亮了一下。

"问得好。"他说,“这正是南向的天花板——它没有’脸’。”

“那怎么办?”

老陈把那块 RK3568 推过来,屏幕亮着,上面跑着一个界面:一个圆形的温度显示,下面一行小字「客厅 · 26.5°C」。

"从下一章开始,我们往北走。"他说。

“学什么?”

"ArkTS 和 ArkUI。"老陈说,“你会给你的板子,做一张脸。”


本章自检

过一遍下面的问题。答不上来的,回去翻对应小节——那些就是你这个章的欠账。

  • ☐ I2C 为什么必须接上拉电阻?忘了会是什么现象?
  • ☐ 手册给的 7 位地址,实际发送时要怎么处理?
  • ☐ 读一个 I2C 寄存器的完整流程是什么?(起始→地址→寄存器→重启→读→停止)
  • ☐ SPI 的 Mode 0/3 是什么?模式不对会怎样?
  • ☐ 舵机 50Hz 时,1.5ms 脉宽对应多少度?
  • ☐ 中断里绝对不能做哪三件事?
  • ☐ 两个线程同时访问 I2C 总线会怎样?怎么防?

**➡️ 下一站:**第 06 章 · 北向初见 —— 换个脑子,去给你的板子做一张「脸」。


=================剧情片段=================

导演:卡。南向五章讲完了。

助理:GPIO、UART、I2C、SPI、PWM、ADC、中断、并发……够扎实了。

导演:这一章是分水岭。 讲完它,主角才算真的能"做一个东西",而不只是"点一盏灯"。

助理:下一章开始北向了,画风会变吗?

导演:会。 南向是 C 语言、寄存器、时序、慢工出细活;北向是 ArkTS、声明式 UI、组件、状态管理——快,而且所见即所得。

助理:主角会不会不适应?

导演:会。 从"我控制一切"到"我不控制渲染,我只描述结果"——这是思维方式的转变。

助理:所以下一章叫——

导演:《北向初见》。 ArkTS 语言、ArkUI 声明式范式、状态管理、渲染控制。

助理:听说 ArkTS 很"死板"?

导演:对,死板换性能。 这一点,主角马上就要刻骨铭心地体会到了。

========================================

未完待续。。。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论

欢迎来到我的博客!