前言
本文是本人备赛物联网赛项的学习笔记,主要供本人学习、复习,不是经验分享或教学,若有错误,大佬轻喷。
一、前置准备
1. 硬件准备
- STM32 开发板(本文以蓝桥杯 CT127C 物联网开发板为例,其他 STM32 开发板仅引脚和芯片型号有差异,流程通用)
- 0.91 寸 I2C 接口 OLED 显示屏(分辨率 128*32,本文配套驱动)
- MicroUSB 数据线(用于电脑和开发板通信、下载程序)
- 电脑一台
2. 软件准备
- STM32CubeMX(用于图形化配置工程,自动生成初始化代码)
- Keil MDK5(用于编写代码、编译和下载程序)
- 串口助手(可选,后续拓展实验用)
3. 必看小白避坑提示
- 工程保存路径绝对不能有中文、空格、特殊符号,否则一定会编译报错!
- OLED 屏 VCC 请接 3.3V,不要接 5V,大概率会烧屏!
- 所有代码复制时,不要漏了大括号、分号,C 语言对语法要求极严格。
二、硬件原理与接线详解
1. 基础认知
我们用的 0.91 寸 OLED 是I2C 通信接口,仅需 2 根信号线 + 2 根电源线就能驱动,相比 SPI 接口接线更少,对新手极度友好。
- I2C 通信有两根核心线:SCL(串行时钟线)、SDA(串行数据线)
- 本实验中,STM32 和 OLED 的引脚是固定映射的,接线绝对不能接反!
2. 引脚对应接线表
| STM32 开发板引脚 | OLED 屏引脚 | 引脚作用 |
|---|---|---|
| 3.3V/VDD | VCC | 电源正极 |
| GND | GND | 电源地 |
| PB6 | SCL | I2C 时钟线 |
| PB7 | SDA | I2C 数据线 |

三、STM32CubeMX 图形化配置,点点鼠标就完成
这一步是新手最容易懵的环节,我拆成 6 个步骤,你跟着点就行,不用死记原理,先跑通实验再深究!
步骤 1:新建 CubeMX 工程,选择对应芯片
- 打开 STM32CubeMX 软件,点击首页的「ACCESS TO MCU SELECTOR」,进入芯片选择界面;
- 左上角搜索框输入你的开发板芯片型号(CT127C 开发板对应STM32WL55JCI7),在右侧结果中选中该芯片;
- 点击右上角「Start Project」,创建工程。
步骤 2:配置系统核心时钟
- 点击顶部「Clock Configuration」选项卡,进入时钟配置界面;
- 按以下参数配置(CT127C 开发板专用,其他开发板可根据芯片手册调整):
- 振荡器配置:开启 LSE(外部低速时钟,给 RTC 用)、MSI 内部时钟,MSI 范围选择RANGE_11(48MHz);
- 系统时钟源:SYSCLK 选择 MSI;
- 分频器:AHB、APB1、APB2 分频全部设为 1,最终系统时钟为 48MHz;
- 配置完成后,界面无红色报错即可。
步骤 3:配置 I2C 外设(OLED 驱动核心)
- 回到顶部「Pinout & Configuration」选项卡,左侧「Categories」→「Peripherals」,找到I2C1;
- 点击 I2C1,模式(Mode)选择「I2C」,此时你会发现开发板的 PB6、PB7 引脚自动被配置为 I2C1_SCL、I2C1_SDA,引脚变成绿色;
- 下方参数配置保持默认即可:
- 地址模式:7 位地址模式
- Timing 参数:0x10805D88
- 模拟滤波器、数字滤波器保持默认开启状态
- 无需手动修改 PB6、PB7 的引脚配置,CubeMX 会自动完成。
【配图 5:CubeMX 中 I2C1 配置界面截图,标注模式选择位置和自动映射的引脚】【配图 6:CubeMX 主界面引脚视图截图,标注 PB6、PB7 变成绿色后的高亮状态】
步骤 4:配套外设配置(RTC + 串口 + GPIO)
因为我们的实验要实现 RTC 时钟读取、按键计数,需要额外配置 3 个外设,同样点点鼠标就完成:
- RTC 配置:左侧找到 RTC,勾选「Activate Clock Source」激活时钟源,「Activate Calendar」激活日历功能,其他参数保持默认;
- USART2 配置:左侧找到 USART2,模式选择「Asynchronous」异步模式,波特率默认 115200,其他参数保持默认;
- DMA 配置:在 USART2 配置界面,点击「DMA Settings」,添加 USART2_RX 的 DMA 接收,模式选择 Normal,其他默认;
- GPIO 按键配置:确认 ASW1、ASW2 对应的 GPIO 引脚为输入模式,下拉 / 上拉根据开发板原理图配置(CT127C 开发板默认浮空输入即可)。
步骤 5:工程生成设置
- 点击顶部「Project Manager」选项卡,进入工程设置界面;
- 「Project」栏目:
- Project Name:给工程起个名字,比如「OLED_Demo」(不能有中文)
- Project Location:选择工程保存路径(不能有中文、空格)
- Toolchain/IDE:选择「MDK-ARM」,版本选 V5
- 「Code Generator」栏目:
- 核心勾选:Generate peripheral initialization as a pair of .c/.h files per peripheral(每个外设生成独立的.c/.h 文件,方便管理)
- 其他保持默认即可。
步骤 6:生成工程
点击右上角的「GENERATE CODE」按钮,等待工程生成。生成完成后,会弹出提示框,点击「Open Project」,就能自动打开 Keil MDK 工程了。
四、Keil MDK 代码编写与移植,复制粘贴也能成功
工程打开后,我们需要添加 OLED 驱动文件,封装自己的功能函数,全程我会给出完整代码,你直接复制替换就行。
步骤 1:添加驱动文件到工程目录
首先我们需要 4 个核心驱动文件:font.h(字库文件)、oled.h(OLED 驱动头文件)、oled.c(OLED 驱动核心实现)、app.h和app.c(我们自己封装的功能函数)。
- 打开你的工程文件夹,进入
Core/Src文件夹,把oled.c、app.c文件放进去; - 进入
Core/Inc文件夹,把font.h、oled.h、app.h文件放进去; - 回到 Keil 工程,左侧 Project 窗口,双击
Core/Src,点击「Add Files」,把刚才放进去的oled.c、app.c添加到工程中,点击 Add 确认。

步骤 2:逐个文件编写完整代码
下面我给出每个文件的完整代码,带详细注释,你直接打开对应文件,全选复制替换即可。
1. font.h
这个文件是 OLED 显示字符的点阵字库,包含 68 和 816 两种字号的 ASCII 字符,赛方会提供直接导入inc文件夹即可
2. oled.h
赛方会提供直接导入inc文件夹即可
3. oled.c
赛方会提供直接导入inc文件夹即可
4. app.h(功能函数头文件,替换原 zsdz.h,直接复制)
cpp
#ifndef __APP_H
#define __APP_H
#include "string.h"
#include "stdio.h"
#include "gpio.h"
#include "oled.h"
// 通用状态宏定义
#define APP_ON 1
#define APP_OFF 2
#define APP_TO 3
// OLED显示行号定义(0.91寸128*32屏,共4行,16号字体占2行)
#define APP_OLED_LINE1 0
#define APP_OLED_LINE2 2
// RTC时间结构体
typedef struct
{
unsigned char Year;
unsigned char Month;
unsigned char WeekDay;
unsigned char Date;
unsigned char Hours;
unsigned char Minutes;
unsigned char Seconds;
}RTC_APP;
// 全局变量声明
extern RTC_APP rtc_value;
// 功能函数声明
void app_write_AL(unsigned short int ALx,unsigned char state);
unsigned char app_read_AL(unsigned short int ALx);
unsigned char app_read_ASW(GPIO_TypeDef *ASWx);
void app_tx_UART(const unsigned char *Data);
void app_UART_IDLE_rx(void);
void app_UART_rx_loop(void);
void app_UART_rx_deal(void);
void app_read_RTC(void);
void OLED_Write(unsigned char Type, unsigned char Data);
void app_write_OLED(unsigned char Lin, unsigned char *Data);
void app_init(void);
#endif
5. app.c(功能函数实现,替换原 zsdz.c,直接复制)
cpp
#include "app.h"
// 串口DMA接收缓存区配置
#define g_u8uart_rx_buffer_len 100 // 最大接收长度
RTC_APP rtc_value; // RTC时间全局变量
unsigned char g_u8uart_rx_len; // 一帧数据接收长度
unsigned char g_u8uart_rx_end_flag; // 接收完成标志位
unsigned char g_u8uart_rx_buffer[g_u8uart_rx_buffer_len]; // 接收缓存数组
//函数名:app_write_AL
//作用:操作LED的状态
//形参:ALx:LED引脚;state:LED状态
//返回值:无
void app_write_AL(unsigned short int ALx,unsigned char state)
{
switch (state)
{
case APP_ON:
HAL_GPIO_WritePin(GPIOB, ALx, GPIO_PIN_RESET);
break;
case APP_OFF:
HAL_GPIO_WritePin(GPIOB, ALx, GPIO_PIN_SET);
break;
case APP_TO:
HAL_GPIO_TogglePin(GPIOB, ALx);
break;
default:
break;
}
}
//函数名:app_read_AL
//作用:读取LED的状态
//形参:ALx:LED引脚
//返回值:LED的状态
unsigned char app_read_AL(unsigned short int ALx)
{
unsigned char state=HAL_GPIO_ReadPin(GPIOB, ALx);
if(state == GPIO_PIN_SET)
return APP_OFF;
else
return APP_ON;
}
//函数名:app_read_ASW
//作用:读取按键的状态(带消抖)
//形参:ASWx:按键引脚
//返回值:按键的状态
unsigned char app_read_ASW(GPIO_TypeDef *ASWx)
{
unsigned char state = APP_OFF;
if(HAL_GPIO_ReadPin(ASWx,GPIO_PIN_8) == GPIO_PIN_RESET)
{
HAL_Delay(20); // 消抖延时
if(HAL_GPIO_ReadPin(ASWx,GPIO_PIN_8) == GPIO_PIN_RESET)
{
state = APP_ON;
while(HAL_GPIO_ReadPin(ASWx,GPIO_PIN_8) == GPIO_PIN_RESET); // 等待按键松开
}
}
return state;
}
//函数名:app_tx_UART
//作用:串口发送数据
//形参:Data:要发送的数据
//返回值:无
void app_tx_UART(const unsigned char *Data)
{
extern UART_HandleTypeDef huart2;
HAL_UART_Transmit(&huart2,Data,strlen((const char *)Data),strlen((const char *)Data));
}
//函数名:fputc
//作用:printf重定向,用于串口调试
int fputc(int ch,FILE *f)
{
extern UART_HandleTypeDef huart2;
HAL_UART_Transmit(&huart2,(unsigned char*)&ch,1,1);
return 0;
}
//函数名:app_UART_IDLE_rx
//作用:利用IDLE中断判断串口数据是否接收完成
//形参:无
//返回值:无
void app_UART_IDLE_rx(void)
{
extern UART_HandleTypeDef huart2;
extern DMA_HandleTypeDef hdma_usart2_rx;
if(__HAL_UART_GET_FLAG(&huart2,UART_FLAG_IDLE)==SET)//idle标志位被置位
{
__HAL_UART_CLEAR_IDLEFLAG(&huart2);//清除idle标志位
HAL_UART_DMAStop(&huart2); //停止DMA传输
g_u8uart_rx_len=g_u8uart_rx_buffer_len -__HAL_DMA_GET_COUNTER(&hdma_usart2_rx);//计算接收数据长度
g_u8uart_rx_end_flag=1;//置位接收完成标志
}
}
//函数名:app_UART_rx_loop
//作用:串口接收循环处理
//形参:无
//返回值:无
void app_UART_rx_loop(void)
{
extern UART_HandleTypeDef huart2;
if(g_u8uart_rx_end_flag)//如果串口接收完成
{
// 串口数据处理函数
app_UART_rx_deal();
// 清除标志位,重新开启DMA接收
g_u8uart_rx_len=0;
g_u8uart_rx_end_flag=0;
HAL_UART_Receive_DMA(&huart2,g_u8uart_rx_buffer,g_u8uart_rx_buffer_len);
}
}
//函数名:app_UART_rx_deal
//作用:串口数据回环处理(收到什么发回什么)
//形参:无
//返回值:无
void app_UART_rx_deal(void)
{
extern UART_HandleTypeDef huart2;
HAL_UART_Transmit(&huart2,g_u8uart_rx_buffer,g_u8uart_rx_len,g_u8uart_rx_len);
}
//函数名:app_read_RTC
//作用:读取RTC实时时间
//形参:无
//返回值:无
void app_read_RTC(void)
{
extern RTC_HandleTypeDef hrtc;
RTC_DateTypeDef data_value;
RTC_TimeTypeDef time_value;
// 必须先读时间,再读日期,否则会出错
HAL_RTC_GetTime(&hrtc,&time_value,RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc,&data_value,RTC_FORMAT_BIN);
// 赋值到全局结构体
rtc_value.Year = data_value.Year;
rtc_value.Month = data_value.Month;
rtc_value.WeekDay = data_value.WeekDay;
rtc_value.Date = data_value.Date;
rtc_value.Hours = time_value.Hours;
rtc_value.Minutes = time_value.Minutes;
rtc_value.Seconds = time_value.Seconds;
}
//函数名:OLED_Write
//作用:OLED底层I2C写函数(核心)
//形参:Type:命令/数据类型;Data:要写入的内容
//返回值:无
void OLED_Write(unsigned char Type, unsigned char Data)
{
extern I2C_HandleTypeDef hi2c1;
unsigned char pData[2];
pData[0] = Type;
pData[1] = Data;
// I2C主机发送函数:I2C句柄、OLED设备地址、数据、长度、超时时间
HAL_I2C_Master_Transmit(&hi2c1, 0x78, pData, 2, 10);
}
//函数名:app_write_OLED
//作用:OLED字符串显示封装函数,简化调用
//形参:Lin:显示行号;Data:要显示的字符串
//返回值:无
void app_write_OLED(unsigned char Lin, unsigned char *Data)
{
OLED_ShowString(0,Lin,Data,16); // 固定使用16号字体,x坐标0
}
//函数名:app_init
//作用:外设初始化总函数,main函数中调用
//形参:无
//返回值:无
void app_init(void)
{
extern UART_HandleTypeDef huart2;
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE); // 使能串口IDLE中断
HAL_UART_Receive_DMA(&huart2,g_u8uart_rx_buffer,g_u8uart_rx_buffer_len); // 开启串口DMA接收
OLED_Init(); // OLED初始化
}
6. main.c(主函数,替换原有内容,直接复制)
cpp
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2024 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "i2c.h"
#include "rtc.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "app.h"
#include "oled.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
MX_RTC_Init();
MX_I2C1_Init();
/* USER CODE BEGIN 2 */
// 我们的功能初始化,必须放在外设初始化之后
app_init();
unsigned char oled_buffer[16]; // OLED显示缓存数组
unsigned char key_count = 0; // 按键计数变量
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
// 1. 读取RTC实时时间
app_read_RTC();
// 格式化时间字符串,显示在OLED第一行
sprintf((char *)oled_buffer,"RTC:%d:%d:%d",rtc_value.Hours,rtc_value.Minutes,rtc_value.Seconds);
app_write_OLED(APP_OLED_LINE1,oled_buffer);
// 2. 读取按键状态,按下一次计数+1
if(app_read_ASW(ASW1_GPIO_Port) == APP_ON) key_count++;
if(app_read_ASW(ASW2_GPIO_Port) == APP_ON) key_count++;
// 格式化按键计数字符串,显示在OLED第二行
sprintf((char *)oled_buffer,"KEY:%d",key_count);
app_write_OLED(APP_OLED_LINE2,oled_buffer);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure LSE Drive Capability
*/
HAL_PWR_EnableBkUpAccess();
__HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
/** Configure the main internal regulator output voltage
*/
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI|RCC_OSCILLATORTYPE_LSE
|RCC_OSCILLATORTYPE_MSI;
RCC_OscInitStruct.LSEState = RCC_LSE_ON;
RCC_OscInitStruct.MSIState = RCC_MSI_ON;
RCC_OscInitStruct.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT;
RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11;
RCC_OscInitStruct.LSIDiv = RCC_LSI_DIV1;
RCC_OscInitStruct.LSIState = RCC_LSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Configure the SYSCLKSource, HCLK, PCLK1 and PCLK2 clocks dividers
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK3|RCC_CLOCKTYPE_HCLK
|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1
|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_MSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.AHBCLK3Divider = RCC_SYSCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
步骤 3:开启 MicroLIB 库(必做!否则 sprintf 会报错)
这是新手 90% 会踩的坑!使用 sprintf 格式化字符串,必须开启 MicroLIB 库:
- 点击 Keil 上方的魔术棒按钮(Options for Target);
- 进入「Target」选项卡,勾选下方的「Use MicroLIB」;
- 点击 OK 确认。
五、工程编译与程序下载
1. 工程编译
- 点击 Keil 上方的「Build」按钮(图标是两个向下的箭头),或者直接按键盘 F7;
- 等待编译完成,看下方 Build Output 窗口,如果显示0 Error(s), 0 Warning(s),就说明编译完美成功!
2. 程序下载到开发板
- 用 MicroUSB 线连接开发板和电脑,确保开发板上电,驱动正常安装;
- 点击 Keil 上方的「Download」按钮(图标是 Flash),或者直接按键盘 F8;
- 等待下载完成,下方提示「Program Done」,就说明程序已经成功下载到开发板里了!
【配图 13:Keil 下载按钮位置截图,标注下载按钮】【配图 14:下载成功后的提示窗口截图】
六、实验现象与效果展示
程序下载完成后,你会看到:
- OLED 屏第一行实时显示 RTC 时间,格式为
RTC:时:分:秒,秒数每秒自动跳动更新; - 第二行显示按键按下的次数
KEY:数字,每按一次开发板上的 ASW1 或 ASW2 按键,数字就会加 1。
【配图 15:OLED 屏实验现象实拍图,标注第一行的实时时间和第二行的按键计数】
自定义显示内容小技巧
想让 OLED 显示你想要的内容?超简单!
- 修改
sprintf里的双引号内容,就能改显示的文字; - 修改
app_write_OLED的第一个参数,就能改显示的行号(可选 0、2,对应第一行、第二行); - 比如想显示你的昵称,就加一行:
sprintf((char *)oled_buffer,"Hello 小白!"); app_write_OLED(0,oled_buffer);