xl不是70头像
关注
从点亮LED开始,构建一台自己组网的小车!封面图

从点亮LED开始,构建一台自己组网的小车!

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

环境搭好之后,下一个问题是:代码从哪进去、任务怎么建、引脚怎么点、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"

#define BLINKY_TASK_STACK_SIZE    0x1000
#define BLINKY_TASK_PRIO          (osPriority_t)(17)

static void *blinky_task(const char *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);
    }
    return NULL;
}

static void blinky_entry(void)
{
    osThreadAttr_t attr;
    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):

#define layer_initcall(func, layer, clayer, priority)            \
    static const init_call_t USED_ATTR __zinitcall_##layer##_##func \
        __attribute__((section(".zinitcall." clayer #priority ".init"))) = (func)

#define layer_initcall_def(func, layer, clayer) \
    layer_initcall(func, layer, clayer, 0)

#define app_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 里被逐个调用:

void app_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 */
#define HAL_PIO_FUNC_GPIO PIN_MODE_0

typedef enum {
    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 那套接口:

#define BLINKY_TASK_STACK_SIZE 0x1000
#define BLINKY_TASK_PRIO 24
#define BSP_LED 7                  // RED
#define CONFIG_BLINKY_DURATION_50MS 50

static void *led_task(const char *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);
    }
    return NULL;
}

static void led_entry(void)
{
    uint32_t ret;
    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 的串口打印:

int printf(const char *restrict fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    UartVprintf(fmt, ap);
    va_end(ap);
    return 0;
}

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

/* drivers/chips/ws63/include/platform_core.h */
#define CODELOADER_UART_TX_PIN S_AGPIO15
#define CODELOADER_UART_RX_PIN S_AGPIO14
#define LOG_UART_BUS           CONFIG_LOG_UART

/* drivers/chips/ws63/porting/uart/uart_porting.c */
uart_attr_t uart_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=1
CONFIG_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 里勾的是哪个样例、编译日志最后一行、串口输出。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2302_80616326/article/details/166884793

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--