0
0
0

鸿蒙造物者之路 04 · 点灯

文章摘要
|

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

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

环境搭完了,源码编通了,固件烧进去了,串口日志里刷出了那行 Hello, OpenHarmony!。

老陈说今晚点灯。

我以为十分钟能搞定。结果这一晚,我第一次意识到——点灯这件事,居然有两种完全不同的境界。

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


这一章你会得到什么

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

  • ☐ 说出配置一个 GPIO 的完整五步,知道哪一步最容易忘
  • ☐ 写出轻量系统的点灯程序(线程 + 延时 + 启动宏)
  • ☐ 理解 HDF 驱动的三段人生(Bind / Init / Release)
  • ☐ 看懂 Dispatch 是怎么把上层的调用送进驱动的
  • ☐ 会写 BUILD.gn,知道忘了加会怎样

**🧰 需要准备:**第 03 章的环境 + Hi3861 + 面包板 + LED + 杜邦线 + 限流电阻。

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

📊 难度:★★★☆☆ 开始有意思了


一、先把我原来的点灯方式摆出来

"你把你在 51 上点灯的代码写一遍。"老陈把白板让给我。

我拿起笔,三行就写完了:

/* 51 单片机点灯 —— 我大一写的 */
#include <reg52.h>

sbit LED = P1^0;          // LED 接在 P1.0

void main() {
    while (1) {
        LED = 0;          // 低电平点亮(共阳接法)
    }
}

"STM32 呢?"老陈问。

我又写:

/* STM32 点灯 —— HAL 库写法 */
int main(void) {
    HAL_Init();
    __HAL_RCC_GPIOA_CLK_ENABLE();           // 开时钟

    GPIO_InitTypeDef gpio;
    gpio.Pin   = GPIO_PIN_5;
    gpio.Mode  = GPIO_MODE_OUTPUT_PP;       // 推挽输出
    gpio.Pull  = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &gpio);

    while (1) {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
        HAL_Delay(500);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
        HAL_Delay(500);
    }
}

老陈点点头:“**很好,这是标准的裸机思路。**你把它的步骤拆开给我看。”

我在旁边列:

1. 开时钟 / 开电源域
2. 配置引脚复用(这个脚要当 GPIO 用,不要当串口用)
3. 配置方向(输入还是输出)
4. 配置上下拉 / 驱动能力
5. 写寄存器让它变高变低

"记住这五步。"老陈说,“等会儿你会发现,在系统里点灯,这五步一步都没少,只是换了个人替你干。”

二、GPIO 到底是个什么东西

"在写代码之前,先把 GPIO 本身搞清楚。"老陈把那块 Hi3861 翻过来,指着芯片引脚。

2.1 一个引脚,多种身份

"GPIO 的全称是 General Purpose Input/Output,通用输入输出。"老陈说,“关键词是’通用’——同一个物理引脚,可以被配置成不同的功能。”

他画了个图:

                    芯片内部
    ┌────────────────────────────────────┐
    │                                    │
    │   ┌──────┐ ┌──────┐ ┌──────┐      │
    │   │ GPIO │ │ UART │ │ I2C  │ ...  │  ← 多种功能单元
    │   └───┬──┘ └───┬──┘ └───┬──┘      │
    │       │        │        │          │
    │   ┌───┴────────┴────────┴───┐     │
    │   │      复用选择器 MUX       │     │  ← 决定这个脚归谁
    │   └──────────┬───────────────┘     │
    └──────────────┼─────────────────────┘
                   │
                   ▼
              物理引脚 PAD

"这个’复用选择器’叫 MUX(多路复用器),配置它的寄存器叫 IOMUX / PINMUX。"老陈说,“新手最常犯的一个错就是:代码写了、方向配了、值也写了,灯就是不亮——因为你忘了配复用,这个脚还在当串口用。”

"我踩过。"我老实说。

“人人都踩过。”

2.2 输出之后还有什么

“引脚配成输出之后,还有几个参数:”

参数 含义 常见取值
方向 Direction 输入 / 输出 GPIO_DIR_IN / GPIO_DIR_OUT
输出模式 推挽 / 开漏 Push-Pull / Open-Drain
上下拉 Pull 内部电阻接到 VCC 还是 GND 上拉 / 下拉 / 浮空
驱动能力 Drive 能灌多大电流 常见 2mA / 4mA / 8mA / 12mA
速率 Slew 边沿翻转快慢 影响 EMI 和功耗
中断触发 输入时才用 上升沿 / 下降沿 / 双边沿 / 高低电平

"开漏和推挽的区别,你说说?"老陈突然提问。

我想了想:“推挽能主动输出高也能主动输出低;开漏只能拉低,要输出高得靠外部上拉电阻。”

“对。那什么时候必须用开漏?”

“……I2C?”

"对,I2C 必须开漏。"老陈说,“因为 I2C 总线上挂了多个设备,要’线与’——任何一个设备拉低,总线就是低。如果都用推挽,一个输出高一个输出低,直接短路烧片。”

他顿了顿:“这些你在单片机课上学过的东西,在鸿蒙里一个字都不用重学。变的只是谁来帮你配这些寄存器。”

三、第一次系统点灯(轻量系统 · Hi3861)

“现在,用鸿蒙的方式点一次。”

3.1 一个关键说明:轻量系统不走 HDF

"先说清楚一件事,这很重要。"老陈的语气认真起来。

“在 Hi3861 这种轻量系统上,你通常不写 HDF 驱动,而是直接调用系统提供的 GPIO 接口。”

“啊?那 HDF 白学了?”

"没白学,只是用不上。"老陈说,“原因是——”

轻量系统(Hi3861, LiteOS-M)
  └─ 走 //base/iot_hardware 提供的 IoTGpio* 接口
     简单直接,一个函数调用搞定
     适合资源紧张、逻辑简单的场景

标准系统(RK3568, Linux)
  └─ 走 HDF 驱动框架
     要写驱动、写 HCS 配置、发布服务
     适合要被多个应用复用、要跨内核迁移的场景

"简单说:小设备图省事,大设备讲规矩。"老陈说,“你手上的板子属于前者,所以我先教你前者。HDF 的完整写法我下一节照样讲——因为那是你以后做标准系统项目的必备技能。”

3.2 认识接口

老陈打开源码里的头文件目录:

base/iot_hardware/peripheral/interfaces/kits/

“这些是系统给你的 GPIO 能力,常用的就这几个:”

函数 作用
IoTGpioInit(gpio) 初始化一个 GPIO
IoTGpioDeinit(gpio) 释放
IoTGpioSetDir(gpio, dir) 设置方向(IOT_GPIO_DIR_IN / IOT_GPIO_DIR_OUT)
IoTGpioGetDir(gpio, &dir) 读方向
IoTGpioSetOutputVal(gpio, val) 输出高低(IOT_GPIO_VALUE0 / IOT_GPIO_VALUE1)
IoTGpioGetOutputVal(gpio, &val) 读当前输出值
IoTGpioGetInputVal(gpio, &val) 读输入电平
IoTGpioSetIsrMode(gpio, mode, ...) 配置中断模式
IoTGpioRegisterIsrFunc(gpio, mode, func, arg) 注册中断回调

"注意 IoT 前缀。"老陈说,“带这个前缀的是面向 IoT 设备的封装接口,轻量系统专属。”

3.3 写代码

“现在写一个让 LED 闪烁的程序。”

老陈口述,我在键盘上敲:

/* applications/sample/wifi-iot/app/led_demo/led_demo.c */
#include <stdio.h>
#include <unistd.h>

#include "ohos_init.h"        /* SYS_RUN / APP_FEATURE_INIT */
#include "cmsis_os2.h"        /* 线程、延时 */
#include "iot_gpio.h"         /* IoTGpio* 接口 */

#define LED_GPIO    9         /* Hi3861 板载蓝灯接在 GPIO9 */
#define DELAY_MS    500

/* ---------- 1. GPIO 初始化 ---------- */
static void LedGpioInit(void)
{
    /* 初始化引脚(内部会处理复用) */
    IoTGpioInit(LED_GPIO);

    /* 设为输出 */
    IoTGpioSetDir(LED_GPIO, IOT_GPIO_DIR_OUT);

    printf("[LED] gpio%d init done, dir=OUT\r\n", LED_GPIO);
}

/* ---------- 2. 闪烁任务 ---------- */
static void LedTask(void *arg)
{
    (void)arg;
    int level = 0;

    while (1) {
        IoTGpioSetOutputVal(LED_GPIO, level);
        printf("[LED] set %d\r\n", level);

        level = !level;                    /* 翻转 */
        osDelay(DELAY_MS / 10);            /* CMSIS 延时单位是 10ms 的 tick */
    }
}

/* ---------- 3. 启动入口 ---------- */
static void LedDemoEntry(void)
{
    osThreadAttr_t attr = {0};
    attr.name       = "LedTask";
    attr.stack_size = 4096;                /* 栈大小,字节 */
    attr.priority   = osPriorityNormal;

    LedGpioInit();

    if (osThreadNew((osThreadFunc_t)LedTask, NULL, &attr) == NULL) {
        printf("[LED] create task FAILED!\r\n");
    } else {
        printf("[LED] task created\r\n");
    }
}

/* 把 LedDemoEntry 注册到系统启动流程 */
APP_FEATURE_INIT(LedDemoEntry);

"几个关键点。"老陈指着屏幕:

第一,APP_FEATURE_INIT 是什么?

"这是一个宏,作用是把你的函数注册进系统的启动流程。"老陈说,“裸机时代你的 main() 是整个世界的起点;在系统里,你的代码只是系统启动的一个环节,你得告诉系统’我有个函数,你启动到某一步的时候帮我调一下’。”

宏 执行时机 用途
SYS_RUN(func) 系统服务初始化阶段 较早,适合系统级服务
APP_FEATURE_INIT(func) 应用特性初始化阶段 应用代码用这个
APP_INIT(func) 更晚 依赖其他服务的场景
MODULE_INIT(func) 模块级 底层模块

“你写应用,记住用 APP_FEATURE_INIT 就行。”

第二,osDelay 的单位。

"CMSIS-RTOS 的 osDelay 参数是 tick 数,不是毫秒。"老陈敲黑板,“在 OpenHarmony 轻量系统里,一个 tick 通常是 10 毫秒。所以延时 500ms 要写 osDelay(50),我上面写成了 DELAY_MS / 10 就是这个道理。”

“我差点写成 osDelay(500)……”

"那就闪一次要五秒。"老陈笑了,“这个错我见过不下二十次。”

第三,栈大小别太小。

“4096 字节起步。你里面如果用了 printf,栈太小会直接溢出死机,而且死得很隐蔽——现象是板子莫名其妙重启。”

3.4 BUILD.gn

“代码写完,还得告诉构建系统去编它。”

# applications/sample/wifi-iot/app/led_demo/BUILD.gn

static_library("led_demo") {
    sources = [
        "led_demo.c",
    ]

    include_dirs = [
        "//utils/native/lite/include",                              # printf / ohos_init
        "//kernel/liteos_m/kal/cmsis",                              # cmsis_os2.h
        "//base/iot_hardware/peripheral/interfaces/kits",           # iot_gpio.h
        "//device/soc/hisilicon/hi3861v100/sdk_liteos/include",     # 芯片 SDK
    ]

    deps = [
        "//base/iot_hardware/peripheral:hal_iothardware",
    ]
}

“static_library 的意思是把这些源文件编成一个静态库,最后会被链接进固件。”

“那谁来引用这个库?”

"上层的应用 BUILD.gn。"老陈说,“一般在 applications/sample/wifi-iot/app/BUILD.gn 里,要加一行:”

# applications/sample/wifi-iot/app/BUILD.gn
lite_component("app") {
    features = [
        "led_demo:led_demo",     # ← 加上这一行,格式是 "目录名:目标名"
        # ...其他应用
    ]
}

"忘记加这一行,是最经典的’代码写了但没编进去’。"老陈说,“编译会 success,但你的代码根本不在固件里。”

3.5 编译、烧录、看结果

hb build
# 烧录 Hi3861_wifiiot_app_allinone.bin
# 打开串口 115200

“看串口——”

[LED] gpio9 init done, dir=OUT
[LED] task created
[LED] set 0
[LED] set 1
[LED] set 0
[LED] set 1
...

我盯着那颗蓝色 LED。

它开始闪了。一秒一次,稳定得像心跳。

“……亮了。”

"嗯,亮了。"老陈靠在椅背上,“现在告诉我——同样是点灯,你刚才干的,跟大一干的那次,本质区别在哪儿?”

我想了很久。

“……我没有碰寄存器。”

“继续。”

“我没有配时钟、没有配复用、没有算寄存器地址。我只是跟系统说’把这个脚设成输出、写个 1’,剩下的它替我做了。”

“还有呢?”

"我的代码不是世界的中心。"我慢慢说,“大一的时候,我的 while(1) 就是整个宇宙;现在,我的代码只是系统里一个普通的线程,旁边还有别的线程在跑网络、跑文件系统、跑别的东西。”

老陈看了我一眼,眼里有点东西。

"这句话你记住。"他说,“这是从’写单片机’到’做系统’的分界线。”

四、换个境界:HDF 里的点灯

"好了,现在看规矩大的那种。"老陈把 RK3568 推过来。

“标准系统上要点灯,你得写一个驱动。”

4.1 为什么要多此一举

“我直接调个 GPIO 接口不就行了?”

"能行,但不该这么干。"老陈说,“原因有三:”

1. 复用性 —— 驱动写一次,所有应用都能调,不用每个应用自己配一遍
2. 权限 —— GPIO 是硬件资源,不该让随便一个应用直接碰
3. 跨内核 —— HDF 驱动换内核不用改代码(第 02 章说过)

"在真实产品里,驱动和应用是分开的、由不同的人写。"老陈说,“驱动工程师把硬件能力封装成标准接口,应用工程师只管调。这是工业化分工。”

4.2 HDF 驱动的三段人生

“每个 HDF 驱动都有三个回调,对应它的一生:”

/* 驱动入口结构 */
struct HdfDriverEntry g_ledDriverEntry = {
    .moduleVersion = 1,
    .moduleName    = "HDF_LED_DRIVER",     /* 驱动名,要跟 HCS 里对上 */
    .Bind          = LedDriverBind,        /* ① 绑定:把驱动挂到设备上 */
    .Init          = LedDriverInit,        /* ② 初始化:真正干活的地方 */
    .Release       = LedDriverRelease,     /* ③ 释放:清理现场 */
};

/* 这个宏把驱动注册进 HDF 框架 */
HDF_INIT(g_ledDriverEntry);
   系统启动
      │
      ▼
   HDF 框架扫描配置
      │
      ▼
   ① Bind()     把驱动实例和设备节点"绑"在一起
      │          通常在这里给设备对象挂上服务(Dispatch)
      ▼
   ② Init()     初始化硬件:配 GPIO、申请资源、建线程
      │          真正的功能在这里实现
      ▼
   驱动运行中   上层通过 Dispatch 调用它
      │
      ▼
   ③ Release()  卸载时清理:释放 GPIO、销毁资源

4.3 一个完整的 LED 驱动

“看代码。”

/* drivers/led/led_driver.c */
#include "hdf_device_desc.h"
#include "hdf_log.h"
#include "gpio_if.h"          /* HDF 的 GPIO 平台接口 */
#include "osal_mutex.h"

#define HDF_LOG_TAG    led_driver
#define LED_GPIO_NUM   9      /* 具体引脚号看你的板子 */

struct LedDevice {
    uint16_t     gpioNum;
    uint8_t      state;
    OsalMutex    lock;        /* 保护并发访问 */
};

/* ---------- ① Bind:挂上服务 ---------- */
static int32_t LedDriverBind(struct HdfDeviceObject *device)
{
    if (device == NULL) {
        HDF_LOGE("Bind: device is NULL");
        return HDF_ERR_INVALID_OBJECT;
    }

    struct LedDevice *led = (struct LedDevice *)OsalMemCalloc(sizeof(*led));
    if (led == NULL) {
        return HDF_ERR_MALLOC_FAIL;
    }

    led->gpioNum = LED_GPIO_NUM;
    led->state   = 0;
    OsalMutexInit(&led->lock);

    /* 关键:把私有数据挂到设备上,Init / Dispatch 里再取出来 */
    device->priv = led;

    /* 关键:把 Dispatch 挂到设备的服务上,上层才能调进来 */
    device->service = &(struct IDeviceIoService){
        .Dispatch = LedDriverDispatch,
        .Open     = NULL,
        .Release  = NULL,
    };

    HDF_LOGI("Bind ok, gpio=%u", led->gpioNum);
    return HDF_SUCCESS;
}

/* ---------- ② Init:初始化硬件 ---------- */
static int32_t LedDriverInit(struct HdfDeviceObject *device)
{
    struct LedDevice *led = (struct LedDevice *)device->priv;

    /* 配置成输出 */
    int32_t ret = GpioSetDir(led->gpioNum, GPIO_DIR_OUT);
    if (ret != HDF_SUCCESS) {
        HDF_LOGE("GpioSetDir failed: %d", ret);
        return ret;
    }

    /* 默认关灯 */
    GpioWrite(led->gpioNum, GPIO_VAL_LOW);

    HDF_LOGI("Init ok");
    return HDF_SUCCESS;
}

/* ---------- ③ Release:清理 ---------- */
static void LedDriverRelease(struct HdfDeviceObject *device)
{
    struct LedDevice *led = (struct LedDevice *)device->priv;
    if (led == NULL) return;

    GpioWrite(led->gpioNum, GPIO_VAL_LOW);
    OsalMutexDestroy(&led->lock);
    OsalMemFree(led);
    device->priv = NULL;

    HDF_LOGI("Release ok");
}

“这是骨架。现在加上被调用的能力——Dispatch。”

4.4 Dispatch:驱动的"对外接口"

“上层怎么让灯亮?通过 Dispatch 消息。”

/* 定义命令字:上层和驱动约定好的"暗号" */
#define LED_CMD_SET_STATE   1     /* 设置开关 */
#define LED_CMD_GET_STATE   2     /* 读取状态 */

/* ---------- Dispatch:处理上层调用 ---------- */
static int32_t LedDriverDispatch(
    struct HdfDeviceIoClient *client,   /* 谁在调用 */
    int32_t cmd,                        /* 干什么(命令字) */
    struct HdfSBuf *data,               /* 带进来的参数 */
    struct HdfSBuf *reply)              /* 要带回去的结果 */
{
    if (client == NULL || client->device == NULL) {
        return HDF_ERR_INVALID_OBJECT;
    }
    struct LedDevice *led = (struct LedDevice *)client->device->priv;
    if (led == NULL) {
        return HDF_FAILURE;
    }

    switch (cmd) {
        case LED_CMD_SET_STATE: {
            uint8_t val = 0;
            /* 从 data 里读出参数(顺序必须和写入时一致) */
            if (!HdfSbufReadUint8(data, &val)) {
                HDF_LOGE("read val failed");
                return HDF_ERR_INVALID_PARAM;
            }

            OsalMutexLock(&led->lock);
            led->state = val ? 1 : 0;
            GpioWrite(led->gpioNum, led->state ? GPIO_VAL_HIGH : GPIO_VAL_LOW);
            OsalMutexUnlock(&led->lock);

            HDF_LOGI("set led = %u", led->state);
            return HDF_SUCCESS;
        }

        case LED_CMD_GET_STATE: {
            /* 把结果写回 reply */
            if (!HdfSbufWriteUint8(reply, led->state)) {
                return HDF_FAILURE;
            }
            return HDF_SUCCESS;
        }

        default:
            HDF_LOGE("unknown cmd: %d", cmd);
            return HDF_ERR_NOT_SUPPORT;
    }
}

“HdfSBuf 是个序列化缓冲区。“老陈解释,”‘序列化’这词听着唬人,其实就是’把数据排成一条队’——你要传三个参数,就按顺序一个一个排进去;对面再按顺序一个一个取出来。上层把参数’写’进去,驱动按同样的顺序’读’出来。反过来,驱动把结果写进 reply,上层再读。”

“顺序错了会怎样?”

"读出垃圾数据。"老陈说,“比如你写了个 uint32 进去,驱动按 uint8 读,那就全乱了。这是 HDF 最常见的 bug,没有之一。”

4.5 HCS 配置:告诉框架"这个驱动长在哪"

“光有代码还不够,你得用配置文件告诉 HDF 这个驱动的存在。”

/* device_info.hcs —— 设备信息:声明驱动 */
root {
    device_info {
        platform :: host {                    /* host = 驱动宿主(容器) */
            hostName = "platform_host";
            priority = 50;                    /* 宿主优先级 */

            device_led :: device {            /* 一个设备 */
                device0 :: deviceNode {       /* 一个设备节点 */
                    policy = 2;               /* 服务发布策略(见下表) */
                    priority = 100;           /* 驱动加载优先级(小的先加载) */
                    preload = 1;              /* 加载方式(见下表) */
                    permission = 0664;        /* 设备节点权限 */
                    moduleName = "HDF_LED_DRIVER";      /* ← 要和代码里的 moduleName 一致 */
                    serviceName = "hdf_led_service";    /* ← 上层按这个名字找服务 */
                    deviceMatchAttr = "led_config";     /* ← 指向私有配置 */
                }
            }
        }
    }
}

“两个字段必须记住:”

字段 含义 常用取值
policy 服务发布给谁看 0=不发布;1=只对内核态;2=内核态+用户态都发布(常用)
preload 什么时候加载 0=不加载;1=系统启动时加载(常用);2=按需加载
/* led_config.hcs —— 私有配置:驱动自己的参数 */
root {
    platform {
        led_config {
            gpioNum = 9;
            defaultState = 0;
            match_attr = "led_config";       /* ← 和 deviceMatchAttr 对上 */
        }
    }
}

“HCS 是文本配置,编译的时候由 hc-gen 工具转成二进制,驱动启动时会读到。”

“改配置要重新编译吗?”

"要。"老陈说,“HCS 是编译期转换的,不是运行时读的。改完配置,重新编译烧录。”

4.6 上层怎么调这个驱动

“驱动写好了,应用怎么调?”

/* 用户态应用调用 HDF 驱动 */
#include "hdf_io_service_if.h"

#define LED_SERVICE_NAME  "hdf_led_service"
#define LED_CMD_SET_STATE 1

int SetLed(int on)
{
    /* 1. 按服务名拿到驱动 */
    struct HdfIoService *serv = HdfIoServiceBind(LED_SERVICE_NAME);
    if (serv == NULL) {
        printf("bind %s failed\n", LED_SERVICE_NAME);
        return -1;
    }

    /* 2. 准备参数缓冲区 */
    struct HdfSBuf *data  = HdfSBufTypedObtain(SBUF_IPC);
    struct HdfSBuf *reply = HdfSBufTypedObtain(SBUF_IPC);

    HdfSbufWriteUint8(data, (uint8_t)on);

    /* 3. 发命令 */
    int ret = serv->dispatcher->Dispatch(&serv->object, LED_CMD_SET_STATE, data, reply);
    if (ret != HDF_SUCCESS) {
        printf("dispatch failed: %d\n", ret);
    }

    /* 4. 释放 */
    HdfSBufRecycle(data);
    HdfSBufRecycle(reply);
    HdfIoServiceRecycle(serv);
    return ret;
}

“四个步骤:Bind 拿服务 → 装参数 → Dispatch 发命令 → 回收。”

“如果 Bind 失败呢?”

"99% 是 serviceName 拼错了,或者 policy 没设成 2。"老陈说,“第一位排查点永远是这两个。”

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

我:老师,我有个问题——为什么非要用 Dispatch 这么绕?直接给我一个 LedOn() 函数不行吗?

老陈:因为驱动和应用不在同一个地址空间。

我:(愣)什么意思?

老陈:你的应用跑在用户态,驱动跑在内核态或者另一个进程里。**它们看不到彼此的内存。**你没法直接调用对方的函数。

我:所以要用消息传?

老陈:对。Dispatch 就是那个"传话筒"。 你把命令和参数打包成消息,框架帮你送过去,驱动处理完再把结果打包送回来。

我:这个"打包"就是 HdfSBuf?

老陈:对。 写进去的字节序列,对面按同样的顺序读出来。就像发电报——双方得约定好格式,不然译出来就是乱码。

我:那为什么叫 cmd 不叫函数名?

老陈:因为跨了进程,函数名传不过去,只能传数字。 所以双方约定:1 表示开灯,2 表示读状态。这个约定叫"接口契约",得写在文档里,两边都遵守。

我:要是有人乱传一个 cmd 呢?

老陈:所以 Dispatch 里必须写 default 分支返回错误。 不认识的命令一律拒绝。

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

五、翻车急救:灯为什么不亮

"最后,给你一份点灯的排查清单。"老陈说,“这辈子你会用到无数次。”

现象 排查顺序
完全不亮 ① 硬件:LED 极性对吗?限流电阻焊了吗?
② 引脚号:你点的 GPIO 号,真的是灯接的那个吗?
③ 复用:这个脚有没有被配成别的功能?
④ 供电:板子供电够吗?
亮了但不闪 ① 任务真的跑起来了吗?看串口日志
② osDelay 单位是不是搞错了(tick ≠ ms)
③ 是不是延时太长,你以为没闪
闪几下就不动了 ① 栈溢出(栈太小 + printf)
② 死循环里没延时,把 CPU 占满了
③ 空指针,任务崩了
串口没日志 ① 波特率对不对(一般 115200)
② 代码编进去了吗?(BUILD.gn + app 的 features 都加了吗?)
③ APP_FEATURE_INIT 写了吗?
HDF 驱动 Bind 失败 ① serviceName 拼对了吗?
② policy 是不是设成 2 了?
③ HCS 里 moduleName 和代码里的一致吗?
④ 驱动真的加载了吗?看 hdf_devmgr 相关日志
Dispatch 返回失败 ① 参数读写顺序一致吗?
② 数据类型大小一致吗?
③ cmd 在 default 分支被拒了吗?

"最后送你一条经验——"老陈站起来收拾东西,“永远先确认’最简单那部分’是对的。”

“什么是最简单的部分?”

"灯本身会不会亮。"老陈说,“拿根杜邦线,把那个脚直接接到 VCC,看灯亮不亮。先证明硬件是好的,再去怀疑软件。”

“……我上周围绕一个不亮的灯查了三个小时代码,最后发现是 LED 焊反了。”

老陈笑了:“欢迎入行。”

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

六、一盏灯背后的东西

临走前,老陈在白板上留了最后一段话,让我抄下来:

大一的时候:
    我 → 寄存器 → 引脚 → 灯

现在(轻量系统):
    我 → IoTGpioSetOutputVal() → 系统 → 寄存器 → 引脚 → 灯

现在(标准系统):
    我(应用) → Dispatch → [内核边界] → HDF 驱动 → GpioWrite() → 寄存器 → 引脚 → 灯

区别不在"灯亮没亮",而在:
    · 谁负责操作寄存器
    · 谁来防止你和别人抢同一根引脚
    · 你写的东西,能不能被别人复用

"看懂这张图,"老陈说,“你就明白什么叫系统了。”

“那接下来学什么?”

"灯只有两个状态,太无聊了。"老陈把一块温湿度传感器丢给我,“下一个——让板子学会’说话’。”

我接过来,看到上面印着一行小字:SHT30。

“这玩意儿怎么跟我板子说话?”

"I2C。"老陈说,“两根线,一根时钟一根数据,能挂一百多个设备。”

他走到门口,补了一句:

“下一章,我们讲总线——I2C、SPI、UART、PWM、ADC。这些是嵌入式真正的五脏六腑。”


本章自检

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

  • ☐ 配置一个 GPIO 输出,需要哪五步?
  • ☐ osDelay(50) 到底延时多久?为什么不是 50 毫秒?
  • ☐ APP_FEATURE_INIT 是干什么的?换成 SYS_RUN 有什么区别?
  • ☐ BUILD.gn 写了,但上层 features 忘了加会怎样?
  • ☐ HDF 的三个回调分别在什么时候被调用?各自该干什么?
  • ☐ Dispatch 的参数读写顺序写反了会怎样?

**➡️ 下一站:**第 05 章 · 五脏六腑 —— 让板子学会「说话」和「感知」。


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

导演:卡。主角点亮了灯,还悟出了一点东西。

助理:这一章技术密度挺高的——GPIO 参数、HDF 三段式、HCS 配置、Dispatch、两套路子。

导演:两套路子是关键。 很多人学鸿蒙南向,最大的困惑就是"我到底该用 IoTGpio 还是 HDF"。

助理:答案是看系统形态。

导演:对。轻量系统图快,标准系统讲规矩。 这个判断力比记住 API 名字值钱。

助理:下一章是总线?

导演:对。GPIO 只能表示一个 bit,要真正做东西,必须会总线。

助理:五种总线一章讲得完吗?

导演:讲得完,但要抓重点。 I2C 和 SPI 是主力,UART 是调试命脉,PWM 和 ADC 是必备配件。

助理:还有中断、DMA、线程同步这些。

导演:都得讲。这一章是南向的"五脏六腑",讲完它,主角才算真的能做一个东西出来。

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

未完待续。。。

支持与分享

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

评论

欢迎来到我的博客!