RT-Thread Nano 提供了shell 控制台组件 FinSH ,对于 Nano 版本的 FinSH 组件,没有使用 RT-Thread 的设备驱动框架,所以移植起来比较简单,只要提供控制台的打印输出函数 rt_hw_console_output 和输入函数 rt_hw_console_getchar 即可。
我使用的硬件平台是 STM32F407ZGT6 ,移植前先准备可以正常运行 RT-Thread Nano 的工程源码。
对于控制台的输入输出硬件接口,我使用的是串口,所以需要对串口外设进行基本的初始化,并且开启串口外设的接收中断(输入使用串口中断方式)。
对于串口外设的初始化的代码,我使用的是CubeMX生成的初始化代码。我发现 CubeMX 生成的初始化代码,只是开启了使用的那个串口的总中断,还要自己单独写代码开启串口的接收中断的,不知道是不是我配置 CubeMX 的问题,反正花了点时间才发现问题。
另外,实现控制台打印需要确认 rtconfig.h 文件中已经定义了 RT_USING_CONSOLE 这个宏。
串口外设初始化代码如下:
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
/* 初始化串口接收数据的信号量 */
rt_sem_init(&(shell_rx_sem), "shell_rx", 0, 0);
/* USER CODE END USART1_MspInit 0 */
/* USART1 clock enable */
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USART1 interrupt Init */
HAL_NVIC_SetPriority(USART1_IRQn, 4, 0); // 中断优先级配置
HAL_NVIC_EnableIRQ(USART1_IRQn); // 使能串口1中断
huart1.Instance->SR &= ~(USART_SR_RXNE); // 清除RXNE标志位
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); // 使能串口接收中断
}
}
rt_hw_console_output 是控制台输出的一个函数,函数名需要保持不变。
UART_HandleTypeDef huart1;
void rt_hw_console_output(const char *str)
{
unsigned int i = 0, size = 0;
char a = '\r';
__HAL_UNLOCK(&huart1);
size = strlen(str);
for (i = 0; i < size; i++)
{
if (*(str + i) == '\n')
{
/* 当输出\n字符时,在\n字符之前先输出\r */
HAL_UART_Transmit(&huart1, (uint8_t *)&a, 1, 1);
}
HAL_UART_Transmit(&huart1, (uint8_t *)(str + i), 1, 1);
}
}
当实现了这个函数之后,启动 RT-Thread Nano 时,就可以在串口终端打印出启动是的 logo 和版本信息等内容了。如下图:

把 RT-Thread Nano 目录下的 components/finsh 目录,复制到 keil 工程目录中,然后把下面4个 C 文件添加到 keil 工程。

另外,rtconfig.h 配置文件中,需要使能 #define RT_USING_FINSH 宏定义,才能使用 FinSH 组件。
rt_hw_console_getchar 函数功能,就是控制台从串口中获取一个字符。
获取一个字符的方式可以采用查询方式。当然查询方式不能死等了(查询不到可读字符时,调用延时函数休眠10ms),因为需要让出CPU给其他线程运行的机会。
也可以采用中断方式(我这里采用中断方式),可以更有效的利用CPU。
中断方式实现原理:
当串口接收到数据产生接收中断时,在中断中把数据存入 ringbuffer 缓冲区中,然后释放信号量。而 shell 线程去获取信号量,可以获取到信号量则读取 ringbuffer 中的数据,没有数据则一直阻塞等待获取串口接收中断释放的信号量。
先把环形缓冲区的读写代码先实现了。
#define BUFFER_SIZE 1024 /* 环形缓冲区的大小 */
typedef struct
{
volatile unsigned int pR; /* 读地址 */
volatile unsigned int pW; /* 写地址 */
unsigned char buffer[BUFFER_SIZE]; /* 缓冲区空间 */
} ring_buffer;
/*
* 函数名:void ring_buffer_init(ring_buffer *dst_buf)
* 输入参数:dst_buf --> 指向目标缓冲区
* 输出参数:无
* 返回值:无
* 函数作用:初始化缓冲区
*/
void ring_buffer_init(ring_buffer *dst_buf)
{
dst_buf->pW = 0;
dst_buf->pR = 0;
}
/*
* 函数名:void ring_buffer_write(unsigned char c, ring_buffer *dst_buf)
* 输入参数:c --> 要写入的数据
* dst_buf --> 指向目标缓冲区
* 输出参数:无
* 返回值:无
* 函数作用:向目标缓冲区写入一个字节的数据,如果缓冲区满了就丢掉此数据
*/
void ring_buffer_write(unsigned char c, ring_buffer *dst_buf)
{
int i = (dst_buf->pW + 1) % BUFFER_SIZE;
if(i != dst_buf->pR) // 环形缓冲区没有写满
{
dst_buf->buffer[dst_buf->pW] = c;
dst_buf->pW = i;
}
}
/*
* 函数名:int ring_buffer_read(unsigned char *c, ring_buffer *dst_buf)
* 输入参数:c --> 指向将读到的数据保存到内存中的地址
* dst_buf --> 指向目标缓冲区
* 输出参数:无
* 返回值:读到数据返回0,否则返回-1
* 函数作用:从目标缓冲区读取一个字节的数据,如果缓冲区空了返回-1表明读取失败
*/
int ring_buffer_read(unsigned char *c, ring_buffer *dst_buf)
{
if(dst_buf->pR == dst_buf->pW)
{
return -1;
}
else
{
*c = dst_buf->buffer[dst_buf->pR];
dst_buf->pR = (dst_buf->pR + 1) % BUFFER_SIZE;
return 0;
}
}
串口接收中断要做的事情就是:把读取到的数据存放进 ringbuffer 中,然后释放信号量唤醒 shell 线程。
static ring_buffer uart1_rx_buf = {0, 0, {0}}; /* 串口接收 ringbuffer */
static struct rt_semaphore shell_rx_sem; /* 定义一个信号量 */
void USART1_IRQHandler(void)
{
int ch = -1;
/* enter interrupt */
rt_interrupt_enter(); //在中断中一定要调用这对函数,进入中断
if ((__HAL_UART_GET_FLAG(&(huart1), UART_FLAG_RXNE) != RESET) &&
(__HAL_UART_GET_IT_SOURCE(&(huart1), UART_IT_RXNE) != RESET))
{
while (1)
{
ch = -1;
if (__HAL_UART_GET_FLAG(&(huart1), UART_FLAG_RXNE) != RESET)
{
ch = huart1.Instance->DR & 0xff;
}
if (ch == -1)
{
break;
}
/* 读取到数据,将数据存入 ringbuffer */
ring_buffer_write(ch, &uart1_rx_buf);
}
rt_sem_release(&shell_rx_sem); // 释放信号量唤醒 shell 线程
}
/* leave interrupt */
rt_interrupt_leave(); //在中断中一定要调用这对函数,离开中断
}
rt_hw_console_getchar 函数是 shell 线程接收一个字符函数,从而实现命令行交互功能的。代码如下:
char rt_hw_console_getchar(void)
{
char ch = 0;
/* 从 ringbuffer 中拿出数据 */
while (ring_buffer_read((unsigned char *)&ch, &uart1_rx_buf) != 0)
{
/* 没有读取到数据,则一直阻塞等待 */
rt_sem_take(&shell_rx_sem, RT_WAITING_FOREVER);
}
return ch;
}
实现完上面的代码之后,就可以实现命令行交互功能了。自己可以在串口终端软件输入 help 命令,可以查看到列出了系统支持的所有命令,说明移植 shell 成功了。