☰
从点亮LED开始,构建一台自己组网的小车!
2026/10/1 14:29:43 网站建设 项目流程

小熊派 Hi3863 第一个程序:GPIO 点灯与串口打印

文章目录

  • 小熊派 Hi3863 第一个程序:GPIO 点灯与串口打印
    • 一、动手前先确认这几样
    • 二、最小程序长什么样
    • 三、app_run() 到底做了什么
      • 它是在什么时候被调用的
    • 四、点灯就三件事
    • 五、led 样例:同一件事的另一种写法
    • 六、printf 打印到哪里去了
    • 七、怎么验证
    • 八、踩坑
      • 坑 1:led 目录放好了,编译却完全没编到它
      • 坑 2:blinky 的默认引脚是 GPIO 2,不是板上 LED 那根
      • 坑 3:入口函数不是 main(),别在里面写业务循环
      • 坑 4:建任务的返回值必须判断
      • 坑 5:printf 没东西,九成是波特率或者端口
      • 坑 6:led 样例里任务函数的签名是被强转过去的
    • 九、这个专栏最后要做成什么:1 主机 + 2 从机的组网儿童车
    • 十、下一篇

环境搭好之后,下一个问题是:代码从哪进去、任务怎么建、引脚怎么点、printf 打到哪。这四个问题在 Hi3863 上都有 SDK 自己的答案,跟"STM32 里写个main()然后while(1)"的习惯完全不一样。

这篇拿 SDK 里两个最短的样例(blinky和led)逐段拆开:app_run()是怎么被自动调用的、GPIO 点灯必须按什么顺序调用、printf一路走到哪个串口。看完你能自己写出第一个能编的样例。

这个专栏最后要做成什么:把一台普通的儿童电动车,改装成一台 1 主机 + 2 从机的 SLE(星闪)组网遥控小车。

第 01 篇把环境打通,这一篇是第一个能上板的程序,之后每一篇都往这台车上加一块——电机 → 遥控 → 组网 → 闭环。完整的 18 篇路线图放在文末第九节。

本篇是源码解析。blinky/led这两个目录都不在实证构建里(那份成功的构建日志启用的是 SLE 一主八从 Server 配置)。下面的宏定义、调用链、配置项都来自 SDK 源码原文;串口输出和上板现象请以你自己的板子为准。


一、动手前先确认这几样

项目我这边的情况说明
开发板小熊派 Hi3863(HiHope NearLink DK3863E V03)核心板 + 底板,点灯还用到交通灯板
主控Hi3863 / WS63,RISC-V 内核系统是 LiteOS
软件HiSpark Studio + 对应版本 SDK样例在application/samples/peripheral/下
本篇用的样例blinky(CMSIS 写法)、led(osal 写法)两个都是最小点灯程序
串口日志口 921600、8 数据位、1 停止位、无校验用 115200 连必然乱码
上板验证本文没有附串口截图和接线照片只讲源码层面能确认的东西

最后一行很重要:这一篇不会出现"我实测看到灯闪了"这种话。你要的是能编、能对着改的代码骨架,这部分源码完全能支撑。

二、最小程序长什么样

application/samples/peripheral/blinky/blinky_cmsis.c去掉版权头就只有这些:

#include"pinctrl.h"#include"soc_osal.h"#include"gpio.h"#include"osal_debug.h"#include"cmsis_os2.h"#include"app_init.h"#defineBLINKY_TASK_STACK_SIZE0x1000#defineBLINKY_TASK_PRIO(osPriority_t)(17)staticvoid*blinky_task(constchar*arg){unused(arg);uapi_pin_set_mode(CONFIG_BLINKY_PIN,PIN_MODE_0);uapi_gpio_set_dir(CONFIG_BLINKY_PIN,GPIO_DIRECTION_OUTPUT);uapi_gpio_set_val(CONFIG_BLINKY_PIN,GPIO_LEVEL_LOW);while(1){osal_msleep(CONFIG_BLINKY_DURATION_MS);uapi_gpio_toggle(CONFIG_BLINKY_PIN);}returnNULL;}staticvoidblinky_entry(void){osThreadAttr_tattr;attr.name="BlinkyTask";attr.attr_bits=0U;attr.cb_mem=NULL;attr.cb_size=0U;attr.stack_mem=NULL;attr.stack_size=BLINKY_TASK_STACK_SIZE;attr.priority=BLINKY_TASK_PRIO;if(osThreadNew((osThreadFunc_t)blinky_task,NULL,&attr)==NULL){/* Create task fail. */}}app_run(blinky_entry);

一眼看不出门道的是最后那行app_run(blinky_entry);——它不在任何函数体内,是裸放在文件末尾的。这就是 Hi3863 样例的入口写法:不写int main(),而是注册一个入口函数。

它的执行骨架如图 1。

五步走完:文件里app_run注册 → 链接器把函数指针收进一个段 → SDK 的main()启动时遍历这个段 → 调用你的 entry → entry 里建任务 → 任务里死循环点灯。

三、app_run() 到底做了什么

这一节是整篇最值得看懂的部分,因为它解释了"为什么可以不写 main"。

app_run展开后是这样一条链(middleware/utils/app_init/app_init.h):

#definelayer_initcall(func,layer,clayer,priority)\staticconstinit_call_tUSED_ATTR __zinitcall_##layer##_##func\__attribute__((section(".zinitcall."clayer #priority".init")))=(func)#definelayer_initcall_def(func,layer,clayer)\layer_initcall(func,layer,clayer,0)#defineapp_run(func)layer_initcall_def(func,run,"app_run")

翻译成人话:app_run(blinky_entry)等于定义一个静态函数指针变量,并把它放进名为.zinitcall.app_run0.init的段里。这就是一个"自注册"机制,效果上跟你手动维护一张函数指针表一样,只是交给编译器和链接器去做了。

另一头在链接脚本里,这一段被包了起来(output/ws63/acore/ws63-liteos-app/linker.lds):

__zinitcall_app_run_start = .; KEEP(*(.zinitcall.app_run*.init)) __zinitcall_app_run_end = .;

最后在app_init.c里被逐个调用:

voidapp_tasks_init(void){init_call_t*initcall=&__zinitcall_app_run_start;init_call_t*initend=&__zinitcall_app_run_end;for(;initcall<initend;initcall++){(*initcall)();}}

注意KEEP(*(.zinitcall.app_run*.init))里那个*——这个段里可以放不止一个入口函数,app_tasks_init()会把它们全跑一遍。所以你想挂自己的初始化代码,写一行app_run(your_entry);就够了,不需要去改 SDK 的main.c。

它是在什么时候被调用的

app_tasks_init()出现在application/ws63/ws63_liteos_application/main.c的main()里,位置很关键:

hw_init();/* 引脚、GPIO、UART、看门狗等底层初始化 *//* ... */main_initialise(NULL,0);/* SDK 自己的任务表 */OHOS_SystemInit();app_tasks_init();/* ← 你的 app_run 入口在这里被调用 */osKernelStart();/* ← 内核这时候才开始跑 */

app_tasks_init()跑在osKernelStart()之前。这件事决定了三条写法上的规矩:

  • entry 里可以放心建任务(osThreadNew/osal_kthread_create),任务会等内核启动后再被调度;
  • entry 里不能写死循环、长延时、等信号量,否则osKernelStart()永远等不到,现场就是"板子毫无反应";
  • hw_init()里已经调过uapi_pin_init()和uapi_gpio_init()了,你不需要自己初始化 GPIO 驱动。

四、点灯就三件事

blinky_task()里对引脚的操作只有三句半,顺序不能换:

/* 1. 把引脚复用成 GPIO 功能 */uapi_pin_set_mode(CONFIG_BLINKY_PIN,PIN_MODE_0);/* 2. 方向设为输出 */uapi_gpio_set_dir(CONFIG_BLINKY_PIN,GPIO_DIRECTION_OUTPUT);/* 3. 先给一个确定的电平 */uapi_gpio_set_val(CONFIG_BLINKY_PIN,GPIO_LEVEL_LOW);/* 4. 循环里翻转 */uapi_gpio_toggle(CONFIG_BLINKY_PIN);

这几个接口都在include/driver/gpio.h和include/driver/pinctrl.h里,返回errcode_t(成功是ERRCODE_SUCC)。样例为了短,没有判返回值,你自己写建议补上。

第一步最容易被忽略。WS63 的引脚基本都是复用的,同一个物理引脚可能既是 GPIO,又是 UART、PWM 或 SPI 的某一根线。uapi_pin_set_mode()就是"这根线现在归谁用"的开关,先设它,后面的电平操作才有效。

这里还有个容易让人犯迷糊的地方:blinky写的是PIN_MODE_0,led样例写的是HAL_PIO_FUNC_GPIO,看着像两个东西,其实是同一个值:

/* drivers/chips/ws63/porting/pinctrl/pinctrl_porting.h */#defineHAL_PIO_FUNC_GPIOPIN_MODE_0typedefenum{PIN_MODE_0=0,PIN_MODE_1=1,/* ... */PIN_MODE_MAX=8}pin_mode_t;

两个都等价于"用模式 0",也就是 GPIO 功能。反过来,如果一根线已经被设成了PIN_MODE_2(比如某个 UART 的 TX),你再对它调uapi_gpio_set_dir()是不会动的。

引脚号来自 Kconfig,不写死在代码里:

# application/samples/peripheral/blinky/Kconfig config BLINKY_PIN int prompt "Choose BLINKY_PIN pin." depends on SAMPLE_SUPPORT_BLINKY default 2 config BLINKY_DURATION_MS int prompt "Duration of blinky in MS." default 500

默认引脚是 GPIO 2,默认周期 500 ms。这两个值跟你的板子上 LED 接在哪根线没有关系,换板子就得改这里——这是后面踩坑一节里的一条。

五、led 样例:同一件事的另一种写法

application/samples/peripheral/led/led_example.c干的事跟blinky一模一样,但用的是 osal 那套接口:

#defineBLINKY_TASK_STACK_SIZE0x1000#defineBLINKY_TASK_PRIO24#defineBSP_LED7// RED#defineCONFIG_BLINKY_DURATION_50MS50staticvoid*led_task(constchar*arg){unused(arg);uapi_pin_set_mode(BSP_LED,HAL_PIO_FUNC_GPIO);uapi_gpio_set_dir(BSP_LED,GPIO_DIRECTION_OUTPUT);uapi_gpio_set_val(BSP_LED,GPIO_LEVEL_LOW);while(1){osal_msleep(CONFIG_BLINKY_DURATION_50MS);uapi_gpio_toggle(BSP_LED);}returnNULL;}staticvoidled_entry(void){uint32_tret;osal_task*taskid;osal_kthread_lock();/* 建任务前先加锁 */taskid=osal_kthread_create((osal_kthread_handler)led_task,NULL,"led_task",BLINKY_TASK_STACK_SIZE);ret=osal_kthread_set_priority(taskid,BLINKY_TASK_PRIO);if(ret!=OSAL_SUCCESS){printf("create task1 failed .\n");}osal_kthread_unlock();}app_run(led_entry);

两者的差异可以对照这张表:

对比项blinky(CMSIS 写法)led(osal 写法)
头文件cmsis_os2.hsoc_osal.h
建任务osThreadNew(func, NULL, &attr)osal_kthread_create(func, NULL, name, stack)
优先级attr.priority = (osPriority_t)(17)osal_kthread_set_priority(task, 24)
失败判断判断osThreadNew是不是 NULL判断set_priority是不是OSAL_SUCCESS
建任务加锁不用osal_kthread_lock()/unlock()
引脚号CONFIG_BLINKY_PIN(Kconfig,默认 2)宏BSP_LED写死 7

两种都能用,选一种保持统一就行。真正要注意的是优先级是两套体系:

  • CMSIS 的osPriority_t是"数字越大越优先",osPriorityNormal等于 24;
  • osal 的优先级最终走到LOS_TaskPriSet(),LiteOS 这边LOS_TASK_PRIORITY_HIGHEST是 0、LOS_TASK_PRIORITY_LOWEST是 31,数字越小越优先。

SDK 自己的main.c里给任务定优先级用的就是 25 / 27 / 12 / 13 这类裸数字;而osal_task.h的函数注释又写着"必须是OSAL_TASK_PRIORITY_HIGH / MIDDLE / LOW之一"(在 LiteOS/FreeRTOS 分支下这几个值是 3 / 6 / 10)。两套说法并存,实际跑起来走的是LOS_TaskPriSet()——看代码时别把两边的心智模型混着用,跟着同一份 SDK 里的样例写最稳。

六、printf 打印到哪里去了

led样例里那句printf("create task1 failed .\n");为什么会在串口助手里出现?完整链路如图 5。

第一跳在kernel/liteos/printf_adapt/printf_adapt.c——SDK 把标准printf重定向到了 LiteOS 的串口打印:

intprintf(constchar*restrict fmt,...){va_list ap;va_start(ap,fmt);UartVprintf(fmt,ap);va_end(ap);return0;}

第二跳是底层 UART。日志口用的是平台固定的那一对引脚,波特率来自工程配置:

/* drivers/chips/ws63/include/platform_core.h */#defineCODELOADER_UART_TX_PINS_AGPIO15#defineCODELOADER_UART_RX_PINS_AGPIO14#defineLOG_UART_BUSCONFIG_LOG_UART/* drivers/chips/ws63/porting/uart/uart_porting.c */uart_attr_tuart_line_config={.baud_rate=CONFIG_LOG_UART_BAUDRATE,.data_bits=UART_DATA_BIT_8,.stop_bits=UART_STOP_BIT_1,.parity=UART_PARITY_NONE};

build/config/target_config/ws63/menuconfig/acore/ws63_liteos_app.config里这两个值是:

CONFIG_LOG_UART=1CONFIG_LOG_UART_BAUDRATE=921600

所以串口工具要开921600 8N1,用 115200 连一定是乱码。

于是"printf 没输出"的排查顺序也就清楚了:波特率 → 端口选对没有 → 那句 printf 到底跑到没有。第三点尤其常见:led样例的printf只写在失败分支里,任务创建成功了它一句话都不打——串口安安静静反而是好消息。

七、怎么验证

这篇给的是源码层面能确认的检查点,不是"我实测看到":

  • blinky_cmsis.c最后一行是app_run(blinky_entry);,不是main();
  • 引脚先uapi_pin_set_mode(),再uapi_gpio_set_dir(),顺序没反;
  • CONFIG_BLINKY_PIN/CONFIG_BLINKY_DURATION_MS跟你的板子对得上;
  • application/samples/peripheral/CMakeLists.txt和同目录Kconfig里有这个样例的开关;
  • KConfig 菜单里勾上了对应样例,编译日志最后出现######### Build target:ws63_liteos_app success;
  • 串口工具波特率设成 921600。

上板前再确认一件事:你要点亮的是哪根线。blinky默认 GPIO 2,led样例是 GPIO 7(对应交通灯板的 RED)。引脚值填错,现象就是"程序在跑、灯不亮"。

八、踩坑

坑 1:led 目录放好了,编译却完全没编到它

这份 SDK 里application/samples/peripheral/led/是从官方样例拷进来的:目录里.c和CMakeLists.txt都齐,但父目录的CMakeLists.txt和Kconfig里没有它的开关,所以它压根不在构建图里,编译日志里搜不到led_example。

补两处即可:

# application/samples/peripheral/CMakeLists.txt 里新增 if(DEFINED CONFIG_SAMPLE_SUPPORT_LED) add_subdirectory_if_exist(led) endif()
# application/samples/peripheral/Kconfig 里新增 config SAMPLE_SUPPORT_LED bool prompt "Support LED Sample." default n depends on ENABLE_PERIPHERAL_SAMPLE

改完去 KConfig 菜单勾上Support LED Sample再编。顺手核对一下led/CMakeLists.txt里登记的文件名和实际文件名一致。

坑 2:blinky 的默认引脚是 GPIO 2,不是板上 LED 那根

CONFIG_BLINKY_PIN的默认值 2 只是个占位值。板子上 LED 挂在别的脚时,程序逻辑完全正常——任务在跑、编译没报错——但灯就是不亮。先查原理图确认 LED 接在哪个 GPIO,再去改 Kconfig 或代码。

坑 3:入口函数不是 main(),别在里面写业务循环

app_run()注册的 entry 是在osKernelStart()之前被调用的。遇到"板子完全不启动",先检查 entry 里有没有while(1)、长osDelay、等信号量之类的阻塞操作。

正解是 entry 只负责建任务,循环放进任务函数里——两个样例都是这么写的。

坑 4:建任务的返回值必须判断

osThreadNew失败返回NULL,osal_kthread_create失败也返回NULL。可blinky里的判断体是空的(只留了一句注释),led判的是set_priority的返回值——两个都不完整。

建任务失败最常见的原因是栈太小或任务名太长。两个样例给的栈都是0x1000(4 KB)。你在任务里塞浮点运算、大数组、长printf之前,先把栈加上去,否则现象是跑着跑着直接崩,很难往栈上想。

坑 5:printf 没东西,九成是波特率或者端口

日志口是921600,不是 115200。另外日志口用的是固定引脚(S_AGPIO15/S_AGPIO14),USB 转串口要接到这一对上,接错口当然什么都没有。

还有一种情况:你确实接到了日志口、波特率也对,但代码里那条printf在失败分支里——没输出反而是正常结果。

坑 6:led 样例里任务函数的签名是被强转过去的

osal_kthread_handler的定义是int (*)(void *data),而led_task写的是void *led_task(const char *arg),两者是靠(osal_kthread_handler)强转接上的。编译能过,但参数类型和返回值都不匹配,属于典型的"能跑但别学"。

自己写的时候建议照osal_kthread_handler的原型来:int your_task(void *arg)。

九、这个专栏最后要做成什么:1 主机 + 2 从机的组网儿童车

先把终点摆出来,不然点灯就只是一次性的小实验。整个专栏的落点是一台改装儿童电动车——把手里的遥控器换成两块 Hi3863 之间的 SLE(星闪)无线链路。

分工我按最常见的做法来写,你可以按自己的车调整:

角色大概装在哪板子 + 外设负责什么
主机遥控端Hi3863 + 遥控接收采遥控信号,通过 SLE 下发指令,汇总车辆状态
从机 1车上Hi3863 + 电机驱动(PWM)收指令,驱动左右电机正反转、调速
从机 2车上Hi3863 + MT6816 磁编码器读车轮转角与速度,回传给主机做闭环

两块从机在同一台车上,主机在手里——这就是"1 主机 2 从机"的全部含义。SDK 里现成的样例是一主八从的框架,我们要做的是在这套框架上裁到 1 主 2 从,再把它接到真实的电机和编码器上。

路线图(篇号跟这篇所在的专栏一致):

阶段篇号你会拿到什么
打地基01 ~ 02环境能编能烧;第一个 GPIO 程序能跑;看懂app_run和任务是怎么建的
让它动03 ~ 05PWM 呼吸灯 → PWM 驱动电机正反转 → 多路 UART 基础收发
听懂遥控06 ~ 08STP23L 激光测距分帧、SBUS 遥控解析(100000 8E2 / 25 字节)、串口控电机
会组网09 ~ 13SLE 一主一从最小例程 → 一主八从的广播与多连接管理 → UART ↔ SLE 双向透传
会闭环14 ~ 17MT6816 读角度、编码器测速与滤波、SLE 遥控 + PID 闭环、UWB 跟随
复盘18多连接的坑与压测结论

按一周 2~3 篇的节奏走,这条线走完,你手上会有两台(甚至三台)Hi3863 在互相说话,而且其中一台真的在驱动车轮。

一句提醒:车上的动力部分是独立的一路。调试控制板的时候先把电机断开,别让程序还没写对,车轮就先转起来了。

十、下一篇

点灯确认的是"代码能进构建、任务能起来、引脚能控制"这条最小链路。下一篇写PWM 呼吸灯:peripheral/pwm/pwm_demo.c逐行解析——把引脚复用成 PWM 功能、设置频率和占空比,再用渐变做出呼吸效果,顺带对比 GPIO 软件翻转和硬件 PWM 的差别。

这一篇看着还是"玩具",但驱动电机调速用的就是同一个 PWM 外设。先把 PWM 玩明白,第 04 篇把电机接上去就顺理成章了。

如果你照着写完灯不亮,把三样东西贴出来基本就能定位:KConfig 里勾的是哪个样例、编译日志最后一行、串口输出。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询