工做笔记——CPLD与MCU经过SPI通讯

1、需求描述

  • MCU须要接收来自CPLD的升级固件数据
  • CPLD对MCU只进行发送数据,不接收MCU的数据
  • CPLD没法告知数据传输的开始和结束,须要MCU自行判断(CPLD只是数据透传,不作数据判断)
  • 数据通讯速率至少是UART通讯的115200波特率
  • PCB上MCU与CPLD之间经过3个普通IO引脚链接

2、功能分析

  • MCU与CPLD之间有3根线,那么能够选择UART通讯或者SPI通讯方式。异步

  • 因为CPLD没法通知MCU数据传输的开始与结束,MCU须要自行判别,那么MCU能够经过中断方式来检测数据传输的开始,经过超时来检测数据传输的结束。函数

  • UART与SPI的区别在于前者是异步通讯后者是同步通讯方式,不管是SPI仍是UART方式都须要MCU经过IO模拟方式软件实现。使用UART传输若是收发双方产生的波特率存在误差则会致使数据传输出错,而同步传输方式有时钟信号的约束,相比异步传输方式数据准确率会更高。若是使用软件模拟UART,须要使用定时器做为波特率发生器。若是波特率比较高,那么定时器中断频率就须要更高,这样会影响整个MCU系统的实时性。综合考虑后选择SPI方式。ui

  • CPLD对MCU只发送数据,那么MCU只须要做为SPI的从机便可,三个IO分配为SPI的CS、CLK、DAT引脚。code

  • 因为CS是低电平有效,那么将CS引脚配置为中断输入方式,当CS中断触发后开始数据接收处理。由于CPLD也不知道数据传输何时结束,因此没法经过将CS置高电平来告诉数据传输的结束,那么CS置高电平只能代表一个字节传输结束。MCU能够经过超时方式来判断一包数据的结束,相似于串口的空闲中断方式。blog

  • SPI数据接收在外部中断中操做。将CLK引脚配置为外部中断的上升沿触发,CS有效的状况下CLK中断触发后进行数据接收。同步

  • SPI空闲中断采用100us周期定时器判断。为了MCU系统的实时性,只有CS中断触发后才会开启定时器,超时判断完成后关闭定时器。it

  • CPLD向MCU发送一字节的时序图以下(速率:200KBit/s):event

3、软件实现

GPIO的配置:无数据CLK为低电平,CS低有效。CS上升沿、降低沿都会触发中断,判断1字节传输的起始与结束;CLK上升沿触发中断,数据在CLK上升沿采样class

/*
***********************************************************************************************
*	函	数: BSP_CPLD_GPIO_Init
*	描	述: 配置CPLD的SPI通讯引脚
*	输	入: 无
*	输	出: 无
***********************************************************************************************
*/
void BSP_CPLD_GPIO_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct = {0};

	/* GPIO Ports Clock Enable */
	CPLD_PIN_CLK_ENABLE();

	/*Configure GPIO pin : PtPin */
	GPIO_InitStruct.Pin 	= CPLD_SPI_CSN_PIN;
	GPIO_InitStruct.Mode 	= GPIO_MODE_IT_RISING_FALLING;
	GPIO_InitStruct.Pull 	= GPIO_PULLUP;
	HAL_GPIO_Init(CPLD_SPI_CSN_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.Pin		= CPLD_SPI_SCK_PIN;
	GPIO_InitStruct.Mode	= GPIO_MODE_IT_RISING;
	GPIO_InitStruct.Pull 	= GPIO_PULLDOWN;
	HAL_GPIO_Init(CPLD_SPI_SCK_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.Pin		= CPLD_SPI_DAT_PIN;
	GPIO_InitStruct.Pull	= GPIO_MODE_INPUT;
	GPIO_InitStruct.Pull 	= GPIO_NOPULL;
	HAL_GPIO_Init(CPLD_SPI_DAT_PORT, &GPIO_InitStruct);
}

CPLD_SPI_CS外部中断函数:用于使能数据接收、空闲检测软件

/*
***********************************************************************************************
*	函	数: CPLD_CS_EXTI_IRQHandler
*	描	述: CPLD_SPI_CS中断函数
*	输	入: 无
*	输	出: 无
***********************************************************************************************
*/
void CPLD_CS_EXTI_IRQHandler(void)
{
	if (__HAL_GPIO_EXTI_GET_IT(CPLD_SPI_CSN_PIN))
	{
		if (READ_BIT(CPLD_SPI_CSN_PORT->IDR, CPLD_SPI_CSN_PIN))
		{
			/* CS高失能SPI数据接收 */
			CLEAR_BIT(g_tCpldSpi.ucState, CPLD_FLAG_CSN);
		}
		else
		{
			/* CS低使能SPI数据接收 */
			s_tCpldSpi.ucByte = 0;
			s_tCpldSpi.ucBitCount = 0;
			SET_BIT(g_tCpldSpi.ucState, CPLD_FLAG_CSN);
			
			/* 开启空闲检测 */
			if (0 == s_tCpldSpi.ucIdleCheck)
			{
				s_tCpldSpi.ucIdleCheck = 1;
				HAL_TIM_Base_Start_IT(&Tim7Handle);
			}
		}
		__HAL_GPIO_EXTI_CLEAR_IT(CPLD_SPI_CSN_PIN);
	}
}

CPLD_SPI_SCK外部中断函数:用于SPI数据的接收

/*
***********************************************************************************************
*	函	数: CPLD_SCK_EXTI_IRQHandler
*	描	述: CPLD_SPI_SCK中断函数
*	输	入: 无
*	输	出: 无
***********************************************************************************************
*/
void CPLD_SCK_EXTI_IRQHandler(void)
{
	if (__HAL_GPIO_EXTI_GET_IT(CPLD_SPI_SCK_PIN))
	{
		/* CSN有效则进行数据接收 */
		if (READ_BIT(g_tCpldSpi.ucState, CPLD_FLAG_CSN))
		{
			if (READ_BIT(CPLD_SPI_DAT_PORT->IDR, CPLD_SPI_DAT_PIN))
			{
				s_tCpldSpi.ucByte |= (0x80 >> s_tCpldSpi.ucBitCount);
			}
			else
			{
				s_tCpldSpi.ucByte &= ~(0x80 >> s_tCpldSpi.ucBitCount);
			}
			/* 收满一字节后存向接收FIFO */	
			if (++s_tCpldSpi.ucBitCount > 7)
			{
				g_tCpldSpi.ucaRxBuf[g_tCpldSpi.usRxWrite] = s_tCpldSpi.ucByte;
				if (++g_tCpldSpi.usRxWrite >= 1024)
				{
					g_tCpldSpi.usRxWrite = 0;
				}
				if (g_tCpldSpi.usRxCount < CPLD_SPI_RX_BUF_LEN)
				{
					g_tCpldSpi.usRxCount++;
				}
				/* SPI收到新数据,设置一个标记,供应用程序查询 */
				SET_BIT(g_tCpldSpi.ucState, CPLD_FLAG_RXNE);
			}
		}
		__HAL_GPIO_EXTI_CLEAR_IT(CPLD_SPI_SCK_PIN);
	}
}

定时器中断函数:判断CPLD_SPI空闲中断的发生

/*
***********************************************************************************************
*	函	数: TIM7_IRQHandler
*	描	述: 定时器7中断函数,100us中断周期
*	输	入: 无
*	输	出: 无
***********************************************************************************************
*/
void TIM7_IRQHandler(void)
{	
	static uint16_t t100us_cnt = 0;
		
	if (__HAL_TIM_GET_FLAG(&Tim7Handle, TIM_FLAG_UPDATE) &&
        __HAL_TIM_GET_IT_SOURCE(&Tim7Handle, TIM_IT_UPDATE))
	{
        /* CPLD-SPI空闲检测,1ms */
        if (READ_BIT(g_tCpldSpi.ucState, CPLD_FLAG_CSN))
        {
            t100us_cnt = 0;
        }
        else
        {
            t100us_cnt++;
        }

        if (t100us_cnt > 10)
        {
            /* SPI收到一帧数据,设置一个标记,供应用程序查询 */
            SET_BIT(g_tCpldSpi.ucState, CPLD_FLAG_IDLE);
#if ENABLE_RTOS
            tx_event_flags_set(&tx_event_flags, TX_EVENT_CPLD_SPI_IDLE, TX_OR);
#endif
            s_tCpldSpi.ucIdleCheck = 0;
            HAL_TIM_Base_Stop_IT(&Tim7Handle);
        }
        __HAL_TIM_CLEAR_IT(&Tim7Handle, TIM_IT_UPDATE);
	}
}

4、功能验证

经屡次发送固件数据验证,MCU均能正常接收数据,而且没有出现数据错误的状况,可用于该项目。

5、拓展

该方法也能够用于实现模拟UART功能,仅提供思路,未通过验证(以115200,8-N-1为例)。

  • 将UART的接收引脚配置为上拉模式、降低沿触发中断(判断起始位)。
  • 中断第一次触发后代表收到起始位,收到起始位后打开定时器中断(若是波特率为115200,那么中断周期应小于8.6us,若是每Bit数据须要屡次采样,则须要更短的中断周期)。
  • 每中断一次判断一次数据引脚,数据收满10Bit后判断是否为中止位,若数据接收正确则存入接收FIFO。
  • 在收到起始位后开始计时,1ms内没有再次收到起始位则认为收到一帧数据,产生软空闲中断,而后关闭定时器。

使用该UART方式优点在于比SPI方式使用更少的引脚,只须要1个IO便可完成通讯。缺点在于若是要求通讯速率高或须要屡次采样,那么产生波特率的定时器中断频率高,若是被其余更高优先级中断打断可能形成波特率不许,数据错误。还有就是UART方式在数据通讯速率上没有SPI有优点。不到万不得已不建议使用软件UART方式。

相关文章
相关标签/搜索