查看: 1430|回复: 1

[分享] 串口(UART)自动波特率识别程序设计

[复制链接]
  • TA的每日心情
    开心
    2024-3-26 15:16
  • 签到天数: 266 天

    [LV.8]以坛为家I

    3299

    主题

    6546

    帖子

    0

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    32024
    最后登录
    2024-4-25
    发表于 2021-6-15 17:43:16 | 显示全部楼层 |阅读模式
    串口(UART)自动波特率识别程序设计
    大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是嵌入式里串口(UART)自动波特率识别程序设计与实现。


    串口(UART)是嵌入式里最基础最常用也最简单的一种通讯(数据传输)方式,可以说是工程师入门通讯领域的启蒙老师,同时串口打印也是嵌入式项目里非常经典的调试与交互方式。


    最精简的串口仅使用两根单向信号线:TXD、RXD,这两根信号线是独立工作的,因此数据收发既可分开也可同时进行,这就是所谓的全双工。串口没有主从机概念,并且没有专门的时钟信号 SCK,所以串口通信也属于异步传输。


    说到异步传输,这就不得不提波特率(每秒钟传输bit数)的问题了,通信双方必须使用一致的波特率才能完成正确的数据传输。正常情况下,我们都是为两个串口设备事先约定好波特率,比如 MCU 与上位机通信,在 MCU 程序里按 115200 的波特率去初始化 UART 外设,然后上位机串口调试助手也设置 115200 波特率,双方再联合工作。


    有时候,我们也希望能有一种灵活的波特率约定方式,比如建立通信前,在上位机串口调试助手里随意设置一种波特率,然后按这个波特率发送数据,MCU 端能自动识别出这个波特率,并用识别出来的波特率去初始化 UART 外设,然后再进行后续数据传输,这种方式就叫自动波特率识别。痞子衡今天要分享的就是在 MCU 里实现自动波特率识别的程序设计:
    1. 程序主页:https://github.com/JayHeng/cortex-m-apps/tree/master/components/autobaud
    复制代码
    一、串口(UART)自动波特率识别程序设计
    1.1 函数接口定义
    首先是设计自动波特率识别程序头文件:autobaud.h ,这个头文件里直接定义如下 3 个接口函数原型。涵盖必备的初始化流程 init()、deinit(),以及最核心的波特率识别功能 get_rate()。
    1. //! @brief 初始化波特率识别
    2. void autobaud_init(void);

    3. //! @brief 检测波特率识别是否已完成,并获取波特率值
    4. bool autobaud_get_rate(uint32_t *rate);

    5. //! @brief 关闭波特率识别
    6. void autobaud_deinit(void);
    复制代码
    1.2 识别设计思想
    关于识别,因为上位机数据是从 RXD 引脚过来的,所以在 MCU 里需要先将 RXD 引脚配置成普通数字输入 GPIO(这个引脚需要上拉,默认保持高电平),然后检测这个 GPIO 的电平跳变(一般用下降沿)并计时。


    下图是典型的 UART 单字节传输时序,I/O 空闲状态是高电平,传输时总是由 1bit 低电平起始位开启,然后是从 LSB 到 MSB 的 8bit 数据位,校验位是可选项(我们暂不开启),最后由 1bit 高电平停止位结束,I/O 回归高电平空闲状态。
    1. Note 1:检测下降沿跳变,是因为 I/O 空闲为高,起始位的存在保证了每 Byte 传输周期总是从下降沿开始。
    2. Note 2:起始位和停止位两个 bit 的存在还兼有波特率容错的功能,通信双方波特率在 3% 的误差内数据传输均可以正常进行。
    复制代码
    11.png
    虽然我们不需要约定上位机波特率,但是要想实现波特率自动识别,上位机初始传输的数据却必须要事先约定好(可理解为接头暗号),这涉及到 MCU 里检测电平跳变次数与相应计时计算。MCU识别完成后将暗号发回给上位机确认。


    痞子衡设计的接头暗号是 0x5A, 0xA6 两个字节,两字节暗号相比单字节暗号容错性更好一些(以防 I/O 上有干扰,导致误识别),根据指定的暗号和 UART 传输时序图,我们很容易得到如下常量定义:
    1. enum _autobaud_counts
    2. {
    3.     //! 0x5A 字节对应的下降沿个数
    4.     kFirstByteRequiredFallingEdges = 4,
    5.     //! 0xA6 字节对应的下降沿个数
    6.     kSecondByteRequiredFallingEdges = 3,
    7.     //! 0x5A 字节(从起始位到停止位)第一个下降沿到最后一个下降沿之间的实际bit数
    8.     kNumberOfBitsForFirstByteMeasured = 8,
    9.     //! 0xA6 字节(从起始位到停止位)第一个下降沿到最后一个下降沿之间的实际bit数
    10.     kNumberOfBitsForSecondByteMeasured = 7,
    11.     //! 两个下降沿之间允许的最大超时(us)
    12.     kMaximumTimeBetweenFallingEdges = 80000,
    13.     //! 对实际检测出的波特率值做对齐处理,以便于更好地配置UART模块
    14.     kAutobaudStepSize = 1200
    15. };
    复制代码
    上述常量定义里,kMaximumTimeBetweenFallingEdges 指定了两个下降沿之间允许的最大时间间隔,超过这个时间,自动波特率程序将丢掉前面统计的下降沿个数,重头开始识别,这个设计也是为了防止 I/O 上有电平干扰,导致误识别。


    kAutobaudStepSize 常量是为了对检测出的波特率值做对齐处理,公式是 rounded = stepSize * (value/stepSize + 0.5),其中 value 是实际检测出的波特率值,rounded 是对齐后的波特率值,用对齐后的波特率值能更好地配置UART外设(这跟UART模块里波特率发生器SBR设计有关)。


    最后就是 I/O 电平下降沿检测方法设计,这里既可以用软件查询(就是循环读取 I/O 输入电平,比较当前值与上一次值的差异),也可以使用GPIO模块自带的边沿中断功能。推荐使用后者,一方面计时更精确,另外也不用阻塞系统。检测到下降沿发生就调用一次如下 pin_transition_callback() 函数,在这个函数里统计跳变次数以及计时。
    1. //! @brief 管脚下降沿跳变回调函数
    2. static void pin_transition_callback(void);
    复制代码
    1.3 主代码实现
    根据上一小节描述的设计思想,我们很容易写出下面的主代码(autobaud_irq.c),代码里痞子衡都做了详细注释。有一点要提的是关于其中系统计时,可参考痞子衡旧文 《嵌入式里通用微秒(microseconds)计时函数框架设计与实现》 。
    1. //! @brief 使能GPIO管脚中断
    2. extern void enable_autobaud_pin_irq(pin_irq_callback_t func);
    3. //! @brief 关闭GPIO管脚中断
    4. extern void disable_autobaud_pin_irq(void);

    5. //!< 已检测到的下降沿个数
    6. static uint32_t s_transitionCount;
    7. //!< 0x5A 字节检测期间内对应计数值
    8. static uint64_t s_firstByteTotalTicks;
    9. //!< 0xA6 字节检测期间内对应计数值
    10. static uint64_t s_secondByteTotalTicks;
    11. //!< 上一次下降沿发生时系统计数值
    12. static uint64_t s_lastToggleTicks;
    13. //!< 下降沿之间最大超时对应计数值
    14. static uint64_t s_ticksBetweenFailure;

    15. void autobaud_init(void)
    16. {
    17.     s_transitionCount = 0;
    18.     s_firstByteTotalTicks = 0;
    19.     s_secondByteTotalTicks = 0;
    20.     s_lastToggleTicks = 0;
    21.     // 计算出下降沿之间最大超时对应计数值
    22.     s_ticksBetweenFailure = microseconds_convert_to_ticks(kMaximumTimeBetweenFallingEdges);
    23.     // 使能GPIO管脚中断,并注册中断处理回调函数
    24.     enable_autobaud_pin_irq(pin_transition_callback);
    25. }

    26. void autobaud_deinit(void)
    27. {
    28.     // 关闭GPIO管脚中断
    29.     disable_autobaud_pin_irq();
    30. }

    31. bool autobaud_get_rate(uint32_t *rate)
    32. {
    33.     if (s_transitionCount == (kFirstByteRequiredFallingEdges + kSecondByteRequiredFallingEdges))
    34.     {
    35.         // 计算出实际检测到的波特率值
    36.         uint32_t calculatedBaud =
    37.             (microseconds_get_clock() * (kNumberOfBitsForFirstByteMeasured + kNumberOfBitsForSecondByteMeasured)) /
    38.             (uint32_t)(s_firstByteTotalTicks + s_secondByteTotalTicks);

    39.         // 对实际检测出的波特率值做对齐处理
    40.         // 公式:rounded = stepSize * (value/stepSize + .5)
    41.         *rate = ((((calculatedBaud * 10) / kAutobaudStepSize) + 5) / 10) * kAutobaudStepSize;

    42.         return true;
    43.     }
    44.     else
    45.     {
    46.         return false;
    47.     }
    48. }

    49. void pin_transition_callback(void)
    50. {
    51.     // 获取当前系统计数值
    52.     uint64_t ticks = microseconds_get_ticks();
    53.     // 计数这次检测到的下降沿
    54.     s_transitionCount++;

    55.     // 如果本次下降沿与上次下降沿之间间隔过长,则从头开始检测
    56.     uint64_t delta = ticks - s_lastToggleTicks;
    57.     if (delta > s_ticksBetweenFailure)
    58.     {
    59.         s_transitionCount = 1;
    60.     }

    61.     switch (s_transitionCount)
    62.     {
    63.         case 1:
    64.             // 0x5A 字节检测时间起点
    65.             s_firstByteTotalTicks = ticks;
    66.             break;

    67.         case kFirstByteRequiredFallingEdges:
    68.             // 得到 0x5A 字节检测期间内对应计数值
    69.             s_firstByteTotalTicks = ticks - s_firstByteTotalTicks;
    70.             break;

    71.         case (kFirstByteRequiredFallingEdges + 1):
    72.             // 0xA6 字节检测时间起点
    73.             s_secondByteTotalTicks = ticks;
    74.             break;

    75.         case (kFirstByteRequiredFallingEdges + kSecondByteRequiredFallingEdges):
    76.             // 得到 0xA6 字节检测期间内对应计数值
    77.             s_secondByteTotalTicks = ticks - s_secondByteTotalTicks;
    78.             // 关闭GPIO管脚中断
    79.             disable_autobaud_pin_irq();
    80.             break;
    81.     }

    82.     // 记录本次下降沿发生时系统计数值
    83.     s_lastToggleTicks = ticks;
    84. }
    复制代码
    二、串口(UART)自动波特率识别程序实现
    前面讲的都是硬件无关设计,但最终还是要落实到具体 MCU 平台上的,其中 GPIO 中断部分是跟 MCU 紧相关的。我们以恩智浦 i.MXRT1011 为例来介绍硬件实现。


    2.1 管脚中断方式实现(基于i.MXRT1011)
    恩智浦 MIMXRT1010-EVK 有板载调试器 DAPLink,这个 DAPLink 中也集成了 USB 转串口的功能,对应的 UART 引脚是 IOMUXC_GPIO_09_LPUART1_RXD 和 IOMUXC_GPIO_10_LPUART1_TXD,我们就选用这个管脚 GPIO1[9] 做自动波特率检测,实现代码如下:
    1. BSP程序:https://github.com/JayHeng/cortex-m-apps/tree/master/apps/autobaud_imxrt1011/bsp/src/pinmux_utility.c
    复制代码
    1. typedef void (*pin_irq_callback_t)(void);
    2. static pin_irq_callback_t s_pin_irq_func;

    3. //! @brief UART引脚功能切换函数
    4. void uart_pinmux_config(bool setGpio)
    5. {
    6.     if (setGpio)
    7.     {
    8.         IOMUXC_SetUartAutoBaudPinMode(IOMUXC_GPIO_09_GPIOMUX_IO09, GPIO1, 9);
    9.     }
    10.     else
    11.     {
    12.         IOMUXC_SetUartPinMode(IOMUXC_GPIO_09_LPUART1_RXD);
    13.         IOMUXC_SetUartPinMode(IOMUXC_GPIO_10_LPUART1_TXD);
    14.     }
    15. }

    16. //! @brief 使能GPIO管脚中断
    17. void enable_autobaud_pin_irq(pin_irq_callback_t func)
    18. {
    19.     s_pin_irq_func = func;
    20.     // 开启GPIO1_9下降沿中断
    21.     GPIO_SetPinInterruptConfig(GPIO1, 9, kGPIO_IntFallingEdge);
    22.     GPIO1->IMR |= (1U << 9);
    23.     NVIC_SetPriority(GPIO1_Combined_0_15_IRQn, 1);
    24.     NVIC_EnableIRQ(GPIO1_Combined_0_15_IRQn);
    25. }

    26. //! @brief GPIO中断处理函数
    27. void GPIO1_Combined_0_15_IRQHandler(void)
    28. {
    29.     uint32_t interrupt_flag = (1U << 9);
    30.     // 仅当GPIO1_9中断发生时
    31.     if ((GPIO_GetPinsInterruptFlags(GPIO1) & interrupt_flag) && s_pin_irq_func)
    32.     {
    33.         //执行一次回调函数
    34.         s_pin_irq_func();
    35.         GPIO_ClearPinsInterruptFlags(GPIO1, interrupt_flag);
    36.     }
    37. }
    复制代码
    2.2 在MIMXRT1010-EVK上实测
    一切就绪,我们现在来实测一下,主函数流程很简单,测试结果也表明达到了预期效果,每次将 MCU 程序复位运行后,串口调试助手里可任意设置波特率。
    1. int main(void)
    2. {
    3.     // 略去系统时钟配置...
    4.     // 初始化定时器
    5.     microseconds_init();
    6.     // 将GPIO1_9先配成输入GPIO
    7.     bool setGpio = true;
    8.     uart_pinmux_config(setGpio);
    9.     // 初始化波特率识别
    10.     autobaud_init();
    11.     // 检测波特率识别是否已完成,并获取波特率值
    12.     uint32_t baudrate;
    13.     while (!autobaud_get_rate(&baudrate));
    14.     // 关闭波特率识别
    15.     autobaud_deinit();
    16.     // 配置UART1引脚
    17.     setGpio = false;
    18.     uart_pinmux_config(setGpio);
    19.     // 初始化UART1外设
    20.     uint32_t uartClkSrcFreq = BOARD_DebugConsoleSrcFreq();
    21.     DbgConsole_Init(1, baudrate, kSerialPort_Uart, uartClkSrcFreq);

    22.     PRINTF("Autobaud test success\r\n");
    23.     PRINTF("Detected baudrate is %d\r\n", baudrate);

    24.     while (1);
    25. }
    复制代码
    12.png
    至此,嵌入式里串口(UART)自动波特率识别程序设计与实现痞子衡便介绍完毕了,掌声在哪里~~~







    签到签到
    回复

    使用道具 举报

  • TA的每日心情
    擦汗
    2021-9-9 22:51
  • 签到天数: 415 天

    [LV.9]以坛为家II

    79

    主题

    3088

    帖子

    21

    金牌会员

    Rank: 6Rank: 6

    积分
    5181
    最后登录
    2022-5-23
    发表于 2021-6-17 00:13:45 | 显示全部楼层
      来来来 掌声在这里,赞赞赞!
    该会员没有填写今日想说内容.
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /4 下一条

    Archiver|手机版|小黑屋|恩智浦技术社区

    GMT+8, 2024-4-25 15:12 , Processed in 0.111644 second(s), 22 queries , MemCache On.

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.

    快速回复 返回顶部 返回列表