鸿蒙造物者之路 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、线程同步这些。
导演:都得讲。这一章是南向的"五脏六腑",讲完它,主角才算真的能做一个东西出来。
========================================
未完待续。。。


