GPT4 as a Hardware Abstraction Layer
Hardware Abstraction Layers (HALs) are software components that provide a uniform interface to interact with hardware,
abstracting away the underlying complexities. REST libraries, on the other hand, simplify communication with RESTful
APIs, providing convenient methods to make requests and handle responses.
Pros & Cons of HALs & REST Libraries #
Benefits #
HALs and REST libraries are essential tools that speed up development, especially in an age before GPT-4. They
allow developers to focus on writing application logic without having to worry about low-level hardware details or the
intricacies of API communication.
Limitations #
Despite their benefits, HALs and REST libraries can also limit development:
- HALs:
- Abstract away hardware, potentially preventing you from performing specific tasks.
- May suffer from poor documentation or maintenance.
- Can cause unexpected behavior and make debugging difficult.
- (Thicc) REST libraries:
- Abstract away the API, potentially preventing you from performing specific tasks.
- May suffer from poor maintenance for your chosen language.
REST APIs which are instead one-to-one mappings of the API’s endpoints can be considered “thin” REST libraries and
do not suffer from the same limitations as thick REST libraries. Generally, the one-to-one mapping of the HAL is
instead considered a “Peripheral Access Crate” (PAC).
Peripheral Access Crates (PACs) #
The PAC is a one-to-one mapping from a microcontroller’s registers to code.
An example of a PAC is the stm32h7
library, which is a Peripheral Access Crate (PAC) for the STM32 family of
microcontrollers. PACs provide a thin wrapper around hardware, offering a more direct way to interact with it.
Example: Using a PAC to Blink an LED #
#[entry]
fn main() -> ! {
let peripherals = stm32h7x3::Peripherals::take().unwrap();
let pwr = &peripherals.PWR;
let rcc = &peripherals.RCC;
rcc.ahb4enr.modify(|_, w| w.gpioden().set_bit());
pwr.cr3.modify(|_, w| w.svden().enabled());
let gpiod = &peripherals.GPIOD;
gpiod.moder.modify(|_, w| w.moder15().output());
loop {
gpiod.odr.modify(|r, w| w.odr15().bit(!r.odr15().bit()));
asm::delay(8_000_000);
}
}
GPT-4 for thin REST and PAC Calls #
GPT-4 makes it much easier to implement logic that combines multiple REST or PAC calls. While reading
documentation to combine these calls is not too difficult, it can be time-consuming. Using GPT-4, you can feed in
the REST API/reference manual docs and obtain code that works for your specific use case, saving a considerable amount
of time. Since the task of transferring from the style of an english doc to code is not creating new knowledge—but just
style transferring—GPT-4 generally performs quite well. Even without docs, GPT-4 usually still generates code that
mostly works—albeit with a few modifications.
Using an svd
file to generate a PAC #
An svd
file is a standardized XML format for describing the registers of a microcontroller. It is used by PACs to
generate code for interacting with the microcontroller’s registers. A portion of the svd
file for the COMP1 peripheral
is listed below:
<peripheral>
<name>COMP1</name>
<description>COMP1</description>
<groupName>COMP1</groupName>
<registers>
<register>
<name>CFGR1</name>
<displayName>CFGR1</displayName>
<description>Comparator configuration register 1</description>
<addressOffset>0xC</addressOffset>
<size>0x20</size>
<access>read-write</access>
<resetValue>0x00000000</resetValue>
<fields>
<field>
<name>POLARITY</name>
<description>COMP channel 1 polarity selection
bit
</description>
<bitOffset>3</bitOffset>
<bitWidth>1</bitWidth>
</field>
</fields>
</register>
</registers>
</peripheral>
The full SVD containing COMP1 can be downloaded here: comp1.svd. The full STM32H743 SVD (which
contains all peripherals) can be downloaded here: STM32H743.svd.
Concatenating the full comp1.svd
with the prompt:
in Rust using the stm32h7 crate modify registers of COMP1 to negate the output of the comparison
and pasting into ChatGPT(4) yields the code:
use stm32h7::stm32h7x3;
fn main() {
let peripherals = stm32h7x3::Peripherals::take().unwrap();
configure_comp1(&peripherals);
}
fn configure_comp1(peripherals: &stm32h7x3::Peripherals) {
let comp1 = &peripherals.COMP1;
comp1.cfgr1.modify(|_, w| w.polarity().set_bit());
}
which is correct.
Without an svd
#
we obtain
comp1.csr.modify( | _, w| w.neg().set_bit());
which is incorrect code (does not compile and uses wrong registers). This shows that GPT4 understands the
semantics of how registers are set with the stm32h7
crate, but does not necessarily know what the registers
are without extra context. We are using GPT4 to transform the [english prompt + SVD] -> [Rust code]
using the
style of the stm32h7
crate. I have seen GPT4 generate exceptional outputs when all it needs to do is
style transfer and is not generating “new” knowledge.
Asking for TIM15 (trimmed svd) #
I had to trim the indentation and removed bit-offset tags so the SVD file could fit in 8k tokens required by GPT-4.
I do not yet have access to gpt-4-32k
which can handle up to 32k
tokens. The prompt:
In Rust using the stm32h7 crate modify registers of TIM15 (OC1) to output a clock duty 50%.
Show how to set the pre-scalar as well. Include comments
We obtain
use stm32h7xx_hal::{
pac::TIM15,
prelude::*,
};
fn config_tim15(tim15: &TIM15, prescaler: u16) {
tim15.psc.write(|w| w.psc().bits(prescaler));
tim15.cr1.modify(|_, w| {
w.opm().continuous()
.arpe().enabled()
});
let arr = tim15.arr.read().arr().bits();
tim15.ccr1.write(|w| w.ccr1().bits(arr / 2));
tim15.ccmr1_output.modify(|_, w| {
w.oc1m().pwm_mode1()
.oc1pe().enabled()
});
tim15.ccer.modify(|_, w| w.cc1e().set_bit());
tim15.cr1.modify(|_, w| w.cen().enabled());
}
This is mostly correct and can be modified to get:
fn main() {
tim15.psc.write(|w| w.psc().bits(TIM15_PRESCALER));
tim15.cr1.modify(|_, w| {
w.opm()
.variant(OPM_A::Disabled)
.arpe()
.variant(ARPE_A::Enabled)
});
let arr = tim15.arr.read().arr().bits();
tim15.ccr1().write(|w| w.ccr().bits(arr / 2));
tim15.ccmr1_output()
.modify(|_, w| w.oc1m().variant(PwmMode1).oc1pe().variant(OC1PE_A::Enabled));
tim15.ccer.modify(|_, w| w.cc1e().set_bit());
tim15.cr1.modify(|_, w| w.cen().enabled());
}
Asking questions #
With the same SVD we can ask
What is TIM15 OC1. Is OC1 a channel or output source or something? What does it stand for?
and we get:
TIM15 is a general-purpose timer that can be used for various timing and control tasks in a microcontroller system.
OC1 (Output Compare 1) is a channel and is an important feature of the TIM15 timer. The acronym “OC” stands for Output
Compare, which refers to the ability of the timer to compare the counter value with a predefined value stored in a
register (CCR1 in this case).
When the counter value matches the value stored in the CCR1 register, an event is generated, which can trigger an
interrupt, create a pulse on an output pin, or perform some other related action. This functionality can be used for
generating periodic signals, controlling PWM (Pulse Width Modulation) outputs, and implementing other timing-related
tasks in the system.
In summary, OC1 in TIM15 is a channel used for comparing the counter value with a predefined value to trigger specific
actions, and it stands for Output Compare 1.
Rust: A Perfect Match for Language Models #
Rust is an excellent choice for working with Language Models like GPT-4 for several reasons:
-
Token efficiency: Rust has a low token count, which is beneficial since you pay for the number of tokens
generated by the Language Model.
- With
serde
, you can use #[derive(Deserialize)]
and #[derive(Serialize)]
for a struct, then
use serde_json::from_str
and serde_json::to_string
to convert between JSON and Rust structs without generating
extra code.
-
Code verification: Rust makes it easier to verify the correctness of the generated code.
- PACs in Rust use
enum
s to represent different registers, ensuring type correctness at compile-time.
- Rust’s strong typing makes it harder for mistakes to go unnoticed, which is crucial when working with Language
Models, as they can occasionally generate incorrect code.
-
Clarity: Rust’s syntax and constructs, like serde
, make it clear what’s happening in the code, which is
important for developers who need to understand and maintain it.
Conclusion #
By combining the power of Rust with GPT-4, you can create efficient, verifiable, and easily understandable code
for your hardware abstraction and REST API needs. This approach allows you to bypass the limitations of traditional HALs
and REST libraries, ultimately leading to more streamlined and effective development processes.