gd32e2-hal
English
A hardware abstraction layer for the GD32E230K8U6 (Cortex-M23), written in
Rust from scratch on top of the gd32e2 PAC.
⚠️ Work in progress. Written by hand, incrementally; the API is unstable. The package is a library (
src/lib.rs→adc,dma,gpio,rcu,spi,time,timer,usart) plus on-hardware test binaries inexamples/; all 13 examples have been flashed and verified on the board — RCU, GPIO, USART (8/9-bit and parity), SPI0/SPI1, ADC, a one-shot DMA transfer, TIMER, blocking delays, PWM, RTT.
Principles
- Errors at compile time, not on the board. Pin identity and mode live in the
type:
Pin<'A', 5, Input>has noset_high, an invalid AF number doesn't compile, and ownership prevents reconfiguring a pin twice or using a port before its clock is on. - Zero-cost. The same register writes as hand-written PAC code;
Pinis a ZST. #![no_std], no heap.
Chip variants
The same pin and AF number reach a different peripheral depending on the variant
(PA2 AF1 is USART0_TX on x4, USART1_TX on x8). The AF map is gated on three
mutually exclusive features; exactly one must be enabled, or it's a
compile_error! rather than a silently truncated pin map.
| feature | datasheet note | applies to |
|---|---|---|
gd32e230x4 |
(1) | x4 |
gd32e230x6 |
(2) | x6 |
gd32e230x8 (default) |
(2) + (3) | x8 — the targeted GD32E230K8U6 |
x8 is a superset of x6; where a row is footnoted only (1) and (3), x6 has no function on that AF at all.
An orthogonal feature, defmt, derives defmt::Format on the public enums
and error types. Off by default; also enables embedded-hal/defmt-03.
What's implemented
GPIO (src/gpio.rs) — const-generic Pin<P, N, MODE> (a ZST: port and pin
number live in the type). Modes as typestate: Input, Analog,
Output<PushPull> / Output<OpenDrain>, Alternate<AF>, Debugger
(PA13/PA14, escaped only via unsafe activate()), and Locked<MODE>
(terminal — the hardware stays locked until reset, so there is no unlock).
dp.gpioa.split(&mut rcu) (GpioExt) enables the port clock and hands out pins.
Transitions: into_input / into_output / into_push_pull_output /
into_open_drain_output / into_analog / into_alternate::<AF>() — the AF
number is checked at compile time against a per-pin ValidAf map. Plus
set_pull, set_speed, lock() (ports A/B only — port F has no LOCK
register). Pin state is inherent and Result-free: set_high / set_low /
toggle (atomic TG) / is_set_high / is_set_low on outputs, is_high /
is_low on inputs and on open-drain outputs. The same pins implement
embedded-hal 1.0 OutputPin / InputPin / StatefulOutputPin, which return
Result<_, Infallible>; both layers call the same private register helpers.
Ports A, B and F; port C isn't bonded on this package.
RCU (src/rcu.rs) — dp.rcu.constrain() (RcuExt). Per-peripheral Enable
and Reset traits, generated by the bus_en!/bus_rst! macros (split apart
since not every peripheral has a reset bit — DMA has none) and wired into
split(), so a port can't be used unclocked. Clock tree: a CFGR builder →
.freeze(&mut rcu, &mut dp.fmc) → a frozen Clocks. PLL from IRC8M
(PllFreq, 8–72 MHz) and bus prescalers (AhbPsc / ApbPsc) are typed enums,
so an unreachable frequency is a compile error instead of silent rounding; flash
wait states are set from the resulting hclk before the source switch.
Usart0Sel and AdcSel / AdcPsc resolve into Clocks for the USART and ADC
modules. rcu.ck_out(src, div) routes an internal clock node onto PA8/PA9
(CkOutSrc / CkOutDiv), so a real frequency can be measured on a pin rather
than trusted. pclk1_tim / pclk2_tim carry the timer branch: hclk at an
undivided APB, twice the bus clock otherwise. Typed frequencies live in
src/time.rs, aliases over fugit (Hertz), which is re-exported. HXTAL is
out of scope — no crystal on this board.
USART (src/usart.rs) — Usart<USARTX, TX, RX, WORD = Byte> owns the
peripheral and both pins. TxPin / RxPin markers (filled by usart_pins!)
reject a wrong pin or AF at compile time; BusClocks picks the right bus
frequency per instance, so the baud calculation can't use the wrong one.
UsartConfig (fluent, Default = 115200 / ×16 / N8) carries baud (named
constants in usart::baud), Oversampling::{X16, X8} and
FrameFormat::{N8, E8, O8, E7, O7} (one source of truth for WL/PCEN/PM).
Blocking inherent API on the Byte width: write_byte / write_bytes /
read_byte / read_bytes, plus flush (waits for TC, not TBE).
Transmission has no error conditions, so the sending half returns no Result.
read_bytes blocks for the first byte, then takes only what is already waiting.
read_ready / write_ready poll RBNE / TBE without blocking. Trait layers
on the same width: embedded-hal-nb Read<u8> / Write<u8> and embedded-io
Read / Write / ReadReady / WriteReady, the latter crate supplying
read_exact / write_all / write_fmt as trait defaults. flush,
read_ready and write_ready exist in both layers; the inherent ones win
method-call syntax, the trait ones are reached by path. Pure 9-bit words via
new_word / write_word / write_words / read_word / read_words on the
Word typestate (UsartConfig9). RX errors are usart::Error (Overrun /
Noise / Framing / Parity), cleared through USART_INTC; portable
ErrorKinds come from the serial::Error and embedded_io::Error impls.
release() returns the peripheral and both pins.
ADC (src/adc.rs) — Adc::new(rcu, adc, clocks), or dp.adc.constrain(...)
through AdcExt, runs the manual's calibration sequence (ADCON, a
14-CK_ADC-cycle delay converted to core cycles, RSTCLB/CLB).
read<PIN: Channel>(&pin, SampTime) -> u16 performs a single blocking,
software-triggered conversion. Channel is implemented only
for Pin<P, N, Analog>, so a pin must actually have gone through
into_analog(). Internal channels: read_vref() -> i32 returns the real VDDA
in mV (derived from the factory VREFINT_CAL in flash, falling back to the
typical ~1.2 V VREFINT if that calibration is blank), and
read_temperature() -> Option<i32> returns tenths of °C — None when CK_ADC
is too fast to satisfy the sensor's minimum sampling time. Scan mode needs DMA
and is deferred.
SPI (src/spi.rs) — SPI0 and SPI1: master, full-duplex, blocking, 8- or
16-bit, software NSS. Spi::new(...) / Spi::new_word(...) take a SpiConfig
(SpiConfig::new(psc) — psc is mandatory and there is no Default, since an
SCK divider has no universal value; .mode() and .bit_order() override the
Mode 0 / MSB-first defaults). Word width is a typestate: transfer_word and
SpiBus<u16> don't exist on Spi<..., Byte>, transfer_byte and SpiBus<u8>
don't exist on Spi<..., Word>. BitOrder is a plain runtime value — it changes
wire serialization, not any method signature. An Instance trait abstracts the
peripheral at the operation level, so one generic Spi<> serves both instances
despite their distinct RegisterBlock types and bit-level divergence (FF16 in
CTL0 on SPI0 vs DZ in CTL1 on SPI1, with BYTEN derived from the width).
Buffer operations are inherent — transfer_bytes / transfer_bytes_in_place /
read_bytes / write_bytes and the _words variants; the SpiBus impls
delegate to them. Errors are spi::Error (Overrun / ModeFault / Crc /
Framing); ErrorKind comes from the spi::Error trait impl, where Crc maps
to Other for lack of an upstream variant. release() returns the peripheral
and pins. Hardware NSS and CRC are deliberately not implemented.
DMA (src/dma.rs) — one-shot transfers, verified on hardware. dp.dma.split(&mut rcu)
(DmaExt) hands out Channel<0>…Channel<4>, each a unique ZST token, so a
channel can't drive two transfers at once. write_to / read_from take the
channel, the peripheral and the buffer by value and return a Transfer;
wait() is the only way to get all three back, which is what makes it impossible
to read a buffer the hardware is still filling. Buffers are &'static [W] /
&'static mut [W]. DmaSrc<N> / DmaDst<N> encode the datasheet's request map
(Table 8-3), so a peripheral paired with the wrong channel doesn't compile rather
than silently never being requested, and the associated Word derives
PWIDTH/MWIDTH from the peripheral's own typestate. The shared DmaPeriph<N>
supertrait also gates the request line (DENT/DENR, DMATEN/DMAREN, ADC
DMA) — raised on start, dropped before the peripheral goes back to its owner —
so the drivers themselves know nothing about DMA. remaining() and is_error()
inspect a running transfer. Circular mode and M2M are deferred.
TIMER (src/timer.rs) — the counter core, verified on hardware. All seven
timers: Timer::new(rcu, timer, clocks) clocks and resets the peripheral and
records its own CK_TIMERx through the Instance trait, which binds each timer
to its bus, so an APB2 timer can't be given the APB1 frequency. start(psc, car)
consumes the stopped Timer and returns a running CountDownTimer, so wait()
doesn't exist on a timer that was never started; stop() takes the peripheral
back the other way. start writes both dividers, loads them out of their shadow
registers with UPG and consumes the update event that raises, so the first
wait() measures a full interval. wait() blocks for one rollover and leaves
the timer running; cnt() reads the counter in ticks, one tick being psc + 1
cycles of the timer's clock, and car() / psc() read the dividers back from
the hardware rather than from a remembered copy. Register access is confined to Instance, so no Deref to a
register block is needed. start_interval(5.secs()) takes a fugit duration in
whatever scale the caller wrote it in — the scale is a const generic on the
method, so millis and micros need no conversion at the call site — and
derives the dividers against this timer's own clock, in u64 and saturating.
start(psc, car) remains for the raw pair. Timer::new(rcu, timer, clocks) and
dp.timer5.constrain(...) through TimerExt are the two entry points, the trait
being one blanket impl over Instance. into_delay() moves the timer to a third
type, Delay, which promises no interval of its own: delay(interval) takes any
fugit scale, sets up the dividers, blocks and stops the counter again, so a
period set elsewhere cannot be overwritten by a delay. embedded-hal's DelayNs
is implemented on it, with delay_us / delay_ms as the trait's own defaults;
resolution is one timer tick. elapsed() converts the counter into a duration of
any fugit scale, in u64 and saturating.
into_pwm(psc, car) / into_pwm_interval(interval) move the timer to a fourth
type, Pwm, which owns the period and hands out channels. channel(pin) takes a
pin and returns a PwmChannel: the channel number comes from the pin through
PwmPin<TIMERX, C>, implemented only for the pin and alternate function the
silicon routes there, and the channel operations live in PwmOps<C>, implemented
only for channels a given timer has — TIMER5 has none, TIMER13 has one. The
pin moves into the channel and comes back from release(). A channel carries its
own handle to the peripheral, taken through the unsafe Instance::steal(), and
touches only its own compare register; several pins reaching one channel yield
several channels writing it. enable() / disable(), set_duty(cv) and
max_duty() are the inherent layer, embedded-hal's SetDutyCycle sits on top.
set_period / set_period_interval change the frequency for every channel at
once; duties keep their tick value, not their share. enable_output() exists only
on the timers with a CCHP register (TIMER0, TIMER14, TIMER15, TIMER16),
whose outputs stay silent until POEN is raised. Complementary outputs, break
inputs, dead time, input capture and interrupts are not implemented.
Usage
use GpioExt;
use pac;
use ;
use ;
let mut dp = take.unwrap;
let mut rcu = dp.rcu.constrain;
let clocks = CFGRdefault
.sysclk // PLL from IRC8M -> 48 MHz
.freeze;
let parts = dp.gpioa.split; // enables the GPIOA clock
let led = parts.pa5.into_output;
led.set_high;
let tx = parts.pa9.; // USART0_TX; ::<3>() wouldn't compile
let rx = parts.pa10.;
let usart0 = new;
if let Ok = usart0.read_byte
Constraints
- PAC-only base, no third-party HAL.
- Flashing and debugging over SWD (ST-Link V2 +
probe-rs,PA13/PA14). Runtime output goes over RTT on the same probe (defmt+defmt-rtt); no USB-serial adapter, andPA9/PA10are used only by the USART examples. - Target
thumbv8m.base-none-eabi; flash 64K, RAM 8K. gd32e2is generated from patched SVDs — verify field names againstdocs/GD32E23x_User_Manual.pdf(PDFs are kept locally, not committed).
Building
The examples link against a memory.x that is git-ignored, since its lengths
depend on the part. Copy the template once, before the first build:
To flash, with an ST-Link on PA13/PA14:
re is cargo run --release --example; .cargo/config.toml points the
target's runner at probe-rs run --chip GD32E230K8, which flashes the ELF
directly (no objcopy, no .bin) and then stays attached, printing the RTT log
until Ctrl-C. The log level is fixed at compile time by DEFMT_LOG in
.cargo/config.toml. The chip keeps running without the probe, so the freed
probe can read any register live:
The default feature targets the GD32E230K8U6 (x8); for another variant:
rust-toolchain.tomlpins the channel and installs the target. On Windows without Visual Studio the host toolchain has to be GNU, otherwise build scripts fail for want of the MSVC linker:rustup default stable-x86_64-pc-windows-gnu.
Roadmap
- DMA: circular mode and
M2M. - Timers: input capture; complementary outputs, break and dead time.
- I²C.
- Interrupt-driven operation (NVIC infrastructure — also affects USART/SPI).
- SPI: half-duplex / single-wire modes (
BDEN/BDOEN/RO). - SPI: hardware NSS, CRC, TI mode, slave — low priority.
- USART: hardware flow control (
CTS/RTS). - RCU:
HXTAL(needs an external crystal on the board). - GPIO: port F alternate functions (no
AFSELregister — needs its own study). - Package / pin-count variants — a second axis, independent of x4/x6/x8:
which pins are bonded out at all (
PC13–PC15,PF6/PF7exist on QFN48 but not on this QFN32). - Trait crates follow their peripherals:
i2c::I2cwith I²C,embedded-dmafor the DMA buffers. - Extract the HAL into its own standalone crate/repo (not just
examples/— splitting the library out entirely).
License
Dual-licensed, at your option:
- Apache License 2.0 (LICENSE-APACHE)
- MIT (LICENSE-MIT)
Any contribution submitted for inclusion in this work is licensed the same way, without additional terms, unless stated otherwise (Apache-2.0, section 5).
Русский
HAL для микроконтроллера GD32E230K8U6 (Cortex-M23), написанный на Rust с
нуля поверх PAC-крейта gd32e2.
⚠️ Работа в процессе. Пишется вручную и постепенно; API нестабилен. Пакет — библиотека (
src/lib.rs→adc,dma,gpio,rcu,spi,time,timer,usart) плюс тестовые бинарники на железо вexamples/; все 13 примеров прошиты и проверены на плате — RCU, GPIO, USART (8/9-бит и чётность), SPI0/SPI1, ADC, разовая передача по DMA, TIMER, блокирующие задержки, PWM, RTT.
Принципы
- Ошибки на компиляции, а не на плате. Идентичность и режим ноги живут в
типе: у
Pin<'A', 5, Input>нетset_high, неверный номер AF не компилируется, а система владения не даст перенастроить ногу дважды или использовать порт до включения такта. - Zero-cost. Те же записи в регистры, что и ручной PAC-код;
Pin— ZST. #![no_std], без кучи.
Варианты чипа
Одна и та же нога на одном и том же номере AF ведёт к разной периферии в
зависимости от варианта (PA2 AF1 — это USART0_TX у x4 и USART1_TX у x8).
Карта AF гейтится тремя взаимоисключающими фичами; ровно одна обязана быть
включена, иначе compile_error!, а не молча урезанная карта пинов.
| фича | сноска datasheet | относится к |
|---|---|---|
gd32e230x4 |
(1) | x4 |
gd32e230x6 |
(2) | x6 |
gd32e230x8 (дефолт) |
(2) + (3) | x8 — целевой GD32E230K8U6 |
x8 — надмножество x6; там, где строка помечена только (1) и (3), у x6 функции на этом AF нет вовсе.
Ортогональная фича defmt вешает defmt::Format на публичные энумы и типы
ошибок. По умолчанию выключена; заодно включает embedded-hal/defmt-03.
Что уже есть
GPIO (src/gpio.rs) — const-generic Pin<P, N, MODE> (ZST: порт и номер
ноги живут в типе). Режимы как typestate: Input, Analog,
Output<PushPull> / Output<OpenDrain>, Alternate<AF>, Debugger
(PA13/PA14, выход только через unsafe activate()) и Locked<MODE>
(терминальный — железо остаётся залоченным до сброса, поэтому unlock нет).
dp.gpioa.split(&mut rcu) (GpioExt) включает такт порта и раздаёт пины.
Переходы: into_input / into_output / into_push_pull_output /
into_open_drain_output / into_analog / into_alternate::<AF>() — номер AF
сверяется на компиляции с per-pin картой ValidAf. Плюс set_pull,
set_speed, lock() (только порты A/B — у порта F нет регистра LOCK).
Состояние ноги — инхерентное и без Result: set_high / set_low / toggle
(атомарный TG) / is_set_high / is_set_low у выходов, is_high / is_low у
входов и у выходов open-drain. Те же пины реализуют embedded-hal 1.0
OutputPin / InputPin / StatefulOutputPin, возвращающие
Result<_, Infallible>; оба слоя зовут одни и те же приватные регистровые
хелперы. Порты A, B и F; порт C на этом корпусе не разведён.
RCU (src/rcu.rs) — dp.rcu.constrain() (RcuExt). Трейты Enable и
Reset на каждую периферию, генерируются макросами bus_en!/bus_rst!
(разнесены порознь, потому что не у каждой периферии есть бит сброса — у DMA
его нет) и вплетены в split(), так что порт нельзя использовать без такта.
Дерево тактов: билдер CFGR → .freeze(&mut rcu, &mut dp.fmc) → замороженный
Clocks. PLL от IRC8M
(PllFreq, 8–72 МГц) и прескейлеры шин (AhbPsc / ApbPsc) — типизированные
энумы, поэтому недостижимая частота даёт ошибку компиляции, а не тихое
округление; wait state'ы flash выставляются от получившегося hclk до
переключения источника. Usart0Sel и AdcSel / AdcPsc оседают в Clocks для
модулей USART и ADC. rcu.ck_out(src, div) выводит внутренний тактовый узел на
PA8/PA9 (CkOutSrc / CkOutDiv), так что реальную частоту можно измерить на
ноге, а не принимать на веру. pclk1_tim / pclk2_tim несут тактовую ветку
таймеров: hclk при неделённой APB, удвоенная частота шины во всех остальных
случаях. Типизированные частоты — в src/time.rs, псевдонимы над fugit
(Hertz), сам крейт реэкспортируется. HXTAL вне скоупа — кварц на плате не
запаян.
USART (src/usart.rs) — Usart<USARTX, TX, RX, WORD = Byte> владеет
периферией и обоими пинами. Маркеры TxPin / RxPin (заполняются
usart_pins!) отсекают неверный пин или AF на компиляции; BusClocks выбирает
правильную частоту шины под конкретный инстанс, так что расчёт baud не может
взять не ту. UsartConfig (fluent, Default = 115200 / ×16 / N8) несёт
baud (именованные константы в usart::baud), Oversampling::{X16, X8} и
FrameFormat::{N8, E8, O8, E7, O7} (единственный источник правды для
WL/PCEN/PM). Блокирующий инхерентный API на ширине Byte: write_byte /
write_bytes / read_byte / read_bytes плюс flush (ждёт TC, а не TBE).
У передачи нет условий ошибки, поэтому передающая половина не возвращает
Result. read_bytes блокируется до первого байта, дальше забирает только уже
пришедшее. read_ready / write_ready опрашивают RBNE / TBE без
блокировки. Трейтовые слои на той же ширине: embedded-hal-nb Read<u8> /
Write<u8> и embedded-io Read / Write / ReadReady / WriteReady; второй
крейт даёт read_exact / write_all / write_fmt дефолтами трейта. flush,
read_ready и write_ready есть в обоих слоях — точечную запись выигрывает
инхерентный, трейтовый доступен по пути. Чистый
9-битный режим — new_word / write_word / write_words / read_word /
read_words на typestate Word (UsartConfig9). Ошибки приёма — usart::Error
(Overrun / Noise / Framing / Parity), сбрасываются через USART_INTC;
переносимые ErrorKind дают impl'ы serial::Error и embedded_io::Error.
release() возвращает периферию и оба пина.
ADC (src/adc.rs) — Adc::new(rcu, adc, clocks), либо dp.adc.constrain(...)
через AdcExt, выполняет процедуру калибровки из мануала (ADCON, задержка 14 тактов CK_ADC в пересчёте на такты
ядра, RSTCLB/CLB). read<PIN: Channel>(&pin, SampTime) -> u16 — одиночное
блокирующее преобразование по софт-триггеру. Channel реализован только для
Pin<P, N, Analog>, поэтому пин обязан реально пройти через into_analog().
Внутренние каналы: read_vref() -> i32 возвращает реальное VDDA в мВ
(вычисляется по заводскому VREFINT_CAL из flash, а если эта калибровка пустая —
по типовому VREFINT ≈ 1.2 В), а
read_temperature() -> Option<i32> — десятые доли °C; None, когда CK_ADC
слишком высока для минимального времени сэмплирования датчика. Scan-режим
требует DMA и отложен.
SPI (src/spi.rs) — SPI0 и SPI1: master, full-duplex, блокирующий, 8 или
16 бит, программный NSS. Spi::new(...) / Spi::new_word(...) принимают
SpiConfig (SpiConfig::new(psc) — psc обязателен, Default нет, поскольку у
делителя SCK нет универсального значения; .mode() и .bit_order()
переопределяют дефолты Mode 0 / MSB-first). Ширина слова — typestate:
transfer_word и SpiBus<u16> не существуют на Spi<..., Byte>, а
transfer_byte и SpiBus<u8> — на Spi<..., Word>. BitOrder — обычное
рантайм-значение: он меняет сериализацию на проводе, но не сигнатуры методов.
Трейт Instance абстрагирует периферию на уровне операций, поэтому один generic
Spi<> обслуживает оба инстанса, несмотря на разные типы RegisterBlock и
расхождение на уровне битов (FF16 в CTL0 у SPI0 против DZ в CTL1 у SPI1,
BYTEN выводится из ширины). Буферные операции инхерентные —
transfer_bytes / transfer_bytes_in_place / read_bytes / write_bytes и те
же в _words-варианте; impl'ы SpiBus делегируют к ним. Ошибки — spi::Error
(Overrun / ModeFault / Crc / Framing); ErrorKind даёт impl трейта
spi::Error, где Crc ложится на Other за отсутствием варианта у апстрима.
release() возвращает периферию и пины. Аппаратный NSS и CRC сознательно не
реализованы.
DMA (src/dma.rs) — разовые передачи, проверены на железе.
dp.dma.split(&mut rcu) (DmaExt) раздаёт Channel<0>…Channel<4> — каждый
уникальный ZST-токен, поэтому один канал не может вести две передачи сразу.
write_to / read_from забирают по значению канал, периферию и буфер и
отдают Transfer; вернуть все три можно только через wait() — именно это
делает невозможным чтение буфера, который железо ещё заполняет. Буферы —
&'static [W] / &'static mut [W]. DmaSrc<N> / DmaDst<N> кодируют карту
запросов из datasheet (Table 8-3), поэтому периферия в паре с неверным каналом не
компилируется, а не простаивает молча, а ассоциированный Word выводит
PWIDTH/MWIDTH из typestate самой периферии. Общий супертрейт DmaPeriph<N>
заодно держит линию запроса (DENT/DENR, DMATEN/DMAREN, ADC DMA) —
поднимает на старте и гасит до возврата периферии владельцу, — так что сами
драйверы про DMA ничего не знают. remaining() и is_error() показывают
состояние идущей передачи. Циклический режим и M2M отложены.
TIMER (src/timer.rs) — ядро счётчика, проверено на железе. Все семь
таймеров: Timer::new(rcu, timer, clocks) тактирует и сбрасывает периферию и
запоминает свой CK_TIMERx через трейт Instance, который привязывает каждый
таймер к его шине, поэтому таймеру с APB2 нельзя подсунуть частоту APB1.
start(psc, car) забирает остановленный Timer и отдаёт запущенный
CountDownTimer, поэтому wait() не существует у незапущенного таймера;
stop() возвращает периферию обратно. start пишет оба делителя, загружает их
из теневых регистров через UPG и гасит порождённое им событие обновления, так
что первый wait() отсчитывает полный интервал. wait() блокирует до одного
переполнения и оставляет таймер бежать; cnt() читает счётчик в тиках, один тик
— psc + 1 тактов такта таймера, car() и psc() читают делители из железа, а
не из запомненной копии. Доступ к регистрам заперт в Instance,
поэтому Deref до регистрового блока не нужен. start_interval(5.secs())
принимает длительность fugit в той шкале, в которой её написал вызывающий —
шкала приезжает const-генериком метода, поэтому millis и micros не требуют
конверсии на месте вызова, — и выводит делители от собственного такта таймера, в
u64 и с насыщением. start(psc, car) остаётся для сырой пары. Точек входа две —
Timer::new(rcu, timer, clocks) и dp.timer5.constrain(...) через TimerExt,
который сделан одним blanket-impl'ом поверх Instance. into_delay() переводит
таймер в третий тип, Delay, который не обещает никакого своего интервала:
delay(interval) принимает любую шкалу fugit, настраивает делители, блокирует
и снова останавливает счётчик, поэтому задержкой нельзя затереть период,
заданный где-то ещё. На нём реализован DelayNs из embedded-hal, delay_us /
delay_ms приходят дефолтами трейта; разрешение — один тик таймера. elapsed()
переводит счётчик в длительность любой шкалы fugit, в u64 и с насыщением.
into_pwm(psc, car) / into_pwm_interval(interval) переводят таймер в четвёртый
тип, Pwm, который владеет периодом и раздаёт каналы. channel(pin) принимает
ногу и возвращает PwmChannel: номер канала приезжает из ноги через
PwmPin<TIMERX, C>, реализованный только для той ноги и того номера AF, куда
канал ведёт в железе, а операции над каналом живут в PwmOps<C>, реализованном
только для каналов, которые у таймера есть — у TIMER5 их нет, у TIMER13 один.
Нога уезжает в канал и возвращается из release(). Канал держит собственный
хэндл периферии, взятый через unsafe Instance::steal(), и трогает только свой
регистр сравнения; несколько ног, ведущих на один канал, дают несколько каналов,
пишущих в него. enable() / disable(), set_duty(cv) и max_duty() — это
инхерентный слой, сверху лежит SetDutyCycle из embedded-hal. set_period /
set_period_interval меняют частоту сразу всем каналам; скважность сохраняется в
тиках, а не в долях. enable_output() существует только у таймеров с регистром
CCHP (TIMER0, TIMER14, TIMER15, TIMER16), выходы которых молчат, пока не
поднят POEN. Комплементарные выходы, break, dead time, input capture и
прерывания не реализованы.
Пример
use GpioExt;
use pac;
use ;
use ;
let mut dp = take.unwrap;
let mut rcu = dp.rcu.constrain;
let clocks = CFGRdefault
.sysclk // PLL от IRC8M -> 48 МГц
.freeze;
let parts = dp.gpioa.split; // включает такт GPIOA
let led = parts.pa5.into_output;
led.set_high;
let tx = parts.pa9.; // USART0_TX; ::<3>() не скомпилируется
let rx = parts.pa10.;
let usart0 = new;
if let Ok = usart0.read_byte
Ограничения
- PAC-only база, без сторонних HAL.
- Прошивка и отладка по SWD (ST-Link V2 +
probe-rs,PA13/PA14). Вывод из прошивки идёт по RTT через тот же зонд (defmt+defmt-rtt); USB-переходник не нужен,PA9/PA10заняты только в примерах на USART. - Target
thumbv8m.base-none-eabi; флеш 64K, ОЗУ 8K. gd32e2сгенерирован из патченных SVD — имена полей сверять поdocs/GD32E23x_User_Manual.pdf(PDF лежат локально, в git не входят).
Сборка
Примеры линкуются с memory.x; он в .gitignore, потому что размеры зависят от
варианта чипа. Один раз перед первой сборкой скопировать шаблон:
Чтобы прошить, с ST-Link на PA13/PA14:
re — это cargo run --release --example; в .cargo/config.toml у цели прописан
runner = "probe-rs run --chip GD32E230K8", который заливает ELF напрямую (без
objcopy и без .bin), а дальше остаётся подключённым и печатает RTT-лог до
Ctrl-C. Уровень лога фиксируется на компиляции переменной DEFMT_LOG из того же
.cargo/config.toml. Чип работает и без зонда, а освободившийся зонд читает
любой регистр вживую:
Дефолтная фича собирает под GD32E230K8U6 (x8); для другого варианта:
rust-toolchain.tomlфиксирует канал и ставит нужный таргет. На Windows без Visual Studio host-toolchain должен быть GNU, иначе build-скрипты падают без MSVC-линкера:rustup default stable-x86_64-pc-windows-gnu.
Roadmap
- DMA: циклический режим и
M2M. - Таймеры: input capture; комплементарные выходы, break, dead time.
- I²C.
- Работа на прерываниях (инфраструктура NVIC — затронет и USART/SPI).
- SPI: half-duplex / однопроводные режимы (
BDEN/BDOEN/RO). - SPI: аппаратный NSS, CRC, TI mode, slave — низкий приоритет.
- USART: аппаратное управление потоком (
CTS/RTS). - RCU:
HXTAL(нужен внешний кварц на плате). - GPIO: альтернативные функции порта F (регистра
AFSELнет — нужен отдельный разбор). - Варианты корпуса / числа ног — вторая ось, независимая от x4/x6/x8: какие
ноги вообще разварены (
PC13–PC15,PF6/PF7есть на QFN48, но не на нашем QFN32). - Трейт-крейты идут за своей периферией:
i2c::I2cс I²C,embedded-dmaдля буферов DMA. - Вынос HAL в полностью отдельный крейт/репозиторий (не просто
examples/— разделение самой библиотеки).
Лицензия
Двойное лицензирование, на выбор пользователя:
- Apache License 2.0 (LICENSE-APACHE)
- MIT (LICENSE-MIT)
Вклад, присланный в проект, лицензируется так же, без дополнительных условий, если явно не оговорено иное (Apache-2.0, раздел 5).