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一下,方便阅读。