2026寒假练 - 基于RP2350实现OLED数字温度与电压表
该项目使用了RP2350,实现了OLED数字温度与电压表的设计,它的主要功能为:读取电位器电压,DS18B20 读取温度,SPI OLED显示。
标签
嵌入式系统
RP2350
2026
寒假
bigzhu
更新2026-03-24
35

2026年寒假在家一起练:OLED数字温度与电压表

2026寒假练 - 基于RP2350实现OLED数字温度与电压表

任务介绍

在本次寒假综合训练中,任务目标是开发一个基于RP2350B核心板的OLED数字温度与电压表。该任务需要实现以下功能:

  1. SPI ADC读取电位器电压,获取实时电压数据。
  2. DS18B20温度传感器读取温度,用于显示实时环境温度。
  3. SPI OLED显示器同时显示数值与条形图,用户可以直观地看到温度与电压的变化。
  4. 按键设置温度上限,当温度超过设定上限时,蜂鸣器发出警报,LED灯指示告警状态。
  5. 双位七段数码管显示当前工作模式或显示页面,帮助用户快速识别当前操作状态。

项目采用树莓派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与其他模块通信。系统主要分为温度读取、电压读取、数据处理、显示与警报模块。

系统框图

项目设计思路

  1. 硬件接口与驱动封装:使用C++编写SPI、UART、GPIO等硬件接口的封装类,简化硬件与软件的交互。
  2. PIO编程:由于硬件接线不匹配,采用树莓派Pico的PIO模块来模拟SPI和UART接口的时序,确保外设与核心板的兼容性。
  3. 数据读取与处理:通过SPI读取电位器的电压值,读取DS18B20传感器的温度,实时处理并更新显示。
  4. 显示与警报:OLED显示实时数据,当温度超过上限时,蜂鸣器和LED进行警报,提醒用户。

调试软件及编程语言说明

编程语言

本项目主要使用C++语言进行开发,利用树莓派Pico的官方SDK进行硬件控制。通过PIO模块编写硬件接口的时序控制,完成对外设的操作。

调试软件

使用VSCODE作为开发环境,通过插件Raspberry Pi Pico支持C/C++编程和调试。调试过程中使用DAP调试器,切换到Debug模式,方便进行代码单步调试。

软件流程图及关键代码介绍

软件流程图

  1. 初始化阶段:初始化SPI、GPIO、UART等硬件接口。
  2. 数据读取:通过SPI接口读取电位器数据,使用PIO读取DS18B20的温度数据。
  3. 数据处理:处理读取到的电压和温度数据,并将其格式化为显示内容。
  4. 显示阶段:将处理后的数据实时显示在OLED上。
  5. 警报处理:当温度超过设定上限时,启动蜂鸣器发出警报,并点亮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++编程有了更深入的理解。

附件下载
oled-temp-voltage.uf2
编译好的固件
oled-temp-voltage.zip
C++源代码
oled-temp-voltage-v2.zip
带温度条形图显示
oled-temp-voltage.uf2
带温度条形图显示固件
团队介绍
嵌入式工程师,电子爱好者,爱折腾~
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号