文章目录
- 一、UART收发回显
- 二、UART指令控制RGB灯
- 三、基于环形队列的UART收发回显
一、UART收发回显
UART只需两根信号线即可完成双向通信,对硬件要求低,使得很多模块都预留UART接口来实现与其他模块或者控制器进行数据传输, 比如GSM模块,WIFI模块、蓝牙模块等等。在硬件设计时,注意还需要一根“共地线”。
我们经常使用UART来实现控制器与电脑之间的数据传输。这使得我们调试程序非常方便,比如我们可以把一些变量的值、 函数的返回值、寄存器标志位等等通过UART发送到串口调试助手,这样我们可以非常清楚程序的运行状态。
不仅仅可以将数据发送到串口调试助手,还可以在串口调试助手发送数据给控制器,控制器程序根据接收到的数据进行下一步工作。
首先,编写一个程序实现开发板与电脑通信,在开发板上电时通过UART发送一串字符串给电脑,然后开发板进入中断接收等待状态, 如果电脑有发送数据过来,开发板就会产生中断,在中断服务函数接收数据,并马上把数据返回发送给电脑。
1. 硬件设计
为利用 UART 实现开发板与电脑通信,需要用到一个USB转串口(UART)的芯片:CH340G。 CH340G 是一个USB总线的转接芯片,实现USB转UART、USB转lrDA红外或者USB转打印机接口,我们使用其USB转UART功能。 具体电路设计见下图
在下面的三块开发板的电路图中,CH340G的TXD引脚与MCU芯片 UART 的RXD引脚连接, CH340G的RXD引脚与MCU芯片 UART 的TXD引脚连接。CH340G芯片集成在开发板上,其地线(GND)已与控制器的GND连通。
P511:SCL4 RXD
P512:SCL4 TXD
2. 软件设计
① FSP配置
在 FSP 配置界面里面点开 “Pins”-> “Peripherals”-> “Connectivity:SCI”-> “SCI4” 来配置SCI模块, 配置为 “Asynchronous UART” 模式,并选择开发板所使用的串口引脚,如下图。
在配置界面底部点击 “Stack”,如下图步骤加入串口UART:
如下图点击刚刚加入的窗口,在左下角的“属性”窗口中配置 名字(name)、通道(Channel)、回调函数(Callback)名字即可, 引脚(Pins)、波特率(Baud Rate)等其他的属性按照默认的配置即可。
使用 printf 函数时,需要使用到堆,默认情况下堆的大小为0,因此我们需要修改堆的大小。 可以在 FSP 配置界面中的“BSP”属性栏的“RA Common”中通过修改“Heap size”来设置堆区大小。 这里需要设置为 8 的整数倍,对于RA6M5推荐至少为4K(0x1000),如下图。
最后点右上角的 “Generate Project Content” 按钮,让软件自动生成配置代码。
② 串口初始化函数
FSP 配置并生成代码之后,首先需要使用 R_SCI_UART_Open 函数打开 SCI4 UART 模块, 我们把这层调用封装为一个 Debug_UART4_Init 函数,如下所示。
/* 调试串口 UART4 初始化 */
void Debug_UART4_Init(void)
{fsp_err_t err = FSP_SUCCESS;err = R_SCI_UART_Open (&g_uart4_ctrl, &g_uart4_cfg);assert(FSP_SUCCESS == err);
}
③ R_SCI_UART_Write函数
串口初始化完成之后,可以直接使用 R_SCI_UART_Write 函数来将字符串写入到串口输出,该函数的原型如下。
fsp_err_t R_SCI_UART_Write (uart_ctrl_t * const p_api_ctrl, uint8_t const * const p_src, uint32_t const bytes)
-
参数 p_src 指向要写入的字符串首地址
-
参数 bytes 为传入的要写入的字符的数目
在使用 R_SCI_UART_Write 函数需要注意的一些事项:
若使用了 R_SCI_UART_Write() 来发送数据, 在数据发送完成之后会导致 uart_send_complete_flag 这个标志位被置位, 因此程序在调用 R_SCI_UART_Write 函数之后需要等待 uart_send_complete_flag 标志位被置位, 然后将该标志位清零。否则当连续调用 R_SCI_UART_Write 函数时可能导致发送数据丢失。 建议使用后文所述的 printf 函数将数据发送到串口。
④ 串口中断回调函数
在前面的 FSP 配置步骤的时候,设置了串口中断回调函数的名字为: debug_uart4_callback。 设置这么一个函数的原因是:每当串口发送或者接收完成一个字符时,都会默认触发串口的中断, 而在串口中断中会调用函数 debug_uart4_callback,在函数里我们需要根据不同的中断情况进行相应的处理。
因此,也需要同时在代码里面定义并实现这么函数 debug_uart4_callback。 把这个函数放到文件“bsp_debug_uart.c”中,该函数代码如下所示。
其中,需要定义一个额外的标志变量 uart_send_complete_flag 来表示串口发送数据已完成。 变量 uart_send_complete_flag 必须加上volatile,否则可能被编译器优化。
/* 发送完成标志 */
volatile bool uart_send_complete_flag = false;/* 串口中断回调 */
void debug_uart4_callback (uart_callback_args_t * p_args)
{switch (p_args->event){case UART_EVENT_RX_CHAR:{/* 把串口接收到的数据发送回去 */R_SCI_UART_Write(&g_uart4_ctrl, (uint8_t *)&(p_args->data), 1);break;}case UART_EVENT_TX_COMPLETE:{uart_send_complete_flag = true;break;}default:break;}
}
⑤ 重定向printf输出到串口
虽然可以直接使用 R_SCI_UART_Write 函数来将字符串输出到串口, 但是这个函数在很多情况下没有 printf 函数那样方便。所以需要添加一段代码来将 printf 输出重定向到串口(UART4)。
将以下的代码添加到源文件“bsp_debug_uart.c”里面。 由于不同C库的 printf 函数的底层实现不同,这里使用条件编译选择我们需要重写的函数。
/* 重定向 printf 输出 */
#if defined __GNUC__ && !defined __clang__
int _write(int fd, char *pBuffer, int size); //防止编译警告
int _write(int fd, char *pBuffer, int size)
{(void)fd;R_SCI_UART_Write(&g_uart4_ctrl, (uint8_t *)pBuffer, (uint32_t)size);while(uart_send_complete_flag == false);uart_send_complete_flag = false;return size;
}
#else
int fputc(int ch, FILE *f)
{(void)f;R_SCI_UART_Write(&g_uart4_ctrl, (uint8_t *)&ch, 1);while(uart_send_complete_flag == false);uart_send_complete_flag = false;return ch;
}
#endif
⑥ hal_entry入口函数
C语言程序的入口函数 main 函数调用了 hal_entry 函数。 在 hal_entry 函数里面编写应用代码。
void hal_entry(void)
{/* TODO: add your own code here */LED_Init(); // LED 初始化Debug_UART4_Init(); // SCI4 UART 调试串口初始化printf("这是一个串口收发回显例程\r\n");printf("打开串口助手发送数据,接收窗口会回显所发送的数据\r\n");while(1){LED1_ON;R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS);LED1_OFF;R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS);}#if BSP_TZ_SECURE_BUILD/* Enter non-secure code */R_BSP_NonSecureEnter();
#endif
}
首先调用 LED_Init 函数初始化板子上的 LED 灯,然后调用 Debug_UART4_Init 函数初始化 SCI4 UART 作为调试串口来使用。 之后就可以使用 printf 函数了,调用 printf 输出提示信息到串口。 接着在 while 循环里是一段让 LED1 每隔一秒钟闪烁的程序。
二、UART指令控制RGB灯
1. 串口中断回调函数
需要在串口中断回调函数,也就是 debug_uart4_callback 函数里判断接收到的字符, 并根据所接收到的不同字符做出不同的操作。 修改 debug_uart4_callback 函数的代码,如下所示。
/* 串口中断回调 */
void debug_uart4_callback (uart_callback_args_t * p_args)
{switch (p_args->event){case UART_EVENT_RX_CHAR:{/* 根据字符指令控制RGB彩灯颜色 */switch (p_args->data){case '1':LED1_ON;break;case '2':LED2_ON;break;case '3':LED3_ON;break;case '4':LED1_OFF;break;case '5':LED2_OFF;break;case '6':LED3_OFF;break;case '7':LED1_ON; LED2_ON; LED3_ON;break;case '8':LED1_OFF; LED2_OFF; LED3_OFF;break;default:break;}break;}case UART_EVENT_TX_COMPLETE:{uart_send_complete_flag = true;break;}default:break;}
}
2. hal_entry入口函数
在 hal_entry 函数里面进行硬件初始化之后,首先打印提示信息,提醒用户从串口输入数字字符。 然后默认关闭所有 LED 灯,在 while 循环里什么都不做,等待用户的输入。
void hal_entry(void)
{/* TODO: add your own code here */LED_Init(); // LED 初始化Debug_UART4_Init(); // SCI4 UART 调试串口初始化printf("这是一个串口控制 LED 例程\r\n");printf("打开串口助手发送以下指令,控制 LED 的状态\r\n");printf ("\t指令 ------ 状态\r\n ");printf ("\t 1 ------ LED1_ON\r\n ");printf ("\t 2 ------ LED2_ON\r\n ");printf ("\t 3 ------ LED3_ON\r\n ");printf ("\t 4 ------ LED1_OFF\r\n ");printf ("\t 5 ------ LED2_OFF\r\n ");printf ("\t 6 ------ LED3_OFF\r\n ");printf ("\t 7 ------ LED 全亮\r\n ");printf ("\t 8 ------ LED 全灭\r\n ");LED1_OFF; LED2_OFF; LED3_OFF; //默认关闭所有 LED 灯while(1){}#if BSP_TZ_SECURE_BUILD/* Enter non-secure code */R_BSP_NonSecureEnter();
#endif
}
三、基于环形队列的UART收发回显
在实际项目开发中,由于有些串口不具备FIFO(如SCI1和SCI2)或FIFO的buffer比较小, 这可能会在数据处理速度小于数据接收速度的时候,导致数据的丢失。因此可以设计一个队列来避免这一问题。 在本实验中,使用环形队列来实现实验1的串口收发回显,将串口接收到的数据暂存在队列中, 待完成一次接收后再将队列中的数据全部发出去。
队列是一种特殊的线性表,只允许在队列头(head)删除元素,在队列尾(tail)添加元素。 当队列添加一个元素,队列尾向后移动,当队列删除一个元素,同样,删除一个元素,队列头向后移动,如下图。
由于存储空间是有限的,如果使用线性队列,删除元素后就会空出一段存储空间,这会造成很大的浪费。 因此实际上更多使用环形队列。并不是说这段存储空间是环形的,而是头指针和尾指针到达存储空间末尾后会回到存储空间起点。 因此在逻辑上这是循环的,如下图。
1. 环形队列的实现
#define DATA_LEN 300 //队列缓存大小typedef struct
{uint16_t head; //头指针uint16_t tail; //尾指针uint8_t data[DATA_LEN]; //队列数据
} Circular_queue_t;extern Circular_queue_t Circular_queue; //环形队列全局变量bool Queue_Init(Circular_queue_t *circular_queue); //初始化队列
bool Queue_isEmpty(Circular_queue_t *circular_queue); //判断队列是否为空
bool Queue_isFull(Circular_queue_t *circular_queue); //判断队列是否已满
bool Queue_Wirte(Circular_queue_t *circular_queue, uint8_t *string, uint16_t len); //写数据
bool Queue_Read(Circular_queue_t *circular_queue, uint8_t *string, uint16_t len); //读数据
uint16_t Queue_HadUse(Circular_queue_t *circular_queue); //返回队列中数据的长度
uint16_t Queue_NoUse(Circular_queue_t *circular_queue); //返回未使用数据的长度
2. 串口中断回调函数
/* 串口中断回调 */
void debug_uart4_callback (uart_callback_args_t * p_args)
{switch (p_args->event){case UART_EVENT_RX_CHAR:{/* 接收到数据后马上写入队列中 */Queue_Wirte(&Circular_queue, (uint8_t*) &p_args->data, 1);break;}case UART_EVENT_TX_COMPLETE:{uart_send_complete_flag = true;break;}default:break;}
}
3. hal_entry入口函数
void hal_entry(void)
{/* TODO: add your own code here */uint8_t Read_Buffer[DATA_LEN];uint16_t Read_Length;LED_Init(); // LED 初始化Debug_UART4_Init(); // SCI4 UART 调试串口初始化Queue_Init((Circular_queue_t*)&Circular_queue); //环形队列初始化printf("这是一个串口环形队列例程\r\n");printf("打开串口助手发送数据 5 个及以上的数据,接收窗口会打印所发送的数据\r\n");while(1){if (Queue_isEmpty(&Circular_queue) == false) //判断队列中的数据不为空{Read_Length = Queue_HadUse(&Circular_queue);if( Read_Length >= 5) // 如果队列中的数据大于等于5个,开始打印队列中的所有数据{printf("Read_Length=%d: ", Read_Length);memset(Read_Buffer, 0, DATA_LEN);/* 读出 Read_Length 个数据 */Queue_Read(&Circular_queue, Read_Buffer, Read_Length);printf("%s\r\n", Read_Buffer);}}R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MILLISECONDS);}#if BSP_TZ_SECURE_BUILD/* Enter non-secure code */R_BSP_NonSecureEnter();
#endif
}