18-CubeMx+Keil+Proteus仿真STM32 - DAC
本文例子参考《STM32单片机开发实例——基于Proteus虚拟仿真与HAL/LL库》
源代码:https://github.com/LanLinnet/STM32F103R6
项目要求
在SPI总线通信的基础上,使用单片机控制DAC芯片MCP4921以1秒为周期输出正弦波,正弦波的波动范围为0-3.3V。
硬件设计
-
在第一节的基础上,在Proteus中添加电路如下图所示。其中我们添加了一个DAC芯片
MCP4921
。
此外,我们还添加了两个虚拟仪表:一个示波器OSCILLOSCOPE
和一个SPI总线调试工具SPI DEBUGGER
。
-
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时进入待机模式。
-
正弦波形的生成:
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秒计算一次采样值,其采样表如下表所示。
-
打开CubeMX,建立工程。STM32F103R6单片机自带一个SPI模块,但是为了便于移植,本项目中采用GPIO引脚模拟SPI时序。设置PA4、PA5、PA7均为
GPIO_Output
点击“Categories”中的“GPIO”,修改GPIO各参数如下图所示。有关SPI通信部分可以参考第17节。
-
点击“Generator Code”生成Keil工程。
软件编写
-
考虑到代码的可移植性,这里将SPI和MCP4921的驱动代码全部封装成函数并分别归入头文件“vSPI.h”和“MCP4921.h”中。我们可以先在
...\Core\Src
文件夹中建立这两个头文件,此时Keil可能找不到对应文件,可以直接将文件拽入Keil中进行编辑,然后再在“main.c”文件中进行include。 -
点击“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_ */
-
随后我们需要在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 */
联合调试
- 点击运行,生成HEX文件。
- 在Proteus中加载相应HEX文件,点击运行。可以看到示波器中显示的波形为正弦波(注意示波器的调整)。