第2-18讲:直接存储器访问(DMA)
-
- 学习目的
- 掌握STC8A8K64D4系列单片机DMA的特点以及编程方法。
- 学习STC8A8K64D4系列单片机串口和ADC使用DMA的编程方法。
- DMA简介
DMA全称是Direct Memory Access,即直接存储器访问。DMA的作用是从一个地址空间在无需CPU干预的情况下将数据“搬运”到另一个地址空间,由此实现外设和存储器之间或者存储器和存储器之间的高速数据传输。
对比一下使用DMA和不使用DMA时串口接收并将数据存储到数组的操作,便于我们理解DMA的功能。
- 普通操作:串口接收到数据后,产生中断,中断服务函数中,软件从串口硬件接收FIFO中读出数据并将数据保存到内存。这个过程中,CPU参与了读取数据、保存数据到数组,串口中断需要保护现场和恢复现场等。
- DMA操作:初始化时配置好串口DMA的源地址、目的地址和接收数据长度并启动DMA传输,之后,串口接收到数据后会自动存入目的地址,此过程无需CPU干预,也没有中断保护现场和恢复现场的过程。
由此可见,DMA的优点是速度快和无需CPU干预,使用DMA可以有效降低CPU的负载。
- STC8A8K64D4支持的CPU操作
STC8A8K64D4并不是所有的外设都支持DMA的,如I2C总线就是不支持DMA的,STC8A8K64D4支持的DMA操作如下:
- M2M_DMA: XRAM 存储器到 XRAM 存储器的数据读写
- ADC_DMA:自动扫描使能的 ADC 通道并将转换的 ADC 数据自动存储到 XRAM 中
- SPI_DMA:自动将 XRAM 中的数据和 SPI 外设之间进行数据交换
- UR1T_DMA:自动将 XRAM 中的数据通过串口 1 发送出去
- UR1R_DMA:自动将串口 1 接收到的数据存储到 XRAM 中
- UR2T_DMA:自动将 XRAM 中的数据通过串口 2 发送出去
- UR2R_DMA:自动将串口 2 接收到的数据存储到 XRAM 中
- UR3T_DMA:自动将 XRAM 中的数据通过串口 3 发送出去
- UR3R_DMA:自动将串口 3 接收到的数据存储到 XRAM 中
- UR4T_DMA:自动将 XRAM 中的数据通过串口 4 发送出去
- UR4R_DMA:自动将串口 4 接收到的数据存储到 XRAM 中
- LCM_DMA: 自动将 XRAM 中的数据和 LCM 设备之间进行数据交换
- STC8A8K64D4的DMA数据传输长度
STC8A8K64D4一次DMA数据传输的最大数据量为 256 字节。
- STC8A8K64D4的DMA中断优先级
- 每种 DMA 对 XRAM 的读写操作都可设置 4 级访问优先级,硬件自动进行 XRAM 总线的访问仲裁,不会影响 CPU 的 XRAM 的访问。
- 相同优先级下,不同 DMA 对 XRAM 的访问顺序如下:SPI_DMA,UR1R_DMA, UR1T_DMA,UR2R_DMA,UR2T_DMA,UR3R_DMA,UR3T_DMA, UR4R_DMA,UR4T_DMA, LCM_DMA, M2M_DMA, ADC_DMA。
- 软件设计
- 串口DMA收发数据实验
- 软件设计
- 注:本节的实验是在“实验2-6-1:串口1数据收发实验”的基础上修改,本节对应的实验源码是:“实验2-18-1:串口1使用DMA收发数据”。
- 关于串口相关的内容,读者可以参阅《第2-6讲:串口通信》,这里,我们关注的是串口使用DMA通信。
-
-
- 实验内容
-
-
串口1使用DMA接收电脑端串口调试助手发过来的数据,并将接收到的数据原样返回,数据传输长度为20个字节。
- 本实验旨在让读者理解串口DMA的使用,没有考虑串口接收不定长数据(这个不属于串口DMA自身的功能),在下一个实验里面会处理串口不定长数据的接收。
-
-
- 代码编写
-
-
- 新建一个名称为“uart_dma.c”的文件及其头文件“uart_dma.h”并保存到工程的“Source”文件夹,并将“uart_dma.c”加入到Keil工程中的“SOURCE”组。
- 引用头文件
因为在uart_dma.c”文件中使用了“uart_dma.c”文件中的函数,所以需要引用下面的头文件“uart_dma.h”。
代码清单:引用头文件
- //引用uart_dma的头文件
- #include " uart_dma.h"
- 串口DMA初始化
使用串口DMA之前,需要对DMA进行相应的配置,包含中断配置、串口发送DMA和接收DMA的配置。初始化时需要注意以下几点:
- 串口DMA发送时DMA的源地址以及串口DMA接收时DMA的目的地址必须位于XRAM,即我们定义的用于DMA发送或接收的数组必须用关键字“xdata”修饰。
- 串口DMA发送和接收的实际数据长度(字节数)等于传输总字节寄存器设置的值加1,如串口发送总字节寄存器设置的值是“1”,那么,实际发送传输长度是2,也就是发送两个字节后才会置位发送中断请求标志位。
- 串口接收具有随机性,因此,初始化函数中应启动DMA接收,即初始化完成后即具备数据接收功能。至于发送,则不需要在初始化函数中启动,在有数据需要发送的时候启动即可。
串口DMA初始化配置代码清单如下。
代码清单:定义串口发送和接收数组
- u8 xdata uart_rx[UART_BUF_LEN]; //定义串口接收缓存数组,位于XRAM区域
- u8 xdata uart_tx[UART_BUF_LEN]; //定义串口接收缓存数据,位于XRAM区域
代码清单:串口DMA初始化
- /**********************************************************************************
- 功能描述:串口1 DMA配置。因为接收具有不确定性,因此,串口接收初始化函数中启动了DMA接收.
- :串口发送无需预先启动,在发送数据的时候启动即可。
- 参 数:无
- 返 回 值:无
- ***********************************************************************************/
- void uart1_dma_config(void)
- {
- P_SW2 = 0x80;
- DMA_UR1T_CFG = 0x80; //开启UART1 DMA发送中断
- DMA_UR1T_STA = 0x00; //UART1 DMA发送状态寄存器清零
- DMA_UR1T_AMT = DMA_AMT_LEN; //设置传输总字节数: n+1
- DMA_UR1T_TXA = uart_tx; //设置UART1 DMA发送源地址
- DMA_UR1R_CFG = 0x80; //开启UART1 DMA接收中断
- DMA_UR1R_STA = 0x00; //UART1 DMA接收状态寄存器清零
- DMA_UR1R_AMT = DMA_AMT_LEN; //设置传输总字节数: n+1
- DMA_UR1R_RXA = uart_rx; //设置UART1 DMA接收目的地址
- DMA_UR1R_CR = 0xA1; //启动UART1 DMA接收,清除 FIFO
- }
- 串口DMA中断
串口DMA中断服务函数中,使用变量“uart1_tx_complete_flag”标记串口DMA发送完成,变量“uart1_rx_complete_flag”标记串口DMA接收完成。
当串口DMA发送完指定长度的数据后,会触发DMA发送完成中断,当串口DMA接收到指定长度的数据(本例中配置的是20个字节),会触发DMA接收完成中断。中断服务函数中读取串口DMA状态寄存器,以此判断中断类型,然后根据中断类型置位“uart1_tx_complete_flag”或“uart1_rx_complete_flag”,应用程序即可通过这两个标志判断串口DMA发送是否完成和串门DMA是否接收到数据,代码清单如下。
代码清单:串口DMA中断服务函数
- /**********************************************************************************
- 功能描述:DMA中断服务程序。因为DMA中断号大于31,所以这里借用了13号中断
- 参 数:无
- 返 回 值:无
- ***********************************************************************************/
- void UART1_DMA_Interrupt(void) interrupt 13
- {
- if (DMA_UR1T_STA & 0x01) //发送完成
- {
- DMA_UR1T_STA &= ~0x01;
- uart1_tx_complete_flag = 1; //DMA发送完成标志置位
- }
- if (DMA_UR1T_STA & 0x04) //数据覆盖
- {
- DMA_UR1T_STA &= ~0x04;
- }
- if (DMA_UR1R_STA & 0x01) //接收完成
- {
- DMA_UR1R_STA &= ~0x01;
- uart1_rx_complete_flag = 1; //DMA接收完成标志置位
- }
- if (DMA_UR1R_STA & 0x02) //数据丢弃
- {
- DMA_UR1R_STA &= ~0x02;
- }
- }
- 串口DMA发送数据
串口DMA发送数据时,设置好发送的数据长度、串口DMA发送源地址,之后启动DMA发送即可。串口DMA发送数据需要注意以下几点:
- DMA发送源地址必须位于XRAM;
- 串口DMA一次传输数据的长度范围为1~256个字节,串口DMA实际发送的数据长度是发送长度配置寄存器设置的值加1,如“DMA_UR1T_AMT”设置的值为0,实际串口1的DMA发送的数据长度是1个字节。
串口DMA发送的代码清单如下:
代码清单:串口DMA发送数据
- /**********************************************************************************
- 功能描述:串口1DMA发送指定长度的数据
- 参 数:p_buf[in]:发送的数据
- :length:发送的数据长度(字节数量),范围:1~256
- 返 回 值:无
- ***********************************************************************************/
- u8 uart1_dma_send(u8 *p_buf,u16 length)
- {
- if((length == 0) || (length > 255))//检查发送的数据长度是否合法
- {
- return ERR_SEND_LENGTH;
- }
- DMA_UR1T_AMT = (u8)(length-1); //设置UART1传输总字节数:n+1
- DMA_UR1T_TXA = (u16)p_buf; //设置UART1 DMA发送源地址
- DMA_UR1T_CR = 0xC0; //启动UART1_DMA发送
- return SUCCESS;
- }
- 串口DMA接收数据
主循环中查询到变量“uart1_rx_complete_flag”置位,表示串口已经接收到20个字节的数据,并且这20个字节已经存入到我们定义在XRAM区域的串口DMA接收缓存数组“uart_rx”。这时,我们可以将数据拷贝到串口DMA发送数组“uart_tx”,并通过串口DMA将数据原样发回。
代码清单:串口DMA接收数据
- /**********************************************************************************
- 功能描述:查询串口DMA接收标志“uart1_rx_complete_flag”,如果置位,读出接收的数据并原样返回
- 参 数:无
- 返 回 值:无
- ***********************************************************************************/
- void uart1_dma_handle(void)
- {
- if(uart1_rx_complete_flag == 1)//接收标志置位
- {
- uart1_rx_complete_flag = 0; //接收标志清零
- memcpy(uart_tx,uart_rx,DMA_AMT_LEN+1); //将接收的数据拷贝到串口DMA发送源地址
- DMA_UR1R_AMT = DMA_AMT_LEN; //设置传输总字节数: n+1
- DMA_UR1R_RXA = uart_rx; //设置UART1 DMA接收目的地址
- DMA_UR1R_CR = 0xA1; //开启 UART1 DMA接收功能,开始 UART1_DMA 自动接收,清除 FIFO
- uart1_dma_send(uart_tx,DMA_AMT_LEN+1); //将接收的数据通过串口发送出去
- }
- }
- 主函数
主函数中,初始化用到的引脚、串口1及其DMA,之后在主循环中调用“uart1_dma_handle()”函数查询串口1接收DMA标志是否置位,如置位,则读出数据,并调用串口1的DMA发送函数“uart1_dma_send”将读取的数据发送,由此完成串口接收数据的回环。
代码清单:主函数
- /**********************************************************************************
- 功能描述:主函数
- 参 数:无
- 返 回 值:int类型
- ***********************************************************************************/
- int main(void)
- {
- P2M1 &= 0x3F; P2M0 &= 0x3F; //设置P2.6、P2.7为准双向口
- P7M1 &= 0xF9; P7M0 &= 0xF9; //设置P7.1、P7.2为准双向口
- P3M1 &= 0xFE; P3M0 &= 0xFE; //设置P3.0为准双向口(串口1的RxD)
- P3M1 &= 0xFD; P3M0 |= 0x02; //设置P3.1为推挽输出(串口1的TxD)
- uart1_dma_config(); //串口DMA初始化
- uart1_init(); //串口1初始化
- EA = 1; //使能总中断
- while(1)
- {
- uart1_dma_handle();//将串口接收到的数据(长度20个字节)原样返回
- }
- }
-
-
- 硬件连接
-
-
本实验使用的是USB转串口,按照下图所示短接J5的跳线帽,将串口1的P3.0和P3.1连接到USB转串口电路。
图1:硬件连接
-
-
-
- 实验步骤
-
-
- 解压“…\第3部分:配套例程源码”目录下的压缩文件“实验2-18-1:串口1使用DMA收发数据”,将解压后得到的文件夹拷贝到合适的目录,如“D\STC8”(这样做的目的是为了防止中文路径或者工程存放的路径过深导致打开工程出现问题)。
- 双击“…\uart1_dma\project”目录下的工程文件“uart1_dma.uvproj”。
- 点击编译按钮编译工程,编译成功后生成的HEX文件“uart1_dma.hex”位于工程的“…\uart1_dma\Project\Object”目录下。
- 打开STC-ISP软件下载程序,下载使用内部IRC时钟,IRC频率选择:24MHz。
- 电脑上打开串口调试助手,选择开发板对应的串口号,将波特率设置为9600bps,之后在发送框输入20个字节的数据,点击发送按钮发送数据。
图2:串口调试助手收发数据
- 观察串口接收的数据,应和发送的数据一样。
-
-
- 串口DMA接收不定长数据实验
-
- 注:本节的实验是在“实验2-18-1:串口1使用DMA收发数据”的基础上修改,本节对应的实验源码是:“实验2-18-2:串口1使用DMA接收不定长数据”。
- 关于串口接收不定长数据:
本例中通过定时器配合串口DMA实现串口接收不定长数据,DMA用于将串口接收的数据存放到指定的XRAM区域(我们定义在XRAM的缓存数组),定时器用于软件实现串口接收超时,实现的流程如下图所示。
图3:串口接收不定长数据的处理
从上面的流程可以看到,串口不定长接收需要使用串口1的中断和Timer0中断,而串口DMA的中断并不是必须使用的。这里,我们使用串口DMA的主要目的是利用DMA“搬运”数据的便利性,即串口接收的数据会自动存储到我们定义在XRAM区域的数组,而不必在串口中断中逐个读取。如果我们不使用串口DMA,不定长数据的流程和上图基本一样,区别是在串口中断中需要执行接收数据读取操作。
-
-
-
- 实验内容
-
-
串口1使用DMA接收电脑端串口调试助手发过来的不定长的数据,并将接收到的数据原样返回,本例的功能需求如下:
- 串口1的DMA发送/接收长度设置为最大值256个字节。
- Timer0的超时时间设置为1ms,即1ms产生一次中断。
- 串口1接收超时时间:5ms,即5ms没有接收到新数据,则认为数据接收完成,置位数据接收完成标志。主循环中查询到数据接收完成标志置位,通过串门发送DMA将接收的数据原样发回。
-
-
- 代码编写
-
-
- 初始化串口1
串口1初始化函数代码清单如下,该函数中设置了串口1使用的引脚:RxD为P3.0,TxD为P3.1。串口1配置为8位数据位、波特率9600bps,开启串口1的中断。
代码清单:串口1初始化
- /**********************************************************************************
- 功能描述:初始化串口1,设置为8位数据位、波特率9600bps (系统时钟使用24MHz)
- 参 数:无
- 返 回 值:无
- ***********************************************************************************/
- void uart1_init(void)
- {
- ES = 0; //初始化前关闭UART1中断
- P_SW1 &= 0x3F; //设置串口1使用的引脚为:RxD--P3.0;TxD--P3.1
- PCON &= 0x3f; //波特率不倍速,串行口工作方式由SM0、SM1决定
- SCON = 0x50; //8位数据,可变波特率(SM0=0,SM1=1)
- AUXR |= 0x40; //定时器时钟1T模式
- AUXR &= 0xFE; //串口1选择定时器1为波特率发生器
- TMOD &= 0x0F; //设置定时器1模式:16位自动重装方式
- TL1 = 0x8F; //设置定时初始值
- TH1 = 0xFD; //设置定时初始值
- ET1 = 0; //禁止定时器1中断
- TR1 = 1; //启动定时器1
- ES = 1; //开启串口1中断
- }
- 初始化Timer0
配置Timer0为定时器、超时时间1ms,代码清单如下。注意,这里只初始化Timer0,但不启动Timer0。
代码清单:串口1初始化
- /**********************************************************************************
- 功能描述:初始化Timer0(系统时钟使用24MHz),定时时间1ms,1T
- 参 数:无
- 返 回 值:无
- ***********************************************************************************/
- static void timer0_init(void)
- {
- AUXR |= 0x80; //定时器时钟1T模式
- TMOD &= 0xF0; //配置Timer0为定时器
- TL0 = 0x40; //设置定时初始值
- TH0 = 0xA2; //设置定时初始值
- TF0 = 0; //清除TF0标志
- IP |= 0x02; //中断优先级配置为最高优先级
- IPH |= 0x02;
- ET0 = 1; //使能定时器0中断
- }
- 定义串口DMA接收缓存及相关的标志
为了方便处理串口1不定长接收,我们声明一个名称为“uart_comm_info_t”的结构体,其成员包括串口DMA接收缓存及相关的标志。
代码清单:串口1不定长接收相关软件标志定义
- typedef struct UartComm_Info
- {
- u8 started; //串口接收第一个字节数据的时候置位
- u8 complete; //数据接收完成标志
- u8 delay_count; //串口接收超时软件计数器
- u16 byte_count; //串口接收字节软件计数
- u8 uart_tx[UART_BUF_LEN]; //串口DMA发送缓存
- u8 uart_rx[UART_BUF_LEN]; //串口DMA接收缓存
- }uart_comm_info_t;
- 串口1中断服务函数
串口1接收到数据进入中断服务函数后,通过“uart_comm_info.started”是否置位判断该数据是不是第一个数据,如果是第一个数据,启动Timer0开始计时并置位“uart_comm_info.started”,否则,清零串口接收超时软件计数器,对串口接收超时重新计时。
代码清单:串口1中断服务函数
- /**********************************************************************************
- * 描 述 : 串口1中断服务函数
- * 入 参 : 无
- * 返回值 : 无
- **********************************************************************************/
- void uart1_isr() interrupt 4 using 1
- {
- if (RI) //是接收中断(接收中断请求标志位为1)
- {
- RI = 0; //清零RI位(该位必须软件清零)
- if(uart_comm_info.started == 0) //是第一个数据
- {
- TR0 = 1; //定时器0开始计时
- uart_comm_info.started = 1;
- }
- else
- {
- uart_comm_info.delay_count = 0; //清零串口接收超时软件计数器
- }
- uart_comm_info.byte_count++; //接收数据长度加1
- }
- if(TI)
- {
- TI = 0;
- }
- }
- Timer0中断服务函数
Timer0每1ms产生一次中断,中断服务函数中将串口接收超时软件计数器“uart_comm_info.delay_count”加1,当串口接收超时软件计数器的值为5,即超时时间达到5ms时,认为一包数据接收完成,置位数据接收完成标志“uart_comm_info.complete”并停止Timer,代码清单如下。
代码清单:Timer0中断服务函数
- /***********************************************************
- * 描 述 : 定时器0中断服务函数
- * 入 参 : 无
- * 返回值 : 无
- ************************************************************/
- void timer0_isr() interrupt 1
- {
- uart_comm_info.delay_count++; //串口接收超时软件计数器加1
- if(uart_comm_info.delay_count >= UART_DELAY_TIME_CNT) //达到超时时间,认为一包数据接收完成
- {
- TR0 = 0; //停止定时器
- uart_comm_info.complete = 1; //数据接收完成标志置位
- }
- }
- 说明:本例中,串口DMA接收的数据原样返回的处理和“实验2-18-1:串口1使用DMA收发数据”的流程一样,这里不再赘述,读者可以参阅“实验2-18-1”代码编写部分的文档和本例的实验源码。
-
-
- 硬件连接
-
-
本实验使用的是USB转串口,按照下图所示短接J5的跳线帽,将串口1的P3.0和P3.1连接到USB转串口电路。
图4:硬件连接
-
-
-
- 实验步骤
-
-
- 解压“…\第3部分:配套例程源码”目录下的压缩文件“实验2-18-2:串口1使用DMA接收不定长数据”,将解压后得到的文件夹拷贝到合适的目录,如“D\STC8”(这样做的目的是为了防止中文路径或者工程存放的路径过深导致打开工程出现问题)。
- 双击“…\uart1_dma_random_length \project”目录下的工程文件“uart1_dma_random_length.uvproj”。
- 点击编译按钮编译工程,编译成功后生成的HEX文件“uart1_dma_random_length.hex”位于工程的“…\uart1_dma_random_length\Project\Object”目录下。
- 打开STC-ISP软件下载程序,下载使用内部IRC时钟,IRC频率选择:24MHz。
- 电脑上打开串口调试助手,选择开发板对应的串口号,将波特率设置为9600bps,之后在发送框输入(1~256)之间任意长度的数据,点击发送按钮发送数据。
图5:串口调试助手收发数据
- 观察串口接收的数据,应和发送的数据一样。
-
- ADC使用DMA实验
-
- 注:本节的实验是在“实验2-11-1:ADC采样电位器电压(查询方式)”的基础上修改,本节对应的实验源码是:“实验2-18-3:ADC使用DMA(1个ADC通道)”。
- 关于ADC应用相关的内容,读者可以参阅《第2-11讲:模数转换ADC》,本节我们关注的是ADC如何使用DMA。
关于ADC DMA,我们需要关注两个方面,一是ADC使用DMA后增加了哪些功能,了解这些新增的功能有助于我们在实际开发时决定是否使用DMA,二是ADC采样数据的存储格式,这关系到我们如何正确的读取ADC采样数据。
- ADC使用DMA后增加的功能:
- 通过使用DMA,ADC采样结果由硬件自动储存到我们指定的位于XRAM区域的地址。
- 可以进行多个ADC通道转换,通过ADC_DMA 通道使能寄存器( DMA_ADC_CHSWx)使能通道,使能的通道由硬件自动完成轮询采样,轮询采样的顺序按照已使能通道的编号从小到大执行。
- 通过ADC_DMA 配置寄存器 2(DMA_ADC_CFG2)可以配置ADC转换的次数,配置后,由硬件自动完成设定次数的采样,并且,硬件会自动计算出ADC采样的平均值以及余数。
- ADC DMA采样数据的存储格式
ADC DMA转换完成后,存储到XRAM的数据包含采样结果、ADC通道编号、平均值和余数,这些数据在XRAM中是如何分布的?
ADC转换结果右对齐时的存储格式如下表所示,注意表中的ADC通道顺序是已使能ADC通道编号从小到大排序的。如使能了两个ADC通道:通道2和通道14,那么下表中“使能的第 1 通道”对应的是ADC通道2,“使能的第 2 通道”对应的是ADC通道14。
表1:ADC DMA采样数据的存储格式
ADC通道 | 偏移地址 | 存储的数据 |
使能的第 1 通道 | 0 | 第 1 次 ADC 转换结果的高字节。 |
1 | 第 1 次 ADC 转换结果的低字节。 | |
…… | …… | |
2n-2 | 第 n 次 ADC 转换结果的高字节。 | |
2n-1 | 第 n次 ADC 转换结果的低字节。 | |
2n | ADC 通道号。 | |
2n+1 | n 次 ADC 转换结果取完平均值之后的余数。 | |
2n+2 | n 次 ADC 转换结果平均值的高字节。 | |
2n+3 | n 次 ADC 转换结果平均值的低字节。 | |
使能的第2 通道 | (2n+4)+0 | 第 1 次 ADC 转换结果的高字节。 |
(2n+4)+1 | 第 1 次 ADC 转换结果的低字节。 | |
…… | …… | |
(2n+4)+2n-2 | 第 n 次 ADC 转换结果的高字节。 | |
(2n+4)+ 2n-1 | 第 n次 ADC 转换结果的低字节。 | |
(2n+4)+ 2n | ADC 通道号。 | |
(2n+4)+ 2n+1 | n 次 ADC 转换结果取完平均值之后的余数。 | |
(2n+4)+ 2n+2 | n 次 ADC 转换结果平均值的高字节。 | |
(2n+4)+ 2n+3 | n 次 ADC 转换结果平均值的低字节。 | |
…… | …… | …… |
使能的第m 通道 | (m-1)(2n+4)+0 | 第 1 次 ADC 转换结果的高字节。 |
(m-1)(2n+4)+1 | 第 1 次 ADC 转换结果的低字节。 | |
…… | …… | |
(m-1)(2n+4)+2n-2 | 第 n 次 ADC 转换结果的高字节。 | |
(m-1)(2n+4)+ 2n-1 | 第 n次 ADC 转换结果的低字节。 | |
(m-1)(2n+4)+ 2n | ADC 通道号。 | |
(m-1)(2n+4)+2n+1 | n 次 ADC 转换结果取完平均值之后的余数。 | |
(m-1)(2n+4)+2n+2 | n 次 ADC 转换结果平均值的高字节。 | |
(m-1)(2n+4)+2n+3 | n 次 ADC 转换结果平均值的低字节。 |
-
-
-
- 实验内容
-
-
使用ADC模拟输入通道14(即引脚P0.6)采样电位器抽头电压,程序中每秒执行一次电压采样,采样结果计算为电压值后通过串口输出(包括ADC采样值、通道号、平均值和余数)。
本实验对于ADC配置部分仍然使用“实验2-11-1:ADC采样电位器电压(查询方式)”中的配置,对于DMA部分的配置如下:
- ADC通道:通道14。
- ADC转换次数:8次。
- ADC DMA中断:开启(因为ADC DMA中断号大于31,所以这里借用了13号中断),用于监视ADC DMA转换是否完成。
-
-
- 代码编写
-
-
- 定义ADC DMA使用的通道数量、转换次数、转换完成标志和存储转换结果的数组,即ADC DMA的目的地址(必须位于XRAM 区域)。
代码清单:定义ADC DMA相关标志和ADC DMA的目的地址
- //1~16, 使用的ADC转换通道数量, 必须和[ADC_DMA 通道使能寄存器(DMA_ADC_CHSWx)]中启用的ADC通道数量一致
- #define ADC_CH_NUM 1
- //ADC转换次数,必须和[ADC_DMA配置寄存器2(DMA_ADC_CFG2)]设置的一致
- #define ADC_SAMPLES_NUM 8
- //每个通道ADC转换数据总字节数=2*转换次数+4
- #define ADC_DATA_SIZE (ADC_SAMPLES_NUM*2 + 4)
- //存储ADC DMA转换结果,即ADC DMA的目的地址
- u8 xdata adc_samples_buff[ADC_CH_NUM][ADC_DATA_SIZE];
- //ADC DMA转换完成标志
- bit adc_dma_complete;
- 初始化ADC DMA。
使能ADC通道14,ADC转换次数设置为8次,开启ADC DMA中断,代码清单如下。
代码清单:初始化ADC DMA
- /**********************************************************************************
- 功能描述:ADC DMA配置
- 参 数:无
- 返 回 值:无
- ***********************************************************************************/
- void adc_dma_config(void)
- {
- P_SW2 = 0x80;
- BMM_ADC_STA = 0x00; //清零ADC DMA状态寄存器
- BMM_ADC_CFG = 0x80; //开启ADC DMA中断
- BMM_ADC_RXAH = (u8)((u16)(&adc_samples_buff) >> 8); //ADC转换数据存储地址,即ADC DMA的目的地址
- BMM_ADC_RXAL = (u8)((u16)(&adc_samples_buff));
- BMM_ADC_CFG2 = 0x0A; //每个通道ADC转换次数:8
- BMM_ADC_CHSW0 = 0x40; //使能ADC通道14
- BMM_ADC_CHSW1 = 0x00; //ADC通道 7~通道 0:关闭
- BMM_ADC_CR = 0xC0; //启动ADC DMA转换
- }
- ADC DMA中断服务函数。
ADC DMA完成扫描所有使能的 ADC 通道后,触发ADC_DMA中断,ADC_DMA 中断请求标志位置位,进入中断服务函数。
中断服务函数中软件清零 ADC DMA 状态寄存器,之后将置位转换完成标志“adc_dma_complete”,应用程序通过查询“adc_dma_complete”标志即可知道ADC DMA转换是否完成。
代码清单:初始化ADC DMA
- /**********************************************************************************
- 功能描述:ADC DMA中断服务程序。因为DMA中断号大于31,所以这里借用了13号中断
- 参 数:无
- 返 回 值:无
- ***********************************************************************************/
- void ADC_DMA_Interrupt(void) interrupt 13
- {
- BMM_ADC_STA = 0; //清零ADC DMA状态寄存器
- adc_dma_complete = 1; //转换完成标志置位
- led_toggle(LED_1);
- }
- ADC DMA转换结果的处理
查询到ADC DMA转换完成标志“adc_dma_complete”置位后,即可读取ADC DMA转换数据,包含:
- ADC通道号;
- 8次采样的数据,读取后计算为实际的电压值;
- 8次采样的数据的平均值,读取后计算为实际的电压值;
- 计算平均值后的余数。
读取的数据通过串口输出,以便我们在电脑上可以使用串口调试助手观察到这些数据。最后,启动ADC DMA转换,开启新的一轮ADC转换。为了方便其他程序模块调用,我们将ADC DMA转换结果处理的相关代码封装到名称为“adc_dma_handle”的函数中,代码清单如下。
代码清单:ADC DMA转换结果的处理
- /**********************************************************************************
- 功能描述:查询ADC DMA转换完成标志“adc_dma_complete”,如果置位,读出ADC采样结果,
- :并将其转换为实际电压值
- 参 数:无
- 返 回 值:无
- ***********************************************************************************/
- void adc_dma_handle(void)
- {
- u8 i,j;
- u16 adc_value; //存放ADC采样值
- float voltage; //存放ADC采样值计算后的电压值
- if(adc_dma_complete)
- {
- adc_dma_complete = 0;
- for(i=0; i<ADC_CH_NUM; i++)
- {
- printf("ADC Channel: %bd\r\n",adc_samples_buff[i][ADC_SAMPLES_NUM*2]);//ADC通道
- for(j=0; j<ADC_SAMPLES_NUM; j++)//8次ADC采样对应的电压值
- {
- adc_value=(adc_samples_buff[i][j*2]<<8)+adc_samples_buff[i][j*2+1]; //读取ADC采样值
- voltage = (2.5*adc_value)/4096; //将ADC采样值转换为电压(单位V)
- printf("voltage: %.2fV\r\n",voltage); //串口打印ADC采样电压
- }
- //8次ADC采样的平均值
- adc_value = (adc_samples_buff[i][ADC_SAMPLES_NUM*2+2]<<8)+adc_samples_buff[i][ADC_SAMPL
- ES_NUM*2+3];
- voltage = (2.5*adc_value)/4096; //将ADC采样值转换为电压(单位V)
- printf("average:%.2fV\r\n",voltage); //串口打印ADC采样电压
- //串口打印计算平均值后的余数
- printf("remainder:%bd\r\n",adc_samples_buff[i][ADC_SAMPLES_NUM*2+1]);
- }
- BMM_ADC_CR = 0xc0; //启动ADC DMA转换
- }
- }
- 主函数
主函数中分别调用ADC和ADC DMA初始化函数完成ADC和DMA的初始化,之后在主循环中调用adc_dma_handle()函数处理ADC DMA。注意,主循环中延时1000ms是为了方便在我们串口调试助手中观察实验数据,实际使用ADC DMA时,无需增加长延时。
代码清单:主函数
- /**********************************************************************************
- 功能描述:主函数
- 参 数:无
- 返 回 值:int类型
- ***********************************************************************************/
- int main(void)
- {
- P2M1 &= 0xBF; P2M0 &= 0xBF; //设置P2.6为准双向口(LED1)
- P3M1 &= 0xFE; P3M0 &= 0xFE; //设置P3.0为准双向口(串口1的RxD)
- P3M1 &= 0xFD; P3M0 |= 0x02; //设置P3.1为推挽输出(串口1的TxD)
- uart1_init(); //串口1初始化
- adc_config(); //初始化ADC
- adc_dma_config(); //初始化ADC DMA
- EA = 1; //使能总中断
- while(1)
- {
- adc_dma_handle(); //ADC DMA处理
- delay_ms(1000); //延时1000ms,方便在串口调试助手中观察实验数据
- }
- }
-
-
- 硬件连接
-
-
本实验需要使用ADC模拟通道输入14(即引脚P0.6)采样电位器抽头电压,因此需要将P06引脚和电位器电路通过跳线帽连接,如下图所示。
图6:硬件连接
-
-
-
- 实验步骤
-
-
- 解压“…\第3部分:配套例程源码”目录下的压缩文件“实验2-18-3:ADC使用DMA(1个ADC通道)”,将解压后得到的文件夹拷贝到合适的目录,如“D\STC8”(这样做的目的是为了防止中文路径或者工程存放的路径过深导致打开工程出现问题)。
- 双击“…\adc_dma\project”目录下的工程文件“adc_dma.uvproj”。
- 点击编译按钮编译工程,编译成功后生成的HEX文件“adc_dma.hex”位于工程的“…\adc_dma\Project\Object”目录下。
- 打开STC-ISP软件下载程序,下载使用内部IRC时钟,IRC频率选择:24MHz。
- 电脑上打开串口调试助手,选择开发板对应的串口号,将波特率设置为9600bps,程序运行后,在串口接收窗口可以看到开发板上报的ADC采样的电压值,如下图所示。
图7:串口调试助手中观察电压值
- 旋转电位器改变电位器抽头电压,观察串口接收的数据,可以看到电压值的变化。
- 说明:使用多个ADC通道和使用1个ADC通道类似,在软件上只需做少量的修改即可。如我们在使用1个ADC通道(通道14)的例子上把他改为使用两个ADC通道(通道14和通道2),只需修改下面两个关联的参数即可。
- 修改使能的ADC通道:在adc_dma_config()函数中修改BMM_ADC_CHSW0和ADC_CH_NUM寄存器。
BMM_ADC_CHSW0 = 0x40; //使能ADC通道14
BMM_ADC_CHSW1 = 0x00; //关闭ADC通道7~通道0
改为:
BMM_ADC_CHSW0 = 0x40; //使能ADC通道14
BMM_ADC_CHSW1 = 0x04; //使能ADC通道2
- 修改ADC通道数量的宏ADC_CH_NUM,将其数值由1改为2。
我们也编写好了ADC DMA使用2个通道的例子,例子源码在资料的“…\第3部分:配套例程源码目录下,名称为“实验2-18-4:ADC使用DMA(2个ADC通道)”,读者在编写的过程中可以参考。