You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

树莓派Pico 4位共阴极7段数码管多路复用显示异常求助

树莓派Pico 4位共阴极7段数码管多路复用显示异常求助

我太懂你这种卡壳的感觉了——明明知道多路复用要逐个点亮数码管、靠人眼暂留实现“同时显示”,但就是不知道怎么把这个逻辑落地到代码里,看着数码管全显示整数部分,急得抓脑袋对吧?

先帮你拆解下当前代码的两个核心问题:

  1. 数字分解逻辑完全错了:你现在的display_voltage只把电压的整数部分拆成了四位,完全没处理小数部分,这就是所有数码管都显示整数的直接原因;
  2. 没有真正实现多路复用:你现在是一次性循环设置四个数码管,但既没有在每个数码管显示后加短暂停留,也没有持续高频刷新——数码管是靠快速切换维持显示的,只跑一次的话,人眼根本捕捉不到切换过程,最后看起来就是所有数码管亮同一个数字。

下面给你一套落地的修改方案,完全贴合你提到的“加中间函数分离数据与显示逻辑”的思路:


核心思路梳理

我们把逻辑拆成三个独立部分:

  1. 数据更新层:把电压值分解成数码管每一位要显示的数字,存在全局变量里(相当于“显示缓存”);
  2. 显示刷新层:高频调用一个函数,逐个点亮数码管、显示缓存里的对应数字,实现多路复用;
  3. 定时器调度:用定时器定期读取电压更新缓存,同时高频触发显示刷新,维持数码管常亮。

修改后的完整代码及解释

1. 先加全局“显示缓存”变量

用来保存每一位要显示的数字和小数点状态,让刷新逻辑直接读取,不用反复计算:

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"

// Pin assignments for 7-segment display (adjust these as needed)
#define SEG_A  2
#define SEG_B  3
#define SEG_C  4
#define SEG_D  5
#define SEG_E  6
#define SEG_F  7
#define SEG_G  8
#define SEG_DP 14 // Decimal point
#define DIGIT_1  9  // Digit 1 (最左边,显示整数位)
#define DIGIT_2  10 // Digit 2 (十分位)
#define DIGIT_3  11 // Digit 3 (百分位)
#define DIGIT_4  12 // Digit 4 (备用,这里留空显示0)

// Button for switching ADC input
#define BUTTON_PIN 15

// Define the 7-segment display segments for digits 0-9
const uint8_t digit_segments[] = {
    0b00111111, // 0
    0b00000110, // 1
    0b01011011, // 2
    0b01001111, // 3
    0b01100110, // 4
    0b01101101, // 5
    0b01111101, // 6
    0b00000111, // 7
    0b01111111, // 8
    0b01101111, // 9
};

// 全局显示缓存:保存每一位要显示的数字和小数点状态
int display_digits[4] = {0, 0, 0, 0}; // 四位数码管的数字
bool display_dp[4] = {false, false, false, false}; // 每一位的小数点状态
int current_refresh_digit = 0; // 当前正在刷新的数码管索引(0-3)

2. 新增「数据更新函数」:把电压转成数码管可识别的位

这个函数只负责计算每一位要显示的数字,不直接操作硬件:

// 更新显示缓存:把电压值分解为四位数码管的显示内容(两位小数)
void update_display_cache(float voltage) {
    // 把电压转成整数(*100+0.5是为了四舍五入,避免浮点误差)
    int scaled_voltage = (int)(voltage * 100 + 0.5); // 比如1.23 → 123,0.45 → 45

    // 分解到四位数码管:整数位、十分位、百分位、备用位
    display_digits[0] = scaled_voltage / 100; // 整数部分(比如123→1)
    display_digits[1] = (scaled_voltage / 10) % 10; // 十分位(123→2)
    display_digits[2] = scaled_voltage % 10; // 百分位(123→3)
    display_digits[3] = 0; // 备用位显示0,也可以设成不亮(把值设成无效数字,比如10,再在display_digit里处理)

    // 设置小数点:只在整数位后面亮
    display_dp[0] = true;
    display_dp[1] = false;
    display_dp[2] = false;
    display_dp[3] = false;
}

3. 新增「多路复用刷新函数」:逐个点亮数码管

这是实现多路复用的核心,每次只亮一个数码管,显示缓存里的对应内容:

// 刷新单个数码管,需要被高频调用
void refresh_single_digit() {
    // 先关闭所有数码管,避免重影
    gpio_put(DIGIT_1, 0);
    gpio_put(DIGIT_2, 0);
    gpio_put(DIGIT_3, 0);
    gpio_put(DIGIT_4, 0);

    // 先关闭所有段,避免上一个数字的残留
    gpio_put(SEG_A, 0);
    gpio_put(SEG_B, 0);
    gpio_put(SEG_C, 0);
    gpio_put(SEG_D, 0);
    gpio_put(SEG_E, 0);
    gpio_put(SEG_F, 0);
    gpio_put(SEG_G, 0);
    gpio_put(SEG_DP, 0);

    // 获取当前要显示的数字和小数点状态
    int target_digit = current_refresh_digit + 1; // 转成1-4的数码管编号
    int target_value = display_digits[current_refresh_digit];
    bool target_dp = display_dp[current_refresh_digit];

    // 处理无效数字(比如备用位想不亮的话,设成10,这里直接显示0)
    if (target_value < 0 || target_value > 9) {
        target_value = 0;
    }

    // 设置当前数字的段码
    uint8_t segments = digit_segments[target_value];
    gpio_put(SEG_A, (segments & 0x01) ? 1 : 0);
    gpio_put(SEG_B, (segments & 0x02) ? 1 : 0);
    gpio_put(SEG_C, (segments & 0x04) ? 1 : 0);
    gpio_put(SEG_D, (segments & 0x08) ? 1 : 0);
    gpio_put(SEG_E, (segments & 0x10) ? 1 : 0);
    gpio_put(SEG_F, (segments & 0x20) ? 1 : 0);
    gpio_put(SEG_G, (segments & 0x40) ? 1 : 0);
    gpio_put(SEG_DP, target_dp ? 1 : 0);

    // 点亮当前数码管(共阴极,置1点亮)
    switch(target_digit) {
        case 1: gpio_put(DIGIT_1, 1); break;
        case 2: gpio_put(DIGIT_2, 1); break;
        case 3: gpio_put(DIGIT_3, 1); break;
        case 4: gpio_put(DIGIT_4, 1); break;
    }

    // 短暂延时(1ms,人眼完全看不到闪烁)
    sleep_ms(1);

    // 切换到下一个要刷新的数码管,循环往复
    current_refresh_digit = (current_refresh_digit + 1) % 4;
}

4. 修改定时器回调:调度数据更新与显示刷新

定时器要同时做两件事:定期读电压更新缓存,高频触发显示刷新:

// 定时器回调函数
bool timer_callback(struct repeating_timer *t) {
    // 每100ms读一次电压(可以调整频率,太频繁会影响ADC精度)
    static absolute_time_t last_adc_read = nil_time;
    if (time_reached(last_adc_read)) {
        float voltage = read_adc_voltage(active_channel);
        printf("Voltage: %.2f\n", voltage);
        update_display_cache(voltage);
        last_adc_read = make_timeout_time_ms(100);
    }

    // 高频刷新数码管(每1ms一次,保证显示稳定)
    refresh_single_digit();

    return true;  // 保持定时器重复运行
}

5. 保留原有函数(仅做小调整)

// 初始化7段数码管GPIO
void init_7segment_display() {
    gpio_init(SEG_A); gpio_set_dir(SEG_A, GPIO_OUT);
    gpio_init(SEG_B); gpio_set_dir(SEG_B, GPIO_OUT);
    gpio_init(SEG_C); gpio_set_dir(SEG_C, GPIO_OUT);
    gpio_init(SEG_D); gpio_set_dir(SEG_D, GPIO_OUT);
    gpio_init(SEG_E); gpio_set_dir(SEG_E, GPIO_OUT);
    gpio_init(SEG_F); gpio_set_dir(SEG_F, GPIO_OUT);
    gpio_init(SEG_G); gpio_set_dir(SEG_G, GPIO_OUT);
    gpio_init(SEG_DP); gpio_set_dir(SEG_DP, GPIO_OUT);

    gpio_init(DIGIT_1); gpio_set_dir(DIGIT_1, GPIO_OUT);
    gpio_init(DIGIT_2); gpio_set_dir(DIGIT_2, GPIO_OUT);
    gpio_init(DIGIT_3); gpio_set_dir(DIGIT_3, GPIO_OUT);
    gpio_init(DIGIT_4); gpio_set_dir(DIGIT_4, GPIO_OUT);

    // 初始状态:所有数码管关闭
    gpio_put(DIGIT_1, 0);
    gpio_put(DIGIT_2, 0);
    gpio_put(DIGIT_3, 0);
    gpio_put(DIGIT_4, 0);
}

// 读取ADC电压
float read_adc_voltage(int channel) {
    adc_select_input(channel);
    uint16_t result = adc_read();
    const float conversion_factor = 3.3f / 4095.0f;
    return result * conversion_factor;
}

// 按键中断:切换ADC通道
int active_channel = 0;
void button_isr(uint gpio, uint32_t events) {
    active_channel = (active_channel + 1) % 2;
}

// 主函数
int main() {
    stdio_init_all();

    // 初始化ADC
    adc_init();
    adc_gpio_init(26);  // GPIO26 = ADC0
    adc_gpio_init(27);  // GPIO27 = ADC1

    // 初始化按键
    gpio_init(BUTTON_PIN);
    gpio_set_dir(BUTTON_PIN, GPIO_IN);
    gpio_pull_up(BUTTON_PIN);
    gpio_set_irq_enabled_with_callback(BUTTON_PIN, GPIO_IRQ_EDGE_FALL, true, &button_isr);

    // 初始化数码管
    init_7segment_display();

    // 启动定时器:1ms周期,高频触发回调
    struct repeating_timer timer;
    add_repeating_timer_ms(1, timer_callback, NULL, &timer);

    // 主循环空转,所有逻辑靠定时器和中断处理
    while (1) {
        tight_loop_contents();
    }

    return 0;
}

关键逻辑解释

  1. 为什么要分离数据与显示?:把电压计算和硬件刷新分开,代码逻辑更清晰,也避免了每次刷新都要计算电压的冗余操作;
  2. 为什么要高频刷新?:人眼的暂留时间大概是10-20ms,我们用1ms一次的刷新频率,远高于感知阈值,所以看起来所有数码管都是常亮的;
  3. 为什么要先关闭所有数码管?:如果不关闭,上一个数码管的电流会串到下一个,导致重影,显示模糊。

替换成这套代码后,你应该就能看到数码管正确显示两位小数的电压值了,旋转电位器时数字会平滑变化,按键切换ADC通道也能正常切换显示。

备注:内容来源于stack exchange,提问作者SlMN099

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.16 02:58:08