鸿蒙造物者之路 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 很"死板"?
导演:对,死板换性能。 这一点,主角马上就要刻骨铭心地体会到了。
========================================
未完待续。。。


