MCU的SPI等通讯接口,需要使用官方的开发库,需要将接口定义在固定复用IO上,如下图所示。

在实际使用中,经常会因为硬件设计错位或者主从时序不一致等原因导致通讯异常,这时可以使用GPIO模拟一个SPI的收发,实现通讯。

需求案例:

MCU作为Master device,通过4线SPI与Slave device通讯;

NSS设计为PA0(output),与slave device的CS连接;

SCK设计为PA1(output) ,与slave device的SCLK连接;

MISO设计为PA2(input) ,与slave device的SDO连接;

MOSI设计为PA3(output) ,与slave device的SDI连接;

Slave器件要求的时序如下图,15位地址和8位数据,最高位为读写控制,写时为低,读时为高。CS拉低时开始读写操作,SCK的下降沿master device开始往slave device写入地址和数据;读取时,master device先写入要读取的寄存器地址,然后slave device在SCK开始为低时向master device回复数据。

注:SPI规定了两个SPI设备这几件通讯必须由主设备来控制从设备,主设备可以通过提供时钟和片选来控制多个从设备。从设备的时钟必须是由主设备的时钟管脚提供,从设备不能产生和控制时钟。

程序实现:

1、初始化GPIO

/*Configure GPIO pin : PA0 PA1 PA3*/

GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_3;

GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;

GPIO_InitStruct.Pull = GPIO_NOPULL;//是否上拉,取决于外围电路

GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;

HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

/*Configure GPIO pins : PA2*/

GPIO_InitStruct.Pin = GPIO_PIN_2;

GPIO_InitStruct.Mode = GPIO_MODE_INPUT;

GPIO_InitStruct.Pull = GPIO_NOPULL; //是否上拉,取决于外围电路

HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

2、设计SPI延时,使用代码执行时间做延时;

void SPI_Delay(void)

{

uint16_t cnt = 5;

while(cnt--);

}

3、设计SPI Master写函数

void SPI_Write(uint16_t addr,uint8_t value)

{

uint32_t TxData;

uint8_t w_cnt;

//写数据拼接,W最高位为低

TxData= (0x000000|(addr<<8)|value);

//<使能>PA0,低有效

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);

SPI_Delay();

for(w_cnt=0;w_cnt<24;w_cnt++)

{

//<时钟>PA1,拉低

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);

//判断待写入数据是0还是1

if(TxData &0x800000)

{

//待写入数据为1,将MOSI拉高

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET);

}

else

{

//待写入数据为0,将MOSI拉低

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET);

}

SPI_Delay();

//待写入数据移至下一位

TxData <<= 1;

//<时钟>PA1,拉高

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1,GPIO_PIN_SET);

SPI_Delay();

}

//写完成,<使能>PA0拉高

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);

SPI_Delay();

}

4、设计SPI Master写函数

uint8_t SPI_Read(uint16_t addr)

{

//读最高位为1

addr =0x8000| addr;

uint8_t w_cnt,r_cnt;

uint8_t value,read_data;

//<使能>PA0,低有效

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);

//判断待写入数据是0还是1

for(w_cnt=0;w_cnt<16;w_cnt++)

{

//<时钟>PA1,拉低

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);

//判断待写入数据是0还是1

if(addr &0x8000)

{

//待写入数据为1,将MOSI拉高

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET);

}

else

{

//待写入数据为0,将MOSI拉低

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET);

}

SPI_Delay();

//待写入数据移至下一位

addr <<= 1;

//<时钟>PA1,拉高

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1,GPIO_PIN_SET);

SPI_Delay();

}

for(r_cnt=8;r_cnt>0;r_cnt--)

{

//<时钟>PA1,拉低

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);

SPI_Delay();

//从SDO中读出当前bit数据

read_data=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_2);

//将读出的数据移位与之前读出的数据进行拼接

read_data=read_data<<(r_cnt-1);

value=value|read_data;

//<时钟>PA1,拉高

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET);

SPI_Delay();

}

//读完成,<使能>PA0拉高

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);

SPI_Delay();

//读完成,返回读值

return value;

}

5、三线SPI或者其他的时序要求都可以按照对应的要求进行模拟;速率跟主频及延时相关,需要根据实际应用测试;实际应用时可以将CS/SCK/SDI对应的GPIO控制#define一下,方便阅读。