ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

从点亮LED开始,构建一台自己组网的小车! 小熊派 Hi3863 第一个程序GPIO 点灯与串口打印文章目录小熊派 Hi3863 第一个程序GPIO 点灯与串口打印一、动手前先确认这几样二、最小程序长什么样三、app_run() 到底做了什么它是在什么时候被调用的四、点灯就三件事五、led 样例同一件事的另一种写法六、printf 打印到哪里去了七、怎么验证八、踩坑坑 1led 目录放好了编译却完全没编到它坑 2blinky 的默认引脚是 GPIO 2不是板上 LED 那根坑 3入口函数不是 main()别在里面写业务循环坑 4建任务的返回值必须判断坑 5printf 没东西九成是波特率或者端口坑 6led 样例里任务函数的签名是被强转过去的九、这个专栏最后要做成什么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 源码原文串口输出和上板现象请以你自己的板子为准。一、动手前先确认这几样项目我这边的情况说明开发板小熊派 Hi3863HiHope NearLink DK3863E V03核心板 底板点灯还用到交通灯板主控Hi3863 / WS63RISC-V 内核系统是 LiteOS软件HiSpark Studio 对应版本 SDK样例在application/samples/peripheral/下本篇用的样例blinkyCMSIS 写法、ledosal 写法两个都是最小点灯程序串口日志口 921600、8 数据位、1 停止位、无校验用 115200 连必然乱码上板验证本文没有附串口截图和接线照片只讲源码层面能确认的东西最后一行很重要这一篇不会出现我实测看到灯闪了这种话。你要的是能编、能对着改的代码骨架这部分源码完全能支撑。二、最小程序长什么样application/samples/peripheral/blinky/blinky_cmsis.c去掉版权头就只有这些#includepinctrl.h#includesoc_osal.h#includegpio.h#includeosal_debug.h#includecmsis_os2.h#includeapp_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.nameBlinkyTask;attr.attr_bits0U;attr.cb_memNULL;attr.cb_size0U;attr.stack_memNULL;attr.stack_sizeBLINKY_TASK_STACK_SIZE;attr.priorityBLINKY_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(;initcallinitend;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_0led样例写的是HAL_PIO_FUNC_GPIO看着像两个东西其实是同一个值/* drivers/chips/ws63/porting/pinctrl/pinctrl_porting.h */#defineHAL_PIO_FUNC_GPIOPIN_MODE_0typedefenum{PIN_MODE_00,PIN_MODE_11,/* ... */PIN_MODE_MAX8}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();/* 建任务前先加锁 */taskidosal_kthread_create((osal_kthread_handler)led_task,NULL,led_task,BLINKY_TASK_STACK_SIZE);retosal_kthread_set_priority(taskid,BLINKY_TASK_PRIO);if(ret!OSAL_SUCCESS){printf(create task1 failed .\n);}osal_kthread_unlock();}app_run(led_entry);两者的差异可以对照这张表对比项blinkyCMSIS 写法ledosal 写法头文件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_PINKconfig默认 2宏BSP_LED写死 7两种都能用选一种保持统一就行。真正要注意的是优先级是两套体系CMSIS 的osPriority_t是数字越大越优先osPriorityNormal等于 24osal 的优先级最终走到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_rateCONFIG_LOG_UART_BAUDRATE,.data_bitsUART_DATA_BIT_8,.stop_bitsUART_STOP_BIT_1,.parityUART_PARITY_NONE};build/config/target_config/ws63/menuconfig/acore/ws63_liteos_app.config里这两个值是CONFIG_LOG_UART1CONFIG_LOG_UART_BAUDRATE921600所以串口工具要开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 2led样例是 GPIO 7对应交通灯板的 RED。引脚值填错现象就是程序在跑、灯不亮。八、踩坑坑 1led 目录放好了编译却完全没编到它这份 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里登记的文件名和实际文件名一致。坑 2blinky 的默认引脚是 GPIO 2不是板上 LED 那根CONFIG_BLINKY_PIN的默认值 2 只是个占位值。板子上 LED 挂在别的脚时程序逻辑完全正常——任务在跑、编译没报错——但灯就是不亮。先查原理图确认 LED 接在哪个 GPIO再去改 Kconfig 或代码。坑 3入口函数不是 main()别在里面写业务循环app_run()注册的 entry 是在osKernelStart()之前被调用的。遇到板子完全不启动先检查 entry 里有没有while(1)、长osDelay、等信号量之类的阻塞操作。正解是 entry 只负责建任务循环放进任务函数里——两个样例都是这么写的。坑 4建任务的返回值必须判断osThreadNew失败返回NULLosal_kthread_create失败也返回NULL。可blinky里的判断体是空的只留了一句注释led判的是set_priority的返回值——两个都不完整。建任务失败最常见的原因是栈太小或任务名太长。两个样例给的栈都是0x10004 KB。你在任务里塞浮点运算、大数组、长printf之前先把栈加上去否则现象是跑着跑着直接崩很难往栈上想。坑 5printf 没东西九成是波特率或者端口日志口是921600不是 115200。另外日志口用的是固定引脚S_AGPIO15/S_AGPIO14USB 转串口要接到这一对上接错口当然什么都没有。还有一种情况你确实接到了日志口、波特率也对但代码里那条printf在失败分支里——没输出反而是正常结果。坑 6led 样例里任务函数的签名是被强转过去的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 里勾的是哪个样例、编译日志最后一行、串口输出。
返回列表