CH58x/CH59x 系列串口使用     ...... 矜辰所致

前言

专栏写了几篇文章,还没有写过一篇基础的外设,最近正好需要使用串口进行数据交互,花了些时间使用了一下,也算是有些理解和心得,正好来记录一下。

本文主要 就是介绍一下CH58x/CH59x 蓝牙芯片 UART 使用 。

我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!

一、串口对应关系

只是为了方便查询,写个对应表格:

端口CH585CH592
(映射引脚没写,映射关系自行查看手册)
UART0-TXPB7 PA14PB7
UART0-RXPB4 PA15PB4
UART1-TXPA9 PB13PA9
UART1-RXPA8 PB12PA8
UART2-TXPA7 PB23PB23
UART2-RXPA6 PB22PB22
UART3-TXPA5 PB21PA5
UART3-RXPA4 PB20PA4

其他一些型号补充(可直接根据对应芯片手册或者示例确定,这里只是方便博主自己后期查看):

CH572D RX1 PA2 TX1 PA3

二、基本使用(官方示例)

在官方的 EVT 包中提供了一个串口使用的例程:

在这里插入图片描述

2.1 初始化

初始化基本流程:

    HSECFG_Capacitance(HSECap_18p);
    SetSysClock(SYSCLK_FREQ);

    /* 配置串口1:先配置IO口模式,再配置串口 */
    GPIOA_SetBits(GPIO_Pin_9);
    GPIOA_ModeCfg(GPIO_Pin_8, GPIO_ModeIN_PU);      // RXD-配置上拉输入
    GPIOA_ModeCfg(GPIO_Pin_9, GPIO_ModeOut_PP_5mA); // TXD-配置推挽输出,注意先让IO口输出高电平
    UART1_DefInit();

为什么看似简单的初始化流程要单独说明一下,因为串口的波特率和串口时钟有关,串口的时钟又是从系统主频分频来的。这里关于波特率有个细节,在本文后面会有说明。

2.1.1 串口映射

上面的串口1 初始化示例,除了 PA9 和 PA8 ,通过手册可以查询串口1 的映射引脚:

在这里插入图片描述

使用映射引脚的示例代码如下:

GPIOB_SetBits(GPIO_Pin_13);
GPIOPinRemap(ENABLE, RB_PIN_UART1);
GPIOB_ModeCfg(GPIO_Pin_12, GPIO_ModeIN_PU);
GPIOB_ModeCfg(GPIO_Pin_13, GPIO_ModeOut_PP_5mA);
UART1_DefInit();

2.2 发送

发送通常来说都是比较简单的,官方提供了发送函数:

void UART1_SendString(uint8_t *buf, uint16_t l)

不同的串口都有这个函数,直接调用即可。

示例如下:

//ex1:
uint8_t TxBuff[] = "This is a tx exam\r\n";
UART1_SendString(TxBuff, sizeof(TxBuff));
//ex2:
UART3_SendString( "Radio test cmd:\r\n", sizeof("Radio test cmd:\r\n"));

当然芯片也支持中断发送,但是博主在实际应用中很少使用这种方式,如果大家有需求可自行实或者查找,网上已有现成的例子。

还有一点需要说明,如果你为了更加熟悉芯片的使用,可以结合代码和芯片文档学习,此方法适用于任何芯片的学习!

比如对于我们这个发送,我们 进入看看他是怎么实现的,我们可以看到是通过判断某个寄存器来进行操作的 :

在这里插入图片描述

我们可以在芯片手册搜索该寄存器名字找到说明部分,如下图:

在这里插入图片描述

这种方式不仅对本文要将的串口使用有帮助,对于芯片的所有外设功能都适用 。

2.3 轮询接收

示例使用如下代码进行轮询接收:

#if 1 // 查询方式:接收数据后发送出去
    while(1)
    {
        len = UART1_RecvString(RxBuff);
        if(len)
        {
            UART1_SendString(RxBuff, len);
        }
    }

使用UART1_RecvString 循环接收数据,只要接收缓冲寄存器 R8_UARTx_RBR 非空,就取出数据然后发送。

测试如下(UART1_SendString 是发送收到的字符,如果不发送回车换行就不会回车换行,下图开始没有勾选发送新行,所以前面不会换行):

在这里插入图片描述

这种方式比较简单,但是因为需要 MCU 一直询问,处于一直工作状态,所以实际使用要考虑应用场合。

2.4 中断接收

例程中还提供了中断接收的示例,需要取消一下注释,同时把轮询接收注释掉。

在这里插入图片描述

通过UART1_INTCfg开启中断,接收需要开启的中断可以跳转查看宏定义,一般就是按照示例这两个中断开启就行:

在这里插入图片描述

然后在中断处理中,有2个地方都会处理数据,第一个是到达设置出触发点,一个是接收超时中断:

        case UART_II_RECV_RDY: // 数据达到设置触发点
            for(i = 0; i != trigB; i++)
            {
                RxBuff[i] = UART1_RecvByte();
                UART1_SendByte(RxBuff[i]);
            }
            break;

        case UART_II_RECV_TOUT: // 接收超时,暂时一帧数据接收完成
            i = UART1_RecvString(RxBuff);
            UART1_SendString(RxBuff, i);
            break;

需要说明的是设置触发点 UART1_ByteTrigCfg(UART_7BYTE_TRIG); 这个问题,这怎么理解呢?

官方文档有说明 :

在这里插入图片描述

官方硬件自带 8 字节的 FIFO,设置这个控制寄存器,在接收的数据超过了这个设定的值的时候会产生UART_II_RECV_RDY 中断。

如果不设置,默认就是 1个字节就产生UART_II_RECV_RDY 中断。

至于中断UART_II_RECV_TOUT 超时中断,芯片会自动根据设置的波特率算好超时时间(4个字节),超过时间没收到数据,就产生超时中断。

为了更直观的说明这个问题,我在示例代码中加入了测试打印:

        case UART_II_RECV_RDY: // 数据达到设置触发点
            for(i = 0; i != trigB; i++)
            {
                RxBuff[i] = UART1_RecvByte();
                UART1_SendByte(RxBuff[i]);
            }
            UART1_SendString( "RDY!!\r\n", sizeof("RDY!!\r\n"));
            break;

        case UART_II_RECV_TOUT: // 接收超时,暂时一帧数据接收完成
            i = UART1_RecvString(RxBuff);
            UART1_SendString(RxBuff, i);
            UART1_SendString( "TOUT!\r\n", sizeof("TOUT!\r\n"));
            break;

我们来看看测试结果:

在这里插入图片描述

通过上面测试说明,大家应该能完全理解这两个中断。

可以想象,不管多长的数据,都是这么一个流程,只要是7字节的倍数,那么每一次都会触发 UART_II_RECV_RDY 后打印,不是 7 的倍数,最后都会以触发UART_II_RECV_TOUT 收尾,当然程序中接收缓存要足够。

比如下面测试,下面测试只能起个示意,因为我在接收中断中加了发送函数,会导致数据量大时候的问题,只要去掉我自己测试添加的打印函数,就可以正常接收:

在这里插入图片描述

三、 一般应用(接收不定长度数据)

示例中,其实已经可以接受不定长度的数据,只是示例中,数据是一段一段就直接发送出来了,每一次都是发送完一段后再接收再发送另外一段,但是我们应用中一般都会接收完一帧数据统一处理,所以我们需要修改一下。

这部分我就不再重复做分析,直接把示例代码贴出来(未使用环形缓冲区)。

3.1 基础框架

定义串口缓冲区:

#define  cmd_max_len   100

typedef struct
{
  uint8_t rx_buff[cmd_max_len];
  uint8_t rx_count;
  uint8_t rx_state;
  uint8_t rx_back;
}uart_rx_buff;

串口初始化:

void cmd_uart_init()
{
#if((CMD_UART == 3)&&(DEBUG != 3))
    GPIOA_SetBits(GPIO_Pin_5);
    GPIOA_ModeCfg(GPIO_Pin_4, GPIO_ModeIN_PU);
    GPIOA_ModeCfg(GPIO_Pin_5, GPIO_ModeOut_PP_5mA);
    UART3_DefInit();
    
    UART3_SendString( "Radio test cmd:\r\n", sizeof("Radio test cmd:\r\n"));
    
    UART3_INTCfg(ENABLE, RB_IER_RECV_RDY | RB_IER_LINE_STAT);
    PFIC_EnableIRQ(UART3_IRQn);
    DelayMs(5);
    clear_uart_buffer(); //前面使用了UART3_SendString发送需要清理一下缓冲区 
#endif
}

数据处理:

数据处理是在我们接收到一帧数据的时候处理的,这个可以放在主循环里面一直查询标志位是否置位:

void app_uart_process(void)
{
    UINT32 irq_status;
    if(cmd_uart.rx_state){

        SYS_DisableAllIrq(&irq_status);
        // 数据处理,这里通过开启任务处理
        //UART0_SendString( cmd_uart.rx_buff, cmd_uart.rx_count);
        tmos_start_task(rfTaskID, CMD_PROCESS_EVENT, 2);
        cmd_uart.rx_count = 0;
        cmd_uart.rx_state = FALSE;
        SYS_RecoverIrq(irq_status);    
    }
}

清除缓存:


uart_rx_buff cmd_uart;  //这个定义是全局变量,串口接收数据结构体

void clear_uart_buffer(void)
{
    while (R8_UART3_RFC) {
        uint8_t dummy = R8_UART3_RBR;  // 读取数据,丢弃
    }
    tmos_memset(&cmd_uart,0, cmd_max_len);
    cmd_uart.rx_count = 0;
    cmd_uart.rx_state = FALSE; 
}

主循环调用:

void Main_Circulation()
{
    while(1)
    {
        TMOS_SystemProcess();
        app_uart_process();
    }
}

3.2 中断处理

中断处理就是在接收完一帧数据以后使得cmd_uart.rx_state = TRUE; 这里提供了两种不同的中断处理方式。

3.2.1 以\r\n结尾

void UART3_IRQHandler(void)
{
    volatile uint8_t i;
    uint8_t read_flag = UART3_GetITFlag();   // ① 只读一次,保存在局部变量

    switch(read_flag)
    {
        case UART_II_LINE_STAT: // 线路状态错误
        {
            UART3_GetLinSTA();
            break;
        }

        case UART_II_RECV_RDY: // 数据达到设置触发点
        case UART_II_RECV_TOUT: // 接收超时,暂时一帧数据接收完成

            while (R8_UART3_RFC)                    // 把硬件 FIFO 读空
            {
                if (cmd_uart.rx_count < cmd_max_len - 1 )              // 防止超界,预留结束符位置
                {
                    uint8_t byte = R8_UART3_RBR;  // 获取接收到的字符
                    cmd_uart.rx_buff[cmd_uart.rx_count ++] = byte ;

                    if (byte == '\n') {
                        if (cmd_uart.rx_count > 0 && cmd_uart.rx_buff[cmd_uart.rx_count-1] == '\r') {
                            cmd_uart.rx_count--;  // 去掉前面的\r
                        }
                        cmd_uart.rx_buff[cmd_uart.rx_count] = '\0';  // 结束符
                        // PRINT("received: %s\r\n", cmd_uart.rx_buff);  // 打印接收到的数据
                        // PRINT("%d\r\n", cmd_uart.rx_count);  // 打印接收到的数据
                        cmd_uart.rx_state = TRUE;  // 设置接收状态
                    }
                }
                else
                {
                    cmd_uart.rx_back = R8_UART3_RBR;             // 超了丢掉
                }
            }
            break;

        case UART_II_THR_EMPTY: // 发送缓存区空,可继续发送
            break;

        case UART_II_MODEM_CHG: // 只支持串口0
            break;

        default:
            break;
    }
}

3.2.1 没有明确数据结尾

不带明确结尾的需要大家自己优化一下,我只做简单测试:

        case UART_II_RECV_RDY: // 数据达到设置触发点
        case UART_II_RECV_TOUT: // 接收超时,暂时一帧数据接收完成
            while (R8_UART3_RFC)                    // 把硬件 FIFO 读空
            {
                if (cmd_uart.rx_count < cmd_max_len)              // 防止超界
                {
                    cmd_uart.rx_buff[cmd_uart.rx_count ++] = R8_UART3_RBR;
                }
                else
                {
                    cmd_uart.rx_back = R8_UART3_RBR;             // 超了丢掉
                }
            }
            /* 只有超时才算一帧结束
             * */
            if (read_flag == UART_II_RECV_TOUT && cmd_uart.rx_count > 0)
            {
                cmd_uart.rx_buff[cmd_uart.rx_count] = 0; // 字符串结束符
                cmd_uart.rx_state = TRUE;              // 通知主循环
            }

            break;

在中断处理函数中,上面把UART_II_RECV_RDY 分支里面的break; 去掉了,这样不管触发UART_II_RECV_RDY 中断还是 UART_II_RECV_TOUT 中断,都直接进行下面的代码操作。

还有一个思路是自己定义一个超时时间,超过时间就认为完成一帧数据处理,逻辑处理方式可参考:CH585 高速 USB模拟 CDC串口应用示例 2.2.3 小节 没有标志结尾数据接收 的处理方式。

四、 其他说明

4.1 高波特率乱码说明

在CH58x 系列芯片使用中,我们把波特率加大到 921600 会有数据乱码,这是怎么回事?

前面说到:串口的波特率和串口时钟有关,串口的时钟又是从系统主频分频来的。

我们来看看波特率设置:

void UART0_BaudRateCfg(uint32_t baudrate)
{
    uint32_t x;

    x = 10 * GetSysClock() / 8 / baudrate;
    x = (x + 5) / 10;
    R16_UART0_DL = (uint16_t)x;
}

R16_UART0_DL 为波特率 除数锁存器 (R16_UARTx_DL)(x=0/1/2/3) ,用于计算波特率。

先获取系统主频,然后分频,再通过需要设置的波特率计算需要放入R16_UART0_DL 的数值。

第一行的乘 10,只是为了第二行做四舍五入计算,需要除去10。

再通过 (x*10 + 5)/10 四舍五入。误差从 1 降低到 0.5 。

“(value * 10 + 5) / 10” 就是C语言没有浮点数时的四舍五入的写法。

我们现在就可以分析乱码的原因:

主频为 62.4M
波特率设置 921600
62.4M/8= 7.8M
7.8M/921600 ≈ 8.46
四舍五入 写 8
实际波特率 7.8M/8 = 975000
误差 = |975000 – 921600|/921600 ≈ 0.058
5.8%误差大会乱码
.
如果是 460800
7.8M/460800 ≈ 16.9
实际波特率 7.8M/17 ≈ 458824
误差 = |458824– 460800|/460800 ≈ 0.004 0.4% 不会乱码。

所以我们使用中,防止乱码的方法:

加大主频,或者减少波特率。

4.2 打印重定向

这个在文章 沁恒微 RISC-V 芯片开发工具 MounRiver Studio 使用 中提到过, 简单放一下:

在这里插入图片描述

上面设置好以后,代码中也要有正确的对应串口初始化。

结语

本文详细介绍了一下 CH58x/CH59x 蓝牙芯片 UART 的使用,我们分析了基本的例程,给了一般应用中接收不定长度的串口处理示例,说明了一些串口使用中会涉及到的问题。

相信通过本文的学习,大家在对芯片串口使用的过程中应该游刃有余了!

好了,本文就到这里。谢谢大家!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐