警告:本文会有大量剧透!如果你想全程靠自己钻研,请不要往下看。本文只是图一乐,记录我做测试的过程中的思考以及遇到的问题。如果你在学习的过程中总觉得很困惑,或者想看看其他人做项目时的思路,可以看看我是如何完成这个项目的。

我的基础情况:双非硕,本科计科,研究生软件工程,2024 年 6 月研究生毕业,2025 年 12 月找到勃勃,浅浅(没写过代码)接触过 STM32F103、ESP32-S3、RK3399Pro、RK3588、I.MX6ULL,会写 C、Python、Java、JavaScript/TypeScript,会用 Linux,会用 PyQT5、Electron,会用 Vue,会用 YOLO 以及使用 RKNN-toolkit 部署模型。其实会的都很浅,很杂,找不到工作,所以找勃勃提升来了,所有东西相当于重新学。研究生毕业找不到工作,被导师拉着一起创业做人工智能教学,奈何本人导师只是二把手没有话语权,工作一年半我对一把手攒满了怨气,最后选择在 2026 年 2 月春节前裸辞,开启了待业学习的日子。选择搞嵌入式是因为我希望能进入机器人行业。

学了大半年,总算来到 IAP 的测试了,勃哥说我进度有点太慢了。没办法,前几个月真的一点都不想学,每天刷 8 小时短视频,玩 8 小时游戏,吃了睡,睡醒玩,玩了吃了睡,乐不思蜀,挂壁半年。STM32 第一层从 2025 年 12 月 20 号学到 2026 年 6 月 13 号,第二层 2026 年 6 月 13 号到 7 月 8 号,FreeRTOS 是 2026 年 7 月 8 号-7 月 15 号。因为我有 PyQT 基础,用 C++ 做个 QT 的上位机没啥问题,所以我说服了勃勃让我跳过 QT 直接进入 IAP 测试。下面将记录完成 IAP 测试的过程中,我的思路、遇到的问题以及解决的办法。

初步思路

测试要求:我做的是 IAP 的简化路线,只需要支持在 boot 中用串口升级程序,在 APP 里支持通过以太网、WiFi 与上位机的通信。flash 只需要划分 3 个区:boot、运行区、存储区即可。

初步思路与计划:因为学 STM32 的时候没弄过 ETH 和 WiFi,这部分知识需要补一下。先从 WiFi 入手,简单看了一下文档,貌似只是使用串口发送一下 AT 指令就好了。那先在 RTOS 上把 WiFi 调通。后面学习一下 lwip,完成在 RTOS 上的 ETH 的通信。都弄完以后再去复习一下 MDK 相关的内容,写一个裸机程序能够支持将程序跳转到新的内存空间执行,然后编写一个能够通过串口接收二进制文件的裸机程序,并将文件存放到程序空间中。以上功能都能使用串口调试工具调通的情况下,再通过 QT 编写一个上位机软件,满足选择文件、打开文件、通过串口发送文件、收发以太网和 WiFi 数据的功能即可。

STM32 程序部分

WiFi 功能测试

简单看了一下,就是通过串口给 ESP8266 芯片发送 AT 指令就好了,应该不难。

利用开发板的 USB 转串口,直接用 PC 连接 WiFi 模块进行测试【成功了】

先试试看能不能直接用开发板上面的 USB 转串口直接接到 ESP8266 的串口引脚上,跳过 STM32 直接让 ESP8266 和 PC 通信,这样可以看看 WiFi 模组正不正常,节省中间很多步骤。

  • 查看开发板原理图,WIFI_TXD 和 WIFI_RXD 是通过跳线帽连接到 STM32 的 PB11 和 PB10 引脚。而 USB 转串口也是用跳线帽将 TXD、RXD 与 PA9 和 PA10 连接。因此理论上如果将跳线帽拿掉,用杜邦线直接飞线将 WIFI_TXD 与 RXD 连接,WIFI_RXD 与 TXD 连接,这样应该就可以直接通过 USB 将 PC 与 ESP8266 连接,通过串口调试工具使用 AT 指令了。

    IMG20260716182220

  • 测试了一下,发现不行,奇怪。再看一下原理图,看看有没有接错或者漏掉的东西。噢噢噢噢噢!原来还需要给 WIFI_EN 引脚来个高电平才能使能,开发板的 PE2 引脚连接了 WIFI_EN 引脚,PE2 引脚从来没有配置过,默认应该悬空吧,用万用表测试一下电压。

    Snipaste_2026-07-16_23-46-17

  • 破案了,现在肯定就是 WIFI_EN 引脚没有给高电平,所以 ESP8266 没有启动,有两种办法,一个给 STM32 写个程序,控制 PE2 引脚输出高电平,但是这也太麻烦了吧。还有一个,就是直接给 WIFI_EN 引脚接上 3V3 电源。懂得都懂,肯定选第二种办法嘛。

    Snipaste_2026-07-16_23-47-32

  • 66666,真的可以了,但是很奇怪,为什么发什么数据,就收到什么数据?难道是芯片有问题?还是说我的板子的 ESP8266 没有固件?

    Snipaste_2026-07-16_18-43-16

  • 看看手册吧 3. ESP8266 模块 — [野火]STM32 模块例程介绍 文档。噢噢噢噢噢!我指令发的有问题,醉了,所有 AT 指令后面要跟一个换行。AT+指令[换行] 这才是真指令啊,成功了。

    Snipaste_2026-07-16_18-48-29

让 PC 能够通过串口调试工具控制 STM32 并用上 WiFi 模块

没想到 PC 直连 WiFi 模组一下就通了,说明 WiFi 模组固件肯定是没啥问题了。那现在就考虑 STM32 与 WiFi 模组通信的问题了。把跳线帽全部插回去,让 STM32 可以和 WiFi 模块通信。目前思路是这样:让 STM32 当个中介,原封不动将 PC 发来的 AT 指令转发给 WiFi 模块,WiFi 模块返回的数据也是同样的原封不动返回给 PC,这样就能实现 PC 对 WiFi 模块的控制了,先这么干。

  • 用 STM32CubeMX 生成一个工程,先把整个 IAP 项目可能用到的引脚都配置一下。3 个 LED、2 个按键、WiFi 有 4 个、ETH 有 9 个、UART 转 USB 有 2 个,还有 NVIC 配置优先级组为 4(要用 FreeRTOS)、RCC 配置 HSE 用外部晶振。USART1、USART3,考虑到以后可能要发文件,内容有点多,把 DMA 也打开,FIFO 忘了干嘛用的,先不开了,DMA 具体怎么配置忘了,先默认吧,遇到问题再说。ETH 不会配置不管了先放着。时钟配置成 168MHz。项目管理把 IDE 配置成 MDK-ARM。这样应该就行了。靠,点到 Tools 的 CAD 里面,卡了 1 分钟!生成代码!

  • OK 代码拿到手,用 keil 打开,先编译一下,完美,没问题。

  • 现在移植 FreeRTOS,我直接用的最新版本的 FreeRTOSv202604.00-LTS,解压出来。在工程根目录下创建 FreeRTOS/Source​和 FreeRTOS/Include​两个文件夹,把 FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel​目录下的所有 .c​文件、FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\portable\MemMang\heap_4.c​、FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\portable\RVDS\ARM_CM4F\port.c​拷贝到 FreeRTOS/Source​,把 FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\include​里的所有文件文件、FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\portable\RVDS\ARM_CM4F\portmacro.h​、FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\examples\template_configuration\FreeRTOSConfig.h​文件拷贝到 FreeRTOS/Include 文件夹下。最终目录结构如下:

    +─FreeRTOS
    │  ├─Include
    │  │      atomic.h
    │  │      CMakeLists.txt
    │  │      croutine.h
    │  │      deprecated_definitions.h
    │  │      event_groups.h
    │  │      FreeRTOS.h
    │  │      FreeRTOSConfig.h
    │  │      list.h
    │  │      message_buffer.h
    │  │      mpu_prototypes.h
    │  │      mpu_syscall_numbers.h
    │  │      mpu_wrappers.h
    │  │      newlib-freertos.h
    │  │      picolibc-freertos.h
    │  │      portable.h
    │  │      portmacro.h
    │  │      projdefs.h
    │  │      queue.h
    │  │      semphr.h
    │  │      StackMacros.h
    │  │      stack_macros.h
    │  │      stdint.readme
    │  │      stream_buffer.h
    │  │      task.h
    │  │      timers.h
    │  │
    │  └─Source
    │          croutine.c
    │          event_groups.c
    │          heap_4.c
    │          list.c
    │          port.c
    │          queue.c
    │          stream_buffer.c
    │          tasks.c
    │          timers.c
    

    然后去 Keil 里面创建一个 FreeRTOS/Source​的 Group​把 FreeRTOS/Source​里的文件都加上,在魔术棒里面把 include 文件加上 FreeRTOS/Include 目录。先编译一下,不过这时候肯定要报错了,因为 freertos 还有很多东西没有配置。凭借本人 7 天入门的 freertos,我总结出了以下需要修改的内容:

    • FreeRTOSConfig.h 文件

      旧值 新值
      configCPU_CLOCK_HZ ( ( unsigned long ) 20000000 ) ( ( unsigned long ) 168000000 )
      configTICK_RATE_HZ 100 1000
      configUSE_TIME_SLICING 0 1
      configUSE_PORT_OPTIMISED_TASK_SELECTION 0 1
      configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_64_BITS TICK_TYPE_WIDTH_32_BITS
      configQUEUE_REGISTRY_SIZE 0 10
      configENABLE_BACKWARD_COMPATIBILITY 0 1
      configTOTAL_HEAP_SIZE 4096 ((size_t)(36*1024))
      configMAX_SYSCALL_INTERRUPT_PRIORITY 0 ( 5 << 4 )
      configCHECK_FOR_STACK_OVERFLOW 2 0
      configUSE_QUEUE_SETS 0 1
      xPortPendSVHandler 新增 PendSV_Handler
      vPortSVCHandler 新增 SVC_Handler
    • stm32f4xx_it.c 文件

      PendSV_Handler​和 SVC_Handler 两个函数都注释掉。

  • OK,完成了上述操作以后,编译一下,没有报错。。。怎么会没有报错呢?我靠,不对啊,怎么会没有报错啊?心里慌慌的,按理说应该还得报错才对啊。用 FreeRTOS 点个灯看看。

    • 先裸机点个灯看看 HAL 库是不是正常,创建 BSP 的 Group 并添加 bsp_led.c​和 bsp_led.h 文件,编写代码。

      // ===============================
      // bsp_led.h
      // ===============================
      #ifndef __BSP_LED_H
      #define __BSP_LED_H
      #include "stm32f4xx.h"
      #include "main.h"
      #include "stm32f4xx_hal_gpio.h"
      
      #define LED_ON GPIO_PIN_RESET
      #define LED_OFF GPIO_PIN_SET
      
      void LED_Turn_On(GPIO_TypeDef* LED_Color, uint16_t LED_Pin);
      void LED_Turn_Off(GPIO_TypeDef* LED_Color, uint16_t LED_Pin);
      void LED_Toggle(GPIO_TypeDef* LED_Color, uint16_t LED_Pin);
      
      
      #endif
      
      // ===============================
      // bsp_led.c
      // ===============================
      #include "bsp_led.h"
      
      // 点亮LED
      void LED_Turn_On(GPIO_TypeDef* LED_Color, uint16_t LED_Pin){
      	HAL_GPIO_WritePin(LED_Color, LED_Pin, LED_ON);
      }
      
      // 熄灭LED
      void LED_Turn_Off(GPIO_TypeDef* LED_Color, uint16_t LED_Pin){
      	HAL_GPIO_WritePin(LED_Color, LED_Pin, LED_OFF);
      }
      
      // 翻转LED状态
      void LED_Toggle(GPIO_TypeDef* LED_Color, uint16_t LED_Pin){
      	HAL_GPIO_TogglePin(LED_Color, LED_Pin);
      }
      
      // ===============================
      // main.c
      // ===============================
      #include "bsp_led.h"
      ...中间内容省略
        while (1)
        {
          /* USER CODE END WHILE */
      		LED_Toggle(LED_R_GPIO_Port, LED_R_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      		LED_Toggle(LED_R_GPIO_Port, LED_R_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      		LED_Toggle(LED_G_GPIO_Port, LED_G_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      		LED_Toggle(LED_G_GPIO_Port, LED_G_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      		LED_Toggle(LED_B_GPIO_Port, LED_B_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      		LED_Toggle(LED_B_GPIO_Port, LED_B_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
          /* USER CODE BEGIN 3 */
        }
      ...剩余内容省略
      

      编译一把过,烧录看看!尴尬啊,烧录烧录不了,报错 No ST-LINK detected

      Snipaste_2026-07-16_22-18-11

      这个问题好解决,就是魔术棒里面 Debug​下的 Debugger​换成 CMSIS-DAP Debugger​,然后 Settings​里面 CMSIS-DAP-JTAG/SW Adapter​选择 Fire CMSIS-DAP​即可。烧录!奇怪,LED 灯怎么不亮?不可能啊?复位一下看看。噢噢噢噢可以了,原来如此啊,我没有配置 Reset and Run​,去魔术棒里面的 Debug->右上settings->Flash Download​下把 Reset and Run 勾选就好了。

    • 既然裸机没问题,那试试用 freertos 运行这个任务看看行不行。修改一下代码。我靠,有点忘了 FreeRTOS 怎么写了。。。回忆一下,反正就是用 xTaskCreate​创建一个任务,然后启动调度器就行了吧,启动调度器的函数我也忘了,看一下 tasks.c​文件,搜索一下 schedule​。。。有点大海捞针啊,记得好像是什么 startScheduler​,搜这个看看。找到了,应该就是这个函数 vTaskStartScheduler​启动调度器,看一下代码.........xPortStartScheduler.......那包不会有错了,就是这个。OK,准备好了,创建一下任务。。。为什么我 xTaskCreate​函数没有代码提示啊?我明明引入了 task.h​了,奇怪啊?task.h​里面明明声明了 xTaskCreate​了,为啥啊?难道是没有包含 FreeRTOS.h​?不应该把,这应该没影响吧?引入一下试试...我靠,还真是!666666 什么奇奇怪怪的问题。。。任务名称“LED_Red_Blink_Task”。。名字会不会有点长啊?18 个字母。。这算几个字节啊?最大任务长度是 16。。这 16 是 bit 还是 byte 还是 word 啊。。注释是说 in characters,那按字母算啊?那 18 字母超了吧?真超了吗?看看代码里面怎么处理这个。。。看到了 for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )​OKOK,感情我给的名称超出长度的部分会被自动裁剪掉,而且没有任何报错,那行那我就要用这么长的任务名字。不过还是有风险,以后尽量不写这么长。。。任务优先级 emmm 现在默认是 5 个优先级,是从 0 开始算还是 1 啊?看一下代码,搜索一下 configMAX_PRIORITIES​,找到了 for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )​,好家伙,从 0 开始是吧。OK,那我给创建任务的任务最高优先级 0 吧。。。唉有点怀疑啊,还是翻一下野火的文档吧。还真是,弄错了,优先级数字越大优先级越高,跟中断服务号是相反的,行,记住了,然后因为从 0 开始,所以最高的优先级应该是 configMAX_PRIORITIES - 1

      总算写好了。搞个两个文件专门放自己创建的任务有关的代码,user_task.c​和 user_task.h

      // ===============================
      // user_task.h
      // ===============================
      #ifndef __USER_TASK_H
      #define __USER_TASK_H
      
      void User_Start_Tasks(void);
      void APP_Create_Task(void);
      void LED_Red_Blink_Task(void);
      
      #endif
      
      // ===============================
      // user_task.c
      // ===============================
      #include "bsp_led.h"
      #include "user_task.h"
      #include "FreeRTOS.h"
      #include "task.h"
      
      TaskHandle_t APP_Create_Task_Handler = NULL;
      TaskHandle_t LED_Red_Blink_Task_Handler = NULL;
      
      void User_Start_Tasks(){
      	BaseType_t xReturn;
      	xReturn = xTaskCreate(
      							(TaskFunction_t) APP_Create_Task,
      							"APP_Create_Task",
      							configMINIMAL_STACK_SIZE,
      							NULL,
      							4,
      							&APP_Create_Task_Handler);
      
      	vTaskStartScheduler();
      }
      
      static void APP_Create_Task(){
      	BaseType_t xReturn;
      	xReturn = xTaskCreate(
      							(TaskFunction_t) LED_Red_Blink_Task,
      							"LED_Red_Blink_Task",
      							configMINIMAL_STACK_SIZE,
      							NULL,
      							2,
      							&LED_Red_Blink_Task_Handler);
      
      	vTaskDelete(APP_Create_Task_Handler);
      }
      
      static void LED_Red_Blink_Task(){
      	uint32_t i = 0;
      	while(1){
      		LED_Toggle(LED_R_GPIO_Port, LED_R_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      		LED_Toggle(LED_R_GPIO_Port, LED_R_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      		LED_Toggle(LED_G_GPIO_Port, LED_G_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      		LED_Toggle(LED_G_GPIO_Port, LED_G_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      		LED_Toggle(LED_B_GPIO_Port, LED_B_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      		LED_Toggle(LED_B_GPIO_Port, LED_B_Pin);
      		for(i = 0; i < 0x7FFFFF; i++);
      	}
      }
      
      // ===============================
      // main.c	做了一点点修改,部分内容省略。
      // 我靠,我回头一看发现怎么写到while里面了,这个要写在while外面,不过没影响,有点抽象了
      // ===============================
        while (1)
        {
          /* USER CODE END WHILE */
      		User_Start_Tasks();
          /* USER CODE BEGIN 3 */
        }
      

      编译一下看看。emmm 有警告 ..\Core\Src\user_task.c(10): warning: #550-D: variable "xReturn" was set but never used,拉倒吧,我就要定义了不用。。不管了烧录一下看看。。。又报错?!

      OKOK 我知道了,刚刚灯太闪了板子电源被我关了。重新上电烧录看一下。我擦,怎么不行?emmmm,噢噢噢噢,启动任务调度器写错地方了,改一下。完美!!!LED 一样在闪哈哈哈哈。那我就纳闷了,那我这 FreeRTOS 移植没问题啊?这次移植就这么顺利吗?算了,能用就行。

  • 忘了要做什么了,看一下前面写的内容。。。噢噢噢,这次是要能让 STM32 成为一个中介啊。那行,那我先把 STM32 和 PC 的通信调通吧。这怎么说?有手就行吧。

    • emmm 其实 HAL 库我不熟,看看 stm32f4xx_hal_uart.h 文件里面都定义了什么函数吧。反正就是一个调包侠,别人有什么我就用什么。

    • 懂了,那我就是用 HAL_UART_Transmit​吧,简单。不过我开了 DMA 啊,得用带 _DMA 的函数吧?DMA 怎么用啊。。。STM32CubeMX 应该都有帮我准备好吧?看一下代码。。还真是啊,等等,怎么有这么多中断?

    • 懂了,就是 DMA 满了就中断呗,那我就是发现有中断以后快进快出,然后给任务发消息应该就能拿到串口发送的数据了。看看中断服务函数是不是都写好了。

    • 果真啊,太爱 STM32CubeMX 了,我看看,emmm 怎么中间都有一个 HAL_DMA_IRQHandler​或者 HAL_UART_IRQHandler​。。不会是我想的那样吧?看看源码。。。啊?不会吧?这是。。。我记得标准库里面没有这个待遇啊。。。让我试试 HAL_UART_Transmit_DMA 先。随便先搞个任务创建一下

      // ===============================
      // user_task.c
      // ===============================
      static void UART1_Test_Task(){
      	while(1){
      		UART1_Test();
      		vTaskDelay(500);
      	}
      }
      static void APP_Create_Task(){
      	BaseType_t xReturn;
      	xReturn = xTaskCreate(
      							(TaskFunction_t) LED_Red_Blink_Task,
      							"LED_Red_Blink_Task",
      							configMINIMAL_STACK_SIZE,
      							NULL,
      							2,
      							&LED_Red_Blink_Task_Handler);
      
      	xReturn = xTaskCreate(
      							(TaskFunction_t) UART1_Test_Task,
      							"UART1_Test_Task",
      							configMINIMAL_STACK_SIZE,
      							NULL,
      							2,
      							&UART1_Test_Task_Handler);
      
      	vTaskDelete(APP_Create_Task_Handler);
      }
      
      // ===============================
      // bsp_uart.c
      // ===============================
      #include "bsp_uart.h"
      
      extern UART_HandleTypeDef huart1;
      extern UART_HandleTypeDef huart3;
      extern DMA_HandleTypeDef hdma_usart1_tx;
      extern DMA_HandleTypeDef hdma_usart1_rx;
      extern DMA_HandleTypeDef hdma_usart3_rx;
      extern DMA_HandleTypeDef hdma_usart3_tx;
      
      void UART1_Test(){
      	HAL_UART_Transmit_DMA(&huart1, (uint8_t *) "Hello World.\n", 12);
      }
      
    • 编译烧录运行!emmm 灯闪的很勤。。但是没有串口内容输出。。难道这玩意儿不是这样用的?调试一下,断点直接放在 UART1_Test()​上。。。奇怪,不会走到断点的位置。。。难道任务一直在 LED_Red_Blink_Task​任务里面?这个 freertos 不是说一个任务分配一点时间片吗?两个任务优先级是一样的啊,不得来回切换吗。。。把 UART1_Test_Task​优先级调高一级看看情况。。。我靠真是啊,会输出一次,然后就在 LED 的任务死循环了。。。我是已经把 FreeRTOSConfig.h​文件里面的 configUSE_PREEMPTION​配置的是 1 啊,抢占式的啊。然后 configUSE_TIME_SLICING​配置成 1 了,所以说这个时间片到底是啥意思?看看手册。。。奇怪,写的确实是相同优先级的任务会轮流运行啊。。。总不能说因为我 UART1_Test_Task​任务调用了 vTaskDelay​进入了阻塞列表了?然后一直没机会进入就绪列表???再捋一下思路,这个 freertos 是会配置 SysTick 的,让每个 Tick 都来一次中断,然后进入 SysTick_Handler​函数,在这个函数中去重新给判断应该给哪个任务运行。。。所以说现在应该看看 SysTick_Handler 在干嘛。。。

    • 我靠!我就说 FreeRTOS 没配置完嘛,原来 BUG 在这里!!!SysTick 在溢出的时候确实会来到 SysTick_Handler​的中断处理函数中,但是这个函数没有去调用 port.c​里面的 xPortSysTickHandler​函数!没有 xPortSysTickHandler​就不会进入 PendSV​的中断,所以就不会跳转到 vTaskSwitchContext​里面去切换任务!!!终于破案了!那把 xPortSysTickHandler​塞到 SysTick_Handler 里面吧!!

    • 这个红色波浪线很影响观感啊妈的,这个函数明明就是定义了,非得在这报警。。。在上面加一个 extern void xPortSysTickHandler( void ); 试试。。。我去真的可以消掉!舒服了。。。编译!烧录!!成功了!!就是发送的文本长度算错了,应该是 13 个字符,写成 12 了。给他改成 13 就行了。牛逼牛逼!

    • 现在既然能发了,那能不能收呢?收要怎么弄?再搞个相同优先级的任务来,然后也是轮流看接收数据?先随便试试这个 HAL_UART_Receive_DMA 函数吧。。。这个函数怎么要我提前给要接收多少的 size。。。这我哪知道啊。。不管了先随便给个数字 12,然后看看怎么个情况吧。。。有点抽象了兄弟们。没弄懂这个接收数据怎么搞的,而且这个 freertos 搞的调试起来挺不方便的啊。这样吧,先给这个输出日志的功能做好,用 freertos 的任务通知的功能,搞个任务专门用来输出日志,优先级高一些,一旦接收到打印输出日志的任务通知,就马上执行输出的任务。先这么干,磨刀不误砍柴工。

      • 。。。。。。有时候挺无语的,就是会突然想搞一些没意义的事。。现在好了,花了 3 个小时搞了个没啥用的日志任务。。可以根据配置的全局变量的参数,通过串口发送一般、警告、错误三个等级的日志。。。确实没啥意义。唉,写代码就经常时不时跑去搞一些奇奇怪怪的东西。不过有了这东西可以保证日志输出肯定没问题了。

        // ===============================
        //bsp_console.h
        // ===============================
        #ifndef __BSP_CONSOLE_H
        #define __BSP_CONSOLE_H
        #include "stm32f4xx.h"
        #include "main.h"
        #include "stm32f4xx_hal_uart.h"
        
        void Console_Log(uint8_t *pdata, char* head);
        void Console_Info(uint8_t *pdata);
        void Console_Warning(uint8_t *pdata);
        void Console_Error(uint8_t *pdata);
        
        #endif
        
        // ===============================
        // bsp_console.c
        // ===============================
        #include "bsp_console.h"
        
        extern UART_HandleTypeDef huart1;
        extern UART_HandleTypeDef huart3;
        extern DMA_HandleTypeDef hdma_usart1_tx;
        extern DMA_HandleTypeDef hdma_usart1_rx;
        extern DMA_HandleTypeDef hdma_usart3_rx;
        extern DMA_HandleTypeDef hdma_usart3_tx;
        
        void Console_Info(uint8_t *pdata){
        	Console_Log(pdata, "[INFO] ");
        }
        
        void Console_Warning(uint8_t *pdata){
        	Console_Log(pdata, "[WARNING] ");
        }
        
        void Console_Error(uint8_t *pdata){
        	Console_Log(pdata, "[ERROR] ");
        }
        
        void Console_Log(uint8_t *pdata, char* head){
        	uint8_t phead = 0;
        	uint8_t ppdata = 0;
        	char tmp_buffer[0xff];
        	while(phead < 0xff && *(head + phead) != '\0'){
        		tmp_buffer[phead] = *(head + phead);
        		phead += 1;
        	}
        	while(phead < 0xff && *(pdata + ppdata) != '\0'){
        		tmp_buffer[phead] = *(pdata + ppdata);
        		ppdata++;
        		phead++;
        	}
        	HAL_UART_Transmit_DMA(&huart1, (uint8_t *)tmp_buffer, phead);
        }
        
        // ===============================
        // user_task.c
        // ===============================
        // 宏定义
        #define CONSOLE_LEVEL_INFO (0x01 << 0)
        #define CONSOLE_LEVEL_WARNING (0x01 << 1)
        #define CONSOLE_LEVEL_ERROR (0x01 << 2)
        // ... 
        // 日志输出等级
        uint32_t Console_Level_Param = CONSOLE_LEVEL_INFO;
        // ... 
        // 创建任务
        xReturn = xTaskCreate(
        						(TaskFunction_t) Console_Log_Task,
        						"Console_Log_Task",
        						configMINIMAL_STACK_SIZE,
        						&Console_Level_Param,
        						4,
        						&Console_Log_Task_Handler);
        // ... 
        // 任务入口函数
        static void Console_Log_Task(uint32_t* CONSOLE_LEVEL){
        	BaseType_t xReturn = pdTRUE;
        	char *r_char;
        	while(1){
        		xReturn = xTaskNotifyWait(0x00000000, 0xFFFFFFFF, (uint32_t *)&r_char, portMAX_DELAY);
        		if(xReturn == pdTRUE){
        			if((*CONSOLE_LEVEL & CONSOLE_LEVEL_INFO) == CONSOLE_LEVEL_INFO){
        				Console_Info((uint8_t *)r_char);
        			}else if((*CONSOLE_LEVEL & CONSOLE_LEVEL_WARNING) == CONSOLE_LEVEL_WARNING){
        				Console_Warning((uint8_t *)r_char);
        			}else{
        				Console_Error((uint8_t *)r_char);
        			}
        		}
        	}
        }
        
        // 使用方式:在其他任务中调用xTaskNotify函数,可以向日志任务发送字符串,最多不要超过245个字符。
        // char *pstr = "Hello World.\n";
        // xTaskNotify(Console_Log_Task_Handler, (uint32_t)pstr, eSetValueWithOverwrite);
        
    • OK 日志输出的任务完成了,刀磨好了,下面还得实现串口输入的功能啊。这个接收我其实不是很能理解啊,首先什么时候可以接收?肯定是上位机发送消息的时候才去接收吧。那就是需要有一个任务去检查串口有没有接收到数据。如果只看寄存器那就是看 USART_SR 寄存器的 RXNE 位的值。可是在 HAL 库中要怎么看呢?加上了 DMA 又怎么看?既然配置了 DMA,那么就当接收到数据以后 DMA 自动就会帮我先暂存一下数据吧。那我就是在每次 DMA 满了的时候去取一下数据就好了吧。

    • 我靠见鬼了,想要自己摸索 HAL 库的串口接收函数到底怎么用,摸索一晚上了,还是没弄明白,眼看着天快亮了。。。先睡觉吧。。。

    • 好了,睡了 5 个小时,醒来是中午 12 点了,没啥胃口,继续研究这个 HAL 库的串口吧。这回还是不头铁了。直接百度查一下 HAL 库的串口接收怎么用吧(参考了这篇文章 STM32HAL 库---串口 DMA 发送接收详解_stm32 串口 dma 接收-CSDN 博客)。。。emmm 把 HAL_UART_Receive_DMA​换成 HAL_UARTEx_ReceiveToIdle_DMA 就能用了。就这问题困扰老夫这么久?这说明什么?说明基础薄弱,核心知识掌握得较差,还是重新捋一下使用 DMA 接收串口信号的流程吧。。再回顾一下串口接收数据和怎么结合 DMA 使用:

      • 串口接收数据:裸机并且不用库实现串口接收的时候,主要就是看 USART_SR 寄存器的 RXNE 位,要么轮询要么中断,而我们根本不用考虑这个数据是如何到 USART_DR,只要无脑等 RXNE 就行。等到了 RXNE 以后,只要从 DR 寄存器读取数据,RXNE 就会自动被硬件清空。因此手写寄存器的时候读数据只有两步:等 RXNE,读 DR。

      • 使用 DMA 搬运串口数据:加入了 DMA 以后,那就是连等 RXNE 和读 DR 都省了。把 DR 在哪、数据放哪告诉 DMA,其他就不用管了。接下来要么轮询 DMA 的 TCIFx 要么等 DMA 通知。那使用 DMA 就变成了等 DMA 消息了啊。。。这我就纳闷了,没啥毛病啊。那 HAL 库到底是怎么实现的 HAL_UART_Receive_DMA​功能啊??不对,前面说等 DMA 标志位就好了,那如果等到了标志位以后,我是不是得手动清除这个标志位啊?不会吧?看一下 UART 或者 DMA 代码里面有没有清除 TCIFx 标志位的函数。。。看了 HAL 库提供的中断服务函数,里面会清除中断标志位啊。。。那这就怪了,是我代码有问题吗??重新调整一下代码。。。还是用 HAL_UART_Receive_DMA 来试试。。

        static void Console_Read_Task(){
        	HAL_StatusTypeDef sTatus;
        	char rData[128] = {0};
        	char *error;
        	while(1){
        		if(HAL_UART_GetState(&huart1) == HAL_UART_STATE_READY){
        			Console_Level_Param = CONSOLE_LEVEL_ERROR;
        			// sTatus = HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rData, 128);
        			sTatus = HAL_UART_Receive_DMA(&huart1, (uint8_t*)rData, 128);
        			if(sTatus == HAL_OK){
        				Console_Level_Param = CONSOLE_LEVEL_INFO;
        				xTaskNotify(Console_Log_Task_Handler, (uint32_t)rData, eSetValueWithOverwrite);
        			}else if(sTatus == HAL_ERROR){
        				error = "HAL_ERROR\n";
        				xTaskNotify(Console_Log_Task_Handler, (uint32_t)error, eSetValueWithOverwrite);
        			}else if(sTatus == HAL_BUSY){
        				error = "HAL BUSY\n";
        				xTaskNotify(Console_Log_Task_Handler, (uint32_t)error, eSetValueWithOverwrite);
        			}else if(sTatus == HAL_TIMEOUT){
        				error = "HAL TIMEOUT\n";
        				xTaskNotify(Console_Log_Task_Handler, (uint32_t)error, eSetValueWithOverwrite);
        			}else{
        				error = "未知错误\n";
        				xTaskNotify(Console_Log_Task_Handler, (uint32_t)error, eSetValueWithOverwrite);
        			}
        		}else{
        			vTaskDelay(50);
        		}
        	}
        }
        

        测试一下。。这就怪了,现在能用了。。难道是因为我之前是吧 vTaskDelay(50); 写在 if 外面?改一下试试。。。还是能用啊?我靠为啥啊?把外面一层 if 扒掉。。

        调试结果虽然很抽象,得发到足够数量的数据才会显示,但是起码还是能接收到数据啊。。。(突然反应过来)难道是因为 char rData[128] = {0};​这个?我一开始初始化变量用的是 char *rData;​或者 char *rData = "Hello";​这样的。。莫非真是因为这个,导致提供给 DMA 的内存存储器地址有问题?改成 char *rData = "Hello";​试试。。还是能用啊?只不过输出的都是 Hello​,但是我只要有发够内容就会返回啊。。见鬼了啊,睡一觉醒来。。还有一个可能,我在另一个任务中也是不停调用 Console_Log_Task 这个任务,会一只占用 DMA,但是这个代码上午被我删了,我加回去试试。。。还是能用。。。啊啊啊啊啊不管了,现在能够怀疑的可能就是我之前测试的时候给板子发送的数据没够,所以一直都没显示。。。找不到问题了,不管了,能用就行。。。太抽象了真的。不过现在既然串口正常可以用了,把代码整理一下,顺便完成对 WiFi 的控制吧

    • 优化了一下接收上位机发送数据的任务,现在功能简单,就是判断是不是有接收到数据,接收到了就原封不动返回给 PC 就好了。

      static void Console_Read_Task(){
      	HAL_StatusTypeDef sTatus;
      	char rx_buffer[128] = {0};
      	char *error_msg;
      	while(1){
      		sTatus = HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t*)rx_buffer, 128);
      		if(sTatus == HAL_OK){
      			Console_Level_Param = CONSOLE_LEVEL_INFO;
      			xTaskNotify(Console_Log_Task_Handler, (uint32_t)rx_buffer, eSetValueWithOverwrite);
      		}else{
      			vTaskDelay(100);
      		}
      	}
      }
      
  • 可算是把串口读取的任务实现了,那现在就是判断上位机发过来的到底是什么玩意了,根据上位机发送的不同的指令来执行不同的操作。我准备这样规定一下,因为功能不多只要能给 WIFI 发信号就行,那就判断如果接收到上位机发送的字符串的前 2 位是不是 AT,如果是那就无脑把收到的指令转发给 WiFi 模块,不是就返回 无效指令。开干!

    • 先吃个饭。。。吃完饭有点犯困,躺了一会儿没睡着,起来继续干了。。。

    • 改造 Console_Read_Task 任务,使其能够识别 AT 指令并执行转发。如果不是 AT 指令就先原封不动输出吧,这个以后也可以改造成别的指令。这部分代码可以写好一些,后期裸机写 boot 程序的时候也能直接复制过去用。。。。

    • emmmm 代码写着写着写偏了,跑去搞一些奇奇怪怪的事。。。先重新规划一下。按照最初心里想的架构,freertos 任务不应该直接去使用 HAL 库,应该通过使用 bsp 里提供的代码来调用串口输出功能的(我自己规定的)。那现在这个任务就不符合我心中的规范了,应该先重构一下。那重构一下就感觉 Console_Read​名字有点不够屌不够精准啊,准备改成 Console_Wait_Command​,这样指向性明确,很清楚就是等待控制台指令的意思。。。。唉感觉这个名字也不太对,等待的操作应该是由任务来执行的吧,和这个没关系,还是叫做 Console_Get_Command 吧。。。

      一通操作可算是写好代码了,烧录一下试试。。。

      怎么乱码啊??? 噢噢噢应该是编码格式问题,代码文件是 GB2312 格式,里面的中文也是 GB2312 编码的,而野火串口调试助手的接收区编码格式现在在 UTF-8,所以中文显示有问题。右上角设置->编码设置改成GB18030​就好了。。。。emm 那乱发指令显示未知是正常的,可是我发个 AT 出去,给我返回 AT 是没错,可是怎么后面还跟着 456789012345678​???啥毛病啊??不过这个感觉一看就是发送的数据缓冲区有脏数据,给他改改。。。不对,发送缓冲区的数据是我直接给的接收缓冲区的地址,那就是接收有问题啊。。。可是接收的代码接收完字符串肯定是有 '\0'​才对啊,这不应该是接收有问题啊。。。噢噢噢噢噢噢!!发送的代码的时候计算字符串长度的时候出错了!因为是用 while(xxx != '\0')​来判断是不是到字符串的尾部,完了就跳出循环,长度忘记算上 '\0'​了。。。。不对。。。还是有问题。。那还是接收缓冲区有问题啊,真就奇了怪了。。。。噢噢噢噢噢,因为发消息的时候我会拼上一个头部,所以申请了一个新的地址来装拼接好的消息,而这个拼好的消息没有用 '\0' 收尾!破案了。。。吗??还是有问题啊!!!!直接 DEBUG 模式吧,我真受不了了。。。

    • 破案了!实锤就是接收的函数有问题了!接收完成以后,并没有这堆字符串加上 '\0'​作为结尾,而是直接把我发送的内容直接写到缓冲区了,还写了 \r\n 难绷啊,那这个算 1 个还是 2 个字符啊?

      那找到问题了,该怎么修复啊?我哪知道要接收多长的数据啊?为了保证接收到的数据是原汁原味的,我决定还是在 Console_Readlines 函数内部创建一个新的缓冲区来存数据,完事来个偷梁换柱,把传进来的指针的地址改成新地址。试试看。。。我靠怎么内容全变成 0 了?我靠我数据呢???。。。。

    • 崩溃了,打断点一个一个去试吧。。。。。。哎,现在的问题好像是我通过串口发送数据,并不会发个 '\0'​给 STM32 啊,所以 DMA 也不会往内存空间里面写 '\0'​,因此我在打印输出的时候没有办通过判断是不是到了 '\0' 来结束输出。。。问了一下豆包,还真是。。事到如今。。还是看看豆包怎么建议吧。

    • 有了豆师傅的指点,现在简直醍醐灌顶啊,豆包建议我用 HAL_UARTEx_RxEventCallback 去判断接收到数据的长度。我看行,就这么办。

      唉,吭哧吭哧干了两三个小时,可算是弄好了,有点无语了,一个串口调试了 2 天,总算写出了让人满意的代码了。现在基本上可以保证 PC 和 STM32 正确通信了。已经晚上 11 点半了,先睡觉。明天去玩,后天再做了。

    • 昨天 18 号玩了一天。。今天激情褪去,有点不想干了。害,不想挂逼了,先硬着头皮继续整吧。。目前进度是串口和 PC 通信是正常了,那可以把同样一套代码用到 UART3 上,这样就可以让 STM32 和 WIFI 模块通信了。先 CV 一下。同样还是创建两个任务,一个任务用于发送 WiFi 指令,另一个任务用于接收 WiFi 发送的消息,同时为了支持这两个任务,给 BSP 也配置上 WiFi 功能。STM32 接收到上位机发过来的命令,解析后发现是 AT 指令就直接发给 WiFi 模块并等待返回结果。

      //bsp_wifi.c
      #include "bsp_wifi.h"
      
      extern UART_HandleTypeDef huart3;
      
      uint8_t* WIFI_Send_Command(uint8_t *pdata, uint16_t size);
      static uint8_t* WIFI_Receive_Message(void);
      
      uint8_t wifi_rx_buffer[WIFI_MAX_BUFFER_SIZE] = {0};
      
      // WIFI指令发送,向WiFi模块发送指令,目前最大仅支持接收256个字符,即512字节。
      uint8_t* WIFI_Send_Command(uint8_t *pdata, uint16_t size){
      	HAL_UART_Transmit_DMA(&huart3, (uint8_t *)pdata, size);
      	return WIFI_Receive_Message();
      }
      
      // WiFi指令接收,接收WiFi模块发回的数据
      static uint8_t* WIFI_Receive_Message(){
      
      	HAL_StatusTypeDef sTatus = HAL_UARTEx_ReceiveToIdle_DMA(&huart3, (uint8_t*)wifi_rx_buffer, WIFI_MAX_BUFFER_SIZE);
      	if(sTatus == HAL_OK){
      		return wifi_rx_buffer;
      	}else{
      		return NULL;
      	}
      }
      
      
      // 修改了user_task.c
      static void Console_Get_Command_Task(){
      	Console_Command_Type cmdType;		// 收到的指令类型
      	char *msg;										// 通过控制台输出的消息
      	while(1){
      		cmdType = Console_Get_Command();		// 调用
      		if(cmdType == Console_Command_WiFi){
      			// 检测到接收的是WiFi指令的任务,那就发送WiFi指令给WiFi模块并等待结果。
      			msg = (char *)WIFI_Send_Command(console_rx_buffer, console_rx_buf_size);
      			if(msg != NULL){
      				xTaskNotify(Console_Log_Task_Handler, (uint32_t)msg, eSetValueWithOverwrite);
      			}else{
      				Console_Log_Task_Param.Console_Level = CONSOLE_LEVEL_ERROR;
      				msg = "WiFi通信异常,接收数据为空!\n";
      				xTaskNotify(Console_Log_Task_Handler, (uint32_t)msg, eSetValueWithOverwrite);
      			}
      		}else if(cmdType == Console_Command_Unknown){
      			Console_Log_Task_Param.Console_Level = CONSOLE_LEVEL_WARNING;
      			msg = "未知指令\n";
      			xTaskNotify(Console_Log_Task_Handler, (uint32_t)msg, eSetValueWithOverwrite);
      		}
      		vTaskDelay(100);	// 100ms应该不会太久吧,反正也不是电竞应该不需要高响应。。。
      	}
      }
      

      烧录测试一下,发现不行。。。emmm 噢噢噢噢忘记给 WiFi 初始化了,应该是要给 WiFi 先使能才对,在 BSP 里面加个一个 wifi_init 的函数并在 main 刚开始的时候调用。。。

      还是不行。。。怪了。。开 debug 瞧瞧。。。怪了,指令都正常发送并且数据都正确,但是没收到任何内容。。。这又是哪里出问题了呢?给 WiFi 的 EN 引脚测一下电压看看,3.3V 左右。。。怪了。。对了,WiFi 模块还有一个 RST 也测试一下。。PG15。。怎么是低电压?看看 WiFi 模组的手册。。低电压有效。。我靠那 WiFi 模组是处于复位状态啊,这不行,耶得给他使能了。。。哈哈哈哈完美,成功了...不对,高兴太早了,怎么第一次收到 WiFi 模组返回的消息是空的啊?怎么上一次的指令这一次才返回给我啊?。。怎么。。噢噢噢噢噢,应该是忘记去回调函数里面配置串口 3 返回的数据内容的长度了,嗯,修改一下。。。我靠我拿到了长度以后,怎么好像在别处的代码都没有用啊?感觉代码变成一坨狗屎了。。。

      经过了一番修改,输出的文本长度混乱的 bug 解决了,,现在还有接收的指令是上一次发送的返回。。这个问题应该是要加个等待来解决,我目前判断 DMA 完成接收的方式是判断 HAL_UARTEx_ReceiveToIdle_DMA​是不是给我返回了 HAL_OK​,但实际上这个 HAL_OK 具体可能在哪些情况下出现,这不太清楚。要么问豆包要么看源码。。我觉得还是问问豆包吧。。果然,豆包说 HAL_UARTEx_ReceiveToIdle_DMA​返回 HAL_OK 只是说明这个函数调用成功,不代表 DMA 接收到有效数据。不过其实我也知道有这个问题,一直装作没看见,因为前面测试的时候已经发现了,如果发送的内容长度超出定义接收的长度就会触发中断,我没有进行任何处理,接收的数据会被截断,我就是偷懒不想改一直无视罢了。。。现在既然问题已经摆在眼前,那也只好先解决了吧。。。。。。。。。。。。

      我感觉我好像还是对 HAL_UARTEx_ReceiveToIdle_DMA​函数的理解不够深刻,所以现在会遇到这么多问题。。。OK,重新研究了一下 HAL_UARTEx_ReceiveToIdle_DMA​函数的逻辑:这个函数第一次调用的时候,就会切换到等待串口接收数据的模式,只要正确切换模式,就会返回 HAL_OK​,但是这个时候其实可能还没有接收到数据,只有在 HAL_UARTEx_RxEventCallback​回调函数被触发的时候,通过判断当前是空闲、溢出还是半满的状态,这时候才知道到底是什么情况。而如果一直没有接收到数据,那么后续 HAL_UARTEx_ReceiveToIdle_DMA​被调用都会返回一个 HAL_BUSY 的值。。。总之我原有的判断逻辑实际上有很严重的错误,现在推倒重来,从最简单的开始:

      // user_hal_callback.c
      // 接收数据完成的回调函数
      void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
      	// 接受完的回调函数,判断是空闲
      	HAL_UART_RxEventTypeTypeDef rxStatue = HAL_UARTEx_GetRxEventType(huart);
      	if(rxStatue == HAL_UART_RXEVENT_IDLE){
      		if(Size > 0){
      			if(huart == &huart1){
      				console_rx_buf_size = Size;
      				console_rx_status = 1;
      			}else if(huart == &huart3){
      				wifi_rx_buf_size = Size;
      				wifi_rx_status = 1;
      			}
      		}
      	}else if(rxStatue == HAL_UART_RXEVENT_TC){		// 如果是接收溢出了的处理方案
      
      	}else if(rxStatue == HAL_UART_RXEVENT_HT){		// 如果是半满了的处理办法
      
      	}
      }
      
      // user_task.c
      static void Test_Uart_Task(){
      	HAL_StatusTypeDef rx_state;
      	HAL_StatusTypeDef tx_state;
      	while(1){
      		rx_state = HAL_UARTEx_ReceiveToIdle_DMA(&huart1, console_rx_buffer, CONSOLE_MAX_BUFFER_SIZE);
      			while(console_rx_status == 0){
      			vTaskDelay(10);
      		}
      		console_rx_status = 0;
      		tx_state = HAL_UART_Transmit_DMA(&huart1, console_rx_buffer, console_rx_buf_size);
      	}
      }
      

      这个代码可以说是相当的简单了,刨去了所有毫无意义的判断,数据的就是就是等待接收到数据并来到空闲的状态。这次代码经过了测试已经完全没有任何问题了!代码还是应该要简单,现在就把这个测试的代码替换到原代码中。

    • 试来试去还是不得劲呐,突然想干脆放弃这堆狗屎代码重构一下吧,因为突然发现自己又犯了以前常常会犯的错误,就是学了点什么东西就硬要全部都用上,不用就感觉有点亏了。。比如我真的有必要去专门为串口输出创建一个任务,并通过通知的方式来输出吗?似乎真的毫无必要,在需要输出的时候输出就好了吧。。。这狗屎代码就是让人不舒服,还是重构一下吧,毕竟现在相比三天前还是提升了不少,,应该能写出比三天前更好的代码吧。。先把狗屎代码都清除掉,重新规划好代码应该怎么写。开始优化代码!

      功夫不负有心人啊。。在简化了代码以后,现在是彻底能用了,笑死我了。每次写代码都会犯同样的错误,就是为了“炫技”去写一些复杂代码,最终都会因为自己太菜了被自己坑了。。以后还是不要这样了,代码能正常使用才是真理!!

      // 现在只保留LED闪烁和控制台输入的任务,这个是等待控制台输入的任务
      static void Console_Wait_Command_Task(){
      	Console_Command_Type cmdType;		// 收到的指令类型
      	Console_Status_Type cStatus; 
      	char *msg;											// 通过控制台输出的消息
      	while(1){
      		cStatus = Console_Readlines();
      		if(cStatus == CONSOLE_OK){
      			Wait_Tx_Rx(&console_rx_complete, 100);
      
      			cmdType = Console_Parse_Command();
      			if(cmdType == Console_Command_WiFi){
      				WIFI_Send_Command(console_rx_buffer, console_rx_buf_size);
      				Wait_Tx_Rx_Forever(&wifi_tx_complete);		// 和WiFi有关的操作,不知道为什么不能让任务调度,一调度就不能用
      
      				WIFI_Receive_Message();
      				Wait_Tx_Rx_Forever(&wifi_rx_complete);		// 和WiFi有关的操作,不知道为什么不能让任务调度,一调度就不能用
      
      				Console_Send_Data(wifi_rx_buffer, wifi_rx_buf_size);
      				Wait_Tx_Rx(&console_tx_complete, 100);
      
      			}else if(cmdType == Console_Command_Unknown){
      				msg = "==> error: 未知指令,无操作\n";
      				Console_Send_Data((uint8_t*)msg, 0);
      				Wait_Tx_Rx(&console_tx_complete, 100);
      			}
      		}
      	}
      }
      
      // 等待发送或者接收,完事会清除标志位
      void Wait_Tx_Rx(uint8_t *flag, uint32_t delayTime){
      	while(*flag != 1){
      		vTaskDelay(delayTime);
      	}
      	*flag = 0;
      }
      
      void Wait_Tx_Rx_Forever(uint8_t *flag){
      	while(*flag != 1);
      	*flag = 0;
      }
      

      而且这个任务拿到了指令以后就直接处理了,不去调用各种乱七八糟的函数了 hhhhh,先能用,优化的事以后再说了,相信未来自己的智慧。😅

  • 这下串口数据转发的工作也完成了,真没想到一个串口搞了三天。。。不过也好,起码裸机程序到时候应该是不会踩坑了。,啊接下来就是试试给 WiFi 做个配置,然后通过串口调试助手中的网络调试助手来看看能不能正常通信了。

    • 配置 WiFi:作为 AP 模式吧,然后让上位机作为客户端去发送数据。。。

      测试的过程中发现功能是能用,但是又有新问题了。。。还不止一个。。

      • 第一个问题:现在我的代码是给 WiFi 发送一次数据才能接收一次。但是如果 WiFi 作为 AP 模式,那 PC 如果持续要发送消息,就没法持续接收了。。这个 bug 得修复啊。
      • 第二个问题:如果作为 Station 模式,那在要请求发送消息以后,如果发送的消息开头不带 AT,这个消息就不会转发给 WiFi 设备。这个问题太抽象了,也得修复啊。。。算了,先放着,串口搞了这么久真的有点崩溃了,先弄一下别的吧。这个代码先提交到 git 上,不然到时候代码丢了就难受了。。
      • 优化点:开启 AP 模式或者开启 station 模式要通过串口发送很多内容,要是能直接写成一个函数直接一键调用就好了。

让 PC 能够通过网口进行通信

  • 这部分好像是要先学会 LwIP,不懂,没学过,先花点时间学一下。。先不学了,看世界杯,明天再开始学。。

  • OK 今天也是学起来了,已经 20 号了,算上 16 号开始,18 号休息,IAP 这是进行到第 4 天了,希望今天可以把 LwIP 移植并且运行起来。为了能够快点用上,我选择跳过大部分内容,只了解一些基本概念,然后把最基础的功能满足了就行了。

    • LwIP 是常于嵌入式的开源轻量化 TCP/IP 协议栈,支持的协议比较完整。支持 ARP、ICMP、IGMP、UDP、TCP、PPP、DNS、DHCP、IP、SNMP、AUTOIP 等。目前常见的网络通信功能都比较能够支持。

    • 3 种编程接口:RAW、NETCONN、SOCKET。

    • 我现在只需要移植过来,然后能用就行了,所以让 AI 帮我总结了一下重点关注哪些内容:

      • 基本能力

        • 完成裸机 / RTOS(FreeRTOS/RT-Thread)双环境 lwIP 完整移植;
        • 网卡驱动适配(ETH/USB 网卡 / WiFi SPI 网卡),能获取 DHCP 动态 IP、静态 IP 配置;
        • 基础协议跑通:ARP、ICMP(ping 通 PC)、UDP/TCP 单连接收发;
        • 上层 API 使用:RAW API / NETCONN API 二选一实现和 PC 双向数据收发;
        • 可稳定连接路由器,局域网内和 PC 网络调试、数据互传。
      • 核心文件

        src/core/init.c        // lwIP初始化总入口,移植第一步必看
        src/core/mem.c         // 内存堆管理,移植内存参数配置依据
        src/core/memp.c        // 内存池,TCP/UDP数据包缓冲池
        src/core/pbuf.c        // 数据包载体pbuf,网卡收发底层依赖
        src/core/ip.c          // IPv4基础收发、路由
        src/core/arp.c         // 地址解析,局域网通信必备
        src/core/icmp.c        // Ping实现,验证链路通断
        src/core/udp.c         // UDP协议栈实现
        src/core/tcp.c         // TCP基础收发逻辑(入门只看连接、收发部分)
        src/core/netif.c       // 网卡抽象层,移植网卡驱动核心
        src/api/netconn.c      // 推荐入门API,封装好、易上手
        src/api/api_msg.c      // RTOS下netconn消息通信
        
      • 调用层看懂接口

        src/include/lwip/xxx.h  // 全部头文件,看懂宏、结构体、函数声明
        src/netif/ethernet.c    // 以太网通用封装,看懂输入输出接口
        ports/xxx/arch.c        // 平台移植层(临界区、系统延时、内存拷贝)
        ports/xxx/sys_arch.c    // RTOS适配层:信号量、消息队列、线程封装
        
    • 。。。算了,还是认认真真读一读野火的《LwIP 应用开发实战指南》吧。

      • hhhhh 看了一天看是没看完,PDF 软件还有笔记软件倒是研究了半天。本人平常比较浮躁,不喜欢看视频,都直接看的书,所以经常用到 PDF 阅读,目前用的是 Sumatra PDF,非常轻量化,非常好用,但是功能很简陋,给 PDF 高亮的话还可以,但如果要写一些批注就麻烦了。所以就想着搞个好点的 PDF 阅读器,这阅读器要是真好用,付钱也可以啊。所以就去找,一开始用的是思源笔记里面的思阅插件,开一个 PDF 文档内存直接飙到 4GB,但是是真的好用啊,而且本人是思源笔记重度用户,这个文档也是思源笔记记录的,但是内存占用实在太大了,所以考虑用下别的。。。然后又是必应搜索又是豆包的,试了 Zotero、Koodo、Okular 感觉都差点意思,Koodo 是真花哨啊。。。最后发现没啥可用的,还是回头去看这个思源笔记的思阅插件,结果发现我目前的插件版本有点旧了 hhhhh 上个月装的,没想到作者一个月内更新了三四次,直接从 1.3.7 升级到了 2.1.4 了。不得不说,这个升级直接解决了这个内存占用的问题,而且还变得更好用了,我服了。强推思源笔记,真的,如果有需要我可以写一篇思源笔记的使用心得。
      • 不行,这个思阅插件的 bug 还是有点多,这会儿打不开 PDF 了。。。还是先回归古法 Sumatra PDF 阅读了,加了思阅的 QQ 群反馈反馈,,,
    • 回归正题。。还是 LwIP,野火的书我是看到了有操作系统的移植了,自己花了一晚上移植,五六个小时,都天亮了,最后都没有移植成功。不知道该怎么调试,有点累了。首先我因为是 CubeMX 生成的代码,HAL 库等级比野火提供的 HAL 库等级更高。。对照着看有挺多地方不太一样,没法照搬,但是硬看又看不懂。。而且我不知道我的板子现在是哪个部分没有初始化好,到底是硬件没初始化还是 LwIP 没有初始化还是底层驱动我没有写清楚还是 LwIP 的配置有问题还是基于 FreeRTOS 的情况下有问题。。头疼。。事实证明还是应该老老实实先用野火的示例代码来学习吧。。今天 21 号了,起床已经中午 12 点半了。。我真的不知道该怎么调试了,总不能把 HAL 换成野火一样的版本然后把代码全部复制过来吧。迷茫了。。。我决定了,还是先自己硬着头皮调试吧,今天要是还是调不通就直接照搬野火的代码吧。。。不能太浮躁,规划一下如何排查错误,下面是记录一下调试的思路:先试试无操作系统无 lwip 纯硬件初始化 ETH 看看能不能成功;再裸机移植 lwip 测试一下能不能通;最后再把 freertos 加进来。这样控制变量一步步排查看看到底是哪一步出了问题。同时现在的代码也先在 git 上创建一个分支来保存修改的过程,做好版本控制。

      • 纯裸机,无 freertos 无 lwip 测试

        • 因为 git 版本已经做好了控制,所以代码可以肆无忌惮地修改了,现在 main 函数里面把 freertos 和 lwip 有关的内容全部注释掉,仅保留 BSP 的一些初始化代码,然后初始化 ETH 看看。。。。。

        • 啊啊啊啊啊没有精神气了,不想搞了,直接 copy 野火的 HAL 库过来改吧。。崩溃了。。总而言之会走到这一步还是因为自己太浮躁了,没有一步一个脚印耐心看完 LwIP,然后一步一步做实验。害,太急了,但是我不想慢下来。还是快点先把 lwip 调通再说吧。不管了,先抄代码了,调通以后再琢磨,不然被 HAL 版本卡住有点脱离主线。嗯。。。核心还是要学习 LwIP,而不是研究 HAL 库不同版本的特性,当务之急还是先把 LwIP 调通了。。

        • OK,HAL 库被我从 1.8.5 暴力改成 1.7.3 了,先和野火的案例对齐了。。然后细节部分的代码一个一个 copy 过来。。。果然问题还是出现了,1.7.3 版本的 HAL 库没有 ‎HAL_UARTEx_ReceiveToIdle_DMA‎ 这个函数。。。这意味着我前面调串口都白干了额。。。还是不能头铁钻牛角尖非得去用这个跟教程不一样的版本啊。。。先学会走,再学跑。。吃一堑长一智,以后不这样了。。。现在得把以前的代码都删了去适应这个 HAL 库了唉。😅

          调整 HAL 库的报应来了。。。1.7.3 的 HAL 有好多函数都没有啊,要崩溃了啊啊啊啊啊,怎么 HAL_UART_RxEventTypeTypeDef​也能 undefined​,,,无语了,呼,冷静,先吃一顿好的缓解一下情绪。。。66666 移植的过程中发现 FreeRTOS 的版本不一样,也出现了一些问题,比如 QueueHandle_t​这个用在 lwip 兼容层做创建信号量的时候返回的数据类型,在野火的版本的 freertos 中是 void *​的类型,而我用的版本是 struct QueueDefinition * 类型。。我不知道说啥了。。看了一下,我用的 freertos 版本是 11.3.0,野火的是 9.0.0。emmm 事实证明初学没有特殊情况最好还是别用最新版本,会出现一些无法掌控的问题。。。现在咋办?我想发是先做个适配吧,如果问题更多了再改一下版本,毕竟 freertos 是正经学过啊。

        • 我靠我服了,我都已经把 HAL 的代码全部复制过来,而且还把 sys_arch、ethernetif、lwipopts 之类的移植有关的文件全部都照抄过来了,还是没法用啊,奇怪了。。。现在有区别的地方是 freertos 版本和 lwip 版本了,freertos 目前没问题,lwip 版本看了一下,都是 2.1.2。。怪了。。沃趣,野火的案例也不能用了。。更怪了我的天啊。。我网络肯定没有配置错啊,静态 IP 配置 192.168.31.200,网关 192.168.31.1,子网掩码不动,没啥问题啊,而且 IP 也没有被占用啊。。怪了,之前试过明明可以啊。。。重新复制一份野火的代码出来。。。还是不行。。。我目前用的是开发板作为服务器的模式,用的是 lwip_tcpecho 里面的代码,但是配置静态 IP 没法用啊。。。试试改成 DHCP?emmm 板子跟我说拿到了 IP 地址,但是路由器管理页面上面明明没有板子啊。。而且服务我也建立不了。。有点抽象了,真不知道该怎么办了。。。但是让板子作为 client 又可以用。为啥啊?要是我自己写的代码没法用那都合理,可是为什么野火给的例程也不能用啊?而且更匪夷所思的是昨天明明可以用。。。总不能我路由器有问题吧???那我也没条件换路由器啊。。。

        • 卧槽,可以 ping 得通!

        • 之前我一直都在用野火的那个串口调试助手里面的网络调试助手,昨天能连上,但是后面软件打不开了,我把配置文件改了一下可以打开了,现在就一直连不上,我怀疑是野火的串口调试助手有严重的 BUG。。。为了验证是不是因为这个问题,我重新下载一下野火的调试助手。。。emmm 还是不行,那就说明不是野火的串口调试助手有问题??

        • 我真的是无语了,野火的串口调试工具有问题啊,我换成正点原子的网络调试助手就能用了,666666。避雷了兄弟们,搞 LwIP 要调试的时候记得用不要用野火的网络调试助手(v1.0.3)。截止目前 2026-07-21,野火最新版本的多功能调试助手依然有这个 bug,触发的条件应该是第一次使用 TCP Client 模式连接开发板以后,如果没有停止监听就直接关掉软件,就会触发这个 bug,这个 bug 会导致第二次无法打开多功能调试助手。如果删除软件重新下载,则会导致网络调试助手功能无法使用。

        • 而且这部分踩坑点还挺多的,就是路由器实际上也会有问题。我后面改成 DHCP 来动态获取 IP 地址了,如果配置成静态 IP,我没试过行不行,因为卡在野火调试工具的那个 bug 了。我的路由器管理页面下,如果开发板没有数据包的传输,在路由器设备状态查看中会看不到设备。。。可能是路由器固件问题吧,我是用的红米 AX6 路由器,官方固件。。所以根据这个经验可以得出,板子的网络到底能不能通,还得看 ping 能不能 ping 通了。

        • 烧录进自己的代码,绝了,可以用了。果然天无绝人之路啊。那 LwIP 就先到此为止了,能用就行,TCP 回显就够了。

boot 程序:等待若干秒,等待过程中如果收到上位机指令,跳转到永久等待模式,等待接收上位机下发的 app.bin

  • 这部分内容印象中好像就是 STM32 大师篇里面介绍的,但是具体怎么操作全忘了。。这会儿得先复习总结一遍以后再来弄了,重点还是如何将 APP 应用搞到别的 ROM 地址,然后 boot 程序去跳转了(凭感觉应该是要有个分区表啥的,然后通过函数入口来直接跳转)。然后好像也就这些内容了,其他的应该没啥困难了吧。。

    我靠,看了一下我的 .map​文件,我的这个程序已经占用了这么多 123.34kB 内存空间了,太牛了。。看了一晚上这个 MDK 部分的内容,感觉就 .map​、elf​输出的文件比较重要,其他的好像不怎么用得上啊。然后还有一个什么 .bss 不知在哪,感觉也挺重要的,但是还没看到。然后程序二进制文件的大小就是最底下这个 ROM Size,占用了 96.45KB。而 STM32 的 ROM,好像有 7 个扇区都是 128KB 的啊,这空间妥妥够的啊。

    现在看到了 .sct 文件了,不得不说啊这个文件才是控制程序存放在什么位置的关键吧?我现在已经有思路了,就是指定 boot 程序在地址的开头,剩余的全部都一股脑丢到另一个节区里面。不知道能不能行,等下搞个简单的项目试试。。。

    OK,看完了 .sct 文件,感觉自己现在强的可怕,先搞个小实验试试。比如就按 IAP 简化版的要求,给 FLASH 划分 3 个区。先搞一个项目,这个项目的目的是能够将 FLASH 空间分为 3 块,然后主要是 boot 程序,支持通过串口将 BIN 文件写入到 APP 区域,然后将 APP 复制到运行区,每次开机会自动跳转到运行区来执行代码。然后在搞一个项目,这个项目专门就是只写 APP 程序并生成 BIN 文件,别的不管。。。不知道这个思路对不对,我也没去网上查 IAP 到底是咋实现的。先按自己想法来吧,后面如果不行再改。(感觉我的这个思路就是类似 Linux 里面的 bootloader、rootfs 的思想了)

    笑死我了,搞了几个小时,验证了自己的想法不对,哈哈哈哈哈。我的思路是这样,boot 就是主程序,通过主程序可以将 bin 文件发送到指定的 ROM 区域,然后主程序在运行了之后会跳到对应 ROM 的区域然后去执行。这个 bin 文件怎么生成?很明显就是整个项目编译以后通过 fromelf​指令生成的。因此我搞了一个电容触摸的实验的 bin 文件作为烧录的测试固件。然后编写 boot 代码,将 bin 文件通过串口写入到 08040000 起始的地址。我将 FLASH 分成 3 个空间,boot 程序占用 0x08000000-0x0803FFFF 一共 0x00040000 的空间,然后运行区和 APP 区平均分剩下的空间。然后编写一个入口函数,入口就是这个 08040000。这样非常完美了,当 boot 程序正常运行的时候,就会执行入口函数进入 08040000 这个地址空间然后去执行这里的代码。理论上好像很完美啊 hhhhh,函数确实跳转过去了,但是最后会跳到 Hard_Fault 的中断去哈哈哈哈哈。。。

    卧槽,问了一下豆包,发现我这个思路没问题啊,问题出在我是通过函数跳过去(我把某个函数定义到 0x08040000),这样很抽象,中断向量表啥的不匹配。要在 APP 程序里面把中断向量表配置好,然后在 boot 程序里面写好 SP 和 reset_entry 的指针,通过汇编代码给切过去,,然后真的就成了啊。我当时是想过这个问题,但是没想到真的是这样啊。还是豆包师傅教得好啊。这就是 boot 程序里面跳转到 APP 代码的关键代码了。

    #define APP_ADDR 0x08040000UL
    typedef void (*AppResetHandler)(void);
    
    __asm void IAP_JumpToApp(uint32_t ulAppMSP, uint32_t ulAppResetEntry)
    {
    /* *INDENT-OFF* */
        PRESERVE8
    
        ; R0 = ulAppMSP
        ; R1 = ulAppResetEntry
    
        MSR     MSP, R0 
        BX      R1 
    /* *INDENT-ON* */
    }
    
    void jumpAPP(){
    	uint32_t app_sp = *(volatile uint32_t *)APP_ADDR;
    	uint32_t app_reset_entry = *(volatile uint32_t *)(APP_ADDR + 4);
        // ========== 校验APP镜像合法性 ==========
        // F4 SRAM范围 0x20000000 ~ 0x2002FFFF(192KB)
        if ((app_sp < 0x20000000UL) || (app_sp > 0x20030000UL))
        {
            // APP无效,直接返回,不跳转
            return;
        }
        if (app_reset_entry < APP_ADDR || app_reset_entry > 0x080FFFFFUL)
        {
            return;
        }
    
        // ========== 4. 载入APP主栈指针 MSP ==========
        IAP_JumpToApp(app_sp, app_reset_entry);
    
        // ========== 5. 跳转至APP复位服务函数 ==========
        ((AppResetHandler)app_reset_entry)();
    
        // 理论永远执行不到这里
        while(1);
    }
    
  • 完美,把可以用 LwIP 和 FreeRTOS 的固件烧进去看看。。。emmm 不行,为啥啊。。。调试一下,,怎么时钟初始化出问题了?噢噢噢噢懂了,就是代码切换走之前先把时钟切回 HSI,不然 HAL 初始化时钟的时候会报错。。行,那也得把 FLASH 的访问时间也弄短了,不然 FLASH 访问乱飞。。。。中断向量表配置一下。。。哈哈哈哈哈成功了。。功夫不负有心人啊

    居然真的成功了,就自己胡思乱想一下,然后验证,最后就成功了。牛逼啊。不过真的花了好多时间啊。。通宵了,现在都早上 8 点了。真怕猝死啊。。先睡了,睡醒来补一补笔记。

    对了判断烧录进度这个,我是直接每接收 1000 个字节就输出一次,这样有点反馈,会知道到了什么进度。

  • 好了这会儿已经睡醒了,睡了 5 个小时左右,最近今天真的是每一天好觉,也不是不想睡,就是做项目有点废寝忘食了,我这人要么就彻底摆烂不学,要么就全部时间投入。。但凡有点三心二意都坚持不了。这会儿也是有点起色了,就借着这股劲干个 40 天,争取 9 月能够找到满意的工作入职。

收尾

  • 有点高兴的太早了。。发现还有一些问题没有解决

    • APP 程序中的串口因为替换了 HAL 库,串口收发这块内容需要重新实现一下。

      也是难搞,弄了一个多小时也是调通了,1.7.3 版本的 HAL 库没有 HAL_UARTEx_ReceiveToIdle_DMA​所以就是得自己实现的,原理就是正常调用 HAL_UART_Receive_DMA​去发送数据,但是要使能 IDLE​中断。在 UART1 的中断服务函数中判断,如果是 IDLE 中断,就停止 DMA,然后计算一下收到了多少数据就可以了。

    • 重新看了一下勃勃的需求,要求 boot 程序发送数据包需要使用一些传输协议比如 Modbus。。主要是要校验,本想着用 MD5 来校验就行了,发现 MD5 计算对于单片机来说还是太复杂了。。问了一下勃勃,说只要串口能发送能烧录就行了,知道怎么弄协议栈就行,不弄也 OK。那正好,直接不弄了。

    • WiFi 功能恢复。之前因为替换了 HAL 库,所以和 WiFi 通信的功能也被暂时阉割了,这下也是成功恢复了。

    • 之前还提到 WiFi 功能这里,如果要和 WiFi 通信,就会出现发送数据的时候如果开头不是 AT 就会无法识别的问题。

      • 这个问题的解决办法我是加入了一个判断指令模式的语句,判断当前指令是控制台模式还是 WiFi 模式,如果是 WiFi 模式那随便发送,如果是控制台模式那还是需要一点点判断什么指令。
      • 调试的时候 WiFi 不太能用,最后发现是 WiFi 没有使能。。。这点一定要注意。
  • 现在还有一个问题点,我的 boot 程序实际上不会自动跳转到 APP 程序中,需要通过串口发送启动的命令。。这个其实无关紧要,搞个延时然后进去就好了。问题是我的 boot 程序实际上是纯手搓寄存器的代码,我实在是懒得去配置 NVIC 中断服务还有 systick 了,但是直接无脑延时又挺不准的。。。反正这个问题现在挺纠结的,干脆将就先用,后面再说了。。

上位机

思路总结

  • 现在万事俱备只欠东风了,只要完成上位机,这个简化版的 IAP 基本上就暂告一段落了。标准版的 IAP 测试,还有 OTA,想想麻烦。。不过感觉应该也差不多,就是划一块 ROM 的空间,接收 TCP 发来的 binary 数据,写入到 APP 区。我目前的 ROM 区划分成 3 块。如果要弄标准版,划分成 5 个 ROM 区,第一块空间放 boot 程序,第二块放一些标志位(存放一些系统启动信息啥的,引导 boot 程序是加载 APP1 还是加载 APP2 的代码,其实也就第一次加载需要搬到运行区去,后续判断加载过了应该直接跳到运行区就好了),第三块运行区,第四块 APP1,第五块 APP2。其实都还好,比简化版需要多花点时间来处理 lwip 的数据收发,还有 WiFi 的数据收发,其他也没了。
  • 接下来就是完成上位机的开发了,说实话我其实不会 C++,但是 QT Designer 以前用得很熟。。实际开发起来应该还好吧,就是先把界面都画好了,后面把信号槽弄好,相关函数代码配置清楚就行了。
  • 那首先就是先画好原型界面,然后主要的几个功能:串口通信、网络通信、WiFi 配置,这 3 个功能配置好就行了。

原型界面

  • 不知道为啥。。这 win11 的 UI 好丑啊。。。当然我自己画的也丑哈哈哈哈,就这样了,能用就行。

    这个 QT Designer 画界面其实很难用,以前用的经验就是先把控件都摆到对应位置上,然后每个空间去配队做布局,最后整个窗口布局做起来就好了。

  • 界面画好了,接下来给每个会用到的控件都重新命名一下。

串口功能

  • OK 完事了,现在先实现读取串口的功能。。。这真不知道怎么弄,C++ 有什么库可以直接读取串口?

    • 看了一下官方给的文档。。在项目的 .pro​文件中添加 QT += serialport,结果编译失败了。。。查了一下发现没有安装相关的组件,从工具 -> QT Maintenance tool -> Start Maintenance tool,去安装一下需要的组件,还有其他一些需要的组件也都加上。。。今天就先干到这了,睡觉!

    • OK,时间也是来到了 23 号了,今天应该是可以弄好了,像这种 QT 之类工具很多都被封装好了,只要调包就好了,管他底层怎么实现的,能用就行。接下来就是化身调包侠来调包了。串口好像只要去实例化这两个工具就行了?

      QSerialPort:提供访问串行端口的函数

      QSerialPortInfo:提供有关现有串行端口的详细信息

      这 QT 的文档里面都不怎么给示例啊,API 有给出列表但是都是一些函数的形参,没说功能。。。不过我看 QT Creator 里面有不少示例,我直接拿示例看看吧。

    • OK 找到需要的东西了

      • 获取系统当前的串口列表:QSerialPortInfo::availablePorts()

      • ....就获取了一个串口,其他的还是不会,样例代码没看懂。。他收到了数据是怎么放上去的啊?再看不懂我要去看视频了。。。我懂了,serial 也是有一个信号槽,只要把读取完数据的信号绑定到对应的槽就好了,简单。

      • 很潦草,但是可以接收到数据了,就是输出的这玩意儿。。。得转换成 GBK 的编码格式吧。。在别的语言里面一般都有个 encoding 之类的选项。。这个 C++ 怎么改?

      • 累了。。刚去床上躺了一个小时。。现在回来继续干。

      • QT 怎么连这个文本编码的功能都这么难用啊。。问了豆包说用 QTStringConverter::decoder​,但是我的版本里面没有这玩意儿。。最后网上找了说用 String::fromLocal8Bit 来转换,但是这么转换是基于本机的语言来转的,如果切成别的语言会有问题。。不管了。。。反正现在接收是正常了。然后就是把按钮状态做个修改。然后搞个关闭串口的功能。。。。

      • OK 完成了,花了 5 分钟。。。接下来是啥?发送消息是吧。。先给发送消息的按钮加个反馈,当串口打开的时候,发送消息的按钮才 enable。

      • 现在卡在编码问题了,服了。单片机发过来的是 GB2312 编码格式的字符串,我本地解码以后拿到的是不知道是什么垃圾,如果是数字就会出错。。。问了豆包,结果:

        现在也不懂出现了什么,串口接收的数据回显的时候会有:绻恍枰蛟趌 这样子的东西,豆包说可能是发送的汉字可能被截断了。我觉得也有可能,通常这个乱码都是出现在中文突然变到非中文的情况。。看看怎么解决吧。。。

        OK 解决了,现在可以正确读取代码了:

        void MainWindow::readData()
        {
            QByteArray chunk = my_serial->readAll();
            m_recvBuf += chunk;
            int pos;
            while ((pos = m_recvBuf.indexOf('\n')) != -1)
            {
                QByteArray line = m_recvBuf.left(pos + 1);
                m_recvBuf = m_recvBuf.mid(pos + 1);
        
                QString text = QTextCodec::codecForName("GBK")->toUnicode(line);
                ui->serialRec_textBrowser->insertPlainText(text);
            }
        }
        
      • 接下来完成一下打开文件然后发送文件的功能。因为我的 boot 程序里面关于升级的部分,在设备开机的时候需要先发送一个 1 进入烧录模式,然后需要发送整个文件的大小(4 字节),然后才是发送文件。如果单靠串口发送也可以,但是挺麻烦的。

        • 先完成打开文件部分。还是调包,QFile

        • 调了两个小时,现在终于是可以了,但是发现以后很离谱的事情。就是我感觉我的 boot 程序挺糟糕的。。所以就想修改一下。。但是惊人的一幕出现了。。只要我对我的 boot 程序做一点修改,boot 就没法烧录。。惊呆了。。。我现在都不敢动我的 boot 程序了,否则功亏一篑。。。

        • 我知道 boot 程序出什么问题了。。。因为我的上位机程序接收的时候有问题。。。笑死了,如果单片机没有发 \n 就接不到数据了。。算了,不管了。。现在出问题的时候不止是单片机可能有问题,也可能是我的上位机 APP 有问题哈哈哈哈。

        • 其实有很多功能我都偷工减料了。。。比如上面那些数据位选择,奇偶校验什么的哈哈哈哈哈,只能看不能用。

网络模式

ETH 以太网模式

  • 现在来到网络的通信模式了,先把 ETH 的给做了,ETH 比较稳,毕竟直接偷的野火的代码。QT 的网络通信方式应该就是 socket 啥的吧,用的 QTcpSocket 库,估计也就是什么创建一个对象,然后配置一下 ip 和端口还有连接模式,建立连接以后调方法发数据就好了。还是调包侠。

    • 果然很简单,就是把 QTcpSocket​引入一下,然后实例化一个对象,调用 connectToHost 方法去连接就可以。。简单啊。
    • 那 WiFi 也是一样了。无聊。
  • 卧槽,出现一个惊天大 bug!!!!对于 WiFi 这一块,我的输出逻辑是,设备接收到 WiFi 指令就马上转发给 WiFi,WiFi 模块收到以后就马上转发回串口。理论上没有任何问题是吧?我用正点原子的串口调试工具也是完全 OK 的。但是!用我自己的工具,就会导致本来应该接收到的数据是:41 54 0D 0D 0A 0D 0A 4F 4B 0D 0A 3D 3D 3E 20 57 69 46 69 D6 B8 C1 EE C4 A3 CA BD A3 BA 0A​,而变成了两段 41 54 0D​和 3D 3D 3E 20 57 69 46 69 D6 B8 C1 EE C4 A3 CA BD A3 BA 0A​,很明显,中间的 0D 0A 0D 0A 4F 4B 0D 0A​丢失了。。。我用了各种办法来弄,比如等待串口一直接收,还有定时器不停轮询。。都不行。不知道为啥 0D 0A 0D 0A 4F 4B 0D 0A 这段会丢失。这个数据我都没有进行任何处理,我怀疑是 QT 串口的函数库有 bug。说不定换个版本的 QT 就可以用?具体我也不懂了。。。这个问题算是 WiFi 数据串口回显的问题,问题不大。

  • 不管了。现在配置一下 WiFi,然后启动 WiFi 的 TCP 服务,然后上位机再测试一下。

    • 完美,完事了,找勃勃汇报去。

问题总结

有很多地方都是一笔带过的,不用怀疑,就是没有卡到我很长时间,都是比较顺利的。一些 3-5 分钟就解决的问题我都没写上,不代表没遇到。问题太多都写出来一个是耗费我的时间,一个也是会显得我太啰嗦了。。。下面是一些印象比较深的问题的总结。。

MDK 相关问题

  • Keil 编译问题 L6050U:The code size of this image (33016 bytes) exceeds the maximum allowed for this version of the linker

  • 有时候明明正常编译成功,但是烧录进去程序没法正常运行,调试的时候程序总是莫名跑飞,各种疑难杂症

    • 看看是不是 Use MicroLIB 没有勾选,咱这也不知道什么原理,反正勾上了以后如果出了问题就不用往这边考虑了
  • 右键点击函数以后没有办法跳转到函数的定义

    • 小问题,魔术棒->Output​->Browse Infomation 选上。
  • 小技巧:可以通过修改 IROM1 地址,直接把程序烧录到对应的 ROM 区,这样就不用总是去 boot 程序烧录 BIN 文件来写入 APP。

FreeRTOS 相关问题

  • 在 FreeRTOS 下,和串口输出有关的任务最好用阻塞的方式输出,不然可能会出现串口输出不完整或者无法输出的问题。我怀疑是任务之间调度的问题,但我不敢确定,暂定是这个问题。在任务中如果是阻塞式输出串口信号就没任何问题。总之这个项目只是一个测试,不要在串口输入输出的实时性上面死磕,能实现就行。
  • 串口通信的优先级一定要高一点,如果和其他任务相同优先级,串口常常会丢失数据,具体原因不明。

WiFi 模组相关问题

  • 要使用 WiFi 模组,一定要记得 WiFi 的使能引脚和复位引脚都拉高了。

LwIP 相关问题

  • 这玩意儿很难,要改的文件不少,直接抄野火了。。。而且项目做完,要问任何和 LwIP 有关的问题,我其实都答不上来。
  • 如果一定要自己移植,重点关注的就这几个文件:arch_sys.c​、ethernetif.c​、lwipopts.h​、bsp_eth.c​;初始化的过程中主要出问题的也就是这几个文件,调通了基本上就可以驱动了。最主要的还是 ethernetif.c​里面的 3 个 low_level_xxxx 了。

汇报内容

  • 单片机 FLASH 分区:

    • 0x08000000 - 0x0803FFFF:BOOT 程序空间
    • 0x08040000 - 0x080A0000:运行区
    • 0x080A0000 - 0x080FFFFF:APP 区
  • BOOT 程序运行逻辑:程序启动进入 BOOT 程序,等待 5s 检测用户输入。

    • 用户输入 1,进入烧录模式,无限循环等待,用户先发送 BIN 文件大小,然后发送 BIN 文件。BOOT 程序在收到文件大小以后,擦除扇区 9 10 11 也就是 APP 区 0x080A0000 - 0x080FFFFF​的空间。完成擦除后开始接收系统发来的 BIN 文件,逐个字节直接写入到 APP 区。待全部写完以后,重复对比校验一次数据,如果全部一致表示校验通过。然后自动将 APP 区的所有数据都搬到运行区,并调低 FLASH 的 FLATENCY、切换 HSI 时钟、关闭中断等,然后把 RUN 区地址压入 MSP 跳转到 0x08040000 运行程序,这个是收到 FreeRTOS 的启发的。
    • 用户输入 2,恢复出厂设置。将 APP 区的代码全部拷贝到运行区。
    • 用户输入 3 或等待超过 5s,跳转到 0x08040000 执行运行区代码。
  • APP 程序:移植了 LwIP 实现 ETH 以太网通信还有 FreeRTOS。

    • 3 个任务

      • 第 1 个用来闪烁 LED,只要 LED 闪烁就说明正常进入系统。
      • 第 2 个任务用来监听串口和 WiFi 输入,其实 WiFi 得单独再弄一个任务的,我是收尾的时候才发现的问题,有点不想改了。
      • 第 3 个任务就是监听以太网输入了,配置了 DHCP 和 TCP Server 模式比较方便调试,直接进行 TCP 数据回显。
  • 上位机使用 QSerialPort 实现串口通信、QTcpSocket 实现网络通信。这个很简单都是调包,没什么好说的。

项目源码:z1178902213/IAP_test: 基于 STM32 的 IAP 升级项目,包含上位机