STM32 上的 USART 接收器

Posted

技术标签:

【中文标题】STM32 上的 USART 接收器【英文标题】:USART receiver on STM32 【发布时间】:2019-08-10 20:55:23 【问题描述】:

您好,我目前正在研究 USART 通信,尝试从任何 GPIO 引脚传输和接收数据。

在接收时,我成功地以任何波特率传输数据,但我卡在了某个点上。

我能够一次接收一个角色。引脚设置为外部下降沿中断,使用 RX 引脚。

但是当我从终端向控制器传输像“test”这样的字符串时,只接收到“t”,其余 3 个字符是垃圾值。我在想,在接收到第一个字符并保存后,下一个字符的中断不会那么快触发。

为了测试目的,此示例代码中的许多内容都进行了硬编码。

这里是接收器的示例代码

void EXTI0_IRQHandler(void)
r0 = GPIOA->IDR;
delay_us(delay_time);
r1 = GPIOA->IDR;
delay_us(delay_time);
r2 = GPIOA->IDR;
delay_us(delay_time);
r3 = GPIOA->IDR;
delay_us(delay_time);
r4 = GPIOA->IDR;
delay_us(delay_time);
r5 = GPIOA->IDR;
delay_us(delay_time);
r6 = GPIOA->IDR;
delay_us(delay_time);
r7 = GPIOA->IDR;
delay_us(delay_time);
r8 = GPIOA->IDR;
delay_us(delay_time);
r9 = GPIOA->IDR;
delay_us(delay_time);
r1 = r1 & 0x00000001;
r2 = r2 & 0x00000001;
r3 = r3 & 0x00000001;
r4 = r4 & 0x00000001;
r5 = r5 & 0x00000001;
r6 = r6 & 0x00000001;
r7 = r7 & 0x00000001;
r8 = r8 & 0x00000001;
x |= r8;
x = x << 1;
x |= r7;
x = x << 1;
x |= r6;
x = x << 1;
x |= r5;
x = x << 1;
x |= r4;
x = x << 1;
x |= r3;
x = x << 1;
x |= r2;
x = x << 1;
x |= r1;
buff1[z++] = x;
EXTI->PR |= 0X00000001;
x=0;
return ;

感谢您的帮助。

【问题讨论】:

停止位、数据位错? 对不起,我忘了提一些要点。我为 1 个起始位、8 个数据位和 1 个停止位应用了逻辑。 在中断处理程序中有很长的延迟(或任何延迟)是不好的做法,它们应该尽可能短。您有什么理由不使用 STM32 中提供的众多 USART 之一? 这些延迟是针对波特率校准的,例如针对 9600 波特率,延迟时间校准为 1\9600 = 104 us。没有什么特别的原因,我只是为了研究目的而尝试它。 这个代码首先违背了拥有 ISR 的全部目的。使用 UART 自己的中断或使用 DMA。这些延误是无稽之谈。 Bit-banging UART 是无稽之谈;过去 40 年发布的每一款 MCU 都有 UART 外设。 【参考方案1】:

您的解决方案的根本问题是您在转换点而不是位中心对位进行采样。在检测到 START 转换时,您只延迟一个位周期,因此在位转换而不是位 中心 处采样 r1 - 这几乎肯定会导致错误,尤其是在高速时边缘可能不是很快。第一个延迟应该是 1.5 位周期长。 (delay_time * 2 / 3) 如下图:

第二个问题是您在 STOP 位之后不必要地延迟,这将导致您错过下一个 START 转换,因为它可能发生在您清除中断标志之前。只要你有r8,你的工作就完成了。

采样r0r9 在任何情况下都没有任何用处,您在任何情况下都丢弃它们,并且状态r0 在任何事件中都隐含在EXTI 转换中,并且r9 仅在发送者是生成无效帧。此外,如果您没有对r9 进行采样,那么延迟也将变得不必要。这些行应该被删除:

delay_us(delay_time);
r9 = GPIOA->IDR;
delay_us(delay_time);

这至少会为您提供两个位周期,您的处理器可以在其中执行其他工作,而不是卡在中断上下文中,但延迟是一个中断处理程序不是一个好习惯 - 它会阻止正常代码和所有低优先级中断的执行使解决方案不适合实时系统。在这种情况下,如果软 UART Rx 是系统所要做的全部,您可能会通过简单地轮询 GPIO 而不是使用中断来获得更好的结果 - 至少其他中断可以正常运行,并且实现起来要简单得多.

您的“展开循环”实现在适当的延迟上也没有任何实际用途 - 即使在非常高的比特率下,循环开销在帧的持续时间内也可能微不足道,如果是这样,您可以调整延迟一点来弥补:

void EXTI0_IRQHandler(void)

    delay_us(delay_time * 2 / 3);
    for( int i = 7; i >= 0; i-- )
    
        x |= GPIOA->IDR << i ;
        delay_us(delay_time);
    

    EXTI->PR |= 0X00000001;
    buff1[z++] = x;
    x = 0 ;
    return ;

一个更强大的软接收器解决方案可以与系统中的其他进程很好地配合使用,应该只使用 EXTI 中断来检测起始位;处理程序应禁用 EXTI,并以波特率加半位周期启动计时器。定时器的中断处理程序在位周期的中心对 GPIO 引脚进行采样,并在 EXTI 之后的第一个中断时,将周期更改为一个位周期。对于每个定时器中断,它都会对位进行采样和计数,直到整个数据字被移入,此时它会禁用定时器并为下一个起始位重新启用 EXTI。

我已经成功地在 STM32 上以 4800 运行 120MHz 并将其推至 38400,但在 26 微秒/位时,它在中断上下文中变得非常繁忙,您的应用程序可能还有其他事情要做?

以下是我的实现的稍微通用化的版本。它使用 STM32 标准外设库调用,而不是直接寄存器访问或后来的 STM32Cube HAL,但您可以根据需要轻松地以一种或另一种方式移植它。取景为 N,8,1。

#define SOFT_RX__BAUD = 4800u ;
#define SOFT_RX_TIMER_RELOAD = 100u ;

void softRxInit( void )

    // Enable SYSCFG clock
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

    // Connect the EXTI Line to GPIO Pin
    SYSCFG_EXTILineConfig( EXTI_PortSourceGPIOB, EXTI_PinSource0 );

    TIM_Cmd( TIM10, DISABLE);

    // NVIC initialisation
    NVIC_InitTypeDef NVIC_InitStructure = 0,0,0,DISABLE;
    NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_TIM10_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 12; 
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    // Enable peripheral clock to timers
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM10, ENABLE);

    TIM_ARRPreloadConfig( TIM10, DISABLE );

    // Generate soft Rx rate clock (4800 Baud)
    TIM_TimeBaseInitTypeDef init = 0;
    TIM_TimeBaseStructInit( &init ) ;
    init.TIM_Period = static_cast<uint32_t>( SOFT_RX_TIMER_RELOAD );
    init.TIM_Prescaler = static_cast<uint16_t>( (TIM10_ClockRate() / (SOFT_RX__BAUD * SOFT_RX_TIMER_RELOAD)) - 1 );
    init.TIM_ClockDivision = 0;
    init.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit( TIM10, &init ) ;

    // Enable the EXTI Interrupt in the NVIC
    NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 12;  
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init( &NVIC_InitStructure );

    // Dummy call to handler to force initialisation 
    // of UART frame state machine
    softRxHandler() ;


// Soft UART Rx START-bit interrupt handler
void EXTI0_IRQHandler()

    // Shared interrupt, so verify that it is the correct one
    if( EXTI_GetFlagStatus( EXTI_Line0 ) == SET )
    
        // Clear the EXTI line pending bit.
        // Same as EXTI_ClearITPendingBit( EXTI_Line11 )
        EXTI_ClearFlag( EXTI_Line0 ) ;

        // Call Soft UART Rx handler
        softRxHandler() ;
    


void TIM1_UP_TIM10_IRQHandler( void )

    // Call Soft UART Rx handler
    softRxHandler() ;

    TIM_ClearITPendingBit( TIM10, TIM_IT_Update );


// Handler for software UART Rx
inline void softRxHandler()

    static const int START_BIT = -1 ;
    static const int STOP_BIT = 8 ;
    static const int HALF_BIT = SOFT_RX_TIMER_RELOAD / 2;
    static const int FULL_BIT = SOFT_RX_TIMER_RELOAD ;
    static int rx_bit_n = STOP_BIT ;
    static const uint8_t RXDATA_MSB = 0x80 ;
    static uint8_t rx_data = 0 ;
    static EXTI_InitTypeDef extiInit =  EXTI_Line0,
                                         EXTI_Mode_Interrupt,
                                         EXTI_Trigger_Falling,
                                         DISABLE  ;

    // Switch START-bit/DATA-bit
    switch( rx_bit_n )
    
        case START_BIT :
        
            // Stop waiting for START_BIT
            extiInit.EXTI_LineCmd = DISABLE;
            EXTI_Init( &extiInit );

            // Enable the Interrupt
            TIM_ClearITPendingBit( TIM10, TIM_IT_Update );
            TIM_ITConfig( TIM10, TIM_IT_Update, ENABLE );

            // Enable the timer (TIM10)
            // Set time to hit centre of data LSB
            TIM_SetAutoreload( TIM10, FULL_BIT + HALF_BIT ) ;
            TIM_Cmd( TIM10, ENABLE );

            // Next = LSB data
            rx_data = 0 ;
            rx_bit_n++ ;
        
        break ;

        // STOP_BIT is only set on first-time initialisation as a state, othewise it is
        // transient within this scase.
        // Use fall through and conditional test to allow
        // case to handle both initialisation and UART-frame (N,8,1) restart.
        case STOP_BIT :
        default :   // Data bits
        
            TIM_ClearITPendingBit( TIM10, TIM_IT_Update );

            if( rx_bit_n < STOP_BIT )
            
                if( rx_bit_n == 0 )
                
                    // On LSB reset time to hit centre of successive bits
                    TIM_SetAutoreload( TIM10, FULL_BIT ) ;
                

                // Shift last bit toward LSB (emulate UART shift register)
                rx_data >>= 1 ;

                // Read Rx bit from GPIO
                if( GPIO_ReadInputDataBit( GPIOB, GPIO_Pin_0 ) != 0 )
                
                    rx_data |= RXDATA_MSB ;
                

                // Next bit
                rx_bit_n++ ;
            

            // If initial state or last DATA bit sampled...
            if( rx_bit_n == STOP_BIT )
            
                // Stop DATA-bit sample timer
                TIM_Cmd( TIM10, DISABLE );

                // Wait for new START-bit
                rx_bit_n = START_BIT ;
                extiInit.EXTI_LineCmd = ENABLE;
                EXTI_Init( &extiInit );

                // Place character in Rx buffer
                serialReceive( rx_data ) ;
            
        
        break ;
    

代码的工作方式与上面时序图中所示的真实 UART 相同,但在我的实现中,STOP 位实际上没有被采样 - 这是不必要的;它仅用于确保随后的 START 位是 1 -> 0 转换,通常可以忽略。如果实际的 UART 不是 1,它可能会产生帧错误,但如果您无论如何都不打算处理此类错误,则检查没有任何目的。

【讨论】:

您的代码可能对 115200 波特率没有帮助。因为在很多情况下,当你用汇编语言编写代码时,你的代码中有跳转指令,而我在使用 115200 buad 速率时只有 8 个 us。许多传感器预编程为 115200 波特率。但我喜欢你的逻辑,并会尝试通过一些修改来实现它 @Vaibhav:115200 的软 UART 在任何情况下都是雄心勃勃的(每比特 8.7 微秒)。您的解决方案只是意味着您的处理器将无法对接收到的数据进行任何处理,因为它始终位于中断处理程序中。 @Vaibhav :在之前的评论中,您认为这是一项学术练习。因此,达到 115200 不会增加您对 UART 的理解。看来您的主要问题只是时间不正确 - 起始位转换后的延迟太短。在许多情况下,高速软 UART 施加的 CPU 负载会影响应用程序的实际功能。 作为学术练习并不意味着如果我能够达到一个水平,我就会停止它。当我能够达到每个级别时,锻炼就结束了。感谢您的关心。 @Vaibhav :您错过了我的观点 - 速度越来越快并不会增强您的“更好地理解 UART”,这是您在评论中声明的目标。此外,115200“每个级别”在多大程度上 - STM32 h/w UART 可以实现每秒兆比特范围内的速率。除了您的问题没有说明所需的比特率之外,您在 cmets 中只提到了 9600。这种解决方案可能无法达到 115200 是一种不公平的批评。它允许其他进程同时运行,而您的进程会阻塞处理器。【参考方案2】:

我在您的代码中看不到您考虑到通常是串行传输一部分的起始位。您似乎只在寻找 8 个数据位和一个停止位。

按照“起始位是停止位的倒数”的约定,您的代码将在字符之间检测到一个额外的边缘,从而显然将您检测到的位流移动一位。

【讨论】:

我写的逻辑对单个字符运行良好。休息,因为中断是下降沿触发的,因此在启动条件下会自动调用中断。然后 r0 负责启动条件,而 r9 负责停止条件。 我假设您在停止位下降沿之前重新启用中断。这为您提供了一个额外的触发器和一位移位。检查你在第一个字符之后得到的“垃圾” - 如果它移动了一位,那就是问题。 我在想我赶上下一个角色的下降沿迟到了。按照通用usart时序图拉高线时产生停止位 @Vaibhav 不必要地对停止位进行延迟采样可能是您只获得一个字符的原因 - 当下一个起始位到达时您处于中断上下文中。停止位可以忽略;它只是为了确保起始位是一个转换。【参考方案3】:

您提到发送字符串“test”时会收到字符“t”。 在字符串中引入足够的字符间延迟。 希望它有效。 您可以使用 docklite 发送具有字符间延迟的字符串。

【讨论】:

以上是关于STM32 上的 USART 接收器的主要内容,如果未能解决你的问题,请参考以下文章

STM32:通过 USART 接收数据

STM32F103(二十五)完美解决USART发送接收floatu16u32数据

STM32F103(二十五)完美解决USART发送接收floatu16u32数据

STM32 一直进入串口接收中断

STM32串口通讯程序

STM32串口中断接收问题