鸿蒙造物者之路 12 · 造物者(大结局)
续写章节|沿用「造物者」系列的武侠讲解体,面向电子信息 / 嵌入式方向的新手
=================前情提要===============
十一章走完:从分不清 OpenHarmony 和 HarmonyOS,到能写驱动、能写界面、能把两者用 NAPI 打通、能签名上架。
老陈把 Hi3861、SHT30、继电器、舵机一股脑推到我面前:
“把你最开始跟我说的那个想法,做出来。”
那是我第一次见他时,他问我的问题——三个设备、三个厂家、三种协议、三个 App。
一个系统,让它们说同一种语言。
========================================
这一章你会得到什么
🎯 学完这一章,你应该能:
- ☐ 完成一个完整的软硬结合项目:从传感器到界面,全链路走通
- ☐ 学会先做能做出来的部分(红外那个伏笔的教训)
- ☐ 建立属于你自己的三条规矩
- ☐ 拿到一份能放进简历的东西
**🧰 需要准备:**前面十一章的全部积累,以及一两周的业余时间。
**⏱ 预计用时:**一两周
📊 难度:★★★★☆ 综合,但每一步你都学过了
一、最后一次开工
那是个九月的晚上,实训楼三楼。还是那间实验室,还是那台风扇嗡嗡响的老台式机,只是桌上多了很多东西。
老陈摊开一张纸:“说说你的设计。”
我在白板上画:
┌──────────────────┐
│ 手机 App │
│ (ArkTS 界面) │
└────────┬─────────┘
│ 分布式软总线 + NAPI
┌────────▼─────────┐
│ Hi3861 主控 │
│ (OpenHarmony) │
└────────┬─────────┘
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ SHT30 │ │ 继电器 │ │ 舵机 │
│ 温湿度 │ │ 控台灯 │ │ 控窗帘 │
│ I2C │ │ GPIO │ │ PWM │
└──────────┘ └──────────┘ └──────────┘
│ │
└──────────► 还差一个:红外 ────────────┘
(控空调)
"红外呢?"老陈问。
"红外发射管接 GPIO,用 PWM 载波。"我说,“我已经把宿舍空调的遥控码抓下来了。”
老陈点点头,没说话。
“怎么了?”
"没什么。"他笑了笑,“就是想起你第一次来,连 OpenHarmony 和 HarmonyOS 都分不清。”
二、一层一层做上去
2.1 硬件层:接线
"先把线接对。"老陈说,“给你十分钟。”
| 外设 | 接口 | Hi3861 引脚 | 说明 |
|---|---|---|---|
| SHT30 | I2C | GPIO0(SCL) / GPIO1(SDA) | 需 4.7k 上拉 |
| 继电器 | GPIO | GPIO9 | 高电平吸合 |
| 舵机 | PWM | GPIO2 | 50Hz,1.5ms 中位 |
| 红外发射 | PWM | GPIO7 | 38kHz 载波 |
| 状态灯 | GPIO | GPIO8 | 系统心跳 |
"注意两点:"老陈说,“第一,继电器和舵机别共用电源,舵机启动电流大会让主控复位;第二,继电器控制的是 220V,接线端子必须绝缘处理,别拿命开玩笑。”
他把一个绝缘端子盒推给我。
2.2 驱动层:把外设都跑起来
“第一件事,还是点灯——只不过这次点的是继电器。”
/* device_ctrl.h —— 设备能力抽象 */
#ifndef DEVICE_CTRL_H
#define DEVICE_CTRL_H
int DevCtrlInit(void); /* 初始化所有外设 */
int RelaySet(int on); /* 继电器开/关 */
int ServoSetAngle(int angle); /* 舵机角度 0~180 */
int IrSend(uint32_t code); /* 发红外码 */
int SensorRead(float *temp, float *humi); /* 读温湿度 */
#endif
/* device_ctrl.c */
#include <stdio.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "iot_gpio.h"
#include "iot_pwm.h"
#include "iot_i2c.h"
#include "device_ctrl.h"
#define RELAY_GPIO 9
#define SERVO_PWM 2
#define IR_PWM 7
#define HEART_GPIO 8
#define I2C_BUS 0
#define SHT30_ADDR 0x44
/* ---------- 初始化 ---------- */
int DevCtrlInit(void)
{
IoTGpioInit(RELAY_GPIO);
IoTGpioSetDir(RELAY_GPIO, IOT_GPIO_DIR_OUT);
IoTGpioSetOutputVal(RELAY_GPIO, IOT_GPIO_VALUE0); /* 默认断开 */
IoTGpioInit(HEART_GPIO);
IoTGpioSetDir(HEART_GPIO, IOT_GPIO_DIR_OUT);
IoTPwmInit(SERVO_PWM);
IoTPwmInit(IR_PWM);
IoTI2cInit(I2C_BUS, 400000);
printf("[DEV] init done\r\n");
return 0;
}
/* ---------- 继电器 ---------- */
int RelaySet(int on)
{
return IoTGpioSetOutputVal(RELAY_GPIO, on ? IOT_GPIO_VALUE1 : IOT_GPIO_VALUE0);
}
/* ---------- 舵机:0.5ms~2.5ms 对应 0~180 度 ---------- */
int ServoSetAngle(int angle)
{
if (angle < 0) angle = 0;
if (angle > 180) angle = 180;
/* 占空比 = (0.5 + angle/180 * 2.0) / 20.0 * 100 */
int duty = (int)((0.5f + angle / 180.0f * 2.0f) / 20.0f * 100.0f);
return IoTPwmStart(SERVO_PWM, duty, 50); /* 50Hz */
}
/* ---------- 红外:38kHz 载波 + NEC 编码 ---------- */
static void IrSendBit(int bit)
{
if (bit) {
IoTPwmStart(IR_PWM, 33, 38000); /* 发载波 */
osDelay(1); /* 560us 左右 */
IoTPwmStop(IR_PWM);
osDelay(2); /* 1690us 左右 */
} else {
IoTPwmStart(IR_PWM, 33, 38000);
osDelay(1);
IoTPwmStop(IR_PWM);
osDelay(1); /* 560us 左右 */
}
}
int IrSend(uint32_t code)
{
/* 简化版:省略引导码和校验,实际要按 NEC 协议发 32 位 */
for (int i = 31; i >= 0; i--) {
IrSendBit((code >> i) & 0x01);
}
return 0;
}
/* ---------- 温湿度 ---------- */
int SensorRead(float *temp, float *humi)
{
uint8_t cmd[2] = { 0x2C, 0x06 };
uint8_t raw[6] = { 0 };
if (IoTI2cWrite(I2C_BUS, (SHT30_ADDR << 1) | 0x00, cmd, 2) != IOT_SUCCESS) {
return -1;
}
osDelay(2); /* 等转换 */
if (IoTI2cRead(I2C_BUS, (SHT30_ADDR << 1) | 0x01, raw, 6) != IOT_SUCCESS) {
return -2;
}
uint16_t tRaw = (raw[0] << 8) | raw[1];
uint16_t hRaw = (raw[3] << 8) | raw[4];
*temp = -45.0f + 175.0f * tRaw / 65535.0f;
*humi = 100.0f * hRaw / 65535.0f;
return 0;
}
"注意 IrSendBit 里的 osDelay 单位是 10ms 的 tick。"老陈瞟了一眼,“你这红外码的延时不对,38kHz 载波要的是微秒级精度,用 osDelay 调不出来。”
“那怎么办?”
"两个办法:一是用硬件 PWM + 精确的循环延时(忙等),二是用定时器。"他指了指屏幕,“先把红外放一边,把主线跑通。”
——后来我才明白,他是在教我一件更重要的事:先做能做出来的部分。
2.3 原生层:NAPI 桥接
// sensor_napi.cpp —— 把 C 能力暴露给 ArkTS
#include "napi/native_api.h"
#include "device_ctrl.h"
// 读传感器(异步 Promise)
struct SensorCtx {
napi_async_work work = nullptr;
napi_deferred deferred = nullptr;
float temp = 0, humi = 0;
int err = 0;
};
static void ExecReadSensor(napi_env env, void *data)
{
SensorCtx *c = static_cast<SensorCtx *>(data);
c->err = SensorRead(&c->temp, &c->humi);
}
static void DoneReadSensor(napi_env env, napi_status st, void *data)
{
SensorCtx *c = static_cast<SensorCtx *>(data);
napi_value obj = nullptr;
napi_create_object(env, &obj);
napi_value t = nullptr, h = nullptr;
napi_create_double(env, c->temp, &t);
napi_create_double(env, c->humi, &h);
napi_set_named_property(env, obj, "temp", t);
napi_set_named_property(env, obj, "humi", h);
napi_resolve_deferred(env, c->deferred, obj);
napi_delete_async_work(env, c->work);
delete c;
}
static napi_value JsReadSensor(napi_env env, napi_callback_info info)
{
auto *c = new SensorCtx();
napi_value promise = nullptr;
napi_create_promise(env, &c->deferred, &promise);
napi_value name = nullptr;
napi_create_string_utf8(env, "readSensor", NAPI_AUTO_LENGTH, &name);
napi_create_async_work(env, nullptr, name,
ExecReadSensor, DoneReadSensor, c, &c->work);
napi_queue_async_work(env, c->work);
return promise;
}
// 控制灯(同步,很快)
static napi_value JsSetLight(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value args[1] = { nullptr };
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int32_t on = 0;
napi_get_value_int32(env, args[0], &on);
RelaySet(on);
napi_value result = nullptr;
napi_get_boolean(env, true, &result);
return result;
}
// 控制窗帘(同步)
static napi_value JsSetCurtain(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value args[1] = { nullptr };
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int32_t angle = 0;
napi_get_value_int32(env, args[0], &angle);
ServoSetAngle(angle);
napi_value result = nullptr;
napi_get_boolean(env, true, &result);
return result;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
napi_property_descriptor desc[] = {
{ "readSensor", nullptr, JsReadSensor, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "setLight", nullptr, JsSetLight, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "setCurtain", nullptr, JsSetCurtain, nullptr, nullptr, nullptr, napi_default, nullptr },
};
napi_define_properties(env, exports, sizeof(desc)/sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
static napi_module g_mod = {
.nm_version = 1, .nm_flags = 0, .nm_filename = nullptr,
.nm_register_func = Init, .nm_modname = "entry", .nm_priv = nullptr,
.reserved = { nullptr },
};
extern "C" __attribute__((constructor)) void Reg(void) { napi_module_register(&g_mod); }
2.4 应用层:ArkTS 界面
// pages/Dashboard.ets
import native from 'libentry.so';
import hilog from '@ohos.hilog';
interface SensorData { temp: number; humi: number; }
@Observed
class RoomState {
lightOn: boolean = false;
curtainAngle: number = 90;
temp: number = 0;
humi: number = 0;
updatedAt: string = '--';
}
@Entry
@Component
struct Dashboard {
@State room: RoomState = new RoomState();
@State loading: boolean = false;
@State log: string[] = [];
private addLog(msg: string): void {
this.log = [`${new Date().toLocaleTimeString()} ${msg}`, ...this.log.slice(0, 4)];
}
async refresh(): Promise<void> {
this.loading = true;
try {
let d: SensorData = await native.readSensor();
this.room.temp = d.temp;
this.room.humi = d.humi;
this.room.updatedAt = new Date().toLocaleTimeString();
this.addLog(`读取成功 ${d.temp.toFixed(1)}°C`);
} catch (e) {
this.addLog('读取失败');
hilog.error(0x0000, 'DASH', 'read failed: %{public}s', JSON.stringify(e));
} finally {
this.loading = false;
}
}
toggleLight(): void {
this.room.lightOn = !this.room.lightOn;
native.setLight(this.room.lightOn ? 1 : 0);
this.addLog(`台灯 ${this.room.lightOn ? '开' : '关'}`);
}
setCurtain(angle: number): void {
this.room.curtainAngle = angle;
native.setCurtain(angle);
this.addLog(`窗帘 ${angle}°`);
}
aboutToAppear(): void { this.refresh(); }
build() {
Column({ space: 16 }) {
// —— 标题 ——
Row() {
Text('宿舍中控').fontSize(22).fontWeight(FontWeight.Bold)
Blank()
if (this.loading) { LoadingProgress().width(24).height(24) }
}.width('100%')
// —— 环境卡片 ——
Column({ space: 4 }) {
Text(`${this.room.temp.toFixed(1)}°C`)
.fontSize(52).fontWeight(FontWeight.Bold)
.fontColor(this.room.temp > 28 ? '#E74C3C' : '#0F9D58')
Text(`湿度 ${this.room.humi.toFixed(0)}% · ${this.room.updatedAt}`)
.fontSize(13).fontColor('#888888')
}
.width('100%').padding(20)
.backgroundColor('#FFFFFF').borderRadius(16)
.shadow({ radius: 10, color: '#15000000', offsetY: 2 })
// —— 控制区 ——
Row({ space: 12 }) {
// 台灯
Column({ space: 8 }) {
Image(this.room.lightOn ? $r('app.media.bulb_on') : $r('app.media.bulb_off'))
.width(36).height(36)
Text('台灯').fontSize(13)
}
.layoutWeight(1).padding(16)
.backgroundColor(this.room.lightOn ? '#FFF4D6' : '#F5F5F5')
.borderRadius(12)
.onClick(() => this.toggleLight())
// 窗帘
Column({ space: 8 }) {
Text('窗帘').fontSize(13)
Slider({ value: this.room.curtainAngle, min: 0, max: 180, step: 10 })
.width(100).height(30)
.onChange((v: number) => this.setCurtain(Math.round(v)))
}
.layoutWeight(1).padding(16)
.backgroundColor('#F5F5F5').borderRadius(12)
// 刷新
Column({ space: 8 }) {
Text('↻').fontSize(24)
Text('刷新').fontSize(13)
}
.layoutWeight(1).padding(16)
.backgroundColor('#F5F5F5').borderRadius(12)
.onClick(() => this.refresh())
}.width('100%')
// —— 日志 ——
Column({ space: 4 }) {
ForEach(this.log, (item: string) => {
Text(item).fontSize(11).fontColor('#999999')
}, (item: string) => item)
}.width('100%').alignItems(HorizontalAlign.Start)
Blank()
Text('数据来自本机的 SHT30 传感器')
.fontSize(11).fontColor('#CCCCCC')
}
.width('100%').height('100%').padding(20).backgroundColor('#F7F7F7')
}
}
2.5 翻车:三个真实的坑
“跑一下。”
我点运行。第一次,界面白屏,日志里刷出一串错误。
[SENSOR] read failed
[DASH] read failed: {"code":-1}
"查。"老陈说。
坑一:I2C 地址
我用万用表量了 SDA 和 SCL,都是 3.3V——没上拉。
“第 05 章讲过什么?”
"……I2C 必须上拉。"我脸红了,找了两个 4.7k 电阻焊上。
再跑,读到数据了。但温度显示 -45.0°C。
坑二:转换公式
"-45 是公式里的最小值,说明 tRaw = 0。"老陈说,“你读到的 6 个字节全是 0。”
“上拉都接了啊……”
"检查时序。"他指着代码,“你发完测量命令,延时了多久?”
“osDelay(2)……20 毫秒?”
"SHT30 高精度模式最多要 15ms,20ms 应该够。"他顿了顿,“除非——你的 osDelay 单位不是 10ms。”
我查了配置:LOSCFG_BASE_CORE_TICK_PER_SECOND。
配置的是 1000,也就是一个 tick = 1 毫秒。
所以 osDelay(2) 只延时了 2 毫秒,传感器根本没转换完。
改成 osDelay(20),温度出来了:26.5°C。
"这就是我说的’别死记参数,要理解单位’。"老陈说。
坑三:舵机抖动
窗帘控制接上之后,舵机一直"嗡嗡"抖。
“PWM 频率对吗?”
“50Hz……”
“IoTPwmStart 的第二个参数是占空比,第三个是频率。你传反了没有?”
我检查——没有。那问题在哪?
老陈拿过示波器(是的,实验室居然有),探头夹在 PWM 引脚上。
“看波形。频率是 50Hz 没错,但是——占空比在跳。”
“为什么?”
“因为你在 Slider 的 onChange 里调用 setCurtain,每动一格就调一次 IoTPwmStart。连续调用会让 PWM 重新初始化,中间有断裂。”
“那怎么改?”
“加个防抖——只在停止滑动后才真正设置角度。”
// 防抖:停止 300ms 后才真正下发
private curtainTimer: number = -1;
onCurtainChange(v: number): void {
this.room.curtainAngle = Math.round(v); // UI 立刻反馈
clearTimeout(this.curtainTimer);
this.curtainTimer = setTimeout(() => {
native.setCurtain(this.room.curtainAngle); // 稳定后才下发
}, 300);
}
舵机安静了。
2.6 加点东西:让它"活"起来
"功能通了,但还不够。"老陈说,“你回想第 08 章——什么叫’能用’的应用?”
我想了想:“会记住东西、会上网、会在后台干活……”
“所以呢?”
我加了三样:
① 记住用户的习惯(Preferences)
import preferences from '@ohos.data.preferences';
// 保存上一次的状态
async saveState(): Promise<void> {
let store = await preferences.getPreferences(getContext(this), 'room_state');
await store.put('lightOn', this.room.lightOn);
await store.put('curtainAngle', this.room.curtainAngle);
await store.flush();
}
async restoreState(): Promise<void> {
let store = await preferences.getPreferences(getContext(this), 'room_state');
this.room.lightOn = await store.get('lightOn', false) as boolean;
this.room.curtainAngle = await store.get('curtainAngle', 90) as number;
}
② 自动化规则
// 温度太高,自动开窗帘通风
async checkAutomation(): Promise<void> {
if (this.room.temp > 28 && this.room.curtainAngle < 90) {
this.setCurtain(180);
this.addLog('温度过高,自动开窗帘');
}
if (this.room.humi > 75) {
this.addLog('湿度过高,建议开除湿');
}
}
③ 桌面卡片(FormExtensionAbility)
"让用户不用打开 App 就能看到温度。"老陈提醒。
onUpdateForm(formId: string): void {
let data = {
'temp': this.room.temp.toFixed(1),
'humi': this.room.humi.toFixed(0)
};
formProvider.updateForm(formId, formBindingData.createFormBindingData(data));
}
2.7 演示那天
一周后,学院的创新展示。
我把板子放在展台上,手机连着同一个 Wi-Fi,桌上的继电器"咔哒"一声,台灯亮了。滑块拖动,舵机转了 90 度,拉着一块小布帘打开了。
手机屏幕上,温度数字从 26.5 跳到 26.6。
有老师问:“这是你自己做的?”
“是。”
“从驱动到 App 都是?”
我点头。
他"哦"了一声,多看了两眼。
老陈站在人群外面,靠着门框,没说话。
2.8 红外那件事
演示完的第三天,我把红外调通了。
起因是老陈临走前问了我一句:“你那个红外,还打算做吗?”
我说 osDelay 精度不够,做不了。
他看了我一眼:“你确定是精度不够,还是你只会用 osDelay?”
那天晚上我翻了芯片手册,找到了硬件定时器。然后又翻了 NEC 协议的资料:
NEC 红外协议(空调遥控常用)
引导码:9ms 载波 + 4.5ms 空闲
数据: 32 位 = 地址(8) + 地址反码(8) + 命令(8) + 命令反码(8)
"0" 的表示:560us 载波 + 560us 空闲 (总 1.12ms)
"1" 的表示:560us 载波 + 1690us 空闲 (总 2.25ms)
载波频率:38kHz(占空比约 1/3)
问题就在 560us 和 1690us——这两个数字,用 10ms 甚至 1ms 精度的延时都做不出来。
我用了三个办法:
第一,用硬件定时器替代 osDelay。 配置一个 1MHz 的定时器(1us 一个计数),靠读计数寄存器做精确延时。
第二,抓码用逻辑分析仪。 我把一个红外接收头接到分析仪上,拿原装遥控器按了一下,波形清清楚楚地显示在屏幕上——引导码多长、每个 bit 是多少微秒、地址码是多少,一目了然。
第三,载波用 PWM 硬件输出,开关用 GPIO 控制。 38kHz 的载波让硬件自己振荡,我只负责在正确的时间点"开"和"关"。
/* 红外发送(最终版,示意) */
#define IR_CARRIER_HZ 38000
#define IR_GPIO 7
/* 微秒级忙等(用硬件定时器计数) */
static void IrDelayUs(uint32_t us)
{
uint32_t start = GetTimerCount(); /* 1MHz 计数 */
while ((GetTimerCount() - start) < us) {
/* 忙等 —— 这种短延时只能用忙等 */
}
}
/* 发一个 NEC bit */
static void IrSendBit(int bit)
{
IoTPwmStart(IR_GPIO, 33, IR_CARRIER_HZ); /* 开载波 */
IrDelayUs(560);
IoTPwmStop(IR_GPIO);
IrDelayUs(bit ? 1690 : 560); /* 0 和 1 的区别在这 */
}
/* 发一帧 NEC 码 */
int IrSendNec(uint8_t addr, uint8_t cmd)
{
/* ① 引导码 */
IoTPwmStart(IR_GPIO, 33, IR_CARRIER_HZ);
IrDelayUs(9000);
IoTPwmStop(IR_GPIO);
IrDelayUs(4500);
/* ② 32 位数据,低位在前 */
uint32_t frame = ((uint32_t)addr << 24) | ((uint32_t)(~addr & 0xFF) << 16)
| ((uint32_t)cmd << 8) | (uint32_t)(~cmd & 0xFF);
for (int i = 0; i < 32; i++) {
IrSendBit((frame >> i) & 0x01);
}
/* ③ 结束位 */
IoTPwmStart(IR_GPIO, 33, IR_CARRIER_HZ);
IrDelayUs(560);
IoTPwmStop(IR_GPIO);
return 0;
}
那天晚上十一点半,我对着宿舍那台空调按下"开",然后按下了我板子上的按钮。
“滴”——空调响了。
我一个人在实验室里,对着一台空调笑了大概一分钟。
后来我把这段写进了项目的 README,标题是:
“关于 osDelay 精度不够这件事——不是工具不行,是我只知道一种工具。”
三、师傅走了
展示结束后的第二天,我去实验室,门开着,桌上放着一个牛皮纸信封。
老陈不在。
信封上写着我的名字。里面是一张纸,还有一块东西——那支他一直在用的记号笔。
纸上是一段字,字迹很硬:
阿粽:
我走了。去了南方一家做车机的公司,那边缺人做鸿蒙南向。
临走前有几句话想跟你说。
第一,别迷信任何一个系统。
我见过太多人,把某个平台当成一辈子吃饭的本事。平台会起会落,但你解决问题的能力不会落。 你会看数据手册、会抓波形、会从现象反推原因——这些走到哪儿都值钱。第二,不要只会一头。
我以前只做南向,觉得写界面是"没技术含量"。后来才发现,一个东西做不出来和做出来没人用,是两个失败。 你比那时候的我幸运,一开始就走的是两头。第三,守住一条线。
你会的技术,能做好事,也能做坏事。改东西这件事,越熟练越危险。只在自己有权限的东西上练手,不碰别人的系统,不绕过别人的付费。 这条线守不住,技术越高越麻烦。最后——
你问我为什么教你。其实没什么特别的原因。我年轻的时候,也遇见过一个愿意在实验室里陪我到深夜的人。 他也没要过什么回报。
你要是以后也碰见一个愣头愣脑、分不清 OpenHarmony 和 HarmonyOS 的年轻人——
记得给他一支笔。
老陈
我捏着那张纸,在实验室坐了很久。
窗外天黑了,风扇还在嗡嗡响。
那块 Hi3861 上的蓝灯,还在一闪一闪。
四、造物者手册
老陈在信封里还夹了几页纸,是他手写的。我整理了一下,抄在这里。
4.1 全景图
┌────────────────────────────────────────────────────┐
│ 应用层 │
│ 传统应用(HAP) │ 元服务(免安装) │ 服务卡片 │
├────────────────────────────────────────────────────┤
│ 框架层 │
│ Ability框架 │ ArkUI(声明式) │ 方舟运行时 │
├────────────────────────────────────────────────────┤
│ 系统服务层 │
│ 分布式软总线 │ 分布式数据 │ 分布式任务调度 │
│ 图形 │ 多媒体 │ 安全 │ AI │ 电源 │ 位置 │ 通知 │
├────────────────────────────────────────────────────┤
│ 内核层 │
│ LiteOS-M │ LiteOS-A │ Linux │
│ HDF 驱动框架(一次开发,多内核部署) │
├────────────────────────────────────────────────────┤
│ 硬件:MCU(Cortex-M/RISC-V) → 应用处理器(Cortex-A)│
└────────────────────────────────────────────────────┘
南向(C/C++,靠近硬件) ←── NAPI ──→ 北向(ArkTS,靠近用户)
4.2 十二章速查表
| 章 | 主题 | 一句话记住 |
|---|---|---|
| 01 | 鸿蒙初开 | OpenHarmony 是根,HarmonyOS 是果,NEXT 断了安卓的奶 |
| 02 | 三种身法 | KAL 让上层看不见内核;HDF 让驱动看不见内核 |
| 03 | 铸剑 | hb set → hb build → 烧 bin → 串口看日志;第一次成功就拍快照 |
| 04 | 点灯 | 轻量用 IoTGpio,标准走 HDF;驱动三段 Bind/Init/Release |
| 05 | 五脏六腑 | I2C 要上拉先扫地址;SPI 试 Mode0/3;UART 是调试命脉 |
| 06 | 北向初见 | 你只描述结果,框架负责过程;@State 变了 UI 自己动 |
| 07 | 舞台 | Ability 是容器,页面在里面跳;aboutToAppear 一次,onPageShow 每次 |
| 08 | 内外兼修 | 离线优先:先落本地再同步;能推送就别后台 |
| 09 | 分身术 | 发现→认证→组网→传输;让一堆设备像一个设备 |
| 10 | 南北打通 | 两个世界一座桥;主线程才能碰 ArkTS |
| 11 | 出师 | HAP 能装、HAR 编译期、HSP 运行时;签名四件套 |
| 12 | 造物者 | 见下面三条 |
4.3 三条铁律
老陈在最后写了三行,字写得特别重:
┌──────────────────────────────────────────────┐
│ │
│ 一、能跑起来的东西,大于完美的设计 │
│ │
│ 先做出来,再做好。做不出来的方案, │
│ 再漂亮也是零。 │
│ │
│ 二、先证明硬件是好的,再怀疑软件 │
│ │
│ 90% 的"代码 bug"其实是接线、供电、 │
│ 虚焊、忘了上拉。万用表比调试器管用。 │
│ │
│ 三、只在自己有权限的东西上练手 │
│ │
│ 这条线守不住,技术越高越危险。 │
│ │
└──────────────────────────────────────────────┘
五、我的三条规矩
老陈走后,我在实验室的白板上,写下了自己的三条。
第一条:任何一个新东西,先让它动起来,再问为什么。
不要一上来就啃架构图。先点一盏灯,先跑一个 Hello World。 手指记住的东西,脑子才记得住。
第二条:遇到 bug,先怀疑最简单的那部分。
我为了一个不亮的灯查过三小时代码,最后是 LED 焊反了。从那以后我养成了习惯:硬件先排查,软件后排查;接线先排查,逻辑后排查。
第三条:每次做成一件事,就把它写下来。
写下来,它才真正是你的。我写这本笔记的时候,才发现好多东西我以为懂了,其实一写就卡住。写,是最狠的检验。
六、那些年
后来我又做了一些东西。
大二那年,我给实验室做了一套设备管理系统——用 OpenHarmony 的轻量系统做了几个采集节点,把全楼的温湿度和用电数据采集上去,然后用 ArkTS 写了个管理端。
后来这个系统真用上了,后勤的老师还来问过我能不能加个报警功能。
我做过一份复盘,数据是这样的:
| 项目 | 数量 |
|---|---|
| 采集节点 | 6 个(分布在三层楼) |
| 单次采集周期 | 5 分钟 |
| 日均数据点 | 约 1,700 条 |
| 累计运行 | 14 个月 |
| 掉线次数 | 3 次(一次断电、一次 WiFi 改密码、一次节点进水) |
| 丢数据条数 | 0 |
最后一行是我最得意的。
不是因为技术多牛,而是因为我把第 08 章那句话听进去了:先落本地,再同步。
每个节点采集完立刻写本地存储,标记未同步;有网就传,传完改标记。中间断过三次网,最长一次断了两天,数据一条没丢。
那时候我才真正明白老陈说的:
“能跑起来的东西,大于完美的设计。”
这个系统一点都不漂亮——界面是我用 ArkUI 拼的,很朴素;后端是个最普通的 HTTP 接口;没有做任何性能优化。
但它跑了十四个月,没丢过一条数据。
这就够了。
大三,我投了几份实习简历。有两家回了我,面试的时候都问了同一个问题:
“你简历上写’从驱动到 App 独立完成过鸿蒙项目’,能具体说说吗?”
我就从那盏灯开始讲。
从一个只会点 51 流水灯的学生,到一个能独立做软硬结合产品的人——中间隔了十二章。
但真要说起来,也就是一年多。
七、门外的人
今年九月,实训楼三楼。
我已经是大三了,还在那间实验室——现在它有了新名字,叫"鸿蒙联合实验室",桌上的 51 开发板挪到了柜子里,取而代之的是一排开发板。
那天下午我在焊一块转接板,烙铁刚沾上焊盘,身后有人说话。
“学长,你这块板子,要是想让它连上手机,你会怎么做?”
我回头。
门口站着一个男孩,手里拎着个帆布包,脸上是很熟悉的、那种又兴奋又迷茫的表情。
我愣了一下。
然后我笑了。
"你先坐。"我指了指旁边的椅子,从桌上拿起那支旧记号笔,转身面向白板。
“在回答你之前,我先问你个事儿——”
我在白板正中间画了一条竖线,分成两半。
“你说的’鸿蒙’,是哪一个鸿蒙?”
本章自检
过一遍下面的问题。答不上来的,回去翻对应小节——那些就是你这个章的欠账。
- ☐ 你能独立从硬件接线做到界面显示吗?中间每一层都清楚吗?
- ☐ 老陈留下的三条铁律是什么?
- ☐ 你自己的三条规矩是什么?
- ☐ 这个项目里,你踩的最深的坑是什么?怎么解决的?
- ☐ 做完之后:下一步你想学什么?
**➡️ 下一站:**没有下一章了。去做出你自己的东西。
=================剧情片段=================
导演:(揉眼睛)灯灭了吧?
助理:灭了。
导演:收工。
助理:就这么结束了?
导演:结束了。 从第一章"分不清三个名字",到这里"他成了问别人这句话的人"——闭环了。
助理:师傅那句"记得给他一支笔",有点戳人。
导演:这是这门手艺真正的传承方式。 不是书,不是视频,是有人愿意在你最懵的时候,陪你到深夜。
助理:那最后这个门外的学弟……
导演:不用写他的故事了。 他会自己走一遍。
助理:从点灯开始?
导演:从点灯开始。
========================================
全 剧 终
本书第 01–02 章的知识体系基于 OpenHarmony / HarmonyOS 公开资料整理;第 03–12 章为面向新手的教学性续写。
技术细节以官方文档为准,动手时请务必查阅你所用 SDK 版本的头文件与 API 文档。文中所有项目均应在自己拥有合法权限的设备与网络环境中进行。


