18-CubeMx+Keil+Proteus仿真STM32 - DAC

本文例子参考《STM32单片机开发实例——基于Proteus虚拟仿真与HAL/LL库》
源代码:https://github.com/LanLinnet/STM32F103R6

项目要求

在SPI总线通信的基础上,使用单片机控制DAC芯片MCP4921以1秒为周期输出正弦波,正弦波的波动范围为0-3.3V。

硬件设计

  1. 第一节的基础上,在Proteus中添加电路如下图所示。其中我们添加了一个DAC芯片MCP4921

    此外,我们还添加了两个虚拟仪表:一个示波器OSCILLOSCOPE和一个SPI总线调试工具SPI DEBUGGER

  2. MCP4921:
    1)简介:STM32F103R6单片机本身不自带DAC,如果设计到数模转换的项目,可以选择DAC芯片MCP4921。MCP4921是美国Microchip公司的串行12位DAC芯片,兼容SPI,最高通信频率为20MHz,一次转换时间为4.5μs,工作电压为2.7-5.5V。
    2)引脚:MCP4921引脚的功能如下表所示。

    3)通信数据格式:MCP4921只有数据输入,没有数据输出,单片机只需要将16位数据(12位数字量和4位配置信息)一起打包发给DAC芯片,DAC随即开始数模转换过程。MCP4921通信数据格式如下表所示。

    • \(\overline A/B\)位:对于MCP4921,由于只有A通道,所以该位只能选0。
    • BUF位:参考电压\(V_{REF}\)输入缓冲器控制位,设1时缓冲,设0时未缓冲。
    • \(\overline{GA}\)位:输出增益选择位,设1时无增益,设0时两倍增益。
    • \(\overline{SHDN}\)位:待机模式设置为,设1时不进入待机模式,设0时进入待机模式。
  3. 正弦波形的生成:
    1)存在问题:MCP4921是12位DAC芯片,因此输入数字量的范围是0x000-0x3FF,输出模拟量电压范围为0-\(V_{REF}\),即无法输出负电压,那么就无法输出完整的正弦波形。
    2)解决方案:

    • 通过外围元器件搭建调理电路使电路能够输出负电压。
    • 将正弦波信号沿纵轴(电压/数字量)正向移动,确保波谷也位于横轴(时间)的上方。

    3)采样表:这里我们选择后一个方案,可以推出正弦波计算公式为
    \(D=512\times\sin\left(2\pi\;t\right)+512\)
    为了提高单片机CPU的执行效率,这里我们使用查表法。在1秒内,每隔0.02秒计算一次采样值,其采样表如下表所示。

  4. 打开CubeMX,建立工程。STM32F103R6单片机自带一个SPI模块,但是为了便于移植,本项目中采用GPIO引脚模拟SPI时序。设置PA4、PA5、PA7均为GPIO_Output点击“Categories”中的“GPIO”,修改GPIO各参数如下图所示。有关SPI通信部分可以参考第17节

  5. 点击“Generator Code”生成Keil工程。

软件编写

  1. 考虑到代码的可移植性,这里将SPI和MCP4921的驱动代码全部封装成函数并分别归入头文件“vSPI.h”和“MCP4921.h”中。我们可以先在...\Core\Src文件夹中建立这两个头文件,此时Keil可能找不到对应文件,可以直接将文件拽入Keil中进行编辑,然后再在“main.c”文件中进行include。

  2. 点击“Open Project”在Keil中打开工程,打开“vSPI.h”,添加代码如下。

    #ifndef INC_VSPI_H_
    #define INC_VSPI_H_
    
    #include "main.h"
    
    //软件延时函数,单位为微秒
    void delay_us(uint16_t n)
    {
      uint16_t i = n * 8;
      while(i--);
    }
    
    //SPI总线使能
    void vSPI_En()
    {
      HAL_GPIO_WritePin(GPIOA, vnCS_Pin, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOA, vSCK_Pin, GPIO_PIN_RESET);
      delay_us(4);
    }
    
    //SPI总线禁止
    void vSPI_Dis()
    {
      HAL_GPIO_WritePin(GPIOA, vSCK_Pin, GPIO_PIN_SET);
      HAL_GPIO_WritePin(GPIOA, vnCS_Pin, GPIO_PIN_SET);
    }
    
    //SPI主站发送1字节
    void vSPI_SndByte(uint8_t dat)		//dat表示发送的字节
    {
      uint8_t i;
      for(i=0; i<8; i++)
      {
        HAL_GPIO_WritePin(GPIOA, vSCK_Pin, GPIO_PIN_RESET);
        delay_us(4);
        if(dat & 0x80)
        {
          HAL_GPIO_WritePin(GPIOA, vMOSI_Pin, GPIO_PIN_SET);
        }
        else 
          HAL_GPIO_WritePin(GPIOA, vMOSI_Pin, GPIO_PIN_RESET);
        dat<<=1;
        //上升沿
        HAL_GPIO_WritePin(GPIOA, vSCK_Pin, GPIO_PIN_SET);
        delay_us(4);
      }
    }
    
    #endif /* INC_VSPI_H_ */
    

    打开“MCP4921.h”,添加代码如下。

    #ifndef INC_MCP4921_H_
    #define INC_MCP4921_H_
    
    #include "main.h"
    #include "vSPI.h"
    
    //写入MCP4921: Cmd-指令(仅高4位)  Dat-数据(12位)
    void MCP4921Write(uint8_t Cmd, uint16_t Dat)
    {
      uint8_t DatM, DatL;		//数据高字节、低字节
      DatL = (uint8_t)(Dat & 0x00ff);	
      DatM = (uint8_t)((Dat>>8) & 0x00ff);
      vSPI_En();		//SPI总线使能
      vSPI_SndByte(0x70|DatM);		//先写高字节
      vSPI_SndByte(DatL);		//再写低字节
      vSPI_Dis();		//SPI总线禁止
    }
    #endif /* INC_MCP4921_H_ */
    
  3. 随后我们需要在main.c文件中的最前面引入我们自定义的头文件

    /* Private includes ----------------------------------------------------------*/
    /* USER CODE BEGIN Includes */
    #include "vSPI.h"		//引入自定义头文件
    #include "MCP4921.h"
    /* USER CODE END Includes */
    

    在全局中定义正弦波输出的表

    /* USER CODE BEGIN PV */
    //查表法
    static uint16_t tD[50] = 
    {
      512, 576, 639, 700, 759, 813, 862, 907, 944, 975,
      999, 1015, 1023, 1023, 1015, 999, 975, 944, 907, 862,
      813, 759, 700, 639, 576, 512, 448, 385, 324, 265, 
      211, 162, 117, 80, 49, 25, 9, 1, 1, 9,
      25, 49, 80, 117, 162, 211, 265, 324, 385, 448
    };
    /* USER CODE END PV */
    

    最后,在main函数中定义循环变量,并调用我们自定义的函数每隔20ms计算一次采样值并输出

    /* USER CODE BEGIN 1 */
    int i;		//循环变量i
    /* USER CODE END 1 */
    
    /* USER CODE BEGIN WHILE */
    while (1)
    {
      for(i=0; i<50; i++)
      {
        MCP4921Write(0x70, tD[i]);
        HAL_Delay(20);		//每隔20ns计算(输出)1次采样值
      }
    /* USER CODE END WHILE */
    
    /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
    

联合调试

  1. 点击运行,生成HEX文件。
  2. 在Proteus中加载相应HEX文件,点击运行。可以看到示波器中显示的波形为正弦波(注意示波器的调整)。
posted @ 2022-05-31 11:48  Sheepeach  阅读(2686)  评论(0编辑  收藏  举报