利用 DAC + DMA + 定时器触发,循环输出自定义波形(支持正弦波和方波),并在每次 DMA 传输完成后更新波形缓冲区,实现波形的实时调整。
代码块
#define DAC_BUFFER_LENGTH 250
uint32_t DAC_Buffer_Rx[DAC_BUFFER_LENGTH]; // 原始波形数据
uint32_t DAC_Buffer_Rx_DMA[DAC_BUFFER_LENGTH]; // 供DMA传输的缓冲区(防止DMA传输中数据被意外修改)
uint8_t DAC_Tx_Flag = 0; // 标志位:通知主循环波形已更新
复制成功
正弦波 Generate_sinwave
Value = 255 * (sin(angle) + 1) / 2
复制成功
方波 Generate_FangWave
当 DAC 触发 DMA 将 250 个点全部转换完毕后,进入该回调。
设计意图:把耗时计算放在主循环中,避免在中断回调中执行复杂运算;利用双缓冲思想避免 DMA 与 CPU 的数据争抢。
利用 ADC + DMA + 定时器触发,循环采集双通道(或序列)ADC 数据,并在传输完成后通过串口打印偶通道的数值(即保留每个采样序列的第 0、2、4… 个数据)。
#define ADC_RX_BUFFER_LENGTH 500 // DMA 传输的总数据个数(所有通道之和)
uint32_t ADC_Rx_Buffer[ADC_RX_BUFFER_LENGTH/2]; // 提取后的偶通道数据
uint32_t ADC_Rx_Buffer_DMA[ADC_RX_BUFFER_LENGTH]; // DMA 直接填充的原始数据
uint8_t ADC_Rx_Flag = 0;
复制成功
设计要点:通过定时器停止—处理—重启的策略,保证数据处理时 ADC 暂停,处理完成后再恢复采样,避免数据丢失或冲突。
代码块
#include "ADC_App.h"
#define ADC_RX_BUFFER_LENGTH 500
uint32_t ADC_Rx_Buffer[ADC_RX_BUFFER_LENGTH/2];
uint32_t ADC_Rx_Buffer_DMA[ADC_RX_BUFFER_LENGTH];
uint8_t ADC_Rx_Flag=0;
void ADC_DMA_Init(void){
HAL_TIM_Base_Start(&htim3);
HAL_ADC_Start_DMA(&hadc1,ADC_Rx_Buffer_DMA,ADC_RX_BUFFER_LENGTH);
__HAL_DMA_DISABLE_IT(&hdma_adc1,DMA_IT_HT);
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc){
HAL_TIM_Base_Stop(&htim3);
HAL_ADC_Stop_DMA(&hadc1);
for(uint16_t i=0;i<ADC_RX_BUFFER_LENGTH/2;i++){
ADC_Rx_Buffer[i]=ADC_Rx_Buffer_DMA[2*i];
}
ADC_Rx_Flag=1;
HAL_TIM_Base_Start(&htim3);
HAL_ADC_Start_DMA(&hadc1,ADC_Rx_Buffer_DMA,ADC_RX_BUFFER_LENGTH);
__HAL_DMA_DISABLE_IT(&hdma_adc1,DMA_IT_HT);
}
void ADC_DMA_Proc(void){
if(!ADC_Rx_Flag) return;
ADC_Rx_Flag=0;
for(uint16_t i=0;i<ADC_RX_BUFFER_LENGTH/2;i++){
Serial_Printf(&huart1,"%lu\r\n",ADC_Rx_Buffer[i]);
}
memset(ADC_Rx_Buffer,0,ADC_RX_BUFFER_LENGTH/2);
}
#include "DAC_APP.h"
#include "math.h"
#define DAC_BUFFER_LENGTH 250
uint32_t DAC_Buffer_Rx[DAC_BUFFER_LENGTH];
uint32_t DAC_Buffer_Rx_DMA[DAC_BUFFER_LENGTH];
uint8_t DAC_Tx_Flag=0;
void Generate_sinwave(float shape_angle){
float add_angle=2*3.1415926/(float)DAC_BUFFER_LENGTH;
for(uint16_t i=1;i<=DAC_BUFFER_LENGTH;i++){
DAC_Buffer_Rx[i-1]=255.0f*(sinf(shape_angle+i*add_angle)+1.0f)/2.0f;
}
}
void Generate_FangWave(float max,float min,float zhankongbi){
for(uint16_t i=1;i<=DAC_BUFFER_LENGTH;i++){
if(i<zhankongbi*DAC_BUFFER_LENGTH){
DAC_Buffer_Rx[i-1]=min;
}else{
DAC_Buffer_Rx[i-1]=max;
}
}
}
void DAC_DMA_Init(void){
// Generate_sinwave(30);
Generate_FangWave(10,100,0.5);
HAL_TIM_Base_Start(&htim6);
HAL_DAC_Start_DMA(&hdac,DAC_CHANNEL_1,DAC_Buffer_Rx_DMA,DAC_BUFFER_LENGTH,DAC_ALIGN_8B_R);
__HAL_DMA_DISABLE_IT(&hdma_dac1,DMA_IT_HT);
}
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac){
if(hdac->Instance==DAC1){
HAL_DAC_Stop_DMA(hdac,DAC_CHANNEL_1);
for(uint16_t i=0;i<DAC_BUFFER_LENGTH;i++){
DAC_Buffer_Rx_DMA[i]=DAC_Buffer_Rx[i];
}
DAC_Tx_Flag=1;
HAL_DAC_Start_DMA(hdac,DAC_CHANNEL_1,DAC_Buffer_Rx_DMA,DAC_BUFFER_LENGTH,DAC_ALIGN_8B_R);
__HAL_DMA_DISABLE_IT(&hdma_dac1,DMA_IT_HT);
}
}
void DAC_Proc(void){
if(!DAC_Tx_Flag) return;
DAC_Tx_Flag=0;
// memset(DAC_Buffer_Rx,2400,DAC_BUFFER_LENGTH);
}
复制成功
#include "u8g2.h"
#include "main.h" // 确保包含了 HAL 库和 I2C 定义
#include "i2c.h" // 确保包含 I2C 句柄定义 (例如 hi2c1)
// u8g2 的 GPIO 和延时回调函数
uint8_t u8g2_gpio_and_delay_stm32(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
case U8X8_MSG_GPIO_AND_DELAY_INIT:
// 初始化 GPIO (如果需要,例如 SPI 的 CS, DC, RST 引脚)
// 对于硬件 I2C,这里通常不需要做什么
break;
case U8X8_MSG_DELAY_MILLI:
// 原因: u8g2 内部某些操作需要毫秒级的延时等待。
// 提供毫秒级延时,直接调用 HAL 库函数。
HAL_Delay(arg_int);
break;
case U8X8_MSG_DELAY_10MICRO:
// 原因: 某些通信协议或显示时序可能需要微秒级延时。
// 提供 10 微秒延时。HAL_Delay(1) 精度不够(通常是毫秒级)。
// 需要更精确的延时,可以使用 CPU NOP 指令或 DWT 定时器。
// 以下简单循环仅为示例,**需要根据你的 CPU 时钟频率精确调整循环次数**。
for (volatile uint32_t i = 0; i < 150; ++i) {} // 示例循环,计数需调整
break;
case U8X8_MSG_DELAY_100NANO:
// 原因: 更精密的时序控制,通常在高速接口或特定操作中需要。
// 提供 100 纳秒延时。非常短,通常用 NOP 指令实现。
// **同样需要根据 CPU 时钟频率调整 NOP 数量**。
__NOP(); __NOP(); __NOP(); // 示例 NOP
break;
case U8X8_MSG_GPIO_I2C_CLOCK: // [[fallthrough]] // Fallthrough 注释表示有意为之
case U8X8_MSG_GPIO_I2C_DATA:
// 控制 SCL/SDA 引脚电平。这些仅在**软件模拟 I2C** 时需要实现。
// 使用硬件 I2C 时,这些消息可以忽略,由 HAL 库处理。
break;
// --- 以下是 GPIO 相关的消息,主要用于按键输入或 SPI 控制 ---
// 如果你的 u8g2 应用需要读取按键或控制 SPI 引脚 (CS, DC, Reset),
// 你需要在这里根据 msg 类型读取/设置对应的 GPIO 引脚状态。
// 对于仅使用硬件 I2C 显示的场景,可以像下面这样简单返回不支持。
case U8X8_MSG_GPIO_CS:
// SPI 片选控制
break;
case U8X8_MSG_GPIO_DC:
// SPI 数据/命令线控制
break;
case U8X8_MSG_GPIO_RESET:
// 显示屏复位引脚控制
break;
case U8X8_MSG_GPIO_MENU_SELECT:
u8x8_SetGPIOResult(u8x8, /* 读取选择键 GPIO 状态 */ 0);
break;
default:
u8x8_SetGPIOResult(u8x8, 1); // 不支持的消息
break;
}
return 1;
}
// u8g2 的硬件 I2C 通信回调函数
uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
static uint8_t buffer[32]; // u8g2 每次传输最大 32 字节
static uint8_t buf_idx;
uint8_t *data;
switch(msg)
{
case U8X8_MSG_BYTE_SEND:
// 原因: u8g2 通常不会一次性发送大量数据,而是分块发送。
// 这个消息用于将一小块数据 (arg_int 字节) 从 u8g2 内部传递到我们的回调函数。
// 我们需要将这些数据暂存到本地 buffer 中,等待 START/END_TRANSFER 信号。
data = (uint8_t *)arg_ptr;
while( arg_int > 0 )
{
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_INIT:
// 原因: 提供一个机会进行 I2C 外设的初始化。
// 初始化 I2C (通常在 main 函数中已完成)
// 由于我们在 main 函数中已经调用了 MX_I2C1_Init(),这里通常可以留空。
break;
case U8X8_MSG_BYTE_SET_DC:
// 原因: 这个消息用于 SPI 通信中控制 Data/Command 选择引脚。
// 设置数据/命令线 (I2C 不需要)
// I2C 通过特定的控制字节 (0x00 或 0x40) 区分命令和数据,因此该消息对于 I2C 无意义。
break;
case U8X8_MSG_BYTE_START_TRANSFER:
// 原因: 标记一个 I2C 传输序列的开始。
buf_idx = 0;
// 我们在这里重置本地缓冲区的索引,准备接收新的数据块。
break;
case U8X8_MSG_BYTE_END_TRANSFER:
// 原因: 标记一个 I2C 传输序列的结束。
// 此时,本地 buffer 中已经暂存了所有需要发送的数据块。
// 这是执行实际 I2C 发送操作的最佳时机。
// 发送缓冲区中的数据
// 注意: u8x8_GetI2CAddress(u8x8) 返回的是 7 位地址 * 2 = 8 位地址
if (HAL_I2C_Master_Transmit(&hi2c1, u8x8_GetI2CAddress(u8x8), buffer, buf_idx, 100) != HAL_OK)
{
return 0; // 发送失败
}
break;
default:
return 0;
}
return 1;
}
复制成功
3.注册u8g2示例
在HeaderFiles.h声明 u8g2 结构体: 在全局范围(例如 `oled_app.c` 或 `main.c`)声明一个 `u8g2_t` 类型的结构体变量。如果需要在多个文件访问,使用 `extern`。
调用初始化函数: 在 `main.c` 的 `main` 函数中,替换掉之前 `OLED_Init()` 的调用,改为使用 u8g2 的初始化序列。这通常包括三个步骤:`setup`, `init display`, `set power save`。
原因: 初始化 u8g2 是一个标准流程,必须在使用任何绘图 API 之前完成:
`u8g2_Setup_...()`: 这是配置的核心。它将 `u8g2_t` 结构体与具体的硬件驱动、通信方式(通过回调函数指针)和缓冲模式关联起来。必须选择与硬件和回调函数完全匹配的 Setup 函数,否则无法正确工作。
`u8g2_InitDisplay()`: 这个函数会通过之前设置的回调函数,向 OLED 控制器 (SSD1306) 发送一系列初始化命令(类似于第 2.4 节描述的序列),配置其内部寄存器,使其准备好接收显示数据。
`u8g2_SetPowerSave(0)`: SSD1306 初始化后可能处于省电模式(屏幕关闭)。调用这个函数并传入参数 0 会发送命令 (0xAF) 来唤醒屏幕,使其正常显示。
代码块
#include "u8g2.h"
#include "mydefine.h" // 假设 extern u8g2_t u8g2; 在这里,或者直接包含定义了 u8g2 的头文件
// 声明回调函数原型 (如果定义在其他 .c 文件)
uint8_t u8g2_gpio_and_delay_stm32(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
int main(void)
{
// ... HAL_Init(), SystemClock_Config(), MX_GPIO_Init() ...
MX_I2C1_Init(); // 确保 I2C 初始化在前
// u8g2 初始化
// 1. Setup: 这是最关键的一步,它配置了 u8g2 实例。
// - 选择与硬件匹配的 setup 函数 (SSD1306, I2C, 128x32, Full Buffer)。
// - &u8g2: 指向要配置的 u8g2 结构体实例的指针。
// - U8G2_R0: 旋转设置。U8G2_R0=0°, U8G2_R1=90°, U8G2_R2=180°, U8G2_R3=270°。
// - u8x8_byte_hw_i2c: 指向你的硬件 I2C 字节传输回调函数的指针。
// - u8g2_gpio_and_delay_stm32: 指向你的 GPIO 和延时回调函数的指针。
u8g2_Setup_ssd1306_i2c_128x32_univision_f(&u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8g2_gpio_and_delay_stm32);
// - &u8g2: u8g2 结构体指针
// - U8G2_R0: 旋转设置 (0度)
// 2. Init Display: 发送初始化序列到 OLED
u8g2_InitDisplay(&u8g2);
// 3. Set Power Save: 唤醒屏幕。
// - 参数 0 表示关闭省电模式 (屏幕亮起)。
// - 参数 1 表示进入省电模式 (屏幕熄灭)。
u8g2_SetPowerSave(&u8g2, 0);
// (可选) 4. Clear Display: 清空屏幕物理显存。
// - 这个函数会立即发送清屏命令到设备。
// - 注意与 `u8g2_ClearBuffer` 的区别,后者仅清空内存中的缓冲区。
// u8g2_ClearDisplay(&u8g2);
// ... 其他初始化 ...
while (1)
{
// ... 主循环 ...
}
}
复制成功
免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删