这是我参与8月更文挑战的第14天,活动详情查看:8月更文挑战
当STM8单片机使用ADC功能读取多个通道的值时,可以使用连续模式,但是连续模式一次只能采样一个通道的值,那么如果要采样多个通道时怎么办呢?STM8提供了一个多通道连续采样扫描模式。也就是说多个通道采样时才有扫描模式,从第0通道开始依次向后扫描,扫描完成后会自动开始第二次扫描。而不像单次扫描模式那样,依次结束后ADC就会自动关闭,需要手动开启第二次扫描。连续扫描模式只需要开启一次,就会一直重复的采样,直到手动关闭,才会结束。
连续扫描模式相比单次扫描模式,在初始化的时候,只是多了一行代码,就是设置COUNT位为1.
下面开始分析要使用连续扫描模式,需要设置哪些寄存器的哪些位。
首先要设置最大的采样通道。单次采样的时候,ADC_CSR寄存器中的通道号指的是要采样的通道号,要采样那个通道就设置为几,而在扫描模式下,这个通道号指的是要扫描的最大通道号,扫描都是从0通道开始。比如想要扫描通道2、和通道3。那么这里的通道值就要设置要采样的最大通道值,也就是要设置为3,那么系统就会从0通道开始扫描到通道3结束,就算通道0和通道1没有用到,系统依然会扫描。
接下来看ADC_CR1寄存器,这个寄存器要设置的有两个,就是ADON位和COUNT位,ADON位用来控制ADC的转换开关,为1时启动转换功能,为0时关闭转换功能。COUNT位用来开启连续转换模式。预分频位默认值为0,刚好符合我们的需求,也就是默认2分频。
接下来看ADC_CR2寄存器,这个寄存器必须设置的位其实只有一个,就是要要使用扫描模式必须设置SCAN位为1,其他位使用默认值就行。如果数据对齐方式使用左对齐的话,就不用设置。这里使用的是数据右对齐,所以需要将ALIGN位也设置为1.其他位默认为0.
最后就是这个ADC_TDR寄存器,用来禁止施密特触发器,主要是用来降低单片机功耗。当然这个寄存器不用设置也可以。如果要设置的话,使用了哪几个ADC通道,就将对应的通道位置1就行。
下面就可以开始编写代码了:
#include "adc.h"
#include "main.h"
#include "led.h"
_Bool ADC_flag = 0; //ADC转换成功标志
u16 ADC_DB[10] = {0};
u16 adc_data[5] = {0};
//AD通道引脚初始化
void ADC_GPIO_Init( void )
{
PD_DDR &= ~( 1 << 2 ); //PD2 设置为输入
PD_CR1 &= ~( 1 << 2 ); //PD2 设置为悬空输入
PD_DDR &= ~( 1 << 3 ); //PD3 设置为输入
PD_CR1 &= ~( 1 << 3 ); //PD3 设置为悬空输入
PC_DDR &= ~( 1 << 4 ); //PC4 设置为输入
PC_CR1 &= ~( 1 << 4 ); //PC4设置为悬空输入
}
//设置为 连续扫描模式
//ch 为ADC通道 连续转换AIN0---AINch 通道的数据
void ADC_CH_Init( u8 ch )
{
char l = 0;
ADC_GPIO_Init();
ADC_CR1 &= ~( 7 << 4 ); //预分频 2
ADC_CR2 &= ~( 1 << 6 ); //不使用外部触发
//禁止 AIN2 AIN4 的施密特触发器,降低 IO 静态功耗 PD5,PD6 上的通道如果施密特方式禁用会导致串口无法收发数据!
ADC_TDRL |= ( 1 << 2 );
ADC_TDRL |= ( 1 << 3 );
ADC_TDRL |= ( 1 << 4 );
ADC_CR1 |= ( 1 << 1 ); //连续转换
ADC_CSR |= 0x04; //配置通道号最大的那个
ADC_CR2 |= ( 1 << 3 ); //右对齐
ADC_CR1 |= ( 1 << 0 ); //开启 ADC
ADC_CR2 |= ( 1 << 1 ); // SCAN = 1 开启扫描模式
//当首次置位ADON位时,ADC从低功耗模式唤醒。为了启动转换必须第二次使用写指令来置位ADC_CR1寄存器的ADON位。
for( l = 0; l < 10; l++ ); //延时,保证ADC模块的上电完成 至少7us
ADC_CR1 |= ( 1 << 0 ); //再次将CR1寄存器的最低位置1 使能ADC 并开始转换
}
/*
注意:在扫描模式(连续扫描模式)中,不要使用位操作指令(BRES)去清除EOC标志位,
这是因为该指令是对整个ADC_CSR寄存器的一个读-修改-写操作。
从CH[3:0]寄存器中读取当前的通道编号和写回该寄存器,将会改变扫描系列的最后通道编号。
在连续扫描模式中正确的清除EOC标志位的方法是 个RAM变量中载入一个字节到ADC_CSR寄存器,
这样来清除EOC标志位同时还重新载入扫描系列新的最后通道编号。
实验发现,位操作指令只在连续扫描模式中会清除CH[3:0]寄存器中的值,但并不影响其他值。
因此将ADC_CSR中的值读出,再将CH[3:0]中原来通道号加入进去,最后重新写入ADC_CSR中即可。写法如下:
ADC1->CSR = (uint8_t)(ADC1->CSR &(~ADC1_FLAG_EOC)|ADC1_CHANNEL_n);
注:ADC1_CHANNEL_n表示扫描到那个通道结束。
*/
u16 ain2_val = 0,ain3_val = 0,ain4_val = 0;
//读取采样电压值
u16 ReadVol_CHx( void )
{
u16 voltage = 0;
u16 temph = 0;
u8 templ = 0;
while( 1 )
{
LED = !LED; //程序运行一圈耗时 10us
while( ( ADC_CSR & 0x80 ) == 0 ); //等待转换结束
//ADC_CSR &= ~( 1 << 7 ); // 不能通过位操作来清零 EOC 标志
ADC_CSR = ADC_CSR & 0x7F | 0x04; // 转换结束标志位清零 EOC
//读取 AIN2 的值
templ = ADC_DB2RL;
temph = ADC_DB2RH;
temph = ( u16 )( templ | ( u16 )( temph << ( u16 )8 ) );
ain2_val = temph;
//读取 AIN3 的值
templ = ADC_DB3RL;
temph = ADC_DB3RH;
temph = ( u16 )( templ | ( u16 )( temph << ( u16 )8 ) );
ain3_val = temph;
//读取 AIN4 的值
templ = ADC_DB4RL;
temph = ADC_DB4RH;
temph = ( u16 )( templ | ( u16 )( temph << ( u16 )8 ) );
ain4_val = temph;
}
return voltage;
}
这里要注意的地方有两个:
一是在扫描模式时,采样的数据结果不是存放在ADC_DR寄存器中,而是存放在ADC_DBxR寄存器中,通道几就存放在对应的ADC_DBxR寄存器中,这个寄存器分为高位和低位两个。比如通道2的数据,就存放在ADC_DB2RL和ADC_DB2RH寄存器中,在读取数据的时候也要注意,如果数据是左对齐必须先读高8位,再读低位。如果数据是右对齐必须先读低8位,在读高8位。
数据对齐在官方手册中有详细的说明,在使用的时候要注意这一点。
二是在数据读取完成后,清除EOC标志位的时候,千万不能使用位清除的方式来进行,比如在单次采样的时候,清除标志位的方式通常是 ADC_CSR &= 0x7F; 直接将最高位清0. 在连续扫描模式下,这样清除标志位就会出问题。因为位操作ADC_CSR寄存器时,系统会进行一个读 - 修改 - 写的操作,那么在读改写的过程中,会改变通道号。而通道在连续扫描模式下会自动变化,这样的话,位操作就会打乱扫描通道的通道号操作,导致采样错误。那么要如何清除这个标志位呢? 官方文档中这样说道:在连续扫描模式中正确的清除EOC标志位的方法是 个RAM变量中载入一个字节到ADC_CSR寄存器,这样来清除EOC标志位同时还重新载入扫描系列新的最后通道编号。
这样听起来有些抽象,简单的说,不要对寄存器进行读改写的操作,而是直接一次性的给ADC_CSR写入它需要的值。这里采用的方式就是
ADC_CSR = ADC_CSR & 0x7F | 0x04;
将寄存器的最高清0,然后重新设置一次转换通道号,将这些值一次性的写入寄存器中,这样就ADC_CSR就会在一轮扫描结束后重新设置它的值,然后从0开始继续扫描,这样就不会打断ADC的通道扫描过程了。
官方文档中对这里也有说明,在使用的时候要注意这一点。
上面的代码是通常查询标志位的方式读取数据的,也可以使用中断的方式来读取数据。
#include "adc.h"
#include "main.h"
#include "led.h"
_Bool ADC_flag = 0; //ADC转换成功标志
u16 ADC_DB[10] = {0};
u16 adc_data[5] = {0};
//AD通道引脚初始化
void ADC_GPIO_Init( void )
{
PD_DDR &= ~( 1 << 2 ); //PD2 设置为输入
PD_CR1 &= ~( 1 << 2 ); //PD2 设置为悬空输入
PD_DDR &= ~( 1 << 3 ); //PD3 设置为输入
PD_CR1 &= ~( 1 << 3 ); //PD3 设置为悬空输入
PC_DDR &= ~( 1 << 4 ); //PC4 设置为输入
PC_CR1 &= ~( 1 << 4 ); //PC4设置为悬空输入
}
//设置为 连续扫描模式
//ch 为ADC通道 连续转换AIN0---AINch 通道的数据
void ADC_CH_Init( u8 ch )
{
char l = 0;
ADC_GPIO_Init();
ADC_CR1 &= ~( 7 << 4 ); //预分频 2
ADC_CR2 &= ~( 1 << 6 ); //不使用外部触发
//禁止 AIN2 AIN4 的施密特触发器,降低 IO 静态功耗 PD5,PD6 上的通道如果施密特方式禁用会导致串口无法收发数据!
ADC_TDRL |= ( 1 << 2 );
ADC_TDRL |= ( 1 << 4 );
ADC_CR1 |= ( 1 << 1 ); //连续转换
ADC_CSR |= 0x04; //配置通道号最大的那个
ADC_CR2 |= ( 1 << 3 ); //右对齐
ADC_CR1 |= ( 1 << 0 ); //开启 ADC
ADC_CR2 |= ( 1 << 1 ); // SCAN = 1 开启扫描模式
ADC_CSR |= ( 1 << 5 ); //EOCIE 使能转换结束中断
//当首次置位ADON位时,ADC从低功耗模式唤醒。为了启动转换必须第二次使用写指令来置位ADC_CR1寄存器的ADON位。
for( l = 0; l < 10; l++ ); //延时,保证ADC模块的上电完成 至少7us
ADC_CR1 |= ( 1 << 0 ); //再次将CR1寄存器的最低位置1 使能ADC 并开始转换
}
/*
注意:在扫描模式(连续扫描模式)中,不要使用位操作指令(BRES)去清除EOC标志位,
这是因为该指令是对整个ADC_CSR寄存器的一个读-修改-写操作。
从CH[3:0]寄存器中读取当前的通道编号和写回该寄存器,将会改变扫描系列的最后通道编号。
在连续扫描模式中正确的清除EOC标志位的方法是 个RAM变量中载入一个字节到ADC_CSR寄存器,
这样来清除EOC标志位同时还重新载入扫描系列新的最后通道编号。
实验发现,位操作指令只在连续扫描模式中会清除CH[3:0]寄存器中的值,但并不影响其他值。
因此将ADC_CSR中的值读出,再将CH[3:0]中原来通道号加入进去,最后重新写入ADC_CSR中即可。写法如下:
ADC1->CSR = (uint8_t)(ADC1->CSR &(~ADC1_FLAG_EOC)|ADC1_CHANNEL_n);
注:ADC1_CHANNEL_n表示扫描到那个通道结束。
*/
u16 ain2_val = 0, ain3_val = 0, ain4_val = 0;
u16 temph = 0;
u8 templ = 0;
//读取采样电压值
u16 ReadVol_CHx( void )
{
u16 voltage = 0;
if( ADC_flag == 1 )
{
ADC_flag = 0;
//单通道扫描模式,转换结果存储在 ADC_DBxR 寄存器中
//读取 AIN2 的值
templ = ADC_DB2RL;
temph = ADC_DB2RH;
temph = ( u16 )( templ | ( u16 )( temph << ( u16 )8 ) );
ain2_val = temph;
//读取 AIN3 的值
templ = ADC_DB3RL;
temph = ADC_DB3RH;
temph = ( u16 )( templ | ( u16 )( temph << ( u16 )8 ) );
ain3_val = temph;
//读取 AIN4 的值
templ = ADC_DB4RL;
temph = ADC_DB4RH;
temph = ( u16 )( templ | ( u16 )( temph << ( u16 )8 ) );
ain4_val = temph;
}
return voltage;
}
//AD中断服务函数 中断号22
#pragma vector = 24 // IAR中的中断号,要在STVD中的中断号上加2
__interrupt void ADC_Handle( void )
{
//ADC_CSR &= ~( 1 << 7 ); // 不能通过位操作来清零 EOC 标志
ADC_CSR = ADC_CSR & 0x7F | 0x04; // 转换结束标志位清零 EOC
ADC_flag = 1; // ADC中断标志 置1
}
当通道一次扫描完成后,会产生一个中断,然后去对应的ADC_DBxR寄存器中读取通道值就可以了,读取完成后,不需要手动开启ADON标志位,系统会自动启动下一次转换。