1
0
0

鸿蒙造物者之路 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 文档。

文中所有项目均应在自己拥有合法权限的设备与网络环境中进行。

支持与分享

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

评论

欢迎来到我的博客!