开发板使用的 RS485 芯片是 SP3485E 芯片,其引脚功能如下:
(安富莱)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
我们通过置位 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);
}
SendBefor()
是回调函数,该函数用于在发送前调用本质是调用 RS485_TX_EN()
开启 RS485 芯片的输出使能:
void RS485_SendBefor(void)
{
RS485_TX_EN(); /* 切换RS485收发芯片为发送模式 */
}
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); /* 使能发送中断(缓冲区空) */
}
该函数主要为第 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--;
}
}
...
}
第 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--;
}
}
...
}
这时候我们执行的逻辑就是上面的代码。主要是第 21 行,会调用 SendOver 回调函数,该函数本质是调用 RS485_RX_EN()
:
void RS485_SendOver(void)
{
RS485_RX_EN(); /* 切换RS485收发芯片为接收模式 */
}
于是乎我们重新将 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解码程序处理字节流 */
}
}
}
...
}
接收程序比较简单,当 USART_RDR 寄存器接收到一个新数据时就会触发 RX 中断,最终还是调用 UartIRQ()
这个中断处理函数。
如第 13 行的操作,将 RDR 寄存器中的置读取到 ch 中然后存入到软件层面的 USART 接收缓存区 _pUart->pRxBuf
中。
第 30 行,ReciveNew()
这个回调函数用于配合一些协议的使用,如 ModBus 协议,我们需要在接收一个数据后进行一些处理操作就可以通过该回调函数进行实现。
该程序源码来源于 安富莱V7开发板的第13个程序。