模拟三线spi读写寄存器

想用stm8写一个模拟三线spi读取寄存器的程序,时钟是8M的,所以没用延时,环境是IAR,三线spi以及需要读取的寄存器地址如下;

img


img

img

下面是自己修改的一点程序,不能运行,可以自行删除修改


```c
#include "stm8s.h"
#include "stdio.h"
#include "iostm8s105k4.h"

#define CSN PC_ODR_ODR6
#define SCK PC_ODR_ODR5
#define MISO PC_ODR_ODR7

#ifdef _RAISONANCE_
#define PUTCHAR_PROTOTYPE int putchar (char c)
#define GETCHAR_PROTOTYPE int getchar (void)
#elif defined (_COSMIC_)
#define PUTCHAR_PROTOTYPE char putchar (char c)
#define GETCHAR_PROTOTYPE char getchar (void)
#else /* _IAR_ */
#define PUTCHAR_PROTOTYPE int putchar (int c)
#define GETCHAR_PROTOTYPE int getchar (void)
#endif /* _RAISONANCE_ */

void GPIO_init(void)
{
  PE_DDR = (1<<5); // 配置PE端口的方向寄存器PE5输出
  PE_CR1 = (1<<5); // 设置PE5为推挽输出
  
  PC_DDR = 0xe0;//(1<<5);//设置PC端口的方向寄存器PC5,6,7输出
  PC_CR1 = 0xe0;//(1<<5);//设置PC5,6,7为推挽输出
  
}

uint8_t spi_transfer(uint8_t data)
{
    // 这里简单地输出一下传输的数据
    printf("SPI transfer: 0x%!X(MISSING)\n", data);
    
    // 返回读取的数据
    return 0xA5; // 假设读取到的数据为0xA5
}

void READ(void){
    int i;
    uint8_t addr = 0x10; // 寄存器地址
    uint8_t data; // 收到的数据
     
    CSN=0;
    for(i=0;i<=15;i++){
      SCK=1;
      //MISO=1;
     
      //uint8_t cmd = 0x00000011; // 命令格式:[1][1][0][0][A2][A1][A0][R/W]
     // spi_transfer(cmd);
     // spi_transfer(addr);
     
      SCK=0;
     // MISO=0;
       
      // 读取收到的数据
    //data = spi_transfer(0); // 发送一个空数据,读取收到的数据
    //printf("Received data: 0x%!!(MISSING)X(MISSING)\n", data);
    //return 0;
    
    //spi_send_byte(0x55); // 发送数据
   } 
   CSN=1;
}

void TIM1_init(void)
{
TIM1_PSCRH = 0x1F; // 8M系统时钟经预分频f=fck/(PSCR+1)
TIM1_PSCRL = 0x3F; // PSCR=0x1F3F,f=8M/(0x1F3F+1)=1000Hz,每个计数周期1ms
TIM1_ARRH = 0x07; // 自动重载寄存器ARR=0x01F4=500,0x01*0xF4=500
TIM1_ARRL = 0xF4; // 每记数500次产生一次中断,即500ms
TIM1_IER = 0x01; // 允许更新中断
TIM1_CR1 = 0x01; // 计数器使能,开始计数
  
}


void main(void)
{
   
  //char ans;
  
  CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1);
    
  UART2_DeInit();
  
  UART2_Init((uint32_t)115200, UART2_WORDLENGTH_8D, UART2_STOPBITS_1, UART2_PARITY_NO,
              UART2_SYNCMODE_CLOCK_DISABLE, UART2_MODE_TXRX_ENABLE);

  printf("\n\rUART2 Example :retarget the C library printf()/getchar() functions to the UART\n\r");
  printf("\n\rEnter Text\n\r");
  
  GPIO_init();
  TIM1_init();
  asm("rim"); // 开全局中断
  
  while (1)
  {
    READ();
    
    //ans = getchar();
    //printf("%c", ans); 
  }
 
}


PUTCHAR_PROTOTYPE
{
  /* Write a character to the UART2 */
  UART2_SendData8(c);
  /* Loop until the end of transmission */
  while (UART2_GetFlagStatus(UART2_FLAG_TXE) == RESET);

  return (c);
}


GETCHAR_PROTOTYPE
{
#ifdef _COSMIC_
  char c = 0;
#else
  int c = 0;
#endif
  
  while (UART2_GetFlagStatus(UART2_FLAG_RXNE) == RESET);
    c = UART2_ReceiveData8();
  return (c);
}

#ifdef USE_FULL_ASSERT

void assert_failed(uint8_t* file, uint32_t line)
{ 
  
  while (1)
  {
  }
}
#endif




该回答引用ChatGPT
根据提供的代码,这个程序主要实现了以下功能:
1. 配置了GPIO,将PE5和PC5、PC6、PC7设置为输出。
2. 实现了一个spi_transfer函数,用于在SPI总线上发送一个字节,并返回收到的字节,但是该函数实现还不完整,需要补充完整。
3. 实现了一个READ函数,用于读取一个寄存器的内容,该函数还没有完整实现,并且需要针对具体的寄存器读写操作进行修改。

下面是建议编写的spi_transfer和READ函数的代码:

c
// 通过SPI发送一个字节并返回所读到的字节
uint8_t spi_transfer(uint8_t data) {
uint8_t incoming = 0;

// 先输出起始边沿,然后每次将一个bit发送出去
SCK = 0;
for(int i=0; i<8; i++){
uint8_t bit = (data << i) & 0x80;
MISO = (bit == 0) ? 0 : 1;
SCK = 1;
incoming |= MISO << (7-i);
SCK = 0;
}

// 返回读取的数据
return incoming;
}

// 读取一个寄存器的内容
void READ(uint8_t regAddr) {
CSN = 0; // CSN拉低表示数据传输开始

// 按照寄存器读取命令格式,将命令、寄存器地址、读写位(0表示读)发送出去
uint8_t cmd = 0x03; // 命令格式:[0][0][1][1][A2][A1][A0][R/W]
uint8_t data = spi_transfer(cmd);
data = spi_transfer(regAddr);
data = spi_transfer(0x00);

CSN = 1; // 数据传输结束,CSN拉高

printf("Received data: 0x%x
", data);
}


需要注意的是,spi_transfer函数根据SPI协议的特性,依次将待发送的8个bit从高到低依次发送出去,并且同时接收从从机返回的数据,然后再将它们组装成一个字节返回。

READ函数可以看出,发送到寄存器的命令格式为:[0][0][1][1][A2][A1][A0][R/W],其中A2,A1,A0是寄存器地址的3个bit,R/W是读写位,为0表示读操作。所以,发送的3个字节分别是命令、寄存器地址和一个空字节(读操作不需要发送数据)。最后,等待从机响应并将收到的字节输出。

另外,需要注意的是,提供的代码中使用了printf输出调试信息,但是在IAR环境下需要手动配置printf函数的重定向,否则调试信息无法输出。具体操作可以参考IAR环境下的printf配置教程。

你少定义了个MOSI吧?

根据你提供的信息,你要使用3线spi,MSB先行,CPOL=0.CPHA=1,是属于mode 1。给你一些代码片段进行参考,由于没有必要的硬件环境测试,下面的代码没有经过测试,仅供参考:

// 使用软件模拟的方式发送SPI数据的函数
// 参数:
// data: 要发送的数据数组指针
// len: 要发送的数据长度(字节数)
void spi_write(uint8_t* data, uint8_t len) {
  for (int i = 0; i < len; i++) { // 对每个字节循环
    for (int j = 7; j >= 0; j--) { // 对每个位循环,MSB先行
      SCK = 0; // 拉低时钟线
      if (data[i] & (1 << j)) { // 判断当前位是否为1
        MOSI = 1; // 发送1
      } else {
        MOSI = 0; // 发送0
      }
      SCK = 1; // 拉高时钟线,上升沿采样数据
    }
  }
}

// 使用软件模拟的方式接收SPI数据的函数
// 参数:
// data: 存储接收到的数据的数组指针
// len: 要接收的数据长度(字节数)
void spi_read(uint8_t* data, uint8_t len) {
  for (int i = 0; i < len; i++) { // 对每个字节循环
    data[i] = 0; // 清零当前字节
    for (int j = 7; j >= 0; j--) { // 对每个位循环,MSB先行
      SCK = 0; // 拉低时钟线,准备发送空字节
      MOSI = 0; // 发送0
      SCK = 1; // 拉高时钟线,下降沿采样数据
      if (MISO) { // 判断当前位是否为1
        data[i] |= (1 << j); // 将当前位设为1
      }
    }
  }
}

// 用于通过SPI协议读取目标寄存器的数据的函数
// 参数:
// reg_addr: 寄存器地址,范围0x00-0xFF
// range: 范围数,范围0-15
// 返回值:
// 一个指向存储读取到的数据的数组的指针,数组长度为(range + 1) * 2 1个
uint8_t* spi_read_reg(uint8_t reg_addr, uint8_t range) {
  // 检查参数是否有效
  if (reg_addr > 0xFF || range > 15) {
    return NULL; // 返回空指针表示错误
  }
  // 创建一个数组来存储读取到的数据
  uint8_t* data = (uint8_t*)malloc((range + 1) * 2);
  if (data == NULL) {
    return NULL; // 返回空指针表示内存分配失败
  }
  // 构造Command_Word,bit15为1表示读,bit12-bit14为0,bit11-bit4为寄存器地址,bit3-bit0为范围数
  uint8_t command[2];
  command[0] = 0x80 | (reg_addr >> 4); // 高字节,MSB先行
  command[1] = (reg_addr << 4) | range; // 低字节,MSB先行
  // 开始SPI通讯
  CSN = 0; // 拉低CS引脚选择从设备
  spi_write(command, 2); // 发送Command_Word
  spi_read(data, (range + 1) * 2); // 接收Data_Word
  CSN = 1; // 拉高CS引脚结束通讯
  return data; // 返回数据数组指针
}



void main(void)
{
   // 必要的硬件初始化
   // 外设初始化
  
  #define REG_ADDR 0x01
  #define RANGE 2

  // 调用spi_read_reg函数,传入寄存器地址,范围数和CS引脚号
  uint8_t* data = spi_read_reg(REG_ADDR, RANGE);
  // 检查返回值是否为空指针
  if (data == NULL) {
    printf("SPI read error!\n");
    return -1;
  }
  // 处理读取到的数据,例如打印出来
  printf("SPI read data:\n");
  for (int i = 0; i < RANGE + 1; i++) {
    // 将两个字节合并为一个16位数据
    uint16_t value = (data[i * 2] << 8) | data[i * 2 + 1];
    printf("Data_Word %d: %04X\n", i, value);
  }
  // 释放数据数组占用的内存
  free(data);

  while (1)
  {

  }
 
}


1.使用逻辑分析仪来观察SPI线上的信号波形,检查时序和数据是否符合你要求。
2.在一些必要的地方进行printf打印调试。
祝您好运,早日调通~~

以下答案由GPT-3.5大模型与博主波罗歌共同编写:
首先需要了解一下SPI通信的时序和流程:

SPI总线上每次通信都由一个主设备和一个或多个从设备组成。主设备和从设备之间进行数据传输时需要一个时钟信号(SCK),以及三根传输线:主设备输出(MOSI)、主设备输入(MISO)以及从设备选择(SS)。在SPI总线上,同一时刻只有一个主设备可以选择一个从设备进行通信。SPI通信时序如下图所示:

SPI时序图

SPI信号时序采用上升沿采样,下降沿改变数据,因此在代码编写时需要注意时钟和数据信号的正负电平的控制。

以下是参考代码,注释中有详细的解释:

#include "stm8s.h"已黑化的小白 #include "stdio.h"已黑化的小白 
#define CSN PC_ODR_ODR6
#define SCK PC_ODR_ODR5
#define MISO PC_IDR_IDR7

void GPIO_init(void)
{
  PE_DDR = (1 << 5);  // 配置PE端口的方向寄存器PE5输出
  PE_CR1 = (1 << 5);  // 设置PE5为推挽输出

  PC_DDR = 0xe0;      // 设置PC5,6,7为输出,即SCK、CSN、MISO
  PC_CR1 = 0xe0;      // 设置PC5,6,7为推挽输出
}

/**
 * @brief SPI发送一个字节,并读取收到的字节
 * 
 * @param data 要发送的数据
 * @return uint8_t 收到的数据
 */
uint8_t spi_transfer(uint8_t data)
{
  uint8_t i = 0;
  uint8_t received_data = 0;

  // 发送数据
  for (i = 0; i < 8; i++) {
    // 先输出数据最高位
    if (data & 0x80) {
      PE_ODR |= (1 << 5);  // 发送高电平
    } else {
      PE_ODR &= (~(1 << 5));  // 发送低电平
    }

    data <<= 1;     // 左移1位,处理下一位数据

    // 发送时钟信号(上升沿)(注意SPI时序采用上升沿采样,下降沿改变数据)
    SCK = 1;
    SCK = 0;

    // 接收数据
    received_data <<= 1;  // 左移1位,等待下一位数据
    if (MISO == 1) {
      received_data |= 0x01;  // 如果MISO线上是高电平,则设置为1
    }
  }

  return received_data;  // 返回接收到的数据
}

/**
 * @brief 读取寄存器
 * 
 * @param addr 寄存器地址
 * @return uint8_t 寄存器值
 */
uint8_t READ(uint8_t addr)
{
  uint8_t value = 0;

  CSN = 0;  // 片选信号低电平有效

  // 发送命令和地址
  spi_transfer(0x0F);    // 读取命令
  spi_transfer(addr);    // 寄存器地址

  // 读取收到的值
  value = spi_transfer(0);   // 发送一个空数据,读取收到的数据

  CSN = 1;  // 片选信号高电平禁止

  return value;
}

void TIM1_init(void)
{
  TIM1_PSCRH = 0x1F;    // 8M系统时钟经预分频f=fck/(PSCR+1)
  TIM1_PSCRL = 0x3F;    // PSCR=0x1F3F,f=8M/(0x1F3F+1)=1000Hz,每个计数周期1ms
  TIM1_ARRH = 0x07;     // 自动重载寄存器ARR=0x01F4=500,0x01*0xF4=500
  TIM1_ARRL = 0xF4;     // 每记数500次产生一次中断,即500ms
  TIM1_IER = 0x01;      // 允许更新中断
  TIM1_CR1 = 0x01;      // 计数器使能,开始计数
}

void main(void)
{
  CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1);

  UART2_DeInit();

  UART2_Init((uint32_t)115200, UART2_WORDLENGTH_8D, UART2_STOPBITS_1, UART2_PARITY_NO,
             UART2_SYNCMODE_CLOCK_DISABLE, UART2_MODE_TXRX_ENABLE);

  printf("\n\rUART2 Example :retarget the C library printf()/getchar() functions to the UART\n\r");
  printf("\n\rEnter Text\n\r");

  GPIO_init();
  TIM1_init();
  asm("rim");  // 开全局中断

  while (1) {
    uint8_t value = READ(0x10);
    printf("Read value: 0x%02X\n", value);
  }
}

PUTCHAR_PROTOTYPE
{
  /* Write a character to the UART2 */
  UART2_SendData8(c);
  /* Loop until the end of transmission */
  while (UART2_GetFlagStatus(UART2_FLAG_TXE) == RESET);

  return (c);
}

/**
 * @brief 用于通过UART2接收数据
 * 
 * @return int 返回接收到的字符
 */
GETCHAR_PROTOTYPE
{
#ifdef _COSMIC_
  char c = 0;
#else
  int c = 0;
#endif

  while (UART2_GetFlagStatus(UART2_FLAG_RXNE) == RESET);
  c = UART2_ReceiveData8();
  return (c);
}

#ifdef USE_FULL_ASSERT

void assert_failed(uint8_t* file, uint32_t line)
{ 
  while (1)
  {
  }
}
#endif

需要注意的一些点:

  • 在此代码中,我们使用PE5输出SPI数据,由于该引脚为开漏输出,需要配置成推挽输出,具体的实现可以参考本代码中GPIO_init()函数的实现。
  • 注意在SPI通信中,主设备不能同时与多个从设备通信,因此要使用CSN信号来选择从设备。
  • 当前代码仅仅是进行了读取操作,写操作的实现需要考虑到其他设备在同一时刻对同一地址执行写操作的情况,为此需要参考哪些协议来实现并根据情况进行添加。
    如果我的回答解决了您的问题,请采纳!
不知道你这个问题是否已经解决, 如果还没有解决的话:
  • 这篇博客: 45、STM8 SPI工作总线原理中的 6、时钟信号的相位和极性 部分也许能够解决你的问题, 你可以仔细阅读以下内容或者直接跳转源博客中阅读:

      使用CPOL和CPHA位,能够组合成四种可能的时序关系。CPOL(时钟极性)位控制在没有数据传输时时钟的空闲状态电平,此位对主模式和从模式下的设备都有效。如果CPOL被清’0’,SCK引脚在空闲状态保持低电平;如果CPOL被置’1’,SCK引脚在空闲状态保持高电平。

      如果CPHA(时钟相位)位被置’1’,SCK时钟的第二个边沿(CPOL位为0时就是下降沿,CPOL位为1时就是上升沿)进行最高数据位的采样,数据在第一个时钟传输周期被锁存。如果CPHA位被清’0’,SCK时钟的第一边沿(CPOL位为0时就是下降沿,CPOL位为1时就是上升沿)进行数据位采样,数据在第二个时钟传输周期被锁存。

      CPOL时钟极性和CPHA时钟相位的组合选择数据捕捉的时钟边沿。下图显示了SPI传输的4种CPHA和CPOL位选择不同的组合主设备与从设备的SCK,MISO,MOSI引脚连接时这些管脚上的时序。

    在这里插入图片描述
    在这里插入图片描述


如果你已经解决了该问题, 非常希望你能够分享一下解决方案, 写成博客, 将相关链接放在评论区, 以帮助更多的人 ^-^