• 使用RS485芯片进行串口通讯


    RS485 芯片说明

    开发板使用的 RS485 芯片是 SP3485E 芯片,其引脚功能如下:

    在这里插入图片描述
    值得说明的点:

    1. $ \overline{RE} $ 和 D I DI DI 正好逻辑互补,这样的好处在于我们可以用一根引脚的高低电平就可以控制该芯片的收发使能;
    2. A A A B B B 为极性互补的输出引脚;
    3. R O RO RO 为数据输入引脚连接到主控芯片, D I DI DI 为数据输出引脚连接到主控芯片;

    实操

    (安富莱)V7开发板 的RS485与芯片连接的原理图如下:在这里插入图片描述
    如上图,我们可以知道,PB11 和 PB10 对应为 USART3 的接收和发送引脚,连接到了芯片的 R O RO RO D I DI DI,对应数据的输出与接收。

    控制芯片收发的引脚为 PB2 引脚,在程序中:

    /* PB2 控制RS485芯片的发送使能 */
    #define RS485_TXEN_GPIO_CLK_ENABLE()      __HAL_RCC_GPIOB_CLK_ENABLE()
    #define RS485_TXEN_GPIO_PORT              GPIOB
    #define RS485_TXEN_PIN                    GPIO_PIN_2
    
    #define RS485_RX_EN()	RS485_TXEN_GPIO_PORT->BSRRH = RS485_TXEN_PIN	// 设置GPIO输出为1
    #define RS485_TX_EN()	RS485_TXEN_GPIO_PORT->BSRRL = RS485_TXEN_PIN	// 设置GPIO输出为0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们通过置位 BSRR 寄存器,直接对 ODR 寄存器(output data register)进行操作。

    发送程序

    void comSendBuf(COM_PORT_E _ucPort, uint8_t *_ucaBuf, uint16_t _usLen)
    {
    	UART_T *pUart;
    
    	pUart = ComToUart(_ucPort);
    	if (pUart == 0)
    	{
    		return;
    	}
    
    	if (pUart->SendBefor != 0)
    	{
    		pUart->SendBefor();		/* 如果是RS485通信,可以在这个函数中将RS485设置为发送模式 */
    	}
    
    	UartSend(pUart, _ucaBuf, _usLen);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    SendBefor() 是回调函数,该函数用于在发送前调用本质是调用 RS485_TX_EN() 开启 RS485 芯片的输出使能:

    void RS485_SendBefor(void)
    {
    	RS485_TX_EN();	/* 切换RS485收发芯片为发送模式 */
    }
    
    • 1
    • 2
    • 3
    • 4

    UartSend(pUart, _ucaBuf, _usLen) 实际的发送:

    static void UartSend(UART_T *_pUart, uint8_t *_ucaBuf, uint16_t _usLen)
    {
    	uint16_t i;
    
    	for (i = 0; i < _usLen; i++)
    	{
    		/* 如果发送缓冲区已经满了,则等待缓冲区空 */
    		while (1)
    		{
    			__IO uint16_t usCount;
    
    			DISABLE_INT();
    			usCount = _pUart->usTxCount;
    			ENABLE_INT();
    
    			if (usCount < _pUart->usTxBufSize)
    			{
    				break;
    			}
    			else if(usCount == _pUart->usTxBufSize)/* 数据已填满缓冲区 */
    			{
    				if((_pUart->uart->CR1 & USART_CR1_TXEIE) == 0)
    				{
    					SET_BIT(_pUart->uart->CR1, USART_CR1_TXEIE);
    				}  
    			}
    		}
    
    		/* 将新数据填入发送缓冲区 */
    		_pUart->pTxBuf[_pUart->usTxWrite] = _ucaBuf[i];
    
    		DISABLE_INT();
    		if (++_pUart->usTxWrite >= _pUart->usTxBufSize)
    		{
    			_pUart->usTxWrite = 0;
    		}
    		_pUart->usTxCount++;
    		ENABLE_INT();
    	}
    
    	SET_BIT(_pUart->uart->CR1, USART_CR1_TXEIE);	/* 使能发送中断(缓冲区空) */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    该函数主要为第 5 行的 for 循环,会将要发送的数据,从用户传入的 _ucaBuf 缓存区拷贝到软件层定义的 USART 的缓存区 _pUart->pTxBuf 中,如果缓存区已经满则需要等到缓存区先发送一部分数据,这时调用者会被阻塞。

    最后一行,开启 TXE 中断,每当发送一个到 USART 的 USART_TDR 寄存器的数据被移动到发送移位寄存器中,就会触发一个 TXE 中断(表面 TDR 寄存器已经为空)。

    详细 USART 操作逻辑参考:USART发送与接收_Bin Watson的博客-CSDN博客

    于是就会触发 USART3_IRQHandler 中断处理函数被调用,该函数最终调用 UartIRQ() 函数:

    static void UartIRQ(UART_T *_pUart)
    {
        uint32_t isrflags   = READ_REG(_pUart->uart->ISR);
    	uint32_t cr1its     = READ_REG(_pUart->uart->CR1);
    	uint32_t cr3its     = READ_REG(_pUart->uart->CR3);
        ...
    
    	/* 处理发送缓冲区空中断 */
    	if ( ((isrflags & USART_ISR_TXE) != RESET) && (cr1its & USART_CR1_TXEIE) != RESET)
    	{
    		//if (_pUart->usTxRead == _pUart->usTxWrite)
    		if (_pUart->usTxCount == 0)
    		{
    			/* 发送缓冲区的数据已取完时, 禁止发送缓冲区空中断 (注意:此时最后1个数据还未真正发送完毕)*/
    			//USART_ITConfig(_pUart->uart, USART_IT_TXE, DISABLE);
    			CLEAR_BIT(_pUart->uart->CR1, USART_CR1_TXEIE);
    
    			/* 使能数据发送完毕中断 */
    			//USART_ITConfig(_pUart->uart, USART_IT_TC, ENABLE);
    			SET_BIT(_pUart->uart->CR1, USART_CR1_TCIE);
    		}
    		else
    		{
    			_pUart->Sending = 1;
    			
    			/* 从发送FIFO取1个字节写入串口发送数据寄存器 */
    			//USART_SendData(_pUart->uart, _pUart->pTxBuf[_pUart->usTxRead]);
    			_pUart->uart->TDR = _pUart->pTxBuf[_pUart->usTxRead];
    			if (++_pUart->usTxRead >= _pUart->usTxBufSize)
    			{
    				_pUart->usTxRead = 0;
    			}
    			_pUart->usTxCount--;
    		}
    
    	}
    	
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    第 12 行的 if 条件,当 usTxCount 为 0 时,表明发送已经完成,这时我们保持 TXE 为置 1 状态,然后开启 TC 中断。当发送移位寄存器发送出最后一个数据后,在判断 TXE 为1的情况下就会触发 TC 中断,于是乎又会进入 UartIRQ()

    static void UartIRQ(UART_T *_pUart)
    {
        uint32_t isrflags   = READ_REG(_pUart->uart->ISR);
    	uint32_t cr1its     = READ_REG(_pUart->uart->CR1);
    	uint32_t cr3its     = READ_REG(_pUart->uart->CR3);
        ...
    
    	/* 数据bit位全部发送完毕的中断 */
    	if (((isrflags & USART_ISR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
    	{
    		//if (_pUart->usTxRead == _pUart->usTxWrite)
    		if (_pUart->usTxCount == 0)
    		{
    			/* 如果发送FIFO的数据全部发送完毕,禁止数据发送完毕中断 */
    			//USART_ITConfig(_pUart->uart, USART_IT_TC, DISABLE);
    			CLEAR_BIT(_pUart->uart->CR1, USART_CR1_TCIE);
    
    			/* 回调函数, 一般用来处理RS485通信,将RS485芯片设置为接收模式,避免抢占总线 */
    			if (_pUart->SendOver)
    			{
    				_pUart->SendOver();
    			}
    			
    			_pUart->Sending = 0;
    		}
    		else
    		{
    			/* 正常情况下,不会进入此分支 */
    
    			/* 如果发送FIFO的数据还未完毕,则从发送FIFO取1个数据写入发送数据寄存器 */
    			//USART_SendData(_pUart->uart, _pUart->pTxBuf[_pUart->usTxRead]);
    			_pUart->uart->TDR = _pUart->pTxBuf[_pUart->usTxRead];
    			if (++_pUart->usTxRead >= _pUart->usTxBufSize)
    			{
    				_pUart->usTxRead = 0;
    			}
    			_pUart->usTxCount--;
    		}
    	}
    	
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    这时候我们执行的逻辑就是上面的代码。主要是第 21 行,会调用 SendOver 回调函数,该函数本质是调用 RS485_RX_EN()

    void RS485_SendOver(void)
    {
    	RS485_RX_EN();	/* 切换RS485收发芯片为接收模式 */
    }
    
    • 1
    • 2
    • 3
    • 4

    于是乎我们重新将 RS485 置于接收状态。

    接收程序

    接收用中断来实现

    static void UartIRQ(UART_T *_pUart)
    {
    	uint32_t isrflags   = READ_REG(_pUart->uart->ISR);
    	uint32_t cr1its     = READ_REG(_pUart->uart->CR1);
    	uint32_t cr3its     = READ_REG(_pUart->uart->CR3);
    	
    	/* 处理接收中断  */
    	if ((isrflags & USART_ISR_RXNE) != RESET)
    	{
    		/* 从串口接收数据寄存器读取数据存放到接收FIFO */
    		uint8_t ch;
    
    		ch = READ_REG(_pUart->uart->RDR);
    		_pUart->pRxBuf[_pUart->usRxWrite] = ch;
    		if (++_pUart->usRxWrite >= _pUart->usRxBufSize)
    		{
    			_pUart->usRxWrite = 0;
    		}
    		if (_pUart->usRxCount < _pUart->usRxBufSize)
    		{
    			_pUart->usRxCount++;
    		}
    
    		/* 回调函数,通知应用程序收到新数据,一般是发送1个消息或者设置一个标记 */
    		//if (_pUart->usRxWrite == _pUart->usRxRead)
    		//if (_pUart->usRxCount == 1)
    		{
    			if (_pUart->ReciveNew)
    			{
    				_pUart->ReciveNew(ch); /* 比如,交给MODBUS解码程序处理字节流 */
    			}
    		}
    	}
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    接收程序比较简单,当 USART_RDR 寄存器接收到一个新数据时就会触发 RX 中断,最终还是调用 UartIRQ() 这个中断处理函数。

    如第 13 行的操作,将 RDR 寄存器中的置读取到 ch 中然后存入到软件层面的 USART 接收缓存区 _pUart->pRxBuf 中。

    第 30 行,ReciveNew() 这个回调函数用于配合一些协议的使用,如 ModBus 协议,我们需要在接收一个数据后进行一些处理操作就可以通过该回调函数进行实现。

    总结

    该程序源码来源于 安富莱V7开发板的第13个程序。

  • 相关阅读:
    [区间dp]Link with Bracket Sequence II 2022杭电多校第4场 1001
    都是星光赶路人
    Python(二)PyCharm
    玩转Mysql系列 - 第23篇:mysql索引管理详解
    vue项目学习笔记
    算法提升:图的Dijkstra(迪杰斯特拉)算法
    C语言既然可以自动为变量分配内存,为什么还要用动态分配内存呢?
    Java架构师高并发架构设计
    python基础知识
    Python Web应用:Python+Django+MySQL
  • 原文地址:https://blog.csdn.net/Bin_Watson/article/details/127590390