RT-Thread作为一款国产系统,代码完全开源,借鉴修改后可以闭源,深受我等借鉴抄袭之人所爱。特别是其软件生态相当丰富,直接上手拿来用也挺好用的。官方维护的社区也挺好玩,有各种开发者贡献各种软件包进一步壮大rtt,可以预见未来在国产雄起的号召下会越来越好。
话说目前IoT领域所用的实时系统基本都是FreeRTOS、uCOS之类,这些都是国外货,而且各家公司研发设计能力不同,大多数情况下使用了RTOS设计的软件包(SDK)仅仅是能用,丝毫没有架构、便利、友好等特性,在别提有啥软件生态。这里咱不提大公司和那些知名度太高的芯片,就中小型企业出品的SDK,那开发便利性上就差了一截,但是如果用了RTT,就可以随便动动手指在界面选一下就能把别人写好的软件包跟新进来使用,这开发无疑会简单许多,而且rtt的架构设计也比较清晰和有意思。
需要赞一下rtt近些年来的思路,他们一直在提升开发者体验、降低入门难度,从menuconfig到现在的中文IDE工具,尤其使用STM32时,直接一键就能编译下载,国人真的有福了。相比国内很多公司研发人员的思维还停留在 “使用我们的产品就默认使用者必须要有相当的开发基础” 程度,我觉得这很不利于国内软件产品的推广和使用。当然有好就有坏,rtt各方面我都挺喜欢,但是也有很多不足之处,比如说如何重头移植rtt到一个新的芯片上,官方文档就没有说的很清楚,需要自行研究摸索了,所以这里本人就以把rtt移植到andes d1088核上作为例子,希望帮助更多的人移植rtt到起芯片中来。
首先是下载一套rtt的代码,怎么下载不多说,重点提示最新版本要在其github上下,其他路线都更新不一定及时,选个release的版本,开发分支那是没保证的尽量不要选。
然后看看rtt的内核基础介绍,了解一下rtt系统工作的原理,这会方便咱移植。
再看看内核移植的篇章,大概了解一下要干些啥,虽然写的也不怎么清楚~~~
接下来就是对代码进行操作了,首先要做得就是内核移植,直白说就是适配rtt的任务调度机制。
先打开libcpu看看有没有现成的代码,显然没有任何andes相关的cpu核被适配,那么就需要我们自力更生实现了。
观察文档,一共需要实现这样的一些接口和变量:
函数和变量 | 描述 |
---|---|
rt_base_t rt_hw_interrupt_disable(void); | 关闭全局中断 |
void rt_hw_interrupt_enable(rt_base_t level); | 打开全局中断 |
rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit); | 线程栈的初始化,内核在线程创建和线程初始化里面会调用这个函数 |
void rt_hw_context_switch_to(rt_uint32_t to); | 没有来源线程的上下文切换,在调度器启动第一个线程的时候调用,以及在 signal 里面会调用 |
void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to); | 从 from 线程切换到 to 线程,用于线程和线程之间的切换 |
void rt_hw_context_switch_interrupt(rt_uint32_t from, rt_uint32_t to); | 从 from 线程切换到 to 线程,用于中断里面进行切换的时候使用 |
rt_uint32_t rt_thread_switch_interrupt_flag; | 表示需要在中断里进行切换的标志 |
rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread; | 在线程进行上下文切换时候,用来保存 from 和 to 线程 |
所以,通过查看andes的cpu手册,我们逐一把这些适配即可,下面我挑核心代码举例。
首先是定义几个变量:
/* flag in interrupt handling */ volatile rt_uint32_t rt_interrupt_from_thread; volatile rt_uint32_t rt_interrupt_to_thread; volatile rt_uint32_t rt_thread_switch_interrupt_flag;
然后编写开关中断:
rt_base_t rt_hw_interrupt_disable(void) { unsigned int ulPSW = __nds32__mfsr(NDS32_SR_PSW); __nds32__gie_dis(); return ulPSW; } void rt_hw_interrupt_enable(rt_base_t level) { if (level & PSW_mskGIE) { __nds32__gie_en(); } }
然后编写线程栈初始化代码:
/* * Initialise the stack of a task. * * Stack Layout: *High|-----------------| *|$R30 (LP) | *|-----------------| *|$R29 (GP) | *|-----------------| *|$R28 (FP) | *|-----------------| *|$R15 | $R25| *|-----------------| *|$R10 | $R24| *|-----------------| *|.| *|.| *|-----------------| *|$R0| *|-----------------| *|$IFC_LP| *|-----------------| *|$LC/$LE/$LB| *|(ZOL)| *|-----------------| *|$IPSW| *|-----------------| *|$IPC| *|-----------------| *|Dummy word| ( Dummy word for 8-byte stack pointer alignment ) *|-----------------| *|$FPU| *|-----------------| *Low * */ rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit) { rt_uint32_t *stk; stk= (rt_uint32_t *)(stack_addr + sizeof(rt_uint32_t)); stk= (rt_uint32_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8); /* R0 ~ R30 registers */ *--stk = (rt_uint32_t) texit;/* R30 : $lp */ *--stk = (rt_uint32_t) &_SDA_BASE_;/* R29 : $GP */ *--stk = 0x1c;/* R28 */ *--stk = 0x19;/* R25 */ *--stk = 0x18;/* R24 */ *--stk = 0x17;/* R23 */ *--stk = 0x16;/* R22 */ *--stk = 0x15;/* R21 */ *--stk = 0x14;/* R20 */ *--stk = 0x13;/* R19 */ *--stk = 0x12;/* R18 */ *--stk = 0x11;/* R17 */ *--stk = 0x10;/* R16 */ *--stk = 0x0f;/* R15 */ *--stk = 0x0e;/* R14 */ *--stk = 0x0d;/* R13 */ *--stk = 0x0c;/* R12 */ *--stk = 0x0b;/* R11 */ *--stk = 0x0a;/* R10 */ *--stk = 0x09;/* R9 */ *--stk = 0x08;/* R8 */ *--stk = 0x07;/* R7 */ *--stk = 0x06;/* R6 */ *--stk = 0x05;/* R5 */ *--stk = 0x04;/* R4 */ *--stk = 0x03;/* R3 */ *--stk = 0x02;/* R2 */ *--stk = 0x01;/* R1 */ *--stk = (rt_uint32_t) parameter;/* R0 : Argument */ /* IFC system register */ *--stk = 0x00;/* IFC_LP */ /* IPSW and IPC system registers */ *--stk = (__nds32__mfsr(NDS32_SR_PSW) | portPSW_GIE | portPSW_CPL) & ~portPSW_IFCON;/* IPSW */ *--stk = (rt_uint32_t) tentry;/* IPC : First instruction PC of task */ /* FPU registers */ stk -= 32; return (rt_uint8_t *)stk; }
然后编写启动第一个任务的函数,那个to变量就是表示任务栈指针(sp),这里要将软件中断(Software interrupt)优先级设置为最低:
void rt_hw_context_switch_to(rt_uint32_t to) { rt_interrupt_to_thread = to; rt_interrupt_from_thread = 0; rt_thread_switch_interrupt_flag = 1; nds32_set_swi_lowest(); nds32_intc_swi_trigger(); __nds32__gie_en(); while (1) printf("never reach here"); }
在编写运行之后的任务切换函数,那个from和to变量表示的也是栈指针(sp),目的是从from线程要切换到to线程去:
void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to) { if (1 != rt_thread_switch_interrupt_flag) { rt_interrupt_from_thread = from; rt_thread_switch_interrupt_flag = 1; } rt_interrupt_to_thread = to; nds32_intc_swi_trigger(); } void rt_hw_context_switch_interrupt(rt_uint32_t from, rt_uint32_t to) { rt_hw_context_switch(from, to); }
这3个任务切换函数都做了一件事,那就是保存好接下来要做的请求,然后触发swi中断(software interrupt)进行真正进行任务切换,swi中断处理我用汇编实现的,具体作用看我用注释标注的即可:
.global OS_SWI_Handler OS_SWI_Handler: /* push registers */ /* We enter here with the orginal $r28~$r30 (fp/gp/lp) is saved */ pushm $r0, $r25 mfsr $r1, $IPC mfsr $r2, $IPSW mfusr$r3, $IFC_LP pushm $r1, $r3 addi$sp,$sp,-8 fsdi.bi $fd15, [$sp], -8 fsdi.bi $fd14, [$sp], -8 fsdi.bi $fd13, [$sp], -8 fsdi.bi $fd12, [$sp], -8 fsdi.bi $fd11, [$sp], -8 fsdi.bi $fd10, [$sp], -8 fsdi.bi $fd9,[$sp], -8 fsdi.bi $fd8,[$sp], -8 fsdi.bi $fd7,[$sp], -8 fsdi.bi $fd6,[$sp], -8 fsdi.bi $fd5,[$sp], -8 fsdi.bi $fd4,[$sp], -8 fsdi.bi $fd3,[$sp], -8 fsdi.bi $fd2,[$sp], -8 fsdi.bi $fd1,[$sp], -8 fsdi$fd0,[$sp + 0] /* save current task's $sp */ la $r0, rt_interrupt_from_thread lwi $r1, [$r0] swi $sp, [$r1] movi $r0, 0x0 mtsr $r0, $INT_PEND! clean SWI pending la$r1, rt_thread_switch_interrupt_flag! get rt_thread_switch_interrupt_flag swi$r0, [$r1]! clear rt_thread_switch_interrupt_flag to 0 /* use new stack pointer */ la $r0, rt_interrupt_to_thread lwi $r1, [$r0] lwi $sp, [$r1] /* pop registers */ fldi.bi $fd0,[$sp], 8 fldi.bi $fd1,[$sp], 8 fldi.bi $fd2,[$sp], 8 fldi.bi $fd3,[$sp], 8 fldi.bi $fd4,[$sp], 8 fldi.bi $fd5,[$sp], 8 fldi.bi $fd6,[$sp], 8 fldi.bi $fd7,[$sp], 8 fldi.bi $fd8,[$sp], 8 fldi.bi $fd9,[$sp], 8 fldi.bi $fd10, [$sp], 8 fldi.bi $fd11, [$sp], 8 fldi.bi $fd12, [$sp], 8 fldi.bi $fd13, [$sp], 8 fldi.bi $fd14, [$sp], 8 fldi.bi $fd15, [$sp], 8 setgie.d dsb popm $r1, $r3 mtusr$r3, $IFC_LP mtsr $r1, $IPC mtsr $r2, $IPSW popm $r0, $r25 popm $r28,$r30 iret
以上就完成了任务调度的适配,接下来需要编写启动的汇编代码:
.global _start _start: ! Initialize the registers used by the compiler nds32_init ! disable cache mfsr$r0, $CACHE_CTL li$r1, ~(CACHE_CTL_mskIC_EN | CACHE_CTL_mskDC_EN) and$r0, $r0, $r1 mtsr$r0, $CACHE_CTL ! System reset handler balentry
_start是我们的程序启动入口点,也需要在链接脚本里面标明,同时还要在链接脚本.text段标注如下几个section:
/* section information for finsh shell */ . = ALIGN(4); PROVIDE(__fsymtab_start = .); KEEP(*(FSymTab)) PROVIDE(__fsymtab_end = .); . = ALIGN(4); PROVIDE(__vsymtab_start = .); KEEP(*(VSymTab)) PROVIDE(__vsymtab_end = .); /* section information for initial. */ . = ALIGN(4); PROVIDE(__rt_init_start = .); KEEP(*(SORT(.rti_fn* ))) PROVIDE(__rt_init_end = .); . = ALIGN(4); PROVIDE(__rtatcmdtab_start = .); KEEP(*(RtAtCmdTab)) PROVIDE(__rtatcmdtab_end = .); . = ALIGN(4);
rtt有个比较好的机制,那就通过定义这几个section,利用一组宏定义的函数就会放入其中,然后在系统的不同阶段会被调用,达到自动调用的目的。其shell命令也是如此实现的,所以此类函数你直接搜调用是搜不到的,需要注意。
从上面的编写可以看到,系统启动之后首先执行到_start处,然后就会跳转到entry函数里,entry函数是rtt里面已经实现了的,entry会做各种初始化,包括创建一个会调用main函数的线程,最后启动线程调度。
整个的启动流程大概如下:_start ---> entry ---> ... ---> rt_hw_board_init ---> ... ---> rt_system_scheduler_start ---> main。
rt_hw_board_init函数是rtt预留的必现要实现的接口,一般在这里要实现tick机制的配置、堆的配置等,大概可以参考我下面实现:
static void eswin_os_tick_handler(void) { eswin_clear_tick_interrupt(); /* enter interrupt */ rt_interrupt_enter(); rt_tick_increase(); /* leave interrupt */ rt_interrupt_leave(); } static void eswin_os_clk_config(void) { arch_irq_unmask(VECTOR_NUM_PIT1); /*Set timeras system tick by default*/ drv_pit_ioctrl(DRV_PIT_CHN_6, DRV_PIT_CTRL_SET_COUNT, CHIP_CLOCK_APB / RT_TICK_PER_SECOND); drv_pit_ioctrl(DRV_PIT_CHN_6, DRV_PIT_CTRL_INTR_ENABLE, DRV_PIT_INTR_ENABLE); drv_pit_ioctrl(DRV_PIT_CHN_6, DRV_PIT_CTRL_CH_MODE_SET, DRV_PIT_CH_MODE_SET_TIMER); /*tick ISR init*/ arch_irq_clean(VECTOR_NUM_PIT1); arch_irq_unmask(VECTOR_NUM_PIT1); arch_irq_register(VECTOR_NUM_PIT1, eswin_os_tick_handler); /* start timer */ drv_pit_ioctrl(DRV_PIT_CHN_6, DRV_PIT_CTRL_CH_MODE_ENABLE, DRV_PIT_CH_MODE_ENABLE_TIMER); } void rt_hw_board_init(void) { eswin_sdk_init(); eswin_io_config(); #ifdef RT_USING_HEAP rt_system_heap_init((void *)eswin_memheap[0].start_addr, (void *)((rt_uint8_t *)eswin_memheap[0].start_addr + (rt_size_t)eswin_memheap[0].size)); #ifdef RT_USING_MEMHEAP int i; for (i = 1; i < ESWIN_MEMHEAP_CNT; i++) { rt_memheap_init(eswin_memheap[i].memheap, eswin_memheap[i].name, eswin_memheap[i].start_addr, eswin_memheap[i].size); } #endif /* RT_USING_MEMHEAP */ #endif /* RT_USING_HEAP */ #ifdef RT_USING_COMPONENTS_INIT rt_components_board_init(); #endif #ifdef RT_USING_CONSOLE rt_console_set_device(RT_CONSOLE_DEVICE_NAME); #endif eswin_os_clk_config(); }
特别需要说一下堆的配置,如果只是一段连续内存做堆,那么直接定义“RT_USING_HEAP”宏,然后使用“rt_system_heap_init”配置即可;如果是几段不连续的内存,那么需要定义“RT_USING_MEMHEAP”宏,然后先使用“rt_system_heap_init”配置第一段内存,然后使用“rt_memheap_init”配置剩下的几段内存,最后还要定义“RT_USING_MEMHEAP_AS_HEAP”宏和“RT_USING_MEMHEAP_AUTO_BINDING”宏。这个具体使用方法内核介绍的内存管理里没有说的很明白,我也是大概看了一遍其内存管理代码才搞明白用法的。。。
这里我感觉还有个不太合理的地方,就是rtt显然没有考虑只有在开始任务调度时才让启用tick(虽然这时候是关闭中断状态)。所以我觉得它至少应该增加个scheduler start hook机制,这样我就可以在scheduler start hook时才启动tick计时器就会比较合理了。
到这里内核移植完成了,系统可以正常运行并进行调度了,但是目前没有适配任何外设驱动,所以就连个打印都不行,所以接下来咱适配一个串口驱动,以方便打印和使用shell。
当然先是看看rtt的uart设备和驱动的介绍,uart有v1和v2之分,这里仅拿v1做适配。
const struct rt_uart_ops eswin_uart_ops = { eswin_drv_uart_configure, eswin_drv_uart_control, eswin_drv_uart_putc, eswin_drv_uart_getc, eswin_drv_uart_dma_transmit }; int eswin_hw_uart_init(void) { struct rt_serial_device *serial; struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT; struct eswin_device_uart *uart; #ifdef BSP_USING_UART0 { serial= &eswin_serial0; uart= &eswin_uart0; serial->ops= &eswin_uart_ops; serial->config= config; serial->config.baud_rate = 115200; rt_hw_serial_register(serial, "uart0", RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX, uart); } #endif /* BSP_USING_UART0 */ return 0; } INIT_BOARD_EXPORT(eswin_hw_uart_init);
可以看出这里需要用“INIT_BOARD_EXPORT”去定义串口设备注册函数,他就会被rtt自动调用,ops主要实现configure、putc、getc就可以满足日常的打印和shell使用了,其他的可以日后根据需要再去适配。
static int eswin_drv_uart_putc(struct rt_serial_device *serial, char c) { struct eswin_device_uart *uart; RT_ASSERT(serial != RT_NULL); uart = (struct eswin_device_uart *)serial->parent.user_data; RT_ASSERT(uart != RT_NULL); while (!(uart->uart_device->LSR & ESWIN_DRV_UART_LSR_THRE)); uart->uart_device->Mux0.THR = c; return (1); } static int eswin_drv_uart_getc(struct rt_serial_device *serial) { int ch; struct eswin_device_uart *uart; RT_ASSERT(serial != RT_NULL); uart = (struct eswin_device_uart *)serial->parent.user_data; RT_ASSERT(uart != RT_NULL); ch = -1; if (uart->uart_device->LSR & ESWIN_DRV_UART_LSR_RDR) ch = (int)uart->uart_device->Mux0.RBR; return ch; }
至此,系统就可以跑起来进行串口打印,并进行shell交互了。
但是我并没有满足,因为我目前使用的这款芯片是一款WiFi6的IoT芯片,正好rtt也是有WLAN框架的,所以顺便也对wifi驱动也做个适配。
rtt自带lwip协议栈,所以我们仅需要对接自己的wifi mac层协议栈和wpa_supplicant控制程序,这里我选择rtt自带的lwip-2.1.2版本。
先使用“INIT_DEVICE_EXPORT”宏进行wifi设备的注册:
int eswin_hw_wifi_init(void) { rt_err_t ret; LOG_D("F:%s L:%d", __FUNCTION__, __LINE__); rt_memset(&wifi_sta, 0, sizeof(wifi_sta)); rt_memset(&wifi_ap,0, sizeof(wifi_ap)); ret= rt_wlan_dev_register(&wifi_sta, RT_WLAN_DEVICE_STA_NAME, &ops, 0, NULL); ret |= rt_wlan_dev_register(&wifi_ap,RT_WLAN_DEVICE_AP_NAME,&ops, 0, NULL); return ret; //RT_EOK; } INIT_DEVICE_EXPORT(eswin_hw_wifi_init);
可以看到,wifi驱动的对接主要也在这组ops的实现上,所以适配这组ops即可完成目的
static const struct rt_wlan_dev_ops ops = { .wlan_init= drv_wlan_init, .wlan_mode= drv_wlan_mode, .wlan_scan= drv_wlan_scan, .wlan_join= drv_wlan_join, .wlan_softap= drv_wlan_softap, .wlan_disconnect= drv_wlan_disconnect, .wlan_ap_stop= drv_wlan_ap_stop, .wlan_ap_deauth= drv_wlan_ap_deauth, .wlan_scan_stop= drv_wlan_scan_stop, .wlan_get_rssi= drv_wlan_get_rssi, .wlan_set_powersave= drv_wlan_set_powersave, .wlan_get_powersave= drv_wlan_get_powersave, .wlan_cfg_promisc= drv_wlan_cfg_promisc, .wlan_cfg_filter= drv_wlan_cfg_filter, .wlan_set_channel= drv_wlan_set_channel, .wlan_get_channel= drv_wlan_get_channel, .wlan_set_country= drv_wlan_set_country, .wlan_get_country= drv_wlan_get_country, .wlan_set_mac= drv_wlan_set_mac, .wlan_get_mac= drv_wlan_get_mac, .wlan_recv= drv_wlan_recv, .wlan_send= drv_wlan_send, };
发送数据对接的是ops中的.wlan_send,接收并不是对接.wlan_recv,而是在接收到数据后上报给wlan框架:
int wifi_lwip_input(void *buf, net_if_t *net_if, void *addr, uint16_t len, net_buf_free_fn free_fn) { rt_err_t err = -RT_ERROR; if (!trap_l2_pkt(fhost_get_idx_by_netif(net_if), addr, len)) { free_fn(buf); return RT_EOK; } if (net_if == wifi_sta.netdev->user_data) { err = rt_wlan_dev_report_data(&wifi_sta, (void *)addr, len); } else { err = rt_wlan_dev_report_data(&wifi_ap, (void *)addr, len); } free_fn(buf); return err ? err : RT_EOK; }
当然还要对接wifi的事件状态:
static sys_err_t eswin_system_event_callback(void *ctx, system_event_t *event) { struct rt_wlan_info sta; struct rt_wlan_buff buff; buff.data = &sta; buff.len = sizeof(sta); if (SYSTEM_EVENT_STA_CONNECTED == event->event_id) { rt_wlan_dev_indicate_event_handle(&wifi_sta, RT_WLAN_DEV_EVT_CONNECT, RT_NULL); } else if (SYSTEM_EVENT_STA_DISCONNECTED == event->event_id) { rt_wlan_dev_indicate_event_handle(&wifi_sta, RT_WLAN_DEV_EVT_DISCONNECT, RT_NULL); } else if (SYSTEM_EVENT_STA_4WAY_HS_FAIL == event->event_id) { rt_wlan_dev_indicate_event_handle(&wifi_sta, RT_WLAN_DEV_EVT_CONNECT_FAIL, RT_NULL); } else if (SYSTEM_EVENT_AP_START == event->event_id) { rt_wlan_dev_indicate_event_handle(&wifi_ap, RT_WLAN_DEV_EVT_AP_START, RT_NULL); } else if (SYSTEM_EVENT_AP_STOP == event->event_id) { rt_wlan_dev_indicate_event_handle(&wifi_ap, RT_WLAN_DEV_EVT_AP_STOP, RT_NULL); } else if (SYSTEM_EVENT_AP_STACONNECTED == event->event_id) { rt_memcpy(sta.bssid, event->event_info.sta_connected.mac, 6); rt_wlan_dev_indicate_event_handle(&wifi_ap, RT_WLAN_DEV_EVT_AP_ASSOCIATED, &buff); } else if (SYSTEM_EVENT_AP_STADISCONNECTED == event->event_id) { rt_memcpy(sta.bssid, event->event_info.sta_connected.mac, 6); rt_wlan_dev_indicate_event_handle(&wifi_ap, RT_WLAN_DEV_EVT_AP_DISASSOCIATED, &buff); } }
至此就完成了wifi驱动的适配,但是目前rtt的wlan框架还不是很成熟,很多地方都不太合理,所以在适配过程中我进行了一顿魔改,哈哈哈~~~
最后只要设置下wifi模式就可以使用wifi了。
int main(void) { /* set wifi work mode */ rt_wlan_set_mode(RT_WLAN_DEVICE_STA_NAME,&RT_WLAN_STATION); rt_wlan_set_mode(RT_WLAN_DEVICE_AP_NAME,&&RT_WLAN_AP); return 0; }
展示几张最终效果图: