1.cubemx设置
参考文章
时钟设置
高速时钟配置
将HCLK设置为最大频率72MHz
GPIO设置
参数设置
GPIO output level
: Low
GPIO mode
: Output Push Pull
GPIO Pull-up/Pull down
: No ull-up and no ull-down
Maximum Output speed
: High
2.SPI基础知识
SPI 是英语 Serial Peripheral Interface 的缩写,顾名思义就是串行外围设备接口,是一种高速的,全双工,同步的通信总线。
SPI 可以同时发出和接收串行数据;可以当作主机或从机工作;提供频率可编程时钟;发送结束中断标志;写冲突保护;总线竞争保护等。
2.1. SPI 基本特性
-
全双工通信:SPI 总线支持全双工通信,即数据可以在两个方向上同时传输。这使得 SPI 在数据传输速度上具有很大的优势。
-
主从模式:SPI 支持主设备和从设备模式。主设备生成时钟信号,从设备根据时钟信号进行数据同步。一个 SPI 总线系统中可以有多个从设备,但同时只能有一个主设备。
-
可编程时钟:SPI 的时钟频率是可编程的,主设备可以根据需要配置适合的时钟频率,以适应不同的从设备和应用场景。
-
简单硬件连接:SPI 通常使用 4 根线进行通信:
- MISO(Master In Slave Out):主设备数据输入,从设备数据输出。
- MOSI(Master Out Slave In):主设备数据输出,从设备数据输入。
- SCLK(Serial Clock):时钟信号,由主设备生成。
- SS(Slave Select):从设备选择信号,由主设备控制,低电平有效。
2.2. SPI 工作原理
SPI 通信过程包括以下步骤:
- 主设备拉低 SS 信号,选择从设备。
- 主设备生成时钟信号,并通过 MOSI 发送数据。
- 从设备在时钟信号的同步下,通过 MISO 返回数据。
- 数据传输完成后,主设备释放 SS 信号,从设备解除选择状态。
2.3. SPI 数据传输模式
SPI 有四种不同的数据传输模式,由时钟极性(CPOL)和时钟相位(CPHA)两个参数决定:
- 模式 0(CPOL = 0, CPHA = 0):时钟空闲状态为低电平,数据在时钟上升沿采样。
- 模式 1(CPOL = 0, CPHA = 1):时钟空闲状态为低电平,数据在时钟下降沿采样。
- 模式 2(CPOL = 1, CPHA = 0):时钟空闲状态为高电平,数据在时钟下降沿采样。
- 模式 3(CPOL = 1, CPHA = 1):时钟空闲状态为高电平,数据在时钟上升沿采样。
3.代码
最早用非SPI通信方式实现。软件SPI实现的代码放在附录里面
main
WS2812_SendRGB(255,255,255);
WS2812_SendRGB(255,255,255);
WS2812_SendRGB(255,255,255);
WS2812_SendRGB(255,255,255);
WS2812_SendRGB(255,255,255);
WS2812_SendRGB(255,255,255);
WS2812.c
#include "stm32f1xx_hal.h"
#include "WS2812.h"
void WS2812_delay_us(uint32_t us)
{
int32_t temp;
// 假设系统时钟为72MHz,SysTick计时器每1/72微秒递减一次
SysTick->LOAD = 72 * us - 1; // 设置LOAD的值为需要的微秒数乘以72,然后减去1
SysTick->VAL = 0x00; // 清空计数器
SysTick->CTRL = 0x01; // 使能SysTick,采用内核时钟源
do
{
temp = SysTick->CTRL; // 读取当前控制寄存器的值
}
while((temp & 0x01) && (!(temp & (1 << 16)))); // 等待计时器标志位,直到计时结束
SysTick->CTRL = 0x00; // 关闭计数器
SysTick->VAL = 0x00; // 清空计数器
}
void WS2812_delay_ns(uint32_t ns)
{
int32_t temp;
// 假设系统时钟为72MHz,SysTick计时器每13.89纳秒递减一次
// 最接近的计数是 ns / 13.89,向下取整
uint32_t count = (ns * 72) / 1000; // 转换为等效的计数,忽略小数部分
SysTick->LOAD = count - 1; // 设置LOAD的值为需要的纳秒数对应的计数值,然后减去1
SysTick->VAL = 0x00; // 清空计数器
SysTick->CTRL = 0x01; // 使能SysTick,采用内核时钟源
do
{
temp = SysTick->CTRL; // 读取当前控制寄存器的值
}
while((temp & 0x01) && (!(temp & (1 << 16)))); // 等待计时器标志位,直到计时结束
SysTick->CTRL = 0x00; // 关闭计数器
SysTick->VAL = 0x00; // 清空计数器
}
void WS2812_Send_0(void)
{
DI_ON;
WS2812_delay_ns(300); // 220ns~380ns
DI_OFF;
WS2812_delay_us(1); // 580ns~1600ns
}
void WS2812_Send_1(void)
{
DI_ON;
WS2812_delay_us(1); // 580ns~1600ns
DI_OFF;
WS2812_delay_ns(320); // 220ns~420ns
}
void WS2812_Send_Reset(void)
{
DI_OFF;
WS2812_delay_ns(300); // >280ns
}
void WS2812_SendRGB(unsigned char R,unsigned char G,unsigned char B)
{
unsigned char i,j,temp[3];
temp[0]=G;
temp[1]=R;
temp[2]=B;
for(j=0;j<3;j++)
for(i=0;i<8;i++)
{
if(temp[j] & (0x80>>i)) //发送1
WS2812_Send_1();
else //发送0
WS2812_Send_0();
}
}
WS2812.h
#ifndef __WS2812_H_
#define __WS2812_H_
#define DI_Pin GPIO_PIN_0
#define DI_Port GPIOD
#define DI_ON HAL_GPIO_WritePin(DI_Port, DI_Pin, GPIO_PIN_SET) //定义TRIG输出高电平
#define DI_OFF HAL_GPIO_WritePin(DI_Port, DI_Pin, GPIO_PIN_RESET) //定义TRIG输出低电平
void WS2812_delay_us(uint32_t us);
void WS2812_delay_ns(uint32_t ns);
void WS2812_Send_0(void);
void WS2812_Send_1(void);
void WS2812_Send_Reset(void);
void WS2812_SendRGB(unsigned char R,unsigned char G,unsigned char B);
#define WS2812_BLACK WS2812_SendRGB(0,0,0)
#define WS2812_WHITE WS2812_SendRGB(255,255,255)
#endif
4.代码解析
代码整体由GPT撰写,我负责细节部分的修改。
几个Delay函数不再赘述,Systick文章中已经详细解释了。
void WS2812_Send_0(void);
void WS2812_Send_1(void);
void WS2812_Send_Reset(void);
这三个函数分别对应下面图中的三种时序波形。
主函数中发送的六个RGB信号会依次传递下去
WS2812_SendRGB(255,255,255);
WS2812_SendRGB(255,255,255);
WS2812_SendRGB(255,255,255);
WS2812_SendRGB(255,255,255);
WS2812_SendRGB(255,255,255);
WS2812_SendRGB(255,255,255);
5.实验现象
6. 附
SPI代码
① MySPI.c
void Soft_SPI_WriteBit(uint8_t bit) {
if (bit)
HAL_GPIO_WritePin(SPI_PORT, MOSI_PIN, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(SPI_PORT, MOSI_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(SPI_PORT, SCK_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(SPI_PORT, SCK_PIN, GPIO_PIN_RESET);
}
void Soft_SPI_WriteByte(uint8_t byte)
{
for (int i = 0; i < 8; i++)
Soft_SPI_WriteBit(byte & (0x80 >> i));
}
② WS2812.c
void WS2812_Send_0(void)
{
Soft_SPI_WriteBit(1);
for (int i = 0; i < 3; i++)
Soft_SPI_WriteBit(0);
}
void WS2812_Send_1(void)
{
Soft_SPI_WriteBit(1);
Soft_SPI_WriteBit(1);
Soft_SPI_WriteBit(0);
}
void WS2812_Send_Reset(void)
{
for (int i = 0; i < 50; i++)
Soft_SPI_WriteBit(0);
}
void WS2812_SendRGB(uint8_t R, uint8_t G, uint8_t B) {
uint8_t temp[3] = {G, R, B};
// 拉低 CS 引脚以开始传输
HAL_GPIO_WritePin(SPI_PORT, CS_PIN, GPIO_PIN_RESET);
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 8; i++) {
if (temp[j] & (0x80 >> i)) {
WS2812_Send_1();
} else {
WS2812_Send_0();
}
}
}
// 发送完数据后拉高 CS 引脚
HAL_GPIO_WritePin(SPI_PORT, CS_PIN, GPIO_PIN_SET);
}
③ 宏(在合适位置添加,根据自己的引脚修改)
#define MOSI_PIN GPIO_PIN_0
#define SCK_PIN GPIO_PIN_1
#define CS_PIN GPIO_PIN_2
#define SPI_PORT GPIOD