电子说
最近遇到了如下需求:
MCU作为主控芯片通过SPI与蓝牙芯片连接。
蓝牙芯片会时不时向MCU发送大量定长的数据包。
这种情况下,如果MCU的SPI接口采用主模式,通过查询的方式询问蓝牙芯片是否有数据要发送,就会非常占用资源,并且遇到突发大量数据也可能会来不及处理。
比较好的一种方法是,MCU采用从模式的SPI。蓝牙芯片无脑向MCU吐数据。如果主控MCU的SPI时钟最大频率大于蓝牙芯片的SPI最大频率,此方法可以跑到蓝牙芯片SPI的传输极限。
大体思路:
初始化SPI为从模式,并为SPI_CS引脚注册中断函数,下降沿触发
在中断函数中,启动SPI的接收。
SPI接收完成后,做其他处理,比如解析,转发等
代码实现
下面是如何实现,平台采用了STM32F1系列芯片,启用SPI DMA传输,RT-Thread 4.0.2,SPI约定为Slave,MODE3,MSB,CS active low。一次传输长度为package_length。
使用内存池+邮箱的缓冲方式,当然也可以使用消息队列,根据自己的喜好。此处对中断做了底半处理。
初始化
SPI初始化
static struct rt_spi_device spi_device; //
static struct stm32_hw_spi_cs spi_cs; //中断引脚
static int spi_init(void)
{
rt_pin_mode(CS_PIN, PIN_MODE_INPUT_PULLUP);
/ attach the device to spi bus /
spi_device = (struct rt_spi_device *)rt_malloc(sizeof(struct rt_spi_device));
RT_ASSERT(uwb_device != RT_NULL);
spi_cs = (struct stm32_hw_spi_cs )rt_malloc(sizeof(struct stm32_hw_spi_cs));
RT_ASSERT(uwb_spi_cs != RT_NULL);
spi_cs->GPIOx = GPIOA;
spi_cs->GPIO_Pin = 4;
result = rt_spi_bus_attach_device(uwb_device, "spi10", "spi1", (void )uwb_spi_cs);
if (result != RT_EOK)
{
LOG_E("%s attach to %s faild, %dn", "spi10", "spi1", result);
return result;
}
LOG_D("%s attach to %s done", UWB_SPI_NAME, UWB_SPI_BUS);
/ get SPI bus /
spi_device->bus->owner = spi_device;
/ configure SPI device /
{
struct rt_spi_configuration cfg;
cfg.data_width = 8;
cfg.mode = RT_SPI_SLAVE | RT_SPI_MODE_3 | RT_SPI_MSB;
cfg.max_hz = 8 * 1000 * 1000;
rt_spi_configure(spi_device, &cfg);
}
if (rt_device_open((rt_device_t)spi_device, RT_DEVICE_FLAG_DMA_RX) != RT_EOK)
{
LOG_E("open UWB SPI device %s error.", "spi10");
return -RT_ERROR;
}
return RT_OK;
}
!!!注意,这里需要修改一下rt_spi_configure函数中的宏定义RT_SPI_MODE_MASK,从
(RT_SPI_CPHA | RT_SPI_CPOL | RT_SPI_MSB)
改为
(RT_SPI_SLAVE | RT_SPI_CPHA | RT_SPI_CPOL | RT_SPI_MSB)
否则无法将SPI接口配置为从模式,调用rt_spi_revice_message会崩溃。
初始化信号量,邮箱和内存池
/* create RX semaphore /
spi_start_sem = rt_sem_create("spi1_start", 0, RT_IPC_FLAG_FIFO);
/ create RX mp /
spi_mp = rt_mp_create("spi_mp", SPI_MB_LEN, RT_ALIGN(sizeof(rt_uint8_t), sizeof(intptr_t)) * package_length);
/ create RX mailbox /
rt_mb_init(&spi_mb, "UWB_mb", &spi_mb_pool[0], sizeof(spi_mb_pool) / 4, RT_IPC_FLAG_FIFO);
为CS引脚绑定中断函数
rt_pin_attach_irq(4, PIN_IRQ_MODE_FALLING, (void ( )(void *))spi_cs_isr, RT_NULL);
此时,可以先不使能中断,可以等待系统所有初始工作完成后,由其他线程使能中断,以启动SPI接收。
到此,初始化的工作就完成了。接下来,要进行数据的接收工作,为此我们需要创建一些其他的函数。
数据的接收
首先我们需要创建一个中断函数,这个中断函数通过CS引脚的下降沿触发,用来通知系统开始接收数据。
static void spi_cs_isr(void)
{
/* enter interrupt /
rt_interrupt_enter();
rt_sem_release(spi_start_sem);
/ leave interrupt */
rt_interrupt_leave();
}
然后,我们还需要:一个线程用来启动DMA接收;一个中断函数用于通知系统DMA接收已经完成。
使用DMA方式的好处是,全部的SPI接收过程可以交给DMA。这种非阻塞的方式使得,系统在这个时候可以搞搞其他事情(相当于双线程???)。在SPI大量传输数据时尤其好用。
static uint8_t *rx_buff = RT_NULL;
static void spi_start_thread_entry(void *parameter)
{
struct rt_spi_message spi_msg;
spi_msg.send_buf = RT_NULL;
spi_msg.length = uwb_package_length;
spi_msg.cs_take = 0;
spi_msg.cs_take = 0;
spi_msg.next = RT_NULL;
while (1)
{
if (rt_sem_take(spi_start_sem, RT_WAITING_FOREVER) == RT_EOK)
{
rx_buff = rt_mp_alloc(spi_mp, RT_WAITING_NO);
if (rx_buff != RT_NULL)
{
rt_spi_revice_message(spi_device, &spi_msg);
}
}
}
}
这里使用了RT_WAITING_NO的方式来申请空间,如果没有申请到(缓冲区已满),就抛弃这条数据。
使用rt_spi_revice_message函数来启动DMA接收,并且约定了接收的长度固定为package_length。
DMA接收完成函数
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1)
{
if (rx_buff != RT_NULL)
{
rt_mb_send(&spi_mb, (rt_uint32_t)rx_buff); //发送邮件
}
}
}
最后,还需要一个线程用于处理接收到的数据
static void spi_dma_clp_thread_entry(void *parameter)
{
rt_uint8_t *net_tx_buff = RT_NULL;
while (1)
{
if (rt_mb_recv(&uwb_mb, (rt_ubase_t *)&net_tx_buff, RT_WAITING_FOREVER) == RT_EOK)
{
//TODO
//data process
}
rt_mp_free(net_tx_buff);
net_tx_buff = RT_NULL;
}
}
到此,基于RT-Thread的SPI从接收就基本完成了。这些只是一个大体的思路,也可以使用自己喜欢的方式,或者添加其他的功能。如果大家有更好的思路,欢迎分享出来。
全部0条评论
快来发表一下你的评论吧 !