嵌入式实验报告

嵌入式实验报告

实验一 开发环境与工具练习

一、 实验目的

  1. 熟悉嵌入式设计与开发项目开发环境包括软件开发环境和硬件开发环境。熟悉MCU配置工具(STM32CubeMX)和集成开发环境(IDE);硬件开发环境,包括国信长天嵌入式竞赛实训平台。

二、 实验内容

  1. 使用MCU配置工具(STM32CubeMX)对实验芯片进行配置练习。
  2. 熟悉配置环境,配置方法,理解芯片的引脚复用,理解芯片的引脚配置参数的含义。
  3. 配置后生成相应的驱动文件。

三、 实验步骤

请写出具体步骤,如有程序,请对程序进行分析写出程序算法的流程图。
请写出具体步骤

  1. 打开STM32CUBEMX,并选择芯片“STM32G431RB”
选择芯片
2. 配置GPIO_Input模式 在右边芯片图中,选择PA0、PB0、PB1、PB2引脚,配置为GPIO_Input模式。
配置输入引脚
3. 配置GPIO_Output模式 选择PA8、PB5~PB9、PC0~PC15、PD2引脚,配置为GPIO_Output模式。
配置输出引脚
4. 配置UART1 配置UART1,波特率为9600,关闭Overrun,同时开启全局中断。
UART配置1
UART中断配置
5. 配置ADC1 配置ADC1,开启通道5和通道11,均配置为单端输入模式。开启Low Power Auto Wait,Overrun behaviour配置为Overrun data overwritten,Number of Conversion配置为2,通道11的转换周期配置为2.5Cycles,通道5转换周期配置为92.5Cycles。
ADC1配置1
ADC1配置2
6. 配置ADC2 配置ADC2,开启通道15的单端输入模式。
ADC2配置
7. 配置TIM1 配置TIM1,开启通道1,配置为PWM Generation CH1N,参数如图所示。
TIM1 PWM配置
8. 配置TIM2 配置TIM2,开启通道1、2,配置为输入捕获模式。
TIM2配置1
TIM2配置2
9. 配置TIM3 开启TIM3,通道1配置为PWM Generation CH1。
TIM3配置
10. 配置系统时钟源和Debug端口 在System Core中配置RCC(HSE/LSE)和SYS(Debug Serial Wire)。
系统时钟源
Debug端口配置
11. 配置时钟树
时钟树
12. 工程命名与生成代码 勾选如图所示的2个内容后,即可创建代码。
生成代码选项
13. 在Keil中编译下载 编译器选择AC5,下载器选择DAP下载器,然后即可编译下载。

四、实验结果及分析

代码成功编译通过,无警告无报错,并且成功下载。下载后的实际运行效果如下图所示。

实验效果图

分析

  • 编译过程无异常,说明工程配置正确(引脚、外设、时钟树等)。
  • 程序下载后硬件响应符合预期,表明 GPIO 输入输出、ADC 采样、PWM 输出及串口通信功能均正常。

实验报告2: LED 流水灯设计

一、 实验目的

  1. 熟悉编程环境Keil,并基于C语言设计基本GPIO控制,实现开发板上的LED亮灯操作。

二、 实验内容

  1. 使用MCU配置工具(STM32CubeMX)对实验芯片进行配置练习。
  2. 熟悉配置环境,配置方法,理解芯片的引脚复用,理解芯片的引脚配置参数的含义。
  3. 配置后生成相应的驱动文件。
  4. 实现实验程序的编译和下载。
  5. 修改程序代码,改变亮灯的快慢。

三、 实验步骤

STM32 G431 引脚配置图

图 1.1 基于 STM32G431 的外设引脚功能分配 (CubeMX 导出)

graph LR %% 方向改为 LR (Left to Right) Start([上电复位]) --> Init[系统与硬件初始化] Init --> MainLoop{While 1} subgraph 任务调度 MainLoop --> RunSched[scheduler_run] RunSched --> Check{时间到?} Check -- 否 --> MainLoop Check -- 是 --> Exec[执行 led_proc] end subgraph 业务逻辑 Exec --> Logic[更新 ucLed 索引并移位] Logic --> Disp[调用 led_disp 更新显示] Disp --> MainLoop end

通过HAL_GetTick()不断刷新时间,像翻看一样检查每个任务(如流水灯)是否到了该运行的时刻。

#include "schedular.h"
uint8_t task_num;

typedef struct{
	void (*task_func)(void);
	uint32_t rate_ms;
	uint32_t last_run;
}task_t;

static task_t scheduler_task[] = 
{
	{led_proc,200,0},//无敌烤肉大王:如果想改流水灯时间,在这里更改
};

void scheduler_init()
{
	task_num = sizeof(scheduler_task) / sizeof(task_t);
}

void scheduler_run()
{
	for(uint8_t i = 0; i < task_num; i ++)
	{
		uint32_t now_time = HAL_GetTick();
		if(now_time >= scheduler_task[i].rate_ms + scheduler_task[i].last_run)
		{
			scheduler_task[i].last_run = now_time;
			scheduler_task[i].task_func();
		}
	}
}

先把所有灯都关了

void system_init(void) {
    GPIOC->ODR |= 0xff00; 
    GPIOD->BSRR = (0x01 << 2);
    GPIOD->BRR = (0x01 << 2);    
}
  /* USER CODE BEGIN 2 *//记得初始化!!!!!!!!!!!!!!
	system_init();
	scheduler_init();//
  while (1)
  {
			scheduler_run();
  }
}

#include "myled.h"

uint8_t ucLed[8];
int8_t index_led = 0;
uint8_t LED_state = 1;  // ĬÈÏģʽ1
uint8_t patTemp = 0;    // ×îÖÕÒªÏÔʾµÄ LED ×Ö½Ú

void led_disp(uint8_t *ucLed)
{
	uint8_t temp = 0x00;
	static uint8_t temp_old = 0xff;
	for(int i = 0; i < 8; i ++)
	temp |= (ucLed[i] << i);
	if(temp != temp_old)
	{
		GPIOC->ODR &= 0x00ff;
		GPIOC->ODR |= ~(temp << 8);
		GPIOD->BSRR |= 0x01 << 2;
		GPIOD->BRR |= 0x01 << 2;
		temp_old = temp;
	}
}

左流(倒序):使用--递减,到头复位7。
右流(正序):使用++递增,到头复位0。展示左流代码

void led_proc(void) {
    static int8_t index = 7;     
    for(int i = 0; i < 8; i++) ucLed[i] = 0; 
    ucLed[index] = 1;          
    led_disp(ucLed);
    if(--index < 0) index = 7;  
}

四、结论

本实验成功实现了基于硬件寄存器控制的 LED 流水灯效果。

现象 1

现象 2

实验三:按键控制 LED 实验报告

一、 实验目的

  1. 熟悉 STM32G431RBT6 硬件开发环境及国信长天嵌入式竞赛实训平台。
  2. 掌握 STM32CubeMX 对 GPIO 输入(按键)与输出(LED)的配置方法。
  3. 理解 74LS573 锁存器的硬件控制原理及 PD2 脉冲的作用。
  4. 掌握基于非阻塞式任务调度器的多任务编程思想,实现按键对 LED 的实时控制。

二、 实验内容

  1. 使用 STM32CubeMX 配置 PB0-PB2、PA0 为输入模式,PC8-PC15 和 PD2 为输出模式。
  2. 编写按键扫描程序,识别按键按下(Down)动作。
  3. 实现按键联动:通过不同按键实现 LED 的点亮、指定跳变及循环移动。
  4. 编译、下载程序并观察实验现象。

三、 实验步骤

graph TD Start([系统上电]) --> Init[GPIO/系统初始化] Init --> TaskLoop{调度器轮询} subgraph 按键逻辑任务 TaskLoop -- 10ms周期 --> KeyProc[读取按键状态] KeyProc --> KeyLogic{判断按键编号} KeyLogic -- B1 --> Set0[index_led = 0] KeyLogic -- B2 --> Add[index_led 循环递增] KeyLogic -- B3 --> Sub[index_led 循环递减] KeyLogic -- B4 --> Set7[index_led = 7] end
核心按键处理代码
#include "mykey.h"

uint8_t key_val = 0;
uint8_t key_old = 0;
uint8_t key_down = 0;
uint8_t key_up = 0;


uint8_t key_read(void)
{
	uint8_t temp = 0;
	
	if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) temp = 1;
	if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) temp = 2;
	if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET) temp = 3;
	if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) temp = 4;
  return temp;
}
void key_proc(void)
{
	key_val = key_read();
	key_down = key_val & (key_old ^ key_val);
	key_up = ~key_val & (key_old ^ key_val);
	key_old = key_val;
	
	switch(key_down)
	{
		case 1:
			index_led = 0;
		break;
		case 4:
			index_led = 7;
		break;
		case 2: 
      index_led =(index_led >=7) ?0:index_led+1;
    break;     
    case 3: 
			index_led =(index_led <=0) ?7 :index_led-1;
    break;
	}
}

四、 实验结果及分析

现象1

现象2:

五、 思考:实验问题与解决方案

1. 现象描述

在使用三目运算符 index_led = (index_led > 7) ? index_led = 0 : index_led++; 时,发现灯光切换到最后一盏(LD8)后,再次按下按键会出现“全灭”现象。将条件改为 >= 7 后现象消失。

2. 根本原因:临界值与数组越界

越界逻辑(> 7):当灯在第 8 盏(index_led = 7)时,判断 7 > 7 为假,程序执行 index_led++ 使其变为 8。由于 ucLed[8] 超出了数组 0-7 的合法范围,导致访问了非法内存,硬件上表现为无灯点亮。

修正逻辑(>= 7):判断 7 >= 7 为真,程序强制将 index_led 重置为 0,确保了索引始终在 0-7 的安全区间内。

这是一份为您整理好的 实验四 LCD显示实验 报告模板,已转换为标准的 Markdown 格式,方便您直接复制到博客园或提交作业。


实验四 LCD显示实验

1. 实验目的

  • 熟悉开发环境:掌握嵌入式开发项目环境,包括软件开发环境(STM32CubeMX、Keil IDE)和硬件开发环境(国信长天嵌入式竞赛实训平台)。
  • 掌握配置流程:理解芯片引脚复用及参数含义,练习使用 STM32CubeMX 生成底层驱动文件。
  • 软硬件结合能力:学习阅读硬件原理图,掌握软硬件结合的调试方法,实现代码编译、下载及 LCD 显示控制。

2. 实验内容

  1. 使用 STM32CubeMX 对实验芯片进行外设及时钟配置。
  2. 理解引脚配置参数,生成对应的工程文件。
  3. Keil 环境下实现程序的编译与下载。
  4. 通过修改代码,实现对 LCD 的精准显示控制(包括特定图案/校徽的解析与绘制)。

3. 实验步骤

3.1 硬件原理分析与引脚配置

根据国信长天嵌入式开发板原理图,LCD 模块通常通过控制引脚(RS, WR, RD, CS等)与数据总线进行通信。在 STM32CubeMX 中:

  • 配置系统时钟(RCC)与调试模式(SYS-Serial Wire)。
  • 配置 LCD 控制相关的 GPIO 端口为 Output 模式。
  • 配置数据总线管脚。
为了让这份实验报告更具专业性和可读性,我为你调整了代码块的排版,补充了详细的逻辑注释,并采用 Markdown 层次化的标题结构。

3.2 界面切换逻辑实现

通过静态变量 count 在定时器循环中实现非阻塞延迟(约 3 秒切换一次),并利用 mode 变量在文字界面与校徽图案间轮询切换。

#include "mylcd.h"
#include <stdarg.h> 
#include <stdio.h> 

// 全局状态变量
uint32_t lcd_timer = 0;
uint8_t lcd_state = 0; // 0:显示文字, 1:显示校徽

void LcdSprintf(uint8_t Line, char *format, ...)
{
    char String[21];
    va_list arg;
    va_start(arg, format);
    vsprintf(String, format, arg); // 处理格式化参数
    va_end(arg);
    LCD_DisplayStringLine(Line, (u8 *)String);
}
void lcd_proc(void)
{
    static uint8_t count = 0;   // 计数变量,累计满 30 次约 3 秒
    static uint8_t mode = 0;    // 显示模式切换标志

    count++; 
    if(count < 30) return;      
    count = 0;                 

    if(mode == 0)
    {
        /* 模式 0:分块背景色文字显示 */
        LCD_Clear(Blue);
        LCD_SetBackColor(Blue);
        LCD_SetTextColor(White);
        LcdSprintf(Line1, "         Hi         ");
        LcdSprintf(Line2, "      LCD Test      ");
        LcdSprintf(Line3, "         go         ");
        LcdSprintf(Line4, "         go         ");
        
        LCD_SetBackColor(White);
        LCD_SetTextColor(Blue); 
        LcdSprintf(Line5, "                    ");
        LcdSprintf(Line6, "       HALLIB       ");
        LcdSprintf(Line7, "          !         ");
        LcdSprintf(Line8, "         @80        ");
        LcdSprintf(Line9, "                    ");
        
        mode = 1; // 准备下一次切换到模式 1
    }
    else
    {
        /* 模式 1:单色校徽图案显示 */
        LCD_SetBackColor(Blue);
        LCD_SetTextColor(White);
        LCD_DrawMonoPict(smulogo); // 绘制校徽
        
        mode = 0; // 准备下一次切换到模式 0
    }
}

3.3 拓展实验相关代码分析
修复问题2

void LCD_DrawMonoPict_MSB(uc32 *Pict)
{
    u32 index = 0, i = 0;
    LCD_SetCursor(0, 319); 
    LCD_WriteRAM_Prepare(); 

    for(index = 0; index < 2400; index++)
    {
        for(i = 0; i < 32; i++)
        {
            // 通过掩码 0x80000000 右移,依次判断第 31 位到第 0 位
            if((Pict[index] & (0x80000000u >> i)) == 0x00)
            {
                LCD_WriteRAM(BackColor);
            }
            else
            {
                LCD_WriteRAM(TextColor);
            }
        }
    }
}

修复问题3

void LCD_DrawMonoPict_ByteSwap(uc32 *Pict)
{
    u32 index = 0;
    uint8_t tmp;
    LCD_SetCursor(0, 319); 
    LCD_WriteRAM_Prepare(); 

    for(index = 0; index < 2400; index++)
    {
        // 1. 将 32 位数据拆分为 4 个字节处理 (i从4到1)
        for(int16_t i = 4; i > 0; i--)
        {
            // 取出当前字节
            tmp = (uint8_t)(Pict[index] >> ((i-1) * 8)); 
            
            // 2. 对当前字节内的 8 位进行低位优先解析
            for(int16_t j = 0; j < 8; j++)
            {
                if((tmp & (1 << j)) == 0x00)
                {
                    LCD_WriteRAM(BackColor);
                }
                else
                {
                    LCD_WriteRAM(TextColor);
                }
            }
        }
    }
}

4. 实验结果及分析

1 2 3 4 5 6

5. 思考与讨论

实验过程中遇到的问题:

  • 现象:尝试下载程序时,Keil 报错提示“No ST-Link detected”或“Target not found”。
  • 原因:国信长天嵌入式平台某些版本使用的是 CMSIS-DAP 调试器,而工程默认设置可能为 ST-Link。
  • 解决方法:在 Keil 的工程选项(Options for Target)中的 Debug 选项卡里,将调试驱动从 ST-Link Debugger 修改为 CMSIS-DAP Debugger,并在 Settings 中确认能够识别到 SW Device。

实验五 UART通信实验

实验目的

  • 熟悉嵌入式设计与开发项目开发环境,包括软件开发环境和硬件开发环境。
  • 熟悉 MCU 配置工具(STM32CubeMX)和集成开发环境(IDE)。
  • 熟悉硬件开发环境,包括国信长天嵌入式竞赛实训平台。
  • 熟悉编程环境 Keil。
  • 熟悉软件编程过程中,软硬件结合及对硬件原理图的使用。

实验内容

  1. 使用 MCU 配置工具(STM32CubeMX)对实验芯片进行配置练习。
  2. 熟悉配置环境、配置方法,理解芯片的引脚复用及引脚配置参数的含义。
  3. 配置后生成相应的驱动文件。
  4. 实现实验程序的编译和下载。
  5. 修改程序代码,实现 UART 通信。

实验步骤

管脚配置

管脚配置如图所示:

管脚配置图
  1. 环形缓冲区实现 (ringbuffer.c)

本模块负责提供数据的异步存储空间,防止主程序处理不及时导致的数据丢失。

#include "ringbuffer.h"
#include <string.h>

// 初始化环形缓冲区
void ringbuffer_init(ringbuffer_t *rb) {
    rb->r = 0; // 读指针清零
    rb->w = 0; // 写指针清零
    rb->itemCount = 0; // 计数清零
    memset(rb->buffer, 0, RINGBUFFER_SIZE); // 缓冲区内存清零
}

// 向缓冲区写入数据
int8_t ringbuffer_write(ringbuffer_t *rb, uint8_t *data, uint32_t num) {
    if(rb->itemCount + num > RINGBUFFER_SIZE) return -1; // 溢出保护
    while(num--) {
        rb->buffer[rb->w] = *data++; // 写入数据
        rb->w = (rb->w + 1) % RINGBUFFER_SIZE; // 指针循环移动
        rb->itemCount++; // 计数增加
    }
    return 0;
}

// 从缓冲区读取数据
int8_t ringbuffer_read(ringbuffer_t *rb, uint8_t *data, uint32_t num) {
    if(rb->itemCount < num) return -1; // 数据不足
    while(num--) {
        *data++ = rb->buffer[rb->r]; // 读取数据
        rb->r = (rb->r + 1) % RINGBUFFER_SIZE; // 指针循环移动
        rb->itemCount--; // 计数减少
    }
    return 0;
}

  1. 串口处理逻辑 (myuart.c)
#include "myuart.h"
#include "usart.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

ringbuffer_t usart_rb; // 声明环形缓冲区实例
uint8_t uart_rx_dma_buffer[128]; // DMA搬运目标缓冲区
char latest_rx_msg[21] = ""; // 存放供LCD显示的接收字符串
int last_tx_rand = 0; // 存放最近一次发的随机数
uint8_t lcd_update_flag = 0; // LCD刷新标志位

// 串口接收事件回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
    if(huart->Instance == USART1) {
        // 将DMA收到的数据存入环形缓冲区
        ringbuffer_write(&usart_rb, uart_rx_dma_buffer, Size);
        // 重启接收,以实现循环监听
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
        __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); // 禁用DMA半传输中断
    }
}

// 串口数据处理任务(10ms/次)
void uart_proc(void) {
    if(ringbuffer_is_empty(&usart_rb)) return; // 缓冲区为空则返回

    uint8_t temp_buf[128];
    uint32_t len = usart_rb.itemCount;
    
    // 从环形缓冲区提取当前所有数据
    ringbuffer_read(&usart_rb, temp_buf, len);
    
    // 拷贝至显示变量并截断为20个字符
    memset(latest_rx_msg, 0, sizeof(latest_rx_msg));
    strncpy(latest_rx_msg, (char*)temp_buf, 20); 
    lcd_update_flag = 1; // 触发LCD更新
}

// 随机数发送任务(每1000ms运行一次)
void uart_tx_random_proc(void) {
    last_tx_rand = rand() % 1000; // 生成随机数
    char ucTx[25];
    sprintf(ucTx, " Hello: %d\r\n", last_tx_rand); // 格式化字符串
    HAL_UART_Transmit(&huart1, (uint8_t*)ucTx, strlen(ucTx), 100); // 执行发送
    lcd_update_flag = 1; // 发送后也刷新LCD显示
}

  1. LCD 显示处理 (mylcd.c)

负责在屏幕指定行更新数据,并加入标志位判断以减少屏幕闪烁。

#include "mylcd.h"
#include <stdio.h>
#include <stdarg.h>

extern int last_tx_rand;
extern char latest_rx_msg[21];
extern uint8_t lcd_update_flag;

// 格式化LCD字符串显示函数
void LcdSprintf(uint8_t Line, char *format,...) {
    char String[21];
    va_list arg;
    va_start(arg, format);
    vsprintf(String, format, arg);
    va_end(arg);
    LCD_DisplayStringLine(Line,(uint8_t *)String);
}

void lcd_proc(void) {
    if(lcd_update_flag == 0) return; // 标志位未置位则跳过

    LCD_SetBackColor(Blue); // 设置底色
    LCD_SetTextColor(White); // 设置字体颜色

    // Line5 显示发给串口助手的随机数
    LcdSprintf(Line5, "   Hello: %d", last_tx_rand);

    // Line8 显示从串口助手接收到的字符串
    LcdSprintf(Line8, "   Rec: %s", latest_rx_msg);

    lcd_update_flag = 0; // 刷新完成
}

  1. 任务调度配置 (scheduler.c)

配置各个模块的运行频率。

static task_t scheduler_task[] = {
    {lcd_proc, 100, 0},             // 100ms周期刷新屏幕
    {uart_proc, 10, 0},            // 10ms周期解析缓冲区
    {uart_tx_random_proc, 1000, 0}, // 1000ms周期发送随机数
};

实验结果及分析

图片1 图片2
我通过配置 Normal模式(单次接收) + 10ms高频调度 解决了接收不正常的问题,核心原理如下: 利用串口空闲中断(Idle),每当发送端发完一帧数据,STM32 硬件就会触发中断。因为是 Normal 模式,DMA 搬完这一包数据后会自动停止。我在回调中断里把这包数据丢进环形缓冲区,并立刻重启下一次 DMA 单次接收,形成闭环。

思考

  1. 了解实验程序中函数的功能
    SysTick_Handler 函数是滴答计数器的中断处理函数,STM32CubeMX 默认每 1ms 进入一次滴答计数器中断。此处用于 main 函数中的屏幕刷新功能。

  2. 了解 UART 通信是如何实现的
    UART 通过两根数据线(TX 和 RX)实现全双工数据传输。发送和接收双方约定好波特率、数据帧结构,即可进行数据收发。

实验报告:实验六 ADC实验

一、 实验目的

  1. 熟悉嵌入式系统开发环境,包括 STM32CubeMX 配置工具、Keil 集成开发环境及国信长天嵌入式竞赛实训平台。
  2. 掌握芯片引脚复用配置、参数含义及驱动文件生成的流程。
  3. 理解 STM32 中 ADC(逐次比较型 SAR)的工作原理,包括电压采样与转换存储过程。
  4. 实现 ADC 数据的采集、变换,并通过 LCD 实时显示采集结果。

二、 实验内容

  1. 使用 STM32CubeMX 完成实验芯片的初始化配置,理解引脚复用功能。
  2. 生成底层驱动代码,并将其导入 Keil 工程。
  3. 编写代码实现 ADC 通道的电压采集。
  4. 修改程序逻辑,将采集到的数字量转换为电压值,并解决 LCD 显示刷新时的溢出问题。
  5. 完成程序的编译、下载与硬件调试。

三、 实验步骤

  1. 硬件管脚配置

时钟与系统配置:配置 RCC 为外部晶振(HSE) ,SYS 的 Debug 模式为 Serial Wire 。
GPIO配置:设置 PC0~PC15 为 Output Open Drain 模式并启用上拉电阻,用于 LCD 控制 。
ADC配置:开启相应 ADC 通道(如连接 R37、R38 电位器的引脚),配置采样时间与转换分辨率。

image
  1. 程序分析与算法流程

  2. 启动 DMA 采集:
    ADC1 采集 60 个数据。
    ADC2 采集 30 个数据。

  3. GUI 初始化 LCD 屏幕并清屏,准备显示界面。

  4. 调度器: 计算任务总数,进入大循环。
    模式 1 (单点移动):将 0-4095 的量程切分为 8 段,每一段对应一颗 LED 点亮。利用位移操作 1 << N 实现。
    模式 2 (条形增长):实现类似进度条的效果。利用掩码算法 (1 << (N+1)) - 1 实现从 L1 到 LN 的连续点亮。
    底层驱动控制:由于 LED 连接在 GPIOC 的高 8 位且通过 SN74HC573 锁存器控制,必须在更新 ODR 寄存器后,对 PD2 进行一次高低电平的“脉冲”操作以锁定数据。

  5. 代码实现 (mylcd.c 关键部分)

void lcd_proc(void)
{
    // 显示 ADC 转换结果
    // 使用格式化输出函数将数据渲染至 LCD 屏幕
    LcdSprintf(Line3, "   R38:%04u   ", (uint16_t)adc1_buff[0]);
    LcdSprintf(Line4, "   MCP:%04u   ", (uint16_t)adc1_buff[1]);
    LcdSprintf(Line5, "   R37:%04u   ", (uint16_t)adc2_buff[0]);
}


void LED_Display_Simple(uint8_t pattern)
{
    static uint8_t old_pattern = 0xFF;
    // 性能优化:若数据未变化则不触发锁存器,减少电磁干扰
    if(pattern == old_pattern) return; 
    GPIOC->ODR &= 0x00FF;            // 清除高8位原有状态
    GPIOC->ODR |= (~pattern << 8);  // 将模式数据移至高8位并写入
    GPIOD->BSRR = (0x01 << 2);      // PD2 置高 (Enable)
    __nop();                        // 极短延迟保证锁存稳定
    GPIOD->BRR = (0x01 << 2);       // PD2 置低 (Hold)

    old_pattern = pattern;
}

void led_proc(void)
{
    // 1. 获取输入源:ADC 转换值( DMA 自动填充至 adc2_buff[0])
    uint32_t r38_val = adc2_buff[0]; 
    uint8_t patTemp = 0;

    // 2. 模式处理逻辑
    switch (LED_state)
    {
        case 1: // 模式1:单点随电位器移动
            if (r38_val < 50)       
				patTemp = 0;        // 死区处理
            else if (r38_val > 4000)
				patTemp = 0x80;    // 亮最后一颗 L8
            else                    
				patTemp = 1 << (r38_val / 512); 
            break;

        case 2: // 模式2:条形进度条
            if (r38_val < 50)     
				patTemp = 0;
            else if (r38_val > 4000) 
				patTemp = 0xFF;    // 全亮
            else               
				patTemp = (1 << (r38_val / 512 + 1)) - 1; 
            break;
    }

    // 3. 更新硬件显示
    LED_Display_Simple(patTemp);
}

四、 实验结果及分析

图片1 图片2 图片3 图片4

五、 思考与问题解决

  1. SysTick 与系统时基功能
    SysTic每 1ms 触发一次 SysTick_Handler 中断,递增全局变量 uwTick。为调度器提供统一的时间轴,确保 LCD 刷新、串口发送等任务拥有稳定的时间步长,有效防止因刷新过快导致的屏幕闪烁。

  2. ADC 采样与转换机制
    采样:内部采样电容获取外部电压。
    转换:通过内部比较器和 DAC,利用二分法在每个时钟周期确定一位二进制数值(从 MSB 到 LSB)。

  3. DMA 协作
    开启 HAL_ADC_Start_DMA 后,ADC 转换完成的数据不经过 CPU,直接通过 DMA 控制器存入 adc1_buffadc2_buff

  4. 遇到的问题及解决方法

  • 现象:当 ADC2 的数值从 4 位(如 1000)变为 3 位(如 999)时,屏幕上 R37 的结果显示不准确,末位常有旧数据残留。
  • 原因分析:原始代码使用 sprintf((char *)lcdString,"R37:%03u", adcValues[1]);。格式占位符 %03u 仅指定了 3 位宽度,当数值变小时,原有位置的字符未被完全覆盖。
  • 解决方法:将格式化字符串中的 %03u 修改为 %04u。这样可以强制以 4 位宽度输出并自动补零,确保显示区域被完全覆盖刷新。

根据您提供的代码和实验要求,我为您整理了一份标准、简洁的实验报告。


实验报告:实验七 TIM 定时器实验

一、 实验目的

  1. 熟悉 STM32CubeMX 配置环境及 Keil 集成开发环境。
  2. 理解 MCU 引脚复用及定时器(TIM)参数配置的含义。
  3. 掌握 定时器溢出中断(Update Interrupt) 的处理流程及 回调函数(Callback) 的编程模式。
  4. 学习通过动态修改重装载值(ARR)实现外设控制(流水灯频率调节)。

二、 实验步骤

  1. 引脚配置:在 STM32CubeMX 中配置 TIM1 为内部时钟源,设置预分频系数(PSC)和重装载值(ARR)。配置 GPIO 端口(PC8-PC15)用于 LED 显示,PD2 用于锁存器控制。
  2. 生成代码:生成基于 HAL 库的项目工程。
  3. 编写逻辑
  • main.c 中调用 HAL_TIM_Base_Start_IT() 开启定时器中断。
  • 实现 HAL_TIM_PeriodElapsedCallback 回调函数,处理流水灯移位逻辑。
  • key_proc 任务中编写按键逻辑,动态修改 TIM1->ARR
  1. 编译下载:通过 ST-Link 下载程序,观察硬件平台 LED 流转速度的变化。

三、 程序分析与流程图

  1. 主程序逻辑 (while 循环)
    主程序采用任务调度模式,通过轮询按键任务来响应用户输入。
  2. 中断回调函数流程 (TIM1 溢出中断)
    每当定时器计数达到 ARR 值时,触发一次中断,执行以下逻辑:

核心算法分析:

  • 循环移位算法ledPattern = (ledPattern << 1) | (ledPattern >> 7);
    该行代码实现了 8 位数据的循环左移。将最高位移出后补到最低位,从而实现 LED 循环流转的效果。
  • 按键调速算法:通过修改 current_reload 变量并写入 TIM1->ARR 寄存器。

\[F_{output} = \frac{F_{clk}}{(PSC+1) \times (ARR+1)} \]

当 ARR 减小时,定时器溢出频率增加,流水灯加速。

四、 关键代码实现

  1. 定时器频率设置
void TIM1_Set_Frequency(uint16_t reloadVal) {
    if(TIM1->ARR != reloadVal) {
        TIM1->ARR = reloadVal; // 动态修改自动重装载寄存器
    }
}

  1. 拓展实验主循环代码
/* 在 scheduler_run() 调度的 key_proc 中实现 */
switch (key_down) {
    case 1: // 加快
        current_reload = (current_reload <= 1000) ? 1000 : current_reload - 500;
        break;
    case 2: // 减慢
        current_reload = (current_reload >= 60000) ? 65535 : current_reload + 500;
        break;
    case 3: // 固定周期
        current_reload = 6250 - 1; 
        break;
}
TIM1_Set_Frequency(current_reload);

五、 实验结果及分析

  • 默认情况:系统初始化后,流水灯以初始 current_reload 设定的频率匀速流转。
  • 加速/减速:按下 B1 或 B2 键后,由于 ARR 寄存器的值发生改变,定时器进入中断的时间间隔缩短或延长,肉眼可见 LED 流转速度明显变化。
  • 固定周期:按下 B3 键后,ARR 被设置为固定值 6249,实现了精确的时间控制。

六、 思考与总结

  1. 关于回调函数
    HAL_TIM_PeriodElapsedCallback 是由 HAL 库统一调用的。在硬件层中断发生时,HAL 库会先处理清除中断标志位等通用操作,再调用该回调函数。这使得开发者只需关注业务逻辑,而无需手动维护中断状态寄存器。
  2. TIM 的计数原理
    定时器通过预分频器(PSC)降低输入时钟频率,计数器从 0 计数到 ARR。因此,总计数周期数为 \(ARR + 1\)。例如,需要 100 个时钟周期触发一次中断,ARR 应设为 99。
  3. 常见问题解决
  • 现象:程序运行正常但 LED 不闪烁。
  • 原因:未开启定时器中断或仅使用了 HAL_TIM_Base_Start()
  • 对策:必须使用 HAL_TIM_Base_Start_IT() 函数启动定时器,以使能溢出中断并开启 NVIC 中断通道。

实验报告:实验八综合实验

一、 实验目的

  1. 掌握基于 STM32 微控制器的多任务时间片轮询(Scheduler)系统的搭建与应用。
  2. 熟悉 2D 游戏逻辑(以俄罗斯方块为例)在嵌入式 LCD 屏幕上的实时渲染与数据结构设计。
  3. 掌握基于三维空间坐标变换、透视投影算法以及 Bresenham 画线算法的 3D 图形引擎底层实现机制。
  4. 体会局部刷新机制(消消隐技术)在低带宽、低刷新率嵌入式显示系统中的应用优势。

二、 实验原理与架构设计

3D 立方体旋转动画与透视投影原理
动画渲染进程 game_ani_proc 的图形学处理核心在于:将三维空间中的顶点坐标,通过绕 Y 轴和 X 轴的矩阵变换后,再经由透视投影算法映射到二维屏幕上。为了解决全屏刷新导致的闪烁问题,实验采用了旧线用黑色擦除、新线用白色绘制的局部消隐技术。
其核心算法流程如下所示:

graph LR A([进入进程]) --> E{第一帧?} E -- 否 --> F[黑线擦除旧帧] --> G[白线画新帧] E -- 是 --> G G --> H[存当前坐标供下次擦除] --> I[累加角度] --> J[置标志位] --> K([退出进程])

三、 实验核心代码分析

  1. 多任务时间片轮询框架
void scheduler_run()
{
	for(uint8_t i = 0; i < task_num; i ++)
	{
		uint32_t now_time = HAL_GetTick();
		if(now_time >= scheduler_task[i].rate_ms + scheduler_task[i].last_run)
		{
		scheduler_task[i].last_run = now_time;
		scheduler_task[i].task_func();
		}
	}
}

分析:本系统配置了多个不同周期的任务(如 10ms 的 key_proc、200ms 的 game_proc、40ms 的 game_ani_proc)。此结构避免了传统 delay() 函数带来的全屏阻塞,保障了多任务并行的实时性与独立性。
2. 俄罗斯方块消行与边界碰撞逻辑

void game_fall_logic(void)
{
    uint8_t can_fall = 1;
    // 如下落边界与碰撞检测...
    if (can_fall) 
    {
        cur_y++;
    } 
    else 
    {
        // 固定方块并刷新到地图数组中
        for (int i = 0; i < 4; i++) {
            int8_t fix_x = cur_x + shapes[cur_shape][i][0];
            int8_t fix_y = cur_y + shapes[cur_shape][i][1];
            if (fix_y >= 0) tetris_map[fix_y][fix_x] = 1;
        }
        game_check_line_clear(); // 检查并消除满行
        // 生成新方块
        cur_y = 0;
        cur_x = 3; 
        cur_shape = rand() % 7;
        // 死亡检测
        if (tetris_map[cur_y + 1][cur_x + 1] == 1) game_init();
    }
}

分析:通过二维数组 tetris_map[20][10] 维护固定方块状态。在下落逻辑中,当方块无法继续下落时,将其动态写入地图,并立即触发 game_check_line_clear() 进行消行与得分累加,实现了完整的游戏数据闭环。
3. 3D 图形局部消隐刷新

// 局部刷新:利用上一帧缓存坐标,用黑色“擦除”掉旧的线条
if (!first_frame) 
{
    for (int i = 0; i < 12; i++) 
    {
        uint8_t p1 = cube_edges[i][0];
        uint8_t p2 = cube_edges[i][1];
        My_LCD_DrawLine(last_projected[p1].x, last_projected[p1].y, 
                        last_projected[p2].x, last_projected[p2].y, Black);
    }
}
// 绘制当前帧:画出白色新线条
for (int i = 0; i < 12; i++) 
{
    uint8_t p1 = cube_edges[i][0];
    uint8_t p2 = cube_edges[i][1];
    My_LCD_DrawLine(current_projected[p1].x, current_projected[p1].y, 
                    current_projected[p2].x, current_projected[p2].y, White);
}

分析:由于嵌入式 LCD 硬件普遍缺少高性能双缓冲机制,全屏清屏会导致剧烈闪烁。本实验巧妙利用 last_projected 数组缓存了上一帧的二维坐标。在绘制新线前,先用背景色(Black)重复绘制一次旧线进行动态擦除,随后绘制新线(White),以极低的计算开销完美实现了流畅无闪烁的 3D 动画渲染。

四、 实验结论

本实验通过设计并实现 2D 俄罗斯方块游戏与 3D 旋转立体动画,深入掌握了基于 STM32 的嵌入式多任务系统开发流程。实验结果表明,时间片轮询调度器运行稳定,3D 透视投影变换矩阵运算准确,采用的局部消隐刷新机制彻底解决了 LCD 屏幕的闪烁问题,各项指标均达到实验预期要求。

posted @ 2026-05-10 22:53  无敌烤肉大王  阅读(26)  评论(0)    收藏  举报