6 调用库接口函数实现ADC和仿真
6.1 CMSIS
从前面的串口程序可以看出,随着硬件复杂程度的增加,寄存器也相应的增加了不少。这意味着开发者要阅读更多的数据手册内容才能编写出相应的代码来,学习的成本越来越高。另一方面,在一个MCU上的代码通常是很难直接用到另一个不同型号的MCU上的,特别是在厂家不同的情况下,这意味这代码的复用性很差。这是之前的嵌入式软件开发的一个通病。自ARM推出Cortex-M系列的ARM核以来,就在努力统一外设的编程接口,力图使所有厂家的同一种类型的外设对上都提供一套统一的接口,这个标准就是CMSIS,以下是官方的简单说明。
Cortex Microcontroller Software Interface Standard (CMSIS)
CMSIS enables consistent device support and simple software interfaces to the processor and its peripherals, simplifying software reuse, reducing the learning curve for microcontroller developers, and reducing the time to market for new devices.
这是一个很大的进步,也是一个不断持续的过程。有了这套统一的接口后,开发者不用再关心硬件的实现细节,只需要知道这些接口函数有什么作用,需要怎样调用就可以了。就目前安装的版本来看,已经有下列外设的驱动统一了接口。
意法半导体也积极响应,将串口的驱动进行了标准化。
不过,这一定是一个漫长的过程,要所有的厂家都来遵守这个标准是比较难的。从上面的列表也可以看出,标准只是将最常用的外设进行了规范,厂家特有的外设也没有包含其中。所以各个厂家又会提供自己的软件包,以更好支持自己的产品系列。这个软件包以DFP的形式提供,官方的相关介绍如下:
我们之前导入的Keil.STM32F1xx_DFP.2.2.0.pack就是意法半导体为STM32F1系列的MCU提供的DFP形式的软件包。里面包含了该系列MCU的寄存器定义,启动代码,驱动代码等,这也就意味这我们可以使用这一套函数库接口来进行外设开发。
6.2 创建工程时选择添加外设驱动库代码
之前创建工程时,在这个对话框中我们只选择了如下两个软件组件。
![](/qrcode.jpg)
在这里我们需要添加ADC。
勾选ADC后,系统提示ADC依赖于另外两个软件组件,分别是Framework和RCC,于是我们将被依赖的组件也勾选上。因为会涉及到管脚配置,所以将GPIO也选上。
然后按照前面的步骤继续创建工程。
6.3 编写代码
adc.h
#ifndef ADC_H
#define ADC_H
void adc_init(void);
unsigned short adc_get_val(void);
#endif
adc.c
#include "stm32f10x.h" // Device header
#include "stm32f10x_rcc.h" // Keil::Device:StdPeriph Drivers:RCC
#include "stm32f10x_gpio.h" // Keil::Device:StdPeriph Drivers:GPIO
#include "stm32f10x_adc.h" // Keil::Device:StdPeriph Drivers:ADC
void adc_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //12MHZ
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
}
unsigned short adc_get_val(void)
{
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));
return ADC_GetConversionValue(ADC1);
}
main.c
#include "adc.h"
float val;
int main(void)
{
adc_init();
while (1) {
val = adc_get_val() * 3.3 / 4096.0;
}
}
从上面的代码可以看到库函数带来的便利性,比如
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
就同时使能了ADC和GPIOA的时钟。这样就不用去关心寄存器的内容了,开发者从读懂芯片手册转变为了读懂函数接口。
6.3 仿真
编译一个调试的初始化脚本debug.ini,保存在工程根目录下即可。 该文件用于模拟产生ADC 的输 入模拟信号,为一正弦波,代码如下。
SIGNAL void AIN1_Sine (void) {
float volts; // peak-to-peak voltage
float duration; // duration in Seconds
float val;
long i, end;
volts = 1.4;
duration = 0.1;
printf ("Sine Wave Signal on AD Channel 1.\n");
end = (duration * 1000000);
for (i = 0 ; i < end; i++) {
val = __sin ((((float) i) / 360.0) * 2 * 3.1415926);
ADC1_IN1 = (val * volts) + 1.4;
swatch (0.0001); // in 100 uSec steps
}
}
DEFINE BUTTON "AIN1 Sin","AIN1_Sine()"
这个脚本会在调试开始时自动执行,上面定义了一个按钮,当该按钮被点击时,AIN1_Sine函数会被调用,然后产生ADC输入的正弦波信号。
按照前面的步骤对工程进行仿真设置,但这里要指定初始化脚本的路径。
设置好后,点击调试按钮开始调试。
因为我们将采样转换得到的值保存在val变量中的,要观察该值,需要在逻辑分析仪中加入该变量。
点击run按钮开始运行程序,之后点击Toolbox对话框上的AIN1 Sin按钮,产生ADC输入的正弦波,仿真结果如下。