上学期末做项目,要用到下位机实现底层的硬件控制。然而STM32板子价格疯涨。看着本来十几块的小板子一下子涨到近40块,这个功能相似的树莓派pico板子倒被我看上了。虽然最后为了求稳,我还是用了STM32板子,不过借着这个机会我也成功搞来了几块pico玩。
网上关于pico的教程十分少且碎,我只能看官方文档慢慢学。目前笔记也不很全、甚至可以说有些乱,但说不定可以帮后来人尽快地上手。
20250107,师弟最近玩树莓派,我给他推了这帖,结果几年没管此帖喜提VIP自动锁。CSDN你干的好啊,蒸蒸日上,再也不见。
目录
上电刷机
挺简单的,按住BOOTSET键用microUSB连电脑,之后就可以用uf2程序以文件复制的方式刷机了。
MicroPython的uf2文件可以从这里下载:microPython环境
而对于C/C++而言,需要编译出uf2文件,然后如上烧进板子中。
管脚图
MicroPython控制
machine
模块
machine.Pin
GPIO类。通过类似下面的代码可以实现对GPIO的操作。
switch = Pin(0, Pin.IN, Pin.PULL_DOWN) # 设置下拉输入
trigger = Pin(1, Pin.IN, Pin.PULL_UP) # 设置上拉输入
led = Pin(25, Pin.OUT) # 板载LED为25号GP
state = 0
def change():
global state
state = 1-state
def setLED(pin): # 中断处理函数,接收一个参数为触发者的Pin对象
led.value(state)
# 无参数时,value()返回引脚的值
# 有参数时,value(i)设置引脚值为i
trigger.irq(setLED, Pin.IRQ_FALLING) # 设置下降沿触发中断
while True:
if switch.value(): # 如果为高电平
change()
while switch.value():
pass # 在降回低电平之前,等待
引脚设置相当方便~
初始化:
初始化需要至少指定GP引脚,GP对应的物理引脚见上图。同时要指定I/O,若未指定,则引脚默认置0。
p1 = Pin(0)
p2 = Pin(1, Pin.IN)
p3 = Pin(2, Pin.OUT)
指定为OUT时,引脚初始为低电平,可以使用high()
函数或on()
函数来置位、用low()
函数或off()
函数来复位、用value(v)
函数来直接指定值、用toggle()
函数来反转高低电平。此时,还可以用value()
函数来查询引脚的电平高低。具体可以看下面两节。
指定为IN时,引脚初始电平为初始化时所处电平,但可以指定上拉输入和下拉输入:
p4 = Pin(3, Pin.IN, Pin.PULL_UP)
p5 = Pin(4, Pin.IN, Pin.PULL_DOWN)
此时,high()
、low()
、on()
、off()
、toggle()
和value(v)
都可以顺利调用但不会生效。使用value()
函数可以正常查询引脚电平。
除了默认的初始化,还可以使用重初始化函数init()
,除了无需设置引脚,其他同上。
设置引脚电平:
仅能对OUT GP设置。高电平为3V3。
使用high()
、on()
、value(1)
来置位,即将引脚电平置高;使用low()
、off()
、value(0)
来复位,即将引脚电平置低;使用toggle()
来反转高低。
led = Pin(25, Pin.OUT)
led.high()
sleep(2)
led.low()
sleep(3)
while True:
led.toggle()
sleep(0.5)
读取引脚电平:
对各类GP,皆可读取。
使用value()
来读取,高电平返回1,低电平返回0。
led = Pin(25, Pin.OUT)
button = Pin(0, Pin.IN, Pin.PULL_DOWN)
while True:
led.value(button.value())
设置/查询中断:
对各类GP皆可设置中断。
使用irq(func[, flag])
函数来设置。第一个参数指定中断触发调用的函数,函数须有一个参数,用来接收触发中断的Pin对象。第二个参数可选,用来指定上升沿/下降沿触发,默认为IRQ_FALLING | IRQ_RISING
,即电平任意变化触发。
函数会返回GP的irq
对象。它的flags()
方法可以查询当前触发状态,未触发则为0,否则为IRQ_FALLING
或IRQ_RISING
之一。它的trigger()
方法返回中断种类,未设置中断则为0,否则为IRQ_FALLING
、IRQ_RISING
或IRQ_FALLING | IRQ_RISING
之一。
无参数调用irq()
则只会返回GP的irq
对象。
button = Pin(0, Pin.IN, Pin.PULL_UP)
def handler(pin):
print('I am pulled down.' if pin.irq().flags() == Pin.IRQ_FALLING else 'I am pulled up.')
button.irq(handler)
machine.PWM
直接上官方例程吧,因为挺简单的。
官方例程:
# Example using PWM to fade an LED.
import time
from machine import Pin, PWM
# Construct PWM object, with LED on Pin(25).
pwm = PWM(Pin(25))
# Set the PWM frequency.
pwm.freq(1000)
# Fade the LED in and out a few times.
duty = 0
direction = 1
for _ in range(8 * 256):
duty += direction
if duty > 255:
duty = 255
direction = -1
elif duty < 0:
duty = 0
direction = 1
pwm.duty_u16(duty * duty)
time.sleep(0.001)
初始化:
使用一个Pin
对象来初始化:
led = Pin(25, Pin.OUT)
pwm = PWM(led)
wave = PWM(Pin(0))
设置/查询频率:
使用freq(f)
来设置频率。频率至少为10Hz(8Hz也可以),最高为125MHz,范围外的设置会导致报错,某些对精度要求高的设置会被自动舍入到附近的分频允许值。
使用无参数的freq()
来查询频率,返回值为频率。
设置/查询占空比:
有两种模式:计数模式和计时模式。
duty_u16(d)
为计数模式设置,占空比为
d
65535
×
100
%
{d\over 65535}\times100 \%
65535d×100%
由于内部设置,某些设置值会被改为附近的、更贴合硬件的值。如果超出了65535的限制,则会得到奇怪的结果。
无参数调用duty_u16()
会得到上述的d值。
duty_ns(t)
为计时模式设置,用来设置每个周期高电平持续的时间(单位:纳秒)。若已设置频率为f,则占空比为
t
f
×
1
0
−
9
×
100
%
tf\times10^{-9} \times100\%
tf×10−9×100%
同样由于内部设置,实际值会与指定的值有点出入。
无参数调用duty_ns()
会得到上述的t值。
初始的频率为1907Hz(我试了几遍,好像的确是这个数),占空比为0。
停止:
使用deinit()
函数以暂停PWM波的生成,设置占空比即可恢复。暂停会维持暂停瞬间的电平高低。设置频率不会恢复PWM波。
machine.UART
官方例程:
from machine import UART, Pin
import time
uart1 = UART(1, baudrate=9600, tx=Pin(8), rx=Pin(9))
uart0 = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1))
txData = b'hello world\n\r'
uart1.write(txData)
time.sleep(0.1)
rxData = bytes()
while uart0.any() > 0:
rxData += uart0.read(1)
print(rxData.decode('utf-8'))
(讲真,一目了然。等之后整理时再细写吧)
machine.SPI
初始化:
spi = SPI(0
baudrate=1000000, # 波特率
polarity=0, # 时钟默认电平
phase=0, # 上升沿/下降沿读取
bits=8, # 位
firstbit=SPI.MSB, # 倒序/顺序
sck=Pin(2), # 指定时钟端口
mosi=Pin(3), # 指定输出端口
miso=Pin(4) # 指定输入端口
)
(用法类似UART)
C/C++控制
这里的操作均在Linux系统上执行。Windows也可以安装相关SDK,但基本全程手动配环境,注意各文件位置(仅附一个Windows下的环境配置教程链接吧,写的相当详细,我就不狗尾续貂了)。
这一大节是我看官方文档时记的笔记,由于文档有在更新,实际开发时还请以文档为准。
个人觉得,在初步入门上手后,看前人的笔记反而不如直接看文档来得高效。
SDK配置
想偷懒的话,直接从官方渠道拿到脚本,然后运行即可。
wget https://raw.githubusercontent.com/raspberrypi/pico-setup/master/pico_setup.sh
sudo bash ./pico_setup.sh
运行前可以读一读脚本代码确认一下它的操作。
也可以自己一步一步安装:
-
选择一处风水宝地。尽量是不包含其他内容的文件夹。
-
安装依赖包:
sudo apt update sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential
-
克隆SDK,也可以选择克隆样例程序:
git clone -b master https://github.com/raspberrypi/pico-sdk.git cd pico-sdk git submodule update --init cd .. git clone -b master https://github.com/raspberrypi/pico-examples.git
注意创建所需的文件夹。
-
在各自文件夹下创新文件夹并编译。
cmake ../ make -j4
-
另外,记得时不时检查一下更新:
cd pico-sdk git pull git submodule update
程序
stdio
需要包含这个头文件:
#include <stdio.h>
输入输出流可以指定为USB或普通串口,默认通过串口。在CMakeLists.txt
中设置
pico_enable_stdio_usb(mylowvel 1)
pico_enable_stdio_uart(mylowvel 0)
来使能USB的IO。使用USB时,可以利用tusb.h
内的函数帮助判断USB是否连接成功,从而避免上位机检测不到的问题:
#include <tusb.h>
// ...
while(!tud_cdc_connected());
程序中使用库函数前,需要先初始化:
stdio_init_all();
其他可用函数同标准C语言中的函数,比如printf
、getchar
之类的。
GPIO
需要包含这两个头文件:
#include "pico/stdlib.h"
#include "hardware/gpio.h"
初始化某个GPIO:
gpio_init(GP_id);
设置/查询某个GPIO的输入/输出方向:
gpio_set_dir(GP_id, GPIO_OUT); // 或 GPIO_IN
gpio_get_dir(GP_id);
设置/查询某个GPIO为上拉/下拉输入:
gpio_pull_up(GP_id);
gpio_is_pulled_up(GP_id); // 返回true/false
gpio_pull_down(GP_id);
gpio_is_pulled_down(GP_id); // 返回true/false
gpio_set_pulls(GP_id, true, false); // 第二个参数指定上拉,第三个参数指定下拉
设置/查询某个GPIO的电平:
gpio_put(GP_id, true); // 或false
gpio_get(GP_id); // 返回true/false
IRQ中断
需要包含这三个头文件:
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/irq.h"
设置中断处理:
gpio_set_irq_enabled_with_callback(GP_id,
GPIO_IRQ_EDGE_FALL, // 或者其他的中断事件,比如GPIO_IRQ_EDGE_RISE等
true, // true则立即启用
&handlingFunction);
中断处理函数类型:
void handlingFunction(uint gpio, uint32_t events);
PWM
需要包含这些头文件:
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/pwm.h"
#include "hardware/clocks.h" // 如果未用到系统时钟频率,可忽略
设置指定GPIO为PWM输出:
gpio_set_function(GP_id, GPIO_FUNC_PWM);
设置GPIO频率:
uint wrap;
uint slice_num = pwm_gpio_to_slice_num(GP_id);
pwm_set_wrap(slice_num, wrap); // 设置频率为 (125M/wrap)Hz
注意:
上面方法设置wrap,最大为65535,如果想设置更小的频率,需要手动设置PWM分频:
uint freq; // 要设置的频率 uint wrap; // 顶值 uint times; // 分频系数 uint slice_num = pwm_gpio_to_slice_num(GP_id); pwm_set_enabled(slice_num, true); // 使能。好像没这一行也行。 pwm_config config = pwm_get_default_config(); // 利用一个设置结构体来准备接下来的初始化 float div = (float)clock_get_hz(clk_sys) / (freq * times); pwm_config_set_clkdiv(&config, div); pwm_config_set_wrap(&config, wrap); pwm_init(slice_num, &config, true);
需要注意,wrap是相对于分频后的时钟而言的。程序中的表达式可以是其他形势,但最终应满足:
t i m e s × w r a p = c l k _ s y s = 125 M H z times\times wrap=clk\_sys=125\ {\rm MHz} times×wrap=clk_sys=125 MHz
设置占空比:
pwm_set_gpio_level(GP_id, level); // 设置占空比为 (level/wrap) × 100%
UART
需要包含这些头文件:
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/uart.h"
初始化:
uart_init(UART_id, 115200); // UART_id为uart0或uart1,标识符均已定义
gpio_set_function(GP_id, GPIO_FUNC_UART); // 将符合的TX注册为UART口,需要参照上图
gpio_set_function(GP_id, GPIO_FUNC_UART); // 将符合的RX注册为UART口,需要参照上图
uart_set_baudrate(UART_id, 115200); // 设置波特率,默认115200
uart_set_format(UART_id, 8, 1, 0); // 设置数据位、停止位、校验位,默认8/1/0
查询串口是否可读/可写:
uart_is_readable(UART_id); // 返回true/false
uart_is_writable(UART_id); // 返回true/false
读/写串口:
uart_getc(UART_id); // 返回读到的字符
uart_putc(UART_id, 'c');
uart_puts(UART_id, "string");
编译
在完成上面的SDK配置和程序编写后,就只需注意CMakeLists.txt
的配置了。
cmake_minimum_required(VERSION 3.12)
include(pico_sdk_import.cmake)
project(your_project_name C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
add_executable(your_project_name
c_or_cpp_file.c
)
target_link_libraries(your_project_name pico_stdlib)
pico_add_extra_outputs(your_project_name)
add_compile_options(-Wall
-Wno-format
-Wno-unused-function
-Wno-maybe-uninitialized
)
其中,pico_sdk_import.cmake
文件可以直接从<PICO_SDK_PATH>/external/pico_sdk_import.cmake
复制过去。
之后就是老一套的:
mkdir build
cd build
cmake ..
make -j4
注意:
编译时,所在目录的完整路径名请尽量只含半角英文。已知在路径名存在中文时,make有大概率会报错!