衡山派结合串口与LED

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_Board=1 则我的灯点亮,然后要是Led_Board=0灯就熄灭,是不是要现在user_led.c这个文件封装两个函数,应该是打开灯一个是关闭灯,但是这两个函数怎么给到user_uart3.c这个文件里面让他使用呢,我现在不知道这个怎么弄,请问你能懂我的意思吗

懂,你的理解是对的:先在 user_led.c 里封装"开灯/关灯"函数,再让 user_uart3.c 通过头文件调用这些函数。

核心规则就一句:

.c 文件负责实现函数,.h 文件负责把函数"声明出去",其他 .c 文件 #include 这个 .h 后就能调用。

  1. 为什么要加 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() 这两个函数。

  1. 为什么函数不能写成 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、效果

相关推荐
The_superstar62 天前
衡山派学习之串口
单片机·嵌入式硬件·串口·衡山派
The_superstar64 天前
衡山派D133EBS入门笔记
笔记·python·c·衡山派·小曹越
河西石头1 个月前
解决串口通讯延时的莫名错误及原理 - UI阻塞罪魁祸首
串口通讯·ui线程·c#编程·线程阻塞·应答超时
FPGA_小田老师2 个月前
Xilinx AXI UART Lite IP核:IP核深度解析
fpga开发·uart·串口通讯·axi转uart
The_superstar62 个月前
陶晶驰串口屏使用
ui·串口屏·串口通讯·ui设计·lcd显示
The_superstar64 个月前
视觉模块与STM32进行串口通讯(匠心制作)
stm32·嵌入式硬件·mongodb·计算机视觉·串口通讯·视觉模块
q_19132846954 个月前
基于Springboot+Vue.js的工业人身安全监测系统
vue.js·spring boot·后端·mysql·计算机毕业设计·串口通讯
兵哥工控5 个月前
MFC使用串口类通讯实例
c++·mfc·串口通讯·串口类
张人玉6 个月前
C# 串口通讯中 SerialPort 类的关键参数和使用方法
开发语言·c#·串口通讯