工作筆記——CPLD與MCU通過SPI通訊
阿新 • • 發佈:2020-12-12
# 一、需求描述
- MCU需要接收來自CPLD的升級韌體資料
- CPLD對MCU只進行傳送資料,不接收MCU的資料
- CPLD無法告知資料傳輸的開始和結束,需要MCU自行判斷(CPLD只是資料透傳,不做資料判斷)
- 資料通訊速率至少是UART通訊的115200波特率
- PCB上MCU與CPLD之間通過3個普通IO引腳連線
# 二、功能分析
- MCU與CPLD之間有3根線,那麼可以選擇UART通訊或者SPI通訊方式。
- 由於CPLD無法通知MCU資料傳輸的開始與結束,MCU需要自行判別,那麼MCU可以通過中斷方式來檢測資料傳輸的開始,通過超時來檢測資料傳輸的結束。
- UART與SPI的區別在於前者是非同步通訊後者是同步通訊方式,不論是SPI還是UART方式都需要MCU通過IO模擬方式軟體實現。使用UART傳輸如果收發雙方產生的波特率存在偏差則會導致資料傳輸出錯,而同步傳輸方式有時鐘訊號的約束,相比非同步傳輸方式資料準確率會更高。如果使用軟體模擬UART,需要使用定時器作為波特率發生器。如果波特率比較高,那麼定時器中斷頻率就需要更高,這樣會影響整個MCU系統的實時性。綜合考慮後選擇SPI方式。
- CPLD對MCU只發送資料,那麼MCU只需要作為SPI的從機即可,三個IO分配為SPI的CS、CLK、DAT引腳。
- 由於CS是低電平有效,那麼將CS引腳配置為中斷輸入方式,當CS中斷觸發後開始資料接收處理。因為CPLD也不知道資料傳輸什麼時候結束,所以無法通過將CS置高電平來告訴資料傳輸的結束,那麼CS置高電平只能表明一個位元組傳輸結束。MCU可以通過超時方式來判斷一包資料的結束,類似於串列埠的空閒中斷方式。
- SPI資料接收在外部中斷中操作。將CLK引腳配置為外部中斷的`上升沿觸發`,CS有效的情況下CLK中斷觸發後進行資料接收。
- SPI空閒中斷採用100us週期定時器判斷。為了MCU系統的實時性,只有CS中斷觸發後才會開啟定時器,超時判斷完成後關閉定時器。
- CPLD向MCU傳送一位元組的時序圖如下(速率:200KBit/s):
![](https://img2020.cnblogs.com/blog/2193174/202012/2193174-20201212185320992-574189725.jpg)
# 三、軟體實現
> GPIO的配置:無資料CLK為低電平,CS低有效。CS上升沿、下降沿都會觸發中斷,判斷1位元組傳輸的起始與結束;CLK上升沿觸發中斷,資料在CLK上升沿取樣
```c
/*
***********************************************************************************************
* 函 數: 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外部中斷函式:用於使能資料接收、空閒檢測
```c
/*
***********************************************************************************************
* 函 數: 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資料的接收
```CQL
/*
***********************************************************************************************
* 函 數: 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空閒中斷的發生
```c
/*
***********************************************************************************************
* 函 數: 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);
}
}
```
# 四、功能驗證
經多次傳送韌體資料驗證,MCU均能正常接收資料,並且沒有出現數據錯誤的情況,可用於該專案。
# 五、拓展
該方法也可以用於實現模擬UART功能,僅提供思路,未經過驗證(以`115200,8-N-1`為例)。
- 將UART的接收引腳配置為上拉模式、下降沿觸發中斷(判斷起始位)。
- 中斷第一次觸發後表明收到起始位,收到起始位後開啟定時器中斷(如果波特率為115200,那麼中斷週期應小於8.6us,如果每Bit資料需要多次取樣,則需要更短的中斷週期)。
- 每中斷一次判斷一次資料引腳,資料收滿10Bit後判斷是否為停止位,若資料接收正確則存入接收FIFO。
- 在收到起始位後開始計時,1ms內沒有再次收到起始位則認為收到一幀資料,產生軟空閒中斷,然後關閉定時器。
使用該UART方式優勢在於比SPI方式使用更少的引腳,只需要1個IO即可完成通訊。缺點在於如果要求通訊速率高或需要多次取樣,那麼產生波特率的定時器中斷頻率高,如果被其他更高優先順序中斷打斷可能造成波特率不準,資料錯誤。還有就是UART方式在資料通訊速率上沒有SPI有優勢。不到萬不得已不建議使用軟體UART