CH58x/CH59x 蓝牙芯片 UART 使用
CH58x/CH59x 系列串口使用 ...... 矜辰所致
前言
专栏写了几篇文章,还没有写过一篇基础的外设,最近正好需要使用串口进行数据交互,花了些时间使用了一下,也算是有些理解和心得,正好来记录一下。
本文主要 就是介绍一下CH58x/CH59x 蓝牙芯片 UART 使用 。
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
一、串口对应关系
只是为了方便查询,写个对应表格:
| 端口 | CH585 | CH592 (映射引脚没写,映射关系自行查看手册) |
|---|---|---|
| UART0-TX | PB7 PA14 | PB7 |
| UART0-RX | PB4 PA15 | PB4 |
| UART1-TX | PA9 PB13 | PA9 |
| UART1-RX | PA8 PB12 | PA8 |
| UART2-TX | PA7 PB23 | PB23 |
| UART2-RX | PA6 PB22 | PB22 |
| UART3-TX | PA5 PB21 | PA5 |
| UART3-RX | PA4 PB20 | PA4 |
其他一些型号补充(可直接根据对应芯片手册或者示例确定,这里只是方便博主自己后期查看):
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 的使用,我们分析了基本的例程,给了一般应用中接收不定长度的串口处理示例,说明了一些串口使用中会涉及到的问题。
相信通过本文的学习,大家在对芯片串口使用的过程中应该游刃有余了!
好了,本文就到这里。谢谢大家!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)