专栏名称: 21ic电子网
即时传播最新电子科技信息,汇聚业界精英精彩视点。
目录
相关文章推荐
半导体行业联盟  ·  第十五届纳博会具身感知专区:探索人形机器人的 ... ·  5 天前  
半导体行业联盟  ·  上海临港,又一芯片基金成立! ·  5 天前  
半导体行业联盟  ·  翠展微完成数亿B+轮融资!翠展微创始人彭昊, ... ·  4 天前  
半导体行业联盟  ·  苏州芯片公司爆雷! ·  6 天前  
半导体行业联盟  ·  北京:围绕集成电路等 引育一批重大产业项目 ·  5 天前  
51好读  ›  专栏  ›  21ic电子网

折腾的项目:终于换对MCU了,搞一个SPI的DMA通信

21ic电子网  · 公众号  · 半导体  · 2021-07-12 16:14

正文

出品 21ic论坛 呐咯密密
网站:bbs.21ic.com

从入职新公司后,一个项目我折腾了两年,都是泪,每次以为开发完了,就会出事故,这次保守点,用新塘一定没问题,一定会成功量产,一定不要重新来过。

这个项目经历百般周折,从国外型号最后到新塘,直接给我整吐了,有些是真的难用啊,也是老大难。最后到今天的主角新塘。

刚开始选用了新塘的NUC029,下载资料,搭建环境,串口测试,SPI测试,然后突然发现这玩意没有DMA,白瞎了这么长时间啊,样板都做好了,结果出幺蛾子。

然后在换M031,这次深思熟虑,认为不会有问题了才开搞。作为一个半成熟的程序猿,肯定不能helloword起手了,直接上UART吧。

环境搭建这里就跳过了,没啥用,我还是用KEIL 5 开发,自行下载个PACK包安装就好了。点此前往新塘官网。搜索自己的MCU型号,打开页面,在资源中有文档和软件。



在文档中下载数据手册等文档,在软件中下载例程和工具,软件中最实用的是以下几个软件:



从上到下依次是:官方例程库,Nu_link驱动,外设引脚配置软件,时钟配置软件。

外设引脚配置软件用于快速配置引脚以及复用,该软件只能配置引脚及其功能,不能配置外设等功能呢,例如串口的相关配置,这些事实现不了的。

时钟配置软件仅用于配置系统时钟以及各外设时钟。这两个软件支持导出.c代码。可复制粘贴到自己的工程。

这两个软件都是非常简单的,这里就不赘述了。

但是有一个时钟配置软件有BUG,以我用的M031SE3AE为例,外部时钟最大可使用32M,但是软件中最大只支持24M,希望官方可以修复。




在M031中,区别于我之前用过的其他MCU,在进入中断函数之后,只要读取串口接收寄存器UART_DAT中的值,便可自动清除中断标志,并不需要去操作其他寄存器。非常好用。


DMA的启动和其他的MCU类似,[size=14.6667px]需重新配置传输个数,[size=14.6667px]RAM[size=14.6667px]地址等,再调用一次初始化函数就行。然后利用[size=14.6667px]while (PDMA->DSCT[UART_TX_DMA_CH].CTL & PDMA_DSCT_CTL_TXCNT_Msk) ;判断数据是否发送完成,实际上就是等待传输个数计数器为0。


这里插一句PB1的作用,PBI就是gpio PB1口。初始化就一句话GPIO_SetMode(PB, BIT1, GPIO_MODE_OUTPUT);这里方便示波器观察时间。在使用库函数初始化PWM时,因为每一次的启动都要调用该函数,库函数的操作很费时间,在触发串口接收中断后将PB1拉低,发送完拉高,在示波器观察到从触发接收中断到第一个串口数据发送出去,也就是DMA启动完成,大约耗时8us,效率低下。于是我将DMA初始化改用寄存器的方式,时间缩小到2.8us,好用!

自从换了新塘的M031,现在对这个MCU真的喜欢,写代码巨舒适,简单快速就能上手。

今天来搞一个SPI的DMA通信,实用的外设,咱一个都不能少。

M031 SPI概述

SPI接口是全双工同步串行数据通讯接口,可做为主机或从机,用 4 线双向通讯。M031 包含 1 组 SPI控制器,用于对从外围设备接收到的数据执行串并转换,并对发送到外围设备的数据进行并串转换。 每个SPI控制器可以配置为主设备或从设备,并支持PDMA功能存取数据缓冲区。 每个SPI控制器还支持I2S模式来连接外部音频编解码器。

特征:

– 1组 SPI 控制器
– 支持主机模式和从机模式
– 传输位长可为 8 ~ 32位
– 提供独立的4级32位(或8级16位)收发FIFO缓存,实际数据位长由SPI的设置决定
– 支持高位优先(MSB)或低位优先(LSB)时序
– 支持字节重排功能
– 支持字节或字暂停模式
– 总线时钟主机模式最高可到24 MHz ,从机模式最高可到16 MHz (当芯片工作在 VDD =1.8~3.6V)
– 支持一数据通道半双工传输
– 支持只接收模式
– 支持 PDMA 传输

框图


代码片

时钟以及GPIO初始化

void SYS_Init(void){
/*---------------------------------------------------------------------------------------------------------*/ /* Init System Clock */ /*---------------------------------------------------------------------------------------------------------*/ /* Unlock protected registers */ SYS_UnlockReg();
/* Enable HIRC clock (Internal RC 48MHz) */ CLK_EnableXtalRC(CLK_PWRCTL_HIRCEN_Msk);
/* Wait for HIRC clock ready */ CLK_WaitClockReady(CLK_STATUS_HIRCSTB_Msk);
/* Select HCLK clock source as HIRC and HCLK source divider as 1 */ CLK_SetHCLK(CLK_CLKSEL0_HCLKSEL_HIRC, CLK_CLKDIV0_HCLK(1));
/* Set both PCLK0 and PCLK1 as HCLK */ CLK->PCLKDIV = CLK_PCLKDIV_APB0DIV_DIV1 | CLK_PCLKDIV_APB1DIV_DIV1;
/* Select IP clock source */ /* Select UART0 clock source is HIRC */ CLK_SetModuleClock(UART0_MODULE, CLK_CLKSEL1_UART0SEL_HIRC, CLK_CLKDIV0_UART0(1)); /* Select UART1 clock source is HIRC */ CLK_SetModuleClock(UART1_MODULE, CLK_CLKSEL1_UART1SEL_HIRC, CLK_CLKDIV0_UART1(1)); /* Select PCLK1 as the clock source of SPI0 */ CLK_SetModuleClock(SPI0_MODULE, CLK_CLKSEL2_SPI0SEL_PCLK1, MODULE_NoMsk);
/* Enable UART0 peripheral clock */ CLK_EnableModuleClock(UART0_MODULE); /* Enable UART1 peripheral clock */ CLK_EnableModuleClock(UART1_MODULE); /* Enable PDMA module clock */ CLK_EnableModuleClock(PDMA_MODULE); /* Enable SPI0 peripheral clock */ CLK_EnableModuleClock(SPI0_MODULE);
/* Update System Core Clock */ /* User can use SystemCoreClockUpdate() to calculate PllClock, SystemCoreClock and CycylesPerUs automatically. */ SystemCoreClockUpdate();
/*---------------------------------------------------------------------------------------------------------*/ /* Init I/O Multi-function */ /*---------------------------------------------------------------------------------------------------------*/
/* Set PB multi-function pins for UART0 RXD=PB.12 and TXD=PB.13 */ SYS->GPB_MFPH = (SYS->GPB_MFPH & ~(SYS_GPB_MFPH_PB12MFP_Msk | SYS_GPB_MFPH_PB13MFP_Msk)) | \ (SYS_GPB_MFPH_PB12MFP_UART0_RXD | SYS_GPB_MFPH_PB13MFP_UART0_TXD);
/* Set PB multi-function pins for UART1 RXD(PB.2) and TXD(PB.3) */ SYS->GPB_MFPL = (SYS->GPB_MFPL & ~(SYS_GPB_MFPL_PB2MFP_Msk | SYS_GPB_MFPL_PB3MFP_Msk)) | \ (SYS_GPB_MFPL_PB2MFP_UART1_RXD | SYS_GPB_MFPL_PB3MFP_UART1_TXD);
/* Setup SPI0 multi-function pins */ /* PA.3 is SPI0_SS, PA.2 is SPI0_CLK, PA.1 is SPI0_MISO, PA.0 is SPI0_MOSI*/ SYS->GPA_MFPL = (SYS->GPA_MFPL & ~(SYS_GPA_MFPL_PA3MFP_Msk | SYS_GPA_MFPL_PA2MFP_Msk | SYS_GPA_MFPL_PA1MFP_Msk | SYS_GPA_MFPL_PA0MFP_Msk)) | (SYS_GPA_MFPL_PA3MFP_SPI0_SS | SYS_GPA_MFPL_PA2MFP_SPI0_CLK | SYS_GPA_MFPL_PA1MFP_SPI0_MISO | SYS_GPA_MFPL_PA0MFP_SPI0_MOSI);
/* Lock protected registers */ SYS_LockReg();}

依据新塘的代码风格,系统的初始化被安排在SYS_Init()中完成所有的系统时钟和各个外设时钟的初始化,在初始化时钟之后初始化外设的引脚,设置各种复用,这里初始化了UART0 和 UART1的外设以及SPI0的外设。因为采用自动的NSS,所以这里将NSS的引脚也复用。
SPI初始化

void SPI_Init(void){    /*---------------------------------------------------------------------------------------------------------*/    /* Init SPI                                                                                                */    /*---------------------------------------------------------------------------------------------------------*/    /* Configure as a master, clock idle low, 32-bit transaction, drive output on falling clock edge and latch input on rising edge. */    /* Set IP clock divider. SPI clock rate = 2 MHz */    SPI_Open(SPI0, SPI_MASTER, SPI_MODE_0, 8, SPI_CLK_FREQ);
/* Enable the automatic hardware slave select function. Select the SS pin and configure as low-active. */ SPI_EnableAutoSS(SPI0, SPI_SS, SPI_SS_ACTIVE_LOW);}

SPI的初始化依旧是两个函数。

第一个SPI_Open(SPI_T *spi,
                  uint32_t u32MasterSlave,
                  uint32_t u32SPIMode,
                  uint32_t u32DataWidth,
                 uint32_t u32BusClock));
内置5个参数,

SPI_T *spi:选定开启的SPI口。我们选择SPI0。

uint32_t u32MasterSlave:选择主模式或者从模式。这里选择主模式。

uint32_t u32SPIMode:选择SPI的模式,这里有4个待选参数,主要是确定SPI的空闲电平,在上升沿还是下降沿读取数据,在第几个沿读数据等,这个在每个MCU都有该配置,只是M031这里做了一个集合。下图为四个可选参数。



下面给一张控制SPI设备的各个寄存器值设定图,


我这里选择SPI_MODE_0,即:将SPI时钟的空闲状态设为低电平,选择数据在SPI总线时钟的下降沿传输,选择数据在SPI总线时钟的上升沿锁存。

uint32_t u32DataWidth:为数据宽度,可在8-32位之间选择,我这里选择最低的8位,这个是比较常见的数据。

uint32_t u32BusClock:设置SPI的时钟,最大24MHz。

第二个函数为SPI_EnableAutoSS(SPI0, SPI_SS, SPI_SS_ACTIVE_LOW);设置硬件使能,自动从机选择功能。如果无需自动从选,可使用SPI_DisableAutoSS(SPI0);进行屏蔽,此时可任选一个通用IO控制从选信号。
SPI读写数据:

/* Write to TX register */        SPI_WRITE_TX(SPI0, 0x50650F);        /* Check SPI0 busy status */        while(SPI_IS_BUSY(SPI0));        i32Err = SPI_READ_RX(SPI0);

SPI_WRITE_TX()进行SPI数据的发送,在等待发送完成后用SPI_READ_RX()进行读数据便可完成一次完整的SPI数据读写。
PDMA设置

void SPI_DMAInit(void){    /* Reset PDMA module */    SYS_ResetModule(PDMA_RST);
/* Enable PDMA channels */ PDMA_Open(PDMA, SPI_OPENED_CH);
/*======================================================================= SPI master PDMA TX channel configuration: ----------------------------------------------------------------------- Word length = 32 bits Transfer Count = DATA_COUNT Source = g_au32MasterToSlaveTestPattern Source Address = Incresing Destination = SPI0->TX Destination Address = Fixed Burst Type = Single Transfer =========================================================================*/ /* Set transfer width (32 bits) and transfer count */ PDMA_SetTransferCnt(PDMA, SPI_MASTER_TX_DMA_CH, PDMA_WIDTH_8, DATA_COUNT); /* Set source/destination address and attributes */ PDMA_SetTransferAddr(PDMA, SPI_MASTER_TX_DMA_CH, (uint32_t)SPI_TX, PDMA_SAR_INC, (uint32_t)&SPI0->TX, PDMA_DAR_FIX); /* Set request source; set basic mode. */ PDMA_SetTransferMode(PDMA, SPI_MASTER_TX_DMA_CH, PDMA_SPI0_TX, FALSE, 0); /* Single request type. SPI only support PDMA single request type. */ PDMA_SetBurstType(PDMA, SPI_MASTER_TX_DMA_CH, PDMA_REQ_SINGLE, 0); /* Disable table interrupt */ PDMA->DSCT[SPI_MASTER_TX_DMA_CH].CTL |= PDMA_DSCT_CTL_TBINTDIS_Msk;
/*======================================================================= SPI master PDMA RX channel configuration: ----------------------------------------------------------------------- Word length = 32 bits Transfer Count = DATA_COUNT Source = SPI0->RX Source Address = Fixed Destination = g_au32MasterRxBuffer Destination Address = Increasing Burst Type = Single Transfer =========================================================================*/ /* Set transfer width (32 bits) and transfer count */ PDMA_SetTransferCnt(PDMA, SPI_MASTER_RX_DMA_CH, PDMA_WIDTH_8, DATA_COUNT); /* Set source/destination address and attributes */ PDMA_SetTransferAddr(PDMA, SPI_MASTER_RX_DMA_CH, (uint32_t)&SPI0->RX, PDMA_SAR_FIX, (uint32_t)SPI_RX, PDMA_DAR_INC); /* Set request source; set basic mode. */ PDMA_SetTransferMode(PDMA, SPI_MASTER_RX_DMA_CH, PDMA_SPI0_RX, FALSE, 0); /* Single request type. SPI only support PDMA single request type. */ PDMA_SetBurstType(PDMA, SPI_MASTER_RX_DMA_CH, PDMA_REQ_SINGLE, 0); /* Disable table interrupt */ PDMA->DSCT[SPI_MASTER_RX_DMA_CH].CTL |= PDMA_DSCT_CTL_TBINTDIS_Msk;
/* Enable SPI master DMA function */ SPI_TRIGGER_TX_PDMA(SPI0); SPI_TRIGGER_RX_PDMA(SPI0);}

这里PDMA设置和UART差不多,不罗里吧嗦的胡扯了,主要就是一个SPI的使能,这里用SPI_TRIGGER_TX_PDMA()和SPI_TRIGGER_RX_PDMA()。这个是UART的SPI没有的。

在这个初始化完成后会进行一次发送和接收,如果不需要请删除                    SPI_TRIGGER_TX_PDMA(SPI0);SPI_TRIGGER_RX_PDMA(SPI0);
如果想要重复PDMA 的发送可做如下处理:

                   /* Re-trigger */                    /* Master PDMA TX channel configuration */                    /* Set transfer width (32 bits) and transfer count */                    PDMA_SetTransferCnt(PDMA, SPI_MASTER_TX_DMA_CH, PDMA_WIDTH_32, DATA_COUNT);                    /* Set request source; set basic mode. */                    PDMA_SetTransferMode(PDMA, SPI_MASTER_TX_DMA_CH, PDMA_SPI0_TX, FALSE, 0);
/* Master PDMA RX channel configuration */ /* Set transfer width (32 bits) and transfer count */ PDMA_SetTransferCnt(PDMA, SPI_MASTER_RX_DMA_CH, PDMA_WIDTH_32, DATA_COUNT); /* Set request source; set basic mode. */ PDMA_SetTransferMode(PDMA, SPI_MASTER_RX_DMA_CH, PDMA_SPI0_RX, FALSE, 0);
/* Enable master's DMA transfer function */ SPI_TRIGGER_TX_PDMA(SPI0); SPI_TRIGGER_RX_PDMA(SPI0);

可以封装为一个函数,每次需要启动DMA就调用一次该函数。但是这里又是老问题,启动DMA的速度慢,示波器测试需要8个微秒左右吧,时间过长。同样我们可以混搭寄存器操作,加快代码执行效率,这个技巧非常实用;

void SPI_DMA_WRITE(void){        PB1 = 0;        /* Master PDMA TX channel configuration */        PDMA->CHCTL |= (1 << SPI_MASTER_TX_DMA_CH); /* Enable PDMA channel */        PDMA->DSCT[SPI_MASTER_TX_DMA_CH].CTL =                (PDMA->DSCT[SPI_MASTER_TX_DMA_CH].CTL & (~(PDMA_DSCT_CTL_TXCNT_Msk | PDMA_DSCT_CTL_OPMODE_Msk))) |                (DATA_COUNT - 1) << PDMA_DSCT_CTL_TXCNT_Pos | /* Transfer count */                1 << PDMA_DSCT_CTL_OPMODE_Pos;        /* Master PDMA RX channel configuration */        PDMA->CHCTL |= (1 << SPI_MASTER_RX_DMA_CH); /* Enable PDMA channel */        PDMA->DSCT[SPI_MASTER_RX_DMA_CH].CTL =                (PDMA->DSCT[SPI_MASTER_RX_DMA_CH].CTL & (~(PDMA_DSCT_CTL_TXCNT_Msk | PDMA_DSCT_CTL_OPMODE_Msk))) |                (DATA_COUNT - 1) << PDMA_DSCT_CTL_TXCNT_Pos | /* Transfer count */                1 << PDMA_DSCT_CTL_OPMODE_Pos;        /* Enable master's DMA transfer function */        SPI0->PDMACTL = (SPI_PDMACTL_RXPDMAEN_Msk | SPI_PDMACTL_TXPDMAEN_Msk);        while (PDMA->DSCT[SPI_MASTER_TX_DMA_CH].CTL & PDMA_DSCT_CTL_TXCNT_Msk) ;        PB1 = 1;}

这样,M031的DMA就完成了,如果需要启动DMA传输,就调用SPI_DMA_WRITE()函数便可。

本文系21ic论坛网友呐咯密密原创,资料下载请点击“阅读原文”。

推荐阅读:
DC-DC工作模式有苦恼?一文教你摆脱困扰
(DC-DC)电感电流“三兄弟”,极限条件来学习
DC-DC转换原理搞不清?来我这里取取经!

 21ic独家“修炼宝典” | 电子必看公众号 | 电子“设计锦囊”

添加管理员微信

你和大牛工程师之间到底差了啥?
加入技术交流群,与高手面对面
推荐文章
半导体行业联盟  ·  上海临港,又一芯片基金成立!
5 天前
半导体行业联盟  ·  苏州芯片公司爆雷!
6 天前
全球局势战略纵横  ·  歼20再曝新涂装令人大开眼界,另有两大目的!
7 年前
同道大叔  ·  4月14日十二星座运势分析
7 年前
禅语心苑  ·  穷养儿子富养女真正的意思是....
7 年前