Skip to content

Commit 1e93f7e

Browse files
committed
added thermistor task
1 parent f4ae764 commit 1e93f7e

5 files changed

Lines changed: 2035 additions & 46 deletions

File tree

7.55 KB
Loading

docs/tock_workshop/index.md

Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,3 +498,398 @@ To test our modifications, as now we no longer need an application, run:
498498
probe-rs erase --chip CY8C624ABZI-S2D44
499499
make flash
500500
```
501+
502+
### Temperature Sensor Capsule
503+
504+
For this task, we will have to read the ambient temperature using the onboard thermistor. A thermistor is a temperature-sensitive resistor in which resistance varies with temperature. The thermistor is in a voltage divider configuration which can be seen bellow:
505+
506+
![Thermistor Configuration](./assets/Thermistor.webp)
507+
508+
Given that the current flowing through $R_{ref}$ also flows through the thermistor:
509+
510+
$$
511+
\frac{V_1 - V_0}{R_{therm}} = \frac{V_2 - V_1}{R_{ref}}
512+
$$
513+
514+
The [Steinhart-Hart](https://en.wikipedia.org/wiki/Steinhart–Hart_equation) equation describes the resistance change of a semiconductor thermistor as related to its temperature. The following equation shows it to be a third-order logarithmic polynomial using three constants.
515+
516+
$$
517+
\frac{1}{T_K} = A + B \cdot \ln(R) + C \cdot \ln(R)^3
518+
$$
519+
520+
Where $A$, $B$ and $C$ are empirical constants, $R$ is the thermistor's resistance in $\Omega$ and $T_k$ is the temperature in kelvin.
521+
522+
Consulting the [board's manual](https://www.infineon.com/assets/row/public/documents/30/44/infineon-cy8cproto-062-4343w-psoc-6-wi-fi-bt-prototyping-kit-guide-usermanual-en.pdf), we can see that the voltage divider uses a `10K` resistor, so $R_{ref} = 10000\Omega$.
523+
524+
#### Current state
525+
526+
The underlying ADC peripheral implementation has two channels configured (`AdcChannel0` and `AdcChannel1`) to sample on demand the two voltage drops on the resistor and thermistor. Also, the four pins mentioned in the schematic are properly setup by the board configuration, in the `TEMPERATURE` section.
527+
528+
#### `TemperatureSensor` in Tock
529+
530+
Tock already has an implementation for a `SyscallDriver` capsule for reading the temperature. It can be found in the `capsules/extra/src/temperature.rs` module. It utilizes a trait for interacting with various temperature sensors called `TemperatureDriver`, that works in the same notification based paradigm as the `Alarm`, notifying the client when the temperature measurement is complete. Our goal will be to implement this trait for our new `Cy8cprotoThermistor` capsule.
531+
532+
As before, the first step will be to define our new module, `cy8cproto_thermistor.rs` and define the structure of our capsule. The capsule will need references to both `AdcChannel`s, and will need to hold a client reference, to notify them.
533+
534+
```rust title="capsules/extra/scr/cy8cproto_thermistor.rs"
535+
pub struct Cy8cprotoThermistor<'a, A: adc::AdcChannel<'a>> {
536+
thermistor_adc: &'a A,
537+
resistor_adc: &'a A,
538+
client: OptionalCell<&'a dyn sensors::TemperatureClient>,
539+
}
540+
```
541+
542+
We need to implement the `TemperatureDriver` trait for our thermistor capsule. The implementation for the `set_client` method is straight-forward, and the `read_temperature` would, in theory, need to start the sampling of both channels.
543+
544+
```rust title="capsules/extra/scr/cy8cproto_thermistor.rs"
545+
impl<'a, A: adc::AdcChannel<'a>> TemperatureDriver<'a> for Cy8cprotoThermistor<'a, A> {
546+
fn set_client(&self, client: &'a dyn sensors::TemperatureClient) {
547+
self.client.set(client);
548+
}
549+
550+
fn read_temperature(&self) -> Result<(), kernel::ErrorCode> {
551+
self.thermistor_adc.sample();
552+
self.resistor_adc.sample();
553+
Ok(())
554+
}
555+
}
556+
```
557+
558+
:::warning Error handling
559+
This implementation disregards error handling for education purposes, both sample functions may fail, and need proper handling.
560+
:::
561+
562+
This capsule will be a client to both the `AdcChannel`s, so the next logical step is to implement the `AdcClient` trait.
563+
564+
```rust title="capsules/extra/scr/cy8cproto_thermistor.rs"
565+
impl<'a, A: adc::AdcChannel<'a>> adc::Client for Cy8cprotoThermistor<'a, A> {
566+
fn sample_ready(&self, sample: u16) {
567+
todo!()
568+
}
569+
}
570+
```
571+
572+
This is where we encounter our first issue. Both channels will use the same interface, and therefore the same method implementation when the sampling is done. This means that the capsule has no way of distinguishing between the samples.
573+
574+
#### State machine design
575+
576+
One viable alternative would be implementing a state machine for sampling both channels and computing the temperature.
577+
578+
```mermaid
579+
---
580+
config:
581+
theme: redux
582+
---
583+
flowchart TD
584+
A(["Idle"]) -- read_temperature() --> B{"Sample Resistor"}
585+
B -- Error --> D(["Report Error"])
586+
B -- Await sample result --> C{"Sample Thermistor"}
587+
C -- Error --> D
588+
C -- Await sample result --> F(["Report Temperature"])
589+
F --> A
590+
D --> A
591+
```
592+
593+
We need to define the internal state of the capsule, and to do that, we will use an `enum`. We will also need to store the result of the resistor sample in capsule.
594+
595+
```rust title="capsules/extra/scr/cy8cproto_thermistor.rs"
596+
#[derive(Clone, Copy)]
597+
enum Status {
598+
Idle,
599+
AwaitingThermistorReading,
600+
AwaitingResistorReading,
601+
}
602+
603+
pub struct Cy8cprotoThermistor<'a, A: adc::AdcChannel<'a>> {
604+
thermistor_adc: &'a A,
605+
resistor_adc: &'a A,
606+
resistor_sample: OptionalCell<u16>,
607+
status: Cell<Status>,
608+
client: OptionalCell<&'a dyn sensors::TemperatureClient>,
609+
}
610+
611+
impl<'a, A: adc::AdcChannel<'a>> Cy8cprotoThermistor<'a, A> {
612+
pub fn new(thermistor_adc: &'a A, resistor_adc: &'a A) -> Self {
613+
Self {
614+
thermistor_adc,
615+
resistor_adc,
616+
resistor_sample: OptionalCell::empty(),
617+
status: Cell::new(Status::Idle),
618+
client: OptionalCell::empty(),
619+
}
620+
}
621+
}
622+
```
623+
624+
The `read_temperature` method will start the resistor measurement and will change the inner state.
625+
626+
```rust title="capsules/extra/scr/cy8cproto_thermistor.rs"
627+
impl<'a, A: adc::AdcChannel<'a>> TemperatureDriver<'a> for Cy8cprotoThermistor<'a, A> {
628+
fn set_client(&self, client: &'a dyn sensors::TemperatureClient) {
629+
self.client.set(client);
630+
}
631+
632+
fn read_temperature(&self) -> Result<(), kernel::ErrorCode> {
633+
let res = self.thermistor_adc.sample();
634+
if res.is_ok() {
635+
self.status.set(Status::AwaitingThermistorReading);
636+
}
637+
res
638+
}
639+
}
640+
```
641+
642+
The implementation of the `AdcClient` sole method, `sample_ready` will have to take into account the current state. If the method is called after the first reading (`status` is `Status::AwaitingResistorReading`), the capsule must store the result and start the thermistor sampling. If the method is called after both readings are performed, then the capsule must perform the computations mentioned above, to report the measured temperature in hundredths of degrees Celsius (centiCelsius).
643+
644+
:::note Error handling
645+
646+
Handle all possible errors that the `sample` calls may yield by resetting both the thermistor state to `Status::Idle` and the `resistor_sample`, and reporting the error to the client.
647+
648+
```rust title="Reporting the result of a temperature reading"
649+
self.client.map(|client| client.callback(temp_reading_result))
650+
```
651+
652+
In our case, the `temp_reading_result` will be the sample error.
653+
:::
654+
655+
#### Computing the temperature
656+
657+
Based on the [Infineon documentation](https://www.infineon.com/assets/row/public/documents/30/42/infineon-an2017-psoc-1-temperature-measurement-with-thermistor-applicationnotes-en.pdf?fileId=8ac78c8c7cdc391c017d072814bc4c7b), the empirical constants' values for the thermistor used by our board are:
658+
659+
```rust
660+
/// Reference Resistor value in Ohms
661+
const R_REF: f64 = 10_000.0;
662+
663+
const A: f64 = 0.000891358;
664+
const B: f64 = 0.000250618;
665+
const C: f64 = 0.000000197;
666+
```
667+
668+
To convert from the numerical values to voltages, we need to understand how ADCs work. Every ADC will have a resolution, which is the number of bits which can be used to store the sample result, and a sampling range which is the minimum-to-maximum input voltage it can measure. Our ADC has a resolution of 11 bits (based on the current configuration of the peripheral) and a input range of 0-3.3 Volts, meaning that the result can rage from `0`, which represents the 0V potential to `2047` ($2^{11} - 1$) which will represent 3V3.
669+
670+
Based on this, we can define a function that takes the discrete value of the sample and returns the voltage.
671+
672+
```rust
673+
fn convert(sample: u16) -> f64 {
674+
((sample as f64) * 3.3) / 2047.0
675+
}
676+
```
677+
678+
Our implementation of the `sample_ready` function should now be complete
679+
680+
```rust title="capsules/extra/scr/cy8cproto_thermistor.rs"
681+
impl<'a, A: adc::AdcChannel<'a>> adc::Client for Cy8cprotoThermistor<'a, A> {
682+
fn sample_ready(&self, sample: u16) {
683+
match self.status.get() {
684+
Status::Idle => (),
685+
Status::AwaitingResistorReading => {
686+
// ... Store the sample
687+
}
688+
Status::AwaitingThermistorReading => {
689+
let sample_0 = self.resistor_sample.take().unwrap();
690+
let sample_1 = sample;
691+
692+
let v1_0 = convert(sample_0);
693+
let v2_1 = convert(sample_1);
694+
695+
let r_therm = R_REF * (v2_1 / v1_0);
696+
697+
let logarithm = libm::log(r_therm);
698+
let logarithm3 = logarithm * logarithm * logarithm;
699+
700+
let temp_k = 1.0f64 / (A + B * logarithm + C * logarithm3);
701+
702+
let temp_celsius = temp_k - 273.15;
703+
704+
self.status.set(Status::Idle);
705+
self.client
706+
.map(|client| client.callback(Ok((temp_celsius * 100.0) as i32)));
707+
}
708+
}
709+
}
710+
}
711+
```
712+
713+
#### Defining the component
714+
715+
As before, we need to ensure the capsule can be easily configured by implementing a new `Component`. You can name the module `cyc8cproto_thermistor.rs`.
716+
717+
We should start from the bottom up, considering what should be needed to instantiate this capsule. These are the `AdcChannel`s and an `MuxAdc`, to be able to multiplex an ADC peripheral to sample multiple channels.
718+
719+
```rust title="boards/components/src/cyc8proto_thermistor.rs"
720+
// This will be the output type of the component
721+
pub type Cy8cprotoThermistorComponentType<A> =
722+
capsules_extra::cy8cproto_thermistor::Cy8cprotoThermistor<'static, A>;
723+
724+
pub struct Cy8cprotoThermistorComponent<A: 'static + adc::Adc<'static>> {
725+
adc_mux: &'static capsules_core::virtualizers::virtual_adc::MuxAdc<'static, A>,
726+
thermistor_channel: A::Channel,
727+
resistor_channel: A::Channel,
728+
}
729+
730+
impl<A: 'static + adc::Adc<'static>> Cy8cprotoThermistorComponent<A> {
731+
pub fn new(
732+
adc_mux: &'static capsules_core::virtualizers::virtual_adc::MuxAdc<'static, A>,
733+
thermistor_channel: A::Channel,
734+
resistor_channel: A::Channel,
735+
) -> Self {
736+
Self {
737+
adc_mux,
738+
thermistor_channel,
739+
resistor_channel,
740+
}
741+
}
742+
}
743+
```
744+
745+
The `finalize` implementation of the `Component` trait will need to create the two virtual `AdcDevices` that multiplex the peripheral, and therefore, the static memory needed must accommodate the two devices, and the thermistor capsule.
746+
747+
```rust title="boards/components/src/cyc8proto_thermistor.rs"
748+
impl<A: 'static + adc::Adc<'static>> Component for Cy8cprotoThermistorComponent<A> {
749+
type StaticInput = (
750+
&'static mut MaybeUninit<AdcDevice<'static, A>>,
751+
&'static mut MaybeUninit<AdcDevice<'static, A>>,
752+
&'static mut MaybeUninit<Cy8cprotoThermistorComponentType<AdcDevice<'static, A>>>,
753+
);
754+
755+
type Output = &'static Cy8cprotoThermistorComponentType<AdcDevice<'static, A>>;
756+
757+
fn finalize(self, static_memory: Self::StaticInput) -> Self::Output {
758+
let thermistor_device =
759+
crate::adc::AdcComponent::new(self.adc_mux, self.thermistor_channel)
760+
.finalize(static_memory.0);
761+
762+
let resistor_device = crate::adc::AdcComponent::new(self.adc_mux, self.resistor_channel)
763+
.finalize(static_memory.1);
764+
765+
let cy8cproto_thermistor = static_memory.2.write(Cy8cprotoThermistorComponentType::new(
766+
thermistor_device,
767+
resistor_device,
768+
));
769+
770+
thermistor_device.set_client(cy8cproto_thermistor);
771+
resistor_device.set_client(cy8cproto_thermistor);
772+
cy8cproto_thermistor
773+
}
774+
}
775+
```
776+
777+
The macro should look like this:
778+
779+
```rust title="boards/components/src/cyc8proto_thermistor.rs"
780+
#[macro_export]
781+
macro_rules! cy8cproto_thermistor_component_static {
782+
($A:ty $(,)?) => {{
783+
let thermistor_device = components::adc_component_static!($A);
784+
let resistor_device = components::adc_component_static!($A);
785+
let cy8cproto_thermistor = kernel::static_buf!(
786+
capsules_extra::cy8cproto_thermistor::Cy8cprotoThermistor<
787+
'static,
788+
capsules_core::virtualizers::virtual_adc::AdcDevice<'static, $A>,
789+
>
790+
);
791+
792+
(thermistor_device, resistor_device, cy8cproto_thermistor)
793+
};};
794+
}
795+
```
796+
797+
#### Adding the capsule to the board
798+
799+
The final step is to add the newly defined capsule to the board's configuration. To do that, we must add the capsule
800+
to the `Cy8cproto0624343w` structure, and to the `SyscallDriverLookup` implementation, and configure it in the `main` function.
801+
802+
```rust title="boards/cy8cproto_62_4343_w/src/main.rs"
803+
/// Supported drivers by the platform
804+
pub struct Cy8cproto0624343w {
805+
// ...
806+
systick: cortexm0p::systick::SysTick,
807+
// highlight-start
808+
temp: &'static capsules_extra::temperature::TemperatureSensor<
809+
'static,
810+
capsules_extra::cy8cproto_thermistor::Cy8cprotoThermistor<
811+
'static,
812+
capsules_core::virtualizers::virtual_adc::AdcDevice<'static, Adc<'static>>,
813+
>,
814+
>,
815+
// highlight-end
816+
}
817+
818+
impl SyscallDriverLookup for Cy8cproto0624343w {
819+
fn with_driver<F, R>(&self, driver_num: usize, f: F) -> R
820+
where
821+
F: FnOnce(Option<&dyn kernel::syscall::SyscallDriver>) -> R,
822+
{
823+
match driver_num {
824+
// ...
825+
capsules_core::gpio::DRIVER_NUM => f(Some(self.gpio)),
826+
// highlight-next-line
827+
capsules_extra::temperature::DRIVER_NUM => f(Some(self.temp)),
828+
_ => f(None),
829+
}
830+
}
831+
}
832+
833+
// ...
834+
835+
pub unsafe fn main() {
836+
// ...
837+
838+
//--------------------------------------------------------------------------
839+
// TEMPERATURE
840+
//--------------------------------------------------------------------------
841+
842+
// ... Pin configurations
843+
844+
let mux_adc = components::adc::AdcMuxComponent::new(&peripherals.adc)
845+
.finalize(components::adc_mux_component_static!(sar::Adc));
846+
847+
// highlight-start
848+
let thermistor = components::cy8cproto_thermistor::Cy8cprotoThermistorComponent::new(
849+
mux_adc,
850+
sar::AdcChannel::Channel0,
851+
sar::AdcChannel::Channel1,
852+
)
853+
.finalize(cy8cproto_thermistor_component_static!(psoc62xa::sar::Adc));
854+
855+
let temp = components::temperature::TemperatureComponent::new(
856+
board_kernel,
857+
capsules_extra::temperature::DRIVER_NUM,
858+
thermistor,
859+
)
860+
.finalize(components::temperature_component_static!(
861+
capsules_extra::cy8cproto_thermistor::Cy8cprotoThermistor<
862+
'static,
863+
capsules_core::virtualizers::virtual_adc::AdcDevice<'static, Adc<'static>>,
864+
>
865+
));
866+
// highlight-end
867+
868+
// ...
869+
}
870+
```
871+
872+
#### Sensors application
873+
874+
Fortunately, `libtock-c` already has a modular example that reads multiple sensors. This example can be found in the `examples/sensors` subdirectory. To enable the temperature readings, we must set the `temperature` variable.
875+
876+
```c title="examples/sensors/main.c"
877+
// ...
878+
#include <libtock/tock.h>
879+
880+
static libtock_alarm_t alarm;
881+
static bool light = false;
882+
// highlight-next-line
883+
static bool temperature = true;
884+
static bool humidity = false;
885+
static bool ninedof = false;
886+
static bool ninedof_accel = false;
887+
static bool ninedof_mag = false;
888+
static bool ninedof_gyro = false;
889+
static bool proximity = false;
890+
static bool sound_pressure = false;
891+
static bool moisture = false;
892+
static bool rainfall = false;
893+
```
894+
895+
Then, compile the application, and flash it along the kernel. Do not forget to update the `APP` variable in the board's `Makefile`.

0 commit comments

Comments
 (0)