文章目录

  • 目的
  • 基础说明
  • 使用演示
  • 其它补充
  • 总结

目的

SPI是单片机中比较常用的一个功能。这篇文章将对CH32V307中相关内容进行说明。

本文使用沁恒官方的开发板 (CH32V307-EVT-R1沁恒RISC-V模块MCU赤兔评估板) 进行演示。

基础说明

SPI的基础概念见下面文章:
SPI基础概念:https://blog.csdn.net/Naisu_kun/article/details/118102844

从上面文章中可以看到SPI主要涉及两条通讯线(MOSI、MISO)、一条时钟线(SCLK)、一条片选线(CS或NSS,非必需)。

除了接线,SPI使用上还需要了解极性和相位的概念,在此之上理解读写时序逻辑等内容。

另在在SPI读数据时需要注意,这时候虽然数据是从机发送的,但时钟还是主机给出的,所以往往读数据时主机还需要通过写数据(任意均可)来给出时钟信号。

使用演示

下面是个简单的使用SPI发送数据的示例:

#include "debug.h"void SPI1_Master_Init(void) {// 初始化时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);// 初始化GPIOGPIO_InitTypeDef GPIO_InitStructure = { 0 };GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;         // CSGPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  // 这里使用普通推挽输出模式,而不是复用模式,所以下面需要手动控制GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init( GPIOA, &GPIO_InitStructure);GPIO_SetBits( GPIOA, GPIO_Pin_4);                 // 拉高不工作GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // SCKGPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init( GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // MISOGPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init( GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; // MOSIGPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init( GPIOA, &GPIO_InitStructure);// 初始化SPI1SPI_InitTypeDef SPI_InitStructure = { 0 };SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  // 全双工模式SPI_InitStructure.SPI_Mode = SPI_Mode_Master;                       // 作为主机SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;                   // 8bit数据SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;                          // 时钟不通讯时低电平SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;                        // 在第一个边缘进行采样SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                           // 片选引脚手动控制SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; // SPI外设时钟源32分频SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;                  // 传输时高比特在前SPI_InitStructure.SPI_CRCPolynomial = 7;SPI_Init(SPI1, &SPI_InitStructure);                                 // 初始化SPI1设置SPI_Cmd(SPI1, ENABLE);                                              // 启动SPI1
}int main(void) {NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);SystemCoreClockUpdate();Delay_Init();USART_Printf_Init(115200);SPI1_Master_Init();                                                       // 初始SPI1while(1) {GPIO_ResetBits( GPIOA, GPIO_Pin_4);                                   // 拉低片选信号,开始SPI通讯u8 data[4] = {0x00, 0xff, 0x22, 0x33};for (int i = 0; i < 4; i++) {while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) { } // 等待可以写数据到发送缓冲SPI_I2S_SendData(SPI1, data[i]);                                  // 写数据}Delay_Us(6);                                                          // 写数据并不代表发送完成,发送完成前后不能拉高片选信号// 目前设置下SPI时钟为 96/32 = 3MHz, 每发送一个字节时间为 8/3 = 2.667us// 通常为了保险最好等待两个字节以上时间GPIO_SetBits( GPIOA, GPIO_Pin_4);                                     // 拉高片选信号,结束SPI通讯Delay_Us(20);}
}

使用逻辑分析仪抓取上面代码实际运行中的一帧数据如下:

需要注意的是逻辑分析仪的采样速度要远远大于SPI时钟速度,不然解析数据时可能出现一个bit的异常。

下面代码是使用SPI1作为主机,SPI2作为从机的实验,除了上面的发送数据外还进行了数据读取操作:

#include "debug.h"void SPI1_Master_Init(void) {// 初始化时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);// 初始化GPIOGPIO_InitTypeDef GPIO_InitStructure = { 0 };GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;         // CSGPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  // 这里使用普通推挽输出模式,而不是复用模式,所以下面需要手动控制GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init( GPIOA, &GPIO_InitStructure);GPIO_SetBits( GPIOA, GPIO_Pin_4);                 // 拉高不工作GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // SCKGPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init( GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // MISOGPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init( GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; // MOSIGPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init( GPIOA, &GPIO_InitStructure);// 初始化SPI1SPI_InitTypeDef SPI_InitStructure = { 0 };SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  // 全双工模式SPI_InitStructure.SPI_Mode = SPI_Mode_Master;                       // 作为主机SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;                   // 8bit数据SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;                          // 时钟不通讯时低电平SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;                        // 在第一个边缘进行采样SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                           // 片选引脚手动控制SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64; // SPI外设时钟源64分频SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;                  // 传输时高比特在前SPI_InitStructure.SPI_CRCPolynomial = 7;SPI_Init(SPI1, &SPI_InitStructure);                                 // 初始化SPI1设置SPI_Cmd(SPI1, ENABLE);                                              // 启动SPI1
}void SPI2_Slave_Init(void) {// 初始化时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);// 初始化GPIOGPIO_InitTypeDef GPIO_InitStructure = { 0 };GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;    // CSGPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 片选信号为低才工作GPIO_Init( GPIOB, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; // SCKGPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init( GPIOB, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; // MISOGPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init( GPIOB, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15; // MOSIGPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init( GPIOB, &GPIO_InitStructure);// 初始化SPI2SPI_InitTypeDef SPI_InitStructure = { 0 };SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  // 全双工模式SPI_InitStructure.SPI_Mode = SPI_Mode_Slave;                        // 作为从机SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;                   // 8bit数据SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;                          // 时钟不通讯时低电平(和主机保持一致)SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;                        // 在第一个边缘进行采样(和主机保持一致)SPI_InitStructure.SPI_NSS = SPI_NSS_Hard;                           // 片选信号依赖外部信号SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64; // 从模式下其实不用关心这个SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;                  // 传输时高比特在前SPI_InitStructure.SPI_CRCPolynomial = 7;SPI_Init(SPI2, &SPI_InitStructure);                                 // 初始化SPI2设置SPI_Cmd(SPI2, ENABLE);                                              // 启动SPI2
}int main(void) {NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);SystemCoreClockUpdate();Delay_Init();USART_Printf_Init(115200);printf("SystemClk:%d\r\n", SystemCoreClock);// 接线方式: PA4-PB12  PA5-PB13  PA6-PB14  PA7-PB15SPI1_Master_Init();                                                       // 初始化SPI1作为主机SPI2_Slave_Init();                                                        // 初始化SPI2作为从机while(1) {u8 data1 = 0;u8 data2 = 0;GPIO_ResetBits( GPIOA, GPIO_Pin_4);                                   // 拉低片选信号,开始SPI通讯while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) { }SPI_I2S_SendData(SPI2, 0x33);                                         // 从机先写一个数据到发送区,这样当主机开始发送消息产生时钟信号时,该数据就会被传输while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) { }SPI_I2S_SendData(SPI1, 0x22);                                         // 主机发送数据给从机while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) { }    // 等待可以读取接收到的数据data2 = SPI_I2S_ReceiveData(SPI2);                                    // 读取接收到的数据while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET) { }data1 = SPI_I2S_ReceiveData(SPI1);GPIO_SetBits( GPIOA, GPIO_Pin_4);                                     // 拉高片选信号,结束SPI通讯printf("rx1 = %d, rx2 = %d \r\n", data1, data2);Delay_Ms(500);}
}

其它补充

SPI除了最基础的上面查询的方式使用外,本身也可以使用中断方式或者DMA方式来使用。不过一般来说只是作为主机使用的话,因为SPI的速度很快,所以很多时候上面方式就够用了。如果有需要的话中断和DMA的方式使用起来也不难。

总结

单片机本身的SPI外设使用比较简单,真正工作时进行SPI通讯更多的是要能够理解SPI的时序逻辑概念,能够读懂芯片手册上的时序逻辑图。

沁恒CH32V307使用记录:SPI基础使用相关推荐

  1. 沁恒CH32V307使用记录:GPIO与EXTI

    文章目录 目的 GPIO(通用输入输出接口) 基础说明 初始化 输出 输入与电平读取 锁定机制 EXTI(外部中断) 基础说明 使用演示 总结 目的 GPIO是单片机最基础的功能,EXTI最常用的场景 ...

  2. 沁恒CH32V307使用记录:使用TIM输出PWM信号

    文章目录 目的 基础说明 使用例程 总结 目的 使用TIM输出PWM信号是单片机中比较常用的一个功能.这篇文章将对CH32V307中相关内容进行说明. 本文使用沁恒官方的开发板 (CH32V307-E ...

  3. Winbond W25Qxx SPI FLASH 使用示例(基于沁恒CH32V307单片机)

    文章目录 目的 基础说明 使用示例 总结 目的 Winbond(华邦)的 W25Qxx 系列 SPI FLASH 是比较常用的芯片,这篇文章将演示单片机中通过SPI使用该芯片的操作过程. 本文使用沁恒 ...

  4. 沁恒CH32V307单片机入门(01):基础说明与流程体验

    文章目录 目的 基础说明 芯片介绍 资料与工具 开发环境 流程体验 开发 调试 下载 总结 目的 工作这几年单片机主要就接触过 Atmel.Renesas.Microchip.ST 这些厂家的,最近几 ...

  5. 沁恒CH32V307单片机入门(02):官方库与工程模板介绍

    文章目录 目的 官方库 工程模板 使用例程 总结 目的 现在开发单片机大多数时候都是面向库开发的,这里将简单介绍下CH32V307的官方库. 在开发过程中新建项目时通常会从某些模板开始,模板包含了库和 ...

  6. 沁恒CH32V307嵌入式比赛开发心得

    开发版介绍 板载资源: CH32V307VCT6 ES8388 音频采集与播放 AP3216C 距离与环境光传感器 (ALS&PS) 128 Mbit 板载 Flash Type-C USB ...

  7. 沁恒微CH32V307开发板试用-RT-Thread+UART+LWIP+LED

    CH32V307V-R0 是 南京沁恒微电子(WCH) 推出的一款基于 RISC-V 内核的开发板,最高主频为 120Mhz,该开发板芯片为 CH32V307-R0. 芯片资料:32位互联型RISC- ...

  8. 沁恒微BLE蓝牙主机寻找从机服务及特征值的记录

    1.今天,22年10月10号,两年多的嵌软工作生涯,突然想记录一下工作中软件开发的事. 2.嗯~,先说一下开发需求吧.刚入公司转正不久,导师告诉我要做一个蓝牙主机,能够根据用户输入的设备名称寻找指定的 ...

  9. 沁恒CH583 USB 自定义HID调试记录

    使用USB HID主要是为了免驱,通过自定义USB HID可以利用USB口来做很多事,比如串口打印,串口升级都可以通过usb口来实现,这样可以省去一个USB转串口器件同时也不用装驱动,如下实现可以通过 ...

最新文章

  1. Linux中crontab定时执行python程序
  2. java客服系统_阿里Java内部资料:2020最全Java技术栈(架构篇+算法篇+大数据)
  3. IP子网划分和vlsm(变长子网掩码)和路由汇总
  4. spring-boot 之 使用Admin监控应用
  5. 【已解决】java.lang.NullPointerException at line 15, Solution.r
  6. 关于div的outline-offset属性和focus事件的接收
  7. python的0基础入门语法_学习小结(1)
  8. 操作数数据类型 char 对于 sum 运算符无效。_数据类型和运算符
  9. jsp界面自动生成文件注释_实施注释界面
  10. Qt工作笔记-Qt元对象系统解析【2合1】
  11. android.mk简单介绍
  12. scala的字符串的方法(五)
  13. 微信小程序的特点是什么?
  14. 软件工程实践专题第三次团队作业
  15. Echarts柱状图常用属性
  16. 东南亚跨境电商shopee平台,教你轻松打造高销量品牌店铺!
  17. 拉丁舞身形研究之恰恰恰
  18. MYSQL不能远程连接
  19. XCode6如何创建Category
  20. [转载]菜鸟的草缸 篇一:器材篇(附鱼缸、过滤器、灯、草泥开箱过程)

热门文章

  1. python request大批量发送请求调用接口时,报错:[WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
  2. cms php vue 开源_vue_cms
  3. vue项目结构及启动文件加载过程分析
  4. ADODB.Connection对象的实例
  5. 读邹欣老师的师生关系有感
  6. 分类决策树考虑了经验风险吗_分类决策树与去还是不去
  7. 寄生式组合继承能否优化?
  8. 徐宥——我的大学[转]
  9. /bin/bash:找不到文件或目录
  10. leetcode 50. Pow(x,n)