先说题外话,只用于笔记,可以跳过。问题:系统断电复位,方向轴ICM-26090芯片的数据往往出现无法读取传感器数据或读取传感器数据的异常情况。乱试找到了解决办法:初始化拉高片选信号线时,HAL_Delay(500); 这个解决方案是片面的,不应该是本质的。我猜以下操作从本质上解决了问题,即通过芯片的电源管理,首先加速度计和陀螺仪disable,然后开始配置,然后enable。(传感器芯片没有这样说,有点坑)
SPI_E_WRITE(PWR_MGMT_2_ADDR_W,0x3F); // Disable 3-axis accelerometer and 3-axis gyro before configuration HAL_Delay(100); // This Delay is important SPI_E_WRITE(CONFIG_ADDR_W,0x00); SPI_E_WRITE(GYRO_CONFIG_ADDR_W,0x00); SPI_E_WRITE(ACCEL_CONFIG_ADDR_W,0x00); SPI_E_WRITE(ACCEL_CONFIG_2_ADDR_W,0x37); SPI_E_WRITE(LP_MODE_CONFIG_ADDR_W,0x70); SPI_E_WRITE(FIFO_EN_ADDR_W,0xF8); SPI_E_WRITE(ODR_DELAY_EN_ADDR_W,0x00); SPI_E_WRITE(INT_ENABLE_ADDR_W,0x00); SPI_E_WRITE(FIFO_WM_TH_ADDR_W,0x00); SPI_E_WRITE(PWR_MGMT_1_ADDR_W,0x00); SPI_E_WRITE(PWR_MGMT_2_ADDR_W,0x00); // Enable 3-axis accelerometer and 3-axis gyro after configuration
上面提到了如何正常读取芯片数据,下面步入正题,STM32芯片的SPI优化通信效率。
1.提高通信时钟的频率
目前最有效的方法是改进SPI Baud Rate(通信时钟频率),但提升也是极限,和STM32通信传感器芯片(从器件)本身就有时钟频率的极限,不能超过从器件承受的时钟极限。
预分频器原本是16,改为4后,波特率(时钟)提高到原来的4倍,相同的代码从189运行us,降到了126.6us,运行效率提高了30%。
单字节访问改为多字节访问。
读取传感器芯片寄存器的数据,需要发送包含(最高位1 一个字节数据为寄存器地址),然后接收一个字节数据。 如果尝试连续发送要读的地址,连续接收呢?
时间优化(45)us),小偷的效果很明显,但是(收到的所有数据都是0)并卵。虽然没用,但我们发现的现象激励了我们。单字节的通信效率远低于多字节。这是什么意思? 说明HAL库的SPI通信封装有很多冗余的东西。 否则,我的单字节通信效率不应该差那么多倍。
因为SPI要操作的芯片是,它收到指令 读指令 地址,中间需要一个间隔时间(芯片本身的操作),然后把你想读的地址的寄存器数据发给你。 如果你连续发送许多命令,芯片无法反应。 导致这种做法失败(这是我当时的理解,请继续阅读以下内容)。
优化尝试3、SPI DMA,并不是YYDS
一直想看能不能用。DMA去操作SPI。 毕竟在之前学习串口通信的过程中,DMA真是太香了。网上相关信息少,现在想想,信息少也是有道理的。为此,我还咨询了一些有经验的前辈,发现原来STM32和DSP同样,也有example这种东西只能边学边用。参考:STM32 HAL获取和查阅库手册的方法,查看官方例程_红尘博客怎么了?-CSDN博客_hal库函数中文手册。我试着发送HAL_SPI_Transmit_DMA发送, 接收还是用HAL_SPI_Receive接收运行时间飙升至207(但207)us 已经大于 200us(5k), 如何定时器仍在正常工作? ),我以为我在朝着正确的方向前进,但我在反向优化?
HAL_SPI_Transmit_DMA(&hspi1,&addr,1); //HAL_SPI_Transmit(&hspi1,&addr,1,3); HAL_SPI_Receive(&hspi1,Rxdata,1,3);
接下来试试DMA接收试。
Errornum = HAL_SPI_Transmit_DMA(&hspi1,&Tx,1); while((hspi1.Instance->SR & SPI_SR_RXNE)!=RESET); while((hspi1.Instance->SR & SPI_SR_BSY)!=RESET); Errornum = HAL_SPI_Receive_DMA(&hspi1,&Rx,1);
传感器芯片SPI通信的DMA解决方案真的很难做,完成后才发现效率真的很感人! 207us 变到 310us,我正在朝着反向优化的方向,渐行渐远。
4.使用优化尝试HAL_SPI_TransmitReceive优化
【流水账警告】
虽然是流水账,但每一步都激发了优化。我以前一直在尝试。 HAL_SPI_TransmitReceive函数失败,尝试HAL_SPI_TransmitReceive_DMA也没有成功。收到了数据,但对。都是00。时间应该不对齐。
但从 HAL_SPI_Receive 启发函数源代码
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout) { #if (USE_SPI_CRC != 0U) __IO uint32_t tmpreg = 0U; #endif /* USE_SPI_CRC */ uint32_t tickstart; HAL_StatusTypeDef errorcode = HAL_OK; if ((hspi->Init.Mode == SPI_MODE_MASTER) && (hspi->Init.Direction == SPI_DIRECTION_2LINES)) { hspi->State = HAL_SPI_STATE_BUSY_RX; /* Call transmit-receive function to send Dummy data on Tx line and generate clock on CLK line */ return HAL_SPI_TransmitReceive(hspi, pData, pData, Size, Timeout); } ... // 也就是说 HAL_SPI_Receive本质上,调用是函数HAL_SPI_TransmitReceive(hspi, pData, pData, Size, Timeout);
我后来才明白下面这段话: HAL_SPI_TransmitReceive它本身没有问题,但我对它的理解有问题。我以为我可以用它来传感器芯片(读命令) 寄存器地址)字节,收到地址寄存器数据字节。 但后来发现,与传感器芯片通信的过程是发送一个(阅读命令 寄存器地址)字节,接收0内容x00字节, 发送任何字节(通常是0x00)给传感器,接收真正想读的寄存器字节。 简单来说就是发两个字节,第一个有用,第二个没用,第一个没用,第二个有用。
从HAL_SPI_Receive 可以推断函数源码HAL_SPI_TransmitReceive 替换HAL_SPI_Receive,效率一定更高。我真的是对的DMA还没有放弃,用HAL_SPI_TransmitReceive_DMA 替换 HAL_SPI_Receive_DMA 试试
spi_cs_A_Enable; Tx = 0xF5; Rx = 0x00; HAL_SPI_Transmit_DMA(&hspi1,(uint8_t*)&Tx,1); HAL_SPI_TransmitReceive_DMA(&hspi1,(uint8_t*)&Tx,(uint8_t*)&Rx,1); //HAL_SPI_TransmitReceive(&hspi1,(uint8_t*)&Tx,(uint8_t*)&Rx,1,3); spi_cs_A_Disable;
310的效率也很感人us 变到 280us。
相信大家都看到了前面的结果, SPI用DMA的效率 没有 直接使用高轮询。
但是我们刚才发现了直接的现象。 HAL_SPI_TransmitReceive 当成HAL_SPI_Receive 可以提高效率。
因此,我们进一步优化了代码
void SPI_A_READ(unsigned char addr, unsigned char * Rxdata){ spi_cs_A_Enable; HAL_SPI_Transmit(&hspi1,&addr,1,3); HAL_SPI_TransmitReceive(&hspi1,&addr,Rxdata,1,3); spi_cs_A_Disable; }
从原来的116.9us (删除一点冗余代码,删除126.6us到了116.9us) 降到了 108us, 虽然没有什么提升,但是聊胜于无。
进一步发散思路,用两个TransmitReceive可不可以呢? 答案是肯定的,不过效率是更低了,125.8us
void SPI_A_READ(unsigned char addr, unsigned char * Rxdata){
spi_cs_A_Enable;
HAL_SPI_TransmitReceive(&hspi1,&addr,Rxdata,1,3);
HAL_SPI_TransmitReceive(&hspi1,&addr,Rxdata,1,3);
spi_cs_A_Disable;
}
▲ 从下面的尝试开始, 优化思路终于步入正向优化的轨迹了。
那一次读取数据,可能用两个来实现,那我可不可以,就用一次,而操作两个数据呢
void SPI_A_READ(unsigned char addr, unsigned char * Rxdata){
unsigned char addr2[2] = {addr, addr};
unsigned char Rxdata2[2] = {0x00,0x00};
spi_cs_A_Enable;
HAL_SPI_TransmitReceive(&hspi1,addr2,Rxdata2,2,3);
*Rxdata = Rxdata2[1];
spi_cs_A_Disable;
}
答案是肯定的, 目前的时间变成了102.65us,虽然效率提升不大,但方向是对的啦。
试试DMA呢?(我也不知道我当时怎么对DMA还不死心)
void SPI_A_READ(unsigned char addr, unsigned char * Rxdata){
unsigned char addr2[2] = {addr, addr};
unsigned char Rxdata2[2] = {0x00,0x00};
spi_cs_A_Enable;
HAL_SPI_TransmitReceive_DMA(&hspi1,addr2,Rxdata2,2);
*Rxdata = Rxdata2[1];
spi_cs_A_Disable;
}
实验证明, 这样做,不能读到芯片的数据。 但时间却是降到了大概1半,168.9us。 如果是其它的SPI通信对象,也许DMA效率未必低,特别是一次性收发大量数据的时候。不过针对我用的这一款传感器芯片的SPI通信,使用DMA的方式,在我这儿我已经彻底死心了。
上面做的操作都是一个字节的读取操作。 有没有可能我连续读两个字节呢? 如果能连续读两个字节,效率有所优化的话,那么把所有需要读取的数据连续读出,效率会更优。 抱着这样的思路,让我们再试一把。
Ten years later
试完了, 连续两个字节是不行滴,拉闸了呀,兄弟。
Ten years later
不放弃,继续优化!
对于传感器芯片个人的理解是,你发一个读命令+地址的字节过去(同时收到一个没有用的数据),芯片收到你的读命令+地址,它把该地址下的寄存器的值复制到移位寄存器上。 你这时候,你随便再发一个没用的数据过去, 然后你将收到你想要读取的芯片上面对应地址的数据。你的目标是读芯片上某个地址的某个数据, 但本质上,这个流程中,你发送了两个字节,收到了两个字节。我的本意是, 我能不能连续的去读芯片上两个地址上的数据呢?我需要发送4个字节,收到4个字节。刚刚测试过了,这样子操作不行,第一个地址的数据读取是正确的,第二个就不对了。 尝试下不同的地址、更低的时钟。都不行。
不过,在我的不懈努力之下,发现,在每两个字节发送中间,添加一个片选的重启,是可以的,黄天不负有心人
/* Transmit and Receive data in 8 Bit mode */
else
{
if ((hspi->Init.Mode == SPI_MODE_SLAVE) || (initial_TxXferCount == 0x01U))
{
*((__IO uint8_t *)&hspi->Instance->DR) = (*hspi->pTxBuffPtr);
hspi->pTxBuffPtr += sizeof(uint8_t);
hspi->TxXferCount--;
}
while ((hspi->TxXferCount > 0U) || (hspi->RxXferCount > 0U))
{
/* Check TXE flag */
if ((__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_TXE)) && (hspi->TxXferCount > 0U) && (txallowed == 1U))
{
if(hspi->TxXferCount==2){ // heqiunong add code
spi_cs_A_Disable; // heqiunong add code
spi_cs_A_Enable; // heqiunong add code
}
把HAL_SPI_TransmitReceive的定义打开,然后添加了如下代码(heqiunong add code),其中
#define spi_cs_A_Enable HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2, GPIO_PIN_RESET)
#define spi_cs_A_Disable HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2, GPIO_PIN_SET)
那让我们看看效果如何。
哈哈哈, 运行时间再次从102.65us 降到了 86.7us。 相比于在开始的189us,现在已经走了很远了。
不过,我还是不满意, 因为它还有下降的空间。
优化尝试5、再次尝试连续读取方式
我们整个程序是读取芯片上的12个字节,6个数据。上一步已尝试连续读取两个字节是可行的,那下一步,就是连续读取12个字节(传感器芯片所有要读的数据是12字节)。
关键修改的点是
if(hspi->TxXferCount==2 || hspi->TxXferCount==4 || hspi->TxXferCount==6 || hspi->TxXferCount==8 || hspi->TxXferCount==10){ // heqiunong add code
spi_cs_A_Disable; // heqiunong add code
spi_cs_A_Enable; // heqiunong add code
}
// 每发送两个字节(相当于读1个字节),就重启一次片选
void SPI_A_READ_12BYTE(unsigned char * addrArray, unsigned char * Rxdata){
unsigned char Rxtemp[24] = {0x00};
spi_cs_A_Enable;
HAL_SPI_TransmitReceive_HQN_A(&hspi1,addrArray,Rxtemp,24,100);
// 这个是自己改的HAL_SPI_TransmitReceive,里面加了那个片选重启操作
Rxdata[0] = Rxtemp[1];
Rxdata[1] = Rxtemp[3];
Rxdata[2] = Rxtemp[5];
Rxdata[3] = Rxtemp[7];
Rxdata[4] = Rxtemp[9];
Rxdata[5] = Rxtemp[11];
Rxdata[6] = Rxtemp[13];
Rxdata[7] = Rxtemp[15];
Rxdata[8] = Rxtemp[17];
Rxdata[9] = Rxtemp[19];
Rxdata[10] = Rxtemp[21];
Rxdata[11] = Rxtemp[23];
spi_cs_A_Disable;
}
unsigned char addrarray[24] = {\
GYRO_XOUT_H_ADDR_R, 0x00, GYRO_XOUT_L_ADDR_R, 0x00, \
GYRO_YOUT_H_ADDR_R, 0x00, GYRO_YOUT_L_ADDR_R, 0x00, \
GYRO_ZOUT_H_ADDR_R, 0x00, GYRO_ZOUT_L_ADDR_R, 0x00, \
ACCEL_XOUT_H_ADDR_R, 0x00, ACCEL_XOUT_L_ADDR_R, 0x00,\
ACCEL_YOUT_H_ADDR_R, 0x00, ACCEL_YOUT_L_ADDR_R, 0x00,\
ACCEL_ZOUT_H_ADDR_R, 0x00, ACCEL_ZOUT_L_ADDR_R, 0x00 };
unsigned char data[12]={0x00};
SPI_A_READ_12BYTE(addrarray, data);
fAGyroX = ((float)((int16_t)(data[0] << 8) + data[1])) / 131; // °/s
fAGyroY = ((float)((int16_t)(data[2] << 8) + data[3])) / 131; // °/s
fAGyroZ = ((float)((int16_t)(data[4] << 8) + data[5])) / 131; // °/s
fAaccX = ((float)((int16_t)(data[6] << 8) + data[7])) / 2048; // g
fAaccY = ((float)((int16_t)(data[8] << 8) + data[9])) / 2048; // g
fAaccZ = ((float)((int16_t)(data[10] << 8) + data[11])) / 2048; // g
效率确实是提升了, 又从86.7us 降到了 62.1us, 但是Z轴陀螺的数据没有读对,读到的全是0,其它的数据都是正确的。 这着实有些许的恶心了。 检测代码,发现是木有问题的。
Ten years later!
终于发现问题了,我虽然是连续12个字节,但是我是发的24个字节。每两个字节就重启片选的操作,我只操作了一半,还有一半没有操作。这个停顿应该有11下。 修改之后,就没有问题了。不过运行时间从62.1us提升到了75us。 总结来讲,86.7us -> 75us 11.7us的效率提升还是可以的。
代码优化到现在,已经从189us优化到75us了, 已经不错啦,效率提升60%。
if( hspi->TxXferCount==2 \
|| hspi->TxXferCount==4 \
|| hspi->TxXferCount==6 \
|| hspi->TxXferCount==8 \
|| hspi->TxXferCount==10 \
|| hspi->TxXferCount==12 \
|| hspi->TxXferCount==14 \
|| hspi->TxXferCount==16 \
|| hspi->TxXferCount==18 \
|| hspi->TxXferCount==20 \
|| hspi->TxXferCount==22 \
){ // heqiunong add code
spi_cs_A_Disable; // heqiunong add code
spi_cs_A_Enable; // heqiunong add code
}
优化尝试6、大胆修改HAL函数,删除冗余代码
毫无疑问,代码肯定还能再优化的,但目前不知道从何下手。 说不知道从何下手,一下手,又下降了10us
因为我们是8Bit的SPI通信,所以里面关于16bit的判断就可以去掉了。 对于超时的错误判断我也去掉了。
HAL_StatusTypeDef HAL_SPI_TransmitReceive_HQN_A(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size,
uint32_t Timeout)
{
uint16_t initial_TxXferCount;
uint32_t tmp_mode;
HAL_SPI_StateTypeDef tmp_state;
uint32_t tickstart;
#if (USE_SPI_CRC != 0U)
__IO uint32_t tmpreg = 0U;
#endif /* USE_SPI_CRC */
/* Variable used to alternate Rx and Tx during transfer */
uint32_t txallowed = 1U;
HAL_StatusTypeDef errorcode = HAL_OK;
/* Check Direction parameter */
assert_param(IS_SPI_DIRECTION_2LINES(hspi->Init.Direction));
/* Process Locked */
__HAL_LOCK(hspi);
/* Init tickstart for timeout management*/
tickstart = HAL_GetTick();
/* Init temporary variables */
tmp_state = hspi->State;
tmp_mode = hspi->Init.Mode;
initial_TxXferCount = Size;
if (!((tmp_state == HAL_SPI_STATE_READY) || \
((tmp_mode == SPI_MODE_MASTER) && (hspi->Init.Direction == SPI_DIRECTION_2LINES) && (tmp_state == HAL_SPI_STATE_BUSY_RX))))
{
errorcode = HAL_BUSY;
goto error;
}
if ((pTxData == NULL) || (pRxData == NULL) || (Size == 0U))
{
errorcode = HAL_ERROR;
goto error;
}
/* Don't overwrite in case of HAL_SPI_STATE_BUSY_RX */
if (hspi->State != HAL_SPI_STATE_BUSY_RX)
{
hspi->State = HAL_SPI_STATE_BUSY_TX_RX;
}
/* Set the transaction information */
hspi->ErrorCode = HAL_SPI_ERROR_NONE;
hspi->pRxBuffPtr = (uint8_t *)pRxData;
hspi->RxXferCount = Size;
hspi->RxXferSize = Size;
hspi->pTxBuffPtr = (uint8_t *)pTxData;
hspi->TxXferCount = Size;
hspi->TxXferSize = Size;
/*Init field not used in handle to zero */
hspi->RxISR = NULL;
hspi->TxISR = NULL;
#if (USE_SPI_CRC != 0U)
/* Reset CRC Calculation */
if (hspi->Init.CRCCalculation == SPI_CRCCALCULATION_ENABLE)
{
SPI_RESET_CRC(hspi);
}
#endif /* USE_SPI_CRC */
/* Check if the SPI is already enabled */
if ((hspi->Instance->CR1 & SPI_CR1_SPE) != SPI_CR1_SPE)
{
/* Enable SPI peripheral */
__HAL_SPI_ENABLE(hspi);
}
// 这里删除了一部分冗余的代码
while ((hspi->TxXferCount > 0U) || (hspi->RxXferCount > 0U))
{
if( hspi->TxXferCount==2 \
|| hspi->TxXferCount==4 \
|| hspi->TxXferCount==6 \
|| hspi->TxXferCount==8 \
|| hspi->TxXferCount==10 \
|| hspi->TxXferCount==12 \
|| hspi->TxXferCount==14 \
|| hspi->TxXferCount==16 \
|| hspi->TxXferCount==18 \
|| hspi->TxXferCount==20 \
|| hspi->TxXferCount==22 \
){ // heqiunong add code
spi_cs_A_Disable; // heqiunong add code
spi_cs_A_Enable; // heqiunong add code
}
/* Check TXE flag */
if ((__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_TXE)) && (hspi->TxXferCount > 0U) && (txallowed == 1U))
{
*(__IO uint8_t *)&hspi->Instance->DR = (*hspi->pTxBuffPtr);
hspi->pTxBuffPtr++;
hspi->TxXferCount--;
/* Next Data is a reception (Rx). Tx not allowed */
txallowed = 0U;
#if (USE_SPI_CRC != 0U)
/* Enable CRC Transmission */
if ((hspi->TxXferCount == 0U) && (hspi->Init.CRCCalculation == SPI_CRCCALCULATION_ENABLE))
{
SET_BIT(hspi->Instance->CR1, SPI_CR1_CRCNEXT);
}
#endif /* USE_SPI_CRC */
}
/* Wait until RXNE flag is reset */
if ((__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_RXNE)) && (hspi->RxXferCount > 0U))
{
(*(uint8_t *)hspi->pRxBuffPtr) = hspi->Instance->DR;
hspi->pRxBuffPtr++;
hspi->RxXferCount--;
/* Next Data is a Transmission (Tx). Tx is allowed */
txallowed = 1U;
}
// 这里删除了一部分冗余的代码, 就是超时的
}
/* Clear overrun flag in 2 Lines communication mode because received is not read */
if (hspi->Init.Direction == SPI_DIRECTION_2LINES)
{
__HAL_SPI_CLEAR_OVRFLAG(hspi);
}
error :
hspi->State = HAL_SPI_STATE_READY;
__HAL_UNLOCK(hspi);
return errorcode;
}
至此, 189us 已优化到 65.2。 优化率:65.6%
我知道SPI通信还能优化, 比如全部换成寄存器级操作。 但,我的优化,暂时就到此为止吧,如果各位大佬有好的思路,欢迎您为我提供。