Простой проект для начинающих электронщиков, которые непрочь попрактиковаться в программировании микроконтроллеров серии PIC32MX на Rust. Здесь мы соберем макетную плату со светодиодом, напишем короткую программу, чтобы им помигать, и загрузим эту программу в микроконтроллер, попутно разобрав нюансы работы с контейнерами Rust и программатором.

Я уже давненько хотел вернуться к проектам с микроконтроллерами. В последний раз я связывался с кодом для микроконтроллеров несколько лет назад, когда собирал другу регулятор нагрева на Arduino Nano. Мой друг интересовался программированием. Он приходил ближе к ночи, и мы вместе работали над этим устройством. Тогда у нас возникли проблемы с подачей питания, которая некорректно работала при использовании любого старого блока питания. Как бы то ни было, в итоге мы постепенно утратили интерес к этой затее, так и не доведя ее до этапа тестирования. Может, дело было в том, что мы проводили отладку уже поздно ночью, к тому же не без участия виски, кто знает…

Однако недавно у меня возникло желание опробовать Rust в программировании встраиваемых устройств. В качестве микроконтроллера для задуманного проекта я решил взять модель из семейства PIC32MX. Он имеет достаточное число контактов ввода-вывода, встроенную поддержку USB и может куда больше, чем просто помигать светодиодом.

Дисклеймер: эта статья предназначена для новичков вроде меня, которые желают попрактиковаться с программированием PIC32 при помощи Rust. Это моя первая попытка программирования микроконтроллера не на плате Arduino. У меня довольно большие пробелы в знаниях по этой теме, так что я всегда открыт к рекомендациям и обсуждению. Можете писать мне в темах на Reddit, Twitter или на электронную почту (страница контактов автора).

Можно было найти готовую макетную плату PIC32, но я решил взять голую микросхему. Это решение не столь удобно, зато предоставляет ��ольше возможностей для обучения. Для проекта нам понадобится ряд компонентов и, естественно, микроконтроллер PIC32 (хотя бы один).

Ниже перечислен необходимый минимум, который потребуется для написания “Hello World” (помигать светодиодом).

  • макетная плата;
  • проволочные перемычки;
  • светодиод;
  • Программатор PICkitT3;
  • PIC32 (в моем случае MX270F256B);
  • набор керамических дисковых конденсаторов;
  • набор резисторов;
  • кнопки и прочее на случай, если вы захотите расширить программу.

Я рекомендую приобрести недорогой стартовый комплект радиолюбителя, в котором есть множество компонентов вроде резисторов, конденсаторов, светодиодов и т.д.

Как сообщает Википедия (англ.):

Микроконтроллер – это небольшой компьютер на базе одной интегральной МОП-микросхемы. Микроконтроллер включает в себя одно или более процессорных ядер, а также память и программируемые периферийные устройства ввода/вывода. Микросхема также зачастую снабжается программной памятью, представленной в виде сегнетоэлектрической RAM, NOR-флэш или OTP ROM, а также небольшим объемом RAM. Микроконтроллеры предназначены для встраиваемых приложений, в отличие от микропроцессоров, используемых в персональных компьютерах или других устройствах, состоящих из различных гибридных микросхем.

Итак, путешествие в кроличью нору началось. Я нашел pic32-rs, контейнер Rust для работы с PIC. После получения своих микросхем мне не терпелось поскорее приступить к делу, но я понятия не имел, с чего начать. Duckduckgo не особо помог мне разобраться. По не ясной причине большинство руководств по PIC оказались устаревшими. Похоже, что интерес к этой серии микроконтроллеров упал с тех пор, как сцену заполонили Arduino и сотни других плат. В итоге же выяснилось, что можно получить немало информации из одной только спецификации для семейства PIC32MX. Помимо этого, в доступе есть руководство для программатора PICkitT3.

Подключение комплектующих


Микроконтроллер, а также кнопку сброса и светодиод я подключил к монтажной плате по приведенной ниже схеме.


Рекомендованный минимум подключений PIC32MX


Таблица распиновки PIC32MX270F256B


Распиновка штекера PICkit3

Я запитал микроконтроллер и реализовал необходимый минимум подключений, но как мне его прошить. Компания Microchip предоставляет версию своих инструментов MPLAB IDE и MPLAB IPE для Linux. В результате же мне не удалось с их помощью даже проверить правильность подключения всей сборки. Тогда было решено обратиться к опенсорс решениям.

Небольшие заминки


Я скопировал pic32-rs локально и после выполнения предложенных инструкций смог успешно скомпилировать пример blinky. Однако при попытках загрузить hex-файл через MPLAB IPE я продолжал получать следующую ошибку:

*****************************************************

Connecting to MPLAB PICkit 3...

Currently loaded firmware on PICkit 3
Firmware Suite Version.....01.56.09
Firmware type..............PIC32MX
Target voltage detected
Target device PIC32MX270F256B found.
Device ID Revision = A2
Loading code from /home/.../Projects/pic32-rs/examples/blinky/blinky.hex...
Warning: /home/.../Projects/pic32-rs/examples/blinky/blinky.hex contains code that is located at addresses that do not exist on the PIC32MX270F256B.
Code incompletely loaded starting at 0x9D000000 (0x3D4).
2021-08-20 11:17:58 +0100 - Hex file loaded successfully.
2021-08-20 11:18:06 +0100 - Programming...


Device Erased...

Programming...

The following memory area(s) will be programmed:
MPLAB's memory is blank so no programming operation was attempted.
2021-08-20 11:18:10 +0100 - Programming complete

Тогда я решил спросить об этом Kiffie, создателя контейнера, который пояснил:

«Проблема в том, что objcopy создает hex-файлы не с физическими, а с виртуальными адресами. Похоже, программатор от Microchip способен обрабатывать hex-файлы только с физическими адресами. У меня такой проблемы не возникало, так как я использую другой инструмент: pic32prog».

Следуя его рекомендации, я смог преобразовать создаваемый компилятором бинарник с помощью xc32-bin2hex, но загрузить полученный hex-файл мне все равно не удавалось. Оказалось же, что для успешного программирования PIC32 необходимо установить керамический конденсатор 10мкФ на выводе 20 (VCAP). Наконец-то светодиод замигал.

Прошивка PIСkit3 под использование с помощью pic32prog


Теперь меня не устраивала необходимость задействовать массивное Java-приложение лишь для загрузки hex-файла после каждого его редактирования. Тогда я начал разбираться с pic32prog в надежде использовать эту утилиту. Я скопировал репозиторий и скомпилировал инструмент, но он отказался распознавать PIСkitT3, так как для него требовалась другая прошивка.

Заглянув в документацию pic32prog, я нашел раздел Flashing on Linux using pickit-3. Для прошивки программатора нужно либо иметь доступ к машине с Windows, либо запустить VM с Windows. Не стоит беспокоиться, если в процессе прошивки система подвиснет и откажется его распознавать. У меня случилось то же самое, но в итоге программатор работает исправно.

Лишний раз повторю, что при использовании этой конфигурации вам не потребуется MATLAB IDE. Просто скомпилируйте код с помощью Cargo (в проекте есть удобный скрипт build.sh) и запишите его командой pic32prog -p blinky.hex. Опция -p обеспечит подачу питания устройству от PIСkit3, так что дополнительного источника 3.3В вам не потребуется.

Что это за hex-числа?


Как видите, blinky/src/main.rs – это небольшая программа, но ввиду своей неопытности я не до конца понимаю, что здесь происходит. Первым мое внимание привлекло следующее:

// Регистры конфигурации PIC32 для PIC32MX1xx и PIC32MX2xx
#[cfg(any(
    feature = "pic32mx1xxfxxxb",
    feature = "pic32mx2xxfxxxb"
))]
#[link_section = ".configsfrs"]
#[no_mangle]
pub static CONFIGSFRS: [u32; 4] = [
    0x0fffffff, // DEVCFG3
    0xfff9ffd9, // DEVCFG2
    0xff7fcfd9, // DEVCFG1
    0x7ffffffb, // DEVCFG0
];

Похоже, что это основные биты настройки регистров конфигурации в семействах микросхем pic32mx1xxfxxxb и pic32mx2xxgxxxb. Чтобы хоть как-то разобраться, я преобразовал эти значения в двоичный формат:

0x0fffffff, // DEVCFG3 00001111 11111111 11111111 11111111
0xfff9ffd9, // DEVCFG2 11111111 11111001 11111111 11011001
0xff7fcfd9, // DEVCFG1 11111111 01111111 11001111 11011001
0x7ffffffb, // DEVCFG0 01111111 11111111 11111111 11111011

В документации без проблем можно обнаружить, что DEVCFG0 имеет возможные значения для всех 32 бит, определенных выше. Например, бит 31 зарезервирован для записи в качестве 0, биты 30-29 должны быть 1, бит 28 указывает на то, включена ли защита кода (1 значит отключена, наш случай). В общем, смысл понятен. Остальную конфигурацию с помощью документации настроить несложно.

Далее мы можем использовать одну из возможностей pic32-hal, а именно pac::Peripherals::take(), для получения опциональной структуры, которая даст нам доступ к периферии, включая отдельные выводы PIC.

В программе blinky мы используем системный тактовый генератор (на PIC32MX270F256B) с частотой 40МГц, с помощью которого управляем встроенным таймером, вводя задержку мигания светодиода. Этот тактовый генератор настраивается с помощью слов конфигурации (через активацию внутреннего RC-генератора 8МГц и PLL).

// настройка объекта для управления тактовым генератором
let sysclock = 40_000_000_u32.hz();
let clock = Osc::new(p.OSC, sysclock);
let mut timer = Delay::new(sysclock);

Когда я связался с создателем контейнера pic32-rs, то он любезно ответил на несколько моих вопросов о правильной настройке и первом запуске. Он также добавил в пример blinky протоколирование UART, так как это может пригодиться при отладке, если вы соберетесь расширить пример и захотите отправлять вывод/значения с микроконтроллера на ПК.

К удобству, у меня под рукой нашелся usb-модуль, который я подключил к своей сборке, соединив его Rx вывод с выводом RB0 AKA PIN 4 у PIC.

Взаимодействие с внешними устройствами реализуется с помощью встроенного в PIC универсального асинхронного приемопередатчика (UART). Важно учитывать, что здесь необходимо использовать одинаковую величину buad (в нашем случае 115200) и для minicom, и при инициализации Uart2. В противном случае в терминал может выводиться неразбериха.

Наблюдательный читатель мог заметить, что в проекте есть пара файлов, а именно 32MX270F256B_procdefs.ld и pic32_common.ld. Для меня они оказались незнакомы, так как до этого из микроконтроллеров я работал только с Arduino. Тогда я снова решил спросить Kiffie, который ответил следующее:

Это фрагмент скрипта компоновщика, который в основном определяет структуру памяти микросхемы. Сюда входят devices.x (часть Peripheral Access Crate, PAC) и pic32_common.ld (основная часть скрипта). В Arduino подробности компоновки могут не быть столь очевидны для пользователя.

Наконец, вот этот простой пример с миганием светодиода (немного отличный от того, что лежит в репозитории pic32-rs).

//! Blinky
//!
//! Классический пример мигания светодиода для PIC32MX1xx или PIC32MX2xx в 28-контактном корпусе
//! Тактовые сигналы генерируются внутренним RC-генератором.  
//! Поэтому никаких внешних компонентов, кроме конденсатора 10мкФ в Vcap (вывод 20) и светодиода, подключенного через резистор (например, 470Ом) к RB7 (вывод 16),//! не требуется.
//! RB0 (вывод 4) используется для вывода текста через UART2.

#![no_main]
#![no_std]

use core::{fmt::Write, panic::PanicInfo};

use embedded_hal::{blocking::delay::DelayMs, digital::v2::*};
use mips_rt::{self, entry};
use pic32_hal::{
    clock::Osc, coretimer::Delay, gpio::GpioExt, pac, time::U32Ext, uart::Uart,
};

// Регистры конфигурации PIC32 для PIC32MX1xx и PIC32MX2xx
#[cfg(any(feature = "pic32mx1xxfxxxb", feature = "pic32mx2xxfxxxb"))]
#[link_section = ".configsfrs"]
#[no_mangle]
pub static CONFIGSFRS: [u32; 4] = [
    0x0fffffff, // DEVCFG3 00001111 11111111 11111111 11111111
    0xfff9ffd9, // DEVCFG2 11111111 11111001 11111111 11011001
    0xff7fcfd9, // DEVCFG1 11111111 01111111 11001111 11011001
    0x7ffffffb, // DEVCFG0 01111111 11111111 11111111 11111011
];

#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();

    let pps = p.PPS;
    pps.rpb0r.write(|w| unsafe { w.rpb0r().bits(0b0010) }); // U2TX on RPB0

    // объект для управления тактовым генератором
    let sysclock = 40_000_000_u32.hz();
    let clock = Osc::new(p.OSC, sysclock);
    let mut timer = Delay::new(sysclock);

    let uart = Uart::uart2(p.UART2, &clock, 115200);
    timer.delay_ms(10u32);
    let (mut tx, _) = uart.split();
    writeln!(tx, "Blinky example\n").unwrap();

    let parts = p.PORTB.split();
    let mut led = parts.rb7.into_push_pull_output();
    let mut on = true;

    loop {
        writeln!(tx, "LED status: {}", on).unwrap();
        if on {
            led.set_high().unwrap();
        } else {
            led.set_low().unwrap();
        }
        on = !on;
        timer.delay_ms(500u32);
    }
}

#[panic_handler]
fn panic(_panic_info: &PanicInfo<'_>) -> ! {
    loop {}
}

Если вы захотите задействовать функционал Cargo, то понадобиться подредактировать файл /.cargo/config и runner = "./flash.sh", после чего команда cargo run будет собирать программу и записывать ее на устройство.

В дальнейшем я планирую заняться примером программы для USB, ну а пока…

Успехов вам в написании кода!