CH582、CH592、CH584沁恒MCU UART实例讲解

UART接口是一个通用的异步串口,应该是最“古老”的通讯协议之一,几乎每片MCU都有至少一个UART接口,介绍UART的书籍也比较多,所以这篇博文其实是没有必要的。只是最近有个项目刚好有用到,所以习惯性的作个梳理。

一、UART通信协议解析:从原理到实践

一)、UART基础概述

UART(通用异步收发器)作为最基础的串行通信协议,凭借其硬件简单、成本低廉的特性,成为嵌入式系统设备间通信的首选方案。其核心特点包括:

  • 异步通信:无需共享时钟信号,通过预定义波特率同步

  • 全双工传输:独立TX/RX线路实现双向数据交互

  • 点对点连接:仅支持两个设备直接通信

二)、硬件架构与接口设计

2.1 物理层实现

UART硬件接口通常包含以下关键组件:

  1. 电平转换电路:支持TTL(0-3.3V/5V)与RS-232(±3-15V)等标准

  2. 收发器芯片:如MAX232(RS-232转换)、MAX485(差分信号)

  3. 终端电阻:RS-485总线需配置120Ω电阻抑制信号反射

2.2 典型连接方案

设备类型

连接方式

适用场景

微控制器间通信

TXA-RXB/RXA-TXB + GND

短距离(<1m)

工业设备

RS-485总线+终端电阻

长距离(<1200m)

计算机外设

USB转UART模块

标准串口扩展

三)、协议帧结构与时序分析

3.1 标准数据帧组成

UART数据帧采用起始位+数据位+校验位+停止位结构:

  • 起始位:1位低电平(逻辑0)标志传输开始

  • 数据位:5-9位有效数据(通常8位对应1字节)

  • 校验位:奇校验/偶校验/无校验(可选)

  • 停止位:1/1.5/2位高电平(逻辑1)标志帧结束

3.2 时序示例(8N1配置)

Bit 0

Bit 1

...

Bit 7

Bit 8

Bit 9

Bit 10

0

1

...

1

1

1

1

起始位

D0

...

D7

停止位

空闲位

四)、关键设计要点与优化策略

4.1 波特率配置

  • 常见速率:9600bps(工业控制)、115200bps(调试接口)

  • 误差控制:双方波特率偏差需<2%

4.2 抗干扰设计

  1. 电气隔离:使用光耦隔离芯片(如HCPL-0720)

  2. 差分信号:RS-485总线抗共模干扰能力达±12V

  3. 软件滤波:中值滤波算法消除偶发噪声

4.3 可靠性增强

  • 错误检测:奇偶校验+CRC循环冗余校验

  • 流量控制:RTS/CTS硬件流控避免数据溢出

  • 超时重传:软件实现超时检测与自动重发

五)、典型应用案例

5.1 智能家居控制系统

  • 架构:STM32主控(UART1)→ 蓝牙模块(透传模式)

  • 参数:115200bps, 8N1, 硬件流控

  • 优化:采用DMA传输降低CPU负载

5.2 工业传感器网络

  • 拓扑:RS-485总线多节点通信

  • 协议:Modbus RTU(基于UART)

  • 挑战:解决信号反射与终端匹配问题

六)、常见问题排查

  1. 数据乱码:检查波特率/校验位/停止位配置

  2. 通信中断:验证GND共地及线路阻抗

  3. 速率受限:提升波特率需缩短走线长度

UART作为跨越数十年的通信标准,其简单性与灵活性使其持续活跃在物联网、工业控制等领域。理解其硬件原理与协议细节,是构建可靠串口通信系统的基石。随着FPGA实现方案的普及,UART在现代系统中仍焕发新的生命力。 

二、沁恒蓝牙芯片的UART接口

1、UART简介

CH585和CH584芯片提供了4组全双工的异步串口UART0/1/2/3。支持全双工和半双工串口通讯, 其中UART0提供发送状态引脚用于切换RS485,并且支持MODEM调制解调器信号CTS、DSR、RI、DCD、 DTR、RTS。

2、 主要特性

 兼容16C550异步串口并且有所增强

 支持5、6、7或者8个数据位以及1或者2个停止位

 支持奇、偶、无校验、空白0、标志1等校验方式

 可编程通讯波特率,最高达9Mbps波特率

 内置8个字节的FIFO先进先出缓冲器,支持4个FIFO触发级

 UART0支持MODEM调制解调器信号CTS、DSR、RI、DCD、DTR、RTS

 UART0支持硬件流控制信号CTS和RTS自动握手和自动传输速率控制,兼容TL16C550C

 支持串口帧错误检测、支持Break线路间隔检测

 支持全双工和半双工串口通讯,UART0提供发送状态引脚用于切换RS485

三、实例应用

/********************************** (C) COPYRIGHT *******************************
 * File Name          : Main.c
 * Author             : WCH
 * Version            : V1.0
 * Date               : 2020/08/06
 * Description        : 串口1收发演示
 *********************************************************************************
 * Copyright (c) 2021 Nanjing Qinheng Microelectronics Co., Ltd.
 * Attention: This software (modified or not) and binary are used for 
 * microcontroller manufactured by Nanjing Qinheng Microelectronics.
 *******************************************************************************/

#include "CH58x_common.h"

uint8_t TxBuff[] = "This is a tx exam\r\n";
uint8_t RxBuff[100];
uint8_t trigB;

/*********************************************************************
 * @fn      main
 *
 * @brief   主函数
 *
 * @return  none
 */
int main()
{
    uint8_t len;

    HSECFG_Capacitance(HSECap_18p);
    SetSysClock(CLK_SOURCE_HSE_PLL_62_4MHz);

    /* 配置串口1:先配置IO口模式,再配置串口 */
    GPIOB_SetBits(GPIO_Pin_7);
    GPIOB_ModeCfg(GPIO_Pin_4, GPIO_ModeIN_PU);      // RXD-配置上拉输入
    GPIOB_ModeCfg(GPIO_Pin_7, GPIO_ModeOut_PP_5mA); // TXD-配置推挽输出,注意先让IO口输出高电平
    UART0_DefInit();

#if 1 // 测试串口发送字符串
    UART0_SendString(TxBuff, sizeof(TxBuff));

#endif

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

#endif

#if 1 // 中断方式:接收数据后发送出去
    UART0_ByteTrigCfg(UART_7BYTE_TRIG);
    trigB = 7;
    UART0_INTCfg(ENABLE, RB_IER_RECV_RDY | RB_IER_LINE_STAT);
    PFIC_EnableIRQ(UART0_IRQn);
#endif

    while(1);
}

/*********************************************************************
 * @fn      UART1_IRQHandler
 *
 * @brief   UART1中断函数
 *
 * @return  none
 */
__INTERRUPT
__HIGH_CODE
void UART0_IRQHandler(void)
{
    volatile uint8_t i;

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

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

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

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

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

        default:
            break;
    }
}
/********************************** (C) COPYRIGHT *******************************
 * File Name          : CH58x_uart0.c
 * Author             : WCH
 * Version            : V1.2
 * Date               : 2021/11/17
 * Description        : source file(ch585/ch584)
 *********************************************************************************
 * Copyright (c) 2021 Nanjing Qinheng Microelectronics Co., Ltd.
 * Attention: This software (modified or not) and binary are used for 
 * microcontroller manufactured by Nanjing Qinheng Microelectronics.
 *******************************************************************************/

#include "CH58x_common.h"

/*********************************************************************
 * @fn      UART0_DefInit
 *
 * @brief   串口默认初始化配置
 *
 * @param   none
 *
 * @return  none
 */
void UART0_DefInit(void)
{
    UART0_BaudRateCfg(115200);
    R8_UART0_FCR = (2 << 6) | RB_FCR_TX_FIFO_CLR | RB_FCR_RX_FIFO_CLR | RB_FCR_FIFO_EN; // FIFO打开,触发点4字节
    R8_UART0_LCR = RB_LCR_WORD_SZ;
    R8_UART0_IER = RB_IER_TXD_EN;
    R8_UART0_DIV = 1;
}

/*********************************************************************
 * @fn      UART0_BaudRateCfg
 *
 * @brief   串口波特率配置
 *
 * @param   baudrate    - 波特率
 *
 * @return  none
 */
void UART0_BaudRateCfg(uint32_t baudrate)
{
    uint32_t x;

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

/*********************************************************************
 * @fn      UART0_ByteTrigCfg
 *
 * @brief   串口字节触发中断配置
 *
 * @param   b       - 触发字节数 refer to UARTByteTRIGTypeDef
 *
 * @return  none
 */
void UART0_ByteTrigCfg(UARTByteTRIGTypeDef b)
{
    R8_UART0_FCR = (R8_UART0_FCR & ~RB_FCR_FIFO_TRIG) | (b << 6);
}

/*********************************************************************
 * @fn      UART0_INTCfg
 *
 * @brief   串口中断配置
 *
 * @param   s       - 中断控制状态,是否使能相应中断
 * @param   i       - 中断类型
 *                    RB_IER_MODEM_CHG  - 调制解调器输入状态变化中断使能位(仅 UART0 支持)
 *                    RB_IER_LINE_STAT  - 接收线路状态中断
 *                    RB_IER_THR_EMPTY  - 发送保持寄存器空中断
 *                    RB_IER_RECV_RDY   - 接收数据中断
 *
 * @return  none
 */
void UART0_INTCfg(FunctionalState s, uint8_t i)
{
    if(s)
    {
        R8_UART0_IER |= i;
        R8_UART0_MCR |= RB_MCR_INT_OE;
    }
    else
    {
        R8_UART0_IER &= ~i;
    }
}

/*********************************************************************
 * @fn      UART0_Reset
 *
 * @brief   串口软件复位
 *
 * @param   none
 *
 * @return  none
 */
void UART0_Reset(void)
{
    R8_UART0_IER = RB_IER_RESET;
}

/*********************************************************************
 * @fn      UART0_SendString
 *
 * @brief   串口多字节发送
 *
 * @param   buf     - 待发送的数据内容首地址
 * @param   l       - 待发送的数据长度
 *
 * @return  none
 */
void UART0_SendString(uint8_t *buf, uint16_t l)
{
    uint16_t len = l;

    while(len)
    {
        if(R8_UART0_TFC != UART_FIFO_SIZE)
        {
            R8_UART0_THR = *buf++;
            len--;
        }
    }
}

/*********************************************************************
 * @fn      UART0_RecvString
 *
 * @brief   串口读取多字节
 *
 * @param   buf     - 读取数据存放缓存区首地址
 *
 * @return  读取数据长度
 */
uint16_t UART0_RecvString(uint8_t *buf)
{
    uint16_t len = 0;

    while(R8_UART0_RFC)
    {
        *buf++ = R8_UART0_RBR;
        len++;
    }

    return (len);
}

以上代码,基于官方提供的demo例程修改得到,可通过转TTL正常与PC机通讯。

沁恒的UART接口有个特点,使用的时候需要根据实际情况灵活配置:

UART接收中断可以配置为1字节、2字节、4字节及7字节触发。

/**
 * @brief  Configuration UART TrigByte num
 */
typedef enum
{
    UART_1BYTE_TRIG = 0, // 1字节触发
    UART_2BYTE_TRIG,     // 2字节触发
    UART_4BYTE_TRIG,     // 4字节触发
    UART_7BYTE_TRIG,     // 7字节触发

} UARTByteTRIGTypeDef;



/*********************************************************************
 * @fn      UART0_ByteTrigCfg
 *
 * @brief   串口字节触发中断配置
 *
 * @param   b       - 触发字节数 refer to UARTByteTRIGTypeDef
 *
 * @return  none
 */
void UART0_ByteTrigCfg(UARTByteTRIGTypeDef b)
{
    R8_UART0_FCR = (R8_UART0_FCR & ~RB_FCR_FIFO_TRIG) | (b << 6);
}


#if 1 // 中断方式:接收数据后发送出去
    UART0_ByteTrigCfg(UART_7BYTE_TRIG);
    trigB = 7;
    UART0_INTCfg(ENABLE, RB_IER_RECV_RDY | RB_IER_LINE_STAT);
    PFIC_EnableIRQ(UART0_IRQn);
#endif

<think>我们正在讨论的是沁恒CH58x系列(特别是CH584M)基于RISC-V架构的MCU。根据引用[4],该芯片使用MounRiver Studio IDE环境进行编译,且代码兼容性较好。固定变量到固定地址的方法与之前讨论的类似,但由于是RISC-V架构,我们需要确认编译器支持的特性。 在RISC-V GCC编译器中(MounRiver Studio使用的工具链),通常使用`__attribute__((section(".section_name")))`结合链接脚本,或者使用`__attribute__((aligned(alignment)))`和绝对地址定位。 但是,注意:在GCC中,直接指定绝对地址的attribute是`__attribute__((at(address)))`,但根据GCC文档,这个属性是ARM编译器(armcc)支持的,在RISC-V GCC中并不直接支持`at`属性。因此,我们需要使用链接脚本来实现。 ### 方法一:使用`__attribute__((section))`和链接脚本(推荐) 1. **在代码中定义变量并指定段名**: 例如,将变量放在一个自定义的段中,比如`.myvars`。 ```c uint32_t my_variable __attribute__((section(".myvars"))) = 0x12345678; ``` 2. **修改链接脚本(.ld文件)**: 在链接脚本中,将自定义段定位到指定的地址。例如,我们想将变量定位到0x20001000(假设这是SRAM中的一个地址)。 打开项目的链接脚本(通常在项目目录下,扩展名为.ld),找到内存区域定义(如RAM),然后添加一个自定义段: ```ld MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 32K /* 假设RAM起始地址和长度 */ ... /* 其他内存区域 */ } SECTIONS { ... /* 其他段 */ .myvars 0x20001000 : { . = ALIGN(4); *(.myvars) . = ALIGN(4); } > RAM ... /* 其他段 */ } ``` 这样,所有放在`.myvars`段中的变量都会被放置在0x20001000地址处。 ### 方法二:使用指针直接访问固定地址(不定义变量,直接操作内存) 如果只是需要访问特定地址,可以直接使用指针: ```c #define MY_VARIABLE_ADDRESS (0x20001000) volatile uint32_t * const my_variable = (uint32_t *)MY_VARIABLE_ADDRESS; // 使用 *my_variable = 0x12345678; ``` 这种方法不需要修改链接脚本,但要注意: - 该地址必须是可写的(如SRAM)。 - 确保没有其他变量或代码使用该地址。 ### 方法三:使用汇编定义符号(高级) 在汇编文件中定义一个符号,并在链接脚本中指定其地址,然后在C中引用。这种方法较为复杂,一般不常用。 ### 注意事项 1. **地址对齐**:确保指定的地址符合变量的对齐要求(例如,32位变量通常需要4字节对齐)。 2. **初始化**:对于需要初始化的变量,链接器会将其放在初始化数据段(.data)中,并在启动时由启动代码进行初始化。但使用链接脚本固定地址后,需要确保启动代码不会覆盖该地址的初始化(因为该地址可能不在.data段中)。因此,通常我们使用未初始化的段(.bss)或者自行初始化。 3. **多变量**:如果多个变量需要放在相邻的固定地址,可以在同一个段中定义多个变量,并在链接脚本中指定整个段的起始地址。 ### 示例:将多个变量放在固定地址 1. 代码: ```c uint32_t var1 __attribute__((section(".my_fixed_section"))); uint32_t var2 __attribute__((section(".my_fixed_section"))); ``` 2. 链接脚本: ```ld .my_fixed_section 0x20001000 : { *(.my_fixed_section) } > RAM ``` 这样,var1和var2将被连续放置在0x20001000和0x20001004(假设对齐为4字节)。 ### 在MounRiver Studio中配置链接脚本 根据引用[4],MounRiver Studio支持RISC-V开发。要修改链接脚本,通常需要: 1. 在项目文件夹中找到链接脚本(如.ld文件)。 2. 修改链接脚本,添加自定义段。 3. 重新编译项目。 如果项目中没有链接脚本,可能需要从例程中复制一个,然后在工程设置中指定自定义链接脚本。 ### 总结 在沁恒CH58x(RISC-V架构)中固定变量到固定地址,推荐使用链接脚本结合`__attribute__((section))`的方法。这种方法灵活且可靠,适用于需要精确控制变量位置的情况。 --- §§相关问题§§ 1. 如何在沁恒CH58x中配置链接脚本以实现自定义内存布局? 2. 沁恒CH58x的SRAM地址范围是多少?如何避免地址冲突? 3. 如何在MounRiver Studio中调试并查看变量地址? 4. 沁恒CH58x的启动过程中如何初始化.data和.bss段? 5. RISC-V架构的沁恒CH58x中断向量表如何重定位?</think>在沁恒 CH58x 系列(特别是 CH584M)RISC-V 架构芯片中,将变量固定到特定地址的方法主要基于 GCC 编译器的特性和链接脚本控制。以下是具体实现方法: ### 一、使用 `__attribute__((section))` + 链接脚本(推荐方法) 这是最可靠且跨平台兼容的方式,适用于 MounRiver Studio 的 RISC-V GCC 工具链[^4]。 1. **代码中定义带段属性的变量**: ```c // 将变量放入自定义段 .fixed_addr_section uint32_t my_variable __attribute__((section(".fixed_addr_section"))) = 0x12345678; ``` 2. **修改链接脚本(.ld 文件)**: 在项目的链接脚本中(如 `CH58x.ld`)添加: ```ld MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 32K /* 根据实际芯片调整 */ } SECTIONS { /* 其他标准段... */ .fixed_addr 0x20001000 : { /* 指定绝对地址 */ KEEP(*(.fixed_addr_section)) /* 保持段不被优化 */ } > RAM } ``` ### 二、直接地址访问(简单场景) 对于不需要初始化的硬件寄存器或共享内存: ```c #define MY_REGISTER (*(volatile uint32_t *)(0x20001000)) void main() { MY_REGISTER = 0x55AA; // 直接写入地址 uint32_t val = MY_REGISTER; // 直接读取地址 } ``` ### 三、特殊编译器扩展(需验证支持) 某些工具链可能支持简化语法(需查阅具体编译器文档): ```c // 语法可能因工具链而异 __attribute__((at(0x20001000))) uint32_t my_var; // 部分编译器支持 ``` --- ### CH58x 关键注意事项 1. **地址有效性验证**: - SRAM 起始地址:`0x20000000`(参考 CH32V307 设计[^2]) - 使用前通过 `SystemInformation()` 函数确认实际内存范围 ```c extern uint32_t __RAM_start; // 链接脚本定义的符号 extern uint32_t __RAM_end; ``` 2. **多核/中断安全**: ```c // 对共享地址的操作需加锁 __disable_irq(); // 关中断 MY_SHARED_VAR += value; __enable_irq(); ``` 3. **调试验证**: - 在 MounRiver Studio 中: ```c printf("Var addr: %p", &my_variable); // 输出地址 ``` - 通过 Memory Viewer 直接查看 `0x20001000` 内容 --- ### 示例完整流程(MounRiver Studio) 1. 创建 `custom_sections.ld` 文件并添加到工程 2. 修改工程设置指定链接脚本: ``` Project Properties > C/C++ Build > Settings > Tool Settings > Linker > General > Script File ``` 3. 代码中使用: ```c // 固定到0x20001000的变量 __attribute__((section(".hw_reg"))) volatile uint32_t USB_STATUS_REG; // 链接脚本对应段 .usb_status 0x20001000 : { KEEP(*(.hw_reg)) } > RAM ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

永远的元子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值