树莓派Pico 4位共阴极7段数码管多路复用显示异常求助
树莓派Pico 4位共阴极7段数码管多路复用显示异常求助
我太懂你这种卡壳的感觉了——明明知道多路复用要逐个点亮数码管、靠人眼暂留实现“同时显示”,但就是不知道怎么把这个逻辑落地到代码里,看着数码管全显示整数部分,急得抓脑袋对吧?
先帮你拆解下当前代码的两个核心问题:
- 数字分解逻辑完全错了:你现在的
display_voltage只把电压的整数部分拆成了四位,完全没处理小数部分,这就是所有数码管都显示整数的直接原因; - 没有真正实现多路复用:你现在是一次性循环设置四个数码管,但既没有在每个数码管显示后加短暂停留,也没有持续高频刷新——数码管是靠快速切换维持显示的,只跑一次的话,人眼根本捕捉不到切换过程,最后看起来就是所有数码管亮同一个数字。
下面给你一套落地的修改方案,完全贴合你提到的“加中间函数分离数据与显示逻辑”的思路:
核心思路梳理
我们把逻辑拆成三个独立部分:
- 数据更新层:把电压值分解成数码管每一位要显示的数字,存在全局变量里(相当于“显示缓存”);
- 显示刷新层:高频调用一个函数,逐个点亮数码管、显示缓存里的对应数字,实现多路复用;
- 定时器调度:用定时器定期读取电压更新缓存,同时高频触发显示刷新,维持数码管常亮。
修改后的完整代码及解释
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; }
关键逻辑解释
- 为什么要分离数据与显示?:把电压计算和硬件刷新分开,代码逻辑更清晰,也避免了每次刷新都要计算电压的冗余操作;
- 为什么要高频刷新?:人眼的暂留时间大概是10-20ms,我们用1ms一次的刷新频率,远高于感知阈值,所以看起来所有数码管都是常亮的;
- 为什么要先关闭所有数码管?:如果不关闭,上一个数码管的电流会串到下一个,导致重影,显示模糊。
替换成这套代码后,你应该就能看到数码管正确显示两位小数的电压值了,旋转电位器时数字会平滑变化,按键切换ADC通道也能正常切换显示。
备注:内容来源于stack exchange,提问作者SlMN099
相关产品推荐
相关产品推荐

