济南市网站建设_网站建设公司_加载速度优化_seo优化
2025/12/24 17:37:58 网站建设 项目流程

引言:

本篇主要讲述用STM32CubeMx初始化单片机以及FreeRtos,利用随机数模拟温湿度再通过UART异步通信串口到上位机电脑,Wifi模块会在部分2中。


STM32CubeMx中时钟、引脚、中断等初始化配置。

基础系统配置

RCC这里开启高速时钟即可,选择晶振,是时钟信号的起搏器,“心脏”。

系统模式就选择烧录模式,要不然你没法烧录到你的单片机里面去

设置UART串口通信

USART1和2是一样的,默认分别是PA9 PA10和PA2 PA3 。这里开启异步,也就是数据一位一位传出去,并且无时钟控制。

这里如果要使用中断的方式来进行数据传输的话要在NVIC的参数设置中开启USART的中断

你还可以去设置中断的优先级,数字越小代表优先级越高,我这里设置相同没毛。

最后点灯随便设置一个GPIO口即可,我这里设置了PA1

硬件连接

单片机和电脑上位机的连接我这里通过USB转TTL来实现,TX接RX RX接TX 最后共地即可,不过这里当时倒是卡了我很久,因为我发现我把USB转TTL接入到虚拟机里面,发现虽然能够识别到USB0设备口,但是就是怎么都打不开串口,在参数设置都一样的情况下,AI告诉我是因为我硬件芯片太旧或者是盗版,ubuntu系统自带的CH340驱动不支持以前的版本了,所以要去github上下载,然后解压到虚拟机即可

然后是ESP8266的连接,WIFI模块连接和USB转TTL差不多 ,TX接RX RX接TX 共地,然后VCC接3.3V。但是我发现我的ESP8266再接上单片机后ST-link直接不亮了,我第一反应是因为接错了导致短路了,但查找资料后发现是我的单片机没法提供稳定的3.3V供电电压,所以我把ESP8266的VCC接到了USB转TTL的5V接口,然电脑给他供电,这样才成功让他工作,如果你的单片机也没法稳定供电,你可以试试用外接的稳定电源。

USB能够稳定红光,ST-link稳定蓝光即可,ESP8266我看网上是说一秒一闪是表示待机状态,但不知道我是盗版还是什么,正常就啥都不显示。


逻辑代码以及任务、队列信号的创建

FreeRtos

为了实现模拟温湿度发送以及点亮LED灯并且实现WIFI传输,我这里主要设置了四个任务。整个程序执行逻辑大概是这样:优先级最高是ESP8266的初始化任务,然后是生成数据任务执行,执行后延伸500ms,通过队列将数据发给发送数据的任务,延时后执行LED灯的指令接收和执行。关于FreeRtos的学习,我是计科专业的,学过操作系统专业课,所以对于实时操作系统里面的任务、以及基于抢占优先级的任务调度制度,还有队列和信号量的设置比较熟悉,我建议想学freertos的可以直接先学操作系统,虽然比较难,但是如果学好了操作系统,接触系统的内核例如内存管理、进程通信和调度都比较好理解。

在STM32cubemx中,在freertos中选择Tasks and Queues选择通过ADD添加任务,下面是添加队列,队列是先进先出的数据结构,很适合用来作为任务之间数据的传递。

利用结构体的方式来封装数据。

// 温湿度数据结构体(用于跨文件访问) typedef struct { float temp; // 温度 float humi; // 湿度 } HumiTemp_Data_t;

模拟生成温湿度 :

void StartTask_Collect(void *argument) { /* USER CODE BEGIN StartTask_Collect */ //vTaskDelay(pdMS_TO_TICKS(1000)); //osDelay(1000); HumiTemp_Data_t data; srand(HAL_GetTick()); //随机数种子 /* Infinite loop */ for(;;) { // 生成模拟温湿度数据 data.temp = 25.0f + (rand() % 10 - 5) / 1.0f; data.humi = 60.0f + (rand() % 10 - 5) / 1.0f; // 限制数据范围 data.temp = (data.temp < 0) ? 0 : (data.temp > 50) ? 50 : data.temp; data.humi = (data.humi < 0) ? 0 : (data.humi > 100) ? 100 : data.humi; // 发送数据到消息队列 osMessageQueuePut(Queue_HumiTempHandle, &data, 0, 100); osDelay(500); } /* USER CODE END StartTask_Collect */ }

发送数据任务:

volatile关键字是用于修饰可能被意外改变的变量。其主要作用是告诉编译器不要对该变量进行优化,每次都直接从内存中读取其值。因为这个标记在任务中以及中断回调函数中都需要被调用,所以加上这个关键字可以保证变量不会被优化。

数据定义 定义在main.h中 利用extern关键字申明,并且在 mian.c里面进行一次初始化即可。

extern uint8_t uart_rx_data; // 串口接收的单字节数据 extern uint8_t uart_rx_len; // 接收长度 extern uint8_t uart_rx_flag; // 接收完成标志 extern uint8_t uart_rx_buf[]; // 接收缓冲区 volatile extern uint8_t uart1_sending; // UART1(电脑)发送标记 extern uint8_t uart2_sending; // UART2(WiFi模块)发送标记 /* 全局变量:放在所有函数外面(中断回调能访问) */ // UART2中断接收相关 extern uint8_t uart2_rx_byte ; // 临时存储单个接收字节 extern uint8_t uart2_rx_buf[]; // 接收缓冲区(足够存AT+OK+换行) extern uint32_t uart2_rx_len; // 已接收字节数 extern uint8_t uart2_rx_complete;// 接收完成标志(可选,简化判断) extern uint8_t recv_data[];//用来存想要接收的指令 extern uint8_t isConnected; //是否已开启AP服务端
void StartTask_UartSend(void *argument) { /* USER CODE BEGIN StartTask_UartSend */ HumiTemp_Data_t data; //初始化结构体 char send_buf[50] = {0}; //定义发送缓冲区 char cipsend_cmd[50] = {0}; // 专门存CIPSEND指令 /* Infinite loop */ for(;;) { // 从消息队列接收数据 osMessageQueueGet(Queue_HumiTempHandle, &data, 0, osWaitForever); // 格式化数据(huart1在main.h中声明为extern) sprintf(send_buf, "Temp:%.1f,C;Humi:%.1f,%%\r\n", data.temp, data.humi); // 中断式发送(非阻塞):等待上一次发送完成后再发 while(uart1_sending == 1) { osDelay(1); // 等待发送完成,避免数据重叠 } uart1_sending = 1; // 标记发送中 // 任务里调用时加返回值检查 HAL_StatusTypeDef ret1 = HAL_UART_Transmit_IT(&huart1, (uint8_t*)send_buf, strlen(send_buf)); if(ret1 != HAL_OK) { // 临时打印错误 HAL_UART_Transmit(&huart1, (uint8_t*)"FAILED\r\n", 8, 100); } if(isConnected == 1) { while(uart2_sending == 1) { osDelay(1); // 等待发送完成,避免数据重叠 } uart2_sending = 1; // 标记发送中 // 拼接带参数的CIPSEND指令(告诉ESP8266:给ID=0的客户端发N字节) sprintf(cipsend_cmd,"AT+CIPSEND=0,%d\r\n", strlen(send_buf)); // 第二步:发送这个指令,等ESP8266返回> if(ESP8266_SendATCmd((uint8_t*)cipsend_cmd,(uint8_t*)">", 1000) == 0) { } HAL_StatusTypeDef ret2 = HAL_UART_Transmit_IT(&huart2, (uint8_t*)send_buf, strlen(send_buf)); /*if(ret2 != HAL_OK) { // 临时打印错误 HAL_UART_Transmit(&huart2, (uint8_t*)"FAILED\r\n", 8, 100); } */ } osDelay(500); } /* USER CODE END StartTask_UartSend */ }

我本来尝试要在UART2中进行通过UART1打印调试信息,但是XCOM中会发现数据卡住了,是因为UART1 被温湿度数据的中断发送(HAL_UART_Transmit_IT)持续占用,导致后续的轮询打印(HAL_UART_Transmit)抢不到硬件资源,最终发不出去,中断的优先级是最高的,因此没法发出去,解决方法是改用中断的方式发出去。

LED控制任务:

void StartTask_LedControl(void *argument) { /* USER CODE BEGIN StartTask_LedControl */ // 初始化串口中断接收 HAL_UART_Receive_IT(&huart1, &uart_rx_data, 1); /* Infinite loop */ for(;;) { // 等待串口接收信号量 osSemaphoreAcquire(Sem_UartRxHandle, osWaitForever); // 解析指令 if (strstr((char*)uart_rx_buf, "LED_ON") != NULL) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); HAL_UART_Transmit(&huart1, (uint8_t*)"LED ON OK\r\n", 11, 100); } else if (strstr((char*)uart_rx_buf, "LED_OFF") != NULL) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); HAL_UART_Transmit(&huart1, (uint8_t*)"LED OFF OK\r\n", 12, 100); } // 清空缓冲区 memset(uart_rx_buf, 0, UART_RX_BUF_SIZE ); uart_rx_len = 0; osDelay(500); } /* USER CODE END StartTask_LedControl */ }

这里通过信号量的方式来触发循环,如果没有等到信号量超时就结束。

中断的函数设置

每次接收一个字节,有数据就会触发中断回调函数,然后把数据放入缓冲区buf里面,通过检查buf的尾部是否有\r\n来看是否结束,如果结束,就触发信号量,在刚才的任务中进行LED灯控制。在uart.c别忘了申明下。

extern osSemaphoreId_t Sem_UartRxHandle; volatile extern uint8_t uart1_sending;
//接收中断回调函数 每次接收一个字节 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { uart_rx_buf[uart_rx_len++] = uart_rx_data;//uart_rx_len++:接收长度计数器,初始值为 0,每次接收 1 字节后自增 1, //确保数据按顺序存入缓冲区(uart_rx_buf[0]存第 1 字节,uart_rx_buf[1]存第 2 字节,以此类推)。 } //检查是否接收结束 if(uart_rx_len>=2&&uart_rx_buf[uart_rx_len-1] == '\n'&& uart_rx_buf[uart_rx_len-2] == '\r' ) { osSemaphoreRelease(Sem_UartRxHandle); // 释放信号量,通知后台线程 } //手动重新启动中断接收 HAL_UART_Receive_IT(&huart1, &uart_rx_data, 1); // 只处理UART2的中断(避免和UART1打印冲突) if(huart->Instance == USART2) { // 把收到的字节存入缓冲区(防溢出) if(uart2_rx_len < UART_RX_BUF_SIZE ) // 留1字节给结束符 { uart2_rx_buf[uart2_rx_len++] = uart2_rx_byte; uart2_rx_buf[uart2_rx_len] = '\0'; // 必须加结束符,否则strstr匹配失败 } // 调试用:实时打印收到的字节(通过UART1输出) //!!这里中断回调里面不能用轮询! //Debug_Printf("zd revc:0x%02X (%c)\r\n", uart2_rx_byte, uart2_rx_byte); // 核心操作:重启中断接收(否则只能收1个字节!) HAL_UART_Receive_IT(&huart2, &uart2_rx_byte, 1); /* // 匹配响应 if(strstr((char*)uart2_rx_buf,"OK") != NULL) { uart2_rx_complete = 1; // 匹配到目标响应,置完成标志 } */ if(strstr((char*)uart2_rx_buf,(char*)recv_data) != NULL) { uart2_rx_complete = 1; // 匹配到目标响应,置完成标志 } } }

发送中断回调,只要发送数据完成,就会清空标志位,然后重启。

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { // 清除发送中标记,允许下一次发送 uart1_sending = 0; HAL_UART_Transmit(&huart1, (uint8_t*)"SUCCESS\r\n",10, 100); } else if(huart->Instance == USART2) { uart2_sending = 0; } }

主函数执行

这里定时器可以不用管,本来之前我是用定时器中断来传输数据的,现在换成了freertos任务的调用。

int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_TIM2_Init(); MX_USART1_UART_Init(); MX_USART2_UART_Init(); /* USER CODE BEGIN 2 */ // 开启定时器TIM2(定时发送数据) HAL_TIM_Base_Start_IT(&htim2); // 开启串口USART1中断接收(接收QT指令) HAL_UART_Receive_IT(&huart1, &uart_rx_data, 1); // 初始化LED为关闭状态 HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); /* USER CODE END 2 */ /* Init scheduler */ osKernelInitialize(); /* Call init function for freertos objects (in cmsis_os2.c) */ MX_FREERTOS_Init(); /* Start scheduler */ osKernelStart(); /* We should never get here as control is now taken by the scheduler */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } // 注意:osKernelStart()之后的代码永远不会执行! // 因为调度器启动后,CPU会被FreeRTOS的任务占用,main函数本身会“暂停” /* USER CODE END 3 */ }

XCOM串口显示:

通过XCOM这款软件可以查看串口发送的数据有没有成功到达我们的电脑(忽视刚开始的乱码,应该是我忘记注释了中文),这个XCOM不能打开时间戳设置,要不然他会数据一直卡在缓冲区里面没发出来!

这里帧数据设置 115200-8-1-none

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询