2026.4.28衡山派学习复习笔记-CSDN博客衡山派学习之串口-CSDN博客前面我们已经单独学了如何点灯控制LED闪烁以及用串口收发数据但是他们都是独立的而一般的实际工程中模块间肯定有相互用的部分想到了这点所以我问codex ai你好我最近正在学习衡山派DS133EBS开发板然后我这两天刚跟着他的教程吧led和串口相关的地方跟着他的官方文档研究了一下 https://wiki.lckfb.com/zh-hans/hspi-d133ebs/beginner/driver-programming/gpio-framework-usage.html https://wiki.lckfb.com/zh-hans/hspi-d133ebs/beginner/driver-programming/uart-framework-usage.html 然后对应的就是user_led文件夹下和user_uart3文件夹下的内容但是我感觉他们都是独立的那我要是想让他们联动的话请问应该怎么做比如说我现在想当我的串口3接收到数据时来控制led的亮灭 比如当接收到Led_Board1 则我的灯点亮然后要是Led_Board0灯就熄灭是不是要现在user_led.c这个文件封装两个函数应该是打开灯一个是关闭灯但是这两个函数怎么给到user_uart3.c这个文件里面让他使用呢我现在不知道这个怎么弄请问你能懂我的意思吗懂你的理解是对的先在 user_led.c 里封装“开灯/关灯”函数再让 user_uart3.c 通过头文件调用这些函数。核心规则就一句.c 文件负责实现函数.h 文件负责把函数“声明出去”其他 .c 文件 #include 这个 .h 后就能调用。为什么要加 user_led.h比如你在 user_led.c 里写了void user_led_on(void) { rt_pin_write(user_led_pin, PIN_HIGH); } ​ void user_led_off(void) { rt_pin_write(user_led_pin, PIN_LOW); }那 user_uart3.c 默认是不知道这两个函数存在的。所以你要新建一个user_led.hifndef __USER_LED_H__ ​ define __USER_LED_H__ ​ int user_led_init(void); void user_led_on(void); void user_led_off(void); ​ endif然后在user_uart3.c里include ../user_led/user_led.h.. 的意思是上一级目录你的目录大概是这样现在user_uart3.c想包含user_led.h。从user_uart3.c所在位置看当前目录helloworld/user_uart3 上一级目录helloworld 目标文件helloworld/user_led/user_led.h所以路径就是include ../user_led/user_led.h这样 user_uart3.c 编译时就知道哦外面有 user_led_on() 和 user_led_off() 这两个函数。为什么函数不能写成 static如果你这样写static void user_led_on(void)这个函数就只能在 user_led.c 内部用别的文件看不到。所以要给 user_uart3.c 调用的函数不能加 staticvoid user_led_on(void) void user_led_off(void)但是 LED 引脚变量应该继续保持 staticstatic rt_base_t user_led_pin;因为引脚变量不需要暴露给 UART 文件UART 只需要调用“开灯/关灯”接口。你要记住的 C 语言模块化方法就是想让 A.c 里的函数给 B.c 用A.c 里实现函数A.h 里声明函数B.c 里 #include A.h并确保 A.c 和 B.c 都参与编译。3.1、重构LED驱动代码user_led.c#include stdlib.h #include stdio.h #include string.h #include getopt.h #include sys/time.h #include rtthread.h #include rtdevice.h #include aic_core.h #include aic_hal_gpio.h #include user_led.h ​ #define USER_LED_PIN_NAME PE.17 #define USER_LED_ON_LEVEL PIN_HIGH #define USER_LED_OFF_LEVEL PIN_LOW ​ #define THREAD_PRIORITY 25 // 线程优先级数字越小优先级越高 #define THREAD_STACK_SIZE 512 // 线程堆栈大小决定了线程可以使用的内存空间 #define THREAD_TIMESLICE 5 // 线程时间片决定了线程在调度时能占用CPU的最长时间 ​ static rt_thread_t led_thread RT_NULL; // 声明线程控制块指针用于后续创建和管理线程 static rt_base_t user_led_pin -1; // 声明引脚编号变量用于存储LED的引脚编号 static rt_bool_t user_led_inited RT_FALSE; ​ // 线程入口函数这是线程启动后执行的函数 static void user_led_thread_entry(void *param) { while(1) // 无限循环使线程持续运行 { /* * 这里保留给LED模块以后自己的后台任务。 * 不在这里闪灯避免和串口命令控制LED亮灭发生冲突。 */ rt_thread_mdelay(100); } } ​ ​ // 初始化LED引脚其他文件使用LED前也可以调用这个函数 int user_led_init(void) { if (user_led_inited RT_TRUE) { return RT_EOK; } ​ user_led_pin rt_pin_get(USER_LED_PIN_NAME); // 获取名为PE.17的引脚编号 if ((user_led_pin -RT_EINVAL) || (user_led_pin -RT_ENOSYS)) // 如果获取失败打印错误信息并返回 { rt_kprintf(Failed to get the pin %s\n, USER_LED_PIN_NAME); return -RT_ERROR; // 获取引脚失败直接返回避免后续操作导致错误 } ​ rt_pin_mode(user_led_pin, PIN_MODE_OUTPUT); // 设置引脚为输出模式 rt_pin_write(user_led_pin, USER_LED_OFF_LEVEL); // 初始时将LED设置为熄灭状态 ​ user_led_inited RT_TRUE; ​ return RT_EOK; } ​ // 初始化LED模块的函数在系统启动时调用 static int usr_led_run(void) { int ret user_led_init(); if (ret ! RT_EOK) { return ret; } ​ /* 创建线程名称是 led_thread入口是 user_led_thread_entry */ led_thread rt_thread_create(led_thread, // 线程名称 user_led_thread_entry, RT_NULL, // 线程入口函数和参数 THREAD_STACK_SIZE, // 线程堆栈大小 THREAD_PRIORITY, // 线程优先级 THREAD_TIMESLICE); // 线程时间片 if (led_thread RT_NULL) // 如果线程创建失败打印错误信息并返回 { rt_kprintf(Failed to create the led_thread\n); return -RT_ERROR; // 线程创建失败直接返回防止对无效的线程进行操作 } ​ /* 如果获得线程控制块启动这个线程 */ rt_thread_startup(led_thread); // 启动线程使其开始执行 ​ return RT_EOK; } ​ void user_led_set(int on) { if (user_led_init() ! RT_EOK) { return; } ​ rt_pin_write(user_led_pin, on ? USER_LED_ON_LEVEL : USER_LED_OFF_LEVEL); } ​ void user_led_on(void) { user_led_set(1); } ​ void user_led_off(void) { user_led_set(0); } ​ // 导出函数自动运行在系统初始化时调用usr_led_run函数 INIT_APP_EXPORT(usr_led_run);现在它的结构是user_led_init()初始化 LED 引脚 usr_led_run() 系统启动时自动调用初始化 LED 并创建线程user_led_set()根据参数控制 LED 亮灭 user_led_on() 开灯 user_led_off() 关灯user_led_thread_entry()保留线程但不再闪灯我保留了你的 LED 线程但线程里只做rt_thread_mdelay(100);原因是如果线程里继续写闪烁代码它会和串口控制冲突。比如串口刚收到 Led_Board1 把灯打开LED 线程下一秒又可能把它关掉。关键变化是把初始化单独拆成了int user_led_init(void)这样以后 user_uart3.c 想控制 LED 时不需要知道 PE.17、PIN_HIGH、PIN_LOW 这些细节只需要调用user_led_on();user_led_off();我还加了static rt_bool_t user_led_inited RT_FALSE;这是为了防止重复初始化。比如系统启动时会调用一次user_led_init()以后user_led_on()里面也可能再调用一次它会发现已经初始化过就直接返回不会重复配置引脚。你现在可以把user_led.c理解成一个“LED 服务模块”内部负责引脚和线程外部只通过user_led.h里的函数使用它。led的Kconfig串口的Kconfig3.2、串口驱动代码#include getopt.h #include string.h #include rtthread.h #include aic_core.h #include stdlib.h #include sys/time.h #include hal_adcim.h #include rtdevice.h #include aic_log.h #include hal_gpai.h #include stdio.h #include aic_hal_gpio.h #include ../user_led/user_led.h ​ ​ #define SAMPLE_UART_NAME uart3 // 串口设备名称 #define RCV_BUFF_SIZE_MAX 1024 // 接收最大字节长度 ​ static struct rt_semaphore rx_sem; // 用于接收消息的信号量 static rt_device_t serial; // 串口设备句柄 static rt_thread_t serial_recv_thread; // 串口接收线程句柄 ​ static char serial_recv_buff[RCV_BUFF_SIZE_MAX]; // 串口接收缓存区 static char serial_recv_flag; // 串口接收标志 static int serial_recv_length; // 接收字节长度 ​ ​ /* 串口发送和打印线程 */ ​ #define THREAD_PRIORITY 25 // 线程优先级 #define THREAD_STACK_SIZE 4096 // 线程大小 #define THREAD_TIMESLICE 20 // 时间片 ​ static rt_thread_t serial_thread RT_NULL; // 线程控制块 ​ ​ // 中断接收回调函数 static rt_err_t uart_input(rt_device_t dev, rt_size_t size) { /* 串口有数据传入后产生中断调用此回调函数释放信号量 */ if (size 0) rt_sem_release(rx_sem); ​ return RT_EOK; } ​ // 串口接收线程入口函数 static void serial_recv_thread_entry(void *param) { rt_kprintf(\nserial_recv_thread_entry run ......\n); ​ while(1) { char temp_recv_buff 0; // 接收临时缓存区 ​ int ret rt_device_read(serial, 0, temp_recv_buff, 1); ​ if(ret 0) // 出现了错误 { pr_debug(read() return [%ld] %s\n, rt_get_errno(), rt_strerror(rt_get_errno())); } ​ if(ret 0) // 未接到数据 { // 重置信号量 rt_sem_control(rx_sem, RT_IPC_CMD_RESET, RT_NULL); ​ // 获取信号量如果没有获取得到则阻塞在这里永远等待。 rt_sem_take(rx_sem, RT_WAITING_FOREVER); } ​ if(ret 1) // 接收到1字节的数据 { // 防止数据超出缓存区的大小 if(serial_recv_length RCV_BUFF_SIZE_MAX - 1) { // 存入接收缓存区并递增长度 serial_recv_buff[serial_recv_length] temp_recv_buff; // rt_kprintf(%x\n, temp_recv_buff); // 打印接收到的字节用于调试 } else { // 如果缓冲区已满则从0开始覆盖旧数据 serial_recv_length 0; serial_recv_buff[serial_recv_length] temp_recv_buff; } ​ // 为接收缓存区最后添加 \0 serial_recv_buff[serial_recv_length] \0; ​ // 设置串口接收完成标志 serial_recv_flag 1; } } } ​ /************************************************ 函数名称 Clear_recv_buff 功 能 清空串口接收缓存区 参 数 无 返 回 值 作 者 LC *************************************************/ static void Clear_recv_buff(void) { // 清空接收缓存区 rt_memset(serial_recv_buff, 0, sizeof(serial_recv_buff)); ​ // 清空标志位 serial_recv_flag 0; ​ // 清空缓存区长度计量 serial_recv_length 0; } ​ /************************************************ 函数名称 serial_send_byte 功 能 串口发送一个字节 参 数 发送的数据 返 回 值 RT_EOK成功 -RT_ERROR失败 作 者 LC *************************************************/ static int Serial_Send_Byte(uint8_t dat) { int ret rt_device_write(serial, 0, dat, 1); if(ret ! 1) { LOG_E(Failed to [Serial_Send_Byte] code[%d] !!!, ret); return -RT_ERROR; } ​ return RT_EOK; } ​ /************************************************ 函数名称 Serial_Send_String 功 能 串口发送字符串 参 数 data_buff缓存区地址 返 回 值 RT_EOK成功 -RT_ERROR失败 作 者 LCKFB *************************************************/ static int Serial_Send_String(uint8_t *data_buff) { int err_count 0; ​ /* 地址为空 或者 值为空 跳出 */ while(data_buff *data_buff) { if(RT_EOK ! Serial_Send_Byte(*data_buff)) { err_count; continue; } } ​ /* 如果err_count不为0则说明发送的时候有错误 */ if(err_count) { LOG_E(serial_send_string failed !!!); return -RT_ERROR; } ​ return RT_EOK; } ​ static void trim_command_tail(uint8_t *data_buff) { int len strlen((char *)data_buff); ​ while (len 0) { char ch data_buff[len - 1]; ​ if ((ch ! \r) (ch ! \n) (ch ! )) { break; } ​ data_buff[--len] \0; } } ​ ​ ​ /****************************************************************** * 函 数 名 称Serial_Recv_DATA * 函 数 说 明接串口的数据 * 函 数 形 参data_buff数据缓存区 * 函 数 返 回 0 未接收到数据 * 其他 接收到的数据长度 * 作 者LCKFB * 备 注无 ******************************************************************/ int Serial_Recv_DATA(uint8_t *data_buff) { int i; ​ /* 判断是否接到了数据 */ if((serial_recv_flag ! 1) || (serial_recv_length 0)) { /* 未接到 */ return 0; } ​ /* 将数据转存到指针指向的地址中 */ for(i 0; i serial_recv_length; i) { data_buff[i] serial_recv_buff[i]; } ​ /* 加入字符串结尾 */ data_buff[i] \0; ​ /* 清除接收的数据、标志位和数据长度。 */ Clear_recv_buff(); ​ return i; // 返回接收到的数据长度 } ​ /************************************************ 函数名称 UART_Init 功 能 串口初始化 参 数 无 返 回 值 RT_EOK成功 -RT_ERROR失败 作 者 LCKFB *************************************************/ static int UART_Init(void) { int ret 0; ​ // 清空接收缓存区 rt_memset(serial_recv_buff,0,sizeof(serial_recv_buff)); ​ // 清空标志位 serial_recv_flag 0; ​ // 清空缓存区长度计量 serial_recv_length 0; ​ rt_kprintf(Try to open(%s)\n, SAMPLE_UART_NAME); ​ // 获取串口句柄 serial rt_device_find(SAMPLE_UART_NAME); if (!serial) { LOG_E(find %s failed!\n, SAMPLE_UART_NAME); ​ return -RT_ERROR; } ​ // 初始化信号量 ret rt_sem_init(rx_sem, rx_sem, 0, RT_IPC_FLAG_FIFO); if (ret ! RT_EOK) { LOG_E(failed to rt_sem_init !\n); return -RT_ERROR; } ​ // 打开串口设备 ret rt_device_open(serial, RT_DEVICE_FLAG_INT_RX); if (ret ! RT_EOK) { LOG_E(open %s failed : %d !\n, SAMPLE_UART_NAME, ret); return -RT_ERROR; } ​ // 设置接收回调函数 rt_device_set_rx_indicate(serial, uart_input); ​ // 创建串口数据接收线程 serial_recv_thread rt_thread_create(serial, serial_recv_thread_entry, RT_NULL, 1024*2, 15, 20); if (serial_recv_thread ! RT_NULL) { // 启动线程 rt_thread_startup(serial_recv_thread); } else { rt_device_close(serial); LOG_E(Failed to [rt_thread_create] !!!); return -RT_ERROR; } ​ return RT_EOK; } ​ static void handle_uart_led_command(uint8_t *recv_buff) { trim_command_tail(recv_buff); ​ if (strcmp((char *)recv_buff, Led_Board1) 0) { user_led_on(); Serial_Send_String((uint8_t *)LED ON\r\n); rt_kprintf(LED is on\n); } else if (strcmp((char *)recv_buff, Led_Board0) 0) { user_led_off(); Serial_Send_String((uint8_t *)LED OFF\r\n); rt_kprintf(LED is off\n); } else { Serial_Send_String((uint8_t *)Unknown command\r\n); rt_kprintf(Unknown command: %s\n, recv_buff); } } // 线程入口函数 static void serial3_thread_entry(void *param) { rt_kprintf(Start serial3_thread_entry...\n); ​ while(1) { int count 0; ​ /* 接收缓存区 */ uint8_t recv_buff[RCV_BUFF_SIZE_MAX] {0}; ​ /* 获取接收到的数据长度 */ count Serial_Recv_DATA(recv_buff); ​ /* 确保 count 不超过 recv_buff 大小避免越界访问 */ if (count sizeof(recv_buff)) { LOG_E(Error: Received data exceeds buffer size! count %d,count); count sizeof(recv_buff); // 限制数据长度避免溢出 } ​ if (count 0) { rt_kprintf(\n\n); rt_kprintf(\nRead Data %s\n, recv_buff); rt_kprintf(\n\n); ​ handle_uart_led_command(recv_buff); } ​ /* 延迟 50 毫秒 */ rt_thread_mdelay(50); } } ​ // 数据发送函数 static void send_demoData(int argc, char **argv) { static int num 1; uint8_t Send_Buff[128] {立创·衡山派D133EBS开发板 * UART框架使用测试}; int ret 0; char buffer[128] {0}; ​ // 使用 snprintf 来格式化要发送的字符串 snprintf(buffer, sizeof(buffer), 【%d】%s, num, (argc 2) ? *(argv1) : (char *)Send_Buff); ​ // 发送数据 ret Serial_Send_String((uint8_t *)buffer); ​ if(ret ! RT_EOK) { LOG_E(%s: The test data transmission failed., __FUNCTION__); } else { rt_kprintf(\n[%d] Send success\n, num); num; // 只有发送成功时才递增 num } } ​ // 导出函数为命令 MSH_CMD_EXPORT(send_demoData, Send test data); ​ static rt_bool_t uart3_started RT_FALSE; static int uart3_start(void) { int ret 0; ​ if (uart3_started RT_TRUE) { rt_kprintf(UART3 already started.\n); return RT_EOK; } ​ ret UART_Init(); if (ret ! RT_EOK) { LOG_E(Failed to [UART_Init] !!!); return ret; } ​ rt_kprintf(UART_Init run END!!\n); ​ serial_thread rt_thread_create(serial3_thread, serial3_thread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (serial_thread RT_NULL) { LOG_E(Failed to create serial3_thread !!!); return -RT_ERROR; } ​ rt_thread_startup(serial_thread); ​ uart3_started RT_TRUE; ​ return RT_EOK; } ​ // 串口接收和发送线程开启 static void uart3_test_on(int argc, char **argv) { uart3_start(); } ​ // 导出函数为命令 MSH_CMD_EXPORT(uart3_test_on, Test transmission and reception using UART3 serial port); ​ //如果你想上电自动启动再加 /* 上电 - RT-Thread 自动调用 uart3_auto_start() - uart3_start() - 初始化 UART3 - 创建 UART3 接收线程 - 之后就能接收 Led_Board1 / Led_Board0 ​ MSH_CMD_EXPORT 可以保留也可以去掉。保留的话以后你还能手动测试但最好再加一个“已经启动”的标志避免你自动启动后又 手动输入 uart3_test_on重复创建线程。 */ static int uart3_auto_start(void) { return uart3_start(); } INIT_APP_EXPORT(uart3_auto_start); ​ ​ /* # 你现在是不是被很多函数搞混了 ​ 我帮你把它们按“角色”重新归类你就清楚了。 ​ ——— ​ ## 一类初始化类 ​ ### UART_Init() ​ 作用把串口系统搭起来 ​ 它负责 ​ - 找设备 - 建信号量 - 打开串口 - 注册回调 - 创建接收线程 ​ ——— ​ ## 二类通知类 ​ ### uart_input() ​ 作用收到数据后通知接收线程 ​ 核心只有一句 ​ rt_sem_release(rx_sem); ​ ——— ​ ## 三类接收类 ​ ### serial_recv_thread_entry() ​ 作用真正从串口读字节并存进缓存区 ​ ### Serial_Recv_DATA() ​ 作用把缓存区里的数据拷给上层用 ​ ### Clear_recv_buff() ​ 作用清空缓存区状态 ​ ——— ​ ## 四类发送类 ​ ### Serial_Send_Byte() ​ 发 1 个字节 ​ ### Serial_Send_String() ​ 发 1 串字符串 ​ ### send_demoData() ​ 这是 msh 命令调用发送函数发测试数据 ​ ——— ​ ## 五类业务展示类 ​ ### serial3_thread_entry() ​ 作用周期性检查有没有收到数据如果有就打印 ​ ### uart3_test_on() ​ 这是启动命令用来启动整个串口测试流程 ​ _______________________________________________ # 我们再用一个实战项目视角来讲 ​ 假设你要做一个“串口控制灯”的项目。 ​ 上位机发 ​ LED_ON ​ 板子收到后开灯。 ​ 你觉得这段代码里哪些部分可以直接复用 ​ 答案是 ​ ## 可以直接复用的 ​ - UART_Init() - uart_input() - serial_recv_thread_entry() - Serial_Send_Byte() - Serial_Send_String() ​ 这些都是“串口基础设施”。 ​ ## 你真正要改的 ​ 是这里 ​ serial3_thread_entry() ​ 现在它只是打印 ​ Read Data %s ​ 而你的项目里可以改成 ​ if (strcmp((char *)recv_buff, LED_ON) 0) { led_on(); } else if (strcmp((char *)recv_buff, LED_OFF) 0) { led_off(); } ​ ​ _____________________________________________________ # 你要学实战我建议你先建立这个最小模型 ​ 以后你一看到 RT-Thread 串口代码先问自己 4 个问题 ​ ## 1. 串口在哪里打开 ​ 找 rt_device_find 和 rt_device_open ​ ## 2. 数据来了谁先知道 ​ 找 rt_device_set_rx_indicate ​ ## 3. 谁真正把数据读出来 ​ 找 rt_device_read ​ ## 4. 读出来以后谁处理 ​ 找线程函数或业务函数 ​ 你按这个框架看代码就不会乱。 _____________________________________________________ ​ # 这段代码的完整执行流程我给你压缩成 10 步 ​ 1. 你在 msh 输入 uart3_test_on 2. uart3_test_on() 调 UART_Init() 3. UART_Init() 找到 uart3 4. UART_Init() 初始化信号量 rx_sem 5. UART_Init() 以中断方式打开串口 6. UART_Init() 注册接收回调 uart_input 7. UART_Init() 创建接收线程 serial_recv_thread_entry 8. 接收线程发现没数据于是卡在 rt_sem_take() 9. 串口来数据中断触发回调里 rt_sem_release() 10. 接收线程被唤醒读取字节存入缓冲区另一个打印线程再把数据取出打印 ​ _____________________________________________________ ​ # 你现在先只记住一句最重要的话 ​ 这份代码的核心思想不是“很多函数”而是“中断负责通知线程负责收数据业务线程负责处理数据”。 ​ 只要你抓住这条主线就不会乱。 ​ */3.3易错点空线程不能空转错误写法while (1) { ​ }这会一直占 CPU。正确写法while (1) { rt_thread_mdelay(100); }MSH_CMD_EXPORT 和 INIT_APP_EXPORT 的区别手动命令MSH_CMD_EXPORT(uart3_test_on, Test UART3);意思是注册一个 MSH 命令只有在 shell 输入uart3_test_on才会执行。自动启动INIT_APP_EXPORT(uart3_auto_start);意思是系统启动时自动执行这个函数。总结MSH_CMD_EXPORT 手动输入命令才执行 INIT_APP_EXPORT 上电自动执行为什么要加 uart3_started如果 UART3 已经上电自动启动了你又在 MSH 输入uart3_test_on可能会重复初始化串口、重复创建线程。所以加 ​ static rt_bool_t uart3_started RT_FALSE; ​ 启动前判断 ​ if (uart3_started RT_TRUE) { return RT_EOK; } ​ 启动成功后 ​ uart3_started RT_TRUE;最终执行流程上电- INIT_APP_EXPORT 调用 uart3_auto_start() - uart3_start() - UART_Init() - 打开 uart3 - 创建串口接收线程 - 创建业务处理线程 ​ 电脑发送 Led_Board1 - UART3 收到数据 - 接收线程存入缓存 - 业务线程取出命令 - 判断字符串 - 调用 user_led_on() ​ 电脑发送 Led_Board0 - 调用 user_led_off()最核心的一句话UART 模块负责“收命令”LED 模块负责“控制灯”两个模块通过 user_led.h 暴露出来的函数连接起来。3.4、效果