2026年寒假在家一起练:OLED数字温度与电压表
2026寒假练 - 基于RP2350实现OLED数字温度与电压表
任务介绍
在本次寒假综合训练中,任务目标是开发一个基于RP2350B核心板的OLED数字温度与电压表。该任务需要实现以下功能:
- SPI ADC读取电位器电压,获取实时电压数据。
- DS18B20温度传感器读取温度,用于显示实时环境温度。
- SPI OLED显示器同时显示数值与条形图,用户可以直观地看到温度与电压的变化。
- 按键设置温度上限,当温度超过设定上限时,蜂鸣器发出警报,LED灯指示告警状态。
- 双位七段数码管显示当前工作模式或显示页面,帮助用户快速识别当前操作状态。
项目采用树莓派Pico系列开发板和VSCODE开发环境进行开发,主要通过PIO编程实现硬件接口,解决了硬件接线不匹配的问题。
项目介绍
本项目基于RP2350B核心板,使用树莓派Pico的PIO(可编程输入输出)模块来实现SPI和UART接口的功能。项目包括温度与电压读取、数据处理、显示和警报功能。通过开发定制的C++类封装外设驱动,简化了硬件与软件的交互,并且能够灵活地调整各个模块的功能。
硬件介绍
RP2350B核心板
RP2350B核心板是树莓派Pico系列的一个变种,提供多个I/O接口,适用于嵌入式应用开发。开发过程中主要利用了其SPI、UART、GPIO等接口。
ADS7868 ADC
该模数转换器(ADC)用于通过SPI接口读取电位器的电压值。ADS7868支持8位分辨率输出,通过SPI接口进行数据传输,数据按MSB(最重要位)优先传输。
DS18B20温度传感器
DS18B20是一款单总线温度传感器,可以通过一根数据线与主机通信。项目中使用PIO编程来读取温度数据。
SSD1306 OLED显示器
该OLED显示器使用四线SPI接口,显示温度、电压及条形图。与RP2350B核心板的GPIO接口连接,显示效果清晰直观。
蜂鸣器(Beeper)
蜂鸣器通过PWM控制发出2KHz的方波,达到报警的目的。当温度超过设定的上限时,蜂鸣器启动报警。
双位七段数码管
该数码管用于显示当前工作模式或页面,方便用户查看系统状态。
方案框图和项目设计思路
项目框架设计如图所示,采用SPI接口与多个外设进行数据交互,PIO模块模拟UART与其他模块通信。系统主要分为温度读取、电压读取、数据处理、显示与警报模块。
系统框图

项目设计思路
- 硬件接口与驱动封装:使用C++编写SPI、UART、GPIO等硬件接口的封装类,简化硬件与软件的交互。
- PIO编程:由于硬件接线不匹配,采用树莓派Pico的PIO模块来模拟SPI和UART接口的时序,确保外设与核心板的兼容性。
- 数据读取与处理:通过SPI读取电位器的电压值,读取DS18B20传感器的温度,实时处理并更新显示。
- 显示与警报:OLED显示实时数据,当温度超过上限时,蜂鸣器和LED进行警报,提醒用户。
调试软件及编程语言说明
编程语言
本项目主要使用C++语言进行开发,利用树莓派Pico的官方SDK进行硬件控制。通过PIO模块编写硬件接口的时序控制,完成对外设的操作。
调试软件
使用VSCODE作为开发环境,通过插件Raspberry Pi Pico支持C/C++编程和调试。调试过程中使用DAP调试器,切换到Debug模式,方便进行代码单步调试。
软件流程图及关键代码介绍
软件流程图
- 初始化阶段:初始化SPI、GPIO、UART等硬件接口。
- 数据读取:通过SPI接口读取电位器数据,使用PIO读取DS18B20的温度数据。
- 数据处理:处理读取到的电压和温度数据,并将其格式化为显示内容。
- 显示阶段:将处理后的数据实时显示在OLED上。
- 警报处理:当温度超过设定上限时,启动蜂鸣器发出警报,并点亮LED指示灯。

添加STEP board
在pico sdk中添加小脚丫RP2350B STEP开发板,新建pico2_step.h`,放到目录:
C:\Users\[user]\.pico-sdk\sdk\2.2.0\src\boards\include\boards\pico2_step.h
[user]修改为自己电脑的用户名,修改了:
#define PICO_RP2350A 0表示使用RP2350B#define PICO_FLASH_SIZE_BYTES (2 * 1024 * 1024)FLASH大小为2MB
详细内容如下:
/*
* Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
// -----------------------------------------------------
// NOTE: THIS HEADER IS ALSO INCLUDED BY ASSEMBLER SO
// SHOULD ONLY CONSIST OF PREPROCESSOR DIRECTIVES
// -----------------------------------------------------
// This header may be included by other board headers as "boards/pico2.h"
#ifndef _BOARDS_PICO2_H
#define _BOARDS_PICO2_H
pico_board_cmake_set(PICO_PLATFORM, rp2350)
// For board detection
#define RASPBERRYPI_PICO2
// --- RP2350 VARIANT ---
#define PICO_RP2350A 0
// --- UART ---
#ifndef PICO_DEFAULT_UART
#define PICO_DEFAULT_UART 0
#endif
#ifndef PICO_DEFAULT_UART_TX_PIN
#define PICO_DEFAULT_UART_TX_PIN 0
#endif
#ifndef PICO_DEFAULT_UART_RX_PIN
#define PICO_DEFAULT_UART_RX_PIN 1
#endif
// --- LED ---
#ifndef PICO_DEFAULT_LED_PIN
#define PICO_DEFAULT_LED_PIN 25
#endif
// no PICO_DEFAULT_WS2812_PIN
// --- I2C ---
#ifndef PICO_DEFAULT_I2C
#define PICO_DEFAULT_I2C 0
#endif
#ifndef PICO_DEFAULT_I2C_SDA_PIN
#define PICO_DEFAULT_I2C_SDA_PIN 4
#endif
#ifndef PICO_DEFAULT_I2C_SCL_PIN
#define PICO_DEFAULT_I2C_SCL_PIN 5
#endif
// --- SPI ---
#ifndef PICO_DEFAULT_SPI
#define PICO_DEFAULT_SPI 0
#endif
#ifndef PICO_DEFAULT_SPI_SCK_PIN
#define PICO_DEFAULT_SPI_SCK_PIN 18
#endif
#ifndef PICO_DEFAULT_SPI_TX_PIN
#define PICO_DEFAULT_SPI_TX_PIN 19
#endif
#ifndef PICO_DEFAULT_SPI_RX_PIN
#define PICO_DEFAULT_SPI_RX_PIN 16
#endif
#ifndef PICO_DEFAULT_SPI_CSN_PIN
#define PICO_DEFAULT_SPI_CSN_PIN 17
#endif
// --- FLASH ---
#define PICO_BOOT_STAGE2_CHOOSE_W25Q080 1
#ifndef PICO_FLASH_SPI_CLKDIV
#define PICO_FLASH_SPI_CLKDIV 2
#endif
pico_board_cmake_set_default(PICO_FLASH_SIZE_BYTES, (2 * 1024 * 1024))
#ifndef PICO_FLASH_SIZE_BYTES
#define PICO_FLASH_SIZE_BYTES (2 * 1024 * 1024)
#endif
// Drive high to force power supply into PWM mode (lower ripple on 3V3 at light loads)
#define PICO_SMPS_MODE_PIN 23
// The GPIO Pin used to read VBUS to determine if the device is battery powered.
#ifndef PICO_VBUS_PIN
#define PICO_VBUS_PIN 24
#endif
// The GPIO Pin used to monitor VSYS. Typically you would use this with ADC.
// There is an example in adc/read_vsys in pico-examples.
#ifndef PICO_VSYS_PIN
#define PICO_VSYS_PIN 29
#endif
pico_board_cmake_set_default(PICO_RP2350_A2_SUPPORTED, 1)
#ifndef PICO_RP2350_A2_SUPPORTED
#define PICO_RP2350_A2_SUPPORTED 1
#endif
#endif
修改CMakeLists.txt中的PICO_BOARD变量为pico2_step
set(PICO_BOARD pico2_step CACHE STRING "Board type")
清理缓存,重新编译。
PIO
本次设计几个核心模块采用pio实现。RP2350B有三个独立的PIO模块,每个模块有四个状态机,共享32条指令。由于pio模块和寄存器深度集成,真正需要写的汇编代码非常少。

ADS7868
引脚功能,数据只有SDO输出

时序图,前四个时钟采样,后面8位输出,所以一次传输共计12bit。

实现spi功能只需要两条指令,后面的c-sdk为c语言初始化函数。
.program spi_cpha0
.side_set 1
out x, 1 side 0 [1] ; Stall here on empty (sideset proceeds even if
in pins, 1 side 1 [1] ; instruction stalls, so we stall with SCK low)
% c-sdk {
#include "hardware/gpio.h"
static inline void pio_spi_init(PIO pio, uint sm, uint prog_offs, uint n_bits,
float clkdiv, bool cpha, bool cpol, uint pin_sck, uint pin_miso) {
pio_sm_config c = cpha ? spi_cpha1_program_get_default_config(prog_offs) : spi_cpha0_program_get_default_config(prog_offs);
sm_config_set_in_pins(&c, pin_miso);
sm_config_set_sideset_pins(&c, pin_sck);
// Only support MSB-first in this example code (shift to left, auto push/pull, threshold=nbits)
sm_config_set_out_shift(&c, false, true, n_bits);
sm_config_set_in_shift(&c, false, true, n_bits);
sm_config_set_clkdiv(&c, clkdiv);
// SCK output are low, MISO is input
pio_sm_set_pins_with_mask(pio, sm, 0, (1u << pin_sck));
pio_sm_set_pindirs_with_mask(pio, sm, (1u << pin_sck), (1u << pin_sck) | (1u << pin_miso));
pio_gpio_init(pio, pin_miso);
pio_gpio_init(pio, pin_sck);
// The pin muxes can be configured to invert the output (among other things
// and this is a cheesy way to get CPOL=1
gpio_set_outover(pin_sck, cpol ? GPIO_OVERRIDE_INVERT : GPIO_OVERRIDE_NORMAL);
// SPI is synchronous, so bypass input synchroniser to reduce input delay.
hw_set_bits(&pio->input_sync_bypass, 1u << pin_miso);
pio_sm_init(pio, sm, prog_offs, &c);
pio_sm_set_enabled(pio, sm, true);
}
封装为C++ 头文件,使用时直接包含头文件。注意这里初始化pio为12bit
pio_spi_init(_pio, _sm, _offset, 12, 31.25f, false, false, _sclk, _miso);
#pragma once
#include "hardware/pio.h"
#include "ads7868_spi.pio.h"
#include "hardware/clocks.h"
#include "pico/stdlib.h"
#include "uart_tx.hpp"
class ADS7868
{
public:
ADS7868(uint pin_sclk, uint pin_miso, uint pin_cs);
void init();
uint32_t read();
void pio_info()
{
UartTX &tx = UartTX::getInstance();
tx.println("ADS7868 initialized on PIO:%p, SM:%d, offset:%d", _pio, _sm, _offset);
}
private:
PIO _pio;
uint _sm, _offset;
uint _sclk, _miso, _cs;
};
ADS7868::ADS7868(uint pin_sclk, uint pin_miso, uint pin_cs)
: _sclk(pin_sclk), _miso(pin_miso), _cs(pin_cs) {}
void ADS7868::init()
{
// cs pin
gpio_init(_cs);
gpio_put(_cs, 1);
gpio_set_dir(_cs, GPIO_OUT);
bool success = pio_claim_free_sm_and_add_program(&spi_cpha0_program, &_pio, &_sm, &_offset);
hard_assert(success);
// float div = (float) clock_get_hz(clk_sys) / (12 * 1000 * 1000);
pio_spi_init(_pio, _sm, _offset, 12, 31.25f, false, false, _sclk, _miso);
}
uint32_t ADS7868::read()
{
uint32_t dst;
gpio_put(_cs, 0);
size_t tx_remain = 1, rx_remain = 1;
io_rw_32 *txfifo = (io_rw_32 *)&_pio->txf[_sm];
io_rw_32 *rxfifo = (io_rw_32 *)&_pio->rxf[_sm];
while (tx_remain || rx_remain)
{
if (tx_remain && !pio_sm_is_tx_fifo_full(_pio, _sm))
{
*txfifo = 0;
--tx_remain;
}
if (rx_remain && !pio_sm_is_rx_fifo_empty(_pio, _sm))
{
dst = *rxfifo;
--rx_remain;
}
}
sleep_us(1);
gpio_put(_cs, 1);
return dst;
}
逻辑分析仪采集的时序图(图中标识标注有误,蓝色的应该为SCLK,紫色的为MISO)

DS18B20
官方SDK有ds18b20 pio模块实现,这里引用自example中的例子。
class onewire
{
private:
PIO _pio;
OW _ow;
static constexpr int kMaxDevices = 10;
uint64_t _romcode[kMaxDevices];
public:
onewire(PIO pio, uint dq_pin);
float read_temperature();
~onewire();
};
onewire::onewire(PIO pio, uint dq_pin) : _pio(pio)
{
uint offset;
UartTX& tx = UartTX::getInstance();
if (pio_can_add_program (pio, &onewire_program)) {
offset = pio_add_program (pio, &onewire_program);
// claim a state machine and initialise a driver instance
if (ow_init (&_ow, pio, offset, dq_pin)) {
// find and display 64-bit device addresses
int num_devs = ow_romsearch (&_ow, _romcode, kMaxDevices, OW_SEARCH_ROM);
tx.println("Found %d ds18b20 devices", num_devs);
for (int i = 0; i < num_devs; i += 1) {
tx.println("\t%d: 0x%llx", i, _romcode[i]);
}
} else {
tx.println ("could not initialise the driver");
}
} else {
tx.println ("could not add the program");
}
}
float onewire::read_temperature()
{
float temperature = 0.0f;
// start temperature conversion in parallel on all devices
// (see ds18b20 datasheet)
ow_reset (&_ow);
ow_send (&_ow, OW_SKIP_ROM);
ow_send (&_ow, DS18B20_CONVERT_T);
// wait for the conversions to finish
while (ow_read(&_ow) == 0);
// read the result from first device
ow_reset (&_ow);
ow_send (&_ow, OW_MATCH_ROM);
for (int b = 0; b < 64; b += 8) {
ow_send (&_ow, _romcode[0] >> b);
}
ow_send (&_ow, DS18B20_READ_SCRATCHPAD);
int16_t temp = 0;
temp = ow_read (&_ow) | (ow_read (&_ow) << 8);
temperature = temp / 16.0f;
return temperature;
}
SSD1306 SPI OLED
采用四线SPI接口



和RP2350 GPIO连接如下:
#define OLED_PIN_CS 41
#define OLED_PIN_DC 42
#define OLED_PIN_RST 43
#define OLED_PIN_MOSI 44
#define OLED_PIN_SCK 45
Beeper
需要使用2KHz的方波驱动才能发声。BEEPER引脚连接到RP2350 GPIO20,通过PWM模块生成所需的2KHz信号。

class Beeper {
private:
uint _slice_num, _pin;
public:
Beeper(uint pin) :
_pin(pin)
{
gpio_set_function(_pin, GPIO_FUNC_PWM);
_slice_num = pwm_gpio_to_slice_num(_pin);
pwm_config config = pwm_get_default_config();
pwm_config_set_clkdiv(&config, 150.0f); // 设置频率
config.top = 500; // 设置PWM周期
pwm_init(_slice_num, &config, true); // 启动PWM
pwm_set_chan_level(_slice_num, PWM_CHAN_A, 250); // 50%占空比
}
void ctrl(bool on) {
pwm_set_enabled(_slice_num, on); // 控制蜂鸣器开关
}
~Beeper() {};
};
功能展示图及说明
实物展示
展示了RP2350B核心板、OLED显示器、温度传感器、蜂鸣器等硬件组件的连接与运行情况。温度、电压及警报状态清晰显示,系统响应快速。

软件调试
在调试过程中,通过VSCODE中的调试工具,成功实现了各个硬件模块的交互,确保了系统的稳定运行。
项目中遇到的难题及解决方法
难题1:硬件接口不匹配
由于RP2350B核心板的硬件接口与外设不完全匹配,导致无法直接使用硬件SPI和UART接口。为了解决这个问题,采用了PIO模块来模拟SPI和UART接口,确保了数据的正确传输。
难题2:蜂鸣器驱动问题
蜂鸣器需要2KHz的方波信号进行驱动,刚开始通过GPIO驱动,发现无法输出声音。查询后才发现必须使用2KHz方波才能驱动,最终通过调整PWM配置和占空比,最终实现了蜂鸣器的正常发声。
心得体会
通过本项目的开发,我深入理解了树莓派Pico的硬件接口及PIO编程的强大功能。虽然面临了一些硬件不匹配和驱动配置的问题,但通过调试和查阅资料,我成功解决了这些问题。通过该项目,我不仅提升了硬件调试的能力,还对嵌入式开发和C++编程有了更深入的理解。