RT-Thread Nano 提供了shell 控制台元件 FinSH ,對于 Nano 版本的 FinSH 元件,沒有使用 RT-Thread 的裝置驅動架構,是以移植起來比較簡單,隻要提供控制台的列印輸出函數
rt_hw_console_output
和輸入函數
rt_hw_console_getchar
即可。
1. 實作Nano控制台的序列槽輸出函數
我使用的硬體平台是 STM32F407ZGT6 ,移植前先準備可以正常運作 RT-Thread Nano 的工程源碼。
1.1 初始化序列槽外設
對于控制台的輸入輸出硬體接口,我使用的是序列槽,是以需要對序列槽外設進行基本的初始化,并且開啟序列槽外設的接收中斷(輸入使用序列槽中斷方式)。
對于序列槽外設的初始化的代碼,我使用的是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); // 使能序列槽接收中斷
}
}
1.2 實作控制台輸出函數 rt_hw_console_output
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 和版本資訊等内容了。如下圖:

2. 實作指令輸入
2.1 添加 FinSH 源碼到 keil
把 RT-Thread Nano 目錄下的 components/finsh 目錄,複制到 keil 工程目錄中,然後把下面4個 C 檔案添加到 keil 工程。
另外,rtconfig.h 配置檔案中,需要使能
#define RT_USING_FINSH
宏定義,才能使用 FinSH 元件。
2.2 實作控制台輸入函數rt_hw_console_getchar
rt_hw_console_getchar
函數功能,就是控制台從序列槽中擷取一個字元。
擷取一個字元的方式可以采用查詢方式。當然查詢方式不能死等了(查詢不到可讀字元時,調用延時函數休眠10ms),因為需要讓出CPU給其他線程運作的機會。
也可以采用中斷方式(我這裡采用中斷方式),可以更有效的利用CPU。
中斷方式實作原理:
當序列槽接收到資料産生接收中斷時,在中斷中把資料存入 ringbuffer 緩沖區中,然後釋放信号量。而 shell 線程去擷取信号量,可以擷取到信号量則讀取 ringbuffer 中的資料,沒有資料則一直阻塞等待擷取序列槽接收中斷釋放的信号量。
2.2.1 實作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;
}
}
2.2.2 序列槽接收中斷
序列槽接收中斷要做的事情就是:把讀取到的資料存放進 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(); //在中斷中一定要調用這對函數,離開中斷
}
2.2.3 實作rt_hw_console_getchar
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 成功了。