前面我们已经单独学了如何点灯,控制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_Board=1 则我的灯点亮,然后要是Led_Board=0灯就熄灭,是不是要现在user_led.c这个文件封装两个函数,应该是打开灯一个是关闭灯,但是这两个函数怎么给到user_uart3.c这个文件里面让他使用呢,我现在不知道这个怎么弄,请问你能懂我的意思吗
懂,你的理解是对的:先在 user_led.c 里封装"开灯/关灯"函数,再让 user_uart3.c 通过头文件调用这些函数。
核心规则就一句:
.c 文件负责实现函数,.h 文件负责把函数"声明出去",其他 .c 文件 #include 这个 .h 后就能调用。
- 为什么要加 user_led.h
比如你在 user_led.c 里写了:
cpp
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.h:
cpp
ifndef __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 调用的函数,不能加 static:
void user_led_on(void)
void user_led_off(void)
但是 LED 引脚变量应该继续保持 static:
static 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
cpp
#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_Board=1 把灯打开,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

串口的Kconfig

3.2、串口驱动代码
cpp
#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_Board=1") == 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_Board=0") == 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) ? *(argv+1) : (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_Board=1 / Led_Board=0
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易错点
空线程不能空转
错误写法:
cpp
while (1)
{
}
这会一直占 CPU。
正确写法:
cpp
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_Board=1 -> UART3 收到数据 -> 接收线程存入缓存 -> 业务线程取出命令 -> 判断字符串 -> 调用 user_led_on() 电脑发送 Led_Board=0 -> 调用 user_led_off()
最核心的一句话:
UART 模块负责"收命令",LED 模块负责"控制灯",两个模块通过 user_led.h 暴露出来的函数连接起来。
3.4、效果


