9. RTTHREAD移植新的BSP开发板
9.1. 简介
你是否有一款新的开发板,RTTHREAD Master分支上还没有支持?
这篇文章简单介绍一下如何想要一款新的开发板或者芯片支持RTTHREAD
一般有三种难度
场景一,如果你的开发板上的MCU,已经有类似的板子已经支持好了,比如STM32系列,可以参考STM32 BSP制作
场景二,如果你的开发板上面的MCU 并没有相关的支持,但是内核和指令集有相应的支持,这个难度也还可以,这种场景是本文讲的主要内容,这种比如MCU都是ARM架构的,内核是M4的,像这类MCU 也是比较方便移植,当然记录这篇文章也是方便后续其他人移植。因为这类芯片汇编代码等都移植好了,所以移植起来相对来说比较方便,但是又不是特别简单,所以这篇文章记录一下。
场景三, 你的开发板上的MCU,即没有相关的内核,也没有对应的汇编文件,这种就难度比较大了。这边这次不做介绍,下次有机会接触对应的芯片的时候,介绍
9.2. 移植前准备
本文场景是一颗内核架构已经在RTTHREAD上面支持了,例如MAX32660,这是一颗ARM-M4F的内核,但是这颗芯片RTTHREAD上还没有支持起来。
首先你得先具备熟悉以下两方面,芯片方面和RTTHREAD方面
芯片方面
我们得先熟悉当前芯片的官方服务例程如何跑的,找到具体的sample和一些网上的资料,分以下一些方面
官方服务例程,这边MAX是eclipse,熟悉eclipse
keil pack 里面的服务例程,这个往往大家会忽略,其实有些厂商支持keil pack装好之后里面都有一些服务例程,熟悉keil的朋友用起来会比较方便。网址
对于移植主要了解以下几个例程,Systick(其他定时器也可以,了解一款),UART(console需要), GPIO(点亮小灯需要) ,初期把整个框架搭起来,console可以使用,了解这几个例程就可以了,
RTTHREAD方面
rtthread方面有以下文档需要熟悉
首先你要对RTTHREAD用法比较熟悉一些,
官方文档地址:RTTHREAD文档中心
RTTHREAD支持的最好的是STM32系列,如果你有一款STM32的开发板,可以先跑一下熟悉一下。
和本文相关的一些文档下面我先列出来:
9.3. 移植步骤
这边我讲下我的移植顺序
点亮小灯
官方服务例程里面通常会有GPIO点亮小灯的例程,通常有开GPIO时钟,设置GPIO管脚的功能为输出,然后就可以闪灯了。找到这几步的函数。
熟悉Systick例程
熟悉下Systick服务例程,定时器如何定时的触发,这个通常是CMSIS接口设置的,一般是通用的,不排除有些特殊配置,根据官方给的例程来就好了。
可以编译通过的bsp
这边可以找STM32F4或者L4系列都可以,拷贝到你需要的文件夹下面
对于keil,可以先改下template.uvprojx 文件,把target改成你对应的开发板
改完之后需要修改rtconfig.h来适应一些配置
这边提供一个最小的rtconfig.h来方便下次配置
#ifndef RT_CONFIG_H__
#define RT_CONFIG_H__
/* Automatically generated file; DO NOT EDIT. */
/* RT-Thread Configuration */
/* RT-Thread Kernel */
#define RT_NAME_MAX 8
#define RT_ALIGN_SIZE 4
#define RT_THREAD_PRIORITY_32
#define RT_THREAD_PRIORITY_MAX 32
#define RT_TICK_PER_SECOND 100
#define RT_USING_IDLE_HOOK
#define RT_IDLE_HOOK_LIST_SIZE 4
#define IDLE_THREAD_STACK_SIZE 256
#define RT_USING_TIMER_SOFT
#define RT_TIMER_THREAD_PRIO 4
#define RT_TIMER_THREAD_STACK_SIZE 512
/* Inter-Thread communication */
#define RT_USING_SEMAPHORE
#define RT_USING_MUTEX
/* Memory Management */
#define RT_USING_MEMPOOL
#define RT_USING_SMALL_MEM
#define RT_USING_HEAP
/* Kernel Device Object */
#define RT_USING_DEVICE
#define RT_USING_CONSOLE
#define RT_CONSOLEBUF_SIZE 128
#define RT_CONSOLE_DEVICE_NAME "uart1"
#define RT_VER_NUM 0x40003
/* RT-Thread Components */
#define RT_USING_COMPONENTS_INIT
#define RT_USING_USER_MAIN
#define RT_MAIN_THREAD_STACK_SIZE 2048
#define RT_MAIN_THREAD_PRIORITY 10
/* C++ features */
/* Command shell */
#define RT_USING_FINSH
#define FINSH_THREAD_NAME "tshell"
#define FINSH_USING_HISTORY
#define FINSH_HISTORY_LINES 5
#define FINSH_USING_SYMTAB
#define FINSH_USING_DESCRIPTION
#define FINSH_THREAD_PRIORITY 20
#define FINSH_THREAD_STACK_SIZE 4096
#define FINSH_CMD_SIZE 80
#define FINSH_USING_MSH
#define FINSH_USING_MSH_DEFAULT
#define FINSH_USING_MSH_ONLY
#define FINSH_ARG_MAX 10
/* Device virtual file system */
/* Device Drivers */
#define RT_USING_DEVICE_IPC
#define RT_PIPE_BUFSZ 512
#define RT_USING_SERIAL
#define RT_SERIAL_USING_DMA
#define RT_SERIAL_RB_BUFSZ 64
#define SOC_MAX32660
#define SOC_MAXIM
/* On-chip Peripheral Drivers */
#define BSP_USING_UART
#define BSP_USING_UART1
#endif
最后两个是需要手动添加的,后面可以手动添加。
scons --target=mdk5
,然后打开project, 删除所有的和芯片相关的内容,保留RTTHREAD内核相关的内容,编译完成之后,根据GPIO里面的需要的文件,添加到RTTHREAD里面
通常对于ARM CORTEX-M4来讲,需要库里面的两个startup文件即可,
- system_max32660.c 这个是C语言写的SystemInit函数
- startup_max32660.s 这个是汇编写的启动函数
这两个文件基本都是不需要修改的。
剩下的就是一个board.c和application.c
application.c
参考demo
/*
* Copyright (c) 2006-2020, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2020-04-29 supperthomas first version
*
*/
#include <rtthread.h>
#include <rtdevice.h>
int main(void)
{
int count = 1;
while (count++)
{
rt_thread_mdelay(500);
}
return RT_EOK;
}
board.c 参考demo
/*
* Copyright (c) 2006-2020, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2020-04-29 supperthomas first version
*
*/
#include <rtthread.h>
#include <rthw.h>
#include "board.h"
/**
* This is the timer interrupt service routine.
*
*/
void SysTick_Handler(void)
{
/* enter interrupt */
rt_interrupt_enter();
rt_tick_increase();
/* leave interrupt */
rt_interrupt_leave();
}
void rt_hw_systick_init(void)
{
/* enter interrupt */
rt_interrupt_enter();
rt_tick_increase();
/* leave interrupt */
rt_interrupt_leave();
}
void rt_hw_board_init(void)
{
// rt_hw_interrupt_enable(0);
// sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE);
/* Activate deep sleep mode */
rt_hw_systick_init();
//#if defined(RT_USING_HEAP)
// rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
//#endif
//#ifdef RT_USING_SERIAL
// rt_hw_uart_init();
//#endif
//#ifdef RT_USING_CONSOLE
// rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
//#endif
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
}
board.h参考demo
/*
* Copyright (c) 2006-2020, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2020-04-29 supperthomas first version
*/
#ifndef _BOARD_H_
#define _BOARD_H_
#include <rtthread.h>
#include <rthw.h>
#if defined(__CC_ARM) || defined(__CLANG_ARM)
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
#define HEAP_END (0x20001000)
void rt_hw_board_init(void);
#endif
上面3个文件都是可以直接编译通过的,根据GPIO例程,添加对应的一些.c文件即可。
这一步保证编译通过即可。
调试systick
systick可以是ARM里面的systick,也可以使用TIMER来代替。
TIMER分两步走,先配置时钟,设置timer的触发tick间隔,然后设置中断服务例程。
这个在官方服务例程中找对应的初始化,放到函数rt_hw_systick_init()中,
这个里面把timer设置成RT_TICK_PER_SECOND即可,这个通常是1000或者100,代表1s, timer起来1000次,1ms一次。
中断服务例程
systick有专门的服务例程SysTick_Handler,根据startup_max32660.s的中断向量表来的,其他TIMER可以找到对应的服务例程。
这边demo中已经写好了,只需要调rt_tick_increase函数即可。
这个时候编译通过,通过DEBUG 断点到main里面的while循环,看是否是0.5s触发一次断点即可。
如果这里调通了证明系统起来就快了。
如果这里调不通,需要整理main的入口等关系问题,
添加小灯GPIO
这个时候就可以点亮一个小灯来看看了。GPIO点亮小灯根据厂商给的demo来设置。无外乎是初始化GPIO然后翻转GPIO。这个时候可以根据厂商的demo来添加相关的文件,可能要添加库文件
添加到main函数里面,看下效果就好了,这一步编译完成之后,可以看到小灯一闪一闪亮晶晶了。这边基本系统已经跑起来了。
添加drv_uart.c
GPIO其实主要方便来看演示效果的,当然rtthread有个非常好的shell系统finsh,这个需要把drv_uart.c support起来就可以用了,当然如果你只用个内核不用finsh的话,也是可以的,但是通常上传到master分支上最好有个finsh功能,方便后面人在你的基础上继续完善其他功能。
这边提供一个可以编译通过的drv_uart.c
/*
* Copyright (c) 2006-2018, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2021-02-08 Supperthomas first version
*/
#include "board.h"
#include "uart.h"
#include "rtdevice.h"
#define UART0_CONFIG \
{ \
.name = "uart0", \
.Instance = MXC_UART_GET_UART(0), \
.irq_type = MXC_UART_GET_IRQ(0), \
}
#define UART1_CONFIG \
{ \
.name = "uart1", \
.Instance = MXC_UART_GET_UART(1), \
.irq_type = MXC_UART_GET_IRQ(1), \
}
struct mcu_uart_config
{
const char *name;
mxc_uart_regs_t *Instance;
IRQn_Type irq_type;
};
struct mcu_uart
{
mxc_uart_regs_t handle;
struct mcu_uart_config *config;
rt_uint16_t uart_dma_flag;
struct rt_serial_device serial;
};
#ifdef RT_USING_SERIAL
//#define DRV_DEBUG
//#define LOG_TAG "drv.usart"
//#include <drv_log.h>
#if !defined(BSP_USING_UART0) && !defined(BSP_USING_UART1)
#error "Please define at least one BSP_USING_UARTx"
/* this driver can be disabled at menuconfig -> RT-Thread Components -> Device Drivers */
#endif
enum
{
#ifdef BSP_USING_UART0
UART0_INDEX,
#endif
#ifdef BSP_USING_UART1
UART1_INDEX,
#endif
};
static struct mcu_uart_config uart_config[] =
{
#ifdef BSP_USING_UART0
UART0_CONFIG,
#endif
#ifdef BSP_USING_UART1
UART1_CONFIG,
#endif
};
static struct mcu_uart uart_obj[sizeof(uart_config) / sizeof(uart_config[0])] = {0};
static rt_err_t mcu_configure(struct rt_serial_device *serial, struct serial_configure *cfg)
{
struct mcu_uart *uart;
RT_ASSERT(serial != RT_NULL);
RT_ASSERT(cfg != RT_NULL);
return RT_EOK;
}
static rt_err_t mcu_control(struct rt_serial_device *serial, int cmd, void *arg)
{
struct mcu_uart *uart;
RT_ASSERT(serial != RT_NULL);
uart = rt_container_of(serial, struct mcu_uart, serial);
switch (cmd)
{
/* disable interrupt */
case RT_DEVICE_CTRL_CLR_INT:
/* disable rx irq */
NVIC_DisableIRQ(uart->config->irq_type);
/* disable interrupt */
break;
/* enable interrupt */
case RT_DEVICE_CTRL_SET_INT:
/* enable rx irq */
/* enable interrupt */
break;
case RT_DEVICE_CTRL_CLOSE:
break;
}
return RT_EOK;
}
static int mcu_putc(struct rt_serial_device *serial, char c)
{
struct mcu_uart *uart;
RT_ASSERT(serial != RT_NULL);
uart = rt_container_of(serial, struct mcu_uart, serial);
//uart->handle.Instance->DR = c;
return 1;
}
static int mcu_getc(struct rt_serial_device *serial)
{
int ch;
struct mcu_uart *uart;
RT_ASSERT(serial != RT_NULL);
uart = rt_container_of(serial, struct mcu_uart, serial);
ch = -1;
//if (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_RXNE) != RESET)
// {
// ch = uart->handle.Instance->DR & 0xff;
// }
return ch;
}
static const struct rt_uart_ops mcu_uart_ops =
{
.configure = mcu_configure,
.control = mcu_control,
.putc = mcu_putc,
.getc = mcu_getc,
};
int rt_hw_usart_init(void)
{
rt_size_t obj_num = sizeof(uart_obj) / sizeof(struct mcu_uart);
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
rt_err_t result = 0;
for (int i = 0; i < obj_num; i++)
{
/* init UART object */
uart_obj[i].config = &uart_config[i];
uart_obj[i].serial.ops = &mcu_uart_ops;
uart_obj[i].serial.config = config;
/* register UART device */
result = rt_hw_serial_register(&uart_obj[i].serial, uart_obj[i].config->name,
RT_DEVICE_FLAG_RDWR
| RT_DEVICE_FLAG_INT_RX
| RT_DEVICE_FLAG_INT_TX
| uart_obj[i].uart_dma_flag
, NULL);
RT_ASSERT(result == RT_EOK);
}
return result;
}
//INIT_BOARD_EXPORT(rt_hw_usart_init);
#endif /* RT_USING_SERIAL */
这边简单讲解一下这些函数作用,你就大概知道如何支持uart了。
rt_hw_uart_init
rt_hw_usart_init
这个函数在rt_hw_board_init
里面systick初始化之后会调用
然后调用rt_console_set_device来设置console,这里是通过名称来关联的。
rt_hw_usart_init
这个函数并不负责初始化uart串口,只是把下面的uart的所有操作注册进去,
static const struct rt_uart_ops mcu_uart_ops =
{
.configure = mcu_configure,
.control = mcu_control,
.putc = mcu_putc,
.getc = mcu_getc,
};
putc和getc
这个比较好理解,就是你的uart驱动发送一个字节,和接收一个字节
这边通常是读取一个字节和发送一个字节的寄存器即可。这边不会涉及到中断等
mcu_configure
这个函数主要初始化uart寄存器,设置波特率等一些静态的参数
mcu_control
这里负责打开中断,开始接受串口数据
中断服务例程
中断服务例程需要通知rtthread去掉getc取数据
这边需要知道中断如何清中断,并且掉rt_hw_serial_isr函数来通知来取数据
void UART1_IRQHandler(void)
{
rt_interrupt_enter();
rt_hw_serial_isr(&(uart_obj[UART1_INDEX].serial), RT_SERIAL_EVENT_RX_IND);
uint32_t intst = 0;
intst = MXC_UART1->int_fl;
MXC_UART1->int_fl = intst;
rt_interrupt_leave();
}
这边可以先支持putc就是UART TX,先看到串口有数据发出来就可以了。之后比较难的就是UART RX了。
这部分要根据官方例程做一些实验,我这边是要知道UART 的IDLE中断什么时候触发,或者一些其他FIFO满中断如何触发。然后设置对应的寄存器即可。
UART调通之后,会看到串口可以打印响应的信息了
能达到这个效果就不错了,可以做很多事情了。
这边调好一个UART之后,可以试着调另外一个UART。把这个放到对应的.c里面即可或者放到application.c里面即可,我们通过命令来测试下即可,这边需要将UART TX RX两个引脚对连起来。
#include <rtthread.h>
// you can connect the uart tx with rx
#define SAMPLE_UART_NAME "uart0" /* ?????? */
#define UART_DATA_TEST_SIZE 10
static struct rt_semaphore rx_sem;
static rt_device_t serial;
static char tx_data[UART_DATA_TEST_SIZE];
static char rx_data[UART_DATA_TEST_SIZE];
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
rt_sem_release(&rx_sem);
return RT_EOK;
}
static void serial_thread_entry(void *parameter)
{
char ch;
int index=0;
while (1)
{
while (rt_device_read(serial, -1, &ch, 1) != 1)
{
rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
}
rx_data[index++] = ch;
rt_kprintf("\r\n transmit data:%x\r\n",ch);
if(index >=UART_DATA_TEST_SIZE)
{
rt_kprintf("\r\n transmit done\r\n");
index = 0;
break;
}
}
}
static int uart_sample(int argc, char *argv[])
{
rt_err_t ret = RT_EOK;
char uart_name[RT_NAME_MAX];
for(int i = 0;i< UART_DATA_TEST_SIZE;i++)
{
tx_data[i] = i;
}
if (argc == 2)
{
rt_strncpy(uart_name, argv[1], RT_NAME_MAX);
}
else
{
rt_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
}
serial = rt_device_find(uart_name);
if (!serial)
{
rt_kprintf("find %s failed!\n", uart_name);
return RT_ERROR;
}
rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
rt_device_set_rx_indicate(serial, uart_input);
rt_device_write(serial, 0, tx_data, UART_DATA_TEST_SIZE);
rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
if (thread != RT_NULL)
{
rt_thread_startup(thread);
}
else
{
ret = RT_ERROR;
}
return ret;
}
MSH_CMD_EXPORT(uart_sample, uart device sample);
PR工程
PR工程是个比较浩大的工程,需要了解bsp里面的每一个细节:
这边你可以学一下scons如何使用的来对对应的文件进行修改:SCons 构建工具
以及Kconfig语法:Kconfig 语法
Kconfig
Kconfig主要用来生成#define头文件,主要服务于rtconfig.h的,所以如果你之前手动在rtconfig.h里添加的宏定义如果想做的比较好的话,需要在对应的Kconfig里面添加。这个主要看你需要menuconfig在哪个目录下面,通常是bsp目录下面的Kconfig索引到其他的例如board/Kconfig
通常在board/Kconfig里面需要添加一些模块UART和GPIO
主要是改board/Kconfig
scons
这个就比较复杂了,耗时也比较多。
首先在你scons的目录下,通常bsp目录下面
SConstruct
这个文件是用来包含一些其他文件夹中的SConscript的
SConscript
所有你需要单独包含的文件夹,都需要用SConscript这个文件来索引该目录下面的需要的配置文件,
board下面的SConscript需要重点维护,其他如果官方的HAL库需要对应的文件,也是需要修改的。
rtconfig.py
这个文件用来修改编译器相关的一些配置,和选项的,这部分比较复杂一些,可以学习一下一些编译选项
link_scripts
这个文件夹里面主要存放的是link相关的一些配置,比如RAM地址和ROM地址
大概需要修改的就是这些文件,其他可能还有比较多,主要的离不开这几个,有些参数可以放到template里面现行设置,后期等找到合适的方式再继续整理。