MCU微课堂 | CKS32F4xx系列产品SPI通信

spi协议是由摩托罗拉公司提出的通讯协议(serial peripheral interface),即串行外围设备接口,是一种高速全双工的通信总线。它在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为pcb在布局上节省了空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议,它被广泛地使用在adc、lcd、flash等设备与mcu之间的通信。
cks32f4xx系列产品spi介绍
cks32f4xx系列的spi外设可用作通讯的主机及从机,支持最高的sck时钟频率为fpclk/2(cks32f407型号的芯片默认fpclk为142mhz,fpclk2为84mhz),完全支持spi协议的4种模式。spi协议根据cpol及cpha的不同状态分成的四种工作模式如下表所示:
cks32f4xx系列的spi架构如下图所示:
图中的1处是spi的引脚mosi、miso、sck、nss。cks32f4xx芯片有多个 spi外设,它们的spi通讯信号引出到不同gpio引脚上,使用时必须配置到这些指定的引脚。关于gpio引脚的复用功能可以查阅芯片数据手册。各个引脚的作用介绍如下:
(1)nss:从设备选择信号线,常称为片选信号线。当有多个spi从设备与 spi主机相连时,设备的其它信号线sck、mosi及miso同时并联到相同的spi 总线上,即无论有多少个从设备,都共同只使用这3条总线;而每个从设备都有独立的一条nss信号线,当主机要选择从设备时,把该从设备的nss信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行spi通讯。所以spi通讯以nss线置低电平为开始信号,以nss线被拉高作为结束信号。
(2)sck:时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,两个设备之间通讯时,通讯速率受限于低速设备。
(3)mosi:主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。
(4)miso:主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。
图中的2处是sck线的时钟信号,由波特率发生器根据“控制寄存器cr1”中的br[0:2]位控制,该位是对fpclk时钟的分频因子,对fpclk的分频结果就是sck引脚的输出时钟频率。
图中的3处是spi的数据控制逻辑。spi的mosi及miso都连接到数据移位寄存器上,数据移位寄存器的内容来源于接收缓冲区及发送缓冲区以及miso、mosi线。当向外发送数据的时候,数据移位寄存器以“发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候,数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。通过写spi 的“数据寄存器dr”把数据填充到发送缓冲区中,通过“数据寄存器dr”,可以获取接收缓冲区中的内容。其中数据帧的长度可以通过“控制寄存器cr1”的“dff位”配置成8位及16位模式;配置“lsbfirst位”可选择msb先行还是 lsb先行。
图中的4处是spi的整体控制逻辑。整体控制逻辑负责协调整个spi外设,控制逻辑的工作模式根据我们配置的“控制寄存器(cr1/cr2)”的参数而改变,基本的控制参数包括spi模式、波特率、lsb先行、主从模式、单双向模式等等。在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(sr)”,我们只要读取状态寄存器相关的寄存器位,就可以了解spi的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生spi中断信号、dma请求及控制nss信号线。实际应用中,我们一般不使用cks32 spi外设的标准nss信号线,而是更简单地使用普通的gpio,软件控制它的电平输出,从而产生通讯起始和停止信号。
cks32f4xx系列的spi作为通讯主机端时收发数据的过程如下:
(1) 控制nss信号线,产生起始信号;
(2) 把要发送的数据写入到“数据寄存器dr”中,该数据会被存储到发送缓冲区;
(3) 通讯开始,sck时钟开始运行。mosi把发送缓冲区中的数据一位一位地传输出去;miso则把数据一位一位地存储进接收缓冲区中;
(4) 当发送完一帧数据的时候,“状态寄存器sr”中的“txe标志位”会被置1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“rxne标志位”会被置1,表示传输完一帧,接收缓冲区非空;
(5) 等待到“txe标志位”为1时,若还要继续发送数据,则再次往“数据寄存器dr”写入数据即可;等待到“rxne标志位”为1时,通过读取“数据寄存器dr”可以获取接收缓冲区中的内容。
假如我们使能了txe或rxne中断,txe或rxne置1时会产生spi中断信号,进入同一个中断服务函数,到spi中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用dma方式来收发“数据寄存器 dr”中的数据。
cks32f4xx系列产品spi的配置
接下来我们讲解如何利用cks32f4xx系列固件库来完成对spi的配置使用。跟其它外设一样,cks32标准库提供了spi初始化结构体及初始化函数来配置 spi外设。了解初始化结构体后我们就能对spi外设运用自如了,代码如下:
typedef struct{      uint16_t spi_direction;                uint16_t spi_mode;                    uint16_t spi_datasize;                uint16_t spi_cpol;                   uint16_t spi_cpha;                     uint16_t spi_nss;                     uint16_t spi_baudrateprescaler;        uint16_t spi_firstbit;                 uint16_t spi_crcpolynomial;       }spi_inittypedef;  
结构体中各个成员变量的介绍及初始化时可被赋的值如下:
1) spi_direction:本成员设置spi的通讯方向,可设置为双线全双工 (spi_direction_2lines_fullduplex),双线只接收 (spi_direction_2lines_rxonly),单线只接收(spi_direction_1line_rx)、单线只发送模式(spi_direction_1line_tx)。
2) spi_mode:本成员设置spi工作在主机模式(spi_mode_master)或从机模式(spi_mode_slave ),这两个模式的最大区别为spi的sck信号线的时序,sck的时序是由通讯中的主机产生的。若被配置为从机模式,cks32的spi外设将接受外来的sck信号:
3) spi_datasize: 本成员可以选择spi通讯的数据帧大小是为8位 (spi_datasize_8b)还是16位(spi_datasize_16b)。
4) spi_cpol和spi_cpha: 这两个成员配置spi的时钟极性cpol和时钟相位cpha,前面讲过这两个配置影响到spi的通讯模式。时钟极性cpol成员可设置为高电平(spi_cpol_high)或低电平(spi_cpol_low )。时钟相位cpha则 可以设置为spi_cpha_1edge(在sck的奇数边沿采集数据)或 spi_cpha_2edge(在sck的偶数边沿采集数据)。
5) spi_nss: 本成员配置nss引脚的使用模式,可以选择为硬件模式 (spi_nss_hard )与软件模式(spi_nss_soft ),在硬件模式中的spi片选信号由 spi硬件自动产生,而软件模式则需要我们自己把相应的gpio端口拉高或置低产生非片选和片选信号。实际中软件模式应用比较多。
6) spi_baudrateprescaler: 本成员设置波特率分频因子,分频后的时钟即为spi的sck信号线的时钟频率。这个成员参数可设置为fpclk的2、4、6、8、16、32、64、128、256分频。可选的值如下所示:
spi_baudrateprescaler_2    //2分频spi_baudrateprescaler_4    //4分频spi_baudrateprescaler_6    //6分频spi_baudrateprescaler_8    //8分频spi_baudrateprescaler_16   //16分频spi_baudrateprescaler_32   //32分频spi_baudrateprescaler_64   //64分频spi_baudrateprescaler_128  //128分频spi_baudrateprescaler_256  //256分频  
7) spi_firstbit: 所有串行的通讯协议都会有msb先行(高位数据在前)还是 lsb先行(低位数据在前)的问题,而cks32f4xx系列的spi模块可以通过这个结构体成员,对这个特性编程控制。
spi_firstbit_msb    //高位数据在前spi_firstbit_lsb     //低位数据在前  
8) spi_crcpolynomial: 这是spi的crc校验中的多项式,若我们使用crc 校验时,就使用这个成员的参数(多项式),来计算crc的值。
配置完这些结构体成员的值,调用库函数spi_init即可把结构体的配置写入到寄存器中。
cks32f4xx读写spi flash实验
串口的dma接发通信实验是存储器到外设和外设到存储器的数据传输。在第24
本小节以一种使用spi通讯的串行flash存储芯片的读写实验为大家讲解 cks32f4xx系列的spi使用方法。实验中的flash芯片(型号:w25q32)是一种使用spi通讯协议的norflash存储器,它的cs/clk/dio/do引脚分别连接到了cks32f4xx对应的spi引脚nss/sck/mosi/miso上,其中cks32f4xx的nss引脚是一个普通的gpio,不是spi的专用nss引脚,所以程序中我们要使用软件控制的方式。
1.编程要点
(1) 初始化通讯使用的目标引脚及端口时钟;  
(2) 使能spi外设的时钟;
(3) 配置spi外设的模式、地址、速率等参数并使能spi外设;
(4) 编写基本spi按字节收发的函数; 
(5) 编写对flash擦除及读写操作的的函数; 
(6) 编写测试程序,对读写数据进行校验。 
2.代码分析
代码清单1:w25q32初始化配置
void w25qxx_init(void){       gpio_inittypedef  gpio_initstructure;      rcc_ahb1periphclockcmd(rcc_ahb1periph_gpiod, enable);       rcc_ahb1periphclockcmd(rcc_ahb1periph_gpiog, enable);       gpio_initstructure.gpio_pin = gpio_pin_6;      gpio_initstructure.gpio_mode = gpio_mode_out;      gpio_initstructure.gpio_otype = gpio_otype_pp;      gpio_initstructure.gpio_speed = gpio_speed_100mhz;     gpio_initstructure.gpio_pupd = gpio_pupd_up;       gpio_init(gpiod,  gpio_initstructure);     gpio_initstructure.gpio_pin = gpio_pin_3;      gpio_init(gpiog,  gpio_initstructure);    gpio_setbits(gpiog,gpio_pin_3);       w25qxx_cs=1;                        //spi flash不选中      spi1_init();                 //初始化spi      spi1_setspeed(spi_baudrateprescaler_4);    //设置为21m时钟      w25qxx_type=w25qxx_readid();         //读取flash id.}  
上面的代码主要是完成对w25q32片选引脚的初始化,spi初始化。spi通信速率设置和读取w25q32的id。
代码清单2:spi初始化函数
void spi1_init(void){         gpio_inittypedef  gpio_initstructure;      spi_inittypedef  spi_initstructure;      rcc_ahb1periphclockcmd(rcc_ahb1periph_gpiob, enable);      rcc_apb2periphclockcmd(rcc_apb2periph_spi1, enable);      gpio_initstructure.gpio_pin = gpio_pin_3|gpio_pin_4|gpio_pin_5;      gpio_initstructure.gpio_mode = gpio_mode_af;      gpio_initstructure.gpio_otype = gpio_otype_pp;      gpio_initstructure.gpio_speed = gpio_speed_100mhz;      gpio_initstructure.gpio_pupd = gpio_pupd_up;       gpio_init(gpiob,  gpio_initstructure);     gpio_pinafconfig(gpiob,gpio_pinsource3,gpio_af_spi1);    gpio_pinafconfig(gpiob,gpio_pinsource4,gpio_af_spi1);     gpio_pinafconfig(gpiob,gpio_pinsource5,gpio_af_spi1);       rcc_apb2periphresetcmd(rcc_apb2periph_spi1,enable);      rcc_apb2periphresetcmd(rcc_apb2periph_spi1,disable);      spi_initstructure.spi_direction = spi_direction_2lines_fullduplex;       spi_initstructure.spi_mode = spi_mode_master;          spi_initstructure.spi_datasize = spi_datasize_8b;        spi_initstructure.spi_cpol = spi_cpol_high;          spi_initstructure.spi_cpha = spi_cpha_2edge;       spi_initstructure.spi_nss = spi_nss_soft;          spi_initstructure.spi_baudrateprescaler = spi_baudrateprescaler_256;        spi_initstructure.spi_firstbit = spi_firstbit_msb;        spi_initstructure.spi_crcpolynomial = 7;        spi_init(spi1,  spi_initstructure);        spi_cmd(spi1, enable);       spi1_readwritebyte(0xff);   }  
上面这段代码主要是完成对spi1的初始化,首先是配置了spi1使用的引脚spi1_sck、spi1_mosi和spi1_miso。然后是根据第2小节的内容完成对spi1外设模式的配置。根据flash芯片w25q32的说明,它支持spi模式0及模式3,支持双线全双工,使用msb先行模式,支持最高通讯时钟为104mhz,数据帧长度为8位。我们要把cks32f4的spi外设中的这些参数配置一致。
代码清单3:spi1单字节收发函数
u8 spi1_readwritebyte(u8 txdata){                  while (spi_i2s_getflagstatus(spi1, spi_i2s_flag_txe) == reset){}       spi_i2s_senddata(spi1, txdata);       while (spi_i2s_getflagstatus(spi1, spi_i2s_flag_rxne) == reset){}       return spi_i2s_receivedata(spi1); }  
本函数中不包含spi起始和停止信号,只是收发的主要过程,所以在调用本函数前后要做好起始和停止信号的操作。通过检测txe标志,获取发送缓冲区的状态,若发送缓冲区为空,则表示可能存在的上一个数据已经发送完毕;等待至发送缓冲区为空后,调用库函数spi_i2s_senddata把要发送的数据“txdata”写入到spi的数据寄存器dr,写入spi数据寄存器的数据会存储到发送缓冲区,由spi外设发送出去;写入完毕后等待rxne事件,即接收缓冲区非空事件。由于spi双线全双工模式下mosi与miso数据传输是同步的,当接收缓冲区非空时,表示上面的数据发送完毕,且接收缓冲区也收到新的数据;等待至接收缓冲区非空时,通过调用库函数spi_i2s_receivedata读取spi的数据寄存器dr,就可以获取接收缓冲区中的新数据了。代码中使用关键字“return”把接收到的这个数据作为spi1_readwritebyte函数的返回值。
搞定了spi的基本收发单元后,还需要了解如何对flash芯片进行读写。flash 芯片自定义了很多指令,我们通过控制cks32f4利用spi总线向flash 芯片发送指令,flash芯片收到后就会执行相应的操作。具体的指令代码可以查看w25q32芯片的数据手册。
代码清单4:读取flash芯片id函数
u16 w25qxx_readid(void){      u16 temp = 0;          w25qxx_cs=0;                  spi1_readwritebyte(0x90);            spi1_readwritebyte(0x00);             spi1_readwritebyte(0x00);             spi1_readwritebyte(0x00);                   temp|=spi1_readwritebyte(0xff)<>16));             spi1_readwritebyte((u8)((dst_addr)>>8));           spi1_readwritebyte((u8)dst_addr);        w25qxx_cs=1;                                           w25qxx_wait_busy();             }  
目标扇区被擦除完毕后,就可以向它写入数据了。与eeprom类似,flash芯片也有页写入命令,使用页写入命令最多可以一次向flash传输256个字节的数据,我们把这个单位称为页大小。在进行页写入时第1个字节为“页写入指令”编码,2-4字节为要写入的“地址a”,接着的是要写入的内容,最多可以发送 256字节数据,这些数据将会从“地址a”开始,按顺序写入到flash的存储矩阵。若发送的数据超出256个,则会覆盖前面发送的数据。
代码清单7:w25q32页写入函数
void w25qxx_write_page(u8* pbuffer,u32 writeaddr,u16 numbytetowrite){       u16 i;          w25qxx_write_enable();                       w25qxx_cs=0;                                       spi1_readwritebyte(w25x_pageprogram);                 spi1_readwritebyte((u8)((writeaddr)>>16));           spi1_readwritebyte((u8)((writeaddr)>>8));           spi1_readwritebyte((u8)writeaddr);           for(i=0;i  
应用的时候我们常常要写入不定量的数据,直接调用“页写入”函数并不是特别方便,所以我们页写入函数的基础上编写了“不定量数据写入”的函数。在实际调用这个“不定量数据写入”函数时,还要注意确保目标扇区处于擦除状态
代码清单8:w25q32不定量数据写入函数
void w25qxx_write(u8* pbuffer,u32 writeaddr,u16 numbytetowrite)   {       u32 secpos;      u16 secoff;      u16 secremain;            u16 i;          u8 * w25qxx_buf;             w25qxx_buf=w25qxx_buffer;              secpos=writeaddr/4096;       secoff=writeaddr%4096;       secremain=4096-secoff;         if(numbytetowrite>16));             spi1_readwritebyte((u8)((readaddr)>>8));           spi1_readwritebyte((u8)readaddr);           for(i=0;i  
函数的入口参数pbuffer是数据存储区、readaddr是开始读取的地址(24bit)、numbytetoread是要读取的字节数(最大65535)。
完成基本的读写函数后,接下来我们编写一个读写测试函数来检验驱动程。
代码清单10:w25q32读写测试函数
uint8_t w25q32_test(void){      u16 i;       printf(写入的数据:rn);      for ( i=0; i<=10; i++ )       {               spi_buf_write[i] = i;            printf(0x%02x , spi_buf_write[i]);       }       w25qxx_write((u8*)spi_buf_write,flash_size-100,11);              printf(写成功,);         printf(读出的数据:rn);        w25qxx_read(datatemp,flash_size-100,11);                for (i=0; i<11; i++)      {              if(datatemp[i] != spi_buf_write[i])            {                  printf(0x%02x , datatemp[i]);                  printf(错误:i2c eeprom写入与读出的数据不一致);                 return 0;            }            printf(0x%02x , datatemp[i]);      }      printf(rn);        printf(spi(w25q32)读写测试成功);      return 1;}  
代码中先填充一个数组,数组的内容为0,1至10,接着把这个数组的内容写入到spi flash中,并将写入的数据打印输出到串口调试助手。写入完毕后再从spi flash的地址中读取数据,把读取到的数据与写入的数据进行校验,若一致说明读写正常,否则读写过程有问题或者spi flash芯片不正常,然后再将读取到的数据打印输出到串口调试助手。
代码清单11:主函数
int main(void){         u16 id = 0;        nvic_prioritygroupconfig(nvic_prioritygroup_2);      delay_init(168);           usart_configuration();      w25qxx_init();            while(1)      {            id = w25qxx_readid();            if (id == w25q32 || id == nm25q32)                  break;                 printf(w25q32 init failedrn);            delay_ms(500);            delay_ms(500);      }       printf(w25q32 init successrn);       w25q32_test();      while(1)       {        }        }  
主函数代码比较简单,主要是完成串口初始化和w25q32的初始化,初始化完成之后会执行w25qxx_readid函数,读取w25q32的id,同时对id进行判断,并将结果通过串口调试助手打印输出。然后会执行一次w25q32测试函数,并将一些测试结果通过串口调试助手打印输出。
来源:中科芯mcu
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理


什么是有源音箱和无源音箱_有源音箱和无源的区别是什么
珠海力士乐叶片泵油封被击穿而串油的原因分析
紫光国微超级汽车芯系列产品助力汽车产业变革
工业互联网平台的技术体系与关键技术
电池修复和维护一样吗?
MCU微课堂 | CKS32F4xx系列产品SPI通信
TB1240AN- -IIC总线控制的TV处理集成电路
元宇宙的概念与应用
聚焦 | 基于深度学习的人工智能技术已触及天花板
Freescale扩展嵌入式电机控制系列提供经济高效的8位M
选气动球阀做为紧急切断阀门时该如何正确地操作
85-015G-XX-10095压力传感器的保温措施是什么?
哪一些策略推动了物联网产业的发展
采集仪器的防护等级介绍
基于UCC28070设计的300W PFC高效电源方案
IP知识百科之AI防火墙
飞航管制结合AR技术迈向虚拟化
为什么要实现无人驾驶落地,告诉你三个理由!
Altera支持硅片融合实现的混合系统架构
为什么笔记本接口越做越少