【瑞萨RA_FSP】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
}

猜你喜欢

转载自blog.csdn.net/Dustinthewine/article/details/130754822