量子隧道 发表于 2025-2-21 21:00:16

[树莓派Pico][PIO][PWM]学习笔记:囫囵吞枣学习PIO

今天又有时间了,从RP2040手册3.6.8章节照葫芦画瓢扒来一个PWM控制器。对它做了一些改装以适配Arduino环境。对汇编代码也改了一些,不用样例中的“写立即执行代码进状态机,装载PWM周期”的方法。而是直接从TX fifo推入周期,让状态机最开始两行代码读到周期后永久保存在ISR里。
代码如下:
#include <Arduino.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/gpio.h"

// PIO 程序机器码
const uint16_t pio_program_instructions[] = {
    0x80a0, //0: pull   block                     
    0x60c0, //1: out    isr, 32                  
    0x9080, //2: pull   noblock         side 0   
    0xa027, //3: mov    x, osr                     
    0xa046, //4: mov    y, isr                     
    0x00a7, //5: jmp    x != y, 7                  
    0x1808, //6: jmp    8               side 1   
    0xa042, //7: nop                              
    0x0085, //8: jmp    y--, 5                     
    0x0002, //9: jmp    2                        
};

// 定义 PIO 程序结构体
const struct pio_program pio_program = {
    .instructions = pio_program_instructions,
    .length = 10, // 指令数量
    .origin = 0, // 分配起始地址
};

PIO pio = pio0; // 使用 PIO 0
uint sm;
uint pin = 2;
uint period = 1000;    //周期
uint level = 0;      //脉宽

void setup() {
//启用串口
Serial.begin(115200);delay(2000);
Serial.println("I AM BEGINNING!");
// 初始化 GPIO 2 为 PIO 功能
gpio_init(pin);
pio_gpio_init(pio, pin);
//gpio_set_function(pin, GPIO_FUNC_PIO0);

// 初始化 PIO
uint offset = pio_add_program(pio, &pio_program);                // 加载 PIO 程序
Serial.printf("Loaded program at %d\n", offset);
sm = pio_claim_unused_sm(pio, true);                           // 分配一个状态机
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
pio_sm_config c = pio_get_default_sm_config();                   //获取一个默认状态机配置
sm_config_set_wrap(&c, offset, offset + pio_program.length - 1); //设置PIO代码循环范围
sm_config_set_sideset(&c, 2, true, false);                     //偷窃delay域2位做Sideset
sm_config_set_sideset_pins(&c, pin);
sm_config_set_clkdiv(&c, 1); // 设置时钟分频(200 MHz / 1 = 200 MHz)
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true); // 启用状态机
//推入周期设置
pio_sm_put_blocking(pio, sm, period);
Serial.printf("Put period = %d\n", period);

//不断更改脉宽
while (true) {
    Serial.printf("Level = %d\n", level);
    pio_sm_put_blocking(pio, sm, level);
    level = (level + 20) % 500;
    delay(100);
}

//pio_sm_put_blocking(pio, sm, 200);//for debugging

}

void loop() {
}



sm_config_set_sideset(&c, 2, true, false);
这一行折腾了很久。原因是(手册给出的)样例程序中并不包含这一句。但是因为不明原因,在arduino环境下是必须设置这一行的。或许在github源头里是有的。另外这个函数的参数2,true,false也是需要倒腾一阵才搞定。这些参数是和汇编代码中的“.side_set 1 opt”有呼应关系的,刚开始没注意到。

还有一些不明白的地方,但是毕竟囫囵吞枣地让它转起来了。这就先凑合着吧。
例如,
pio_program.origin中的origin,
uint offset = pio_add_program(pio, &pio_program)中的offset,
汇编中的jmp指令,
这三者之间的关系我就不是很清楚。问了deepseek很多次,绕着圈地问,都没得到很准确的回答。
origin是指定汇编程序加载位置的,设为-1,就会由系统自动指定加载位置。我的程序中,会加载到offset=22那个地址。考虑到机器码是10条指令,那么看来系统倾向于把指令加载到32字指令空间的末尾。
但是,jmp指令可是在绝对地址里跳的。这么乱加载程序,应该会导致程序跳错地址。
然而实践中并不会跳错。
因为没搞懂,所以我指定了origin=0。加载后也会返回offset=0。

最后给出机器码结构体的汇编源文件及其注释:
.program pwm
.side_set 1 opt
pull block            ;Pull the period from FIFO to OSR then to ISR
out isr,32
period:
pull noblock    side 0 ; Pull from FIFO to OSR if available, else copy X to OSR.
mov x, osr             ; Copy most-recently-pulled value back to scratch X
mov y, isr             ; ISR contains PWM period. Y used as counter.
countloop:
jmp x!=y noset         ; Set pin high if X == Y, keep the two paths length matched
jmp skip      side 1
noset:
nop                  ; Single dummy cycle to keep the two paths the same length
skip:
jmp y-- countloop      ; Loop until Y hits 0, then pull a fresh PWM value from FIFO
jmp period

scoopydoo 发表于 2025-2-21 22:01:09

本帖最后由 scoopydoo 于 2025-2-21 22:35 编辑

老兄专研得很深啊!

JMP 指令中的地址确实是绝对地址,但是当你调用 pio_add_program() 这个函数的时候,如果 offset 非负的话会调用一个 add_program_at_offset() 函数,并在其中对 JMP 指令做重定位。

// these assert if unable
int pio_add_program(PIO pio, const pio_program_t *program) {
    uint32_t save = hw_claim_lock();
    int offset = find_offset_for_program(pio, program);
    if (offset >= 0) {
      offset = add_program_at_offset(pio, program, (uint) offset);
    }
    hw_claim_unlock(save);
    return offset;
}


static int add_program_at_offset(PIO pio, const pio_program_t *program, uint offset) {
    int rc = add_program_at_offset_check(pio, program, offset);
    if (rc != 0) return rc;
    for (uint i = 0; i < program->length; ++i) {
      uint16_t instr = program->instructions;
      pio->instr_mem = pio_instr_bits_jmp != _pio_major_instr_bits(instr) ? instr : instr + offset;
    }
    uint32_t program_mask = (1u << program->length) - 1;
    _used_instruction_space |= program_mask << offset;
    return (int)offset;
}

量子隧道 发表于 2025-2-21 22:26:10

scoopydoo 发表于 2025-2-21 22:01
老兄专研的很多深啊!

JMP 指令中的地址确实是绝对地址,但是当你调用 pio_add_program() 这个函数的时 ...

老兄研究的才是真的深:handshake :lol
看来加载时把我的程序目标码改了。

scoopydoo 发表于 2025-2-21 22:34:42

量子隧道 发表于 2025-2-21 22:26
老兄研究的才是真的深
看来加载时把我的程序目标码改了。

俺没研究过,是看了你的帖子才去看代码的 ... ;P

量子隧道 发表于 2025-2-21 23:34:41

scoopydoo 发表于 2025-2-21 22:34
俺没研究过,是看了你的帖子才去看代码的 ...

那也说明您老体系清晰定位精准啊。
我猜这种改跳转代码或基地址加偏移地址的做法应该是操作系统或编译系统的常见做法。毕竟,很多场合是要在内存里动态加载目标码的。而跳转代码在编写的时候并不知道本程序的绝对位置,只知道在本程序中的跳转位置。

scoopydoo 发表于 2025-2-21 23:48:40

量子隧道 发表于 2025-2-21 23:34
那也说明您老体系清晰定位精准啊。
我猜这种改跳转代码或基地址加偏移地址的做法应该是操作系统或编译系 ...

确实是常规做法,好像叫做代码重定位 code relocation :lol
页: [1]
查看完整版本: [树莓派Pico][PIO][PWM]学习笔记:囫囵吞枣学习PIO