# GD32L23X-GCC-CLion-RTTnano **Repository Path**: newflydd/gd32-l23-x-gcc-clion-rttnano ## Basic Information - **Project Name**: GD32L23X-GCC-CLion-RTTnano - **Description**: GD32L233CCT6的GCC工程,在CLion下编译,移植RT-Thread nano - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-11-25 - **Last Updated**: 2023-11-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # GD32L23X移植RT-Thread nano ## 工程结构 下载[RT-Thread nano3.1.5](https://github.com/RT-Thread/rtthread-nano/archive/refs/heads/master.zip),拷贝相关文件至CLion工程: 1. 拷贝nano库中的`components`, `include`, `libcpu`, `src`三个文件夹至工程的`libraries`目录 2. 拷贝任意一份示例`rtconfig.h`文件至工程根目录 ## board.c 追加以下代码至`board.c` ```C #if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP) #define RT_HEAP_SIZE 1024 static uint32_t rt_heap[RT_HEAP_SIZE]; // heap default size: 4K(1024 * 4) RT_WEAK void *rt_heap_begin_get(void) { return rt_heap; } RT_WEAK void *rt_heap_end_get(void) { return rt_heap + RT_HEAP_SIZE; } #endif /** * This function will initial your board. */void rt_hw_board_init() { /* System Clock Update */ SystemCoreClockUpdate(); /* System Tick Configuration */ SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND); /* Call components board initial (use INIT_BOARD_EXPORT()) */ #ifdef RT_USING_COMPONENTS_INIT rt_components_board_init(); #endif #if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP) rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get()); #endif } void SysTick_Handler(void) { /* enter interrupt */ rt_interrupt_enter(); rt_tick_increase(); /* leave interrupt */ rt_interrupt_leave(); } ``` 1. 以上代码开辟了一段静态内存给RTT作为堆空间,用作内存分配。 2. 配置了`rt_hw_board_init`函数,该函数会被RTT系统的`entry`入口函数所调用,继而引导操作系统 3. `rt_hw_board_init`需要配置系统时钟,根据不同MCU进行移植,GD32使用的是`SysTick_Config`函数 4. 重写了系统滴答定时器的中断服务函数`SysTick_Handler`,用作RTOS的基础时钟 ## 删减重复定义的中断函数 原工程`gd32l23x_it.c`中定义了大量的空中断服务函数,其中部分会被RTT接管,因此需要删减该文件中,被视为重复定义的部分中断服务函数,包括: 1. 硬件异常中断:`HardFault_Handler`,该函数被RTT接管做异常处理 2. 可编程中断:`PendSV_Handler`,该函数被RTT接管用作操作系统的任务调度 3. 滴答定时器中断:`SysTick_Handler`,上一节在`board.c`文件中,重构了该函数,用作操作系统心跳 ## 修改启动汇编文件 修改`libraries/CMSIS/startup_gd32l233.s`文件第*58* 行,将用户段引导入口调整至RTT的`entry`入口函数: ```C /* Call SystemInit function */ bl SystemInit /* Call static constructors */ bl __libc_init_array /*Call the main function */ bl entry ``` 前面章节提到,RTT的初始化入口函数是`src/components.c`文件中的`entry`,这个函数最终会调用到`board.c`中的`rt_hw_board_init`函数,在用户程序开始之前,完成RTT的初始化。 ## 配置rtconfig.h 使能`RT_USING_USER_MAIN`和`RT_USING_HEAP`两个函数定义,让`main`函数作为一个动态的用户线程进行启动,同时使能了动态内存管理。 ## 结束 至此,就完成所有的RTT nano的移植了,编写测试程序如下: ```C void thread2Entry(void *parameter) { rt_thread_mdelay(500); while (1) { gpio_bit_toggle(GPIOA, GPIO_PIN_8); rt_thread_mdelay(1000); gpio_bit_toggle(GPIOA, GPIO_PIN_8); rt_thread_mdelay(1000); } } int main() { rcu_periph_clock_enable(RCU_GPIOA); gpio_mode_set(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_7 | GPIO_PIN_8); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_7 | GPIO_PIN_8); rt_thread_t tid = rt_thread_create("thd2", thread2Entry, RT_NULL, 1024, 9, 20); rt_thread_startup(tid); while (1) { gpio_bit_write(GPIOA, GPIO_PIN_7, 1); rt_thread_mdelay(1000); gpio_bit_write(GPIOA, GPIO_PIN_7, 0); rt_thread_mdelay(1000); } } ``` 可以看到两个LED灯在两个线程中交替闪烁。 ## 其他 ### 使用HEAP_BEGIN和HEAP_END最大化堆内存空间 在[[#board.c]]章节里,使用显性的方式,手动静态分配了一段4K的内存空间,用来为RTOS提供动态堆内存管理。随着工程逻辑的不断伸缩,需要最大化堆内存空间,根据MCU自身RAM大小,以及已编程申请的静态内存,自动分配剩余堆空间大小。可以参考完整版RT-Thread,做以下修改: #### 链接文件增加标记 在GD32L23X的GCC工程中,有一份链接文件,在该文件中定义了MCU的数据分区,其中定义了FLASH大小256KB,RAM大小32KB。 约*113* 行处,追加分区位置标记点:`__bss_end = .;`,形成以下格式: ```C .bss : { /* the symbol ¡®_sbss¡¯ will be defined at the bss section start */ _sbss = .; __bss_start__ = _sbss; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); /* the symbol ¡®_ebss¡¯ will be defined at the bss section end */ _ebss = .; __bss_end__ = _ebss; } >RAM __bss_end = .; ``` 这样,在工程的C代码中,就拥有了一个整形变量`__bss_end`,指向了整个业务逻辑静态内存分配最后的位置。 #### 编辑board.h 编辑`board.h`文件,追加以下代码: ```C #define GD32L23x_SRAM_SIZE (32) #define GD32L23x_SRAM_END (0x20000000 + GD32L23x_SRAM_SIZE * 1024) #define HEAP_END GD32L23x_SRAM_END #if defined(__ARMCC_VERSION) extern int Image$$RW_IRAM1$$ZI$$Limit; #define HEAP_BEGIN ((void *)&Image$$RW_IRAM1$$ZI$$Limit) #elif __ICCARM__ #pragma section="CSTACK" #define HEAP_BEGIN (__segment_end("CSTACK")) #else extern int __bss_end; #define HEAP_BEGIN ((void *)&__bss_end) #endif ``` 以上,关于ARMCC和IAR编译器的宏定义,可以忽略,关注GCC编译器的`#define HEAP_BEGIN ((void *)&__bss_end)`定义。 #### 修改board.c文件 修改`rt_hw_board_init()`函数中的`rt_system_heap_init`调用: ```C #if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP) rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END); #endif ``` 以上即可使用`HEAP_BEGIN`和`HEAP_END`两个宏,可弹性伸缩地最大化利用堆空间。 ### 实现FinSH串口命令行 RTT内核重要功能之一就是FinSH命令行,用户可以在命令行中方便地观察线程和内存使用情况、观察内核对象(如定时器、信号量、消息队列等)、使用日志,以及使用命令行进行函数调用。 #### 实现命令行输出 1. 将串口初始化函数移动到`board.c`中的`rt_hw_board_init`函数中。 2. 实现`rt_hw_console_output`函数 3. 确保串口TC中断有正确的响应和标志位清除,参考[[03-UART#中断类型|GD32中的UART中断类型]]。 4. 打开`rtconfig.h`中的`RT_USING_CONSOLE`宏定义 ```C void print(const char* c) { while (RESET == usart_flag_get(USART0, USART_FLAG_TBE)); usart_data_transmit(USART0, *c); } void rt_hw_console_output(const char *str) { rt_size_t i = 0, size = 0; char a = '\r'; size = rt_strlen(str); for (i = 0; i < size; i++) { if (*(str + i) == '\n') { print(&a); } print(str + i); } } void USART0_IRQHandler(void) { if (RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) { /* read one byte from the receive data register */ // usart_flag_clear(USART1,USART_FLAG_RBNE); usart_data_receive(USART0); usart_interrupt_flag_clear(USART0, USART_INT_FLAG_RBNE); } if (RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_TC)) { /* write one byte to the transmit data register */ usart_interrupt_flag_clear(USART0, USART_INT_FLAG_TC); } } ``` 完成以上工作以后,即可以在用户逻辑中调用`rt_kprintf`函数进行字符串格式化输出到串口了,同时在MCU复位后,可以在串口命令行中观察到RTT的LOGO输出。 #### FinSH组件 1. 添加`libraries/rt-thread/components/finsh`路径到工程的头文件搜索空间 2. 将`libraries/rt-thread/components/finsh`路径下的C文件添加到工程编译空间 3. 在`rtconfig.h`中引入`finsh_config.h`头文件 ##### 修改链接文件 RTT的FinSH组件由一个后台线程管理串口的输入输出,该线程使用RTT的自动初始化机制`INIT_APP_EXPORT(finsh_system_init);`进行线程的启动,因此需要打通RTT的自动初始化机制。 RTT的自动初始化宏会寻找分区表中`.rti_fn.`等区域,而GD32L23X的链接脚本是不包含这部分内容的,参考其他RTT BSP工程,可以将以下代码追加到GD32L233工程链接文件的FLASH区域: ```C .text : { . = ALIGN(4); *(.text) *(.text*) *(.glue_7) *(.glue_7t) *(.eh_frame) KEEP (*(.init)) KEEP (*(.fini)) /* section information for finsh shell */ . = ALIGN(4); __fsymtab_start = .; KEEP(*(FSymTab)) __fsymtab_end = .; . = ALIGN(4); __vsymtab_start = .; KEEP(*(VSymTab)) __vsymtab_end = .; /* section information for initial. */ . = ALIGN(4); __rt_init_start = .; KEEP(*(SORT(.rti_fn*))) __rt_init_end = .; . = ALIGN(4); PROVIDE(__ctors_start__ = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array)) PROVIDE(__ctors_end__ = .); . = ALIGN(4); /* the symbol ¡®_etext¡¯ will be defined at the end of code section */ _etext = .; } >FLASH ``` ##### 使用信号量和串口中断实现命令行输入 FinSH默认在21号优先级的线程上不断调用`rt_hw_console_getchar`函数,侦听输入字节,因此可以使用信号量的方式来阻塞FinSH线程让渡给其他低优先级线程,参考一下代码: ```C int8_t rxChar = -1; char rt_hw_console_getchar(void){ if(RT_EOK == rt_sem_take(rx_sem, RT_WAITING_FOREVER)){ return rxChar; } return -1; } void USART0_IRQHandler(void) { if (RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) { /* read one byte from the receive data register */ // usart_flag_clear(USART1,USART_FLAG_RBNE); rxChar = usart_data_receive(USART0); rt_sem_release(rx_sem); usart_interrupt_flag_clear(USART0, USART_INT_FLAG_RBNE); } if (RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_TC)) { /* write one byte to the transmit data register */ usart_interrupt_flag_clear(USART0, USART_INT_FLAG_TC); } } ``` 这里使用的名为`rx_sem`的信号量,需要提前初始化。 还可以参考[官方文档](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-nano/finsh-port/an0045-finsh-port?id=%e4%b8%ad%e6%96%ad%e7%a4%ba%e4%be%8b),使用环形缓冲器的方式继续提高串口输入的响应性能。