本章教程为大家讲解USART应用之485总线。虽然这几年无线网络的使用率有所上升,有线的串行网络仍然提供最有力、最可靠的通信,特别是在恶劣的环境中。在需要抗噪、抗静电、抗电压故障的工业,建筑自动化领域仍然是有线通信的天下。
31.1 初学者重要提示
- 学习本章节前,务必优先学习第30章,RS485用到的串口FIFO也是建立在30章的基础上。
- 了解了本章31.2和31.3小节的基础知识后,强烈推荐看此贴的两个文档,对RS485讲解的比较透彻,中文版:http://www.armbbs.cn/forum.php?mod=viewthread&tid=90753 。
- STM32H7支持RS485的硬件流控制,即有一个专门的引脚来控制485 PHY的收发状态切换。V7开发板用的USART3,需要用PD12来控制,而这个引脚要用于FMC,所以用的是一个通用IO。
- 经常会有网友咨询为什么程序里面收发切换没有做延迟处理,这里就涉及到一个关键的知识点TXE发送空中断和TC发送完成中断的区别,详细看教程中说明即可。
31.2 RS485的基础知识
智能仪表是随着80年代初单片机技术的成熟而发展起来的,现在世界仪表市场基本被智能仪表所垄断。究其原因就是企业信息化的需要,企业在仪表选型时其中的一个必要条件就是要具有联网通讯接口。最初是数据模拟信号输出简单过程量,后来仪表接口是RS232接口,这种接口可以实现点对点的通信方式,但这种方式不能实现联网功能。随后出现的RS485解决了这个问题。
EIA-485(过去叫做RS-485或者RS485)是隶属于OSI模型物理层的电气特性规定为2线、半双工、平衡传输线多点通信的标准,是由电信行业协会(TIA)及电子工业联盟(EIA)联合发布的标准。实现此标准的数字通信网可以在有电子噪声的环境下进行长距离有效率的通信。在线性多点总线的配置下,可以在一个网络上有多个接收器。因此适用在工业环境中。
EIA一开始将RS(Recommended Standard)做为标准的前缀,不过后来为了便于识别标准的来源,已将RS改为EIA/TIA。电子工业联盟(EIA)已结束运作,此标准目前是电信行业协会(TIA)维护,名称为TIA-485,但工程师仍继续用RS-485来称呼此协议。
RS-485的数据最高传输速率为10Mbsp。
RS-485接口是采用平衡驱动器和差分接收器的组合,抗共模干扰能力增强,即抗干扰噪声性好。
RS-485最大的通信距离约为1219m,最高传输速率为10Mbsp,传输速率与传输距离成反比,在100Kb/S的传输速率下,才可以达到最大的通信距离,如果需传输更长的距离,需要加485中继器。RS-485总线一般最大支持32个节点,如果使用特制的485芯片,可以达到128个或者256个节点,最大的可以支持到400个节点。
关于RS485的逻辑状态,不同厂家的芯片的定义可能不同,但不影响正常的数据收发,这里以TI的为例做个说明,TI的定义方式如下:
A表示非反向输出non-inverting output,B表示反向输出inverting output。
当VA > VB 的时候表示逻辑状态0,被称为ON。
当VA < VB 的时候表示逻辑状态1,被称为OFF。
对应到实际芯片框图上就是下面这样(DE发送使能,D是发送数据端,RE是接收使能,R是接收数据端):
当用户在D(Driver)引脚输入逻辑高电平时,将在485总线上实现逻辑状态0,即ON状态。接收端R(Receiver)将收到逻辑高电平。
当用户在D(Driver)引脚输入逻辑低电平时,将在485总线上实现逻辑状态1,即OFF状态。接收端R(Receiver)将收到逻辑低电平。
发送状态下,大于|±1.5V |可以有效表示逻辑状态1和逻辑状态0:
接收状态下,大于|±200mv|可以有效表示逻辑状态1和逻辑状态0:
31.3 RS485硬件设计
STM32H743XIH6最多可以支持8个独立的串口。其中串口4和串口5和SDIO的GPIO是共用的,也就是说,如果要用到SD卡,那么串口4和串口5将不能使用。串口7和SPI3共用,串口8和RGB硬件接口共用。串口功能可以分配到不同的GPIO。我们常用的管脚分配如下:
串口USART1 TX = PA9, RX = PA10
串口USART2 TX = PA2, RX = PA3
串口USART3 TX = PB10, RX = PB11
串口UART4 TX = PC10, RX = PC11 (和SDIO共用)
串口UART5 TX = PC12, RX = PD2 (和SDIO共用)
串口USART6 TX = PG14, RX = PC7
串口UART7 TX = PB4, RX = PB3 (和SPI1/3共用)
串口UART8 TX = PJ8, RX =PJ9 (和RGB硬件接口共用)
STM32-V7开发板使用了4个串口设备。
- 串口1用于RS232接口,很多例子的pritnf结果就是输出到串口1
- 串口2用于GPS
- 串口3用于RS485接口
- 串口6 用于TTL串口插座,板子上有GPRS插座和串口WIFI插座。
下面是相关的原理图:
串口3,RS485
关于485的PHY芯片SP3485E要注意以下几个问题:
- SP3485E允许在同一总线上连接32个收发器。
- PB2(RS485_TX_EN)引脚控制收发状态,高电平表示使能发送,低电平表示用于接收。
- 由于 485的RO是输出引脚,如果软件上将PB2配置为输出就会冲突(PB2输出高,RO输出低就会短路)。加电阻的话,PB2就可以做其他用途。主要是开发板才这样弄,正式产品不加。
- 电阻R15和R165的作用是避免CPU复位期间,TX为高阻时影响总线数据。
- 电阻R4和R2是保证空闲时处于确定的逻辑状态,提供可靠性。
- 电阻R3是终端电阻。使用终端电阻是为了阻抗匹配,防止不匹配引起的噪声干扰。一般在300米以下,19200bps不需要接终端电阻。终端电阻要接在传输总线的两端。
31.4 RS485驱动设计
RS485的驱动实现是建立在第31章讲解的串口FIFO基础上,关键的知识点已经在第31章节做了详细讲解,这里把485驱动涉及到的两个关键地方做个说明。
31.4.1 RS485驱动初始化
RS485驱动的初始化要对收发控制引脚进行配置,这点要注意,对应的代码如下:

/*********************************************************************************************************** 函 数 名: bsp_InitUart* 功能说明: 初始化串口硬件,并对全局变量赋初值.* 形 参: 无* 返 回 值: 无**********************************************************************************************************/void bsp_InitUart(void){ UartVarInit(); /* 必须先初始化全局变量,再配置硬件 */ InitHardUart(); /* 配置串口的硬件参数(波特率等) */ RS485_InitTXE(); /* 配置RS485芯片的发送使能硬件,配置为推挽输出 */}/*********************************************************************************************************** 函 数 名: RS485_InitTXE* 功能说明: 配置RS485发送使能口线 TXE* 形 参: 无* 返 回 值: 无**********************************************************************************************************/void RS485_InitTXE(void){ GPIO_InitTypeDef gpio_init; /* 打开GPIO时钟 */ RS485_TXEN_GPIO_CLK_ENABLE(); /* 配置引脚为推挽输出 */ gpio_init.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */ gpio_init.Pull = GPIO_NOPULL; /* 上下拉电阻不使能 */ gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* GPIO速度等级 */ gpio_init.Pin = RS485_TXEN_PIN; HAL_GPIO_Init(RS485_TXEN_GPIO_PORT, &gpio_init); }
31.4.2 RS485驱动回调函数初始化
由于RS485是半双工,收发要做切换,初始化的时候专门配套了回调函数:

/*********************************************************************************************************** 函 数 名: UartVarInit* 功能说明: 初始化串口相关的变量* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void UartVarInit(void){#if UART3_FIFO_EN == 1 g_tUart3.uart = USART3; /* STM32 串口设备 */ g_tUart3.pTxBuf = g_TxBuf3; /* 发送缓冲区指针 */ g_tUart3.pRxBuf = g_RxBuf3; /* 接收缓冲区指针 */ g_tUart3.usTxBufSize = UART3_TX_BUF_SIZE; /* 发送缓冲区大小 */ g_tUart3.usRxBufSize = UART3_RX_BUF_SIZE; /* 接收缓冲区大小 */ g_tUart3.usTxWrite = 0; /* 发送FIFO写索引 */ g_tUart3.usTxRead = 0; /* 发送FIFO读索引 */ g_tUart3.usRxWrite = 0; /* 接收FIFO写索引 */ g_tUart3.usRxRead = 0; /* 接收FIFO读索引 */ g_tUart3.usRxCount = 0; /* 接收到的新数据个数 */ g_tUart3.usTxCount = 0; /* 待发送的数据个数 */ g_tUart3.SendBefor = RS485_SendBefor; /* 发送数据前的回调函数 */ g_tUart3.SendOver = RS485_SendOver; /* 发送完毕后的回调函数 */ g_tUart3.ReciveNew = RS485_ReciveNew; /* 接收到新数据后的回调函数 */ g_tUart3.Sending = 0; /* 正在发送中标志 */#endif}
上面代码中置红的部分是专用于485总线的,对应的代码如下:

/*********************************************************************************************************** 函 数 名: RS485_SendBefor* 功能说明: 发送数据前的准备工作。对于RS485通信,请设置RS485芯片为发送状态,* 并修改 UartVarInit()中的函数指针等于本函数名,比如 g_tUart2.SendBefor = RS485_SendBefor* 形 参: 无* 返 回 值: 无**********************************************************************************************************/void RS485_SendBefor(void){ RS485_TX_EN(); /* 切换RS485收发芯片为发送模式 */}/*********************************************************************************************************** 函 数 名: RS485_SendOver* 功能说明: 发送一串数据结束后的善后处理。对于RS485通信,请设置RS485芯片为接收状态,* 并修改 UartVarInit()中的函数指针等于本函数名,比如 g_tUart2.SendOver = RS485_SendOver* 形 参: 无* 返 回 值: 无**********************************************************************************************************/void RS485_SendOver(void){ RS485_RX_EN(); /* 切换RS485收发芯片为接收模式 */}/*********************************************************************************************************** 函 数 名: RS485_ReciveNew* 功能说明: 接收到新的数据* 形 参: _byte 接收到的新数据* 返 回 值: 无**********************************************************************************************************///extern void MODH_ReciveNew(uint8_t _byte);void RS485_ReciveNew(uint8_t _byte){// MODH_ReciveNew(_byte);}
发送数据前的准备工作。对于RS485通信,请设置RS485芯片为发送状态。
发送一串数据结束后的善后处理。对于RS485通信,请设置RS485芯片为接收状态。
接收到新的数据,用于Modbus通信协议。这里未用到Modbus,所以将对应的内容注释掉了。
31.4.3 RS485发送处理
串口数据的发送主要涉及到下面四个函数,调用关系是如下:
RS485_SendStr –> RS485_SendBuf –> comSendBuf -> UartSend
实际应用中,大家调用函数RS485_SendStr,RS485_SendBuf或者comSendBuf均可。另外特别注意代码中置红的部分,用于设置485发送使能。

/*********************************************************************************************************** 函 数 名: RS485_SendBuf* 功能说明: 通过RS485芯片发送一串数据。注意,本函数不等待发送完毕。* 形 参: _ucaBuf : 数据缓冲区* _usLen : 数据长度* 返 回 值: 无**********************************************************************************************************/void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen){ comSendBuf(COM3, _ucaBuf, _usLen);}/*********************************************************************************************************** 函 数 名: RS485_SendStr* 功能说明: 向485总线发送一个字符串,0结束。* 形 参: _pBuf 字符串,0结束* 返 回 值: 无**********************************************************************************************************/void RS485_SendStr(char *_pBuf){ RS485_SendBuf((uint8_t *)_pBuf, strlen(_pBuf));}/*********************************************************************************************************** 函 数 名: comSendBuf* 功能说明: 向串口发送一组数据。数据放到发送缓冲区后立即返回,由中断服务程序在后台完成发送* 形 参: _ucPort: 端口号(COM1 - COM8)* _ucaBuf: 待发送的数据缓冲区* _usLen : 数据长度* 返 回 值: 无**********************************************************************************************************/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);}/*********************************************************************************************************** 函 数 名: UartSend* 功能说明: 填写数据到UART发送缓冲区,并启动发送中断。中断处理函数发送完毕后,自动关闭发送中断* 形 参: 无* 返 回 值: 无**********************************************************************************************************/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
; DISABLE_INT(); if (++_pUart->usTxWrite >= _pUart->usTxBufSize) { _pUart->usTxWrite = 0; } _pUart->usTxCount++; ENABLE_INT(); } SET_BIT(_pUart->uart->CR1, USART_CR1_TXEIE); /* 使能发送中断(缓冲区空) */}
函数UartSend的作用就是把要发送的数据填到发送缓冲区里面,并使能发送空中断。
- 如果要发送的数据没有超过发送缓冲区大小,实现起来还比较容易,直接把数据填到FIFO里面,并使能发送空中断即可。
- 如果超过了FIFO大小,就需要等待有空间可用,针对这种情况有个重要的知识点,就是当缓冲刚刚填满的时候要判断发送空中断是否开启了,如果填满了还没有开启,就会卡死在while循环中,所以多了一个刚填满时的判断,填满了还没有开启发送空中断,要开启下。
注意:由于函数UartSend做了static作用域限制,仅可在bsp_uart_fifo.c文件中调用。函数RS485_SendStr,RS485_SendBuf或者comSendBuf是供用户调用的。
31.4.4 RS485数据接收
下面我们再来看看接收的函数:

/*********************************************************************************************************** 函 数 名: comGetChar* 功能说明: 从接收缓冲区读取1字节,非阻塞。无论有无数据均立即返回。* 形 参: _ucPort: 端口号(COM1 - COM8)* _pByte: 接收到的数据存放在这个地址* 返 回 值: 0 表示无数据, 1 表示读取到有效字节**********************************************************************************************************/uint8_t comGetChar(COM_PORT_E _ucPort, uint8_t *_pByte){ UART_T *pUart; pUart = ComToUart(_ucPort); if (pUart == 0) { return 0; } return UartGetChar(pUart, _pByte);}/*********************************************************************************************************** 函 数 名: UartGetChar* 功能说明: 从串口接收缓冲区读取1字节数据 (用于主程序调用)* 形 参: _pUart : 串口设备* _pByte : 存放读取数据的指针* 返 回 值: 0 表示无数据 1表示读取到数据**********************************************************************************************************/static uint8_t UartGetChar(UART_T *_pUart, uint8_t *_pByte){ uint16_t usCount; /* usRxWrite 变量在中断函数中被改写,主程序读取该变量时,必须进行临界区保护 */ DISABLE_INT(); usCount = _pUart->usRxCount; ENABLE_INT(); /* 如果读和写索引相同,则返回0 */ //if (_pUart->usRxRead == usRxWrite) if (usCount == 0) /* 已经没有数据 */ { return 0; } else { *_pByte = _pUart->pRxBuf[_pUart->usRxRead]; /* 从串口接收FIFO取1个数据 */ /* 改写FIFO读索引 */ DISABLE_INT(); if (++_pUart->usRxRead >= _pUart->usRxBufSize) { _pUart->usRxRead = 0; } _pUart->usRxCount--; ENABLE_INT(); return 1; }}
函数comGetChar是专门供用户调用的,用于从接收FIFO中读取1个数据。具体代码的实现也比较好理解,主要是接收FIFO的调整。
注意:由于函数UartGetChar做了static作用域限制,仅可在bsp_uart_fifo.c文件中调用。
31.4.5 RS485驱动中断服务程序的处理
串口中断服务程序是实现RS485驱动的关键部分,主要实现如下三个功能:
- 收到新的数据后,会将数据压入RX_FIFO。
- 检测到发送缓冲区空后,会从TX_FIFO中取下一个数据并发送。
- 对于RS485半双工串口,发送前会设置一个GPIO=1控制RS485收发器进入发送状态,当最后一个字节的最后一个bit传送完毕后,设置这个GPIO=0让RS485收发器进入接收状态。
下面我们分析一下串口中断处理的完整过程。
当产生串口中断后,CPU会查找中断向量表,获得中断服务程序的入口地址。入口函数为USART1_IRQHandler,这个函数在启动文件startup_stm32h743xx.s汇编代码中已经有实现。我们在c代码中需要重写一个同样名字的函数就可以重载它。如果不重载,启动文件中缺省的中断服务程序就是一个死循环,等于 while(1);
我们将串口中断服务程序放在bsp_uart_fifo.c文件,没有放到 stm32h7xx_it.c。当应用不需要串口功能时,直接从工程中删除bsp_uart_fifo.c接口,不必再去整理stm32h7xx_it.c这个文件。下面展示的代码是8个串口的中断服务程序,RS485用的USART3。

#if UART1_FIFO_EN == 1void USART1_IRQHandler(void){ UartIRQ(&g_tUart1);}#endif#if UART2_FIFO_EN == 1void USART2_IRQHandler(void){ UartIRQ(&g_tUart2);}#endif#if UART3_FIFO_EN == 1void USART3_IRQHandler(void){ UartIRQ(&g_tUart3);}#endif#if UART4_FIFO_EN == 1void UART4_IRQHandler(void){ UartIRQ(&g_tUart4);}#endif#if UART5_FIFO_EN == 1void UART5_IRQHandler(void){ UartIRQ(&g_tUart5);}#endif#if UART6_FIFO_EN == 1void USART6_IRQHandler(void){ UartIRQ(&g_tUart6);}#endif#if UART7_FIFO_EN == 1void UART7_IRQHandler(void){ UartIRQ(&g_tUart7);}#endif#if UART8_FIFO_EN == 1void UART8_IRQHandler(void){ UartIRQ(&g_tUart8);}#endif
大家可以看到,这8个中断服务程序都调用了同一个处理函数UartIRQ。我们只需要调通一个串口FIFO驱动,那么其他的串口驱动也就都通了。
下面,我们来看看UartIRQ函数的实现代码。

/*********************************************************************************************************** 函 数 名: UartIRQ* 功能说明: 供中断服务程序调用,通用串口中断处理函数* 形 参: _pUart : 串口设备* 返 回 值: 无**********************************************************************************************************/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; /* 填入串口接收FIFO */ if (++_pUart->usRxWrite >= _pUart->usRxBufSize) /* 接收FIFO的写指针+1 */ { _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解码程序处理字节流 */ } } } /* 处理发送缓冲区空中断 */ 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--; } } /* 数据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--; } } /* 清除中断标志 */ SET_BIT(_pUart->uart->ICR, UART_CLEAR_PEF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_FEF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_NEF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_OREF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_IDLEF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_TCF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_LBDF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_CTSF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_CMF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_WUF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_TXFECF); }
中断服务程序的处理主要分为两部分,接收数据的处理和发送数据的处理,详情看程序注释即可,已经比较详细,下面重点把思路说一下:
接收数据的处理是判断ISR寄存器的USART_ISR_RXNE标志是否置位,如果置位表示RDR接收寄存器已经存入数据。然后将数据读入到接收FIFO空间。
特别注意里面的ReciveNew处理,这个在Modbus协议里面要用到。
发送数据主要是发送空中断TEX和发送完成中断TC的处理,当TXE=1时,只是表示发送数据寄存器为空了,此时可以填充下一个准备发送的数据了。当为TDR发送寄存器赋值后,硬件启动发送,等所有的bit传送完毕后,TC标志设置为1。如果是RS232全双工通信,可以只用TXE标志控制发送过程。如果是RS485半双工通通信,就需要利用TC标志了,因为在最后一个bit传送完毕后,需要设置RS485收发器进入到接收状态。
31.5 RS485板级支持包(bsp_uart_fifo.c)
串口驱动文件bsp_uart_fifo.c主要实现了如下几个API供用户调用:
- bsp_InitUart
- comSendBuf
- comSendChar
- comGetChar
31.5.1 函数bsp_InitUart
函数原型:
void bsp_InitUart(void)
函数描述:
此函数主要用于串口的初始化,使用所有其它API之前,务必优先调用此函数。
使用举例:
串口的初始化函数在bsp.c文件的bsp_Init函数里面调用。
31.5.2 函数comSendBuf
函数原型:
void comSendBuf(COM_PORT_E _ucPort, uint8_t *_ucaBuf, uint16_t _usLen);
函数描述:
此函数用于向串口发送一组数据,非阻塞方式,数据放到发送缓冲区后立即返回,由中断服务程序在后台完成发送。
函数参数:
- 第1个参数_ucPort是端口号,范围COM1 - COM8。
- 第2个参数_ucaBuf是待发送的数据缓冲区地址。
- 第3个参数_usLen是要发送数据的字节数。
注意事项:
- 此函数的解读在第30章30.3.5小节。
- 发送的数据最好不要超过bsp_uart_fifo.h文件中定义的发送缓冲区大小,从而实现最优的工作方式。因为超过后需要在发送函数等待有发送空间可用。
使用举例:
调用此函数前,务必优先调用函数bsp_InitUart进行初始化。
const char buf1[] = "接收到串口命令1rn";
comSendBuf(COM1, (uint8_t *)buf1, strlen(buf1));
31.5.3 函数RS485_SendBuf
函数原型:
void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen)
函数描述:
此函数用于向RS485总线发送一组数据,非阻塞方式,数据放到发送缓冲区后立即返回,由中断服务程序在后台完成发送。此函数是通过调用函数comSendBuf实现的。
函数参数:
- 第1个参数_ucaBuf是数据缓冲区。
- 第2个参数_usLen是数据长度。
注意事项:
调用此函数前,务必优先调用函数bsp_InitUart进行初始化。
31.5.4 函数RS485_SendStr
函数原型:
void RS485_SendStr(char *_pBuf)
函数描述:
此函数用于向RS485总线发送一个字符串,非阻塞方式,数据放到发送缓冲区后立即返回,由中断服务程序在后台完成发送。此函数是通过调用函数RS485_SendBuf实现的。
函数参数:
注意事项:
调用此函数前,务必优先调用函数bsp_InitUart进行初始化。
31.5.5 函数comGetChar
函数原型:
uint8_t comGetChar(COM_PORT_E _ucPort, uint8_t *_pByte)
函数描述:
此函数用于从接收缓冲区读取1字节,非阻塞。无论有无数据均立即返回。
函数参数:
- 第1个参数_ucPort是端口号,范围COM1 - COM8。
- 第2个参数_pByte用于存放接收到的数据。
- 返回值,返回0表示无数据, 1 表示读取到有效字节。
注意事项:
使用举例:
调用此函数前,务必优先调用函数bsp_InitUart进行初始化。
比如从串口1读取一个字符就是:comGetChar(COM1, &read)
31.6 RS485驱动移植和使用
RS485移植步骤如下:
- 第1步:复制bsp_uart_fifo.h和bsp_uart_fifo.c到自己的工程目录,并添加到工程里面。
- 第2步:根据485要使用的串口和收发缓冲大小,修改下面的宏定义即可。

#define UART1_FIFO_EN 1#define UART2_FIFO_EN 0#define UART3_FIFO_EN 0#define UART4_FIFO_EN 0#define UART5_FIFO_EN 0#define UART6_FIFO_EN 0#define UART7_FIFO_EN 0#define UART8_FIFO_EN 0/* 定义串口波特率和FIFO缓冲区大小,分为发送缓冲区和接收缓冲区, 支持全双工 */#if UART1_FIFO_EN == 1 #define UART1_BAUD 115200 #define UART1_TX_BUF_SIZE 1*1024 #define UART1_RX_BUF_SIZE 1*1024#endif#if UART2_FIFO_EN == 1 #define UART2_BAUD 9600 #define UART2_TX_BUF_SIZE 10 #define UART2_RX_BUF_SIZE 2*1024#endif#if UART3_FIFO_EN == 1 #define UART3_BAUD 9600 #define UART3_TX_BUF_SIZE 1*1024 #define UART3_RX_BUF_SIZE 1*1024#endif#if UART4_FIFO_EN == 1 #define UART4_BAUD 115200 #define UART4_TX_BUF_SIZE 1*1024 #define UART4_RX_BUF_SIZE 1*1024#endif#if UART5_FIFO_EN == 1 #define UART5_BAUD 115200 #define UART5_TX_BUF_SIZE 1*1024 #define UART5_RX_BUF_SIZE 1*1024#endif#if UART6_FIFO_EN == 1 #define UART6_BAUD 115200 #define UART6_TX_BUF_SIZE 1*1024 #define UART6_RX_BUF_SIZE 1*1024#endif#if UART7_FIFO_EN == 1 #define UART7_BAUD 115200 #define UART7_TX_BUF_SIZE 1*1024 #define UART7_RX_BUF_SIZE 1*1024#endif#if UART8_FIFO_EN == 1 #define UART8_BAUD 115200 #define UART8_TX_BUF_SIZE 1*1024 #define UART8_RX_BUF_SIZE 1*1024#endif

/* 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#define RS485_TX_EN() RS485_TXEN_GPIO_PORT->BSRRL = RS485_TXEN_PIN
- 第4步:根据使用的串口,配置下面函数的几个参数成员,下面是以串口3为例说明(置红部分):

static void UartVarInit(void){#if UART3_FIFO_EN == 1 g_tUart3.uart = USART3; /* STM32 串口设备 */ g_tUart3.pTxBuf = g_TxBuf3; /* 发送缓冲区指针 */ g_tUart3.pRxBuf = g_RxBuf3; /* 接收缓冲区指针 */ g_tUart3.usTxBufSize = UART3_TX_BUF_SIZE; /* 发送缓冲区大小 */ g_tUart3.usRxBufSize = UART3_RX_BUF_SIZE; /* 接收缓冲区大小 */ g_tUart3.usTxWrite = 0; /* 发送FIFO写索引 */ g_tUart3.usTxRead = 0; /* 发送FIFO读索引 */ g_tUart3.usRxWrite = 0; /* 接收FIFO写索引 */ g_tUart3.usRxRead = 0; /* 接收FIFO读索引 */ g_tUart3.usRxCount = 0; /* 接收到的新数据个数 */ g_tUart3.usTxCount = 0; /* 待发送的数据个数 */ g_tUart3.SendBefor = RS485_SendBefor; /* 发送数据前的回调函数 */ g_tUart3.SendOver = RS485_SendOver; /* 发送完毕后的回调函数 */ g_tUart3.ReciveNew = RS485_ReciveNew; /* 接收到新数据后的回调函数 */ g_tUart3.Sending = 0; /* 正在发送中标志 */#endif}
- 第5步:这几个驱动文件主要用到HAL库的GPIO和串口驱动文件,简单省事些可以添加所有HAL库.C源文件进来。
- 第6步,应用方法看本章节配套例子即可。
31.7 实验例程设计框架
通过程序设计框架,让大家先对配套例程有一个全面的认识,然后再理解细节,本次实验例程的设计框架如下:

第1阶段,上电启动阶段:
第2阶段,进入main函数:
- 第1步,硬件初始化,主要是MPU,Cache,HAL库,系统时钟,滴答定时器,LED和串口。
- 第2步,485应用程序设计部分,具体分为两个部分,接收命令和发送命令。
31.8 实验例程说明(MDK)
配套例子:
V7-016_RS485多机通讯
实验目的:
实验内容:
- 由于通信距离较短,SP3485E芯片上缺省未贴的电阻不需要贴上,大家可以根据需要贴上电阻做测试。
- 本例子支持多个485节点,不需要设置主设备和从设备,所有节点下载此程序即可。
- 开发板的485-A端子连接到一起,485-B端子连接到一起,具体连接看工程Doc文件夹中的截图。
实验操作:
- 按下开发板上的K1键点亮LED1,松开熄灭LED1,同时打印按键事件到串口1。485总线上的其它开发板做相同的动作。
- 按下开发板上的K2键,启动50ms的自动重装定时器,每隔50ms翻转LED2,并向485总线上的其它开发板发送按键K2按下消息,从而也实现每隔50ms翻转LED2。
- 按下开发板上的K3按键,停止K2按键启动的50ms自动重载定时器,485总线上的其它开发板做相同的动作。
- 按下开发板上的摇杆(上下左右,OK共5种),会通过串口1打印摇杆事件。485总线上的其它开发板做相同的动作。
注意事项:
- RS485 PHY的发送使能用的引脚PB11,测试前需要将板子J5处的跳线帽短接到PB11端。
多机接线效果:


上电后串口打印的信息:
波特率 115200,数据位 8,奇偶校验位无,停止位 1

程序设计:
系统栈大小分配:

RAM空间用的DTCM:

硬件外设初始化
硬件外设的初始化是在 bsp.c 文件实现:

/*********************************************************************************************************** 函 数 名: bsp_Init* 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次* 形 参:无* 返 回 值: 无**********************************************************************************************************/void bsp_Init(void){ /* 配置MPU */ MPU_Config(); /* 使能L1 Cache */ CPU_CACHE_Enable(); /* STM32H7xx HAL 库初始化,此时系统用的还是H7自带的64MHz,HSI时钟: - 调用函数HAL_InitTick,初始化滴答时钟中断1ms。 - 设置NVIV优先级分组为4。 */ HAL_Init(); /* 配置系统时钟到400MHz - 切换使用HSE。 - 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用于代码执行时间测量,MDK5.25及其以上版本才支持,IAR不支持。 - 默认不开启,如果要使能此选项,务必看V7开发板用户手册第xx章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder并开启 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart();#endif bsp_InitKey(); /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */ bsp_InitTimer(); /* 初始化滴答定时器 */ bsp_InitUart(); /* 初始化串口 */ bsp_InitExtIO(); /* 初始化FMC总线74HC574扩展IO. 必须在 bsp_InitLed()前执行 */ bsp_InitLed(); /* 初始化LED */ }
MPU配置和Cache配置:
数据Cache和指令Cache都开启。配置了AXI SRAM区(本例子未用到AXI SRAM)和FMC的扩展IO区。

/*********************************************************************************************************** 函 数 名: MPU_Config* 功能说明: 配置MPU* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void MPU_Config( void ){ MPU_Region_InitTypeDef MPU_InitStruct; /* 禁止 MPU */ HAL_MPU_Disable(); /* 配置AXI SRAM的MPU属性为Write back, Read allocate,Write allocate */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置FMC扩展IO的MPU属性为Device或者Strongly Ordered */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x60000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /*使能 MPU */ HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);}/*********************************************************************************************************** 函 数 名: CPU_CACHE_Enable* 功能说明: 使能L1 Cache* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void CPU_CACHE_Enable(void){ /* 使能 I-Cache */ SCB_EnableICache(); /* 使能 D-Cache */ SCB_EnableDCache();}
主功能:
主功能的实现主要分为两部分:
- 获取其它RS485设备发来的命令,并执行相应功能。
通过函数comGetChar(COM3, &ucDataTravel)以查询、非阻塞方式获取其它开发板发来的数据。
通过函数comSendChar(COM3, ucDataTravel)以非阻塞方式向其它485设备发送命令,以此来执行同样的功能。

/*********************************************************************************************************** 函 数 名: main* 功能说明: c程序入口* 形 参: 无* 返 回 值: 错误代码(无需处理)**********************************************************************************************************/int main(void){ uint8_t ucKeyCode; /* 按键代码 */ uint8_t ucDataTravel; /* 发送变量 */ uint8_t ucDataRec; /* 接收变量 */ bsp_Init(); /* 硬件初始化 */ PrintfLogo(); /* 打印例程名称和版本等信息 */ PrintfHelp(); /* 打印操作提示 */ /* 进入主程序循环体 */ while (1) { bsp_Idle(); /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 */ /* 获取其它开发板通过485总线发来的数据 */ if(comGetChar(COM3, &ucDataRec)) { switch (ucDataRec) { case KEY_DOWN_K1: /* 获得K1键按下消息 */ bsp_LedOn(1); printf("K1键按下, LED1点亮rn"); break; case KEY_UP_K1: /* 获得K1键释放消息 */ bsp_LedOff(1); printf("K1键弹起, LED1熄灭rn"); break; case KEY_DOWN_K2: /* 获得K1键按下消息 */ bsp_LedToggle(2); break; case JOY_DOWN_U: /* 获得摇杆UP键按下 */ printf("摇杆上键按下rn"); break; case JOY_DOWN_D: /* 获得摇杆DOWN键按下 */ printf("摇杆下键按下rn"); break; case JOY_DOWN_L: /* 获得摇杆LEFT键按下 */ printf("摇杆左键按下rn"); break; case JOY_DOWN_R: /* 获得摇杆RIGHT键按下 */ printf("摇杆右键按下rn"); break; case JOY_DOWN_OK: /* 获得摇杆OK键按下 */ printf("摇杆OK键按下rn"); break; case JOY_UP_OK: /* 获得摇杆OK键弹起 */ printf("摇杆OK键弹起rn"); break; default: /* 其它的键值不处理 */ break; } } /* 判断定时器超时时间 */ if (bsp_CheckTimer(0)) { /* 每隔50ms 进来一次 */ bsp_LedToggle(2); /* 向其它开发板发送按键K2按下的消息 */ ucDataTravel = KEY_DOWN_K2; comSendChar(COM3, ucDataTravel); } /* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */ ucKeyCode = bsp_GetKey(); /* 读取键值, 无键按下时返回 KEY_NONE = 0 */ if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { case KEY_DOWN_K1: /* K1键按下 */ ucDataTravel = KEY_DOWN_K1; comSendChar(COM3, ucDataTravel); bsp_LedOn(1); printf("K1键按下, LED1点亮rn"); break; case KEY_UP_K1: /* K1键弹起 */ ucDataTravel = KEY_UP_K1; comSendChar(COM3, ucDataTravel); bsp_LedOff(1); printf("K1键弹起, LED1熄灭rn"); break; case KEY_DOWN_K2: /* K2键按下 */ bsp_StartAutoTimer(0, 50); /* 启动1个50ms的自动重装的定时器 */ break; case KEY_DOWN_K3: /* K3键按下 */ bsp_StopTimer(0); /* 停止自动重装的定时器 */ break; case JOY_DOWN_U: /* 摇杆UP键按下 */ ucDataTravel = JOY_DOWN_U; comSendChar(COM3, ucDataTravel); printf("摇杆上键按下rn"); break; case JOY_DOWN_D: /* 摇杆DOWN键按下 */ ucDataTravel = JOY_DOWN_D; comSendChar(COM3, ucDataTravel); printf("摇杆下键按下rn"); break; case JOY_DOWN_L: /* 摇杆LEFT键按下 */ ucDataTravel = JOY_DOWN_L; comSendChar(COM3, ucDataTravel); printf("摇杆左键按下rn"); break; case JOY_DOWN_R: /* 摇杆RIGHT键按下 */ ucDataTravel = JOY_DOWN_R; comSendChar(COM3, ucDataTravel); printf("摇杆右键按下rn"); break; case JOY_DOWN_OK: /* 摇杆OK键按下 */ ucDataTravel = JOY_DOWN_OK; comSendChar(COM3, ucDataTravel); printf("摇杆OK键按下rn"); break; case JOY_UP_OK: /* 摇杆OK键弹起 */ ucDataTravel = JOY_UP_OK; comSendChar(COM3, ucDataTravel); printf("摇杆OK键弹起rn"); break; default: /* 其它的键值不处理 */ break; } } }}
31.9 实验例程说明(IAR)
配套例子:
V7-016_RS485多机通讯
实验目的:
实验内容:
- 由于通信距离较短,SP3485E芯片上缺省未贴的电阻不需要贴上,大家可以根据需要贴上电阻做测试。
- 本例子支持多个485节点,不需要设置主设备和从设备,所有节点下载此程序即可。
- 开发板的485-A端子连接到一起,485-B端子连接到一起,具体连接看工程Doc文件夹中的截图。
实验操作:
- 按下开发板上的K1键点亮LED1,松开熄灭LED1,同时打印按键事件到串口1。485总线上的其它开发板做相同的动作。
- 按下开发板上的K2键,启动50ms的自动重装定时器,每隔50ms翻转LED2,并向485总线上的其它开发板发送按键K2按下消息,从而也实现每隔50ms翻转LED2。
- 按下开发板上的K3按键,停止K2按键启动的50ms自动重载定时器,485总线上的其它开发板做相同的动作。
- 按下开发板上的摇杆(上下左右,OK共5种),会通过串口1打印摇杆事件。485总线上的其它开发板做相同的动作。
注意事项:
- RS485 PHY的发送使能用的引脚PB11,测试前需要将板子J5处的跳线帽短接到PB11端。
多机接线效果:


上电后串口打印的信息:
波特率 115200,数据位 8,奇偶校验位无,停止位 1

程序设计:
系统栈大小分配:

RAM空间用的DTCM:

硬件外设初始化
硬件外设的初始化是在 bsp.c 文件实现:

/*********************************************************************************************************** 函 数 名: bsp_Init* 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次* 形 参:无* 返 回 值: 无**********************************************************************************************************/void bsp_Init(void){ /* 配置MPU */ MPU_Config(); /* 使能L1 Cache */ CPU_CACHE_Enable(); /* STM32H7xx HAL 库初始化,此时系统用的还是H7自带的64MHz,HSI时钟: - 调用函数HAL_InitTick,初始化滴答时钟中断1ms。 - 设置NVIV优先级分组为4。 */ HAL_Init(); /* 配置系统时钟到400MHz - 切换使用HSE。 - 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用于代码执行时间测量,MDK5.25及其以上版本才支持,IAR不支持。 - 默认不开启,如果要使能此选项,务必看V7开发板用户手册第xx章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder并开启 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart();#endif bsp_InitKey(); /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */ bsp_InitTimer(); /* 初始化滴答定时器 */ bsp_InitUart(); /* 初始化串口 */ bsp_InitExtIO(); /* 初始化FMC总线74HC574扩展IO. 必须在 bsp_InitLed()前执行 */ bsp_InitLed(); /* 初始化LED */ }
MPU配置和Cache配置:
数据Cache和指令Cache都开启。配置了AXI SRAM区(本例子未用到AXI SRAM)和FMC的扩展IO区。

/*********************************************************************************************************** 函 数 名: MPU_Config* 功能说明: 配置MPU* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void MPU_Config( void ){ MPU_Region_InitTypeDef MPU_InitStruct; /* 禁止 MPU */ HAL_MPU_Disable(); /* 配置AXI SRAM的MPU属性为Write back, Read allocate,Write allocate */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置FMC扩展IO的MPU属性为Device或者Strongly Ordered */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x60000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /*使能 MPU */ HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);}/*********************************************************************************************************** 函 数 名: CPU_CACHE_Enable* 功能说明: 使能L1 Cache* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void CPU_CACHE_Enable(void){ /* 使能 I-Cache */ SCB_EnableICache(); /* 使能 D-Cache */ SCB_EnableDCache();}
主功能:
主功能的实现主要分为两部分:
- 获取其它RS485设备发来的命令,并执行相应功能。
通过函数comGetChar(COM3, &ucDataTravel)以查询、非阻塞方式获取其它开发板发来的数据。
通过函数comSendChar(COM3, ucDataTravel)以非阻塞方式向其它485设备发送命令,以此来执行同样的功能。

/*********************************************************************************************************** 函 数 名: main* 功能说明: c程序入口* 形 参: 无* 返 回 值: 错误代码(无需处理)**********************************************************************************************************/int main(void){ uint8_t ucKeyCode; /* 按键代码 */ uint8_t ucDataTravel; /* 发送变量 */ uint8_t ucDataRec; /* 接收变量 */ bsp_Init(); /* 硬件初始化 */ PrintfLogo(); /* 打印例程名称和版本等信息 */ PrintfHelp(); /* 打印操作提示 */ /* 进入主程序循环体 */ while (1) { bsp_Idle(); /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 */ /* 获取其它开发板通过485总线发来的数据 */ if(comGetChar(COM3, &ucDataRec)) { switch (ucDataRec) { case KEY_DOWN_K1: /* 获得K1键按下消息 */ bsp_LedOn(1); printf("K1键按下, LED1点亮rn"); break; case KEY_UP_K1: /* 获得K1键释放消息 */ bsp_LedOff(1); printf("K1键弹起, LED1熄灭rn"); break; case KEY_DOWN_K2: /* 获得K1键按下消息 */ bsp_LedToggle(2); break; case JOY_DOWN_U: /* 获得摇杆UP键按下 */ printf("摇杆上键按下rn"); break; case JOY_DOWN_D: /* 获得摇杆DOWN键按下 */ printf("摇杆下键按下rn"); break; case JOY_DOWN_L: /* 获得摇杆LEFT键按下 */ printf("摇杆左键按下rn"); break; case JOY_DOWN_R: /* 获得摇杆RIGHT键按下 */ printf("摇杆右键按下rn"); break; case JOY_DOWN_OK: /* 获得摇杆OK键按下 */ printf("摇杆OK键按下rn"); break; case JOY_UP_OK: /* 获得摇杆OK键弹起 */ printf("摇杆OK键弹起rn"); break; default: /* 其它的键值不处理 */ break; } } /* 判断定时器超时时间 */ if (bsp_CheckTimer(0)) { /* 每隔50ms 进来一次 */ bsp_LedToggle(2); /* 向其它开发板发送按键K2按下的消息 */ ucDataTravel = KEY_DOWN_K2; comSendChar(COM3, ucDataTravel); } /* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */ ucKeyCode = bsp_GetKey(); /* 读取键值, 无键按下时返回 KEY_NONE = 0 */ if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { case KEY_DOWN_K1: /* K1键按下 */ ucDataTravel = KEY_DOWN_K1; comSendChar(COM3, ucDataTravel); bsp_LedOn(1); printf("K1键按下, LED1点亮rn"); break; case KEY_UP_K1: /* K1键弹起 */ ucDataTravel = KEY_UP_K1; comSendChar(COM3, ucDataTravel); bsp_LedOff(1); printf("K1键弹起, LED1熄灭rn"); break; case KEY_DOWN_K2: /* K2键按下 */ bsp_StartAutoTimer(0, 50); /* 启动1个50ms的自动重装的定时器 */ break; case KEY_DOWN_K3: /* K3键按下 */ bsp_StopTimer(0); /* 停止自动重装的定时器 */ break; case JOY_DOWN_U: /* 摇杆UP键按下 */ ucDataTravel = JOY_DOWN_U; comSendChar(COM3, ucDataTravel); printf("摇杆上键按下rn"); break; case JOY_DOWN_D: /* 摇杆DOWN键按下 */ ucDataTravel = JOY_DOWN_D; comSendChar(COM3, ucDataTravel); printf("摇杆下键按下rn"); break; case JOY_DOWN_L: /* 摇杆LEFT键按下 */ ucDataTravel = JOY_DOWN_L; comSendChar(COM3, ucDataTravel); printf("摇杆左键按下rn"); break; case JOY_DOWN_R: /* 摇杆RIGHT键按下 */ ucDataTravel = JOY_DOWN_R; comSendChar(COM3, ucDataTravel); printf("摇杆右键按下rn"); break; case JOY_DOWN_OK: /* 摇杆OK键按下 */ ucDataTravel = JOY_DOWN_OK; comSendChar(COM3, ucDataTravel); printf("摇杆OK键按下rn"); break; case JOY_UP_OK: /* 摇杆OK键弹起 */ ucDataTravel = JOY_UP_OK; comSendChar(COM3, ucDataTravel); printf("摇杆OK键弹起rn"); break; default: /* 其它的键值不处理 */ break; } } }}
31.10 总结
本章节就为大家讲解这么多,485通信依然在实际项目中非常实用。有线的串行网络仍然提供最有力、最可靠的通信,特别是在恶劣的环境中。
本章教程为大家讲解USART应用之485总线。虽然这几年无线网络的使用率有所上升,有线的串行网络仍然提供最有力、最可靠的通信,特别是在恶劣的环境中。在需要抗噪、抗静电、抗电压故障的工业,建筑自动化领域仍然是有线通信的天下。
31.1 初学者重要提示
- 学习本章节前,务必优先学习第30章,RS485用到的串口FIFO也是建立在30章的基础上。
- 了解了本章31.2和31.3小节的基础知识后,强烈推荐看此贴的两个文档,对RS485讲解的比较透彻,中文版:http://www.armbbs.cn/forum.php?mod=viewthread&tid=90753 。
- STM32H7支持RS485的硬件流控制,即有一个专门的引脚来控制485 PHY的收发状态切换。V7开发板用的USART3,需要用PD12来控制,而这个引脚要用于FMC,所以用的是一个通用IO。
- 经常会有网友咨询为什么程序里面收发切换没有做延迟处理,这里就涉及到一个关键的知识点TXE发送空中断和TC发送完成中断的区别,详细看教程中说明即可。
31.2 RS485的基础知识
智能仪表是随着80年代初单片机技术的成熟而发展起来的,现在世界仪表市场基本被智能仪表所垄断。究其原因就是企业信息化的需要,企业在仪表选型时其中的一个必要条件就是要具有联网通讯接口。最初是数据模拟信号输出简单过程量,后来仪表接口是RS232接口,这种接口可以实现点对点的通信方式,但这种方式不能实现联网功能。随后出现的RS485解决了这个问题。
EIA-485(过去叫做RS-485或者RS485)是隶属于OSI模型物理层的电气特性规定为2线、半双工、平衡传输线多点通信的标准,是由电信行业协会(TIA)及电子工业联盟(EIA)联合发布的标准。实现此标准的数字通信网可以在有电子噪声的环境下进行长距离有效率的通信。在线性多点总线的配置下,可以在一个网络上有多个接收器。因此适用在工业环境中。
EIA一开始将RS(Recommended Standard)做为标准的前缀,不过后来为了便于识别标准的来源,已将RS改为EIA/TIA。电子工业联盟(EIA)已结束运作,此标准目前是电信行业协会(TIA)维护,名称为TIA-485,但工程师仍继续用RS-485来称呼此协议。
RS-485的数据最高传输速率为10Mbsp。
RS-485接口是采用平衡驱动器和差分接收器的组合,抗共模干扰能力增强,即抗干扰噪声性好。
RS-485最大的通信距离约为1219m,最高传输速率为10Mbsp,传输速率与传输距离成反比,在100Kb/S的传输速率下,才可以达到最大的通信距离,如果需传输更长的距离,需要加485中继器。RS-485总线一般最大支持32个节点,如果使用特制的485芯片,可以达到128个或者256个节点,最大的可以支持到400个节点。
关于RS485的逻辑状态,不同厂家的芯片的定义可能不同,但不影响正常的数据收发,这里以TI的为例做个说明,TI的定义方式如下:
A表示非反向输出non-inverting output,B表示反向输出inverting output。
当VA > VB 的时候表示逻辑状态0,被称为ON。
当VA < VB 的时候表示逻辑状态1,被称为OFF。
对应到实际芯片框图上就是下面这样(DE发送使能,D是发送数据端,RE是接收使能,R是接收数据端):
当用户在D(Driver)引脚输入逻辑高电平时,将在485总线上实现逻辑状态0,即ON状态。接收端R(Receiver)将收到逻辑高电平。
当用户在D(Driver)引脚输入逻辑低电平时,将在485总线上实现逻辑状态1,即OFF状态。接收端R(Receiver)将收到逻辑低电平。
发送状态下,大于|±1.5V |可以有效表示逻辑状态1和逻辑状态0:
接收状态下,大于|±200mv|可以有效表示逻辑状态1和逻辑状态0:
31.3 RS485硬件设计
STM32H743XIH6最多可以支持8个独立的串口。其中串口4和串口5和SDIO的GPIO是共用的,也就是说,如果要用到SD卡,那么串口4和串口5将不能使用。串口7和SPI3共用,串口8和RGB硬件接口共用。串口功能可以分配到不同的GPIO。我们常用的管脚分配如下:
串口USART1 TX = PA9, RX = PA10
串口USART2 TX = PA2, RX = PA3
串口USART3 TX = PB10, RX = PB11
串口UART4 TX = PC10, RX = PC11 (和SDIO共用)
串口UART5 TX = PC12, RX = PD2 (和SDIO共用)
串口USART6 TX = PG14, RX = PC7
串口UART7 TX = PB4, RX = PB3 (和SPI1/3共用)
串口UART8 TX = PJ8, RX =PJ9 (和RGB硬件接口共用)
STM32-V7开发板使用了4个串口设备。
- 串口1用于RS232接口,很多例子的pritnf结果就是输出到串口1
- 串口2用于GPS
- 串口3用于RS485接口
- 串口6 用于TTL串口插座,板子上有GPRS插座和串口WIFI插座。
下面是相关的原理图:
串口3,RS485
关于485的PHY芯片SP3485E要注意以下几个问题:
- SP3485E允许在同一总线上连接32个收发器。
- PB2(RS485_TX_EN)引脚控制收发状态,高电平表示使能发送,低电平表示用于接收。
- 由于 485的RO是输出引脚,如果软件上将PB2配置为输出就会冲突(PB2输出高,RO输出低就会短路)。加电阻的话,PB2就可以做其他用途。主要是开发板才这样弄,正式产品不加。
- 电阻R15和R165的作用是避免CPU复位期间,TX为高阻时影响总线数据。
- 电阻R4和R2是保证空闲时处于确定的逻辑状态,提供可靠性。
- 电阻R3是终端电阻。使用终端电阻是为了阻抗匹配,防止不匹配引起的噪声干扰。一般在300米以下,19200bps不需要接终端电阻。终端电阻要接在传输总线的两端。
31.4 RS485驱动设计
RS485的驱动实现是建立在第31章讲解的串口FIFO基础上,关键的知识点已经在第31章节做了详细讲解,这里把485驱动涉及到的两个关键地方做个说明。
31.4.1 RS485驱动初始化
RS485驱动的初始化要对收发控制引脚进行配置,这点要注意,对应的代码如下:

/*********************************************************************************************************** 函 数 名: bsp_InitUart* 功能说明: 初始化串口硬件,并对全局变量赋初值.* 形 参: 无* 返 回 值: 无**********************************************************************************************************/void bsp_InitUart(void){ UartVarInit(); /* 必须先初始化全局变量,再配置硬件 */ InitHardUart(); /* 配置串口的硬件参数(波特率等) */ RS485_InitTXE(); /* 配置RS485芯片的发送使能硬件,配置为推挽输出 */}/*********************************************************************************************************** 函 数 名: RS485_InitTXE* 功能说明: 配置RS485发送使能口线 TXE* 形 参: 无* 返 回 值: 无**********************************************************************************************************/void RS485_InitTXE(void){ GPIO_InitTypeDef gpio_init; /* 打开GPIO时钟 */ RS485_TXEN_GPIO_CLK_ENABLE(); /* 配置引脚为推挽输出 */ gpio_init.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */ gpio_init.Pull = GPIO_NOPULL; /* 上下拉电阻不使能 */ gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* GPIO速度等级 */ gpio_init.Pin = RS485_TXEN_PIN; HAL_GPIO_Init(RS485_TXEN_GPIO_PORT, &gpio_init); }
31.4.2 RS485驱动回调函数初始化
由于RS485是半双工,收发要做切换,初始化的时候专门配套了回调函数:

/*********************************************************************************************************** 函 数 名: UartVarInit* 功能说明: 初始化串口相关的变量* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void UartVarInit(void){#if UART3_FIFO_EN == 1 g_tUart3.uart = USART3; /* STM32 串口设备 */ g_tUart3.pTxBuf = g_TxBuf3; /* 发送缓冲区指针 */ g_tUart3.pRxBuf = g_RxBuf3; /* 接收缓冲区指针 */ g_tUart3.usTxBufSize = UART3_TX_BUF_SIZE; /* 发送缓冲区大小 */ g_tUart3.usRxBufSize = UART3_RX_BUF_SIZE; /* 接收缓冲区大小 */ g_tUart3.usTxWrite = 0; /* 发送FIFO写索引 */ g_tUart3.usTxRead = 0; /* 发送FIFO读索引 */ g_tUart3.usRxWrite = 0; /* 接收FIFO写索引 */ g_tUart3.usRxRead = 0; /* 接收FIFO读索引 */ g_tUart3.usRxCount = 0; /* 接收到的新数据个数 */ g_tUart3.usTxCount = 0; /* 待发送的数据个数 */ g_tUart3.SendBefor = RS485_SendBefor; /* 发送数据前的回调函数 */ g_tUart3.SendOver = RS485_SendOver; /* 发送完毕后的回调函数 */ g_tUart3.ReciveNew = RS485_ReciveNew; /* 接收到新数据后的回调函数 */ g_tUart3.Sending = 0; /* 正在发送中标志 */#endif}
上面代码中置红的部分是专用于485总线的,对应的代码如下:

/*********************************************************************************************************** 函 数 名: RS485_SendBefor* 功能说明: 发送数据前的准备工作。对于RS485通信,请设置RS485芯片为发送状态,* 并修改 UartVarInit()中的函数指针等于本函数名,比如 g_tUart2.SendBefor = RS485_SendBefor* 形 参: 无* 返 回 值: 无**********************************************************************************************************/void RS485_SendBefor(void){ RS485_TX_EN(); /* 切换RS485收发芯片为发送模式 */}/*********************************************************************************************************** 函 数 名: RS485_SendOver* 功能说明: 发送一串数据结束后的善后处理。对于RS485通信,请设置RS485芯片为接收状态,* 并修改 UartVarInit()中的函数指针等于本函数名,比如 g_tUart2.SendOver = RS485_SendOver* 形 参: 无* 返 回 值: 无**********************************************************************************************************/void RS485_SendOver(void){ RS485_RX_EN(); /* 切换RS485收发芯片为接收模式 */}/*********************************************************************************************************** 函 数 名: RS485_ReciveNew* 功能说明: 接收到新的数据* 形 参: _byte 接收到的新数据* 返 回 值: 无**********************************************************************************************************///extern void MODH_ReciveNew(uint8_t _byte);void RS485_ReciveNew(uint8_t _byte){// MODH_ReciveNew(_byte);}
发送数据前的准备工作。对于RS485通信,请设置RS485芯片为发送状态。
发送一串数据结束后的善后处理。对于RS485通信,请设置RS485芯片为接收状态。
接收到新的数据,用于Modbus通信协议。这里未用到Modbus,所以将对应的内容注释掉了。
31.4.3 RS485发送处理
串口数据的发送主要涉及到下面四个函数,调用关系是如下:
RS485_SendStr –> RS485_SendBuf –> comSendBuf -> UartSend
实际应用中,大家调用函数RS485_SendStr,RS485_SendBuf或者comSendBuf均可。另外特别注意代码中置红的部分,用于设置485发送使能。

/*********************************************************************************************************** 函 数 名: RS485_SendBuf* 功能说明: 通过RS485芯片发送一串数据。注意,本函数不等待发送完毕。* 形 参: _ucaBuf : 数据缓冲区* _usLen : 数据长度* 返 回 值: 无**********************************************************************************************************/void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen){ comSendBuf(COM3, _ucaBuf, _usLen);}/*********************************************************************************************************** 函 数 名: RS485_SendStr* 功能说明: 向485总线发送一个字符串,0结束。* 形 参: _pBuf 字符串,0结束* 返 回 值: 无**********************************************************************************************************/void RS485_SendStr(char *_pBuf){ RS485_SendBuf((uint8_t *)_pBuf, strlen(_pBuf));}/*********************************************************************************************************** 函 数 名: comSendBuf* 功能说明: 向串口发送一组数据。数据放到发送缓冲区后立即返回,由中断服务程序在后台完成发送* 形 参: _ucPort: 端口号(COM1 - COM8)* _ucaBuf: 待发送的数据缓冲区* _usLen : 数据长度* 返 回 值: 无**********************************************************************************************************/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);}/*********************************************************************************************************** 函 数 名: UartSend* 功能说明: 填写数据到UART发送缓冲区,并启动发送中断。中断处理函数发送完毕后,自动关闭发送中断* 形 参: 无* 返 回 值: 无**********************************************************************************************************/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
; DISABLE_INT(); if (++_pUart->usTxWrite >= _pUart->usTxBufSize) { _pUart->usTxWrite = 0; } _pUart->usTxCount++; ENABLE_INT(); } SET_BIT(_pUart->uart->CR1, USART_CR1_TXEIE); /* 使能发送中断(缓冲区空) */}
函数UartSend的作用就是把要发送的数据填到发送缓冲区里面,并使能发送空中断。
- 如果要发送的数据没有超过发送缓冲区大小,实现起来还比较容易,直接把数据填到FIFO里面,并使能发送空中断即可。
- 如果超过了FIFO大小,就需要等待有空间可用,针对这种情况有个重要的知识点,就是当缓冲刚刚填满的时候要判断发送空中断是否开启了,如果填满了还没有开启,就会卡死在while循环中,所以多了一个刚填满时的判断,填满了还没有开启发送空中断,要开启下。
注意:由于函数UartSend做了static作用域限制,仅可在bsp_uart_fifo.c文件中调用。函数RS485_SendStr,RS485_SendBuf或者comSendBuf是供用户调用的。
31.4.4 RS485数据接收
下面我们再来看看接收的函数:

/*********************************************************************************************************** 函 数 名: comGetChar* 功能说明: 从接收缓冲区读取1字节,非阻塞。无论有无数据均立即返回。* 形 参: _ucPort: 端口号(COM1 - COM8)* _pByte: 接收到的数据存放在这个地址* 返 回 值: 0 表示无数据, 1 表示读取到有效字节**********************************************************************************************************/uint8_t comGetChar(COM_PORT_E _ucPort, uint8_t *_pByte){ UART_T *pUart; pUart = ComToUart(_ucPort); if (pUart == 0) { return 0; } return UartGetChar(pUart, _pByte);}/*********************************************************************************************************** 函 数 名: UartGetChar* 功能说明: 从串口接收缓冲区读取1字节数据 (用于主程序调用)* 形 参: _pUart : 串口设备* _pByte : 存放读取数据的指针* 返 回 值: 0 表示无数据 1表示读取到数据**********************************************************************************************************/static uint8_t UartGetChar(UART_T *_pUart, uint8_t *_pByte){ uint16_t usCount; /* usRxWrite 变量在中断函数中被改写,主程序读取该变量时,必须进行临界区保护 */ DISABLE_INT(); usCount = _pUart->usRxCount; ENABLE_INT(); /* 如果读和写索引相同,则返回0 */ //if (_pUart->usRxRead == usRxWrite) if (usCount == 0) /* 已经没有数据 */ { return 0; } else { *_pByte = _pUart->pRxBuf[_pUart->usRxRead]; /* 从串口接收FIFO取1个数据 */ /* 改写FIFO读索引 */ DISABLE_INT(); if (++_pUart->usRxRead >= _pUart->usRxBufSize) { _pUart->usRxRead = 0; } _pUart->usRxCount--; ENABLE_INT(); return 1; }}
函数comGetChar是专门供用户调用的,用于从接收FIFO中读取1个数据。具体代码的实现也比较好理解,主要是接收FIFO的调整。
注意:由于函数UartGetChar做了static作用域限制,仅可在bsp_uart_fifo.c文件中调用。
31.4.5 RS485驱动中断服务程序的处理
串口中断服务程序是实现RS485驱动的关键部分,主要实现如下三个功能:
- 收到新的数据后,会将数据压入RX_FIFO。
- 检测到发送缓冲区空后,会从TX_FIFO中取下一个数据并发送。
- 对于RS485半双工串口,发送前会设置一个GPIO=1控制RS485收发器进入发送状态,当最后一个字节的最后一个bit传送完毕后,设置这个GPIO=0让RS485收发器进入接收状态。
下面我们分析一下串口中断处理的完整过程。
当产生串口中断后,CPU会查找中断向量表,获得中断服务程序的入口地址。入口函数为USART1_IRQHandler,这个函数在启动文件startup_stm32h743xx.s汇编代码中已经有实现。我们在c代码中需要重写一个同样名字的函数就可以重载它。如果不重载,启动文件中缺省的中断服务程序就是一个死循环,等于 while(1);
我们将串口中断服务程序放在bsp_uart_fifo.c文件,没有放到 stm32h7xx_it.c。当应用不需要串口功能时,直接从工程中删除bsp_uart_fifo.c接口,不必再去整理stm32h7xx_it.c这个文件。下面展示的代码是8个串口的中断服务程序,RS485用的USART3。

#if UART1_FIFO_EN == 1void USART1_IRQHandler(void){ UartIRQ(&g_tUart1);}#endif#if UART2_FIFO_EN == 1void USART2_IRQHandler(void){ UartIRQ(&g_tUart2);}#endif#if UART3_FIFO_EN == 1void USART3_IRQHandler(void){ UartIRQ(&g_tUart3);}#endif#if UART4_FIFO_EN == 1void UART4_IRQHandler(void){ UartIRQ(&g_tUart4);}#endif#if UART5_FIFO_EN == 1void UART5_IRQHandler(void){ UartIRQ(&g_tUart5);}#endif#if UART6_FIFO_EN == 1void USART6_IRQHandler(void){ UartIRQ(&g_tUart6);}#endif#if UART7_FIFO_EN == 1void UART7_IRQHandler(void){ UartIRQ(&g_tUart7);}#endif#if UART8_FIFO_EN == 1void UART8_IRQHandler(void){ UartIRQ(&g_tUart8);}#endif
大家可以看到,这8个中断服务程序都调用了同一个处理函数UartIRQ。我们只需要调通一个串口FIFO驱动,那么其他的串口驱动也就都通了。
下面,我们来看看UartIRQ函数的实现代码。

/*********************************************************************************************************** 函 数 名: UartIRQ* 功能说明: 供中断服务程序调用,通用串口中断处理函数* 形 参: _pUart : 串口设备* 返 回 值: 无**********************************************************************************************************/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; /* 填入串口接收FIFO */ if (++_pUart->usRxWrite >= _pUart->usRxBufSize) /* 接收FIFO的写指针+1 */ { _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解码程序处理字节流 */ } } } /* 处理发送缓冲区空中断 */ 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--; } } /* 数据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--; } } /* 清除中断标志 */ SET_BIT(_pUart->uart->ICR, UART_CLEAR_PEF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_FEF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_NEF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_OREF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_IDLEF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_TCF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_LBDF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_CTSF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_CMF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_WUF); SET_BIT(_pUart->uart->ICR, UART_CLEAR_TXFECF); }
中断服务程序的处理主要分为两部分,接收数据的处理和发送数据的处理,详情看程序注释即可,已经比较详细,下面重点把思路说一下:
接收数据的处理是判断ISR寄存器的USART_ISR_RXNE标志是否置位,如果置位表示RDR接收寄存器已经存入数据。然后将数据读入到接收FIFO空间。
特别注意里面的ReciveNew处理,这个在Modbus协议里面要用到。
发送数据主要是发送空中断TEX和发送完成中断TC的处理,当TXE=1时,只是表示发送数据寄存器为空了,此时可以填充下一个准备发送的数据了。当为TDR发送寄存器赋值后,硬件启动发送,等所有的bit传送完毕后,TC标志设置为1。如果是RS232全双工通信,可以只用TXE标志控制发送过程。如果是RS485半双工通通信,就需要利用TC标志了,因为在最后一个bit传送完毕后,需要设置RS485收发器进入到接收状态。
31.5 RS485板级支持包(bsp_uart_fifo.c)
串口驱动文件bsp_uart_fifo.c主要实现了如下几个API供用户调用:
- bsp_InitUart
- comSendBuf
- comSendChar
- comGetChar
31.5.1 函数bsp_InitUart
函数原型:
void bsp_InitUart(void)
函数描述:
此函数主要用于串口的初始化,使用所有其它API之前,务必优先调用此函数。
使用举例:
串口的初始化函数在bsp.c文件的bsp_Init函数里面调用。
31.5.2 函数comSendBuf
函数原型:
void comSendBuf(COM_PORT_E _ucPort, uint8_t *_ucaBuf, uint16_t _usLen);
函数描述:
此函数用于向串口发送一组数据,非阻塞方式,数据放到发送缓冲区后立即返回,由中断服务程序在后台完成发送。
函数参数:
- 第1个参数_ucPort是端口号,范围COM1 - COM8。
- 第2个参数_ucaBuf是待发送的数据缓冲区地址。
- 第3个参数_usLen是要发送数据的字节数。
注意事项:
- 此函数的解读在第30章30.3.5小节。
- 发送的数据最好不要超过bsp_uart_fifo.h文件中定义的发送缓冲区大小,从而实现最优的工作方式。因为超过后需要在发送函数等待有发送空间可用。
使用举例:
调用此函数前,务必优先调用函数bsp_InitUart进行初始化。
const char buf1[] = "接收到串口命令1rn";
comSendBuf(COM1, (uint8_t *)buf1, strlen(buf1));
31.5.3 函数RS485_SendBuf
函数原型:
void RS485_SendBuf(uint8_t *_ucaBuf, uint16_t _usLen)
函数描述:
此函数用于向RS485总线发送一组数据,非阻塞方式,数据放到发送缓冲区后立即返回,由中断服务程序在后台完成发送。此函数是通过调用函数comSendBuf实现的。
函数参数:
- 第1个参数_ucaBuf是数据缓冲区。
- 第2个参数_usLen是数据长度。
注意事项:
调用此函数前,务必优先调用函数bsp_InitUart进行初始化。
31.5.4 函数RS485_SendStr
函数原型:
void RS485_SendStr(char *_pBuf)
函数描述:
此函数用于向RS485总线发送一个字符串,非阻塞方式,数据放到发送缓冲区后立即返回,由中断服务程序在后台完成发送。此函数是通过调用函数RS485_SendBuf实现的。
函数参数:
注意事项:
调用此函数前,务必优先调用函数bsp_InitUart进行初始化。
31.5.5 函数comGetChar
函数原型:
uint8_t comGetChar(COM_PORT_E _ucPort, uint8_t *_pByte)
函数描述:
此函数用于从接收缓冲区读取1字节,非阻塞。无论有无数据均立即返回。
函数参数:
- 第1个参数_ucPort是端口号,范围COM1 - COM8。
- 第2个参数_pByte用于存放接收到的数据。
- 返回值,返回0表示无数据, 1 表示读取到有效字节。
注意事项:
使用举例:
调用此函数前,务必优先调用函数bsp_InitUart进行初始化。
比如从串口1读取一个字符就是:comGetChar(COM1, &read)
31.6 RS485驱动移植和使用
RS485移植步骤如下:
- 第1步:复制bsp_uart_fifo.h和bsp_uart_fifo.c到自己的工程目录,并添加到工程里面。
- 第2步:根据485要使用的串口和收发缓冲大小,修改下面的宏定义即可。

#define UART1_FIFO_EN 1#define UART2_FIFO_EN 0#define UART3_FIFO_EN 0#define UART4_FIFO_EN 0#define UART5_FIFO_EN 0#define UART6_FIFO_EN 0#define UART7_FIFO_EN 0#define UART8_FIFO_EN 0/* 定义串口波特率和FIFO缓冲区大小,分为发送缓冲区和接收缓冲区, 支持全双工 */#if UART1_FIFO_EN == 1 #define UART1_BAUD 115200 #define UART1_TX_BUF_SIZE 1*1024 #define UART1_RX_BUF_SIZE 1*1024#endif#if UART2_FIFO_EN == 1 #define UART2_BAUD 9600 #define UART2_TX_BUF_SIZE 10 #define UART2_RX_BUF_SIZE 2*1024#endif#if UART3_FIFO_EN == 1 #define UART3_BAUD 9600 #define UART3_TX_BUF_SIZE 1*1024 #define UART3_RX_BUF_SIZE 1*1024#endif#if UART4_FIFO_EN == 1 #define UART4_BAUD 115200 #define UART4_TX_BUF_SIZE 1*1024 #define UART4_RX_BUF_SIZE 1*1024#endif#if UART5_FIFO_EN == 1 #define UART5_BAUD 115200 #define UART5_TX_BUF_SIZE 1*1024 #define UART5_RX_BUF_SIZE 1*1024#endif#if UART6_FIFO_EN == 1 #define UART6_BAUD 115200 #define UART6_TX_BUF_SIZE 1*1024 #define UART6_RX_BUF_SIZE 1*1024#endif#if UART7_FIFO_EN == 1 #define UART7_BAUD 115200 #define UART7_TX_BUF_SIZE 1*1024 #define UART7_RX_BUF_SIZE 1*1024#endif#if UART8_FIFO_EN == 1 #define UART8_BAUD 115200 #define UART8_TX_BUF_SIZE 1*1024 #define UART8_RX_BUF_SIZE 1*1024#endif

/* 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#define RS485_TX_EN() RS485_TXEN_GPIO_PORT->BSRRL = RS485_TXEN_PIN
- 第4步:根据使用的串口,配置下面函数的几个参数成员,下面是以串口3为例说明(置红部分):

static void UartVarInit(void){#if UART3_FIFO_EN == 1 g_tUart3.uart = USART3; /* STM32 串口设备 */ g_tUart3.pTxBuf = g_TxBuf3; /* 发送缓冲区指针 */ g_tUart3.pRxBuf = g_RxBuf3; /* 接收缓冲区指针 */ g_tUart3.usTxBufSize = UART3_TX_BUF_SIZE; /* 发送缓冲区大小 */ g_tUart3.usRxBufSize = UART3_RX_BUF_SIZE; /* 接收缓冲区大小 */ g_tUart3.usTxWrite = 0; /* 发送FIFO写索引 */ g_tUart3.usTxRead = 0; /* 发送FIFO读索引 */ g_tUart3.usRxWrite = 0; /* 接收FIFO写索引 */ g_tUart3.usRxRead = 0; /* 接收FIFO读索引 */ g_tUart3.usRxCount = 0; /* 接收到的新数据个数 */ g_tUart3.usTxCount = 0; /* 待发送的数据个数 */ g_tUart3.SendBefor = RS485_SendBefor; /* 发送数据前的回调函数 */ g_tUart3.SendOver = RS485_SendOver; /* 发送完毕后的回调函数 */ g_tUart3.ReciveNew = RS485_ReciveNew; /* 接收到新数据后的回调函数 */ g_tUart3.Sending = 0; /* 正在发送中标志 */#endif}
- 第5步:这几个驱动文件主要用到HAL库的GPIO和串口驱动文件,简单省事些可以添加所有HAL库.C源文件进来。
- 第6步,应用方法看本章节配套例子即可。
31.7 实验例程设计框架
通过程序设计框架,让大家先对配套例程有一个全面的认识,然后再理解细节,本次实验例程的设计框架如下:

第1阶段,上电启动阶段:
第2阶段,进入main函数:
- 第1步,硬件初始化,主要是MPU,Cache,HAL库,系统时钟,滴答定时器,LED和串口。
- 第2步,485应用程序设计部分,具体分为两个部分,接收命令和发送命令。
31.8 实验例程说明(MDK)
配套例子:
V7-016_RS485多机通讯
实验目的:
实验内容:
- 由于通信距离较短,SP3485E芯片上缺省未贴的电阻不需要贴上,大家可以根据需要贴上电阻做测试。
- 本例子支持多个485节点,不需要设置主设备和从设备,所有节点下载此程序即可。
- 开发板的485-A端子连接到一起,485-B端子连接到一起,具体连接看工程Doc文件夹中的截图。
实验操作:
- 按下开发板上的K1键点亮LED1,松开熄灭LED1,同时打印按键事件到串口1。485总线上的其它开发板做相同的动作。
- 按下开发板上的K2键,启动50ms的自动重装定时器,每隔50ms翻转LED2,并向485总线上的其它开发板发送按键K2按下消息,从而也实现每隔50ms翻转LED2。
- 按下开发板上的K3按键,停止K2按键启动的50ms自动重载定时器,485总线上的其它开发板做相同的动作。
- 按下开发板上的摇杆(上下左右,OK共5种),会通过串口1打印摇杆事件。485总线上的其它开发板做相同的动作。
注意事项:
- RS485 PHY的发送使能用的引脚PB11,测试前需要将板子J5处的跳线帽短接到PB11端。
多机接线效果:


上电后串口打印的信息:
波特率 115200,数据位 8,奇偶校验位无,停止位 1

程序设计:
系统栈大小分配:

RAM空间用的DTCM:

硬件外设初始化
硬件外设的初始化是在 bsp.c 文件实现:

/*********************************************************************************************************** 函 数 名: bsp_Init* 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次* 形 参:无* 返 回 值: 无**********************************************************************************************************/void bsp_Init(void){ /* 配置MPU */ MPU_Config(); /* 使能L1 Cache */ CPU_CACHE_Enable(); /* STM32H7xx HAL 库初始化,此时系统用的还是H7自带的64MHz,HSI时钟: - 调用函数HAL_InitTick,初始化滴答时钟中断1ms。 - 设置NVIV优先级分组为4。 */ HAL_Init(); /* 配置系统时钟到400MHz - 切换使用HSE。 - 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用于代码执行时间测量,MDK5.25及其以上版本才支持,IAR不支持。 - 默认不开启,如果要使能此选项,务必看V7开发板用户手册第xx章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder并开启 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart();#endif bsp_InitKey(); /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */ bsp_InitTimer(); /* 初始化滴答定时器 */ bsp_InitUart(); /* 初始化串口 */ bsp_InitExtIO(); /* 初始化FMC总线74HC574扩展IO. 必须在 bsp_InitLed()前执行 */ bsp_InitLed(); /* 初始化LED */ }
MPU配置和Cache配置:
数据Cache和指令Cache都开启。配置了AXI SRAM区(本例子未用到AXI SRAM)和FMC的扩展IO区。

/*********************************************************************************************************** 函 数 名: MPU_Config* 功能说明: 配置MPU* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void MPU_Config( void ){ MPU_Region_InitTypeDef MPU_InitStruct; /* 禁止 MPU */ HAL_MPU_Disable(); /* 配置AXI SRAM的MPU属性为Write back, Read allocate,Write allocate */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置FMC扩展IO的MPU属性为Device或者Strongly Ordered */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x60000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /*使能 MPU */ HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);}/*********************************************************************************************************** 函 数 名: CPU_CACHE_Enable* 功能说明: 使能L1 Cache* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void CPU_CACHE_Enable(void){ /* 使能 I-Cache */ SCB_EnableICache(); /* 使能 D-Cache */ SCB_EnableDCache();}
主功能:
主功能的实现主要分为两部分:
- 获取其它RS485设备发来的命令,并执行相应功能。
通过函数comGetChar(COM3, &ucDataTravel)以查询、非阻塞方式获取其它开发板发来的数据。
通过函数comSendChar(COM3, ucDataTravel)以非阻塞方式向其它485设备发送命令,以此来执行同样的功能。

/*********************************************************************************************************** 函 数 名: main* 功能说明: c程序入口* 形 参: 无* 返 回 值: 错误代码(无需处理)**********************************************************************************************************/int main(void){ uint8_t ucKeyCode; /* 按键代码 */ uint8_t ucDataTravel; /* 发送变量 */ uint8_t ucDataRec; /* 接收变量 */ bsp_Init(); /* 硬件初始化 */ PrintfLogo(); /* 打印例程名称和版本等信息 */ PrintfHelp(); /* 打印操作提示 */ /* 进入主程序循环体 */ while (1) { bsp_Idle(); /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 */ /* 获取其它开发板通过485总线发来的数据 */ if(comGetChar(COM3, &ucDataRec)) { switch (ucDataRec) { case KEY_DOWN_K1: /* 获得K1键按下消息 */ bsp_LedOn(1); printf("K1键按下, LED1点亮rn"); break; case KEY_UP_K1: /* 获得K1键释放消息 */ bsp_LedOff(1); printf("K1键弹起, LED1熄灭rn"); break; case KEY_DOWN_K2: /* 获得K1键按下消息 */ bsp_LedToggle(2); break; case JOY_DOWN_U: /* 获得摇杆UP键按下 */ printf("摇杆上键按下rn"); break; case JOY_DOWN_D: /* 获得摇杆DOWN键按下 */ printf("摇杆下键按下rn"); break; case JOY_DOWN_L: /* 获得摇杆LEFT键按下 */ printf("摇杆左键按下rn"); break; case JOY_DOWN_R: /* 获得摇杆RIGHT键按下 */ printf("摇杆右键按下rn"); break; case JOY_DOWN_OK: /* 获得摇杆OK键按下 */ printf("摇杆OK键按下rn"); break; case JOY_UP_OK: /* 获得摇杆OK键弹起 */ printf("摇杆OK键弹起rn"); break; default: /* 其它的键值不处理 */ break; } } /* 判断定时器超时时间 */ if (bsp_CheckTimer(0)) { /* 每隔50ms 进来一次 */ bsp_LedToggle(2); /* 向其它开发板发送按键K2按下的消息 */ ucDataTravel = KEY_DOWN_K2; comSendChar(COM3, ucDataTravel); } /* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */ ucKeyCode = bsp_GetKey(); /* 读取键值, 无键按下时返回 KEY_NONE = 0 */ if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { case KEY_DOWN_K1: /* K1键按下 */ ucDataTravel = KEY_DOWN_K1; comSendChar(COM3, ucDataTravel); bsp_LedOn(1); printf("K1键按下, LED1点亮rn"); break; case KEY_UP_K1: /* K1键弹起 */ ucDataTravel = KEY_UP_K1; comSendChar(COM3, ucDataTravel); bsp_LedOff(1); printf("K1键弹起, LED1熄灭rn"); break; case KEY_DOWN_K2: /* K2键按下 */ bsp_StartAutoTimer(0, 50); /* 启动1个50ms的自动重装的定时器 */ break; case KEY_DOWN_K3: /* K3键按下 */ bsp_StopTimer(0); /* 停止自动重装的定时器 */ break; case JOY_DOWN_U: /* 摇杆UP键按下 */ ucDataTravel = JOY_DOWN_U; comSendChar(COM3, ucDataTravel); printf("摇杆上键按下rn"); break; case JOY_DOWN_D: /* 摇杆DOWN键按下 */ ucDataTravel = JOY_DOWN_D; comSendChar(COM3, ucDataTravel); printf("摇杆下键按下rn"); break; case JOY_DOWN_L: /* 摇杆LEFT键按下 */ ucDataTravel = JOY_DOWN_L; comSendChar(COM3, ucDataTravel); printf("摇杆左键按下rn"); break; case JOY_DOWN_R: /* 摇杆RIGHT键按下 */ ucDataTravel = JOY_DOWN_R; comSendChar(COM3, ucDataTravel); printf("摇杆右键按下rn"); break; case JOY_DOWN_OK: /* 摇杆OK键按下 */ ucDataTravel = JOY_DOWN_OK; comSendChar(COM3, ucDataTravel); printf("摇杆OK键按下rn"); break; case JOY_UP_OK: /* 摇杆OK键弹起 */ ucDataTravel = JOY_UP_OK; comSendChar(COM3, ucDataTravel); printf("摇杆OK键弹起rn"); break; default: /* 其它的键值不处理 */ break; } } }}
31.9 实验例程说明(IAR)
配套例子:
V7-016_RS485多机通讯
实验目的:
实验内容:
- 由于通信距离较短,SP3485E芯片上缺省未贴的电阻不需要贴上,大家可以根据需要贴上电阻做测试。
- 本例子支持多个485节点,不需要设置主设备和从设备,所有节点下载此程序即可。
- 开发板的485-A端子连接到一起,485-B端子连接到一起,具体连接看工程Doc文件夹中的截图。
实验操作:
- 按下开发板上的K1键点亮LED1,松开熄灭LED1,同时打印按键事件到串口1。485总线上的其它开发板做相同的动作。
- 按下开发板上的K2键,启动50ms的自动重装定时器,每隔50ms翻转LED2,并向485总线上的其它开发板发送按键K2按下消息,从而也实现每隔50ms翻转LED2。
- 按下开发板上的K3按键,停止K2按键启动的50ms自动重载定时器,485总线上的其它开发板做相同的动作。
- 按下开发板上的摇杆(上下左右,OK共5种),会通过串口1打印摇杆事件。485总线上的其它开发板做相同的动作。
注意事项:
- RS485 PHY的发送使能用的引脚PB11,测试前需要将板子J5处的跳线帽短接到PB11端。
多机接线效果:


上电后串口打印的信息:
波特率 115200,数据位 8,奇偶校验位无,停止位 1

程序设计:
系统栈大小分配:

RAM空间用的DTCM:

硬件外设初始化
硬件外设的初始化是在 bsp.c 文件实现:

/*********************************************************************************************************** 函 数 名: bsp_Init* 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次* 形 参:无* 返 回 值: 无**********************************************************************************************************/void bsp_Init(void){ /* 配置MPU */ MPU_Config(); /* 使能L1 Cache */ CPU_CACHE_Enable(); /* STM32H7xx HAL 库初始化,此时系统用的还是H7自带的64MHz,HSI时钟: - 调用函数HAL_InitTick,初始化滴答时钟中断1ms。 - 设置NVIV优先级分组为4。 */ HAL_Init(); /* 配置系统时钟到400MHz - 切换使用HSE。 - 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用于代码执行时间测量,MDK5.25及其以上版本才支持,IAR不支持。 - 默认不开启,如果要使能此选项,务必看V7开发板用户手册第xx章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder并开启 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart();#endif bsp_InitKey(); /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */ bsp_InitTimer(); /* 初始化滴答定时器 */ bsp_InitUart(); /* 初始化串口 */ bsp_InitExtIO(); /* 初始化FMC总线74HC574扩展IO. 必须在 bsp_InitLed()前执行 */ bsp_InitLed(); /* 初始化LED */ }
MPU配置和Cache配置:
数据Cache和指令Cache都开启。配置了AXI SRAM区(本例子未用到AXI SRAM)和FMC的扩展IO区。

/*********************************************************************************************************** 函 数 名: MPU_Config* 功能说明: 配置MPU* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void MPU_Config( void ){ MPU_Region_InitTypeDef MPU_InitStruct; /* 禁止 MPU */ HAL_MPU_Disable(); /* 配置AXI SRAM的MPU属性为Write back, Read allocate,Write allocate */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置FMC扩展IO的MPU属性为Device或者Strongly Ordered */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x60000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /*使能 MPU */ HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);}/*********************************************************************************************************** 函 数 名: CPU_CACHE_Enable* 功能说明: 使能L1 Cache* 形 参: 无* 返 回 值: 无**********************************************************************************************************/static void CPU_CACHE_Enable(void){ /* 使能 I-Cache */ SCB_EnableICache(); /* 使能 D-Cache */ SCB_EnableDCache();}
主功能:
主功能的实现主要分为两部分:
- 获取其它RS485设备发来的命令,并执行相应功能。
通过函数comGetChar(COM3, &ucDataTravel)以查询、非阻塞方式获取其它开发板发来的数据。
通过函数comSendChar(COM3, ucDataTravel)以非阻塞方式向其它485设备发送命令,以此来执行同样的功能。

/*********************************************************************************************************** 函 数 名: main* 功能说明: c程序入口* 形 参: 无* 返 回 值: 错误代码(无需处理)**********************************************************************************************************/int main(void){ uint8_t ucKeyCode; /* 按键代码 */ uint8_t ucDataTravel; /* 发送变量 */ uint8_t ucDataRec; /* 接收变量 */ bsp_Init(); /* 硬件初始化 */ PrintfLogo(); /* 打印例程名称和版本等信息 */ PrintfHelp(); /* 打印操作提示 */ /* 进入主程序循环体 */ while (1) { bsp_Idle(); /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 */ /* 获取其它开发板通过485总线发来的数据 */ if(comGetChar(COM3, &ucDataRec)) { switch (ucDataRec) { case KEY_DOWN_K1: /* 获得K1键按下消息 */ bsp_LedOn(1); printf("K1键按下, LED1点亮rn"); break; case KEY_UP_K1: /* 获得K1键释放消息 */ bsp_LedOff(1); printf("K1键弹起, LED1熄灭rn"); break; case KEY_DOWN_K2: /* 获得K1键按下消息 */ bsp_LedToggle(2); break; case JOY_DOWN_U: /* 获得摇杆UP键按下 */ printf("摇杆上键按下rn"); break; case JOY_DOWN_D: /* 获得摇杆DOWN键按下 */ printf("摇杆下键按下rn"); break; case JOY_DOWN_L: /* 获得摇杆LEFT键按下 */ printf("摇杆左键按下rn"); break; case JOY_DOWN_R: /* 获得摇杆RIGHT键按下 */ printf("摇杆右键按下rn"); break; case JOY_DOWN_OK: /* 获得摇杆OK键按下 */ printf("摇杆OK键按下rn"); break; case JOY_UP_OK: /* 获得摇杆OK键弹起 */ printf("摇杆OK键弹起rn"); break; default: /* 其它的键值不处理 */ break; } } /* 判断定时器超时时间 */ if (bsp_CheckTimer(0)) { /* 每隔50ms 进来一次 */ bsp_LedToggle(2); /* 向其它开发板发送按键K2按下的消息 */ ucDataTravel = KEY_DOWN_K2; comSendChar(COM3, ucDataTravel); } /* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */ ucKeyCode = bsp_GetKey(); /* 读取键值, 无键按下时返回 KEY_NONE = 0 */ if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { case KEY_DOWN_K1: /* K1键按下 */ ucDataTravel = KEY_DOWN_K1; comSendChar(COM3, ucDataTravel); bsp_LedOn(1); printf("K1键按下, LED1点亮rn"); break; case KEY_UP_K1: /* K1键弹起 */ ucDataTravel = KEY_UP_K1; comSendChar(COM3, ucDataTravel); bsp_LedOff(1); printf("K1键弹起, LED1熄灭rn"); break; case KEY_DOWN_K2: /* K2键按下 */ bsp_StartAutoTimer(0, 50); /* 启动1个50ms的自动重装的定时器 */ break; case KEY_DOWN_K3: /* K3键按下 */ bsp_StopTimer(0); /* 停止自动重装的定时器 */ break; case JOY_DOWN_U: /* 摇杆UP键按下 */ ucDataTravel = JOY_DOWN_U; comSendChar(COM3, ucDataTravel); printf("摇杆上键按下rn"); break; case JOY_DOWN_D: /* 摇杆DOWN键按下 */ ucDataTravel = JOY_DOWN_D; comSendChar(COM3, ucDataTravel); printf("摇杆下键按下rn"); break; case JOY_DOWN_L: /* 摇杆LEFT键按下 */ ucDataTravel = JOY_DOWN_L; comSendChar(COM3, ucDataTravel); printf("摇杆左键按下rn"); break; case JOY_DOWN_R: /* 摇杆RIGHT键按下 */ ucDataTravel = JOY_DOWN_R; comSendChar(COM3, ucDataTravel); printf("摇杆右键按下rn"); break; case JOY_DOWN_OK: /* 摇杆OK键按下 */ ucDataTravel = JOY_DOWN_OK; comSendChar(COM3, ucDataTravel); printf("摇杆OK键按下rn"); break; case JOY_UP_OK: /* 摇杆OK键弹起 */ ucDataTravel = JOY_UP_OK; comSendChar(COM3, ucDataTravel); printf("摇杆OK键弹起rn"); break; default: /* 其它的键值不处理 */ break; } } }}
31.10 总结
本章节就为大家讲解这么多,485通信依然在实际项目中非常实用。有线的串行网络仍然提供最有力、最可靠的通信,特别是在恶劣的环境中。
举报