ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

CH32V307 上 FreeRTOS 与 LwIP 移植实战:从工程搭建到网络调优

CH32V307 上 FreeRTOS 与 LwIP 移植实战:从工程搭建到网络调优 1. 为什么要在 CH32V307 上折腾 FreeRTOS 加 LwIPCH32V307 这颗芯片这两年在 RISC-V 圈子里热度一直不低自带以太网 MAC、USB 高速接口、还有 144MHz 的主频和 64KB SRAM做联网设备的主控非常合适。但很多人拿到官方例程之后会发现一个问题裸机跑网络能通一旦业务逻辑复杂起来主循环里塞满了各种轮询和延时代码很快就变成一团乱麻。这时候把 FreeRTOS 和 LwIP 一起搬上来基本是绕不开的一步。我这次做的项目是一个带网口的工业数据采集终端需要同时处理 Modbus 采集、TCP 上报、本地串口调试和几个定时任务。裸机方案试过用状态机硬扛跑到第三个任务的时候中断响应就开始飘了。换成 FreeRTOS 加 LwIP 之后任务之间用队列和信号量解耦网络收发交给协议栈自己的线程主控逻辑清爽了很多。整个过程在 MounRiver Studio 上完成工具链是它自带的 RISC-V GCC没有换其他 IDE。这篇内容适合两类人看一类是刚接触 CH32V307、想在 MounRiver Studio 里把 FreeRTOS 和 LwIP 跑起来的嵌入式新手另一类是从 STM32 转过来、习惯了 CubeMX 一键生成、到了 RISC-V 平台发现没有现成配置工具的老手。我会把配置过程中每一步为什么这么做讲清楚包括内存怎么分、中断优先级怎么排、协议栈参数怎么调最后附上我踩过的坑和排查方法。整套流程实测可复现代码结构可以直接抄。需要提前说明的是MounRiver Studio 的工程结构和 Keil、IAR 差别不小它的链接脚本、启动文件、库文件组织方式都有自己的逻辑照搬 STM32 的经验很容易卡住。下面我按实际操作的顺序从工程创建一直讲到网络能 ping 通、TCP 能收发。2. 环境准备与工程骨架搭建2.1 MounRiver Studio 安装与工具链定位MounRiver Studio 是基于 Eclipse 改的安装包官网直接下装完之后建议把工作空间放在一个没有中文和空格的路径下这个不是迷信是实测下来路径里有空格时某些 make 步骤会报奇怪的错。安装完成后工具链默认在安装目录的toolchain文件夹里RISC-V GCC 的可执行文件在toolchain/gcc-riscv/riscv-none-embed-gcc/bin下面。如果你想知道具体装到了哪里在 IDE 里右键工程看属性C/C Build 的 Settings 里能看到编译器路径或者直接在安装目录搜riscv-none-embed-gcc.exe也能找到。这里有个细节值得说MounRiver Studio 自带的 GCC 版本和芯片厂商的库是有配套关系的不要随便去换一个更新版本的 GCC我试过换高版本之后链接阶段报了一堆关于-march和-mabi的警告虽然能编过但运行不稳定。用官方配套的版本最省心。2.2 创建工程与选择芯片型号新建工程的时候选 RISC-V 下的 CH32V307 系列MounRiver 会帮你生成一个带启动文件、链接脚本和基础外设库的骨架。生成的工程里默认有一个Startup文件夹放启动汇编一个Ld文件夹放链接脚本Link.ld还有Core、Debug、Peripheral等目录。这个骨架是裸机工程我们要做的是往里面加 FreeRTOS 和 LwIP 的源码。创建时注意两点一是芯片型号要选对CH32V307 有 VCT6、RCT6 等不同封装SRAM 大小都是 64KB但 Flash 有 256KB 和 384KB 的区别选错了链接脚本里的 Flash 长度会对不上二是调试器配置如果你用的是 WCH-Link在 Debug Configuration 里选 WCH-Link 对应的选项别选成 OpenOCD 的通用配置否则下载会失败。2.3 目录结构规划在动手加代码之前先把目录结构理清楚不然后面文件一多会乱。我用的结构是这样的Project/ ├── Core/ 芯片内核相关 ├── Debug/ 调试相关 ├── Peripheral/ 外设驱动 ├── FreeRTOS/ │ ├── Source/ 内核源码 │ └── Portable/ 移植层 ├── LwIP/ │ ├── src/ 协议栈源码 │ └── port/ 移植层 ├── App/ 应用代码 └── Ld/ 链接脚本FreeRTOS 的 Portable 层要选GCC/RISC-V对应的目录CH32V307 是 RV32IMAFC 架构带硬件浮点和原子指令所以 port 文件用portASM.S和port.c这一套。LwIP 的 port 层需要自己写ethernetif.c和lwipopts.h这两个文件是整个移植的核心后面会重点讲。提示FreeRTOS 源码里的portable目录有很多平台只保留GCC/RISC-V和MemMang两个其余删掉否则编译时可能因为找不到头文件报错。3. FreeRTOS 移植的核心细节3.1 内核文件裁剪与 FreeRTOSConfig.h 配置FreeRTOS 源码里Source目录下的tasks.c、queue.c、list.c、timers.c、event_groups.c这几个是必须的croutine.c用不到可以删。portable/MemMang里选一个堆管理方案我用的是heap_4.c因为它支持碎片合并长时间运行比 heap_2 稳。heap 的大小在FreeRTOSConfig.h里通过configTOTAL_HEAP_SIZE定义CH32V307 有 64KB SRAM我分了 20KB 给 FreeRTOS 堆剩下的留给 LwIP 和全局变量。FreeRTOSConfig.h是整个移植的指挥中心几个关键配置我列一下配置项值说明configCPU_CLOCK_HZ144000000系统主频configTICK_RATE_HZ10001ms 一个 tickconfigMAX_PRIORITIES16优先级数量configMINIMAL_STACK_SIZE128空闲任务栈单位是字configTOTAL_HEAP_SIZE20480堆大小字节configUSE_PREEMPTION1抢占式调度configUSE_MUTEXES1启用互斥量configCHECK_FOR_STACK_OVERFLOW2栈溢出检测configTICK_RATE_HZ设成 1000 是有讲究的。设太高比如 10000系统光在 tick 中断里就消耗大量 CPU设太低比如 100任务延时精度不够LwIP 的定时器也会变粗。1000Hz 是嵌入式的常用值1ms 的调度粒度对绝大多数场景够用。configCHECK_FOR_STACK_OVERFLOW设成 2 表示用填充模式检测任务栈被写穿的时候能触发vApplicationStackOverflowHook调试阶段强烈建议打开等稳定了再关掉省点开销。3.2 中断优先级与 SysTick 配置RISC-V 的中断优先级机制和 Cortex-M 不一样CH32V307 用的是 PFIC可编程快速中断控制器优先级数值越小优先级越高。FreeRTOS 要求configMAX_SYSCALL_INTERRUPT_PRIORITY以上的中断不能调用 FreeRTOS 的 API这个阈值要设对。在 CH32V307 上我把 SysTick 和软件中断的优先级设成最低也就是数值最大。SysTick 中断处理函数里调用xPortSysTickHandler这个函数负责 tick 递增和任务调度判断。软件中断用于任务切换portYIELD就是触发这个中断。具体配置在port.c里configKERNEL_INTERRUPT_PRIORITY和configMAX_SYSCALL_INTERRUPT_PRIORITY这两个宏要对应到 PFIC 的优先级寄存器。我实测下来把内核中断优先级设成 15最低外设中断设成 5 到 10 之间这样外设中断能抢占内核但不会干扰调度器的临界区。注意如果你在中断里调用了xQueueSendFromISR这类 API中断优先级必须低于configMAX_SYSCALL_INTERRUPT_PRIORITY否则会触发断言。这个坑我在调串口接收中断的时候踩过现象是随机死机查了两天才定位到优先级配错了。3.3 启动流程与第一个任务FreeRTOS 的启动流程是硬件初始化 → 创建任务 → 调用vTaskStartScheduler→ 调度器启动 SysTick 和软件中断 → 切换到第一个任务。在 CH32V307 上main函数里先调SystemInit配时钟再初始化外设然后创建任务。我习惯先创建一个StartTask在里面做所有外设和协议栈的初始化初始化完再创建业务任务最后vTaskDelete(NULL)把自己删掉。这样做的好处是初始化代码不占用主任务栈而且初始化顺序清晰。void StartTask(void *pvParameters) { taskENTER_CRITICAL(); // 外设初始化 UART_Init(115200); // LwIP 初始化 LwIP_Init(); // 创建业务任务 xTaskCreate(TcpReportTask, TCP, 512, NULL, 4, NULL); xTaskCreate(ModbusTask, Modbus, 512, NULL, 3, NULL); taskEXIT_CRITICAL(); vTaskDelete(NULL); }任务栈大小单位是字512 个字就是 2KB。TCP 任务因为要处理协议栈回调栈给大一点Modbus 任务逻辑简单512 字够用。实际调试时用uxTaskGetStackHighWaterMark看剩余栈再根据水位调整。4. LwIP 协议栈的移植与网络配置4.1 LwIP 版本选择与源码组织LwIP 我用的是 2.1.3 版本这个版本稳定资料也多。源码里src目录下分core、api、netif、apps几个部分。core是协议栈核心netif是网络接口层我们要写的ethernetif.c就放在netif对应的移植目录里。LwIP 有两种 APIraw API 和 socket API。raw API 是回调式的效率高但写起来绕socket API 是阻塞式的写起来像 Linux 网络编程但需要 FreeRTOS 的 sys_arch 支持。我选的是 socket API因为业务逻辑里 TCP 收发用 socket 写起来直观而且 FreeRTOS 已经提供了信号量和队列sys_arch 实现起来不难。lwipopts.h是 LwIP 的配置头文件几个关键参数参数值说明MEM_SIZE8192协议栈堆大小PBUF_POOL_SIZE16pbuf 池数量TCP_SND_BUF4096TCP 发送缓冲TCP_WND4096TCP 接收窗口LWIP_DHCP1启用 DHCPLWIP_NETCONN1启用 socket APILWIP_SOCKET1启用 socket 接口MEM_SIZE给 8KB 是因为我的应用同时只有两三个 TCP 连接数据量不大。如果你要做大文件传输这个值要往上加但注意 CH32V307 总共才 64KB SRAMFreeRTOS 堆 20KB 加 LwIP 堆 8KB再加上全局变量和栈已经用掉大半要留余量。4.2 以太网驱动与 ethernetif.c 实现CH32V307 自带 10M/100M 以太网 MACPHY 芯片一般是内置的 10M PHY 或者外接的 100M PHY。官方库里有eth.c驱动我们要做的是把它和 LwIP 的 netif 对接起来。ethernetif.c里要实现几个关键函数low_level_init负责初始化 MAC 和 PHYlow_level_output负责发送数据包ethernetif_input负责接收数据包并递交给 LwIP。接收这块我用了中断加信号量的方式MAC 收到包触发中断中断里释放一个信号量ethernetif_input任务阻塞在这个信号量上收到信号后从 DMA 描述符里读数据。void ETH_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; if (ETH_GetITStatus(ETH_IT_RX)) { ETH_ClearITPendingBit(ETH_IT_RX); xSemaphoreGiveFromISR(xSemaphoreEthRx, xHigherPriorityTaskWoken); } portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }这里有个性能考量如果每个包都触发一次中断高流量下中断开销会很大。CH32V307 的 MAC 支持中断合并可以配置成收到多个包再触发一次中断。我实测在 100M 满速下不合并中断时 CPU 占用率能到 40%合并之后降到 15% 左右。具体配置在ETH_InitStructure里的ETH_Interrupt和ETH_InterruptMask字段。4.3 PHY 初始化与链路检测PHY 初始化是网络能不能通的关键。CH32V307 内置 PHY 的话上电后要等 PHY 稳定然后通过 SMI 接口读 PHY 的 ID 寄存器确认通信正常。我遇到过 PHY 读不到 ID 的情况查下来是 MDIO 的时钟分频配错了CH32V307 的 MDIO 时钟不能超过 2.5MHz分频系数要根据 HCLK 算。链路检测我放在一个独立任务里每 500ms 读一次 PHY 的状态寄存器检测到链路状态变化就调用netif_set_link_up或netif_set_link_down。这个任务优先级设低一点2 就行不影响数据收发。void LinkTask(void *pvParameters) { while (1) { uint16_t phyStatus ETH_ReadPHYRegister(PHY_ADDRESS, PHY_BSR); if (phyStatus PHY_LINKED_STATUS) { if (!netif_is_link_up(gnetif)) { netif_set_link_up(gnetif); } } else { if (netif_is_link_up(gnetif)) { netif_set_link_down(gnetif); } } vTaskDelay(pdMS_TO_TICKS(500)); } }提示PHY 地址要确认对内置 PHY 和外接 PHY 的地址不一样读错地址会一直读不到链路状态。用示波器看 MDIO 波形是最快的确认方法。5. 系统联调与性能优化5.1 内存布局与链接脚本调整CH32V307 的 SRAM 是 64KB起始地址 0x20000000。链接脚本里要划分好各个段.text放 Flash.data放已初始化全局变量.bss放未初始化全局变量堆和栈放在 SRAM 末尾。FreeRTOS 的堆是静态数组放在.bss里LwIP 的堆也是。栈的话主栈在启动文件里定义任务栈从 FreeRTOS 堆里分配。我建议在链接脚本里把_heap_stack_end和_heap_stack_start标清楚避免堆栈重叠。实测下来64KB SRAM 的分配大概是FreeRTOS 堆 20KBLwIP 堆 8KB全局变量和缓冲区 20KB主栈 4KB剩余 12KB 做余量。如果编译后发现.bss段超了先看是不是哪个缓冲区定义太大了比如 TCP 窗口和 pbuf 池。5.2 任务优先级与栈大小调优任务优先级排布直接影响系统响应。我的排布是网络接收任务优先级 5TCP 上报任务 4Modbus 采集任务 3链路检测任务 2空闲任务 0。网络接收优先级最高是因为它要尽快把数据从 DMA 取走否则丢包。栈大小调优用uxTaskGetStackHighWaterMark这个函数返回任务运行过程中栈的最小剩余量。我一般先给一个偏大的值跑一段时间后看水位再往下调。比如 TCP 任务一开始给 1024 字跑下来水位还有 600 字就调到 768 字省下来的给其他任务。任务优先级初始栈调优后栈水位EthRx5512512280TcpReport41024768320Modbus3512512200Link22562561505.3 网络吞吐测试与瓶颈定位联调阶段我用 iperf 测吞吐CH32V307 跑 TCP 发送能到 60Mbps 左右接收能到 45Mbps。这个成绩在 100M 网口里算中等瓶颈主要在内存拷贝次数。LwIP 从 pbuf 到应用缓冲区有一次拷贝应用处理完再拷贝到发送 pbuf 又一次两次拷贝在高速下很吃 CPU。优化手段有两个一是用 zero-copy 的 pbuf让应用直接操作 pbuf 里的数据省掉一次拷贝二是把 TCP 发送缓冲调大减少发送次数。我试过把TCP_SND_BUF从 4096 调到 8192发送吞吐提升了大概 15%但内存占用也上去了要根据实际余量权衡。注意调大 TCP 缓冲后要重新测栈水位因为协议栈内部处理大缓冲时用的栈会变多我调完之后 TCP 任务栈水位从 320 掉到 180差点溢出。6. 常见问题与排查实录6.1 编译链接类问题问题一链接时报region RAM overflowed。这个基本是内存超了。先看 map 文件里哪个段最大通常是.bss。检查 FreeRTOS 堆和 LwIP 堆是不是设太大或者有没有定义了大数组没释放。我遇到过一次是调试用的串口缓冲区定义了 4KB改成 512 字节就好了。问题二undefined reference to xPortSysTickHandler。这是port.c没加进编译或者FreeRTOSConfig.h里configUSE_TICKLESS_IDLE配错了。检查工程里portable/GCC/RISC-V目录下的文件是否都加入了编译列表。问题三multiple definition of _start。启动文件重复了MounRiver 生成的工程自带一个启动文件如果你从别处拷了一个进来就会冲突。保留一个就行。6.2 运行类问题问题四程序跑起来就进HardFault。RISC-V 里叫异常先看mcause寄存器确定异常类型。常见原因是栈溢出或者访问了非法地址。打开configCHECK_FOR_STACK_OVERFLOW在vApplicationStackOverflowHook里打个断点能快速定位是哪个任务栈爆了。问题五网络 ping 不通。按这个顺序查PHY 链路是否 up → MAC 是否收到包 → ARP 是否响应 → IP 地址是否配对。我一般先在ethernetif_input里打断点看有没有包进来有包进来但 ping 不通多半是 IP 配置或者 ARP 表的问题。问题六TCP 连接建立后很快断开。检查TCP_WND和TCP_SND_BUF是不是太小或者MEMP_NUM_TCP_PCB数量不够。还有一种可能是 keepalive 超时LwIP 默认的 keepalive 是关闭的如果对端有 keepalive 要求要在lwipopts.h里打开LWIP_TCP_KEEPALIVE。6.3 常见问题速查表现象可能原因排查方法编译报 RAM 溢出堆或缓冲区太大看 map 文件减小配置运行进异常栈溢出或空指针开栈检测看 mcauseping 不通PHY 或 IP 配置问题逐层查链路、MAC、ARPTCP 频繁断开缓冲太小或 keepalive调大缓冲开 keepalive吞吐上不去拷贝次数多用 zero-copy调大缓冲中断里调 API 死机优先级配错检查中断优先级阈值6.4 独家避坑技巧第一个技巧调试网络的时候把LWIP_DEBUG打开LWIP_DBG_MIN_LEVEL设成LWIP_DBG_LEVEL_WARNING这样协议栈内部的警告会打出来很多配置错误在启动阶段就能发现。但正式发布要关掉debug 输出很占串口带宽。第二个技巧FreeRTOS 的vTaskList和vTaskGetRunTimeStats这两个函数在调试任务调度时非常有用能看到每个任务的运行时间和状态。用之前要在FreeRTOSConfig.h里打开对应的宏并且提供一个高精度的计时器。我一般用 TIM2 做这个计时器1us 分辨率。第三个技巧LwIP 的pbuf泄漏是隐蔽性很强的问题表现是运行一段时间后网络卡死。排查方法是定期打印memp_get_memory的统计信息看 pbuf 池的 used 数量是不是只增不减。如果是检查应用层有没有忘记pbuf_free。7. 我个人的一些实操体会这套 FreeRTOS 加 LwIP 的移植方案我在三个项目上用过从数据采集终端到网关设备稳定性都还可以。最大的感受是RISC-V 平台虽然生态比 ARM 差一些但 CH32V307 的官方库和 MounRiver Studio 的配合已经能覆盖大部分需求关键是配置要细致尤其是中断优先级和内存分配这两块配错了现象很随机查起来费时间。另外提一句FreeRTOS 的 heap 管理方案选择要看应用场景。heap_4 适合频繁申请释放不同大小内存的场景但如果你的应用都是固定大小的分配heap_3 配合标准 malloc 也行只是要注意线程安全。我现在的习惯是能用静态分配就不用动态任务栈和队列都用静态创建这样内存布局在编译期就确定了运行时不会有碎片问题。最后分享一个调试网络的小习惯我会在ethernetif_input里加一个计数器每收到一个包就加一然后通过串口定期打印。这个计数器能直观反映网络负载配合任务运行时间统计能快速判断瓶颈在协议栈还是在应用层。这个习惯帮我省了很多猜测的时间。
返回列表