前言
本项目实现了2023寒假一起练平台(1)- 基于STEP Pico的嵌入式系统学习平台的项目四的要求,“ 制作一个电压表”,即
利用板上的电位计调节电压从0-3.3V之间变化,在OLED显示屏上显示电压值,可以以数字的方式,也可以以图形的方式来显示。
本项目使用树莓派pico的内置ADC,采集电位器输出的电压,让后将电压以数值的形式显示在OLED屏上。同时,通过图形化的方式,记录并展示电位器电压的历史数据。
硬件介绍
2023寒假一起练平台(1)是基于树莓派pico的嵌入式系统学习平台
树莓派pico是树莓派基金会推出的一款低成本的双核MCU开发板,pico的核心是名为RP2040单片机,内部拥有两个主频为133MHz的Cortex-M0+内核和高达264KB的SRAM。除此之外,还拥有ADC,SPI,IIC,USB,PIO等众多外设。它可以通过C/C++ SDK, Micro Python, Arduino等多种手段进行开发。是电子爱好者的绝佳入门平台。
当然,树莓派官方版本的PICO开发板,它的上面除了一颗LED外,并没有携带更多的外设,对于学习研究来说十分不方便。所以本次活动使用的平台是基于硬禾改进版本的PICO,除了与官方版本兼容之外,额外增加了4颗可编程RGB-LED灯以及一个复位按键。还将micro-usb接口更改为了更为通用的type-C接口。
除此之外,为了更好的学习pico的开发,本次活动还搭配了一块树莓派pico的扩展底板,底板上扩展了更多的外设:
- 2个按键输入
- 4个单色LED
- 12个WS2812B RGB三色灯
- 1个姿态传感器
- 1个128*64 OLED显示屏
- 1个蜂鸣器
- 1个可调电位计(用于电压表)
- 1路音频信号输入(用于示波器)
- 8位R-2R电阻网络构成的DAC(用于DDS信号发生器)
底板的引脚映射图如下:
本项目使用底板上的电位器,将电位器上采集到的电压显示在底板的OLED显示屏上
开发环境介绍
PICO可以通过C/C++ SDK, Micro Python, Arduino等多种手段进行开发。本项目中使用的是经典的Arduino IDE来进行开发。
实现思路
对象和参数的定义
首先,因为我们需要驱动OLED显示屏来进行文本的显示。所以需要一个显示屏的驱动库和图形库。在本项目中,我使用了Adafruit的SSD1306屏幕驱动库和AdafruitGFX图形库。
为了使用这两个库,我们先通过Arduino的库管理器进行安装,然后在文件的开始处将这两个库以及依赖的基础库包含进来:
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
然后我们需要创建一个OLED库的驱动对象,而创建这个对象则需要我们提供一些硬件参数,主要是屏幕的尺寸以及PICO与屏幕连接使用的引脚编号。
为了方便后期的移植与更改,此处我们使用宏定义将需要的参数先定义好,然后再以参数的形式传入到OLED驱动的构造函数中:
// 定义显示屏的宽度与高度
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// 定义OLED显示屏的物理引脚
#define OLED_MOSI 11
#define OLED_CLK 10
#define OLED_DC 9
#define OLED_CS -1
#define OLED_RESET 8
// 定义电位器连接的ADC引脚
#define ADC_PIN 28
// 显示屏驱动对象
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
这里需要注意 1. 我们同时将电位器所连接的引脚编号一同定义,方便后面的使用 2. 因为OLED的CS引脚并未连接到PICO,而是直接接地。所以我们此处的OLED的CS(使能)引脚需要定义为-1,也就是不使用CS控制
因为我设计了需要使用图形化的方式记录电压的变化曲线,所以这个地方,还要额外定义用来记录历史电压所需要的缓存以及相关变量。
// 采集到的数据历史缓存
static uint16_t hisDataSize = 0;
static uint8_t hisData[128] = {0};
系统的初始化
在所有的参数和对象定义完毕之后,我们就可以开始着手编写进行整个系统的软硬件初始化代码了。
我们首先初始化串口,用于可能的调试信息打印:
// 初始化串口通讯
Serial.begin(115200);
然后设置ADC的分辨率:
// 设置ADC的分辨率
analogReadResolution(12);
其实,设置ADC分辨率并不是必要的操作。PICO内置的ADC拥有12位的最高分辨率,所以我们这里设置的是12位的最高分辨率,也就是0~4095的范围。如果不设置的话,ADC也能正常工作,只不过分辨率只有8位,也就是0~255的范围。相对来说就没有那么精确了。但是这样也有一个额外的好处,那就是采样的速度可以变快。不错对于本项目的需求来说,ADC的采样速度并不是我们关注的重心。所以我们优先使用高分辨率的采样方式。
然后我们初始化显示屏。
// 初始化显示屏
if (!display.begin(SSD1306_SWITCHCAPVCC)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;);
}
这里代码中需要进行一个错误处理。原因是adafruit的这个图形库是全屏缓存的,所以需要128x64/8共计1KB的缓存。如果内置的SRAM不足,这里请求分配内存的时候就会失败。失败之后我们就直接中止程序就好。
当然,由于pico拥有高达264KB的SRAM,所以基本不可能出现这个错误,但是处于程序的鲁棒性考虑,我们还是保留这个错误处理代码。
显示屏初始化结束后,我们调用一下display()这个函数来测试OLED屏是否初始化成功,如果成功,则显示屏上就会出现Adafruit家的logo。如果你没有在屏幕上看到正确的logo,那说明显示屏没有被正常初始化。这种情况大概率是因为显示驱动的初始化参数不正确。这时候你就需要检查显示屏的物理连接引脚与创建驱动时设置的引脚是否一致了。
// 清屏
display.display();
delay(2000);
至此,全部的硬件初始化流程就结束了
主要业务逻辑循环
全部的初始化结束之后,我们就开始进入程序的主循环函数loop函数了,它会不停的循环连续执行,我们在需要在这里放置主要的代码逻辑。
void loop() {
// 主要业务逻辑代码
}
在每次循环的开始,我们先对显示屏缓冲区进行清空操作,防止上次显示的内容与本次显示的内容发生混叠。
// 清屏
display.clearDisplay();
然后,我们读取ADC的数值.
// 读取ADC数值
uint16_t adcVal = analogRead(ADC_PIN);
因为我们刚刚设置的ADC分辨率是12位,所以这里ADC采样结果的范围就是0~4095。又因为根据硬件设计,电压范围是0~3.3v,也就是0~3300mV。
所以我们这里经过简单的运算就可以获得电位器输出的电压了,当然此处的电压是以毫伏为单位的。
// 转换成电压
uint16_t voltage = (adcVal * 3300) / 4095;
得到电压后,我们根据本轮采集到的电压更新历史数据缓存,将电压记录,并随后在屏幕上绘制出来。
这里的代码需要注意区分缓冲区未满和已满的情况。
在缓冲区未满的情况下,我们只需要简单的进行数据填充即可。而如果缓冲区已满,则我们需要先移动缓冲区中所有的数据,然后在添加本轮采集到的数据。
因为我们最终使用的是图形的方式来展示历史数据(面积图),为了方便之后图形的绘制,在缓存中,我们存放的数据并不是原生的电压数据,而是进行了一次转换,将其转换为了面积图中每个数据项所对应的数据条的高度。
// 更新历史数据记录
if (hisDataSize != 128) {
hisData[hisDataSize++] = (adcVal * 50) / 4095;
}
else {
for (int i = 0; i < 127; i++) {
hisData[i] = hisData[i + 1];
}
hisData[127] = (adcVal * 50) / 4095;
}
所有的数据都准备就绪了,我们开始进行图形和文本的绘制工作。
首先是显示电压值。这里多加了一个判断,这个判断的含义是每采集二十次电压,显示一次。因为ADC的采样速度是非常快的,如果不加限制,那么屏幕上的数据就会显示的飞快而难以阅读,所以我们设定这个循环是间隔25ms执行一次,也就是说每秒50帧。但即使是这样,对于文本的阅读来说,这个数值的更新速度还是太快了,所以我们每20轮采样后,更新一次电压,相当于0.5s更新一次数据,这样看起来就会好很多。
因为刚刚采集得到的结果是毫伏为单位的电压,这里,我们通过sprintf函数,来将其转化为单位为V的文本形式。
然后通过图形库提供的代码,将文本化的电压显示在屏幕的左上角。
// 在屏幕上显示测量的电压值
// 此处为了防止电压更新过快看不清楚,所以每20次循环(也就是20帧),更新一次电压显示
if ((loops % 20) == 0) {
sprintf(volToDisp, "Vol: %d.%03d V", voltage / 1000, voltage % 1000);
}
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.cp437(true);
display.printf(volToDisp);
接着,我们绘制历电压图表,这个就比较简单了,我们只需要将缓存中的数据,绘制成高低不同的竖线,然后从左到右依次显示即可。出来的即是历史电压数据的面积图,可以很方便用来观察电压的历史变化。
// 绘制历史数据图表
for (int i = 0; i < hisDataSize; i++) {
display.drawLine(i, 63, i, 63 - hisData[i], SSD1306_WHITE);
}
所有绘制到这里就结束了,最后,我们需要调用display函数将显存中的数据显示到屏幕上。如同上面所说,我们等待25毫秒再进行下一轮的操作。
display.display();
delay(25);
loops++;
程序到这里,也就全部结束了,我们已经实现了所有的功能。
全部的代码如下:
/**************************************************************************
2023寒假练平台(一)项目四程序代码
Created by Geralt
**************************************************************************/
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// 定义显示屏的宽度与高度
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// 定义OLED显示屏的物理引脚
#define OLED_MOSI 11
#define OLED_CLK 10
#define OLED_DC 9
#define OLED_CS -1
#define OLED_RESET 8
// 定义电位器连接的ADC引脚
#define ADC_PIN 28
// 显示屏驱动对象
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
// 采集到的数据历史缓存
static uint16_t hisDataSize = 0;
static uint8_t hisData[128] = {0};
void setup() {
// 初始化串口通讯
Serial.begin(115200);
// 设置ADC的分辨率
analogReadResolution(12);
// 初始化显示屏
if (!display.begin(SSD1306_SWITCHCAPVCC)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;);
}
// 清屏
display.display();
delay(2000);
}
void loop() {
// 循环次数
static uint32_t loops = 0;
// 字符串缓存
char volToDisp[32];
// 清屏
display.clearDisplay();
// 读取ADC数值
uint16_t adcVal = analogRead(ADC_PIN);
// 转换成电压
uint16_t voltage = (adcVal * 3300) / 4095;
// 更新历史数据记录
if (hisDataSize != 128) {
hisData[hisDataSize++] = (adcVal * 50) / 4095;
}
else {
for (int i = 0; i < 127; i++) {
hisData[i] = hisData[i + 1];
}
hisData[127] = (adcVal * 50) / 4095;
}
// 在屏幕上显示测量的电压值
// 此处为了防止电压更新过快看不清楚,所以每20次循环(也就是20帧),更新一次电压显示
if ((loops % 20) == 0) {
sprintf(volToDisp, "Vol: %d.%03d V", voltage / 1000, voltage % 1000);
}
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.cp437(true);
display.printf(volToDisp);
// 绘制历史数据图表
for (int i = 0; i < hisDataSize; i++) {
display.drawLine(i, 63, i, 63 - hisData[i], SSD1306_WHITE);
}
display.display();
delay(25);
loops++;
}
结语
通过本次活动,我们学习到了树莓派pico开发的一些基本知识。在玩的开心的同时也学到了很多知识。希望我在本次活动中的分享能够帮助到你。也希望电子森林的活动能越办越好。谢谢大家